爬虫入门(二)

备注

本篇博客主要参考了图灵程序设计丛书:

  • 《Python网络爬虫权威指南》(Web Scraping with Python, 2E)

作者:【美】瑞安·米切尔

注意

大多数主流网站都会在它们的 robots.txt 文件里注明禁止爬虫接入的 url,这和相关法律责任有关,爬虫时需要注意各种细节!!

穿越网页表单与登录窗口进行抓取

Web 正在朝着页面交互、社交媒体等趋势不断演进,表单和登录窗口成了许多网站不可或缺的一部分

以下会举几个例子,利用爬虫和 Web 服务器进行数据交互

Python Requests 库

Requests 库是由 Kenneth Reitz 创建的(同时他还创建了 pipenv 工具)

这是一个擅长处理 HTTP 请求、cookie、header 等内容的 Python 第三方库

使用pip就可以简单安装上

1
pip install requests

提交一个基本调单

安全起见,就以书上提供的网站来举例 http://pythonscraping.com/pages/files/form.html

网站页面如图

接着打开浏览器检查器,可以看到网页源码里的<form>标签

1
2
3
4
5
<form method="post" action="processing.php">
First name: <input type="text" name="firstname"><br>
Last name: <input type="text" name="lastname"><br>
<input type="submit" value="Submit" id="submit">
</form>

两个输入字段的名称name是:

  • firstname
  • lastname

这两个字段的名称决定了表单提交后要被 POST 到服务器上的可变参数的名称

所以要模拟表单提交数据的行为,变量名称就要与字段名称对应

