import sys
import os
import re
import openpyxl
import traceback
from aip import AipOcr
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QTableWidget, QTableWidgetItem, QMessageBox,
QHeaderView, QProgressBar, QFileDialog, QMenu, QAction
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QMutex
from PyQt5.QtGui import QFont, QDragEnterEvent, QDropEvent, QClipboard
# ===================== 核心配置(用户需替换OCR密钥)=====================
APP_ID = "换成你自己的" # 替换为你的百度OCR APP_ID
API_KEY = "换成你自己的" # 替换为你的百度OCR API_KEY
SECRET_KEY = "换成你自己的" # 替换为你的百度OCR SECRET_KEY
# 安全配置(避免闪退关键)
SUPPORTED_FORMATS = {'jpg', 'jpeg', 'png', 'bmp', 'gif', 'tiff', 'webp'}
MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 最大支持10MB图片
MIN_IMAGE_SIZE = 1024 # 最小1KB
# 身份证特征关键词
BACK_SIDE_KEYWORDS = ['签发机关', '有效期限', '签发日期', '有效期至', '公安', '派出所']
FRONT_CORE_KEYWORDS = ['姓名', '公民身份号码', '出生', '性别', '民族', '住址']
# ======================================================================
# 全局异常捕获(防止闪退)
def except_hook(exc_type, exc_value, exc_traceback):
error_msg = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
QMessageBox.critical(None, "程序异常(已防止闪退)", f"错误详情:\n{error_msg[:500]}")
sys.__excepthook__(exc_type, exc_value, exc_traceback)
sys.excepthook = except_hook
# 验证OCR密钥配置
if not all([APP_ID, API_KEY, SECRET_KEY]):
QMessageBox.critical(None, "配置错误", "请先配置百度OCR密钥!\n步骤:\n1. 登录百度AI控制台(ai.baidu.com)\n2. 创建「身份证识别」应用\n3. 复制APP_ID、API_KEY、SECRET_KEY到代码中")
sys.exit(1)
# 初始化百度OCR客户端(增加超时配置)
client = AipOcr(APP_ID, API_KEY, SECRET_KEY)
client.setConnectionTimeoutInMillis(5000) # 连接超时5秒
client.setSocketTimeoutInMillis(10000) # 读取超时10秒
def format_birthday(birthday_str):
"""格式化出生年月(增加异常捕获)"""
try:
if not birthday_str or str(birthday_str).strip() == '未知':
return '未知'
clean_str = re.sub(r'[^\d]', '', str(birthday_str).strip())
if len(clean_str) != 8:
return '格式错误'
year, month, day = clean_str[:4], clean_str[4:6], clean_str[6:8]
if 1 <= int(month) <= 12 and 1 <= int(day) <= 31:
return f"{year}-{month}-{day}"
return '日期无效'
except Exception:
return '格式错误'
def analyze_idcard_fields(words_result):
"""分析身份证字段(增强参数校验)"""
try:
if not isinstance(words_result, dict):
return False, "识别结果格式异常"
all_fields = list(words_result.keys())
all_text = ' '.join([str(item.get('words', '')).strip() for item in words_result.values()])
front_core_match = sum(1 for field in FRONT_CORE_KEYWORDS if field in all_fields)
back_match = sum(1 for kw in BACK_SIDE_KEYWORDS if kw in all_fields or kw in all_text)
has_required = '姓名' in all_fields and '公民身份号码' in all_fields
if not has_required:
return False, "缺少姓名或身份证号"
if front_core_match >= 3:
return True, f"含{front_core_match}个正面核心字段"
return False, f"正面字段仅{front_core_match}个,背面元素{back_match}个"
except Exception as e:
return False, f"字段分析失败:{str(e)[:20]}"
class RecognizeThread(QThread):
"""识别线程(支持结果合并,线程安全)"""
progress_signal = pyqtSignal(int, str)
new_result_signal = pyqtSignal(list, int, int) # 新识别的结果(而非全部)
error_signal = pyqtSignal(str)
def __init__(self, image_paths, global_result_list, global_mutex):
super().__init__()
self.image_paths = image_paths
self.global_result_list = global_result_list # 全局结果列表(共享)
self.global_mutex = global_mutex # 全局锁(保证去重安全)
self.is_running = True
self.local_mutex = QMutex() # 本地锁
def run(self):
try:
# 安全筛选有效图片
valid_image_paths = []
for path in self.image_paths:
try:
if not isinstance(path, str) or not os.path.exists(path) or not os.path.isfile(path):
continue
ext = os.path.splitext(path)[1].lower()[1:]
if ext not in SUPPORTED_FORMATS:
continue
file_size = os.path.getsize(path)
if MIN_IMAGE_SIZE <= file_size <= MAX_IMAGE_SIZE:
valid_image_paths.append(path)
except Exception:
continue
total = len(valid_image_paths)
if total == 0:
self.error_signal.emit("未找到有效图片!\n支持:JPG/PNG/BMP/GIF/TIFF/WebP\n大小:1KB-10MB")
return
self.progress_signal.emit(0, f"已接收{total}张有效图片,开始识别...")
new_results = [] # 本次识别的新结果(未去重)
for idx, file_path in enumerate(valid_image_paths):
# 线程安全判断是否继续运行
self.local_mutex.lock()
if not self.is_running:
self.local_mutex.unlock()
self.progress_signal.emit(0, "识别已取消")
return
self.local_mutex.unlock()
progress = int((idx + 1) / total * 100)
filename = os.path.basename(file_path)
self.progress_signal.emit(progress, f"正在识别第{idx+1}/{total}张:{filename}")
# 安全读取图片文件
try:
with open(file_path, 'rb') as f:
image_data = f.read()
if len(image_data) < MIN_IMAGE_SIZE or len(image_data) > MAX_IMAGE_SIZE:
self.progress_signal.emit(progress, f"跳过:{filename} - 大小超出限制")
continue
except FileNotFoundError:
self.progress_signal.emit(progress, f"跳过:{filename} - 文件已删除")
continue
except PermissionError:
self.progress_signal.emit(progress, f"跳过:{filename} - 无读取权限")
continue
except Exception as e:
self.progress_signal.emit(progress, f"跳过:{filename} - 读取失败")
continue
# 安全调用OCR接口
try:
res = client.idcard(
image_data,
'front',
{
'detect_direction': 'true',
'detect_risk': 'false',
'accuracy': 'high'
}
)
except Exception as e:
self.progress_signal.emit(progress, f"警告:{filename} - 接口调用超时")
continue
# 安全处理OCR返回结果
try:
if res.get("error_code"):
err_code = res["error_code"]
if err_code == 17:
self.error_signal.emit("OCR配额不足!请登录百度AI控制台充值或明日再试")
return
elif "side does not match" in str(res.get("error_msg", "")):
self.progress_signal.emit(progress, f"跳过:{filename} - 识别为身份证背面")
else:
self.progress_signal.emit(progress, f"跳过:{filename} - OCR错误[{err_code}]")
continue
words_result = res.get("words_result", {})
if not words_result:
self.progress_signal.emit(progress, f"跳过:{filename} - 未识别到文字")
continue
is_valid_front, judge_msg = analyze_idcard_fields(words_result)
if not is_valid_front:
self.progress_signal.emit(progress, f"跳过:{filename} - {judge_msg}")
continue
# 整理核心识别结果
id_info = {
'姓名': words_result.get('姓名', {}).get('words', '').strip() or '未知',
'身份证号码': words_result.get('公民身份号码', {}).get('words', '').strip() or '未知',
'性别': words_result.get('性别', {}).get('words', '').strip() or '未知',
'民族': words_result.get('民族', {}).get('words', '').strip() or '未知',
'出生年月': format_birthday(words_result.get('出生', {}).get('words', '未知')),
'住址': words_result.get('住址', {}).get('words', '').strip() or '未知'
}
# 二次校验必填字段
if id_info['姓名'] == '未知' or id_info['身份证号码'] == '未知':
self.progress_signal.emit(progress, f"跳过:{filename} - 核心字段缺失")
continue
# 身份证号格式校验
id_num = id_info['身份证号码']
if len(id_num) != 18 or not id_num[:17].isdigit() or not (id_num[-1].isdigit() or id_num[-1].upper() == 'X'):
self.progress_signal.emit(progress, f"跳过:{filename} - 身份证号格式错误")
continue
# 全局去重(线程安全)
self.global_mutex.lock()
exists = any(item['身份证号码'] == id_num for item in self.global_result_list)
if not exists:
self.global_result_list.append(id_info)
new_results.append(id_info)
self.progress_signal.emit(progress, f"成功:{id_info['姓名']} - {id_num[:6]}...(新增)")
else:
self.progress_signal.emit(progress, f"跳过:{filename} - 身份证号已存在(历史数据)")
self.global_mutex.unlock()
except Exception as e:
self.progress_signal.emit(progress, f"跳过:{filename} - 结果处理失败")
continue
# 发送本次新增的结果(用于UI追加显示)
self.new_result_signal.emit(new_results, total, len(new_results))
except Exception as e:
self.error_signal.emit(f"线程异常:{str(e)[:50]}")
def stop(self):
"""安全停止线程"""
self.local_mutex.lock()
self.is_running = False
self.local_mutex.unlock()
class IDCardDragDropRecognizer(QMainWindow):
"""主窗口(支持合并结果+复制功能)"""
def __init__(self):
super().__init__()
self.init_base_config()
self.init_ui_components()
self.init_runtime_vars()
self.setAcceptDrops(True)
def init_base_config(self):
self.setWindowTitle("身份证识别1.0")
self.setGeometry(100, 100, 1100, 600)
self.setFont(QFont("Microsoft YaHei", 10))
self.setMinimumSize(900, 500)
def init_runtime_vars(self):
self.global_result_list = [] # 全局结果列表(存储所有识别结果)
self.global_mutex = QMutex() # 全局结果锁(保证线程安全)
self.recognize_thread = None
self.dragging = False
def init_ui_components(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(15)
main_layout.setContentsMargins(20, 20, 20, 20)
# 标题
title_label = QLabel("身份证识别工具")
title_label.setFont(QFont("Microsoft YaHei", 14, QFont.Bold))
title_label.setAlignment(Qt.AlignCenter)
main_layout.addWidget(title_label)
# 拖拽提示区域(更新功能提示)
self.drag_label = QLabel("""
<div style="text-align: center; color: #666; font-size: 11pt;">
<p>📌 支持多次拖拽图片,结果自动合并(按身份证号去重)</p>
<p>✅ 支持格式:JPG、PNG、BMP、GIF、TIFF、WebP</p>
<p>📏 支持大小:1KB ~ 10MB(避免超大文件闪退)</p>
<p>🔍 识别字段:姓名、身份证号、性别、民族、出生年月、住址</p>
<p>📋 操作支持:右键复制选中单元格/行/所有数据(适配Excel粘贴)</p>
<p>📊 累计识别:<span style="color: #2196F3;">0</span> 条有效信息</p>
</div>
""")
self.drag_label.setStyleSheet("""
border: 2px dashed #CCCCCC;
border-radius: 8px;
padding: 20px;
background-color: #F9F9F9;
""")
self.drag_label.setWordWrap(True)
main_layout.addWidget(self.drag_label)
# 功能按钮区域
btn_layout = QHBoxLayout()
self.clear_btn = QPushButton("清空结果")
self.clear_btn.setStyleSheet("""
QPushButton {
background-color: #FF9800;
color: white;
border: none;
border-radius: 4px;
padding: 10px 24px;
font-size: 11pt;
}
QPushButton:disabled {
background-color: #E0E0E0;
color: #999999;
}
""")
self.clear_btn.clicked.connect(self.clear_results)
self.clear_btn.setEnabled(False)
self.cancel_btn = QPushButton("取消识别")
self.cancel_btn.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
padding: 10px 24px;
font-size: 11pt;
}
QPushButton:disabled {
background-color: #E0E0E0;
color: #999999;
}
""")
self.cancel_btn.clicked.connect(self.cancel_recognition)
self.cancel_btn.setEnabled(False)
self.save_btn = QPushButton("导出Excel")
self.save_btn.setStyleSheet("""
QPushButton {
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
padding: 10px 24px;
font-size: 11pt;
}
QPushButton:disabled {
background-color: #E0E0E0;
color: #999999;
}
""")
self.save_btn.clicked.connect(self.export_to_excel)
self.save_btn.setEnabled(False)
btn_layout.addWidget(self.clear_btn)
btn_layout.addWidget(self.cancel_btn)
btn_layout.addStretch(1)
btn_layout.addWidget(self.save_btn)
main_layout.addLayout(btn_layout)
# 进度区域
progress_layout = QVBoxLayout()
self.progress_bar = QProgressBar()
self.progress_bar.setAlignment(Qt.AlignCenter)
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 1px solid #CCCCCC;
border-radius: 4px;
text-align: center;
height: 25px;
}
QProgressBar::chunk {
background-color: #2196F3;
border-radius: 2px;
}
""")
self.status_label = QLabel("等待拖拽图片...(可多次拖拽,右键可复制结果)")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("color: #666666;")
self.status_label.setWordWrap(True)
progress_layout.addWidget(self.progress_bar)
progress_layout.addWidget(self.status_label)
main_layout.addLayout(progress_layout)
# 结果表格(启用右键菜单)
self.table_columns = ["姓名", "身份证号码", "性别", "民族", "出生年月", "住址"]
self.result_table = QTableWidget()
self.result_table.setColumnCount(len(self.table_columns))
self.result_table.setHorizontalHeaderLabels(self.table_columns)
self.result_table.setStyleSheet("""
QTableWidget {
border: 1px solid #CCCCCC;
gridline-color: #F0F0F0;
}
QHeaderView::section {
background-color: #F5F5F5;
border: none;
border-bottom: 1px solid #CCCCCC;
padding: 8px;
font-weight: bold;
}
QTableWidget::item {
padding: 8px;
}
""")
# 启用右键菜单
self.result_table.setContextMenuPolicy(Qt.CustomContextMenu)
self.result_table.customContextMenuRequested.connect(self.show_table_context_menu)
# 表头自适应配置
header = self.result_table.horizontalHeader()
for i in range(len(self.table_columns)-1):
header.setSectionResizeMode(i, QHeaderView.ResizeToContents)
header.setSectionResizeMode(len(self.table_columns)-1, QHeaderView.Stretch)
main_layout.addWidget(self.result_table, 1)
# ===================== 右键菜单与复制功能 =====================
def show_table_context_menu(self, pos):
"""显示表格右键菜单"""
if self.result_table.rowCount() == 0:
return # 无数据时不显示菜单
menu = QMenu()
# 添加菜单选项
copy_cell_action = QAction("复制选中单元格", self)
copy_row_action = QAction("复制选中行", self)
copy_all_action = QAction("复制所有数据", self)
# 绑定功能
copy_cell_action.triggered.connect(self.copy_selected_cells)
copy_row_action.triggered.connect(self.copy_selected_rows)
copy_all_action.triggered.connect(self.copy_all_data)
# 添加到菜单
menu.addAction(copy_cell_action)
menu.addAction(copy_row_action)
menu.addAction(copy_all_action)
# 在鼠标位置显示菜单
menu.exec_(self.result_table.mapToGlobal(pos))
def copy_selected_cells(self):
"""复制选中的单元格(支持多个单元格)"""
try:
selected_items = self.result_table.selectedItems()
if not selected_items:
QMessageBox.warning(self, "提示", "未选中任何单元格!")
return
# 按行排序,同一行的单元格按列排序
selected_items.sort(key=lambda x: (x.row(), x.column()))
# 构建复制文本(单元格之间用制表符分隔,换行分隔不同行)
copy_text = ""
current_row = -1
for item in selected_items:
if item.row() != current_row:
if current_row != -1:
copy_text += "\n" # 换行
current_row = item.row()
else:
copy_text += "\t" # 制表符分隔单元格
# 处理住址字段的换行符
cell_text = item.text().replace("\n", " ")
copy_text += cell_text
# 复制到剪贴板
clipboard = QApplication.clipboard()
clipboard.setText(copy_text)
QMessageBox.information(self, "复制成功", f"已复制{len(selected_items)}个单元格数据到剪贴板!")
except Exception as e:
QMessageBox.warning(self, "复制失败", f"单元格复制错误:{str(e)}")
def copy_selected_rows(self):
"""复制选中的整行(支持多行)"""
try:
selected_rows = list(set(item.row() for item in self.result_table.selectedItems()))
if not selected_rows:
QMessageBox.warning(self, "提示", "未选中任何行!")
return
selected_rows.sort() # 按行号排序
# 构建复制文本(表头+数据,制表符分隔列,换行分隔行)
copy_text = "\t".join(self.table_columns) + "\n" # 表头
for row in selected_rows:
row_data = []
for col in range(len(self.table_columns)):
item = self.result_table.item(row, col)
cell_text = item.text().replace("\n", " ") if item else ""
row_data.append(cell_text)
copy_text += "\t".join(row_data) + "\n"
# 复制到剪贴板
clipboard = QApplication.clipboard()
clipboard.setText(copy_text.strip()) # 去除末尾换行
QMessageBox.information(self, "复制成功", f"已复制{len(selected_rows)}行完整数据到剪贴板!")
except Exception as e:
QMessageBox.warning(self, "复制失败", f"行复制错误:{str(e)}")
def copy_all_data(self):
"""复制所有数据(含表头)"""
try:
total_rows = self.result_table.rowCount()
if total_rows == 0:
QMessageBox.warning(self, "提示", "暂无数据可复制!")
return
# 构建复制文本(表头+所有行数据)
copy_text = "\t".join(self.table_columns) + "\n" # 表头
for row in range(total_rows):
row_data = []
for col in range(len(self.table_columns)):
item = self.result_table.item(row, col)
cell_text = item.text().replace("\n", " ") if item else ""
row_data.append(cell_text)
copy_text += "\t".join(row_data) + "\n"
# 复制到剪贴板
clipboard = QApplication.clipboard()
clipboard.setText(copy_text.strip())
QMessageBox.information(self, "复制成功", f"已复制所有{total_rows}行数据(含表头)到剪贴板!")
except Exception as e:
QMessageBox.warning(self, "复制失败", f"全量复制错误:{str(e)}")
# ===================== 拖放功能 =====================
def dragEnterEvent(self, event: QDragEnterEvent):
try:
if event.mimeData().hasUrls():
event.acceptProposedAction()
self.drag_label.setStyleSheet("""
border: 2px dashed #2196F3;
border-radius: 8px;
padding: 20px;
background-color: #E3F2FD;
""")
self.status_label.setText("松开鼠标开始识别...(结果将合并,右键可复制)")
self.status_label.setStyleSheet("color: #2196F3;")
self.dragging = True
except Exception:
event.ignore()
def dragLeaveEvent(self, event):
try:
if self.dragging:
self.drag_label.setStyleSheet("""
border: 2px dashed #CCCCCC;
border-radius: 8px;
padding: 20px;
background-color: #F9F9F9;
""")
self.update_drag_label_count() # 更新累计数量显示
self.status_label.setText("等待拖拽图片...(可多次拖拽,右键可复制结果)")
self.status_label.setStyleSheet("color: #666666;")
self.dragging = False
except Exception:
pass
def dropEvent(self, event: QDropEvent):
try:
# 恢复样式
self.drag_label.setStyleSheet("""
border: 2px dashed #CCCCCC;
border-radius: 8px;
padding: 20px;
background-color: #F9F9F9;
""")
self.update_drag_label_count() # 更新累计数量显示
self.dragging = False
# 安全获取拖拽路径
urls = event.mimeData().urls()
image_paths = []
for url in urls:
try:
local_path = url.toLocalFile()
if os.path.isfile(local_path):
image_paths.append(local_path)
except Exception:
continue
if not image_paths:
self.status_label.setText("未识别到有效文件!请拖拽图片文件(1KB-10MB)")
self.status_label.setStyleSheet("color: #F44336;")
return
# 停止当前线程(安全退出)
self.global_mutex.lock()
if self.recognize_thread and self.recognize_thread.isRunning():
self.recognize_thread.stop()
self.recognize_thread.wait(2000)
self.global_mutex.unlock()
# 重置进度条,保持历史结果
self.progress_bar.setValue(0)
self.cancel_btn.setEnabled(True)
self.save_btn.setEnabled(len(self.global_result_list) > 0) # 有历史结果就启用导出
self.clear_btn.setEnabled(len(self.global_result_list) > 0) # 有历史结果就启用清空
# 启动新线程(传入全局结果列表和锁)
self.recognize_thread = RecognizeThread(
image_paths,
self.global_result_list,
self.global_mutex
)
self.recognize_thread.progress_signal.connect(self.update_progress_status)
self.recognize_thread.new_result_signal.connect(self.append_new_results) # 追加新结果
self.recognize_thread.error_signal.connect(self.show_error_message)
self.recognize_thread.finished.connect(self.thread_finish_callback)
self.recognize_thread.start()
except Exception as e:
QMessageBox.warning(self, "拖拽异常", f"拖拽处理失败:{str(e)}")
# ===================== 结果处理 =====================
def append_new_results(self, new_results, total_count, new_count):
"""追加新识别的结果到表格(不清空历史)"""
try:
current_row = self.result_table.rowCount() # 获取当前表格行数
self.result_table.setRowCount(current_row + len(new_results)) # 增加行数
# 填充新结果
for idx, info in enumerate(new_results):
row_idx = current_row + idx
data_values = [
info['姓名'], info['身份证号码'],
info['性别'], info['民族'], info['出生年月'], info['住址']
]
for col_idx, value in enumerate(data_values):
table_item = QTableWidgetItem(str(value))
table_item.setTextAlignment(Qt.AlignCenter)
if col_idx == 5: # 住址列
table_item.setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.result_table.setItem(row_idx, col_idx, table_item)
# 更新状态和按钮
total_valid = len(self.global_result_list)
self.status_label.setText(
f"本次识别完成:共{total_count}张图片,新增{new_count}条有效信息,累计{total_valid}条(右键可复制)"
)
self.status_label.setStyleSheet("color: #4CAF50;")
self.save_btn.setEnabled(total_valid > 0)
self.clear_btn.setEnabled(total_valid > 0)
self.update_drag_label_count() # 更新拖拽提示区的累计数量
except Exception as e:
QMessageBox.warning(self, "显示异常", f"结果追加失败:{str(e)}")
def clear_results(self):
"""清空所有结果(表格+全局列表)"""
reply = QMessageBox.question(
self, "确认清空", "是否清空所有识别结果?(不可恢复)",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
self.global_mutex.lock()
self.global_result_list.clear()
self.global_mutex.unlock()
self.result_table.setRowCount(0)
self.status_label.setText("已清空所有结果,等待拖拽图片...(右键可复制)")
self.status_label.setStyleSheet("color: #666666;")
self.save_btn.setEnabled(False)
self.clear_btn.setEnabled(False)
self.update_drag_label_count() # 重置累计数量显示
def update_drag_label_count(self):
"""更新拖拽提示区的累计结果数量"""
total_valid = len(self.global_result_list)
self.drag_label.setText(f"""
<div style="text-align: center; color: #666; font-size: 11pt;">
<p>📌 支持多次拖拽图片,结果自动合并(按身份证号去重)</p>
<p>✅ 支持格式:JPG、PNG、BMP、GIF、TIFF、WebP</p>
<p>📏 支持大小:1KB ~ 10MB(避免超大文件闪退)</p>
<p>🔍 识别字段:姓名、身份证号、性别、民族、出生年月、住址</p>
<p>📋 操作支持:右键复制选中单元格/行/所有数据(适配Excel粘贴)</p>
<p>📊 累计识别:<span style="color: #2196F3;">{total_valid}</span> 条有效信息</p>
</div>
""")
# ===================== 识别相关 =====================
def cancel_recognition(self):
self.global_mutex.lock()
if self.recognize_thread and self.recognize_thread.isRunning():
reply = QMessageBox.question(self, "确认取消", "是否取消当前识别任务?", QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.recognize_thread.stop()
self.status_label.setText("正在取消识别...")
self.cancel_btn.setEnabled(False)
self.global_mutex.unlock()
def update_progress_status(self, progress_value, status_text):
try:
self.progress_bar.setValue(progress_value)
self.status_label.setText(status_text)
if "成功:" in status_text:
self.status_label.setStyleSheet("color: #4CAF50;")
elif "跳过:" in status_text and "背面" in status_text:
self.status_label.setStyleSheet("color: #9E9E9E;")
elif "跳过:" in status_text:
self.status_label.setStyleSheet("color: #FF9800;")
elif "警告:" in status_text:
self.status_label.setStyleSheet("color: #F44336;")
else:
self.status_label.setStyleSheet("color: #666666;")
except Exception:
pass
def show_error_message(self, error_text):
try:
QMessageBox.critical(self, "操作错误", error_text)
self.status_label.setText(f"错误:{error_text[:50]}...(右键可复制已有结果)")
self.status_label.setStyleSheet("color: #F44336;")
except Exception:
pass
def thread_finish_callback(self):
self.global_mutex.lock()
self.recognize_thread = None
self.cancel_btn.setEnabled(False)
# 确保导出和清空按钮状态正确
total_valid = len(self.global_result_list)
self.save_btn.setEnabled(total_valid > 0)
self.clear_btn.setEnabled(total_valid > 0)
self.global_mutex.unlock()
# ===================== Excel导出 =====================
def export_to_excel(self):
self.global_mutex.lock()
if not self.global_result_list:
self.global_mutex.unlock()
QMessageBox.warning(self, "提示", "暂无有效数据可导出!")
return
# 复制一份结果,避免导出过程中数据被修改
export_data = self.global_result_list.copy()
self.global_mutex.unlock()
save_path, _ = QFileDialog.getSaveFileName(
self, "保存Excel文件",
os.path.join(os.getcwd(), "身份证核心字段合并结果.xlsx"),
"Excel文件 (*.xlsx);;所有文件 (*.*)"
)
if not save_path:
return
if not save_path.endswith(".xlsx"):
save_path += ".xlsx"
try:
# 验证写入权限
with open(save_path, 'w') as f:
pass
os.remove(save_path)
workbook = openpyxl.Workbook()
worksheet = workbook.active
worksheet.title = "身份证核心字段合并结果"
header_font = openpyxl.styles.Font(bold=True, color="FFFFFF")
header_fill = openpyxl.styles.PatternFill(start_color="2196F3", end_color="2196F3", fill_type="solid")
center_alignment = openpyxl.styles.Alignment(horizontal='center', vertical='center')
# 写入表头
for col_idx, header_text in enumerate(self.table_columns, 1):
cell = worksheet.cell(row=1, column=col_idx, value=header_text)
cell.font = header_font
cell.fill = header_fill
cell.alignment = center_alignment
# 写入所有合并后的结果
for row_idx, info in enumerate(export_data, 2):
worksheet.cell(row=row_idx, column=1, value=info['姓名']).alignment = center_alignment
worksheet.cell(row=row_idx, column=2, value=info['身份证号码']).alignment = center_alignment
worksheet.cell(row=row_idx, column=3, value=info['性别']).alignment = center_alignment
worksheet.cell(row=row_idx, column=4, value=info['民族']).alignment = center_alignment
worksheet.cell(row=row_idx, column=5, value=info['出生年月']).alignment = center_alignment
addr_cell = worksheet.cell(row=row_idx, column=6, value=info['住址'])
addr_cell.alignment = openpyxl.styles.Alignment(horizontal='left', vertical='center', wrap_text=True)
# 调整列宽
column_widths = [12, 20, 8, 8, 15, 45]
for col_idx, width in enumerate(column_widths, 1):
worksheet.column_dimensions[openpyxl.utils.get_column_letter(col_idx)].width = width
# 调整行高
worksheet.row_dimensions[1].height = 25
for row_idx in range(2, len(export_data) + 2):
worksheet.row_dimensions[row_idx].height = 35
workbook.save(save_path)
QMessageBox.information(self, "导出成功", f"合并结果已导出到:\n{save_path}\n共{len(export_data)}条有效信息")
if QMessageBox.question(self, "打开文件", "是否立即打开Excel文件?", QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
if sys.platform.startswith('win'):
os.startfile(save_path)
elif sys.platform.startswith('darwin'):
os.system(f'open "{save_path}"')
else:
os.system(f'xdg-open "{save_path}"')
except PermissionError:
QMessageBox.critical(self, "保存失败", "文件被占用或无写入权限!")
except Exception as e:
QMessageBox.critical(self, "导出失败", f"Excel导出错误:{str(e)}")
# ===================== 窗口关闭 =====================
def closeEvent(self, event):
self.global_mutex.lock()
if self.recognize_thread and self.recognize_thread.isRunning():
self.recognize_thread.stop()
self.recognize_thread.wait(1000)
self.global_mutex.unlock()
event.accept()
if __name__ == '__main__':
# 高DPI适配
if hasattr(Qt, 'AA_EnableHighDpiScaling'):
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
app = QApplication(sys.argv)
app.setStyle("Fusion")
main_window = IDCardDragDropRecognizer()
main_window.show()
sys.exit(app.exec_())