Python技术迷

太好用了,这个更人性化的正则库 -- Humre

我先说个场景哈,特别日常那种。

前几天晚上九点多,我在公司楼下等外卖,手机上远程连着服务器,改一个小脚本,本来就想写个简单的日志过滤:把某个接口的慢请求抓出来。结果一打开那段老代码,第一眼看到这么个玩意儿:

import re

pattern = re.compile(
r'^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+'
r'(INFO|WARN|ERROR)\s+user=([\w.-]+)\s+ip=([\d.]+)\s+cost=(\d+)ms$'
)

我盯着那一坨 \d{2}、\s+、[\w.-],脑子里只有一个声音: “我当初写这坨的时候,脑回路是怎么长的来着?”

外卖到了我都没想明白。

就这种时候,你就会开始怀疑人生:正则确实好用,但人类是不是不太适合直接和那堆符号打交道。

结果第二天早上,在工位上刷 GitHub 的时候刷到一个库,名字叫 Humre,说白了就是“给人看的正则”。我试了半天,觉得这东西确实挺适合推荐给大家的。

说人话的正则是个啥玩意儿

先把心态放平一点,Humre不是要推翻 re 模块、发明新正则引擎,它干的事儿特别简单粗暴:

它只帮你把“人能看懂的拼法”变成普通的正则字符串,然后还是交给 re.compile() 去跑。

所以你可以这么理解:

  • 以前你自己手写:r'\d{3}-\d{3}-\d{4}'
  • 用 Humre 的时候,是写:exactly(3, DIGIT) + "-" + exactly(3, DIGIT) + "-" + exactly(4, DIGIT)
  • 最后这个拼出来的还是那个 \d{3}-\d{3}-\d{4},只是你平时看的、改的,是第二种形式

装一下库也很正常:

pip install humre

Python 3.5 往上的版本都能用,作者就是写《Automate the Boring Stuff with Python》的那个 Al Sweigart。

先拿最经典的例子开刀:手机号

刚才那个美国手机号的例子,咱直接对比一眼会更直观一点。

普通写法,大概这样:

import re

phone_re = re.compile(r'\d{3}-\d{3}-\d{4}')

Humre 的写法就变成了这样:

from humre import *

phone_pattern = compile(
    exactly(3, DIGIT),
"-",
    exactly(3, DIGIT),
"-",
    exactly(4, DIGIT),
)

m = phone_pattern.search("Call 415-555-1234 today!")
print(m.group())
# 415-555-1234

几个点你感受一下:

  • DIGIT 这个常量= \d,你肉眼看过去就知道是“一个数字”
  • exactly(3, DIGIT) 一眼就知道是“3个数字”
  • 中间的 "-" 就是普通字符串,不用再纠结到底要不要转义

而 compile() 这个函数,其实就是包了一层 re.compile(),你也可以把 phone_pattern 看成一个正常的 re.Pattern 对象来用。

Humre 的核心套路:函数 + 常量,全部都是“字符串”

为啥说它“更人性化”,其实就两件事:

  1. 把那些鬼画符变成有名字的常量,比如 DIGIT、WORD、WHITESPACE 等等
  2. 把量词和分组变成普通函数,比如 one_or_more()、optional()、group() 之类

重点有个小认知:Humre 的函数返回的都是普通字符串,常量本身也是字符串。

所以你可以像玩字符串一样把它到处拼:

from humre import *

# 5 位数字 + 可选空格 + 至少一个非空白
pattern_str = (
    exactly(5, DIGIT)
    + optional(WHITESPACE)
    + one_or_more(NONWHITESPACE)
)

print(pattern_str)  # \d{5}\s?\S+

# 甚至塞到 f-string 里
count = 2
pattern_str2 = f"I need {exactly(count, DIGIT)} apples"
print(pattern_str2)  # I need \d{2} apples

你看,这样 IDE 的高亮、括号匹配、自动补全全都正常工作,根本不用去纠结那一堆反斜杠。

再来一个稍微贴近一点业务的例子:解析日志行

