按照数据搜索整理文件.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import os
  2. import re
  3. import sys
  4. import shutil
  5. import threading
  6. import time
  7. import pyautogui
  8. import pyperclip
  9. import json
  10. from pathlib import Path
  11. from tkinter import *
  12. from tkinter import ttk, filedialog, messagebox, scrolledtext
  13. objTemp = {}
  14. # 配置文件路径
  15. CONFIG_FILE = Path.home() / '.file_search_copy_config.json'
  16. def load_config():
  17. """加载保存的配置"""
  18. if CONFIG_FILE.exists():
  19. try:
  20. with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
  21. return json.load(f)
  22. except:
  23. return {}
  24. return {}
  25. def save_config(search_dir, output_dir):
  26. """保存配置"""
  27. try:
  28. config = {
  29. 'search_dir': search_dir,
  30. 'output_dir': output_dir,
  31. 'last_update': time.strftime("%Y-%m-%d %H:%M:%S")
  32. }
  33. with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
  34. json.dump(config, f, ensure_ascii=False, indent=2)
  35. return True
  36. except Exception as e:
  37. print(f"保存配置失败: {e}")
  38. return False
  39. def parse_jit_to_dict(text: str) -> dict:
  40. """
  41. 将 JIT 格式的物流文本解析为 {订单号: 车次号} 的字典
  42. 参数:
  43. text: 原始字符串内容
  44. 返回:
  45. dict: 订单号到车次号的映射
  46. """
  47. result = {}
  48. # 按行分割,每一行是一个完整的 JIT 记录
  49. lines = text.strip().split('\n')
  50. for line in lines:
  51. if not line.strip():
  52. continue
  53. # 用制表符分割字段(JIT 格式用 \t 分隔)
  54. fields = line.split('\t')
  55. if len(fields) < 3:
  56. continue
  57. # 第一个字段:可能包含一个或多个订单号(逗号分隔)
  58. order_field = fields[0].strip()
  59. # 第三个字段:车次号 SC202606030063 这种
  60. shipment_no = fields[2].strip() if len(fields) > 2 else ''
  61. if not shipment_no:
  62. continue
  63. # 处理订单号可能包含逗号分隔的情况
  64. # 注意:有些订单号后面会有 ; 结尾,先去掉
  65. # 订单号的格式通常是 POCY... 可能带 S01 后缀
  66. # 方式1:按逗号拆分订单号
  67. # 每个订单号格式类似 POCY2606030080586S01
  68. # 可能有最后一个订单号不完整的情况,这里取完整的标准订单号
  69. # 提取标准格式的订单号(POCY开头,后面数字+S或数字)
  70. # 先用正则提取所有 POCY 开头的订单号
  71. order_ids = re.findall(r'(POCY\w+)', order_field)
  72. # 如果没找到标准 POCY 订单号,尝试按逗号直接拆分
  73. if not order_ids:
  74. # 按逗号拆分,并清理可能的分号和空白
  75. parts = [p.strip().rstrip(';') for p in order_field.split(',') if p.strip()]
  76. for p in parts:
  77. # 如果部分看起来像是订单号(长度大于10)
  78. if len(p) > 10 and not p.isdigit():
  79. order_ids.append(p)
  80. # 映射到车次号
  81. for oid in order_ids:
  82. if oid: # 非空
  83. result[oid] = shipment_no
  84. return result
  85. class FileSearchCopyApp:
  86. def __init__(self, root):
  87. self.root = root
  88. self.root.title("文件搜索复制工具")
  89. self.root.geometry("950x800")
  90. # 加载保存的配置
  91. self.saved_config = load_config()
  92. # 设置样式
  93. style = ttk.Style()
  94. style.theme_use('clam')
  95. # 主框架
  96. main_frame = ttk.Frame(root, padding="10")
  97. main_frame.grid(row=0, column=0, sticky=(W, E, N, S))
  98. # 搜索目录选择
  99. ttk.Label(main_frame, text="搜索目录:").grid(row=0, column=0, sticky=W, pady=5)
  100. self.search_dir = StringVar()
  101. # 自动填充保存的路径
  102. if self.saved_config.get('search_dir'):
  103. self.search_dir.set(self.saved_config['search_dir'])
  104. ttk.Entry(main_frame, textvariable=self.search_dir, width=80).grid(row=0, column=1, padx=5, pady=5)
  105. ttk.Button(main_frame, text="浏览", command=self.select_search_dir).grid(row=0, column=2, padx=5, pady=5)
  106. # 输出目录选择
  107. ttk.Label(main_frame, text="输出目录:").grid(row=1, column=0, sticky=W, pady=5)
  108. self.output_dir = StringVar()
  109. # 自动填充保存的路径
  110. if self.saved_config.get('output_dir'):
  111. self.output_dir.set(self.saved_config['output_dir'])
  112. ttk.Entry(main_frame, textvariable=self.output_dir, width=80).grid(row=1, column=1, padx=5, pady=5)
  113. ttk.Button(main_frame, text="浏览", command=self.select_output_dir).grid(row=1, column=2, padx=5, pady=5)
  114. # 自动获取数据按钮
  115. auto_frame = ttk.Frame(main_frame)
  116. auto_frame.grid(row=2, column=0, columnspan=3, pady=10)
  117. self.auto_button = ttk.Button(auto_frame, text="🤖 自动获取数据", command=self.auto_get_data, width=20)
  118. self.auto_button.pack()
  119. # 数据输入区域
  120. ttk.Label(main_frame, text="数据输入(制表符分隔,每行一条记录,提取第3列):").grid(row=3, column=0, columnspan=3, sticky=W, pady=5)
  121. self.text_area = scrolledtext.ScrolledText(main_frame, height=12, width=110, font=('Consolas', 9))
  122. self.text_area.grid(row=4, column=0, columnspan=3, pady=5)
  123. # 按钮框架
  124. button_frame = ttk.Frame(main_frame)
  125. button_frame.grid(row=5, column=0, columnspan=3, pady=10)
  126. ttk.Button(button_frame, text="加载示例数据", command=self.load_example).pack(side=LEFT, padx=5)
  127. ttk.Button(button_frame, text="清空数据", command=self.clear_data).pack(side=LEFT, padx=5)
  128. ttk.Button(button_frame, text="清空日志", command=self.clear_log).pack(side=LEFT, padx=5)
  129. # 启动按钮
  130. self.start_button = ttk.Button(button_frame, text="启动搜索复制", command=self.start_process, width=20)
  131. self.start_button.pack(side=LEFT, padx=20)
  132. # 进度条
  133. self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
  134. self.progress.grid(row=6, column=0, columnspan=3, sticky=(W, E), pady=5)
  135. # 统计信息框架
  136. stats_frame = ttk.LabelFrame(main_frame, text="统计信息", padding="5")
  137. stats_frame.grid(row=7, column=0, columnspan=3, sticky=(W, E), pady=5)
  138. self.stats_label = ttk.Label(stats_frame, text="等待处理...")
  139. self.stats_label.pack()
  140. # 日志输出区域
  141. ttk.Label(main_frame, text="运行日志:").grid(row=8, column=0, columnspan=3, sticky=W, pady=5)
  142. self.log_area = scrolledtext.ScrolledText(main_frame, height=15, width=110, fg='blue')
  143. self.log_area.grid(row=9, column=0, columnspan=3, pady=5)
  144. # 配置网格权重
  145. root.columnconfigure(0, weight=1)
  146. root.rowconfigure(0, weight=1)
  147. main_frame.columnconfigure(1, weight=1)
  148. main_frame.rowconfigure(4, weight=1)
  149. main_frame.rowconfigure(9, weight=1)
  150. def select_search_dir(self):
  151. directory = filedialog.askdirectory()
  152. if directory:
  153. self.search_dir.set(directory)
  154. # 保存配置
  155. save_config(self.search_dir.get(), self.output_dir.get())
  156. def select_output_dir(self):
  157. directory = filedialog.askdirectory()
  158. if directory:
  159. self.output_dir.set(directory)
  160. # 保存配置
  161. save_config(self.search_dir.get(), self.output_dir.get())
  162. def clear_data(self):
  163. self.text_area.delete(1.0, END)
  164. def clear_log(self):
  165. self.log_area.delete(1.0, END)
  166. def log_message(self, message, is_error=False):
  167. """在日志区域显示消息"""
  168. timestamp = time.strftime("%H:%M:%S")
  169. self.log_area.insert(END, f"[{timestamp}] {message}\n")
  170. self.log_area.see(END)
  171. if is_error:
  172. # 可以添加错误计数或其他处理
  173. pass
  174. def auto_get_data(self):
  175. """自动获取数据:点击指定位置,然后获取剪贴板内容"""
  176. # 在新线程中执行,避免界面卡顿
  177. thread = threading.Thread(target=self._auto_get_data_thread)
  178. thread.daemon = True
  179. thread.start()
  180. def _auto_get_data_thread(self):
  181. global objTemp
  182. """自动获取数据的线程函数"""
  183. try:
  184. # 禁用按钮,防止重复点击
  185. self.auto_button.config(state=DISABLED)
  186. self.log_message("开始自动获取数据...")
  187. self.log_message("请在5秒内切换到目标窗口...")
  188. # 等待5秒,让用户切换到目标窗口
  189. for i in range(5, 0, -1):
  190. self.log_message(f"倒计时: {i} 秒")
  191. time.sleep(1)
  192. # pyautogui.moveTo(389, 290, duration=0.3)
  193. # pyautogui.click(button="right")
  194. # time.sleep(0.5)
  195. # pyautogui.moveTo(482, 472, duration=0.3)
  196. # time.sleep(1)
  197. # pyautogui.moveTo(606, 472, duration=0.3)
  198. # pyautogui.click(button="left")
  199. # time.sleep(1)
  200. # 获取剪贴板内容
  201. # clipboard_content2 = pyperclip.paste()
  202. # objTemp = parse_jit_to_dict(clipboard_content2)
  203. # 定义点击位置
  204. click_positions = [
  205. (55, 663, "第一个位置", "left"),
  206. (115, 719, "第一个位置", "right"),
  207. (213, 897, "第二个位置", "left"),
  208. (334, 897, "第三个位置", "left")
  209. ]
  210. # 执行点击操作
  211. for x, y, description, button in click_positions:
  212. self.log_message(f"准备点击{description}: ({x}, {y})")
  213. time.sleep(0.5) # 间隔0.5秒
  214. # 移动并点击
  215. pyautogui.moveTo(x, y, duration=0.3)
  216. pyautogui.click(button=button)
  217. self.log_message(f"✓ 已点击{description}: ({x}, {y})")
  218. # 等待一下确保复制操作完成
  219. time.sleep(0.5)
  220. # 获取剪贴板内容
  221. clipboard_content = pyperclip.paste()
  222. if clipboard_content:
  223. self.log_message(f"成功获取剪贴板内容,长度: {len(clipboard_content)} 字符")
  224. self.log_message("内容预览(前200字符):")
  225. preview = clipboard_content[:200] + "..." if len(clipboard_content) > 200 else clipboard_content
  226. self.log_message(preview)
  227. # 设置到输入框
  228. self.text_area.delete(1.0, END)
  229. self.text_area.insert(1.0, clipboard_content)
  230. self.log_message("✓ 数据已设置到输入框")
  231. # ========== 新增:等待1秒后自动启动搜索复制 ==========
  232. self.log_message("等待1秒后自动启动搜索复制...")
  233. time.sleep(1)
  234. # 在主线程中启动搜索复制(因为涉及UI更新)
  235. self.root.after(0, self.auto_start_process)
  236. # ==============================================
  237. else:
  238. self.log_message("警告:剪贴板内容为空!", True)
  239. except pyautogui.FailSafeException:
  240. self.log_message("操作被用户中断(触发了紧急停止)", True)
  241. except Exception as e:
  242. self.log_message(f"自动获取数据失败: {str(e)}", True)
  243. finally:
  244. self.auto_button.config(state=NORMAL)
  245. self.log_message("自动获取数据流程结束")
  246. def auto_start_process(self):
  247. """自动启动搜索复制(由自动获取数据完成后调用)"""
  248. self.log_message("=" * 60)
  249. self.log_message("【自动启动】开始执行搜索复制任务")
  250. self.log_message("=" * 60)
  251. # 直接调用启动处理(会在新线程中运行)
  252. self.start_process()
  253. def load_example(self):
  254. example_data = """SCSQ202605260234 POCY2605260119298S01 S25092303841500101 S250923038415 8条装深浅条纹套装(4方巾+2毛巾+2浴巾) 8条装灰色 1 https://p16-she-sg.ibyteimg.com/tos-alisg-i-f7lenjegsh-sg/7e711c20b2ff4205910b659d75fd2d4e~tplv-f7lenjegsh-origin-webp.webp?dr=16384&from=3626923735&idc=sg1&ps=933b5bde&shcp=3c3d9ffb&shp=d82e76d7&t=16 0 灰+2*深浅条纹(1毛巾+2方巾+1浴巾) 25042214242424 0 灰+2*深浅条纹(1毛巾+2方巾+1浴巾) 2504221424 Gray 8pcs-2bath towel+2towel+4hand towel        
  255. SCSQ202605260219 POCY2605260123778S01 S25122500884400102 S251225008844 80*210cm拼接浴裙+公主帽套装 粉色 1 https://p16-she-sg.ibyteimg.com/tos-alisg-i-f7lenjegsh-sg/85be0d0d66ab46c6bb5ccb056fc02eb3~tplv-f7lenjegsh-origin-webp.webp?dr=16384&from=3626923735&idc=sg1&ps=933b5bde&shcp=3c3d9ffb&shp=d82e76d7&t=16 0 Pink And White-XXXL-XXXXL 26010529164844 0 Pink And White-XXXL-XXXXL 2601052916 Pink And White XXXL-XXXXL.       """
  256. self.text_area.delete(1.0, END)
  257. self.text_area.insert(1.0, example_data)
  258. def update_stats(self, total_orders, processed_orders, total_files):
  259. """更新统计信息"""
  260. self.stats_label.config(text=f"总订单数: {total_orders} | 已处理: {processed_orders} | 复制文件数: {total_files}")
  261. def search_files(self, search_dir, order_number):
  262. """在搜索目录中搜索包含订单号的文件"""
  263. found_files = []
  264. search_path = Path(search_dir)
  265. try:
  266. # 遍历所有文件
  267. for file_path in search_path.rglob('*'):
  268. if file_path.is_file() and order_number in file_path.name:
  269. found_files.append(file_path)
  270. except Exception as e:
  271. self.log_message(f"搜索 {order_number} 时出错: {str(e)}", True)
  272. return found_files
  273. def copy_files(self, files, output_dir, order_number):
  274. """复制文件到目标目录"""
  275. # 创建订单号目录
  276. target_dir = Path(output_dir) / order_number
  277. target_dir.mkdir(parents=True, exist_ok=True)
  278. copied_count = 0
  279. for file_path in files:
  280. try:
  281. # 构建目标文件路径
  282. target_file = target_dir / file_path.name
  283. # 如果文件已存在,添加序号
  284. if target_file.exists():
  285. base_name = file_path.stem
  286. extension = file_path.suffix
  287. counter = 1
  288. while target_file.exists():
  289. new_name = f"{base_name}_{counter}{extension}"
  290. target_file = target_dir / new_name
  291. counter += 1
  292. # 复制文件
  293. shutil.copy2(file_path, target_file)
  294. copied_count += 1
  295. self.log_message(f" 已复制: {file_path.name} -> {target_file}")
  296. except Exception as e:
  297. self.log_message(f" 复制失败 {file_path.name}: {str(e)}", True)
  298. return copied_count
  299. def process_data(self):
  300. """处理数据的主函数"""
  301. search_dir = self.search_dir.get()
  302. output_dir = self.output_dir.get()
  303. data_text = self.text_area.get(1.0, END).strip()
  304. # 验证输入
  305. if not search_dir:
  306. self.log_message("错误:请选择搜索目录!", True)
  307. return False
  308. if not output_dir:
  309. self.log_message("错误:请选择输出目录!", True)
  310. return False
  311. if not data_text:
  312. self.log_message("错误:请在数据输入框中输入数据!", True)
  313. return False
  314. if not os.path.exists(search_dir):
  315. self.log_message(f"错误:搜索目录不存在! {search_dir}", True)
  316. return False
  317. if not os.path.exists(output_dir):
  318. self.log_message(f"错误:输出目录不存在! {output_dir}", True)
  319. return False
  320. # 解析数据,提取订单号(每行的第3列)
  321. lines = data_text.split('\n')
  322. order_numbers = [] # 保留顺序
  323. order_numbers_set = set() # 用于去重
  324. 平台单号列表 = {}
  325. for line_num, line in enumerate(lines, 1):
  326. if not line.strip():
  327. continue
  328. # 按制表符分割
  329. parts = line.split('\t')
  330. if len(parts) >= 3:
  331. order_number = parts[2].strip() # 第3列(索引2)
  332. 平台单号 = parts[1].strip() # 第2列(索引1)
  333. 平台单号列表[order_number] = 平台单号
  334. if order_number:
  335. if order_number not in order_numbers_set:
  336. order_numbers.append(order_number)
  337. order_numbers_set.add(order_number)
  338. self.log_message(f"提取订单号 [{len(order_numbers)}]: {order_number}")
  339. else:
  340. self.log_message(f"跳过重复订单号: {order_number}")
  341. else:
  342. self.log_message(f"警告:第{line_num}行第3列为空")
  343. else:
  344. self.log_message(f"警告:第{line_num}行格式不正确,字段数不足3个(当前{len(parts)}个字段)")
  345. if not order_numbers:
  346. self.log_message("错误:未能提取到有效的订单号!", True)
  347. return False
  348. self.log_message(f"\n共提取到 {len(order_numbers)} 个唯一的订单号")
  349. self.log_message("=" * 80)
  350. # 处理每个订单号
  351. total_files_copied = 0
  352. processed_orders = 0
  353. for i, order_number in enumerate(order_numbers, 1):
  354. self.log_message(f"\n[{i}/{len(order_numbers)}] 处理订单号: {order_number}")
  355. self.log_message(f"搜索目录: {search_dir}")
  356. # 搜索文件
  357. found_files = self.search_files(search_dir, order_number)
  358. if found_files:
  359. self.log_message(f"找到 {len(found_files)} 个相关文件:")
  360. for file_path in found_files:
  361. try:
  362. rel_path = file_path.relative_to(search_dir)
  363. self.log_message(f" - {rel_path}")
  364. except:
  365. self.log_message(f" - {file_path}")
  366. # 复制文件
  367. copied_count = self.copy_files(found_files, output_dir, order_number)
  368. total_files_copied += copied_count
  369. self.log_message(f"✓ 成功复制 {copied_count} 个文件到 {output_dir}/{order_number}")
  370. processed_orders += 1
  371. else:
  372. self.log_message(f"✗ 警告:未找到包含 {order_number} 的文件", True)
  373. # 更新统计信息
  374. self.update_stats(len(order_numbers), processed_orders, total_files_copied)
  375. self.log_message("\n" + "=" * 80)
  376. self.log_message("处理完成!")
  377. self.log_message(f"总订单数: {len(order_numbers)}")
  378. self.log_message(f"成功处理: {processed_orders}")
  379. self.log_message(f"失败/未找到: {len(order_numbers) - processed_orders}")
  380. self.log_message(f"共复制文件: {total_files_copied}")
  381. return True
  382. def run_in_thread(self):
  383. """在新线程中运行处理程序"""
  384. self.start_button.config(state=DISABLED)
  385. self.progress.start()
  386. try:
  387. success = self.process_data()
  388. if success:
  389. messagebox.showinfo("完成", "文件搜索复制完成!")
  390. else:
  391. messagebox.showerror("错误", "处理过程中出现错误,请查看日志。")
  392. except Exception as e:
  393. self.log_message(f"程序运行错误: {str(e)}", True)
  394. messagebox.showerror("错误", f"程序运行错误: {str(e)}")
  395. finally:
  396. self.progress.stop()
  397. self.start_button.config(state=NORMAL)
  398. def start_process(self):
  399. """启动处理程序"""
  400. # 在新线程中运行,避免界面卡顿
  401. thread = threading.Thread(target=self.run_in_thread)
  402. thread.daemon = True
  403. thread.start()
  404. def main():
  405. root = Tk()
  406. app = FileSearchCopyApp(root)
  407. root.mainloop()
  408. if __name__ == "__main__":
  409. main()