简介:Simian(Similarity Analyser)是一款面向软件开发与测试人员的代码重复检测工具,主要用于扫描代码库中的相似代码段,识别冗余代码与复制粘贴式编程,从而降低维护成本与潜在缺陷。它支持Java、C#、C、C++、JavaScript等多种语言,常被集成到Maven、Gradle等构建流程或持续集成环节,在每次提交时把关代码质量。本资源包共59个文件,约3.43MB,以38个html文档为主,涵盖更新日志、特性说明、安装指南与客户案例等官方页面;同时包含dll、jar、exe等可执行与依赖组件,以及gif、png、jpg等界面图像资源,另有dtd、xsl、pdf、txt等配置与许可文件,结构较为完整。目前已有1418人学习下载。借助其中的安装说明、规则阈值配置参考与报告格式定义,读者可快速完成环境搭建,理解检测敏感度设置与报告解读方法,并将重复检测纳入日常构建,持续提升代码可维护性与整体稳定性。
1. 接手遗留代码库的第一天,我用 simian 扫出了 47 处重复块
刚接手一个维护了六年的老系统时,最让人头皮发麻的不是看不懂业务逻辑,而是同一个工具类在三个模块里各有一份几乎一模一样的拷贝。改一处,另外两处忘了同步,线上就出玄学问题。这种“复制粘贴式复用”在长期迭代的项目里几乎不可避免,靠人眼 review 根本盯不过来。simian(Similarity Analyser)就是干这个的:它扫描源码目录,把连续多行高度相似的代码块揪出来,告诉你哪些地方在重复造轮子。它不依赖编译器,纯文本比对,Java、C#、C++、JavaScript、Python 等常见语言都能直接跑。适合谁?接手遗留系统的维护者、做代码质量门禁的 CI 负责人,以及想量化“重复率”这个指标的技术主管。这篇笔记按“它怎么判断重复 → 怎么跑起来 → 参数怎么调 → 坑在哪”的顺序拆一遍,最后给一个能直接塞进流水线的用法。
2. simian 的比对逻辑:为什么它不编译也能找出克隆代码
2.1 基于行窗口的相似度判定,而不是语法树
simian 的核心思路很朴素:把源文件按行切开,用一个滑动窗口在文件内和文件间比对连续行的相似程度。它不构建抽象语法树,也不做类型推导,所以启动快、语言无关。默认情况下,它把每一行做归一化处理——去掉首尾空白、忽略空行和纯注释行——然后看连续若干行是否在另一个位置以相同或高度相似的形式出现。判定“相似”的阈值由-threshold控制,默认是 10 行。也就是说,两个代码块至少有 10 行连续内容达到相似标准,才会被报告。这个设计的好处是误报相对可控,坏处是它对“把一行拆成两行”这种格式变化不敏感,但对“改变量名”反而能识别——因为归一化后变量名差异会被相似度算法容忍。
常见做法是先用默认阈值跑一遍全量扫描,看看重复的规模,再决定要不要收紧。如果一上来就把阈值调到 6 行,报告会多到没法看,反而失去重点。
2.2 归一化规则决定了哪些差异会被忽略
simian 在比对前会做几件事:去掉行首行尾空白、把连续空白压成一个空格、忽略大小写(可配置)、忽略空行、忽略以特定符号开头的注释行。这些规则意味着,如果你只是把userName改成user_name,或者调整了缩进,simian 仍然会认为这两块是重复的。反过来,如果你在重复块中间插了一行有实际逻辑的代码,窗口就被打断,可能不再报告。理解这一点很关键:simian 找的是“文本层面的连续相似”,不是“语义等价”。所以它报出来的结果需要人工判断——有些是真正该抽取的重复逻辑,有些只是碰巧长得像的样板代码(比如一堆 getter/setter)。
我一般会先看报告里的块大小,超过 30 行的重复块优先处理,10 到 20 行之间的先记录、批量评估。低于 10 行的默认不报,因为那种级别的重复抽取收益太低,反而增加抽象成本。
2.3 支持的语言与文件识别方式
simian 通过文件扩展名来判断语言,进而决定用哪套注释符号和归一化规则。常见映射包括.java、.cs、.cpp、.h、.js、.py、.rb、.php等。如果项目里有自定义扩展名,可以用-includes参数手动指定要扫描的文件模式。它不要求项目能编译,也不要求依赖装齐,所以对那种“拉下来根本跑不起来”的老代码库特别友好——你只需要有源码文件就能扫。这一点在实际维护场景里很实用:很多遗留系统连构建脚本都丢了,但重复代码的问题依然要治。
3. 把 simian 跑起来:命令行参数与目录扫描实操
3.1 最小可用命令与输出解读
假设你已经拿到了 simian 的可执行包,解压后得到一个 jar 文件(Java 版)或者对应平台的可执行文件。最简用法就是指定一个源码目录:
# 扫描 src 目录下所有支持的文件,使用默认阈值 10 行 java -jar simian.jar -threshold=10 src/执行后会输出类似下面的报告:
Similarity Analyser 2.x ... Found 12 duplicate blocks: src/moduleA/Utils.java:120-145 src/moduleB/Helper.java:88-113 (26 lines, 100% similarity)每一条报告包含两个(或多个)文件路径加行号范围,以及重复行数和相似度百分比。行号范围是闭区间,方便你直接跳到编辑器里对照。相似度 100% 表示归一化后完全一致,低于 100% 说明有少量差异但仍在阈值内。我一般会先把 100% 且行数超过 20 的块过一遍,这些基本是“复制后只改了个别标识符”的硬克隆,抽取价值最高。
3.2 用 includes 和 excludes 控制扫描范围
真实项目里不可能全量扫,测试代码、生成代码、第三方库通常要排除。simian 提供-includes和-excludes两个参数,都接受通配符模式:
# 只扫描 java 和 js 文件,排除 test 目录和 generated 目录 java -jar simian.jar \ -includes=**/*.java,**/*.js \ -excludes=**/test/**,**/generated/**,**/node_modules/** \ -threshold=12 \ src/这里有几个细节值得注意。-includes的默认值已经覆盖了常见源码扩展名,但如果你项目里有.vue或.tsx这类文件,需要手动加进去,否则会被跳过。-excludes的匹配是相对于你传入的根目录的,路径分隔符在 Windows 和 Linux 下都支持正斜杠,不用改。另外,-threshold我习惯设成 12 而不是默认的 10,因为 10 行左右的重复块里有不少是巧合性的样板代码,调到 12 能减少噪音,同时不会漏掉真正的大块克隆。
3.3 输出格式与集成到脚本里
simian 默认输出纯文本,也支持-outputType=xml或-outputType=csv,方便后续用脚本解析。比如在 CI 里,我会让它输出 XML,然后用一段 Python 统计重复块数量和总重复行数,超过阈值就失败:
import xml.etree.ElementTree as ET import sys # 解析 simian 输出的 XML 报告 tree = ET.parse('simian-report.xml') root = tree.getroot() blocks = root.findall('.//duplicate') total_lines = sum(int(b.get('lineCount', 0)) for b in blocks) # 重复块超过 20 个或总重复行超过 500 行则判定为不通过 if len(blocks) > 20 or total_lines > 500: print(f"重复检查未通过: {len(blocks)} 个块, {total_lines} 行重复") sys.exit(1) print(f"重复检查通过: {len(blocks)} 个块, {total_lines} 行重复")这段脚本的逻辑很直白:把 XML 里每个 duplicate 节点的行数累加,再和块数量一起做双重判断。参数lineCount是 simian 在 XML 里给出的重复行数,直接可用。阈值 20 和 500 是根据项目规模拍的,小项目可以收紧到 10 和 200。注意 XML 的根节点和属性名可能随版本略有差异,第一次跑的时候先看一眼实际输出再改脚本。
4. 避坑与排查:simian 用起来容易翻车的几个地方
4.1 报告里出现大量 getter/setter 重复
现象:扫描一个 Java 实体类较多的项目,报告里全是十几行的 getter/setter 块,看起来重复率很高,但实际没有抽取价值。
原因:simian 基于文本相似度,而 getter/setter 天然长得一样,归一化后几乎无法区分。默认阈值 10 行正好覆盖了典型的一组 getter/setter。
解决:用-excludes把实体类目录或*DTO*.java、*Entity*.java排除掉,或者把阈值提高到 15 以上。更好的做法是结合-ignoreIdentifiers参数(如果版本支持),让 simian 在比对时忽略标识符名称差异,这样只有结构真正相同的块才会被报出来。
4.2 扫描速度突然变慢,内存占用飙升
现象:扫描一个包含数万文件的大目录时,simian 跑了十几分钟还没结束,或者直接抛内存溢出。
原因:simian 默认会把所有文件内容加载到内存里做两两比对,文件数量一多,内存和 CPU 都吃不消。另外,如果-includes没限制好,把node_modules或target目录也扫进去,文件数会爆炸。
解决:先用-excludes把依赖目录、构建产物、日志目录全部排除。如果还是慢,可以分模块多次扫描,每次只传一个子目录。Java 版可以通过-Xmx调大堆内存,但根本办法还是缩小扫描范围。我一般会在项目根目录维护一个simian-excludes.txt,把常见的排除模式写进去,每次跑的时候用-excludesFile引用。
4.3 行号对不上,报告里的位置在编辑器里找不到
现象:报告显示Utils.java:120-145有重复,但跳到编辑器第 120 行发现内容对不上。
原因:simian 在归一化时会忽略空行和注释行,但报告里的行号是原始文件的行号。如果你在扫描后、查看前对文件做了格式化或增删行,行号自然偏移。另一个可能是文件编码问题导致 simian 按字节读取时行数计算有偏差。
解决:确保扫描和查看之间文件没有被修改。如果文件编码不是 UTF-8,在命令行加-encoding=UTF-8或对应编码显式指定。对于行号偏移,最稳妥的方式是拿报告里的行号范围去文件里搜那段代码的特征字符串,而不是死盯行号。
4.4 阈值调低后误报太多,调高又漏报
现象:阈值设 8 时报告几百条,设 15 时只剩几条,找不到一个“刚好”的值。
原因:不同模块的代码密度和重复模式差异很大,一个全局阈值很难同时适配所有目录。
解决:分目录设置不同阈值,或者跑两遍——第一遍用低阈值生成全量报告,第二遍用高阈值过滤出重点。我通常的做法是:先用默认 10 跑一次,把报告按重复行数降序排列,人工看前 20 条,根据实际情况决定是收紧还是放宽。不要追求一次调参就完美,重复检测本身就是一个迭代过程。
4.5 在 CI 里因为重复率波动导致构建不稳定
现象:同一个项目,两次构建的重复块数量差了好几个,导致门禁时好时坏。
原因:simian 的扫描结果受文件顺序、归一化细节影响,加上代码在分支间合并时行号变化,报告数量会有正常波动。如果门禁阈值卡得太死,就会误伤。
解决:把门禁条件从“绝对数量”改成“相对基线”。比如记录主干分支的重复行数作为基线,PR 构建时只要不超过基线的 110% 就通过。或者只对新增文件做重复检测,而不是全量扫描。这样既控制了新增重复,又不会被历史包袱拖累。
5. 进阶用法:把 simian 塞进流水线并做趋势跟踪
前面讲的都是单次扫描,但重复代码治理是个长期活儿。我现在的习惯是:在 CI 里加一个独立阶段跑 simian,输出 XML 报告并归档,同时把关键指标(重复块数、重复总行数、重复率)写进一个时序数据库或简单的 CSV 文件。这样每周能看到趋势——是在恶化还是在改善。具体做法是,在流水线脚本里加一段:
# 在 CI 中运行 simian 并生成带时间戳的报告 java -jar simian.jar \ -includes=**/*.java,**/*.js,**/*.py \ -excludes=**/test/**,**/generated/**,**/node_modules/**,**/dist/** \ -threshold=12 \ -outputType=xml \ -outputFile=simian-report-$(date +%Y%m%d).xml \ src/ # 用 Python 提取指标并追加到趋势文件 python3 - <<'EOF' import xml.etree.ElementTree as ET import csv, datetime, glob, os # 找到最新生成的报告文件 reports = sorted(glob.glob('simian-report-*.xml')) if not reports: raise SystemExit('未找到 simian 报告') latest = reports[-1] tree = ET.parse(latest) root = tree.getroot() blocks = root.findall('.//duplicate') total_lines = sum(int(b.get('lineCount', 0)) for b in blocks) # 追加到趋势 CSV with open('simian-trend.csv', 'a', newline='') as f: writer = csv.writer(f) writer.writerow([datetime.date.today(), len(blocks), total_lines]) print(f"今日重复块: {len(blocks)}, 重复行: {total_lines}") EOF这段脚本做了两件事:跑 simian 生成带日期的 XML,然后解析出块数和总行数追加到 CSV。参数-outputFile指定输出路径,$(date +%Y%m%d)保证每天的报告不覆盖。趋势文件积累几周后,你就能回答“这个季度重复代码是多了还是少了”这种问题,比单次扫描有说服力得多。
还有一个技巧:把 simian 的报告和代码评审结合起来。每次 PR 合并前,只对变更的文件跑一次 simian,如果新增了超过 15 行的重复块,就在评审里标出来。这样重复代码在进入主干之前就被拦住,而不是等积累成山再治理。我一般会在本地 pre-commit 钩子里加一个轻量检查,只扫暂存区的文件,阈值设 15,超过就提示“检测到新增重复块,建议抽取公共方法”。这个习惯坚持了半年,团队里新产生的硬克隆少了七成。
从那以后我每次接手新代码库,第一件事就是跑一遍 simian 摸清重复底数,再决定重构优先级。希望帮到你。
本文还有配套的精品资源,点击获取