ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

通配符与正则表达式:从原理到实战,彻底搞懂模式匹配

通配符与正则表达式:从原理到实战,彻底搞懂模式匹配

1. 从两个符号说起:为什么通配符和正则总被混为一谈

刚入行那会儿,我在一台测试服务器上执行了一条删除命令,本意是清理某个临时目录下的日志文件,结果因为把通配符和正则的语法搞混了,差点把整个项目的构建产物全删了。幸好当时有备份,但那次教训让我彻底明白了一件事:通配符和正则表达式虽然长得像亲戚,骨子里却是两套完全不同的东西。

这两个东西在日常工作中出现的频率极高。你在终端里敲ls *.log,用的是通配符;你在代码里写^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$做邮箱校验,用的是正则表达式。它们都涉及“模式匹配”这个概念,但服务的场景、解析的引擎、支持的语法规则完全不同。很多人学的时候觉得“差不多”,用的时候才发现“差很多”。

这篇文章适合谁看?如果你是刚接触命令行的开发新手,或者写了几年代码但一直靠搜索复制正则的“CV工程师”,又或者是需要做日志分析、批量文件处理的运维人员,那这篇内容应该能帮你把这两个东西彻底理清楚。我会从底层原理讲到实操细节,把常见的坑一个个挖出来,让你以后看到*和.*的时候不再犹豫。

核心关键词就三个:通配符、正则表达式、模式匹配。围绕这三个词,我们把整个知识体系拆开揉碎。

2. 通配符的本质:Shell 层面的文件名匹配工具

2.1 通配符到底是谁在解析

很多人以为*是操作系统内核认识的东西,其实不是。通配符是由 Shell 解析的,也就是说,当你在终端输入ls *.txt的时候,Shell 会先扫描当前目录,把所有以.txt结尾的文件名找出来,替换到命令里,然后才把替换后的结果交给ls去执行。这个过程叫globbing,中文一般叫“文件名通配”或“路径名扩展”。

理解这一点非常关键。因为这意味着通配符的能力边界完全由 Shell 决定,跟具体命令没关系。ls、cp、mv、rm这些命令本身并不认识*,它们收到的已经是展开后的文件列表了。

你可以做个简单验证:在一个空目录下执行echo *.txt,如果目录里没有匹配的文件,大多数 Shell 会把*.txt原样输出,而不是报错。这就说明 Shell 在展开的时候发现没有匹配项,于是保留了原始字符串。

2.2 两个核心通配符:问号和星号

通配符体系里最常用的就是两个符号:

  • ?(问号):匹配任意单个字符。比如file?.txt可以匹配file1.txt、fileA.txt,但匹配不了file12.txt。
  • *(星号):匹配任意零个或多个字符。比如*.log可以匹配a.log、error_2024.log,甚至单独的.log(因为零个字符也算)。

这两个符号的语义非常直观,但实际使用中有几个容易忽略的细节。

第一,*在通配符里不匹配路径分隔符/。也就是说*只能匹配当前目录层级的文件名,不能跨目录。如果你想递归匹配,需要用**(在某些 Shell 中需要开启globstar选项)或者配合find命令。

第二,?严格匹配一个字符,不多不少。file?.txt不会匹配file.txt,因为问号必须对应一个实际存在的字符。

第三,通配符的匹配是大小写敏感的,取决于文件系统的设置。在 Linux 的 ext4 文件系统上,*.TXT和*.txt是不同的模式。

2.3 扩展通配符与字符类

除了?和*,Shell 还支持一些扩展语法:

  • [abc]:匹配方括号内的任意一个字符。file[123].txt匹配file1.txt、file2.txt、file3.txt。
  • [a-z]:匹配指定范围内的任意一个字符。file[a-c].txt等价于file[a,b,c].txt。
  • [!abc]或[^abc]:匹配不在方括号内的任意一个字符。注意不同 Shell 对!和^的支持不一样,bash 里两个都支持,但 POSIX 标准只规定了!。
  • {a,b,c}:大括号扩展,匹配列举的任意一个完整字符串。file.{txt,log,md}会展开成file.txt file.log file.md三个独立的参数。

