解析XML格式的网络接口返回数据时,如果还靠正则表达式或者字符串截取来提取字段,代码会很快变得难以维护。R语言中的xml2包把XML文档解析成可遍历的节点树,配合XPath表达式可以按路径、条件、属性灵活定位内容。本文用一个RSS订阅源作为样本,演示从在线读取XML到提取完整数据框的完整流程。

一、安装与读取XML网络数据
xml2包是R语言处理XML和HTML的核心工具,底层基于libxml2库。安装时只需要执行install.packages函数,加载后即可使用read_xml读取本地文件或网络地址。读取网络数据时,read_xml会直接向URL发起请求并解析响应内容,不需要额外借助httr或curl,但如果需要设置请求头或处理编码,可以先用httr获取文本再传给read_xml。
下面这段代码演示了最基础的安装和读取过程。示例地址使用了测试域名,实际使用时替换成目标XML接口即可。read_xml返回一个xml_document对象,打印它会显示文档的根节点结构。
install.packages("xml2")
library(xml2)
url = "https://ipipp.com/feed.xml"
doc = read_xml(url)
doc
如果XML内容已经保存在本地文件,read_xml同样支持路径参数。对于HTML页面或XML中带有HTML片段的情况,xml2还提供了read_html函数。读取完成后,可以通过xml_name、xml_children等函数查看节点名称和子节点,这些函数在后文的调试部分会再次用到。
二、XPath语法核心:定位节点、属性与文本
XPath不是R特有的语法,而是一种在XML文档中查找节点的通用语言。xml2包中主要使用xml_find_all和xml_find_first两个函数执行XPath查询。前者返回所有匹配节点组成的节点集,后者只返回第一个匹配节点。拿到节点后,再用xml_text提取文本内容,用xml_attr提取属性值。
常见的XPath表达式中,//表示在文档任意层级查找,/表示从当前节点向下查找,.表示当前节点,..表示父节点。如果要限定节点名称,直接写节点名即可。比如//item表示所有item节点,//item/title表示所有item节点下的title子节点。谓词用方括号表示条件,比如//item[1]表示第一个item,//item[position() < 4]表示前三个item。在R代码中,XPath表达式作为字符串传入,需要注意引号转义。
属性选择使用@符号。例如//item[@type='article']会选择type属性值为article的item节点。如果只需要提取属性值,可以使用xml_attr。下面的代码展示了如何提取所有item节点的标题文本和link属性。
items = xml_find_all(doc, "//item") titles = xml_text(xml_find_all(doc, "//item/title")) links = xml_text(xml_find_all(doc, "//item/link")) # 如果link是属性而不是子节点 hrefs = xml_attr(items, "href")
实际使用中还要注意相对路径和绝对路径的区别。xml_find_all(doc, "//title")会在整个文档中查找所有title节点,而xml_find_all(items, "./title")只在items节点集的每个item内部查找title子节点。后者可以避免把channel下的title也误提取出来。
三、实战:解析RSS并提取文章列表
RSS是XML格式的典型应用,结构通常包含channel和多个item节点。假设一个RSS源的内容如下所示,channel下有站点标题和多个item,每个item又包含标题、链接、发布时间和分类等字段。
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>技术博客</title>
<item>
<title>第一篇文章</title>
<link>https://ipipp.com/post1</link>
<pubDate>2025-01-15 10:00:00</pubDate>
<category>R语言</category>
</item>
<item>
<title>第二篇文章</title>
<link>https://ipipp.com/post2</link>
<pubDate>2025-01-16 09:30:00</pubDate>
<category>数据分析</category>
</item>
</channel>
</rss>
针对上面这个结构,可以先抓取所有item节点,再对每个item节点应用相对XPath提取子节点文本。这种先定位集合再逐项提取的方式比反复从根节点查询更清晰,也更容易处理不同节点之间的对应关系。xml_text会返回字符向量,长度与匹配节点数一致。
items = xml_find_all(doc, "//item") title = xml_text(xml_find_all(items, "./title")) link = xml_text(xml_find_all(items, "./link")) pubDate = xml_text(xml_find_all(items, "./pubDate")) category = xml_text(xml_find_all(items, "./category")) df = data.frame( title = title, link = link, pubDate = pubDate, category = category, stringsAsFactors = FALSE ) print(df)
如果某个item缺少某个子节点,xml_find_all会返回空节点集,xml_text则返回零长度结果,这会导致data.frame各列长度不一致而报错。更稳妥的做法是使用xml_find_first逐个提取,xml_find_first在没有匹配时返回一个空节点,xml_text在空节点上返回NA,从而保证列长度与item数量一致。也可以写一个辅助函数,对每个item循环处理。
下面是用xml_find_first改写的版本,适合字段可能缺失的XML数据。同时演示了如何把发布时间字符串转换为真正的日期时间类型,方便后续排序和过滤。
get_node_text = function(node, xpath) {
result = xml_find_first(node, xpath)
xml_text(result)
}
items = xml_find_all(doc, "//item")
title = sapply(items, get_node_text, xpath = "./title")
link = sapply(items, get_node_text, xpath = "./link")
pubDate = sapply(items, get_node_text, xpath = "./pubDate")
df2 = data.frame(
title = title,
link = link,
pubDate = as.POSIXct(pubDate, format = "%Y-%m-%d %H:%M:%S"),
stringsAsFactors = FALSE
)
print(df2)
四、进阶:处理命名空间与复杂结构
很多XML接口会使用命名空间,例如Atom订阅源的根节点通常是feed,带有xmlns属性。如果直接用//item这样的XPath,可能无法匹配到带命名空间的节点。xml2提供了xml_ns函数查看文档中的命名空间映射,也允许在xml_find_all中通过ns参数声明前缀。
对于命名空间不复杂的情况,最简单的办法是使用local-name()函数绕过命名空间前缀。例如//*[local-name()='item']会选择所有本地名称为item的节点,不管它属于哪个命名空间。这种方式虽然损失了一些精确性,但在快速提取数据时非常有效。
# 查看命名空间 xml_ns(doc) # 使用local-name绕过命名空间 items = xml_find_all(doc, "//*[local-name()='item']") # 使用命名空间前缀查询 ns = xml_ns(doc) items2 = xml_find_all(doc, "//d1:item", ns = c(d1 = "http://www.w3.org/2005/Atom"))
调试XML结构时,xml_structure函数可以打印出嵌套层级,xml_path能返回某个节点的完整XPath路径。当不确定数据在哪一层时,先查看结构再写XPath可以少走很多弯路。下面的代码展示了如何查看根节点的子节点名称以及某个节点的路径。
xml_name(doc) xml_children(doc) xml_path(xml_find_first(doc, "//item")) xml_structure(doc)
把XML数据转换成数据框之后,就可以用R语言擅长的向量化操作继续分析。比如统计每个分类下的文章数量,或者筛选出最近三天发布的文章。xml2包本身只负责解析和提取,后续数据清洗和统计可以交给dplyr、data.table等工具完成。
最后提醒一点:网络XML数据可能因为编码、网络波动或结构变更导致解析失败。建议在实际项目中加入tryCatch错误处理,并定期检查xml_ns和xml_structure的输出是否与预期一致。掌握read_xml、xml_find_all、xml_text、xml_attr这四个函数,配合XPath语法,已经可以覆盖绝大多数XML数据提取需求。

