XPath & LXML
XPath (XML Path Language) 是设计来在XML文档中查找信息的语言,它同样适用于HTML。
我们在爬虫时,可以使用 XPath 来做相应的信息抽取。
⚠️【注意】需要安装好 LXML。
XPath常用规则
表达式
|
描述
|
nodename
|
选取此节点的所有子节点
|
/
|
从当前节点选取直接子节点
|
//
|
从当前节点选取子孙节点
|
.
|
选取当前节点
|
..
|
选取当前节点的父节点
|
@
|
选取属性
|
我们常用
//
开头的 XPath 规则来选取所有符合要求的节点。
另外,常用运算符见
XPath 运算符
。
导入 HTML
从字符串导入 HTML
导入了 LXML 库的 etree 模块,然后声明了一段 HTML 文本,调用 HTML 类进行初始化,这样我们就成功构造了一个 XPath 解析对象。
⚠️【注意】etree 模块可以对 HTML 文本进行修正。
调用 tostring() 方法即可输出修正后的 HTML 代码,结果是 bytes 类型(可以利用 decode() 方法转成 str 类型)
from lxml import etree
text = '''
<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive"><a href="link3.html">third item</a></li>
<li class="item-1"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>
html = etree.HTML(text)
result = etree.tostring(html)
print(result.decode('utf-8'))
从文件导入 HTML
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))
获取所有节点
获取一个 HTML 中的所有节点,使用规则 //*
:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//*')
print(result)
我们得到了一个 由 Element 类型组成的列表。
获取所有指定标签
如果我们想获取所有 li 标签,我们可以把上例中的html.xpath() 中的规则改为 '//li'
:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li')
print(result)
如果无法获取任何匹配结果,html.xpath 将会返回 []
获取子节点
选择 li 节点所有直接 a 子节点,使用规则 '//li/a'
:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a')
print(result)
要获取其下所有子孙a节点可以这样://li//a
获取特定属性的节点
用 @ 符号进行属性过滤。
smt[...] 是有 ... 限制的smt。
选中 href 是 link4.html 的 a 节点,规则是 '//a[@href="link4.html"]
:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//a[@href="link4.html"]')
print(result)
获取父节点
如果我们想获取上例的父节点, 然后再获取其 class 属性:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//a[@href="link4.html"]/../@class')
print(result)
关于节点轴的使用,详见 XPath Axes
获取节点中的的文本
XPath 中的 text() 方法可以获取节点中的直接文本(不包括其子节点中的文本)。
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]/text()')
print(result)
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a/@href')
print(result)
用上面这种方法只能获取只有一个值的属性,
对于下面这种:
<li class="li li-first"><a href="link.html">first item</a></li>
li 节点的 class 属性有两个值,上面这个方法会失效,我们可以使用 contains() 函数:
from lxml import etree
text = '''
<li class="li li-first"><a href="link.html">first item</a></li>
html = e
result = html.xpath('//li[contains(@class, "li")]/a/text()')
print(result)
这里还可以使用运算符 and 来连接:
from lxml import etree
text = '''
<li class="li li-first" name="item"><a href="link.html">first item</a></li>
html = etree.HTML(text)
result = html.xpath('//li[contains(@class, "li") and @name="item"]/a/text()')
print(result)
点击链接查看详细的 XPath 教程 、 lxml 库。
第二招 BeautifulSoup
本节主要介绍解析库 BeautifulSoup 的使用。
BeautifulSoup
BeautifulSoup 提供一些简单的、Python式的函数用来处理导航、搜索、修改分析树等功能。它通过解析文档为用户提供需要抓取的数据。利用它我们可以提高解析效率。
BeautifulSoup 拥有完善的官方中文文档,可以查看 BeautifulSoup官方文档
⚠️【注意】需要安装好 BeautifulSoup 和 LXML。
BeautifulSoup 可以使用多种解析器,主要的几种如下:
解析器 | 使用方法 | 优势 | 劣势 |
---|
Python标准库 | BeautifulSoup(markup, "html.parser") | Python的内置标准库、执行速度适中 、文档容错能力强 | Python 2.7.3 or 3.2.2)前的版本中文容错能力差 |
LXML HTML 解析器 | BeautifulSoup(markup, "lxml") | 速度快、文档容错能力强 | 需要安装C语言库 |
LXML XML 解析器 | BeautifulSoup(markup, "xml") | 速度快、唯一支持XML的解析器 | 需要安装C语言库 |
html5lib | BeautifulSoup(markup, "html5lib") | 最好的容错性、以浏览器的方式解析文档、生成 HTML5 格式的文档 | 速度慢、不依赖外部扩展 |
我们一般使用 LXML 解析器来进行解析,使用方法如下:
from bs4 import BeautifulSoup
soup = BeautifulSoup('<p>Hello</p>', 'lxml')
print(soup.p.string)
BeaufulSoup对象的初始化
使用如下代码就可以导入HTML,完成BeautifulSoup对象的初始化,并自动更正(如闭合未闭合的标签)。
soup = BeautifulSoup(markup, "lxml")
初始化之后我们还可以对要解析的字符串以标准的缩进格式输出:
print(soup.prettify())
节点选择器
选择元素的时候直接通过调用节点的名称就可以选择节点元素,
调用 string 属性就可以得到节点内的文本。
from bs4 import BeautifulSoup
html = """
<html><head><title>The Dormouse's story</title></head>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
soup = BeautifulSoup(html, 'lxml')
print(soup.title)
print(type(soup.title))
print(soup.title.string)
print(soup.head)
print(soup.p)
我们还可以进行 嵌套选择,即做类似 父.子.孙
的选择:
print(soup.head.title.string)
有时候我们难以做到一步就可以选择到想要的节点元素,这时我们可以先选中某一个节点元素,然后以它为基准再选择它的子节点、父节点、兄弟节点等等
获取子孙节点
选取到了一个节点元素之后,如果想要获取它的直接 子节点 可以调用 contents
属性,将返回一个依次列有所有子节点的list。
如p标签之类的节点中可能既包含文本,又包含节点,返回的结果会将他们以列表形式都统一返回。
soup.p.contents
'''(result)
'Once upon a time ... were\n',
<a class="sister" href="..." id="link1"><!-- Elsie --></a>,
',\n',
<a class="sister" href="..." id="link2">Lacie</a>,
' and\n',
<a class="sister" href="..." id="link3">Tillie</a>,
';\nand ... well.'
同时,查询 子节点,我们还可以使用 children
属性,它将返回一个 list_iterator object,化为 list 之后,就和 contents 一样了:
>>> s.p.children
<list_iterator object at 0x109d6a8d0>
>>> a = list(soup.p.children)
>>> b = soup.p.contents
>>> a == b
我们可以逐个编号输出子节点:
for i, child in enumerate(soup.p.children):
print(i, child)
要得到所有的 子孙节点(所有下属节点)的话可以调用 descendants 属性,descendants 会递归地查询所有子节点(深度优先),得到的是所有的子孙节点,返回结果是一个 <generator object Tag.descendants at 0x109d297c8>
:
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.p.descendants)
for i, d in enumerate(soup.p.descendants):
print(i, d)
获取父节点和祖先节点
如果要获取某个节点元素的父节点,可以调用 parent
属性,返回一个节点:
>>> soup.span.parent
如果我们要想获取所有的祖先节点(一层层向上找,直到整个html),可以调用 parents
属性,返回一个generator:
>>> soup.span.parents
<generator object PageElement.parents at 0x109d29ed0>
>>> list(soup.span.parents)
⚠️【注意】父是 parent,祖先是 parents
获取兄弟节点
要获取同级的节点也就是兄弟节点,我们可以调用了四个不同的属性,它们的作用不尽相同:
next_sibling:获取节点向下一个兄弟节点,返回节点。
previous_sibling:获取向上一个兄弟节点,返回节点。
next_siblings:获取向下所有兄弟节点,返回一个generator。
previous_siblings:获取向上所有兄弟节点,返回一个generator。
>>> from bs4 import BeautifulSoup
>>> html = """
... <html>
... <body>
... <p class="story">
... Once upon a time there were three little sisters; and their names were
... <a href="http://example.com/elsie" class="sister" id="link1">
... <span>Elsie</span>
... </a>
... Hello
... <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a>
... and
... <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>
... and they lived at the bottom of a well.
... </p>
... """
>>> soup = BeautifulSoup(html, 'lxml')
>>> soup.a
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
>>> soup.a.next_sibling
'\n Hello\n '
>>> soup.a.previous_sibling
'\n Once upon a time there were three little sisters; and their names were\n '
>>> soup.a.next_siblings
<generator object PageElement.next_siblings at 0x1110e57c8>
>>> soup.a.previous_siblings
<generator object PageElement.previous_siblings at 0x1110e5de0>
>>> for i in soup.a.previous_siblings:
... print(i)
Once upon a time there were three little sisters; and their names were
>>> for i in soup.a.next_siblings:
... print(i)
Hello
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
and they lived at the bottom of a well.
方法选择器
有时难以利用节点选择器直接找到想要的节点时,我们可以利用 find_all()、find() 等方法,传入相应等参数就可以灵活地进行查询,得到想要的节点,然后通过关联选择就可以轻松获取需要的信息。
find()
find() 传入一些属性或文本来得到符合条件的元素,返回第一个匹配的元素。
find(name , attrs , recursive , text , **kwargs)
使用实例如下:
from bs4 import BeautifulSoup
html='''
<div class="panel">
<div class="panel-heading">
<h4>Hello</h4>
<div class="panel-body">
<ul class="list" id="list-1">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
<ul class="list list-small" id="list-2">
<li class="element">Foo</li>
<li class="element">Bar</li>
soup = BeautifulSoup(html, 'lxml')
print(soup.find(name='ul')
print(soup.find(attrs={'class': 'element'}))
print(soup.find(text=re.compile('.*?o.*?', re.S)))
findall()
find_all,类似于 find,但是 find_all 查询所有符合条件的元素,返回所有匹配的元素组成的列表。
还有诸如find_parents()、find_next_siblings()、find_previous_siblings()等的find,基本使用都差不多,只是搜索范围不同,详见 文档。
CSS选择器
BeautifulSoup 还提供了 CSS 选择器。
使用 CSS 选择器,只需要调用 select() 方法,传入相应的 CSS 选择器即可,返回的结果是符合 CSS 选择器的节点组成的列表:
from bs4 import BeautifulSoup
html='''
<div class="panel">
<div class="panel-heading">
<h4>Hello</h4>
<div class="panel-body">
<ul class="list" id="list-1">
<li class="element">Foo</li>
<li class="element">Bar</li>
<li class="element">Jay</li>
<ul class="list list-small" id="list-2">
<li class="element">Foo</li>
<li class="element">Bar</li>
soup = BeautifulSoup(html, 'lxml')
print(soup.select('.panel .panel-heading'))
print(soup.select('ul li'))
print(soup.select('#list-2 .element'))
print(type(soup.select('ul')[0]))
获取完整标签
要获取一个标签的完整html代码,只需要写它的节点选择器即可:
soup.title
获取标签类型
利用 name 属性来获取节点的类型(p、a、title、pre 等):
print(soup.title.name)
获取标签内容
正如我们之前所说,调用 string 属性就可以得到节点内的文本:
soup.title.string
⚠️【注意】如果标签下包含其他标签,.string
是不起作用的,它会返回一个 None:
>>> from bs4 import BeautifulSoup
>>> html = '<p>Foo<a href="#None">Bar</a></p>'
>>> soup = BeautifulSoup(html, 'lxml')
>>> print(soup.p.string)
获取内容,还可以使用节点的 get_text() 方法:
soup.p.get_text()
利用get_text,可以获取标签下所有文本,包括其子节点中的:
>>> from bs4 import BeautifulSoup
>>> html = '<p>Foo<a href="#None">Bar</a></p>'
>>> soup = BeautifulSoup(html, 'lxml')
>>> print(soup.p.string)
>>> print(soup.p.get_text())
FooBar
每个节点可能有多个属性,比如 id,class,我们可以调用 attrs 获取所有属性,进而可以通过字典的取值方法(中括号加属性名称,或调用其get()
方法)获取特定属性:
print(soup.p.attrs)
print(soup.p.attrs['name'])
'''(results)
{'class': ['title'], 'name': 'dromouse'}
dromouse
也可以直接使用中括号和属性名:
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
for ul in soup.select('ul'):
print(ul['id'])
print(ul.attrs['id'])
第三招 PyQuery
本节主要介绍解析库 PyQuery 的使用。
PyQuery
pyquery: a jquery-like library for python
PyQuery 的使用方法和 jQuery 十分类似。如果您是惯用 jQuery 的老前端了,请不妨一试。
⚠️【注意】需要安装好 PyQuery。
PyQuery 初始化时可以传入多种形式的数据源,如内容是 HTML 的字符串、源的URL、本地的文件名等。
字符串初始化
from pyquery import PyQuery as pq
html = '''
<h1>Header</h1>
<p>Something</p>
<p>Other thing</p>
<p>In div</p>
doc = pq(html)
print(doc('p'))
'''(results)
<p>Something</p>
<p>Other thing</p>
<p>In div</p>
URL初始化
from pyquery import PyQuery as pq
doc = pq(url='http://www.baidu.com', encoding='utf-8')
print(doc('title'))
'''(result)
<title>百度一下,你就知道</title>
CSS选择器
详见 CSS 选择器表。
查找子节点用 children('css-selector')
方法,参数为空则为全部。
查找子孙节点用 find('css-selector')
方法,参数不可为空!
查找父节点用 parent('css-selector')
方法,参数为空则为全部。
查找祖先节点用 parents('css-selector')
方法,参数为空则为全部。
查找兄弟节点用 siblings('css-selector')
方法,参数为空则为全部。
>>> p = doc('div')
[<div
>>> type(p)
<class 'pyquery.pyquery.PyQuery'>
>>> p.find('#head')
[<div
>>> print(p.find('#head'))
<div id="head"> ... </div>
用 PyQuery 选择到的结果可以遍历:
>>> for i in p.parent():
... print(i, type(i))
<Element a at 0x1055332c8> <class 'lxml.html.HtmlElement'>
<Element a at 0x105533368> <class 'lxml.html.HtmlElement'>
<Element a at 0x105533458> <class 'lxml.html.HtmlElement'>
注意是lxml的Element了,要用lxml的方法处理。
attr()
获取属性
a = doc('a')
print(a.attr('href'))
attr() 必须传入要选择的属性名。
若对象包含多个节点,调用对象的attr(),只会返回第一个对象的对应结果。要返回每一个的需要遍历。
text()
获取文本
a = doc('a')
a.text()
这个会输出所有包含节点的文本join的结果。
PyQuery 还可以操作节点,这个不是本文重点,再次不做介绍。