假设你线上日志长这样:

[2025-06-01 10:23:01] INFO  user=alice  ip=10.0.0.1  cost=123ms

我们一般想干几件事:

  • 把时间戳抠出来
  • 把日志等级抠出来
  • 把 user、ip、cost 抠出来

先看传统 re 写法(不算太夸张的那种):

import re

log_re = re.compile(
r'^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+'
r'(INFO|WARN|ERROR)\s+'
r'user=([\w.-]+)\s+'
r'ip=([\d.]+)\s+'
r'cost=(\d+)ms$'
)

你过两周再回来,看见这么多 \d 和 \s+,脑子一定要重新编译一遍。

用 Humre 写一遍,会变成下面这样(故意用 named_group,更接近实战):

from humre import *

timestamp = group(
    exactly(4, DIGIT), "-", exactly(2, DIGIT), "-", exactly(2, DIGIT),
    WHITESPACE,
    exactly(2, DIGIT), ":", exactly(2, DIGIT), ":", exactly(2, DIGIT),
)

level = group_either("INFO", "WARN", "ERROR")

user = named_group(
"user",
    one_or_more(chars("A-Za-z0-9._-")),
)

ip = named_group(
"ip",
    one_or_more(chars("0-9.")),
)

cost = named_group(
"cost",
    one_or_more(DIGIT),
)

log_pattern = compile(
    starts_with("["),
    timestamp,
"]", one_or_more(WHITESPACE),
    level, one_or_more(WHITESPACE),
"user=", user, one_or_more(WHITESPACE),
"ip=", ip, one_or_more(WHITESPACE),
"cost=", cost, "ms",
    ends_with(""),
)

text = "[2025-06-01 10:23:01] INFO  user=alice  ip=10.0.0.1  cost=123ms"
m = log_pattern.search(text)
print(m.group("user"), m.group("ip"), m.group("cost"))
# alice 10.0.0.1 123

说实话代码行数是多了,但:

  • 各部分是“拼出来”的,你可以一块块看
  • 把 timestamp、user 这些变量抽出来,哪天需要改格式,定位也更轻松
  • named_group("user", ...) 这种比 (?P<user>...) 还是友好多了([GitHub][1])

常用那几个 API,用几次就记住了

Humre 的 API 列表挺长的,真要全背下来也没必要。对日常开发来说,你记住几个“高频组合”就够用了:

  • 量词系列

    • exactly(n, X):就是 X{n}
    • between(min, max, X):X{min,max}
    • at_least(n, X):X{n,}
    • one_or_more(X):X+
    • zero_or_more(X):X*
    • optional(X):X?
  • 分组 & 或者

    • group(X, Y, Z):相当于 (XYZ)
    • noncap_group(...):(?:...) 不捕获
    • either("A", "B", "C"):A|B|C
  • 边界

    • starts_with(X):^X
    • ends_with(X):X$
    • starts_and_ends_with(X):^X$
  • 字符类

    • chars("A-Z0-9"):[A-Z0-9]
    • nonchars("A-Z"):[^A-Z]
  • 几个常量

    • DIGIT = \d,WORD = \w,WHITESPACE = \s
    • ANYTHING / EVERYTHING / SOMETHING 一系列“匹配任意字符”的模式
    • NUMBER、EURO_NUMBER 这种已经写好的“带逗号的小数”模式

你没必要看文档背下来,实际用的时候 IDE 自动补全+文档提示就够了。上面这些就是 README 里的那一长串 quick reference 里,我个人感觉最常用的那一撮。([GitHub][1])

和 re 的“混搭”姿势

有同学可能会问一个很现实的问题:我不可能把老代码全换成 Humre 吧?

没必要。

因为 Humre 函数最终都会返回普通字符串,所以你完全可以混写,比如:

from humre import *
import re

# 大块复杂的部分用 Humre
user_part = one_or_more(WORD)

# 特简单的部分继续用原始写法
pattern = re.compile(
r"^user=" + user_part + r"\s+age=" + r"\d{1,3}$"
)

