【PDF提取表格】如何提取发票内容文字并导出到Excel表格,并将发票用发票号改名,基于pdf电子发票的应用实现

news2025/6/7 2:11:42

应用场景

该应用主要用于企业财务部门或个人处理大量电子发票,实现以下功能:

  1. 自动从 PDF 电子发票中提取关键信息(如发票号码、日期、金额、销售方等)
  2. 将提取的信息整理并导出到 Excel 表格,方便进行财务统计和报销
  3. 使用发票号码自动重命名发票文件,便于文件管理和检索

界面设计

界面采用直观的标签页设计,主要包含以下部分:

  1. 文件选择区域:提供两种方式选择发票文件
    • 单个或多个文件选择
    • 整个文件夹选择
  2. 文件列表显示:显示已选择的发票文件
  3. 导出设置:设置输出 Excel 文件的路径
  4. 处理进度显示:包括进度条和状态文本
  5. 日志区域:实时显示处理过程中的信息和错误

详细代码步骤

import os
import re
import pandas as pd
import pdfplumber
from datetime import datetime
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import threading
import logging

class InvoiceProcessor:
    def __init__(self):
        self.invoice_data = []
        self.logger = self.setup_logger()
        
    def setup_logger(self):
        """设置日志记录"""
        logger = logging.getLogger('InvoiceProcessor')
        logger.setLevel(logging.INFO)
        formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
        
        # 输出到文件
        file_handler = logging.FileHandler('invoice_processor.log')
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)
        
        # 输出到控制台
        stream_handler = logging.StreamHandler()
        stream_handler.setFormatter(formatter)
        logger.addHandler(stream_handler)
        
        return logger
    
    def extract_invoice_info(self, pdf_path):
        """从PDF中提取发票信息"""
        try:
            with pdfplumber.open(pdf_path) as pdf:
                text = ""
                for page in pdf.pages:
                    text += page.extract_text() or ""
            
            # 使用正则表达式提取发票信息
            invoice_number = re.search(r'发票号码[::]?\s*(\d+)', text)
            invoice_date = re.search(r'开票日期[::]?\s*(\d{4}年\d{1,2}月\d{1,2}日)', text)
            total_amount = re.search(r'金额[::]?\s*¥?\s*(\d+\.\d{2})', text)
            
            # 提取纳税人识别号
            taxpayer_id = re.search(r'纳税人识别号[::]?\s*([0-9A-Z]+)', text)
            
            # 提取购买方名称
            buyer_name = re.search(r'购买方名称[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9]+)', text)
            
            # 提取销售方名称
            seller_name = re.search(r'销售方名称[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9]+)', text)
            
            # 提取服务名称(项目名称)
            service_name = re.search(r'服务名称[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9]+)', text)
            if not service_name:
                service_name = re.search(r'货物或应税劳务、服务名称\s*([\u4e00-\u9fa5a-zA-Z0-9]+)', text)
            
            # 提取价税合计
            total_tax_amount = re.search(r'价税合计[::]?\s*[人民币]*\s*¥?\s*(\d+\.\d{2})', text)
            if not total_tax_amount:
                total_tax_amount = total_amount
            
            # 提取密文区
            cipher_text = re.search(r'密文区[::]?\s*([\dA-Z]+)', text)
            
            # 提取备注
            remark = re.search(r'备注[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\s]+)', text)
            
            # 提取校验码
            check_code = re.search(r'校验码[::]?\s*(\d+)', text)
            
            # 提取税率
            tax_rate = re.search(r'税率[::]?\s*(\d+\.\d+%)', text)
            
            # 提取税额
            tax_amount = re.search(r'税额[::]?\s*¥?\s*(\d+\.\d{2})', text)
            
            # 提取机器编号
            machine_code = re.search(r'机器编号[::]?\s*(\d+)', text)
            
            # 提取收款人
            cashier = re.search(r'收款人[::]?\s*([\u4e00-\u9fa5]+)', text)
            
            # 提取复核人
            reviewer = re.search(r'复核[::]?\s*([\u4e00-\u9fa5]+)', text)
            
            # 提取开票人
            drawer = re.search(r'开票人[::]?\s*([\u4e00-\u9fa5]+)', text)
            
            # 提取发票类型
            invoice_type = re.search(r'([增值税专用|增值税普通|机动车销售统一|通用机打|电子|卷式|定额])发票', text)
            
            # 提取地址、电话
            address_phone = re.search(r'地址、电话[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\s]+)', text)
            
            # 提取开户行及账号
            bank_info = re.search(r'开户行及账号[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\s]+)', text)
            
            # 提取机器编号
            machine_number = re.search(r'机器编号[::]?\s*([0-9]+)', text)
            
            # 提取发票代码
            invoice_code = re.search(r'发票代码[::]?\s*([0-9]+)', text)
            
            # 提取密码区
            password_area = re.search(r'密码区[::]?\s*([\dA-Za-z]+)', text)
            
            # 提取二维码信息
            qr_code = re.search(r'二维码[::]?\s*([\dA-Za-z]+)', text)
            
            # 提取商品或服务明细
            details_pattern = r'商品或服务名称\s*规格型号\s*单位\s*数量\s*单价\s*金额\s*税率\s*税额\s*([\s\S]*?)合计'
            details_match = re.search(details_pattern, text)
            details = details_match.group(1).strip() if details_match else ""
            
            # 提取合计金额(大写)
            total_amount_in_words = re.search(r'合计金额[::]?\s*([\u4e00-\u9fa5零壹贰叁肆伍陆柒捌玖拾佰仟万亿元角分整]+)', text)
            
            # 提取合计税额
            total_tax = re.search(r'合计税额[::]?\s*¥?\s*(\d+\.\d{2})', text)
            
            # 提取价税合计(大写)
            total_amount_in_words_full = re.search(r'价税合计\s*\(大写\)\s*([\u4e00-\u9fa5零壹贰叁肆伍陆柒捌玖拾佰仟万亿元角分整]+)', text)
            
            # 提取价税合计(小写)
            total_amount_in_figures = re.search(r'价税合计\s*\(小写\)\s*¥?\s*(\d+\.\d{2})', text)
            
            # 提取收款人、复核人、开票人
            signature_info = re.search(r'收款人[::]?\s*([\u4e00-\u9fa5]+)\s*复核[::]?\s*([\u4e00-\u9fa5]+)\s*开票人[::]?\s*([\u4e00-\u9fa5]+)', text)
            
            # 提取销售方纳税人识别号
            seller_tax_id = re.search(r'销售方[::]?\s*纳税人识别号[::]?\s*([0-9A-Z]+)', text)
            
            # 提取销售方地址、电话
            seller_address_phone = re.search(r'销售方[::]?\s*地址、电话[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\s]+)', text)
            
            # 提取销售方开户行及账号
            seller_bank_info = re.search(r'销售方[::]?\s*开户行及账号[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\s]+)', text)
            
            # 提取购买方纳税人识别号
            buyer_tax_id = re.search(r'购买方[::]?\s*纳税人识别号[::]?\s*([0-9A-Z]+)', text)
            
            # 提取购买方地址、电话
            buyer_address_phone = re.search(r'购买方[::]?\s*地址、电话[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\s]+)', text)
            
            # 提取购买方开户行及账号
            buyer_bank_info = re.search(r'购买方[::]?\s*开户行及账号[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\s]+)', text)
            
            # 提取机器编号、发票代码、发票号码组合信息
            machine_invoice_info = re.search(r'机器编号[::]?\s*([0-9]+)\s*发票代码[::]?\s*([0-9]+)\s*发票号码[::]?\s*([0-9]+)', text)
            
            # 提取日期(不同格式)
            date_patterns = [
                r'日期[::]?\s*(\d{4}年\d{1,2}月\d{1,2}日)',
                r'日期[::]?\s*(\d{4}/\d{1,2}/\d{1,2})',
                r'日期[::]?\s*(\d{4}-\d{1,2}-\d{1,2})',
                r'日期[::]?\s*(\d{4}\.\d{1,2}\.\d{1,2})'
            ]
            
            invoice_date = None
            for pattern in date_patterns:
                date_match = re.search(pattern, text)
                if date_match:
                    invoice_date = date_match.group(1)
                    break
            
            # 提取金额(不同格式)
            amount_patterns = [
                r'金额[::]?\s*¥?\s*(\d+\.\d{2})',
                r'金额[::]?\s*¥?\s*(\d+\,\d+\.\d{2})',
                r'价税合计[::]?\s*[人民币]*\s*¥?\s*(\d+\.\d{2})',
                r'合计[::]?\s*¥?\s*(\d+\.\d{2})'
            ]
            
            total_amount = None
            for pattern in amount_patterns:
                amount_match = re.search(pattern, text)
                if amount_match:
                    total_amount = amount_match.group(1)
                    break
            
            # 提取发票号码(不同格式)
            invoice_number = None
            for pattern in [
                r'发票号码[::]?\s*(\d+)',
                r'发票号码[::]?\s*([A-Z]\d+)',
                r'发票号[::]?\s*(\d+)',
                r'发票号[::]?\s*([A-Z]\d+)'
            ]:
                number_match = re.search(pattern, text)
                if number_match:
                    invoice_number = number_match.group(1)
                    break
            
            # 提取其他可能的信息
            # 提取二维码区域信息
            qr_area = re.search(r'二维码[::]?\s*([\d\w\s]+)', text)
            
            # 提取机器编号(多种可能位置)
            machine_code = None
            for pattern in [
                r'机器编号[::]?\s*(\d+)',
                r'机打代码[::]?\s*(\d+)',
                r'机打号码[::]?\s*(\d+)'
            ]:
                machine_match = re.search(pattern, text)
                if machine_match:
                    machine_code = machine_match.group(1)
                    break
            
            # 提取校验码
            check_code = re.search(r'校验码[::]?\s*([\dA-Z]+)', text)
            
            # 提取购买方信息(公司名称)
            buyer_info = re.search(r'购买方[::]?\s*名称[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\(\)\(\)]+)', text)
            
            # 提取销售方信息(公司名称)
            seller_info = re.search(r'销售方[::]?\s*名称[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\(\)\(\)]+)', text)
            
            # 提取项目内容
            items_pattern = r'项目[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\s]+)'
            items_match = re.search(items_pattern, text)
            items = items_match.group(1).strip() if items_match else ""
            
            # 提取服务内容
            service_pattern = r'服务内容[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\s]+)'
            service_match = re.search(service_pattern, text)
            service = service_match.group(1).strip() if service_match else ""
            
            # 提取金额明细
            amount_details_pattern = r'金额明细[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\s\(\)\(\)\d\.\,]+)'
            amount_details_match = re.search(amount_details_pattern, text)
            amount_details = amount_details_match.group(1).strip() if amount_details_match else ""
            
            # 提取备注信息
            remark_pattern = r'备注[::]?\s*([\u4e00-\u9fa5a-zA-Z0-9\s\(\)\(\)\d\.\,]+)'
            remark_match = re.search(remark_pattern, text)
            remark = remark_match.group(1).strip() if remark_match else ""
            
            # 提取收款人、复核人、开票人
            signature_pattern = r'收款人[::]?\s*([\u4e00-\u9fa5]+)\s*复核[::]?\s*([\u4e00-\u9fa5]+)\s*开票人[::]?\s*([\u4e00-\u9fa5]+)'
            signature_match = re.search(signature_pattern, text)
            cashier = signature_match.group(1) if signature_match else ""
            reviewer = signature_match.group(2) if signature_match else ""
            drawer = signature_match.group(3) if signature_match else ""
            
            # 提取发票类型
            invoice_type_pattern = r'([增值税专用|增值税普通|机动车销售统一|通用机打|电子|卷式|定额])发票'
            invoice_type_match = re.search(invoice_type_pattern, text)
            invoice_type = invoice_type_match.group(1) if invoice_type_match else ""
            
            # 提取日期并格式化
            date_str = invoice_date.group(1) if invoice_date else ""
            try:
                if '年' in date_str and '月' in date_str and '日' in date_str:
                    date_obj = datetime.strptime(date_str, '%Y年%m月%d日')
                elif '/' in date_str:
                    date_obj = datetime.strptime(date_str, '%Y/%m/%d')
                elif '-' in date_str:
                    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
                elif '.' in date_str:
                    date_obj = datetime.strptime(date_str, '%Y.%m.%d')
                else:
                    date_obj = None
                formatted_date = date_obj.strftime('%Y-%m-%d') if date_obj else ""
            except:
                formatted_date = date_str
            
            # 提取金额并转换为浮点数
            amount_str = total_amount.group(1) if total_amount else "0.00"
            amount_str = amount_str.replace(',', '')  # 移除千位分隔符
            try:
                amount = float(amount_str)
            except:
                amount = 0.00
            
            # 提取发票号码
            number = invoice_number.group(1) if invoice_number else ""
            
            # 提取纳税人识别号
            tax_id = taxpayer_id.group(1) if taxpayer_id else ""
            
            # 提取购买方名称
            buyer = buyer_name.group(1) if buyer_name else ""
            
            # 提取销售方名称
            seller = seller_name.group(1) if seller_name else ""
            
            # 提取服务名称
            service = service_name.group(1) if service_name else ""
            
            # 创建发票信息字典
            invoice_info = {
                '文件路径': pdf_path,
                '发票号码': number,
                '发票日期': formatted_date,
                '金额': amount,
                '纳税人识别号': tax_id,
                '购买方名称': buyer,
                '销售方名称': seller,
                '服务名称': service,
                '发票类型': invoice_type,
                '收款人': cashier,
                '复核人': reviewer,
                '开票人': drawer,
                '备注': remark,
                '提取状态': '成功'
            }
            
            # 添加其他可能提取的信息
            additional_info = {
                '机器编号': machine_code.group(1) if machine_code else "",
                '发票代码': invoice_code.group(1) if invoice_code else "",
                '校验码': check_code.group(1) if check_code else "",
                '价税合计': total_tax_amount.group(1) if total_tax_amount else "",
                '税额': tax_amount.group(1) if tax_amount else "",
                '税率': tax_rate.group(1) if tax_rate else "",
                '密文区': cipher_text.group(1) if cipher_text else "",
                '销售方纳税人识别号': seller_tax_id.group(1) if seller_tax_id else "",
                '购买方纳税人识别号': buyer_tax_id.group(1) if buyer_tax_id else "",
                '销售方地址电话': seller_address_phone.group(1) if seller_address_phone else "",
                '购买方地址电话': buyer_address_phone.group(1) if buyer_address_phone else "",
                '销售方开户行账号': seller_bank_info.group(1) if seller_bank_info else "",
                '购买方开户行账号': buyer_bank_info.group(1) if buyer_bank_info else "",
                '金额大写': total_amount_in_words.group(1) if total_amount_in_words else "",
                '价税合计大写': total_amount_in_words_full.group(1) if total_amount_in_words_full else "",
                '商品服务明细': details,
                '二维码信息': qr_code.group(1) if qr_code else ""
            }
            
            # 合并主要信息和附加信息
            invoice_info.update(additional_info)
            
            return invoice_info
            
        except Exception as e:
            self.logger.error(f"提取文件 {pdf_path} 时出错: {str(e)}")
            return {
                '文件路径': pdf_path,
                '发票号码': '',
                '发票日期': '',
                '金额': 0.0,
                '提取状态': f'失败: {str(e)}'
            }
    
    def process_invoices(self, pdf_paths):
        """处理多个发票文件"""
        self.invoice_data = []
        total_files = len(pdf_paths)
        
        for i, pdf_path in enumerate(pdf_paths):
            try:
                progress = (i + 1) / total_files * 100
                yield progress, f"正在处理: {os.path.basename(pdf_path)}"
                
                invoice_info = self.extract_invoice_info(pdf_path)
                self.invoice_data.append(invoice_info)
                
                # 如果成功提取到发票号码,重命名文件
                if invoice_info['发票号码']:
                    dirname = os.path.dirname(pdf_path)
                    base_name = f"{invoice_info['发票号码']}.pdf"
                    new_path = os.path.join(dirname, base_name)
                    
                    # 处理文件已存在的情况
                    counter = 1
                    while os.path.exists(new_path):
                        base_name = f"{invoice_info['发票号码']}_{counter}.pdf"
                        new_path = os.path.join(dirname, base_name)
                        counter += 1
                    
                    os.rename(pdf_path, new_path)
                    invoice_info['新文件路径'] = new_path
                    self.logger.info(f"文件已重命名: {pdf_path} -> {new_path}")
                else:
                    invoice_info['新文件路径'] = pdf_path
                    self.logger.warning(f"未能提取到发票号码,文件未重命名: {pdf_path}")
            except Exception as e:
                self.logger.error(f"处理文件 {pdf_path} 时出错: {str(e)}")
                yield progress, f"处理文件 {os.path.basename(pdf_path)} 时出错: {str(e)}"
        
        self.logger.info(f"处理完成,共处理 {total_files} 个文件")
        yield 100, "处理完成"
    
    def export_to_excel(self, output_path):
        """将提取的发票信息导出到Excel"""
        if not self.invoice_data:
            self.logger.warning("没有数据可导出")
            return False
        
        try:
            df = pd.DataFrame(self.invoice_data)
            
            # 确保目录存在
            output_dir = os.path.dirname(output_path)
            if not os.path.exists(output_dir):
                os.makedirs(output_dir)
            
            # 导出到Excel
            with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
                df.to_excel(writer, sheet_name='发票信息', index=False)
                
                # 获取工作簿和工作表对象以进行格式设置
                workbook = writer.book
                worksheet = writer.sheets['发票信息']
                
                # 设置列宽
                for i, col in enumerate(df.columns):
                    column_width = max(len(str(x)) for x in df[col])
                    column_width = max(column_width, len(col)) + 2
                    column_width = min(column_width, 50)  # 最大宽度限制
                    worksheet.column_dimensions[chr(65 + i)].width = column_width
                
                # 创建表头样式
                header_format = workbook.add_format({
                    'bold': True,
                    'text_wrap': True,
                    'valign': 'top',
                    'fg_color': '#D7E4BC',
                    'border': 1
                })
                
                # 应用表头样式
                for col_num, value in enumerate(df.columns.values):
                    worksheet.cell(row=0, column=col_num).value = value
                    worksheet.cell(row=0, column=col_num).set_format(header_format)
            
            self.logger.info(f"数据已成功导出到 {output_path}")
            return True
        except Exception as e:
            self.logger.error(f"导出到Excel时出错: {str(e)}")
            return False

