飞书提交数据.py 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186
  1. import tkinter as tk
  2. from tkinter import ttk, filedialog, messagebox, scrolledtext
  3. import pandas as pd
  4. import requests
  5. import json
  6. import threading
  7. import time
  8. import random
  9. import re
  10. from datetime import datetime
  11. import os
  12. import shutil
  13. from openpyxl import load_workbook
  14. from openpyxl.utils import get_column_letter
  15. from PIL import Image
  16. import io
  17. import base64
  18. import zlib
  19. class FeishuSubmitApp:
  20. def __init__(self, root):
  21. self.root = root
  22. self.root.title("飞书表格数据提交工具 - 自动提取图片")
  23. self.root.geometry("1100x800")
  24. # 状态变量
  25. self.is_running = False
  26. self.stop_flag = False
  27. self.excel_file = ""
  28. self.cookie_string = ""
  29. # 图片缓存
  30. self.image_cache = {} # {行号: [图片数据列表]}
  31. self.image_extract_folder = None
  32. self.uploaded_cache = {} # 缓存已上传的图片token {行号: attachment_info}
  33. # 从curl提取的默认配置
  34. self.default_headers = {
  35. "accept": "application/json, text/plain, */*",
  36. "accept-language": "zh-CN,zh;q=0.9",
  37. "content-type": "application/json",
  38. "f-version": "docs-6-17-1781628518399",
  39. "origin": "https://xhd.feishu.cn",
  40. "priority": "u=1, i",
  41. "referer": "https://xhd.feishu.cn/share/base/form/shrcnJqFUFF0sqlmrfsWpDNf8df",
  42. "sec-ch-ua": '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
  43. "sec-ch-ua-mobile": "?0",
  44. "sec-ch-ua-platform": '"Windows"',
  45. "sec-fetch-dest": "empty",
  46. "sec-fetch-mode": "cors",
  47. "sec-fetch-site": "same-origin",
  48. "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
  49. "x-csrftoken": "83aae12269bced10680d3ccf817f82262305e7d0-1781579108",
  50. "x-tt-logid": "02178275091573600000000000000000000ffff5bd640caae0cb8",
  51. "x-tt-trace": "1"
  52. }
  53. self.url = "https://xhd.feishu.cn/space/api/bitable/share/content"
  54. self.share_token = "shrcnJqFUFF0sqlmrfsWpDNf8df"
  55. self.setup_ui()
  56. self.load_config()
  57. def setup_ui(self):
  58. """构建界面"""
  59. # 主容器
  60. main_frame = ttk.Frame(self.root, padding="10")
  61. main_frame.pack(fill=tk.BOTH, expand=True)
  62. # ========== 第一行:文件选择 ==========
  63. file_frame = ttk.LabelFrame(main_frame, text="📁 数据文件", padding="10")
  64. file_frame.pack(fill=tk.X, pady=(0, 10))
  65. ttk.Label(file_frame, text="Excel文件:").grid(row=0, column=0, sticky=tk.W)
  66. self.file_path_var = tk.StringVar()
  67. ttk.Entry(file_frame, textvariable=self.file_path_var, width=60).grid(row=0, column=1, padx=(10, 10))
  68. ttk.Button(file_frame, text="浏览...", command=self.select_file).grid(row=0, column=2)
  69. ttk.Label(file_frame, text="工作表:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0))
  70. self.sheet_var = tk.StringVar(value="Sheet1")
  71. ttk.Entry(file_frame, textvariable=self.sheet_var, width=20).grid(row=1, column=1, sticky=tk.W, padx=(10, 10), pady=(10, 0))
  72. ttk.Label(file_frame, text="(默认: Sheet1)").grid(row=1, column=2, sticky=tk.W, pady=(10, 0))
  73. # ========== 第二行:Cookie配置 ==========
  74. cookie_frame = ttk.LabelFrame(main_frame, text="🍪 Cookie 配置", padding="10")
  75. cookie_frame.pack(fill=tk.X, pady=(0, 10))
  76. ttk.Label(cookie_frame, text="Cookie:").grid(row=0, column=0, sticky=tk.W)
  77. self.cookie_text = tk.Text(cookie_frame, height=4, width=80, wrap=tk.WORD)
  78. self.cookie_text.grid(row=0, column=1, padx=(10, 10), pady=(0, 5), sticky=tk.W)
  79. cookie_btn_frame = ttk.Frame(cookie_frame)
  80. cookie_btn_frame.grid(row=1, column=1, sticky=tk.W, padx=(10, 0))
  81. ttk.Button(cookie_btn_frame, text="从剪贴板粘贴", command=self.paste_cookie).pack(side=tk.LEFT, padx=(0, 10))
  82. ttk.Button(cookie_btn_frame, text="保存配置", command=self.save_config).pack(side=tk.LEFT, padx=(0, 10))
  83. ttk.Button(cookie_btn_frame, text="加载默认", command=self.load_default_cookie).pack(side=tk.LEFT)
  84. # ========== 第三行:发送控制 ==========
  85. control_frame = ttk.LabelFrame(main_frame, text="⚙️ 发送控制", padding="10")
  86. control_frame.pack(fill=tk.X, pady=(0, 10))
  87. # 间隔配置
  88. ttk.Label(control_frame, text="最小间隔(秒):").grid(row=0, column=0, sticky=tk.W)
  89. self.min_interval_var = tk.StringVar(value="20")
  90. ttk.Entry(control_frame, textvariable=self.min_interval_var, width=10).grid(row=0, column=1, padx=(5, 15), sticky=tk.W)
  91. ttk.Label(control_frame, text="最大间隔(秒):").grid(row=0, column=2, sticky=tk.W)
  92. self.max_interval_var = tk.StringVar(value="50")
  93. ttk.Entry(control_frame, textvariable=self.max_interval_var, width=10).grid(row=0, column=3, padx=(5, 15), sticky=tk.W)
  94. ttk.Label(control_frame, text="起始行:").grid(row=0, column=4, sticky=tk.W)
  95. self.start_row_var = tk.StringVar(value="2")
  96. ttk.Entry(control_frame, textvariable=self.start_row_var, width=8).grid(row=0, column=5, padx=(5, 15), sticky=tk.W)
  97. ttk.Label(control_frame, text="(表头在第1行)").grid(row=0, column=6, sticky=tk.W)
  98. # 操作按钮
  99. btn_frame = ttk.Frame(control_frame)
  100. btn_frame.grid(row=1, column=0, columnspan=7, pady=(15, 0), sticky=tk.W)
  101. self.start_btn = ttk.Button(btn_frame, text="🚀 开始发送", command=self.start_send)
  102. self.start_btn.pack(side=tk.LEFT, padx=(0, 10))
  103. self.stop_btn = ttk.Button(btn_frame, text="⏹ 停止", command=self.stop_send, state=tk.DISABLED)
  104. self.stop_btn.pack(side=tk.LEFT, padx=(0, 10))
  105. ttk.Button(btn_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT, padx=(0, 10))
  106. ttk.Button(btn_frame, text="📸 提取图片", command=self.extract_images).pack(side=tk.LEFT)
  107. # 状态显示
  108. self.status_var = tk.StringVar(value="就绪")
  109. ttk.Label(control_frame, textvariable=self.status_var, foreground="blue").grid(row=2, column=0, columnspan=7, pady=(10, 0), sticky=tk.W)
  110. # ========== 日志显示 ==========
  111. log_frame = ttk.LabelFrame(main_frame, text="📋 发送日志", padding="10")
  112. log_frame.pack(fill=tk.BOTH, expand=True)
  113. self.log_text = scrolledtext.ScrolledText(log_frame, height=20, wrap=tk.WORD, font=("Consolas", 9))
  114. self.log_text.pack(fill=tk.BOTH, expand=True)
  115. # 日志颜色标签
  116. self.log_text.tag_config("success", foreground="green")
  117. self.log_text.tag_config("error", foreground="red")
  118. self.log_text.tag_config("info", foreground="blue")
  119. self.log_text.tag_config("warning", foreground="orange")
  120. self.log_text.tag_config("debug", foreground="purple")
  121. # ========== 底部状态栏 ==========
  122. status_bar = ttk.Frame(self.root)
  123. status_bar.pack(fill=tk.X, side=tk.BOTTOM)
  124. ttk.Label(status_bar, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W).pack(fill=tk.X, padx=5, pady=2)
  125. def select_file(self):
  126. """选择Excel文件"""
  127. file_path = filedialog.askopenfilename(
  128. title="选择Excel文件",
  129. filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
  130. )
  131. if file_path:
  132. self.file_path_var.set(file_path)
  133. self.excel_file = file_path
  134. self.log(f"已选择文件: {file_path}", "info")
  135. # 尝试自动识别工作表
  136. try:
  137. xl = pd.ExcelFile(file_path)
  138. sheets = xl.sheet_names
  139. if sheets:
  140. self.sheet_var.set(sheets[0])
  141. self.log(f"自动选择工作表: {sheets[0]}", "info")
  142. except Exception as e:
  143. self.log(f"读取工作表失败: {str(e)}", "warning")
  144. def paste_cookie(self):
  145. """从剪贴板粘贴Cookie"""
  146. try:
  147. clipboard_text = self.root.clipboard_get()
  148. if "cookie:" in clipboard_text.lower() or "-H" in clipboard_text:
  149. cookie_match = re.search(r'-b\s+"([^"]+)"', clipboard_text)
  150. if not cookie_match:
  151. cookie_match = re.search(r'-H\s+"cookie:\s*([^"]+)"', clipboard_text, re.IGNORECASE)
  152. if cookie_match:
  153. self.cookie_text.delete(1.0, tk.END)
  154. self.cookie_text.insert(1.0, cookie_match.group(1))
  155. self.log("已从剪贴板提取Cookie", "success")
  156. return
  157. self.cookie_text.delete(1.0, tk.END)
  158. self.cookie_text.insert(1.0, clipboard_text)
  159. self.log("已粘贴Cookie", "info")
  160. except:
  161. messagebox.showwarning("提示", "无法读取剪贴板内容")
  162. def load_default_cookie(self):
  163. """加载默认Cookie"""
  164. default_cookie = "_uuid_hera_ab_path_1=7651649498397052109; __tea__ug__uid=4144561781538482924; _gcl_au=1.1.775810441.1781538483; lgw_csrf_token=9dcbe9c422a1a2df5727bebebf4136760e1ad6dc-1781538485; passport_web_did=7651823325329411260; passport_trace_id=7651823325353757894; QXV0aHpDb250ZXh0=ba539bcfe4a042c8bc1e99d4d03add38; s_v_web_id=verify_mqg251ox_Tp4ofGaR_NCf4_4zII_8Ad3_c3HxGb4PXIuN; session=XN0YXJ0-5b9t9966-1122-4520-890c-e9313b1f818a-WVuZA; session_list=XN0YXJ0-5b9t9966-1122-4520-890c-e9313b1f818a-WVuZA; login_recently=1; is_anonymous_session=; _csrf_token=83aae12269bced10680d3ccf817f82262305e7d0-1781579108; lang=zh; et=c0c63f4480fdf14d7e6e9a21bab2b7e1; ccm_cdn_host=//lf-package-cn.feishucdn.com/obj/feishu-static; locale=zh-CN; landing_url=https://www.feishu.cn/; Hm_lvt_a79616d9322d81f12a92402ac6ae32ea=1781538483,1781579751; Hm_lpvt_a79616d9322d81f12a92402ac6ae32ea=1781579751; HMACCOUNT=5CA5EB76287246C7; _ga_VPYRHN104D=GS2.1.s1781579004$o2$g1$t1781579751$j60$l0$h0; _uetvid=9877555068d111f1992d03b61d301033; _ga=GA1.2.692261332.1781538484; i18n_locale=zh-CN; i18n_locale=zh-CN; _gid=GA1.2.1918132955.1781749497; site_env=pre=0; sl_session=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODE3OTMyNTgsInVuaXQiOiJldV9uYyIsInJhdyI6eyJtZXRhIjoiQVdvYzIrdU1BNHZSYUhkQjl4N2lRQUpxTUx6RjlrN012R293dk1YMlRzeThhakM5WXdRQURMSUNLZ0VBUVVGQlFVRkJRVUZCUVVKeFRVd3hha2h6UkUxNFVUMDkiLCJzdW0iOiI1MWU3ZWM3YzU0NTAzNTljMzRhMjE3ODdlM2U1OGNmZjQzNzZmM2IzYjJjNzU4NWVjMGE3MjQyZDZjNTBiNmNkIiwibG9jIjoiemhfY24iLCJhcGMiOiJSZWxlYXNlIiwiaWF0IjoxNzgxNzUwMDU4LCJzYWMiOnsiVXNlclN0YWZmU3RhdHVzIjoiMSIsIlVzZXJUeXBlIjoiNDIifSwibG9kIjpudWxsLCJjbmYiOnsiamt0Ijoic3VWaTdnNDNJX0lUb0w2ZGo5QXhEWnBacUhkYldzNWJaaVhvV0s4akZrdyJ9LCJucyI6ImxhcmsiLCJuc191aWQiOiI3NjQ2MjI4MDcyMDgxMDk5NzI5IiwibnNfdGlkIjoiNzUyNzU1NzgzMTgwMzk0NDk2MiIsIm90IjoxLCJjdCI6MTc4MTU3OTEwNywicnQiOjE3ODE2Njg4OTMsInRjcyI6MiwidGJ0IjowfX0.6MrIkLX5grSs5PUBr3U-TC5keonxZZDADzbJ0c00y9HsalJhhQ0gbmfk2u2w5RWhMkXt46a50yMc9E7Po6kymg; msToken=Oyg-XUAwfnJlSSSvRBdj1W_LxhRCXLlWMx9mk0Ryxvku6FZLH0ysyeYnmGrIG_yvUClyZT2cLYe5rhC1PtVygRRXOhYTvrDcL8Oidp-ZHHI-LNFKPJk-She-3kUxNTnwb3fYj5FfI36vX-1eFZ_neFVmoTtgD1GhK3M3oNjh5H-Y; swp_csrf_token=6fe8dbf3-4ab2-4f46-b4f3-5c9cba7364cd; t_beda37=a427edf4f48d9fa049a454a58600fa2f188ffacaf053a85e3782d6615e737b03; passport_app_access_token=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODE3OTU2NDcsInVuaXQiOiJldV9uYyIsInJhdyI6eyJtX2FjY2Vzc19pbmZvIjp7IjE0MyI6eyJpYXQiOjE3ODE3NDk1MTksImFjY2VzcyI6dHJ1ZX0sIjIiOnsiaWF0IjoxNzgxNzUyMTY1LCJhY2Nlc3MiOnRydWV9LCIxMDAiOnsiaWF0IjoxNzgxNzUyMjk1LCJhY2Nlc3MiOnRydWV9LCI0Ijp7ImlhdCI6MTc4MTc1MjQ0NywiYWNjZXNzIjp0cnVlfX0sInN1bSI6IjUxZTdlYzdjNTQ1MDM1OWMzNGEyMTc4N2UzZTU4Y2ZmNDM3NmYzYjNiMmM3NTg1ZWMwYTcyNDJkNmM1MGI2Y2QifX0.6o43emSe8iOBsXTPUbx4cShf-DdIpGuTTYDR5NDAAvA6ZByV_biiboF32sk5SjLAuTfEEy1xWpZ_35Fb-nxvJg"
  165. self.cookie_text.delete(1.0, tk.END)
  166. self.cookie_text.insert(1.0, default_cookie)
  167. self.log("已加载默认Cookie", "info")
  168. def save_config(self):
  169. """保存Cookie配置到文件"""
  170. cookie = self.cookie_text.get(1.0, tk.END).strip()
  171. if not cookie:
  172. messagebox.showwarning("提示", "Cookie不能为空")
  173. return
  174. try:
  175. with open("cookie_config.txt", "w", encoding="utf-8") as f:
  176. f.write(cookie)
  177. self.log("Cookie配置已保存", "success")
  178. messagebox.showinfo("成功", "Cookie配置已保存到 cookie_config.txt")
  179. except Exception as e:
  180. self.log(f"保存失败: {str(e)}", "error")
  181. def load_config(self):
  182. """加载保存的Cookie配置"""
  183. try:
  184. with open("cookie_config.txt", "r", encoding="utf-8") as f:
  185. cookie = f.read().strip()
  186. if cookie:
  187. self.cookie_text.delete(1.0, tk.END)
  188. self.cookie_text.insert(1.0, cookie)
  189. self.log("已加载保存的Cookie配置", "info")
  190. except FileNotFoundError:
  191. pass
  192. except Exception as e:
  193. self.log(f"加载配置失败: {str(e)}", "warning")
  194. def log(self, message, tag="info"):
  195. """添加日志"""
  196. timestamp = datetime.now().strftime("%H:%M:%S")
  197. self.log_text.insert(tk.END, f"[{timestamp}] {message}\n", tag)
  198. self.log_text.see(tk.END)
  199. self.root.update_idletasks()
  200. def clear_log(self):
  201. """清空日志"""
  202. self.log_text.delete(1.0, tk.END)
  203. def parse_cookie(self, cookie_string):
  204. """解析Cookie字符串为字典"""
  205. cookies = {}
  206. for item in cookie_string.split(';'):
  207. item = item.strip()
  208. if '=' in item:
  209. key, value = item.split('=', 1)
  210. cookies[key.strip()] = value.strip()
  211. return cookies
  212. def _get_csrf_token(self):
  213. """从cookie中提取csrf_token"""
  214. cookie_text = self.cookie_text.get(1.0, tk.END).strip()
  215. # 匹配 _csrf_token=xxx 或 csrf_token=xxx
  216. match = re.search(r'_csrf_token=([^;]+)', cookie_text)
  217. if match:
  218. return match.group(1)
  219. # 也尝试匹配 x-csrftoken
  220. match = re.search(r'x-csrftoken[=:]\s*([^;\s]+)', cookie_text, re.IGNORECASE)
  221. if match:
  222. return match.group(1)
  223. # 返回默认值
  224. return "83aae12269bced10680d3ccf817f82262305e7d0-1781579108"
  225. def _get_mime_type(self, filename):
  226. """获取文件MIME类型"""
  227. ext = os.path.splitext(filename)[1].lower()
  228. mime_map = {
  229. '.jpg': 'image/jpeg',
  230. '.jpeg': 'image/jpeg',
  231. '.png': 'image/png',
  232. '.gif': 'image/gif',
  233. '.bmp': 'image/bmp',
  234. '.webp': 'image/webp'
  235. }
  236. return mime_map.get(ext, 'image/jpeg')
  237. def upload_image(self, image_data, filename="image.jpg"):
  238. """
  239. 完整上传图片到飞书,返回file_token信息
  240. """
  241. try:
  242. cookie_string = self.cookie_text.get(1.0, tk.END).strip()
  243. if not cookie_string:
  244. self.log(" ❌ Cookie为空,无法上传图片", "error")
  245. return None
  246. cookies = self.parse_cookie(cookie_string)
  247. csrf_token = self._get_csrf_token()
  248. self.log(f" 🔍 使用的CSRF token: {csrf_token[:30] if csrf_token else 'None'}...", "debug")
  249. self.log(f" 🔍 图片大小: {len(image_data)} bytes", "debug")
  250. # 构建通用请求头
  251. base_headers = {
  252. "accept": "application/json, text/plain, */*",
  253. "accept-language": "zh-CN,zh;q=0.9",
  254. "f-version": "docs-6-17-1781628518399",
  255. "x-csrftoken": csrf_token,
  256. "x-tt-logid": "02178275091573600000000000000000000ffff5bd640caae0cb8",
  257. "x-tt-trace": "1",
  258. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  259. }
  260. # ========== 步骤1: 获取uploadCode ==========
  261. self.log(f" 📤 [1/4] 获取上传code...", "info")
  262. code_url = "https://xhd.feishu.cn/space/api/bitable/external/share/uploadCode"
  263. code_params = {
  264. "shareToken": self.share_token,
  265. "fileName": filename,
  266. "size": len(image_data),
  267. "mountPoint": "bitable_tmp_point"
  268. }
  269. self.log(f" 📋 请求URL: {code_url}", "debug")
  270. self.log(f" 📋 请求参数: {json.dumps(code_params, ensure_ascii=False)}", "debug")
  271. code_response = requests.get(code_url, params=code_params, headers=base_headers, cookies=cookies, timeout=30)
  272. self.log(f" 📡 响应状态码: {code_response.status_code}", "debug")
  273. if code_response.status_code != 200:
  274. self.log(f" ❌ 获取code失败: HTTP {code_response.status_code}", "error")
  275. self.log(f" ❌ 响应内容: {code_response.text}", "error")
  276. return None
  277. try:
  278. code_data = code_response.json()
  279. except Exception as e:
  280. self.log(f" ❌ 解析JSON失败: {str(e)}", "error")
  281. return None
  282. upload_code = code_data.get('data', {}).get('uploadCode')
  283. if not upload_code:
  284. self.log(f" ❌ 响应中没有uploadCode字段", "error")
  285. self.log(f" ❌ data内容: {code_data.get('data', {})}", "error")
  286. return None
  287. self.log(f" ✅ 获取uploadCode成功: {upload_code[:30]}...", "success")
  288. # ========== 步骤2: 准备上传 ==========
  289. self.log(f" 📤 [2/4] 准备上传,获取upload_id...", "info")
  290. prepare_url = "https://xhd.feishu.cn/space/api/box/upload/prepare/authcode/"
  291. prepare_headers = base_headers.copy()
  292. prepare_headers["content-type"] = "application/json"
  293. prepare_headers["x-command"] = "space.api.box.upload.prepare.authcode"
  294. prepare_body = {
  295. "code": upload_code,
  296. "mount_point": "bitable_tmp_point",
  297. "mount_node_token": self.share_token
  298. }
  299. self.log(f" 📋 请求体: {json.dumps(prepare_body, ensure_ascii=False)}", "debug")
  300. prepare_response = requests.post(prepare_url, headers=prepare_headers, cookies=cookies, json=prepare_body, timeout=30)
  301. self.log(f" 📡 响应状态码: {prepare_response.status_code}", "debug")
  302. if prepare_response.status_code != 200:
  303. self.log(f" ❌ 准备上传失败: HTTP {prepare_response.status_code}", "error")
  304. self.log(f" ❌ 响应内容: {prepare_response.text}", "error")
  305. return None
  306. try:
  307. prepare_data = prepare_response.json()
  308. except Exception as e:
  309. self.log(f" ❌ 解析JSON失败: {str(e)}", "error")
  310. return None
  311. if prepare_data.get('code') != 0:
  312. self.log(f" ❌ 准备上传失败: code={prepare_data.get('code')}, msg={prepare_data.get('msg', '未知错误')}", "error")
  313. return None
  314. upload_id = prepare_data.get('data', {}).get('upload_id')
  315. if not upload_id:
  316. self.log(f" ❌ 响应中没有upload_id字段", "error")
  317. self.log(f" ❌ data内容: {prepare_data.get('data', {})}", "error")
  318. return None
  319. self.log(f" ✅ 获取upload_id成功: {upload_id}", "success")
  320. self.log(f" 📤 [3/4] 上传文件内容 ({len(image_data)} bytes)...", "info")
  321. # 使用 Adler-32 计算 checksum(飞书使用的算法)
  322. import zlib
  323. checksum = zlib.adler32(image_data) & 0xffffffff
  324. upload_url = f"https://internal-api-drive-stream.feishu.cn/space/api/box/stream/upload/merge_block/?upload_id={upload_id}"
  325. upload_headers = base_headers.copy()
  326. upload_headers["content-type"] = "application/octet-stream"
  327. upload_headers["x-command"] = "space.api.box.stream.upload.merge_block"
  328. upload_headers["x-seq-list"] = "0"
  329. upload_headers["x-block-list-checksum"] = str(checksum) # 注意:是数字字符串,不是十六进制
  330. upload_headers["x-block-origin-size"] = str(len(image_data))
  331. self.log(f" 📋 文件大小: {len(image_data)} bytes, checksum(Adler-32): {checksum}", "debug")
  332. upload_response = requests.post(upload_url, headers=upload_headers, cookies=cookies, data=image_data, timeout=60)
  333. self.log(f" 📡 响应状态码: {upload_response.status_code}", "debug")
  334. self.log(f" 📡 响应内容: {upload_response.text[:500]}", "debug")
  335. if upload_response.status_code != 200:
  336. self.log(f" ❌ 上传文件失败: HTTP {upload_response.status_code}", "error")
  337. self.log(f" ❌ 响应内容: {upload_response.text}", "error")
  338. return None
  339. # 检查上传是否真正成功
  340. try:
  341. upload_result = upload_response.json()
  342. if upload_result.get('code') != 0:
  343. self.log(f" ❌ 上传文件失败: code={upload_result.get('code')}, msg={upload_result.get('message', '未知错误')}", "error")
  344. return None
  345. except:
  346. pass
  347. self.log(f" ✅ 文件上传成功", "success")
  348. # ========== 步骤4: 完成上传 ==========
  349. self.log(f" 📤 [4/4] 完成上传,获取file_token...", "info")
  350. finish_url = "https://xhd.feishu.cn/space/api/box/upload/finish/"
  351. finish_headers = base_headers.copy()
  352. finish_headers["content-type"] = "application/json"
  353. finish_headers["x-command"] = "space.api.box.upload.finish"
  354. finish_body = {
  355. "upload_id": upload_id,
  356. "num_blocks": 1,
  357. "push_open_history_record": 0
  358. }
  359. self.log(f" 📋 请求体: {json.dumps(finish_body, ensure_ascii=False)}", "debug")
  360. finish_response = requests.post(finish_url, headers=finish_headers, cookies=cookies, json=finish_body, timeout=30)
  361. self.log(f" 📡 响应状态码: {finish_response.status_code}", "debug")
  362. self.log(f" 📡 响应内容: {finish_response.text[:500]}", "debug")
  363. if finish_response.status_code != 200:
  364. self.log(f" ❌ 完成上传失败: HTTP {finish_response.status_code}", "error")
  365. self.log(f" ❌ 响应内容: {finish_response.text}", "error")
  366. return None
  367. try:
  368. finish_data = finish_response.json()
  369. except Exception as e:
  370. self.log(f" ❌ 解析JSON失败: {str(e)}", "error")
  371. return None
  372. if finish_data.get('code') != 0:
  373. self.log(f" ❌ 完成上传失败: code={finish_data.get('code')}, msg={finish_data.get('message', '未知错误')}", "error")
  374. self.log(f" ❌ 完整响应: {json.dumps(finish_data, ensure_ascii=False, indent=2)}", "error")
  375. return None
  376. file_token = finish_data.get('data', {}).get('file_token')
  377. if not file_token:
  378. self.log(f" ❌ 响应中没有file_token字段", "error")
  379. self.log(f" ❌ data内容: {finish_data.get('data', {})}", "error")
  380. return None
  381. # 获取图片信息
  382. width, height = 0, 0
  383. try:
  384. from PIL import Image
  385. import io
  386. pil_img = Image.open(io.BytesIO(image_data))
  387. width, height = pil_img.size
  388. except:
  389. pass
  390. result = {
  391. 'attachmentToken': file_token,
  392. 'id': file_token,
  393. 'mimeType': self._get_mime_type(filename),
  394. 'name': filename,
  395. 'size': len(image_data),
  396. 'timeStamp': int(time.time() * 1000),
  397. 'height': height,
  398. 'width': width
  399. }
  400. self.log(f" ✅ 图片上传完成! file_token: {file_token}", "success")
  401. return result
  402. except requests.exceptions.Timeout:
  403. self.log(f" ❌ 上传超时", "error")
  404. return None
  405. except Exception as e:
  406. self.log(f" ❌ 上传图片异常: {str(e)}", "error")
  407. import traceback
  408. self.log(f" 详细: {traceback.format_exc()}", "debug")
  409. return None
  410. def extract_images_to_cache(self, file_path, sheet_name):
  411. """
  412. 提取Excel中的所有图片到缓存
  413. 返回: dict {行号: [图片数据列表]}
  414. """
  415. image_cache = {}
  416. try:
  417. wb = load_workbook(file_path, data_only=True)
  418. ws = wb[sheet_name]
  419. if hasattr(ws, '_images') and ws._images:
  420. self.log(f" 📸 开始提取 {len(ws._images)} 张图片到缓存...", "info")
  421. for idx, img in enumerate(ws._images):
  422. try:
  423. # 获取位置信息
  424. if hasattr(img, 'anchor') and hasattr(img.anchor, '_from'):
  425. anchor_from = img.anchor._from
  426. row = anchor_from.row + 1
  427. col = anchor_from.col + 1
  428. else:
  429. row = idx + 1
  430. col = 0
  431. # 获取图片数据
  432. image_data = None
  433. img_format = 'jpg'
  434. filename = f"image_{row}_{idx}.jpg"
  435. if hasattr(img, '_data') and callable(img._data):
  436. image_data = img._data()
  437. elif hasattr(img, '_data') and not callable(img._data):
  438. image_data = img._data
  439. elif hasattr(img, 'image'):
  440. if hasattr(img.image, '_data') and callable(img.image._data):
  441. image_data = img.image._data()
  442. elif hasattr(img.image, '_data'):
  443. image_data = img.image._data
  444. if image_data is None:
  445. continue
  446. # 检测图片格式
  447. try:
  448. from PIL import Image
  449. import io
  450. pil_img = Image.open(io.BytesIO(image_data))
  451. img_format = pil_img.format if pil_img.format else 'jpg'
  452. filename = f"image_{row}_{idx}.{img_format.lower()}"
  453. except:
  454. pass
  455. if row not in image_cache:
  456. image_cache[row] = []
  457. image_cache[row].append({
  458. 'data': image_data,
  459. 'col': col,
  460. 'format': img_format,
  461. 'filename': filename
  462. })
  463. except Exception as e:
  464. self.log(f" ⚠️ 提取图片失败: {str(e)}", "warning")
  465. wb.close()
  466. total = sum(len(imgs) for imgs in image_cache.values())
  467. self.log(f" ✅ 成功提取 {total} 张图片到缓存,分布在 {len(image_cache)} 行", "success")
  468. except Exception as e:
  469. self.log(f" ❌ 提取图片失败: {str(e)}", "error")
  470. return image_cache
  471. def build_request_body(self, row_dict, row_index):
  472. """根据Excel行数据构建请求体"""
  473. try:
  474. # 获取各列数据
  475. name = str(row_dict.get("姓名", "")) if pd.notna(row_dict.get("姓名")) else ""
  476. gender = str(row_dict.get("性别", "")) if pd.notna(row_dict.get("性别")) else ""
  477. phone = str(row_dict.get("手机号码", "")) if pd.notna(row_dict.get("手机号码")) else ""
  478. remark = str(row_dict.get("备注", "")) if pd.notna(row_dict.get("备注")) else ""
  479. institution = str(row_dict.get("机构", "")) if pd.notna(row_dict.get("机构")) else ""
  480. target_school_text = str(row_dict.get("意向学校", "")) if pd.notna(row_dict.get("意向学校")) else ""
  481. consulting_text = str(row_dict.get("咨询项目", "")) if pd.notna(row_dict.get("咨询项目")) else ""
  482. product_text = str(row_dict.get("产品", "")) if pd.notna(row_dict.get("产品")) else ""
  483. source = str(row_dict.get("线索来源", "")) if pd.notna(row_dict.get("线索来源")) else ""
  484. market_person = str(row_dict.get("市场人员", "")) if pd.notna(row_dict.get("市场人员")) else ""
  485. userId = str(row_dict.get("用户ID", "")) if pd.notna(row_dict.get("用户ID")) else ""
  486. channel_code = str(row_dict.get("渠道代码", "")) if pd.notna(row_dict.get("渠道代码")) else ""
  487. # 去掉数字的小数点
  488. phone = phone.split('.')[0]
  489. self.log(f" 📝 提取数据: 姓名={name}, 手机={phone}, 渠道代码={channel_code}, 意向学校={target_school_text}, 咨询项目={consulting_text}, 产品={product_text}", "debug")
  490. # 意向校区映射字典
  491. school_mapping = {
  492. "上海": "optqw2jaJ5",
  493. "徐汇": "optTD4UQsM",
  494. "人广": "opt7hQjSZQ",
  495. "长宁": "opt7A8WBtE",
  496. "浦东": "optnyoxnTK",
  497. "杨浦": "optgArj17r",
  498. "闵行": "optCIwo1J1",
  499. "松江": "optuZzXsYm",
  500. "锦秋个人学校": "optLQYDW3S",
  501. "封闭学院": "optitJpd5G",
  502. "大学生-市区": "optHlvKsKy",
  503. "大学生-曹崇杨": "optdcoQXBt",
  504. "大学生-临港": "opt3i4fOKJ",
  505. "大学生-松江": "optG7eGFUg",
  506. "大学生-奉贤": "optJJqnLgY",
  507. "常州": "opt5wuiYjW"
  508. }
  509. # 咨询项目映射字典
  510. consulting_mapping = {
  511. "语培": "opt96MRele",
  512. "留学": "optQFQsXIe",
  513. "考研": "opti3wbRF7",
  514. "AF艺术": "optaZC2VCL",
  515. "美行音乐": "optzSqwgLp",
  516. "锦秋个人学校": "opt9N4AGbJ",
  517. "研学考团": "optdYBLknN",
  518. "尚思": "optf8lTDcV"
  519. }
  520. # 语培产品映射字典(完整版)
  521. product_mapping = {
  522. "ACT": "optlGkVm5n",
  523. "AEAS": "optqX3b1rJ",
  524. "ALEVEL": "optgMNcV8D",
  525. "AP": "optWKWYQgo",
  526. "BC": "opt8huX4Nv",
  527. "DSE": "optxFBYmUs",
  528. "ESL": "optaYj9RZM",
  529. "GMAT": "optgwu03Nf",
  530. "GRE": "optEOyf5TR",
  531. "IB": "optBhWIw7x",
  532. "IGCSE": "optIuoUFpo",
  533. "KET": "optjcLdrsB",
  534. "PET": "optY42DNFo",
  535. "PTE": "optOrHJvTX",
  536. "SAT": "optTxT8NEu",
  537. "SSAT": "optJ2a0tkB",
  538. "Toefl Junior": "optEGInE0d",
  539. "国际备考": "optgwn1azI",
  540. "国际研学及考团": "optAmxtqGN",
  541. "国家地理": "opttXZvoj3",
  542. "剑桥英语": "optQkkhzD5",
  543. "锦秋竞赛": "optUgXXmpF",
  544. "考研": "optgQrGHTS",
  545. "留学预备": "optWxtOU8b",
  546. "美高": "opt36E5u0t",
  547. "硕果": "optNKeSnho",
  548. "硕果竞赛": "optMzk3TpL",
  549. "腾飞计划": "optYTD9h04",
  550. "团企培": "optW5UDF6G",
  551. "托福": "optKuZ2zCq",
  552. "雅思": "optWsal1MB",
  553. "用英语讲中国故事": "optinda27s",
  554. "在线": "opt2X07PY4",
  555. "浙大国际本科": "optZFmVTTC",
  556. "语培待定/未知": "optTNitLA5",
  557. "大学四级": "optAqYPFmN",
  558. "大学六级": "optpdfKgdl",
  559. "菁英树": "optBFhBHFX",
  560. "其它": "opt698ww1H"
  561. }
  562. # 处理意向学校:根据文本查找对应的ID
  563. target_school_id = None
  564. if target_school_text:
  565. # 精确匹配
  566. if target_school_text in school_mapping:
  567. target_school_id = school_mapping[target_school_text]
  568. self.log(f" 🏫 意向学校: {target_school_text} -> {target_school_id}", "debug")
  569. else:
  570. # 尝试模糊匹配(包含关系)
  571. matched = False
  572. for school_name, school_id in school_mapping.items():
  573. if school_name in target_school_text or target_school_text in school_name:
  574. target_school_id = school_id
  575. matched = True
  576. self.log(f" 🏫 意向学校模糊匹配: {target_school_text} -> {school_name} ({target_school_id})", "debug")
  577. break
  578. if not matched:
  579. self.log(f" ⚠️ 未找到匹配的意向学校: {target_school_text}", "warning")
  580. # 处理咨询项目:根据文本查找对应的ID(支持多选)
  581. consulting_ids = []
  582. if consulting_text:
  583. # 如果是多个项目,用分隔符分割(如逗号、顿号等)
  584. consulting_items = re.split(r'[,,、/ ]+', consulting_text)
  585. for item in consulting_items:
  586. item = item.strip()
  587. if item:
  588. if item in consulting_mapping:
  589. consulting_ids.append(consulting_mapping[item])
  590. self.log(f" 📚 咨询项目: {item} -> {consulting_mapping[item]}", "debug")
  591. else:
  592. # 尝试模糊匹配
  593. matched = False
  594. for consulting_name, consulting_id in consulting_mapping.items():
  595. if consulting_name in item or item in consulting_name:
  596. consulting_ids.append(consulting_id)
  597. matched = True
  598. self.log(f" 📚 咨询项目模糊匹配: {item} -> {consulting_name} ({consulting_id})", "debug")
  599. break
  600. if not matched:
  601. self.log(f" ⚠️ 未找到匹配的咨询项目: {item}", "warning")
  602. # 处理产品:根据文本查找对应的ID(支持多选)
  603. product_ids = []
  604. if product_text:
  605. product_items = re.split(r'[,,、/ ]+', product_text)
  606. for item in product_items:
  607. item = item.strip()
  608. if item:
  609. if item in product_mapping:
  610. product_ids.append(product_mapping[item])
  611. self.log(f" 📦 产品: {item} -> {product_mapping[item]}", "debug")
  612. else:
  613. # 尝试模糊匹配(支持大小写不敏感)
  614. matched = False
  615. item_lower = item.lower()
  616. for product_name, product_id in product_mapping.items():
  617. # 支持大小写不敏感的匹配
  618. if product_name.lower() == item_lower or product_name in item or item in product_name:
  619. product_ids.append(product_id)
  620. matched = True
  621. self.log(f" 📦 产品模糊匹配: {item} -> {product_name} ({product_id})", "debug")
  622. break
  623. if not matched:
  624. self.log(f" ⚠️ 未找到匹配的产品: {item}", "warning")
  625. # 构建备注文本
  626. remark_text = f"姓名:{name},性别:{gender},手机号码:{phone}"
  627. if remark:
  628. remark_text += f",{remark}"
  629. # 构建data JSON
  630. data_obj = {
  631. "fld43Khnuw": {
  632. "type": 1,
  633. "value": [{"type": "text", "text": remark_text}]
  634. },
  635. "fldexDPwcE": {"type": 3, "value": "optmlZt00z"} if institution else {"type": 3},
  636. # 意向学校 - 使用映射后的ID
  637. "fldWt6JrgK": {"type": 3, "value": target_school_id} if target_school_id else {"type": 3},
  638. # 咨询项目 - 使用映射后的ID列表(多选)
  639. "fldirDDoLV": {"type": 4, "value": consulting_ids} if consulting_ids else {"type": 4},
  640. # 语培产品 - 使用映射后的ID列表(多选)
  641. "fldudfcNVO": {"type": 4, "value": product_ids} if product_ids else {"type": 4},
  642. "fldS90QCXo": {"type": 1, "value": [{"type": "text", "text": name}]} if name else {"type": 1},
  643. "fldsA4YdS6": {"type": 3},
  644. "fldSSElgkA": {"type": 13, "value": {"fullPhoneNum": phone}} if phone else {"type": 13},
  645. "fldZPL52gS": {"type": 1},
  646. "fldPIaD4UH": {"type": 13},
  647. "fldUGeDVDF": {"type": 13},
  648. "fldKZ5WKyN": {"type": 1},
  649. "fld0MHPEaW": {"type": 3},
  650. "fldOLvk3zZ": {"type": 3},
  651. "fldkSVVnuu": {"type": 1},
  652. "fldCfRL3qM": {"type": 3, "value": "optTWIvAFb"} if source else {"type": 3},
  653. "fldodpd0fR": {
  654. "type": 11,
  655. "value": {
  656. "users": [{
  657. "userId":userId,
  658. "name":market_person,
  659. "avatarUrl":"https://s3-imfile.feishucdn.com/static-resource/v1/v3_0013q_44a1d366-78d6-4a4b-ad37-294ff4b5d55g~?image_size=72x72&cut_type=default-face&quality=&format=jpeg&sticker_format=.webp",
  660. "enName":market_person,
  661. "notify":False,
  662. }]
  663. }
  664. } if market_person else {"type": 11},
  665. # ====== 渠道代码 - 使用映射后的ID ======
  666. "flde7thxT7": {"type": 3, "value": channel_code},
  667. "fldaxavu52": {"type": 3, "value": "optHsn4zKU"},
  668. "fldMpwPaI1": {"type": 17} # 图片字段,默认空
  669. }
  670. # ========== 检查并处理图片 ==========
  671. if self.image_cache and row_index in self.image_cache:
  672. images = self.image_cache[row_index]
  673. self.log(f" 🖼️ 检测到 {len(images)} 个嵌入图片在第 {row_index} 行", "info")
  674. # 上传第一张图片
  675. if images:
  676. img_info = images[0]
  677. # 检查是否已经上传过
  678. if row_index in self.uploaded_cache:
  679. img_value = self.uploaded_cache[row_index]
  680. self.log(f" ♻️ 使用已上传的图片: {img_value.get('attachmentToken')}", "debug")
  681. else:
  682. # 上传图片
  683. self.log(f" 📤 开始上传图片...", "info")
  684. uploaded = self.upload_image(img_info['data'], img_info['filename'])
  685. if uploaded:
  686. self.uploaded_cache[row_index] = uploaded
  687. img_value = uploaded
  688. self.log(f" ✅ 图片上传成功,token: {uploaded.get('attachmentToken')}", "success")
  689. else:
  690. self.log(f" ⚠️ 图片上传失败,跳过该图片", "warning")
  691. img_value = None
  692. # 如果上传成功,添加到data_obj
  693. if img_value:
  694. data_obj["fldMpwPaI1"] = {
  695. "type": 17,
  696. "value": [img_value]
  697. }
  698. self.log(f" 📎 图片已添加到请求中", "debug")
  699. else:
  700. # 没有图片,确保图片字段为空
  701. data_obj["fldMpwPaI1"] = {"type": 17}
  702. # 构建完整的请求体
  703. body = {
  704. "shareToken": self.share_token,
  705. "data": json.dumps(data_obj, ensure_ascii=False),
  706. "preUploadEnable": True
  707. }
  708. return body
  709. except Exception as e:
  710. self.log(f"构建请求体失败: {str(e)}", "error")
  711. import traceback
  712. self.log(f"详细错误: {traceback.format_exc()}", "debug")
  713. return None
  714. def send_request(self, row_data, row_index):
  715. """发送单条请求"""
  716. try:
  717. self.log(f"\n{'='*50}", "info")
  718. self.log(f"📤 准备发送第 {row_index} 行数据", "info")
  719. cookie_string = self.cookie_text.get(1.0, tk.END).strip()
  720. if not cookie_string:
  721. self.log("❌ Cookie为空,请先配置Cookie", "error")
  722. return False, "Cookie为空"
  723. # 构建请求体
  724. body = self.build_request_body(row_data, row_index)
  725. if body is None:
  726. self.log("❌ 构建请求体失败", "error")
  727. return False, "构建请求体失败"
  728. # 记录请求体大小
  729. body_str = json.dumps(body, ensure_ascii=False)
  730. self.log(f"📦 请求体大小: {len(body_str)} 字节", "debug")
  731. # 准备请求
  732. headers = self.default_headers.copy()
  733. cookies = self.parse_cookie(cookie_string)
  734. # 更新动态头
  735. headers["x-csrftoken"] = self._get_csrf_token()
  736. headers["f-version"] = "docs-6-17-1781628518399"
  737. self.log(f"🌐 发送请求到: {self.url}", "debug")
  738. # 发送请求
  739. response = requests.post(
  740. self.url,
  741. headers=headers,
  742. cookies=cookies,
  743. json=body,
  744. timeout=30
  745. )
  746. self.log(f"📡 响应状态码: {response.status_code}", "debug")
  747. # 解析响应
  748. try:
  749. resp_data = response.json()
  750. resp_text = json.dumps(resp_data, ensure_ascii=False, indent=2)
  751. if isinstance(resp_data, dict):
  752. if resp_data.get('code') != 0 and resp_data.get('code') is not None:
  753. self.log(f"⚠️ 服务器返回错误码: {resp_data.get('code')}", "warning")
  754. self.log(f" 错误信息: {resp_data.get('msg', '未知错误')}", "error")
  755. if 'data' in resp_data:
  756. self.log(f" 📊 响应数据: {json.dumps(resp_data['data'], ensure_ascii=False)[:200]}...", "debug")
  757. except:
  758. resp_text = response.text
  759. self.log("⚠️ 响应不是有效的JSON格式", "warning")
  760. if response.status_code == 200:
  761. self.log(f"✅ 第{row_index}行 发送成功", "success")
  762. return True, resp_text
  763. else:
  764. self.log(f"❌ 第{row_index}行 发送失败 (HTTP {response.status_code})", "error")
  765. self.log(f" 响应内容: {resp_text[:500]}...", "error")
  766. return False, resp_text
  767. except requests.exceptions.Timeout:
  768. self.log(f"⏱️ 第{row_index}行 请求超时", "error")
  769. return False, "请求超时"
  770. except requests.exceptions.ConnectionError:
  771. self.log(f"🔌 第{row_index}行 连接失败,请检查网络", "error")
  772. return False, "连接失败"
  773. except Exception as e:
  774. self.log(f"💥 第{row_index}行 异常: {str(e)}", "error")
  775. import traceback
  776. self.log(f"详细错误: {traceback.format_exc()}", "debug")
  777. return False, str(e)
  778. finally:
  779. self.log(f"{'='*50}\n", "info")
  780. def send_all(self):
  781. """发送所有数据的主循环"""
  782. try:
  783. excel_file = self.file_path_var.get()
  784. sheet_name = self.sheet_var.get()
  785. min_interval = float(self.min_interval_var.get())
  786. max_interval = float(self.max_interval_var.get())
  787. start_row = int(self.start_row_var.get()) - 1
  788. if not excel_file:
  789. self.log("❌ 请先选择Excel文件", "error")
  790. self.status_var.set("❌ 请先选择Excel文件")
  791. return
  792. # 清空上次上传缓存
  793. self.uploaded_cache = {}
  794. # ========== 自动提取所有图片到缓存 ==========
  795. self.log(f"🔍 正在自动提取Excel中的嵌入图片...", "info")
  796. self.image_cache = self.extract_images_to_cache(excel_file, sheet_name)
  797. if self.image_cache:
  798. total_images = sum(len(imgs) for imgs in self.image_cache.values())
  799. self.log(f"📊 共提取 {total_images} 张图片,分布在 {len(self.image_cache)} 行", "info")
  800. rows_with_images = sorted(self.image_cache.keys())
  801. if len(rows_with_images) <= 20:
  802. self.log(f"📋 有图片的行: {rows_with_images}", "debug")
  803. else:
  804. self.log(f"📋 有图片的行: {rows_with_images[:20]} ... (共{len(rows_with_images)}行)", "debug")
  805. else:
  806. self.log(f"📷 未检测到任何嵌入图片", "info")
  807. # ========== 读取Excel数据 ==========
  808. try:
  809. df = pd.read_excel(excel_file, sheet_name=sheet_name)
  810. self.log(f"📊 成功读取Excel: {len(df)} 行数据", "info")
  811. self.log(f"📋 列名: {', '.join(df.columns.tolist())}", "info")
  812. except Exception as e:
  813. self.log(f"❌ 读取Excel失败: {str(e)}", "error")
  814. self.status_var.set(f"❌ 读取失败: {str(e)}")
  815. return
  816. if len(df) == 0:
  817. self.log("⚠️ Excel中没有数据", "warning")
  818. self.status_var.set("⚠️ 没有数据")
  819. return
  820. df = df.iloc[start_row:]
  821. total = len(df)
  822. success_count = 0
  823. fail_count = 0
  824. self.log(f"🚀 开始发送,共 {total} 条数据", "info")
  825. self.status_var.set(f"🔄 发送中... 0/{total}")
  826. for idx, (_, row) in enumerate(df.iterrows()):
  827. if self.stop_flag:
  828. self.log("⏹️ 已停止发送", "warning")
  829. self.status_var.set("⏹️ 已停止")
  830. break
  831. # ===== 关键修复:计算Excel实际行号 =====
  832. # DataFrame的索引 + 2(因为第1行是表头,DataFrame从0开始)
  833. # 再加上start_row(起始行的偏移)
  834. excel_row_number = idx + 2 + start_row
  835. self.log(f" 📍 DataFrame索引: {idx}, Excel行号: {excel_row_number}", "debug")
  836. # 检查该行是否有图片(使用Excel实际行号)
  837. if self.image_cache and excel_row_number in self.image_cache:
  838. img_count = len(self.image_cache[excel_row_number])
  839. self.log(f" 🖼️ 第{excel_row_number}行有 {img_count} 个嵌入图片,将自动上传并添加", "info")
  840. success, resp_text = self.send_request(row.to_dict(), excel_row_number)
  841. if success:
  842. success_count += 1
  843. else:
  844. fail_count += 1
  845. self.status_var.set(f"🔄 发送中... {idx+1}/{total} (成功:{success_count} 失败:{fail_count})")
  846. if idx < total - 1 and not self.stop_flag:
  847. interval = random.uniform(min_interval, max_interval)
  848. self.log(f"⏳ 等待 {interval:.1f} 秒...", "info")
  849. time.sleep(interval)
  850. self.log(f"🏁 发送完成!成功: {success_count}, 失败: {fail_count}", "success" if fail_count == 0 else "warning")
  851. self.status_var.set(f"✅ 完成 成功:{success_count} 失败:{fail_count}")
  852. except Exception as e:
  853. self.log(f"💥 程序异常: {str(e)}", "error")
  854. import traceback
  855. self.log(f"详细错误: {traceback.format_exc()}", "debug")
  856. self.status_var.set(f"❌ 异常: {str(e)}")
  857. finally:
  858. self.is_running = False
  859. self.start_btn.config(state=tk.NORMAL)
  860. self.stop_btn.config(state=tk.DISABLED)
  861. def start_send(self):
  862. """启动发送线程"""
  863. if self.is_running:
  864. return
  865. if not self.file_path_var.get():
  866. messagebox.showwarning("提示", "请先选择Excel文件")
  867. return
  868. cookie = self.cookie_text.get(1.0, tk.END).strip()
  869. if not cookie:
  870. messagebox.showwarning("提示", "请先配置Cookie")
  871. return
  872. try:
  873. float(self.min_interval_var.get())
  874. float(self.max_interval_var.get())
  875. int(self.start_row_var.get())
  876. except ValueError:
  877. messagebox.showerror("错误", "间隔或行号请输入有效数字")
  878. return
  879. self.is_running = True
  880. self.stop_flag = False
  881. self.start_btn.config(state=tk.DISABLED)
  882. self.stop_btn.config(state=tk.NORMAL)
  883. self.status_var.set("🔄 正在发送...")
  884. thread = threading.Thread(target=self.send_all, daemon=True)
  885. thread.start()
  886. def stop_send(self):
  887. """停止发送"""
  888. self.stop_flag = True
  889. self.log("⏹️ 正在停止...", "warning")
  890. self.stop_btn.config(state=tk.DISABLED)
  891. # ========== 提取图片功能(手动) ==========
  892. def extract_images_to_folder(self, file_path, sheet_name, output_folder=None):
  893. """提取Excel中的所有图片并保存到文件夹"""
  894. try:
  895. if output_folder is None:
  896. base_dir = os.path.dirname(file_path)
  897. base_name = os.path.splitext(os.path.basename(file_path))[0]
  898. output_folder = os.path.join(base_dir, f"{base_name}_提取的图片")
  899. if not os.path.exists(output_folder):
  900. os.makedirs(output_folder)
  901. mapping_file = os.path.join(output_folder, "图片映射表.txt")
  902. wb = load_workbook(file_path, data_only=True)
  903. ws = wb[sheet_name]
  904. image_count = 0
  905. error_count = 0
  906. with open(mapping_file, 'w', encoding='utf-8') as f:
  907. f.write("="*60 + "\n")
  908. f.write("Excel图片提取映射表\n")
  909. f.write(f"源文件: {file_path}\n")
  910. f.write(f"工作表: {sheet_name}\n")
  911. f.write(f"提取时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
  912. f.write("="*60 + "\n\n")
  913. f.write("行号\t列号\t列字母\t文件名\t尺寸\t格式\n")
  914. f.write("-"*60 + "\n")
  915. if hasattr(ws, '_images') and ws._images:
  916. self.log(f" 📸 开始提取 {len(ws._images)} 张图片...", "debug")
  917. for idx, img in enumerate(ws._images):
  918. try:
  919. if hasattr(img, 'anchor') and hasattr(img.anchor, '_from'):
  920. anchor_from = img.anchor._from
  921. row = anchor_from.row + 1
  922. col = anchor_from.col + 1
  923. col_letter = get_column_letter(col)
  924. else:
  925. row = idx + 1
  926. col = 0
  927. col_letter = '?'
  928. image_data = None
  929. if hasattr(img, '_data') and callable(img._data):
  930. image_data = img._data()
  931. elif hasattr(img, '_data') and not callable(img._data):
  932. image_data = img._data
  933. elif hasattr(img, 'image'):
  934. if hasattr(img.image, '_data') and callable(img.image._data):
  935. image_data = img.image._data()
  936. elif hasattr(img.image, '_data'):
  937. image_data = img.image._data
  938. if image_data is None:
  939. error_count += 1
  940. continue
  941. # 检测格式并保存
  942. img_format = 'jpg'
  943. width, height = 0, 0
  944. try:
  945. from PIL import Image
  946. import io
  947. pil_img = Image.open(io.BytesIO(image_data))
  948. img_format = pil_img.format if pil_img.format else 'jpg'
  949. width, height = pil_img.size
  950. filename = f"第{row}行_列{col_letter}_{idx+1}.{img_format.lower()}"
  951. filepath = os.path.join(output_folder, filename)
  952. pil_img.save(filepath)
  953. except:
  954. filename = f"第{row}行_列{col_letter}_{idx+1}.jpg"
  955. filepath = os.path.join(output_folder, filename)
  956. with open(filepath, 'wb') as f_out:
  957. f_out.write(image_data)
  958. f.write(f"{row}\t{col}\t{col_letter}\t{filename}\t{width}x{height}\t{img_format}\n")
  959. image_count += 1
  960. if image_count % 50 == 0:
  961. self.log(f" 📸 已提取 {image_count} 张图片...", "debug")
  962. except Exception as e:
  963. error_count += 1
  964. self.log(f" ⚠️ 提取图片失败: {str(e)}", "warning")
  965. wb.close()
  966. with open(mapping_file, 'a', encoding='utf-8') as f:
  967. f.write("\n" + "="*60 + "\n")
  968. f.write(f"提取完成: 成功 {image_count} 张, 失败 {error_count} 张\n")
  969. f.write("="*60 + "\n")
  970. self.log(f"✅ 成功提取 {image_count} 张图片到: {output_folder}", "success")
  971. if error_count > 0:
  972. self.log(f"⚠️ 有 {error_count} 张图片提取失败", "warning")
  973. self.log(f"📋 映射文件: {mapping_file}", "info")
  974. return output_folder
  975. except Exception as e:
  976. self.log(f"❌ 提取图片失败: {str(e)}", "error")
  977. return None
  978. def extract_images(self):
  979. """手动提取图片的入口方法"""
  980. excel_file = self.file_path_var.get()
  981. if not excel_file:
  982. messagebox.showwarning("提示", "请先选择Excel文件")
  983. return
  984. sheet_name = self.sheet_var.get()
  985. if not messagebox.askyesno("确认",
  986. f"即将提取Excel中的所有图片\n\n"
  987. f"文件: {os.path.basename(excel_file)}\n"
  988. f"工作表: {sheet_name}\n\n"
  989. f"是否继续?"):
  990. return
  991. def extract_thread():
  992. self.log("📸 开始提取图片...", "info")
  993. self.status_var.set("🔄 正在提取图片...")
  994. self.start_btn.config(state=tk.DISABLED)
  995. output_folder = self.extract_images_to_folder(excel_file, sheet_name)
  996. if output_folder:
  997. self.status_var.set(f"✅ 图片已提取到: {output_folder}")
  998. messagebox.showinfo("提取成功",
  999. f"图片已成功提取!\n\n"
  1000. f"保存位置: {output_folder}\n\n"
  1001. f"请查看文件夹中的 '图片映射表.txt' 文件")
  1002. else:
  1003. self.status_var.set("❌ 提取失败")
  1004. messagebox.showerror("提取失败", "图片提取失败,请查看日志了解详情。")
  1005. self.start_btn.config(state=tk.NORMAL)
  1006. thread = threading.Thread(target=extract_thread, daemon=True)
  1007. thread.start()
  1008. if __name__ == "__main__":
  1009. root = tk.Tk()
  1010. app = FeishuSubmitApp(root)
  1011. root.mainloop()