??利用Ajax的接口,找到其规律,可以通过某些参数构造出对应的请求,可以轻松的爬取到数据。
??但是在很多情况下,Ajax请求的接口通常含有加密参数,例如token,sign等。这种情况我们如果不深入分析找到加密的构造逻辑,很难直接模拟Ajax请求。
??对于这种情况如果我们深挖其逻辑进行破解难度比较高,这里我们可以采用另外一种办法可以用selenium模拟浏览器的运行来获取数据。
??Selenium是一个自动化测试工具,利用它可以驱动浏览器完成一些特定的操作。也可以获取当前页面的网页源代码,做到可见即可爬,对于JavaScript渲染的页面来说,Selenium非常有效。
??以Chrome浏览器为例讲解。首先安装Selenium包。并且需要对应浏览器版本的ChromeDriver驱动,将其放在浏览器安装目录下。
pip install selenium
# 最新版本语句可能略微发生变化,本文以3.3.1为例。
#pip install selenium==3.3.1
??示例代码如下:
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()
??运行代码会自动弹出Chrome浏览器。会跳转到百度页面,在搜索框输入python,然后跳转到搜索页。此时控制台会输出URL,Cookie内容,和真实的网页源代码。
用selenium驱动加载浏览器网页,可以获得JavaScript渲染的结果,无需关心是否加密。
??Selenium支持许多浏览器。像Chrome、Firefox、Edge、Safari等,一些手机端的浏览器也支持。
# 初始化浏览器对象
from selenium import webdriver
browser = webdriver.Chrome()
# 或者Chrome替换为Firebox、Edge等等
??可以使用get方法请求网页,向参数传入指定的URL链接即可,以访问淘宝,并打印网页源代码,然后关闭浏览器为例。
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://taobao.com')
print(browser.page_source)
browser.close()
# 以id属性为例
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://taobao.com')
input = browser.find_element_by_id('q')
print(input)
browser.close()
??获取单个节点可以使用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。
此外,selenium提供了通用方法find_element,使用这个方法需要传入查找方式和方式的取值两个参数。例如find_element_by_id(id)等价于find_element(By.ID,id)
# 查找淘宝左侧导航栏所有条目
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://taobao.com')
lis = browser.find_elements_by_css_selector('.service-bd li')
print(lis)
browser.close()
??该结果得到的类型为列表类型,列表每个节点都是WebElement类型,如果使用find_element方法只会返回一个节点,该节点为WebElement类型。
??获取多个节点是也可以使用上面单个节点的方法element后面加个s而已。
??同理,lis = browser.find_elements_by_css_selector(‘.service-bd li’)等价于
lis = browser.find_elements(By.CSS_SELECTOR, ‘.service-bd li’)。
??Selenium可以驱动浏览器执行一些操作。比如常见的用法:send_keys输入文字,clear清空文字,click点击按钮等。
# 打开淘宝,在搜索框输入iphone,等待一秒,清空输入框,在输入ipad点击搜索
from selenium import webdriver
import time
browser = webdriver.Chrome()
browser.get('https://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()
??上个实例是针对于某个节点执行的。但是在一些操作中,没有特定的执行对象,比如鼠标拖拽、键盘按键等,这种需要动作链来执行。
# 打开一个拖拽实例,将选中的节点拖拽到另一个节点
from selenium import webdriver
from selenium.webdriver import ActionChains# 执行高级用户行为(鼠标移动拖拽等)
browser = webdriver.Chrome()
url = 'https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable'
browser.get(url)
browser.switch_to.frame('iframeResult')# 找到此窗口
source = browser.find_element_by_id('draggable')
target = browser.find_element_by_id('droppable')
actions = ActionChains(browser)
actions.drag_and_drop(source,target)# 拖拽操作
actions.perform()# 触发
??有些操作,selenium并没有提供API,例如下拉进度条,这种情况可以模拟运行JavaScript,下拉进度条使用execute_script方法即可实现。
# 利用execute_script方法将进度条拉到最底部,然后弹出警告提示框
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 Buttom")')
??有了该方法,没有被提供API的功能都可以运行JavaScript的方式实现。
??前面我们通过page_source已经获取了网页源码,这是可以通过之前的方法解析库(正则表达式、xpath、BeautifulSoup等)来提取所需信息。
??不过,selenium提供了选择节点的方法,同时也提供了相关的方法和属性来获取节点信息,例如属性、文本值等。
from selenium import webdriver
browser = webdriver.Chrome()
url = 'https://spa2.scrape.center/'
browser.get(url)
logo = browser.find_element_by_class_name('logo-image')
print(logo)
print(logo.get_attribute('src'))
from selenium import webdriver
browser = webdriver.Chrome()
url = 'https://spa2.scrape.center/'
browser.get(url)
input = browser.find_element_by_class_name('logo-title')
print(input.text)
from selenium import webdriver
browser = webdriver.Chrome()
url = 'https://spa2.scrape.center/'
browser.get(url)
input = browser.find_element_by_class_name('logo-title')
print(input.id)
print(input.location)
print(input.tag_name)
print(input.size)
??在网页中,有一种节点叫做iframe,即子Frame。其结构与外部网页结构完全一致。selenium打开一个网页默认在父Frame里面操作。如果页面中有子Frame,默认是获取不了其节点信息的,这时需要使用swtch_to.frame方法切换Frame。
??这里和动作链时演示的网页一样,首先通过swtch_to.frame方法切换到子Frame里,然后尝试获取其中的logo节点,如果找不到(子Frame没有logo节点)抛出NoSuchElementException异常,捕捉异常输出NO LOGO,接着切回父Frame,重新获取。
from selenium import webdriver
import time
from selenium.common.exceptions import NoSuchElementException
browser = webdriver.Chrome()
url = 'https://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)
??所以,当页面中包含子Frame时,需要使用swtch_to.frame方法切换到对应的Frame,在进行操作。
??在Selenium中,get方法在网页框架加载结束后才会结束执行,如果我们尝试在get方法之行结束后获取网页源码,可能并不完整,因为某些页面还有额外的Ajax请求,页面还会经由JavaScript渲染。所以在必要的时候时候可以设置浏览器延时来确保节点已经被加载出来。
??这里有隐式和显式两种等待方式。
from selenium import webdriver
browser = webdriver.Chrome()
browser.implicitly_wait(10)
browser.get('https://spa2.scrape.center/')
input = browser.find_element_by_class_name('logo-image')
print(input)
??这里用implicitly_wait方法实现的。
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)
??这里引用WebDriverWait对象,指定最长等待时间为10,赋值给wait变量。然后调用wait的until方法,传入等待条件。
??presence_of_element_located代表节点出现其参数是节点的定位元组(By.ID,‘q’)即搜索框。在10秒内加载不出来则抛出异常。
??element_to_be_clickable代表按钮可点击。在10秒内加载不出来则抛出异常。
??Selenium可以使用forward实现前进,使用back方法实现后退。
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(2)
browser.forward()
browser.close()
??这里连续访问三个页面,使用back方法回到第二个页面,接着调用forward方法前进到第三个页面。
??使用Selenium还可以对Cookie进行操作,比如获取、添加、删除等。
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())
??该案例访问知乎,获取所有的cookie,然后添加一个cookie,传入一个字典。之后再次获取所有的cookie,最后delete_all_cookies方法删除所有cookie再输出。
??访问网页的时候,有时会开启一个个选项卡,在使用Selenium时同样可以对选项卡操作。
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://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.window方法,参数是目的选项卡的代号。
??使用Selenium时难免遇到一些异常,这是同样可以使用try except语句捕获各种异常。
??我们可以使用访问百度,尝试选择一个不存在的节点。
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.baidu.com')
browser.find_element_by_id('hello')
??这时我们可以看到抛出了NoSuchElementException异常,这通常表示节点未找到,我们可以使用try except语句捕获各类异常,改写如下:
from selenium import webdriver
from selenium.common.exceptions import TimeoutException,NoSuchElementException
browser = webdriver.Chrome()
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()
??现在很多的网站也加强了对Selenium检测,防止爬虫恶意爬取,对Selenium进行了屏蔽。
??检测的原理就是当前浏览器窗口下的window.navigator对象是否包含webdriver属性。因为正常使用浏览器时,这个属性应该是undefined,一旦使用了Selenium,这个就会设置为webdriver属性。很多网站通过JavaScript语句判断是否存在webdriver属性,如果存在直接屏蔽。
??这里很多人会想如果我直接使用下面的语句JavaScript语句把webdriver属性置空不就解决了吗。
Object.defineProperty(navigator,"webdriver",{get:()=>undefined})
??这行代码缺失可以将webdriver属性留空,但是execute_script方法在页面加载完毕后才调用这行语句的,在此之前浏览器已经检测到了,太晚了。
??在Selenium中可以使用DCP解决这个问题,利用它可以实现在每个页面刚加载时就执行JavaScript语句。这里使用执行DCP的方法叫做Page.addScriptToEvaluateOnNewDocument,将上方JavaScript传入即可。
from selenium import webdriver
from selenium.webdriver import ChromeOptions
option = ChromeOptions()
option.add_experimental_option('excludeSwitches',['enable-automation'])
option.add_experimental_option('useAutomationExtension',False)
browser = webdriver.Chrome(options=option)
browser.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument',{
'source':'Object.defineProperty(navigator,"webdriver",{get:()=>undefined})'
})
browser.get('https://antispider1.scrape.center/')
??前面每次访问都会打开一个浏览器窗口,这样无疑很麻烦,而且还会增加资源加载的时间和网络带宽。
??Chrome浏览器在60版本之后,就支持了无头模式。
from selenium import webdriver
from selenium.webdriver import ChromeOptions
option = ChromeOptions()
option.add_argument('--headless')
browser = webdriver.Chrome(options=option)
browser.set_window_size(1366,748)
browser.get('https://www.baidu.com')
browser.get_screenshot_as_file('preview.png')
??运行后就会发现,窗口不会再弹出,代码依然正常,并且输出了页面截图。