乐于分享
好东西不私藏

【效率黑科技】新版PDF数电发票多行跨行被逼疯?我用Python自研提取算法,一招彻底根治!

【效率黑科技】新版PDF数电发票多行跨行被逼疯?我用Python自研提取算法,一招彻底根治!

“带你横跨办公自动化的数据江海”

@摸鱼

闻道有先后,术业有专攻。

各位大佬好!

~我依旧是你们的老朋友摸鱼~

♥ 1

在职场摸爬滚打的这十多年里,我用Python悄悄干了不少“正事”——不知不觉攒下了一整套办公自动化的实用项目技巧。去年10月初创立了公众号 「码海听潮」 ,初衷很简单:把重复的劳动交给代码,把摸鱼的时间留给生活。

目前已经吭哧吭哧更新了近100篇原创文章,每一篇都是实操干货,不讲虚的,只聊怎么用代码真正解放双手,帮大家早点下班、准点摸鱼

♥ 2

♥ 3好叻,多了不说,少了不唠,咱直接上干货。

办公需求场景

从崩溃到优雅的进化

有一个存放神秘PDF数电发票的文件夹,里面包含多张新版PDF格式的数电发票,涵盖增值税专用发票、增值税普通发票以及普通发票。部分发票涉及多行明细且存在跨行情况。现需提取所有这些PDF发票中的明细数据,并将其保存到Excel表格中,以便统一记录和整理。要是这种类似的需求你的Big Boss安排你去完成,请问阁下该如何应对?

  • 需求的PDF数电发票文件夹列表

  • 多行明细且存在跨行的PDF数电发票

办公痛点分析