其次,表单的操作发生在 processing.php (绝对路径就是 http://pythonscraping.com/pages/files/processing.php)

requests 库提交表单很简单,代码如下

1
2
3
4
5
6
7
8
9
paras = {
'firstname': 'Nope',
'lastname': 'CH'
}
r = requests.post('http://pythonscraping.com/pages/files/processing.php',
data=paras)
print(r.text)

# Hello there, Nope CH!

对于大多数网站来说,关注其 <form> 元素里的 action 字段,字段内容为表单提交后网站会显示的页面 URL

单选按钮、复选框和其他输入

HTML 标准里提供了大量可用的表单输入字段

  • 单选按钮
  • 复选框
  • 下拉选框
  • 等等

HTML5 还增加了其他控件

  • 滚动条(范围输入字段)
  • 邮箱
  • 日期

自定义的 JavaScript 字段可以实现:

  • 取色器(colorpicker)
  • 日历
  • 其他功能

无论表单字段看起来多么复杂,仍然只需要关注两件事

  1. 字段名称————查看源代码中的name属性获得
  2. 字段值————有时会比较复杂(由JavaScript生成)

如果遇到了一个看起来很复杂的 POST 表单,并且想查看浏览器向服务器传递了哪些参数,可以用浏览器检查器或者开发者工具查看

Network -> ALL -> processing.php -> Headers -> Form Data

可以看到刚刚提交的参数

提交文件和图片

在网页 http://pythonscraping.com/files/form2.html 上由一个文件上传表单

网页源码如下

1
2
3
4
<form action="../pages/files/processing2.php" method="post" enctype="multipart/form-data">
Submit a jpg, png, or gif: <input type="file" name="uploadFile"><br>
<input type="submit" value="Upload File">
</form>

<input>标签里有一个file属性,其他的大同小异,用 requests 库处理起来也非常相似

1
2
3
4
5
6
files = {
'uploadFile': open('files/pic.png', 'rb')
}
r = requests.post('http://pythonscraping.com/pages/files/processing2.php',
files=files)
print(r.text)

可以在控制台看到输出结果

1
2
uploads/pic.png
The file pic.png has been uploaded.

处理登录和cookie

大多数现代网站都用 cookie 跟踪用户是否已经登录的状态信息

一旦网站验证了你的登录凭据,就会在你的浏览器上将其保存为一个 cookie,里面通常包含一个由服务器生成的令牌、登录有效时限和状态跟踪信息

网站会把这个 cookie 当作一种验证证据,在你浏览网站的每个页面时都出示给服务器

该网站 http://pythonscraping.com/pages/cookies/login.html 上有一个简单的登录表单(填写密码时必须是 ‘password’)

打开浏览器检查器可以找到如下两条信息

1
2
<input type="text" name="username">
<input type="password" name="password">

用 requests 库跟踪 cookie

1
2
3
4
5
6
7
8
9
10
11
params = {
'username': 'Nopech',
'password': 'password'
}
r = requests.post('http://pythonscraping.com/pages/cookies/welcome.php',
data=params)
print('Cookies: ', r.cookies.get_dict())
print('===================')
r = requests.get('http://pythonscraping.com/pages/cookies/profile.php',
cookies=r.cookies)
print(r.text)

可以看到如下打印信息

1
2
3
Cookies:  {'loggedin': '1', 'username': 'Nopech'}
===================
Hey Nopech! Looks like you're still logged into the site!

对于简单的访问,这样处理没有问题,但是如果网站比较复杂,经常暗自调整 cookie,或者你不想使用 cookie,requests 库的 session 函数就可以完美解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

session = requests.Session()

params = {
'username': 'Nopech',
'password': 'password'
}
s = session.post('http://pythonscraping.com/pages/cookies/welcome.php',
data=params)
print('Cookies: ', s.cookies.get_dict())
print('===================')
s = session.get('http://pythonscraping.com/pages/cookies/profile.php',
cookies=s.cookies)
print(s.text)

打印信息与上个例子相同

该例中,会话(session)对象会持续跟踪会话信息,包括 cookie, header,甚至是 HTTP 协议的信息,比如 HTTPAdapter

HTTP基本接入认证

在发明 cookie 之前,处理网站登录的一种常用方法就是用 HTTP 基本接入认证(HTTP basic access authentication)

在一些安全性较高的网站或公司网站、以及一些 API 上,可能会经常遇见

http://pythonscraping.com/pages/auth/login.php 网页上就是该认证方法创建的页面

用户名任意填写,密码必须是 ‘password’

requests 库下有一个 auth 模块,专门处理 HTTP 认证

1
2
3
4
5
6
7
8
import requests
from requests.auth import AuthBase, HTTPBasicAuth

auth = HTTPBasicAuth('Nopech', 'password')

r = requests.post('http://pythonscraping.com/pages/cookies/welcome.php',
auth=auth)
print(r.text)

输出结果如下

1
2
# <h2>Welcome to the Website!</h2>
# Whoops! You logged in wrong. Try again with any username, and the password "password"<br><a href="login.html">Log in here</a>

这看着像普通的 POST 请求,但是有一个 HTTPBasicAuth 对象作为 auth 参数传递到了请求中

显示的结果将是用户名和密码验证成功的页面

如果验证失败,就是一个拒绝接入页面

抓取 JavaScript

客户端脚本语言是在浏览器上运行的

客户端语言成功的前提是浏览器能够正确地解释和执行这类语言,这也是在浏览器上禁用 JavaScript 非常容易的语音

目前为止,JavaScript 是 Web 上最常用也是支持者最多的客户端脚本语言,它可以收集用户跟踪数据,不需要重载页面直接提交表单,在页面中嵌入多媒体文件,甚至运行在线游戏

用 Selenium 执行 JavaScript

Selenium 是一个强大的网页抓取工具,最初是为网站自动化测试开发的

它还被用于获取精确的网站快照,可以让浏览器自动加载网站,获取需要的数据,甚至对页面截屏,或者判断网站上是否发生了某些操作

Selenium 自身不带浏览器,需要与第三方浏览器集成才能运行

在浏览器上运行 Selenium,会打开一个窗口,进入网站,然后执行代码中设置的动作

当然,有一个叫 PhantomJS 的工具可以代替真实的浏览器

PhantomJS 是一个无头(headless)浏览器,会把网站加载到内存并执行页面上的 JavaScript,但不会向用户展示网页的图形界面

安装 Selenium

1
pip install selenium

PhantomJS 可以从它的官方网站下载,因为其是一个功能完善的浏览器,并非 Python 库

Selenium 库是一个在 WebDriver 对象上调用的 API

WebDriver 有点像可以加载网站的浏览器,但是它可以像 BeautifulSoup 对象一样用来查找页面元素,与页面上的元素交互(发送文本、点击等),以及执行其他动作来运行网络爬虫

下面的代码可以获得测试页面 http://pythonscraping.com/pages/javascript/ajaxDemo.html 上的内容

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

driver = webdriver.Chrome(executable_path='path/to/Chrome')
driver.get('http://pythonscraping.com/pages/javascript/ajaxDemo.html')
time.sleep(3)
print(driver.find_elements_by_id('content').text)
driver.close()

顺利执行就会看到类似如下的输出结果

1
2
Here is some important text you want to retrieve!
A button to click!

当然,也可能会报浏览器版本不匹配等错误,如:

1
2
selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 86
Current browser version is 88.0.4324.150 with binary path xxx

这时,可以通过 webdriver-manager 解决

1
2
3
4
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())