class InvoiceApp:
    def __init__(self, root):
        self.root = root
        self.root.title("发票处理工具")
        self.root.geometry("800x600")
        self.root.minsize(600, 400)
        
        # 设置中文字体
        self.root.option_add("*Font", "SimHei 10")
        
        self.processor = InvoiceProcessor()
        self.selected_files = []
        self.output_path = ""
        
        self.create_widgets()
    
    def create_widgets(self):
        """创建界面组件"""
        # 创建主框架
        main_frame = ttk.Frame(self.root, padding="20")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 文件选择区域
        file_frame = ttk.LabelFrame(main_frame, text="文件选择", padding="10")
        file_frame.pack(fill=tk.X, pady=(0, 10))
        
        # 文件选择按钮
        ttk.Button(file_frame, text="选择发票文件", command=self.select_files).pack(side=tk.LEFT, padx=(0, 10))
        ttk.Button(file_frame, text="选择文件夹", command=self.select_directory).pack(side=tk.LEFT)
        
        # 文件列表
        self.file_listbox = tk.Listbox(file_frame, height=5, width=70)
        self.file_listbox.pack(fill=tk.X, pady=(10, 0))
        
        # 导出设置区域
        export_frame = ttk.LabelFrame(main_frame, text="导出设置", padding="10")
        export_frame.pack(fill=tk.X, pady=(0, 10))
        
        # 输出文件路径
        ttk.Label(export_frame, text="输出Excel文件:").pack(side=tk.LEFT, padx=(0, 10))
        
        self.output_var = tk.StringVar(value="发票信息汇总.xlsx")
        output_entry = ttk.Entry(export_frame, textvariable=self.output_var, width=50)
        output_entry.pack(side=tk.LEFT, padx=(0, 10))
        
        ttk.Button(export_frame, text="浏览...", command=self.select_output_path).pack(side=tk.LEFT)
        
        # 处理按钮
        self.process_button = ttk.Button(main_frame, text="开始处理", command=self.process_invoices)
        self.process_button.pack(pady=(10, 20))
        
        # 进度条
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, length=100)
        self.progress_bar.pack(fill=tk.X, pady=(0, 10))
        
        # 状态标签
        self.status_var = tk.StringVar(value="就绪")
        ttk.Label(main_frame, textvariable=self.status_var).pack(anchor=tk.W)
        
        # 日志区域
        log_frame = ttk.LabelFrame(main_frame, text="处理日志", padding="10")
        log_frame.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
        
        self.log_text = tk.Text(log_frame, height=10, wrap=tk.WORD)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        
        # 添加日志处理程序
        self.add_log_handler()
    
    def add_log_handler(self):
        """添加日志处理程序将日志输出到界面"""
        class TextHandler(logging.Handler):
            def __init__(self, text_widget):
                logging.Handler.__init__(self)
                self.text_widget = text_widget
            
            def emit(self, record):
                msg = self.format(record)
                def append():
                    self.text_widget.configure(state='normal')
                    self.text_widget.insert(tk.END, msg + '\n')
                    self.text_widget.configure(state='disabled')
                    self.text_widget.yview(tk.END)
                self.text_widget.after(0, append)
        
        handler = TextHandler(self.log_text)
        handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
        self.processor.logger.addHandler(handler)
    
    def select_files(self):
        """选择多个文件"""
        file_paths = filedialog.askopenfilenames(
            title="选择发票PDF文件",
            filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")]
        )
        
        if file_paths:
            self.selected_files = list(file_paths)
            self.update_file_listbox()
    
    def select_directory(self):
        """选择文件夹"""
        directory = filedialog.askdirectory(title="选择包含发票PDF的文件夹")
        
        if directory:
            pdf_files = [
                os.path.join(directory, f) 
                for f in os.listdir(directory) 
                if f.lower().endswith('.pdf')
            ]
            
            if pdf_files:
                self.selected_files = pdf_files
                self.update_file_listbox()
            else:
                messagebox.showinfo("提示", "所选文件夹中未找到PDF文件")
    
    def update_file_listbox(self):
        """更新文件列表显示"""
        self.file_listbox.delete(0, tk.END)
        for file_path in self.selected_files:
            self.file_listbox.insert(tk.END, os.path.basename(file_path))
        
        file_count = len(self.selected_files)
        self.status_var.set(f"已选择 {file_count} 个文件")
    
    def select_output_path(self):
        """选择输出文件路径"""
        file_path = filedialog.asksaveasfilename(
            title="保存Excel文件",
            defaultextension=".xlsx",
            filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")]
        )
        
        if file_path:
            self.output_var.set(file_path)
    
    def process_invoices(self):
        """处理发票文件"""
        if not self.selected_files:
            messagebox.showwarning("警告", "请先选择发票文件")
            return
        
        output_path = self.output_var.get()
        if not output_path:
            messagebox.showwarning("警告", "请设置输出Excel文件路径")
            return
        
        # 禁用处理按钮防止重复点击
        self.process_button.config(state=tk.DISABLED)
        
        # 启动处理线程
        threading.Thread(target=self._process_invoices_thread, daemon=True).start()
    
    def _process_invoices_thread(self):
        """在单独线程中处理发票,避免阻塞UI"""
        output_path = self.output_var.get()
        
        try:
            # 处理发票
            for progress, status in self.processor.process_invoices(self.selected_files):
                self.progress_var.set(progress)
                self.status_var.set(status)
                self.root.update_idletasks()
            
            # 导出到Excel
            if self.processor.export_to_excel(output_path):
                self.status_var.set(f"处理完成,数据已导出到 {output_path}")
                messagebox.showinfo("成功", f"处理完成,数据已导出到 {output_path}")
            else:
                self.status_var.set("导出Excel失败")
                messagebox.showerror("错误", "导出Excel失败,请查看日志")
        except Exception as e:
            self.status_var.set(f"处理过程中出错: {str(e)}")
            messagebox.showerror("错误", f"处理过程中出错: {str(e)}")
        finally:
            # 重新启用处理按钮
            self.process_button.config(state=tk.NORMAL)

