《Learning Scrapy》(中文版)第2章 理解HTML和XPath


序言
第1章 Scrapy介绍
第2章 理解HTML和XPath
第3章 爬虫基础
第4章 从Scrapy到移动应用
第5章 快速构建爬虫
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy编程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和实时分析


为了从网页提取信息,了解网页的结构是非常必要的。我们会快速学习HTML、HTML的树结构和用来筛选网页信息的XPath。

HTML、DOM树结构和XPath

从这本书的角度,键入网址到看见网页的整个过程可以分成四步:

  • 在浏览器中输入网址URL。URL的第一部分,也即域名(例如gumtree.com),用来搜寻网络上的服务器。URL和其他像cookies等数据形成了一个发送到服务器的请求request。
  • 服务器向浏览器发送HTML。服务器也可能发送XML或JSON等其他格式,目前我们只关注HTML。
  • HTML在浏览器内部转化成树结构:文档对象模型(DOM)。
  • 根据布局规范,树结构转化成屏幕上的真实页面。

研究下这四个步骤和树结构,可以帮助定位要抓取的文本和编写爬虫。

URL
URL包括两部分:第一部分通过DNS定位服务器,例如当你在浏览器输入https://mail.google.com/mail/u/0/#inbox这个地址时,产生了一个mail.google.com的DNS请求,后者为你解析了一台服务器的IP地址,例如173.194.71.83。也就是说,https://mail.google.com/mail/u/0/#inbox转换成了https://173.194.71.83/mail/u/0/#inbox。

URL其余的部分告诉服务器这个请求具体是关于什么的,可能是一张图片、一份文档或是触发一个动作,例如在服务器上发送一封邮件。

HTML文档

服务器读取URL,了解用户请求,然后回复一个HTML文档。HTML本质是一个文本文件,可以用TextMate、Notepad、vi或Emacs等软件打开。与大多数文本文件不同,HTML严格遵循万维网联盟(World Wide Web Consortium)的规定格式。这个格式超出了本书的范畴,这里只看一个简单的HTML页面。如果你打开http://example.com,点击查看源代码,就可以看到HTML代码,如下所示:

<!doctype html>
<html>
  <head>
      <title>Example Domain</title>
      <meta charset="utf-8" />
      <meta http-equiv="Content-type"
              content="text/html; charset=utf-8" />
      <meta name="viewport" content="width=device-width,
              initial-scale=1" />
      <style type="text/css"> body { background-color: ... 
              } </style>
  <body>
      <div>
              <h1>Example Domain</h1>
              <p>This domain is established to be used for
                 illustrative examples examples in documents.
                 You may use this domain in examples without
                 prior coordination or asking for permission.</p>
              <p><a >
                 More information...</a></p>
      </div>
  </body>
</html>

为了便于阅读,我美化了这个HTML文档。你也可以把整篇文档放在一行里。对于HTML,大多数情况下,空格和换行符不会造成什么影响。

尖括号里的字符称作标签,例如<html>或<head>。<html>是起始标签,</html>是结束标签。标签总是成对出现。某些网页没有结束标签,例如只用<p>标签分隔段落,浏览器对这种行为是容许的,会智能判断哪里该有结束标签</p>。
<p>与</p>之间的内容称作HTML的元素。元素之间可以嵌套元素,比如例子中的<div>标签,和第二个<p>标签,后者包含了一个<a>标签。

有些标签稍显复杂,例如<a >,带有URL的href部分称作属性。
最后,许多标签元素包含有文本,例如<h1>标签中的Example Domain。对我们而言,<body>标签之间的可见内容更为重要。头部标签<head>中指明了编码字符,由Scrapy对其处理,就不用我们浪费精力了。

树结构

不同的浏览器有不同的借以呈现网页的内部数据结构。但DOM树是跨平台且不依赖语言的,可以被几乎所有浏览器支持。

只需右键点击,选择查看元素,就可以在浏览器中查看网页的树结构。如果这项功能被禁止了,可以在选项的开发者工具中修改。

你看到的树结构和HTML很像,但不完全相同。无论原始HTML文件使用了多少空格和换行符,树结构看起来都会是一样的。你可以点击任意元素,或是改变属性,这样可以实时看到对HTML网页产生了什么变化。例如,如果你双击了一段文字,并修改了它,然后点击回车,屏幕上这段文字就会根据新的设置发生改变。在右边的方框中,在属性标签下面,你可以看到这个树结构的属性列表。在页面底部,你可以看到一个面包屑路径,指示着选中元素的所在位置。


重要的是记住,HTML是文本,而树结构是浏览器内存中的一个对象,你可以通过程序查看、操作这个对象。在Chrome浏览器中,就是通过开发者工具查看。

浏览器中的页面

HTML文本和树结构和我们平时在浏览器中看到的页面截然不同。这恰恰是HTML的成功之处。HTML文件就是要具有可读性,可以区分网页的内容,但不是按照呈现在屏幕上的方式。这意味着,呈现HTML文档、进行美化都是浏览器的职责,无论是对于功能齐备的Chrome、移动端浏览器、还是Lynx这样的文本浏览器。

