
做文本处理这几年我越来越觉得正则表达式是那种“越早学会越划算”的技能。处理日志、清洗数据、解析接口返回、校验表单几乎每个项目里都会碰到这类需求给定一段文本要把符合条件的部分找出来、换掉或者拆开。有人习惯写一堆循环加 if 判断也能出结果但代码又长又脆换成正则往往一行就讲清楚了。这篇内容我打算用实际操作的角度来聊先讲清楚正则到底解决什么问题再把最常用的语法逐块拆开接着分别演示 Java 和 PHP 里的落地姿势最后附上一份常见问题的排查清单。无论你是刚接触编程的新手还是写过几年代码但每次用正则都要现查的老手应该都能从中捞到点干货。先给个我自己的体会正则不是背出来的是用出来的。你不需要记住所有元字符只要把最核心的那三四十个规则吃透配合好用的测试工具碰到复杂需求能拆解成小块来写就足够应付绝大多数场景了。真正难的不是语法本身而是怎么把业务需求“翻译”成模式这个能力没有捷径但有很多技巧可以缩短学习曲线。1. 文本处理中的正则先搞清楚它解决什么问题1.1 正则的价值把“规则”变成“模式”文本处理的核心无非四件事查找、提取、替换、校验。正则表达式解决的是这四件事里最难的一类——当你要找的目标不是一个固定字符串而是符合某种“形状”的文本时正则几乎是唯一趁手的工具。举例从一段访问日志里把所有 5 开头的状态码挑出来。用字符串查找你得先知道具体是 500、502 还是 503然后逐个判断正则可以写成\b5\d{2}\b意思是“单词边界 数字5 任意两位数字 单词边界”一个模式就把一类值全覆盖了。这正是正则的核心思想用符号描述一类文本的共性结构。这个“模式匹配”的思路本质上是在抽象化你的需求。你不需要枚举每一个可能的取值只需要描述它们的共同特征。比如提取所有形如 user_123 的 ID模式就是user_\d找出所有被方括号包裹的时间戳模式就是\[[\d:.]\]。文本越杂乱、格式越不固定正则的优势就越明显。我见过不少同事遇到提取需求第一反应是写循环加 substring最后要么漏了边界情况要么代码一长串没法维护。其实这类场景的正则往往五分钟就能写完而且可读性更好——因为模式本身就是对文本规则的最直接描述。1.2 反直觉的边界哪些场景不该用正则不过我得先泼一盆冷水。正则不是万能的有些场景用它反而添乱解析 HTML/XML用专门的解析库比如 Jsoup、DOMDocument别用正则硬匹配嵌套标签嵌套结构是正则的天然短板。解析 JSON/YAML直接用对应语言的内置解析器自己写正则解析早晚会出事故。需要算术或逻辑判断的场景比如“金额大于 100 的记录”正则只负责格式匹配数值比较交给代码。简单的前缀/后缀判断startsWith、endsWith这类方法更直白可读性更好。我自己定过一条原则能用专用工具就用专用工具正则留给“没有现成解析器”的自由文本场景。比如日志、用户输入、不规范的数据文件这些才是正则的主场。还有个容易被忽略的点正则只能做基于字符的匹配它没有语义概念。\d能匹配数字串但不知道这个数字代表的是年龄还是价格能匹配到一段日期但不知道业务上这个日期是否合法。所以正则的结果通常要配合业务代码做二次加工这是它的边界。看到一条正则“好像能匹配”和“确实匹配我要的东西”之间差的往往是这层业务校验。2. 正则表达式核心语法按块拆开讲2.1 字符类一个符号描述一类字符正则的最小单位是“匹配一个字符”。最简单的写法是字面量比如a就匹配字符 a。但实战里我们更多用字符类来匹配某一类字符[abc]匹配 a、b、c 中的任意一个[a-z]匹配 a 到 z 之间任意小写字母[^0-9]匹配非数字字符^在方括号开头表示取反\d等价于[0-9]匹配一个数字\w等价于[A-Za-z0-9_]匹配字母、数字、下划线\s匹配空白字符包括空格、制表符、换行.匹配除换行外的任意字符这里有个新手最容易犯的错把.当成“匹配任意内容”于是写a.b想匹配“a 和 b 之间随便什么”。这个想法方向对但.只匹配一个字符要匹配任意长度得配合量词写成a.*b。另外.默认不匹配换行在 Java 里可以用Pattern.DOTALL改变这个行为在 PHP 里是/s修饰符。这些细节记不住没关系关键是要知道“有这回事”遇到匹配不上再回头查。实际使用中我还有个习惯能用\d、\w这类预定义字符类就尽量不用[0-9]、[A-Za-z0-9_]这种长写法。一是代码短二是在不同语言里预定义字符类往往自带 Unicode 支持比如加修饰符后\w能匹配中文扩展性更好。2.2 量词与贪婪、懒惰匹配控制重复次数光有字符类只能匹配固定长度的文本量词让模式有了伸缩性*前面的字符出现 0 次或多次前面的字符出现 1 次或多次?前面的字符出现 0 次或 1 次{n}恰好 n 次{n,}至少 n 次{n,m}n 到 m 次比如\d{4}-\d{2}-\d{2}就能匹配2024-06-18这种日期格式。\d则匹配任意长度的连续数字哪怕只有一个数字。{n,m}在处理定长字段时特别有用比如银行卡号、身份证号这类固定位数或固定区间位数的数据用精确量词能大幅减少误匹配。量词最值得讲的是贪懒之别。*和默认是贪婪的会尽可能多地匹配。举个例子文本是b加粗/b和b再加粗/b用b.*/b去匹配会一口气从第一个b吃到最后一个/b把中间全部吞掉。要得到每对标签各自的内容得用懒惰量词b.*?/b?跟在量词后面表示“尽量少匹配”匹配到第一个/b就停。这个差异在提取类需求里几乎是必踩的坑建议直接背下来提取多个片段时默认先试懒惰版本。懒惰也不是万能的。如果文本结构本身就复杂懒惰量词可能匹配得过短导致结果残缺。这时候最好的办法是缩小字符类范围比如用[^]*代替.*?来匹配“不是左尖括号的一串字符”既快又准。这类“否定字符类 贪婪”的写法其实比“任意字符 懒惰”更稳是处理结构化文本时我很喜欢用的手段。2.3 分组、锚点与断言从单点匹配到整体控制分组用括号()实现作用有两个一是把多个字符当整体操作比如(ab)匹配 ababab二是捕获内容匹配到的分组会在结果里单独取出来Java 里用matcher.group(1)PHP 里用$matches[1]访问。如果你只需要分组不需要捕获用(?:...)非捕获分组性能和代码可读性都更好。锚点负责定位^匹配文本开头$匹配文本结尾。比如^\d{6}$能严格校验一个 6 位数字的字符串防止它在中间混入其他字符如果去掉锚点\d{6}在一个 8 位数字的字符串里也能匹配成功校验就形同虚设。\b匹配单词边界想匹配独立的单词cat而不是concatenate里的 cat就得写成\bcat\b。断言lookaround是进阶利器最常用的两条(?...)前瞻断言表示“后面跟着什么但本身不消耗字符”(?!...)负向前瞻表示“后面不跟着什么”。举个例子校验强密码时要求至少 8 位且同时包含字母和数字可以写成^(?.*[A-Za-z])(?.*\d).{8,}$。两个断言分别检查各自的条件互不干扰最后.{8,}保证长度。这种写法初看绕但拆开理解后就发现断言非常适合表达“同时满足多个条件”的需求在其他工具里想实现这种逻辑往往要写好几层判断。3. Java 与 PHP 中的正则落地实操3.1 JavaPattern 与 Matcher 的正确分工Java 把正则的使用分成两个类Pattern负责编译模式Matcher负责执行匹配。编译一次、复用多次是它的核心用法。实际开发里最常见的错误是每次循环都调Pattern.compile白白浪费资源正确的做法是把 Pattern 定义为静态常量。private static final Pattern USER_ID_PATTERN Pattern.compile(^user_\\d$); public boolean isUserId(String input) { return USER_ID_PATTERN.matcher(input).matches(); }注意matches()要求整个字符串都匹配模式相当于自动加了^和$而find()是在字符串里寻找子串。这个区别经常被忽略用find()去校验输入只要中间某段符合就返回 true很容易放过非法数据。我建议校验类需求一律用matches()提取类需求才用find()。还有一个 Java 特有的坑字符串里的反斜杠需要转义。写正则\d时在 Java 源码里要写成\\d少写一个反斜杠直接编译报错。很多新手在这里卡住以为是正则写错了实际上是 Java 字符串转义的问题。如果正则特别长可以用Pattern.quote()把纯文本部分包起来避免一堆转义符干扰阅读。3.2 Java 身份证号码校验正则只是第一步身份证号校验是网上搜烂了的题目很多人贴一个正则就完事。这里我得说点实在的18 位身份证号码的规则格式部分用正则解决但最后一位是校验码必须用算法验证光靠正则挡不住伪造。正则部分18 位号码前 17 位是数字最后一位可能是数字或大写 X模式是private static final Pattern ID_CARD_PATTERN Pattern.compile(^\\d{17}[\\dXx]$);但这只是格式校验。真正的有效性校验还差一步加权计算把前 17 位分别乘以固定的加权因子结果求和后对 11 取模根据余数对照表得到校验码再和最后一位比对。加权因子是{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}余数对应的校验码是{1,0,X,9,8,7,6,5,4,3,2}。完整的校验流程是正则判断格式再走一遍加权计算两次都通过才算合法。// 1. 格式校验 if (!ID_CARD_PATTERN.matcher(idCard).matches()) { return false; } // 2. 校验码计算 int[] weights {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}; char[] checkCodes {1,0,X,9,8,7,6,5,4,3,2}; int sum 0; for (int i 0; i 17; i) { sum (idCard.charAt(i) - 0) * weights[i]; } char expected checkCodes[sum % 11]; char actual Character.toUpperCase(idCard.charAt(17)); return expected actual;这个案例的意义在于正则负责“长相”算法负责“真伪”两者配合才是完整的校验方案。以后面试或者实际项目里再遇到类似校验问题能说出这层逻辑比甩一条正则高级得多。3.3 PHPpreg_ 系列函数与定界符习惯PHP 的正则走的是 PCRE 风格和 Java 的语法大同小异但有几个使用习惯完全不同。第一是定界符PHP 要求模式必须被一对定界符包裹最常见的是/pattern/如果模式里有很多斜杠建议换成~pattern~或#pattern#省去转义的麻烦。第二是函数命名PHP 把匹配、替换、拆分都拆成了独立函数preg_match匹配一次preg_match_all匹配全部preg_replace替换preg_split拆分。一个容易记错的地方是参数顺序preg_replace($pattern, $replacement, $subject)是模式在前、替换串居中、目标串最后和许多语言的参数顺序不一样写多了容易和str_replace搞混。$pattern /^1[3-9]\d{9}$/; if (preg_match($pattern, $phone)) { echo 手机号格式正确; } // 提取所有邮箱 preg_match_all(/[\w.-][\w-]\.[\w.-]/, $text, $matches);还有 PHP 特有的修饰符要留意/u表示按 UTF-8 处理模式处理中文时必须加上否则\w对中文的匹配行为会非常奇怪/i表示忽略大小写/s让.匹配换行。调试的时候可以用preg_last_error()检查上次匹配是否因为回溯超限而失败返回PREG_BACKTRACK_LIMIT_ERROR就说明正则写得太贪了。这个函数很少人用但排查线上正则性能问题的时候能省大量时间。4. 高频正则速查与复杂模式拆解思路4.1 直接能抄的常用正则清单整理一份我平时用得最多的正则清单覆盖日常开发的大部分需求直接复制改改就能用。场景正则表达式说明邮箱地址^[\w.-][\w-]\.[\w.-]$实际业务可适当放宽11 位手机号^1[3-9]\d{9}$覆盖目前主要号段IPv4 地址^((25[0-5]2[0-4]\d日期^\d{4}-\d{2}-\d{2}$只校验格式不验证日期合法性URL^https?://[\w.-](:\d)?(/[\w./?%-]*)?$覆盖大多数场景中文字符[\u4e00-\u9fa5]匹配单个汉字正整数^[1-9]\d*$不包括 0文件扩展名.(jpgpng这类清单网上很多但我的建议是抄可以用之前必须自己测一遍。理由很简单正则的细节太多同一个需求在不同语言和不同数据样本下的表现可能有差异。比如邮箱正则^[\w.-][\w-]\.[\w.-]$对ab..com这种双点域名依然会通过因为[\w.-]允许连续点要更严格就得逐个域名标签校验。这就是为什么我一直强调常用正则可以背但真正落地时要在测试工具里拿真实数据过一遍别让一条看似正确的正则放进生产代码里埋雷。4.2 复杂需求怎么拆从条件到模式遇到复杂需求我习惯先写注释再写正则。把需求拆成几个小条件每个小条件对应一段子模式最后拼起来。比如需求提取文本里所有形如keyvalue的键值对其中 key 只能是字母数字下划线value 可以是任意非空字符不含空格。拆解过程key 部分[A-Za-z_]\w*首字符不能是数字等号value 部分任意非空且不含空白的内容用\S表示整体要作为片段提取\b[A-Za-z_]\w*\S再把需要的内容分组取出来\b([A-Za-z_]\w*)(\S)这样在 PHP 里$matches[1]是 key$matches[2]是 value。这个方法在复杂场景下特别管用。我处理过一条从应用日志里提取异常堆栈的正则拆完之后每一段都一目了然开头匹配异常类名、中间匹配 at 开头的栈帧行、最后用否定字符类收底。写正则最忌讳的就是闷头一口气写完一长串出了问题根本没法调。拆成小块每块单独测过再合并出错了也容易定位。工具方面我常用 regex101 这类在线测试环境左侧写模式、中间放测试文本、右侧看分组和匹配详情调试效率比在编辑器里反复试错高很多。5. 常见问题排查与避坑心得5.1 高频问题速查表把我这些年遇到的高频问题整理成一张表按“现象 → 原因 → 解法”的顺序列出来排查时可以直接对照。现象常见原因解决办法匹配不到预期内容忘记转义特殊字符检查点号、括号、斜杠是否被正确转义匹配结果比预期长贪婪量词改用懒惰写法*?、?校验不严格非法数据能通过缺少锚点或用了 find()加^$校验用 matches()Java 里写\d编译报错Java 字符串里反斜杠需要转义写成\\dPHP 报了 unknown modifier定界符和模式内字符冲突换用~或#定界处理中文匹配不到未处理 UnicodeJava 用(?U)PHP 用/u修饰符程序卡死或极慢灾难性回溯避免嵌套量词加锚点用原子组或占有量词5.2 让我少踩坑的几个实操习惯第一所有正则先在小样本上测试再上全量数据。我吃过一次亏用一条正则跑全量历史日志跑了十分钟没跑完kill 掉一看是回溯爆炸。从那以后凡是涉及量词嵌套的模式我一定先构造极端样本来测性能比如超长字符串、大量连续相同字符等。正则性能问题在数据量小的时候完全看不出来但一到线上就是事故现场。第二写复杂正则一定要留版本。我自己是把常用正则统一放在一个配置文件或者枚举类里每个都带注释说明适用场景和坑点。时间一长这些东西就是团队的公共资产后人不至于因为看不懂你当年的“神级正则”而骂娘。代码里的正则如果不能一眼看懂就一定要写注释这句话我重复多少遍都不为过。第三能用字符类就不用点号能加锚点就加锚点。\d比.更快也更精确因为引擎不需要各种回溯尝试^锚定开头能让引擎快速排除不匹配的文本。这些细节单独看都是小优化但在处理千万级行数据时差距是数量级的。正则写出来不只是给机器跑的也是给下一个维护者看的越精确的模式意图越清楚。最后再分享一个小技巧。我每次拿到一条陌生正则会在测试工具里把匹配过程的高亮打开看不同分组实际吃掉了哪些字符再一点一点删掉部分子模式观察它对结果的影响。这比盯着表达式干想快得多。正则这个东西用多了会发现它其实是一套很朴素的语言字符类描述长相量词描述重复锚点描述位置断言描述关系。把这四个维度吃透再配合实际的匹配测试你在文本处理上就能少吃很多苦少加很多班。