if __name__ == "__main__":
    root = tk.Tk()
    app = InvoiceApp(root)
    root.mainloop()    
  1. 环境准备:安装必要的库,如 pdfplumber、pandas、tkinter 等
  2. 发票内容提取
    • 使用 pdfplumber 打开并读取 PDF 文件
    • 使用正则表达式从文本中提取关键信息(发票号码、日期、金额等)
    • 处理不同格式的发票,提高提取准确性
  3. 文件重命名
    • 使用提取的发票号码作为新文件名
    • 处理文件已存在的情况,避免冲突
  4. Excel 导出
    • 使用 pandas 将提取的数据整理成 DataFrame
    • 导出到 Excel 文件,并设置适当的格式
  5. 图形界面开发
    • 使用 tkinter 创建用户友好的界面
    • 实现文件选择、进度显示和日志记录功能
    • 使用多线程处理避免界面卡顿

总结优化

  1. 优点
    • 自动化处理,减少人工输入错误
    • 界面友好,操作简单
    • 详细的日志记录,方便排查问题
    • 支持多种发票格式和内容提取
  2. 优化方向
    • 提高复杂发票的识别准确率,可以考虑使用 OCR 技术
    • 添加数据验证和纠错功能
    • 支持更多格式的发票文件(如图片格式)
    • 增加数据统计和分析功能
    • 实现批量处理和定时任务功能
    • 添加用户权限管理和数据安全保护

