banner
ming5ming

ming5ming

⭐一起去追那遥不可及的星⭐ 正在赛马!

selenium爬蟲

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">:

id-card

粉絲的粉絲數位於<div id="id-card">下的<span class="idc-meta-item">下:

fansNum

匹配方法#

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

寫爬蟲#

mermaid-diagram-2023-01-06-041353.png


初始化:

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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。