ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Excel VBA正则表达式实战:从杂乱文本中高效提取与清洗数据

Excel VBA正则表达式实战:从杂乱文本中高效提取与清洗数据 上周帮同事处理一张四千多行的订单备注表备注栏长这样“张三 138****1234 上海市浦东新区XX路1号 购买3件白色L码”。他原本打算用Excel自带的查找替换、分列、LEFT加MID函数去硬拆搞了一个下午拆到后面发现手机号后面还跟着“收货”地址里面又嵌着逗号整个表格严重错位。我打开文件写了十几行VBA配合正则表达式十秒不到姓名、电话、地址、件数、颜色尺码全部拆进独立列里同事愣住问我是不是用了什么外挂。这就是Excel VBA和正则表达式组合起来最典型的价值。正则表达式这套匹配文本的规则解决的核心问题就是“字符串不规整”你要从一段乱七八糟的文本里提取手机号、金额、日期、身份证号要按照规则批量替换掉多余内容要判断某段文字是否符合格式——这些活如果全靠手工或者纯函数硬凑写出来的公式又绕又脆而交给正则往往一两行代码就结束了。这一章内容对两类人最有用一类是已经会VBA基础循环和单元格操作、但遇到复杂字符串就头疼的人另一类是写过一点正则、但不知道怎么在VBA环境里落地、总是遇到报错的人。前几章我们聊过数组、字典和ADO的数据处理思路这一章换个方向专门研究文本处理的终极武器。1. 为什么正则这么“香”Excel原生文本方案的三个硬伤1.1 查找替换和分列解决不了“结构不统一”的文本先说一个最现实的场景你的表格里有一列“备注”内容长这样张三 13812341234 上海市浦东新区XX路1号 3件白色L码。这一列看起来好像有规律但仔细一看有的行用空格分隔有的行用逗号分隔有的行中间还混着“联系人张三”这样的前缀。这时候你用Excel的“分列”功能指定空格作为分隔符结果第一行拆对了第二行拆出来5列第三行又变成2列整个表直接乱套。查找替换就更不用说了。查找替换的本质是“精确匹配”或“通配符匹配”它能处理“把所有‘张三’改成‘李四’”这种单一规则但处理不了“把这段文字里所有13开头的11位数字都找出来”这种结构性需求。通配符里的*和?能力太弱*只能表示“任意一串字符”你没法告诉它“必须是数字”“必须刚好11位”“前面不能跟其他数字”这样的约束。所以只要数据没有统一的格式手工肉眼拆就是无底洞几千行能折腾一整天。1.2 工作表函数能应付一部分但写起来像天书有基础的朋友可能会说用LEFT、MID、RIGHT、FIND、SUBSTITUTE这些函数组合一下不就行了确实简单的场景可以。比如固定格式的“订单号ORD12345”你知道“ORD”后面跟6位数字那用MID(A2,4,6)就能提出来。但问题在于一旦条件不固定公式就开始爆炸。我见过有人写过一个提取金额的公式大概是IF(ISNUMBER(FIND(元,A2)),MID(A2,FIND(元,A2)-6,6),)这种还要嵌套IFERROR、要判断小数点在不在、要判断负数有没有括号最后写出来一个粮票一样长的公式维护的人看到就想离职。更关键的是函数方案一次性只能解决“这一列”的问题换一个文本格式整个公式推倒重写。正则表达式的思路完全不同。它不关心文本的“位置”只关心文本的“结构”。比如你要提取金额模式就是“可能有负号、后面跟数字、可能有小数部分”这和你文本前面是“实付”还是“应付”还是“价格”没有关系。用结构描述替代位置计算是文本处理思维上的一次升级。1.3 这套引擎是Windows自带的不需要额外装插件很多人以为在Excel里用正则要装什么插件或者加载项其实完全不需要。VBA调用的是微软VBScript正则表达式引擎这个组件是Windows系统自带的COM对象注册名是VBScript.RegExp。从Excel 2007到Office 365从32位到64位基本都稳定可用。你在VBA里写一句CreateObject(VBScript.RegExp)正则能力就直接到手了。这个方案还有个隐藏优势跨宿主程序复用。同一套正则逻辑你在Excel的VBA里能用在Access的VBA里也能用在WPS的宏编辑器里大概率也能用前提是WPS版本支持宏。我曾经把一套文本清洗的VBA代码从Excel原封不动搬到Access里处理数据库字段正则部分一行没改效果完全一致。所以花时间学这一章收益不只是“会处理Excel表格”而是掌握了一套通用的文本处理能力。2. 第一次跑通VBA正则对象、属性和四步调用套路2.1 前期绑定还是后期绑定先决定代码的写法在VBA里调用正则有两种写法。第一种是“前期绑定”打开VBA编辑器点菜单“工具→引用”勾选Microsoft VBScript Regular Expressions 5.5然后代码里直接写Dim reg As New RegExp。这样写的好处是写代码的时候有自动提示输入reg.会弹出属性方法列表不容易拼错。坏处是万一换了一台没有勾选这个引用的电脑代码会直接报“用户定义类型未定义”对新手不友好。第二种是“后期绑定”不勾选引用直接用CreateObject创建对象。代码写成Dim reg As Object、Set reg CreateObject(VBScript.RegExp)。这样写的坏处是失去了自动提示好处是代码到哪里都能跑。我个人建议写工具类的VBA代码一律用后期绑定。特别是你想把工作簿发给同事用或者你自己经常在不同电脑间切换后期绑定能少一堆莫名其妙的兼容问题。Sub CreateRegExp() 后期绑定创建正则对象 Dim reg As Object Set reg CreateObject(VBScript.RegExp) 设置匹配模式 reg.Pattern \d 匹配连续数字 reg.Global True 全局匹配而不是只匹配第一个 reg.IgnoreCase True 忽略大小写 测试 End Sub这里有一个新手常踩的坑reg.Pattern里面写反斜杠比如\d在VBA字符串里不需要写成\\d因为VBA里的反斜杠没有转义作用。很多人从Python或者Java转过来习惯性地双写反斜杠结果正则匹配不到任何内容还以为自己模式写错了。记住VBA里\d就是\d直接写。2.2 五个属性和方法覆盖所有正则操作RegExp对象的完整接口不复杂核心就三个属性加三个方法。成员类型作用Pattern属性设置或返回正则表达式模式Global属性是否全局匹配。False时只匹配第一个结果True时匹配所有结果IgnoreCase属性是否忽略大小写默认FalseMultiLine属性是否把字符串视为多行让^和$匹配每行的开头和结尾Test方法测试字符串是否匹配返回True或FalseExecute方法执行匹配返回MatchCollection集合Replace方法按模式替换字符串支持$1等分组引用Test适合做判断比如“这个字符串里有没有手机号”Execute适合做提取比如“把这段文字里所有手机号捞出来”Replace适合做清洗比如“把日期格式统一成2024-01-05”。这三个方法各管一摊80%的文本处理需求都能覆盖。剩下20%的复杂场景基本就是Execute提取后配合数组、字典做二次加工。2.3 第一个能跑的例子从一行文本里抓出所有手机号写一个实际可运行的例子。假设A列是订单备注B列开始输出提取到的手机号。手机号用最简单的规则1开头第二位是3到9后面再跟9位数字。正则模式就是1[3-9]\d{9}。Sub ExtractPhone() Dim reg As Object Dim matches As Object Dim m As Object Dim r As Range Dim outRow As Long Dim strText As String 正则对象放循环外避免重复创建 Set reg CreateObject(VBScript.RegExp) reg.Pattern 1[3-9]\d{9} reg.Global True outRow 1 For Each r In Range(A1:A Range(A Rows.Count).End(xlUp).Row) strText r.Value If reg.Test(strText) Then Set matches reg.Execute(strText) For Each m In matches Cells(outRow, 2).Value m.Value outRow outRow 1 Next m End If Next r End Sub这段代码里有两个关键细节。第一Set matches reg.Execute(strText)执行一次能拿到这个单元格里所有匹配项所以要用For Each m In matches去遍历m.Value就是匹配到的手机号字符串。第二reg对象必须在For Each循环外创建如果每循环一个单元格就创建一次正则对象四千行跑下来性能会差好几倍。这个习惯一旦养成了后面处理大数据量会少踩很多坑。跑完这个例子你可能会在“立即窗口”用Debug.Print reg.Replace(电话13812341234, [隐藏])测试替换效果也可以试试把Global改成False看看匹配结果差异你会发现GlobalFalse时Execute只返回第一个手机号这就是为什么我代码里习惯性把GlobalTrue写上的原因。3. VBA正则语法速查这几张表够你应付八成场景3.1 常用元字符和量词一览这一节把最常用的正则语法过一遍。别被符号吓到核心逻辑其实就三条[]表示“这一个位置可以是哪些字符”量词表示“前面那个字符出现多少次”()表示“把这一段打包成一个整体”。理解了这三条正则你就入门了。语法含义示例\d匹配一位数字\d\d\d匹配三位数字\w匹配字母、数字、下划线注意只匹配ASCII字符\w匹配一串单词字符\s匹配空白字符包括空格、制表符、换行\s匹配连续空白.匹配除换行符外的任意字符a.c匹配abc、a1c^匹配字符串开头^\d表示以数字开头$匹配字符串结尾\d$表示以数字结尾*前面的字符出现0次或多次ab*匹配a、ab、abb前面的字符出现1次或多次ab匹配ab、abb?前面的字符出现0次或1次ab?匹配a、ab{n}前面的字符出现正好n次\d{4}匹配四位数字{n,}前面的字符出现至少n次\d{2,}匹配两位以上数字{n,m}前面的字符出现n到m次\d{1,3}匹配1到3位数字[]字符类匹配其中任意一个字符[abc]匹配a、b、c[^]否定字符类[^0-9]匹配非数字()分组(ab)匹配ab、abab|或a|b匹配a或b一个新手特别容易困惑的地方.不匹配换行符。所以如果你在处理一段跨行的文本想要“匹配任意字符”不能用.*要用[\s\S]*或者[\s\S]。\s匹配空白\S匹配非空白两者合在一起就包含了所有字符。这个坑在抓取网页文本时特别常见网页源码里到处都是\r\n换行你写div.*/div匹配不到必须用div[\s\S]*/div。再补充一个中文匹配的关键点\w在VBScript正则里不会匹配中文。你写\w去匹配“张三李四”结果是匹配不到的。要匹配中文可以直接把起止字符写进字符类[一-龥]。这个范围的原理是中文字符在Unicode编码里是连续分布的从一U4E00到龥U9FA5。这是VBA正则处理中文最实用的写法建议直接背下来。3.2 分组捕获和SubMatches提取完整信息的钥匙正则里的括号不仅是“打包”还有一个重要能力叫“捕获分组”。比如要匹配“商品名称白色T恤 价格199元”里的名称和价格可以写模式商品名称(.?)\s价格(\d元)括号里捕获到的内容会按顺序存进SubMatches集合SubMatches(0)对应第一个括号SubMatches(1)对应第二个括号。看一个实际提取身份证号出生日期例子。身份证号是18位中间的8位是出生日期比如110101199003077733。Sub ExtractBirthday() Dim reg As Object Dim matches As Object Dim m As Object Dim strText As String Set reg CreateObject(VBScript.RegExp) reg.Pattern (\d{6})(\d{4})(\d{2})(\d{2})\d{4} reg.Global True strText 客户身份证号110101199003077733请核对 Set matches reg.Execute(strText) For Each m In matches Debug.Print 出生年份 m.SubMatches(1) Debug.Print 出生月份 m.SubMatches(2) Debug.Print 出生日期 m.SubMatches(3) Next m End Subm.SubMatches(1)取到的就是出生年份分组(\d{4})里的内容。注意SubMatches是一个从0开始索引的集合第一个括号对应0第二个对应1跟数组的习惯一致。这个写法在处理“一条文本里夹着多个结构化信息”时特别高效比你先用Mid数位置、再算偏移量要可靠得多。3.3 VBScript正则的三个语法限制避免“复制粘贴就报错”如果你之前是在Python的re模块或者JavaScript里学的正则转到VBA会碰到几个不支持的语法。这里集中说免得你从网上复制一段现成正则过来直接报运行时错误5017。第一VBScript正则不支持字符串的\uXXXX写法。在Python里你可以写[\u4e00-\u9fa5]匹配中文在VBA里这样写无效必须直接写中文字符区间[一-龥]。同理\p{L}这种Unicode属性写法也不支持。第二VBScript正则不支持后行断言也就是(?...)这种结构。向前断言(?...)和负向前瞻(?!...)是支持的但“匹配前面必须是某字符”的能力没有。替代方案通常是连上下文一起匹配出来再用SubMatches或者Replace把多余部分去掉。比如想提取价格199元里的数字写成价格(\d)元取第一个分组效果等价。第三VBScript正则不支持非捕获分组(?:...)和命名分组(?name...)。所有括号都会被当成捕获组参与编号。所以网上很多精细优化的正则直接搬进来轻则不支持重则理解错导致结果不对。我处理这类问题的方法是拿到别人的正则先扫一遍有没有这三个语法有就先简化重写不要抱着侥幸心理测试。4. 四个高频场景实战提取、清洗、分类都能用正则4.1 批量提取金额带小数、带负号一次搞定财务对账经常遇到“退款-12.50元”这种字样要快速汇总金额。正则模式-?\d(\.\d)?可以覆盖“可选的负号、整数部分、可选的小数部分”。如果想连同千分位一起处理比如“1,234.56”模式改成-?\d{1,3}(,\d{3})*(\.\d)?。Sub ExtractAmount() Dim reg As Object Dim matches As Object Dim m As Object Dim r As Range Dim total As Double Dim i As Long Set reg CreateObject(VBScript.RegExp) reg.Pattern -?\d(\.\d)? reg.Global True For Each r In Range(A1:A Range(A Rows.Count).End(xlUp).Row) Set matches reg.Execute(r.Value) For Each m In matches 转为数值累加 total total Val(m.Value) i i 1 Next m Next r MsgBox 共提取到 i 笔金额合计 total End Sub这里有一个隐藏细节Val(m.Value)会自动处理字符串开头可能带的正负号但如果金额是负数且用了括号表示比如(12.50)Val函数不会把括号识别成负数正则得预先处理。我一般先做一步reg.Replace把括号形式替换成负号形式再跑提取。这类“先清洗、再提取、后计算”的三段式流程是文本处理项目里的标准动作。4.2 日期清洗把各种不规整写法统一成标准格式导出来的Excel日期经常五花八门2024.1.5、2024-01-05、2024年1月5日、2024/01/05。如果直接用Excel的单元格格式去改很多是文本字符串根本不认。用正则把它们统一成2024-01-05算是最经典的正则替换案例。模式这样写(\d{4})[年.\-/](\d{1,2})[月.\-/](\d{1,2})日?。拆解一下第一个分组捕获4位年份接下来匹配“年、点、横杠、斜杠”任意一个分隔符第二个分组捕获1到2位月份再匹配“月、点、横杠、斜杠”任意一个分隔符第三个分组捕获1到2位日期最后的日?表示“日”字可以出现也可以不出现。Sub NormalizeDate() Dim reg As Object Dim r As Range Dim result As String Dim y As Long, m As Long, d As Long Set reg CreateObject(VBScript.RegExp) reg.Pattern (\d{4})[年.\-/](\d{1,2})[月.\-/](\d{1,2})日? reg.Global True For Each r In Range(A1:A Range(A Rows.Count).End(xlUp).Row) If reg.Test(r.Value) Then 从第一次匹配的分组里取出年月日 y CLng(reg.Execute(r.Value)(0).SubMatches(0)) m CLng(reg.Execute(r.Value)(0).SubMatches(1)) d CLng(reg.Execute(r.Value)(0).SubMatches(2)) r.Value Format(DateSerial(y, m, d), yyyy-mm-dd) End If Next r End Sub这里用DateSerial函数组装日期比手工拼接字符串补零要稳妥。1月会被Format自动补成017日补成07。注意代码里我把reg.Execute(r.Value)(0)重复用了两遍最理想的做法是先把匹配对象存进一个变量减少重复调用代码更流畅。我写这段是为了先讲清楚逻辑实际项目里不会这样连写三遍Execute。4.3 正则加字典给备注文本打标签并快速分类统计正则和VBA字典是黄金搭档。字典负责“键值对统计”正则负责“从杂乱文本里识别标签”。比如一张工单表备注里可能写着“客户要求加急”“订单备注促销活动”“普通申请”等你想统计各标签出现次数。Sub CountTags() Dim reg As Object Dim matches As Object Dim m As Object Dim dict As Object Dim r As Range Set reg CreateObject(VBScript.RegExp) reg.Pattern 紧急|加急|普通|促销|退款 reg.Global True Set dict CreateObject(Scripting.Dictionary) For Each r In Range(A1:A Range(A Rows.Count).End(xlUp).Row) If reg.Test(r.Value) Then Set matches reg.Execute(r.Value) For Each m In matches If dict.Exists(m.Value) Then dict(m.Value) dict(m.Value) 1 Else dict.Add m.Value, 1 End If Next m End If Next r 输出统计结果到D列 Dim i As Long Dim key As Variant i 1 For Each key In dict.Keys Cells(i, 4).Value key Cells(i, 5).Value dict(key) i i 1 Next key End Sub这段代码最大的价值在于词典的Keys()天然去重统计结果直接写回单元格比用CountIf一列一列去数不知道快多少。而且正则模式里的|表示“或”你可以随时加新标签比如加一个“投诉”正则改成紧急|加急|普通|促销|退款|投诉就行。维护成本几乎为零这在Excel函数方案里很难做到。4.4 网页抓取数据后的文本清理去标签、提取链接很多朋友做网页数据下载时把HTML源码直接粘进了单元格里面全是div、span、class...这些标签。正则清理HTML是这个场景的看家本领。最简单的清理HTML标签模式就是[^]。意思是“一个小于号、后面跟一个或多个非大于号字符、最后加一个大于号”。Replace成空字符串标签全没了。注意[^]这个写法很关键它保证了匹配到的是“最短的”标签不会把整个HTML大段误删。提取链接是另一个高频需求。假设网页源码里有a hrefhttp://example.com/page title某页面想提取href属性里的网址模式写href([^]*)。取SubMatches(0)就是链接地址。这里[^]*的意思是“在双引号里面匹配任意多个非双引号字符”这样链接里即使有空格也能完整捕获。需要提醒的是正则处理HTML只适用于简单、可控的页面结构。如果你要解析结构复杂、嵌套严重、属性顺序不固定的HTML用正则硬写会非常痛苦不如用XMLHTTP请求后换专业的解析工具或者调用HTMLDocument对象去遍历DOM。VBA环境里有现成的MSHTML库可以解析HTML文档对象那才是重型场景的正道。正则适合“快速清洗文本、抓取字段”不必死磕到底。5. 正则排查清单报错、匹配不到、跑得慢都能快速定位5.1 常见报错和结果不对的原因速查现象常见原因解决办法运行时错误5017正则语法错误检查括号是否配对、[是否闭合、量词是否放到无意义位置明明有内容却匹配不到模式里用了\u或其他不支持的语法改成[一-龥]这种VBA支持的写法只匹配到第一个结果Global是False设置reg.Global True替换结果把整段文字都删了贪婪匹配导致匹配范围过大用.*?或[^]*这类非贪婪写法从网页文本提取总是失败文本里有换行.不匹配换行用[\s\S]*替代.*中文匹配不到\w和\d不覆盖中文直接用[一-龥]范围匹配Replace之后有$1没被替换分组数量对不上确认模式里的括号数量和$1的编号对应关于贪婪匹配多说两句。模式.*在匹配a123/a时因为.*会尽量吃更多字符所以匹配到的结果是a123/a整个字符串而不是a。想让匹配停下来要么用[^]*限定“只能是非大于号字符”要么用*?推进非贪婪匹配。两者在VBA正则里都有效我实际项目中用[^]*更多因为意图更明确避免某些场景下非贪婪的边界行为跟预想不一致。5.2 宏被禁用、加载项被禁用的处理办法正则写得再好如果文件打开就提示“宏已被禁用”代码根本跑不起来。Excel默认的安全策略是禁用所有宏所以你需要把文件另存为xlsm格式然后去“文件→选项→信任中心→信任中心设置→宏设置”里选择“启用所有宏”。自己写的工具启用宏是没问题的但要留意不要随意运行来路不明的工作簿宏。如果你在Excel里遇到“加载项被禁用”的提示多半是某个COM加载项启动时报错被Excel自动禁用了。处理路径是“文件→选项→加载项→管理COM加载项→转到”在列表里重新勾选被禁用的项并确定。有时候加载项之间还会打架表现为快捷键失灵比如CtrlC和CtrlV用不了或者工具栏选项消失。我遇到过装了一个PDF导入工具后整个Excel的快捷键全失效的情况禁用那个加载项后恢复正常。这类问题跟代码本身无关但排查的时候容易耽误时间知道路径能少走弯路。还有一个小细节修改VBA代码后如果Excel提示“是否保存对VBA工程所做更改”记得保存并且该文件必须是xlsm格式才能保留宏代码。如果你另存为xlsx代码会静默丢失下次打开只剩数据。这个坑很多人碰到过一次就再也不会忘。5.3 几千行数据跑得慢的优化技巧正则本身执行速度是很快的感觉慢通常是VBA的瓶颈。最常见的问题是在循环里反复操作单元格频繁读写工作表。优化思路有三个。第一正则对象创建一次不要放在循环里。一个CreateObject虽然开销不大但几千行每行都创建一次慢得肉眼可见。第二把数据一次性读入数组在内存里用正则处理完再批量写回。数组操作比单元格逐行读写快一个数量级这个从我们前面聊VBA数组那章就强调过。第三处理前保存Application.ScreenUpdating False和Application.Calculation xlCalculationManual处理完在Finally或结尾恢复界面不刷新、公式不重算速度提升非常明显。更要警惕的是“灾难性回溯”。如果你写的正则模式里出现了嵌套的重复比如(.*)*、(\d)这样的组合当匹配失败时引擎会尝试指数级的回溯路径小数据感觉不到几千行文本可能直接卡死。遇到模式复杂、跑起来特别慢的情况优先简化量词嵌套把多个*、压缩成单一的[\s\S]*或[^]*通常能救回来。我在实际项目中处理文本类VBA任务已经习惯了“先写一小段测试代码在立即窗口里验证正则模式确认匹配结果无误再套进正式数据处理循环”的工作方式。测正则时用最短的代表性字符串跑通了再上完整数据一次报错排查成本远低于在几千行数据里反复试错。这个小习惯对我个人来说帮了大忙。正则表达式这套东西初次接触会觉得满屏幕符号像乱码但一旦你习惯了“用结构描述文本”的思路再回头用Excel的手工操作和公式拼接就会有一种明显的降维打击感。很多时候我处理一张乱糟糟的导入表前后不超二十分钟一半时间花在理解字段逻辑上另一半就是写正则加跑测试真正到了清洗环节反而是最轻松的。下一章我准备聊聊VBA读写文件其实文件解析、日志分析这些场景正则依然是绝对主力。
返回列表