网络爬虫 —— xml2 和 lxml

33 篇文章 3 订阅
3 篇文章 0 订阅

网络爬虫 —— xml2 和 lxml

xml2

xml2 包是对 libxml2C 编写的库)的封装,可以很方法快速地在 R 中解析 XMLHTML 文件,前面我们也使用了两个读取函数和两个解析函数。

read_xmlread_html 基本上是一样的,都可以读取字符串、文件或在线网址

content <- '<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>'

tree <- read_xml(content)

结点信息

每个结点对象都有一些保存了一些该结点的基本信息,可使用下面的函数来获取

函数功能函数功能
xml_attrs获取所有属性的值xml_attr获取指定属性的值
xml_has_attr是否存在某一属性xml_name属性标签
xml_text获取结点包含的所有文本信息xml_structure展示结点的结构
xml_type获取结点的类型xml_path获取结点的 XPath 表达式

获取结点的属性

xml_attrs(tree)
#             type               id 
# "protein coding"           "7157" 
xml_attr(tree, 'id')
# [1] "7157"
xml_has_attr(tree, "id")
# [1] TRUE
xml_has_attr(tree, "name")
# [1] FALSE

获取结点对应的标签

xml_name(tree)
# [1] "gene"

获取结点的所有文本信息,会将子结点的信息也合并在一起

xml_text(tree)
# [1] "TP53chr1775717207590868"

获取结点的结构化信息

xml_structure(tree)
# <gene [type, id]>
#   <name [alias]>
#     {text}
#   <position [base]>
#     <chrom>
#       {text}
#     <start>
#       {text}
#     <end>
#       {text}

获取 position 结点的类型以及 XPath 表达式

pos <- html_elements(tree, xpath = "position")
xml_type(pos)
# [1] "element"
xml_path(pos)
# [1] "/gene/position"

结点关系

结点一般都会单独存在,都会与其他节点构成不同层级的关系,如子结点、父节点和兄弟节点等,获取结点关系的函数有

函数功能函数功能
xml_children获取所有元素结点xml_child获取指定位置的子结点
xml_contents获取所有子结点xml_parents获取所有祖先结点
xml_siblings获取所有兄弟结点xml_parent返回结点的父结点
xml_length返回子结点的数量xml_root获取根结点

获取指定位置的子结点

xml_child(pos, 2)
# {xml_node}
# <start>
xml_child(pos, 3)
# {xml_node}
# <end>
xml_length(pos)
# [1] 3

获取所有元素子结点

test <- '<test>
  Hello <br/>
  <bold>
    World
  </bold>
</test>'
x <- read_xml(test)
xml_children(x)
# {xml_nodeset (2)}
# [1] <br/>
# [2] <bold>\n    World\n  </bold>

xml_contents 会获取所有子结点,包含元素子结点和字符串结点

xml_contents(x)
# {xml_nodeset (5)}
# [1] \n  Hello 
# [2] <br/>
# [3] \n  
# [4] <bold>\n    World\n  </bold>
# [5] \n
sapply(xml_contents(x), xml_type)
# [1] "text"    "element" "text"    "element" "text" 

获取元素的父结点

chrom <- html_element(tree, xpath = "//chrom")
p1 <- xml_parent(chrom)
xml_name(p1)
# [1] "position"
p2 <- xml_parents(chrom)
xml_name(p2)
# [1] "position" "gene" 

获取元素的所有兄弟结点和根结点

xml_siblings(chrom)
# {xml_nodeset (2)}
# [1] <start>7571720</start>
# [2] <end>7590868</end>
xml_name(xml_root(chrom))
# [1] "gene"

结点搜索

xml2 也提供了类似于 html_elements 的搜索函数,但是只支持 XPath 语法,根据不同的搜索方式和值的类型,可分为

函数功能函数功能
xml_find_all返回所有匹配结果xml_find_first返回第一个匹配结果
xml_find_num返回数值型匹配结果xml_find_chr返回字符串型匹配结果
xml_find_lgl返回逻辑型匹配结果xml_find_onexml_find_first ,已弃用

返回所有的属性值

xml_find_all(tree, xpath = "//@*")
# {xml_nodeset (4)}
# [1]  type="protein coding"
# [2]  id="7157"
# [3]  alias="P53"
# [4]  base="0"

返回 position 标签下的第一个子结点

xml_find_first(tree, xpath = "position/child::*")
# {xml_node}
# <chrom>

而对于另外三个和类型相关的函数,一般都要搭配 XPath 函数使用,例如

xml_find_num(tree, xpath = "count(position/child::*)")
# [1] 3
xml_find_chr(tree, xpath = "string(position/chrom/text())")
# [1] "chr17"
xml_find_lgl(tree, xpath = "boolean(//@id > 10)")
# [1] TRUE

rvest 补充

虽然前面介绍的这些函数已经提供了足够丰富的功能了,但是既然我们也使用到了 rvest 包,何不顺便介绍一下该包提供的一些功能函数呢?

该包提供的函数不是很多,除了上面介绍的 html_elementhtml_elements 之外,常用的函数还有

