ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

正则量词完全指南:从\d到{m,n},轻松匹配多个字符

2026/9/30 2:07:18 拓冰建站 浏览量
正则量词完全指南:从\d到{m,n},轻松匹配多个字符 做表单校验的时候很多新手会遇到一个很经典的困惑想用正则表达式匹配多个字符比如校验手机号是不是11位、提取一段连续的数字结果发现单个\d只能匹配一个数字\d*又连空字符串都能通过。于是折腾半天表达式写了一长串还是不达标。这个问题的核心就在于正则表达式里处理“数量”的那一组符号*、、?、{m}、{m,n}。它们统称量词quantifier作用是告诉正则引擎“前面的单元要重复多少次”。把这几个量词吃透了你写出的正则表达式会立刻从“能用”变成“好用”从“能跑”变成“敢上线”。这篇就围绕匹配多个字符这个话题把这几个量词讲透。1. 为什么需要量词单个字符匹配的根本局限1.1 最朴素的写法一个字符一个字符地排正则表达式最基本的操作就是匹配单个字符。想匹配字母a写a就行想匹配一个数字写\d就行。这看起来很直接但一落到真实需求里就出问题了。比如要匹配一个6位数字验证码不用量词的写法是\d\d\d\d\d\d。6位还算好如果换成11位手机号要写11个\d写出来又臭又长。再换成18位身份证号那就是灾难。更麻烦的是很多场景根本不是“固定位数”而是“位数不定”比如一段连续的数字、多个连续的空格、一个 URL 里的可选协议头。这时候靠“一个字符写一次”的思路完全走不通必须有一种机制来描述“重复”。量词就是干这个的。它不匹配任何具体字符而是修饰它前面的那个“匹配单元”告诉引擎这个单元要重复多少次。重复的次数可以是固定值可以是一个区间也可以是“0次或多次”这种开放区间。提示这个“修饰前面单元”的概念是理解量词的钥匙。ab*并不是“匹配多个 ab”而是“匹配一个 a后面跟着 0 个或多个 b”。很多人踩的坑一半以上是栽在这里。1.2 量词的本质重复前面那个“单元”“单元”这个词听起来抽象其实很具体。在量词眼里它前面可以是单个字符比如a*字符类比如[0-9]转义符号比如\d{3}一个分组比如(ab)一个非捕获分组比如(?:abc){2}量词的修改范围就是它前面紧挨着的那个单元不会往前多管也不会管到后面。这个规则在遇到分组时尤其容易乱。举个实际例子ab匹配的是a后面跟 1 个或多个b所以它能匹配ab、abb、abbb但匹配不了abab因为只管到了紧挨着的b。(ab)匹配的是“一个或多个连续的ab序列”所以它能匹配ab、abab、ababab对于字符串ababb它只能匹配开头的abab。这两种写法看起来只差一个括号语义天差地别。我见过不少人在做连续重复字符匹配时把(ab)写成ab结果线上数据一直匹配不全查了半天才发现是括号的问题。建议在写量词之前先想清楚自己要重复的是“单个字符”还是“一组字符”必要时果断加括号不要省。1.3 量词能让表达式匹配到“空”这是初学者最容易忽略的一个特性。*表示“0次或多次”?表示“0次或1次”它们都允许匹配 0 次也就是“什么都不匹配”。举例来说\d*去匹配字符串abc它会成功因为它在位置 0 处就停下了匹配结果长度为 0。这不是 bug而是量词的既定语义。理解了这一点你就能明白为什么\d*不能用来做必填校验而必须用\d或者配合^和$锚点。2. 四种基础量词*、、? 的语义与实战场景2.1 星号*零次或多次*表示它前面的单元可以出现 0 次、1 次、或任意多次。它表达的核心语义是“可有可无但有就全收”。典型场景是处理“可选的空白字符”。比如解析用户输入的配置项时等号两边可能有空格也可能没有表达式写\s*\s*就能同时匹配keyvalue、key value、key value。如果把*换成要求至少有一个空白字符那keyvalue这种紧凑写法就匹配不上了。*另一个常见用途是匹配跨行内容比如[\s\S]*这个在第四章讲.与换行问题时会详细展开。要注意的是*匹配 0 次是合法结果。用\d*去匹配一个没有数字的字符串它照样能“成功”只是匹配结果是空。所以在做必填校验的时候*往往是罪魁祸首。2.2 加号一次或多次和*的唯一区别就是它要求前面的单元“至少出现一次”。语义上可以理解为“有且必须有且越多越好”。最常见的应用就是提取连续数字。比如从字符串订单号20240815金额298.5元里提取所有数字用正则\d就能得到20240815、298、5三段匹配。如果这里用\d*你会得到一堆空匹配结果里全是空字符串对数据清洗完全没帮助。也常用于“匹配连续空格再压缩”。许多编辑器都有“把多个空格合并成一个”的功能做法就是把一个空格加一个加号替换成一个空格。这里如果用*那零个空格也会被替换所有没有空格的地方都会被插入一个空格结果全乱套。所以记住一条经验凡是要求“必须出现”的内容用凡是“可有可无”的内容用*。2.3 问号?零次或一次?表示前面的单元出现 0 次或 1 次用一句人话说就是“这个部分是可选的”。最经典的用法是匹配 URL 的协议头。https?://可以同时匹配以http://开头和以https://开头的地址因为s?表示“那个 s 可以有也可以没有”。再比如匹配复数单词colou?r能同时匹配英式拼写colour和美式拼写color。需要注意?在正则里还有另一个角色跟在其他量词后面表示非贪婪模式比如*?、?、??。这是第四章的重点现在先记着一个原则当?单独出现在一个单元后面时它的意思是“0或1次”当它出现在其他量词后面时它改变的是匹配策略。2.4 三种量词对比与等价关系量词含义最少次数最多次数*零次或多次0无限一次或多次1无限?零次或一次01这三种量词其实都是{m,n}的语法糖写法等价于*{0,}{1,}?{0,1}不写量词{1}记下这个等价关系很有用。有时候你会看到别人的表达式里写{0,}不用觉得奇怪那就是*的展开写法。反过来如果你觉得自己写的*语义不够明确也可以改成{0,}代码的可读性因人而异但这段关系一定要心里有数。3. 精确控制{m} 与 {m,n}3.1 {m}恰好出现 m 次当需求是“固定长度”时{m}是最精确的写法。比起手写 11 个\d\d{11}简洁得多而且一眼就能看出“需要11位数字”。实际工作中固定长度校验用得很频繁手机号校验一般会写^1[3-9]\d{9}$其中\d{9}表示后 9 位数字身份证号码主体是 17 位数字加一位校验位表达式会写\d{17}[\dXx]匹配某个系统生成的 8 位订单号直接用\d{8}。{1}理论上和不写量词等价但有些团队规范里会要求“即使只出现一次也写明”目的是让表达式的可读性更强。这个看团队约定我个人在写复杂表达式时也会用{1}把“数量”这个意图显式标出来省得别人或自己三个月后看代码时还得猜。3.2 {m,n}从 m 次到 n 次{m,n}表示前面的单元最少出现 m 次最多出现 n 次。这是处理“长度区间校验”的主力。最常见的例子是用户名或昵称校验。假设要求用户名是 3 到 16 个字符那么用^.{3,16}$或者更严格的^\w{3,16}$都可以。再比如密码长度 8 到 20 位写法是^.{8,20}$。这里要特别强调一点{m,n}中间的逗号必须是英文逗号而且逗号后面不能有空格。有些人从文档里复制带空格的写法结果在引擎里直接报错排查半天才发现是空格问题。另外{m,n}的区间是闭区间包含 m 和 n 本身。\d{4,6}会匹配 4 位、5 位、6 位的数字。如果你希望“至少 4 位最多不限”应该写\d{4,}也就是下一节要说的省略上限写法。3.3 {m,}至少 m 次{m,}表示至少出现 m 次没有上限。它和{m,n}的区别就在于把 n 省略了。这个写法在对长度有“下限要求”但没有“上限要求”时很有用。比如校验一个字段“至少 6 位字符位数不限”表达式就是^.{6,}$。再比如匹配版本号中可能很长的数字部分\d{2,}会匹配两位及以上的连续数字。注意{m,}里的逗号不能省。写{m}表示恰好 m 次写{m,}表示至少 m 次这两个表达式差一个逗号语义完全不同。我说句实话这个逗号是我见过的最容易手滑漏掉的地方没有之一。3.4 边界锚点与长度校验的配合写长度校验时{m,n}通常要和^、$锚点配合。原因很简单\d{11}去匹配一个 13 位数字的字符串它不会报错而是会从字符串中找出一段连续的 11 位数字来匹配成功。这往往不是你想要的。举个例子字符串1381234567890有 13 位数字\d{11}会匹配到前 11 位13812345678。如果你是想校验“这个字符串本身是不是 11 位手机号”那这个匹配结果就是错的。正确的写法是^1[3-9]\d{9}$^要求从字符串开头开始匹配$要求匹配到字符串结尾结束这样整串长度就被锁死在 11 位。这个“锚点 量词”的组合是日常写校验正则最常见的套路一定要养成习惯。凡是“校验整个输入”的场景首尾锚点基本不能省凡是“从文本里捞一段”的场景锚点反而可能坏事要慎用。3.5 量词与字符类、分组结合量词前面不一定非得是单个字符和字符类、分组结合才更能发挥威力。[a-zA-Z]{4}匹配 4 个字母(ab){2}匹配abab(\d{4})-(\d{2})-(\d{2})匹配2024-08-15格式的日期并且能分别捕获年月日(?:https?:\/\/)?\w匹配一个可有协议头的网址。第 4 个例子是“可选分组”的典型应用(?:...)是非捕获分组后面的?表示这整个协议头部分出现 0 次或 1 次。这样表达式既能匹配https://example.com也能匹配example.com。很多新手在这里会写成https?://结果就是顺序完全不对因为?只作用于它前面的那个s管不到后面的://。4. 贪婪、懒惰与占有量词背后的匹配原理4.1 默认贪婪能吃多少就先吃多少正则表达式默认的匹配策略是贪婪的greedy。也就是说当一个量词面对“可以匹配更多字符”和“可以匹配更少字符”两种选择时它会先尝试匹配更多直到撞墙再一步步往回退。这个“往回退”的过程叫回溯backtracking。理解回溯是理解量词运行机制的关键。拿最常见的例子来说用.去匹配div内容/div很多人以为会匹配到div然后没了但实际结果是它匹配了整行div内容/div。原因是匹配左尖括号后会贪婪地把后面所有字符都吞下去直到字符串末尾然后引擎发现后面还需要匹配一个于是开始从末尾往回退每退一个字符就检查是不是最后在字符串最后那个处匹配成功。所以结果是“从头吃到尾”。这个行为在设计正则时要特别留意。提取 HTML 标签时用.十有八九会得到比预期长得多的结果。别问我是怎么知道的问就是上过当。4.2 懒惰匹配量词后面加问号把量词后面加上?就变成了懒惰匹配lazy / non-greedy。懒惰的策略正好相反能匹配少的时候就先匹配少不够再扩展。*?尽可能少地重复最少 0 次?尽可能少地重复最少 1 次??尽可能少地重复最少 0 次{m,n}?尽可能少地重复最少 m 次继续拿上面的例子说.?匹配div内容/div时?会先只匹配一个字符然后尝试找如果不行再扩展一个字符再试。于是第一次匹配会停在第一个处结果是div如果继续往后匹配第二个结果是/div。懒惰匹配在处理成对标签时很常用。比如想提取b加粗/b里的内容写b(.*?)/b会比b(.*)/b安全得多。后者在多个标签同时出现时会把内容一路吃到最后一个/b才停。注意懒惰匹配不是数学意义上的“最短匹配”它只是改变了尝试顺序匹配结果仍然是从左到右扫描时第一个能成立的结果。在某些复杂表达式里懒惰匹配的结果可能仍然不是你心里想的“最短片段”。这一点要靠实际测试来确认。4.3 回溯的代价与灾难性回溯贪婪和懒惰都涉及回溯大部分情况下回溯是可控的。但有一种情况会让回溯呈指数级增长卡死你的程序这就是“灾难性回溯”。典型例子是(a)b去匹配一个由大量a组成但没有b的字符串。(a)有嵌套量词引擎会尝试无数种分组方式去划分那些a每一种都失败后才换下一种。随着a的数量增加尝试次数爆炸式增长。在 Java、JavaScript、Python 的re模块里这种表达式能把 CPU 打满让接口卡死。早年不少 ReDoS 攻击就是利用这个原理。应对方法有几个改写表达式避免“量词套量词”。比如把(a)改成a语义等效但性能天差地别用原子组atomic group或占有量词比如a*这种写法在很多语言里表示“占有模式”匹配了就不回溯。但 JavaScript、Python 的re模块都不支持Java 和部分工具支持给输入长度加限制防止恶意超长字符串进入正则。在实际项目里我建议对线上用到的正则做一次“量词审查”凡是出现量词紧挨着量词、或者分组内又有量词再被外面量词包住的情况都要重点警惕。这类表达式的性能问题属于“平时没事、数据一多就炸”的类型测试环境很难发现上线后才现原形。4.4 点号不匹配换行跨行匹配的正确写法与量词配合时.有一个著名限制它默认匹配除换行符以外的任意字符。因此.*匹配不了包含换行的文本。在匹配多行内容时有两个常用替代品[\s\S]匹配任意空白或非空白字符也就是“任意字符”的等价写法(?s)修饰符单行模式 / DOTALL让.也能匹配换行符写法是(?s).*。比如要从一个多行日志里提取某个字段日志内容横跨几行写(\S)可能断掉写.*又过不了换行这时[\s\S]*?往往是最稳妥的选择。我在处理大段网页源码和日志文本时几乎固定用[\s\S]来替代.因为它的语义直白不受引擎修饰符的影响代码在不同语言间迁移时也不用担心行为差异。5. 实战应用几类高频匹配多个字符的场景5.1 校验类手机号、身份证、密码长度这些场景最容易考到量词也最容易踩长度校验的坑。手机号校验是最典型的一个。随着号段不断放开目前大陆手机号第一位是 1第二位一般是 3 到 9后面 9 位任意数字。一个比较通用的表达式是^1[3-9]\d{9}$这里^和$锁死整串长度[3-9]限定第二位\d{9}匹配后 9 位。整体 11 位缺一不可。身份证校验更考验位数控制。18 位身份证号由 17 位数字和 1 位校验位组成校验位可能是数字也可能是 X 或 x表达式为^\d{17}[\dXx]$这里\d{17}是固定 17 位后面[\dXx]占 1 位总长度严格 18 位。如果把{17}写成{16}或{18}整个校验就是错的。密码长度校验通常要求“8 到 20 位任意字符”写法是^.{8,20}$。如果要求“必须包含字母和数字”那要拆分多个条件分别判断量词在其中起的是控制每个字符类型数量的作用。5.2 提取类数字、标签、连续内容从文本中提取内容是量词的另一个主战场。提取所有连续数字用\d。这个表达式在日志分析、数据清洗里出现频率极高。比如从abc123def456里提取会得到123和456两个匹配。如果用\d*结果里会混入一堆空匹配提取效果大打折扣。提取 HTML 标签之间的内容用div[^]*(.*?)/div。这里[^]*匹配标签内的属性部分.*?懒惰匹配标签体内容。很多新手会写成div(.*)/div如果页面只有一个 div 还能凑合多个 div 嵌套或并列时结果会把中间所有内容都吞掉。压缩连续空格用替换为单个空格。在清洗用户输入的文本时这个方法比写循环判断空格是否连续要省事得多。5.3 分组与量词组合复杂重复单元量词和分组结合才能描述“重复一整段”的需求而不是只重复一个字符。(ab){2,3}匹配abab或ababab(\d{2}:){2}\d{2}匹配12:34:56这种时间格式(\d{2}:){2}表示“两位数字加冒号”重复两次再补上最后的两位数字(?:0[1-9]|1[0-2])配合量词匹配月份能锁定 01 到 12 的范围。需要注意的是分组里的内容会占用捕获组编号。如果你只是想让某些字符“作为一个整体重复”而不需要单独引用它建议用非捕获分组(?:...)来避免影响后续的组编号。这在表达式里分组较多时尤其重要否则后面想用\1引用第一个组时会发现编号全被无关分组挤占了。6. 常见问题与避坑指南6.1 常见问题速查表问题现象可能原因解决办法\d*匹配到一堆空字符串*允许 0 次空位置也能匹配成功改用\d或配合^、$使用{m,n}报错或匹配不到写成中文逗号或逗号后有多余空格改成英文半角逗号形如{2,5}全角问号当量词用输入法没有切换写成了中文标点确保量词?是英文半角符号ab*匹配不到预期的abab量词只管前面一个字符b*不等于(ab)*需要重复整段时加括号(ab)*.*匹配范围过大贪婪量词默认尽量多吃吃多了再回溯改用.*?懒惰匹配或精确到字符类跨行文本匹配不到.默认不匹配换行符用[\s\S]*或用(?s).*校验 11 位手机号却匹配了 13 位数字中的前 11 位量词只保证位数不保证整串长度首尾加锚点^1[3-9]\d{9}$嵌套量词导致接口卡死灾难性回溯尝试次数指数级增长改写表达式避免量词套量词6.2 写量词前先问自己三个问题每次写含量词的正则时我建议你先在心里过一遍这几个问题能省去大量后期调试第一这个部分到底能不能是“0次”如果能用*或?如果必须出现至少一次用或{1,}。这个区分直接决定了空字符串会不会误通过校验。第二我要重复的是“一个字符”还是“一组字符”如果是一组必须加括号。很多身份校验的 bug 都出在这个上面比如匹配时间格式\d{2}:\d{2}:\d{2}和(\d{2}:){2}\d{2}前者写了三遍后者用分组加量词二者意思等价但后者更容易改成“支持更多段”的格式。第三这个量词我希望它贪婪还是懒惰默认是贪婪。如果匹配结果超出了预期范围第一反应应该是把.*改成.*?而不是急着加各种奇怪的字符类去“修正边界”。6.3 测试与调试的实用建议最后聊点调试工具和工作习惯。我建议所有做开发的人都在本地或浏览器里备一个正则测试工具这类工具能实时显示匹配结果和分组捕获。测试时务必覆盖这些边界输入空字符串刚好等于最小次数的输入刚好等于最大次数的输入超过最大次数的输入包含换行符的输入包含中文、空格、特殊符号的输入。很多人写完正则只测“正常情况”结果在空字符串和超长输入上翻车。尤其是把正则直接用于生产环境做参数校验时这些边界情况几乎必然会遇到。另外正则表达式在不同语言里的行为细节有差异比如回溯限制、修饰符支持、命名分组语法等。在一个引擎里测试通过不代表在另一个引擎里行为完全一致。如果你要跨语言复用同一个表达式最好在目标语言的实际环境里各测一遍别偷懒。我在实际项目中还养成了一个习惯核心的正则表达式会写注释或放在配置里而不是散落在代码各处。这样做的好处是后续维护的人能快速知道这个表达式在匹配什么、为什么这样写、量词的含义是什么。正则本身已经是出了名的“写时爽、看时哭”多一点注释就是给未来的自己少留一点坑。