动态渲染页面爬取

前面了解到了Ajax的分析和爬取方式,这其实也是JavaScript动态渲染页面的一种方式,通过直接分析Ajax,仍然可以借助requests或urllib来实现数据爬取

但是JavaScript的动态渲染页面不止Ajax这一种,例如有些网站的分页是由JavaScript生成的,并非是原始的HTML代码,其中也并不包含Ajax

Selenium的使用

Selenium是一个自动化测试工具,利用它可以驱动浏览器执行指定的动作,如点击,下来等操作,同时还可以获取浏览器当前呈现的页面的源代码,做到可见即可爬

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait

browser=webdriver.Chrome()
try:
browser.get("https://www.baidu.com")
input=browser.find_element_by_id('kw')
input.send_keys('Python')
input.send_keys(Keys.ENTER)
wait=WebDriverWait(browser,10)
wait.until(EC.presence_of_element_located((By.ID , 'content_left')))
print(browser.current_url)
print(browser.get_cookies())
print(browser.page_source)
finally:
browser.close()

输出结果:
当前URL,cookies和网页源代码

再执行上条指令后,会自动弹出一个Chrome浏览器,浏览器首先会跳转到百度,然后在搜索框中输出Python,接着跳转到搜索结果页面
avatar
avatar

这样我们就可以直接拿到JavaScript渲染的结果了

声明浏览器对象

Selenium支持非常多的浏览器,如Chrome,Firefox,Edge等,还有Android,BlackBerry等手机端浏览器,还支持无界面浏览器PhantomJS

1
2
3
4
5
6
7
from selenium import webdriver

browser=webdriver.Chrome()
browser=webdriver.Firefox()
browser=webdriver.Edge()
browser=webdriver.PhantomJS()
browser=webdriver.Safari()

这样就完成了浏览器对象的初始化,并将其赋值为browser对象

访问页面

我们可以使用get()方法来请求网页,参数传URL即可

1
2
3
4
5
6
from selenium import webdriver

browser=webdriver.Chrome()
browser.get('https://www.taobao.com')
print(browser.page_source)
browser.close()

运行后发现,弹出了Chrome浏览器并且自动访问了淘宝,然后控制台输出了淘宝页面的源代码,随后浏览器关闭

查找结点

selenium可以驱动浏览器完成各种操作,比如填充表单,模拟点击等。比如,我们想要完成向某个输入框输入文字的操作,需要知道这个输入框在哪里,而selenium提供了一系列查找节点的方法

单个节点

比如,想要从淘宝页面中提取搜索框这个节点,首先观察它的源代码
avatar
可以发现它的id是q,name也是q,此外还有许多其他属性,我们就可以以多种方式获取了
比如find_element_by_name()是根据name获取,find_element_by_id()是根据id获取
另外,还有根据XPath,CSS选择器等获取方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from selenium import webdriver

browser=webdriver.Chrome()
browser.get('https://www.taobao.com')
input_first=browser.find_element_by_id('q')
input_second=browser.find_element_by_css_selector('#q')
input_third=browser.find_element_by_xpath('//*[@id="q"]')
print(input_first,input_second,input_third)
browser.close()

输出结果:
<selenium.webdriver.remote.webelement.WebElement (session="b332cd2cb2191a08286eada1aceb4b4e", element="1b3bf42c-4780-4c65-9b6f-fca9190503f4")>
<selenium.webdriver.remote.webelement.WebElement (session="b332cd2cb2191a08286eada1aceb4b4e", element="1b3bf42c-4780-4c65-9b6f-fca9190503f4")>
<selenium.webdriver.remote.webelement.WebElement (session="b332cd2cb2191a08286eada1aceb4b4e", element="1b3bf42c-4780-4c65-9b6f-fca9190503f4")>

我们发现结果一至,可以看到这三个节点都是WebElement类型
获取单个节点的方法:

1
2
3
4
5
6
7
8
find_element_by_id
find_element_by_name
find_element_by_xpath
find_element_by_link_text
find_element_by_partial_link_text
find_element_by_tag_name
find_element_by_class_name
find_element_by_css_selector

此外还提供了通用方法find_element(),它需要传入两个参数,查找方法By和值
实际就是上述方法的通用版本

1
2
3
4
5
6
7
8
9
10
from selenium import webdriver
from selenium.webdriver.common.by import By