这里要特别提醒:大括号扩展{}和其他通配符的展开时机不同。大括号扩展发生在通配符展开之前,而且它不检查文件是否存在。echo file.{txt,log}不管文件存不存在都会输出file.txt file.log,而echo file.[tl]*如果没匹配到文件,可能就原样输出了。

2.4 通配符的典型应用场景

通配符最常出现在这些场景里:

  • 批量文件操作:rm *.tmp、cp *.jpg /backup/、mv report_*.pdf archive/
  • 查找特定类型文件:ls -la /var/log/*.log
  • 配合 find 命令:find . -name "*.conf" -type f
  • 构建工具配置:Makefile 里的*.o、.gitignore里的*.class

注意:在find命令中使用通配符时,模式需要用引号包起来,否则 Shell 会先展开,导致find收到的是已经展开的文件列表而不是模式本身。这是一个非常高频的错误。

3. 正则表达式:文本处理的通用模式语言

3.1 正则的定位与解析引擎

如果说通配符是 Shell 的“文件名筛选器”,那正则表达式就是文本内容的通用模式匹配语言。它不依赖 Shell,而是由各种编程语言、文本处理工具自己实现的解析引擎来执行。Python 的re模块、JavaScript 的RegExp对象、grep 命令、sed 命令,背后都是各自的正则引擎。

正则的表达能力远超通配符。通配符只能做简单的文件名匹配,而正则可以做复杂的文本提取、替换、校验、分组捕获等操作。代价就是学习曲线更陡,语法更复杂,坑也更多。

3.2 基础元字符速查

正则的元字符比较多,我按使用频率分组列一下:

元字符含义示例
.匹配任意单个字符(除换行符外)a.c匹配abc、a1c
*前一个元素重复零次或多次ab*c匹配ac、abc、abbbc
+前一个元素重复一次或多次ab+c匹配abc、abbbc,不匹配ac
?前一个元素重复零次或一次ab?c匹配ac、abc
^匹配字符串开头^hello匹配以 hello 开头的行
$匹配字符串结尾world$匹配以 world 结尾的行
\d匹配数字,等价于[0-9]\d{3}匹配三个数字
\w匹配字母、数字、下划线\w+匹配一个或多个单词字符
\s匹配空白字符\s+匹配一个或多个空格/制表符
[]字符类,匹配其中任意一个[aeiou]匹配任意元音字母
()分组,也用于捕获(ab)+匹配ab、abab
``或运算
{n,m}重复次数范围a{2,4}匹配aa、aaa、aaaa

这张表建议存下来,写正则的时候对着看,比每次去搜索快得多。

3.3 贪婪匹配与非贪婪匹配

这是正则里最容易踩的坑之一。默认情况下,*、+、{n,m}都是贪婪的,也就是说它们会尽可能多地匹配字符。举个例子:

假设有字符串<div>hello</div><div>world</div>,你用<div>.*</div>去匹配,结果会匹配整个字符串,而不是两个独立的 div。因为.*贪婪地吃掉了从第一个<div>到最后一个</div>之间的所有内容。

解决办法是在量词后面加?,变成非贪婪模式:<div>.*?</div>。这样它会尽可能少地匹配,第一次遇到</div>就停下来。

实操心得:处理 HTML 或 XML 这类嵌套结构时,正则本身就不是最佳工具。但如果非要用,记得默认加?变成非贪婪,否则结果往往不是你想要的。

3.4 分组、捕获与反向引用

圆括号()在正则里有两个作用:一是把多个元素组合成一个整体,二是捕获匹配到的内容供后续使用。

比如(\d{4})-(\d{2})-(\d{2})匹配日期格式2024-01-15,第一个捕获组是2024,第二个是01,第三个是15。在 Python 里可以用match.group(1)来获取。

反向引用则是用\1、\2来引用前面捕获组的内容。比如(\w+)\s+\1可以匹配重复的单词,像hello hello。

这个功能在文本替换里特别有用。比如把2024-01-15格式改成15/01/2024,用 sed 可以这样写:

echo "2024-01-15" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/'

3.5 POSIX 正则与 PCRE 的区别

很多人不知道正则其实有好几个流派。最基础的是POSIX 基本正则(BRE)和POSIX 扩展正则(ERE),然后是PCRE(Perl Compatible Regular Expressions)。

区别在哪?BRE 里+、?、{}这些量词需要加反斜杠才能生效,而 ERE 不需要。PCRE 则支持更多高级特性,比如\d、\w、\s、非贪婪匹配、前瞻断言等。

grep 默认用的是 BRE,加-E参数切换到 ERE。Python 的re模块、JavaScript 的 RegExp 基本都遵循 PCRE 风格。所以你在 Python 里写的\d+直接拿到 grep 里用,可能就不生效,需要改成[0-9]\+或者加-E参数。

4. 通配符与正则的核心差异对照

4.1 同一个符号,不同的含义

这是最容易混淆的地方。*在通配符里表示“任意多个任意字符”,但在正则里表示“前一个元素重复零次或多次”。?在通配符里表示“任意一个字符”,在正则里表示“前一个元素出现零次或一次”。

符号通配符含义正则含义
*匹配任意多个任意字符前一个元素重复零次或多次
?匹配任意一个字符前一个元素重复零次或一次
.普通字符,就是点本身匹配任意单个字符
[abc]匹配 a、b、c 中任意一个同左,含义一致
{n,m}大括号扩展,列举字符串前一个元素重复 n 到 m 次

看这个表就能明白,为什么把正则语法直接拿到 Shell 里用会出问题。比如你想在 Shell 里匹配所有以.txt结尾的文件,写*.txt是对的;但如果你在 Python 里写*.txt去做正则匹配,*前面没有元素,正则会报错或者行为异常。

4.2 解析层级与作用范围

通配符工作在Shell 层,作用于文件名和路径。它不关心文件内容,只关心文件名长什么样。

正则工作在应用层,作用于文本内容。它不关心文件名,只关心字符串里的字符序列。

这个差异决定了两者的使用场景完全不重叠。你要批量重命名文件,用通配符;你要从日志里提取 IP 地址,用正则。当然,有些工具会同时涉及两者,比如find命令的-name参数用通配符,-regex参数用正则。这时候就要特别注意你写的是哪种语法。

4.3 转义规则的差异

通配符里需要转义的字符很少,基本上只有*、?、[、]这几个。而且转义方式统一用反斜杠\。

正则里需要转义的字符就多了:.、*、+、?、(、)、[、]、{、}、^、$、|、\。这些字符在正则里都有特殊含义,想匹配它们本身就得加反斜杠。

常见问题:在 Shell 里写正则的时候,反斜杠本身也可能被 Shell 解析一次,所以有时候需要写双反斜杠\\。这就是所谓的“双重转义”问题。建议在 Shell 里使用正则时,尽量用单引号包裹模式字符串,避免 Shell 提前解析。

5. 实操场景:从文件名批量处理到日志提取

5.1 场景一:批量重命名照片文件

假设你有一批照片,命名格式是IMG_20240101_001.jpg到IMG_20240101_999.jpg,你想把日期部分去掉,只保留序号。

用通配符配合 Shell 循环可以这样写:

for f in IMG_20240101_*.jpg; do newname=$(echo "$f" | sed 's/IMG_20240101_//') mv "$f" "$newname" done

这里IMG_20240101_*.jpg是通配符,负责筛选文件;sed里的s/IMG_20240101_//是正则替换,负责去掉日期前缀。两者各司其职,配合得很自然。

5.2 场景二:从日志中提取所有 IP 地址

日志文件里每行可能包含多个 IP,格式不固定。用正则来提取:

grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' access.log | sort | uniq -c | sort -rn

这条命令拆开看:grep -oE表示用 ERE 正则、只输出匹配部分;\b是单词边界,防止匹配到更长的数字串;([0-9]{1,3}\.){3}匹配前三段数字加点的组合;[0-9]{1,3}匹配最后一段。后面的sort | uniq -c | sort -rn是统计每个 IP 出现的次数并按降序排列。

注意:这个正则并不校验 IP 的合法性,999.999.999.999也会被匹配到。如果需要严格校验,正则会复杂很多,建议用专门的 IP 解析库来处理。

5.3 场景三:find 命令中的通配符与正则

find命令同时支持两种模式,但参数不同:

# 使用通配符匹配文件名 find /var/log -name "*.log" -type f # 使用正则匹配完整路径 find /var/log -regex ".*\.log$" -type f

-name用的是通配符语法,-regex用的是正则语法。注意-regex匹配的是完整路径,不是单独的文件名,所以模式里通常需要.*开头。

5.4 场景四:代码中的邮箱格式校验

这是一个经典的正则应用场景:

import re pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' email = "user@example.com" if re.match(pattern, email): print("格式正确") else: print("格式错误")

这个正则的每一部分都有明确意图:^和$锚定首尾,防止部分匹配;[a-zA-Z0-9._%+-]+匹配用户名部分;@匹配分隔符;[a-zA-Z0-9.-]+匹配域名主体;\.[a-zA-Z]{2,}匹配顶级域名,要求至少两个字母。

实操心得:邮箱校验的正则没有“标准答案”,因为邮箱规范非常复杂。上面这个版本覆盖了 99% 的常见情况,但如果你要做严格的 RFC 合规校验,建议用专门的库而不是自己写正则。

6. 常见问题与排查技巧实录

6.1 通配符不生效?先检查这几项

问题一:模式被引号包住了。ls "*.txt"不会展开通配符,因为引号阻止了 Shell 的 globbing。正确写法是ls *.txt或者ls *.txt(不加引号)。

问题二:当前目录没有匹配的文件。大多数 Shell 默认行为是保留原字符串。如果你希望没有匹配时报错,可以设置shopt -s failglob(bash)。

问题三:文件名以点开头。*默认不匹配以.开头的隐藏文件。要匹配隐藏文件,需要显式写.*或者设置dotglob选项。

6.2 正则匹配结果不对?排查思路

症状一:匹配了多余的内容。大概率是贪婪匹配导致的,尝试在量词后加?变成非贪婪。

症状二:该匹配的没匹配上。检查是否需要转义特殊字符,检查^和$是否误用,检查字符类里是否有遗漏的字符。

症状三:在不同工具里行为不一致。确认你用的是 BRE 还是 ERE 还是 PCRE。grep 默认 BRE,grep -E 是 ERE,Python 和 JavaScript 是 PCRE 风格。

症状四:性能很差。避免嵌套量词,比如(a+)+这种模式在某些输入下会导致灾难性回溯。尽量用更精确的字符类代替.。

6.3 速查表:常见错误与修正

错误写法问题正确写法
ls "*.log"引号阻止通配符展开ls *.log
grep "\d+" filegrep 默认不支持\dgrep -E "[0-9]+" file
.*匹配 HTML 标签贪婪匹配吃掉全部.*?非贪婪
[a-z]匹配大写字母范围不含大写[a-zA-Z]
^$匹配空行正确,但注意^和$在 multiline 模式下的行为根据引擎确认
file?.txt匹配file.txt?必须匹配一个字符用file*.txt或file?.txt配合实际文件名

6.4 几个我踩过的坑

第一个坑:在find命令里用-name的时候加了引号,以为引号是必须的,结果发现加不加引号行为不一样。后来才明白,加引号是防止 Shell 提前展开,让find自己处理模式。如果你确定当前目录没有匹配的文件,加不加引号效果一样;但如果有匹配的文件,不加引号就会出问题。

第二个坑:在 Python 里写正则的时候,字符串本身的反斜杠需要转义。比如\d在 Python 字符串里要写成\\d或者用原始字符串r'\d'。我一开始不知道原始字符串的用法,写了很多双反斜杠,后来全部改成r''前缀,清爽多了。

第三个坑:用sed做替换的时候,替换部分里的&有特殊含义,表示整个匹配内容。如果你想输出字面的&,需要写成\&。这个坑我在处理包含&符号的文本时踩过,替换结果完全不对,排查了半天才发现是&的问题。

7. 进阶技巧:把两者用在对的地方

7.1 什么时候用通配符,什么时候用正则

判断标准很简单:操作对象是文件名,用通配符;操作对象是文本内容,用正则。

批量删除临时文件?通配符。从日志里提取时间戳?正则。重命名一批文件?通配符筛选加正则替换。校验用户输入格式?正则。

不要试图用通配符去做正则的事,也不要反过来。虽然有些场景下两者都能实现类似效果,但用对了工具,代码会更简洁,维护成本更低。

7.2 性能考量:正则不是越快越好

正则引擎的回溯机制决定了某些模式在特定输入下会非常慢。比如(a|b)*这种模式,在匹配长字符串时可能产生指数级的回溯。优化思路是尽量用更精确的字符类代替|和.,减少嵌套分组,避免在量词内部使用量词。

如果性能要求很高,可以考虑用字符串的startswith、endswith、in等原生方法代替简单正则。比如判断字符串是否以某个前缀开头,str.startswith()比正则快得多。

7.3 可读性:给正则加注释

复杂的正则建议用扩展模式(Python 里是re.VERBOSE),允许在模式里加空白和注释:

pattern = re.compile(r''' ^ # 字符串开头 [a-zA-Z0-9._%+-]+ # 用户名部分 @ # @ 符号 [a-zA-Z0-9.-]+ # 域名主体 \.[a-zA-Z]{2,} # 顶级域名 $ # 字符串结尾 ''', re.VERBOSE)

这样写出来的正则,三个月后你自己还能看懂。不然面对一长串密密麻麻的符号,回忆成本太高了。

7.4 测试工具推荐

写正则的时候,别硬憋。用在线测试工具实时验证,效率高很多。我常用的有 regex101 和 regexr,前者支持多种语言的正则风格切换,后者界面更直观。不过要注意,在线工具的结果仅供参考,最终还是要以你实际运行环境的行为为准。

另外,Python 里可以用re.DEBUG标志来查看正则的解析过程,对理解正则引擎的工作原理很有帮助:

import re re.compile(r'\d+', re.DEBUG)

这会输出正则的解析树,让你看到引擎是怎么理解你的模式的。

8. 我个人的一些使用习惯

写了这么多年代码和脚本,我在通配符和正则的使用上形成了一些固定习惯,分享出来供参考。

第一,在 Shell 里永远用单引号包裹正则模式。这样能避免 Shell 对反斜杠和特殊字符的二次解析,减少很多莫名其妙的错误。

第二,复杂的正则一定先写测试用例。我会在 Python 里写几个 assert 语句,验证正则对预期输入和边界输入的行为,确认无误后再嵌入到实际代码里。

第三,通配符能搞定的事不用正则。批量文件操作优先用通配符加 Shell 循环,代码更短,可读性更好,性能也更高。

第四,正则里尽量用\d、\w、\s这些简写,除非你需要明确指定字符范围。简写更短,意图也更清晰。

第五,遇到跨行匹配先想想是不是该用别的工具。正则处理多行文本时,不同引擎对.和^、$的处理方式不一样,容易出问题。如果文本结构复杂,用解析器比用正则更靠谱。

最后再分享一个小技巧:如果你不确定某个模式在 Shell 里会被展开成什么,在命令前面加echo先看一眼。比如echo *.log会输出展开后的文件列表,确认无误后再把echo换成实际要执行的命令。这个习惯帮我避免了很多次误操作。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进