脚本目录重复代码治理:从18个文件到6个模块的重构实践
1. 脚本目录里的“重复建设”是怎么发生的1.1 从18个文件、1803行说起某天我打开自己的脚本目录想找一个批量重命名文件的工具。结果翻了一遍发现里面躺着18个文件加起来1803行代码。更让我难受的是其中4个脚本在做几乎一模一样的事情——都是读取某个目录下的文件列表按规则处理后输出结果。区别只在于一个处理图片、一个处理文档、一个处理表格、一个处理日志。这不是个例。我后来跟几位做自动化运维和数据处理的朋友聊发现大家都有类似的经历。脚本目录就像一个杂物间每次遇到新需求就随手写一个写完往目录里一扔下次遇到类似需求时早就忘了之前写过于是又写一个新的。时间一长重复代码越积越多维护成本直线上升。这篇文章想聊的就是当你发现自己的脚本目录里出现大量重复逻辑时该怎么系统性地做一次“代码去重”和“结构重组”。我会从问题诊断、方案设计、实操步骤、避坑经验四个层面展开适合所有写过十几个以上脚本、开始感到维护吃力的开发者阅读。不管你是用 Python、Shell 还是 Node.js 写脚本思路都是相通的。1.2 重复脚本的四种典型形态在动手整理之前先要搞清楚“重复”到底以什么形式存在。根据我的观察和实际整理经验脚本重复大致分为四类第一类复制粘贴型重复。两个脚本之间有80%以上的代码完全相同只是变量名或输入路径不一样。这种最容易识别也最容易合并。第二类逻辑同构型重复。代码结构相似但实现细节不同。比如一个用os.listdir遍历目录另一个用pathlib.Path.glob但后续的处理流程几乎一致。这种需要抽象出公共接口。第三类功能碎片型重复。同一个功能被拆散在多个脚本里每个脚本只做一小部分。比如日志解析功能A脚本负责读取、B脚本负责过滤、C脚本负责统计但实际上完全可以合并成一个带参数的脚本。第四类隐性重复。表面上看两个脚本做的事不同但底层调用的工具函数、配置读取逻辑、错误处理方式完全一样。这种最隐蔽也最值得优先处理。我自己的18个文件里第一类和第二类各占一半左右。那4个“干同一件事”的脚本本质上都是“遍历目录→按规则筛选→执行操作→输出报告”这个流程的不同实例。1.3 为什么我们总是重复造轮子说到底重复脚本的产生有几个深层原因。一是写脚本时追求“快”复制一个现有脚本改改变量就能跑比从头设计一个通用方案快得多。二是缺乏脚本索引写完就忘下次遇到类似需求时根本想不起来之前写过。三是害怕改动觉得现有脚本能跑就行重构万一引入bug反而麻烦。四是没有抽象意识习惯用面向过程的方式写脚本没有把公共逻辑抽成函数或模块的习惯。理解这些原因很重要因为去重不是单纯的技术问题而是工作习惯的问题。如果不在流程上做改变整理完这一轮过几个月又会积累出一堆新重复。2. 去重前的诊断与分类方法2.1 用“功能矩阵”快速定位重复动手改代码之前先做一次全面盘点。我的做法是建一张功能矩阵表行是脚本文件名列是功能模块如“读取配置”“遍历目录”“数据转换”“输出报告”“错误处理”等在交叉格子里标记该脚本是否包含该功能。脚本名称读取配置遍历目录数据转换输出报告错误处理rename_v1.py是是是否否rename_v2.py是是是是否batch_img.py是是是否是log_parse.py否是是是是report_gen.py是否是是否这张表一画出来重复区域一目了然。“遍历目录”和“数据转换”两列几乎全是“是”说明这两个功能最适合抽成公共模块。而“读取配置”虽然出现频率高但每个脚本的配置项不同需要设计成可扩展的配置加载器。提示功能矩阵不需要很精确凭印象填就行。目的是快速找到“高频功能”而不是做严格的代码审计。2.2 判断哪些该合并、哪些该保留不是所有重复都值得合并。我的判断标准有三条调用频率如果某个功能在5个以上脚本中出现合并收益最高。变更频率如果某个逻辑经常需要修改比如输出格式调整合并后只需改一处收益明显。耦合程度如果两个脚本虽然逻辑相似但依赖的外部工具完全不同强行合并反而增加复杂度不如保留独立。那4个“干同一件事”的脚本我最终决定合并其中3个保留1个。保留的那个是因为它依赖一个特殊的第三方库合并进来会让公共模块变得臃肿。这个取舍后面会详细说。2.3 整理前的备份与版本标记这一步很多人会跳过但我强烈建议做。在动手重构之前先把整个脚本目录复制一份命名为scripts_backup_日期。然后在原目录里初始化一个 Git 仓库如果还没有的话把所有文件提交一次打上 tag 比如pre-refactor。cd ~/scripts git init git add . git commit -m 重构前完整备份 git tag pre-refactor这样做的好处是万一重构过程中发现某个脚本有隐藏的依赖关系可以随时回退对比。我就在重构到一半时发现一个脚本被定时任务调用参数格式和预期不一样靠对比备份才快速定位到问题。3. 公共模块的抽取与设计3.1 先抽“工具函数层”去重的第一步是把最底层、最通用的操作抽成独立模块。以我的脚本目录为例出现频率最高的操作包括读取 YAML/JSON 配置文件遍历指定目录并按扩展名过滤文件统一的日志输出格式文件读写时的异常捕获我把这些抽成了一个common_utils.py大概120行。关键设计原则是每个函数只做一件事参数尽量少返回值明确。# common_utils.py import os import json import logging from pathlib import Path def load_config(config_path): 加载JSON配置文件返回字典 with open(config_path, r, encodingutf-8) as f: return json.load(f) def list_files(directory, extensionsNone): 遍历目录可按扩展名过滤 path Path(directory) if extensions: files [] for ext in extensions: files.extend(path.glob(f*.{ext})) return sorted(files) return sorted(path.iterdir()) def setup_logger(name, levellogging.INFO): 创建统一格式的日志器 logger logging.getLogger(name) if not logger.handlers: handler logging.StreamHandler() formatter logging.Formatter( %(asctime)s [%(levelname)s] %(name)s: %(message)s ) handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(level) return logger这三个函数看起来简单但抽出来之后每个业务脚本平均减少了15到20行代码。更重要的是日志格式统一了排查问题时不用再适应不同脚本的输出风格。3.2 再抽“流程模板层”工具函数解决的是“零件”问题但脚本的“装配流程”往往也是重复的。比如那4个脚本都是“读配置→遍历文件→逐个处理→汇总输出”这个流程。这时候可以用模板方法模式把流程骨架固定下来把变化的部分留给子类或回调函数实现。# pipeline.py class FileProcessor: def __init__(self, config): self.config config self.results [] def run(self): files list_files(self.config[input_dir], self.config.get(extensions)) for f in files: try: result self.process_file(f) self.results.append(result) except Exception as e: self.logger.error(f处理 {f} 失败: {e}) self.output_report() def process_file(self, filepath): raise NotImplementedError def output_report(self): raise NotImplementedError这样图片处理脚本只需要继承FileProcessor并实现process_file和output_report两个方法代码量从原来的80多行降到20行左右。而且错误处理、日志记录这些横切关注点自动统一了。3.3 配置驱动的参数设计合并脚本时最大的挑战是不同脚本的参数不一样。比如重命名脚本需要“命名规则”参数图片处理脚本需要“目标尺寸”参数。我的解决方案是用配置文件驱动每个脚本对应一个JSON配置公共模块只读取配置中的键值对。{ input_dir: /data/photos, extensions: [jpg, png], operation: resize, params: { width: 800, height: 600, keep_ratio: true }, output_dir: /data/processed }这样做的好处是新增一种处理类型时不需要改公共模块的代码只需要写一个新的处理函数并注册到映射表里。配置和代码分离也方便非开发者调整参数。注意配置文件不要放在脚本目录里建议单独建一个configs/目录。我一开始混在一起放结果重构时差点把配置文件当脚本删掉。4. 实操从18个文件到6个模块的完整过程4.1 第一步冻结现有脚本建立调用清单在改任何代码之前我先做了一件事找出所有脚本的调用来源。有些脚本是被 crontab 定时调用的有些是被其他脚本 import 的还有些是手动执行的。我用grep在整个 home 目录下搜索脚本文件名整理出一张调用关系表。grep -rn rename_v1\|batch_img\|log_parse ~/ --include*.sh --include*.py --includecrontab* 2/dev/null结果发现有两个脚本被定时任务引用还有一个被另一个更大的分析脚本 import。这意味着我不能直接删除或重命名它们必须先建立兼容层。我的做法是保留原文件名但内容改成一行调用新模块的代码。比如rename_v1.py变成from modules.renamer import main if __name__ __main__: main()这样外部调用完全不受影响内部实现已经切换到新模块。4.2 第二步逐个合并每合并一个就测试重构最忌讳“一口气全改完再测试”。我的节奏是合并一个功能立刻跑测试确认无误后再合并下一个。具体流程是从功能矩阵中选一个高频功能比如“遍历目录”在common_utils.py中实现该功能修改一个脚本让它调用新函数运行该脚本对比输出结果和修改前是否一致确认无误后再修改下一个脚本这个过程比较枯燥但能最大程度降低风险。我合并“遍历目录”功能时就发现原来的脚本里有一个隐藏行为某些脚本会跳过隐藏文件某些不会。如果直接统一成“跳过隐藏文件”会导致部分脚本行为改变。最后我在函数里加了一个skip_hiddenTrue参数默认跳过需要包含隐藏文件的脚本显式传False。4.3 第三步用测试脚本验证行为一致性为了确保合并后的行为和老脚本完全一致我写了一个简单的对比测试脚本。思路是用同一组输入数据分别跑老脚本和新模块对比输出文件的MD5或内容差异。import subprocess import hashlib def file_md5(path): with open(path, rb) as f: return hashlib.md5(f.read()).hexdigest() # 跑老脚本 subprocess.run([python, old_script.py, --input, test_data]) old_result file_md5(output/result.txt) # 跑新模块 subprocess.run([python, -m, modules.new_processor, --config, test_config.json]) new_result file_md5(output/result.txt) assert old_result new_result, 输出不一致这个测试帮我抓到了两个问题一个是新模块的排序逻辑和老脚本不同老脚本按修改时间排序新模块默认按文件名排序另一个是浮点数精度处理有细微差异。如果没有这个对比测试这些问题可能要等到实际使用时才会暴露。4.4 第四步清理与归档所有脚本合并完成后我把不再需要的文件移到一个archive/目录而不是直接删除。保留三个月后再清理。同时更新了 README说明每个模块的用途和调用方式。最终的目录结构从原来的18个散落文件变成了scripts/ ├── common_utils.py # 通用工具函数 ├── pipeline.py # 流程模板 ├── modules/ │ ├── renamer.py # 重命名模块 │ ├── img_processor.py # 图片处理模块 │ ├── log_analyzer.py # 日志分析模块 │ └── report_gen.py # 报告生成模块 ├── configs/ # 配置文件 ├── archive/ # 归档的旧脚本 └── README.md代码总量从1803行降到了约900行但功能覆盖完全一样而且新增处理类型时只需要写一个几十行的模块文件。5. 常见问题与避坑经验5.1 合并后参数不兼容怎么办这是最常见的问题。老脚本A用--dir指定目录老脚本B用--path合并后只能保留一个。我的处理方式是新模块统一用一套参数命名然后在兼容层做参数映射。# 兼容层示例 import argparse parser argparse.ArgumentParser() parser.add_argument(--input, -i, destinput_dir) parser.add_argument(--dir, destinput_dir) # 兼容老参数 args parser.parse_args()这样老调用方式仍然可用新代码则统一用--input。等所有调用方都迁移完成后再移除兼容参数。5.2 循环依赖怎么破抽取公共模块时容易遇到循环依赖common_utils引用了pipeline而pipeline又引用了common_utils。我的经验是严格分层工具层不依赖任何业务层流程层只依赖工具层业务模块依赖流程层和工具层。如果发现反向依赖说明抽象层级设计有问题需要重新划分。5.3 性能反而下降的意外情况合并后有一次发现处理速度变慢了。排查后发现新模块在每次处理文件时都重新读取了配置文件而老脚本只在启动时读一次。解决办法是在FileProcessor.__init__中加载配置并缓存而不是在process_file中反复读取。实操心得重构后一定要做一次性能对比。功能一致不代表体验一致尤其是涉及大量文件处理的脚本。5.4 问题速查表问题现象可能原因排查方法输出结果和老脚本不一致排序逻辑、默认参数不同用相同输入对比MD5定时任务报错参数格式不兼容检查crontab中的调用命令处理速度变慢重复读取配置或重复初始化加计时日志定位瓶颈某些文件被跳过隐藏文件过滤规则不同检查 skip_hidden 参数日志格式混乱多个logger实例冲突统一用 setup_logger 创建5.5 我踩过的最大的坑过度抽象一开始我雄心勃勃想把所有脚本合并成一个“万能脚本”用一个大配置文件控制所有行为。结果写到一半发现配置文件变得极其复杂一个操作要嵌套三四层参数才能描述清楚。后来我退回到“公共模块独立入口”的方案每个业务场景仍然有自己的入口脚本只是内部调用公共模块。这样既减少了重复又保持了每个脚本的独立性。这个教训让我明白去重的目标是减少维护成本不是追求代码行数最少。如果合并后配置复杂度超过了一定阈值就应该停下来重新评估。6. 后续维护与扩展建议6.1 建立脚本登记习惯整理完之后我在 README 里加了一个简单的登记表每新增一个脚本必须填写用途、输入、输出、依赖模块。这样下次想写新脚本时先翻一下登记表看看有没有现成的模块可以复用。脚本名用途依赖模块最后更新renamer.py批量重命名common_utils2024-01img_processor.py图片缩放common_utils, pipeline2024-026.2 定期做“脚本体检”我给自己定了一个规矩每季度花半小时翻一遍脚本目录看看有没有新增的重复逻辑。如果发现两个脚本有超过30%的相似代码就考虑抽取公共部分。这个习惯坚持了半年脚本目录再也没有回到混乱状态。6.3 什么情况下应该停止合并最后说一个反向经验不是所有重复都值得消除。如果两个脚本虽然逻辑相似但一个每周跑一次、另一个每月跑一次而且未来可能朝不同方向演化那强行合并反而会增加耦合。我的判断标准是如果合并后的模块需要频繁加参数来区分场景那就说明它们本来就不该合并。我在实际整理中发现18个文件里真正值得合并的是那4个核心重复脚本其他14个各有各的用途保持独立反而更清晰。去重不是目的让脚本目录变得可维护、可扩展才是。

