拓冰建站拓冰建站
首页 / 资讯中心 / 正文

WPS表格JS宏与正则表达式:字符组与任选结构实现条件计数

先和你交个底这篇文章聊的是 WPS 表格里的 JS 宏怎么和正则表达式配合重点落在字符组和任选结构上最终目的是解决一类很常见但原生功能处理起来很别扭的问题条件计数。比如你有一列数据要数一数里面哪些单元格满足某种字符模式像包含数字、包含特定后缀、或者排除掉某些干扰项。如果你之前只会用 CountIf 或者自己写一堆循环判断这篇希望能帮你把思路打开。我最早接触这个场景是被一个不算复杂的表格逼出来的。有一列编号格式乱得很有的带字母有的纯数字有的中间还有横线领导要我一分钟之内给出“其中真正符合规范的到底有多少条”。WPS 的筛选功能能做一部分但规则一多筛完还得人工比对效率太低。后来我把正则表达式搬进 JS 宏里发现这套组合拳能覆盖绝大多数“按规则批量计数”的需求而且改条件的时候只需要改一行正则不用重写逻辑。1. 条件计数这件事为什么值得单独拿出来说1.1 不是所有“数一数”都能直接数表格里数个数最直观的解法是 CountIf 和 CountIfs。日期区间、数值大小、完全匹配的文本这三个场景它们处理得的确漂亮。但一旦条件变成“包含某类字符”“符合某种格式”“不要什么但必须有什么”CountIf 的通配符就捉襟见肘了。举个例子CountIf 支持星号和问号*AB*能匹配“包含 AB 的单元格”。可如果我要统计“第二位是数字 3 或 5且以字母 E 结尾”的编号有多少个CountIf 就写不出来。不是语法不行而是通配符根本不支持这种结构化的字符位置匹配。正则表达式在这里的优势是位置感知和模式描述。它不只告诉你“有没有”还能告诉你“在哪一位上是什么”。这种能力对条件计数来说是质的提升因为你不再需要拆列、辅助列、多次筛选才能凑出一个答案。1.2 JS 宏是 WPS 里最适合干这件事的入口WPS 表格里能跑脚本的方式有好几种最普及的是 VBA 宏但 WPS 的新趋势是 JS 宏。JS 宏有个天然优势JavaScript 的正则表达式能力是内建且完整的你不需要额外引用库不需要学习 VBA 里那套略有差异的正则语法直接用标准的 RegExp 对象就行。对于熟悉前端或者 Node.js 的人来说这套 API 基本上零成本迁移。就算你没写过 JS 宏只要在 WPS 里打开 JS 宏编辑器把 JavaScript 那套逻辑搬进去稍微适配一下表格对象模型就能跑起来。还有一个客观现实新版本 WPS 的 JS 宏编辑器体验越来越顺错误提示、调试输出、断点这些基础功能都有不像早期版本那样只能“瞎写瞎试”。所以从长期角度看在 WPS 里用 JS 做数据处理投入回报比很高。1.3 字符组和任选是条件计数的两大核心武器正则表达式本身知识点很多但如果你只是想用它做条件计数优先掌握两块就够用了字符组和任选结构。再配合少量量词和定位符就能覆盖绝大部分场景。字符组解决的是一对多的字符匹配问题比如“这一位可以是任何数字”“这一位可以是 a、b、c 中的一个”。任选解决的则是多模式并列的问题比如“这个单元格要么以 AB 开头要么以 CD 开头”。两者侧重点不同但经常搭配使用。把这两个工具用熟练之后条件计数的规则描述会变得非常清晰。2. 字符组单个位置上的一对多匹配2.1 三种写法对应三种不同的需求粒度字符组在正则里的写法是方括号比如[abc]、[0-9]、[a-z]。它表达的意思是“当前这个位置可以是方括号内列举的任意一个字符”。按照写法差异我习惯把字符组分成三种列举型[abc]明确列出允许的所有字符。适合字符集合很小且没有规律的情况。范围型[0-9]、[a-z]、[A-Z]用横线表示连续区间。适合字符集合连续且有序的情况。排除型[^0-9]在方括号开头加一个脱字符表示“除这些字符之外的任意字符”。适合你想表达“不要什么”的场景。这三种写法的优先级怎么排我的经验是能明确列举就列举能范围化就范围化不到万不得已别用排除型。因为排除型匹配的范围太宽容易误伤。举一个具体例子。有一列数据是设备编号规范要求是“一位字母 五位数字”比如A12345。你要统计符合规范的数量正则可以写/^[A-Z][0-9]{5}$/这里[A-Z]表示第一位必须是大写字母[0-9]表示后五位必须是数字{5}表示数字要连续出现 5 次^和$分别锁定开头和结尾确保整个单元格完全匹配而不是部分匹配。写完之后思路非常直白第一位走字符组后五位走字符组加量词。不需要拆列不需要辅助公式。2.2 排除型字符组的正确用法和隐患排除型字符组我个人只在两个场景用一是明确要过滤掉某些敏感字符二是配合其他规则做“兜底”。比如统计“不含数字的文本单元格”的数量正则可以写/^[^0-9]$/这个表达式的含义是从开头到结尾每一个字符都必须不是数字。表示至少有一个字符。但这里有个坑[^0-9]是匹配“任意不是数字的字符”它包含换行符吗在 JS 正则里默认情况下点号不匹配换行方括号也一样不匹配换行符。所以在 WPS 单元格里如果文本中间有换行符^[^0-9]$这个表达式就会匹配失败。这不是你写的错而是对“字符”这个概念理解得不全面。解决方法是要么在提取数据之前先清理掉单元格内的换行符要么用[\s\S]这种比排除型字符组更“宽”的方式。我在实际处理时更喜欢先用一个清洗步骤把换行符统一替换掉然后再跑正则。这看起来多了一步但能省掉后面排查问题的很多时间。你宁可让数据在源头干净也别让正则去兼容各种异常。2.3 字符组与量词搭配时的性能陷阱字符组本身匹配速度非常快但一旦和量词搭配就可能产生性能问题尤其当文本较长、规则较复杂的时候。典型的例子是模式[0-9]去匹配一个很长的字符串。是贪婪量词它会尽可能多地匹配数字。如果后面还有其他条件正则引擎可能需要回溯也就是“先尽量多匹配不行就退一步再试”。当数据量达到几千行甚至上万行时这种回溯的累计开销不可小觑。WPS 的 JS 宏跑在表格环境里本身不是为高吞吐量设计的。我做过一次粗略测试在 10 万行数据上逐格执行一条带多个字符组和量词的正则耗时比单纯遍历单元格高出数倍。所以我的建议是先在应用正则之前用简单的条件把明显不符合的数据过滤掉。比如你要统计的单元格必须包含数字那就先用typeof value string判断类型或者判断长度。正则表达式尽量写得“锚定明确”。能用^和$锁定边界就不要留着让引擎全串搜索。避免嵌套量词比如(?:[0-9]){2,}这种它会显著增加回溯分支。3. 任选结构多条规则之间的“或”运算3.1 竖线把多条规则串成一条正则里的任选结构用竖线|表示含义是“左边匹配或者右边匹配”。在条件计数时最直观的用法是统计“满足 A 规则或者满足 B 规则”的单元格。比如要统计“以 AB 开头或者以 CD 开头”的编号数量正则写/^(AB|CD)/这里^锚定开头(AB|CD)是一个捕获分组内部用竖线连接两个选项。含义是以 AB 开头或者以 CD 开头。不用分组行吗行但优先级会出问题。^AB|CD这个写法引擎会解读成“以 AB 开头的字符串或者包含 CD 的字符串”范围完全不对。所以只要涉及任选和前后条件结合就一定要用分组把范围框起来。3.2 字符组和任选的边界感刚接触正则的人很容易混淆字符组和任选。字符组[AB]匹配的是单个字符它在“这一位是 A 或者这一位是 B”的场景下有效。任选(AB|CD)匹配的是整个片段它在“这一段是 AB 或者这一段是 CD”的场景下有效。两者的选用原则很简单看你要匹配的内容是单个字符还是多个字符的组合。统计“第二位是 A 或 B”的编号用字符组/^.[AB]/统计“第二段是 AB 或 BC”的编号用任选/^.(AB|BC)/这两个例子看起来相似但语义完全不同。前者的[AB]只占一个位置后者的(AB|BC)占两个位置。写错一个符号计数结果可能差好几倍。3.3 捕获分组与性能控制任选结构离不开分组但分组也分“捕获组”和“非捕获组”。在条件计数的场景里我们只关心匹配成功与否并不需要提取出具体匹配到的内容所以最好用非捕获组来减少不必要的操作。/^(?:AB|CD)/(?:...)表示非捕获组它只负责圈定范围不记录匹配内容。在逐行处理大量单元格时这种细节能轻微提升性能更重要的是让代码意图更明确你不是要拿这个分组的结果进行后续替换只是让任选结构生效。如果你用match()方法处理成千上万行数据捕获组会为每一次匹配保留额外的存储信息虽然现代引擎优化得不错但我们在 WPS 桌面的 JS 环境里仍然建议尽量贴地写少占点内存是一点。3.4 量词和任选相遇时的分支爆炸任选结构本身性能通常不错但当它在量词内部反复出现时就会产生分支爆炸。比如/^(?:AB|CD|EF|GH|IJ){2,5}$/这个正则的含义是整个字符串由 AB、CD、EF、GH、IJ 这五个片段中的任意几个拼接而成总共重复 2 到 5 次。引擎需要尝试所有可能的组合方式如果匹配失败回溯的次数会非常多。实际项目中我很少写这种嵌套过深的模式。因为条件计数的核心目标是“判断是否符合规则”而不是“把规则描述得无比抽象”。如果一条规则需要嵌套三层以上我会先怀疑是业务数据本身不规范而不是正则写得不够强。4. 条件计数完整实操从场景到代码4.1 一个能直接照做的业务场景先设定一个具体场景后面所有的代码都围绕它展开。假设你现在维护一张设备登记表A 列是设备编号格式要求如下总共 6 到 8 位字母和数字混合必须以大写字母开头不能包含 “X” 或 “Z” 这两个字母编号中至少要包含一个数字。你要统计出“完全符合上述规则的设备编号”有多少行并暂时不考虑重复项和空单元格。这个场景完美地覆盖了字符组和任选结构且难度适中。下面我就拿这个例子来写完整代码。4.2 写宏之前先搭好对象模型WPS 的 JS 宏环境里最常用的几个对象是ActiveSheet、Range、Debug。我在写条件计数宏之前一定会先确认数据和范围对象是否正确。核心思路是先用Range拿到一列数据的单元格区域然后循环遍历每一行在循环内取出单元格的值判断是否为字符串再跑正则命中则计数加一。function CountQualifiedRecords() { // 获取当前工作表 var sheet ActiveSheet; // 这里默认数据在 A1 开始连续向下排到 A200 var rng sheet.Range(A1:A200); var rows rng.Rows.Count; var count 0; // 空单元格计数用于排查 var emptyCount 0; // 逐行遍历 for (var i 1; i rows; i) { var cell rng.Cells.Item(i, 1); var value cell.Value2; // 空值直接跳过并记录 if (value null || value undefined || value ) { emptyCount; continue; } // 确保是文本类型防止数字被隐式转换 var text String(value).trim(); // 核心正则校验规则 var pattern /^[A-Z][A-Z0-9]{5,7}$/; // 补充规则不能包含 X 或 Z且至少包含一个数字 var noXZ /^[^XZ]$/; var hasDigit /[0-9]/; // 判断是否满足全部条件 if (pattern.test(text) noXZ.test(text) hasDigit.test(text)) { count; } } Debug.Print(符合规范的编号数量 count); Debug.Print(空单元格数量 emptyCount); }这里我用了三个正则分别表达三类条件。你可能会说为什么不用一个正则搞定我也想但“不能包含 X 或 Z”和“至少要包含一个数字”这两个条件的组合用单个正则写出来可读性很差而且极容易出 bug。在实际项目中我倾向于把规则拆成多个正则每个正则负责一个维度的校验然后用把它集中起来。这带来的最大好处是可维护性改规则的时候你只需要动对应的一行正则其他逻辑不用管。4.3 案例 A字符组的扩展写法上面的代码里核心正则[A-Z][A-Z0-9]{5,7}用了两层字符组。第一层[A-Z]限定第一位必须是大写字母第二层[A-Z0-9]限定后续字符可以是大写字母也可以是数字可重复 5 到 7 次最终总长度 6 到 8 位。这个写法直观但有一个细节容易被忽略字符组里的-在中间位置表示范围如果写在开头或结尾则被当作字面横线处理。比如[-A-Z0-9]表示“横线、大写字母、数字”中任意一个。如果你的数据里恰好包含“编号中间带横线”的格式你会想把横线纳入字符组。那可以写成/^[A-Z][A-Z0-9-]{5,7}$/这里把横线放在A-Z0-9之后紧邻]这样它被当作字面量解析不会产生“范围歧义”。但这个写法有一个副作用它允许编号中间出现连续多个横线比如A--123。如果你要保持“横线最多出现一次且不能在首尾”正则就要复杂得多。这又回到了单一正则与组合正则的取舍。4.4 案例 B任选结构做多类别计数继续扩展场景。假设编号系统的规则改成“编号必须以AB、CD或EF开头其他规则不变。”这就是任选结构明显优于字符组的地方。function CountWithAlternation() { var sheet ActiveSheet; var rng sheet.Range(A1:A200); var rows rng.Rows.Count; var count 0; for (var i 1; i rows; i) { var cell rng.Cells.Item(i, 1); var value cell.Value2; if (!value) continue; var text String(value).trim(); // 任选结构开头必须是 AB、CD 或 EF 之一 var pattern /^(?:AB|CD|EF)[A-Z0-9]{4,6}$/; var hasDigit /[0-9]/; if (pattern.test(text) hasDigit.test(text)) { count; } } Debug.Print(符合条件的编号数量 count); }这里的(?:AB|CD|EF)就是任选核心。我特意用了非捕获组而不是捕获组因为后面只要test()的结果不需要提取匹配值。如果你业务里需要“分别统计以 AB 开头的有多少、以 CD 开头的有多少、以 EF 开头的有多少”那就不能只用一个test()了而是要分别跑三个正则或者用match()配合exec()提取前缀再归类。function CountByPrefixCategory() { var sheet ActiveSheet; var rng sheet.Range(A1:A200); var rows rng.Rows.Count; var countAB 0, countCD 0, countEF 0, countOther 0; for (var i 1; i rows; i) { var value rng.Cells.Item(i, 1).Value2; if (!value) continue; var text String(value).trim(); var m text.match(/^(AB|CD|EF)/); if (m) { switch (m[1]) { case AB: countAB; break; case CD: countCD; break; case EF: countEF; break; } } else { countOther; } } Debug.Print(AB 开头 countAB); Debug.Print(CD 开头 countCD); Debug.Print(EF 开头 countEF); Debug.Print(其他 countOther); }这里特意用了捕获组因为要提取前缀值。所以“是否使用捕获组”不是死规则完全取决于你有没有提取需求。要用match()拿到分组内容时捕获组是必需的。4.5 案例 C排除型字符组的实际过滤效果排除型字符组在条件计数里最常见的用途是过滤掉不该出现的字符。比如编号中出现过小写字母、空格、或者特殊符号你要把有这些“杂讯”的单元格单独统计出来方便后续清洗。function CountNoisyRecords() { var sheet ActiveSheet; var rng sheet.Range(A1:A200); var rows rng.Rows.Count; var countNoise 0; for (var i 1; i rows; i) { var value rng.Cells.Item(i, 1).Value2; if (!value) continue; var text String(value).trim(); // 允许的范围A-Z、0-9、横线 // 只要出现不在允许范围内的字符就视为杂讯记录 var forbidden /[^A-Z0-9-]/; if (forbidden.test(text)) { countNoise; Debug.Print(发现杂讯记录 text); } } Debug.Print(含杂讯的记录数量 countNoise); }这个例子的核心是把排除型字符组当作“安全巡检员”它不直接做条件计数而是帮你在正式计数之前清点出问题数据。在真实项目中这个前置步骤往往比最终计数更值钱因为它暴露了数据质量问题。5. 性能、边界与调试5.1 大数据量场景下的处理效率如果你只有几百行数据上面这些宏都是一眨眼的事。但到了上万行逐格test()的效率问题就凸显出来了。我在处理一份约 5 万行的设备清单时用上面这种纯 JS 循环方式跑一次大约需要几秒钟如果正则写得复杂比如带多层量词和回溯时间会拉长到几十秒。优化思路我按优先级排序先把数据整体读入二维数组一次性从 Range 取到values再在内存里遍历。这比逐格Cells.Item()访问快很多。正则的pattern只创建一次不放在循环体内反复new RegExp。用字面量/pattern/代替构造函数减少解析步骤。匹配次数多时优先用test()而不是match()因为test()不需要构造匹配结果数组。能用简单字符串方法indexOf、startsWith、includes快速排除的先过滤再让正则处理剩余部分。这里给出一个读入二维数组的改进版本function CountQualifiedFast() { var sheet ActiveSheet; var rng sheet.Range(A1:A50000); var values rng.Value2; // 一次性取回二维数组 var count 0; var pattern /^[A-Z][A-Z0-9]{5,7}$/; var noXZ /^[^XZ]$/; var hasDigit /[0-9]/; for (var row 1; row values.length; row) { var value values[row - 1][0]; if (!value) continue; var text String(value).trim(); if (pattern.test(text) noXZ.test(text) hasDigit.test(text)) { count; } } Debug.Print(快速模式计数 count); }注意values是一个二维数组第一维是行第二维是列。用row - 1是因为数组索引从 0 开始而行号从 1 开始。这个细节很容易写错我踩过一次坑后专门在代码里注释了索引关系。5.2 空值、类型转换与隐藏行JS 宏取单元格值时最常见的坑是Value2返回的类型。数字单元格返回的是Number文本单元格返回的是String空单元格返回的是null。如果直接用String(value)处理null会得到字符串null再跑正则就会莫名其妙匹配失败。所以用String(value)之前一定要先判断空值。我更推荐先判空再转字符串if (value null || value undefined) continue; var text String(value);另外Debug.Print是标准输出接口但如果你写的是函数而非过程也可以直接通过弹窗或者把结果写回某个单元格来查看。我做一些临时调试时喜欢把结果写到工作表旁边的一个单元格里比如Range(D1).Value2 count这样可以快速肉眼校验。还有一个经常被忽略的点隐藏行。Range默认包含隐藏行如果你只统计可见行需要先判断Rows.Item(i).Hidden是否为true。但这个判断会增加循环开销所以除非明确要求“只看可见行”否则我一般不管隐藏行。5.3 调试正则表达式的三个实用方法写正则的时候一次通过是运气反复调试才是常态。我的调试方法按步骤排第一在 JS 宏里用Debug.Print输出每次匹配的结果信息包括文本内容和正则模式。如果数据量大只输出“未通过但看起来很像”的记录。第二把正则复制到浏览器开发者工具的 Console 里面用 Node.js 环境或者任意在线正则测试工具先验证一遍。WPS 的 JS 引擎遵循的是标准 ECMAScript 正则规范和浏览器基本一致所以在这个环境验证过的正则搬到 WPS 里不会有太大出入。第三拆解正则。把一个大正则拆成几个小正则分别测试逐步排除问题。这个方法和代码调试里的“二分查找”思路一样。下面举一个实际的调试例子。我之前写了一个正则^[A-Z][0-9]{5,}$本意是匹配“一位大写字母后跟至少五位数字”的编号但数据里出现了类似A123456Z这种以字母结尾的编号。这个正则本身没问题问题在于业务需求的表述不够精确“至少五位数字”并没有说明“后面可以跟字母”。所以正则写出来后匹配结果要么包含不想要的记录要么漏掉想要的记录。最后我通过拆解和反复查看未匹配数据才把需求明确成“编号必须全部由大写字母和数字组成且以一位大写字母开头至少包含六位数字”。修正后的正则变成/^[A-Z][A-Z0-9]{6,}$/这个例子说明写作正则的时候你要同时做两件事把正则写对把需求问清楚。后者往往更重要。6. 常见问题实测与避坑清单6.1 正则不生效的常见原因条件计数宏跑完结果却是 0或者明显偏少。我总结过几个高频原因正则漏了^和$。如果不锚定边界/[0-9]/会对一个带有数字的长字符串返回 true这不是你要的完全匹配。字符组里的范围写反了。[z-a]在 JS 正则里直接报错因为范围起点大于终点。写[a-z]才对。大小写敏感。JS 正则默认区分大小写[a-z]不能匹配A。如果规则不区分大小写在正则末尾加i标识/^[a-z]$/i。把test()用在全局正则上。如果正则带了g标识test()会改变lastIndex状态导致同一正则第二次测试同一个字符串返回不同的结果。这绝对是个坑。在条件计数里我建议统一不带g标识除非你确实需要往返匹配多次。6.2 CountIf 和正则什么时候该用哪个并不是所有场景都要上正则。如果条件只是“等于某个值”或“处于某个区间”CountIf 比自己写循环快得多也简单得多。我的原则是等于、大于、小于、包含通配符用 CountIf 或 CountIfs。需要按字符位置匹配、需要排除型匹配、需要多重任选条件用正则。需要同时做分类计数并输出每个类别的数量用正则加循环。这两种方式不冲突很多时候我会组合用先用 CountIf 排除掉一部分明显不符合的数据再用正则处理剩余的复杂逻辑。这能有效减少正则引擎的负担。6.3 我踩过但后来记住的坑挑三个印象最深的坑分享给你。第一个坑是“把 String(value) 处理 null”。当时我不小心把一个空单元格转成了null正则去匹配 “null” 这个字符串结果最后查出来一堆异常数据。后来我定了规矩所有取到的单元格值先判空再处理。第二个坑是“全局正则的 lastIndex 状态”。我第一次用带g的正则去做逐行test()结果数据量一上去结果就变得随机。查了半天才发现是lastIndex在作怪。从那以后我写条件计数时一概不带g。第三个坑是“把字符组和任选混用”。我曾经写[AB|CD]这种表达式想着表达“AB 或者 CD”但字符组匹配的是单个字符[AB|CD]表示的其实是“A、B、|、C、D”这五个字符中的任意一个完全不是我要的意思。正确写法是(?:AB|CD)。6.4 为后续扩展留好接口条件计数做完往往还会跟着“标记、统计、汇总”这些后续动作。写宏的时候我习惯把计数逻辑封装成一个函数接收一个 Range 参数和一个正则参数返回计数结果。这样下次换一列数据或者换规则只需要换参数不用改函数体。function CountByRegexRange(rng, regex) { var values rng.Value2; var count 0; for (var row 1; row values.length; row) { var value values[row - 1][0]; if (!value) continue; if (regex.test(String(value))) { count; } } return count; }这个函数不够完美比如正则是否带g、值类型是否需要trim但作为起步工具已经足够实用。后续你可以根据自己的数据特征去增加参数、增加空值处理、增加多种规则的组合判断。以我个人的体会正则表达式在 WPS 表格里的价值不在于替代所有原生函数而在于给你提供一种“用模式去描述数据”的能力。字符组和任选结构是这种能力的两块基石配合条件计数这个具体需求你可以在五分钟内写出一条以前要拆列、加辅助列、人工比对才能完成的统计逻辑。如果你刚开始接触建议先把我上面几个示例逐行敲一遍感受一下正则和表格数据之间是怎么衔接的。踩过几次坑之后你就会发现这组合一旦会用就很难再回去了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门