网络爬虫 —— xml2 和 lxml

网络爬虫 ------ xml2 和 lxml

文章目录

xml2

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

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

r 复制代码
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 表达式

获取结点的属性

r 复制代码
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

获取结点对应的标签

r 复制代码
xml_name(tree)
# [1] "gene"

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

r 复制代码
xml_text(tree)
# [1] "TP53chr1775717207590868"

获取结点的结构化信息

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

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

r 复制代码
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 获取根结点

获取指定位置的子结点

r 复制代码
xml_child(pos, 2)
# {xml_node}
# <start>
xml_child(pos, 3)
# {xml_node}
# <end>
xml_length(pos)
# [1] 3

获取所有元素子结点

r 复制代码
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 会获取所有子结点,包含元素子结点和字符串结点

r 复制代码
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" 

获取元素的父结点

r 复制代码
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" 

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

r 复制代码
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_one xml_find_first ,已弃用

返回所有的属性值

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

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

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

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

r 复制代码
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 从字符串构造结点树

获取结点的属性

r 复制代码
html_attr(tree, 'name')
# [1] NA
html_attr(tree, 'type')
# [1] "protein coding"
html_attrs(tree)
#             type               id 
# "protein coding"           "7157"

获取所有子结点

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

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

r 复制代码
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"

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

r 复制代码
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 参数指定一个字典,字典的键为属性名,值为属性值,并返回一个结点对象

python 复制代码
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'

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

python 复制代码
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"/>'

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

python 复制代码
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 来访问标签的属性,其返回的是结点自身的属性,在修改其值时,也会影响结点对应的属性值

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

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

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

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

python 复制代码
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 函数为其添加子结点

python 复制代码
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 在该结点之后添加一个兄弟结点

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

python 复制代码
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"/>'

替换子结点

python 复制代码
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"/>'

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

python 复制代码
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 类型

python 复制代码
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 属性

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

python 复制代码
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 则只能传入标签名称

python 复制代码
next(genes.iter('gene')).tag
# 'gene'
next(genes.iter('gene')).attrib
# {'name': 'TP53', 'id': '7157'}

搜索第一个匹配的结点

python 复制代码
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 来获取对应结点的路径表示

python 复制代码
genes.getroottree().getpath(g3)
# '/genes/gene[3]'

遍历所有子结点

python 复制代码
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

结点的父结点

python 复制代码
g1.getparent().tag
# 'genes'

获取相邻的两个兄弟结点

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

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

python 复制代码
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'}

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

相关推荐
黑色的糖果2 分钟前
echarts横向立体3D柱状图
前端·javascript·echarts
茶卡盐佑星_8 分钟前
vue3.0所采用的composition Api与vue2.x使用的Option Api有什么区别
前端·javascript·vue.js
乐安lan12 分钟前
01前端导入
前端
lauo15 分钟前
【WEB前端2024】3D智体编程:乔布斯3D纪念馆-第55课-芝麻开门(语音 识别 控制3D纪念馆开门 和 关门)
前端·javascript·人工智能·3d·机器人·开源·语音识别
2401_8576226616 分钟前
探索 WebKit 的动感世界:设备方向和运动支持全解析
前端·webkit
开源博客18 分钟前
Vue3 如何接入 i18n 实现国际化多语言
前端·vue·i18n·vite
Ann_R18 分钟前
el-date-picker 开始时间选定后,结束时间不可选择开始时间之前的日期
前端·vue.js·elementui
濮水大叔26 分钟前
2024已过半,还没试过在vue3中使用ioc容器吗?
前端·typescript·vue3·ioc·tsx
幸运小男神32 分钟前
elementUI中table组件固定列时会渲染两次模板内容问题
前端·javascript·elementui
煸橙干儿~~36 分钟前
el-table 树状表格查询符合条件的数据
javascript·vue.js·elementui