相关新闻

PS5通用操作手册:从初始化、扩容到串流与安全的完整指南

PS5通用操作手册:从初始化、扩容到串流与安全的完整指南

每次有人问我“AnyPS5”到底是个什么东西,我都得先解释一遍:它不是某个游戏,也不是什么新机型,而是一个很朴素的念头——不管你是刚把PS5抱回家的新手,还是已经玩了两年的老玩家,总会遇到一批绕不开的通用问…

2026/10/12 11:03:11 阅读更多 →
AI会话失忆怎么办?用claude-mem为终端AI助手搭建长期记忆层

AI会话失忆怎么办?用claude-mem为终端AI助手搭建长期记忆层

1. 每个会话都是“第一次见面”:我为什么会对记忆工具上头 先说个场景。你手里同时维护着两三个项目,每个项目都有一堆背景约束、技术债、历史决策。这天你调完一个 bug,隔了两天再打开终端,想继续之前那条思路,结果 A…

2026/10/12 11:03:11 阅读更多 →
跳频信号盲检测:从时频图到体制逆向的全流程实现

跳频信号盲检测:从时频图到体制逆向的全流程实现

简介:本资源是一份面向通信工程专业高年级本科生及信号处理初学者的跳频信号盲检测与参数估计仿真实验材料,聚焦FHSS系统中无先验信息条件下的信号识别与关键参数提取问题,适用于课程设计、毕业设计及科研入门场景。压缩包共1个文件&#xff…

