先想清楚要匹配什么
写正则前先用一句话描述目标:「11 位、以 1 开头的数字」「形如 2026-09-24 的日期」「两个汉字以上的连续中文」。然后逐个片段翻译:数字用 [0-9] 或 \d,重复次数用 {n},固定字符直接写。把这句中文描述留在注释里,半年后回来看还知道自己在匹配什么——这是最省钱的习惯。反过来,如果描述不清(例如「看起来像网址的东西」),正则也一定写不对。
五个最基本的构件
一、字符类:[0-9] 是一位数字,[a-zA-Z] 是一位字母,[^0-9] 是「非数字」。二、量词:{3} 恰好三次,{2,5} 二到五次,+ 一次以上,* 零次以上,? 零次或一次。三、分组与捕获:(abc) 把一组内容括起来,便于加量词或单独取出,例如 (\d{3})-(\d{4}) 能把区号与号码分开。四、锚点:^ 表示行首,$ 表示行尾——不加锚点,\d{11} 会在一串 20 位数字里也匹配到 11 位。五、或:| 表示「其中之一」,记得用括号限定范围,cat|dog 与 (cat|dog)s 的语义完全不同。
贪婪与非贪婪
量词默认是贪婪的:<.*> 会从第一个尖括号一直吃到「最后一个」尖括号,而不是最近的一个。要只匹配最短的一段,就在量词后加问号:<.*?>。这是「替换时把整行都改掉了」的最常见原因。反过来也有教训:如果你的目标是「直到行尾」,用 .* 或 [^\n]* 反而更准确,别习惯性全写成 .*?。判断办法是看匹配结果的长度,长度异常就先怀疑贪婪。
修饰符 g / i / m / s
g 表示全局:不加它,替换只会命中第一处,这也是「明明有匹配却只改了一处」的元凶。i 忽略大小写,适合匹配 HTML 标签名或邮箱域名。m 让 ^ 与 $ 匹配每一行的首尾(而不是整段文本的首尾),处理多行日志时必须加。s 让点号 . 也能匹配换行符——很多语言的在线工具默认不开,所以跨行匹配常常失败,此时用 [\s\S] 更保险。注意各语言默认值不同:Python 的 ^$ 在多行文本里表现就与 JS 不同。
几个常见需求的写法
手机号(中国大陆):^1[3-9]\d{9}$。邮箱(够用即可):^[\w.+-]+@[\w-]+\.[\w.-]+$。日期:^\d{4}-\d{2}-\d{2}$。URL:^https?:\/\/[^\s]+$。中文(两个以上汉字):[\u4e00-\u9fa5]{2,}。去除多余空行:把 \n{3,} 替换成 \n\n。清理行尾空格:[ \t]+$ 替换为空。这些都是「先跑通、再收紧」的例子:邮箱的完整 RFC 正则长到没人愿意看,工程上按需求收口就够。
调试与性能
调试三步:先在几行样本上确认匹配与分组,再放到全量数据上跑,最后检查替换结果是否被多改了。性能上要警惕嵌套量词:(a+)+b 这类写法在长文本上会触发灾难性回溯,表现为页面卡死或脚本超时;改成 (a+)b 或加锚点、限定字符范围即可大幅缓解。另外记住正则不是万能的:解析 HTML、JSON、CSV 都应该用专门的解析器,用正则硬拆只会在边界情况(嵌套标签、字段内逗号)上翻车。
想马上动手:在 正则测试工具 里贴样本、看高亮与分组,确认后在代码里落地;处理字符串前后差异用 文本对比,需要给正则转义 URL 参数时用 URL 编码。Try it now: paste a sample into the regex tester to check highlights and capture groups, compare results with the text diff, and escape parameters with URL encoding.
常见问题
为什么替换只生效了一处?
九成是没加 g(全局)修饰符。加上后所有匹配才会全部替换;另外注意替换字符串里 $1、$2 代表捕获组,写错会得到意外结果。
怎么匹配中文?
常用 [\u4e00-\u9fa5] 表示一个基本区汉字,配合 {2,} 表示连续两个以上。注意它不包含扩展区汉字与标点,需要全量覆盖时可改用 Unicode 属性写法(如 \p{Script=Han})。
能用正则解析 HTML 吗?
不适合。HTML 的嵌套结构会让正则迅速失控,正确做法是用解析器;正则只适合处理格式确定的短文本,例如从一行日志里取时间戳。