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

正则表达式

PuXian 不装包就能用正则:七个内建函数。它们的匹配语义与传统语言有四处不同,这四处是本页的重点——本页每一条都是在这台机器上跑出来的。

函数清单

函数返回值语义
regex_valid(pat)bool模式是否合法
regex_match(pat, s)bool整串匹配(等价于两边都加了 ^…$)
regex_find(pat, s)str 或 null第一个命中片段,没命中返回 null
regex_find_all(pat, s)list全部命中,没命中返回 []
regex_search(pat, s)dict{start, end, match, groups},位置是 0 基、end 不含
regex_replace(pat, s, repl)str替换;支持 $1 反向引用
regex_split(pat, s)list按模式切分,保留空段

四处与别处不同

一、regex_match 是整串匹配,不是「包含」

px
print(regex_match("[0-9]+", "12345"))     # true
print(regex_match("[0-9]+", "a12345b"))   # false —— 有数字,但不匹配「整串」

要找「包含数字」,用 regex_find;想验证一个字段「只能是数字」,regex_match 正好,连 ^$ 都不用写。

二、regex_replace 的第三个参数是替换串,不是被替换串

px
print(regex_replace("[0-9]+", "#", "a12345b"))       # "#"       —— 错误顺序
print(regex_replace("[0-9]+", "a12345b", "#"))       # "a#b"     —— 正确:模式, 原文, 替换

参数顺序是 (模式, 原文, 替换)。写反了不会报错,只是得到一个你没想到的结果——这类静默错误最难查。

三、非贪婪量词 *? 静默失效

px
print(regex_find("<.*>",  "<a><b>"))    # <a><b>   贪婪,命中到最后一个 >
print(regex_find("<.*?>", "<a><b>"))    # null     非贪婪:不报错、不命中
这条最危险*? 不抛错,直接当作「匹配不到」。要取最短片段,用否定字符类:<[^>]*> —— 实测 regex_find_all("<[^>]*>", "<a><b>") 得到 [<a>, <b>]。

四、前瞻 / 后顾会抛运行时错误

px
regex_find("foo(?=bar)", "foobar")
# 运行时错误 [i_call_builtin 行429]: regex: 不支持的组语法 `(?…)`
# (支持 `(?:…)` `(?P<n>…)` `(?i)` `(?s)` `(?m)`)

支持的非捕获组是 (?:…),命名组 (?P<name>…),内联标志 (?i) (?s) (?m)。前瞻 / 后顾 / \u 转义一律报错,而且是运行时错误——PuXian 没有 try/catch,在 Web 接口里就是 500。

用户输入当模式必须先校验先 regex_valid(p),为 false 就返回 400。否则用户随便传个 (?=x),进程直接抛错。

实测速查

表达式输入输出
regex_find("\d{3}", s)a12345b123
regex_find("\w+", s)  hello_1  hello_1
regex_find("\bcat\b", s)a cat herecat
regex_search("(\d+)-(\d+)", s)x12-34y{start:1, end:6, match:"12-34", groups:["12","34",null…]}
regex_replace("(\d+)-(\d+)", s, "$2-$1")12-3434-12
regex_split("[,;]", s)a,b;c[a, b, c]
regex_find_all("(?m)^\w+", s)aa\nbb[aa, bb]
regex_match("(?s)a.*b", s)a\nbtrue
regex_find("a{2,3}", s)aaaaaaa
regex_find("[一-龥]+", s)abc中文def中文

1. 表单校验:整串匹配最省事

px
def is_email(s):
    return regex_match("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+[.][A-Za-z]{2,}", s)

def is_cn_mobile(s):
    return regex_match("1[3-9][0-9]{9}", s)

print(is_email("a@b.com"))     # true
print(is_cn_mobile("13800138000"))  # true
print(is_cn_mobile("12800138000"))  # false

注意 [.] 而不是 \.:反斜杠转义的健壮性不如字符类,能不用就不用。

2. 从日志里抽字段

px
def main():
    var line = "2026-11-05 17:43:02 [ERROR] user=1001 ip=10.1.2.3 cost=253ms"
    var t = regex_find("[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}", line)
    var ip = regex_find("ip=[0-9.]+", line)
    print(t)                              # 2026-11-05 17:43:02
    print(ip)                             # ip=10.1.2.3
    print(regex_replace("ip=", ip, ""))   # 10.1.2.3
(?i) 是「后续不区分大小写」的开关它写在模式开头表示整串忽略大小写,单独出现时匹配空串。想「只对某一段忽略大小写」没法用 (?i:…) 圈定——那种分组写法在这里会报错。

3. 脱敏:反向引用一次替换

px
def mask_phone(s):
    return regex_replace("(1[3-9][0-9])[0-9]{4}([0-9]{4})", s, "$1****$2")

print(mask_phone("联系电话 13812345678"))
# 联系电话 138****5678

4. 切分不定分隔符

px
var parts = regex_split("[,;|]", "a,b;c|d")
print(parts)     # [a, b, c, d]
print(len(regex_split(",", "a,,b")))   # 3 —— 空段保留,与 Python re.split 一致

性能与边界

  • 正则是内建,不需要装包、不需要 import,解释轨与编译轨行为一致。
  • 不要在「每一个字符」上跑正则:先 contains() 粗筛,再进正则。
  • regex_find_all 没命中返回 [],不是 null;regex_find 返回 null。两者判空写法不同,写混了会得到「永远为真」的判断。

和 CSV / JSON 的分工

结构化数据别用正则硬啃:CSV 用 csv_parse,JSON 用 json_parse_opt,日志这种半结构化才轮到正则。反过来,用正则去解析嵌套括号/引号的文本,一定会写错。