2026/10/12 11:03:11 阅读更多 →

最新新闻

机器学习最小可行知识链:从公式到可调试代码的完整实践

机器学习最小可行知识链:从公式到可调试代码的完整实践

简介:本资源是邹博主讲的《机器学习全方位解析:从基础到实践》配套教学包,面向零基础入门者与希望系统夯实理论与工程能力的学习者,解决机器学习概念抽象、代码实践脱节、算法调优无从下手等核心痛点。压缩包共613个文件&#xff…

2026/10/12 13:00:09 阅读更多 →
Harbor v2.13.1 ARM64离线部署实战:内网镜像仓库搭建与避坑指南

Harbor v2.13.1 ARM64离线部署实战:内网镜像仓库搭建与避坑指南

简介:本资源为 Harbor 最新 v2.13.1 的 ARM64 架构离线安装包,面向在 ARM64 服务器或 Kubernetes 集群上部署私有镜像仓库的运维与开发人员,解决国产化平台、信创环境及边缘节点无法直接拉取官方 x86 安装包的问题。压缩包共 6 个文件&#x…

2026/10/12 13:00:09 阅读更多 →
物联网平台二次开发选型指南:架构边界、扩展点与实战避坑

物联网平台二次开发选型指南:架构边界、扩展点与实战避坑

