《Learning Scrapy》(中文版)第2章 理解HTML和XPath使用Chrome浏览器获得XPath表达式常见工作提前应对网页发生改变

为了从网页提取信息,了解网页的结构是非常必要的。我们会快速学习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 href="http://www.iana.org/domains/example">
                 More information...</a></p>
      </div>
  </body>
</html>

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

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

有些标签稍显复杂,例如<a href="http://www.iana.org/domains/example">,带有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 href="http://www.iana.org/domains/example">More information...</a> ]

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

$x('//div//a')
  [ <a href="http://www.iana.org/domains/example">More information...</a> ]
$x('//div/a')
  [ ]

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

$x('//a/@href')
[href="http://www.iana.org/domains/example"]

你也可以只通过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 href="http://www.iana.org/domains/example">More information...</a>]
$x('//a[@href="http://www.iana.org/domains/example"]')
[<a href="http://www.iana.org/domains/example">More information...</a>]
$x('//a[contains(@href, "iana")]')
[<a href="http://www.iana.org/domains/example">More information...</a>]
$x('//a[starts-with(@href, "http://www.")]')
[<a href="http://www.iana.org/domains/example">More information...</a>]
$x('//a[not(contains(@href, "abc"))]')
[ <a href="http://www.iana.org/domains/example">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 href="http://www.iana.org/domains/example">More information...</a></p>']
response.xpath('//html/head/title').extract()
  [u'<title>Example Domain</title>']
response.xpath('//a').extract()
  [u'<a href="http://www.iana.org/domains/example">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 href="http://www.iana.org/domains/example">More information...</a>']

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

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

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

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

$x('/html/body/div/p[2]/a')
[<a href="http://www.iana.org/domains/example">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常常会在表达式中加入许多常数
//*[@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表达式。还要记住,尽管id最好要有某种特点,但在许多HTML文档中,id都很杂乱无章。

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏企鹅号快讯

坚定地使用 CSS Custom Properties

好久没译文,最近看到这篇 Getting Hardboiled with CSS Custom Properties。觉得不错,翻译出来给大家。现在 CSS 新...

20070
来自专栏互联网杂技

jQuery 升级踩坑大全

背景 jQuery想必各个web工程师都再熟悉不过了,不过现如今很多网站还采用了很古老的jQuery版本。其实如果早期版本使用不当,可能会有DOMXSS漏洞,非...

61380
来自专栏Google Dart

AngularDart Material Design 屑 顶

“chip”小部件呈现“chip”格式的对象 - 带阴影的圆形框,通常用于水平列表。可以使用任何实现HasUIDisplayName接口的对象,或者根本不使用任...

14740
来自专栏更流畅、简洁的软件开发方式

【自然框架】n级下拉列表框的原理

  其实原理也很简单,分成两个部分,一个是服务器端,一个是客户端。   首先要设置记录集,这里用DataSet来装载,二级联动,里面就要有两个DataTable...

28870
来自专栏Python小屋

基于Python+tkinter+pygame的音乐播放器完整源码

import os import tkinter import tkinter.filedialog import random import time imp...

47540
来自专栏前端知识分享

Vue入门---常用指令详解

Vue是一个MVVM(Model / View / ViewModel)的前端框架,相对于Angular来说简单、易学上手快,近两年也也别流行,发展速度较快,已...

12510
来自专栏MasiMaro 的技术博文

windows编程学习笔记(三)ListBox的使用方法

ListBox是Windows中的一种控件,一般被当做子窗口使用,Windows中所有子窗口都是通过发送一个通知码到父窗口父窗口通过WM_COMMAND消息接收...

12220
来自专栏程序猿

用 Python 向你比个心

之前写了一篇用 Python 画一个小猪佩奇和哆啦 A 梦,然后最近看到有人用 turtle 画了一个心,觉得挺有意思的,于是把代码复制到本地,再加了个播放音乐...

32150
来自专栏老付的网络博客

Vuejs template快速入门

一直想写一个Vuejs的系列的博客,苦于一直没有时间,今天的这个博客也是思考了很久,打算随便写点东西,今天说说Vuejs的模版的使用。

1.5K20
来自专栏前端说吧

JS案例 - 基于vue的移动端长按手势

当时首先想到要做长按事件的时候,我想到的是vue内部的自定义指令,毕竟官网里边有这么一句描述:

22920

扫码关注云+社区

领取腾讯云代金券