使用这个应用程序,你可以轻松地处理大量电子发票,提高工作效率和准确性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2401141.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Hugging Face 最新开源 SmolVLA 小模型入门教程(一)

系列文章目录 目录 系列文章目录 前言 一、引言 二、认识 SmolVLA! 三、如何使用SmolVLA? 3.1 安装 3.2 微调预训练模型 3.3 从头开始训练 四、方法 五、主要架构 5.1 视觉语言模型(VLM) 5.2 动作专家:流匹…

封闭内网安装配置VSCode Anconda3 并配置 PyQt5开发

封闭内网安装配置VSCode Anconda3 并配置 PyQt5开发 零一 vscode1.1 下载 vscode1.2 下载插件1.3 安装 二 anaconda 32.1 下载2.2 新建虚拟环境1 新建快捷方式,启动base2 新建虚拟环境 3 配置Qt designer3.1 designer.exe和uic.exe3.2 设置插件,3.4 ui文件转为py文件 4使用4.1 …

大话软工笔记—组合要素2之逻辑

1. 逻辑的概念 逻辑,指的是思维的规律和规则,是对思维过程的抽象。 结合逻辑的一般定义以及信息系统的设计方法,对逻辑的概念进行抽提、定义为三个核心内涵,即:规律、顺序、规则。 (1)规律&a…

