selenium爬虫

selenium 是什么？

Selenium 是支持 web 浏览器自动化的一系列工具和库的综合项目。

它提供了扩展来模拟用户与浏览器的交互，用于扩展浏览器分配的分发服务器， 以及用于实现 W3C WebDriver 规范 的基础结构， 该 规范 允许您为所有主要 Web 浏览器编写可互换的代码。

python 中的 selenium 库是 selenium 的接口，它可以模拟浏览器像人一样操作页面，获取网页信息.

基于这个特点，selenium 在爬取某些网站信息时代码逻辑更简单，且不用逆向 js 加密代码.

然而，因为是模拟操作，所以爬取效率比不上其他爬虫.

为了展现 selenium 的强大，我们举个例子:

爬取 bilibili 个人页面的 粉丝名字 及 粉丝的粉丝数

注意: 爬取数据时要注意网站的 robots.txt 内的规定，同时不要有太高的爬取频率，以免对网站产生负担。本文爬取的 粉丝名字 与 粉丝的粉丝数 属于公开内容.

安装

Copy
$ pip install selenium

分析网站

在个人空间的粉丝页面下，粉丝信息位于 下的 元素内.

然而在 内没有粉丝的粉丝数这一信息，而是在服务器上，没有储存在本地。它是通过鼠标移动到粉丝头像或者名字后，触发 js 传输到本地的。这个操作是由 AJAX 技术实现的.

AJAX(Asynchronous JavaScript and XML) 是一种在无需重新加载整个网页的情况下，能够更新部分网页的技术.

在鼠标移动到头像上时，会在 末端生成一个 :

粉丝的粉丝数位于 下的下:

匹配方法

selenium 内匹配元素有很多方法:

xpath (最常用)

by id

by name/tag name/class name

by link

by css selector

xpath 之所以好用是因为 xpath 可以使用相对路径匹配，并且语法简单.
比如匹配粉丝头像可以这样写:

Copy
//div[@id="id-card"]

而在 XML 下该元素的位置:

Copy

...

...

...





当然 css selector 有时也很好用:

XML:

Copy


Site content goes here.



css selector:

Copy
p.content

写爬虫

初始化:

Copy
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

获取页码:

Copy
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]

遍历所有页:

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

Copy
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)

Copy
def getCard(li, actions):
cover = li.find_element("xpath", './/a[@class="cover"]')
actions.move_to_element(cover)
actions.perform()
actions.reset_actions()

获取并储存数据:

Copy
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])

完整代码:

Copy
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