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前一个元素重复一次或多次abc匹配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}都是贪婪的也就是说它们会尽可能多地匹配字符。举个例子假设有字符串divhello/divdivworld/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然后是PCREPerl 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 userexample.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 failglobbash。问题三文件名以点开头。*默认不匹配以.开头的隐藏文件。要匹配隐藏文件需要显式写.*或者设置dotglob选项。6.2 正则匹配结果不对排查思路症状一匹配了多余的内容。大概率是贪婪匹配导致的尝试在量词后加?变成非贪婪。症状二该匹配的没匹配上。检查是否需要转义特殊字符检查^和$是否误用检查字符类里是否有遗漏的字符。症状三在不同工具里行为不一致。确认你用的是 BRE 还是 ERE 还是 PCRE。grep 默认 BREgrep -E 是 EREPython 和 JavaScript 是 PCRE 风格。症状四性能很差。避免嵌套量词比如(a)这种模式在某些输入下会导致灾难性回溯。尽量用更精确的字符类代替.。6.3 速查表常见错误与修正错误写法问题正确写法ls *.log引号阻止通配符展开ls *.loggrep \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换成实际要执行的命令。这个习惯帮我避免了很多次误操作。