中文与 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 的争论就尘埃落定了。

参考资源

本章由 WorkBuddy 原创撰写,示例均经实际验证。