browser=webdriver.Chrome()
browser.get('https://www.taobao.com')
input_first=browser.find_element(By.ID,'q')
print(input_first)
browser.close()

输出结果与上面一至

多个节点

如果查找的目标在网页中只有一个,那么完全可以使用find_element()方法,但是如果有多个节点,就需要使用find_elements()方法了

比如淘宝导航条的所有条目
avatar

先查看这些条目,发现都在class为service-bd的ul节点的li节点中

1
2
3
4
5
6
7
8
9
10
from selenium import webdriver

browser=webdriver.Chrome()
browser.get('https://www.taobao.com')
lis=browser.find_elements_by_css_selector('.service-bd li')
print(lis)
browser.close()

输出结果:
[<selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="7237cd9e-6812-41b2-839c-5a5ef0790077")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="cd8c7478-6486-4376-8c25-a562dad5be68")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="01ac2aac-0431-49f5-9e9b-244fb11a5b99")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="5f10b89b-f4f2-4efb-a084-3ce219e81aa2")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="b99fefe4-b5d0-42c9-84d2-068ba0738d53")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="4ce8e958-225e-480b-9e43-d0510bd6b7ee")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="c37b736b-8d99-4424-a38c-f5270bdc274c")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="a1c19196-5d1e-414b-ae4b-44ad67938651")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="387c966a-29d2-4ced-a1bf-ae11d24f3252")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="f74ecd4d-68eb-4be6-b50a-6ad5f792b9af")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="5c413df1-2c09-4a10-88ee-c27287535d6e")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="73ee7920-b2dd-44dd-b5b5-97a36b5684e1")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="5ef053b7-90a1-4a0d-b504-284d8d9ed497")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="555d8a92-90bd-4724-99ab-fcdb88397077")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="98329539-7cf1-404a-8903-d3ea72f3bb6d")>, <selenium.webdriver.remote.webelement.WebElement (session="cb088f704736092b306130aab2abf8f1", element="df6d8c38-5f1d-4872-8a30-d6e0e1ca02ad")>]

这样就获取到了所有条目的源代码

获取多个节点

1
2
3
4
5
6
7
8
find_elements_by_id
find_elements_by_name
find_elements_by_xpath
find_elementst_by_link_text
find_elements_by_partial_link_text
find_elements_by_tag_name
find_elements_by_class_name
find_elementst_by_css_selector

同时还有通用方法

1
lis=browser.find_elements(By.CSS_SELECTOR,'.service-bd li')

节点交互

我们还可以让浏览器执行一些操作,比较常见的如输入文字时用send_keys()方法,清空文字时用clear()方法,点击按钮时用click()方法

1
2
3
4
5
6
7
8
9
10
11
12
from selenium import webdriver
import time

browser=webdriver.Chrome()
browser.get('https://www.taobao.com')
input=browser.find_element_by_id('q')
input.send_keys('iphone')
time.sleep(1)
input.clear()
input.send_keys('ipad')
button=browser.find_element_by_class_name('btn-search')
button.click()

这里首先驱动浏览器打开淘宝,然后用find_element_by_id()方法获取输入框,然后用send_keys()方法输入iphone文字,等待一秒后用clear()方法清空输入框,再次调用send_keys()方法输入ipad文字,之后再用find_element_by_class_name()方法获取搜索按钮,最后调用click()方法完成搜索动作

更多交互请参考文档:
中文文档:
https://python-selenium-zh.readthedocs.io/zh_CN/latest/
官方文档:
https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.remote.webelement

动作链

在上面的实例中,发现一些交互动作都是针对某个节点执行的。比如,对于输入框,我们就是调用它的输入文字和清空文字方法,对于按钮,就调用它的点击方法.其次,还有另外一些操作,它们没有特定的执行对象,比如鼠标拖拽,键盘按键等

1
2
3
4
5
6
7
8
9
10
11
12
from selenium import webdriver
from selenium.webdriver import ActionChains

browser=webdriver.Chrome()
url="http://www.runoob.com/try/try.php?filename=jqueryui-api-droppable"
browser.get(url)
browser.switch_to.frame('iframeResult')
source = browser.find_element_by_css_selector('#draggable')
target = browser.find_element_by_css_selector('#droppable')
actions = ActionChains(browser)
actions.drag_and_drop(source , target)
actions.perform()

运行后会将小方块拖拽到大方块中
avatar

更多动作链操作请参考文档:
中文文档:
https://python-selenium-zh.readthedocs.io/zh_CN/latest/
官方文档:
https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.remote.webelement

执行JavaScript

对于某些操作,Selenium API并没有提供。比如,下拉进度条,它可以直接模拟运行JavaScript,此时使用execute_script()方法即可实现

1
2
3
4
5
6
from selenium import webdriver

browser=webdriver.Chrome()
browser.get("https://www.zhihu.com/explore")
browser.execute_script('window.scrollTo(0,document.body.scrollHeight)')
browser.execute_script('alert("To Bottom")')

avatar
这里就利用execute_script()方法将进度条下拉到最底部,然后弹出alert提示框

所以说有了这个方法,基本上API没有提供的所有功能都可以用执行JavaScript的方式来实现了

获取节点信息

前面通过Beautiful Soup,pyquery等提取信息

Selenium中提供了选择节点的方法,返回的是WebElement类型,那么它也有相关的方法和属性来直接提取节点信息,如属性,文本等。这样的话,我们就可以不用通过解析源代码来提取信息了

获取属性

我们可以通过get_attribute()方法来获取节点的属性,前提是先选中这个节点

1
2
3
4
5
6
7
8
from selenium import webdriver

browser=webdriver.Chrome()
url='https://www.zhihu.com/explore'
browser.get(url)
logo=browser.find_element_by_id('zh-top-link-logo')
print(logo)
print(logo.get_attribute('class'))

通过get_attribute()方法,然后传入想要获取的属性名,就可以得到它的值了

获取文本值

每个WebElement节点都有text属性,直接调用这个属性就可以得到节点内部的文本信息,这相当于Beautiful Soup中的get_text()方法,pyquery的text()方法

1
2
3
4
5
6
7
8
9
10
from selenium import webdriver

browser=webdriver.Chrome()
url='https://www.zhihu.com/explore'
browser.get(url)
input=browser.find_element_by_class_name('ExploreHomePage-ContentSection-header')
print(input.text)

输出结果:
最新专题

这样就获得了最新专题节点的内容

获取id,位置,标签名和大小

WebElement节点还有一些其他的属性,比如id属性可以获取节点的id,location属性可以获取该节点在页面中的相对位置,tag_name属性可以获取标签名称,size属性可以获取节点的大小,也就是宽高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from selenium import webdriver

browser=webdriver.Chrome()
url='https://www.zhihu.com/explore'
browser.get(url)
input=browser.find_element_by_class_name('ExploreHomePage-ContentSection-header')
print(input.text)
print(input.id)
print(input.location)
print(input.tag_name)
print(input.size)

输出结果:
最新专题
b74d780d-b4f2-4a35-9221-cb40a5e48902
{'x': 10, 'y': 84}
div
{'height': 36, 'width': 1000}

这样就获取到了id,位置,标签名和大小

切换Frame

网页中有一个节点叫做iframe,也就是子Frame,相当于页面中的子页面,它的结构和外部网页的结构完全一致。Selenium打开页面后,它默认是在父级Frame里操作,而此时如果页面中还有子Frame,它是不能获取到子Frame里面的节点的,这时就需要switch_to.frame()方法来切换Frame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

browser=webdriver.Chrome()
url='http://www.runoob.com/try/try.php?filename=jqueryui-api-droppable'
browser.get(url)
browser.switch_to.frame('iframeResult')
try:
logo=browser.find_element_by_class_name('logo')
except NoSuchElementException:
print("NO LOGO")
browser.switch_to.parent_frame()
logo=browser.find_element_by_class_name('logo')
print(logo)
print(logo.text)

输出结果:
NO LOGO
<selenium.webdriver.remote.webelement.WebElement (session="74a6a23d440b28de1d52cee1269b0ca6", element="909ba7c7-467d-4bb7-86aa-3df9d4981362")>
RUNOOB.COM

首先通过switch_to.frame()方法切换到子Frame里面,然后尝试获取子Frame里的logo节点,如果找不到的话,就会抛出NoSuchElementException异常。接下来,重新切换到父级Frame,再次重新获取节点,就可以成功捕获了

延时等待

get()方法会在网页框架加载结束后结束执行,此时如果获取page_source,可能并不是浏览器完全加载完成的页面,如果某些页面有额外的Ajax请求,我们在网页源代码中也不一定能获取到。所以,这里需要延时等待一定时间,确保节点已经加载出来

