普贤教程 › 生态包
正则表达式
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) | a12345b | 123 |
regex_find("\w+", s) | hello_1 | hello_1 |
regex_find("\bcat\b", s) | a cat here | cat |
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-34 | 34-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\nb | true |
regex_find("a{2,3}", s) | aaaa | aaa |
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,日志这种半结构化才轮到正则。反过来,用正则去解析嵌套括号/引号的文本,一定会写错。