px普贤教程PuXian
普贤教程 › 生态包

解析与提取

没有正则的时候怎么解析?这五个包给的是「组合子」「递归下降」两条路:parser 是通用组合子,pxml 是配置语言,htmlparse 是容错 HTML,mailparse 是 RFC 5322 邮件,readstat 是可读性统计。它们都不依赖外部服务,全部纯函数。

parser:组合子

一个 parser 就是一个函数 (input, pos) → Ok([新位置, 值]) | Err(消息)。先用内建原语拼出原子的,再用 p_seq / p_alt / p_many 组合。

px
import parser

print(str(p_many1(p_digit)("123abc", 0)))   # Ok([3, [1, 2, 3]])
print(str(p_ident("name_1 x", 0)))          # Ok([6, name_1])
print(str(p_alt([p_int, p_ident])("x1", 0)))# Ok([2, x1])   int 失败自动换 ident
print(str(p_many(p_digit)("abc", 0)))       # Ok([0, []])  零次也算成功
print(str(p_opt(p_int)("abc", 0)))          # Ok([0, null])
text
Ok([3, [1, 2, 3]])
Ok([6, name_1])
Ok([2, x1])
Ok([0, []])
Ok([0, null])
原语不吃空白
p_int(" -42,", 0) 直接 Err(位置 0: 期望整数) —— 组合子不做隐式跳过。要跳空白必须显式串上 p_ws:
px
var p = p_seq([p_ws, p_int])
print(str(p("  -42,", 0)))     # Ok([5, [null, -42]])
text
Ok([5, [null, -42]])

注意 p_seq 的返回值是各子 parser 结果组成的 list,位置在第一个元素;p_ws 不产出值所以是 null。

常用原语一览:p_char(c) p_str(s) p_any p_digit p_letter p_alnum p_space p_ws p_ident p_int p_parse_int;组合子 p_seq p_alt p_many p_many1 p_opt p_map p_between p_sep_by p_sep_by1 p_take_while p_end。

没有匿名函数
PuXian 不支持 def (x): ... 字面量。要给 p_map 传函数、给 td_run 传断言,都必须先 def name(x): 声明成具名函数再传名字。

pxml:配置语言的解析器

px
import std.pxml

def main():
    var doc = pxml_parse("server {\n  port = 8080\n  tls = true\n  host = \"cx.dzty.club\"\n}")
    print(type(doc))          # result
    print(str(doc.is_ok()))
    print(str(doc.unwrap()))
text
result
true
{server: {host: cx.dzty.club, port: 8080, tls: true}}
返的是 Result,不是字典
成功 Ok(配置树)、失败 Err("line N: …")。判断用 doc.is_ok(),取值用 doc.unwrap(),函数里也可以写 let cfg = pxml_parse(text)?。不要索引:doc["ok"] 会报 R1002: 此类型不支持索引: result。

htmlparse:容错解析

px
import htmlparse

var html = "<div class=\"x\"><a href=\"/a\">一</a><a href=\"/b\">二</a><p>文本 &amp; 实体</p></div>"
var el = hp_parse(html).ok()

print(str(hp_extract_links(el)))            # [{href: /a, text: 一}, {href: /b, text: 二}]
print(hp_text(el))                          # 一二文本 &amp; 实体
print(hp_decode_entities(hp_text(el)))      # 一二文本 & 实体
print(hp_attr(hp_find_first(el, "a"), "href"))   # /a
text
[{href: /a, text: 一}, {href: /b, text: 二}]
一二文本 &amp; 实体
一二文本 & 实体
/a
两个「看着像 bug 其实是契约」的点
① hp_text 不解码实体,& 原样返回,要自己再过 hp_decode_entities; ② 多个子节点的文本直接拼接、不加分隔符(一二文本)—— 抓正文时如果想保留词边界,得自己遍历子节点加空格。

常用:hp_find / hp_find_rec(按标签找)、hp_find_by_attr(按属性找)、hp_inner_html / hp_outer_html、hp_extract_images、hp_table_parse、hp_strip_tags(一次性去标签,返回 string)。

mailparse:邮件头解码

px
import mailparse

var raw = "From: =?UTF-8?B?5pmo5pum?= <a@b.com>\nSubject: =?UTF-8?B?5rWL6K+V?=\nContent-Type: text/plain; charset=utf-8\n\nhello body"
var m = mail_parse(raw)
print(str(m))
print(mail_decode_rfc2047("=?UTF-8?B?5pmo5pum?="))   # 晨曦
text
Ok({body: hello body, date: null, from: 晨曦 <a@b.com>, headers: {Content-Type: text/plain; charset=utf-8, From: =?UTF-8?B?5pmo5pum?= <a@b.com>, Subject: =?UTF-8?B?5rWL6K+V?=}, subject: 测试, to: null})
晨曦

from / subject 已解码,headers 保留原始(未解码)字符串——排查乱码时两处对照最有用。date 与 to 缺席时是 null,不是空串。支持 B(base64)与 Q(quoted-printable)两种编码词。

readstat:可读性统计

px
import readstat

var s = "The quick brown fox jumps over the lazy dog. PuXian is a static language."
print(rs_sentences(s))              # 2
print(str(rs_counts(s)))            # {sentences: 2, syllables: 20, words: 14}
print(rs_flesch(s))                 # Ok(78.87285714285717)
print(rs_grade(s))                  # Ok(小学)
text
2
{sentences: 2, syllables: 20, words: 14}
Ok(78.87285714285717)
Ok(小学)
它是给英文写的
rs_words 按空白切词、音节按元音组估算,中文整句会被当成一个「词」——中文文本的可读性分数没有意义,别拿它做中文质量指标。