整理美团数据.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. import json
  2. import os
  3. import re
  4. import requests
  5. from pathlib import Path
  6. from urllib.parse import urlparse
  7. from tkinter import *
  8. from tkinter import ttk, filedialog, messagebox, scrolledtext
  9. import threading
  10. from datetime import datetime
  11. class ProductDownloaderGUI:
  12. def __init__(self, root):
  13. self.root = root
  14. self.root.title("商品资源下载器")
  15. self.root.geometry("900x650")
  16. self.root.resizable(True, True)
  17. # 设置样式
  18. style = ttk.Style()
  19. style.theme_use('clam')
  20. # 创建主框架
  21. main_frame = ttk.Frame(root, padding="10")
  22. main_frame.pack(fill=BOTH, expand=True)
  23. # 输入区域
  24. input_frame = ttk.LabelFrame(main_frame, text="商品数据输入", padding="10")
  25. input_frame.pack(fill=BOTH, expand=True, pady=(0, 10))
  26. # 输入框
  27. self.input_text = scrolledtext.ScrolledText(input_frame, height=18, font=("Consolas", 10))
  28. self.input_text.pack(fill=BOTH, expand=True)
  29. # 输出目录选择
  30. output_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="10")
  31. output_frame.pack(fill=X, pady=(0, 10))
  32. dir_select_frame = ttk.Frame(output_frame)
  33. dir_select_frame.pack(fill=X)
  34. self.output_dir = StringVar()
  35. self.output_entry = ttk.Entry(dir_select_frame, textvariable=self.output_dir)
  36. self.output_entry.pack(side=LEFT, fill=X, expand=True, padx=(0, 5))
  37. browse_btn = ttk.Button(dir_select_frame, text="浏览...", command=self.browse_output_dir)
  38. browse_btn.pack(side=RIGHT)
  39. # 进度条
  40. self.progress_var = DoubleVar()
  41. self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100)
  42. self.progress_bar.pack(fill=X, pady=(0, 10))
  43. # 状态标签
  44. self.status_label = ttk.Label(main_frame, text="就绪")
  45. self.status_label.pack(pady=(0, 10))
  46. # 按钮区域
  47. button_frame = ttk.Frame(main_frame)
  48. button_frame.pack(fill=X)
  49. # 启动按钮
  50. self.start_btn = ttk.Button(button_frame, text="▶ 启动下载", command=self.start_download, width=15)
  51. self.start_btn.pack(side=LEFT, padx=5)
  52. clear_btn = ttk.Button(button_frame, text="清空输入", command=self.clear_input)
  53. clear_btn.pack(side=LEFT, padx=5)
  54. # 退出按钮
  55. exit_btn = ttk.Button(button_frame, text="退出", command=self.root.quit)
  56. exit_btn.pack(side=RIGHT, padx=5)
  57. # 设置下载会话
  58. self.session = requests.Session()
  59. self.session.headers.update({
  60. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
  61. })
  62. def browse_output_dir(self):
  63. """选择输出目录"""
  64. directory = filedialog.askdirectory()
  65. if directory:
  66. self.output_dir.set(directory)
  67. def clear_input(self):
  68. """清空输入框"""
  69. self.input_text.delete(1.0, END)
  70. def extract_urls(self, data):
  71. """从JSON数据中提取图片和视频URL - 适配多种数据结构"""
  72. urls = {
  73. 'main_images': [], # 主图
  74. 'detail_images': [], # 详情图
  75. 'videos': [] # 视频
  76. }
  77. seen_urls = set()
  78. def add_url(url, url_type):
  79. """添加URL,避免重复"""
  80. if url and url not in seen_urls:
  81. seen_urls.add(url)
  82. if url_type == 'main':
  83. urls['main_images'].append(url)
  84. elif url_type == 'detail':
  85. urls['detail_images'].append(url)
  86. elif url_type == 'video':
  87. urls['videos'].append(url)
  88. try:
  89. # 获取data部分
  90. data_content = data.get('data', {})
  91. # 1. 提取主图 - pictures字段
  92. pictures = data_content.get('pictures', [])
  93. for pic in pictures:
  94. add_url(pic, 'main')
  95. # 2. 提取主图 - opt_pictures字段
  96. opt_pictures = data_content.get('opt_pictures', [])
  97. for pic in opt_pictures:
  98. add_url(pic, 'main')
  99. # 3. 从SKU中提取主图
  100. skus = data_content.get('skus', [])
  101. for sku in skus:
  102. sku_pic = sku.get('picture')
  103. if sku_pic:
  104. add_url(sku_pic, 'main')
  105. # 4. 提取详情图 - pic_content.contents
  106. pic_content = data_content.get('pic_content', {})
  107. contents = pic_content.get('contents', [])
  108. for content in contents:
  109. if content and isinstance(content, str):
  110. add_url(content, 'detail')
  111. # 5. 提取视频
  112. video_infos = data_content.get('product_video_infos', [])
  113. for video_info in video_infos:
  114. video_url = video_info.get('url')
  115. if video_url:
  116. add_url(video_url, 'video')
  117. # 视频封面图作为主图
  118. main_picture = video_info.get('mainPicture')
  119. if main_picture:
  120. add_url(main_picture, 'main')
  121. # 6. 使用正则表达式补充提取所有图片URL
  122. data_str = json.dumps(data, ensure_ascii=False)
  123. # 匹配图片URL
  124. image_pattern = r'https?://[^\s"\']+\.(?:jpg|jpeg|png|gif|webp)(?:\?[^\s"\']*)?'
  125. all_images = re.findall(image_pattern, data_str, re.IGNORECASE)
  126. # 视频匹配
  127. video_pattern = r'https?://[^\s"\']+\.(?:mp4|mov|avi|flv|webm)(?:\?[^\s"\']*)?'
  128. all_videos = re.findall(video_pattern, data_str, re.IGNORECASE)
  129. # 分类正则匹配到的图片
  130. for img_url in all_images:
  131. if img_url in seen_urls:
  132. continue
  133. # 根据URL特征分类
  134. if 'head_photo' in img_url or 'main' in img_url.lower() or 'sku' in img_url.lower():
  135. add_url(img_url, 'main')
  136. # 添加正则匹配到的视频
  137. for video_url in all_videos:
  138. if video_url not in seen_urls:
  139. add_url(video_url, 'video')
  140. except Exception as e:
  141. print(f"提取URL时出错: {e}")
  142. # 限制数量
  143. urls['main_images'] = urls['main_images'][:15]
  144. urls['detail_images'] = urls['detail_images'][:40]
  145. urls['videos'] = urls['videos'][:5]
  146. return urls
  147. def extract_product_info(self, data):
  148. """提取商品简要信息"""
  149. product_info = {
  150. 'id': '未知',
  151. 'name': '未命名商品',
  152. 'min_price': '未知',
  153. 'origin_price': '未知',
  154. 'month_saled': '未知',
  155. 'month_saled_content': '未知',
  156. 'sku_label': '无',
  157. 'brand': '未知',
  158. 'sku_count': 0,
  159. 'image_count': 0,
  160. 'video_count': 0,
  161. 'sku_list': [],
  162. 'standard_params': [],
  163. 'delivery_info': '',
  164. 'activity_infos': []
  165. }
  166. try:
  167. data_content = data.get('data', {})
  168. # 基础信息
  169. product_info['id'] = data_content.get('id', '未知')
  170. product_info['name'] = data_content.get('name', '未命名商品')
  171. product_info['min_price'] = data_content.get('min_price', '未知')
  172. product_info['month_saled_content'] = data_content.get('month_saled_content', '未知')
  173. # 提取划线价(从unify_price中获取)
  174. unify_price = data_content.get('unify_price', {})
  175. product_info['origin_price'] = unify_price.get('underlined_price', '未知')
  176. if product_info['origin_price'] == '未知':
  177. # 如果unify_price中没有,从skus中获取
  178. skus = data_content.get('skus', [])
  179. if skus:
  180. product_info['origin_price'] = skus[0].get('origin_price', '未知')
  181. # SKU信息
  182. skus = data_content.get('skus', [])
  183. product_info['sku_count'] = len(skus)
  184. product_info['sku_label'] = data_content.get('sku_label', '规格')
  185. # 提取每个SKU的详细信息
  186. for sku in skus:
  187. sku_info = {
  188. 'spec': sku.get('spec', ''),
  189. 'price': sku.get('price', ''),
  190. 'origin_price': sku.get('origin_price', ''),
  191. 'stock': sku.get('stock', ''),
  192. 'upccode': sku.get('upccode', ''),
  193. 'picture': sku.get('picture', '')
  194. }
  195. product_info['sku_list'].append(sku_info)
  196. # 提取标准参数
  197. standard_list = data_content.get('standard_productinfo_list', [])
  198. for item in standard_list:
  199. product_info['standard_params'].append({
  200. 'fieldName': item.get('fieldName', ''),
  201. 'value': item.get('value', '')
  202. })
  203. # 提取品牌(从标准参数中获取)
  204. for param in product_info['standard_params']:
  205. if param['fieldName'] == '品牌':
  206. product_info['brand'] = param['value']
  207. break
  208. if product_info['brand'] == '未知':
  209. # 如果标准参数中没有,尝试从名称中提取
  210. name = product_info['name']
  211. brand_keywords = ['农夫山泉', '可口可乐', '百事', '康师傅', '统一', '伊利', '蒙牛', '华为', '小米', '苹果']
  212. for brand in brand_keywords:
  213. if brand in name:
  214. product_info['brand'] = brand
  215. break
  216. # 图片数量
  217. pictures = data_content.get('pictures', [])
  218. product_info['image_count'] = len(pictures)
  219. # 视频数量
  220. video_infos = data_content.get('product_video_infos', [])
  221. product_info['video_count'] = len(video_infos)
  222. # 配送信息
  223. delivery_info = data_content.get('delivery_info', {})
  224. if delivery_info:
  225. product_info['delivery_info'] = delivery_info.get('delivery_content', '')
  226. # 活动信息
  227. activity_infos = data_content.get('activity_infos', [])
  228. for act in activity_infos:
  229. product_info['activity_infos'].append(act.get('activity_text', ''))
  230. except Exception as e:
  231. print(f"提取商品信息时出错: {e}")
  232. return product_info
  233. def save_product_info(self, product_info, output_path, urls):
  234. """保存商品简要信息到data.txt"""
  235. try:
  236. txt_path = output_path / 'data.txt'
  237. with open(txt_path, 'w', encoding='utf-8') as f:
  238. # 商品名称
  239. f.write(f"商品: {product_info.get('name', '未命名商品')}\n")
  240. # 价格信息
  241. min_price = product_info.get('min_price', '未知')
  242. origin_price = product_info.get('origin_price', '未知')
  243. if min_price != '未知' and origin_price != '未知':
  244. f.write(f"价格: {min_price}元起 (划线价 {origin_price}元)\n")
  245. # 计算折扣
  246. try:
  247. discount = int((1 - float(min_price) / float(origin_price)) * 100)
  248. if discount > 0:
  249. f.write(f"折扣: {discount}% OFF\n")
  250. except:
  251. pass
  252. elif min_price != '未知':
  253. f.write(f"价格: {min_price}元起\n")
  254. else:
  255. f.write(f"价格: 未知\n")
  256. # 销量
  257. month_saled = product_info.get('month_saled_content', '未知')
  258. f.write(f"销量: {month_saled}\n")
  259. f.write("\n")
  260. # SKU信息
  261. sku_list = product_info.get('sku_list', [])
  262. sku_label = product_info.get('sku_label', '规格')
  263. if sku_list:
  264. f.write(f"可选{sku_label} (共{len(sku_list)}种):\n")
  265. for i, sku in enumerate(sku_list, 1):
  266. spec = sku.get('spec', '')
  267. price = sku.get('price', '')
  268. stock = sku.get('stock', '')
  269. upccode = sku.get('upccode', '')
  270. f.write(f" {i}. {spec} - {price}元")
  271. if stock and stock != '未知' and stock != '':
  272. f.write(f" (库存{stock})")
  273. if upccode:
  274. f.write(f" 代码:{upccode}")
  275. f.write("\n")
  276. else:
  277. f.write(f"无{sku_label}信息\n")
  278. f.write("\n")
  279. # 参数信息
  280. standard_params = product_info.get('standard_params', [])
  281. if standard_params:
  282. params_str = " | ".join([f"{p['fieldName']}:{p['value']}" for p in standard_params])
  283. f.write(f"参数: {params_str}\n")
  284. f.write("\n")
  285. # 配送信息
  286. delivery_info = product_info.get('delivery_info', '')
  287. if delivery_info:
  288. f.write(f"配送: {delivery_info}\n")
  289. # 活动信息
  290. activity_infos = product_info.get('activity_infos', [])
  291. for act in activity_infos:
  292. if act:
  293. f.write(f"优惠: {act}\n")
  294. return True
  295. except Exception as e:
  296. print(f"保存data.txt失败: {e}")
  297. return False
  298. def download_file(self, url, filepath, timeout=30):
  299. """下载文件"""
  300. try:
  301. print(url)
  302. print(filepath)
  303. response = self.session.get(url, timeout=timeout, stream=True)
  304. if response.status_code == 200:
  305. with open(filepath, 'wb') as f:
  306. for chunk in response.iter_content(chunk_size=8192):
  307. if chunk:
  308. f.write(chunk)
  309. return True
  310. else:
  311. return False
  312. except Exception as e:
  313. print(f"下载失败 {url}: {e}")
  314. return False
  315. def sanitize_filename(self, name):
  316. """清理文件名中的非法字符"""
  317. name = re.sub(r'[<>:"/\\|?*]', '_', name)
  318. if len(name) > 100:
  319. name = name[:100]
  320. return name.strip()
  321. def download_resources(self, product_name, output_path, urls, progress_callback):
  322. """下载所有资源"""
  323. results = {
  324. 'success': True,
  325. 'downloaded': 0,
  326. 'total': 0,
  327. 'errors': []
  328. }
  329. total_items = len(urls['main_images']) + len(urls['detail_images']) + len(urls['videos'])
  330. # 下载主图
  331. for i, url in enumerate(urls['main_images'], 1):
  332. results['total'] += 1
  333. path = urlparse(url).path
  334. ext = os.path.splitext(path)[1] or '.jpg'
  335. if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
  336. ext = '.jpg'
  337. filename = f"zhutu_{i}{ext}"
  338. filepath = output_path / filename
  339. progress_callback(f"下载主图 {i}/{len(urls['main_images'])}...",
  340. int(results['total'] / total_items * 100) if total_items > 0 else 0)
  341. if self.download_file(url, filepath):
  342. results['downloaded'] += 1
  343. else:
  344. results['errors'].append(f"主图{i}: {url}")
  345. # 下载详情图
  346. for i, url in enumerate(urls['detail_images'], 1):
  347. results['total'] += 1
  348. path = urlparse(url).path
  349. ext = os.path.splitext(path)[1] or '.jpg'
  350. if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
  351. ext = '.jpg'
  352. filename = f"xiangqing_{i}{ext}"
  353. filepath = output_path / filename
  354. progress_callback(f"下载详情图 {i}/{len(urls['detail_images'])}...",
  355. int(results['total'] / total_items * 100) if total_items > 0 else 0)
  356. if self.download_file(url, filepath):
  357. results['downloaded'] += 1
  358. else:
  359. results['errors'].append(f"详情图{i}: {url}")
  360. # 下载视频
  361. for i, url in enumerate(urls['videos'], 1):
  362. results['total'] += 1
  363. path = urlparse(url).path
  364. ext = os.path.splitext(path)[1] or '.mp4'
  365. if ext not in ['.mp4', '.mov', '.avi', '.webm']:
  366. ext = '.mp4'
  367. filename = f"shipin_{i}{ext}"
  368. filepath = output_path / filename
  369. progress_callback(f"下载视频 {i}/{len(urls['videos'])}...",
  370. int(results['total'] / total_items * 100) if total_items > 0 else 0)
  371. if self.download_file(url, filepath):
  372. results['downloaded'] += 1
  373. else:
  374. results['errors'].append(f"视频{i}: {url}")
  375. return results
  376. def start_download(self):
  377. """启动下载任务"""
  378. input_data = self.input_text.get(1.0, END).strip()
  379. if not input_data:
  380. messagebox.showerror("错误", "请输入商品数据")
  381. return
  382. output_dir = self.output_dir.get()
  383. if not output_dir:
  384. messagebox.showerror("错误", "请选择输出目录")
  385. return
  386. self.start_btn.config(state=DISABLED, text="⏳ 下载中...")
  387. self.progress_var.set(0)
  388. thread = threading.Thread(target=self.download_task, args=(input_data, output_dir))
  389. thread.daemon = True
  390. thread.start()
  391. def download_task(self, input_data, output_dir):
  392. """下载任务"""
  393. try:
  394. # 解析JSON
  395. data = json.loads(input_data)
  396. # 提取商品信息
  397. product_info = self.extract_product_info(data)
  398. # 清理产品名作为文件夹名
  399. safe_name = self.sanitize_filename(product_info['name'])
  400. # 创建产品文件夹
  401. product_path = Path(output_dir) / safe_name
  402. product_path.mkdir(exist_ok=True)
  403. # 提取URL
  404. self.update_status_thread("正在分析数据,提取资源链接...")
  405. urls = self.extract_urls(data)
  406. # 保存商品简要信息到data.txt
  407. self.update_status_thread("正在保存商品信息...")
  408. if self.save_product_info(product_info, product_path, urls):
  409. self.update_status_thread("商品信息已保存")
  410. else:
  411. self.update_status_thread("保存商品信息失败")
  412. total_urls = len(urls['main_images']) + len(urls['detail_images']) + len(urls['videos'])
  413. if total_urls == 0:
  414. self.root.after(0, messagebox.showwarning, "警告", "未找到任何图片或视频资源\n但data.txt已保存")
  415. self.update_status_thread("未找到资源")
  416. self.enable_button_thread()
  417. return
  418. self.update_status_thread(f"发现 {len(urls['main_images'])} 张主图, {len(urls['detail_images'])} 张详情图, {len(urls['videos'])} 个视频")
  419. def update_progress(msg, progress):
  420. self.update_status_thread(msg)
  421. self.root.after(0, self.progress_var.set, progress)
  422. results = self.download_resources(safe_name, product_path, urls, update_progress)
  423. self.root.after(0, self.progress_var.set, 100)
  424. if results['downloaded'] > 0:
  425. msg = f"✅ 下载完成!\n\n成功下载 {results['downloaded']}/{results['total']} 个文件\n保存路径: {product_path}\n\n商品信息已保存为 data.txt"
  426. if results['errors']:
  427. msg += f"\n\n⚠️ 失败 {len(results['errors'])} 个文件"
  428. self.root.after(0, messagebox.showinfo, "完成", msg)
  429. self.update_status_thread(f"下载完成 - 成功 {results['downloaded']}/{results['total']}")
  430. else:
  431. self.root.after(0, messagebox.showerror, "错误", "下载失败,没有成功下载任何文件\n但data.txt已保存")
  432. self.update_status_thread("下载失败")
  433. except json.JSONDecodeError as e:
  434. self.root.after(0, messagebox.showerror, "JSON解析错误", f"输入的不是有效的JSON格式:\n{str(e)}")
  435. self.update_status_thread("JSON解析失败")
  436. except Exception as e:
  437. self.root.after(0, messagebox.showerror, "错误", f"处理过程中出现错误:\n{str(e)}")
  438. self.update_status_thread("处理失败")
  439. finally:
  440. self.enable_button_thread()
  441. def update_status_thread(self, message):
  442. """从线程更新状态"""
  443. self.root.after(0, lambda: self.status_label.config(text=message))
  444. def enable_button_thread(self):
  445. """从线程启用按钮"""
  446. self.root.after(0, lambda: self.start_btn.config(state=NORMAL, text="▶ 启动下载"))
  447. def main():
  448. root = Tk()
  449. app = ProductDownloaderGUI(root)
  450. root.mainloop()
  451. if __name__ == "__main__":
  452. main()