print(pattern.pattern)
# ^user=\w+\s+age=\d{1,3}$

甚至你可以只在“看不懂”的那部分上用 Humre,把复杂块单独抽成一个变量,让这坨东西看起来不那么吓人就行。

flags 也能用,而且还是那一套

有的时候,我们会给 re.compile 传些 flag,比如忽略大小写、多行匹配之类的。Humre 也是支持的,只是把常量拿到了自己名下,比如:

from humre import *

pattern = compile(
    starts_with("error:"),
    ANYTHING,
    flags=IGNORECASE | MULTILINE,
)

text = """ERROR: first
warn: ...
error: second
"""


print(pattern.findall(text))
# ['ERROR: first', 'error: second']

IGNORECASE、MULTILINE 这些都和 re.IGNORECASE、re.MULTILINE 一一对应,底层还是传给 re.compile 去用。

性能会不会很拉胯?

这个问题我也好奇过,特地看了下作者的解释:Humre 的函数只是做一些字符串拼接,你只在“定义正则”的那一刻调用一次,后面用 pattern.search()、pattern.findall() 的时候,性能完全就是 Python 原生 re 的那一套,不会慢到哪去。

说人话就是:如果你项目慢到要考虑“拼正则字符串这几十个函数调用”的开销,那你大概率已经有更大的性能问题要先解决了。

随便举个小表单校验的例子哈,比如你有个注册页:

  • 用户名:字母开头,后面可以跟数字、下划线、点,长度 4~16
  • 邮箱:简单校验,不需要 RFC 那种地狱级别
  • 年龄:0~120 的整数,先简单用正则做一层挡板

用 Humre 大概会写成这样:

from humre import *
import re

# 用户名:字母开头,后面字母/数字/下划线/点,4~16
username_pattern = compile(
    starts_and_ends_with(
        group(
            LETTER,                        # 首字符字母
            between(3, 15, chars("A-Za-z0-9._")),  # 剩下 3~15
        )
    )
)

# 非严格邮箱,只大致挡一下
email_pattern = compile(
    starts_and_ends_with(
        group(
            one_or_more(chars("A-Za-z0-9._%+-")),
"@",
            one_or_more(chars("A-Za-z0-9.-")),
".",
            between(2, 6, LETTER),
        )
    ),
    flags=IGNORECASE,
)

# 年龄:0~120,先用正则挡非数字 + 长度,再用 int 判断范围
age_pattern = compile(
    starts_and_ends_with(
        between(1, 3, DIGIT)
    )
)


defis_valid_username(s: str) -> bool:
return bool(username_pattern.fullmatch(s))


defis_valid_email(s: str) -> bool:
return bool(email_pattern.fullmatch(s))


defis_valid_age(s: str) -> bool:
ifnot age_pattern.fullmatch(s):
returnFalse
return0 <= int(s) <= 120


if __name__ == "__main__":
    print(is_valid_username("dong_01"))   # True
    print(is_valid_username("1dong"))     # False

    print(is_valid_email("[email protected]"))   # True
    print(is_valid_email("bad@@example"))       # False

    print(is_valid_age("25"))      # True
    print(is_valid_age("999"))     # False

你想象一下,如果这一套用原始正则写完,是不是整个模块全是 r'^[A-Za-z][A-Za-z0-9._]{3,15}$' 这种东西?你每次改规则,都要从左到右数花括号和中括号,稍微走神一下就会漏掉一个字符。

我自己的习惯是:

  • 简单的小匹配(比如 ^\d{4}$ 这类),继续用 re 原始写法
  • 一旦正则超过一行,或者开始出现一堆括号、管道,就考虑用 Humre 把结构写清楚

你要是最近刚好有一坨看不懂的老正则,可以抽一段用 Humre 改写试试,体会会比较明显。

行了,我得去把昨天遗留的那个日志过滤脚本改完了,不然明天凌晨报警又得被叫起来…

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

🔥虎哥私藏精品🔥

虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB