「Noah Veltman的午餐会」网页抓取

分享给朋友:

在BBC工作期间,Noah Veltman 为编辑人员和设计师们组织了一系列午餐小会。这些小型讲座的内容不是关于如何写代码或者完成某个具体项目,而是让这些非工程人员了解在新闻实践中经常会遇到的技术话题,让技术问题不再那么高深莫测。当非工程人员对基础的技术有所了解后,“这个项目要花多长时间?”、“我们如何做出这样的东西?”、“我们要如何妥协才能在技术上实现它?”如此这般的问题就会被有效率的双向对话所替代。

出于这样的目标,这个系列中将不太涉及真正的写代码的教学。但对那些想好好钻研开发的人来说,这个系列将提供非常值得尝试的努力方向。这些都是真实发生在午餐会上的讨论,所以也许变成文字后少了一些临场感,但无论是对于程序员、记者还是设计师来说,这些都仍然是很有帮助的、值得学习的材料。

Noah Veltman现任纽约WNYC电台数据新闻团队的开发人员,擅长交互图表、地图和基于数据的新闻应用开发。此前他曾获得Knight-Mozilla开放新闻奖学金并在伦敦BBC NEWS工作。数据新闻网将陆续推出Noah Veltman午餐会的其他资源。

【Noah Veltman的午餐会】系列由数据新闻网独家授权编译,更多该系列文章:
【Noah Veltman的午餐会】Excel vs. Database
【Noah Veltman的午餐会】制作在线地图

网页抓取指的是编写一段能够自动从网页提取信息的代码。打个比方,数据抓取的过程就好像智能扫地机器人把网页从头到尾吸一遍那样。

如果想获取包含某一关键词的所有联邦法律条文,你可以直接从美国国会图书馆的数据库里查找。假设搜到了三条结果,你可能会逐一复制粘贴,保存到表格里;但如果搜到了三千条结果,你八成就不想这么折腾了。若你能明确法律条文搜索的基准,接下来就可以交给抓取工具(爬虫)了。你甚至可以让爬虫翻页,这样即使有几十页搜索结果也不怕了。

什么时候用抓取工具?

能抓取的信息基本上通过手动搜索和复制粘贴也能得到。从这个角度上说,信息抓取工具的意义无非就是省时省力,也不用一直盯着。假如你需要获取很多数据、或者每隔一段时间就需要拿到特定数据,那么用API或抓取工具是最好的选择。大多数情况下,能用API的话最好不过。若API不兼容,那么再用抓取工具。打个比方,假如你要开门,那API就是一把钥匙,而抓取工具就是退而求之的撬棍。

抓取的难点在哪里?

理解爬虫如何工作,先要了解什么样的数据难以抓取。

不连贯的数据

写爬虫的关键在于数据的连贯性。写出来的代码就是用来指示爬虫不停地寻找特定信息。第一步是去发现,仔细观察你想要抓取的页面,看页面来源,想办法弄清楚你想要的信息出现的规律。如果爬虫得出结果却一无所用,那么你给出的指令可能过于宽泛了。当爬虫找不到你想要的数据时,指令可能过于狭窄。写出合适的指令是最重要的一步。

假设你想从纽约时报的网页上抓取所有标题,不包括包括视频和社论的标题。这些标题并不是简单列出,很难抓取。读者的阅读体验很好,但对计算机来说,寻找规律却变得非常困难,下图为纽约时报网页:

1

如果我们打开纽约时报的移动端,发现标题排列非常连贯,寻找排版规律并抓取就会变得相对容易,下图为从移动终端上打开的纽约时报页面:

2

总的来说,给抓取工具下指令可以用两种工具组:

  • 文档对象模型(DOM)

网页是由HTML标签嵌套而成的,就像家族系谱树那样,我们把这种树型结构叫做文档对象模型。

比如说,表格内有行,每一行又包含单元格,单元格里包含文字,一个表格(HTML标签嵌套而成),看起来可能是这个样子:

<table> The whole table

  <tr>

    <td>COUNTRY NAME</td>

    <td>POPULATION</td>

  </tr>

  <tr>

    <td>United States</td>

    <td>313 million</td>

  </tr>

  <tr>

    <td>Canada</td>

    <td>34 million</td>

  </tr>

</table>

爬虫穿梭于这些代码中。假如你想要抓取所有国家的名字,那么给爬虫下达的指令就可以是: “find the table, and get the data from the first cell in each row.”(找到表格,把每行第一个单元格里的数据提取出来),就是这么简单。

