首页 / 资讯中心 / 文章详情

simian代码重复检测工具:从原理到CI门禁配置实战

simian代码重复检测工具:从原理到CI门禁配置实战 ★ FEATURED ARTICLE
简介Simian 是一款经典的代码重复检测工具面向 Java、C#、C/C、JavaScript 等多语言项目用于在持续集成流程中发现冗余代码与复制粘贴片段帮助开发者降低维护成本。这份资源收录了 Simian 2.3.35 的完整发行组件共 59 个文件压缩包仅 3.43MB其中包含可直接运行的 jar、exe 程序配套的 simian.dtd 与 simian.xsl 管理报告格式另有 JDK 1.5 安装日志、官方文档更新日志、特性、安装指南、客户案例等以及大量 javadoc HTML 页面适合需要研究旧版工具行为或自行集成检测流程的开发者参考。资源包文件类型以 38 个 HTML、5 个 DLL、5 个 GIF 为主还有 PDF 许可协议与 CSS、PNG 等辅助素材整体结构完整。目前已有 1416 人下载学习对于想快速上手 Simian、了解其报告输出和持续集成用法的读者这是一份小而全的离线资料包。1. simian代码重复检测工具先看它如何抓住“改头换面”的重复代码接手某个遗留系统时我在评审单里看到两段 300 多行的逻辑几乎一模一样只有变量名不同、缩进不一样肉眼排查花了大半天。simian代码重复检测工具就是为这类场景准备的它不按文本直接比对而是把源码拆成 token 流在文件之间找“结构重复”的片段再按阈值输出重复块位置缩进、换行、注释这些表面差异基本不影响结论。它是经典的代码判重工具支持 Java、C#、JavaScript、TypeScript 等主流语言参数既可以用命令行传也可以写进配置文件统一管理输出支持文本、XML、CSV 三种格式。老项目用它量化重复债务新项目放进持续集成当质量门禁都很顺手。适合被复制粘贴坑过的开发者也适合想给团队立一套代码规矩的技术负责人。2. simian 的判重原理与工具选型为什么它比文本 diff 更可靠文本 diff 只能告诉你“这两行长得像”遇到改过缩进、拆行、换了变量名的重复代码就会漏掉。simian 的做法是先做词法切分把源码变成一串 token再在 token 序列里找重复片段这一层抽象让它能识别“结构层”的复制粘贴而不是受排版干扰。2.1 从字符 diff 到 token 指纹simian 如何识别“改过缩进”的重复两张代码表面完全不同逻辑结构却一样是复制粘贴最常见的形态。看下面这对文件// A.java public int add(int x, int y) { return x y; } // B.java public int sum(int a, int b) { return a b; }如果拿文本 diff 去比对两个文件可能没有任何一行完全相同但 simian 会把各自切成 token 序列public、int、IDENTIFIER、(、IDENTIFIER、,……再加上若干字面量 token。后半段return x y;和return a b;在 token 类型上是同构的是否判重取决于标识符和字面量规则怎么开。常见做法是把“忽略标识符”“忽略字符串值”“忽略数字”这些开关按团队规范打开这样只要结构一致就会被点名。这里的关键点是 threshold 的单位是 token 数不是行数。连续出现多少个 token 的重复片段才算问题由你定默认经验值在 6 到 10 之间。阈值越低误报越多调太高又会漏掉短小的重复块具体数值后面单独说。正因为判断发生在 token 层注释、空行、空格、换行全都不参与匹配这两段代码即使一个从 80 列格式改成炫酷菱形缩进结果也相同。这是 simian 和grep、文本 diff 类方案的本质区别也是它这么多年还在被用的原因。2.2 和常见候选工具对比单 jar 和跨语言是它最大的优势谈到重复代码检测通常绕不开几个选择我把使用体验放在一起看候选运行方式多语言覆盖输出与门禁友好度simian本资源单个 Java jar常见企业语言大多覆盖text/xml/csv门禁场景可直接读取退出码某 Java 静态分析平台的 CPD 组件和该平台一起运行覆盖广但平台较重报告丰富、图形化好跑全量时内存占用偏高某个 Node 生态的重复检测工具Node 命令行前端技术栈为主JSON/HTML 展示漂亮增量扫描脚本多我一般会这样选如果团队原本就在用那套 Java 静态分析平台CPD 能顺手拿到重复报表如果项目是纯前端或 Node 生态那个 Node 工具的可视化更愉悦。但放到企业内部后端项目里仓库里既有 Java 又有 JavaScript还可能混着 SQL、XML 模板simian 的优势就出来了一个 jar 包、一条命令行CI 里加步骤没有额外环境依赖。这也是我把它当第一选择的原因而不是因为它功能列表比谁多。2.3 安装与首次运行拿 jar 包跑通第一条命令simian 的产物是一个可执行 jar前提是本机已装好 Java 运行时。下载后把 jar 放在项目根目录的tools/子目录里首次扫描建议用最朴素的一条命令验证环境java -jar tools/simian.jar -includes**/*.java -threshold6 -reportFormattext这里-includes**/*.java表示只扫描所有 Java 源文件通配符走 ant 风格能递归匹配子目录-threshold6是连续 6 个 token 重复就报告-reportFormattext让人直接读终端输出。我给.java套了双引号防止 shell 先把通配符展开成一层目录名这个习惯建议一直保留。跑通之后你会看到两类输出有重复块时会列出源文件、起始行和结束位置没有重复时静默通过只打印统计摘要。如果项目是第一次做这种扫描6 这个阈值通常会报出一批短片段先别急着调低看两三条报告确认文件路径是否合理、排除规则对不对再进入下一步配置。3. 命令行参数与输出格式把调优后的规则固化成“门禁”上一章的命令能跑通但不够用真正的项目需要把 exclude 目录、语言指定、报告文件都定下来。这章把参数逐个拆开最后落成一个工程内通用的配置。3.1 核心选项逐个拆解includes、excludes、threshold、language常用参数其实就七个记住它们就能覆盖大部分场景选项作用常用值参考-includes指定参与扫描的文件模式src/**,test/**-excludes排除目录或文件**/generated/**,**/target/**-threshold连续重复 token 数下限6 到 10历史老代码可以先 10-language强制指定语言解析规则java、csharp、javascript-failOnDuplicates发现重复时返回非零退出码true-reportFormat输出格式text/xml/csv-reportFile报告写入路径build/reports/simian/simian.xml一个生产环境可用的扫描命令大概是这个样子java -jar tools/simian.jar \ -includessrc/**,test/** \ -excludes**/generated/**,**/target/** \ -threshold8 \ -languagejava \ -failOnDuplicatestrue \ -reportFormatxml \ -reportFilebuild/reports/simian/simian.xml逐段解释src/**,test/**把业务代码和测试代码都纳入扫描测试里也有大量复制粘贴不能漏excludes里generated和target是 MyBatis 生成类、构建插件的产物目录不排除会带来海量假重复threshold8是我在 Java 项目里常用的起始值比 6 少一半误报又不会放过有明显复制痕迹的段落languagejava避免个别后缀奇怪的源文件被当成别的语言解析failOnDuplicatestrue让有重复时进程返回非零码CI 里才能做门禁。注意老项目首次全量扫描阈值建议从 10 起跳。一次跑出来几千个重复块对团队没有正向激励先拿到一个能看的基线再逐步下调到 8、6看重复数增长曲线找到一个“开始明显变陡”的位置作为阈值。3.2 输出格式文本、XML 与 CSV 各管一段text 格式适合人眼快速定位CSV 方便脚本做聚合XML 适合给持续集成解析。实际项目里我习惯一次性输出 XML再由脚本转摘要。先看 CSV 命令怎么出java -jar simian.jar -includes**/*.java -threshold8 -reportFormatcsv -reportFilesimian.csv拿到 CSV 后可以用 Python 快速统计重复块最多的文件定位“重灾区”import csv from collections import Counter counter Counter() with open(simian.csv, newline, encodingutf-8) as f: for row in csv.reader(f): # 第一列一般是源文件路径首行可能有表头先跳过 if not row or row[0].startswith(source) or row[0].startswith(file): continue path row[0] counter[path] 1 for path, count in counter.most_common(10): print(f{count:4d} {path})这段脚本的逻辑是逐行读 CSV取第一列文件路径做计数。不同版本的 simian 列名略有差异第一次运行时先打印两行看下表头再决定跳过条件比我这里写死的字符串更可靠。sorted 之后你就能知道哪些文件是复制粘贴的重灾区通常也是后续重构优先级最高的地方。3.3 用配置文件固化规则让团队跑的是同一套标准命令行参数一多就有人记错、漏传结果别人报 20 个重复块你报 200 个对不上。simian 支持把参数写进配置文件一行一个keyvalueincludessrc/**,test/** excludes**/generated/**,**/target/** threshold8 languagejava failOnDuplicatestrue reportFormatxml reportFilebuild/reports/simian/simian.xml然后一条命令读取java -jar tools/simian.jar -configconfig/simian.properties配置文件提交到代码仓库同事拉下来直接用同一套规则后续调阈值也只在文件里改一处。这个文件就是团队的“后悔药”上次扫描用的什么参数翻一下记录就知道不会再出现“为什么我这儿结果和你不一样”的争论。4. 把 simian 接入现有流程报告落地与 CI 门禁命令行跑通只是一个起点真正让重复率受控的是把它编进持续集成。这章给出我在持续集成平台里加重复检测步骤的写法以及报告怎么解析成团队看得懂的摘要。4.1 在持续集成任务里写一个重复扫描步骤先明确退出码的含义-failOnDuplicatestrue时没有重复返回 0有重复返回非 0参数错误也可能返回非 0。所以脚本里不能简单用set -e一棍子打死要分支处理。下面是一个通用 bash 示例set -e # 先清掉上次构建的旧报告避免误读 rm -f build/reports/simian/simian.xml || true # 执行扫描保存退出码供后续判断 set e java -jar tools/simian.jar -configconfig/simian.properties status$? set -e if [ $status -eq 0 ]; then echo simian: no duplicate blocks found elif [ $status -eq 1 ]; then echo simian: found duplicate blocks, check the report # 需要硬性门禁时这里可以决定是否继续 # exit 1 else echo simian: scan error or invalid config, exit code $status exit $status fi # 把报告归档为构建产物方便打开定位 mkdir -p artifacts cp build/reports/simian/simian.xml artifacts/simian.xml这段的关键点是先set e拿到真实退出码再恢复set -e避免有重复块时脚本被中途打断连报告都来不及存档。status的三种分支也顺手把“参数错误”和“发现重复”区分开排查时不用猜。我通常把这一步放在单元测试之后、制品构建之前。原因很简单重复检测跑得太早大量代码还没定型误报会干扰节奏跑得太晚改代码的成本已经上去了。放在测试后意味着语法和功能是通的这时候谈重复率团队接受度最高。4.2 解析报告把 XML 转成一条可读的构建摘要XML 报告包含重复块的详细位置但太长不适合直接贴在构建日志里。我习惯加一段解析脚本只抽取关键数字和 Top 10 文件import xml.etree.ElementTree as ET from collections import Counter tree ET.parse(build/reports/simian/simian.xml) root tree.getroot() # simian 不同版本的节点名略有差异这里兼容常见写法 blocks [] for node in root.iter(): if node.tag.lower() in (duplicate, repeat, block): blocks.append(node) print(ftotal duplicate blocks: {len(blocks)}) # 统计每个源文件被重复命中的次数 files Counter() for blk in blocks: for item in blk.iter(): tag item.tag.lower() if sourcefile in tag or file in tag: if item.text: files[item.text.strip()] 1 for path, cnt in files.most_common(10): print(f{cnt:4d} {path})逻辑说明先遍历 XML 找重复块节点再嵌套遍历子节点找文件名。因为不同版本的 simian 节点名有出入我按sourcefile或file关键字模糊匹配而不是写死某个标签。跑一次之后把实际打印的节点结构看一眼再决定要不要收紧条件。这段脚本不进代码仓库也没有关系但项目里至少要在构建脚本里保留一个调用它的步骤让重复块“总量”这个数字出现在构建页面上。数据只有被看见才会被重视。5. 避坑simian 使用中的五个高频翻车现场下面几条全是实际用出来的血泪经验。每一条都按“现象、原因、解决”写清楚遇到类似问题可以直接对着处理。5.1 threshold 设成 4重复结果多到没人看现象命令跑完输出几百个重复块告警刷屏代码评审时没人愿意打开报告。 原因连续 4 个 token 在 Java 里太常见了return null;、if (x null)、throw new RuntimeException都轻松撞上大量“非典型重复”被标记出来。 解决把 threshold 先提到 8 或 10拿到一份不那么刺眼的基线之后每次降 2 再跑观察重复块数量的增幅。如果从 8 降到 6 时数量暴涨三倍说明当前仓库的合理阈值是 8再低只会制造噪音。这个“突变点”比我拍脑袋定的数字可靠得多。5.2 没排除 build/target结果重复率虚高现象扫描报告里重复块集中在target/generated-sources或build目录业务代码反而干干净净。 原因-includes写得太宽或者根本没写构建产物被当作源码扫进来生成的代码天然和源码高度相似。 解决把排除目录补全-excludes**/target/**,**/build/**,**/generated/**,**/node_modules/**。这两个目录一定不能省否则报告数据完全失真且会让团队对工具的信任度迅速下降。5.3 failOnDuplicates 的退出码方向用反现象CI 任务要么永远红要么永远绿和有没有重复毫无关系。 原因不少脚本写成java -jar simian.jar ... || true把“有重复返回非零”和“执行失败”混为一谈或者反过来认为返回 0 才是有问题。 解决像 4.1 那样先保存退出码再分支。记住语义没有重复返回 0有重复返回非零参数错误也可能非零。想只用报告判断的话就解析 XML 后看重复块数量而不是只看退出码。5.4 多语言仓库混扫模板文件被当成同一种语言现象PHP 和 HTML 模板、Java 和 JSP 混在一起报告里出现了“跨语言重复”的诡异片段。 原因simian 按扩展名推断语言遇到.tpl、.jsp、.vue这类不标准后缀时可能落到错误的解析器token 切分规则一变误报就来了。 解决按语言拆成多次扫描先扫 Java再单独扫 JavaScript每个场景用-language强制指定对模板类文件可以单独建一个 exclude 列表或者把它们放到单独的扫描任务里用文本类规则处理。5.5 大仓库扫描直接内存溢出现象命令跑了一半报OutOfMemoryError或者干脆卡死不动。 原因默认 JVM 堆太小几百万行代码的仓库全量扫描时token 流和重复匹配都占内存堆被耗尽。 解决给 JVM 分配大堆JAVA_OPTS-Xmx2g java -jar tools/simian.jar ...同时收窄-includes到真正的源码目录把node_modules、第三方依赖、生成代码先排除掉。仓库实在太大按模块分多次跑最后合并报告比单次全量扫完要稳得多。6. 进阶用法用 simian 做持续重构的度量标尺6.1 把重复量记录成趋势表限制重复代码不只是靠某一次扫描而是让重复量随时间可见。我用一个小脚本把每次扫描的重复 token 数追加到一个 CSV每周自动跑一次形成趋势线import xml.etree.ElementTree as ET from datetime import date tree ET.parse(build/reports/simian/simian.xml) root tree.getroot() # 兼容不同版本的 summary 写法找 attribute 里含 duplicate 和 token 的值 duplicate_token 0 for node in root.iter(): attrs {k.lower(): v for k, v in node.attrib.items()} for key, val in attrs.items(): if duplicate in key and token in key: duplicate_token max(duplicate_token, int(val)) with open(duplicate-trend.csv, a, encodingutf-8) as f: f.write(f{date.today().isoformat()},{duplicate_token}\n)每周看一眼这个数字比任何口号都直观。它让我意识到“复制粘贴不是一次重构能解决的但趋势向下就意味着团队在改变习惯”。6.2 只扫描本次改动文件避免老历史淹没新问题全量扫描会把多年积攒的老重复债全部翻出来新改动在里面完全不显眼。另一种常见做法是和版本控制配合扫描本次提交涉及的文件changed$(git diff --name-only HEAD~1 -- *.java) if [ -n $changed ]; then java -jar tools/simian.jar -threshold8 $changed fi注意$changed如果有文件路径带空格展开后会被切分稳妥的做法是循环逐个传入或者把 CI 的 working directory 控制得足够规整。这种做法适合“渐进式治理”新代码不允许制造新的重复老债务另开专项处理两者互不干扰。从那以后我每次接手老项目第一件事就是把 simian 配好阈值先放到 10 跑一遍留个基线报告之后的每个迭代都对比上周的重复量。看到数字往下走比自己嘴上说“注意代码质量”有说服力得多希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站