回到刚刚的例子,我们可以用其他的选择器来获取标签或元素:

  • driver.find_element_by_css_selector('#content')
  • driver.find_element_by_tag_name('div')
  • driver.find_element_by_class_name('class-name')

如果想选择多个元素,大部分选择器可以用 elements 来返回一个列表

  • driver.find_elements_by_css_selector('#content')
  • driver.find_elements_by_tag_name('div')

另外,如果还是想用 BeautifulSoup 来解析网页内容,可以用 WebDriver 的 page_source 函数返回页面的源代码字符串

1
2
page = driver.page_source
bs = BeautifulSoup(page, 'html.parser')

再次回到刚刚的例子,对于time.sleep()函数,这并不是无用的,因为很多网站页面加载时间是不确定的,具体依赖服务器某一毫秒的负载情况,以及多变的网速

不过用time.sleep()函数效率不够高,可能也会失效,这时,就可以换种方法,让 Selenium 不断检查某元素是否存在,以此确定页面是否加载完毕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('http://pythonscraping.com/pages/javascript/ajaxDemo.html')
try:
element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located(
(By.ID, 'loadedButton')
)
)
finally:
print(driver.find_element_by_id('content').text)
driver.close()

该程序检查 ID 为 loadedButton 的按钮是否存在,以此判断页面是不是已经完全加载

程序导入了一些新模块,WebDriverWaitexpected_conditions 组合起来构成了 Selenium 的隐式等待(imlicit wait)

隐式等待与显示等待的不同之处,在于隐式等待是等 DOM 中某个状态发生后再继续运行代码,而显示等待明确设置了等待时间

隐式等待中,DOM 触发的状态是用 expected_conditions 定义的,在 Selenium 库里元素被触发的期望条件有很多:

  • 弹出一个提示框
  • 一个元素被选中
  • 页面的标题改变了,或者文本显示在页面上或某个元素里
  • 一个元素对 DOM 可见,或者一个元素从 DOM 中消失了

大多数期望条件在使用前都需要你指定等待的目标元素,元素用定位器指定

定位器是一种抽象的查询语言,用 By 对象表示,可以用于不同场合,包括创建选择器,如下两个例子

1
2
3
4
5
EC.presence_of_element_located(
(By.ID, 'loadedButton')
)

driver.find_element(By.ID, 'loadedButton')

定位器通过 By 对象进行选择有很多种策略,如:

  • ID
  • CLASS_NAME
  • CSS_SELECTOR
  • LINK_TEXT
  • XPATH

处理重定向

客户端重定向是在服务器将页面内容发送到浏览器之前,由浏览器执行 JavaScript 完成的页面跳转,而不是服务器完成的跳转

在进行网页抓取时,客户端重定向与服务器端重定向的差异非常明显

Selenium 可以执行这种 JavaScript 重定向,但是需要学会怎么判断一个页面已经完成重定向,接下来,以网站 http://pythonscraping.com/pages/javascript/redirectDemo1.html 来举例

首先从页面开始加载时就监视 DOM 中的一个元素,然后重复调用这个元素,直到 Selenium 抛出一个 `` 异常,即元素不在页面的 DOM 里了,这说明网站已经跳转

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
import time

from selenium import webdriver
from selenium.common.exceptions import StaleElementReferenceException
from webdriver_manager.chrome import ChromeDriverManager


def wait_for_load(driver):
elem = driver.find_element_by_tag_name('html')
count = 0
while True:
count += 1
if count > 20:
print('Timing out after 10 seconds and returning')
return
time.sleep(.5)
try:
elem == driver.find_element_by_tag_name('html')
except StaleElementReferenceException:
return


driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('http://pythonscraping.com/pages/javascript/redirectDemo1.html')
wait_for_load(driver)
print(driver.page_source)

这个程序每0.5秒检查一次网页,看看 html 标签还在不在,时限为10秒

除此之外,还可以编写一个类似的循环来检查当前页面的 URL,直到 URL 发生改变,或者匹配到你寻找的特定 URL

等待元素的出现和消失时 Selenium 中一个常见的任务,这里提供一个15秒的时限和一个 XPath 选择器,完成同样的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('http://pythonscraping.com/pages/javascript/redirectDemo1.html')
try:
body_elem = WebDriverWait(driver, 15).until(
EC.presence_of_element_located(
(By.XPATH, '//body[contains(text(), "This is the page you are looking for!")]')
)
)
print(body_elem.text)
except TimeoutException:
print('Did not find the element')
finally:
driver.close()