但是还有标题行怎么办?只是用这样的指令会把“country name(国家名字这一栏的标题)”也保存为一个国家名。这不是我们想要的结果。又或者说,假如页面上有好几个表格,那时该怎么办呢?就算你只在网页上看到一个表格,那也并不代表页面上只有这一个,页面顶端的菜单选项可能就是一个表格,这时,就应该细化指令,比如说:“find the table that has ‘COUNTRY NAME’ in the first cell, and get the data from the first cell in each row besides the first.”(找到第一个单元格中包含国家名的表格,并把除了标题行外的每行第一个单元格中的数据抽取出来)

给爬虫下达的指令可以用一些特定的标签,比如:

  • 找到所有<td>标签
  • 找到第二个<img>标签
  • 从第三个<table>中找到所有<a>标签

当然,你也可以用标签属性,比如说IDs和classes,来进行更有效的搜索,(比如,在具有external特性的class中找到所有<a>标签,针对的源代码可能就是<a href=”http://google.com/” class=”external”>)

  • 正则表达式 Regular Expression

正则表达式是更专业级别的抓取工具,DOM的数据排列比较凌乱的情况下可以用它。正则表达式能够检索,并提取出符合某一特定句法规则的字符串。

假如手头有一篇新闻,你想抓取里面提到的所有人名。 如果只有一篇文章,那么手动搜索就够了,但是如果你掌握了一篇文章的抓取规律,你就可以对成千上百篇文章进行相似操作。

下面这一正则表达式就能够被用来检索:

/[A-Z][a-z]+\s[A-Z][a-z]+/ (If you are noticing the way this expression isn’t optimized, stop reading, this primer isn’t for you 如果你已经想吐槽这个表达式多不精练了,大神,这篇文章的目标受众不是你啊)

这一句法模式可以被用来检索人名,也就是由大写字母开头后面跟着小写字母,空格,以及另一个大写字母开头后面跟着小写字母这样语法结构的内容。将这一段代码用于任何网页都能找到并提取出所有人名,听起来很赞吧?且慢。

下面几个是用这一段代码你最可能会碰到的错误结果,一些并不是人名,但却符合这种句法结构的短语有:

  • Corpus Christi 地名,美国德克萨斯州圣体市,位于墨西哥湾沿岸
  • Lockheed Martin公司名,洛克希德·马丁,美国航空航天器材制造商
  • Christmas Eve 圣诞夜
  • South Dakota 南达科他州

另外,类似于“The Giants win the pennant!”(巨人队赢得了冠军!)这样的句式,由于第一个句首定冠词the大写,又恰好碰到棒球队名大写,正好凑成两个单词大写开头,也会被误当成是符合条件的检索结果被保存下来。

下面几个是人名,但却可能因为不符合给出的检索条件而被忽略:

  • Sammy Davis, Jr.  (Jr.为junior缩写,译为二世)
  • Henry Wadsworth Longfellow(有middle name,因而有三个符合条件的单词组成人名)
  • Jean-Claude Van Damme(同上)
  • Ian McKellen(英国著名演员,演绎角色包括指环王中的甘道夫爷爷,他的姓氏拼法比较独特,这种拼法于苏格兰后裔中比较常见,起源于凯尔特语)
  • Bono(艺名,u2乐队主唱)
  • The Artist Formerly Known as Prince(绰号)

这里主要想提醒大家,爬虫要做到精确其实并不容易。爬虫根据指令行事,结果取决于写指令的人。大部分抓取工作都包括事后校验,以确保结果正确。

抓取的信息越有规律可循,写指令就越容易。抓取一个列表远比抓取一个复杂的视觉页面来得容易。抓取类似邮编的信息(美帝邮编是五位数字)应该不难,但如果要找类似地址这样的内容就难了,它们有规律可循但又可以有n种写法。

规则之外的例外是抓取过程遇到的最大的困难,特别是歧义比较小的时候,如果一个词的另一个意思和你想要表达的相差巨大,那么你很容易就会注意到这个差异,若是相差比较小就很难去意识到可能有歧义。创造抓取工具看的是一个样本的数据,或者基于这一样本的规则,但是没人能肯定样本之外的数据符合这一规则。有可能你通过看前三页的规律写出的抓取工具会因为第58页的一个偏差而得到差异巨大的结果,这也就是为什么抽查和不停检测在数据抓取过程中非常重要的原因。

AJAX(Asynchronous JavaScript and XML”,异步的JavaScript与XML技术)和动态数据

基本的抓取工具打开网页的过程和你手动打开一个是一样的,只不过没有一个互动的过程罢了,但是如果页面载入数据用的是AJAX,或者用了javascript会基于互动更改网页,那么抓取的难度就增加了。你更希望抓取的信息在最开始就载入了。

需要登录才能取得的数据

抓取公共数据远比从需要登录的网页获得数据要简单。你需要给爬虫创造一个cookie地址,即让爬虫有个“身份证”,有些系统能够检测出这个把戏。(当然也是好事,说明信息安全有了保障,不然可能会被黑客利用。)

需要爬虫自行探索的数据

如果你知道网址,那直接告诉爬虫网址是什么就可以了。但更多情况下你需要抓取很多网页,且并不知道网址,这时抓取工具就需要身兼二职,先通过链接找到源页面,再把数据和结果扒拉下来。简单的版本是,当有几百个可能含有你想要信息的页面放在你的面前,这时你要做的就是“教”会你的抓取工具去“点击”“下一页”并在每一页上重复抓取动作。难点在于,有时候你需要先从网站的犄角旮旯中找到想要的数据。除了确保抓取的数据是正确的,首先得确认第一步就走对了。

下面我们来看看两个例子:

  • 例一:美国国会图书馆数据库

假设你现在要找美国第112届国会通过的所有法案的编号,名称,以及提出法案的议员或者倡议人,你可以登录美国国会图书馆的页面。它其实已经给所有法律进行了初步分类,其中就包括根据每一届国会进行的分类,这已经给你省下了不少时间了。点击第112届国会,会有两个链接指向两个页面,我们要做的就是抓取两个网址结合结果来看:

3

每一页上都呈现了法律条文名及支持者,看上去很规整:

4

为了最后抓取到精确的信息,我们还需要用上正确的源代码,这一步可能会比较混乱,代码是这样的:

<p>
<hr/><font size=”3″><strong>
BACK | <a href=”/cgi-bin/bdquery/L?d112:./list/bd/d112pl.lst:151[1-283](Public_Laws)[[o]]|TOM:/bss/d112query.html|”>FORWARD</a> | <a href=”/bss/d112query.html”>NEW SEARCH</a>  | <a href=”/home/thomas.html”>HOME</a>
</strong></font><hr/> Items <b>1</b> through <b>150</b> of <b>283</b> <center><h3>Public Laws</h3></center><p><b>  1.</b> <a href=”/cgi-bin/bdquery/z?d112:HR00366:|TOM:/bss/d112query.html|”>H.R.366 </a>:  To provide for an additional temporary extension of programs under the Small Business Act and the Small Business Investment Act of 1958, and for other purposes.<br /><b>Sponsor:</b> —此处省略n行杂乱的源代码—<hr/>

这是什么东西?淡定!内容很多,我们可以先比较一下我们从页面上看到的内容和这里出现的代码,寻找句法规则。从中选出一条特定的法律,来看一下:

<p><b>  1.</b> <a href=”/cgi-bin/bdquery/z?d112:HR00366:|TOM:/bss/d112query.html|”>H.R.366 </a>:  To provide for an additional temporary extension of programs under the Small Business Act and the Small Business Investment Act of 1958, and for other purposes.<br /><b>Sponsor:</b> <a href=”/cgi-bin/bdquery/?&amp;Db=d112&amp;[email protected]([email protected]((@1(Rep+Graves++Sam))+01656))”>Rep Graves, Sam</a> [MO-6]     (introduced 1/20/2011) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>Cosponsors</b> (None)
<br /><b>Committees: </b>House Small Business
<br /><b>Latest Major Action:</b>  Became Public Law No: 112-1 [<b>GPO:</b> <a href=”/cgi-bin/toGPObsspubliclaws/http://www.gpo.gov/fdsys/pkg/PLAW-112publ1/html/PLAW-112publ1.htm”>Text</a>, <a href=”/cgi-bin/toGPObsspubliclaws/http://www.gpo.gov/fdsys/pkg/PLAW-112publ1/pdf/PLAW-112publ1.pdf”>PDF</a>]<hr/>

法案的编号隐藏在这个链接中:

<a href=”…”>H.R.366 </a>

但不是所有的链接都包含法案编号,这个链接在首尾两处包含两个<b>的标签,那我们来试试是不是可以用上它,虽然仍旧不太精确:

<b>GPO:</b> <a href=”…”>Text</a>

下一步就是细化了,我们可以明确我们想要的链接是由数字开头,后面一个句号,并且首尾缀有两个<b>标签:

<b>  1.</b><a href=”…”>H.R.366 </a>

当然方法不止一种,任何句法只要奏效都可以。

我们还能抓取提出法案议员的名字,只要抓取任何出现在<b>Sponsor:</b> 后面的链接就可以了。

<b>Sponsor:</b> <a href=”/cgi-bin/bdquery/?&amp;Db=d112&amp;[email protected]([email protected]((@1(Rep+Graves++Sam))+01656))”>Rep Graves, Sam</a>  leads us to  <a href=”/cgi-bin/bdquery/?&amp;Db=d112&amp;[email protected]([email protected]((@1(Rep+Graves++Sam))+01656))”>Rep Graves, Sam</a>  which leads us to  Rep Graves, Sam

当然我们还得考虑到例外情况,很多法案都是多个议员联名提出的,又或者有些法案没有特定的提出议员,我们还得确保我们对名字的解析是正确的。并不是每一个议员的名字开头都是Rep.,像参议员,他们的名字前缀就都是Sen., 那么是不是还有一些别的比较诡异的前缀呢? 比如说来自关岛的代表议员,虽然可能并不具投票权,但是仍旧可以提出法案,那么他的名字前很可能就有Del.的前缀,或者是一些类似的文字。这种时候我们能做的就是抽查,并且我们还要确认名字的写法是姓氏在前,姓名由逗号分开(这种情况下我们就可以直接根据逗号出现的位置区分姓和名),还是一般的写法,名在前,姓氏在后。若我们把得到的第一页结果下拉,我们就能发现不少例外:

Sen Rockefeller, John D., IV

现在我们知道如果我们把姓和名根据第一个逗号区分开来,我们得到的名字会是John D., IV Rockefeller,若是根据后一个都还区分,那么名字又会变成IV Rockefeller, John D。(正确的姓氏是Rockefeller,名是John, 中间名的首字母缩写是D,也就是约翰.D.洛克菲勒五世)这种情况下我们需要更复杂的区分方法,通过所有逗号区分姓和名,若是区分出来的结果多于两个,应该加到名字的最后,变成正确的:John D. Rockefeller IV(约翰.D.洛克菲勒五世)

  • 例二:体育,运动队,场馆和代表颜色

如果你想要一个美国橄榄球联盟所有队伍和场馆,包括场馆地址和队伍代表色的列表,可以上Wikipedia,能找到的表格就是这个:

5

这个表格看起来挺简单直接的,第二列是场馆名称,第五列是地址,第一列是队伍,也就是说我们想找的四个信息,其中三个已经包含在这个表格里了。

但事情没这么简单。Wikipedia有时候会在文本中插入注释,我们抓取的信息中可不能包含这些注释内容。如果你下拉这个表格,看到MetLife Stadium(大都会人寿球场,纽约巨人队和喷气机队主场),例外就出现了。这个场地是两支队伍的主场,那么我们就得保存这一行两次。

若我们需要的是比城市更精确的坐标,让抓取工具在每一行中对stadium(体育场)下的链接进行操作即可。每一个体育场的主页上,我们都能找到下面这样的内容:

6

我们可以保存经纬度,同样的,我们也能用相似的方法从team(s) (队伍)这一列找到指向队伍代表颜色的链接,得到的结果如下:

7

8

仔细观察这个页面,我们可以找到类似这样的结果:

这样的背景颜色就是我们想要的,并且每一个颜色还提供了样色,那再好不过了。我们可以下达一些类似的指令,比如:“Find the table cell that says ‘Team colors’ inside it, then go to the next cell and get the background color of every <span> inside a pair of<p> tags.”(找到含有team color字样的单元格,并从旁边的单元格中抓取每一个由<p>标签缀在首尾的<span>背景颜色。)

 “#0C2340″,”#FFB81C”,”#0072CE”这是三个特殊的颜色编码,其中还有用英语表达的”white”白色。我们要做的就是把这些记下来,可能的话把白色也转换成一个特定的色彩编码,这样的话就一一对应了。但是我们还是一样要小心例外情况的发生,并不是所有页面都会包含这些内容,可能会有不同的展示方式,如果你打开旧金山49人队的页面,你就会看到颜色的排版方式是这样的:

9

11

看来,我们之前所归纳出的规则过细了,这些<span>标签并不在两个<p>里面,因此,我们还得把规则放宽一些,抓取单元格中任意<span>标签的背景色。

最后,介绍一个适合初学者的小工具:ScraperWiki

爬虫本质上是一段代码。初学者可以看看这个网站:ScraperWiki,它能帮助你写一些简单爬虫,免去自己设定服务器的麻烦。这个工具最好用的地方就在于它能解析别人写的爬虫,你可以通过看别人怎么写代码,哪些代码对应的作用是哪些,自己慢慢学着写,当然你也能复制别人的代码,并在此基础上根据自己的需求做出改动,这个Twitter爬虫就是最好的例子:https://scraperwiki.com/scrapers/ddj_twitter_scraper_9/

改动下查询命令,你就能定制自己的twitter爬虫,保存符合搜索条件的微博,并把它们整合到一个表格中。

还有一个抓取报纸新闻头条的爬虫:https://scraperwiki.com/scrapers/bbc_headlines_8/

如果在后面再加一个关键词作为筛选条件,你就能有自己的新闻头条爬虫了。

本文译者:沈晨

作者简介

数据新闻网

数据新闻网以引介全球范围内最顶尖的数据新闻实践为初衷,以推动数据开放及媒体革新为宗旨,面向中国的新闻从业者、媒体管理者、新传教育者以及对传媒感兴趣的设计师、程序员,提供线上信息平台与线下交流机会。