中文与 Unicode 匹配:\u 范围、汉字区间、全角半角、emoji 的坑
汉字匹配:一个区间打天下
处理中文文本的第一课:常用汉字在 Unicode 里集中排在 U+4E00 到 U+9FA5("基本区"),写成字符组就是 [\u4e00-\u9fa5]。Python 直接支持 \u 转义:
import re
title = "万象云档 Emp9 专栏上线"
print(re.findall(r"[\u4e00-\u9fa5]{2,}", title))
# 输出:['万象云档', '专栏上线']
连续汉字被整体捞出,英文与数字自动排除——中文分词前的粗切、敏感词初筛、提取中文标签,都是这个模式的变体。
需要更完整的覆盖时(基本区之外还有扩展 A 区等生僻字),可以放宽为 [\u4e00-\u9fff],或使用脚本属性语法。JavaScript 的 u 标志开启后支持 \p{...} 类:
const re = /\p{Script=Han}+/gu;
console.log("万象云档 Emp9 专栏".match(re)); // ['万象云档', '专栏']
\p{Script=Han} 表示"属于汉字文字系统的字符",由 Unicode 标准维护,比手写区间更权威、覆盖更全。Python 的 re 模块不支持 \p{} 语法(第三方 regex 模块支持),跨语言时以各自文档为准。
全角与半角:肉眼一样,码点两样
中文排版的经典陷阱:ABC123 是全角字符,码点在 U+FF01–U+FF5E,与半角 ABC123 完全不是同一个字符。用户表单里一个全角手机号,能让 ^\d{11}$ 静默失败。解法是先归一化再校验:
import re
form = "热线 4001234567"
fixed = re.sub(r"[0-9]", lambda m: chr(ord(m.group(0)) - 0xFEE0), form)
print(fixed)
# 输出:热线 4001234567
全角数字码点比半角正好大 0xFEE0,减回去即完成转换。字母与常用标点同理。所有面向用户输入的校验,前面都该站一道全角归一化,这一步能消掉一半"我明明输对了却说格式错误"的工单。
常用区间速查表:
| 区间 | 内容 | 常见用途 |
|---|---|---|
\u4e00-\u9fa5 |
常用汉字(基本区核心) | 中文识别、分词粗切 |
\u3400-\u4dbf |
汉字扩展 A 区 | 生僻姓名处理 |
\u3000-\u303f |
中文标点(。、《》等) | 标点归一化 |
\uff01-\uff5e |
全角 ASCII 形态 | 全角转半角 |
\uff61-\uff9f |
半角片假名 | 日文文本清洗 |
\u4e00-\u9fff 全集兜底 |
基本区全部 | 放宽版汉字判断 |
emoji:一个字符可能"不是一个人在战斗"
emoji 主要分布在 U+1F300 以上的增补平面,Python 源码里用 \U0001F300(大写 U、八位十六进制)表示。注意别把区间当万能筛——✅(U+2705)、❗(U+2757)等"符号类 emoji"散落在基本平面:
import re
msg = "上线 🚀 完成 🔥 待办 ✅"
print(re.findall(r"[\U0001F300-\U0001FAFF]", msg))
# 输出:['🚀', '🔥']
✅ 不在区间内被排除——如果你把这条区间当"emoji 过滤器",就会漏掉一批。真正健壮的 emoji 识别应使用 Unicode 属性类(如 Python regex 模块的 \p{Emoji})或成熟的表情解析库。
更深的坑是组合序列:👨👩👧 这样的"家庭 emoji"由三个人物加两个零宽连接符(U+200D)拼成,占 8 个以上码点;在未开 u 标志的 JavaScript 里,一个 🚀 甚至是两个 UTF-16 代理单元,/./ 会把它劈成两半:
console.log("🚀".length); // 2(UTF-16 代码单元,不是字符数)
console.log(/[🚀]/u.test("🚀")); // true,u 标志按码点整读
所以 JS 处理 emoji 时,u 标志不是"高级选项"而是必选项;字符串长度统计要用 Array.from() 或 Intl.Segmenter 才符合人类直觉。
组合拳实战:清洗一条混合弹幕
把"中文、表情、全角、半角"混在一行的弹幕清洗成纯文本标签:
import re
danmu = "哈哈哈哈😂太好笑了8个字都不止!!"
clean = re.sub(r"[\U0001F300-\U0001FAFF]", "", danmu) # 去增补平面 emoji
clean = re.sub(r"[\u3000-\u303f!?。,]", "", clean) # 去中文标点
print(re.findall(r"[\u4e00-\u9fa5]+", clean))
# 输出:['哈哈哈哈太好笑了', '个字都不止']
注意结果在 8 处断成了两截——全角数字既不是汉字也不在上面的标点区间里,第三步想清掉就要再加 [\uff01-\uff5e]。这个"剥洋葱"的过程就是 Unicode 文本处理的日常:每类字符一个区间,一层层剥,每层都实测。
经验之谈:涉及中文的正则,永远准备三类测试样本:纯中文、混合全角半角、带 emoji 与生僻字。三关都过再上线。 Unicode 的水比想象中深,"我测过一条"不等于测过。
三个 Unicode 自测题
题一:[一-龥]+ 能从 "第 3 章完" 里提取什么?
import re
print(re.findall(r"[一-龥]+", "第 3 章完"))
# 输出:['第', '章完']
数字与空格把连续汉字切成两段——提取中文时,先想清楚分隔符是什么,别指望正则"懂中文"。
题二:全角字母怎么转半角?数字用 chr(ord(x)-0xFEE0),字母同理,只是区间换成 [A-Za-z]:
import re
print(re.sub(r"[A-Za-z]", lambda m: chr(ord(m.group(0)) - 0xFEE0), "ABc"))
# 输出:ABc
题三:判断一个字符串"只含中文和空格",写出整串校验。(参考答案:^[一-龥\s]+$——注意 \s 在 Python 里含全角空格,在 JS 里不含,跨语言要复查。)
三道题的共同教训:Unicode 正则的每个细节都要实测。区间写错一位、\s 语义差一点,结果就完全不同。把这三题的错误类型(区间断错、忘记映射、字符类语义漂移)记进你的检查清单,处理中文时逐项过一遍。
一个高频疑问
问:\w 能当"单词字符"用来匹配英文标识符吗? Python 里恰恰不行——默认 Unicode 模式下 \w 连汉字都收,用户名1 整体会被 \w+ 吞掉。要匹配"编程标识符"这种 ASCII 语义,必须显式写 [A-Za-z_][A-Za-z0-9_]* 或加 re.A 标志把 \w 压回 ASCII:
import re
print(re.findall(r"\w+", "变量 count_1 与 中文词"))
# 输出:['变量', 'count_1', '与', '中文词']
print(re.findall(r"\w+", "变量 count_1 与 中文词", re.A))
# 输出:['count_1']
同一模式、同一文本,只差一个标志,结果一个是"全要"一个是"只要 ASCII"——这就是 Unicode 默认模式的威力与陷阱。凡是涉及"标识符、变量名、协议字段"的匹配,Python 侧默认该考虑 re.A;而处理自然语言文本时则相反,默认的 Unicode 模式才是你要的。分清这两种语境,\w 的争论就尘埃落定了。
参考资源
- MDN Unicode 属性类与 u 标志:MDN 正则·Unicode 属性
- Python re 模块对 Unicode 的支持说明:re — Regular expression operations
本章由 WorkBuddy 原创撰写,示例均经实际验证。
