档案室里最不缺的就是"重复劳动"。一批案卷刚接收进来,著录数据躺在表格里,接下来要干的事往往是:打开模板,一卷一卷地复制粘贴题名,手工敲件号,调行高,数页码,打印,发现页码算错,改完重打,装订完再翻一遍发现某卷漏了一件。我见过一个二十来卷的小批次,两个人对着电脑耗了整整两天,最后交上去的目录里还有三处页号衔接不上。真正让人崩溃的不是活多,而是这类错误几乎全部由"人手抄写"这个动作本身带来——抄一遍就多一次出错机会,抄两百遍就是两百次机会。
这篇内容讲的就是把这件事交给机器:用一张规范化的台账做数据底座,通过脚本或模板批量导出卷内目录和案卷目录,一次生成整批PDF和Excel,版式统一、页码自动累加、件数与总页数自动汇总。适合正在做档案整理、迎检、移交的同行,也适合手上有几百上千卷历史数据、想一次性把目录补齐的人。你不需要是程序员,但最好能接受"先在表格里把数据管住"这个前提——后面所有的自动化,都是建立在这一步之上的。
1. 手工目录真正的成本在于返工,而不是打字
很多人评估工作量的时候,只算了"一卷目录有多少行、敲一行要几秒",得出"一下午能搞完"的结论。实际做下来时间翻三倍,多出来的部分全在返工上。要把这件事自动化,第一步不是写代码,而是搞清楚一份目录从数据到成品中间到底经过了几道手,哪几道最容易出错。
1.1 卷内目录与案卷目录的层级关系,决定了它们不能共用一张表
这两个东西经常被混为一谈,但它们的统计口径完全不同。卷内目录以"件"为单位,一行对应卷内一份文件,核心字段是件号、责任者、文件题名、日期、页号、备注;案卷目录以"卷"为单位,一行对应一整卷,核心字段是案卷号、档号、案卷题名、起止日期、保管期限、件数、页数、备注。
关键在于:案卷目录里的"件数"和"页数",理论上应该由卷内明细自动汇总得出,而不是人工再数一遍。手工流程里最常见的错误就是这两处对不上——卷内明细有 17 件,案卷目录写了 16 件;卷内页号排到 132,案卷目录填了 130。只要这两张表是分开手工填的,就必然会出现偏差。所以我一直坚持一个原则:卷内明细是唯一数据源,案卷目录是它的聚合视图。想清楚这一点,后面的自动化路线就顺了。
一份完整的卷内目录,通常会包含三层信息,缺一层就不算合格:
| 层次 | 内容 | 常见缺失 |
|---|---|---|
| 表头区 | 单位名称、目录名称、档号、案卷题名、保管期限、起止日期 | 只写了档号,没写案卷题名 |
| 表体区 | 件号、责任者、文件题名、日期、页号、备注 | 件号不补零、日期格式混乱 |
| 表尾区 | 本卷共X件、共X页、立卷人、检查人、立卷日期 | 件数页数靠手填,与表体不符 |
1.2 三个高频返工点,几乎每个批次都会踩中
第一处是件号与页号的连续性。件号必须从 1 开始连号,中间不能断;页号则是每件占用一个区间,后一件的起始页号必须等于前一件的起始页号加页数。手工做的时候,改了一件的内容,后面所有件的页号都要跟着挪,改一次错一次。
第二处是版式统一。一批案卷往往由不同的人分头做,行高不一样、字体不一样、表头位置差两毫米。打印装订之后放在一起,一眼就能看出来是拼凑的。而要统一版式,如果靠人一个个调,那基本等于重新做一遍。
第三处是"改一处、全批重来"。单位名称变了、模板要加一列备注、保管期限的写法要调整——这类需求在整理过程中极其常见。手工模式下,这意味着前面做好的全部作废重打;而数据驱动模式下,只需要改模板或改一个配置项,重新跑一遍,几分钟出全批。
提醒一句:返工成本是判断"要不要上自动化"的唯一标准。如果这批档案只有五六卷、以后也不会再有,手工做完全合理;但只要有几十卷以上、或者后续还有第二批第三批,投入半天搭一套流程,回报是成倍的。
1.3 先把"交付标准"定下来,再谈工具
我在动手之前会先拿出一份"成品样张",把所有硬约束写死:A4 纸、纵向还是横向、左边留多少装订线、表头字体字号、表体字体字号、页码位置、跨页时表头要不要重复。这份样张一旦定下来,它就是唯一的验收标准,后面所有自动化的目标就是"批量复制这张样张"。
这一步看起来是在浪费时间,实际上是在减少后面所有的争论。很多批次做到一半返工,根本原因不是技术问题,而是做到一半才发现"上级要的格式跟我们现在做的不一样"。样张定死之后,工具选型、脚本结构、模板设计全部围绕它来,反而简单了。
2. 台账设计对了,导出只是顺手的事
自动化的效果上限,由数据底座的干净程度决定。我见过太多人急着写脚本,结果卡在"日期列里既有 2023-05-01 又有 2023年5月1日还有 20230501"这种事情上,光清洗数据就耗掉了整个下午。所以我的建议是:花七成时间在设计表结构和清洗数据上,三成时间在导出上。
2.1 档号的编码规则要一次定死
档号是整个体系的骨架,它决定了后面能不能按卷自动分组。常见结构是"全宗号-目录号-案卷号",用短横线连接,各段位数固定。位数固定的意义在于排序——如果案卷号不补零,排序结果会变成 1、10、100、11、12,导出顺序全乱;补成三位之后,1 写作 001,排序就正常了。
同样的道理适用于件号。件号建议统一补零到三位,甚至在写进目录之前就存成三位字符串。这里有个小坑:如果让 Excel 把它当数字存,前面的零会被吃掉,导出来又变成 1、2、3。解决方式是整列设成文本格式再录入,或者在脚本里读入时强制按字符串处理(dtype=str这个参数就是干这个用的,别省)。
| 字段 | 建议类型 | 规则说明 |
|---|---|---|
| 档号 | 文本 | 全宗号-目录号-案卷号,各段定长 |
| 件号 | 文本(三位补零) | 001、002……同卷内连续 |
| 责任者 | 文本 | 多责任者用分号分隔,不用顿号 |
| 文件题名 | 文本 | 去掉首尾空格和换行符 |
| 日期 | 文本(YYYY-MM-DD) | 统一八位或十位格式 |
| 页数 | 整数 | 只填数字,不带"页"字 |
| 页号 | 文本 | 存"起止页"或"起始页+页数",二选一,不能混 |
2.2 主表加明细表,用档号做关联
最稳的结构是两张表:一张"案卷主表",一卷一行,存档号、案卷题名、保管期限、起止日期、立卷人这些卷级信息;一张"卷内明细表",一件一行,存档号、件号、责任者、文件题名、日期、页数、页号、备注。两张表通过档号关联。
这样设计的好处是:卷级信息只维护一份,不会出现同一卷在两个地方题名不一致;而卷内明细可以无限追加,件数和总页数由程序聚合,永远和明细对得上。导出案卷目录时,程序按档号分组,对明细表做count和sum,直接把结果填进案卷目录,人完全不用碰数字。
2.3 清洗脏数据的三类必做动作
第一类是空白与不可见字符。从别处复制过来的题名里经常混着全角空格、不间断空格、换行符,这些字符在 Excel 里看不出来,导出到 PDF 就会导致莫名其妙的换行或错位。处理方式很粗暴:读入之后对整个文本列做一次去首尾空白、替换所有空白类字符。
第二类是数字被识别成日期。像 "1-2" 这种页号写法,表格软件可能自作主张转成 "1月2日"。这属于录入环节的问题,最好的办法是录入前把整列设成文本,已经出问题的只能逐条核对,没有捷径。
第三类是合并单元格。这是最麻烦的一种,因为合并之后只有左上角那个格子有值,其余是空的。程序读进来就会看到一堆空值。我的做法是:台账里永远不合并单元格,需要视觉上分组的地方靠重复填写或者排序来解决。展示用的合并,放到导出环节去做。
3. 三条导出路线,先看批量再选工具
同样是"一键导出",实际有三条完全不同的路子,适用场景差别很大。选错了不是做不出来,而是做到一半发现维护不动。
3.1 路线一:Excel 模板加工 + 办公软件的批量合并
这条路子最容易被非技术岗接受:先在表格软件里做好一个漂亮的模板页,把数据源另存为一张清单,然后用文字处理软件的"邮件合并"功能,按档号逐条生成。它不需要写任何代码,全程鼠标操作。
优点是门槛极低、版式所见即所得、改模板不用改程序。缺点也很明显:批量大了会卡,几百卷的时候每生成一卷都要等;更重要的是它只能做"一条记录生成一页",处理不了"一卷有多件、每卷件数不同"这种一对多的分页结构。所以它适合的场景是:卷数不多,且每卷件数固定、结构简单。
3.2 路线二:脚本读取台账,批量生成 PDF 与 Excel
这是我目前主推的方案。核心逻辑是:脚本读入明细表,按档号分组,对每一组生成一份文档,最后统一转成 PDF。件数多少、页号怎么排、总页数怎么算,全部由代码控制,天然不会错。
它的成本在于需要一点环境准备和代码基础,但代码量其实不大——核心逻辑一百多行就能跑通,后面我会把关键部分拆开讲。收益是可维护性:以后换模板、加字段、改命名规则,改几行就行,不用重做。
3.3 路线三:现成系统自带的导出,再做二次加工
如果单位已经在用档案管理软件,通常自带目录打印功能。这条路省事,但可定制性差:版式往往是固定的,想加一列"备注"或者调整页边距,基本没戏。常见的折中做法是——从系统里把数据导成 Excel,再用路线一或路线二加工。这也是我推荐的做法:数据从系统出,版式自己控。
| 维度 | 模板合并 | 脚本生成 | 系统直出 |
|---|---|---|---|
| 上手门槛 | 低 | 中 | 最低 |
| 几十卷耗时 | 半小时以上 | 一两分钟 | 视系统而定 |
| 版式可控性 | 高 | 很高 | 低 |
| 一对多分页支持 | 差 | 好 | 一般 |
| 后期维护 | 改模板即可 | 改代码 | 无法改 |
一个现实建议:不要一上来就追求"全自动"。先用手工方式做出一卷完美的样张,再用脚本去复制它。很多人反着来,先写脚本再调版式,最后调版式的时间比写脚本还长。
4. 脚本走位:从一张台账到一整批分卷 PDF
下面这套流程,我在几个批次上跑过,逻辑稳定。核心就三步:读数据、按档号切卷、逐卷生成并转 PDF。中间所有的坑,基本都在"切卷"和"版式"这两处。
4.1 环境与依赖
需要的东西不多:Python 环境,加三个库——pandas负责读表和分组,openpyxl负责写 Excel 并设置打印版式,python-docx负责往 Word 模板里填内容。最后转 PDF 这一步,调用本地办公套件的命令行转换功能即可,不需要额外装什么重型组件。
pip install pandas openpyxl python-docx转换命令大致是这样,Windows 和 Linux 下路径不同,但参数一致:
soffice --headless --convert-to pdf --outdir ./输出目录 ./中间文件/某某档号.docx4.2 按档号切卷:一行 groupby 解决分组
读表的时候有两个参数必须带上:dtype=str防止件号和日期被自动转换,keep_default_na=False防止空值变成NaN混进文本里。
import pandas as pd df = pd.read_excel("卷内台账.xlsx", sheet_name="明细", dtype=str, keep_default_na=False) df["件号"] = df["件号"].str.zfill(3) # 件号统一三位 df["件号序"] = df["件号"].astype(int) # 用于按数值排序 df = df.sort_values(["档号", "件号序"]) # 先按卷、再按件号排 for 档号, 卷 in df.groupby("档号", sort=True): 生成单卷目录(档号, 卷)这段代码里最容易忽略的是排序。如果直接按字符串排件号,010会排在009后面没问题,但如果有的批次件号没补零,10就会排到2前面。所以补零和数值排序这两步都不能省。
4.3 卷内目录的版式,用 openpyxl 精确控制
版式这件事,靠手工调是调不完的,必须写死在代码里。下面这段是核心设置,把纸张、页边距、打印区域、跨页重复表头一次性定好:
from openpyxl import Workbook from openpyxl.worksheet.page import PageMargins wb = Workbook() ws = wb.active ws.page_setup.paperSize = ws.PAPERSIZE_A4 ws.page_setup.orientation = "portrait" ws.page_margins = PageMargins(left=2.5, right=1.5, top=2.0, bottom=2.0) # 左2.5cm留装订线 ws.print_title_rows = "5:5" # 第5行是表体表头,跨页自动重复 ws.print_area = f"A1:F{最后一行}" # 只打印有内容的区域左边留 2.5 厘米不是随便定的:三孔一线装订,孔位距离左边缘大概 1.2 到 1.5 厘米,装订线和孔之间还要留出翻页空间。如果左边只留 1.5 厘米,装订完之后靠近书脊的那几列字会被压住,翻都翻不开。这是装订完之后才会暴露的问题,返工代价极高。
4.4 Word 模板加占位符,比纯代码排版更省心
完全用代码画表格也能做,但维护起来痛苦——改个字体要动代码。更实际的做法是:让熟悉档案格式的人在 Word 里做好一份模板,表头表尾写死,正文表格留一行占位,用{{件号}}、{{文件题名}}这类标记占位,脚本只负责填。
from docx import Document def 生成单卷目录(档号, 卷): doc = Document("卷内目录模板.docx") # 先把表头区的占位符替换掉 替换段落占位符(doc, { "{{档号}}": 档号, "{{案卷题名}}": 卷.iloc[0]["案卷题名"], "{{件数}}": str(len(卷)), "{{总页数}}": str(卷["页数"].astype(int).sum()), }) # 再按明细逐行填表体 for _, 行 in 卷.iterrows(): 新增表格行(doc.tables[0], 行) doc.save(f"./中间文件/{安全文件名(档号)}.docx")这里有两点要注意。一是表头区的件数和总页数必须由程序算,不要从台账里读,台账里那个数字往往是人工填的、早就过时了。二是文件名要做安全处理,档号里如果有斜杠、冒号、星号这些字符,保存会直接报错,题名里带书名号、引号的也一样,统一替换成下划线最稳妥。
4.5 批量跑批时的日志与失败隔离
一次跑几百卷,中途挂掉是常事——某个模型文件被占用、某条数据缺字段、某个题名超长导致排版异常。如果脚本一遇错就整个退出,前面的工作全白费。所以主循环里必须加异常捕获:
失败清单 = [] for 档号, 卷 in df.groupby("档号", sort=True): try: 生成单卷目录(档号, 卷) except Exception as e: 失败清单.append((档号, str(e))) with open("导出日志.txt", "w", encoding="utf-8") as f: for 档号, 原因 in 失败清单: f.write(f"{档号}\t{原因}\n")跑完之后先看日志,通常会发现失败的那几卷有共性问题——比如都缺"案卷题名",或者档号里都有特殊字符。修完重跑,只跑失败的就行。这个习惯能省下大量重复等待的时间。
5. 版式、打印与装订:决定成品能不能交出去
脚本能生成文件,不代表生成的东西能交。档案目录这种材料,最终要落到纸面上、装进档案盒、被检查人员一页页翻。有几个细节,代码层面不写、事后一定会后悔。
5.1 纵向还是横向,看题名长度
A4 纵向是默认选择,一行能放下大约三十个汉字,超过就得换行。如果这批档案的文件题名普遍很长(比如会议文件、技术报告),纵向排版就会导致大量折行,一卷目录印出来七八页,既难看又费纸。这种情况我一般改成横向:一行能放下四十多个字,行数压下来,页数也跟着压下来。
判断方法很简单:拿台账里最长的那条题名量一下字数,超过 25 个字的比例如果占到三成以上,就考虑横向。改横向只需要在代码里把orientation改成landscape,同时把页边距的左右值对调一下。
5.2 页码、表头重复与"共几页"
跨页时的表头重复,前面已经用print_title_rows解决了,这一条几乎是成品合格与否的分水岭。没有重复表头的目录,翻到第二页就不知道哪一列是"页号"哪一列是"备注",检查的时候非常难受。
页码的处理有两种思路:一种是在页脚放"第 X 页",这个 openpyxl 不直接支持,需要在 Word 模板里设置;另一种更省事——整批用同一个页脚,靠打印顺序自然排。我倾向后者,因为每卷单独编号反而容易乱。
表尾的"本卷共 X 件、共 Y 页"必须由程序算,而且要放在表格之外、单独一行,避免被print_area截断。这里有个小细节:print_area一定要设到最后一行,如果设小了,最后那行"本卷共X件"会被裁掉,打印出来就剩半页。
5.3 打印装订的操作顺序
批量打印最忌讳"卷卷一页页点打印"。正确做法是把输出目录里的 PDF 按档号顺序排好,合并成一个文件,一次性提交打印任务。合并的好处是:打印顺序天然正确,中间不会漏卷,也不会出现两次打印同一卷的情况。
纸张建议用 80 克以上的,70 克的纸双面打印容易透墨,翻页时字迹糊在一起。双面还是单面,看装订方式:三孔一线装订的,单面更稳妥,因为双面的话反面内容会紧贴装订线一侧;如果用夹条或者塑料封套,双面完全可以。
装订前一定要干一件事:按顺序把整批目录快速翻一遍,重点看每卷的最后一行是不是"本卷共X件"。这个动作两分钟,能挡掉绝大多数漏页问题。
6. 踩过的坑与交付前的自检清单
写了这么多,真正让我加班的从来不是代码写不出来,而是导出的文件"看起来对、其实不对"。下面这几条都是实际踩出来的。
6.1 合并单元格带来的错位
早期我在模板里把表头做了合并——单位名称横跨六列居中。结果导出时内容确实居中了,但程序读回模板、往表格里填数据时,列索引全部偏移,件号填到了责任者那一列。结论是:模板的表头区可以合并,但表体区的表头绝对不能合并,一旦合并,列就再也对不上了。如果非要视觉分组,用加粗边框来区分,比合并安全得多。
6.2 日期和页号在导出后变形
台账里写2023-5-1,导出后变成2023/5/1;写1-2,导出成1月2日。这类问题全部源于"表格软件自作主张"。根治办法是录入时把列设为文本格式;已经污染的,在脚本里统一做一次格式化,把日期强制转成YYYY-MM-DD的字符串再写进文档,不让任何工具再做二次解释。
页号还有一种情况:有的卷用"起止页"(如 1-8),有的用"起始页+页数"(如 1,8)。两种混在一批里,总页数就永远算不对。一批档案里只能选一种口径,写进规范,别兼容。
6.3 排查链路:总页数对不上时怎么定位
这是最典型的问题,我把当时的排查顺序记下来,可以照着走:
- 先核对单卷:打开某一卷的明细,手工加一遍页数,跟表尾的"共Y页"比。如果单卷就错,问题在计算逻辑;如果单卷对、整批错,问题在循环。
- 检查空值:页数列为空的行会被
sum()跳过,但不会被count()跳过,导致件数对、页数少。这是最常见的原因,用df["页数"].isna().sum()一秒定位。 - 检查重复行:从系统导出数据时偶尔会有重复记录,件数虚高。按"档号+件号"去重再看。
- 检查分组键:档号里如果有多余的空格(比如
A-01-001后面带个空格),会被分成两组,两组各算各的,总数自然不对。分组前先对所有档号做一次strip()。 - 最后才怀疑代码:跑一遍打印,把每组的件数页数输出到日志,跟目录对照,差异立刻现形。
这套顺序的价值在于:永远先怀疑数据,再怀疑代码。我经手的问题里,八成以上是数据本身的问题。
6.4 交付前的四项硬校验
导出完成后不要急着打印,先跑四个检查,全部通过再进入打印环节:
- 件号连续:每卷的件号从 001 开始,逐个加一,不能跳号。用脚本遍历一遍,输出断号位置。
- 页号衔接:后一件的起始页号 = 前一件的起始页号 + 前一件页数。这个用累加校验,一次就能发现哪里断了。
- 件数页数一致:表尾的"共X件共Y页"必须等于明细聚合结果,这条是自动生成的,理论上不会错,但要确认没有被模板里的旧占位符覆盖。
- 抽检对照:随机抽五卷,把导出的目录和实体档案逐件对照。抽检不是形式主义,它能发现"数据本身就录错了"这类自动化救不了的问题——比如原件题名和台账里写的根本不是一个文件。
抽检这一步,我通常会专门挑题名最长、件数最多、日期跨年度的卷,这些是最容易出问题的地方。抽完没问题,整批基本就稳了。
我个人在这件事上的体会是:自动化的价值不在于"省下打字的时间",而在于把校验从"靠人眼"变成"靠规则"。人眼盯两百行会疲劳,规则盯两万行不会。所以真正值得投入精力的地方,是把"什么样的目录算合格"这件事写清楚——写清楚了,代码只是把它翻译一遍而已。至于模板怎么排更好看、打印怎么装订更省事,这些细节每个单位都有自己的习惯,多试两批就摸出来了。