也就是说,网页的发展对网页开发者和用户都提出了极大的开发网页方面的需求。CSS就是这样被发明出来,用以服务HTML元素。对于Scrapy,我们不涉及CSS。

既然如此,树结构对呈现出来的网页有什么作用呢?答案就是盒模型。正如DOM树可以包含其它元素或是文字,同样的,盒模型里面也可以内嵌其它内容。所以,我们在屏幕上看到的网页是原始HTML的二维呈现。树结构是其中的一维,但它是隐藏的。例如,在下图中,我们看到三个DOM元素,一个<div>和两个内嵌的<h1>和<p>,出现在浏览器和DOM中:

用XPath选择HTML元素
如果你以前接触过传统的软件工程,并不知道XPath,你可能会担心,在HTML文档中查询某个信息,要进行复杂的字符串匹配、搜索标签、处理特殊字符、解析整个树结构等繁琐工作。对于XPath,所有的这些都不是问题,你可以轻松提取元素、属性或是文字。

在Chrome中使用XPath,在开发者工具中点击控制台标签,使用$x功能。例如,在网页http://example.com/的控制台,输入$x('//h1'),就可以移动到<h1>元素,如截图所示:


你在控制台中看到的是一个包含所选元素的JavaScript数组。如果你将光标移动到这个数组上,你可以看到被选择的元素被高亮显示。这个功能很有用。

XPath表达式
HTML文档的层级结构的最高级是<html>标签,你可以使用元素名和斜杠线选择任意元素。例如,下面的表达式返回了http://example.com/上对应的内容:

$x('/html')
  [ <html>...</html> ]
$x('/html/body')
  [ <body>...</body> ]
$x('/html/body/div')
  [ <div>...</div> ]
$x('/html/body/div/h1')
  [ <h1>Example Domain</h1> ]
$x('/html/body/div/p')
  [ <p>...</p>, <p>...</p> ]
$x('/html/body/div/p[1]')
  [ <p>...</p> ]
$x('/html/body/div/p[2]')
  [ <p>...</p> ]

注意,<p>标签在<div>标签内有两个,所以会返回两个。你可以用p[1]和p[2]分别返回两个元素。

从抓取的角度,文档的标题或许是唯一让人感兴趣的,它位于文档的头部,可以用下面的额表达式找到:

$x('//html/head/title')
  [ <title>Example Domain</title> ]

对于大文档,你可能要写很长的XPath表达式,以获取所要的内容。为了避免这点,两个斜杠线//可以让你访问到所有的同名元素。例如,//p可以选择所有的p元素,//a可以选择所有的链接。

$x('//p')
  [ <p>...</p>, <p>...</p> ]
$x('//a')
  [ <a >More information...</a> ]

//a可以用在更多的地方。例如,如果要找到所有<div>标签的链接,你可以使用//div//a。如果a前面只有一个斜杠,//div/a会返回空,因为在上面的例子中<div>标签下面没有<a>。

$x('//div//a')
  [ <a >More information...</a> ]
$x('//div/a')
  [ ]

你也可以选择属性。http://example.com/上唯一的属性是链接href,可以通过下面的方式找到:

$x('//a/@href')
[]

你也可以只通过text( )函数选择文字:

$x('//a/text()')
["More information..."]

可以使用*标志选择某层下所有的元素,例如:

$x('//div/*')
[<h1>Example Domain</h1>, <p>...</p>, <p>...</p>]

寻找特定属性,例如@class、或属性有特定值时,你会发现XPath非常好用。例如,//a[@href]可以找到所有链接,//a[@href="http://www.iana.org/domains/example"]则进行了指定的选择。
当属性值中包含特定字符串时,XPath会极为方便。例如,

