第 1 章 初见网络爬虫

第 1 章 初见网络爬虫

一旦你开始抓取网页,就会感受到浏览器为我们做的所有细节。网页上如果没有 HTML 文本格式层、CSS 样式层、JavaScript 执行层和图像渲染层,乍看起来会有点儿吓人,但是在这一章和下一章中,我们将介绍如何在不借助浏览器帮助的情况下格式化和理解数据。

本章将首先向网络服务器发送 GET 请求(获取网页内容的请求)以获取具体网页,再从网页中读取 HTML 内容,最后做一些简单的信息提取,将我们要寻找的内容分离出来。

1.1 网络连接

如果你没在网络或网络安全上花过太多时间,那么互联网的原理可能看起来有点儿神秘。准确地说,每当打开浏览器连接 http://google.com 的时候,我们不会思考网络正在做什么,而且如今也不必思考。实际上,我认为很神奇的是,计算机接口已经如此先进,让大多数人上网的时候完全不思考网络是如何工作的。

但是,网页抓取需要抛开一些接口的遮挡,不仅是在浏览器层(它如何解释所有的 HTML、CSS 和 JavaScript),有时也包括网络连接层。

我们通过下面的例子让你对浏览器获取信息的过程有一个基本的认识。Alice 有一台网络服务器。Bob 有一台台式机正试图连接到 Alice 的服务器。当一台机器想与另一台机器对话时,下面的某个行为将会发生。

(1) Bob 的电脑发送一串 1 和 0 比特值,表示电路上的高低电压。这些比特构成了一种信息,包括请求头和消息体。请求头包含当前 Bob 的本地路由器 MAC 地址和 Alice 的 IP 地址。消息体包含 Bob 对 Alice 服务器应用的请求。

(2) Bob 的本地路由器收到所有 1 和 0 比特值,把它们理解成一个数据包(packet),从 Bob 自己的 MAC 地址“寄到”Alice 的 IP 地址。他的路由器把数据包“盖上”自己的 IP 地址作为“发件”地址,然后通过互联网发出去。

(3) Bob 的数据包游历了一些中介服务器,沿着正确的物理 / 电路路径前进,到了 Alice 的服务器。

(4) Alice 的服务器在她的 IP 地址收到了数据包。

(5) Alice 的服务器读取数据包请求头里的目标端口,然后把它传递到对应的应用——网络服务器应用。(目标端口通常是网络应用的 80 端口,可以理解成数据包的“房间号”,IP 地址就是“街道地址”)。

(6) 网络服务器应用从服务器处理器收到一串数据,数据是这样的:

  • 这是一个 GET 请求
  • 请求文件 index.html

(7) 网络服务器应用找到对应的 HTML 文件,把它打包成一个新的数据包发送给 Bob,然后通过它的本地路由器发出去,用同样的过程回传到 Bob 的机器上。

瞧!我们就这样实现了互联网。

那么,在这场数据交换中,Web 浏览器是从哪里开始参与的?完全没有参与。其实,在互联网的历史中,浏览器是一个比较新的发明,始于 1990 年的 Nexus 浏览器。

的确,Web 浏览器是一个非常有用的应用,它创建信息的数据包,命令操作系统发送它们,然后把你获取的数据解释成漂亮的图像、声音、视频和文字。但是,Web 浏览器就是代码,而代码可以分解成许多基本组件,可重写、重用,以及做成我们想要的任何东西。Web 浏览器可以让处理器将数据发送到那些对接无线(或有线)网络接口的应用上,但是你可以用短短的 3 行 Python 代码实现这些功能:

from urllib.request import urlopen

html = urlopen('http://pythonscraping.com/pages/page1.html')
print(html.read())

