动态网页入门

  |  

摘要: 爬虫获取异步加载网页的方法,ajax请求、Selenium、ChromeDriver

【对数据分析、人工智能、金融科技、风控服务感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:潮汐朝夕
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


动态网页是相对于静态网页而言的,有时网站元素是由 JavaScript 动态生成的。此时就需要新的数据采集方式了。

对动态网页进行信息采集,目前主要有两种方法:

  1. 逆向分析动态网页,手动分析 Network 面板的 AJAX 请求进行 HTML 的信息采集。
  2. 在 Chrome 浏览器中使用 Selenium 库模拟动态网页动作,直接在浏览器中采集已经加载好的 HTML 信息。

基础概念

AJAX

AJAX 是 Asynchronous JavaScript And XML 的首字母缩写,意为异步 JavaScript 与 XML。使用 AJAX 技术,可以在不刷新网页的情况下更新网页数据。使用 AJAX 技术的网页,一般会使用 HTML 编写网页的框架。在打开网页的时候,首先加载的是这个框架。剩下的部分将会在框架加载完成以后再通过 JavaScript 从后台加载。

使用了 AJAX 的网页,其中的一些内容并不在源代码中存在,这也是判断网页是否使用了 AJAX 的方法。

JSON

JSON 的全称是JavaScript Object Notation,是一种轻量级的数据交换格式。网络之间使用 HTTP 方式传递数据的时候,绝大多数情况下传递的都是字符串。因此,当需要把 Python 里面的数据发送给网页或者其他编程语言的时候,可以先将 Python 的数据转化为 JSON 格式的字符串,然后将字符串传递给其他语言,其他语言再将 JSON 格式的字符串转换为它自己的数据格式。

请求头

Headers 称为请求头,浏览器可以将一些信息通过 Headers 传递给服务器,服务器也可以将一些信息通过 Headers 传递给浏览器。电商网站常常应用的 Cookies 就是 Headers 里面的一个部分。


静态网页和动态网页的区别

给定一个网页,判断它是静态网页还是动态网页的方法如下:

(1) 按 F12,调用 Chrome 开发者工具,找到 Elements 面板,上面显示的是浏览器执行 JavaScript 之后生成的 HTML 源代码。

(2) 右键单击页面,选择查看网页源代码,也可以得到一份 HTML 源代码。

如果这两者的 HTML 完全一致,则是静态网页;如果有些在 (1) 的 HTML 中有的内容,在 (2) 中找不到,那么就是由 JavaScript 动态生成加载的动态网页。


逆向分析采集动态网页数据

很多网页是 HTML 静态生成的内容,直接从 HTML 源代码中就能找到。但很多网站使用 AJAX 和动态 HTML,这样被加载的内容是不能在源代码中找到的。此时如果要抓取被加载的内容,使用 Google Chrome 浏览器的开发者模式进行逆向分析是一种解法。

例子

获取 https://www.ptpress.com.cn/ 的新书推荐板块中的书籍信息。

以人民邮电出版社网站 https://www.ptpress.com.cn/ 为例,通过逆向分析来采集相应内容。

按 F12,打开 https://www.ptpress.com.cn/ 的 Chrome 开发者工具:

打开网络(Network)面板后,会发现有很多响应。在网络面板中,XHR 是 AJAX 中的概念,表示 XML-HTTP-request,一般 JavaScript 加载的文件隐藏在 JS 或者 XHR 中。

以新书推荐书模块为例,XHR 的 Preview 标签中有需要的信息。在网络面板的 XHR 中查看资源的 Preview 信息,可以看到的 HTML 信息


若要采集新书推荐书模块的书名、ID 信息,步骤如下。

  • step1: 首先找到资源下的 Header 标签,找到 Request URL 的网址信息:

  • step2: 打开这个网页,得到需要采集的 JSON 信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
