Python-Core-50-Courses/第38课:抓取网页动态内容.md

282 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 第38课抓取网页动态内容
根据权威机构发布的全球互联网可访问性审计报告全球约有四分之三的网站其内容或部分内容是通过JavaScript动态生成的这就意味着在浏览器窗口中“查看网页源代码”时无法在HTML代码中找到这些内容也就是说我们之前用的抓取数据的方式无法正常运转了。解决这样的问题基本上有两种方案一是获取提供动态内容的数据接口这种方式也适用于抓取手机 App 的数据;另一种是通过自动化测试工具 Selenium 运行浏览器获取渲染后的动态内容。对于第一种方案我们可以使用浏览器的“开发者工具”或者更为专业的抓包工具Charles、Fiddler、Wireshark等来获取到数据接口后续的操作跟上一个章节中讲解的获取“360图片”网站的数据是一样的这里我们不再进行赘述。这一章我们重点讲解如何使用自动化测试工具 Selenium 来获取网站的动态内容。
### Selenium 介绍
Selenium 是一个自动化测试工具,利用它可以驱动浏览器执行特定的行为,最终帮助爬虫开发者获取到网页的动态内容。简单的说,只要我们在浏览器窗口中能够看到的内容,都可以使用 Selenium 获取到,对于那些使用了 JavaScript 动态渲染技术的网站Selenium 会是一个重要的选择。下面,我们还是以 Chrome 浏览器为例,来讲解 Selenium 的用法,大家需要先安装 Chrome 浏览器并下载它的驱动。Chrome 浏览器的驱动程序可以在[ChromeDriver官网](https://chromedriver.chromium.org/downloads)进行下载,驱动的版本要跟浏览器的版本对应,如果没有完全对应的版本,就选择版本代号最为接近的版本。
<img src="https://github.com/jackfrued/mypic/raw/master/20220310134558.png" style="zoom: 35%">
### 使用Selenium
我们可以先通过`pip`来安装 Selenium命令如下所示。
```Shell
pip install selenium
```
#### 加载页面
接下来,我们通过下面的代码驱动 Chrome 浏览器打开百度。
```Python
from selenium import webdriver
# 创建Chrome浏览器对象
browser = webdriver.Chrome()
# 加载指定的页面
browser.get('https://www.baidu.com/')
```
如果不愿意使用 Chrome 浏览器,也可以修改上面的代码操控其他浏览器,只需创建对应的浏览器对象(如 Firefox、Safari 等)即可。运行上面的程序,如果看到如下所示的错误提示,那是说明我们还没有将 Chrome 浏览器的驱动添加到 PATH 环境变量中,也没有在程序中指定 Chrome 浏览器驱动所在的位置。
```Shell
selenium.common.exceptions.WebDriverException: Message: 'chromedriver' executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home
```
解决这个问题的办法有三种:
1. 将下载的 ChromeDriver 放到已有的 PATH 环境变量下,建议直接跟 Python 解释器放在同一个目录,因为之前安装 Python 的时候我们已经将 Python 解释器的路径放到 PATH 环境变量中了。
2. 将 ChromeDriver 放到项目虚拟环境下的 `bin` 文件夹中Windows 系统对应的目录是 `Scripts`),这样 ChromeDriver 就跟虚拟环境下的 Python 解释器在同一个位置,肯定是能够找到的。
3. 修改上面的代码,在创建 Chrome 对象时,通过`service`参数配置`Service`对象,并通过创建`Service`对象的`executable_path`参数指定 ChromeDriver 所在的位置,如下所示:
```Python
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
browser = webdriver.Chrome(service=Service(executable_path='venv/bin/chromedriver'))
browser.get('https://www.baidu.com/')
```
#### 查找元素和模拟用户行为
接下来,我们可以尝试模拟用户在百度首页的文本框输入搜索关键字并点击“百度一下”按钮。在完成页面加载后,可以通过`Chrome`对象的`find_element`和`find_elements`方法来获取页面元素Selenium 支持多种获取元素的方式包括CSS 选择器、XPath、元素名字标签名、元素 ID、类名等前者可以获取单个页面元素`WebElement`对象),后者可以获取多个页面元素构成的列表。获取到`WebElement`对象以后,可以通过`send_keys`来模拟用户输入行为,可以通过`click`来模拟用户点击操作,代码如下所示。
```Python
from selenium import webdriver
from selenium.webdriver.common.by import By
browser = webdriver.Chrome()
browser.get('https://www.baidu.com/')
# 通过元素ID获取元素
kw_input = browser.find_element(By.ID, 'kw')
# 模拟用户输入行为
kw_input.send_keys('Python')
# 通过CSS选择器获取元素
su_button = browser.find_element(By.CSS_SELECTOR, '#su')
# 模拟用户点击行为
su_button.click()
```
如果要执行一个系列动作,例如模拟拖拽操作,可以创建`ActionChains`对象,有兴趣的读者可以自行研究。
#### 隐式等待和显式等待
这里还有一个细节需要大家知道,网页上的元素可能是动态生成的,在我们使用`find_element`或`find_elements`方法获取的时候,可能还没有完成渲染,这时会引发`NoSuchElementException`错误。为了解决这个问题,我们可以使用隐式等待的方式,通过设置等待时间让浏览器完成对页面元素的渲染。除此之外,我们还可以使用显示等待,通过创建`WebDriverWait`对象,并设置等待时间和条件,当条件没有满足时,我们可以先等待再尝试进行后续的操作,具体的代码如下所示。
```Python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
browser = webdriver.Chrome()
# 设置浏览器窗口大小
browser.set_window_size(1200, 800)
browser.get('https://www.baidu.com/')
# 设置隐式等待时间为10秒
browser.implicitly_wait(10)
kw_input = browser.find_element(By.ID, 'kw')
kw_input.send_keys('Python')
su_button = browser.find_element(By.CSS_SELECTOR, '#su')
su_button.click()
# 创建显示等待对象
wait_obj = WebDriverWait(browser, 10)
# 设置等待条件等搜索结果的div出现
wait_obj.until(
expected_conditions.presence_of_element_located(
(By.CSS_SELECTOR, '#content_left')
)
)
# 截屏
browser.get_screenshot_as_file('python_result.png')
```
上面设置的等待条件`presence_of_element_located`表示等待指定元素出现,下面的表格列出了常用的等待条件及其含义。
| 等待条件 | 具体含义 |
| ---------------------------------------- | ------------------------------------- |
| `title_is / title_contains` | 标题是指定的内容 / 标题包含指定的内容 |
| `visibility_of` | 元素可见 |
| `presence_of_element_located` | 定位的元素加载完成 |
| `visibility_of_element_located` | 定位的元素变得可见 |
| `invisibility_of_element_located` | 定位的元素变得不可见 |
| `presence_of_all_elements_located` | 定位的所有元素加载完成 |
| `text_to_be_present_in_element` | 元素包含指定的内容 |
| `text_to_be_present_in_element_value` | 元素的`value`属性包含指定的内容 |
| `frame_to_be_available_and_switch_to_it` | 载入并切换到指定的内部窗口 |
| `element_to_be_clickable` | 元素可点击 |
| `element_to_be_selected` | 元素被选中 |
| `element_located_to_be_selected` | 定位的元素被选中 |
| `alert_is_present` | 出现 Alert 弹窗 |
#### 执行JavaScript代码
对于使用瀑布式加载的页面,如果希望在浏览器窗口中加载更多的内容,可以通过浏览器对象的`execute_scripts`方法执行 JavaScript 代码来实现。对于一些高级的爬取操作,也很有可能会用到类似的操作,如果你的爬虫代码需要 JavaScript 的支持,建议先对 JavaScript 进行适当的了解,尤其是 JavaScript 中的 BOM 和 DOM 操作。我们在上面的代码中截屏之前加入下面的代码,这样就可以利用 JavaScript 将网页滚到最下方。
```Python
# 执行JavaScript代码
browser.execute_script('document.documentElement.scrollTop = document.documentElement.scrollHeight')
```
#### Selenium反爬的破解
有一些网站专门针对 Selenium 设置了反爬措施,因为使用 Selenium 驱动的浏览器,在控制台中可以看到如下所示的`webdriver`属性值为`true`,如果要绕过这项检查,可以在加载页面之前,先通过执行 JavaScript 代码将其修改为`undefined`。
<img src="https://github.com/jackfrued/mypic/raw/master/20220310154246.png" style="zoom:50%">
另一方面我们还可以将浏览器窗口上的“Chrome正受到自动测试软件的控制”隐藏掉完整的代码如下所示。
```Python
# 创建Chrome参数对象
options = webdriver.ChromeOptions()
# 添加试验性参数
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_experimental_option('useAutomationExtension', False)
# 创建Chrome浏览器对象并传入参数
browser = webdriver.Chrome(options=options)
# 执行Chrome开发者协议命令在加载页面时执行指定的JavaScript代码
browser.execute_cdp_cmd(
'Page.addScriptToEvaluateOnNewDocument',
{'source': 'Object.defineProperty(navigator, "webdriver", {get: () => undefined})'}
)
browser.set_window_size(1200, 800)
browser.get('https://www.baidu.com/')
```
#### 无头浏览器
很多时候,我们在爬取数据时并不需要看到浏览器窗口,只要有 Chrome 浏览器以及对应的驱动程序,我们的爬虫就能够运转起来。如果不想看到浏览器窗口,我们可以通过下面的方式设置使用无头浏览器。
```Python
options = webdriver.ChromeOptions()
options.add_argument('--headless')
browser = webdriver.Chrome(options=options)
```
### API参考
Selenium 相关的知识还有很多,我们在此就不一一赘述了,下面为大家罗列一些浏览器对象和`WebElement`对象常用的属性和方法。具体的内容大家还可以参考 Selenium [官方文档的中文翻译](https://selenium-python-zh.readthedocs.io/en/latest/index.html)。
#### 浏览器对象
表1. 常用属性
| 属性名 | 描述 |
| ----------------------- | -------------------------------- |
| `current_url` | 当前页面的URL |
| `current_window_handle` | 当前窗口的句柄(引用) |
| `name` | 浏览器的名称 |
| `orientation` | 当前设备的方向(横屏、竖屏) |
| `page_source` | 当前页面的源代码(包括动态内容) |
| `title` | 当前页面的标题 |
| `window_handles` | 浏览器打开的所有窗口的句柄 |
表2. 常用方法
| 方法名 | 描述 |
| -------------------------------------- | ----------------------------------- |
| `back` / `forward` | 在浏览历史记录中后退/前进 |
| `close` / `quit` | 关闭当前浏览器窗口 / 退出浏览器实例 |
| `get` | 加载指定 URL 的页面到浏览器中 |
| `maximize_window` | 将浏览器窗口最大化 |
| `refresh` | 刷新当前页面 |
| `set_page_load_timeout` | 设置页面加载超时时间 |
| `set_script_timeout` | 设置 JavaScript 执行超时时间 |
| `implicit_wait` | 设置等待元素被找到或目标指令完成 |
| `get_cookie` / `get_cookies` | 获取指定的Cookie / 获取所有Cookie |
| `add_cookie` | 添加 Cookie 信息 |
| `delete_cookie` / `delete_all_cookies` | 删除指定的 Cookie / 删除所有 Cookie |
| `find_element` / `find_elements` | 查找单个元素 / 查找一系列元素 |
#### WebElement对象
表1. WebElement常用属性
| 属性名 | 描述 |
| ---------- | -------------- |
| `location` | 元素的位置 |
| `size` | 元素的尺寸 |
| `text` | 元素的文本内容 |
| `id` | 元素的 ID |
| `tag_name` | 元素的标签名 |
表2. 常用方法
| 方法名 | 描述 |
| -------------------------------- | ------------------------------------ |
| `clear` | 清空文本框或文本域中的内容 |
| `click` | 点击元素 |
| `get_attribute` | 获取元素的属性值 |
| `is_displayed` | 判断元素对于用户是否可见 |
| `is_enabled` | 判断元素是否处于可用状态 |
| `is_selected` | 判断元素(单选框和复选框)是否被选中 |
| `send_keys` | 模拟输入文本 |
| `submit` | 提交表单 |
| `value_of_css_property` | 获取指定的CSS属性值 |
| `find_element` / `find_elements` | 获取单个子元素 / 获取一系列子元素 |
| `screenshot` | 为元素生成快照 |
### 简单案例
下面的例子演示了如何使用 Selenium 从“360图片”网站搜索和下载图片。
```Python
import os
import time
from concurrent.futures import ThreadPoolExecutor
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
DOWNLOAD_PATH = 'images/'
def download_picture(picture_url: str):
"""
下载保存图片
:param picture_url: 图片的URL
"""
filename = picture_url[picture_url.rfind('/') + 1:]
resp = requests.get(picture_url)
with open(os.path.join(DOWNLOAD_PATH, filename), 'wb') as file:
file.write(resp.content)
if not os.path.exists(DOWNLOAD_PATH):
os.makedirs(DOWNLOAD_PATH)
browser = webdriver.Chrome()
browser.get('https://image.so.com/z?ch=beauty')
browser.implicitly_wait(10)
kw_input = browser.find_element(By.CSS_SELECTOR, 'input[name=q]')
kw_input.send_keys('苍老师')
kw_input.send_keys(Keys.ENTER)
for _ in range(10):
browser.execute_script(
'document.documentElement.scrollTop = document.documentElement.scrollHeight'
)
time.sleep(1)
imgs = browser.find_elements(By.CSS_SELECTOR, 'div.waterfall img')
with ThreadPoolExecutor(max_workers=32) as pool:
for img in imgs:
pic_url = img.get_attribute('src')
pool.submit(download_picture, pic_url)
```
运行上面的代码,检查指定的目录下是否下载了根据关键词搜索到的图片。