
在WPS文字里贴代码这件事我想每个技术作者都经历过从编辑器复制一段代码过来缩进全乱、字体大小和正文混在一起背景也没有读者根本分不清哪行是代码哪行是说明文字。手动逐行调样式几分钟能搞定一小段但一份几十页的技术文档下来半天的工夫就搭进去了。后来我琢磨着用WPS自带的JS宏把这件事自动化——识别哪些段落是代码再统一套上等宽字体、浅灰背景、固定缩进。折腾了几个版本之后现在的脚本基本达到了“粘贴完跑一下样式自己就位”的效果这篇文章把完整思路和可用的代码都放出来适合经常在WPS里写技术文档、做培训教材、交课程作业的朋友直接参考。1. 为什么要给WPS写“代码段智能识别”脚本痛点与技术选型1.1 文档里贴代码的痛点先说最直接的痛点。从IDE复制代码到WPS时常见的问题有三类第一缩进信息几乎必然丢失因为WPS默认会把连续空格折叠或者重新按段落格式排Python这类靠缩进的语言贴出来基本就没法看第二字体混乱代码里的西文和中文混排后WPS倾向于用宋体或等线渲染和编辑器里的等宽字体效果差很多第三没有背景区分代码和正文视觉上完全没有边界。手动修正不是不行但步骤非常繁琐先要全选代码段设置西文字体为Consolas或Courier New中文字体设为微软雅黑或等线再逐段调整段前段后、左缩进、首行缩进最后还要给每段加底纹。而且多数时候要重复操作几十个段落每次设置完一个段落再点下一个重复劳动非常枯燥。如果一个文档里有十几个代码块手动操作的时间基本等于重写一遍文档。1.2 为什么选JS宏而不是VBA这里直接说结论在WPS里做自动化JS宏是比VBA更合适的选择尤其是对本身就会写JavaScript的开发者来说。对比维度VBAJS宏运行环境部分WPS版本需要额外安装VBA插件WPS默认内置JS宏引擎语法门槛面向老Office用户的Basic语法标准JavaScript语法正则、字符串处理能力更强跨平台Windows为主不依赖特定平台API可调性较传统同样能调用WPS对象模型方法命名接近VBA习惯VBA在WPS里并不是不能用但你会发现官方对JS宏的投入明显更大新功能、新API都是优先给JS宏用的。从工程角度讲JS宏的代码更容易用正则做文本判断——而“智能识别代码段”这件事的核心恰恰是文本特征分析正则好不好用直接决定脚本的代码量。1.3 这个脚本做的事与不做的事开始写代码之前先明确边界。这个宏做的事情是遍历文档段落根据关键词、符号特征、缩进特征给每个段落打分分数达到阈值的段落判定为“代码行”再把连续的代码行合并成一个代码块统一设置字体、字号、底色、缩进。它不做的事情也说明白不会帮你格式化代码本身缩进乱掉的代码还是需要你从编辑器里复制前确保源文件格式正确不会处理表格单元格里的代码因为这个版本遍历的是文档主文本区域的段落正好避免破坏表格排版。这个脚本最适合的场景是你的文档正文用中文书写代码段以独立段落形式插入段落之间有明确的换行分隔。如果代码是嵌在正文句子中间的那不属于这个脚本的处理范围。2. 识别规则设计打分机制如何区分“代码行”和“普通正文”2.1 三条规则的权重设计智能识别的关键不是“找代码”而是“不误伤正文”。我试过简单用关键词匹配效果很差——普通英文段落里经常出现if、for、while这类词单靠关键词会把一大段英文散文误判成代码。最终我采用的是打分制三条规则分别加分总分达到3分才算代码行。第一条规则是关键词匹配。维护一个覆盖常见语言的公共关键词表包含function、def、class、import、return、if、for、while、var、let、const、int、char、bool、string、public、private、static、void、new、try、catch、throw、extends、interface、namespace、include、printf、console、log、print等。这里要注意用正则的词边界匹配避免把“normal”里的“for”或者“ifly”里的“if”误算进去。命中关键词得2分。第二条规则是半角符号特征。代码和中文正文最大的区别在于标点符号代码几乎全用半角符号而且常见的有分号结尾、等号赋值、大括号包裹块结构、函数调用括号这些特征在中文正文里很少出现。统计一段文本里是否包含等号、大括号、小括号、分号、井号、双斜线注释出现数量超过3个加2分至少1个加1分。特别注意只统计半角符号中文全角括号不算这一步就把绝大多数中文正文排除了。第三条规则是文本形态特征。如果行首出现连续空格或制表符说明这段文本有缩进结构如果文本里有形如“函数名(参数)”的调用结构如果文本以http://或www开头直接判为普通文本不进入代码判定。行首缩进加1分函数调用结构加1分URL直接返回0分。2.2 关键代码judgeLine打分函数下面是打分函数的核心代码我把它独立成一个方法便于调试和调整权重function judgeLine(text) { text String(text || ).replace(/\r?\n$/, ); var trimmed text.trim(); // 空行单独标记不参与代码判定交给后面合并逻辑处理 if (!trimmed) return 2; // 明显的URL、邮件地址等直接放行避免把超链接误判为代码 if (/^(https?:\/\/|www\.|[\w.][\w.])/i.test(trimmed)) return 0; var score 0; // 规则1关键词匹配词边界 var keywords [function,def,class,import,from,return, if,else,for,while,var,let,const, int,char,bool,string,double,float, public,private,protected,static,void, new,try,catch,finally,throw,extends, implements,interface,namespace,using, include,define,printf,console,log,print]; for (var i 0; i keywords.length; i) { var re new RegExp(\\b keywords[i] \\b, i); if (re.test(trimmed)) { score 2; break; } } // 规则2半角符号特征 var symCount 0; if (trimmed.indexOf() 0) symCount; if (trimmed.indexOf({) 0) symCount; if (trimmed.indexOf(}) 0) symCount; if (trimmed.indexOf(() 0) symCount; if (trimmed.indexOf()) 0) symCount; if (trimmed.indexOf(;) 0) symCount; if (trimmed.indexOf(#) 0) symCount; if (trimmed.indexOf(//) 0) symCount 2; if (/[\w.\]\)]\s*\(/.test(trimmed)) symCount; if (/,\s*\w/.test(trimmed)) symCount; if (symCount 3) score 2; else if (symCount 1) score 1; // 规则3行首缩进特征 if (/^\s{2,}|\t/.test(trimmed)) score 1; if (/^\/?[\w]/.test(trimmed)) score 1; // 总分达到3判定为代码行 return score 3 ? 1 : 0; }几个设计细节说明一下。关键词匹配用词边界但一次命中就加分即使一行里出现多个关键词也只加一次避免长句子因为多个关键词堆分。符号特征统计的是“种类”而不是“数量”因为一行代码里通常同时存在等号、分号、括号统计种类更能表达代码的结构化特征。缩进特征的权重最低只加1分因为正文里也有合理的左缩进段落。2.3 连续代码块的合并策略含空行处理单行判断只是第一步真正让文档美观的是“代码块”的合并处理。如果代码段的中间有个空行简单按行判断会把这个空行截断导致样式不连贯。所以我在判断结果里用三个值1代表代码行0代表普通行2代表空行。合并时从文档第一段遍历到末尾当发现第一个代码行时开启一个待处理块之后只要遇到代码行就继续扩展遇到空行时先不关闭块继续往后看等到真正的普通行出现才关闭当前块。这样一来代码块内部的空行会一并纳入样式范围视觉上不会出现中间断了一条白边的情况。这种合并策略还有一个额外收益设置样式时可以对整个块统一操作字体和背景色而不是一行一行单独操作。不仅能减少API调用次数还能保证块内字体设置完全一致不会出现某一行漏掉的情况。3. 样式引擎实现让代码块一眼可读3.1 字体与颜色识别完成之后进入样式设置环节。代码块字体的选择上西文我用Consolas这个字体在Windows系统基本都有等宽效果比默认的宋体好一个档次。如果机器上有更现代的等宽字体比如JetBrains Mono也可以在代码里改成自定义字体名。中文部分要单独设置因为代码里的注释经常有中文我用微软雅黑保证和正文风格统一。字号方面我选10.5磅也就是五号字的大小。这个字号放在文档里不会比正文大太多阅读时视线能稳定聚焦同时又不显得拥挤。字色用深灰色0x333333而不是纯黑纯黑在浅灰背景下看久了有点扎眼深灰更柔和。function applyCodeBlockStyle(doc, startIndex, endIndex) { var startRange doc.Paragraphs.Item(startIndex).Range; var endRange doc.Paragraphs.Item(endIndex).Range; var blockRange startRange.Duplicate; blockRange.End endRange.End; // 整块统一字体、字号、字色 blockRange.Font.Name Consolas; blockRange.Font.NameFarEast 微软雅黑; blockRange.Font.Size 10.5; blockRange.Font.Color 0x333333; // 浅灰背景 try { blockRange.Shading.BackgroundPatternColor 0xF2F2F2; } catch (e) {} // 逐段设置段落格式 for (var i startIndex; i endIndex; i) { var p doc.Paragraphs.Item(i); p.FirstLineIndent 0; p.LeftIndent 14.17; // 约0.5厘米 p.SpaceBefore 2; p.SpaceAfter 2; } }3.2 背景、缩进与段距背景色是整个样式方案里最关键的视觉元素。浅灰底纹0xF2F2F2是我调了几次之后选定的打印时不会太费墨屏幕阅读时又足够区分正文区域。这里需要提一个WPS JS宏的坑颜色值的RGB顺序和想象中可能相反。如果设置完发现背景变成了奇怪的颜色把十六进制数值反过来写例如0xF2F2F2改成0xF2F2F2交换高低字节为0xF2F2F2实际上浅灰色反转后看起来差别不大但如果你设置更深些的颜色就会明显发现问题。缩进处理上有两个细节容易被忽略。第一正文段落如果设置了首行缩进2字符粘贴代码后这个格式会被继承导致代码块每行都缩进得特别奇怪所以要显式把FirstLineIndent清零。第二左缩进设置14.17磅约等于0.5厘米这个宽度足够形成视觉错层又不会让长代码过早换行。段前段后间距我设置得比较小各2磅。这样做的考虑是代码块在文档里通常以“成套”的形式出现块内行间距应该紧凑块与前后正文之间反而应该有一个更大的间隔。这个间隔由正文段落本身的段前段后控制代码块内部保持紧凑更接近编辑器里的观感。3.3 幂等运行与性能优化运行宏之后如果发现这次识别阈值不对要调整或者觉得背景色不好看要换一个你可能想重新运行一次。这时最烦人的是已经设置过的段落又叠了一层样式虽然结果不会出错但底纹等设置被重复应用会带来不必要的等待。我在脚本上做了简单的幂等判断思路如果整段文字的Font.Name已经是Consolas并且Shading颜色已经是目标浅灰就可以跳过。不过这个判断需要读取所有段落的属性对于长文档反而增加耗时。实际测试下来全文档遍历一次大概几十毫秒重复设置样式的影响远没有想象中的大。所以我最终版本没有把幂等判断写进主流程而是保留了一个更实用的优化关闭屏幕刷新。实现方式是在宏的开头设置Application.ScreenUpdating为false结尾恢复true。屏幕刷新关闭后WPS在宏运行期间不会实时重绘界面几十页的文档处理时间能缩短一半以上。注意有些WPS版本对ScreenUpdating属性的支持有差异所以用try-catch包住即便设置失败也不影响主流程。4. 完整可用版代码及安装步骤4.1 完整代码下面是完整可用的版本你只需要在WPS JS宏编辑器中新建一个模块把代码整体粘进去然后运行SmartCodeStyle函数即可。/** * WPS JS宏智能识别代码段并设置样式 * 适用WPS文字 PC版 * 运行开发工具 - JS宏 - 选中SmartCodeStyle - 运行 */ function SmartCodeStyle() { var app WPS.Application; var doc app.ActiveDocument; // 关闭屏幕刷新提高运行速度 try { app.ScreenUpdating false; } catch (e) {} var paras doc.Paragraphs; var total paras.Count; // 第一遍判断每个段落的代码特征 var flags []; for (var i 1; i total; i) { var p paras.Item(i); var text p.Range.Text; flags.push(judgeLine(text)); } // 第二遍合并连续代码块并应用样式 var start -1; for (var i 1; i total; i) { if (start -1) { if (flags[i - 1] 1) start i; } else { // 遇到代码行或空行继续扩展当前块 if (flags[i - 1] 1 || flags[i - 1] 2) continue; // 遇到真正的普通行关闭当前块 applyCodeBlockStyle(doc, start, i - 1); start -1; } } // 处理末尾剩余的块 if (start ! -1) applyCodeBlockStyle(doc, start, total); try { app.ScreenUpdating true; } catch (e) {} try { app.Alert(代码块样式处理完成。); } catch (e) {} } function judgeLine(text) { text String(text || ).replace(/\r?\n$/, ); var trimmed text.trim(); if (!trimmed) return 2; if (/^(https?:\/\/|www\.|[\w.][\w.])/i.test(trimmed)) return 0; var score 0; var keywords [function,def,class,import,from,return, if,else,for,while,var,let,const, int,char,bool,string,double,float, public,private,protected,static,void, new,try,catch,finally,throw,extends, implements,interface,namespace,using, include,define,printf,console,log,print]; for (var i 0; i keywords.length; i) { var re new RegExp(\\b keywords[i] \\b, i); if (re.test(trimmed)) { score 2; break; } } var symCount 0; if (trimmed.indexOf() 0) symCount; if (trimmed.indexOf({) 0) symCount; if (trimmed.indexOf(}) 0) symCount; if (trimmed.indexOf(() 0) symCount; if (trimmed.indexOf()) 0) symCount; if (trimmed.indexOf(;) 0) symCount; if (trimmed.indexOf(#) 0) symCount; if (trimmed.indexOf(//) 0) symCount 2; if (/[\w.\]\)]\s*\(/.test(trimmed)) symCount; if (/,\s*\w/.test(trimmed)) symCount; if (symCount 3) score 2; else if (symCount 1) score 1; if (/^\s{2,}|\t/.test(trimmed)) score 1; if (/^\/?[\w]/.test(trimmed)) score 1; return score 3 ? 1 : 0; } function applyCodeBlockStyle(doc, startIndex, endIndex) { var startRange doc.Paragraphs.Item(startIndex).Range; var endRange doc.Paragraphs.Item(endIndex).Range; var blockRange startRange.Duplicate; blockRange.End endRange.End; blockRange.Font.Name Consolas; blockRange.Font.NameFarEast 微软雅黑; blockRange.Font.Size 10.5; blockRange.Font.Color 0x333333; try { blockRange.Shading.BackgroundPatternColor 0xF2F2F2; } catch (e) {} for (var i startIndex; i endIndex; i) { var p doc.Paragraphs.Item(i); p.FirstLineIndent 0; p.LeftIndent 14.17; p.SpaceBefore 2; p.SpaceAfter 2; } }4.2 在WPS中创建与运行JS宏实际操作步骤是打开WPS文字在菜单栏找到“开发工具”选项卡。如果看不到这个选项卡去“文件-选项-自定义功能区”在右侧主选项卡列表里勾选“开发工具”。进入开发工具后点击“JS宏”按钮打开宏管理对话框。在“宏名称”输入框里填个名字比如SmartCodeStyle点击“创建”进入宏编辑器。左侧能看到当前文档对应的工程树编辑区粘贴上面整段代码保存后回到文档页面。回到宏对话框选中刚才创建的宏点击“运行”WPS就会开始扫描整个文档并处理代码段了。如果文档很长屏幕会短暂卡一下属正常现象处理完成后会弹出提示。4.3 绑定快捷键或按钮每次都从宏对话框点运行还是不够方便建议绑定快捷键。在WPS文字里通过“文件-选项-自定义功能区-键盘快捷方式”进入快捷键设置在“类别”里选“宏”右边找到SmartCodeStyle指定一个组合键比如CtrlAltC。这样之后写完文档按一下快捷键就能完成全部代码段样式处理。我个人的习惯是再绑定一个快速访问工具栏按钮。在快速访问工具栏上添加宏按钮之后每次处理代码段只需要一次点击肌肉记忆形成之后效率非常高。5. 实测效果与避坑清单5.1 测试样例与判定结果为了验证识别规则的效果我拿几种典型文本做了测试结果如下表输入示例关键词得分符号得分缩进得分总分判定结果这是一段普通的中文技术说明文字。0000普通行The quick brown fox jumps over the lazy dog.0000普通行if (a 0) { return a; }2204代码行def calculate_score(data):2103代码行int x 10;2204代码行print(hello)2215代码行请参考 https://example.com 文档0000普通行可以看到普通中文段落和英文句子即使包含一些常用词因为没有足够多的代码符号特征总分达不到3分不会被误判。而典型的代码行即使缺少缩进特征也能靠关键词加符号的组合达到阈值。5.2 常见误判场景及应对实际使用中我遇到最多的误判场景是文本中包含大量数学符号或变量名比如“设 x 某个值, y 另一个值”这类行含有等号和逗号符号计数达到1分但总分通常超不过阈值。还有一种情况是伪代码或算法描述例如“if x大于0 then 返回结果”包含关键词“if”和“return”但都是中文描述由于没有半角符号总分也只有2分不会被误判。如果某些段落确实被误判了一个应急办法是在宏运行前先选中不需要处理的段落设成其他颜色或者加上特殊标记。更好的办法是调整评分阈值把score 3改成score 4这样会更保守一些漏判一些短代码行的概率增大但误判率更低。根据我的经验阈值设为3在绝大多数中文技术文档里是最均衡的。还有一个容易被忽略的地方代码块里的空行如果特别多连续超过两个空行合并逻辑仍然会把它们划入代码块。视觉上看是一块浅灰区域里出现一段空白观感其实还好。如果希望严格控制可以把合并逻辑改成“最多允许连续计入一个空行”但这会明显增加代码复杂度我并没有把这部分放进主脚本。5.3 不同WPS版本的API兼容性注意事项WPS的JS宏API在不同版本之间有一些细微差别主要在两方面。一是Shading属性大多数版本支持Range.Shading.BackgroundPatternColor但也有版本不支持所以代码里加了try-catch即便背景色没设置成功字体和缩进仍会正常处理。二是ScreenUpdating属性写入Boolean值的行为在不同版本略有不同同样用try-catch容错。如果你使用的WPS版本里遇到“对象不支持此属性或方法”的报错先检查两行try-catch保护的代码是否生效。核心的字体设置、段落格式化这些基础API在近年版本中都能正常工作。另外我建议运行宏之前先CtrlS保存一次文档万一哪里出问题可以快速撤销恢复尤其是处理上百段的超长文档时这个习惯能避免很多麻烦。最后再分享一个我实际使用中很受益的小技巧把这套宏保存在一个自己常用的文档模板里以后新建的技术文档都会自动带这个宏不用每次重新创建模块。你可以先手动把代码放进空文档的JS宏模块然后把这个文档另存为.wpt模板格式新建文档时选择这个模板宏就一直在那里了。配合快捷键和快速访问工具栏按钮整套流程跑下来非常顺。