"data": [
{
"picPath": "https://cdn.ptpress.cn/uploadimg/Material/978-7-115-56569-3/72jpg/56569_s300.jpg",
"bookName": "计算机体系结构:量化研究方法(第6版)",
"bookId": "44266acf-7332-4d7f-b3e7-4c119c232912"
},
{
"picPath": "https://cdn.ptpress.cn/uploadimg/Material/978-7-115-58263-8/72jpg/58263_s300.jpg",
"bookName": "计算机科学概论(第13版)",
"bookId": "0017af51-ef24-4d67-8e52-82ae6052cc4b"
},
{
"picPath": "https://cdn.ptpress.cn/uploadimg/Material/978-7-115-59717-5/72jpg/59717_s300.jpg",
"bookName": "Python深度学习(第2版)",
"bookId": "4dcb1346-8b92-42fd-ae1d-c19f1eaaad80"
},
{
"picPath": "https://cdn.ptpress.cn/uploadimg/Material/978-7-115-58389-5/72jpg/58389_s300.jpg",
"bookName": "数据分析咖哥十话 从思维到实践促进运营增长",
"bookId": "3e2d1256-810d-4afa-83eb-afc07b6c3144"
},
{
"picPath": "https://cdn.ptpress.cn/uploadimg/Material/978-7-115-55284-6/72jpg/55284_s300.jpg",
"bookName": "01改变世界:计算机发展史趣谈",
"bookId": "a077f874-a968-4614-a0a8-d6f49a9b1e43"
},
{
"picPath": "https://cdn.ptpress.cn/uploadimg/Material/978-7-115-59600-0/72jpg/59600_s300.jpg",
"bookName": "趣学算法(第2版)",
"bookId": "bba8823a-f470-40b8-9fc7-3f4405a690ae"
},
{
"picPath": "https://cdn.ptpress.cn/uploadimg/Material/978-7-115-59350-4/72jpg/59350.jpg",
"bookName": "Python编程快速上手2:趣味小项目轻松学",
"bookId": "f26eb4ca-55b1-4f4a-9300-b4b85d29cd72"
},
{
"picPath": "https://cdn.ptpress.cn/uploadimg/Material/978-7-115-59354-2/72jpg/59354_s300.jpg",
"bookName": "中国游戏风云",
"bookId": "76ab06b2-3240-4816-acc6-dd0d6b522fa3"
}
],
"msg": "返回数据成功!",
"success": true
}
  • step3: 基于 JSON 信息,采集新书推荐的书名、ID 信息,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import json

url = "https://www.ptpress.com.cn/recommendBook/getRecommendBookListForPortal?bookTagId=2725fe7b-b2c2-4769-8f6f-c95f04c70275"

# 在需要采集的 URL 进行 HTTP 请求
return_data = requests.get(url).text
# 将 HTTP 相应的数据转为 JSON
data = json.loads(return_data)
# 索引到需要采集的内容信息
infos = data["data"]

for record in infos:
bookName = record["bookName"]
bookId = record["bookId"]
print("新书名:{}\n新书ID:{}".format(bookName, bookId))

运行结果如下,注意动态信息会更新,所以不同时间采集到的信息会不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
新书名:计算机体系结构:量化研究方法(第6版)
新书ID:44266acf-7332-4d7f-b3e7-4c119c232912
新书名:计算机科学概论(第13版)
新书ID:0017af51-ef24-4d67-8e52-82ae6052cc4b
新书名:Python深度学习(第2版)
新书ID:4dcb1346-8b92-42fd-ae1d-c19f1eaaad80
新书名:数据分析咖哥十话 从思维到实践促进运营增长
新书ID:3e2d1256-810d-4afa-83eb-afc07b6c3144
新书名:01改变世界:计算机发展史趣谈
新书ID:a077f874-a968-4614-a0a8-d6f49a9b1e43
新书名:趣学算法(第2版)
新书ID:bba8823a-f470-40b8-9fc7-3f4405a690ae
新书名:Python编程快速上手2:趣味小项目轻松学
新书ID:f26eb4ca-55b1-4f4a-9300-b4b85d29cd72
新书名:中国游戏风云
新书ID:76ab06b2-3240-4816-acc6-dd0d6b522fa3

使用 Selenium 采集动态网页数据

Selenium 是一种自动化测试工具,能模拟浏览器的行为,它支持各种浏览器,包括 Chrome、Firefox 等主流界面式浏览器,在浏览器里面安装一个 Selenium 的插件,即可方便地实现 Web 界面的测试。

Selenium 安装

1
pip install selenium

selenium 调用浏览器时需要补丁文件,Firefox 的补丁为 geckodriver;Chrome 的补丁为 chromedriver。

chromedirver 的下载链接如下,两个链接都可以,下载与自己的 Chrome 版本适配的即可:

有时候 chromedriver 已经有了,例如我的电脑上 which chromedriver 的结果如下:

1
/usr/local/bin/chromedriver

这样的话就不用单独安装了。

已经存在的 chromedriver 可能版本与 chrome 不一致,那么还是回到前面的链接下载版本适配的 chromedriver,然后移动到 /usr/local/bin 中。

分析的网页结构

分析 https://www.ptpress.com.cn/search/books/ 的网页结构,首先获取该 URL 对应的 HTML:

1
2
3
4
5
6
7
from selenium import webdriver

driver = webdriver.Chrome()
driver.get("https://www.ptpress.com.cn/search/books")
data = driver.page_source
print(data)
driver.quit()

结果是一段 HTML,从中可以分析该网页的结构。。

页面等待

