中国登记结算公司股份查询-PDF批量重命名工具-开发记录

GitHub项目地址:

点击进入

一、项目背景

在处理中登系统导出的证券持有变更信息PDF时,需要:

  • 批量读取 PDF
  • 提取:
    • 证券子账户号码
    • 持有人名称
  • 自动生成规范命名文件
  • 保留原文件
  • 输出至独立子文件夹
  • 支持拖拽批量处理

为提升效率,开发本地离线可执行工具。


二、功能需求说明

1.输入方式

支持:

  • 拖拽单个 PDF
  • 拖拽多个 PDF
  • 拖拽文件夹
  • 拖拽多个文件夹
  • 混合拖拽
  • 双击运行后弹窗选择文件夹

2.命名规则

新文件名 = 证券子账户号码-持有人名称.pdf

例如:

A290074561-李大强.pdf

3.输出规则

  • 不修改原文件
  • 在原目录生成子文件夹:
已重命名输出
  • 所有新文件复制到该文件夹
  • 自动防止重名:
A290074561-李大强(2).pdf

4.日志生成

自动生成:

重命名日志.csv

内容:

| 源文件名 | 输出文件名 |


三、技术实现方案

技术栈

  • Python 3.13
  • pdfplumber(PDF文本解析)
  • shutil(文件复制)
  • PyInstaller(打包EXE)

四、核心代码实现

字段提取逻辑

m1 = re.search(r"证券子账户号码\s*:\s*([A-Za-z0-9]+)", text)
m2 = re.search(r"持有人名称\s*:\s*(\S+)", text)

新文件名生成

new_filename = f"{sub_account}-{holder}.pdf"

防重名机制

A290074561-李大强.pdf
A290074561-李大强(2).pdf
A290074561-李大强(3).pdf

复制而非重命名

shutil.copy2(src_path, dst_path)

保证:

  • 原文件保留
  • 时间戳保留

五、完整最终脚本

import os
import sys
import re
import time
import csv
import shutil
import pdfplumber

OUTPUT_DIR_NAME = "已重命名输出"

def pick_folder_dialog():
    try:
        import tkinter as tk
        from tkinter import filedialog
        root = tk.Tk()
        root.withdraw()
        root.attributes("-topmost", True)
        folder = filedialog.askdirectory(title="请选择要处理的PDF文件夹")
        root.destroy()
        return folder
    except Exception:
        return ""

def extract_fields(pdf_path: str):
    with pdfplumber.open(pdf_path) as pdf:
        text = (pdf.pages[0].extract_text() or "")

    # 提取证券子账户号码
    m1 = re.search(r"证券子账户号码\s*:\s*([A-Za-z0-9]+)", text)
    sub_account = m1.group(1) if m1 else ""

    # 提取持有人名称
    m2 = re.search(r"持有人名称\s*:\s*(\S+)", text)
    holder_name = m2.group(1) if m2 else ""

    return sub_account, holder_name

def safe_target_path(folder: str, filename: str) -> str:
    target = os.path.join(folder, filename)
    if not os.path.exists(target):
        return target

    base, ext = os.path.splitext(filename)
    k = 2
    while True:
        new_name = f"{base}({k}){ext}"
        new_path = os.path.join(folder, new_name)
        if not os.path.exists(new_path):
            return new_path
        k += 1

def collect_pdfs_from_folder(folder: str):
    return [
        os.path.join(folder, n)
        for n in os.listdir(folder)
        if n.lower().endswith(".pdf")
    ]

def process_pdf_files(pdf_files):
    by_folder = {}
    for p in pdf_files:
        by_folder.setdefault(os.path.dirname(p), []).append(p)

    for src_folder, files in by_folder.items():
        out_folder = os.path.join(src_folder, OUTPUT_DIR_NAME)
        os.makedirs(out_folder, exist_ok=True)

        print(f"\n来源目录: {src_folder}")
        print(f"输出目录: {out_folder}")
        print("-" * 60)

        records = []
        ok = 0

        for src_path in files:
            src_name = os.path.basename(src_path)

            try:
                sub_account, holder = extract_fields(src_path)

                if not sub_account:
                    print(f"未找到证券子账户号码: {src_name}")
                    continue
                if not holder:
                    print(f"未找到持有人名称: {src_name}")
                    continue

                new_filename = f"{sub_account}-{holder}.pdf"
                dst_path = safe_target_path(out_folder, new_filename)

                shutil.copy2(src_path, dst_path)

                records.append([src_name, os.path.basename(dst_path)])
                print(f"已生成: {os.path.basename(dst_path)}")
                ok += 1

            except Exception as e:
                print(f"处理失败: {src_name} | {e}")

        log_path = os.path.join(out_folder, "重命名日志.csv")
        try:
            with open(log_path, "w", newline="", encoding="utf-8-sig") as f:
                w = csv.writer(f)
                w.writerow(["源文件名", "输出文件名"])
                w.writerows(records)
        except Exception as e:
            print(f"写日志失败: {e}")

        print("\n完成:成功输出", ok, "个文件")
        print("日志位置:", log_path)

def main():
    args = sys.argv[1:]

    if not args:
        folder = pick_folder_dialog()
        if folder:
            process_pdf_files(collect_pdfs_from_folder(folder))
        else:
            print("未选择文件夹。")
        input("\n按回车退出...")
        return

    all_pdfs = []
    for raw in args:
        p = raw.strip('"')
        if os.path.isdir(p):
            all_pdfs.extend(collect_pdfs_from_folder(p))
        elif os.path.isfile(p) and p.lower().endswith(".pdf"):
            all_pdfs.append(os.path.abspath(p))
        else:
            print(f"跳过: {p}")

    if not all_pdfs:
        print("未找到可处理的PDF文件。")
        input("\n按回车退出...")
        return

    process_pdf_files(all_pdfs)
    input("\n全部处理完成,按回车退出...")

if __name__ == "__main__":
    main()

六、打包EXE流程

1.创建干净虚拟环境

python -m venv packenv
.\packenv\Scripts\activate
pip install -U pip
pip install pyinstaller pdfplumber

2.打包命令

pyinstaller --onefile --console --clean --noconfirm rename_pdf_drag.py -n PDF重命名工具

3.输出结果

生成:

dist\PDF重命名工具.exe

七、常见问题及解决方案

1.拖拽无反应

原因:

  • 使用管理员运行
  • 拖拽方式错误

解决:

  • 不要以管理员身份运行
  • 直接拖文件/文件夹到exe图标

2.打包卡在 PKG 阶段

原因:

  • 全局环境包含 torch / pandas / scipy 等大型库

解决:

  • 使用干净 venv 打包

3. dist 文件夹为空

可能:

  • 杀毒软件拦截
  • 打包未完成

八、最终目录结构示例

某目录
├── 原PDF文件
├── 已重命名输出
│   ├── A290074561-李大强.pdf
│   ├── 重命名日志.csv

九、安全与合规说明

  • 本工具完全本地运行
  • 不联网
  • 不上传数据
  • 适用于证券登记类敏感文件的内网处理