| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- import tkinter as tk
- from tkinter import filedialog, messagebox
- import pandas as pd
- from openpyxl import load_workbook
- import os
- import sys
- import re
- from collections import Counter
- class ExcelMapperApp:
- def __init__(self, root):
- self.root = root
- self.root.title("Excel 自动填表工具")
- self.root.geometry("750x550")
- self.root.resizable(True, True)
-
- if getattr(sys, 'frozen', False):
- self.base_dir = os.path.dirname(sys.executable)
- else:
- self.base_dir = os.path.dirname(os.path.abspath(__file__))
-
- self.source_file = tk.StringVar()
- self.header_row = tk.StringVar(value="3")
- self.source_header_row = tk.StringVar(value="2")
- self.df = None
- self.status_text = tk.StringVar(value="请选择来源Excel文件")
-
- self.setup_ui()
-
- def setup_ui(self):
- title_label = tk.Label(self.root, text="Excel 自动填表工具", font=("Arial", 16, "bold"))
- title_label.pack(pady=10)
-
- file_frame = tk.LabelFrame(self.root, text="文件选择", font=("Arial", 10, "bold"))
- file_frame.pack(pady=10, padx=20, fill="x")
-
- tk.Label(file_frame, text="来源数据Excel:").grid(row=0, column=0, sticky="w", pady=5)
- tk.Entry(file_frame, textvariable=self.source_file, width=50).grid(row=0, column=1, padx=5, pady=5)
- tk.Button(file_frame, text="浏览", command=self.select_source_file).grid(row=0, column=2, pady=5)
-
- tk.Label(file_frame, text="来源表头行号(横向格式用):").grid(row=1, column=0, sticky="w", pady=5)
- tk.Entry(file_frame, textvariable=self.source_header_row, width=10).grid(row=1, column=1, sticky="w", padx=5, pady=5)
- tk.Label(file_frame, text="(第2行是字段名)", fg="gray").grid(row=1, column=1, sticky="e", padx=5, pady=5)
-
- tk.Label(file_frame, text="模板表头行号:").grid(row=2, column=0, sticky="w", pady=5)
- tk.Entry(file_frame, textvariable=self.header_row, width=10).grid(row=2, column=1, sticky="w", padx=5, pady=5)
- tk.Label(file_frame, text="(第3行是字段名)", fg="gray").grid(row=2, column=1, sticky="e", padx=5, pady=5)
-
- template_path = os.path.join(self.base_dir, "模板.xlsx")
- output_path = os.path.join(self.base_dir, "输出.xlsx")
- tk.Label(file_frame, text=f"目标模板: {template_path}", fg="blue").grid(row=3, column=0, columnspan=3, sticky="w", pady=3)
- tk.Label(file_frame, text=f"输出文件: {output_path}", fg="green").grid(row=4, column=0, columnspan=3, sticky="w", pady=3)
-
- btn_frame = tk.Frame(self.root)
- btn_frame.pack(pady=10)
- tk.Button(btn_frame, text="🔍 查看来源结构", command=self.inspect_source,
- bg="#9C27B0", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
- tk.Button(btn_frame, text="🚀 开始生成", command=self.generate_output,
- bg="#4CAF50", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
- tk.Button(btn_frame, text="清空日志", command=self.clear_log,
- bg="#f44336", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
-
- log_frame = tk.LabelFrame(self.root, text="运行日志", font=("Arial", 10, "bold"))
- log_frame.pack(pady=10, padx=20, fill="both", expand=True)
-
- self.log_text = tk.Text(log_frame, height=14, font=("Courier", 9))
- self.log_text.pack(side="left", fill="both", expand=True)
-
- scrollbar = tk.Scrollbar(log_frame, command=self.log_text.yview)
- scrollbar.pack(side="right", fill="y")
- self.log_text.config(yscrollcommand=scrollbar.set)
-
- status_bar = tk.Label(self.root, textvariable=self.status_text, relief="sunken", anchor="w",
- font=("Arial", 9), bg="#f0f0f0")
- status_bar.pack(side="bottom", fill="x")
-
- def log(self, msg):
- self.log_text.insert(tk.END, msg + "\n")
- self.log_text.see(tk.END)
- self.root.update()
- print(msg)
-
- def clear_log(self):
- self.log_text.delete(1.0, tk.END)
-
- def select_source_file(self):
- file_path = filedialog.askopenfilename(
- title="选择来源Excel文件",
- filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
- )
- if file_path:
- self.source_file.set(file_path)
- self.status_text.set(f"已选择: {os.path.basename(file_path)}")
- self.log(f"📁 已选择来源文件: {file_path}")
-
- def inspect_source(self):
- """检测并显示来源格式"""
- source_path = self.source_file.get()
- if not source_path:
- messagebox.showwarning("警告", "请先选择来源Excel文件!")
- return
-
- self.log("\n" + "="*60)
- self.log("🔍 分析来源Excel结构...")
-
- try:
- wb = load_workbook(source_path)
- ws = wb.active
- self.log(f"📊 共 {ws.max_row} 行, {ws.max_column} 列")
-
- # 判断是横向还是纵向
- first_row_vals = []
- for col in range(1, min(20, ws.max_column + 1)):
- val = ws.cell(row=1, column=col).value
- if val:
- first_row_vals.append(str(val))
-
- is_horizontal = len(first_row_vals) >= 3 and not any(':' in v for v in first_row_vals[:3])
-
- if is_horizontal:
- self.log(f"\n📌 检测到:横向格式(多列表格)")
- self.log(f" 第1行有 {len(first_row_vals)} 个有值单元格")
- self.log(f" 前几个: {first_row_vals[:5]}")
-
- # 显示第2行列名
- row2_vals = []
- for col in range(1, min(30, ws.max_column + 1)):
- val = ws.cell(row=2, column=col).value
- if val:
- row2_vals.append(str(val))
- if row2_vals:
- self.log(f"\n📌 第2行(列名): {row2_vals[:10]}...")
- else:
- self.log(f"\n📌 检测到:纵向格式(从上往下排列)")
-
- # 显示前20行
- self.log(f"\n📌 前20行内容:")
- for row in range(1, min(21, ws.max_row + 1)):
- val = ws.cell(row=row, column=1).value
- if val:
- self.log(f" 第{row}行: {str(val)[:80]}")
-
- except Exception as e:
- self.log(f"❌ 分析失败: {str(e)}")
-
- def parse_vertical_format(self, ws):
- """解析纵向格式(Key: Value 格式),相同Item Code合并"""
- products_dict = {} # 用字典存储,key是Item Code
- current_item_code = None
- current_product = {}
-
- field_map = {
- "Item Code": "Item Code",
- "产品名称": "产品名称",
- "颜色": "颜色",
- "材质": "材质",
- "组装长度 (英寸)": "组装长度(英寸)",
- "组装宽度 (英寸)": "组装宽度(英寸)",
- "组装高度 (英寸)": "组装高度(英寸)",
- "产品重量 (磅)": "产品重量(磅)",
- "长度 (英寸)": "包装尺寸-长度(英寸)",
- "宽度 (英寸)": "包装尺寸-宽度(英寸)",
- "高度 (英寸)": "包装尺寸-高度(英寸)",
- "重量 (磅)": "包装尺寸-重量(磅)",
- }
-
- price_pattern = re.compile(r'^\$?([\d.]+)$')
-
- for row in range(1, ws.max_row + 1):
- val = ws.cell(row=row, column=1).value
- if val is None:
- continue
-
- val_str = str(val).strip()
- if not val_str:
- continue
-
- # 检测是否是 "Key: Value" 格式
- if ':' in val_str:
- parts = val_str.split(':', 1)
- key = parts[0].strip()
- value = parts[1].strip() if len(parts) > 1 else ''
-
- if key == "Item Code":
- # 保存当前产品到字典
- if current_item_code and current_product:
- if current_item_code in products_dict:
- # 合并(不覆盖已有字段)
- for k, v in current_product.items():
- if k not in products_dict[current_item_code] or not products_dict[current_item_code][k]:
- products_dict[current_item_code][k] = v
- else:
- products_dict[current_item_code] = current_product.copy()
-
- # 开始新产品
- current_item_code = value
- current_product = {"Item Code": value}
- else:
- mapped_key = field_map.get(key, key)
- # 只有当前产品中没有该字段时才设置(保留第一次出现的值)
- if mapped_key not in current_product or not current_product[mapped_key]:
- current_product[mapped_key] = value
- else:
- # 没有冒号,可能是价格
- match = price_pattern.match(val_str.replace('$', '').strip())
- if match and current_item_code:
- price_val = match.group(1)
- # 判断是 Base Cost 还是 MSRP
- if current_item_code in products_dict:
- existing = products_dict[current_item_code]
- if "优惠单价" not in existing or not existing["优惠单价"]:
- existing["优惠单价"] = price_val
- elif "Manufacturer Suggested Retail Price" not in existing or not existing["Manufacturer Suggested Retail Price"]:
- existing["Manufacturer Suggested Retail Price"] = str(float(price_val) + 70)
- else:
- if "优惠单价" not in current_product or not current_product["优惠单价"]:
- current_product["优惠单价"] = price_val
- elif "Manufacturer Suggested Retail Price" not in current_product or not current_product["Manufacturer Suggested Retail Price"]:
- current_product["Manufacturer Suggested Retail Price"] = str(float(price_val) + 70)
-
- # 保存最后一个产品
- if current_item_code and current_product:
- if current_item_code in products_dict:
- for k, v in current_product.items():
- if k not in products_dict[current_item_code] or not products_dict[current_item_code][k]:
- products_dict[current_item_code][k] = v
- else:
- products_dict[current_item_code] = current_product
-
- # 转换为列表
- return list(products_dict.values())
-
- def generate_output(self):
- self.log("\n" + "="*60)
- self.log("🚀 开始处理...")
-
- source_path = self.source_file.get()
- template_path = os.path.join(self.base_dir, "模板.xlsx")
-
- if not source_path:
- messagebox.showwarning("警告", "请选择来源Excel文件!")
- return
-
- if not os.path.exists(template_path):
- self.log(f"❌ 找不到模板文件: {template_path}")
- messagebox.showerror("错误", f"找不到模板文件:\n{template_path}")
- return
-
- try:
- header_row = int(self.header_row.get())
- source_header = int(self.source_header_row.get())
-
- # ========== 1. 检测并读取来源数据 ==========
- wb_source = load_workbook(source_path)
- ws_source = wb_source.active
-
- # 检测格式
- first_row_vals = []
- for col in range(1, min(20, ws_source.max_column + 1)):
- val = ws_source.cell(row=1, column=col).value
- if val:
- first_row_vals.append(str(val))
-
- is_horizontal = len(first_row_vals) >= 3 and not any(':' in v for v in first_row_vals[:3])
-
- if is_horizontal:
- self.log(f"📌 检测到横向格式,使用 pandas 读取...")
- self.df = pd.read_excel(source_path, header=source_header - 1)
- self.log(f"✅ 成功读取 {len(self.df)} 行数据")
- else:
- self.log(f"📌 检测到纵向格式,使用解析器读取...")
- products = self.parse_vertical_format(ws_source)
- self.log(f"✅ 成功解析 {len(products)} 个产品")
- self.df = pd.DataFrame(products)
- self.log(f"📋 解析到的字段: {list(self.df.columns)}")
-
- # ========== 2. 加载模板(直接修改原文件) ==========
- self.log(f"\n📖 加载模板: {template_path}")
- wb = load_workbook(template_path)
- ws = wb.active
-
- # ========== 3. 找到已有数据的最后一行 ==========
- # 从第4行开始找,找到第一个完全空的行
- last_row = 3 # 从第4行开始检查
- for row in range(4, ws.max_row + 2):
- is_empty = True
- for col in range(1, min(20, ws.max_column + 1)): # 检查前20列
- if ws.cell(row=row, column=col).value is not None:
- is_empty = False
- break
- if is_empty:
- last_row = row
- break
- else:
- last_row = ws.max_row + 1
-
- self.log(f"📌 模板中已有数据到第 {last_row - 1} 行,从第 {last_row} 行开始追加")
-
- # ========== 4. 读取模板表头 ==========
- col_indices = {}
- for col in range(1, ws.max_column + 1):
- cell_value = ws.cell(row=header_row, column=col).value
- if cell_value:
- col_name = str(cell_value).strip()
- if col_name and not col_name.startswith("Unnamed"):
- col_indices[col_name] = col
-
- # ========== 5. 目标列映射 ==========
- target_cols = [
- "Supplier Part Number", "Manufacturer Part Number", "Base Cost",
- "Manufacturer Suggested Retail Price", "Product Weight",
- "Carton Weight 1", "Carton Height 1", "Carton Width 1", "Carton Depth 1",
- "Base Color", "Base Material", "Color", "Material",
- "Overall Depth - Front to Back", "Overall Height - Top to Bottom",
- "Overall Product Weight", "Overall Width - Side to Side",
- "Top Color", "Top Material"
- ]
-
- found_cols = {}
- for target in target_cols:
- if target in col_indices:
- found_cols[target] = col_indices[target]
-
- if not found_cols:
- self.log(f"\n❌ 没有找到任何目标列!")
- return
-
- # ========== 6. 匹配来源列 ==========
- source_mapping = {
- "Item Code": "Item Code",
- "产品名称": "产品名称",
- "优惠单价": "优惠单价",
- "产品重量": "产品重量(磅)",
- "包装尺寸-重量": "包装尺寸-重量(磅)",
- "包装尺寸-高度": "包装尺寸-高度(英寸)",
- "包装尺寸-宽度": "包装尺寸-宽度(英寸)",
- "包装尺寸-长度": "包装尺寸-长度(英寸)",
- "颜色": "颜色",
- "材质": "材质",
- "组装长度": "组装长度(英寸)",
- "组装高度": "组装高度(英寸)",
- "组装宽度": "组装宽度(英寸)",
- }
-
- source_map = {}
- for key, expected in source_mapping.items():
- found = False
- for col in self.df.columns:
- if col == expected:
- source_map[key] = col
- found = True
- break
- if not found:
- # 部分匹配
- for col in self.df.columns:
- if expected in col or col in expected:
- source_map[key] = col
- found = True
- break
- if not found:
- source_map[key] = None
-
- # ========== 7. 填充数据 ==========
- def get_key(row):
- item = row.get("Item Code", "")
- name = row.get("产品名称", "")
- return f"{item}_{name}"
-
- keys = [get_key(row) for _, row in self.df.iterrows()]
- key_count = Counter(keys)
- key_index = Counter()
-
- row_num = last_row
- filled_count = 0
-
- self.log(f"\n✍️ 开始追加数据...")
-
- for idx, (_, src_row) in enumerate(self.df.iterrows()):
- item_code = src_row.get("Item Code", "")
- product_name = src_row.get("产品名称", "")
-
- if pd.isna(item_code):
- item_code = ""
- if pd.isna(product_name):
- product_name = ""
-
- key = get_key(src_row)
-
- if key_count[key] > 1:
- key_index[key] += 1
- suffix = f".{key_index[key] - 1}"
- mfr_suffix = f".{key_index[key]}"
- else:
- suffix = ""
- mfr_suffix = ""
-
- self.log(f"\n--- 追加第 {idx+1}/{len(self.df)} 个产品 ---")
- self.log(f" Item Code: '{item_code}'")
- self.log(f" 产品名称: '{str(product_name)[:50]}...'")
-
- fill_count = 0
-
- # Supplier Part Number
- if "Supplier Part Number" in found_cols:
- val = f"{item_code}{product_name}{suffix}"
- ws.cell(row=row_num, column=found_cols["Supplier Part Number"], value=val)
- fill_count += 1
-
- # Manufacturer Part Number
- if "Manufacturer Part Number" in found_cols:
- val = f"{item_code}{product_name}{mfr_suffix}"
- ws.cell(row=row_num, column=found_cols["Manufacturer Part Number"], value=val)
- fill_count += 1
-
- # Base Cost
- if "Base Cost" in found_cols:
- col = source_map.get("优惠单价")
- if col and col in src_row:
- val = src_row[col]
- if pd.notna(val):
- ws.cell(row=row_num, column=found_cols["Base Cost"], value=val)
- fill_count += 1
-
- # MSRP
- if "Manufacturer Suggested Retail Price" in found_cols:
- col = source_map.get("优惠单价")
- if col and col in src_row:
- val = src_row[col]
- try:
- num = float(val) if pd.notna(val) else 0
- result = num + 70
- ws.cell(row=row_num, column=found_cols["Manufacturer Suggested Retail Price"], value=result)
- fill_count += 1
- except:
- pass
- else:
- msrp_col = source_map.get("Manufacturer Suggested Retail Price")
- if msrp_col and msrp_col in src_row:
- val = src_row[msrp_col]
- if pd.notna(val):
- ws.cell(row=row_num, column=found_cols["Manufacturer Suggested Retail Price"], value=val)
- fill_count += 1
-
- # Product Weight
- if "Product Weight" in found_cols:
- col = source_map.get("产品重量")
- if col and col in src_row:
- val = src_row[col]
- if pd.notna(val):
- ws.cell(row=row_num, column=found_cols["Product Weight"], value=val)
- fill_count += 1
-
- # Carton
- carton_map = [
- ("Carton Weight 1", "包装尺寸-重量"),
- ("Carton Height 1", "包装尺寸-高度"),
- ("Carton Width 1", "包装尺寸-宽度"),
- ("Carton Depth 1", "包装尺寸-长度"),
- ]
- for target, src_name in carton_map:
- if target in found_cols:
- col = source_map.get(src_name)
- if col and col in src_row:
- val = src_row[col]
- if pd.notna(val):
- ws.cell(row=row_num, column=found_cols[target], value=val)
- fill_count += 1
-
- # Color/Material
- cm_map = [
- ("Base Color", "颜色"),
- ("Base Material", "材质"),
- ("Color", "颜色"),
- ("Material", "材质"),
- ("Top Color", "颜色"),
- ("Top Material", "材质"),
- ]
- for target, src_name in cm_map:
- if target in found_cols:
- col = source_map.get(src_name)
- if col and col in src_row:
- val = src_row[col]
- if pd.notna(val):
- ws.cell(row=row_num, column=found_cols[target], value=val)
- fill_count += 1
-
- # Overall
- overall_map = [
- ("Overall Depth - Front to Back", "组装长度"),
- ("Overall Height - Top to Bottom", "组装高度"),
- ("Overall Product Weight", "产品重量"),
- ("Overall Width - Side to Side", "组装宽度"),
- ]
- for target, src_name in overall_map:
- if target in found_cols:
- col = source_map.get(src_name)
- if col and col in src_row:
- val = src_row[col]
- if pd.notna(val):
- ws.cell(row=row_num, column=found_cols[target], value=val)
- fill_count += 1
-
- self.log(f" 📊 本行填充 {fill_count} 个字段")
- row_num += 1
- filled_count += 1
-
- # ========== 8. 直接保存到模板文件 ==========
- wb.save(template_path)
- self.log(f"\n✅ 成功追加到模板文件: {template_path}")
- self.log(f"📊 共追加 {filled_count} 行数据")
- self.status_text.set(f"✅ 已追加 {filled_count} 行到模板")
- messagebox.showinfo("成功", f"已追加 {filled_count} 行数据到模板文件:\n{template_path}")
-
- except Exception as e:
- self.log(f"\n❌ 错误: {str(e)}")
- import traceback
- self.log(traceback.format_exc())
- messagebox.showerror("错误", f"生成失败:\n{str(e)}")
- if __name__ == "__main__":
- root = tk.Tk()
- app = ExcelMapperApp(root)
- root.mainloop()
|