函数功能函数功能
html_attr获取指定属性的值html_attrs获取所有属性的值
html_children获取所有子结点html_name获取结点对应的标签
html_text获取结点的文本html_text2获取的文本保留了网页上看到的效果
html_table将表格内容转换为数据框minimal_html从字符串构造结点树

获取结点的属性

html_attr(tree, 'name')
# [1] NA
html_attr(tree, 'type')
# [1] "protein coding"
html_attrs(tree)
#             type               id 
# "protein coding"           "7157"

获取所有子结点

html_children(pos)
# {xml_nodeset (3)}
# [1] <chrom>chr17</chrom>
# [2] <start>7571720</start>
# [3] <end>7590868</end>

获取结点中的文本,html_text 会原样输出而 html_text2 和网页上看到的效果一致

x <- minimal_html(
  "<p> This is a 
  paragraph <br> This is a 
  new line"
)
html_text(x)
# [1] " This is a \n  paragraph  This is a \n  new line"
html_text2(x)
# [1] "This is a paragraph\nThis is a new line"

将网页中的表格转换为数据框类型

t <- '
<table border="1">
<thead><tr><th>Symbol</th><th>ID</th></tr></thead>
<tbody>
  <tr><td>TP53</td><td>7157</td></tr>
  <tr><td>KRAS</td><td>3845</td></tr>
</tbody></table>'
x <- minimal_html(t)
html_table(html_element(x, "table"))
# # A tibble: 2 × 2
#   Symbol    ID
#   <chr>  <int>
# 1 TP53    7157
# 2 KRAS    3845

lxml

lxml 是两个 C 库:libxml2libxsltPython 封装,其相较于 R 中的 xml2 包具有更多的功能,并且兼顾文件的解析速度及操作的灵活性,主要使用的是 lxml.etree 模块进行解析。。

属性操作

一般 XML 树结构都是使用 Element 函数来创建对应的结点对象,该对象相当于一个容器,能够不断为其添加不同的子结点,子结点也可以是另一个结点。我们可以创建一个结点,可以在创建的同时为 attrib 参数指定一个字典,字典的键为属性名,值为属性值,并返回一个结点对象

genes = etree.Element(_tag='genes', attrib={'type': 'protein coding'})
type(genes)
# lxml.etree._Element
etree.tostring(genes)  # 返回结点的字符串表示
# b'<genes type="protein coding"/>'
genes.tag
# 'genes'

或者以关键字参数的方式来指定

genes = etree.Element(_tag='genes', attrib={'type': 'protein coding'}, 
                      ref='hg19', id='0')
etree.tostring(genes)
# b'<genes ref="hg19" id="0" type="protein coding"/>'

标签内的属性是无序的键-值对,因此通常可将结点对象作为字典来使用

genes.items()
# [('ref', 'hg19'), ('id', '0'), ('type', 'protein coding')]
genes.keys()
# ['ref', 'id', 'type']
genes.get('type')
# 'protein coding'
genes.set('type', 'Protein-Coding')
etree.tostring(genes)
# b'<genes ref="hg19" id="0" type="Protein-Coding"/>'

或者使用计算属性 attrib 来访问标签的属性,其返回的是结点自身的属性,在修改其值时,也会影响结点对应的属性值

attr = genes.attrib
attr
# {'ref': 'hg19', 'id': '0', 'type': 'Protein-Coding'}
attr['ref'] = 'hg38'
genes.get('ref')
# 'hg38'

一般来说同一个标签内的文本存储在 text 属性中,例如

genes.text = "genes list"
etree.tostring(genes)
# b'<genes ref="hg38" id="0" type="Protein-Coding">genes list</genes>'

而文本也可以放在不同标签之间,该文本会存储在 tail 属性中,例如在 HTML 中,使用 <br/> 标签在文本之间添加换行符

html = etree.Element("html")
body = etree.SubElement(html, "body")  # 添加子结点
body.text = "A line"
br = etree.SubElement(body, "br")
br.tail = "A new line"
etree.tostring(html)
# b'<html><body>A line<br/>A new line</body></html>'

结点操作

对于单个结点来说,它就是一个根结点,不存在其父结点或子结点,我们可以使用不同的函数为其添加各种关系,如添加或删除子结点、兄弟结点等。我们可以使用 SubElement 函数为其添加子结点

g1 = etree.SubElement(genes, _tag='gene', name='TP53', id='7157')
g2 = etree.SubElement(genes, _tag='gene', name='KRAS', id='3845')
len(genes)  # 计算子结点的数量
# 2
type(g1)
# lxml.etree._Element

调用该函数会返回一个结点对象,我们可以用这种方式不断为结点树添加更多的结点。除了这种方式之外,还可以使用其他函数来操作结点,主要包括

函数功能函数功能
append在末尾添加一个子结点insert在指定位置插入子结点
replace替换子结点remove根据结点地址删除一个子结点
clear删除所有子结点及其属性值index获取子结点的索引位置
addprevious在该结点之前添加一个兄弟结点addnext在该结点之后添加一个兄弟结点

