from lxml import etreehtml = etree.parse('./data/sample1.html', etree.HTMLParser())result = etree.tostring(html)print(result.decode('utf-8'))
9.1.2 所有节点
我们一般会用 // 开头的 XPath 规则来选取所有符合要求的节点。这里以前面的 HTML 文本为例,如果要选取所有节点,可以这样实现:
from lxml import etreeresult = html.xpath('//*')print(result)
[<Element html at 0x19e5448d3c0>, <Element body at 0x19e244543c0>, <Element div at 0x19e56dd1280>, <Element ul at 0x19e56dd1300>, <Element li at 0x19e56dd1700>, <Element a at 0x19e56dd1740>, <Element li at 0x19e56dd1400>, <Element a at 0x19e56dd16c0>, <Element li at 0x19e56dd1780>, <Element a at 0x19e56dd1140>, <Element li at 0x19e56dd17c0>, <Element a at 0x19e56dd1800>, <Element li at 0x19e56dd1840>, <Element a at 0x19e56dd1880>]
这里使用 * 代表匹配所有节点,也就是整个 HTML 文本中的所有节点都会被获取。可以看到,返回形式是一个列表,每个元素是 Element 类型,其后跟了节点的名称,如 html、body、div、ul、li、a 等,所有节点都包含在列表中了。
当然,此处匹配也可以指定节点名称。如果想获取所有 li 节点,示例如下:
result = html.xpath('//li')print(result)print(result[0])
[<Element li at 0x19e56dd1700>, <Element li at 0x19e56dd1400>, <Element li at 0x19e56dd1780>, <Element li at 0x19e56dd17c0>, <Element li at 0x19e56dd1840>]
<Element li at 0x19e56dd1700>
这里要选取所有 li 节点,可以使用 //,然后直接加上节点名称即可,调用时直接使用 xpath 方法即可。
9.1.3 子节点
我们通过 / 或 // 即可查找元素的子节点或子孙节点。假如现在想选择 li 节点的所有直接子节点 a,可以这样实现:
result = html.xpath('//li/a')print(result)
[<Element a at 0x19e56dd1140>, <Element a at 0x19e56dd1bc0>, <Element a at 0x19e56dd1880>, <Element a at 0x19e56dd1c80>, <Element a at 0x19e56dd1c00>]
这里通过追加 /a 即选择了所有 li 节点的所有直接子节点 a。因为 //li 用于选中所有 li 节点,/a 用于选中 li 节点的所有直接子节点 a,二者组合在一起即获取所有 li 节点的所有直接子节点 a。
result = html.xpath('//ul//a')print(result)
[<Element a at 0x19e56dd1140>, <Element a at 0x19e56dd1bc0>, <Element a at 0x19e56dd1880>, <Element a at 0x19e56dd1c80>, <Element a at 0x19e56dd1c00>]
运行结果是相同的。
但是如果这里用 //ul/a,就无法获取任何结果了。因为 / 用于获取直接子节点,而在 ul 节点下没有直接的 a 子节点,只有 li 节点,所以无法获取任何匹配结果,代码如下:
比如,现在首先选中 href 属性为 link4.html 的 a 节点,然后获取其父节点,再获取其 class 属性,相关代码如下:
result = html.xpath('//a[@href="link4.html"]/../@class')print(result)
['item-1']
检查一下结果发现,这正是我们获取的目标 li 节点的 class 属性。
同时,我们也可以通过 parent:: 来获取父节点,代码如下:
result = html.xpath('//a[@href="link4.html"]/parent::*/@class')print(result)
['item-1']
9.1.5 属性匹配
在选取的时候,我们还可以用 @ 符号进行属性过滤。比如,这里如果要选取 class 为 item-0 的 li 节点,可以这样实现:
result = html.xpath('//li[@class="item-0"]')print(result)
[<Element li at 0x19e56dd7280>, <Element li at 0x19e56dd7140>]
这里我们通过加入 [@class="item-0"],限制了节点的 class 属性为 item-0,而 HTML 文本中符合条件的 li 节点有两个,所以结果应该返回两个匹配到的元素
可见,匹配结果正是两个,至于是不是那正确的两个,后面再验证。
9.1.6 文本获取
我们用 XPath 中的 text 方法获取节点中的文本,接下来尝试获取前面 li 节点中的文本,相关代码如下:
result = html.xpath('//li[@class="item-0"]/text()')print(result)
['\n ']
奇怪的是,我们并没有获取到任何文本,只获取到了一个换行符,这是为什么呢?因为 XPath 中 text 方法前面是 /,而此处 / 的含义是选取直接子节点,很明显 li 的直接子节点都是 a 节点,文本都是在 a 节点内部的,所以这里匹配到的结果就是被修正的 li 节点内部的换行符,因为自动修正的 li 节点的尾标签换行了。
其中一个节点因为自动修正,li 节点的尾标签添加的时候换行了,所以提取文本得到的唯一结果就是 li 节点的尾标签和 a 节点的尾标签之间的换行符。
因此,如果想获取 li 节点内部的文本,就有两种方式,一种是先选取 a 节点再获取文本,另一种就是使用 //。接下来,我们来看下二者的区别。
首先,选取 a 节点再获取文本,代码如下:
result = html.xpath('//li[@class="item-0"]/a/text()')print(result)
['first item', 'fifth item']
result = html.xpath('//li[@class="item-0"]//text()')print(result)
['first item', 'fifth item', '\n ']
这里的返回结果是 3 个。可想而知,这里是选取所有子孙节点的文本,其中前两个就是 li 的子节点 a 内部的文本,另外一个就是最后一个 li 节点内部的文本,即换行符。
9.1.7 属性获取
我们知道用 text 方法可以获取节点内部文本,那么节点属性该怎样获取呢?其实还是用 @ 符号就可以。例如,我们想获取所有 li 节点下所有 a 节点的 href 属性,代码如下:
[<Element html at 0x19e24406d00>, <Element body at 0x19e56de5880>, <Element div at 0x19e56de58c0>, <Element ul at 0x19e56de5900>]
[<Element div at 0x19e56de58c0>]
['item-0']
[<Element a at 0x19e56de5800>]
[<Element span at 0x19e56de58c0>]
[<Element a at 0x19e56de5900>]
[<Element li at 0x19e56de5800>, <Element li at 0x19e56de59c0>, <Element li at 0x19e56de5980>, <Element li at 0x19e56de5940>]
from bs4 import BeautifulSoupsoup = BeautifulSoup('<p>Hello</p>', 'lxml')print(soup.p.string)
Hello
在后面,Beautiful Soup 的用法实例也统一用这个解析器来演示。
9.2.2 基本使用
下面首先用实例来看看 Beautiful Soup 的基本用法:
html ="""<html><head><title>The Dormouse's story</title></head><body><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>"""from bs4 import BeautifulSoupsoup = BeautifulSoup(html, 'lxml')print(soup.prettify())print(soup.title.string)
<html>
<head>
<title>
The Dormouse's story
</title>
</head>
<body>
<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 class="sister" href="http://example.com/elsie" id="link1">
<!-- Elsie -->
</a>
,
<a class="sister" href="http://example.com/lacie" id="link2">
Lacie
</a>
and
<a class="sister" href="http://example.com/tillie" id="link3">
Tillie
</a>
;
and they lived at the bottom of a well.
</p>
<p class="story">
...
</p>
</body>
</html>
The Dormouse's story
html ="""<html><head><title>The Dormouse's story</title></head><body><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>"""from bs4 import BeautifulSoupsoup = BeautifulSoup(html, 'lxml')print(soup.title)print(type(soup.title))print(soup.title.string)print(soup.head)print(soup.p)
<title>The Dormouse's story</title>
<class 'bs4.element.Tag'>
The Dormouse's story
<head><title>The Dormouse's story</title></head>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
这里依然选用刚才的 HTML 代码,首先打印输出 title 节点的选择结果,输出结果正是 title 节点加里面的文字内容。接下来,输出它的类型,是 bs4.element.Tag 类型,这是 Beautiful Soup 中一个重要的数据结构。经过选择器选择后,选择结果都是这种 Tag 类型。Tag 具有一些属性,比如 string 属性,调用该属性,可以得到节点的文本内容,所以接下来的输出结果正是节点的文本内容。
接下来,我们又尝试选择了 head 节点,结果也是节点加其内部的所有内容。最后,选择了 p 节点。不过这次情况比较特殊,我们发现结果是第一个 p 节点的内容,后面的几个 p 节点并没有选到。也就是说,当有多个节点时,这种选择方式只会选择到第一个匹配的节点,其他的后面节点都会忽略。
html ="""<html> <head> <title>The Dormouse's story</title> </head> <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> <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>"""from bs4 import BeautifulSoupsoup = BeautifulSoup(html, 'lxml')print(soup.p.contents)
['\n Once upon a time there were three little sisters; and their names were\n ', <a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>, '\n', <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, '\n and\n ', <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>, '\n and they lived at the bottom of a well.\n ']
可以看到,返回结果是列表形式。p 节点里既包含文本,又包含节点,最后会将它们以列表形式统一返回。
需要注意的是,列表中的每个元素都是 p 节点的直接子节点。比如第一个 a 节点里面包含一层 span 节点,这相当于孙子节点了,但是返回结果并没有单独把 span 节点选出来。所以说,contents 属性得到的结果是直接子节点的列表。
同样,我们可以调用 children 属性得到相应的结果:
from bs4 import BeautifulSoupsoup = BeautifulSoup(html, 'lxml')print(soup.p.children)for i, child inenumerate(soup.p.children):print(i, child)
<list_iterator object at 0x0000019E5706B130>
0
Once upon a time there were three little sisters; and their names were
1 <a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
2
3 <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
4
and
5 <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
6
and they lived at the bottom of a well.
print(soup.p.descendants)for i, child inenumerate(soup.p.descendants):print(i, child)
<generator object Tag.descendants at 0x0000019E5706D510>
0
Once upon a time there were three little sisters; and their names were
1 <a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
2
3 <span>Elsie</span>
4 Elsie
5
6
7 <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
8 Lacie
9
and
10 <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
11 Tillie
12
and they lived at the bottom of a well.
父节点和祖先节点
如果要获取某个节点元素的父节点,可以调用 parent 属性:
html ="""<html> <head> <title>The Dormouse's story</title> </head> <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> </p> <p class="story">...</p>"""from bs4 import BeautifulSoupsoup = BeautifulSoup(html, 'lxml')print(soup.a.parent)
<p class="story">
Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
</p>
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>"""from bs4 import BeautifulSoupsoup = BeautifulSoup(html, 'lxml')print('Next Sibling', soup.a.next_sibling)print('Prev Sibling', soup.a.previous_sibling)print('Next Siblings', list(enumerate(soup.a.next_siblings)))print('Prev Siblings', list(enumerate(soup.a.previous_siblings)))
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">Bob</a><a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> </p>"""from bs4 import BeautifulSoupsoup = BeautifulSoup(html, 'lxml')print('Next Sibling:')print(type(soup.a.next_sibling))print(soup.a.next_sibling)print(soup.a.next_sibling.string)print('Parent:')print(type(soup.a.parents))print(list(soup.a.parents)[0])print(list(soup.a.parents)[0].attrs['class'])
import rehtml='''<div class="panel"> <div class="panel-body"> <a>Hello, this is a link</a> <a>Hello, this is a link, too</a> </div></div>'''from bs4 import BeautifulSoupsoup = BeautifulSoup(html, 'lxml')print(soup.find_all(text=re.compile('link')))
['Hello, this is a link', 'Hello, this is a link, too']
C:\Users\xinlu\AppData\Local\Temp\ipykernel_5920\2083358073.py:12: DeprecationWarning:
The 'text' argument to find()-type methods is deprecated. Use 'string' instead.
好,既然刚才提取的结果是一个可迭代对象 SelectorList,那么要获取提取到的所有 li 节点的文本内容就要对结果进行遍历了,写法如下:
from parsel import Selectorselector = Selector(text=html)items = selector.css('.item-0')for item in items: text = item.xpath('.//text()').get()print(text)
这里 get 方法的作用是从 SelectorList 里面提取第一个 Selector 对象,然后输出其中的结果。
result = selector.xpath('//li[contains(@class, "item-0")]//text()').get()print(result)
first item
其实这里我们使用 //li[contains(@class, "item-0")]//text() 选取了所有 class 包含 item-0 的 li 节点的文本内容。应该来说,返回结果 SelectorList 应该对应三个 li 对象,而这里 get 方法仅仅返回了第一个 li 对象的文本内容,因为其实它会只提取第一个 Selector 对象的结果。
那有没有能提取所有 Selector 的对应内容的方法呢?有,那就是 getall 方法。
result = selector.xpath('//li[contains(@class, "item-0")]//text()').getall()print(result)