前一段时间在写一个 RSS 阅读器的微信小程序,由于 RSS 文件内容是基于 xml 的,然而翻了好多开源的 xml 解析器,发现都相互依赖了很多东西,而且体积都很大,不是很适合我在微信小程序中使用。
在寻找的过程中突然记起了以前看过 developit 写过的一个只有 1kb 解析 markdown 的库 -- snarkdown ,其原理是基于一个正则表达式,然后循环去匹配,于是我琢磨能否基于正则写一个解析 xml 的工具,目标是足够小,解析足够快,能够将 xml 解析为json对象,因此在这里简单说一下实现的原理。
标签元素的分类
一个很普通的 xml 是这个样子的:
<?xml version="1.0" encoding="UTF-8"?>
<sample>
<chartest desc="Test for CHARs">Character data here!</chartest>
<cdatatest desc="Test for CDATA" misc="true"><![CDATA[CDATA here!]]></cdatatest>
<cdatawhitespacetest desc="Test for CDATA with whitespace" misc="true"><![CDATA[ ]]></cdatawhitespacetest>
<nochartest desc="No data" misc="false" />
<nochildrentest desc="No data" misc="false" />
<ordertest>
<one>1</one>
<two>2</two>
<three>3</three>
<one>4</one>
<two>5</two>
<three>6</three>
</ordertest>
</sample>
经过分析发现,一般的xml文件包含下列四种情况:
xml的声明,以 <? 开头, ?> 结尾,中间包含一些属性值: <?xml version="1.0" encoding="UTF-8"?>
自关闭的元素,以 /> 结尾,中间包含一些属性值: <nochartest desc="No data" misc="false" />
带有关闭元素,如 <a>...</a> ,且中间不包含与元素重名的子元素: <chartest desc="Test for CHARs">Character data here!</chartest>
带有关闭元素,且中间包含与元素重名的子元素:
<one>
<one>1</one>
</one>
声明元素与自关闭元素的匹配处理
对于上面的前两种,用一个正则可以很容易的进行匹配:
/<\??([a-z][\w\.\-]*)(\s[^>]*?)?[\/\?]>/img
正则表达式的最后标记为 img , i 代表忽略大小写, m 代表跨多行, g 代表循环匹配。
匹配的过程如下:
<\?? 允许元素以 <? 或者是 < 开头
([a-z][\w\.\-]*) 用来匹配元素的名称,以 a-z 其中的一个字母开头,然后跟随着任意个字母、数字、英文句号或者连字符。
(\s[^>]*?)? 接下来可以是空格与非 > 字符的任意字符,如果有的话则作为元素的属性值
[\/\?]> 可以是 /> 或者是 ?> 结尾
不包含与元素重名的子元素的处理
符合此条件的元素可以包含的子元素有两种,一种是纯字符串,另外一种是已其他字符命名的元素。
先说第二种子元素的形式,处理子元素的嵌套是这个正则的重点。
以其他字符命名的元素如果是自关闭元素那么在进行上一步循环匹配的时候就已经处理过了,如果是包含字符串的元素和非同名元素,那么在这一步也会进行循环处理,另外如果包含同名元素,那么在下一步还是会进行处理。
对于这一类的元素使用如下的正则进行处理:
/<([a-z][\w\.\-]*)(\s.*?)?>((?:(?!<\1).)*)<\/\1>/img
img的作用在上面已经说过了,不再赘述。
匹配的过程如下:
< :允许元素以 < 开头
([a-z][\w\.\-]*) :用来匹配元素的名称,以 a-z 其中的一个字母开头,然后跟随着任意个字母、数字、英文句号或者连字符。反向引用的索引为1。
(\s.*?)? :用来匹配属性值
((?:(?!<\1).)*) :包含任意多个不以 < 加上第二步匹配到的元素名称开头的内容,这里使用了反向引用来进行匹配
<\/\1> :以 </ 加上元素的名称作为结尾
包含与元素重名子元素的元素的处理
包含与元素重名子元素有三种情况:
元素的子元素下面除了重名的元素还包含其他内容,如:
<one>
123
<one>456</one>
</one>
重名子元素为自关闭标签: <one attr="123" />
重名子元素带有关闭标签: <one attr="123" ></one> 此时已不需要考虑子元素是否还含有子元素,因为是循环处理,在上面的两种情况中已经进行了处理。
因此使用如下正则进行处理:
/<([\w\-]+)(\s+.*?)?>((?:<\1.*?<\/\1>|<\1[^>]*?\/>|(?:(?!<\1).)*)*)<\/\1>/img
匹配的过程如下:
<([\w\-]+)(\s+.*?)?> 以字母、数字或者连字符组成的内容作为元素名。反向引用的索引为 1。
((?:<\1.:mdps:&:split:mdps:ref:prefix:0:mdps:&:split:?\/>|(?:(?!<\1).):mdps:&:split:mdps:ref:prefix:1:mdps:&:split:) 我们可以把它分为三部分:
<\1.*?<\/\1> 带有关闭标签
<\1[^>]*?\/> 自关闭标签
(?:(?!<\1).)* 包含非标签名的其他内容
<\/\1> 以 </ 加上元素的名称作为结尾
下面是匹配一个rss源内容和结果:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title><![CDATA[知乎热榜]]></title>
<link>https://www.zhihu.com/billboard</link>
<description><![CDATA[知乎热榜]]></description>
<generator>FeedIamGy</generator>
<webMaster>feed.iam.gy</webMaster>
<language>zh-cn</language>
<lastBuildDate>Thu Sep 13 2018 10:42:34 GMT+0800 (CST)</lastBuildDate>
<ttl>3000</ttl>
</channel>
</rss>
结果如下:
{
"xml": {
"_attr": {
"version": "1.0",
"encoding": "UTF-8"
}
},
"rss": {
"channel": {
"title": {
"_value": "知乎热榜",
"_attr": {}
},
"link": {
"_value": "https://www.zhihu.com/billboard",
"_attr": {}
},
"description": {
"_value": "知乎热榜",
"_attr": {}
},
"generator": {
"_value": "FeedIamGy",
"_attr": {}
},
"webMaster": {
"_value": "feed.iam.gy",
"_attr": {}
},
"language": {
"_value": "zh-cn",
"_attr": {}
},
"lastBuildDate": {
"_value": "Thu Sep 13 2018 10:42:34 GMT+0800 (CST)",
"_attr": {}
},
"ttl": {
"_value": "3000",
"_attr": {}
},
"_attr": {}
},
"_attr": {
"version": "2.0"
}
}
}