你可以使用 GitHub 仓库中的 iPython notebook for Chapter 1(https://github.com/REMitchell/python-scraping/blob/master/Chapter01_BeginningToScrape.ipynb)运行以上代码,也可以把上面这段代码保存为 scrapetest.py,然后在终端运行如下命令:

$ python scrapetest.py

注意,如果你的设备上也安装了 Python 2.x,并且同时运行两个版本的 Python,可能需要直接指明版本才能运行 Python 3.x 代码:

$ python3 scrapetest.py

这将会输出 http://pythonscraping.com/pages/page1.html 这个网页的全部 HTML 代码。更准确地说,这会输出在域名为 http://pythonscraping.com 的服务器上 < 网络应用根地址 >/ pages 文件夹里的 HTML 文件 page1.html 的源代码。

为什么将这些地址理解为“文件”而不是“页面”非常关键呢?现在大多数网页需要加载许多相关的资源文件,可能是图像文件、JavaScript 文件、CSS 文件,或你需要连接的其他各种网页内容。当 Web 浏览器遇到一个标签时,比如 <img src="cuteKitten.jpg">,会向服务器发起另一个请求,以获取 cuteKitten.jpg 文件中的数据为用户充分渲染网页。

当然,你的 Python 程序没有返回并向服务器请求多个文件的逻辑,它只能读取你直接请求的单个 HTML 文件。

from urllib.request import urlopen

上面的代码其实已经表明了它的含义:它查找 Python 的 request 模块(在 urllib 库里面),只导入 urlopen 函数。

urllib 是 Python 的标准库(就是说你不用额外安装就可以运行这个例子),包含了从网页请求数据,处理 cookie,甚至改变像请求头和用户代理这些元数据的函数。我们将在本书中广泛使用 urllib,所以建议你读读这个库的 Python 文档。

urlopen 用来打开并读取一个从网络获取的远程对象。因为它是一个非常通用的函数(可以轻松读取 HTML 文件、图像文件或其他任何文件流),所以我们将在本书中频繁地使用它。

1.2 BeautifulSoup简介

“美味的汤,绿色的浓汤,
在热气腾腾的盖碗里装!
谁不愿意尝一尝,这样的好汤?
晚餐用的汤,美味的汤!”

BeautifulSoup 库的名字取自刘易斯 • 卡罗尔在《爱丽丝梦游仙境》里的同名诗歌。在故事中,这首诗是素甲鱼 1 唱的。

1Mock Turtle,它本身是一个双关语,指英国维多利亚时代的流行菜肴素甲鱼汤,它其实不是用甲鱼而是用牛肉做的,如同中国的豆制品素鸡,名为素鸡,其实与鸡无关。——译者注

就像它在仙境中的说法一样,BeautifulSoup 尝试化平淡为神奇。它通过定位 HTML 标签来格式化和组织复杂的网页信息,用简单易用的 Python 对象为我们展现 XML 结构信息。

1.2.1 安装BeautifulSoup

由于 BeautifulSoup 库不是 Python 标准库,因此需要单独安装。如果你安装过 Python 库,可以使用你最喜爱的安装器并略过本小节,直接阅读 1.2.2 节。

对于还没有安装过 Python 库的新手(或者需要温习的读者)来说,以下介绍的方法将会用于安装本书中的多个库,所以在后面你可能需要回顾本小节。

在本书中,我们将使用 BeautifulSoup 4(也叫 BS4)。Crummy.com 中有 BeautifulSoup 4 的完整安装说明。Linux 系统上的基本安装方法是:

$ sudo apt-get install python-bs4

对于 macOS 系统,首先用以下命令安装 Python 的包管理器 pip:

$ sudo easy_install pip

然后运行以下命令来安装库。

$ pip install beautifulsoup4

另外,注意如果你的设备上同时安装了 Python 2.x 和 Python 3.x,你需要用 python3 运行 Python 3.x:

$ python3 myScript.py

安装包的时候,也要使用这条命令,否则包有可能安装到 Python 2.x 而不是 Python 3.x 里:

$ sudo python3 setup.py install

如果用 pip 安装,你还可以用 pip3 安装 Python 3.x 版本的包:

$ pip3 install beautifulsoup4

在 Windows 系统上安装包与在 Linux 和 macOS 上安装差不多。从下载页面下载最新的 BeautifulSoup 4 源代码,解压后进入文件,然后执行:

> python setup.py install

这样就可以了!BeautifulSoup 将被当作设备上的一个 Python 库。你可以在 Python 终端里导入它测试一下:

$ python
> from bs4 import BeautifulSoup

如果没有错误,说明导入成功了。

另外,还有一个 Windows 版 pip 的 .exe 格式安装器,装了之后你就可以轻松安装和管理包了:

> pip install beautifulsoup4

用虚拟环境保存库文件

如果你同时负责多个 Python 项目,或者想要轻松打包某个项目及其关联的库文件,再或者你担心已安装的库之间可能有冲突,那么你可以安装一个 Python 虚拟环境来分而治之。

当不用虚拟环境安装一个 Python 库的时候,你实际上是全局安装它。这通常需要有管理员权限,或者以 root 身份安装,这个库文件对设备上的每个用户和每个项目来说都是存在的。好在创建虚拟环境非常简单:

$ virtualenv scrapingEnv

这样就创建了一个叫作 scrapingEnv 的新环境,你需要先激活它再使用:

$ cd scrapingEnv/
$ source bin/activate

激活环境之后,你会在命令行提示符前面看到环境名称,提醒你当前处于虚拟环境中。后面你安装的任何库和执行的任何程序都在这个环境下运行。

在新建的 scrapingEnv 环境里,可以安装并使用 BeautifulSoup:

(scrapingEnv)ryan$ pip install beautifulsoup4
(scrapingEnv)ryan$ python
> from bs4 import BeautifulSoup
>

当不再使用虚拟环境中的库时,可以通过 deactivate 命令来退出环境:

(scrapingEnv)ryan$ deactivate
ryan$ python
> from bs4 import BeautifulSoup
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named 'bs4'

将项目关联的所有库单独放在一个虚拟环境里,还有助于轻松打包整个环境发送给其他人。只要他们机器上安装的 Python 版本和你的相同,你打包的代码就可以直接通过虚拟环境运行,不需要再安装任何库。

尽管本书的例子都不要求你使用虚拟环境,但是请记住,你可以在任何时候激活并使用它。

1.2.2 运行BeautifulSoup

BeautifulSoup 库最常用的对象恰好就是 BeautifulSoup 对象。让我们把本章开头的例子调整一下再运行看看:

from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen('http://www.pythonscraping.com/pages/page1.html')

bs = BeautifulSoup(html.read(), 'html.parser')
print(bs.h1)

输出结果是:

<h1>An Interesting Title</h1>

这里仅仅返回了页面上的第一个 h1 标签实例。通常情况下,一个页面也只有一个 h1 标签,但是在 Web 中这个惯例经常被打破,因此你应该意识到这里仅仅检索了该标签的第一个实例,而不一定是你寻找的那个。

和前面网页抓取的例子一样,你导入 urlopen 函数,然后调用 html.read() 获取网页的 HTML 内容。除了文本字符串,BeautifulSoup 还可以使用 urlopen 直接返回的文件对象,而不需要先调用 .read() 函数:

bs = BeautifulSoup(html, 'html.parser')

这样就可以把 HTML 内容传到 BeautifulSoup 对象,转换成下面的结构:

  • html → <html><head>...</head><body>...</body></html>
    • head → <head><title>A Useful Page</title></head>
      • title → <title>A Useful Page</title>
    • body → <body><h1>An Int...</h1><div>Lorem ip...</div></body>
      • h1 → <h1>An Interesting Title</h1>
      • div → <div>Lorem Ipsum dolor...</div>

可以看出,我们从网页中提取的 <h1> 标签被嵌在 BeautifulSoup 对象结构的第二层(htmlbodyh1)。但是,当我们从对象里提取 h1 标签的时候,可以直接调用它:

bs.h1

其实,下面的所有函数调用都可以产生相同的结果:

bs.html.body.h1
bs.body.h1
bs.html.h1

当你创建一个 BeautifulSoup 对象时,需要传入两个参数:

bs = BeautifulSoup(html.read(), 'html.parser')

第一个参数是该对象所基于的 HTML 文本,第二个参数指定了你希望 BeautifulSoup 用来创建该对象的解析器。在大多数情况下,你选择任何一个解析器都差别不大。

html.parser 是 Python 3 中的一个解析器,不需要单独安装。如果不是特殊场景的需要,本书中都将使用这个解析器。

另一个常用的解析器是 lxml,可以通过 pip 命令安装:

$ pip3 install lxml

BeautifulSoup 使用 lxml 解析器时,只需要改变解析器参数:

bs = BeautifulSoup(html.read(), 'lxml')

html.parser 相比,lxml 的优点在于解析“杂乱”或者包含错误语法的 HTML 代码的性能更优一些。它可以容忍并修正一些问题,例如未闭合的标签、未正确嵌套的标签,以及缺失的头(head)标签或正文(body)标签。lxml 也比 html.parser 更快,但是考虑到网络本身的速度将总是你最大的瓶颈,在网页抓取中速度并不是一个必备的优势。

lxml 的一个缺点是它必须单独安装,并且它依赖于第三方的 C 语言库。相对于 html.parser 来说,这可能会导致可移植性和易用性问题。

另外一个常用的 HTML 解析器是 html5lib。和 lxml 一样,html5lib 也是一个具有容错性的解析器,它甚至可以容忍语法更糟糕的 HTML。它也依赖于外部依赖,并且比 lxmlhtml.parser 都慢。尽管如此,如果你处理的是一些杂乱的或者手写的 HTML 网站,那么该解析器可能是一个不错的选择。

可以通过安装并将 html5lib 字符串传递给 BeautifulSoup 对象来使用它:

bs = BeautifulSoup(html.read(), 'html5lib')

希望这个例子可以向你展示 BeautifulSoup 库的强大与简单。其实,任何 HTML(或 XML)文件的任意节点信息都可以被提取出来,只要目标信息的旁边或附近有标签就行。第 2 章将进一步探讨一些更复杂的 BeautifulSoup 函数,还会介绍正则表达式,以及如何把正则表达式用于 BeautifulSoup 以提取网站信息。

1.2.3 可靠的网络连接以及异常的处理

Web 是十分复杂的。网页数据格式不友好、网站服务器死机、目标数据的标签找不到,都是很麻烦的事情。网页抓取最痛苦的遭遇之一,就是爬虫运行的时候你洗洗睡了,梦想着明天一早数据就都会抓取好放在数据库里,结果第二天醒来,你看到的却是一个因某种数据格式异常导致运行错误的爬虫,在前一天当你不再盯着屏幕去睡觉之后,没过一会儿爬虫就不再运行了。那个时候,你可能想骂发明网站(以及那些奇葩的网络数据格式)的人,但是你真正应该斥责的人是你自己,为什么一开始不估计可能会出现的异常!

让我们看看爬虫 import 语句后面的第一行代码,看看如何处理可能出现的异常:

html = urlopen('http://www.pythonscraping.com/pages/page1.html')

这行代码主要会发生两种异常:

  • 网页在服务器上不存在(或者获取页面的时候出现错误)
  • 服务器不存在

发生第一种异常时,程序会返回 HTTP 错误。HTTP 错误可能是“404 Page Not Found”“500 Internal Server Error”等。对于所有类似情形,urlopen 函数都会抛出 HTTPError 异常。我们可以用下面的方式处理这种异常:

from urllib.request import urlopen
from urllib.error import HTTPError

try:
    html = urlopen('http://www.pythonscraping.com/pages/page1.html')
except HTTPError as e:
    print(e)
    # 返回空值,中断程序,或者执行另一个方案
else:
    # 程序继续。注意:如果你已经在上面异常捕捉那一段代码里返回或中断(break),
    # 那么就不需要使用else语句了,这段代码也不会执行

如果程序返回 HTTP 错误代码,程序就会显示错误内容,不再执行 else 语句后面的代码。

如果服务器不存在(就是说链接 http://www.pythonscraping.com 打不开,或者是 URL 链接写错了),urlopen 会抛出一个 URLError 异常。这就意味着获取不到服务器,并且由于远程服务器负责返回 HTTP 状态代码,所以不能抛出 HTTPError 异常,而且还应该捕获到更严重的 URLError 异常。你可以增加以下检查代码:

from urllib.request import urlopen
from urllib.error import HTTPError
from urllib.error import URLError

try:
    html = urlopen('https://pythonscrapingthisurldoesnotexist.com')
except HTTPError as e:
    print(e)
except URLError as e:
    print('The server could not be found!')
else:
    print('It Worked!')

当然,即使从服务器成功获取网页,如果网页上的内容并非完全是我们期望的那样,仍然可能会出现异常。每当你调用 BeautifulSoup 对象里的一个标签时,增加一个检查条件以保证标签确实存在是很聪明的做法。如果你想要调用的标签不存在,BeautifulSoup 就会返回 None 对象。不过,如果再调用这个 None 对象下面的子标签,就会发生 AttributeError 错误。

下面这行代码(nonExistentTag 是虚拟的标签,BeautifulSoup 对象里实际没有)

print(bs.nonExistentTag)

会返回一个 None 对象。处理和检查这个对象是十分必要的。如果你不检查,直接调用这个 None 对象的子标签,就会有麻烦,如下所示。

print(bs.nonExistentTag.someTag)

这时就会返回一个异常:

AttributeError: 'NoneType' object has no attribute 'someTag'

那么怎么才能避免这两种情形的异常呢?最简单的方式就是对两种情形进行检查:

try:
    badContent = bs.nonExistingTag.anotherTag
except AttributeError as e:
    print('Tag was not found')
else:
    if badContent == None:
        print ('Tag was not found')
    else:
        print(badContent)

初看这些检查与错误处理的代码会觉得有点儿累赘,但是我们可以重新简单组织一下代码,让它变得不那么难写(更重要的是,不那么难读)。例如,下面的代码是上面爬虫的另一种写法:

from urllib.request import urlopen
from urllib.error import HTTPError
from bs4 import BeautifulSoup

def getTitle(url):
    try:
        html = urlopen(url)
    except HTTPError as e:
        return None
    try:
        bs = BeautifulSoup(html.read(), 'html.parser')
        title = bs.body.h1
    except AttributeError as e:
        return None
    return title

title = getTitle('http://www.pythonscraping.com/pages/page1.html')
if title == None:
    print('Title could not be found')
else:
    print(title)

在这个例子中,我们创建了一个 getTitle 函数,它可以返回网页的标题,如果获取网页的时候遇到问题就返回一个 None 对象。在 getTitle 函数里面,我们像前面那样检查了 HTTPError,还检查了由于 URL 输入错误引起的 URLError,然后把两行 BeautifulSoup 代码封装在一个 try 语句里面。这两行中的任何一行有问题,都可能抛出 AttributeError(如果服务器不存在,html 就是一个 None 对象,html.read() 就会抛出 AttributeError)。其实,我们可以在 try 语句里面放任意多行代码,或者调用一个在任意位置都可以抛出 AttributeError 的函数。

在写爬虫的时候,思考代码的总体格局,让代码既可以捕捉异常又容易阅读,这是很重要的。如果你还希望重用大量代码,那么拥有像 getSiteHTMLgetTitle 这样的通用函数(具有周密的异常处理功能)会让快速、稳定地抓取网页变得简单易行。

目录

  • 版权声明
  • O'Reilly Media, Inc. 介绍
  • 前言
  • 第一部分 创建爬虫
  • 第 1 章 初见网络爬虫
  • 第 2 章 复杂 HTML 解析
  • 第 3 章 编写网络爬虫
  • 第 4 章 网络爬虫模型
  • 第 5 章 Scrapy
  • 第 6 章 存储数据
  • 第二部分 高级网页抓取
  • 第 7 章 读取文档
  • 第 8 章 数据清洗
  • 第 9 章 自然语言处理
  • 第 10 章 穿越网页表单与登录窗口进行抓取
  • 第 11 章 抓取 JavaScript
  • 第 12 章 利用 API 抓取数据
  • 第 13 章 图像识别与文字处理
  • 第 14 章 避开抓取陷阱
  • 第 15 章 用爬虫测试网站
  • 第 16 章 并行网页抓取
  • 第 17 章 远程抓取
  • 第 18 章 网页抓取的法律与道德约束
  • 关于作者
  • 关于封面