fit_content.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. # 读取飞书表格的抓取结果字段,取出body_text和title,替换识别结果中的body_text和title
  2. #
  3. # 功能说明:
  4. # 1. 读取飞书表格中的"抓取结果"字段
  5. # 2. 从抓取结果中提取body_text和title
  6. # 3. 读取"识别结果"字段中的现有内容
  7. # 4. 用抓取结果中的body_text和title替换识别结果中的相应字段
  8. # 5. 保持识别结果中的images_comprehension字段不变
  9. # 6. 更新飞书表格中的识别结果字段
  10. #
  11. # 使用方法:
  12. # 1. 设置环境变量:
  13. # - FEISHU_APP_ID: 飞书应用ID
  14. # - FEISHU_APP_SECRET: 飞书应用密钥
  15. # - FEISHU_FILE_TOKEN: 飞书文件Token
  16. # - FEISHU_TABLE_ID: 飞书表格ID (可选,也可在运行时传入)
  17. # - FEISHU_CRAWL_FIELD: 抓取结果字段名 (默认: '抓取结果')
  18. # - FEISHU_IDENTIFY_FIELD: 识别结果字段名 (默认: '识别结果')
  19. #
  20. # 2. 运行脚本:
  21. # python fit_content.py [table_id] [--dry-run]
  22. #
  23. # 示例:
  24. # python fit_content.py tblNdje7z6Cf3hax # 正常模式
  25. # python fit_content.py tblNdje7z6Cf3hax --dry-run # 试运行模式
  26. # python fit_content.py --dry-run # 使用环境变量中的表格ID,试运行模式
  27. #
  28. # 注意事项:
  29. # - 试运行模式会显示将要处理的内容,但不会实际更新飞书表格
  30. # - 脚本会自动处理分页,支持大量数据
  31. # - 如果抓取结果或识别结果解析失败,会跳过该记录并继续处理其他记录
  32. import json
  33. import os
  34. import sys
  35. from typing import Dict, Any, List, Optional
  36. from dotenv import load_dotenv
  37. # 导入自定义模块
  38. sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  39. from utils.fei_shu import FeiShu
  40. class ContentFitter:
  41. def __init__(self, table_id: Optional[str] = None):
  42. # 加载环境变量
  43. load_dotenv()
  44. # 初始化飞书客户端
  45. self.feishu = FeiShu()
  46. # 获取表格ID:优先使用传入的参数,其次使用环境变量
  47. self.table_id = table_id or os.getenv('FEISHU_TABLE_ID')
  48. if not self.table_id:
  49. raise ValueError("请设置环境变量 FEISHU_TABLE_ID 或在运行时传入 table_id 参数")
  50. # 字段名称配置
  51. self.crawl_field = os.getenv('FEISHU_CRAWL_FIELD', '抓取结果')
  52. self.identify_field = os.getenv('FEISHU_IDENTIFY_FIELD', '识别结果')
  53. def extract_crawl_content(self, crawl_result) -> Dict[str, str]:
  54. """从抓取结果中提取body_text和title"""
  55. title = ''
  56. body_text = ''
  57. if not crawl_result:
  58. return {'title': title, 'body_text': body_text}
  59. try:
  60. # 如果是字符串格式,尝试直接解析
  61. if isinstance(crawl_result, str):
  62. json_data = json.loads(crawl_result)
  63. elif isinstance(crawl_result, list) and len(crawl_result) > 0:
  64. # 如果是数组格式,取第一个元素
  65. crawl_data = crawl_result[0]
  66. if isinstance(crawl_data, dict):
  67. if 'text' in crawl_data:
  68. # 如果crawl_data是包含text字段的字典
  69. json_data = json.loads(crawl_data['text'])
  70. else:
  71. # 如果crawl_data是直接的字典数据
  72. json_data = crawl_data
  73. else:
  74. # 如果crawl_data不是字典,尝试直接解析
  75. json_data = crawl_data
  76. elif isinstance(crawl_result, dict):
  77. # 如果crawl_result本身就是字典
  78. json_data = crawl_result
  79. else:
  80. # 其他情况,尝试直接使用
  81. json_data = crawl_result
  82. # 确保json_data是字典类型
  83. if not isinstance(json_data, dict):
  84. print(f"抓取结果格式不正确,期望字典类型,实际类型: {type(json_data)}")
  85. return {'title': title, 'body_text': body_text}
  86. # 提取标题和正文内容
  87. title = json_data.get('title', '')
  88. body_text = json_data.get('body_text', '')
  89. except (json.JSONDecodeError, KeyError, TypeError, AttributeError) as e:
  90. print(f"解析抓取结果失败: {e}")
  91. # 如果解析失败,尝试直接使用文本内容
  92. if isinstance(crawl_result, str):
  93. body_text = crawl_result
  94. elif isinstance(crawl_result, list) and len(crawl_result) > 0:
  95. # 如果是列表,尝试将第一个元素转为字符串
  96. body_text = str(crawl_result[0])
  97. return {'title': title, 'body_text': body_text}
  98. def extract_identify_content(self, identify_result) -> Dict[str, Any]:
  99. """从识别结果中提取现有内容"""
  100. images_comprehension = []
  101. title = ''
  102. body_text = ''
  103. if not identify_result:
  104. print(f" 调试: identify_result为空")
  105. return {'images_comprehension': images_comprehension, 'title': title, 'body_text': body_text}
  106. print(f" 调试: identify_result类型: {type(identify_result)}")
  107. print(f" 调试: identify_result内容前100字符: {identify_result[:100]}...")
  108. try:
  109. # 如果是字符串格式,尝试解析JSON
  110. if isinstance(identify_result, str):
  111. print(f" 调试: 尝试解析字符串格式的identify_result")
  112. json_data = self.safe_json_loads(identify_result)
  113. if json_data is None:
  114. print(f" 调试: JSON解析失败,返回空结果")
  115. return {'images_comprehension': images_comprehension, 'title': title, 'body_text': body_text}
  116. elif isinstance(identify_result, dict):
  117. print(f" 调试: identify_result本身就是字典")
  118. json_data = identify_result
  119. elif isinstance(identify_result, list) and len(identify_result) > 0:
  120. print(f" 调试: identify_result是列表,合并所有元素的内容")
  121. # 合并列表中所有元素的text字段
  122. combined_text = ""
  123. for i, item in enumerate(identify_result):
  124. if isinstance(item, dict) and 'text' in item:
  125. combined_text += item['text']
  126. print(f" 调试: 合并第{i+1}个元素的text,当前长度: {len(combined_text)}")
  127. print(f" 调试: 合并后的文本长度: {len(combined_text)}")
  128. if combined_text:
  129. json_data = self.safe_json_loads(combined_text)
  130. if json_data is None:
  131. print(f" 调试: 合并后JSON解析失败")
  132. return {'images_comprehension': images_comprehension, 'title': title, 'body_text': body_text}
  133. else:
  134. print(f" 调试: 没有找到text字段")
  135. return {'images_comprehension': images_comprehension, 'title': title, 'body_text': body_text}
  136. else:
  137. print(f" 调试: identify_result是其他类型: {type(identify_result)}")
  138. json_data = identify_result
  139. # 确保json_data是字典类型
  140. if not isinstance(json_data, dict):
  141. print(f"识别结果格式不正确,期望字典类型,实际类型: {type(json_data)}")
  142. return {'images_comprehension': images_comprehension, 'title': title, 'body_text': body_text}
  143. print(f" 调试: json_data键: {list(json_data.keys())}")
  144. # 检查是否有text字段,如果有,尝试解析其中的JSON
  145. if 'text' in json_data and isinstance(json_data['text'], str):
  146. print(f" 调试: 发现text字段,尝试解析其中的JSON")
  147. text_content = self.safe_json_loads(json_data['text'])
  148. if text_content and isinstance(text_content, dict):
  149. print(f" 调试: text字段解析成功,键: {list(text_content.keys())}")
  150. # 从text_content中提取字段
  151. images_comprehension = text_content.get('images_comprehension', [])
  152. title = text_content.get('title', '')
  153. body_text = text_content.get('body_text', '')
  154. else:
  155. print(f" 调试: text字段解析失败或不是字典")
  156. # 如果text字段解析失败,尝试直接提取images_comprehension数组
  157. print(f" 调试: 尝试直接从text字段中提取images_comprehension数组")
  158. images_comprehension = self.extract_images_comprehension_from_text(json_data['text'])
  159. title = json_data.get('title', '')
  160. body_text = json_data.get('body_text', '')
  161. else:
  162. # 直接从json_data中提取字段
  163. images_comprehension = json_data.get('images_comprehension', [])
  164. title = json_data.get('title', '')
  165. body_text = json_data.get('body_text', '')
  166. print(f" 调试: 提取的images_comprehension类型: {type(images_comprehension)}, 值: {images_comprehension}")
  167. # 确保images_comprehension是列表格式
  168. if not isinstance(images_comprehension, list):
  169. if isinstance(images_comprehension, str):
  170. # 如果是字符串,尝试解析为列表
  171. try:
  172. print(f" 调试: images_comprehension是字符串,尝试解析为列表")
  173. images_comprehension = json.loads(images_comprehension)
  174. if not isinstance(images_comprehension, list):
  175. images_comprehension = []
  176. except (json.JSONDecodeError, TypeError):
  177. print(f" 调试: 解析images_comprehension字符串失败")
  178. images_comprehension = []
  179. else:
  180. print(f" 调试: images_comprehension不是列表也不是字符串,设置为空列表")
  181. images_comprehension = []
  182. # 调试信息:打印images_comprehension的结构
  183. if images_comprehension:
  184. print(f" 调试: images_comprehension类型: {type(images_comprehension)}, 长度: {len(images_comprehension)}")
  185. if len(images_comprehension) > 0:
  186. print(f" 调试: 第一个元素类型: {type(images_comprehension[0])}")
  187. if isinstance(images_comprehension[0], dict):
  188. print(f" 调试: 第一个元素键: {list(images_comprehension[0].keys())}")
  189. else:
  190. print(f" 调试: images_comprehension为空")
  191. except (json.JSONDecodeError, KeyError, TypeError, AttributeError) as e:
  192. print(f"解析识别结果失败: {e}")
  193. print(f" 调试: identify_result类型: {type(identify_result)}")
  194. if isinstance(identify_result, str):
  195. print(f" 调试: identify_result内容前100字符: {identify_result[:100]}...")
  196. return {'images_comprehension': images_comprehension, 'title': title, 'body_text': body_text}
  197. def merge_content(self, crawl_content: Dict[str, str], identify_content: Dict[str, Any]) -> Dict[str, Any]:
  198. """合并抓取内容和识别内容,用抓取内容替换识别内容中的title和body_text"""
  199. return {
  200. 'images_comprehension': identify_content.get('images_comprehension', []), # 保持数组格式
  201. 'title': crawl_content.get('title', ''), # 使用抓取结果的title
  202. 'body_text': crawl_content.get('body_text', '') # 使用抓取结果的body_text
  203. }
  204. def update_feishu_record(self, record_id: str, merged_content: Dict[str, Any]):
  205. """更新飞书表格中的记录"""
  206. try:
  207. import lark_oapi as lark
  208. # 创建更新记录
  209. update_record = (lark.bitable.v1.AppTableRecord.builder()
  210. .record_id(record_id)
  211. .fields({
  212. self.identify_field: json.dumps(merged_content, ensure_ascii=False)
  213. })
  214. .build())
  215. # 执行更新
  216. self.feishu.update_record(self.table_id, update_record)
  217. print(f"已更新记录 {record_id}")
  218. except Exception as e:
  219. print(f"更新飞书记录失败: {e}")
  220. def process_single_record(self, record, dry_run: bool = False) -> bool:
  221. """处理单条记录"""
  222. try:
  223. fields = record.fields
  224. # 提取抓取结果
  225. crawl_result = fields.get(self.crawl_field, '')
  226. if not crawl_result:
  227. print(f"记录 {record.record_id} 没有抓取结果,跳过")
  228. return True
  229. # 提取识别结果
  230. identify_result = fields.get(self.identify_field, '')
  231. print(f" 调试: 原始identify_result类型: {type(identify_result)}")
  232. if isinstance(identify_result, str):
  233. print(f" 调试: 原始identify_result内容前200字符: {identify_result[:200]}...")
  234. # 从抓取结果中提取title和body_text
  235. crawl_content = self.extract_crawl_content(crawl_result)
  236. # 从识别结果中提取现有内容
  237. identify_content = self.extract_identify_content(identify_result)
  238. # 合并内容,用抓取结果替换识别结果中的title和body_text
  239. merged_content = self.merge_content(crawl_content, identify_content)
  240. print(f"处理记录 {record.record_id}")
  241. print(f" 抓取结果 - 标题: {crawl_content['title'][:50] if crawl_content['title'] else '无标题'}...")
  242. print(f" 抓取结果 - 内容长度: {len(crawl_content['body_text'])} 字符")
  243. # 处理images_comprehension的打印
  244. images_comp = identify_content['images_comprehension']
  245. if isinstance(images_comp, list) and len(images_comp) > 0:
  246. # 显示第一个元素的内容预览
  247. first_item = images_comp[0]
  248. if isinstance(first_item, dict):
  249. content_preview = first_item.get('content', '')[:50] if first_item.get('content') else '无内容'
  250. print(f" 识别结果 - 图片理解: [{len(images_comp)}项] 第一项内容: {content_preview}...")
  251. else:
  252. images_comp_text = str(first_item)[:50] + "..." if len(str(first_item)) > 50 else str(first_item)
  253. print(f" 识别结果 - 图片理解: [{len(images_comp)}项] {images_comp_text}")
  254. else:
  255. print(f" 识别结果 - 图片理解: 无图片理解")
  256. if not dry_run:
  257. # 更新飞书表格
  258. self.update_feishu_record(record.record_id, merged_content)
  259. else:
  260. print(f" [试运行] 将更新识别结果字段,新内容: {json.dumps(merged_content, ensure_ascii=False)[:100]}...")
  261. return True
  262. except Exception as e:
  263. print(f"处理记录 {record.record_id} 失败: {e}")
  264. return False
  265. def process_all_records(self, dry_run: bool = False):
  266. """处理所有记录"""
  267. mode_text = "试运行模式" if dry_run else "正常模式"
  268. print(f"开始处理飞书表格 {self.table_id} 中的所有记录 ({mode_text})")
  269. page_token = None
  270. total_processed = 0
  271. total_success = 0
  272. while True:
  273. try:
  274. # 获取记录
  275. result = self.feishu.get_all_records(self.table_id, page_token)
  276. if not result.items:
  277. print("没有找到记录")
  278. break
  279. print(f"获取到 {len(result.items)} 条记录")
  280. # 处理每条记录
  281. for record in result.items:
  282. total_processed += 1
  283. if self.process_single_record(record, dry_run):
  284. total_success += 1
  285. # 检查是否有下一页
  286. if not result.has_more:
  287. break
  288. page_token = result.page_token
  289. print(f"继续获取下一页,token: {page_token}")
  290. except Exception as e:
  291. print(f"获取记录失败: {e}")
  292. break
  293. print(f"处理完成!总共处理 {total_processed} 条记录,成功 {total_success} 条")
  294. def safe_json_loads(self, json_str: str) -> Any:
  295. """安全地解析JSON字符串,处理可能的语法错误"""
  296. if not isinstance(json_str, str):
  297. return json_str
  298. try:
  299. return json.loads(json_str)
  300. except json.JSONDecodeError as e:
  301. print(f" 调试: JSON解析失败: {e}")
  302. # 尝试修复常见的JSON语法错误
  303. try:
  304. # 移除多余的逗号
  305. fixed_json = json_str.replace(',,', ',')
  306. # 移除末尾的逗号
  307. fixed_json = fixed_json.rstrip(',')
  308. # 移除末尾的多个逗号
  309. while fixed_json.endswith(',}'):
  310. fixed_json = fixed_json[:-2] + '}'
  311. while fixed_json.endswith(',]'):
  312. fixed_json = fixed_json[:-2] + ']'
  313. # 尝试修复未终止的字符串
  314. if 'Unterminated string' in str(e):
  315. print(f" 调试: 检测到未终止的字符串,尝试修复")
  316. # 查找最后一个完整的JSON对象
  317. import re
  318. # 查找匹配的大括号
  319. brace_count = 0
  320. end_pos = -1
  321. for i, char in enumerate(fixed_json):
  322. if char == '{':
  323. brace_count += 1
  324. elif char == '}':
  325. brace_count -= 1
  326. if brace_count == 0:
  327. end_pos = i
  328. break
  329. if end_pos > 0:
  330. fixed_json = fixed_json[:end_pos + 1]
  331. print(f" 调试: 截取到位置 {end_pos + 1}")
  332. return json.loads(fixed_json)
  333. except json.JSONDecodeError:
  334. print(f" 调试: 修复JSON后仍然解析失败")
  335. # 尝试更激进的修复
  336. try:
  337. # 如果还是失败,尝试找到最后一个有效的JSON对象
  338. import re
  339. # 查找最后一个完整的JSON对象
  340. pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
  341. matches = re.findall(pattern, json_str)
  342. if matches:
  343. last_match = matches[-1]
  344. return json.loads(last_match)
  345. except:
  346. pass
  347. # 最后的尝试:手动构建JSON对象
  348. try:
  349. print(f" 调试: 尝试手动提取关键字段")
  350. # 尝试提取images_comprehension字段
  351. import re
  352. # 查找images_comprehension数组的开始
  353. pattern = r'"images_comprehension":\s*\[(.*?)\]'
  354. match = re.search(pattern, json_str, re.DOTALL)
  355. if match:
  356. array_content = match.group(1)
  357. # 尝试解析数组内容
  358. try:
  359. # 构建一个简单的JSON对象
  360. simple_json = f'{{"images_comprehension": [{array_content}]}}'
  361. return json.loads(simple_json)
  362. except:
  363. pass
  364. except:
  365. pass
  366. return None
  367. def extract_images_comprehension_from_text(self, text: str) -> list:
  368. """直接从文本中提取images_comprehension数组"""
  369. try:
  370. import re
  371. # 查找images_comprehension数组的开始和结束
  372. pattern = r'"images_comprehension":\s*\[(.*?)\]'
  373. match = re.search(pattern, text, re.DOTALL)
  374. if match:
  375. array_content = match.group(1)
  376. print(f" 调试: 找到images_comprehension数组内容,长度: {len(array_content)}")
  377. # 尝试解析数组内容
  378. try:
  379. # 构建一个简单的JSON对象
  380. simple_json = f'{{"images_comprehension": [{array_content}]}}'
  381. result = json.loads(simple_json)
  382. return result.get('images_comprehension', [])
  383. except json.JSONDecodeError as e:
  384. print(f" 调试: 解析数组内容失败: {e}")
  385. # 尝试手动解析数组中的对象
  386. try:
  387. # 查找数组中的每个对象
  388. object_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
  389. objects = re.findall(object_pattern, array_content)
  390. print(f" 调试: 找到 {len(objects)} 个对象")
  391. parsed_objects = []
  392. for obj_str in objects:
  393. try:
  394. obj = json.loads(obj_str)
  395. parsed_objects.append(obj)
  396. except:
  397. # 如果单个对象解析失败,跳过
  398. continue
  399. return parsed_objects
  400. except Exception as e2:
  401. print(f" 调试: 手动解析对象失败: {e2}")
  402. return []
  403. else:
  404. print(f" 调试: 未找到images_comprehension数组")
  405. return []
  406. except Exception as e:
  407. print(f" 调试: 提取images_comprehension失败: {e}")
  408. return []
  409. def main():
  410. """主函数"""
  411. import argparse
  412. parser = argparse.ArgumentParser(description='读取飞书表格抓取结果,替换识别结果中的body_text和title')
  413. parser.add_argument('table_id', nargs='?', help='飞书表格ID')
  414. parser.add_argument('--dry-run', action='store_true', help='试运行模式,只显示会处理的记录,不实际更新')
  415. args = parser.parse_args()
  416. try:
  417. # 创建ContentFitter实例
  418. fitter = ContentFitter(args.table_id)
  419. # 处理所有记录
  420. fitter.process_all_records(dry_run=args.dry_run)
  421. except Exception as e:
  422. print(f"程序执行失败: {e}")
  423. sys.exit(1)
  424. if __name__ == '__main__':
  425. main()