01

 痛点1:效率极其低下,耗时惊人

    • 每张发票都需要手动打开PDF,逐行对照录入Excel。当文件夹中包含几十甚至上百张发票,且每张都有多行明细时,工作量巨大,且无法并行处理。

    02

     痛点2:容易出错且难以校对

      • 录入错误:人工抄写或打字时,容易输错金额、税号、发票号码(数字长且易混淆,例如 0 与 O)。

      • 定位错误:跨行明细(如一行货物、一行服务费)在PDF中可能跨页或格式不规整,人工容易漏掉某一行或重复录入。

      • 抬头与明细关联错误:可能将A发票的明细行错贴到B发票的记录里。

      03

      痛点3:格式处理繁琐

      • PDF中的多行明细往往没有表格线,仅依靠文字分行排列。人工需要肉眼判断哪些文字属于同一行(例如:商品名称、数量、单价、金额分别对应哪几个字段),跨行时更容易看岔行。

      • 如果一张发票有10行明细,人工可能需要切换10次视线位置来抄写,疲劳度极高。

      由此可见若的话若操作成千上百个PDF数电发票,整个操作流程繁琐且耗时,高频次的鼠标点击和键盘输入使操作者手指疲劳,堪称”键盘敲冒烟”式的体力劳动,加上人工疲劳操作极易导致遗漏文件夹。于是乎这时候,按以往的 “解题套路”,Python 的专属 BGM 该响起来了 ——go~ go~ go~,救苦救难的大救星这不就来了!!

      @摸鱼

      问题拆解思路

      1. 遍历发票文件夹

      遍历指定文件夹中的数电发票 PDF 文件,生成待处理文件路径列表。

      2. 逐份解析发票内容

      循环读取每一份 PDF 发票,基于文本坐标信息构建发票识别算法,按字段规则自动提取票面信息及明细行数据。

      3. 数据写入 Excel

      将提取结果结构化写入Excel表格,形成可归档、可对账的发票数据清单。

      下面,我就用python代码让各位大佬见识一下,什么叫”传统文化遇上赛博效率”(仅展示部分代码,非完整代码,需完整代码看文章末尾说明~)

      import sysimport osfrom pathlib import Pathfrom PyQt6.QtWidgets import (    QApplicationQMainWindowQWidgetQVBoxLayoutQHBoxLayout,    QPushButtonQLabelQLineEditQTextEditQProgressBar,    QFileDialogQMessageBoxQGroupBoxQFrame)from PyQt6.QtCore import Qt, QThread, pyqtSignalfrom PyQt6.QtGui import QFont, QIconclass ProcessThread(QThread):    """处理线程,避免界面卡顿"""    progress = pyqtSignal(int, int, str)  # 当前进度,总数,状态信息    finished = pyqtSignal(bool, str, int, int)  # 成功标志,输出文件路径,成功数,失败数    log = pyqtSignal(str)  # 日志信息    def __init__(self, folder_path):        super().__init__()        self.folder_path = folder_path    def run(self):        try:            # 导入处理函数            from invoice_processor import batch_process_with_subfolders            self.log.emit("开始处理发票文件...")            # 调用处理函数            result = batch_process_with_subfolders(self.folder_path)            if result is not None:                output_file = Path(self.folder_path) / "发票明细汇总.xlsx"                self.finished.emit(True, str(output_file), 00)            else:                self.finished.emit(False""00)        except Exception as e:            self.finished.emit(False""00)            self.log.emit(f"处理出错: {str(e)}")class InvoiceProcessorGUI(QMainWindow):    """发票处理工具主界面"""    def __init__(self):        super().__init__()        self.process_thread = None        self.init_ui()    def init_ui(self):        """初始化界面"""        self.setWindowTitle("新版数电PDF发票批量处理工具(支持8列多行发票明细)-欢迎关注微信公众号:码海听潮")        self.setMinimumSize(700600)        # 设置窗口样式        self.setStyleSheet("""            QMainWindow {                background-color: #f5f5f5;            }            QGroupBox {                font-weight: bold;                border: 1px solid #cccccc;                border-radius: 5px;                margin-top: 10px;                padding-top: 10px;            }            QGroupBox::title {                subcontrol-origin: margin;                left: 10px;                padding: 0 5px 0 5px;            }            QPushButton {                background-color: #4CAF50;                color: white;                border: none;                padding: 8px 15px;                border-radius: 4px;                font-size: 12px;            }            QPushButton:hover {                background-color: #45a049;            }            QPushButton:pressed {                background-color: #3d8b40;            }            QPushButton#btn_browse {                background-color: #2196F3;            }            QPushButton#btn_browse:hover {                background-color: #0b7dda;            }            QPushButton#btn_process {                background-color: #ff9800;                font-size: 14px;                font-weight: bold;                padding: 10px 20px;            }            QPushButton#btn_process:hover {                background-color: #e68900;            }            QPushButton:disabled {                background-color: #cccccc;            }            QLineEdit {                border: 1px solid #cccccc;                border-radius: 4px;                padding: 5px;                font-size: 12px;            }            QTextEdit {                border: 1px solid #cccccc;                border-radius: 4px;                font-family: 'Consolas', monospace;                font-size: 11px;            }            QLabel {                font-size: 12px;            }            QProgressBar {                border: 1px solid #cccccc;                border-radius: 4px;                text-align: center;            }            QProgressBar::chunk {                background-color: #4CAF50;                border-radius: 3px;            }        """)        # 创建中央部件        central_widget = QWidget()        self.setCentralWidget(central_widget)        # 主布局        main_layout = QVBoxLayout(central_widget)        main_layout.setSpacing(15)        main_layout.setContentsMargins(20202020)        # 标题        title_label = QLabel("PDF发票批量解析工具")        title_font = QFont("微软雅黑"16QFont.Weight.Bold)        title_label.setFont(title_font)        title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)        title_label.setStyleSheet("color: #333333; margin-bottom: 10px;")        main_layout.addWidget(title_label)        # 文件夹选择区域        folder_group = QGroupBox("文件夹设置")        folder_layout = QHBoxLayout()        folder_layout.setSpacing(10)        self.folder_path_edit = QLineEdit()        self.folder_path_edit.setPlaceholderText("请选择包含PDF发票的文件夹...")        folder_layout.addWidget(self.folder_path_edit)        self.browse_btn = QPushButton("浏览文件夹")        self.browse_btn.setObjectName("btn_browse")        self.browse_btn.clicked.connect(self.browse_folder)        folder_layout.addWidget(self.browse_btn)        folder_group.setLayout(folder_layout)        main_layout.addWidget(folder_group)        # 处理选项区域        options_group = QGroupBox("处理选项")        options_layout = QHBoxLayout()        self.recursive_check = QLabel("✓ 已启用子文件夹递归扫描")        self.recursive_check.setStyleSheet("color: #666666; padding: 5px;")        options_layout.addWidget(self.recursive_check)        options_layout.addStretch()        options_group.setLayout(options_layout)        main_layout.addWidget(options_group)        # 日志输出区域        log_group = QGroupBox("处理日志")        log_layout = QVBoxLayout()        self.log_text = QTextEdit()        self.log_text.setReadOnly(True)        self.log_text.setMaximumHeight(300)        log_layout.addWidget(self.log_text)        log_group.setLayout(log_layout)        main_layout.addWidget(log_group)        # 进度条        self.progress_bar = QProgressBar()        self.progress_bar.setVisible(False)        main_layout.addWidget(self.progress_bar)        # 按钮区域        button_layout = QHBoxLayout()        button_layout.addStretch()        self.process_btn = QPushButton("开始处理发票")        self.process_btn.setObjectName("btn_process")        self.process_btn.clicked.connect(self.start_processing)        self.process_btn.setMinimumWidth(150)        button_layout.addWidget(self.process_btn)        self.clear_log_btn = QPushButton("清空日志")        self.clear_log_btn.clicked.connect(self.clear_log)        button_layout.addWidget(self.clear_log_btn)        button_layout.addStretch()        main_layout.addLayout(button_layout)        # 状态栏        self.statusBar().showMessage("就绪")        # 初始化日志        self.add_log("欢迎使用PDF发票批量解析工具")        self.add_log("请选择包含PDF发票的文件夹后点击「开始处理发票」")    def browse_folder(self):        """浏览文件夹"""        folder_path = QFileDialog.getExistingDirectory(            self            "选择文件夹"            ""            QFileDialog.Option.ShowDirsOnly        )        if folder_path:            self.folder_path_edit.setText(folder_path)            self.add_log(f"已选择文件夹: {folder_path}")    def add_log(self, message):        """添加日志信息"""        self.log_text.append(f"[{self.get_current_time()}] {message}")        # 自动滚动到底部        scrollbar = self.log_text.verticalScrollBar()        scrollbar.setValue(scrollbar.maximum())    def get_current_time(self):        """获取当前时间字符串"""        from datetime import datetime        return datetime.now().strftime("%H:%M:%S")    def clear_log(self):        """清空日志"""        self.log_text.clear()        self.add_log("日志已清空")    def start_processing(self):        """开始处理"""        folder_path = self.folder_path_edit.text().strip()        if not folder_path:            QMessageBox.warning(self"警告""请先选择文件夹!")            return        if not os.path.exists(folder_path):            QMessageBox.warning(self"警告""选择的文件夹不存在!")            return        # 检查是否有PDF文件        pdf_files = list(Path(folder_path).rglob("*.pdf")) + list(Path(folder_path).rglob("*.PDF"))        if not pdf_files:            QMessageBox.warning(self"警告""所选文件夹中没有找到PDF文件!")            return        # 确认开始处理        reply = QMessageBox.question(            self            "确认"            f"将在文件夹「{folder_path}」中查找PDF文件(包含子文件夹)\n"            f"共找到 {len(pdf_files)} 个PDF文件,是否开始处理?",            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No        )        if reply != QMessageBox.StandardButton.Yes:            return        # 禁用按钮,启动处理线程        self.process_btn.setEnabled(False)        self.browse_btn.setEnabled(False)        self.progress_bar.setVisible(True)        self.progress_bar.setRange(00)  # 不确定进度        self.statusBar().showMessage("正在处理中,请稍候...")        self.add_log(f"开始处理文件夹: {folder_path}")        self.add_log(f"找到 {len(pdf_files)} 个PDF文件")        # 创建并启动处理线程        self.process_thread = ProcessThread(folder_path)        self.process_thread.log.connect(self.add_log)        self.process_thread.finished.connect(self.on_processing_finished)        self.process_thread.start()    def on_processing_finished(self, success, output_file, success_count, fail_count):        """处理完成回调"""        self.process_btn.setEnabled(True)        self.browse_btn.setEnabled(True)        self.progress_bar.setVisible(False)        self.progress_bar.setRange(0100)        if success:            self.add_log("=" * 50)            self.add_log("处理完成!")            self.add_log(f"结果文件已保存至: {output_file}")            self.statusBar().showMessage("处理完成")            QMessageBox.information(                self,                "处理完成",                f"发票处理完成!\n\n结果文件已保存至:\n{output_file}",                QMessageBox.StandardButton.Ok            )        else:            self.add_log("处理失败,请检查文件格式")            self.statusBar().showMessage("处理失败")            QMessageBox.critical(                self,                "处理失败",                "发票处理失败,请检查PDF文件格式是否正确。",                QMessageBox.StandardButton.Ok            )    def closeEvent(self, event):        """关闭窗口事件"""        if self.process_thread and self.process_thread.isRunning():            reply = QMessageBox.question(                self,                "确认退出",                "正在处理中,确定要退出吗?",                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No            )            if reply == QMessageBox.StandardButton.Yes:                self.process_thread.terminate()                event.accept()            else:                event.ignore()        else:            event.accept()def main():    """主函数"""    app = QApplication(sys.argv)    # 设置应用图标(如果有的话)    # app.setWindowIcon(QIcon("icon.png"))    window = InvoiceProcessorGUI()    window.show()    sys.exit(app.exec())if __name__ == "__main__":    main()

      最终对所有pdf数电发票的明细信息进行了提取保存,多行明细且存在跨行情况也能完美提取到,完美实现了之前既定的需求….

      通过上面Python自动化脚本,仅用几分钟的时间就完成原需手动操作数小时甚至数天的工作任务。从最初准备手动人工机械操作的麻木到用python实现高效自动化的畅快,工作效率获得指数级提升,终于实现了不加班熬夜的自由!

      大佬们也可以举一反三,参照上面的代码思路根据自己工作中的实际情况来具体问题具体分析,实现自己定制化的需求。

      结语

      当Python遇见办公,牛马打工人终于笑出了猪叫声

      【职场人必看】每天早上一睁眼,想到又要面对:

      1.📊 堆积如山的Excel表格

      2.📑 机械重复的复制粘贴

      3.✍️ 永远改不完的各类文档

      4.诸如此类的更多……..

      是不是连Ctrl+Alt+Delete的心都有了?

      别慌!别急,摸鱼这位“职场外挂”已经带着Python代码来拯救你了!

      友情提示:考虑到没有python环境的朋友需要打包好的成品exe,摸鱼早已贴心打包好,本篇文章代码打包的exe截图如下:

      另外,《码海听潮》公众号所有文章码和exe程序已打包好上传绿联nas私有云,有需要的大佬扫一扫上面博主的个人微信二维码,需要的大佬需支付9.9元永久拥有公众号资源(写原创干货费时费力,属实不易),邀请您进入社区群获取下载链接!!,群内提供python办公自动化交流问题,解决问题,且码海听潮微信公众号文章发布会第一时间会更新到群里,非诚勿扰哈!

      码海听潮官方社区群如下:

      赶紧微信扫一扫下方二维码添加摸鱼君微信