import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext import pandas as pd import requests import json import threading import time import random import re from datetime import datetime import os class FeishuSubmitApp: def __init__(self, root): self.root = root self.root.title("飞书表格数据提交工具") self.root.geometry("1100x800") # 状态变量 self.is_running = False self.stop_flag = False self.excel_file = "" self.cookie_string = "" # 从curl提取的默认配置 self.default_headers = { "accept": "application/json, text/plain, */*", "accept-language": "zh-CN,zh;q=0.9", "content-type": "application/json", "f-version": "docs-6-9-1780996889854", "origin": "https://xhd.feishu.cn", "priority": "u=1, i", "referer": "https://xhd.feishu.cn/share/base/form/shrcnJqFUFF0sqlmrfsWpDNf8df", "request-id": "HgNmkEtK9iNf-7646228072081099729", "sec-ch-ua": '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "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", "x-csrftoken": "83aae12269bced10680d3ccf817f82262305e7d0-1781579108", "x-request-id": "HgNmkEtK9iNf-7646228072081099729", "x-tt-logid": "02178175254676300000000000000000000ffffe33263555f5be8", "x-tt-trace": "1", "x-tt-trace-id": "HgNmkEtK9iNf-7646228072081099729" } self.url = "https://xhd.feishu.cn/space/api/bitable/share/content" self.share_token = "shrcnJqFUFF0sqlmrfsWpDNf8df" self.setup_ui() self.load_config() def setup_ui(self): """构建界面""" # 主容器 main_frame = ttk.Frame(self.root, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # ========== 第一行:文件选择 ========== file_frame = ttk.LabelFrame(main_frame, text="📁 数据文件", padding="10") file_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(file_frame, text="Excel文件:").grid(row=0, column=0, sticky=tk.W) self.file_path_var = tk.StringVar() ttk.Entry(file_frame, textvariable=self.file_path_var, width=60).grid(row=0, column=1, padx=(10, 10)) ttk.Button(file_frame, text="浏览...", command=self.select_file).grid(row=0, column=2) ttk.Label(file_frame, text="工作表:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0)) self.sheet_var = tk.StringVar(value="Sheet1") ttk.Entry(file_frame, textvariable=self.sheet_var, width=20).grid(row=1, column=1, sticky=tk.W, padx=(10, 10), pady=(10, 0)) ttk.Label(file_frame, text="(默认: Sheet1)").grid(row=1, column=2, sticky=tk.W, pady=(10, 0)) # ========== 第二行:Cookie配置 ========== cookie_frame = ttk.LabelFrame(main_frame, text="🍪 Cookie 配置", padding="10") cookie_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(cookie_frame, text="Cookie:").grid(row=0, column=0, sticky=tk.W) self.cookie_text = tk.Text(cookie_frame, height=4, width=80, wrap=tk.WORD) self.cookie_text.grid(row=0, column=1, padx=(10, 10), pady=(0, 5), sticky=tk.W) cookie_btn_frame = ttk.Frame(cookie_frame) cookie_btn_frame.grid(row=1, column=1, sticky=tk.W, padx=(10, 0)) ttk.Button(cookie_btn_frame, text="从剪贴板粘贴", command=self.paste_cookie).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(cookie_btn_frame, text="保存配置", command=self.save_config).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(cookie_btn_frame, text="加载默认", command=self.load_default_cookie).pack(side=tk.LEFT) # ========== 第三行:发送控制 ========== control_frame = ttk.LabelFrame(main_frame, text="⚙️ 发送控制", padding="10") control_frame.pack(fill=tk.X, pady=(0, 10)) # 间隔配置 ttk.Label(control_frame, text="最小间隔(秒):").grid(row=0, column=0, sticky=tk.W) self.min_interval_var = tk.StringVar(value="2") ttk.Entry(control_frame, textvariable=self.min_interval_var, width=10).grid(row=0, column=1, padx=(5, 15), sticky=tk.W) ttk.Label(control_frame, text="最大间隔(秒):").grid(row=0, column=2, sticky=tk.W) self.max_interval_var = tk.StringVar(value="5") ttk.Entry(control_frame, textvariable=self.max_interval_var, width=10).grid(row=0, column=3, padx=(5, 15), sticky=tk.W) ttk.Label(control_frame, text="起始行:").grid(row=0, column=4, sticky=tk.W) self.start_row_var = tk.StringVar(value="2") ttk.Entry(control_frame, textvariable=self.start_row_var, width=8).grid(row=0, column=5, padx=(5, 15), sticky=tk.W) ttk.Label(control_frame, text="(表头在第1行)").grid(row=0, column=6, sticky=tk.W) # 操作按钮 btn_frame = ttk.Frame(control_frame) btn_frame.grid(row=1, column=0, columnspan=7, pady=(15, 0), sticky=tk.W) self.start_btn = ttk.Button(btn_frame, text="🚀 开始发送", command=self.start_send) self.start_btn.pack(side=tk.LEFT, padx=(0, 10)) self.stop_btn = ttk.Button(btn_frame, text="⏹ 停止", command=self.stop_send, state=tk.DISABLED) self.stop_btn.pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(btn_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT) # 状态显示 self.status_var = tk.StringVar(value="就绪") ttk.Label(control_frame, textvariable=self.status_var, foreground="blue").grid(row=2, column=0, columnspan=7, pady=(10, 0), sticky=tk.W) # ========== 日志显示 ========== log_frame = ttk.LabelFrame(main_frame, text="📋 发送日志", padding="10") log_frame.pack(fill=tk.BOTH, expand=True) self.log_text = scrolledtext.ScrolledText(log_frame, height=20, wrap=tk.WORD, font=("Consolas", 9)) self.log_text.pack(fill=tk.BOTH, expand=True) # 日志颜色标签 self.log_text.tag_config("success", foreground="green") self.log_text.tag_config("error", foreground="red") self.log_text.tag_config("info", foreground="blue") self.log_text.tag_config("warning", foreground="orange") self.log_text.tag_config("debug", foreground="purple") # ========== 底部状态栏 ========== status_bar = ttk.Frame(self.root) status_bar.pack(fill=tk.X, side=tk.BOTTOM) ttk.Label(status_bar, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W).pack(fill=tk.X, padx=5, pady=2) def select_file(self): """选择Excel文件""" file_path = filedialog.askopenfilename( title="选择Excel文件", filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")] ) if file_path: self.file_path_var.set(file_path) self.excel_file = file_path self.log(f"已选择文件: {file_path}", "info") # 尝试自动识别工作表 try: xl = pd.ExcelFile(file_path) sheets = xl.sheet_names if sheets: self.sheet_var.set(sheets[0]) self.log(f"自动选择工作表: {sheets[0]}", "info") except Exception as e: self.log(f"读取工作表失败: {str(e)}", "warning") def paste_cookie(self): """从剪贴板粘贴Cookie""" try: clipboard_text = self.root.clipboard_get() # 尝试从curl命令中提取cookie if "cookie:" in clipboard_text.lower() or "-H" in clipboard_text: # 尝试从curl -b 或 -H "cookie: ..." 提取 cookie_match = re.search(r'-b\s+"([^"]+)"', clipboard_text) if not cookie_match: cookie_match = re.search(r'-H\s+"cookie:\s*([^"]+)"', clipboard_text, re.IGNORECASE) if cookie_match: self.cookie_text.delete(1.0, tk.END) self.cookie_text.insert(1.0, cookie_match.group(1)) self.log("已从剪贴板提取Cookie", "success") return # 直接粘贴 self.cookie_text.delete(1.0, tk.END) self.cookie_text.insert(1.0, clipboard_text) self.log("已粘贴Cookie", "info") except: messagebox.showwarning("提示", "无法读取剪贴板内容") def load_default_cookie(self): """加载默认Cookie""" 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" self.cookie_text.delete(1.0, tk.END) self.cookie_text.insert(1.0, default_cookie) self.log("已加载默认Cookie", "info") def save_config(self): """保存Cookie配置到文件""" cookie = self.cookie_text.get(1.0, tk.END).strip() if not cookie: messagebox.showwarning("提示", "Cookie不能为空") return try: with open("cookie_config.txt", "w", encoding="utf-8") as f: f.write(cookie) self.log("Cookie配置已保存", "success") messagebox.showinfo("成功", "Cookie配置已保存到 cookie_config.txt") except Exception as e: self.log(f"保存失败: {str(e)}", "error") def load_config(self): """加载保存的Cookie配置""" try: with open("cookie_config.txt", "r", encoding="utf-8") as f: cookie = f.read().strip() if cookie: self.cookie_text.delete(1.0, tk.END) self.cookie_text.insert(1.0, cookie) self.log("已加载保存的Cookie配置", "info") except FileNotFoundError: pass except Exception as e: self.log(f"加载配置失败: {str(e)}", "warning") def log(self, message, tag="info"): """添加日志""" timestamp = datetime.now().strftime("%H:%M:%S") self.log_text.insert(tk.END, f"[{timestamp}] {message}\n", tag) self.log_text.see(tk.END) self.root.update_idletasks() def clear_log(self): """清空日志""" self.log_text.delete(1.0, tk.END) def parse_cookie(self, cookie_string): """解析Cookie字符串为字典""" cookies = {} for item in cookie_string.split(';'): item = item.strip() if '=' in item: key, value = item.split('=', 1) cookies[key.strip()] = value.strip() return cookies def extract_image_info(self, row): """ 从行数据中提取图片信息 返回: dict 包含图片字段名称和值 """ image_fields = {} # 常见的图片字段名(根据你的实际情况调整) possible_image_columns = ['图片', '照片', '附件', '头像', 'image', 'photo', 'picture', 'img'] # 检查所有列,寻找可能包含图片的列 for col in row.index: col_lower = str(col).lower() # 检查列名是否包含图片相关关键词 if any(keyword in col_lower for keyword in ['图', '片', '照', '像', 'image', 'photo', 'pic', 'img', '附件']): value = row.get(col) if pd.notna(value) and value: # 检查是否为文件路径或URL value_str = str(value).strip() if value_str: # 检查是否为文件路径 if os.path.exists(value_str): image_fields[col] = { 'type': 'file', 'path': value_str, 'size': os.path.getsize(value_str) } self.log(f" 📎 检测到图片文件: {col} -> {os.path.basename(value_str)} ({os.path.getsize(value_str)} bytes)", "debug") # 检查是否为URL elif value_str.startswith(('http://', 'https://')): image_fields[col] = { 'type': 'url', 'url': value_str } self.log(f" 🔗 检测到图片URL: {col} -> {value_str}", "debug") else: self.log(f" ℹ️ 列 '{col}' 包含非图片数据: {value_str[:50]}...", "debug") if not image_fields: self.log(" 📝 未检测到图片字段", "debug") return image_fields def build_request_body(self, row): """根据Excel行数据构建请求体""" try: # 获取各列数据 name = str(row.get("姓名", "")) if pd.notna(row.get("姓名")) else "" gender = str(row.get("性别", "")) if pd.notna(row.get("性别")) else "" phone = str(row.get("手机号码", "")) if pd.notna(row.get("手机号码")) else "" remark = str(row.get("备注", "")) if pd.notna(row.get("备注")) else "" institution = str(row.get("机构", "")) if pd.notna(row.get("机构")) else "" target_school = str(row.get("意向学校", "")) if pd.notna(row.get("意向学校")) else "" consulting = str(row.get("咨询项目", "")) if pd.notna(row.get("咨询项目")) else "" product = str(row.get("产品", "")) if pd.notna(row.get("产品")) else "" source = str(row.get("线索来源", "")) if pd.notna(row.get("线索来源")) else "" market_person = str(row.get("市场人员", "")) if pd.notna(row.get("市场人员")) else "" # 提取图片信息 self.log(" 📷 检查图片字段...", "debug") image_info = self.extract_image_info(row) if image_info: self.log(f" ✅ 找到 {len(image_info)} 个图片字段: {', '.join(image_info.keys())}", "debug") # 这里可以添加图片上传逻辑 # 注意:飞书可能使用特定的字段ID存储图片 # 你需要根据实际的字段ID来设置 else: self.log(" ⚠️ 未找到图片字段,请检查Excel列名是否包含'图片'、'照片'等关键词", "warning") # 构建备注文本(包含所有信息) remark_text = f"姓名:{name},性别:{gender},手机号码:{phone}" if remark: remark_text += f",{remark}" # 构建data JSON data_obj = { "fld43Khnuw": { "type": 1, "value": [{"type": "text", "text": remark_text}] }, "fldexDPwcE": {"type": 3, "value": "optmlZt00z"} if institution else {"type": 3}, "fldWt6JrgK": {"type": 3, "value": "optTD4UQsM"} if target_school else {"type": 3}, "fldirDDoLV": {"type": 4, "value": ["opt96MRele"]} if consulting else {"type": 4}, "fldudfcNVO": {"type": 4, "value": ["optgwn1azI"]} if product else {"type": 4}, "fldS90QCXo": {"type": 1, "value": [{"type": "text", "text": name}]} if name else {"type": 1}, "fldsA4YdS6": {"type": 3}, "fldSSElgkA": {"type": 13, "value": {"fullPhoneNum": phone}} if phone else {"type": 13}, "fldZPL52gS": {"type": 1}, "fldPIaD4UH": {"type": 13}, "fldUGeDVDF": {"type": 13}, "fldKZ5WKyN": {"type": 1}, "fld0MHPEaW": {"type": 3}, "fldOLvk3zZ": {"type": 3}, "fldkSVVnuu": {"type": 1}, "fldCfRL3qM": {"type": 3, "value": "optTWIvAFb"} if source else {"type": 3}, "fldodpd0fR": { "type": 11, "value": { "users": [{ "userId": "7646228072081099729", "name": "俞玥戎", "avatarUrl": "https://s1-imfile.feishucdn.com/static-resource/v1/v3_00128_e0f4db42-191a-4878-8546-67a4dcb9005g~?image_size=72x72&cut_type=default-face&quality=&format=jpeg&sticker_format=.webp", "enName": "俞玥戎", "notify": False }] } } if market_person else {"type": 11}, "flde7thxT7": {"type": 3, "value": "optKdksJ5a"}, "fldaxavu52": {"type": 3, "value": "optHsn4zKU"}, "fldMpwPaI1": {"type": 17} } # 如果检测到图片,尝试添加到data_obj中 # 注意:你需要知道飞书表格中图片字段的实际ID # 这里假设图片字段ID为 fldImageXxx,你需要根据实际情况修改 if image_info: # 这里需要根据实际的字段ID来设置 # 示例:如果有图片,添加到某个字段 # for field_name, info in image_info.items(): # if info['type'] == 'file': # # 处理文件上传 # pass # elif info['type'] == 'url': # # 处理URL # pass self.log(" 💡 提示: 请根据实际字段ID配置图片上传", "info") # 构建完整的请求体 body = { "shareToken": self.share_token, "data": json.dumps(data_obj, ensure_ascii=False), "preUploadEnable": True } return body except Exception as e: self.log(f"构建请求体失败: {str(e)}", "error") return None def send_request(self, row_data, row_index): """发送单条请求""" try: # 检查是否有图片字段 self.log(f"\n{'='*50}", "info") self.log(f"📤 准备发送第 {row_index} 行数据", "info") cookie_string = self.cookie_text.get(1.0, tk.END).strip() if not cookie_string: self.log("❌ Cookie为空,请先配置Cookie", "error") return False, "Cookie为空" # 构建请求体 body = self.build_request_body(row_data) if body is None: self.log("❌ 构建请求体失败", "error") return False, "构建请求体失败" # 记录请求体大小 body_str = json.dumps(body, ensure_ascii=False) self.log(f"📦 请求体大小: {len(body_str)} 字节", "debug") # 检查data字段中的图片内容 data_str = body.get('data', '') if data_str: # 检查是否包含图片相关的字段 if 'fld' in data_str and ('image' in data_str.lower() or 'picture' in data_str.lower()): self.log(" 🖼️ 请求中包含图片相关字段", "debug") else: self.log(" 📝 请求中未检测到图片字段", "debug") # 准备请求 headers = self.default_headers.copy() # 更新Cookie cookies = self.parse_cookie(cookie_string) self.log(f"🌐 发送请求到: {self.url}", "debug") # 发送请求 response = requests.post( self.url, headers=headers, cookies=cookies, json=body, timeout=30 ) # 记录响应状态 self.log(f"📡 响应状态码: {response.status_code}", "debug") self.log(f"📋 响应头: {dict(response.headers)}", "debug") # 解析响应 try: resp_data = response.json() resp_text = json.dumps(resp_data, ensure_ascii=False, indent=2) # 检查响应中是否有错误信息 if isinstance(resp_data, dict): if resp_data.get('code') != 0 and resp_data.get('code') is not None: self.log(f"⚠️ 服务器返回错误码: {resp_data.get('code')}", "warning") self.log(f" 错误信息: {resp_data.get('msg', '未知错误')}", "error") if 'data' in resp_data: self.log(f" 📊 响应数据: {json.dumps(resp_data['data'], ensure_ascii=False)[:200]}...", "debug") except: resp_text = response.text self.log("⚠️ 响应不是有效的JSON格式", "warning") if response.status_code == 200: self.log(f"✅ 第{row_index}行 发送成功", "success") # 检查返回的data中是否包含图片上传信息 try: resp_json = response.json() if 'data' in resp_json: data_content = resp_json['data'] if isinstance(data_content, dict): # 检查是否有图片上传相关信息 if 'image' in str(data_content).lower() or 'picture' in str(data_content).lower(): self.log(" 🖼️ 响应中包含图片处理信息", "debug") except: pass return True, resp_text else: self.log(f"❌ 第{row_index}行 发送失败 (HTTP {response.status_code})", "error") self.log(f" 响应内容: {resp_text[:500]}...", "error") return False, resp_text except requests.exceptions.Timeout: self.log(f"⏱️ 第{row_index}行 请求超时", "error") return False, "请求超时" except requests.exceptions.ConnectionError: self.log(f"🔌 第{row_index}行 连接失败,请检查网络", "error") return False, "连接失败" except Exception as e: self.log(f"💥 第{row_index}行 异常: {str(e)}", "error") return False, str(e) finally: self.log(f"{'='*50}\n", "info") def send_all(self): """发送所有数据的主循环""" try: # 获取配置 excel_file = self.file_path_var.get() sheet_name = self.sheet_var.get() min_interval = float(self.min_interval_var.get()) max_interval = float(self.max_interval_var.get()) start_row = int(self.start_row_var.get()) - 1 # 转为0索引 if not excel_file: self.log("❌ 请先选择Excel文件", "error") self.status_var.set("❌ 请先选择Excel文件") return # 读取Excel try: df = pd.read_excel(excel_file, sheet_name=sheet_name) self.log(f"📊 成功读取Excel: {len(df)} 行数据", "info") self.log(f"📋 列名: {', '.join(df.columns.tolist())}", "info") # 检查是否有图片相关列 image_cols = [col for col in df.columns if any(kw in str(col).lower() for kw in ['图', '片', '照', '像', 'image', 'photo', 'pic', 'img', '附件'])] if image_cols: self.log(f"📷 发现图片相关列: {', '.join(image_cols)}", "info") # 检查这些列是否有数据 for col in image_cols: non_empty = df[col].notna().sum() if non_empty > 0: self.log(f" ✅ 列 '{col}' 有 {non_empty} 行数据", "info") # 显示一些示例数据 sample = df[col].dropna().iloc[0] if non_empty > 0 else None if sample: self.log(f" 📎 示例数据: {str(sample)[:100]}...", "debug") else: self.log(f" ⚠️ 列 '{col}' 为空", "warning") else: self.log("⚠️ 未发现图片相关列,请检查列名是否包含'图片'、'照片'等关键词", "warning") except Exception as e: self.log(f"❌ 读取Excel失败: {str(e)}", "error") self.status_var.set(f"❌ 读取失败: {str(e)}") return if len(df) == 0: self.log("⚠️ Excel中没有数据", "warning") self.status_var.set("⚠️ 没有数据") return # 从指定行开始 df = df.iloc[start_row:] total = len(df) success_count = 0 fail_count = 0 self.log(f"🚀 开始发送,共 {total} 条数据", "info") self.status_var.set(f"🔄 发送中... 0/{total}") for idx, (_, row) in enumerate(df.iterrows()): if self.stop_flag: self.log("⏹️ 已停止发送", "warning") self.status_var.set("⏹️ 已停止") break row_index = start_row + idx + 1 # 实际行号 # 发送请求 success, resp_text = self.send_request(row.to_dict(), row_index) if success: success_count += 1 else: fail_count += 1 # 更新状态 self.status_var.set(f"🔄 发送中... {idx+1}/{total} (成功:{success_count} 失败:{fail_count})") # 随机间隔(最后一条不等待) if idx < total - 1 and not self.stop_flag: interval = random.uniform(min_interval, max_interval) self.log(f"⏳ 等待 {interval:.1f} 秒...", "info") time.sleep(interval) # 完成统计 self.log(f"🏁 发送完成!成功: {success_count}, 失败: {fail_count}", "success" if fail_count == 0 else "warning") self.status_var.set(f"✅ 完成 成功:{success_count} 失败:{fail_count}") except Exception as e: self.log(f"💥 程序异常: {str(e)}", "error") self.status_var.set(f"❌ 异常: {str(e)}") finally: self.is_running = False self.start_btn.config(state=tk.NORMAL) self.stop_btn.config(state=tk.DISABLED) def start_send(self): """启动发送线程""" if self.is_running: return # 验证配置 if not self.file_path_var.get(): messagebox.showwarning("提示", "请先选择Excel文件") return cookie = self.cookie_text.get(1.0, tk.END).strip() if not cookie: messagebox.showwarning("提示", "请先配置Cookie") return try: float(self.min_interval_var.get()) float(self.max_interval_var.get()) int(self.start_row_var.get()) except ValueError: messagebox.showerror("错误", "间隔或行号请输入有效数字") return # 重置状态 self.is_running = True self.stop_flag = False self.start_btn.config(state=tk.DISABLED) self.stop_btn.config(state=tk.NORMAL) self.status_var.set("🔄 正在发送...") # 启动线程 thread = threading.Thread(target=self.send_all, daemon=True) thread.start() def stop_send(self): """停止发送""" self.stop_flag = True self.log("⏹️ 正在停止...", "warning") self.stop_btn.config(state=tk.DISABLED) if __name__ == "__main__": root = tk.Tk() app = FeishuSubmitApp(root) root.mainloop()