| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- import json
- from datetime import datetime, timezone, timedelta
- import tkinter as tk
- from tkinter import ttk, messagebox, filedialog
- import pandas as pd
- import os
- from threading import Thread
- def parse_records_from_text(json_text):
- """
- 从粘贴的JSON文本中解析记录数据
- """
- try:
- # 1. 解析外层JSON
- if isinstance(json_text, dict):
- data = json_text
- else:
- data = json.loads(json_text)
-
- # 获取records字段
- records_str = data.get('data', {}).get('records', '')
- if not records_str:
- return {'error': 'records字段为空', 'records': []}
-
- # 解析内层records JSON
- records_data = json.loads(records_str)
- record_map = records_data.get('recordMap', {})
-
- if not record_map:
- return {'error': 'recordMap为空', 'records': []}
-
- # 获取fieldMap
- formula_info = records_data.get('formulaInfo', {})
- field_map = formula_info.get('fieldMap', {})
-
- # 解析所有记录
- results = []
- for record_key, record in record_map.items():
- # 提取手机号
- phone = None
- if 'fld0af1E3B' in record:
- phone_data = record['fld0af1E3B'].get('value', {})
- if phone_data and isinstance(phone_data, dict):
- phone = phone_data.get('fullPhoneNum')
-
- # 提取批量数据编码
- batch_code = None
- if 'fldbQDk5b7' in record:
- values = record['fldbQDk5b7'].get('value', [])
- if values and len(values) > 0:
- batch_code = values[0].get('number')
-
- # 提取有顾问跟进
- has_advisor = None
- if 'fldSiIb9nZ' in field_map:
- field_value = field_map['fldSiIb9nZ'].get(record_key, {}).get('value')
- if field_value:
- try:
- field_data = json.loads(field_value)
- if field_data.get('data') and len(field_data['data']) > 0:
- has_advisor = field_data['data'][0]
- except:
- pass
-
- # 提取批量创建时间
- batch_create_time = None
- if 'fldlzQO4tZ' in field_map:
- field_value = field_map['fldlzQO4tZ'].get(record_key, {}).get('value')
- if field_value:
- try:
- field_data = json.loads(field_value)
- if field_data.get('data') and len(field_data['data']) > 0:
- timestamp_data = field_data['data'][0]
- if isinstance(timestamp_data, dict) and 'value' in timestamp_data:
- timestamp_ms = timestamp_data['value']
- if timestamp_ms and isinstance(timestamp_ms, (int, float)):
- if timestamp_ms > 1000000000000:
- timestamp_sec = timestamp_ms / 1000
- else:
- timestamp_sec = timestamp_ms
- dt = datetime.fromtimestamp(timestamp_sec, tz=timezone(timedelta(hours=8)))
- batch_create_time = dt.strftime('%Y-%m-%d %H:%M:%S')
- except:
- pass
-
- # 提取批量创建人员
- batch_creator = None
- if 'fldm8odlu2' in field_map:
- field_value = field_map['fldm8odlu2'].get(record_key, {}).get('value')
- if field_value:
- try:
- field_data = json.loads(field_value)
- if field_data.get('data') and len(field_data['data']) > 0:
- batch_creator = field_data['data'][0]
- except:
- pass
-
- # 提取创建人姓名(从recordMeta)
- created_user = None
- if 'recordMeta' in records_data:
- meta = records_data['recordMeta'].get(record_key, {}).get('recMeta', {})
- if 'createdUser' in meta:
- # 这里存储的是用户ID,可以尝试从userMap获取姓名
- user_map = records_data.get('userMap', {})
- user_info = user_map.get(meta['createdUser'], {})
- if user_info:
- created_user = user_info.get('name') or user_info.get('enName')
- if not created_user:
- created_user = meta['createdUser']
-
- results.append({
- '手机号': phone or '无数据',
- '批量数据编码': batch_code or '无数据',
- '有顾问跟进': has_advisor or '未知',
- '批量创建时间': batch_create_time or '无数据',
- '批量创建人员': batch_creator or '无数据',
- '创建人': created_user or '无数据'
- })
-
- # 映射显示名称
- advisor_map = {
- 'optXaXhoOh': '有顾问跟进',
- 'optJdspBAs': '无顾问跟进'
- }
-
- for record in results:
- if record['有顾问跟进'] in advisor_map:
- record['有顾问跟进'] = advisor_map[record['有顾问跟进']]
-
- return {'error': None, 'records': results, 'count': len(results)}
-
- except json.JSONDecodeError as e:
- return {'error': f'JSON解析错误: {str(e)}', 'records': []}
- except Exception as e:
- return {'error': f'处理错误: {str(e)}', 'records': []}
- def parse_and_display():
- """解析粘贴的JSON数据并显示在表格中"""
- json_text = text_input.get("1.0", tk.END).strip()
- if not json_text:
- messagebox.showwarning("提示", "请粘贴JSON数据!")
- return
-
- # 禁用按钮
- btn_parse.config(state='disabled')
- btn_parse.config(text='解析中...')
- btn_export.config(state='disabled')
-
- # 清空表格
- tree.delete(*tree.get_children())
-
- # 更新状态
- label_status.config(text="正在解析数据...", foreground='blue')
- root.update()
-
- def do_parse():
- try:
- result = parse_records_from_text(json_text)
-
- if result.get('error'):
- root.after(0, lambda: messagebox.showerror("错误", f"解析失败: {result['error']}"))
- root.after(0, lambda: label_status.config(text="解析失败", foreground='red'))
- else:
- records = result.get('records', [])
- count = result.get('count', 0)
-
- # 显示在表格中
- for record in records:
- root.after(0, lambda r=record:
- tree.insert('', 'end', values=(
- r['手机号'],
- r['批量数据编码'],
- r['有顾问跟进'],
- r['批量创建时间'],
- r['批量创建人员']
- )))
- root.update()
-
- # 保存结果到内存,供导出使用
- global last_results
- last_results = records
-
- # 更新状态
- root.after(0, lambda: label_status.config(
- text=f"解析完成!共 {count} 条记录",
- foreground='green'))
- root.after(0, lambda: btn_export.config(state='normal'))
-
- if count == 0:
- root.after(0, lambda: messagebox.showinfo("提示", "未解析到任何记录"))
-
- except Exception as e:
- root.after(0, lambda: messagebox.showerror("错误", f"解析失败: {str(e)}"))
- root.after(0, lambda: label_status.config(text="解析失败", foreground='red'))
- finally:
- root.after(0, lambda: btn_parse.config(state='normal'))
- root.after(0, lambda: btn_parse.config(text='解析数据'))
-
- Thread(target=do_parse, daemon=True).start()
- def export_to_excel():
- """导出结果到Excel"""
- if not tree.get_children():
- messagebox.showwarning("提示", "没有数据可导出,请先解析数据!")
- return
-
- # 获取表格数据
- data = []
- for item in tree.get_children():
- values = tree.item(item)['values']
- data.append({
- '手机号': values[0],
- '批量数据编码': values[1],
- '有顾问跟进': values[2],
- '批量创建时间': values[3],
- '批量创建人员': values[4]
- })
-
- if not data:
- messagebox.showwarning("提示", "没有数据可导出!")
- return
-
- # 选择保存位置
- file_path = filedialog.asksaveasfilename(
- defaultextension=".xlsx",
- filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv")],
- title="保存数据",
- initialfile=f"飞书数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
- )
-
- if not file_path:
- return
-
- try:
- df = pd.DataFrame(data)
- df.to_excel(file_path, index=False, engine='openpyxl')
- messagebox.showinfo("成功", f"数据已导出到:\n{file_path}")
- label_status.config(text=f"已导出到:{os.path.basename(file_path)}", foreground='green')
- except Exception as e:
- messagebox.showerror("错误", f"导出失败: {str(e)}")
- def clear_all():
- """清空所有"""
- text_input.delete("1.0", tk.END)
- tree.delete(*tree.get_children())
- label_status.config(text="已清空", foreground='gray')
- btn_export.config(state='disabled')
- global last_results
- last_results = []
- def load_example():
- """加载示例数据"""
- example_text = """{"code":0,"msg":"","data":{"records":"{\\"tableID\\":\\"tblXQQeIwMqSFSME\\",\\"recordMap\\":{\\"recvtyUtg2C0Xl\\":{\\"fld0af1E3B\\":{\\"value\\":{\\"fullPhoneNum\\":\\"18616756877\\"}},\\"fldbQDk5b7\\":{\\"value\\":[{\\"number\\":\\"SHPLSJ202608281532312\\"}]}},\\"recvtyUtg2Z6yf\\":{\\"fld0af1E3B\\":{\\"value\\":{\\"fullPhoneNum\\":\\"13918255747\\"}},\\"fldbQDk5b7\\":{\\"value\\":[{\\"number\\":\\"SHPLSJ202608281532385\\"}]}}},\\"formulaInfo\\":{\\"fieldMap\\":{\\"fldSiIb9nZ\\":{\\"recvtyUtg2C0Xl\\":{\\"value\\":\\"{\\\\\\"bus_type\\\\\\":[3],\\\\\\"data\\\\\\":[\\\\\\"optJdspBAs\\\\\\"]}\\"},\\"recvtyUtg2Z6yf\\":{\\"value\\":\\"{\\\\\\"bus_type\\\\\\":[3],\\\\\\"data\\\\\\":[\\\\\\"optJdspBAs\\\\\\"]}\\"}},\\"fldm8odlu2\\":{\\"recvtyUtg2C0Xl\\":{\\"value\\":\\"{\\\\\\"bus_type\\\\\\":[201],\\\\\\"data\\\\\\":[\\\\\\"/\\\\\\"]}\\"},\\"recvtyUtg2Z6yf\\":{\\"value\\":\\"{\\\\\\"bus_type\\\\\\":[201],\\\\\\"data\\\\\\":[\\\\\\"/\\\\\\"]}\\"}},\\"fldlzQO4tZ\\":{\\"recvtyUtg2C0Xl\\":{\\"value\\":\\"{\\\\\\"bus_type\\\\\\":[5],\\\\\\"data\\\\\\":[null]}\\"},\\"recvtyUtg2Z6yf\\":{\\"value\\":\\"{\\\\\\"bus_type\\\\\\":[5],\\\\\\"data\\\\\\":[{\\\\\\"value\\\\\\":1787082110000}]}\\"}}}},\\"recordMeta\\":{\\"recvtyUtg2C0Xl\\":{\\"recMeta\\":{\\"createdUser\\":\\"7646228072081099729\\"}},\\"recvtyUtg2Z6yf\\":{\\"recMeta\\":{\\"createdUser\\":\\"7646228072081099729\\"}}}}}"}}"""
- text_input.delete("1.0", tk.END)
- text_input.insert("1.0", example_text)
- label_status.config(text="已加载示例数据,点击「解析数据」查看结果", foreground='blue')
- # 创建GUI窗口
- root = tk.Tk()
- root.title("飞书JSON数据解析工具")
- root.geometry("950x700")
- root.resizable(False, False)
- # 设置样式
- style = ttk.Style()
- style.theme_use('clam')
- # 顶部:输入区域
- frame_top = ttk.LabelFrame(root, text="粘贴飞书API返回的JSON数据", padding="10")
- frame_top.pack(fill='x', padx=10, pady=5)
- text_input = tk.Text(frame_top, font=('Consolas', 9), height=10, wrap='none')
- text_input.pack(fill='x', expand=True)
- # 添加水平滚动条
- scrollbar_h = ttk.Scrollbar(frame_top, orient='horizontal', command=text_input.xview)
- text_input.configure(xscrollcommand=scrollbar_h.set)
- scrollbar_h.pack(fill='x')
- # 提示和按钮区域
- hint_frame = ttk.Frame(frame_top)
- hint_frame.pack(fill='x', pady=(5, 0))
- ttk.Label(hint_frame, text="提示:粘贴完整的API返回JSON(包含data.records字段)",
- font=('微软雅黑', 9), foreground='gray').pack(side='left')
- btn_frame = ttk.Frame(frame_top)
- btn_frame.pack(fill='x', pady=(5, 0))
- btn_parse = ttk.Button(btn_frame, text="解析数据", command=parse_and_display, width=12)
- btn_parse.pack(side='left', padx=(0, 10))
- btn_example = ttk.Button(btn_frame, text="加载示例", command=load_example, width=10)
- btn_example.pack(side='left', padx=(0, 10))
- btn_clear = ttk.Button(btn_frame, text="清空", command=clear_all, width=10)
- btn_clear.pack(side='left', padx=(0, 10))
- btn_export = ttk.Button(btn_frame, text="导出Excel", command=export_to_excel, width=12, state='disabled')
- btn_export.pack(side='left')
- # 中间:结果显示表格
- frame_table = ttk.LabelFrame(root, text="解析结果", padding="10")
- frame_table.pack(fill='both', expand=True, padx=10, pady=5)
- # 创建Treeview
- columns = ('手机号', '批量数据编码', '有顾问跟进', '批量创建时间', '批量创建人员')
- tree = ttk.Treeview(frame_table, columns=columns, show='headings', height=15)
- # 设置列标题和宽度
- tree.heading('手机号', text='手机号')
- tree.heading('批量数据编码', text='批量数据编码')
- tree.heading('有顾问跟进', text='有顾问跟进')
- tree.heading('批量创建时间', text='批量创建时间')
- tree.heading('批量创建人员', text='批量创建人员')
- tree.column('手机号', width=150, anchor='center')
- tree.column('批量数据编码', width=200, anchor='center')
- tree.column('有顾问跟进', width=150, anchor='center')
- tree.column('批量创建时间', width=180, anchor='center')
- tree.column('批量创建人员', width=180, anchor='center')
- # 添加滚动条
- scrollbar = ttk.Scrollbar(frame_table, orient='vertical', command=tree.yview)
- tree.configure(yscrollcommand=scrollbar.set)
- tree.pack(side='left', fill='both', expand=True)
- scrollbar.pack(side='right', fill='y')
- # 底部:状态栏
- frame_bottom = ttk.Frame(root, padding="5")
- frame_bottom.pack(fill='x')
- label_status = ttk.Label(frame_bottom, text="请粘贴JSON数据,点击「解析数据」", font=('微软雅黑', 9), foreground='gray')
- label_status.pack(side='left')
- # 存储最近一次查询结果(用于导出)
- last_results = []
- # 运行主循环
- root.mainloop()
|