1. 为什么“适合二开”才是物联网平台选型的硬指标做物联网平台选型这件事,我踩过不少坑。早年做某智慧园区项目,技术评审时大家盯着功能清单看——设备接入、规则引擎、数据大屏、告警中心,列表很丰满,厂商演示也很流畅。结果签完…

2026/10/12 13:00:09 阅读更多 →
批处理驱动Matlab实现参数自动扫描:原理、传参与排错指南

批处理驱动Matlab实现参数自动扫描:原理、传参与排错指南

说实话,用批处理去反复跑Matlab这事儿,我最早是在一次大规模参数寻优中被逼出来的。那时候手里有一堆实验数据要拟合,模型参数有四五种组合方式,每组参数跑一遍得几十秒,加起来几百个组合,手动改一个跑一个…

2026/10/12 13:00:08 阅读更多 →
PyTorch新手必学:ResNet18从原理到手写实现详解

PyTorch新手必学:ResNet18从原理到手写实现详解

1. 项目概述:为什么ResNet18是PyTorch新手绕不开的“第一座桥”如果你刚打开Jupyter Notebook,敲下import torch,接着想跑一个真正能分类图像的模型——不是MNIST手写数字那种玩具级任务,而是能在CIFAR-10上稳定达到92%准确率、结…

2026/10/12 13:00:08 阅读更多 →
LCA最近公共祖先算法详解:倍增法、Tarjan与树链剖分

LCA最近公共祖先算法详解:倍增法、Tarjan与树链剖分

1. 从一道面试题说起:为什么LCA值得花时间搞透第一次接触LCA是在刷题的时候,题目很直白:给一棵树,多次询问两个节点的最近公共祖先。当时我的第一反应是暴力——两个节点分别往上走,标记走过的路径,第一个相…

2026/10/12 12:59:08 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →