数据解析
1. 正则表达式
1 | import re |
2. XPath
XPath,全称 XML Path Language,即 XML 路径语言,它是一门在XML文档中查找信息的语言。XPath 最初设计是用来搜寻XML文档的,但是它同样适用于 HTML 文档的搜索。
XPath概览
XPath 的选择功能十分强大,它提供了非常简洁明了的路径选择表达式,另外它还提供了超过 100 个内建函数用于字符串、数值、时间的匹配以及节点、序列的处理等等,几乎所有我们想要定位的节点都可以用XPath来选择。
XPath 于 1999 年 11 月 16 日 成为 W3C 标准,它被设计为供 XSLT、XPointer 以及其他 XML 解析软件使用,更多的文档可以访问其官方网站:https://www.w3.org/TR/xpath/。
XPath常用规则
nodename
选取此节点的所有子节点
/
从当前节点选取直接子节点
//
从当前节点选取子孙节点
.
选取当前节点
..
选取当前节点的父节点
@
选取属性
例如:
1 | //title[@lang='eng'] |
这就是一个 XPath 规则,它就代表选择所有名称为 title,同时属性 lang 的值为 eng 的节点。
在后文我们会介绍 XPath 的详细用法,通过 Python 的 LXML 库利用 XPath 进行 HTML 的解析。
实例引入
我们现用一个实例来感受一下使用 XPath 来对网页进行解析的过程,代码如下:
1 | from lxml import etree |
另外我们也可以直接读取文本文件进行解析,示例如下:
1 | from lxml import etree |
所有节点
我们一般会用 // 开头的 XPath 规则来选取所有符合要求的节点,以上文的 HTML 文本为例,如果我们要选取所有节点,可以这样实现:
1 | from lxml import etree |
我们在这里使用 * 代表匹配所有节点,也就是整个 HTML 文本中的所有节点都会被获取,可以看到返回形式是一个列表,每个元素是 Element 类型,其后跟了节点的名称,如 html、body、div、ul、li、a 等等,所有的节点都包含在列表中了。
当然此处匹配也可以指定节点名称,如果我们想获取所有 li 节点,示例如下:
1 | from lxml import etree |
在这里我们要选取所有li
节点可以使用 //
,然后直接加上节点的名称即可,调用时直接调用 xpath() 方法即可提取。
子节点
我们通过/
或 //
即可查找元素的子节点或子孙节点,加入我们现在想选择li
节点所有直接 a
子节点,可以这样来实现:
1 | from lxml import etree |
在这里我们通过追加一个 /a
即选择了所有li
节点的所有直接 a
子节点,因为//li
是选中所有li
节点, /a
是选中li
节点的所有直接子节点a
,二者组合在一起即获取了所有li
节点的所有直接 a 子节点。
父节点
我们知道通过连续的/
或//
可以查找子节点或子孙节点,那假如我们知道了子节点怎样来查找父节点呢?在这里我们可以用..
来获取父节点。
比如我们现在首先选中 href 是 link4.html 的 a 节点,然后再获取其父节点,然后再获取其 class 属性,代码如下:
1 | from lxml import etree |
同时我们也可以通过 parent:: 来获取父节点,代码如下:
1 | from lxml import etree |
属性匹配
在选取的时候我们还可以用 @ 符号进行属性过滤,比如在这里如果我们要选取 class 为 item-1 的 li 节点,可以这样实现:
1 | from lxml import etree |
在这里我们通过加入 [@class=”item-0”] 就限制了节点的 class 属性为 item-0,而 HTML 文本中符合条件的 li 节点有两个,所以返回结果应该返回两个匹配到的元素,结果如下:
1 | [<Element li at 0x10a399288>, <Element li at 0x10a3992c8>] |
文本获取
我们用 XPath 中的 text() 方法可以获取节点中的文本,我们接下来尝试获取一下上文 li 节点中的文本,代码如下:
1 | from lxml import etree |
运行结果如下:
1 | ['\n '] |
很奇怪的是我们并没有获取到任何文本,而是只获取到了一个换行符,这是为什么呢?因为 XPath 中 text() 前面是 /,而此 / 的含义是选取直接子节点,而此处很明显 li 的直接子节点都是 a 节点,文本都是在 a 节点内部的,所以这里匹配到的结果就是被修正的 li 节点内部的换行符,因为自动修正的li节点的尾标签换行了。
即选中的是这两个节点:
1 | <li class="item-0"><a href="link1.html">first item</a></li> |
其中一个节点因为自动修正,li 节点的尾标签添加的时候换行了,所以提取文本得到的唯一结果就是 li 节点的尾标签和 a 节点的尾标签之间的换行符。
因此,如果我们想获取 li 节点内部的文本就有两种方式,一种是选取到 a 节点再获取文本,另一种就是使用 //,我们来看下二者的区别是什么。
首先我们选取到 a 节点再获取文本,代码如下:
1 | from lxml import etree |
运行结果:
1 | ['first item', 'fifth item'] |
可以看到这里返回值是两个,内容都是属性为 item-0 的 li 节点的文本,这也印证了我们上文中属性匹配的结果是正确的。
在这里我们是逐层选取的,先选取了 li 节点,又利用 / 选取了其直接子节点 a,然后再选取其文本,得到的结果恰好是符合我们预期的两个结果。
我们再来看下用另一种方式 // 选取的结果,代码如下:
1 | from lxml import etree |
运行结果:
1 | ['first item', 'fifth item', '\n '] |
不出所料,这里返回结果是三个,可想而知这里是选取所有子孙节点的文本,其中前两个就是 li 的子节点 a 节点内部的文本,另外一个就是最后一个 li 节点内部的文本,即换行符。
所以说,如果我们要想获取子孙节点内部的所有文本,可以直接用 // 加 text() 的方式获取,这样可以保证获取到最全面的文本信息,但是可能会夹杂一些换行符等特殊字符。如果我们想获取某些特定子孙节点下的所有文本,可以先选取到特定的子孙节点,然后再调用 text() 方法获取其内部文本,这样可以保证获取的结果是整洁的。
属性获取
我们知道了用 text() 可以获取节点内部文本,那么节点属性该怎样获取呢?其实还是用 @ 符号就可以,例如我们想获取所有 li 节点下所有 a 节点的 href 属性,代码如下:
1 | from lxml import etree |
在这里我们通过 @href 即可获取节点的 href 属性,注意此处和属性匹配的方法不同,属性匹配是中括号加属性名和值来限定某个属性,如 [@href=”https://ask.hellobi.com/link1.html“],而此处的 @href 指的是获取节点的某个属性,二者需要做好区分。
运行结果:
1 | ['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html'] |
可以看到我们成功获取了所有 li 节点下的 a 节点的 href 属性,以列表形式返回。
属性多值匹配
有时候某些节点的某个属性可能有多个值,例如下面例子:
1 | from lxml import etree |
在这里 HTML 文本中的 li 节点的 class 属性有两个值 li 和 li-first,但是此时如果我们还想用之前的属性匹配获取就无法匹配了,代码运行结果:
1 | [] |
这时如果属性有多个值就需要用 contains() 函数了,代码可以改写如下:
1 | from lxml import etree |
这样我们通过 contains() 方法,第一个参数传入属性名称,第二个参数传入属性值,这样只要此属性包含所传入的属性值就可以完成匹配了。
运行结果:
1 | ['first item'] |
此种选择方式在某个节点的某个属性有多个值的时候经常会用到,如某个节点的 class 属性通常有多个。
多属性匹配
另外我们可能还遇到一种情况,我们可能需要根据多个属性才能确定一个节点,这是就需要同时匹配多个属性才可以,那么这里可以使用运算符 and 来连接,示例如下:
1 | from lxml import etree |
在这里 HTML 文本的 li 节点又增加了一个属性 name,这时候我们需要同时根据 class 和 name 属性来选择,就可以 and 运算符连接两个条件,两个条件都被中括号包围,运行结果如下:
1 | ['first item'] |
这里的 and 其实是 XPath 中的运算符,另外还有很多运算符,如 or、mod 等等。
按序选择
有时候我们在选择的时候可能某些属性同时匹配了多个节点,但是我们只想要其中的某个节点,如第二个节点,或者最后一个节点,这时该怎么办呢?
这时可以利用中括号传入索引的方法获取特定次序的节点,示例如下:
1 | from lxml import etree |
第一次选择我们选取了第一个 li 节点,中括号中传入数字1即可,注意这里和代码中不同,序号是以 1 开头的,不是 0 开头的。
第二次选择我们选取了最后一个 li 节点,中括号中传入 last() 即可,返回的便是最后一个 li 节点。
第三次选择我们选取了位置小于 3 的 li 节点,也就是位置序号为 1 和 2 的节点,得到的结果就是前 2 个 li 节点。
第四次选择我们选取了倒数第三个 li 节点,中括号中传入 last()-2即可,因为 last() 是最后一个,所以 last()-2 就是倒数第三个。
运行结果如下:
1 | ['first item'] |
在这里我们使用了 last()、position() 等函数,XPath 中提供了 100 多个函数,包括存取、数值、字符串、逻辑、节点、序列等处理功能,具体所有的函数作用可以参考:http://www.w3school.com.cn/xpath/xpath_functions.asp。
节点轴选择
XPath 提供了很多节点轴选择方法,英文叫做 XPath Axes,包括获取子元素、兄弟元素、父元素、祖先元素等等,在一定情况下使用它可以方便地完成节点的选择,我们用一个实例来感受一下:
1 | from lxml import etree |
运行结果:
1 | [<Element html at 0x107941808>, <Element body at 0x1079418c8>, <Element div at 0x107941908>, <Element ul at 0x107941948>] |
第一次选择我们调用了 ancestor 轴,可以获取所有祖先节点,其后需要跟两个冒号,然后是节点的选择器,这里我们直接使用了 *,表示匹配所有节点,因此返回结果是第一个 li 节点的所有祖先节点,包括 html,body,div,ul。
第二次选择我们又加了限定条件,这次在冒号后面加了 div,这样得到的结果就只有 div 这个祖先节点了。
第三次选择我们调用了 attribute 轴,可以获取所有属性值,其后跟的选择器还是 *,这代表获取节点的所有属性,返回值就是 li 节点的所有属性值。
第四次选择我们调用了 child 轴,可以获取所有直接子节点,在这里我们又加了限定条件选取 href 属性为 link1.html 的 a 节点。
第五次选择我们调用了 descendant 轴,可以获取所有子孙节点,这里我们又加了限定条件获取 span 节点,所以返回的就是只包含 span 节点而没有 a 节点。
第六次选择我们调用了 following 轴,可以获取当前节点之后的所有节点,这里我们虽然使用的是 * 匹配,但又加了索引选择,所以只获取了第二个后续节点。
第七次选择我们调用了 following-sibling 轴,可以获取当前节点之后的所有同级节点,这里我们使用的是 * 匹配,所以获取了所有后续同级节点。
3. BeautifulSoup
安装
1 | pip install beautifulsoup4 |
如何使用
1 | # 将一段文档传入BeautifulSoup 的构造方法,就能得到一个文档的对象, 可以传入一段字符串或一个文件句柄. |
对象种类
Tag。
Tag
对象与XML或HTML原生文档中的tag相同。1
2
3
4soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b
type(tag)
# <class 'bs4.element.Tag'>NavigableString。字符串常被包含在tag内.Beautiful Soup用
NavigableString
类来包装tag中的字符串。1
2
3
4tag.string
# u'Extremely bold'
type(tag.string)
# <class 'bs4.element.NavigableString'>
BeautifulSoup。
BeautifulSoup
对象表示的是一个文档的全部内容.大部分时候,可以把它当作Tag
对象,它支持 遍历文档树 和 搜索文档树 中描述的大部分的方法.Comment。
Comment
是文档的注释部分。1
2
3
4
5markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
soup = BeautifulSoup(markup)
comment = soup.b.string
type(comment)
# <class 'bs4.element.Comment'>
遍历文档树
find_all()
find_all( name , attrs , recursive , string , **kwargs )
find_all()
方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件.这里有几个例子:
1 | soup.find_all("title") |
name 参数
name
参数可以查找所有名字为 name
的tag,字符串对象会被自动忽略掉.
简单的用法如下:
1 | soup.find_all("title") |
重申: 搜索 name
参数的值可以使任一类型的 过滤器 ,字符窜,正则表达式,列表,方法或是 True
.
keyword 参数
如果一个指定名字的参数不是搜索内置的参数名,搜索时会把该参数当作指定名字tag的属性来搜索,如果包含一个名字为 id
的参数,Beautiful Soup会搜索每个tag的”id”属性.
1 | soup.find_all(id='link2') |
如果传入 href
参数,Beautiful Soup会搜索每个tag的”href”属性:
1 | soup.find_all(href=re.compile("elsie")) |
搜索指定名字的属性时可以使用的参数值包括 字符串 , 正则表达式 , 列表, True .
下面的例子在文档树中查找所有包含 id
属性的tag,无论 id
的值是什么:
1 | soup.find_all(id=True) |
使用多个指定名字的参数可以同时过滤tag的多个属性:
1 | soup.find_all(href=re.compile("elsie"), id='link1') |
有些tag属性在搜索不能使用,比如HTML5中的 data-* 属性:
1 | data_soup = BeautifulSoup('<div data-foo="value">foo!</div>') |
但是可以通过 find_all()
方法的 attrs
参数定义一个字典参数来搜索包含特殊属性的tag:
1 | data_soup.find_all(attrs={"data-foo": "value"}) |
find()
find( name , attrs , recursive , string , **kwargs )
find_all()
方法将返回文档中符合条件的所有tag,尽管有时候我们只想得到一个结果.比如文档中只有一个
find_all()
方法来查找标签就不太合适, 使用 find_all
方法并设置 limit=1
参数不如直接使用 find()
方法.下面两行代码是等价的:
1 | soup.find_all('title', limit=1) |
唯一的区别是 find_all()
方法的返回结果是值包含一个元素的列表,而 find()
方法直接返回结果.
find_all()
方法没有找到目标是返回空列表, find()
方法找不到目标时,返回 None
.
1 | print(soup.find("nosuchtag")) |
soup.head.title
是 tag的名字 方法的简写.这个简写的原理就是多次调用当前tag的 find()
方法:
1 | soup.head.title |
CSS选择器
Beautiful Soup支持大部分的CSS选择器 http://www.w3.org/TR/CSS2/selector.html [6] , 在 Tag
或 BeautifulSoup
对象的 .select()
方法中传入字符串参数, 即可使用CSS选择器的语法找到tag:
1 | soup.select("title") |
通过tag标签逐层查找:
1 | soup.select("body a") |
找到某个tag标签下的直接子标签 [6] :
1 | soup.select("head > title") |
找到兄弟节点标签:
1 | soup.select("#link1 ~ .sister") |
通过CSS的类名查找:
1 | soup.select(".sister") |
通过tag的id查找:
1 | soup.select("#link1") |
同时用多种CSS选择器查询元素:
1 | soup.select("#link1,#link2") |
通过是否存在某个属性来查找:
1 | soup.select('a[href]') |
通过属性的值来查找:
1 | soup.select('a[href="http://example.com/elsie"]') |
通过语言设置来查找:
1 | multilingual_markup = """ |
返回查找到的元素的第一个
1 | soup.select_one(".sister") |
对于熟悉CSS选择器语法的人来说这是个非常方便的方法.Beautiful Soup也支持CSS选择器API, 如果你仅仅需要CSS选择器的功能,那么直接使用 lxml
也可以, 而且速度更快,支持更多的CSS选择器语法,但Beautiful Soup整合了CSS选择器的语法和自身方便使用API.
爬虫解析数据实例
1 | from ast import parse |