$x('//a[@href]')
[<a >More information...</a>]
$x('//a[@)
[<a >More information...</a>]
$x('//a[contains(@href, "iana")]')
[<a >More information...</a>]
$x('//a[starts-with(@href, "http://www.")]')
[<a >More information...</a>]
$x('//a[not(contains(@href, "abc"))]')
[ <a >More information...</a>]

http://www.w3schools.com/xsl/xsl_functions.asp在线文档中你可以找到更多类似的函数,但并非都常用。

在Scrapy终端中可以使用同样的命令,在命令行中输入

scrapy shell "http://example.com"

终端会向你展示许多写爬虫时碰到的变量。其中最重要的是响应,在HTML中是HtmlResponse,这个类可以让你在Chrome使用xpath( )方法$x。下面是一些例子:

response.xpath('/html').extract()
  [u'<html><head><title>...</body></html>']
response.xpath('/html/body/div/h1').extract()
  [u'<h1>Example Domain</h1>']
response.xpath('/html/body/div/p').extract()
  [u'<p>This domain ... permission.</p>', u'<p><a >More information...</a></p>']
response.xpath('//html/head/title').extract()
  [u'<title>Example Domain</title>']
response.xpath('//a').extract()
  [u'<a >More information...</a>']
response.xpath('//a/@href').extract()
  [u'http://www.iana.org/domains/example']
response.xpath('//a/text()').extract()
  [u'More information...']
response.xpath('//a[starts-with(@href, "http://www.")]').extract()
  [u'<a >More information...</a>']

这意味着,你可用Chrome浏览器生成XPath表达式,以便在Scrapy爬虫中使用。

使用Chrome浏览器获得XPath表达式

Chrome浏览器可以帮助我们获取XPath表达式这点确实对开发者非常友好。像之前演示的那样检查一个元素:右键选择一个元素,选择检查元素??⒄吖ぞ弑淮蚩?,该元素在HTML的树结构中被高亮显示,可以在右键打开的菜单中选择Copy XPath,表达式就复制到粘贴板中了。


你可以在控制台中检测表达式:

$x('/html/body/div/p[2]/a')
[<a >More information...</a>]

常见工作

下面展示一些XPath表达式的常见使用。先来看看在维基百科上是怎么使用的。维基百科的页面非常稳定,不会在短时间内改变排版。

  • 取得id为firstHeading的div下的span的text:
//h1[@id="firstHeading"]/span/text()
  • 取得id为toc的div下的ul内的URL:
//div[@id="toc"]/ul//a/@href
  • 在任意class包含ltr和class包含skin-vector的元素之内,取得h1的text,这两个字符串可能在同一class内,或不在。
//*[contains(@class,"ltr") and contains(@class,"skin-vector")]//h1//text()

实际应用中,你会在XPath中频繁地使用class。在这几个例子中,你需要记住,因为CSS的板式原因,你会看到HTML的元素总会包含许多特定的class属性。这意味着,有的<div>的class是link,其他导航栏的<div>的class就是link active。后者是当前生效的链接,因此是可见或是用CSS特殊色高亮显示的。当抓取的时候,你通常是对含有某个属性的元素感兴趣的,就像之前的link和link active。XPath的contains( )函数就可以帮你选择包含某一class的所有元素。

  • 选择class属性是infobox的table的第一张图片的URL:
//table[@class="infobox"]//img[1]/@src
  • 选择class属性是reflist开头的div下面的所有URL链接:
//div[starts-with(@class,"reflist")]//a/@href
  • 选择div下面的所有URL链接,并且这个div的下一个相邻元素的子元素包含文字References:
//*[text()="References"]/../following-sibling::div//a
  • 取得所有图片的URL:
//img/@src

提前应对网页发生改变

爬取的目标常常位于远程服务器。这意味着,如果它的HTML发生了改变,XPath表达式就无效了,我们就不得不回过头修改爬虫的程序。因为网页的改变一般就很少,爬虫的改动往往不会很大。然而,我们还是宁肯不要回头修改。一些基本原则可以帮助我们降低表达式失效的概率:

  • 避免使用数组序号
    Chrome常?;嵩诒泶锸街屑尤胄矶喑J?/li>
//*[@id="myid"]/div/div/div[1]/div[2]/div/div[1]/div[1]/a/img

如果HTML上有一个广告窗的话,就会改变文档的结构,这个表达式就会失效。解决的方法是,尽量找到离img标签近的元素,根据该元素的id或class属性,进行抓取,例如:

//div[@class="thumbnail"]/a/img
  • 用class抓取效果不一定好
    使用class属性可以方便的定位要抓取的元素,但是因为CSS也要通过class修改页面的外观,所以class属性可能会发生改变,例如下面用到的class:
//div[@class="thumbnail"]/a/img

过一段时间之后,可能会变成:

//div[@class="preview green"]/a/img
  • 数据指向的class优于排版指向的class
    在上一个例子中,使用thumbnail和green两个class都不好。thumbnail比green好,但这两个都不如departure-time。前面两个是用来排版的,departure-time是有语义的,和div中的内容有关。所以,在排版发生改变的情况下,departure-time发生改变的可能性会比较小。应该说,网站作者在开发中十分清楚,为内容设置有意义的、一致的标记,可以让开发过程收益。

  • id通常是最可靠的
    只要id具有语义并且数据相关,id通常是抓取时最好的选择。部分原因是,JavaScript和外链锚点总是使用id获取文档中特定的部分。例如,下面的XPath非常可靠:

//*[@id="more_info"]//text( )

相反的例子是,指向唯一参考的id,对抓取没什么帮助,因为抓取总是希望能够获取具有某个特点的所有信息。例如:

//[@id="order-F4982322"]

这是一个非常差的XPath表达式?;挂亲。」躨d最好要有某种特点,但在许多HTML文档中,id都很杂乱无章。

总结
编程语言的不断进化,使得创建可靠的XPath表达式从HTML抓取信息变得越来越容易。在本章中,你学到了HTML和XPath的基本知识、如何利用Chrome自动获取XPath表达式。你还学会了如何手工写XPath表达式,并区分可靠和不够可靠的XPath表达式。第3章中,我们会用这些知识来写几个爬虫。


序言
第1章 Scrapy介绍
第2章 理解HTML和XPath
第3章 爬虫基础
第4章 从Scrapy到移动应用
第5章 快速构建爬虫
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy编程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和实时分析


最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容