隐式等待

当使用隐式等待执行测试的时候,如果selenium没有在DOM中找到节点,将继续等待,超出设定时间后,则抛出找不到节点的异常。换句话说,当查找节点而节点并没有立即出现的时候,隐式等待将等待一段时间再查找DOM,默认事件为0

1
2
3
4
5
6
7
from selenium import webdriver

browser=webdriver.Chrome()
browser.implicitly_wait(10)
browser.get('https://www.zhihu.com/explore')
input=browser.find_element_by_class_name('ExploreHomePage-ContentSection-header')
print(input)

这里使用了隐式等待implicitly_wait()

显式等待

隐式等待的效果其实并没有那么好,因为我们只规定了一个固定时间,而页面加载时间还会受到网络条件的影响

这里还有一种更合适的显式等待方法,它指定要查找的节点,然后指定一个最长等待时间。如果在规定时间内加载出来了节点,就返回查找的节点,反之

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

browser=webdriver.Chrome()
browser.get('https://www.taobao.com')
wait=WebDriverWait(browser,10)
input=wait.until(EC.presence_of_element_located((By.ID,'q')))
button=wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,'.btn-search')))
print(input,button)

输出结果:
<selenium.webdriver.remote.webelement.WebElement (session="8ed44db593e504803b1e1bbdaa40096f", element="2e6789ed-7862-419b-adca-2cea31a3d3c9")> <selenium.webdriver.remote.webelement.WebElement (session="8ed44db593e504803b1e1bbdaa40096f", element="4d2571b6-fd8d-47e2-8490-f6fddf96959b")>

首先引入WebDriverWait这个对象,指定最长等待时间,然后调用它的until()方法,传入要等待条件expected_conditions。比如,这里传入了presence_of_element_located这个条件,代表节点出现的意思,其参数是节点的定位元组,也就是ID为q的节点搜索框

这样可以做到的效果就是,在10秒内如果ID为q的节点成功加载出来,就返回该节点;如果超过10秒还没有加载出来,就抛出异常。

对于按钮,可以更改一下等待条件,比如改为element_to_be_clickable,也就是可点击,所以查找按钮时查找CSS选择器为.btn-search的按钮,如果10秒内它是可点击的,也就是成功加载出来了,就返回这个按钮节点,反之

更多操作请参考文档
中文文档:
https://python-selenium-zh.readthedocs.io/zh_CN/latest/
官方文档:
https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.remote.webelement

前进和后退

平时使用浏览器时都有前进和后退功能,selenium也可以完成这个操作,它使用back()方法后退,使用forward()方法前进。

1
2
3
4
5
6
7
8
9
10
11
import time
from selenium import webdriver

browser=webdriver.Chrome()
browser.get('https://www.baidu.com')
browser.get('https://www.taobao.com')
browser.get('https://www.python.org')
browser.back()
time.sleep(1)
browser.forward()
browser.close()

这样我们连续访问3个页面,然后调用back方法回到第二个页面,接下来再调用forward()方法又可以前进到第三个页面

cookies

使用selenium,还可以方便地对Cookies进行操作,例如获取,添加,删除Cookies等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from selenium import webdriver

browser=webdriver.Chrome()
browser.get('https://www.zhihu.com/explore')
print(browser.get_cookies())
browser.add_cookie({'name':'name','domain':'www.zhihu.com','value':'germey'})
print(browser.get_cookies())
browser.delete_all_cookies()
print(browser.get_cookies())