7.Demo Js执行同步任务,微任务,宏任务的顺序(3)

一个包含 同步任务、微任务(Promise)、宏任务(setTimeout) 的例子,JS 是怎么调度这些任务的。 🎯 例子代码(建议复制到浏览器控制台运行) console.log(‘同步任务 1’); setTimeo…

边缘计算网关赋能沸石转轮运行故障智能诊断的配置实例

一、项目背景 在环保行业,随着国家对大气污染治理要求的不断提高,VOCs废气处理成为了众多企业的重要任务。沸石转轮作为一种高效的VOCs治理设备,被广泛应用于石油化工、汽车制造、印刷包装等主流行业。这些行业生产规模大、废气排放量多&…

UE5 2D角色PaperZD插件动画状态机学习笔记

UE5 2D角色PaperZD插件动画状态机学习笔记 0.安装PaperZD插件 这是插件下载安装地址 https://www.fab.com/zh-cn/listings/6664e3b5-e376-47aa-a0dd-f7bbbd5b93c0 1.右键创建PaperZD 动画序列 2.添加动画序列 3,右键创建PaperZD AnimBP (动画蓝图&am…

Ubuntu 16.04 密码找回

同事整理的供参考: 进入GRUB菜单 重启系统,在启动过程中长按Shift键(或Esc键)进入GRUB引导菜单。 若未显示GRUB菜单,可尝试在启动时连续按多次Shift/Esc键。 在GRUB菜单中选择默认的Ubuntu启动项(第一…

【论文阅读】DanceGRPO: Unleashing GRPO on Visual Generation

DanceGRPO: Unleashing GRPO on Visual Generation 原文摘要 研究背景与问题 生成模型的突破:扩散模型和整流流等生成模型在视觉内容生成领域取得了显著进展。核心挑战:如何让模型的输出更好地符合人类偏好仍是一个关键问题。现有方法的局限性&#xff1…

CentOS在vmware局域网内搭建DHCP服务器【踩坑记录】

1. 重新设置环境 配置dhcp服务踩了不少坑,这里重头搭建记录一下: 1.1 centos 网卡还原 如果之前搭了乱七八糟的环境,导致NAT模式也没法上网,这里重新还原 我们需要在NAT模式下联网,下载DHCP服务 先把centos的网卡还…

AI炼丹日志-28 - Audiblez 将你的电子书epub转换为音频mp3 做有声书

点一下关注吧!!!非常感谢!!持续更新!!! 🚀 大模型与Java双线更新中! 目前《大语言模型实战》已连载至第22篇,探索 MCP 自动操作 FigmaCursor 实…

74. 搜索二维矩阵 (力扣)

给你一个满足下述两条属性的 m x n 整数矩阵: 每行中的整数从左到右按非严格递增顺序排列。每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false 。…

8088单板机C语言sprintf()格式化串口输出---Prj04

#include "tiny_stdarg.h" // 使用自定义可变参数实现#define ADR_273 0x0200 #define ADR_244 0x0400 #define LED_PORT 0x800 #define PC16550_THR 0x1f0 #define PC16550_LSR 0x1f5 / //基本的IO操作函数 / char str[]"Hello World! 20250531 Ve…

板凳-------Mysql cookbook学习 (九)

第4章:表管理 4.0 引言 MySQL :: 员工样例数据库 :: 3 安装 https://dev.mysql.com/doc/employee/en/employees-installation.html Employees 数据库与几种不同的 存储引擎,默认情况下启用 InnoDB 引擎。编…

基于SpringBoot的“嗨玩旅游”网站设计与实现(源码+定制+开发)嗨玩旅游平台开发:景点展示与个性化推荐系统(SpringBoot)

博主介绍: ✌我是阿龙,一名专注于Java技术领域的程序员,全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师,我在计算机毕业设计开发方面积累了丰富的经验。同时,我也是掘金、华为云、阿里云、InfoQ等平台…

python版若依框架开发:python版若依部署

python版若依框架开发 从0起步,扬帆起航。 python版若依部署文章目录 python版若依框架开发1.源码2.概述3.部署1.源码 https://gitee.com/insistence2022/RuoYi-Vue-FastAPI 请诸君移步上述链接,即可对python版若依项目进行初步了解。 2.概述 若依框架本身基于java,可以快…

h5的aliplayer-min.js 加密视频会走到debugger

h5的aliplayer-min.js 如果 https://g.alicdn.com/apsara-media-box/imp-web-player/2.19.0/aliplayer-min.js走加密视频的话会有debugger 更换aliplayer-min.js版本解决了 https://g.alicdn.com/apsara-media-box/imp-web-player/2.25.1/aliplayer-min.js 对应css&#xff1a…

DashBoard安装使用

DashBoard安装使用 一、实验目的 1、掌握dashboard 的安装部署 2、熟悉图像化部署任务:产生pod---定义服务--验证访问 二、实验内容: 1、配置步骤 1.1、Helm安装 离线安装(适用于内网/离线环境) # 根据系统架构选择版本&am…

极客大挑战 2019 EasySQL 1(万能账号密码,SQL注入,HackBar)

题目 做法 启动靶机,打开给出的网址 随便输点东西进去,测试一下 输入1、1’、1"判断SQL语句闭合方式 输入以上两个都是以下结果 但是,输入1’时,出现的是另外结果 输入1,1"时,SQL语句没有…

采用 Docker GPU 部署的 Ubuntu 或者 windows 桌面环境

# 国内下载不了 docker pull gezp/ubuntu-desktop:24.04-cu12.6.2# 阿里云镜像 docker pull registry.cn-hongkong.aliyuncs.com/gezp/ubuntu-desktop:24.04-cu12.6.2# create container with nomachine docker run -d --restarton-failure --name myubuntu --shm-size1024m -e…

关于面试找工作的总结(四)

不同情况下收到offer后的处理方法 1.不会去的,只是面试练手2.还有疑问,考虑中3.offer/职位不满足期望的4.已确认,但又收到更好的5.还想挽回之前的offer6.确认,准备入职7.还想拖一下的1.不会去的,只是面试练手 HR您好,非常荣幸收到贵司的offer,非常感谢一直以来您的帮助,…