first commit

This commit is contained in:
2026-07-18 15:04:19 +08:00
commit 7fdcab3405
26 changed files with 2749 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
build/*
dist/*
outputs/*
+29
View File
@@ -0,0 +1,29 @@
# cwExcel - Python Desktop Application
一个基于 Python Tkinter 的桌面应用程序。
## 环境要求
- Python 3.8+
- TkinterPython 内置)
## 安装依赖
```bash
pip install -r requirements.txt
```
## 运行
```bash
python main.py
```
## 打包
```bash
pip install pyinstaller
pyinstaller --noconfirm cwExcel.spec
```
输出在 `dist/cwExcel/cwExcel.exe`
+6
View File
@@ -0,0 +1,6 @@
"""
app 包 - 应用程序核心模块
"""
from app.application import MainApplication
__all__ = ["MainApplication"]
Binary file not shown.
Binary file not shown.
Binary file not shown.
+22
View File
@@ -0,0 +1,22 @@
"""
主应用程序类
"""
import tkinter as tk
from app.ui import MainWindow
class MainApplication:
"""应用程序主控制器"""
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("cwExcel")
self.root.geometry("1024x680")
self.root.minsize(800, 500)
# 构建主窗口
self.main_window = MainWindow(root)
def run(self):
"""启动应用程序主循环"""
self.root.mainloop()
+633
View File
@@ -0,0 +1,633 @@
# spacy_training/scripts/predict.py
import re
import sys
import cn2an
import os
import os.path
from tqdm import tqdm
from pathlib import Path
# PyInstaller --noconsole 兼容:无控制台时重定向输出到空设备
if sys.stdout is None:
sys.stdout = open(os.devnull, 'w')
if sys.stderr is None:
sys.stderr = open(os.devnull, 'w')
# 单位与连接符定义
MAX_ITEM_OFFSET = 10
UNIT_PATTERN = "个|张|片|块|条|幅|根"
SIZE_UNITS_PATTERN = r'mm|cm|dm|m'
SIZE_JOIN_PATTERN = "xX×"
NUMS_PATTERN = r'\d+(?:\.\d+)' # 修复:原缺少 ? 导致整数不匹配
CNT_PATTERN = rf'{NUMS_PATTERN}?[^克+\s\d-]*'
EXCEPT_TEXT = "解析失败"
INDEX_COL = '序号'
base_colume = ['解析备注', '尺寸备注', '描述', '异常信息']
mapping_size = {
"width_mm": "",
"height_mm": "",
# "original_size": "原始尺寸",
# "total_quantity": "总数量",
"style_count": "款数",
"quantity_per_style": "数量",
"unit": "单位",
"描述": "描述",
"解析备注": "解析备注",
"size_unit": "尺寸单位",
"exception_msg": '异常信息'
}
import spacy
import pandas as pd
# 将 doc 转换为字典
def doc_to_dict(doc):
return {
"text": doc.text,
"entities": [
{
"text": ent.text,
"label": ent.label_,
"start": ent.start_char,
"end": ent.end_char
}
for ent in doc.ents
],
}
def predict_file(df, model_path="./spacy_training/model", key='备注'):
print(model_path)
nlp = spacy.load(model_path)
if "sentencizer" not in nlp.pipe_names:
nlp.add_pipe("sentencizer")
# 统一表头空格
df.columns = df.columns.str.strip()
key = key.strip()
# 流式批处理:用 as_tuples 边读边处理,batch_size=32 兼顾速度与内存
stream = ((str(row[key]).strip(), {k: v for k, v in row.items()}) for _, row in df.iterrows())
for doc, base_row in tqdm(nlp.pipe(stream, as_tuples=True, batch_size=32), total=len(df)):
yield doc.text, base_row, doc_to_dict(doc)
from dataclasses import dataclass
@dataclass
class StructuredOrder:
key: str
width_mm: float
height_mm: float
original_size: str
style_count: int
quantity_per_style: float
unit: str
total_quantity: float
original_qty: str
size_position: list
qty_position: list
exception_msg: str
calc_type: str
size_unit: str
meta: dict
def expand_to_structured(raw_pairs, size_unit=''):
unit_map = {
'': 100, '': 1000, '': 10000,
'百万': 1000000, '千万': 10000000, '亿': 100000000,
}
UNIT_REGEX = re.compile(rf'({UNIT_PATTERN})$')
exception_msg = []
def extract_multiplier(text):
"""
从字符串中提取数值乘数,支持:
- 阿拉伯数字 + 中文单位:3.5万 → 35000
- 纯中文数字 + 单位:三万五千 → 35000
- 纯中文:两千万 → 20000000
"""
# 去除末尾单位(保留前面的部分)
clean = UNIT_REGEX.sub('', text.strip())
# 如果去单位后为空,原字符串可能是纯单位(如“万”),默认系数为1
if not clean:
matched_unit = UNIT_REGEX.search(text)
if matched_unit:
unit = matched_unit.group(1)
return unit_map.get(unit, 1.0)
return 1.0
# 尝试直接转阿拉伯数字
try:
return float(clean)
except ValueError:
pass
# 尝试转中文数字(如“三万五千”虽然不合理,但“三万”可以)
try:
# 注意:cn2an 可以直接处理“三万五千”这种
num = cn2an.cn2an(clean, "smart") # smart 模式支持混合写法
if isinstance(num, (int, float)):
# 检查原字符串是否有单位后缀(比如“三万”中的“万”已被去除,需补回)
matched_unit = UNIT_REGEX.search(text)
if matched_unit:
unit = matched_unit.group(1)
num *= unit_map[unit]
return float(num)
except Exception:
exception_msg.append(f"尺寸解析异常:{text}")
pass
return 1.0
def parse_size(size_str):
join_str = ''.join(re.findall(f"[{SIZE_JOIN_PATTERN}]+", size_str))
if len(join_str) == 0 or re.search('比例|等高|等比|等宽', size_str):
result = f'异常值:{size_str}'
return result, result, size_unit
size_str = size_str.replace(join_str, "x")
match = re.match(rf'({NUMS_PATTERN}?)([a-z]+)?[{SIZE_JOIN_PATTERN}]({NUMS_PATTERN}?)([a-z]+)?', size_str, re.IGNORECASE)
if not match:
return None, None, size_unit
w_val, w_unit, h_val, h_unit = match.groups()
unit = (w_unit or h_unit or size_unit).lower()
conv = {'m': 1000, 'dm': 100, 'cm': 10, 'mm': 1}
if unit not in conv:
print(f'未知单位: {unit} size_str:{size_str} size_unit:{size_unit}')
exception_msg.append(f"尺寸解析异常:{size_str}, 未知单位: {unit}")
return None, None, unit
w_mm = conv[unit] * float(w_val)
h_mm = conv[unit] * float(h_val)
return round(w_mm, 3), round(h_mm, 3), unit
def parse_quantity(qty_str):
qty_str = "".join(qty_str.split())
multi_match = re.match((
rf'[共计]?(.+?)款\w*?([共计]?[\d.一二两三四五六七八九十百千万亿]+)({UNIT_PATTERN})'
), qty_str, re.IGNORECASE)
if multi_match:
style_part, qty_part = multi_match.group(1), multi_match.group(2)
if qty_part[0] in '共计':
qty_part = qty_part[1:]
calc_type = 'total'
else:
calc_type = 'single'
style_count = int(extract_multiplier(style_part))
quantity_per_style = int(extract_multiplier(qty_part))
unit = multi_match.group(3)
return {"style_count": style_count, "quantity_per_style": quantity_per_style, "unit": unit, "calc_type": calc_type}
single_match = re.match(rf'(.+?)({UNIT_PATTERN})', qty_str, re.IGNORECASE)
if single_match:
qty_part = single_match.group(1)
if qty_part[0] in '共计':
qty_part = qty_part[1:]
calc_type = 'total'
else:
calc_type = 'single'
quantity_per_style = int(extract_multiplier(qty_part))
unit = single_match.group(2)
return {"style_count": 1, "quantity_per_style": quantity_per_style, "unit": unit, "calc_type": calc_type}
return {"style_count": 0, "quantity_per_style": 0, "unit": ""}
structured = []
for size_info, qty_info in raw_pairs:
s_start, s_end, s_text, *_ = size_info
q_start, q_end, q_text, *_ = qty_info
if s_text == EXCEPT_TEXT:
width_mm, height_mm = EXCEPT_TEXT, EXCEPT_TEXT
else:
size_parsed = parse_size(s_text)
if size_parsed:
width_mm, height_mm, size_unit = size_parsed
else:
width_mm, height_mm, size_unit = None, None, None
if q_text == EXCEPT_TEXT:
style_count, quantity_per_style, unit = 0, EXCEPT_TEXT, EXCEPT_TEXT
else:
qty_parsed = parse_quantity(q_text)
style_count, quantity_per_style, unit = qty_parsed["style_count"], qty_parsed["quantity_per_style"], qty_parsed["unit"]
structured.append(StructuredOrder(
key=f"{s_start}{s_end}{s_text}",
width_mm=width_mm,
height_mm=height_mm,
original_size=s_text,
style_count=style_count,
quantity_per_style=quantity_per_style,
unit=unit,
total_quantity=style_count * quantity_per_style if style_count and style_count else None,
original_qty=q_text,
size_position=[s_start, s_end],
qty_position=[q_start, q_end],
exception_msg="; ".join(exception_msg),
calc_type=qty_parsed.get('calc_type', None),
size_unit=size_unit,
meta={
"size_text_except": s_text if s_text == EXCEPT_TEXT or (not width_mm and not height_mm) else None,
"qty_text_except": q_text if q_text == EXCEPT_TEXT else None,
}
))
return structured
def decode(doc_dict_list, is_horizontal=False):
ent_group = {
# "单号": ["单号"],
"刮刮膜尺寸": ["刮刮膜尺寸"],
# "产品": ["产品"],
# "材质": ["材质"],
# "工艺": ["工艺"],
# "用户": ["用户信息"],
# "加急": ["加急"],
"数量": ["尺寸", "数量"],
}
columns = list(ent_group.keys())
columns.remove('数量')
columns.extend(base_colume)
group_reflect = {v: k for k, vs in ent_group.items() for v in vs}
result_items = []
exception_list = []
for doc_dict in doc_dict_list:
sentence = doc_dict["text"]
entities = doc_dict["entities"]
is_except = False
# 强关联实体需要进行组合处理
# ======================================== 实体分组 ========================================
ent_group_dict = {k: [] for k in ent_group.keys()}
base_ent_group_dict = {}
for ent in entities:
start, end, label, text = ent["start"], ent["end"], ent["label"], ent["text"]
if label not in group_reflect:
continue
group_name = group_reflect[label]
ent_group_dict[group_name].append((start, end, text, label))
base_ent_group_dict[group_name] = text
result = []
# 解析出尺寸和数量的组合
# ======================================== 解析尺寸和数量 ========================================
group_list = []
size_and_qty_list = [[], []]
# 数量需要和尺寸进行组合处理
base_ent_group_dict.pop("数量", None)
for cnt_item in ent_group_dict["数量"]:
size_info, qty_info = size_and_qty_list
label = cnt_item[-1]
if label == '数量':
qty_info.append(cnt_item)
elif label == '尺寸':
start, end, text, _label = cnt_item
if f'-({text[:2]}' in sentence:
continue
if qty_info:
group_list.append(size_and_qty_list)
size_and_qty_list = [[], []]
size_info, qty_info = size_and_qty_list
size_info.append(cnt_item)
# 存在数量和尺寸
if size_and_qty_list[0] or size_and_qty_list[1]:
group_list.append(size_and_qty_list)
# 如果只有一对尺寸,有可能反过来描述
if len(group_list) == 2:
_1, qty_info = group_list[0]
size_info, _2 = group_list[1]
if not _1 and not _2:
group_list[:] = [[qty_info, size_info]]
# print(f'订单中尺寸和数量组合:{group_list}')
# 解析出尺寸和数量的组合
# ======================================== 解析长、宽、款数、数量 ========================================
size_unit = ''
for size_info, qty_info in group_list:
units = set(''.join(re.findall(SIZE_UNITS_PATTERN, ent[2])) for ent in size_info)
units = [unit for unit in units if unit]
if len(units) == 1:
size_unit = units[0]
size_and_qty_parsed_result = []
for size_info, qty_info in group_list:
size_len, qty_len = len(size_info), len(qty_info)
if size_len == 0:
print(f'解析异常,尺寸为空,数量为{qty_info},原文:{sentence}', entities)
is_except = True
# 起始位置、结束位置、尺寸文本、尺寸标签
qty_info = [(0, 0, EXCEPT_TEXT, '数量')]
if qty_len == 0:
print(f'解析异常,数量为空,尺寸为{size_info},原文:{sentence}', entities)
is_except = True
# 起始位置、结束位置、尺寸文本、尺寸标签
size_info = [(0, 0, EXCEPT_TEXT, '尺寸')]
# continue
# print('size_info, qty_info', size_info, qty_info)
if size_len == qty_len:
decode_desc = "单尺寸、单款式描述的订单" if size_len == 1 else "多尺寸、多款式描述一一匹配的订单"
for size, qty in zip(size_info, qty_info):
expand = expand_to_structured([(size, qty)], size_unit) or []
exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
for item in expand:
parsed_item = item.__dict__.copy()
parsed_item['描述'] = f"{size[2]}|{qty[2]}"
parsed_item['解析备注'] = decode_desc
parsed_item['size_len'] = size_len
parsed_item['qty_len'] = qty_len
size_and_qty_parsed_result.append(parsed_item)
elif size_len>1 and qty_len>1:
if size_len > qty_len:
total_qty = []
for size, qty in zip(size_info, qty_info):
expand = expand_to_structured([(size, qty)], size_unit) or []
for item in expand:
total_qty.append(item.style_count)
if size_len == sum(total_qty):
decode_desc = "多尺寸、多款式描述:多款式描述和尺寸相等的订单"
item = []
for idx, qty in enumerate(total_qty):
item.extend(qty_info[idx] for _ in range(qty))
print('解析:', size_info, qty_info, item)
# breakpoint()
qty_info = item
qty_len = len(qty_info)
else:
# qty_info = []
decode_desc = f"匹配异常:多尺寸、多款式描述的订单: {size_info} {qty_info}"
else:
# qty_info = []
decode_desc = f"匹配异常:多尺寸、多款式描述的订单: {size_info} {qty_info}"
for size, qty in zip(size_info, qty_info):
expand = expand_to_structured([(size, qty)], size_unit) or []
exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
for item in expand:
parsed_item = item.__dict__.copy()
parsed_item['style_count'] = 1
parsed_item['描述'] = f"{size[2]}|{qty[2]}"
parsed_item['解析备注'] = decode_desc
parsed_item['size_len'] = size_len
parsed_item['qty_len'] = qty_len
size_and_qty_parsed_result.append(parsed_item)
else:
# 多尺寸-单数量、单尺寸-多数量 告警多尺寸-多数量
iter_item = ((size, qty) for size in size_info for qty in qty_info)
if size_len > 1 and qty_len == 1:
decode_desc = "多尺寸、单款式描述的订单"
elif size_len == 1 and qty_len > 1:
decode_desc = "单尺寸、多款式描述的订单"
else:
decode_desc = f"异常:多尺寸、多款式描述的订单: {size_info} {qty_info}"
for item in iter_item:
size, qty = item
expand = expand_to_structured([(size, qty)], size_unit) or []
exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')]
if exceptions: print("解析异常", exceptions, '原文', sentence, entities)
for item in expand:
parsed_item = item.__dict__.copy()
parsed_item['描述'] = f"{size[2]}|{qty[2]}"
parsed_item['解析备注'] = decode_desc
parsed_item['size_len'] = size_len
parsed_item['qty_len'] = qty_len
size_and_qty_parsed_result.append(parsed_item)
result.extend(size_and_qty_parsed_result)
for item in result:
# print('item', item)
new_item = base_ent_group_dict.copy()
for key in mapping_size:
col_name = mapping_size[key]
new_item[col_name] = item[key]
columns.append(col_name)
size_len = item.pop('size_len', None)
qty_len = item.pop('qty_len', None)
calc_type = item.pop('calc_type', None)
if size_len > 1 and calc_type == 'total':
new_item[f'数量'] = f"警告:异常值(总计值:{new_item[f'数量']})"
if size_len == 1 or size_len == qty_len:
pass
# new_item[f'解析备注'] = '单尺寸的订单'
else:
if size_len == new_item['款数']:
new_item[f'款数'] = 1
# new_item[f'解析备注'] = '尺寸数量和款数相同的订单'
elif new_item['款数'] == 1:
new_item[f'款数'] = 1
# new_item[f'解析备注'] = '单款的订单'
else:
new_item[f'款数'] = "异常值:款数和尺寸数不一致"
new_item[f'数量'] = "异常值:款数和尺寸数不一致"
# new_item[f'解析备注'] = '多尺寸并且和款数不同的订单'
result_items.append(new_item)
# print(json.dumps(new_item, indent=4, ensure_ascii=False))
if not result:
if base_ent_group_dict:
base_ent_group_dict['是否异常'] = is_except
base_ent_group_dict[f'解析备注'] = '未解析到实际尺寸的订单1'
result_items.append(base_ent_group_dict.copy())
else:
result_items.append({
"是否异常": is_except,
"解析备注": "未解析到任何实体"
})
return exception_list, columns, result_items
def get_model_path():
"""获取模型路径,兼容开发环境和PyInstaller打包"""
if getattr(sys, '_MEIPASS', None):
# PyInstaller 打包后
return str(Path(sys._MEIPASS) / 'app' / 'resources' / 'models' / '订单尺寸识别' / 'best_model')
# 开发环境
return str(Path(__file__).parent / 'resources' / 'models' / '订单尺寸识别' / 'best_model')
def parse_finance_data(file_path, target_index, is_horizontal, sheet_name="Sheet1"):
df = pd.read_excel(file_path, dtype=str, sheet_name=sheet_name)
total_amount = len(df)
model_path = get_model_path()
print(f"使用模型路径: {model_path}")
items = predict_file(df, model_path, target_index)
print(f"解析完成,共{items}")
# items = predict_file(file_path, './resources/models/lintao/best_model', target_index)
exception_list, results, outputs_columns = [], [], []
origin_item = None
parsed_dict = {}
mapping_size_values = mapping_size.values()
for text, origin_item, item in items:
decode_result = []
base_info = {}
result_index = parsed_dict.get(f"{text}_index")
if result_index:
exceptions, columns = [], []
try:
decode_item = parsed_dict[text][result_index]
for key in origin_item:
if key in mapping_size_values:
continue
decode_item[key] = origin_item[key]
decode_item['尺寸备注'] = '多组尺寸解析'
parsed_dict[f"{text}_index"] += 1
continue
except IndexError:
print(f'解析到的尺寸不足:{text}{result_index} {parsed_dict[text]}')
base_item = parsed_dict[text][0]
for col in base_colume:
base_info[col] = base_item.get(col, '')
base_info[""] = '异常值:没有解析到那么多尺寸'
decode_items = []
parsed_dict[f"{text}_index"] += 1
else:
exceptions, columns, decode_items = decode([item], is_horizontal) or []
# if '(250731202428388557)' in text:
# print(result_index, text, exceptions, columns, decode_items)
# breakpoint()
if len(columns) > len(outputs_columns):
outputs_columns = columns
if len(decode_items) > 1:
for item in decode_items:
item['尺寸备注'] = '多组尺寸解析'
for idx, decode_item in enumerate(decode_items):
base_row = origin_item.copy()
if idx and INDEX_COL in base_row: base_row[INDEX_COL] = ''
# base_row = origin_item.copy() if idx == 0 else {}
base_row.update(decode_item)
is_except = base_row.pop('是否异常', False)
if is_except:
for col in base_colume:
origin_item[col] = base_row.get(col, '')
decode_result.append(origin_item)
exception_list.append(base_row)
else:
decode_result.append(base_row)
if not decode_items:
if base_info:
origin_item.update(base_info)
decode_result.append(origin_item)
else:
if is_horizontal:
new_decode_result = decode_result[0].copy()
for idx, item in enumerate(decode_result[1:], 1):
for key in mapping_size_values:
new_key = f"{key}{idx}"
new_decode_result[new_key] = item[key]
if new_key not in outputs_columns:
outputs_columns.append(new_key)
decode_result = [new_decode_result]
if text not in parsed_dict:
parsed_dict[text] = decode_result
parsed_dict[f"{text}_index"] = 0
parsed_dict[f"{text}_index"] += 1
# if decode_result:
# results.append(decode_result[0])
if len(decode_items) > 1:
for item in decode_result[1:]:
item['尺寸备注'] = '复制新增:多组尺寸解析'
results.extend(decode_result)
# break
print('横向解析', outputs_columns)
if origin_item:
# outputs_columns.
_outputs_columns = list(origin_item.keys()) + outputs_columns
col_set = set()
outputs_columns = []
for col in _outputs_columns:
if col not in col_set:
col_set.add(col)
outputs_columns.append(col)
df = pd.DataFrame(results, columns=outputs_columns)
file_name = os.path.basename(file_path).replace(".xlsx", '')
filename = file_name + '_解析' + ('_横向排列' if is_horizontal else '_纵向排列')
upload_dir = Path(file_path).parent
upload_dir.mkdir(parents=True, exist_ok=True)
save_file_path = upload_dir / f'{filename}_1.xlsx'
# 使用 ExcelWriter 同时写入多个 sheet
with pd.ExcelWriter(save_file_path, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='正常解析', index=False)
if exception_list:
except_df = pd.DataFrame(exception_list, columns=outputs_columns)
except_df.to_excel(writer, sheet_name='异常解析', index=False)
print(f'解析结果保存在:{save_file_path}')
return save_file_path, total_amount
if __name__ == "__main__":
# print(expand_to_structured([[(0,1,'600x50cm','r'), (0,1,'3款各1张','e')]]))
# exit()
# predict("Apple is opening a new office in Tokyo.")
# predict("Google hired Sarah Connor from Berlin last year.")
file_info = ['d:/会计组/新领图8月.xlsx', '备注']
# file_info = ['d:/会计组/即客2025年8月尺寸整理.xlsx', '系统文件名']
file_info = ['d:/会计组/8月订单明细9.6.xlsx', '文件名']
file_info = ['d:/会计组/ZHX-8月订单明细.xlsx', '文件名']
file_info = ['d:/会计组/国税数据源/智韬2025年8月尺寸整理(1).xlsx', '系统文件名']
file_info = ['d:/会计组/彩印通8月数码9.23.xlsx', 'ERP系统文件名']
file_info = ['d:/会计组/CYT8月明细9.24.xlsx', 'ERP系统文件名']
file_info = ['d:/会计组/9.1-9.26.xlsx', '备注']
file_info = ['d:/会计组/JD.xlsx', '文件名']
file_info = ['d:/会计组/泰州即客2025年9月尺寸整理.xlsx', '系统文件名']
file_info = ['d:/会计组/ZHX需拆明细9月.xlsx', '文件名', False]
file_info = ['d:/会计组/七彩2024年9月账单尺寸整理.xlsx', '系统文件名', True]
file_info = ['d:/会计组/艾印图文2024年9月账单尺寸整理.xlsx', '系统文件名', True]
file_info = ['d:/会计组/9月转印.xlsx', 'erp', True]
file_info = ['d:/会计组/9月CYT.xlsx', '文件名', False]
file_info = ['d:/会计组/智韬2025年9月尺寸整理.xlsx', '系统文件名', True]
file_info = ['d:/会计组/9月UV转印贴.xlsx', '文件名', True]
file_info = ['d:/会计组/彩印通2025年9月(数码).xlsx', '文件名', True]
file_info = ['d:/会计组/9月名片.xlsx', '文件名', True]
file_info = ['d:/会计组/9月不干胶.xlsx', '文件名', True]
file_path, target_index, is_horizontal = file_info
parse_finance_data(file_path, target_index, is_horizontal)
@@ -0,0 +1,135 @@
[paths]
train = null
dev = null
vectors = null
init_tok2vec = null
[system]
seed = 0
gpu_allocator = null
[nlp]
lang = "zh"
pipeline = ["ner"]
disabled = []
before_creation = null
after_creation = null
after_pipeline_creation = null
batch_size = 1000
vectors = {"@vectors":"spacy.Vectors.v1"}
[nlp.tokenizer]
@tokenizers = "spacy.zh.ChineseTokenizer"
segmenter = "char"
[components]
[components.ner]
factory = "ner"
incorrect_spans_key = null
moves = null
scorer = {"@scorers":"spacy.ner_scorer.v1"}
update_with_oracle_cut_size = 100
[components.ner.model]
@architectures = "spacy.TransitionBasedParser.v2"
state_type = "ner"
extra_state_tokens = false
hidden_width = 64
maxout_pieces = 2
use_upper = true
nO = null
[components.ner.model.tok2vec]
@architectures = "spacy.HashEmbedCNN.v2"
pretrained_vectors = null
width = 96
depth = 4
embed_size = 2000
window_size = 1
maxout_pieces = 3
subword_features = true
[corpora]
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[training]
seed = ${system.seed}
gpu_allocator = ${system.gpu_allocator}
dropout = 0.1
accumulate_gradient = 1
patience = 1600
max_epochs = 0
max_steps = 20000
eval_frequency = 200
frozen_components = []
annotating_components = []
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
before_to_disk = null
before_update = null
[training.batcher]
@batchers = "spacy.batch_by_words.v1"
discard_oversize = false
tolerance = 0.2
get_length = null
[training.batcher.size]
@schedules = "compounding.v1"
start = 100
stop = 1000
compound = 1.001
t = 0.0
[training.logger]
@loggers = "spacy.ConsoleLogger.v1"
progress_bar = false
[training.optimizer]
@optimizers = "Adam.v1"
beta1 = 0.9
beta2 = 0.999
L2_is_weight_decay = true
L2 = 0.01
grad_clip = 1.0
use_averages = false
eps = 0.00000001
learn_rate = 0.001
[training.score_weights]
ents_f = 1.0
ents_p = 0.0
ents_r = 0.0
ents_per_type = null
[pretraining]
[initialize]
vectors = ${paths.vectors}
init_tok2vec = ${paths.init_tok2vec}
vocab_data = null
lookups = null
before_init = null
after_init = null
[initialize.components]
[initialize.tokenizer]
pkuseg_model = null
pkuseg_user_dict = "default"
@@ -0,0 +1,41 @@
{
"lang":"zh",
"name":"pipeline",
"version":"0.0.0",
"spacy_version":">=3.8.7,<3.9.0",
"description":"",
"author":"",
"email":"",
"url":"",
"license":"",
"spacy_git_version":"4b65aa7",
"vectors":{
"width":0,
"vectors":0,
"keys":0,
"name":null,
"mode":"default"
},
"labels":{
"ner":[
"\u4ea7\u54c1",
"\u522e\u522e\u819c\u5c3a\u5bf8",
"\u52a0\u6025",
"\u5355\u53f7",
"\u5c3a\u5bf8",
"\u5de5\u827a",
"\u6570\u91cf",
"\u6750\u8d28",
"\u7528\u6237\u4fe1\u606f"
]
},
"pipeline":[
"ner"
],
"components":[
"ner"
],
"disabled":[
]
}
@@ -0,0 +1,13 @@
{
"moves":null,
"update_with_oracle_cut_size":100,
"multitasks":[
],
"min_action_freq":1,
"learn_tokens":false,
"beam_width":1,
"beam_density":0.0,
"beam_update_prob":0.0,
"incorrect_spans_key":null
}
@@ -0,0 +1 @@
‚¥movesÚ0{"0":{},"1":{"\u522e\u522e\u819c\u5c3a\u5bf8":-1,"\u6570\u91cf":-2,"\u5c3a\u5bf8":-3,"\u5355\u53f7":-4,"\u4ea7\u54c1":-5,"\u6750\u8d28":-6,"\u5de5\u827a":-7,"\u52a0\u6025":-8,"\u7528\u6237\u4fe1\u606f":-9},"2":{"\u522e\u522e\u819c\u5c3a\u5bf8":-1,"\u6570\u91cf":-2,"\u5c3a\u5bf8":-3,"\u5355\u53f7":-4,"\u4ea7\u54c1":-5,"\u6750\u8d28":-6,"\u5de5\u827a":-7,"\u52a0\u6025":-8,"\u7528\u6237\u4fe1\u606f":-9},"3":{"\u522e\u522e\u819c\u5c3a\u5bf8":-1,"\u6570\u91cf":-2,"\u5c3a\u5bf8":-3,"\u5355\u53f7":-4,"\u4ea7\u54c1":-5,"\u6750\u8d28":-6,"\u5de5\u827a":-7,"\u52a0\u6025":-8,"\u7528\u6237\u4fe1\u606f":-9},"4":{"":1,"\u522e\u522e\u819c\u5c3a\u5bf8":-1,"\u6570\u91cf":-2,"\u5c3a\u5bf8":-3,"\u5355\u53f7":-4,"\u4ea7\u54c1":-5,"\u6750\u8d28":-6,"\u5de5\u827a":-7,"\u52a0\u6025":-8,"\u7528\u6237\u4fe1\u606f":-9},"5":{"":1}}£cfg§neg_keyÀ
@@ -0,0 +1,3 @@
{
"segmenter":"char"
}
@@ -0,0 +1 @@
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
{
"mode":"default"
}
+6
View File
@@ -0,0 +1,6 @@
"""
ui 包 - 用户界面组件
"""
from app.ui.main_window import MainWindow
__all__ = ["MainWindow"]
Binary file not shown.
Binary file not shown.
+676
View File
@@ -0,0 +1,676 @@
"""
主窗口 UI 组件
"""
import os
import sys
import threading
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import openpyxl
# ---- 配色 ----
C = {
"bg": "#f0f2f5",
"white": "#ffffff",
"text": "#1a1a2e",
"text_muted": "#6b7280",
"accent": "#4f46e5",
"accent_hover": "#4338ca",
"accent_light": "#eef2ff",
"border": "#e5e7eb",
"success": "#059669",
}
# 列宽
COL_W = [220, 1, 220, 1, 220, 1, 200]
COL_X = [0]
for w in COL_W[:-1]:
COL_X.append(COL_X[-1] + w)
ROW_H = 44
CELL_PAD = 8
# ======================================================================
# 自定义下拉组件
# ======================================================================
class Dropdown(tk.Frame):
"""纯自定义下拉选择器 — 不用任何系统 Combobox"""
def __init__(self, parent, values=None, default="", on_select=None,
font=("Microsoft YaHei", 10), width=180, **kw):
super().__init__(parent, bg=C["white"], highlightbackground=C["border"],
highlightthickness=1, cursor="hand2", **kw)
self.values = values or []
self._selected = default or ""
self._on_select = on_select
self._font = font
self._popup = None
self._hover = False
# 文字标签
self._label = tk.Label(
self, text=self._selected or "请选择",
font=font, bg=C["white"], fg=C["text"] if self._selected else C["text_muted"],
anchor=tk.W, padx=8,
)
self._label.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 箭头
self._arrow = tk.Label(
self, text="", font=("Microsoft YaHei", 8),
bg=C["white"], fg="#9ca3af", padx=6,
)
self._arrow.pack(side=tk.RIGHT, fill=tk.Y)
# 事件绑定
for w in (self, self._label, self._arrow):
w.bind("<Button-1>", self._toggle)
w.bind("<Enter>", self._on_enter)
w.bind("<Leave>", self._on_leave)
def _on_enter(self, _):
self._hover = True
self.configure(highlightbackground=C["accent"])
self._label.configure(bg="#f9fafb")
self._arrow.configure(bg="#f9fafb", fg=C["accent"])
def _on_leave(self, _):
self._hover = False
if not self._popup:
self.configure(highlightbackground=C["border"])
self._label.configure(bg=C["white"])
self._arrow.configure(bg=C["white"], fg="#9ca3af")
def _toggle(self, event=None):
if self._popup:
self._close()
else:
self._open()
def _open(self):
self.configure(highlightbackground=C["accent"])
self._label.configure(bg="#f9fafb")
self._arrow.configure(bg="#f9fafb", fg=C["accent"])
self._popup = tk.Toplevel(self)
self._popup.overrideredirect(True)
self._popup.configure(bg=C["white"])
# 计算位置
x = self.winfo_rootx()
y = self.winfo_rooty() + self.winfo_height() + 1
w = self.winfo_width()
item_h = 32
max_h = min(len(self.values) * item_h, 240)
list_h = max(item_h, max_h)
self._popup.geometry(f"{w}x{list_h}+{x}+{y}")
# 外边框
border = tk.Frame(self._popup, bg=C["border"])
border.pack(fill=tk.BOTH, expand=True)
# 列表
self._listbox = tk.Listbox(
border, font=self._font,
bg=C["white"], fg=C["text"],
selectbackground=C["accent_light"],
selectforeground=C["accent"],
selectborderwidth=0,
activestyle="none",
highlightthickness=0,
borderwidth=0,
relief=tk.FLAT,
height=len(self.values),
)
self._listbox.pack(fill=tk.BOTH, expand=True, padx=1, pady=1)
for i, v in enumerate(self.values):
self._listbox.insert(tk.END, f" {v}")
if v == self._selected:
self._listbox.selection_set(i)
self._listbox.see(i)
self._listbox.bind("<ButtonRelease-1>", self._on_pick)
self._listbox.bind("<Return>", self._on_pick)
self._listbox.bind("<Escape>", lambda e: self._close())
self._listbox.focus_set()
# 点击外部关闭
self._popup.bind("<FocusOut>", lambda e: self.after(100, self._check_focus))
# 绑定全局点击关闭(鼠标点击 popup 外)
self._grab_bind = self.winfo_toplevel().bind(
"<Button-1>", lambda e: self._on_global_click(e), add="+"
)
def _on_global_click(self, event):
if self._popup:
px, py = event.x_root, event.y_root
px2 = self._popup.winfo_rootx()
py2 = self._popup.winfo_rooty()
pw = self._popup.winfo_width()
ph = self._popup.winfo_height()
if not (px2 <= px <= px2 + pw and py2 <= py <= py2 + ph):
self._close()
def _check_focus(self):
if self._popup and not self._popup.focus_displayof():
self._close()
def _close(self, *_):
if self._popup:
self._popup.destroy()
self._popup = None
self._listbox = None
self.configure(highlightbackground=C["border"])
self._label.configure(bg=C["white"])
self._arrow.configure(bg=C["white"], fg="#9ca3af")
# 移除全局绑定
try:
self.winfo_toplevel().unbind("<Button-1>", self._grab_bind)
except Exception:
pass
def _on_pick(self, event):
sel = self._listbox.curselection()
if sel:
idx = sel[0]
self._selected = self.values[idx]
self._label.configure(text=self._selected, fg=C["text"])
if self._on_select:
self._on_select(self._selected)
self._close()
# ── 公开 API ──
def get(self):
return self._selected
def set_values(self, values, default=None):
self.values = values
if default is not None:
self._selected = default
self._label.configure(text=default, fg=C["text"] if default else C["text_muted"])
# 如果已打开则重建
if self._popup:
self._close()
self._open()
# ======================================================================
# 主窗口
# ======================================================================
class MainWindow:
"""应用程序主窗口"""
def __init__(self, root: tk.Tk):
self.root = root
self.root.configure(bg=C["bg"])
self._files_data = []
self._build_ui()
def _build_ui(self):
# ── 选择卡片 ──
card = tk.Frame(self.root, bg=C["white"])
card.pack(fill=tk.X, padx=24, pady=(20, 0))
card_border = tk.Frame(card, bg=C["white"],
highlightbackground=C["border"],
highlightthickness=1)
card_border.pack(fill=tk.X)
tk.Label(
card_border, text="📂 选择 Excel 文件",
font=("Microsoft YaHei", 12, "bold"),
bg=C["white"], fg=C["text"], anchor=tk.W,
).pack(fill=tk.X, padx=20, pady=(16, 10))
input_row = tk.Frame(card_border, bg=C["white"])
input_row.pack(fill=tk.X, padx=20, pady=(0, 10))
entry_border = tk.Frame(
input_row, bg=C["white"],
highlightbackground=C["border"], highlightthickness=1,
)
entry_border.pack(side=tk.LEFT, fill=tk.X, expand=True)
self._path_entry = tk.Entry(
entry_border,
font=("Microsoft YaHei", 10),
bg="#f9fafb", fg=C["text_muted"],
relief=tk.FLAT, borderwidth=0,
readonlybackground="#f9fafb",
)
self._path_entry.pack(fill=tk.X, padx=10, pady=8)
self._path_entry.insert(0, "请选择 Excel 文件(.xlsx / .xls,可多选)")
self._path_entry.configure(state="readonly")
self._browse_btn = tk.Button(
input_row, text="浏览...",
font=("Microsoft YaHei", 10, "bold"),
bg=C["accent"], fg="white", relief=tk.FLAT,
activebackground=C["accent_hover"], activeforeground="white",
cursor="hand2", padx=20, pady=7, borderwidth=0,
command=self._on_select_files,
)
self._browse_btn.pack(side=tk.LEFT, padx=(8, 0))
status_bar = tk.Frame(card_border, bg="#f9fafb")
status_bar.pack(fill=tk.X)
self._status_label = tk.Label(
status_bar, text=" 未选择任何文件",
font=("Microsoft YaHei", 9),
bg="#f9fafb", fg=C["text_muted"], anchor=tk.W,
)
self._status_label.pack(side=tk.LEFT, padx=16, pady=(6, 6))
# ── 表格区域 ──
self._table_area = tk.Frame(self.root, bg=C["bg"])
self._table_area.pack(fill=tk.BOTH, expand=True, padx=24, pady=(12, 0))
# ── 日志面板(底部固定高度,内部独立滚动) ──
self._log_frame = tk.Frame(self.root, bg="white", height=130)
self._log_frame.pack(fill=tk.X, padx=24, pady=(8, 8))
self._log_frame.pack_propagate(False)
log_header = tk.Frame(self._log_frame, bg="white")
log_header.pack(fill=tk.X)
tk.Label(log_header, text="📋 处理日志",
font=("Microsoft YaHei", 9, "bold"),
bg="white", fg=C["text"], anchor=tk.W,
).pack(side=tk.LEFT, padx=10, pady=(6, 2))
log_text_frame = tk.Frame(self._log_frame, bg="white")
log_text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 8))
self._log_text = tk.Text(
log_text_frame,
font=("Consolas", 9),
bg="#f9fafb", fg=C["text"],
wrap=tk.WORD, state=tk.DISABLED,
relief=tk.FLAT, borderwidth=0,
height=6,
)
log_scrollbar = tk.Scrollbar(log_text_frame, orient=tk.VERTICAL,
command=self._log_text.yview)
self._log_text.configure(yscrollcommand=log_scrollbar.set)
log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self._log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self._show_empty()
# ==================================================================
# 空状态
# ==================================================================
def _show_empty(self):
for w in self._table_area.winfo_children():
w.destroy()
drop = tk.Frame(self._table_area, bg="#fafafe",
highlightbackground="#c7d2fe",
highlightthickness=2)
drop.pack(fill=tk.BOTH, expand=True)
inner = tk.Frame(drop, bg="#fafafe")
inner.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
tk.Label(inner, text="📂", font=("Segoe UI Emoji", 40),
bg="#fafafe", fg="#c7d2fe").pack(pady=(0, 4))
tk.Label(inner, text="点击上方「浏览...」按钮选择文件",
font=("Microsoft YaHei", 13, "bold"),
bg="#fafafe", fg=C["text_muted"]).pack()
tk.Label(inner, text="支持 .xlsx、.xls 格式,可同时选择多个文件",
font=("Microsoft YaHei", 9),
bg="#fafafe", fg="#c0c4cc").pack(pady=(2, 0))
# ==================================================================
# 读取 xlsx 信息(用内置 zipfile,避免 openpyxl 打包问题)
# ==================================================================
def _read_xlsx_info(self, filepath):
import pandas as pd
sheet_names = []
columns = []
for engine in ["openpyxl", "xlrd"]:
try:
xls = pd.ExcelFile(filepath, engine=engine)
sheet_names = xls.sheet_names
if sheet_names:
df = pd.read_excel(xls, sheet_name=sheet_names[0], nrows=1)
columns = [str(c) for c in df.columns.tolist()]
xls.close()
break
except Exception:
continue
return sheet_names, columns
# ==================================================================
# 选择文件
# ==================================================================
def _on_select_files(self):
paths = filedialog.askopenfilenames(
title="选择 Excel 文件",
filetypes=[
("Excel 文件", "*.xlsx *.xls"),
("Excel 2007+", "*.xlsx"),
("Excel 97-2003", "*.xls"),
],
)
if not paths:
return
for p in paths:
if any(d["path"] == p for d in self._files_data):
continue
try:
sheet_names, columns = self._read_xlsx_info(p)
first_sheet = sheet_names[0] if sheet_names else None
self._files_data.append({
"path": p,
"filename": os.path.basename(p),
"sheet_names": sheet_names,
"columns": columns,
"selected_sheet": first_sheet,
"selected_col": columns[0] if columns else None,
"parse_mode": "横向排列",
})
except Exception as e:
messagebox.showerror("读取失败", f"无法读取文件:\n{os.path.basename(p)}\n\n{e}")
continue
n = len(self._files_data)
if n == 0:
self._path_entry.configure(state="normal")
self._path_entry.delete(0, tk.END)
self._path_entry.insert(0, "请选择 Excel 文件(.xlsx / .xls,可多选)")
self._path_entry.configure(state="readonly", fg=C["text_muted"])
self._status_label.configure(text=" 未选择任何文件")
elif n == 1:
self._path_entry.configure(state="normal")
self._path_entry.delete(0, tk.END)
self._path_entry.insert(0, self._files_data[0]["path"])
self._path_entry.configure(state="readonly", fg=C["text"])
self._status_label.configure(
text=f" 已选择 1 个文件 | 点击「浏览...」可继续添加",
fg=C["success"],
)
else:
self._path_entry.configure(state="normal")
self._path_entry.delete(0, tk.END)
self._path_entry.insert(0, f"已选择 {n} 个文件")
self._path_entry.configure(state="readonly", fg=C["text"])
self._status_label.configure(
text=f" 已选择 {n} 个文件 | 点击「浏览...」可继续添加",
fg=C["success"],
)
if self._files_data:
self._build_table()
else:
self._show_empty()
# ==================================================================
# 构建表格
# ==================================================================
def _build_table(self):
for w in self._table_area.winfo_children():
w.destroy()
self._table_frame = tk.Frame(self._table_area, bg=C["bg"])
self._table_frame.pack(fill=tk.BOTH, expand=True)
self._draw_header()
for fi, fd in enumerate(self._files_data):
self._draw_row(fi, fd)
# ── 确认按钮 ──
btn_bar = tk.Frame(self._table_frame, bg=C["bg"])
btn_bar.pack(fill=tk.X, pady=(16, 0))
self._confirm_btn = tk.Button(
btn_bar, text="✅ 开始解析",
font=("Microsoft YaHei", 12, "bold"),
bg=C["accent"], fg="white", relief=tk.FLAT,
activebackground=C["accent_hover"], activeforeground="white",
cursor="hand2", padx=28, pady=10, borderwidth=0,
command=self._on_confirm,
)
self._confirm_btn.pack(side=tk.RIGHT)
self._progress_label = tk.Label(
btn_bar, text="",
font=("Microsoft YaHei", 9),
bg=C["bg"], fg=C["text_muted"], anchor=tk.E,
)
self._progress_label.pack(side=tk.RIGHT, padx=(0, 12))
def _draw_header(self):
hdr = tk.Frame(self._table_frame, bg=C["white"], height=38)
hdr.pack(fill=tk.X)
hdr.pack_propagate(False)
headers = ["文件名", "工作表", "解析列", "解析方式"]
for i, title in enumerate(headers):
col_idx = i * 2
x, w = COL_X[col_idx], COL_W[col_idx]
tk.Label(
hdr, text=title, font=("Microsoft YaHei", 10, "bold"),
bg=C["white"], fg=C["text"], anchor=tk.W,
).place(x=x + CELL_PAD, y=0, width=w - CELL_PAD * 2, height=38)
if i < 3:
tk.Frame(hdr, bg=C["border"], width=1).place(
x=COL_X[col_idx + 1], y=6, height=26
)
tk.Frame(self._table_frame, bg=C["border"], height=1).pack(fill=tk.X)
def _draw_row(self, fi: int, fd: dict):
row = tk.Frame(self._table_frame, bg=C["white"], height=ROW_H)
row.pack(fill=tk.X)
row.pack_propagate(False)
# ── 文件名 ──
x, w = COL_X[0], COL_W[0]
tk.Label(
row, text=f"📄 {fd['filename']}",
font=("Microsoft YaHei", 10), bg=C["white"], fg=C["text"], anchor=tk.W,
).place(x=x + CELL_PAD, y=0, width=w - CELL_PAD * 2, height=ROW_H)
tk.Frame(row, bg=C["border"], width=1).place(x=COL_X[1], y=6, height=ROW_H - 12)
# ── 工作表(自定义 Dropdown) ──
x, w = COL_X[2], COL_W[2]
dd_sheet = Dropdown(
row,
values=fd["sheet_names"],
default=fd["selected_sheet"] or "",
on_select=lambda val, i=fi: self._on_sheet_change(i, val),
)
dd_sheet.place(x=x + CELL_PAD, y=6, width=w - CELL_PAD * 2, height=ROW_H - 12)
fd["sheet_dd"] = dd_sheet
tk.Frame(row, bg=C["border"], width=1).place(x=COL_X[3], y=6, height=ROW_H - 12)
# ── 解析列(自定义 Dropdown) ──
x, w = COL_X[4], COL_W[4]
dd_col = Dropdown(
row,
values=fd["columns"],
default=fd["selected_col"] or "",
on_select=lambda val, i=fi: self._on_col_change(i, val),
)
dd_col.place(x=x + CELL_PAD, y=6, width=w - CELL_PAD * 2, height=ROW_H - 12)
fd["col_dd"] = dd_col
tk.Frame(row, bg=C["border"], width=1).place(x=COL_X[5], y=6, height=ROW_H - 12)
# ── 解析方式(自定义 Dropdown) ──
x, w = COL_X[6], COL_W[6]
dd_mode = Dropdown(
row,
values=["横向排列", "纵向排序"],
default=fd["parse_mode"],
on_select=lambda val, i=fi: self._on_mode_change(i, val),
)
dd_mode.place(x=x + CELL_PAD, y=6, width=w - CELL_PAD * 2, height=ROW_H - 12)
fd["mode_dd"] = dd_mode
fd["row_frame"] = row
tk.Frame(self._table_frame, bg=C["border"], height=1).pack(fill=tk.X)
# ==================================================================
# 切换工作表 → 更新解析列
# ==================================================================
def _on_sheet_change(self, idx: int, new_sheet: str):
fd = self._files_data[idx]
fd["selected_sheet"] = new_sheet
try:
import pandas as pd
for engine in ["openpyxl", "xlrd"]:
try:
df = pd.read_excel(fd["path"], sheet_name=new_sheet, nrows=1, engine=engine)
columns = [str(c) for c in df.columns.tolist()]
break
except Exception:
continue
fd["columns"] = columns
first_col = columns[0] if columns else None
fd["selected_col"] = first_col
fd["col_dd"].set_values(columns, default=first_col)
except Exception as e:
messagebox.showerror("错误", f"读取工作表失败: {e}")
# ==================================================================
# 下拉选择回调
# ==================================================================
def _on_col_change(self, idx: int, val: str):
self._files_data[idx]["selected_col"] = val
def _on_mode_change(self, idx: int, val: str):
self._files_data[idx]["parse_mode"] = val
# ==================================================================
# 日志输出
# ==================================================================
def _log_write(self, text: str):
"""向底部日志面板追加一行"""
self._log_text.configure(state=tk.NORMAL)
self._log_text.insert(tk.END, text + "\n")
self._log_text.see(tk.END)
self._log_text.configure(state=tk.DISABLED)
def _log_clear(self):
"""清空日志面板"""
self._log_text.configure(state=tk.NORMAL)
self._log_text.delete("1.0", tk.END)
self._log_text.configure(state=tk.DISABLED)
# ==================================================================
# 确认解析
# ==================================================================
def _on_confirm(self):
if not self._files_data:
return
self._confirm_btn.configure(state=tk.DISABLED, text="⏳ 解析中...")
self._log_clear()
self._log_write("🚀 开始解析...")
# 捕获 print 输出到日志面板
class _LogWriter:
def __init__(self, root, log_func):
self._root = root
self._log = log_func
self._buf = ""
def write(self, s):
self._buf += s
if "\n" in self._buf:
lines = self._buf.split("\n")
self._buf = lines[-1]
for line in lines[:-1]:
stripped = line.strip()
if stripped:
self._root.after(0, lambda t=stripped: self._log(t))
def flush(self):
if self._buf.strip():
self._root.after(0, lambda t=self._buf.strip(): self._log(t))
self._buf = ""
def _run():
import sys
from app import finance_parse as fp
total = len(self._files_data)
success = 0
old_stdout = sys.stdout
old_stderr = sys.stderr
log_writer = _LogWriter(self.root, self._log_write)
sys.stdout = log_writer
sys.stderr = open(os.devnull, "w")
try:
for i, fd in enumerate(self._files_data):
fname = fd["filename"]
self.root.after(0, lambda n=i+1, fn=fname:
self._log_write(f"⏳ [{n}/{total}] 正在处理: {fn}"))
try:
save_path, amount = fp.parse_finance_data(
file_path=fd["path"],
target_index=fd["col_dd"].get(),
is_horizontal=(fd["mode_dd"].get() == "横向排列"),
sheet_name=fd["sheet_dd"].get(),
)
success += 1
fd["result"] = save_path
fd["amount"] = amount
self.root.after(0, lambda fn=fname, sp=save_path, amt=amount:
self._log_write(f"✅ [{success}/{total}] 完成: {fn}{amt}"))
except Exception as e:
fd["error"] = str(e)
self.root.after(0, lambda fn=fname, err=str(e):
self._log_write(f"❌ [{i+1}/{total}] 失败: {fn}{err}"))
finally:
sys.stdout = old_stdout
sys.stderr = old_stderr
self.root.after(0, lambda: self._on_parse_done(success, total))
threading.Thread(target=_run, daemon=True).start()
def _on_parse_done(self, success: int, total: int):
self._confirm_btn.configure(state=tk.NORMAL, text="✅ 开始解析")
# 从列表中移除已成功解析的文件,保留失败的
self._files_data = [fd for fd in self._files_data if "error" in fd]
if success == total:
self._log_write(f"🎉 全部 {total} 个文件解析完成!结果已保存到原文件所在目录。")
messagebox.showinfo("解析完成",
f"全部 {total} 个文件解析完成!\n\n结果已保存到原文件所在目录。")
else:
failed_count = len(self._files_data)
self._log_write(f"⚠️ 完成 {success}/{total},有 {failed_count} 个文件失败。")
errors = []
for fd in self._files_data:
if "error" in fd:
errors.append(f"{fd['filename']}: {fd['error']}")
msg = f"完成 {success}/{total}\n\n失败:\n" + "\n".join(errors[:5])
messagebox.showwarning("解析完成(部分失败)", msg)
# 刷新 UI:清除成功项后重建表格或显示空状态
if self._files_data:
self._build_table()
n = len(self._files_data)
self._status_label.configure(
text=f" 剩余 {n} 个失败文件 | 可调整配置后重新解析",
fg="#dc2626",
)
else:
self._show_empty()
self._path_entry.configure(state="normal")
self._path_entry.delete(0, tk.END)
self._path_entry.insert(0, "请选择 Excel 文件(.xlsx / .xls,可多选)")
self._path_entry.configure(state="readonly", fg=C["text_muted"])
self._status_label.configure(text=" 未选择任何文件")
+52
View File
@@ -0,0 +1,52 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas = [('app/resources/models/订单尺寸识别/best_model', 'app/resources/models/订单尺寸识别/best_model')]
binaries = []
hiddenimports = ['spacy', 'cn2an', 'tqdm', 'pandas', 'scipy']
tmp_ret = collect_all('spacy')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('openpyxl')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
a = Analysis(
['main.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='cwExcel',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='cwExcel',
)
+16
View File
@@ -0,0 +1,16 @@
"""
cwExcel - Python Desktop Application
入口模块
"""
import tkinter as tk
from app import MainApplication
def main():
root = tk.Tk()
app = MainApplication(root)
app.run()
if __name__ == "__main__":
main()
+5
View File
@@ -0,0 +1,5 @@
openpyxl==3.1.2
pandas==2.0.3
spacy==3.7.5
cn2an==0.5.24
tqdm==4.68.3