ARTICLE DETAIL

资讯详情

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

WPS JSA正则懒惰匹配:轻松拆分工单数据并转换格式

WPS JSA正则懒惰匹配:轻松拆分工单数据并转换格式 前段时间处理一张客服工单导出表时我碰上一个典型的“看着简单、动手很麻烦”的活儿几百条记录全部堆在 A1 单元格里每条记录有编号、日期、客户、电话、金额、备注中间用竖线分隔可备注字段里又有换行、逗号、甚至多余竖线。我要把它拆成二维表日期转成真正可计算的日期金额转成数值这就是一次典型的“数据拆分且转换”。折腾到最后最关键的转折点是 WPS 的 JSAJS 宏里一个正则写法懒惰匹配。如果你也经常要处理粘贴进表格里的长文本、日志、工单、订单流水这篇文章应该对你有用。我做这套脚本时没有用 VBA直接用 WPS 自带的 JavaScript 宏也就是 JSA代码结构比 VBA 清爽不少正则支持也更符合 JavaScript 的语法习惯。下面我会把思路、代码、踩坑记录都摊开讲不绕弯子。1. 一次真实的工单数据清洗为什么“拆”要比“分列”麻烦先说原始数据长什么样。在 Sheet1 的 A1 单元格里放的是下面这段文本实际当然比这长得多这里只截取三段做演示记录开始|编号O-1001|日期2024-08-21|客户张三|电话18812341234|金额199.00|备注8月21日请工作日送货联系电话确认 记录开始|编号O-1002|日期2024-08-21|客户李四|电话13912345678|金额250.00|备注包装里放一张手写贺卡 记录开始|编号O-1003|日期2024-08-20|客户王五|电话13711112222|金额50.50|备注备注字段跨了两行 第二行也算备注内容不要被换行干扰希望得到的目标表格内容是下面这样的结构编号日期客户电话金额备注O-10012024-08-21张三188123412341998月21日请工作日送货联系电话确认O-10022024-08-21李四13912345678250包装里放一张手写贺卡O-10032024-08-20王五1371111222250.5备注字段跨了两行 第二行也算备注内容第一反应肯定是WPS 自带的分列功能不就能按竖线拆吗确实可以但分列有两个硬伤几千条记录不是一行一条而是全挤在一个单元格里。如果先查换行符备注里偏偏又有换行分列会把记录切得乱七八糟。记录与记录之间没有统一的“行”概念你要先想办法把每一条记录变成一行才能用分列。你也可以说用查找替换把“记录开始”全部替换成回车不就行了吗问题在于备注里的换行会跟着混进来最后仍然需要对结果做二次清洗。这活儿看着小实际做起来很考验数据规律。所以我决定用 WPS 的 JSA 来写一个宏先把长文本按照“记录开始”到“记录结束”的边界切分成多个独立块这是“拆分”再把每个独立块里的字段提取出来将日期、金额转成 Excel 能识别的类型这是“转换”。而第一步切片靠的就是正则表达式里的懒惰匹配。2. 先把“懒惰匹配”讲透正则引擎在切分文本时怎么选边界很多人在网上搜到“懒惰匹配”这个概念知道写法是*?或?但没搞懂它到底解决什么问题。我用一个极简例子说明白。假设你有一段文本记录开始 编号1001 内容AA 记录结束 记录开始 编号1002 内容BB 记录结束现在想提取“记录开始”和“记录结束”之间的内容。如果写成贪婪匹配记录开始(.*)记录结束正则引擎会一路吞到最后一个“记录结束”匹配到的结果是编号1001 内容AA 记录结束 记录开始 编号1002 内容BB这不是你想要的。因为贪婪模式追求“最长匹配”它会把所有能吞的都吞掉。如果改成懒惰匹配记录开始(.*?)记录结束引擎会老老实实从当前“记录开始”往后找遇到第一个“记录结束”就停于是匹配到的是编号1001 内容AA继续执行第二次匹配时它会接着从上次结束的位置往后找得到编号1002 内容BB这就是懒惰匹配在数据拆分里的典型用途把一段连续文本按边界标记切成多个独立记录。它不追求一口气吃完整段数据而是“找到一个完整单位就先停下来”配合全局匹配g一条一条切。如果用生活例子类比贪婪匹配像是一个人在自助餐厅里把所有菜品全部夹到盘里最后才知道哪些能吃懒惰匹配则是要求他按顺序每样先夹一点吃完再夹下一盘。对数据解析来说显然“一盘一盘来”更可控。在 WPS 的 JSA 里正则直接使用 JavaScript 语法惰性写法和 JS 完全一致。上面这个例子用代码写就是这样var text 记录开始 编号1001 内容AA 记录结束 记录开始 编号1002 内容BB 记录结束; var reg /记录开始([\s\S]*?)记录结束/g; var m; while ((m reg.exec(text)) ! null) { console.log(m[1]); }这里有个细节很重要我没有用.*?而是用[\s\S]*?。原因是 JavaScript 正则里的.默认不匹配换行符如果文本跨行.*?会匹配不到完整内容。[\s\S]的意思是“空白字符和所有非空白字符都算”等价于真正意义的“任意字符”。3. 在 WPS 里搭一个 JSA 脚本的工作环境WPS 的 JavaScript 宏入口不同版本略有差异但大体路径是一致的。以 WPS 表格为例在顶部选项卡找到“开发工具”里面会有“JS 宏”或“宏”相关入口。点击进入编辑器后你会看到一个类似代码编辑器的界面里面可以新建模块写入 JavaScript 代码。如果你的 WPS 没有显示“开发工具”选项卡通常需要在“文件 - 选项 - 自定义功能区”里把它勾选出来。这一步和 Excel 显示“开发工具”的思路差不多。另外执行宏之前要注意安全设置。WPS 默认可能会禁用宏或者在你打开带宏文件时弹出提示。建议把包含宏的工作簿另存为支持宏的格式避免文件关闭后代码丢失。在 WPS 表格中常见的做法是保存为.xlsm格式这样下次打开还能继续编辑和运行这段 JSA 脚本。我这里把脚本放在两个位置一个主函数负责读取数据、写结果几个辅助函数负责字段提取、日期转换、金额转换。这样可读性更好也方便以后拿同一套机制处理其他文本。4. 完整脚本拆解段落切片、字段提取、类型转换一条龙4.1 第一步用懒惰匹配把长文本里的记录切片核心正则我建议写成这样var recReg /记录开始([\s\S]*?)(?记录开始|记录结束)/g;这一段代码有讲究记录开始是每条记录的起始标记。([\s\S]*?)是非贪婪捕获先从最短范围开始尝试。(?记录开始|记录结束)是零宽正向断言意思是“后面必须是记录开始或记录结束”但断言部分不会被计入匹配结果。使用(?记录开始|记录结束)而不是直接在后面匹配“记录结束”是为了防止最后一条记录缺少“记录结束”时被漏掉。举个例子如果最后一条记录的末尾没有“记录结束”四个字用记录结束直接结尾就无法匹配而用向前断言 $遇到字符串末尾同样能成立。在你编写循环时要注意exec方法的工作原理。JavaScript 正则开启g后regex.exec(str)会记住上次匹配结束的位置保存在lastIndex属性里下次继续从那个位置往后找所以适合用来逐个提取多个匹配结果。具体代码如下function SplitAndConvert() { var src ThisWorkbook.Sheets(Sheet1); var dst ThisWorkbook.Sheets(Sheet2); var rawText String(src.Range(A1).Value2 || ); if (!rawText) { alert(A1 里没有可处理的数据); return; } // 表头 var headers [编号, 日期, 客户, 电话, 金额, 备注]; for (var col 0; col headers.length; col) { dst.Cells(1, col 1).Value2 headers[col]; } var recReg /记录开始([\s\S]*?)(?记录开始|记录结束)/g; var outRow 2; var match; while ((match recReg.exec(rawText)) ! null) { var recordText clearEdge(match[1]); var no getField(recordText, 编号); var dateStr getField(recordText, 日期); var customer getField(recordText, 客户); var phone getField(recordText, 电话); var amountStr getField(recordText, 金额); var note getField(recordText, 备注); dst.Cells(outRow, 1).Value2 no; dst.Cells(outRow, 2).Value2 toDate(dateStr); dst.Cells(outRow, 3).Value2 customer; dst.Cells(outRow, 4).Value2 phone; dst.Cells(outRow, 5).Value2 toNumber(amountStr); dst.Cells(outRow, 6).Value2 note; outRow; } // 日期列格式 dst.Range(B2:B (outRow - 1)).NumberFormat yyyy-mm-dd; alert(共拆分 (outRow - 2) 条记录); }clearEdge用来去掉记录文本首尾可能残留的空格、管道符等无用信息我简单写成function clearEdge(str) { return str.replace(/^\s*\|?\s*/, ).replace(/\s*\|?\s*$/, ).trim(); }4.2 第二步从每条记录里提取字段值现在我们已经拿到类似下面这样的recordText编号O-1001|日期2024-08-21|客户张三|电话18812341234|金额199.00|备注8月21日请工作日送货联系电话确认这时候可以使用getField函数按字段名提取值。我的写法是function getField(recordText, key) { var reg new RegExp(key ([^|]*?)(?:\\||$)); var m reg.exec(recordText); return m ? trimStr(m[1]) : ; }这个函数的工作原理先拼出编号这样的模式然后用([^|]*?)捕获值要求是一直到下一个竖线|或字符串末尾$才算结束。[^|]表示“不是竖线的任何字符”配合非贪婪匹配能把字段值稳定取出来。这里为什么不用贪心的(.*)因为这条记录后面还有别的字段如果贪心很可能把后续字段一并吞进去。比如编号(.*)在 JavaScript 默认不是全局匹配的情况下会一直匹配到最后一个竖线之后的内容显然不是我们想要的。用([^|]*?)来限定“只取到下一个竖线之前”比单纯依赖懒惰匹配更可靠。getField里我使用了new RegExp而不是直接写一个字面量这样可以根据不同字段名动态构造正则代码更简洁。如果你只有固定几个字段也可以写成几个固定的正则表达式语义更直观。trimStr就是个普通的去首尾空格函数function trimStr(str) { return String(str).replace(/^\s|\s$/g, ); }4.3 第三步把日期和金额转成 Excel 真正能用的类型很多从系统导出的数据日期本质上只是“长得像日期的字符串”。这种字符串放进 Excel 之后如果用筛选或计算会出现各种问题。所以转换是必不可少的一步。日期转换我写了toDatefunction toDate(str) { if (!str) return null; var parts String(str).split(-); if (parts.length ! 3) return null; var y parseInt(parts[0], 10); var mo parseInt(parts[1], 10) - 1; var d parseInt(parts[2], 10); var dt new Date(y, mo, d); return isNaN(dt.getTime()) ? null : dt; }这里有一个值得注意的点JavaScript 的Date对象月份从 0 开始所以parseInt(parts[1], 10) - 1。我见过不少新手踩过这个坑解析出来的日期总是差一个月。金额转换我写了toNumberfunction toNumber(str) { var v parseFloat(String(str).replace(/,/g, )); return isNaN(v) ? 0 : v; }如果源数据里有1,299.00这种带货币符号和千分位的格式可以先统一去掉逗号和货币符号再parseFloat这里我只处理逗号实际使用按你的源数据情况再加替换规则就行。写回表格时我使用的是.Value2。在 WPS 的 JSA 里.Value2和.Value的区别主要在于.Value2不太受单元格格式影响更适合做数据传递和再计算。日期对象直接赋给.Value2后再统一设置NumberFormat yyyy-mm-dd单元格里就会正常显示日期。4.4 把脚本跑起来之后的输出效果脚本执行后Sheet2 会自动生成表头和数据。我实际测试的结果是A列编号比如 O-1001B列日期显示为 2024-08-21且单元格格式是日期C列客户D列电话E列金额变成数值可以对它求和F列备注原样保留整个过程不需要鼠标逐行操作运行一次 JSA 宏就完成了。这就是“数据拆分且转换”最直接的落地效果。5. 实际运行时的几个坑我都替你踩过了5.1.*不能匹配换行跨行文本记得用[\s\S]这是文本处理里最容易翻车的问题。如果备注字段里有换行而你用.*?去匹配会发现匹配结果在一行内结束明明应该把第二行备注内容也捕获进来结果却只抓到一半。我第一版脚本用的就是var recReg /记录开始(.*?)记录结束/g;结果第三段记录只有第一行入了结果第二行备注内容被漏掉了。排查半天才发现是.不匹配换行导致的改成[\s\S]*?后立刻正常。这个坑很隐蔽尤其是数据量大的时候不一定每次都能一眼看出来。5.2 exec 循环和 lastIndex 的“状态残留”问题使用带g标志的正则时lastIndex会在每次exec后自动移动。如果你在同一个函数里重复调用同一个正则可能会出现“明明能匹配到第二次却不返回结果”的诡异情况因为lastIndex已经停在末尾了。建议养成两个习惯正则对象尽量局部创建避免长期复用同一个全局对象。如果必须复用在每次进入循环前重置lastIndex 0。尤其是你写了多个辅助函数比如在getField里也创建了正则这些正则没有g标志通常不会有这种问题。但主切片正则一定带着g所以要格外小心。5.3 最后一条记录没有结束标记时容易被静默丢弃我在实际数据里遇到过一次导出系统最后一条记录没有“记录结束”这个收尾标记。如果用/记录开始([\s\S]*?)记录结束/g去匹配最后一条记录就彻底丢失了。这也是我建议使用/记录开始([\s\S]*?)(?记录开始|记录结束)/g的原因。用向前断言(?...)在遇到下一个“记录开始”或字符串末尾时都能让当前匹配正常结束。加了$这一层兜底漏数据的概率就低很多。5.4 大文本跑不动时先关掉屏幕刷新再分批处理如果 A1 里的文本有几十万字符正常跑一次不会太慢但如果你在循环里频繁操作单元格比如逐个写入几百行数据WPS 的界面刷新会拖慢速度。此时我一般会这样做Application.ScreenUpdating false; // 真正的循环处理 Application.ScreenUpdating true;若数据实在太大建议先把原始文本按固定的“记录开始”标记切分成多个数组块每块处理完再写回 Excel避免在脚本内部长时间占用内存。实际处理几万条记录时这个思路明显比一次性全量正则匹配更稳。5.5 正则里的中文标记和转义本文示例里所有标记都是中文比如“记录开始”“记录结束”这在 JavaScript 正则里完全合法不需要额外转义。但如果你的标记里有[、(、*、?、|这类正则元字符就要记得加反斜杠转义。例如字段分隔符本身是竖线|在正则里表示“或”所以我在getField里写成\\|来匹配真正的竖线字符。6. 这套“懒惰匹配拆数据”的思路还能顺手干不少活这套思路不只能用来处理工单表。我在后续工作中把同一个脚本改几条正则标记处理过下面几类情况网页复制到 Excel 的搜索结果列表。每条结果的标题、链接、摘要混在一个单元格里用类似http([\s\S]*?)下次标题的边界模式去掉外壳。日志文件的定时拆分。比如设备日志里每段日志以时间戳级别开头用懒惰匹配把几十万字符的日志按一条条切出来再提取关键字段。批量清洗通讯录里的备注字段。比如“备注转到市场部后续跟进”分隔符混乱用懒惰匹配提取“转到”和“后续跟进”两个部分自动生成列。说得直白一点懒惰匹配解决的核心问题只有一个当你面对一个很长、没有固定行分隔的字符串需要按照某种重复出现的边界切片时它就是最好的手术刀。WPS 的 JSA 让你能把这把手术刀封装成一次点击就能运行的宏省下的是大量重复手工作业的时间。最后分享一个我的操作习惯写这类脚本时我不会一次性写完所有功能而是先在编辑器里跑一段最简单的alert(recordText)确认切片结果正确再逐步加字段提取、日期转换、金额转换。每加一个功能就验证一次看起来慢实际上比全写完再统一排查要快很多。毕竟数据处理的问题八成的 bug 都出在对原始数据规律的误判上先确认边界再追求功能这个是稳的。
返回列表