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

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