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):
    #get 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}')
        #write info into csv file
        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):
    #get card list
    ulList = driver.find_elements("xpath", '//ul[@class="relation-list"]/li')
    #spawn 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():
    #init driver
    uid = input("bilibili uid:")
    url = "https://space.bilibili.com/" + uid + "/fans/fans"
    driver, actions = initDriver(url)
    page = getPageNum(driver)

    #spawn card info(ajax)
    spawnCards(page, driver, actions)
    writeData(driver)

    driver.quit()

if __name__ == "__main__":
    main()

结果#

图片

反思#

可以改进的地方:

  • 由于 ajax 异步加载,必须等待页面加载完毕后才能进行元素定位。而使用time.sleep()方法不够效率和优雅,WebDriverWait()方法可以解决。它可以轮询页面状态,当页面加载完毕返回true.
  • 使用了多次含重复路径的 xpath 表达式,匹配占用了太多内存.
  • 完全可以并发的提取数据,更快的获取结果。但出于对服务器负荷的考虑,只写了单线程版本.

References#

selenium doc

推荐阅读:

Ajax

Xpath

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。