输出结果:
[{'domain': 'www.zhihu.com', 'httpOnly': False, 'name': 'KLBRSID', 'path': '/', 'secure': False, 'value': 'cdfcc1d45d024a211bb7144f66bda2cf|1587952898|1587952896'}, {'domain': '.zhihu.com', 'expiry': 1619488898, 'httpOnly': False, 'name': 'Hm_lvt_98beee57fd2ef70ccdd5ca52b9740c49', 'path': '/', 'secure': False, 'value': '1587952898'}, {'domain': '.zhihu.com', 'httpOnly': False, 'name': 'Hm_lpvt_98beee57fd2ef70ccdd5ca52b9740c49', 'path': '/', 'secure': False, 'value': '1587952898'}, {'domain': '.zhihu.com', 'expiry': 1682560896.073367, 'httpOnly': False, 'name': 'd_c0', 'path': '/', 'secure': False, 'value': '"AIBbIT5rLhGPTpMVZJCLnmYZXYPvh8AXzpM=|1587952896"'}, {'domain': 'www.zhihu.com', 'httpOnly': False, 'name': 'SESSIONID', 'path': '/', 'secure': False, 'value': 'Ns5jG8TIaSE1aAjeOzckOayn6V6nDAubFTUf1DfCC2H'}, {'domain': '.zhihu.com', 'httpOnly': False, 'name': '_xsrf', 'path': '/', 'secure': False, 'value': '6e9342c2-b0fd-4474-9099-193fef3e5595'}, {'domain': '.zhihu.com', 'expiry': 1651024896.073016, 'httpOnly': False, 'name': '_zap', 'path': '/', 'secure': False, 'value': '2dbb5fb5-9f25-44ae-ac01-5183941f56df'}]

[{'domain': '.www.zhihu.com', 'httpOnly': False, 'name': 'name', 'path': '/', 'secure': True, 'value': 'germey'}, {'domain': 'www.zhihu.com', 'httpOnly': False, 'name': 'KLBRSID', 'path': '/', 'secure': False, 'value': 'cdfcc1d45d024a211bb7144f66bda2cf|1587952898|1587952896'}, {'domain': '.zhihu.com', 'expiry': 1619488898, 'httpOnly': False, 'name': 'Hm_lvt_98beee57fd2ef70ccdd5ca52b9740c49', 'path': '/', 'secure': False, 'value': '1587952898'}, {'domain': '.zhihu.com', 'httpOnly': False, 'name': 'Hm_lpvt_98beee57fd2ef70ccdd5ca52b9740c49', 'path': '/', 'secure': False, 'value': '1587952898'}, {'domain': '.zhihu.com', 'expiry': 1682560896.073367, 'httpOnly': False, 'name': 'd_c0', 'path': '/', 'secure': False, 'value': '"AIBbIT5rLhGPTpMVZJCLnmYZXYPvh8AXzpM=|1587952896"'}, {'domain': 'www.zhihu.com', 'httpOnly': False, 'name': 'SESSIONID', 'path': '/', 'secure': False, 'value': 'Ns5jG8TIaSE1aAjeOzckOayn6V6nDAubFTUf1DfCC2H'}, {'domain': '.zhihu.com', 'httpOnly': False, 'name': '_xsrf', 'path': '/', 'secure': False, 'value': '6e9342c2-b0fd-4474-9099-193fef3e5595'}, {'domain': '.zhihu.com', 'expiry': 1651024896.073016, 'httpOnly': False, 'name': '_zap', 'path': '/', 'secure': False, 'value': '2dbb5fb5-9f25-44ae-ac01-5183941f56df'}]

[]

可以看到我们添加的cookie被添加到了cookies中

选项卡管理

访问网页的时候,会开启一个个选项卡。在selenium中,我们可以对选项卡进行操作

1
2
3
4
5
6
7
8
9
10
11
12
import time
from selenium import webdriver

browser=webdriver.Chrome()
browser.get('https://www.baidu.com')
browser.execute_script('window.open()')
print(browser.window_handles)
browser.switch_to.window(browser.window_handles[1])
browser.get('https://www.taobao.com')
time.sleep(1)
browser.switch_to.window(browser.window_handles[0])
browser.get('https://python.org')

先访问了百度,然后调用了execute_script()方法,这里传入window.open()这个JavaScript语句开启一个选项卡。然后用windows_handles属性获取当前开启地所有选项卡,返回的是选项卡的代码列表。想要切换,只需要使用switch_to.window()方法,其中是选项卡的代号即可。

总结

1.安装selenium之后,还需要根据浏览器的不同,安装对应的webdriver,例如Chrome的webdriver

2.webdriver需要放在浏览器的根目录中,并且在环境变量中引入,如果在管理员模式下cmd中,输入对应webdriver的名字,有正常显示就表示环境变量成功

3.如果需要通过python来对浏览器进行操作,还需要将对应的webdriver放入到python的根目录下

4.selenium可以操作浏览器自动完成一些操作,所以需要注意一些危险操作,以免操作信息泄露

5.selenium功能强大,可以使用动作链和节点交互功能完成一系列重复性操作,例如抢课等