接上文 JavaScript动态渲染页面爬取——Selenium的使用(一)https://developer.aliyun.com/article/1621774
延时等待
在Selenium中,get方法在网页框架中加载结束后才会结束执行,如果我们尝试在get方法执行完毕时获取网页源代码,其结果可能并不是浏览器完全加载完成的页面,因为某些页面有额外的Ajax请求,页面还会经由JavaScript渲染。所以,在必要的时候,我们需要设置浏览器延时等待一定的时间,确保节点已经加载出来。
等待方式有两种:一种是隐式等待,一种是显式等待。
隐式等待
使用隐式等待执行测试时,如果Selenium没有在DOM中找到节点,将继续等待,在超出设定时间后,抛出找不到节点的异常。换句话说,在查找节点而节点没有立即出现时,隐式等待会先等待一段时间再查找DOM,默认的等待时间是0。示例如下:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
options = webdriver.ChromeOptions()
services = Service(executable_path='../chromedriver')
browser = webdriver.Chrome(service=services, options=options)
browser.implicitly_wait(10)
browser.get('https://spa2.scrape.center/')
input = browser.find_element(By.CLASS_NAME, 'logo-image')
print(input)
运行结果如下:
<selenium.webdriver.remote.webelement.WebElement (session="278b608e46fa3c124a3fa7867b93a34e",
element="8b49c4ec-73bc-4176-948f-3a735fd84e55")>
这里我们用implicitly_wait方法实现了隐式等待。
显示等待
隐式等待效果其实不好,因为只规定了一个固定时间,而页面的加载时间会受到网络条件的影响。还有一种更适合的等待方式——显式等待,这种方式会指定要查找的节点和最长等待时间。如果在规定时间内加载出了要查找的节点,就返回这个节点;如果到了规定时间依然没有加载出点,就抛出超时异常。示例如下:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
options = webdriver.ChromeOptions()
services = Service(executable_path='../chromedriver')
browser = webdriver.Chrome(service=services, options=options)
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)
这里首先引入WebDriverWait对象,指定最长等待时间为10,并赋值给wait变量,然后调用wait的until方法,传入等待条件。
这里先传入了presence_of_element_located这个条件,代表节点出现,其参数是节点的定位元组(By.ID, ‘q’),表示节点ID为q的节点(即搜索框)。这样做达到的效果是如果节点ID为q的节点在10秒内成功加载出来了,就返回该节点;如果超过10秒还没加载出来,就抛出异常。
然后传入的等待条件是element_to_be_clickable,代表按钮可点击,所以查找按钮时要查找CSS选择器为.btn-search的按钮,如果10秒内它是可点击的,就是按钮节点成功加载出来了,就返回该节点;如果超过10秒还是不可点击,也就是按钮节点没有加载出来,就抛出异常。
运行代码,在网速较佳的情况下是可以成功加载节点的。控制台输出结果如下:
<selenium.webdriver.remote.webelement.WebElement (session="84cc41714d2e631dd447da556a3585d0",
element="90a9a753-b7dc-45c5-b827-556a4f7a8fda")>
<selenium.webdriver.remote.webelement.WebElement (session="84cc41714d2e631dd447da556a3585d0",
element="16b85e70-d0cb-4217-ba93-77ea65a62474")>
可以看到,成功输出了两个节点,都是WebElement类型的。如果网络有问题,10秒到了还是没有成功加载,就抛出TimeoutException异常。
前进和后退
平常使用浏览器时,都是前进和后退功能,Selenium也可以完成这个操作,它使用forward方法实现前进,使用back方法实现后退。示例如下:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
import time
options = webdriver.ChromeOptions()
services = Service('../chromedriver')
browser = webdriver.Chrome(service=services, options=options)
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方法回到第2个页面,接着调用forward方法又前进到第3个页面。
Cookie
使用Selenium,还可以方便地对Cookie进行操作,例如获取、添加、删除等。示例如下:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
options = webdriver.ChromeOptions()
services = Service('../chromedriver')
browser = webdriver.Chrome(service=services, options=options)
browser.get('https://www.zhihu.com/explore')
browser.add_cookie({
'name':'name', 'domain':'www.zhihu.com', 'value':'germey'})
print(browser.get_cookies())
browser.delete_all_cookies()
print(browser.get_cookies())
这里我们先访问了知乎。知乎页面加载完成后,浏览器其实已经生成Cookie了。然后,调用浏览器对象的get_cookes方法获取所有的Cookie。接着,添加一个Cookie,这里传入一个字典,包含name、domain和value等键值。之后,再次获取所有的Cookie,会发现结果中多了一项,就是我们新加的Cookie。最后,调用delete_all_cookies方法删除所有的Cookie并再次获取,会发现此事结果就空了。
控制台输出结果如下:
[{
'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': 'ed2ad9934af8a1f80db52dcb08d13344|1711448448|1711448445'},
{
'domain': '.zhihu.com', 'expiry': 1742984447, 'httpOnly': False,
'name': 'Hm_lvt_98beee57fd2ef70ccdd5ca52b9740c49', 'path': '/',
'secure': False, 'value': '1711448448'},
{
'domain': '.zhihu.com', 'httpOnly': False,
'name': 'Hm_lpvt_98beee57fd2ef70ccdd5ca52b9740c49', 'path': '/',
'secure': False, 'value': '1711448448'},
{
'domain': '.zhihu.com', 'expiry': 1806056445,
'httpOnly': False, 'name': 'd_c0', 'path': '/',
'secure': False, 'value': 'ABBY2F-lXhiPTq3E1glsHl-yDs3b6u15x4w=|1711448445'},
{
'domain': '.zhihu.com', 'httpOnly': False, 'name': '_xsrf', 'path': '/',
'secure': False, 'value': '34131e3a-3c44-4ff2-857a-563bd80c2f3f'},
{
'domain': '.zhihu.com', 'expiry': 1774520445, 'httpOnly': False,
'name': '_zap', 'path': '/', 'secure': False,
'value': '62da9fb2-33b9-4234-b23e-a192b6f507e4'}]
[]
通过以上方法操作Cookie还是非常方便的。
选项卡管理
访问网页的时候,会开启一个选项卡。在Selenium中,我们也可以对选项卡做操作。示例如下:
import time
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
services = Service(executable_path='../chromedriver')
options = webdriver.ChromeOptions()
browser = webdriver.Chrome(service=services, options=options)
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语句,表示新开启一个选项卡。接着,我们想切换到这个新开的选项卡。window_handles属性用于获取当前开启的所有选项卡,返回值是选项卡的代号列表。要想切换选项卡,只需要调用switch_to.windows方法即可,其中参数是目的选项卡的代号。这里我们将新开选项卡的代号传入,就切换到了第2个选项卡,然后在这个选项卡下打开一个新页面,再重新调用switch_to.window方法切换回到第1个选项卡。控制台的输出结果如下:
['CDwindow-D97D968F0BD8741543EE3C6FB42692CF', 'CDwindow-E218A8DBC3864BBE41F29F61A2E1ED78']
异常处理
在使用Selenium的过程中,难免会遇到一些异常,例如超时、节点未找到等,一旦出现此类异常,程序便不会继续运行了。此时我们可以使用try except语句捕获各种异常。
首先,演示一下节点未找到的异常,示例如下:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
options = webdriver.ChromeOptions()
services = Service(executable_path='../chromedriver')
browser = webdriver.Chrome(service=services, options=options)
browser.get('https://www.baidu.com')
browser.find_element(By.ID, 'hello')
这里首先打开百度页面,然后尝试选择一个并不存在的节点,就会遇到节点未找到的异常。控制台的输出结果如下:
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element:
{
"method":"css selector","selector":"[id="hello"]"}
(Session info: chrome=97.0.4692.71);
For documentation on this error,
please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception
可以看到,这里抛出了NoSuchElementException异常,这通常表示节点未找到。为了防止程序遇到异常而中断运行,我们需要捕获这些异常,示例如下:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException, NoSuchElementException
options = webdriver.ChromeOptions()
services = Service(executable_path='../chromedriver')
browser = webdriver.Chrome(service=services, options=options)
try:
browser.get('https://www.baidu.com')
except TimeoutException:
print('Time Out')
try:
browser.find_element(By.ID, 'hello')
except NoSuchElementException:
print('No Element')
finally:
browser.close()
这里我们使用try except语句捕获各类异常。例如,对查找节点的方法find_element_by_id捕获NoSuchElementException异常,这样一旦出现这样的错误,就会进行异常处理,程序也不会中断。控制台的输出结果如下:
No Element
反屏蔽
现在有很多网站增加了对Selenium的检测,防止一些爬虫的恶意爬取,如果检测到有人使用Selenium打开浏览器,就直接屏蔽。
在大多数情况下,检测到基本原理是检测浏览器窗口下的window.navigator对象中是否包含webdriver属性。因为在正常使用浏览器时,这个属性应该是undefined,一旦使用了Selenium,它就会给window.navigator对象设置webdriver属性。很多网站通过JavaScript语句判断是否存在webdriver属性,如果存在就直接屏蔽。
一个典型的案例网站https://antispider1.scrape.center/就是使用上述原理,检测是否存在webdriver属性,如果我们使用Selenium直接爬取该网站的数据,网站就会返回如图所示的页面:
在Selenium中,可以用CDP(即Chrome Devtools Protocol,Chrome开发工具协议)解决这个问题,利用它可以实现在每个页面刚加载的时候就执行JavaScript语句,将webdriver属性设置空,这里执行的CDP方法叫做Page.addScriptToEvaluateOnNewDocument,将上面的JavaScript语句传入其中即可。另外,还可以加入几个选项来隐藏WebDriver提示条和自动化扩展信息,代码实现如下:
from selenium import webdriver
from selenium.webdriver import ChromeOptions
from selenium.webdriver.chrome.service import Service
import time
option = ChromeOptions()
service = Service(executable_path='../chromedriver')
option.add_experimental_option('excludeSwitches', ['enable-automation'])
option.add_experimental_option('useAutomationExtension', False)
browser = webdriver.Chrome(service=service, options=option)
browser.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument',
{
'source': 'Object.defineProperty(navigator, "webdriver",{get:() => undefined})'})
browser.get('https://antispider1.scrape.center/')
time.sleep(16)
这样就能加载出整个页面了,如图所示:
在多数时候,以上方法可以实现Selenium的反屏蔽。但也存在一些特殊网站会对WebDriver属性设置更多的特征检测,这种情况下可能需要具体排查。
无头模式
Chrome浏览器从60版起,已经开启了对无头模式的支持,即Headless。无头模式下,网站运行的时候不会弹出窗口,从而减少了干扰,同时还减少了一些资源(如图片)的加载,所以无头模式也在一定程度上节省了资源加载的时间和网络带宽。
我们可以借助ChromeOptions对象开启Chrome浏览器的无头模式,代码实现如下:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
option = webdriver.ChromeOptions()
option.add_argument('--headless')
services = Service(executable_path='../chromedriver')
browser = webdriver.Chrome(options=option, service=services)
browser.set_window_size(1366, 768)
browser.get('https://www.taonan.gov.cn')
browser.get_screenshot_as_file('preview.png')
这里利用ChromeOptions对象的add_argument方法添加了一个参数—headless,从而开启了无头模式。在无头模式下,最好设置一下窗口的大小,因此这里调用了set_window_size方法。之后打开页面,并调用get_screenshot_as_file方法输出了页面截图。
运行这段代码后,会发现窗口不会再弹出来了,代码依然正常运行,最后输出的页面截图如下图所示:
这样我们就在无头模式下完成了页面的爬取和截图操作。