从这些函数名称可以看出,操作结点看起来像是在操作一个列表,即将所有的子结点归结为一个列表,可以不断为其添加新的结点,并给每个节点添加了顺序索引,方便访问。例如,结点的添加和删除

genes.remove(g2)
len(genes)
# 1
genes.append(g2)
etree.tostring(genes[0])
# b'<gene name="TP53" id="7157"/>'
genes.insert(1, etree.Element(_tag='gene', name='BRAF', id='673'))
len(genes)
# 3
etree.tostring(genes[1])
# b'<gene name="BRAF" id="673"/>'

替换子结点

old = etree.SubElement(genes, _tag='gene', name='ROS', id='6089')
new = etree.Element(_tag='gene', name='ALK', id='238')
genes.replace(old, new)
etree.tostring(genes[-1])
# b'<gene name="ALK" id="238"/>'

在结点的前后添加兄弟节点

genes.index(g2)
# 2
g3 = etree.Element('gene', name='EGFR', id='1956', 
                   type='epidermal growth factor receptor')
g4 = etree.Element('gene', name='PIK3CA', id='5290', type='subunit')
g2.addnext(g4)      # 添加在后一个
g2.addprevious(g3)  # 添加在前一个
len(genes)
# 6

最后,打印结点内容,可以使用 pretty_print 美化输出,并使用 decodebytes 字符串转换为 str 类型

print(etree.tostring(genes, pretty_print=True).decode())
# <genes ref="hg19" id="0" type="protein coding">
#   <gene name="TP53" id="7157"/>
#   <gene name="BRAF" id="673"/>
#   <gene name="EGFR" id="1956" type="epidermal growth factor receptor"/>
#   <gene name="KRAS" id="3845"/>
#   <gene name="PIK3CA" id="5290" type="subunit"/>
#   <gene name="ALK" id="238"/>
# </genes>

迭代搜索

该包也提供了类似于 XPath 语法的路径搜索方法,可以使用这些方法来搜索结点

方法功能方法功能
iterfind遍历所有匹配路径表达式的结点findall返回所有匹配的结点列表
find返回第一个匹配的结点findtext返回第一个匹配的 text 属性

获取所有的子结点,可以传入标签名称或者使用路径表达式返回一个列表

for g in genes.iterfind(path='gene'):
    print(g.get('name'), end=' ')
# TP53 BRAF EGFR KRAS PIK3CA ALK
genes.findall('./gene[@type]')
# [<Element gene at 0x7fdd32853cc0>, <Element gene at 0x7fdd3251e800>]

iter 则只能传入标签名称

next(genes.iter('gene')).tag
# 'gene'
next(genes.iter('gene')).attrib
# {'name': 'TP53', 'id': '7157'}

搜索第一个匹配的结点

genes.find('./gene[@type]').get('name')
# 'EGFR'
genes.findtext('./gene[@type]')
# ''
g3.text = 'receptor'
genes.findtext('./gene[@type]')
# 'receptor'

对于某一个结点来说,我们可以使用一些方法来获取与其相关的其他结点的信息

方法功能方法功能
getchildren该结点的所有直接子结点iterchildren该结点的所有子结点迭代器
getnext该结点的后一个兄弟结点getprevious该结点的前一个兄弟结点
getparent该结点的父结点getroottree返回整个结点树
iterancestors该结点的祖先结点迭代器getiterator该结点的深度优先遍历迭代器
iterdescendants该结点的子孙结点迭代器itersiblings该结点之前或之后的所有结点迭代器

结点的 XPath 路径表示,可以先获取整个结点树,然后使用 getpath 来获取对应结点的路径表示

genes.getroottree().getpath(g3)
# '/genes/gene[3]'

遍历所有子结点

for e in genes.iterchildren():
    print(e.get('name'), end=' ')
# TP53 BRAF EGFR KRAS PIK3CA ALK
for e in genes.getchildren():
    print(e.get('id'), end=' ')
# 7157 673 1956 3845 5290 238

结点的父结点

g1.getparent().tag
# 'genes'

获取相邻的两个兄弟结点

g2.getnext().attrib
# {'name': 'PIK3CA', 'id': '5290', 'type': 'subunit'}
g2.getprevious().attrib
# {'name': 'EGFR', 'id': '1956', 'type': 'epidermal growth factor receptor'}

对结点之前或之后的兄弟结点进行遍历,主要通过 preceding 参数来控制方向

for g in g3.itersiblings():
    print(g.attrib)
# {'name': 'KRAS', 'id': '3845'}
# {'name': 'PIK3CA', 'id': '5290', 'type': 'subunit'}
# {'name': 'ALK', 'id': '238'}
for g in g3.itersiblings(preceding=True):
    print(g.attrib)
# {'name': 'BRAF', 'id': '673'}
# {'name': 'TP53', 'id': '7157'}

这些方法完全可以替代前面介绍的两种选择器语法,以调用对象方法的方式来获取对应的结点。在编写代码时,完全可以结合三者优势之处混合使用,以最快最简便的方式来实现数据的提取。

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

名本无名

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

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

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

打赏作者

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

抵扣说明:

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

余额充值