当浏览器加载一个页面时,页面中的元素可能会以不同的时间间隔加载,这使得定位元素比较困难,使用页面等待可以解决这个问题。页面等待在执行的操作之间提供了一些松弛,主要用于定位一个元素或任何其他带有该元素的操作。

Selenium WebDriver 提供了两种类型的等待 — 隐式和显式:

  • 显式等待使网络驱动程序在继续执行之前等待某个条件的发生。
  • 隐式等待使 WebDriver 在尝试定位一个元素时,在一定的时间内轮询 DOM。

显示等待的 WebDriverWait 函数的特性如下:

  • WebDriverWait 函数默认每 500 毫秒调用一次 ExpectedCondition,直到成功返回。
  • ExpectedCondition 的成功返回类型是布尔值,对于所有其他 ExpectedCondition 类型,则返回 True 或非 Null 返回值。
  • 如果 WebDriverWait 在 10 秒内没有发现元素返回,就会抛出 TimeoutException 异常。
1
2
3
# driver: 接收 str 表示打开的网页
# time: 接收数值表示等待时间,单位秒
WebDriverWait(driver, time)

示例代码 (确认元素是否可点击):

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

driver = webdriver.Chrome()
driver.get("https://www.ptpress.com.cn/search/books")

# 确认元素是否可单击(等待“确认”按钮加载完成)
wait = WebDriverWait(driver, 10)
confirm_btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#app > div:nth-child(1) > div > div > div > button > i")))

print(confirm_btn)
driver.close()

结果:

1
<selenium.webdriver.remote.webelement.WebElement (session="1174b58105b1b719998d1f7475edf678", element="a0ac6c8e-bbd4-4b53-974f-1fbb9baa1397")>

页面操作

选择选项卡

在浏览器切换选项卡,下面是一个代码示例,首先打开 http://www.ptpress.com.cn/search/books ,然后在第一个选项卡放网页 http://www.tipdm.org 、第二个选项卡放网页 http://www.tipdm.com

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

driver = webdriver.Chrome()
driver.get('http://www.ptpress.com.cn/search/books')
driver.execute_script('window.open()')
print(driver.window_handles)
driver.switch_to.window(driver.window_handles[1])
driver.get('http://www.tipdm.com')
time.sleep(1)
driver.switch_to.window(driver.window_handles[0])
driver.get('http://www.tipdm.org')

填充表单

HTML 表单包含了表单元素,而表单元素指的是不同类型的 input 元素、复选框、单选按钮、提交按钮等。

在 Elements 中定位“搜索”按钮并复制该元素的 selector,如下图:

于是形成以下代码:

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

driver = webdriver.Chrome()
driver.get("https://www.ptpress.com.cn/search/books")

# 确认元素是否可单击(等待“确认”按钮加载完成)
wait = WebDriverWait(driver, 10)
confirm_btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#app > div:nth-child(1) > div > div > div > button > i")))

# 单击搜索按钮
confirm_btn.click()

执行 JavaScript

Selenium 库中的 execute_script 方法能够直接调用 JavaScript 方法来实现翻页到底部、弹框等操作。例如,在网页 http://www.ptpress.com.cn/search/books 中通过 JavaScript 翻页到底部,并弹框提示。

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

driver = webdriver.Chrome()
driver.get("https://www.ptpress.com.cn/search/books")

# 翻页到底部
driver.execute_script('window.scrollTo(0, document.body.scrollHeight)')
driver.execute_script('alert("Python爬虫")')

元素提取

Selenium 提供了 find_element/find_elements 方法,可以配合 By 类来来定位页面中的元素。用法见后面的例子。

定位一个元素

查找网页 http://www.ptpress.com.cn/search/books 响应的搜索框架的元素,如下图:

从图中可以看到,id 为 searchVal。分别通过元素 ID、CSS 选择器、XPath 表达式来获取搜索框架的元素代码如下

1
2
3
4
5
6
7
8
9
driver = webdriver.Chrome()
driver.get("https://www.ptpress.com.cn/search/books")

input_first = driver.find_element(By.ID,"searchVal")
input_second = driver.find_element(By.CSS_SELECTOR, "#searchVal")
input_third = driver.find_element(By.XPATH, '//*[@id="searchVal"]')
print(input_first)
print(input_second)
print(input_third)

获取的结果都一样,如下:

1
<selenium.webdriver.remote.webelement.WebElement (session="1df03aa87eb15943052d2fd9247c8207", element="f65d85b7-12a2-41a7-957d-837213f2d43f")>

定位多个元素

还是网页 http://www.ptpress.com.cn/search/books 这次看顶部导航条,下面获取对应的多个元素,如下图:

可以看到 id 是 nav。通过 ID 获取多个元素的代码如下:

