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 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") # ========== 底部状态栏 ========== 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 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 "" # 构建备注文本(包含所有信息) 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} } # 构建完整的请求体 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: 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: return False, "构建请求体失败" # 准备请求 headers = self.default_headers.copy() # 更新Cookie cookies = self.parse_cookie(cookie_string) # 发送请求 response = requests.post( self.url, headers=headers, cookies=cookies, json=body, timeout=30 ) # 解析响应 try: resp_data = response.json() resp_text = json.dumps(resp_data, ensure_ascii=False, indent=2) except: resp_text = response.text if response.status_code == 200: self.log(f"✅ 第{row_index}行 发送成功 | {resp_text[:200]}...", "success") return True, resp_text else: self.log(f"❌ 第{row_index}行 发送失败 (HTTP {response.status_code}) | {resp_text[:200]}...", "error") return False, resp_text except requests.exceptions.Timeout: self.log(f"⏱️ 第{row_index}行 请求超时", "error") return False, "请求超时" except Exception as e: self.log(f"💥 第{row_index}行 异常: {str(e)}", "error") return False, str(e) 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") 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()