first commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
build/*
|
||||
dist/*
|
||||
outputs/*
|
||||
@@ -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`。
|
||||
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
app 包 - 应用程序核心模块
|
||||
"""
|
||||
from app.application import MainApplication
|
||||
|
||||
__all__ = ["MainApplication"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
Binary file not shown.
@@ -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 @@
|
||||
�
|
||||
@@ -0,0 +1 @@
|
||||
�
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"mode":"default"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
ui 包 - 用户界面组件
|
||||
"""
|
||||
from app.ui.main_window import MainWindow
|
||||
|
||||
__all__ = ["MainWindow"]
|
||||
Binary file not shown.
Binary file not shown.
@@ -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=" 未选择任何文件")
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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()
|
||||
@@ -0,0 +1,5 @@
|
||||
openpyxl==3.1.2
|
||||
pandas==2.0.3
|
||||
spacy==3.7.5
|
||||
cn2an==0.5.24
|
||||
tqdm==4.68.3
|
||||
Reference in New Issue
Block a user