1
lis = driver.find_elements(By.ID, "nav")

结果如下:

1
[<selenium.webdriver.remote.webelement.WebElement (session="1df03aa87eb15943052d2fd9247c8207", element="6ce84f07-2df5-4d5b-9c20-c01de8e36a29")>]

预期条件

Selenium 库提供了一些常用的较为便利的判断方法:

1
from selenium.webdriver.support import expected_conditions as EC

例如前面的代码中我们用了 EC.element_to_be_clickable 来确认元素是否可单击。

一些常用的判断方法如下:

方法 作用
title_is 标题是某内容
title_contains 标题包含某内容
presence_of_element_located 元素加载出,传入定位元组,例如 (By.ID, 'p')
visibility_of_element_located 元素可见,传入定位元组
visibility_of 传入元素对象
presence_of_all_elements_located 所有元素加载出
text_to_be_present_in_element 某个元素文本包含某文字
text_to_be_present_in_element_value 某个元素值包含某文字
frame_to_be_available_and_switch_to_it 加载并切换
invisibility_of_element_located 元素不可见
element_to_be_clickable 元素可单击
staleness_of 判断一个元素是否仍在 DOM,可判断页面是否已经刷新
element_to_be_selected 元素可选择,传入元素对象
element_located_to_be_selected 元素可选择,传入定位元组
element_selection_state_to_be 传入元素对象及状态,相等返回 True,否则返回 False
element_located_selection_state_to_be 传入定位元组及状态,相等返回 True,否则返回 False
alert_is_present 是否出现 Alert

完整例子

综合前面的知识点与示例,形成一个完整的项目:在 http://www.ptpress.com.cn/search/book 的搜索框中搜索“Python编程”关键词,保存结果。

整个流程见代码注释,知识点都是前面提到过的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import re
import time

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
from bs4 import BeautifulSoup

# 创建WebDriver对象
driver = webdriver.Chrome()

# 等待变量
wait = WebDriverWait(driver,10)

# 打开网页
driver.get('http://www.ptpress.com.cn/search/books')

# 等待“搜索”按钮加载完成
search_btn = driver.find_element(By.ID, "searchVal")

#在搜索框填写“Python编程”
search_btn.send_keys('Python编程')

# 确认元素是否已经出现
element_appear = wait.until(EC.presence_of_element_located((By.ID, 'searchVal')))

# 等待“确认”按钮加载完成
confirm_btn = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, '#app > div:nth-child(1) > div > div > div > button > i'))
)

# 单击“确认”按钮
confirm_btn.click()

# 等待5秒
time.sleep(5)

# 获取 HTML
html = driver.page_source

# 使用 BeautifulSoup 找到书籍信息的模块
soup = BeautifulSoup(html,'lxml')
a = soup.select('.rows')

# 使用正则表达式解析书籍图片信息
ls1 = '<img src="(.*?)"/></div>'
pattern = re.compile(ls1, re.S)
res_img = re.findall(pattern,str(a))

# 使用正则表达式解析书籍文字信息
ls2 = '<img src=".*?"/></div>.*?<p>(.*?)</p></a>'
pattern1 = re.compile(ls2, re.S)
res_test = re.findall(pattern1, str(a))

print(res_test,res_img)
driver.close()

结果如下:

1
['Python编程零基础入门', 'Python编程快速上手2:趣味小项目轻松学', 'Python编程做中学', '趣学Python编程', 'Python编程基础(第2版)(微课版)', 'Python编程超简单', 'Python编程轻松进阶', '(包销不上架)人工智能编程实践(Python编程6级)', 'Python编程入门与算法进阶'] ['https://cdn.ptpress.cn/uploadimg/Material/978-7-115-51977-1/72jpg/51977_s300.jpg', 'https://cdn.ptpress.cn/uploadimg/Material/978-7-115-59350-4/72jpg/59350.jpg', 'https://cdn.ptpress.cn/uploadimg/Material/978-7-115-58939-2/72jpg/58939_s300.jpg', 'https://cdn.ptpress.cn/uploadimg/Material/978-7-115-33595-1/72jpg/33595_s300.jpg', 'https://cdn.ptpress.cn/uploadimg/Material/978-7-115-57563-0/72jpg/57563_s300.jpg', 'https://cdn.ptpress.cn/uploadimg/Material/978-7-115-57429-9/72jpg/57429_s300.jpg', 'https://cdn.ptpress.cn/uploadimg/Material/978-7-115-59242-2/72jpg/59242_s300.jpg', 'https://cdn.ptpress.cn/uploadimg/Material/978-7-115-58576-9/72jpg/58576.jpg', 'https://cdn.ptpress.cn/uploadimg/Material/978-7-115-58359-8/72jpg/58359_s300.jpg']

Share