网络爬虫 —— XPath 与 CSSSelect

20 篇文章 0 订阅
3 篇文章 0 订阅

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,第三方库 beautifulsoup4lxml ,个人倾向于使用 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 对象包含两个方法:xpathcssselect,可以直接调用对应的选择语法,获取数据。

虽然我们用到了两个额外的包,但只需要其中最简单的功能,更多的是介绍该对象的使用,而不会涉及其他内容的介绍,更多细致内容可查看相应的官方文档。

XPath 语法

XPath 是一种路径选择语法,以表达式的方式,根据结点及结点之间的关系,加入不同的属性或索引判断,以及一些功能函数,能够快速定位需要检索的内容。

对于树结构,最重要的是要理清更层次结点之间的关系,可以照着族谱那样理解,主要包括

  • 父结点:即直接父结点,上述示例中 genomegene 的父结点
  • 子结点:即直接子结点,genegenome 的子结点
  • 同胞结点:具有相同父结点的子结点,两个 gene 结点时互为同胞的
  • 祖先结点:从父结点上溯到根结点的所有结点,chrom 的祖先节点包括 positiongenegenome
  • 子孙结点:从子结点一直到叶子结点的所有结点,gene 的子孙结点包括 namepositionchromstartend

其中,尖括号定义的是元素结点,最外层的一个是根节点,而在尖括号内部定义的值称为属性,这些属性是一种特殊的子结点,仅属于同一个尖括号内的元素结点,即 typeidgene 的子结点但不是 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

选择 entry1entry236 的结点

etree.xpath('//relation[@entry1 = 36 or @entry2 = 36]')
# [<Element relation at 0x7fab2df598c0>,
#  <Element relation at 0x7fab2df59900>,
#  <Element relation at 0x7fab2df59a40>,
#  <Element relation at 0x7fab2df5c400>]

选取 entry136entry252 的结点

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 子结点 graphicsname 属性中获取基因的名称

eles[0].xpath('graphics/@name')
# ['CHEK1, CHK1...']
eles[1].xpath('graphics/@name')
# ['TP53, BCC7, BMFS5, LFS1, P53, TRP53']

可以看出 CHEK1TP53 基因之间存在某种互作关系,是哪种互作关系呢?

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 ...

获取位置小于 3entry

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-aftersubstring-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)是一种专门用于为结构化文档(HTMLXML)添加样式的语言,能够以高度定制化的方式将数据呈现出不同的效果。要达到数据的精细化控制,必须要有高效的数据选择模式,即 CSS 选择器,选择数据并为其定义对应的展现规则,例如字体、颜色和间距等。

相较于 XPath 语法,CSS 选择器结构更加简单容易些,而且速度也更快,但是缺点也很明显,没有父选择器。CSSHTMLJsvascript 是当今 web 服务中不可或缺的三大支柱,在 HTML 文件中可以看到更多的样式。

解析网页

我们分别使用 RPython 来解析该网页

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">

选择 idheader 的标签

html_elements(htree, css = "#header")
# {xml_nodeset (1)}
# [1] <div id="header">\n  <div class="logo">\n    <a href="/kegg/"><img src= ...

选择 classmain 的标签,是一个 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})

在这里,我们调用标准库 collectionsCounter 类来进行统计,其中 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

选择 idmetabolismenergy 的标签

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>

选择 classnavlistlogo 的标签

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

选择 classtitleidcontent 的标签

for e in htree.cssselect('.title,#content'):
    print(e.attrib)
# {'class': 'title'}
# {'id': 'content'}

获取类型为 hiddensearchinput 标签

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

获取 idcellular 的标签之后的一个 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 标签内部,就是每个子分类中通路信息的列表,该如何获取这个元素呢?

由于我们知道 bdiv 是相邻的,可以再添加一个相邻选择器

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

可以看到,列表选择器交替获取 dta 标签的文本。

伪选择器

伪选择器包含两种,一种是伪类:),是添加到选择器的关键字,指定要选择的元素的特殊状态;另一种是伪元素::),是一个附加至选择器末的关键词,允许你对被选择元素的特定部分修改样式。在我们的选择其中,只支持伪类,不支持伪元素的使用,可使用的伪类主要包括

伪类功能
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,其中 ab 都必须为整数,并且元素的第一个子元素的下标为 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 ...

选择 classnavlist 的标签中没有子标签的标签

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 选择器,似乎也不需要太复杂的函数就可以将 XMLHTML 文件中的内容提取出来,而且也不需要编写复杂的代码,或许我们根本不需要对包有更多的了解,就能胜任这份工作了。确实,我也在思考是否本章已经可以结束了,但好像还是缺点东西。我们在前面的示例中,为了简化示例和方便理解,尽量避免使用更多的函数或方法来操作对象。接下去两节的内容我们将会简单介绍两个包的使用,着重于介绍如何从对象中读取内容

  • 42
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

名本无名

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值