commit 7fdcab3405135ee7ab801412d81f4b33cc85a812 Author: zhuyiyi <649091362@qq.com> Date: Sat Jul 18 15:04:19 2026 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7988f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/* +dist/* +outputs/* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e6b2c3 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# cwExcel - Python Desktop Application + +一个基于 Python Tkinter 的桌面应用程序。 + +## 环境要求 + +- Python 3.8+ +- Tkinter(Python 内置) + +## 安装依赖 + +```bash +pip install -r requirements.txt +``` + +## 运行 + +```bash +python main.py +``` + +## 打包 + +```bash +pip install pyinstaller +pyinstaller --noconfirm cwExcel.spec +``` + +输出在 `dist/cwExcel/cwExcel.exe`。 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..95735ac --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,6 @@ +""" +app 包 - 应用程序核心模块 +""" +from app.application import MainApplication + +__all__ = ["MainApplication"] diff --git a/app/__pycache__/__init__.cpython-38.pyc b/app/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..5598c6e Binary files /dev/null and b/app/__pycache__/__init__.cpython-38.pyc differ diff --git a/app/__pycache__/application.cpython-38.pyc b/app/__pycache__/application.cpython-38.pyc new file mode 100644 index 0000000..7038b49 Binary files /dev/null and b/app/__pycache__/application.cpython-38.pyc differ diff --git a/app/__pycache__/finance_parse.cpython-38.pyc b/app/__pycache__/finance_parse.cpython-38.pyc new file mode 100644 index 0000000..cc0895a Binary files /dev/null and b/app/__pycache__/finance_parse.cpython-38.pyc differ diff --git a/app/application.py b/app/application.py new file mode 100644 index 0000000..7bb4e0f --- /dev/null +++ b/app/application.py @@ -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() diff --git a/app/finance_parse.py b/app/finance_parse.py new file mode 100644 index 0000000..db633fb --- /dev/null +++ b/app/finance_parse.py @@ -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) + + \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/config.cfg b/app/resources/models/订单尺寸识别/best_model/config.cfg new file mode 100644 index 0000000..28cff7e --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/config.cfg @@ -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" \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/meta.json b/app/resources/models/订单尺寸识别/best_model/meta.json new file mode 100644 index 0000000..79c80c2 --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/meta.json @@ -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":[ + + ] +} \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/ner/cfg b/app/resources/models/订单尺寸识别/best_model/ner/cfg new file mode 100644 index 0000000..6cd11cf --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/ner/cfg @@ -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 +} \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/ner/model b/app/resources/models/订单尺寸识别/best_model/ner/model new file mode 100644 index 0000000..9f5135a Binary files /dev/null and b/app/resources/models/订单尺寸识别/best_model/ner/model differ diff --git a/app/resources/models/订单尺寸识别/best_model/ner/moves b/app/resources/models/订单尺寸识别/best_model/ner/moves new file mode 100644 index 0000000..1c5d9bc --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/ner/moves @@ -0,0 +1 @@ +moves0{"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}}cfgneg_key \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/tokenizer/cfg b/app/resources/models/订单尺寸识别/best_model/tokenizer/cfg new file mode 100644 index 0000000..a3e8b3d --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/tokenizer/cfg @@ -0,0 +1,3 @@ +{ + "segmenter":"char" +} \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/vocab/key2row b/app/resources/models/订单尺寸识别/best_model/vocab/key2row new file mode 100644 index 0000000..5416677 --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/vocab/key2row @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/vocab/lookups.bin b/app/resources/models/订单尺寸识别/best_model/vocab/lookups.bin new file mode 100644 index 0000000..5416677 --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/vocab/lookups.bin @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/vocab/strings.json b/app/resources/models/订单尺寸识别/best_model/vocab/strings.json new file mode 100644 index 0000000..abc026b --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/vocab/strings.json @@ -0,0 +1,1103 @@ +[ + "\n", + " ", + "!", + "(", + ")", + "+", + ",", + "-", + ".", + "/", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ":", + ";", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "ROOT", + "S", + "T", + "U", + "V", + "Y", + "Z", + "[", + "]", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "zh", + "{", + "}", + "\u00d7", + "\u2014", + "\u3010", + "\u3011", + "\u4e00", + "\u4e03", + "\u4e07", + "\u4e09", + "\u4e0a", + "\u4e0b", + "\u4e0d", + "\u4e0e", + "\u4e13", + "\u4e16", + "\u4e18", + "\u4e1a", + "\u4e1b", + "\u4e1c", + "\u4e1d", + "\u4e24", + "\u4e25", + "\u4e2a", + "\u4e2b", + "\u4e2d", + "\u4e30", + "\u4e36", + "\u4e39", + "\u4e3a", + "\u4e3b", + "\u4e3d", + "\u4e48", + "\u4e49", + "\u4e4b", + "\u4e4c", + "\u4e50", + "\u4e5d", + "\u4e71", + "\u4e7e", + "\u4e86", + "\u4e8c", + "\u4e91", + "\u4e94", + "\u4e95", + "\u4e9b", + "\u4ea6", + "\u4ea7\u54c1", + "\u4eac", + "\u4eae", + "\u4eba", + "\u4ec0", + "\u4eca", + "\u4ed3", + "\u4ed4", + "\u4ed5", + "\u4ed8", + "\u4ee3", + "\u4eec", + "\u4ef6", + "\u4efb", + "\u4f18", + "\u4f1a", + "\u4f1f", + "\u4f26", + "\u4f38", + "\u4f3c", + "\u4f4e", + "\u4f55", + "\u4f59", + "\u4f5b", + "\u4f5c", + "\u4f60", + "\u4f73", + "\u4f8b", + "\u4f9d", + "\u4fa0", + "\u4faf", + "\u4fca", + "\u4fdd", + "\u4fe1", + "\u4fee", + "\u5019", + "\u5029", + "\u503e", + "\u5047", + "\u505a", + "\u5065", + "\u513f", + "\u5143", + "\u5144", + "\u5148", + "\u5149", + "\u514b", + "\u5154", + "\u515c", + "\u5168", + "\u516b", + "\u516c", + "\u516d", + "\u5170", + "\u5171", + "\u5174", + "\u5176", + "\u5185", + "\u518d", + "\u5199", + "\u519b", + "\u51ac", + "\u51b0", + "\u51bb", + "\u51c0", + "\u51c6", + "\u51c9", + "\u51cc", + "\u51e1", + "\u51e4", + "\u51ef", + "\u51f0", + "\u51f8", + "\u51f9", + "\u51fa", + "\u51fb", + "\u5200", + "\u5206", + "\u5207", + "\u5212", + "\u5218", + "\u5219", + "\u521a", + "\u521b", + "\u5229", + "\u522b", + "\u522e", + "\u522e\u522e\u819c\u5c3a\u5bf8", + "\u5230", + "\u5236", + "\u5237", + "\u5238", + "\u523a", + "\u523b", + "\u524d", + "\u529b", + "\u529f", + "\u52a0", + "\u52a0\u6025", + "\u52a8", + "\u52a9", + "\u52b3", + "\u52c7", + "\u5305", + "\u5316", + "\u5317", + "\u533a", + "\u533b", + "\u5343", + "\u534a", + "\u534e", + "\u5353", + "\u5355", + "\u5355\u53f7", + "\u5356", + "\u5357", + "\u535a", + "\u535c", + "\u5361", + "\u5370", + "\u5373", + "\u5374", + "\u5382", + "\u538b", + "\u538d", + "\u5398", + "\u539f", + "\u53bb", + "\u53ca", + "\u53cb", + "\u53cc", + "\u53cd", + "\u53d1", + "\u53d4", + "\u53d6", + "\u53e3", + "\u53e4", + "\u53ea", + "\u53eb", + "\u53ee", + "\u53ef", + "\u53f3", + "\u53f6", + "\u53f7", + "\u53f8", + "\u5403", + "\u5404", + "\u5408", + "\u5409", + "\u540a", + "\u540c", + "\u540d", + "\u540e", + "\u5416", + "\u541b", + "\u542b", + "\u542f", + "\u5434", + "\u5439", + "\u543e", + "\u544a", + "\u5468", + "\u5473", + "\u548c", + "\u5495", + "\u5496", + "\u54aa", + "\u54c1", + "\u54c6", + "\u54d1", + "\u54e5", + "\u54e6", + "\u54ed", + "\u54f2", + "\u5510", + "\u551b", + "\u5546", + "\u5561", + "\u5565", + "\u5566", + "\u5584", + "\u559d", + "\u55b7", + "\u565c", + "\u5676", + "\u56db", + "\u56de", + "\u56e0", + "\u56e2", + "\u56ed", + "\u56f0", + "\u56fd", + "\u56fe", + "\u5706", + "\u571f", + "\u5723", + "\u5728", + "\u5730", + "\u5733", + "\u5740", + "\u5757", + "\u575a", + "\u5764", + "\u578b", + "\u579a", + "\u57ab", + "\u57ce", + "\u57fa", + "\u5802", + "\u5821", + "\u58a8", + "\u58c1", + "\u58eb", + "\u58ee", + "\u58f3", + "\u58f9", + "\u5904", + "\u5907", + "\u590f", + "\u5915", + "\u5916", + "\u591a", + "\u591c", + "\u591f", + "\u5927", + "\u5929", + "\u5934", + "\u5939", + "\u5947", + "\u5956", + "\u5957", + "\u5965", + "\u5973", + "\u5976", + "\u597d", + "\u5982", + "\u5988", + "\u598d", + "\u5996", + "\u5999", + "\u59b9", + "\u59cb", + "\u59d0", + "\u59ff", + "\u5a01", + "\u5a03", + "\u5a77", + "\u5a7b", + "\u5a9b", + "\u5b50", + "\u5b54", + "\u5b57", + "\u5b66", + "\u5b69", + "\u5b81", + "\u5b87", + "\u5b89", + "\u5b8b", + "\u5b8c", + "\u5b9a", + "\u5b9d", + "\u5b9e", + "\u5ba0", + "\u5ba2", + "\u5ba4", + "\u5bb0", + "\u5bb5", + "\u5bb6", + "\u5bb9", + "\u5bbd", + "\u5bc4", + "\u5bc6", + "\u5bcc", + "\u5bf8", + "\u5bf9", + "\u5c01", + "\u5c04", + "\u5c0f", + "\u5c10", + "\u5c11", + "\u5c14", + "\u5c1a", + "\u5c24", + "\u5c31", + "\u5c3a", + "\u5c3a\u5bf8", + "\u5c3c", + "\u5c42", + "\u5c45", + "\u5c71", + "\u5cb8", + "\u5ddd", + "\u5dde", + "\u5de5", + "\u5de5\u827a", + "\u5de6", + "\u5de7", + "\u5dee", + "\u5df1", + "\u5df2", + "\u5df4", + "\u5e02", + "\u5e03", + "\u5e05", + "\u5e06", + "\u5e08", + "\u5e0c", + "\u5e18", + "\u5e1b", + "\u5e1c", + "\u5e26", + "\u5e45", + "\u5e54", + "\u5e72", + "\u5e73", + "\u5e74", + "\u5e78", + "\u5e7c", + "\u5e7f", + "\u5e84", + "\u5e86", + "\u5e8f", + "\u5e94", + "\u5e95", + "\u5e97", + "\u5e9f", + "\u5ea6", + "\u5eb7", + "\u5efa", + "\u5f00", + "\u5f02", + "\u5f04", + "\u5f0f", + "\u5f20", + "\u5f2f", + "\u5f39", + "\u5f3a", + "\u5f53", + "\u5f62", + "\u5f69", + "\u5f80", + "\u5f84", + "\u5f85", + "\u5f88", + "\u5f90", + "\u5f97", + "\u5fae", + "\u5fb7", + "\u5fbd", + "\u5fc3", + "\u5fc6", + "\u5fd7", + "\u5fe7", + "\u5feb", + "\u5ff5", + "\u601d", + "\u6021", + "\u6025", + "\u602a", + "\u603b", + "\u6052", + "\u607a", + "\u60c5", + "\u60ca", + "\u60dc", + "\u60f3", + "\u60f9", + "\u6101", + "\u610f", + "\u6148", + "\u6167", + "\u61d2", + "\u61ff", + "\u6210", + "\u6211", + "\u6218", + "\u6237", + "\u623f", + "\u624b", + "\u6253", + "\u6263", + "\u626b", + "\u6279", + "\u627f", + "\u6280", + "\u628a", + "\u6298", + "\u62a5", + "\u62c9", + "\u62cd", + "\u62d2", + "\u62db", + "\u62fc", + "\u62fe", + "\u6301", + "\u6302", + "\u6309", + "\u631e", + "\u6363", + "\u636e", + "\u638c", + "\u6392", + "\u63a5", + "\u63d0", + "\u63d2", + "\u63e1", + "\u640f", + "\u6446", + "\u644a", + "\u6495", + "\u64e6", + "\u6539", + "\u653e", + "\u653f", + "\u6551", + "\u6563", + "\u6570", + "\u6570\u91cf", + "\u6587", + "\u658c", + "\u6597", + "\u6599", + "\u65b0", + "\u65b9", + "\u65cf", + "\u65d7", + "\u65e0", + "\u65e5", + "\u65e9", + "\u65f6", + "\u65fa", + "\u660e", + "\u6613", + "\u6615", + "\u661f", + "\u662f", + "\u6641", + "\u6643", + "\u664b", + "\u6653", + "\u665a", + "\u665f", + "\u6668", + "\u6674", + "\u667a", + "\u6696", + "\u66b4", + "\u66f2", + "\u66f9", + "\u66fc", + "\u6700", + "\u6708", + "\u6709", + "\u670b", + "\u6728", + "\u672a", + "\u672b", + "\u6731", + "\u6735", + "\u673a", + "\u6746", + "\u674e", + "\u6750", + "\u6750\u8d28", + "\u6761", + "\u6765", + "\u6768", + "\u676d", + "\u676f", + "\u6770", + "\u677f", + "\u6797", + "\u679c", + "\u679d", + "\u67ab", + "\u67c4", + "\u67cf", + "\u67d2", + "\u67d4", + "\u67dc", + "\u67e0", + "\u6800", + "\u6807", + "\u6808", + "\u6811", + "\u6837", + "\u6839", + "\u683c", + "\u6842", + "\u6843", + "\u6846", + "\u6848", + "\u6865", + "\u6881", + "\u6885", + "\u68a6", + "\u68cd", + "\u68ee", + "\u690e", + "\u6930", + "\u697c", + "\u6986", + "\u6a21", + "\u6a31", + "\u6a58", + "\u6a59", + "\u6aac", + "\u6b21", + "\u6b23", + "\u6b3e", + "\u6b46", + "\u6b4c", + "\u6b63", + "\u6b65", + "\u6bb7", + "\u6bcf", + "\u6bd4", + "\u6bd5", + "\u6bdb", + "\u6beb", + "\u6c11", + "\u6c34", + "\u6c49", + "\u6c5f", + "\u6c7d", + "\u6c89", + "\u6c90", + "\u6ca1", + "\u6cb3", + "\u6cb9", + "\u6cbb", + "\u6cc9", + "\u6ce1", + "\u6ce2", + "\u6ce5", + "\u6ce8", + "\u6d01", + "\u6d0b", + "\u6d17", + "\u6d25", + "\u6d2a", + "\u6d3b", + "\u6d41", + "\u6d46", + "\u6d4e", + "\u6d59", + "\u6d69", + "\u6d6a", + "\u6d6e", + "\u6d74", + "\u6d77", + "\u6d9b", + "\u6da6", + "\u6daf", + "\u6dc7", + "\u6dd8", + "\u6df1", + "\u6e05", + "\u6e14", + "\u6e21", + "\u6e2f", + "\u6e38", + "\u6e56", + "\u6e90", + "\u6eaa", + "\u6f2b", + "\u6f47", + "\u6fa1", + "\u6fc0", + "\u706b", + "\u706c", + "\u706f", + "\u7070", + "\u7075", + "\u70ad", + "\u70ae", + "\u70b9", + "\u70c2", + "\u70eb", + "\u70ed", + "\u710a", + "\u7136", + "\u7167", + "\u716e", + "\u718a", + "\u7199", + "\u71a0", + "\u71a8", + "\u71d5", + "\u7206", + "\u7231", + "\u7247", + "\u7248", + "\u724c", + "\u7259", + "\u725b", + "\u7269", + "\u7279", + "\u72d7", + "\u72ec", + "\u7315", + "\u732a", + "\u732b", + "\u732c", + "\u7334", + "\u7389", + "\u738b", + "\u73ab", + "\u73af", + "\u73b0", + "\u73b2", + "\u73bb", + "\u73e0", + "\u73ed", + "\u7426", + "\u742a", + "\u742e", + "\u7433", + "\u7470", + "\u7483", + "\u74dc", + "\u74f7", + "\u7518", + "\u751f", + "\u7528", + "\u7528\u6237\u4fe1\u606f", + "\u752c", + "\u7532", + "\u7535", + "\u7537", + "\u753b", + "\u7545", + "\u754c", + "\u7586", + "\u75c5", + "\u75d5", + "\u766b", + "\u767b", + "\u767d", + "\u767e", + "\u7684", + "\u7687", + "\u76ae", + "\u76c6", + "\u76d6", + "\u76df", + "\u76ef", + "\u76f4", + "\u76f8", + "\u7701", + "\u770b", + "\u771f", + "\u7740", + "\u7761", + "\u777f", + "\u77e5", + "\u77e9", + "\u77f3", + "\u7801", + "\u7834", + "\u786e", + "\u78b3", + "\u795e", + "\u7965", + "\u798f", + "\u79b9", + "\u79c0", + "\u79cd", + "\u79d1", + "\u79d8", + "\u79e6", + "\u7a0d", + "\u7a57", + "\u7a7a", + "\u7a7f", + "\u7acb", + "\u7ae0", + "\u7aef", + "\u7b11", + "\u7b19", + "\u7b2c", + "\u7b49", + "\u7b52", + "\u7b71", + "\u7b7e", + "\u7b94", + "\u7bb1", + "\u7c73", + "\u7c79", + "\u7c7b", + "\u7c98", + "\u7ca5", + "\u7cbe", + "\u7cca", + "\u7cd6", + "\u7cfb", + "\u7d20", + "\u7d2b", + "\u7ea2", + "\u7ea7", + "\u7eac", + "\u7eaf", + "\u7eb1", + "\u7eb5", + "\u7eb8", + "\u7eb9", + "\u7ebf", + "\u7ec3", + "\u7ec4", + "\u7ec5", + "\u7ec8", + "\u7edd", + "\u7edf", + "\u7eee", + "\u7ef3", + "\u7ef4", + "\u7f0e", + "\u7f18", + "\u7f1d", + "\u7f29", + "\u7f3a", + "\u7f57", + "\u7f8a", + "\u7f8e", + "\u7f99", + "\u7fa4", + "\u7fbd", + "\u7fca", + "\u7fd4", + "\u8000", + "\u8001", + "\u8005", + "\u8033", + "\u8054", + "\u805a", + "\u8083", + "\u8089", + "\u808c", + "\u80a0", + "\u80cc", + "\u80d6", + "\u80f6", + "\u80fd", + "\u819c", + "\u81ea", + "\u81f4", + "\u8272", + "\u827a", + "\u827e", + "\u828a", + "\u8299", + "\u82a6", + "\u82ad", + "\u82af", + "\u82b1", + "\u82b3", + "\u82cd", + "\u82cf", + "\u82e5", + "\u82f1", + "\u82f9", + "\u8303", + "\u8304", + "\u8309", + "\u8317", + "\u831c", + "\u8336", + "\u8349", + "\u8363", + "\u8389", + "\u838e", + "\u8393", + "\u83b1", + "\u83b2", + "\u83b9", + "\u83c7", + "\u83ca", + "\u83dc", + "\u83e9", + "\u840c", + "\u840d", + "\u8425", + "\u8431", + "\u843d", + "\u846b", + "\u8499", + "\u84c9", + "\u84d3", + "\u84dd", + "\u8521", + "\u852b", + "\u8537", + "\u854a", + "\u8587", + "\u85b0", + "\u85cf", + "\u866b", + "\u8679", + "\u86cb", + "\u86d9", + "\u86f0", + "\u8717", + "\u871c", + "\u87f9", + "\u884c", + "\u8857", + "\u8863", + "\u8865", + "\u888b", + "\u88c1", + "\u88c5", + "\u88d4", + "\u88f9", + "\u897f", + "\u8981", + "\u8986", + "\u89c1", + "\u89d2", + "\u8ba1", + "\u8ba4", + "\u8bb0", + "\u8bb8", + "\u8bbe", + "\u8bd5", + "\u8bda", + "\u8bed", + "\u8bf7", + "\u8bfa", + "\u8c01", + "\u8c03", + "\u8c46", + "\u8c5a", + "\u8c61", + "\u8d1d", + "\u8d21", + "\u8d22", + "\u8d27", + "\u8d28", + "\u8d2d", + "\u8d30", + "\u8d34", + "\u8d35", + "\u8d38", + "\u8d39", + "\u8d5e", + "\u8d77", + "\u8d85", + "\u8ddf", + "\u8def", + "\u8eab", + "\u8f66", + "\u8f69", + "\u8f6c", + "\u8fb0", + "\u8fb9", + "\u8fbd", + "\u8fbe", + "\u8fce", + "\u8fd0", + "\u8fd9", + "\u8fdc", + "\u8fea", + "\u8ff0", + "\u8ff7", + "\u8ff9", + "\u9000", + "\u9001", + "\u900f", + "\u9012", + "\u901a", + "\u9020", + "\u9047", + "\u9053", + "\u90a3", + "\u90ae", + "\u90b5", + "\u90d1", + "\u90dd", + "\u90e1", + "\u90e8", + "\u90ed", + "\u90fd", + "\u914d", + "\u9152", + "\u9171", + "\u9192", + "\u91c7", + "\u91ca", + "\u91cc", + "\u91cd", + "\u91cf", + "\u91d1", + "\u946b", + "\u9488", + "\u94a9", + "\u94b0", + "\u94b1", + "\u94c3", + "\u94db", + "\u94dc", + "\u94dd", + "\u94f6", + "\u94fa", + "\u9501", + "\u950c", + "\u9526", + "\u9534", + "\u9542", + "\u9547", + "\u956d", + "\u957f", + "\u95e8", + "\u95f4", + "\u9601", + "\u961f", + "\u9633", + "\u963f", + "\u9648", + "\u9650", + "\u9655", + "\u9675", + "\u9676", + "\u969c", + "\u96c5", + "\u96c6", + "\u96e8", + "\u96ea", + "\u96f6", + "\u96fe", + "\u9700", + "\u9706", + "\u972d", + "\u9732", + "\u9752", + "\u9759", + "\u975e", + "\u9762", + "\u978b", + "\u97f5", + "\u9879", + "\u987a", + "\u987e", + "\u9897", + "\u989c", + "\u98ce", + "\u98de", + "\u9965", + "\u996d", + "\u9970", + "\u9971", + "\u9988", + "\u9999", + "\u9a6c", + "\u9a70", + "\u9a7f", + "\u9a91", + "\u9ad8", + "\u9c7c", + "\u9c81", + "\u9c94", + "\u9c9c", + "\u9e1f", + "\u9e2d", + "\u9e4f", + "\u9ea6", + "\u9ebb", + "\u9ec4", + "\u9ece", + "\u9ed1", + "\u9f20", + "\u9f50", + "\u9f99", + "\u9f9f", + "\uff01", + "\uff08", + "\uff09", + "\uff0c", + "\uff1a", + "\uff1b" +] \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/vocab/vectors b/app/resources/models/订单尺寸识别/best_model/vocab/vectors new file mode 100644 index 0000000..ebadaa5 Binary files /dev/null and b/app/resources/models/订单尺寸识别/best_model/vocab/vectors differ diff --git a/app/resources/models/订单尺寸识别/best_model/vocab/vectors.cfg b/app/resources/models/订单尺寸识别/best_model/vocab/vectors.cfg new file mode 100644 index 0000000..32c800a --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/vocab/vectors.cfg @@ -0,0 +1,3 @@ +{ + "mode":"default" +} \ No newline at end of file diff --git a/app/ui/__init__.py b/app/ui/__init__.py new file mode 100644 index 0000000..791fa95 --- /dev/null +++ b/app/ui/__init__.py @@ -0,0 +1,6 @@ +""" +ui 包 - 用户界面组件 +""" +from app.ui.main_window import MainWindow + +__all__ = ["MainWindow"] diff --git a/app/ui/__pycache__/__init__.cpython-38.pyc b/app/ui/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..54805eb Binary files /dev/null and b/app/ui/__pycache__/__init__.cpython-38.pyc differ diff --git a/app/ui/__pycache__/main_window.cpython-38.pyc b/app/ui/__pycache__/main_window.cpython-38.pyc new file mode 100644 index 0000000..d9e3b71 Binary files /dev/null and b/app/ui/__pycache__/main_window.cpython-38.pyc differ diff --git a/app/ui/main_window.py b/app/ui/main_window.py new file mode 100644 index 0000000..692e97a --- /dev/null +++ b/app/ui/main_window.py @@ -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("", self._toggle) + w.bind("", self._on_enter) + w.bind("", 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("", self._on_pick) + self._listbox.bind("", self._on_pick) + self._listbox.bind("", lambda e: self._close()) + self._listbox.focus_set() + + # 点击外部关闭 + self._popup.bind("", lambda e: self.after(100, self._check_focus)) + + # 绑定全局点击关闭(鼠标点击 popup 外) + self._grab_bind = self.winfo_toplevel().bind( + "", 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("", 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=" 未选择任何文件") diff --git a/cwExcel.spec b/cwExcel.spec new file mode 100644 index 0000000..ccb8bd2 --- /dev/null +++ b/cwExcel.spec @@ -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', +) diff --git a/main.py b/main.py new file mode 100644 index 0000000..2de6e8e --- /dev/null +++ b/main.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..02d785c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +openpyxl==3.1.2 +pandas==2.0.3 +spacy==3.7.5 +cn2an==0.5.24 +tqdm==4.68.3