From 7c30e5932814e7f2c318aa8a8b9d693ef5365928 Mon Sep 17 00:00:00 2001 From: python Date: Tue, 9 Dec 2025 14:13:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BAAI=E6=9C=8D=E5=8A=A1=E7=9A=84?= =?UTF-8?q?JSON=E8=A7=A3=E6=9E=90=E8=83=BD=E5=8A=9B=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=AF=B9jsonrepair=E5=BA=93=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=BB=A5=E5=A4=84=E7=90=86=E4=B8=8D=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E6=88=96=E6=A0=BC=E5=BC=8F=E9=94=99=E8=AF=AF=E7=9A=84JSON?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E8=BF=9B=E5=AD=97=E6=AE=B5=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E4=BB=A5=E5=85=81=E8=AE=B8=E9=83=A8=E5=88=86?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E4=B8=BA=E7=A9=BA=EF=BC=8C=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=8F=90=E5=8F=96=E7=9A=84=E5=AE=B9=E9=94=99?= =?UTF-8?q?=E6=80=A7=E5=92=8C=E5=87=86=E7=A1=AE=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 1 + services/ai_service.py | 209 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 194 insertions(+), 16 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5f5d77a..b82ed6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ flasgger==0.9.7.1 python-docx==1.1.0 minio==7.2.3 openpyxl==3.1.2 +jsonrepair==0.27.0 diff --git a/services/ai_service.py b/services/ai_service.py index d27da71..ae9e9ec 100644 --- a/services/ai_service.py +++ b/services/ai_service.py @@ -9,6 +9,14 @@ import requests import json from typing import Dict, List, Optional +# 尝试导入jsonrepair库,如果不可用则使用备用方案 +try: + import jsonrepair + JSONREPAIR_AVAILABLE = True +except ImportError: + JSONREPAIR_AVAILABLE = False + print("[AI服务] 警告: jsonrepair库未安装,将使用基础JSON修复功能。建议运行: pip install jsonrepair") + class AIService: """AI服务类""" @@ -287,7 +295,7 @@ class AIService: # 清理后的内容(前500字符) print(f"[AI服务] 清理后的内容(前500字符): {content[:500]}") - # 尝试解析JSON + # 尝试解析JSON(使用增强的修复机制) extracted_data = self._extract_json_from_text(content) if extracted_data: print(f"[AI服务] JSON解析成功,提取到 {len(extracted_data)} 个字段") @@ -311,11 +319,17 @@ class AIService: for key in ['target_name', 'target_gender', 'target_age', 'target_date_of_birth', 'target_organization', 'target_position']: if key in normalized_data: print(f"[AI服务] 后处理后 {key} = '{normalized_data[key]}'") - return normalized_data + # 即使提取的字段不完整,也返回结果(更宽容的处理) + if any(v for v in normalized_data.values() if v): # 至少有一个非空字段 + print(f"[AI服务] 返回提取的数据(包含 {sum(1 for v in normalized_data.values() if v)} 个非空字段)") + return normalized_data + else: + print(f"[AI服务] 警告:提取的数据全部为空,但继续返回(允许部分字段为空)") + return normalized_data - # 如果无法提取JSON,记录错误 - print(f"[AI服务] 警告:无法从内容中提取JSON,尝试备用解析方法") - print(f"[AI服务] 完整内容: {content}") + # 如果无法提取JSON,记录错误但尝试更宽容的处理 + print(f"[AI服务] 警告:无法从内容中提取完整JSON,尝试备用解析方法") + print(f"[AI服务] 清理后的内容(前500字符): {content[:500]}") # 尝试从文本中提取 parsed_data = self._parse_text_response(content, output_fields) @@ -323,8 +337,34 @@ class AIService: print(f"[AI服务] 使用备用方法解析成功,提取到 {len(parsed_data)} 个字段") return parsed_data - # 如果所有方法都失败,抛出异常 - raise Exception(f"无法从API返回内容中提取JSON数据。原始内容长度: {len(raw_content)}, 清理后内容长度: {len(content)}。请检查API返回的内容格式是否正确。") + # 如果所有方法都失败,尝试最后一次修复尝试 + print(f"[AI服务] 所有解析方法都失败,尝试最后一次修复...") + # 尝试使用jsonrepair(如果可用)进行最后修复 + if JSONREPAIR_AVAILABLE: + try: + repaired_content = jsonrepair.repair_json(content) + if repaired_content: + try: + extracted_data = json.loads(repaired_content) + if extracted_data and isinstance(extracted_data, dict): + print(f"[AI服务] 使用jsonrepair最后修复成功,提取到 {len(extracted_data)} 个字段") + normalized_data = self._normalize_field_names(extracted_data, output_fields) + normalized_data = self._normalize_date_formats(normalized_data, output_fields) + normalized_data = self._post_process_inferred_fields(normalized_data, output_fields) + return normalized_data + except json.JSONDecodeError: + pass + except Exception as e: + print(f"[AI服务] jsonrepair最后修复也失败: {e}") + + # 如果所有方法都失败,返回空字典而不是抛出异常(更宽容) + # 这样至少不会导致整个调用失败,前端可以显示部分结果 + print(f"[AI服务] 警告:无法从API返回内容中提取JSON数据,返回空结果。原始内容长度: {len(raw_content)}, 清理后内容长度: {len(content)}") + print(f"[AI服务] 完整内容: {content}") + # 返回一个包含所有输出字段的空字典,而不是抛出异常 + empty_result = {field['field_code']: '' for field in output_fields} + print(f"[AI服务] 返回空结果(包含 {len(empty_result)} 个字段,全部为空)") + return empty_result else: raise Exception("API返回格式异常:未找到choices字段或choices为空") @@ -349,7 +389,15 @@ class AIService: return json.loads(json_str) except json.JSONDecodeError as e: print(f"[AI服务] JSON解析失败(代码块): {e}") - # 尝试修复后再次解析 + # 尝试使用jsonrepair修复(如果可用) + if JSONREPAIR_AVAILABLE: + try: + repaired = jsonrepair.repair_json(json_str) + if repaired: + return json.loads(repaired) + except Exception as repair_error: + print(f"[AI服务] jsonrepair修复失败: {repair_error}") + # 尝试基础修复后再次解析 json_str = self._fix_json_string(json_str) try: return json.loads(json_str) @@ -367,7 +415,15 @@ class AIService: return json.loads(json_str) except json.JSONDecodeError as e: print(f"[AI服务] JSON解析失败(代码块): {e}") - # 尝试修复后再次解析 + # 尝试使用jsonrepair修复(如果可用) + if JSONREPAIR_AVAILABLE: + try: + repaired = jsonrepair.repair_json(json_str) + if repaired: + return json.loads(repaired) + except Exception as repair_error: + print(f"[AI服务] jsonrepair修复失败: {repair_error}") + # 尝试基础修复后再次解析 json_str = self._fix_json_string(json_str) try: return json.loads(json_str) @@ -380,7 +436,15 @@ class AIService: return json.loads(cleaned_text) except json.JSONDecodeError as e: print(f"[AI服务] JSON解析失败(直接解析): {e}") - # 尝试修复后再次解析 + # 尝试使用jsonrepair修复(如果可用) + if JSONREPAIR_AVAILABLE: + try: + repaired = jsonrepair.repair_json(cleaned_text) + if repaired: + return json.loads(repaired) + except Exception as repair_error: + print(f"[AI服务] jsonrepair修复失败: {repair_error}") + # 尝试基础修复后再次解析 fixed_text = self._fix_json_string(cleaned_text) try: return json.loads(fixed_text) @@ -424,6 +488,90 @@ class AIService: except json.JSONDecodeError: pass + # 方法5: 如果所有方法都失败,尝试部分提取(即使JSON不完整) + partial_data = self._extract_partial_json(text) + if partial_data: + print(f"[AI服务] 使用部分JSON提取,提取到 {len(partial_data)} 个字段") + return partial_data + + return None + + def _extract_partial_json(self, text: str) -> Optional[Dict]: + """ + 从可能不完整的JSON中提取可用字段 + 即使JSON格式不完整,也尝试提取能够解析的字段 + """ + result = {} + + # 尝试找到JSON对象的开始位置 + start_idx = text.find('{') + if start_idx == -1: + return None + + # 提取从{开始的内容 + json_content = text[start_idx:] + + # 尝试使用正则表达式提取键值对 + # 匹配模式: "key": "value" 或 "key": value + # 处理各种可能的格式错误 + + # 模式1: "key": "value" + pattern1 = r'"([^"]+?)"\s*:\s*"([^"]*?)"' + matches1 = re.finditer(pattern1, json_content, re.DOTALL) + for match in matches1: + key = match.group(1) + value = match.group(2) + # 清理键名(移除可能的转义字符) + key = key.replace('\\"', '').strip() + if key: + # 处理以_开头的字段名(如_professional_rank -> professional_rank) + if key.startswith('_') and len(key) > 1: + key = key[1:] + if key not in result: # 避免覆盖已有值 + result[key] = value + + # 模式2: "key": value (非字符串值,如数字、布尔值) + pattern2 = r'"([^"]+?)"\s*:\s*([^,}\]]+?)(?=\s*[,}\]])' + matches2 = re.finditer(pattern2, json_content, re.DOTALL) + for match in matches2: + key = match.group(1).strip() + value_str = match.group(2).strip() + # 清理键名 + key = key.replace('\\"', '').strip() + if key and key not in result: # 避免覆盖已有值 + # 处理以_开头的字段名 + original_key = key + if key.startswith('_') and len(key) > 1: + key = key[1:] + # 尝试解析值 + if value_str.lower() in ('true', 'false'): + result[key] = value_str.lower() == 'true' + elif value_str.lower() == 'null': + result[key] = None + elif value_str.replace('.', '', 1).replace('-', '', 1).isdigit(): + try: + if '.' in value_str: + result[key] = float(value_str) + else: + result[key] = int(value_str) + except ValueError: + result[key] = value_str + else: + result[key] = value_str + + # 模式3: 处理字段名缺少引号的情况(如 key: "value") + pattern3 = r'([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*"([^"]*?)"' + matches3 = re.finditer(pattern3, json_content, re.DOTALL) + for match in matches3: + key = match.group(1).strip() + value = match.group(2) + if key and key not in result: # 避免覆盖已有值 + result[key] = value + + # 如果提取到了字段,返回结果 + if result: + return result + return None def _clean_json_string(self, json_str: str) -> str: @@ -445,29 +593,58 @@ class AIService: def _fix_json_string(self, json_str: str) -> str: """ 尝试修复常见的JSON格式错误 + 增强版:处理反斜杠、控制字符、不完整JSON等问题 """ - # 移除末尾的逗号(在 } 或 ] 之前) + # 首先尝试使用jsonrepair库(如果可用) + if JSONREPAIR_AVAILABLE: + try: + repaired = jsonrepair.repair_json(json_str) + if repaired and repaired != json_str: + print(f"[AI服务] 使用jsonrepair修复JSON成功") + return repaired + except Exception as e: + print(f"[AI服务] jsonrepair修复失败: {e},使用基础修复方法") + + # 基础修复方法 + # 1. 移除控制字符(除了换行符、制表符等) + json_str = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]', '', json_str) + + # 2. 修复字段名中的反斜杠转义问题(如 "_professional_rank\" -> "_professional_rank") + # 处理字段名前的反斜杠 + json_str = re.sub(r'\\"([^"]+?)\\":', r'"\1":', json_str) + json_str = re.sub(r'\\"([^"]+?)\\":', r'"\1":', json_str) # 再次处理,确保修复所有情况 + + # 3. 修复字段名缺少开头引号的问题(如 _professional_rank" -> "_professional_rank") + json_str = re.sub(r'([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)"\s*:', r'\1"\2":', json_str) + + # 4. 修复字段名缺少结尾引号的问题(如 "_professional_rank -> "_professional_rank") + json_str = re.sub(r'"([^"]+?)\s*:\s*"', r'"\1": "', json_str) + + # 5. 移除末尾的逗号(在 } 或 ] 之前) json_str = re.sub(r',\s*}', '}', json_str) json_str = re.sub(r',\s*]', ']', json_str) - # 修复字段名中的错误(如 .target_gender -> target_gender) + # 6. 修复字段名中的错误(如 .target_gender -> target_gender) # 处理前导点和尾随空格 json_str = re.sub(r'["\']\s*\.([^"\']+?)\s*["\']\s*:', r'"\1":', json_str) json_str = re.sub(r'["\']\.([^"\']+?)["\']\s*:', r'"\1":', json_str) - # 修复字段名中的空格(如 "target name" -> "target_name") + # 7. 修复字段名中的空格(如 "target name" -> "target_name") json_str = re.sub(r'["\']([^"\']+?)\s+([^"\']+?)["\']\s*:', r'"\1_\2":', json_str) - # 修复字段名中的尾随空格(如 "target_gender " -> "target_gender") + # 8. 修复字段名中的尾随空格(如 "target_gender " -> "target_gender") json_str = re.sub(r'["\']([^"\']+?)\s+["\']\s*:', r'"\1":', json_str) - # 修复字段名中的前导空格(如 " target_gender" -> "target_gender") + # 9. 修复字段名中的前导空格(如 " target_gender" -> "target_gender") json_str = re.sub(r'["\']\s+([^"\']+?)["\']\s*:', r'"\1":', json_str) - # 尝试修复未加引号的字段名(但要避免破坏字符串值) + # 10. 尝试修复未加引号的字段名(但要避免破坏字符串值) # 只修复在冒号前的未加引号的标识符 json_str = re.sub(r'([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:', r'\1"\2":', json_str) + # 11. 修复字符串值中的转义问题(但保留必要的转义) + # 这里要小心,不要破坏合法的转义序列 + return json_str def _normalize_field_names(self, extracted_data: Dict, output_fields: List[Dict]) -> Dict: