在 Python 中使用 XPath

根据字节点中的属性值提取父节点


今天使用 Python 的 lxml 模块来提取网页中的内容, 有一个 XPath 的用法不明白, 问题是, table 子节点下有一系列 tr 子节点, 每个 tr 子节点里有 3 列 (td), 如果 td 中 style 属性的值为 color: 0, 那么就不提取它所属的这一 tr 子节点。

查了很久 stackoverflow 才解决, 方法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from lxml import etree

html = '''
<html>
<table>
<tr id="id_l107" class="new nick nick_cxreg">
<td class="time" ><a href="/perl6/2016-09-21>05:07</a></td>
<td style="color: #8d8100" class="nick">cxreg</td>
<td class="msg">probably useful</a></td>
</tr>

<tr id="id_l108" class="new nick nick_mask">
<td class="time" ><a href="/perl6/2016-09-21>05:09</a></td>
<td style="color: #8d8100" class="nick">mask</td>
<td class="msg">maybe better directed at <a href="/moarvm/today">#moarvm</a></td>
</tr>

<tr id="id_l74" class="cont special dark">
<td class="time" ><a href="/perl6/2016-09-21>02:37</a></td>
<td style="color: 0" class="nick"></td>
<td class="msg">mcmillhj joined <a href="/perl6/today">#perl6</a></td>
</tr>
<table>
</html>
'''


selector = etree.HTML(html)
content = selector.xpath('//tr[descendant::td[@style!="color: 0"]]/td/text()')

for each in content:
print(each)
```

打印出:

cxreg
probably useful:
mask
maybe better directed at

1
2
3
4
5

说明得到我们想要的结果了。怎么验证我们取得的是两个 tr 呢?

```python
content = selector.xpath('//tr[descendant::td[@style!="color: 0"]]')

打印出

1
2
<Element tr at 0x139c3b0>
<Element tr at 0x139c368>

说明的确提取出了 2 个 tr 元素。descendant 是后代的意思。上面那句代码意思是

过滤 tr 元素, 如果 tr 的后代元素 td 的属性 style 值不为 “color: 0”, 那么就提取这个 tr 子节点。否则不提取。

方法二,先过滤掉 td 然后使用 .. 语法返回到父级元素:

1
content = selector.xpath('//tr/td[starts-with(@style,"color: #")]/../td/text()')

这里找到 td 元素中属性以 color: #开头的 td (过滤掉了 color: 0 这样的 td), 然后使用 .. 语法得到只含有 color: # 子元素的 tr 父节点。以上两种方法异曲同工, 殊途同归。并且方法二最后得到的也是两个 tr 元素:

1
content = selector.xpath('//tr/td[starts-with(@style,"color: #")]/..')

打印:

1
2
<Element tr at 0x222f170>
<Element tr at 0x222f198>

程序最后主要代码如下:

1
2
3
4
5
6
content = selector.xpath('//tr[descendant::td[@style!="color: 0"]]')
for each in content:
info = each.xpath('string(.)')
msg = info.replace('\n', '')
print(msg)
print('-' * 85)

xpath.png

lxml 的安装



关于 lxml 的安装, Python 2.7 以下的就不说了, 说下 Python 3 下怎么安装 lxml:

Windows 7 32bit/64bit 系统下直接使用 pip install lxml 会提示 lxml Unable to find vcvarsall.bat。 我们采用本地安装, 线安装 wheel 模块:

1
pip install wheel

然后到模块仓库pythonlibs (相当于 Perl 的 metacpan)下载 lxml, 我下载的是 lxml-3.6.4-cp35-cp35m-win32.whl(即使你是 64 bit的系统)。然后在该文件所在目录下执行

1
pip install  lxml-3.6.4-cp35-cp35m-win32.whl

这样就安装成功了。如下图所示

result.png

Ubuntu 系统下你需要先安装依赖包:

1
sudo apt-get libxml2, libxml2-devel, libxlst, libxlst-devel, python-libxml2, python-libxslt

然后再安装

1
pip install lxml