ARTICLE DETAIL

资讯详情

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

VB.NET+VSTO打造Excel字符串处理插件:正则提取与批量清洗源码解析

VB.NET+VSTO打造Excel字符串处理插件:正则提取与批量清洗源码解析 如果你的工作里经常要跟Excel里的客户姓名、地址、订单号、备注信息打交道那你一定体会过这种场景明明只是一列联系方式里面混着手机号、固话、邮箱甚至带了两串微信号明明只是想把地址里的空格去掉结果连换行符都留在单元格里。字符串清洗这件事Excel自带函数能做一部分但跨列提取、正则匹配、批量规整、处理几万行数据时公式就开始变得鸡肋。这次要分享的“哆哆Excel插件”就是一套围绕字符串操作做的Excel工具箱底层用VB.NET编写完整源代码附在包里。它不是什么大平台就是把日常最磨人的那些字符串处理场景做成几个按钮点一下批量出结果。适合两类人参考一类是每天被Excel脏数据折磨的办公党另一类是正在考虑用VSTO做二次开发、想找个能直接改的例子的.NET开发者。1. 为什么Excel自带函数搞不定字符串清洗1.1 公式能做的和做不了的Excel自带LEFT、RIGHT、MID、TRIM、SUBSTITUTE等字符串函数应付单个单元格的简单处理完全够用。但是数据量一旦到几千行、几万行公式的问题就暴露出来了先要找个空列写公式再下拉填充最后还得“粘贴为值”才能把结果固定。这一步操作链又臭又长而且处理结果一旦公式所在单元格被误删后面的人根本不知道这列数是怎么来的。更麻烦的是没有正则。拿工作里最常见的“从混合字符串里提取手机号”来说Excel公式写法是嵌套一长串MID/FIND遇到不同长度的字符就崩。比如“张三13800001234”和“李四电话13800001234”这两种格式公式很难用一个表达式兼容而正则只需要一个模式1[3-9]\d{9}无论前后有多少无关字符都能一次性提干净。就这一点就足够让我放弃纯公式方案。加上还要处理全角空格、不可见字符、批量替换多列数据纯函数基本是在为难自己。1.2 从VBA到VB.NET插件我为什么改方案可能有人会问为什么不用VBAVBA当然可以做字符串处理最早一版我也是用VBA写的。但是用下来有三个痛点一是VBA里调用正则需要创建RegExp对象虽然能用但语法细节、多行匹配、分组提取的写法跟.NET的Regex差别挺大调试起来不够直观二是VBA的IDE断点调试还行但消息循环和Excel对象互相牵着卡死一次就要重启整个Excel很费时间三是代码不好维护几十个函数塞在Module里时间一长自己都找不到哪段逻辑对应哪个按钮。VB.NET加VSTO提供的是一套更完整的工程化体验。正则表达式是原生的System.Text.RegularExpressions字符串操作可以做成独立类库用单元测试覆盖界面层、业务层、数据访问层能分开调试时可以在Visual Studio里直接看变量、堆栈不会动不动把Excel搞崩。代价也明显插件需要安装.NET Framework和VSTO运行时部署比一个宏文件重但从维护角度看这个代价值得付。对比项VBA宏VB.NET VSTO插件运行环境Excel内置无需额外依赖需要.NET Framework和VSTO运行时正则能力RegExp对象功能受限完整的.NET Regex类支持分组、超时等开发调试IDE调试容易卡死ExcelVisual Studio断点加即时窗口部署方式发放xlsm文件ClickOnce安装支持自动更新维护成本模块堆一起难梳理接口、业务、UI分层易维护这里要说明这不是说VBA不行。如果只是偶尔处理一个小文件VBA仍然是最快路径。但如果要把字符串操作做成一套反复使用的工具给整个团队用还要持续加功能VB.NET插件明显更稳。所以哆哆插件最终选了VB.NET不是追求技术难度而是为了后续少加班。2. 哆哆插件字符串操作功能拆解从核心类到正则实现2.1 功能清单和使用入口设计我做插件前先统计了日常Excel数据清洗群和内部数据录入岗的常见需求最终收敛出几个核心功能空格清理半角空格、全角空格、制表符、不换行空格、零宽字符。正则提取手机号、固话、邮箱、身份证号、日期、金额、邮编、URL、自定义模式。智能替换普通文本替换、正则替换、大小写敏感开关。智能拆分按分隔符、按固定宽度、按汉字/字母/数字边界。合并拼接加前缀后缀、按分隔符合并多列。格式规整大小写转换、全半角转换、数字千分位。数据去重整行去重、按某列去重。这些功能不是拍脑袋加的。做工具时我给自己定了一条原则入口必须十分钟上手。所以主界面不搞复杂向导全部是一套“选中区域 → 点击按钮 → 弹一个小参数框 → 回填结果”的模式。参数框设计也刻意做了减法。正则输入框只有两个一个预置模式下拉框一个自定义表达式输入框。预置模式放在RegexPatterns常量类里用户只需改里面的正则字符串不需要动插件逻辑。这样既照顾不会正则的用户也保留高级能力。下拉框里放了高频模式手机号、固话、邮箱、身份证号、日期、金额、邮编、中文字符、URL等。2.2 正则提取与批量替换的核心代码字符串操作核心逻辑要跟UI完全解耦。我单独建了一个StringOps.vb类所有函数都是Shared方法不依赖Excel对象模型。这样以后想接命令行、接其他入口都能复用。 StringOps.vb - 字符串操作核心类与UI完全解耦 Imports System.Text Imports System.Text.RegularExpressions Public NotInheritable Class StringOps summary 使用正则表达式提取匹配项多个结果用换行拼接 /summary Public Shared Function RegexExtract(input As String, pattern As String, Optional multiline As Boolean False) As String If String.IsNullOrEmpty(input) OrElse String.IsNullOrEmpty(pattern) Then Return input End If Dim options As RegexOptions RegexOptions.Compiled Or RegexOptions.CultureInvariant If multiline Then options options Or RegexOptions.Multiline Dim sb As New StringBuilder() For Each m As Match In Regex.Matches(input, pattern, options) sb.AppendLine(m.Value) Next Return sb.ToString().TrimEnd(vbCrLf) End Function summary 正则替换replacement为空时表示删除匹配部分 /summary Public Shared Function RegexReplace(input As String, pattern As String, replacement As String) As String If String.IsNullOrEmpty(input) OrElse String.IsNullOrEmpty(pattern) Then Return input End If Return Regex.Replace(input, pattern, replacement, RegexOptions.Compiled Or RegexOptions.CultureInvariant) End Function End Class这段代码里有几个细节值得说。为什么用Compiled因为批量场景下这个函数会被调用几千次Compiled模式第一次调用虽然慢但后续命中更快。为什么用CultureInvariant正则匹配如果依赖文化规则比如某些字符的分类不同系统会看到不同结果。字符串工具要尽量保持环境一致避免在同事电脑上跑出不一样的结果。返回值用StringBuilder而不是直接做字符串拼接是因为几万行数据下拼接会产生大量临时字符串内存和耗时都会上升。2.3 编码与全半角看不见的字符如何处理做字符串插件最先踩的坑就是“看不见的字符”。从网页复制、从PDF转Excel、从旧系统导出的数据里最常见的几个问题字符全角空格Unicode U3000、不换行空格U00A0、零宽空格U200B、以及制表符。Excel的TRIM函数只处理普通半角空格对这些几乎不处理。所以插件里增加了一个“清理不可见字符”功能按代码点扫描把空白类字符统一转成半角空格再合并。Public Shared Function NormalizeWhitespace(input As String) As String If String.IsNullOrEmpty(input) Then Return input Dim sb As New StringBuilder(input.Length) Dim pendingSpace As Boolean False For Each ch As Char In input If [Char].IsWhiteSpace(ch) OrElse AscW(ch) 160 OrElse AscW(ch) 12288 Then pendingSpace True Else If pendingSpace Then sb.Append( c) pendingSpace False End If sb.Append(ch) End If Next If pendingSpace Then sb.Append( c) Return sb.ToString().Trim() End Function用生活类比来说这些不可见字符就像是夹在句子里的橡皮屑肉眼看不见但正则命中的时候会突然失效。比如邮箱格式本身没错但因为前后混了个全角空格正则匹配\S\S\.\S到空格处就被切断。所以任何字符串处理流程第一步应该先做规范化再谈提取和替换这一步几乎不耗时但能把后续所有正则的命中率提上来。全半角转换也很重要。很多人输入中文标点或者从手机端复制了全角数字、全角字母。转换逻辑不复杂遍历字符对全角字符ASCII码在第33到126区间内对应全角编码减去0xFEE0转成半角全角空格U3000单独转成普通空格。代码虽短但不处理就会导致后续字符串比较或正则匹配时出现“看起来一模一样正则就是不对”的诡异问题。3. 用VSTO Ribbon把字符串工具集成进Excel项目结构3.1 VSTO项目结构与完整源代码文件清单Visual Studio里新建项目选择Office/SharePoint下的Excel VSTO Add-in语言选VB.NET框架选.NET Framework 4.7.2或4.8。项目创建后自动生成ThisAddIn.vb再把源码文件按职责拆开。哆哆插件的文件组织是这样的ThisAddIn.vb加载项生命周期负责初始化Ribbon。RibbonController.vbRibbon XML的代码入口实现GetCustomUI。Ribbon.xml功能区布局定义对应按钮和分组。StringOps.vb字符串操作核心逻辑与UI完全解耦。RangeDataHelper.vbExcel区域批量读写、数组转换、异常处理。RegexPatterns.vb预置正则表达式常量集中管理。PromptDialog.vb参数输入小窗体用于填写自定义正则和替换文本。这个结构的好处是以后想给同一套字符串操作逻辑换个入口比如做成命令行工具或者接入其他办公软件StringOps和RangeDataHelper可以直接拿过去用。RibbonController和Ribbon.xml只是薄薄一层壳核心逻辑不跟界面绑死。这也是我强调“附源代码”的意义不是给一段能跑的代码就完事而是给一套能拆开改的架构。3.2 Ribbon界面与回调函数Ribbon有两种做法一个是用Visual Studio的Ribbon设计器拖控件另一个是Ribbon XML写布局。设计器上手快但按钮多、逻辑复杂时XML的可维护性明显更强而且XML能用imageMso直接调用Office内置图标不用自己准备一堆图片资源。哆哆插件选了XML方案。customUI xmlnshttp://schemas.microsoft.com/office/2006/03/ribbon ribbon tabs tab idtabDodoString label哆哆字符串工具 group idgrpClean label一键清洗 button idbtnCleanInvisible label清理不可见字符 sizelarge imageMsoDelete onActionOnCleanInvisible/ button idbtnNormalizeWhitespace label空格与全半角 sizelarge imageMsoPasteText onActionOnNormalizeWhitespace/ /group group idgrpExtract label批量提取 button idbtnExtractRegex label自定义正则提取 sizelarge imageMsoFunctions onActionOnExtractRegex/ /group group idgrpReplace label替换合并 button idbtnRegexReplace label正则替换 sizelarge imageMsoReplace onActionOnRegexReplace/ button idbtnMergeColumns label多列合并 sizelarge imageMsoTableTools onActionOnMergeColumns/ /group /tab /tabs /ribbon /customUI要让XML生效RibbonController要继承Office.IRibbonExtensibility并实现GetCustomUI。GetCustomUI从嵌入资源里读取Ribbon.xml每次Excel启动加载插件时会调用一次。Public Class RibbonController Implements Office.IRibbonExtensibility Public Function GetCustomUI(ribbonID As String) As String Implements Office.IRibbonExtensibility.GetCustomUI Dim ns GetType(RibbonController).Namespace Using stream As IO.Stream GetType(RibbonController).Assembly.GetManifestResourceStream(ns .Ribbon.xml) Using reader As New IO.StreamReader(stream) Return reader.ReadToEnd() End Using End Using End Function End Class回调函数签名必须符合VSTO约定就是Sub加ByVal control As Office.IRibbonControl。按钮点击后先拿当前选中区域交给RangeDataHelper处理。这里有个细节Globals.ThisAddIn.Application.Selection拿到的是Object转化成Excel.Range前要判断类型因为用户可能点选了图表或形状直接转Range会抛异常。3.3 Range批量读写与性能优化字符串处理本身很快慢的几乎都是Excel区域操作。最容易犯的错误是一个单元格读取一次、处理一次、写入一次。第一版插件就是这么干的处理5000行数据要一分多钟用户等到怀疑人生。后来改成一次性把整块区域读入二维数组内存里全部算完再一次写回同样5000行耗时降到两秒左右。Public Function ProcessSelection(process As Func(Of String, String)) As Integer Dim app Globals.ThisAddIn.Application Dim selection As Object app.Selection Dim rng As Excel.Range TryCast(selection, Excel.Range) If rng Is Nothing Then Throw New InvalidOperationException(请先选中一个单元格区域) End If Dim valueArray As Object(,) DirectCast(rng.Value2, Object(,)) Dim changedCount As Integer 0 For i As Integer 1 To valueArray.GetLength(0) For j As Integer 1 To valueArray.GetLength(1) Dim raw TryCast(valueArray(i, j), String) If raw IsNot Nothing Then Dim result process(raw) If result raw Then valueArray(i, j) result changedCount 1 End If End If Next Next If changedCount 0 Then rng.Value2 valueArray End If Return changedCount End FunctionValue2只取原始值不取公式和格式存取速度快很多。但有个大坑如果单元格是日期类型Value2返回的是OADate数字比如45123不是字符串。所以做字符串提取前要么把目标列先设置成文本格式要么在处理日期时用rng.Text逐单元格读取成字符串数组。这个分支必须根据数据类型处理。另一个性能点循环里尽量不要访问Excel对象属性连rng.Rows.Count都算COM调用放循环里开销很大。习惯做法是开头把行列数和区域值全部读到本地变量循环里只用本地数组写完回写也只做一次COM调用。这就像搬行李一趟一趟跑不如直接堆到推车上一次运过去。4. 从编译发布到加载排查落地过程全记录4.1 编译配置与ClickOnce部署编译前要确认目标框架。VSTO插件目前在Windows平台主要用.NET FrameworkVisual Studio新建项目时会自动选好版本。我建议保持项目默认的4.7.2或4.8因为Office补丁环境大多已包含。真正的硬性依赖是VSTO运行时如果用户机器上装的是裸Office第一次安装ClickOnce时会提示缺依赖需要勾选一起安装。发布最简单用ClickOnce。右键项目选发布指定一个局域网共享路径或HTTP路径版本号自动增长。安装时用户打开安装页面点一下即可后续更新也是静默拉取。这里有个提醒ClickOnce默认安装路径是用户级的也就是说登录Windows的每个用户都要自己装一遍。如果团队使用共享电脑建议用本机管理员账号统一安装或者接受每个用户各自安装的现实。证书问题也要提前想。没有商业代码签名证书安装时Office会提示“发布者未知”第一次用需要在文件选项加载项里手动勾选。团队内部使用可以自己生成自签名证书并安装到客户机的“受信任的根证书颁发机构”提示就会少很多。这不是技术难事但往往是推送插件时最先被吐槽的点。4.2 加载失败的常见原因与处理现象常见原因处理方法功能区不显示旧版插件残留或多个Excel实例关闭所有EXCEL进程后重启检查COM加载项列表提示“无法创建ActiveX组件”VSTO运行时缺失安装VSTO运行时检查.NET Framework版本按钮置灰当前工作表不是普通工作表在普通Sheet页测试插件可能只处理Range加载项在列表但勾选后自动取消注册表键损坏或权限不对删除HKCU下对应Addins键后重新安装只有当前用户能看到ClickOnce按用户安装换安装方式或对每个用户单独安装最让人崩溃的情况是“别人机器上都好我的Excel就是不出现”。这时候先别急着重装Office按顺序排查任务管理器确认没有EXCEL残留打开文件选项加载项管理COM加载项确认是否在列表且已打勾。如果打勾仍然不出现去注册表确认HKCU\Software\Microsoft\Office\Excel\Addins下注册的Manifest路径是否指向正确位置再检查该位置的dll和manifest文件是否都在。这一步基本能解决九成问题。调试阶段建议在ThisAddIn_Startup里加日志把启动状态写到临时文件。用File.AppendAllText记录当前时间、Excel版本、加载项路径方便远程排障。断点调试时要注意别在Startup里卡太久Excel会认为加载项无响应弹出错框。4.3 Excel版本与位数的兼容性Office 64位和32位在部署上有个容易踩的坑。如果Office是64位插件编译目标必须是x64或AnyCPUx86编译的dll在64位Excel里不会加载。反过来在32位Office里用x64也加载不了。最省心的做法是编译目标选AnyCPUVSTO运行时会按当前Office位数去加载对应环境。如果你手改过平台目标发布前一定要拿两种位数环境各测一遍。还要提醒极老版本。Excel 2007虽然能装VSTO 4 runtime但很多新API不支持Excel 2010以后整体顺畅。哆哆插件的目标场景基本是Excel 2016以上所以代码里没有特意做大量兼容分支如果你的用户群里有老版本需要提前确认。5. 实战记录5000行脏数据字符串标准化处理全过程5.1 接到手的数据长什么样这是一张从老业务系统导出的联系人表5000多行。联系方式一列长这样“张三 13800001234 备注老客户”、“李四0592-1234567”、“王五 邮箱zhangsantest.com 转销售”。有的地方是半角逗号有的地方是全角逗号还有一行出现三个横杠。目标是把姓名、电话、邮箱拆成三列并把多余空格清干净。用公式处理会很枯燥因为格式太乱了。原始内容目标结果张三 13800001234 备注老客户姓名张三手机13800001234李四0592-1234567姓名李四固话0592-1234567王五 邮箱zhangsantest.com 转销售姓名王五邮箱zhangsantest.com这种数据在业务系统导出时非常典型常见规则写不出来因为每一行的字段顺序都不一样。解决思路很直接先标准化格式再用正则按字段类型分别提取最后回填到新列。5.2 插件处理操作步骤与正则参数处理流程分四个步骤全选联系方式列运行“清理不可见字符”再运行“空格与全半角转换”。这一步把全角括号、全角逗号都变成半角为后面的正则扫清障碍。姓名提取。先运行“正则替换”把“备注”“转销售”“等客户”这类高频业务词删掉再运行“自定义正则提取”输入[\u4e00-\u9fa5]{2,4}提取连续的中文字符。分别提取手机号、固话、邮箱。手机号输入1[3-9]\d{9}固话输入0\d{2,3}-?\d{7,8}邮箱输入[A-Za-z0-9._%-][A-Za-z0-9.-]\.[A-Za-z]{2,}。把提取结果分别写入姓名、电话、邮箱三列。插件默认把提取结果写到原区域右侧的新列避免覆盖原始数据。操作时要注意一点如果一格里同时有手机号和固话正则提取会返回多行结果拼接在一起。所以实际操作建议是先提取手机号到B列再在原始列用正则替换删除已提取内容再提取邮箱。分步骤提取字段之间互不干扰准确率更高。5.3 处理结果与复盘最终4000多行能在5分钟内处理完。剩下几百行是原始数据本身就缺信息或格式特别诡异比如手误输入“13 8000001234”数字中间夹了空格。这种情况任何规则都救不回来只能人工补录。插件做的事是把可量化的脏处理压缩到最短把真正需要人判断的异常暴露出来。复盘时有三个经验值得记住。第一预处理永远比后处理重要全半角转换这种基础操作要先做。第二正则模式不要试图一次把全部字段提出来分步骤提取成功率更高。第三提取结果要保留原始行到新列不要覆盖原始列否则万一正则写错原始数据就毁掉了。我在插件设计里默认输出到右侧新列就是为了这个。6. 踩坑速查表我在字符串插件项目里遇到的坑6.1 正则表达式相关的坑贪婪匹配是第一个坑。\d会把连在一起的数字全吞掉提取固定位号码时一定要用精确位数比如\d{11}或者加上边界。例如(?!\d)1[3-9]\d{9}(?!\d)可以防止从更长的数字里截出一段。第二个坑是换行符.默认不匹配换行处理Excel多行文本时要加RegexOptions.Singleline或者用[\s\S]*否则“提取第一行”和“提取整段”的结果会不一样。第三个坑是替换字符串里的$1。在VB.NET的Regex.Replace里$1是分组引用如果用户输入的替换文本里真实包含$1字样结果会被替换为分组内容。想呈现字面$1要写成$$1或者在处理纯文本替换时用Regex.Escape。最后一个建议是给正则加超时。大块文本上正则回溯有时候会爆炸.NET里可以设置匹配超时Regex.Match(input, pattern, options, TimeSpan.FromSeconds(2))超时抛异常避免Excel假死。6.2 COM互操作与性能相关坑不要在循环里反复读Range的Value这是最常见的性能杀手。我在群里见过同事在循环里用cell.Value2逐个读2万行数据跑了十几分钟。数组批量读取是唯一正解。不要在后台线程里写Excel对象。.NET的Task或后台线程操作Excel COM对象会随机崩字符串计算可以放后台但写回Range必须在UI线程。用async/await把计算放Task里计算完后在await后续的UI线程上下文写回实测有效。ReleaseComObject也不是万能的。如果用了数组方案通常不需要逐单元格释放。如果确实大量创建Range对象记住在循环结束后调用Marshal.FinalReleaseComObject然后GC.Collect。但过度使用GC会拖慢程序所以按需处理。6.3 编码与文化差异相关坑大小写转换用ToUpperInvariant()而不是ToUpper()。后者受系统区域影响某些语言环境下特殊字符大小写规则不同同样的输入在不同同事电脑上可能得到不同结果。工具类尽量全部使用Invariant后缀的API保持环境一致。中文字符串比较和排序默认使用区域设置。如果想做严格字符串相等判断使用StringComparer.Ordinal避免“看起来一样实际却被忽略标点合并”的误判。从Access、SQL Server导出的字符串还可能带尾随空格或不可见空字符NormalizeWhitespace那步必须纳入标准流程不能省略。最后再分享一个我在实际项目里反复用的组合拳拿到脏数据第一件事不是直接跑提取按钮而是先执行清理不可见字符和全半角转换。这两步像炒菜前的备菜看着不起眼却决定后面所有正则的命中率。另外这份VB.NET源代码里的正则模式最好单独建一个常量类遇到新格式就在那改不要散落在按钮回调里。字符串操作插件最难的不是写函数而是把规则沉淀成可维护的模式库。把这一步做好后面换什么数据都不会慌。
返回列表