XPath 与 CSSSelect
文章目录
前言
超文本标记语言(HTML
)和可扩展标记语言(XML
)是两种结构相似的标记语言,其中 HTML
主要用于展示数据,专注于数据的外观,且标签固定;而 XML
主要被设计用于传输和携带数据信息,专注于数据的内容,没有预定义的标签,需要自行定义。
这两种标记语言所定义的内容结构是大致相同的,都是以一种树结构的形式存储。例如,对于 XML
文件,我们可以简单具有自我描述性的语法来定义数据
<?xml version="1.0" encoding="UTF-8"?>
<genome>
<gene type="protein coding" id="3845">
<name alias="RASK2 KRAS2 RALD">KRAS</name>
<position base="0">
<chrom>chr12</chrom>
<start>25358180</start>
<end>25403863</end>
</position>
</gene>
<gene type="protein coding" id="7157">
<name alias="P53">TP53</name>
<position base="0">
<chrom>chr17</chrom>
<start>7571720</start>
<end>7590868</end>
</position>
</gene>
</genome>
第一行是声明语句,定义版本及编码方式,第二行使用了一个根结点(<genome>
),即树的根结点,后面又跟了两个子结点(<gene>
),表示不同的基因,内部又定义了不同的分支存储基因的信息。可以看到,每个标签里面都可以定义相关的属性值,例如 type
指示基因的类型,id
表示基因的 ID
。这样一个文件就可以存储基因组上不同基因的结构化信息。
而对于 HTML
文件,我们只能使用预定义的标签,且每个标签都有其含义
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Genome</title>
</head>
<body>
<table border="1">
<tr><th>Symbol</th><th>ID</th></tr>
<tr><th>KRAS</th><th>3845</th></tr>
<tr><th>TP53</th><th>7157</th></tr>
</table>
</body>
</html>
虽然标签不同,展示方式不一样,但是结构基本一致都可以使用相对固定的规则来获取当中的信息。有规律就能够程序化获取其中的信息,最基础的方式是使用正则表达式,但写起来较为复杂,当然还有更加简便的方式。
我们主要介绍两种方法来快速获取结构化内容中的信息:XPath
语法和 CSS
选择器
前期准备
在开始介绍选择语法之前,我们先介绍如何获取 XML
文件并解析成相应的对象,能够直接将语法应用于对象中,看到实际的输出效果。
我们以 KEGG
数据库中的 Cell cycle
通路结构文件作为 XPath
测试的例子,其网址为:https://rest.kegg.jp/get/hsa04110/kgml
。该文件的结构可以查看 KEGG
提供的示意图,包含通路图的所有元件信息,也就是说根据该文件可以自己绘制出通路图,并在图片上添加自定义的一些东西
同样地,对于 CSS
选择器我们使用 KEGG
中通路数据库的首页 https://www.kegg.jp/kegg/pathway.html 来进行测试,该网页展示了所有的通路,及其分类
R:rvest
在 R
中,自带的 XML
包提供了较为丰富的函数来处理 XML
文件,但是感觉用起来不太方便,好像也不支持CSS
选择器。而 tidyverse
家族的 rvest
包,可以实现简单的网页爬虫,其元素选择函数 html_elements
正好支持 XPath
语法和 CSS
选择器。
该包并没有提供在线 XML
文件解析函数,而是调用 xml2
包中的 read_xml
函数,其内部很多函数也是基于 xml2
包中的函数来实现的
# install.packages("xml2")
library(xml2) # 1.3.2
url <- 'https://rest.kegg.jp/get/hsa04110/kgml'
tree <- read_xml(url)
str(tree)
# List of 2
# $ node:<externalptr>
# $ doc :<externalptr>
# - attr(*, "class")= chr [1:2] "xml_document" "xml_node"
然后使用导入 rvest
包,即可对 tree
对象进行操作
library(rvest) # 1.0.2
Python:lxml
在 Python
中,用于 XML
文件解析的包也挺多的,比如内置的 xml
,第三方库 beautifulsoup4
和 lxml
,个人倾向于使用 lxml
包,简单方便。为了方便,我们的数据将直接从网页获取,而不是下载到本地,因此需要用到第三方网页处理库 request
。
pip install lxml # 4.8.0
pip install requests # 2.27.1
lxml
在使用 CSS
选择器时会调用到 cssselect
包,也需要先确保环境中有安装
pip install cssselect # 1.1.0
首先,使用 requests.get
方法,以 get
请求的方式获取网页源码
import requests
url = 'https://rest.kegg.jp/get/hsa04110/kgml'
req = requests.get(url)
返回的 req
对象的 text
属性就存储了该通路的结构化信息,使用 lxml.etree.XML
可以对将该字符串进行解析
from lxml.etree import XML
etree = XML(req.text)
生成的 etree
对象包含两个方法:xpath
和 cssselect
,可以直接调用对应的选择语法,获取数据。
虽然我们用到了两个额外的包,但只需要其中最简单的功能,更多的是介绍该对象的使用,而不会涉及其他内容的介绍,更多细致内容可查看相应的官方文档。
XPath 语法
XPath
是一种路径选择语法,以表达式的方式,根据结点及结点之间的关系,加入不同的属性或索引判断,以及一些功能函数,能够快速定位需要检索的内容。
对于树结构,最重要的是要理清更层次结点之间的关系,可以照着族谱那样理解,主要包括
- 父结点:即直接父结点,上述示例中
genome
是gene
的父结点 - 子结点:即直接子结点,
gene
是genome
的子结点 - 同胞结点:具有相同父结点的子结点,两个
gene
结点时互为同胞的 - 祖先结点:从父结点上溯到根结点的所有结点,
chrom
的祖先节点包括position
、gene
、genome
- 子孙结点:从子结点一直到叶子结点的所有结点,
gene
的子孙结点包括name
、position
、chrom
、start
和end
其中,尖括号定义的是元素结点,最外层的一个是根节点,而在尖括号内部定义的值称为属性,这些属性是一种特殊的子结点,仅属于同一个尖括号内的元素结点,即 type
和 id
是 gene
的子结点但不是 genome
的子结点,因此,属性只能通过其父节点访问。
还有一种命名空间结点,主要用于区分两个不同的 XML
文件中标签一样但意义不同的情况下,使用 xmlns
属性来进行区分,对我们来说用处不大
选择结点
选择结点的方式为
表达式 | 描述 |
---|---|
node | 选择此结点的所有 node 子结点 |
/ | 从根节点开始选择,即绝对位置 |
// | 相对位置,所有能够匹配该规则的结点 |
. | 选择当前结点 |
.. | 选择当前结点的父结点 |
@ | 选择当前结点的属性 |
* | 通配符,匹配任意元素结点 |
R
使用相对位置和绝对位置选择所有的 entry
结点
entrys <- html_elements(tree, xpath="//entry")
length(entrys)
# [1] 115
entrys <- html_elements(tree, xpath="/pathway/entry")
length(entrys)
# [1] 115
选择第一个 entry
的所有属性
html_elements(entrys[[1]], xpath = "@*")
# {xml_nodeset (4)}
# [1] id="4"
# [2] name="hsa:1029"
# [3] type="gene"
# [4] link="https://www.kegg.jp/dbget-bin/www_bget?hsa:1029"
html_elements(entrys[[1]], xpath = "@id")
# {xml_nodeset (1)}
# [1] id="4"
获取 entry
的父结点 pathway
html_elements(entrys[[1]], xpath = "..")
# {xml_nodeset (1)}
# [1] <pathway name="path:hsa04110" org="hsa" number="04110" title="Cell cycl ...
获取所有子结点
html_elements(entrys[[1]], xpath = "graphics")
# {xml_nodeset (1)}
# [1] <graphics name="CDKN2A, ARF, CDK4I, CDKN2, CMM2, INK4, INK4A, MLM, MTS- ...
Python
获取所有基因或组分之间的互作关系,xpath
方法默认返回的都是列表形式,每个结点都是以 Element
对象的形式存储
relation = etree.xpath('//relation')
len(relation)
# 79
relation[:3]
# [<Element relation at 0x7f2fa9158dc0>,
# <Element relation at 0x7f2fa9158f00>,
# <Element relation at 0x7f2faf8ed4c0>]
提取第一个组分的所有属性值,xpath
方法的查询结果不会包含属性名称,可以使用 attrib
获取其属性字典
relation[0].xpath('@*')
# ['219', '54', 'GErel']
relation[0].xpath('@type')
# ['GErel']
relation[0].attrib
# {'entry1': '219', 'entry2': '54', 'type': 'GErel'}
获取所有名称为 subtype
的子结点,
relation[0].xpath('subtype')
# [<Element subtype at 0x7f2fa91582c0>]
relation[0].attrib
节点集合
使用轴,可以获取与当前结点相关的结点集合。所谓的轴,即以当前结点作为标志,从树的不同方向(或轴)前进来提取与当前结点相关的结点集合,如父结点和子孙结点等。主要包含如下轴
轴名称 | 功能 |
---|---|
ancestor | 选取当前结点的祖先结点,总是包含根结点 |
ancestor-or-self | 选取当前结点及其祖先结点 |
attribute | 选取当前结点的属性 |
child | 选取当前结点的子结点 |
descendant | 选取当前结点的子孙结点 |
descendant-or-self | 选取当前结点及其子孙结点 |
following | 选取当前结点之后的所有结点,不包括其任何子孙、属性及命名空间结点 |
following-sibling | 选取当前结点之后的所有同级结点,如果为属性或命名空间结点则返回空 |
namespace | 选取当前结点的所有命名空间结点 |
parent | 选取当前结点的父结点 |
preceding | 选取当前结点之前的所有结点,不包括其任何祖先、属性及命名空间结点 |
preceding-sibling | 选取当前结点之前的所有同级结点,如果为属性或命名空间结点则返回空 |
self | 选取当前结点本身 |
轴通常与结点测试和谓词搭配使用,使用形式为:
轴名::结点测试[谓词]
结点测试主要用于获取轴内部的结点,可以使用的值有属性或结点名称、通配符及函数,函数主要包括:
node
:获取指定轴上的所有结点text
:获取指定轴上的所有文本结点的内容,即闭合标签之间的的文本(<a>This</a>
)comment
:获取指定轴上的所有注释结点
谓词(或称谓词表达式)通常是放在方括号中的表达式,主要用于查找某个特定的结点或者包含某个指定值的结点,该表达式可以是位置索引或一些判断条件值
R
获取结点的所有子结点
test <- entrys[[100]]
html_elements(test, xpath = "child::*")
# {xml_nodeset (3)}
# [1] <graphics fgcolor="#000000" bgcolor="#FFFFFF" type="rectangle" x="457" ...
# [2] <component id="70"/>
# [3] <component id="74"/>
获取当前结点的孙子结点
html_elements(test, xpath = "child::component")
# {xml_nodeset (2)}
# [1] <component id="70"/>
# [2] <component id="74"/>
获取结点的所有属性,相当于 @*
html_elements(test, xpath = "attribute::*")
# {xml_nodeset (3)}
# [1] id="222"
# [2] name="undefined"
# [3] type="group"
html_elements(test, xpath = "attribute::name")
# {xml_nodeset (1)}
# [1] name="undefined"
获取结点之前的所有结点
test <- entrys[[3]]
html_elements(test, xpath = "preceding-sibling::*")
# {xml_nodeset (2)}
# [1] <entry id="4" name="hsa:1029" type="gene" link="https://www.kegg.jp/dbg ...
# [2] <entry id="5" name="hsa:51343" type="gene" link="https://www.kegg.jp/db ...
html_elements(test, xpath = "preceding::*")
# {xml_nodeset (4)}
# [1] <entry id="4" name="hsa:1029" type="gene" link="https://www.kegg.jp/dbg ...
# [2] <graphics name="CDKN2A, ARF, CDK4I, CDKN2, CMM2, INK4, INK4A, MLM, MTS- ...
# [3] <entry id="5" name="hsa:51343" type="gene" link="https://www.kegg.jp/db ...
# [4] <graphics name="FZR1, CDC20C, CDH1, FZR, FZR2, HCDH, HCDH1" fgcolor="#0 ...
可以看到,preceding-sibling
只包含同级的所有结点,而 preceding
包含该结点之前的所有结点及这些结点的子结点
函数测试
html_elements(test, xpath = "child::node()")
# {xml_nodeset (1)}
# [1] <graphics name="MCM2, BM28, CCNL1, CDCL1, D3S3194, DFNA70, MITOTIN, cdc ...
html_elements(test, xpath = "child::text()")
# {xml_nodeset (0)}
html_elements(test, xpath = "child::comment()")
# {xml_nodeset (0)}
Python
获取当前结点的子结点的父结点,即结点本身
r = relation[20]
r.attrib
# {'entry1': '221', 'entry2': '66', 'type': 'PPrel'}
r.xpath('child::*/parent::*')[0].attrib
# {'entry1': '221', 'entry2': '66', 'type': 'PPrel'}
获取当前结点的所有 subtype
子结点
r.xpath('subtype')
# [<Element subtype at 0x7f2fbfd30380>, <Element subtype at 0x7f2fbfd305c0>]
获取当前结点及其子孙结点
r.xpath('descendant-or-self::*')
# [<Element relation at 0x7f2fbfd30540>,
# <Element subtype at 0x7f2fbfd30380>,
# <Element subtype at 0x7f2fbfd305c0>]
不同于 R
中的结果,text
函数的结果是有值的
r.xpath('descendant-or-self::text()')
# ['\n ', '\n ', '\n ']
而且在使用 node
函数时,我们可以发现其并不等同于 *
r.xpath('descendant-or-self::node()')
# [<Element relation at 0x7f2fbfd30540>,
# '\n ',
# <Element subtype at 0x7f2fbfd30380>,
# '\n ',
# <Element subtype at 0x7f2fbfd305c0>,
# '\n ']
运算符
在 xpath
语法中,也支持一些运算符操作对表达式求值,主要包括
运算符 | 功能 | 运算符 | 功能 |
---|---|---|---|
+ | 加法 | - | 减法 |
* | 乘法 | div | `除法 |
= | 等于 | != | 不等于 |
< | 小于 | > | 大于 |
<= | 小于等于 | >= | 大于等于 |
or | 逻辑或 | and | 逻辑与 |
mod | 求余数 | ` | ` |
其中最需要注意的就是判断相等只要一个等于号,而不是两个等于号。 |
代表的是两个结点集合之间的并集,而不是逻辑或,逻辑或使用的是 or
R
在谓词表达式中,整数值代表索引,即选择的结点集合中的第几个结点,例如
html_elements(tree, xpath="//entry[1+1]")
# {xml_nodeset (1)}
# [1] <entry id="5" name="hsa:51343" type="gene" link="https://www.kegg.jp/db ...
html_elements(tree, xpath="//entry[5*20]")
# {xml_nodeset (1)}
# [1] <entry id="222" name="undefined" type="group">\n <graphics fgcolor="#0 ...
html_elements(tree, xpath="//entry[3 div 3]")
# {xml_nodeset (1)}
# [1] <entry id="4" name="hsa:1029" type="gene" link="https://www.kegg.jp/dbg ...
html_elements(tree, xpath="//entry[2-1]")
# {xml_nodeset (1)}
# [1] <entry id="4" name="hsa:1029" type="gene" link="https://www.kegg.jp/dbg ...
选择属性值大于或小于某一值的结点集合
html_elements(tree, xpath="//entry[@id < 7]")
# {xml_nodeset (3)}
# [1] <entry id="4" name="hsa:1029" type="gene" link="https://www.kegg.jp/dbg ...
# [2] <entry id="5" name="hsa:51343" type="gene" link="https://www.kegg.jp/db ...
# [3] <entry id="6" name="hsa:4171 hsa:4172 hsa:4173 hsa:4174 hsa:4175 hsa:41 ...
html_elements(tree, xpath="//entry[@id >= 235]")
# {xml_nodeset (3)}
# [1] <entry id="235" name="undefined" type="group">\n <graphics fgcolor="#0 ...
# [2] <entry id="236" name="undefined" type="group">\n <graphics fgcolor="#0 ...
# [3] <entry id="237" name="undefined" type="group">\n <graphics fgcolor="#0 ...
Python
选择 entry1
或 entry2
为 36
的结点
etree.xpath('//relation[@entry1 = 36 or @entry2 = 36]')
# [<Element relation at 0x7fab2df598c0>,
# <Element relation at 0x7fab2df59900>,
# <Element relation at 0x7fab2df59a40>,
# <Element relation at 0x7fab2df5c400>]
选取 entry1
为 36
且 entry2
为 52
的结点
rel = etree.xpath('//relation[@entry1 = 36 and @entry2 = 52]')
rel
# [<Element relation at 0x7fab2df598c0>]
同时取出这两个 id
值对应的结点
eles = etree.xpath('//entry[@id=36] | //entry[@id=52]')
eles
# [<Element entry at 0x7fab2da3aa00>, <Element entry at 0x7fab2df32100>]
查看两个结点集合的属性信息
rel[0].xpath('@*')
# ['36', '52', 'PPrel']
eles[0].xpath('@id | @name | @type')
# ['36', 'hsa:1111 hsa:11200', 'gene']
eles[1].xpath('@id | @name | @type')
# ['52', 'hsa:7157', 'gene']
从 type
属性可以看出,这两个结点都是基因,我们可以在 entry
子结点 graphics
的 name
属性中获取基因的名称
eles[0].xpath('graphics/@name')
# ['CHEK1, CHK1...']
eles[1].xpath('graphics/@name')
# ['TP53, BCC7, BMFS5, LFS1, P53, TRP53']
可以看出 CHEK1
和 TP53
基因之间存在某种互作关系,是哪种互作关系呢?
rel[0].xpath('child::*/@*')
# ['activation', '-->', 'phosphorylation', '+p']
可以看到,是 CHEK1
基因磷酸化并激活了 TP53
基因
核心函数
XPath
也可以使用一些函数进行计算,根据数据的类型不同主要可以分为下面 4
种
结点函数
函数 | 功能 | 函数 | 功能 |
---|---|---|---|
last | 返回表达式的最后一个位置 | position | 返回表达式的位置 |
count | 计算结点集合的数量 | name | 返回结点集合的名称 |
R
这些函数可以作为谓词表达式放在方括号中,例如获取最后一个 entry
html_elements(tree, xpath="//entry[last()]")
# {xml_nodeset (1)}
# [1] <entry id="237" name="undefined" type="group">\n <graphics fgcolor="#0 ...
上面的代码也可以使用位置索引
html_elements(tree, xpath="//entry[115]")
# {xml_nodeset (1)}
# [1] <entry id="237" name="undefined" type="group">\n <graphics fgcolor="#0 ...
获取位置小于 3
的 entry
html_elements(tree, xpath="//entry[position() < 3]")
# {xml_nodeset (2)}
# [1] <entry id="4" name="hsa:1029" type="gene" link="https://www.kegg.jp/dbg ...
# [2] <entry id="5" name="hsa:51343" type="gene" link="https://www.kegg.jp/db ...
使用集合统计的函数会报错,所以在这里我们临时使用了 xml2
包中的两个函数,分别用于解析 XPath
不同类型的返回值
xml_find_num(tree, xpath = "count(//entry)")
# [1] 115
xml_find_chr(tree, xpath = "name(//entry)")
# [1] "entry"
Python
对于 Python
来说,解析并没有遇到什么问题
etree.xpath('//relation[last()]')
# [<Element relation at 0x7f2fa9145b80>]
etree.xpath('//relation[position() > 75]')
# [<Element relation at 0x7f2fa9145980>,
# <Element relation at 0x7f2fa9145a00>,
# <Element relation at 0x7f2fa9145a40>,
# <Element relation at 0x7f2fa9145b80>]
可以正常调用函数,只是计数的结果返回了浮点数
r.xpath('name(descendant-or-self::*)')
# 'relation'
r.xpath('name(descendant::*)')
# 'subtype'
r.xpath('count(descendant::*)')
# 2.0
字符串函数
函数 | 功能 |
---|---|
string | 将参数转换为字符串 |
concat | 将所有字符串参数连接起来 |
contains | 判断第一个字符串是否包含第二个字符串 |
starts-with | 判断第一个参数是否以第二个参数开头 |
substring-before | 返回第一个字符串中第二个字符串首次出现位置之前的所有字符,不存在返回空 |
substring-after | 同 substring-before ,返回之后的字符串 |
substring | 截取子字符串,第二个参数为起始位置,第三个参数为截取长度,默认为剩余长度 |
string-length | 计算字符串的长度 |
normalize-space | 删除字符串前后的空白字符 |
translate | 将字符串中,出现在第二个字符串中的字符替换为第三个字符串中同位置的字符 |
R
这些函数主要用于字符串的处理,例如,连接字符串
html_elements(test, xpath = "//entry[@id = concat(1,2)]")
# {xml_nodeset (1)}
# [1] <entry id="12" name="hsa:23594" type="gene" link="https://www.kegg.jp/d ...
判断字符串是否存在,在函数中可以调用属性值,获取 id
属性包含 25
的所有结点
html_elements(test, xpath = "//entry[contains(@id, 25)]")
# {xml_nodeset (2)}
# [1] <entry id="25" name="hsa:701" type="gene" link="https://www.kegg.jp/dbg ...
# [2] <entry id="225" name="undefined" type="group">\n <graphics fgcolor="#0 ...
截取那么属性值之后的数字来进行判断,提取对应的结点
html_elements(test, xpath = "//entry[substring-after(@name, 'hsa:') = 7157]")
# {xml_nodeset (1)}
# [1] <entry id="52" name="hsa:7157" type="gene" link="https://www.kegg.jp/db ...
删除字符串前后的空白符,在某些属性值左右存在空白符时可以使用
html_elements(test, xpath = "//entry[@id = normalize-space('\t222 \n')]")
# {xml_nodeset (1)}
# [1] <entry id="222" name="undefined" type="group">\n <graphics fgcolor="#0 ...
Python
获取所有包含抑制关系的结点
inhibit = etree.xpath('//subtype[starts-with(@name, "inhibit")]/..')
len(inhibit)
# 38
for e in inhibit[0].xpath('child::*'):
print(e.attrib)
# {'name': 'inhibition', 'value': '--|'}
# {'name': 'phosphorylation', 'value': '+p'}
也可以使用下面的选择方式,效果是一样的
inhibit = etree.xpath('//subtype[substring(@name, 1, 7) = "inhibit"]/..')
len(inhibit)
# 38
获取 name
属性字符串长度为 12
的结点
etree.xpath('//subtype[string-length(@name) = 12]/..')
# [<Element relation at 0x7f2fa9131940>, <Element relation at 0x7f2fa9131300>]
l12 = etree.xpath('//subtype[string-length(@name) = 12]/..')
for e in l12[0].xpath('child::*'):
print(e.attrib)
# {'name': 'dissociation', 'value': '-+-'}
使用替换函数,将小写字母替换为大写并进行判断
pprel = etree.xpath('./*[translate(@type, "opq", "OPQ") = "PPrel"]')
for e in pprel[0].xpath('child::*'):
print(e.attrib)
# {'name': 'inhibition', 'value': '--|'}
# {'name': 'phosphorylation', 'value': '+p'}
布尔函数
函数 | 功能 | 函数 | 功能 |
---|---|---|---|
boolean | 将参数转换为布尔值 | not | 对参数取反 |
true | 返回真 | false | 返回假 |
R
获取 id
属性存在的所有子结点
test <- entrys[[100]]
html_elements(test, xpath = "child::*[boolean(@id)]")
# {xml_nodeset (2)}
# [1] <component id="70"/>
# [2] <component id="74"/>
根据 id
属性值是否大于 72
来提取子结点
html_elements(test, xpath = "child::*[not(@id > 72)]")
# {xml_nodeset (2)}
# [1] <graphics fgcolor="#000000" bgcolor="#FFFFFF" type="rectangle" x="457" ...
# [2] <component id="70"/>
html_elements(test, xpath = "child::*[@id > 72]")
# {xml_nodeset (1)}
# [1] <component id="74"/>
Python
获取 name
属性存在的所有子结点
r.xpath('child::*[boolean(@name)]')
# [<Element subtype at 0x7f2fbfd30380>, <Element subtype at 0x7f2fbfd305c0>]
判断 value
值是否为 +p
的来选择子结点
r.xpath('child::*[@value = "+p"]')
# [<Element subtype at 0x7f2fbfd305c0>]
r.xpath('child::*[not(@value = "+p")]')
# [<Element subtype at 0x7f2fbfd30380>]
数值函数
函数 | 功能 | 函数 | 功能 |
---|---|---|---|
number | 将参数转换为整数值 | sum | 计算结点集合转换为数值之后的总和 |
floor | 向下取整 | ceiling | 向上取整 |
round | 近似整数 |
R
将属性转换为数值类型并进行判断
html_elements(test, xpath = "child::*[number(@id) = 70]")
# {xml_nodeset (1)}
# [1] <component id="70"/>
或者直接将数值传入函数
html_elements(test, xpath = "child::*[@id = floor(70.222)]")
# {xml_nodeset (1)}
# [1] <component id="70"/>
html_elements(test, xpath = "child::*[@id = ceiling(73.222)]")
# {xml_nodeset (1)}
# [1] <component id="74"/>
Python
四舍五入求近似值
etree.xpath('//entry[@id=round(36.6)]/@id')
# ['37']
etree.xpath('//entry[@id=round(36.2)]/@id')
# ['36']
对属性值求和
etree.xpath('sum(//entry/child::*[@id<100]/@id)')
# 1540.0
XPath
语法的关键是要理清楚各结点之间的结构关系,如果你熟悉树状结构,理解起来应该比较简单
CSS 选择器
层叠样式表(CSS
)是一种专门用于为结构化文档(HTML
、XML
)添加样式的语言,能够以高度定制化的方式将数据呈现出不同的效果。要达到数据的精细化控制,必须要有高效的数据选择模式,即 CSS
选择器,选择数据并为其定义对应的展现规则,例如字体、颜色和间距等。
相较于 XPath
语法,CSS
选择器结构更加简单容易些,而且速度也更快,但是缺点也很明显,没有父选择器。CSS
、HTML
和 Jsvascript
是当今 web
服务中不可或缺的三大支柱,在 HTML
文件中可以看到更多的样式。
解析网页
我们分别使用 R
和 Python
来解析该网页
R
url <- 'https://www.kegg.jp/kegg/pathway.html'
htree <- read_html(url)
Python
from lxml.etree import HTML
url = 'https://www.kegg.jp/kegg/pathway.html'
req = requests.get(url)
htree = HTML(req.text)
网页结果大概如下图所示,我们要获取的内容主要就在 <h4>
、<b>
和 <dt>
这三个标签内
CSS
选择器主要包含如下几种
基本选择器
基本选择器可分为:
选择器 | 使用方式 | 功能 |
---|---|---|
通用选择器 | * | 选择所有元素 |
元素选择器 | element | 选择所有名称为 element 的标签(元素或结点) |
ID 选择器 | #id | 选择 id 属性值为 id 的标签 |
类选择器 | .class | 选择 class 属性值为 class 的标签 |
属性选择器 | [attr] | 选择带有 attr 属性的所有标签 |
R
计算网页中共有多少个标签
length(html_elements(htree, css = "*"))
# [1] 2311
选择所有 input
标签
html_elements(htree, css = "input")
# {xml_nodeset (6)}
# [1] <input type="text" name="map" size="4" value="map">
# [2] <input type="button" value="Organism" οnclick="window.open('/kegg-bin/f ...
# [3] <input type="search" name="keyword" size="40">
# [4] <input type="hidden" name="mode" value="1">
# [5] <input type="hidden" name="viewImage" value="true">
# [6] <input type="submit" value="Go">
选择 id
为 header
的标签
html_elements(htree, css = "#header")
# {xml_nodeset (1)}
# [1] <div id="header">\n <div class="logo">\n <a href="/kegg/"><img src= ...
选择 class
为 main
的标签,是一个 div
标签
html_elements(htree, css = ".main")
# {xml_nodeset (1)}
# [1] <div class="main">\n\n<hr class="frame3">\n<div style="float:left;">\n< ...
Python
查看网页中包含多少个超链接,然后统计每种标签包含的超链接的个数
len(htree.cssselect('[href]'))
# 604
from collections import Counter
Counter([e.tag for e in htree.cssselect('[href]')])
# Counter({'link': 1, 'a': 603})
在这里,我们调用标准库 collections
的 Counter
类来进行统计,其中 tag
属性获取结点对应的标签。其实,对于属性的选择可以添加几种运算操作,来对属性值进行判断,而不只是简单的判断属性是否存在,主要包括
操作符 | 功能 | 操作符 | 功能 |
---|---|---|---|
= | 该属性的值等于某个值 | ~= | 该属性的值列表中包含某个单词 |
` | =` | 该属性的值为某个字符串或以字符串加 - 开头 | ^= |
$= | 该属性的值以某个字符串结尾 | *= | 该属性的值中包含某个字符串 |
选择 class
属性值为 dropdown
的标签
htree.cssselect('[class="dropdown"]')
# [<Element div at 0x7f2fbfeab0c0>,
# <Element div at 0x7f2faa510bc0>,
# <Element div at 0x7f2faa5141c0>]
选择 alt
属性值为以空格分隔的列表,且列表中有一个值为 KEGG
,通常是因为一个属性可以有多个值,每个值又对应一种样式,最后该标签将呈现叠加的样式。
for e in htree.cssselect('[alt*="KEGG"]'):
print(e.attrib)
# {'src': '/Fig/kegg128.gif', 'alt': 'KEGG icon'}
*=
则只是测试字符串的包含关系,与其不同。例如,获取 style
属性值中包含 float
的标签
for e in htree.cssselect('[style*="float"]'):
print(e.attrib)
# {'style': 'width:150px; float:left;'}
# {'style': 'float:left;'}
# {'style': 'float:left;'}
# {'style': 'float:right;'}
htree.cssselect('[style~="float"]')
# []
判断属性值的开头和结尾
for e in htree.cssselect('[class^="txt"]'):
print(e.attrib)
# {'class': 'txt3'}
# {'class': 'txt3'}
# {'class': 'txt3'}
for e in htree.cssselect('[id$="nav"]'):
print(e.attrib)
# {'id': 'topnav', 'ontouchstart': ''}
# {'id': 'dbnav', 'class': 'bar3'}
|=
只能匹配这个字符串或该字符串加 -
的前缀
for e in htree.cssselect('[charset|="utf"]'):
print(e.attrib)
# {'charset': 'utf-8'}
htree.cssselect('[class|="txt"]')
# []
for e in htree.cssselect('[class|="txt3"]'):
print(e.attrib)
# {'class': 'txt3'}
# {'class': 'txt3'}
# {'class': 'txt3'}
选择器列表
选择器列表是由多个选择器构成,每个选择器之间用逗号分开,不同选择器之间是或的关系,即只要匹配其中一个选择器就行
R
选择 id
为 metabolism
或 energy
的标签
html_elements(htree, css = "#metabolism, #energy")
# {xml_nodeset (2)}
# [1] <h4 id="metabolism">1. Metabolism</h4>
# [2] <b id="energy">1.2 Energy metabolism</b>
选择 class
为 navlist
或 logo
的标签
html_elements(htree, css = ".navlist, .logo")
# {xml_nodeset (3)}
# [1] <div class="navlist">\n <a href="/kegg/">KEGG</a>\n </div>
# [2] <div class="navlist">\n <a href="https://www.kanehisa.jp/">Kanehisa ...
# [3] <div class="logo">\n <a href="/kegg/"><img src="/Fig/kegg128.gif" al ...
Python
选择 class
为 title
或 id
为 content
的标签
for e in htree.cssselect('.title,#content'):
print(e.attrib)
# {'class': 'title'}
# {'id': 'content'}
获取类型为 hidden
和 search
的 input
标签
for e in htree.cssselect('input[type="hidden"],input[type="search"]'):
print(e.attrib)
# {'type': 'search', 'name': 'keyword', 'size': '40'}
# {'type': 'hidden', 'name': 'mode', 'value': '1'}
# {'type': 'hidden', 'name': 'viewImage', 'value': 'true'}
组合选择器
选择器 | 使用方式 | 功能 |
---|---|---|
后代选择器 | A B | 选择 A 标签内名称为 B 的所有子孙结点 |
子选择器 | A > B | 选择A 标签内的所有子标签 B |
同胞选择器 | A ~ B | 选择所有在 A 之后且与 A 为同胞的 B 标签 |
相邻选择器 | A + B | 选择 A 标签之后紧邻的一个同胞 B 标签 |
R
选择 form
标签内的所有 span
标签
html_elements(htree, css = "form span")
# {xml_nodeset (2)}
# [1] <span class="small">Select prefix</span>
# [2] <span class="small">Enter keywords</span>
选择 form
标签内的直接子标签 div
html_elements(htree, css = "form > span")
# {xml_nodeset (0)}
html_elements(htree, css = "form > div")
# {xml_nodeset (3)}
# [1] <div style="width:150px; float:left;">\n <span class="small">Select ...
# [2] <div style="float:left;">\n <span class="small">Enter keywords</span ...
# [3] <div style="clear:both;"></div>
Python
获取 id
为 cellular
的标签之后的一个 b
标签,其中 text
属性用于获取起始和终止标签之间的文本信息
for e in htree.cssselect('#cellular + b'):
print(e.text)
# 4.1 Transport and catabolism
根据第一个分类的 id
获取该标签,然后获取其后与其同级的所有 h4
标签,即可获取所有的通路分类
for e in htree.cssselect('#metabolism, #metabolism ~ h4'):
print(e.text)
# 1. Metabolism
# 2. Genetic Information Processing
# 3. Environmental Information Processing
# 4. Cellular Processes
# 5. Organismal Systems
# 6. Human Diseases
# 7. Drug Development
使用元素对象的 get
方法可以获取其属性值,那么我们就可以获取每个分类标签的 id
type(e)
# lxml.etree._Element
e.get('id')
# 'drug'
有了 id
可以获取其该标签其后的与其同级的所有 b
标签,该标签存储了每个子分类通路。例如
for b in htree.cssselect('#{} ~ b'.format(e.get('id'))):
print(b.text)
# 7.1 Chronology: Antiinfectives
# 7.2 Chronology: Antineoplastics
# 7.3 Chronology: Nervous system agents
# 7.4 Chronology: Other drugs
# 7.5 Target-based classification: G protein-coupled receptors
# 7.6 Target-based classification: Nuclear receptors
# 7.7 Target-based classification: Ion channels
# 7.8 Target-based classification: Transporters
# 7.9 Target-based classification: Enzymes
# 7.10 Structure-based classification
# 7.11 Skeleton-based classification
再分析文件的结构,我们可以发现,在上面遍历的每一个 b
标签之后的 div
标签内部,就是每个子分类中通路信息的列表,该如何获取这个元素呢?
由于我们知道 b
与 div
是相邻的,可以再添加一个相邻选择器
div = htree.cssselect('#{} ~ b + div'.format(e.get('id')))
获取最终的通路 ID
和通路名称
for t in div[-1].cssselect('dt, a'):
print(t.text)
# 07110
# Benzoic acid family
# 07112
# 1,2-Diphenyl substitution family
# 07114
# Naphthalene family
# 07117
# Benzodiazepine family
可以看到,列表选择器交替获取 dt
和 a
标签的文本。
伪选择器
伪选择器包含两种,一种是伪类(:
),是添加到选择器的关键字,指定要选择的元素的特殊状态;另一种是伪元素(::
),是一个附加至选择器末的关键词,允许你对被选择元素的特定部分修改样式。在我们的选择其中,只支持伪类,不支持伪元素的使用,可使用的伪类主要包括
伪类 | 功能 |
---|---|
not(selector) | 对选择器取反 |
empty | 选择没有子标签的标签 |
only-child | 选择没有任何兄弟标签的标签 |
first-child | 选择父标签的第一个子标签 |
last-child | 选择父标签的最后一个子标签 |
first-of-type | 选择子标签列表中,第一个给定类型的标签 |
last-of-type | 选择子标签列表中,最后一个给定类型的标签 |
nth-child(n) | 选择当前标签的所有兄弟标签中的第几个子标签 |
nth-last-child(n) | 选择从后数第几个子标签 |
nth-of-type(n) | 选择标签相同且为兄弟标签中的第几个标签 |
nth-last-of-type(n) | 选择从后数第几个标签 |
后面四个伪类都会按先后顺序对结果进行排序,然后选择括号中表达式匹配到的标签集合,其中表达式的形式为:形式为 an+b
,其中 a
和 b
都必须为整数,并且元素的第一个子元素的下标为 1
,最后获取的都是一个线性的序列。例如,0n+1
或数字的 1
匹配第一个标签,2n+1
为奇数位置的标签。
在这里,我们主要针对下面这些标签来进行说明
R
选择 form
标签中没有子元素的 div
,子元素可以是标签或文本(包括空格),注释或处理指令都不会产生影响。
html_elements(htree, css = "form div:empty")
# {xml_nodeset (1)}
# [1] <div style="clear:both;"></div>
对选择器取反,选择 form
的子标签 div
中非 input
的标签
# html_elements(htree, css = "form div :not(input)")
# {xml_nodeset (5)}
# [1] <span class="small">Select prefix</span>
# [2] <br>
# [3] <span class="small">Enter keywords</span>
# [4] <br>
# [5] <a href="javascript:void(window.open('/kegg/document/help_pathway_searc ...
选择 class
为 navlist
的标签中没有子标签的标签
html_elements(htree, css = ".navlist :only-child")
# {xml_nodeset (2)}
# [1] <a href="/kegg/">KEGG</a>
# [2] <a href="https://www.kanehisa.jp/">Kanehisa Lab</a>
选择 form
中同一个 div
下第一次出现的 input
标签
html_elements(htree, css = "form input:first-of-type")
# {xml_nodeset (2)}
# [1] <input type="text" name="map" size="4" value="map">
# [2] <input type="search" name="keyword" size="40">
选择 form
中同一个 div
下的第一个子标签是 span
的标签
html_elements(htree, css = "form span:first-child")
# {xml_nodeset (2)}
# [1] <span class="small">Select prefix</span>
# [2] <span class="small">Enter keywords</span>
html_elements(htree, css = "form input:first-child")
# {xml_nodeset (0)}
Python
选择在父标签(div
)中为奇数位置的 input
标签
for e in htree.cssselect('input:nth-child(2n+1)'):
print(e.attrib)
# {'type': 'text', 'name': 'map', 'size': '4', 'value': 'map'}
# {'type': 'search', 'name': 'keyword', 'size': '40'}
# {'type': 'hidden', 'name': 'viewImage', 'value': 'true'}
选择所有兄弟标签中第一个 input
标签
for e in htree.cssselect('input:nth-of-type(1)'):
print(e.attrib)
# {'type': 'text', 'name': 'map', 'size': '4', 'value': 'map'}
# {'type': 'search', 'name': 'keyword', 'size': '40'}
由于 form
标签下的两个 div
标签中,第一个标签都是 span
,所以对 input
使用 nth-child
伪类将返回空
htree.cssselect('form input:nth-child(1)')
# []
htree.cssselect('form span:nth-child(1)')
# [<Element span at 0x7fdd320ca580>, <Element span at 0x7fdd323dd700>]
CSS
选择器的语法较少,一般都是直接定位到元素,并不能获取元素的属性或文本信息,而需要调用对象的方法或属性。
通过上面的例子我们可以发现,使用
XPath
语法和CSS
选择器,似乎也不需要太复杂的函数就可以将XML
或HTML
文件中的内容提取出来,而且也不需要编写复杂的代码,或许我们根本不需要对包有更多的了解,就能胜任这份工作了。确实,我也在思考是否本章已经可以结束了,但好像还是缺点东西。我们在前面的示例中,为了简化示例和方便理解,尽量避免使用更多的函数或方法来操作对象。接下去两节的内容我们将会简单介绍两个包的使用,着重于介绍如何从对象中读取内容