selenium 是什麼?#
Selenium 是支持 web 瀏覽器自動化的一系列工具和庫的綜合項目。
它提供了擴展來模擬用戶與瀏覽器的互動,用於擴展瀏覽器分配的分發伺服器,以及用於實現 W3C WebDriver 規範的基礎結構,該規範允許您為所有主要 Web 瀏覽器編寫可互換的代碼。
python 中的 selenium 庫是 selenium 的接口,它可以模擬瀏覽器像人一樣操作頁面,獲取網頁信息。
基於這個特點,selenium 在爬取某些網站信息時代碼邏輯更簡單,且不用逆向 js 加密代碼。
然而,因為是模擬操作,所以爬取效率比不上其他爬蟲。
為了展現 selenium 的強大,我們舉個例子:
爬取 bilibili 個人頁面的粉絲名字及粉絲的粉絲數
注意: 爬取數據時要注意網站的 robots.txt 內的規定,同時不要有太高的爬取頻率,以免對網站產生負擔。本文爬取的粉絲名字與粉絲的粉絲數屬於公開內容。
安裝#
$ pip install selenium
分析網站#
在個人空間的粉絲頁面下,粉絲信息位於<ul>
下的<li>
元素內。
然而在<li>
內沒有粉絲的粉絲數這一信息,而是在伺服器上,沒有儲存在本地。它是通過鼠標移動到粉絲頭像或者名字後,觸發 js 傳輸到本地的。這個操作是由 AJAX 技術實現的。
AJAX(Asynchronous JavaScript and XML) 是一種在無需重新加載整個網頁的情況下,能夠更新部分網頁的技術。
在鼠標移動到頭像上時,會在<body>
末端生成一個<div id="id-card">
:
粉絲的粉絲數位於<div id="id-card">
下的<span class="idc-meta-item">
下:
匹配方法#
selenium 內匹配元素有很多方法:
- xpath (最常用)
- by id
- by name/tag name/class name
- by link
- by css selector
xpath 之所以好用是因為 xpath 可以使用相對路徑匹配,並且語法簡單。
比如匹配粉絲頭像可以這樣寫:
//div[@id="id-card"]
而在 XML 下該元素的位置:
<html>
...
<body>
...
<div id = "id-card">
...
</div>
</body>
</html>
當然 css selector 有時也很好用:
XML:
<html>
<body>
<p class="content">Site content goes here.</p>
</body>
<html>
css selector:
p.content
寫爬蟲#
初始化:
def initDriver(url):
#設置headless瀏覽器
options = webdriver.ChromeOptions()
options.add_argument('headless')
options.add_experimental_option('excludeSwitches', ['enable-logging'])
#初始化
driver = webdriver.Chrome(options=options)
actions = ActionChains(driver)
#打開鏈接
driver.get(url)
driver.implicitly_wait(10)
return driver, actions
獲取頁碼:
def getPageNum(driver):
#通過xpath匹配底部翻頁元素位置,獲取頁碼數
text = driver.find_element("xpath", '//ul[@class="be-pager"]/span[@class="be-pager-total"]')
.get_attribute("textContent")
.split(' ')
return text[1]
遍歷所有頁:
def spawnCards(page, driver, actions):
#遍歷所有頁
for i in range(1,int(page) + 1):
print(f"get data in page {i}\n")
#觸發ajax生成card
spawn(driver, actions)
if (i != int(page)):
#翻頁
goNextPage(driver, actions)
time.sleep(6)
生成 card:
def spawn(driver, actions):
#得到card list
ulList = driver.find_elements("xpath", '//ul[@class="relation-list"]/li')
#生成 card
for li in ulList:
getCard(li, actions)
time.sleep(2)
def getCard(li, actions):
cover = li.find_element("xpath", './/a[@class="cover"]')
actions.move_to_element(cover)
actions.perform()
actions.reset_actions()
獲取並儲存數據:
def writeData(driver):
#獲取 card list
cardList = driver.find_elements("xpath", '//div[@id="id-card"]')
for card in cardList:
up_name = card.find_element("xpath", './/img[@class="idc-avatar"]').get_attribute("alt")
up_fansNum = card.find_elements('css selector','span.idc-meta-item')[1].get_attribute("textContent")
print(f'name:{up_name}, {up_fansNum}')
#寫入csv文件
with open('.\\date.csv', mode='a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([up_name, up_fansNum])
完整代碼:
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
import time
import csv
def initDriver(url):
options = webdriver.ChromeOptions()
options.add_argument('headless')
options.add_experimental_option('excludeSwitches', ['enable-logging'])
driver = webdriver.Chrome(options=options)
actions = ActionChains(driver)
driver.get(url)
driver.get(url)
driver.implicitly_wait(10)
return driver, actions
def getPageNum(driver):
text = driver.find_element("xpath", '//ul[@class="be-pager"]/span[@class="be-pager-total"]').get_attribute("textContent").split(' ')
return text[1]
def goNextPage(driver, actions):
bottom = driver.find_element("xpath", '//li[@class="be-pager-next"]/a')
actions.click(bottom)
actions.perform()
actions.reset_actions()
def getCard(li, actions):
cover = li.find_element("xpath", './/a[@class="cover"]')
actions.move_to_element(cover)
actions.perform()
actions.reset_actions()
def writeData(driver):
#獲取 card list
cardList = driver.find_elements("xpath", '//div[@id="id-card"]')
for card in cardList:
up_name = card.find_element("xpath", './/img[@class="idc-avatar"]').get_attribute("alt")
up_fansNum = card.find_elements('css selector','span.idc-meta-item')[1].get_attribute("textContent")
print(f'name:{up_name}, {up_fansNum}')
#寫入信息到csv文件
with open('.\\date.csv', mode='a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([up_name, up_fansNum])
def spawn(driver, actions):
#獲取 card list
ulList = driver.find_elements("xpath", '//ul[@class="relation-list"]/li')
#生成 card
for li in ulList:
getCard(li, actions)
time.sleep(2)
def spawnCards(page, driver, actions):
for i in range(1,int(page) + 1):
print(f"get data in page {i}\n")
spawn(driver, actions)
if (i != int(page)):
goNextPage(driver, actions)
time.sleep(6)
def main():
#初始化驅動
uid = input("bilibili uid:")
url = "https://space.bilibili.com/" + uid + "/fans/fans"
driver, actions = initDriver(url)
page = getPageNum(driver)
#生成卡片信息(ajax)
spawnCards(page, driver, actions)
writeData(driver)
driver.quit()
if __name__ == "__main__":
main()
結果#
反思#
可以改進的地方:
- 由於 ajax 異步加載,必須等待頁面加載完畢後才能進行元素定位。而使用
time.sleep()
方法不夠效率和優雅,WebDriverWait()
方法可以解決。它可以輪詢頁面狀態,當頁面加載完畢返回true
。 - 使用了多次含重複路徑的 xpath 表達式,匹配佔用了太多內存。
- 完全可以並發的提取數據,更快的獲取結果。但出於對伺服器負荷的考慮,只寫了單線程版本。
References#
selenium doc
推薦閱讀:
Ajax
Xpath