网络爬虫 ------ xml2 和 lxml
文章目录
- [网络爬虫 ------ xml2 和 lxml](#网络爬虫 —— xml2 和 lxml)
xml2
xml2
包是对 libxml2
(C
编写的库)的封装,可以很方法快速地在 R
中解析 XML
和 HTML
文件,前面我们也使用了两个读取函数和两个解析函数。
read_xml
和 read_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_element
和 html_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
库:libxml2
和 libxslt
的 Python
封装,其相较于 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
美化输出,并使用 decode
将 bytes
字符串转换为 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'}
这些方法完全可以替代前面介绍的两种选择器语法,以调用对象方法的方式来获取对应的结点。在编写代码时,完全可以结合三者优势之处混合使用,以最快最简便的方式来实现数据的提取。