|
@@ -594,14 +594,14 @@ def process_single_talent_card(talent_data, minio_md_path):
|
|
|
|
|
|
def process_webpage_with_QWen(markdown_text, publish_time):
|
|
|
"""
|
|
|
- 使用阿里云的 Qwen VL Max 模型解析网页 markdown 文本中的名片信息
|
|
|
+ 使用阿里云的 Qwen VL Max 模型解析单个人员的 markdown 文本中的名片信息
|
|
|
|
|
|
Args:
|
|
|
- markdown_text (str): 网页的 markdown 格式文本内容
|
|
|
+ markdown_text (str): 单个人员的 markdown 格式文本内容
|
|
|
publish_time (str): 发布时间,用于career_path中的date字段
|
|
|
|
|
|
Returns:
|
|
|
- dict: 解析的名片信息
|
|
|
+ list: 解析的名片信息列表(通常包含1个人员信息)
|
|
|
"""
|
|
|
# 阿里云 Qwen API 配置
|
|
|
QWEN_API_KEY = os.environ.get('QWEN_API_KEY', 'sk-8f2320dafc9e4076968accdd8eebd8e9')
|
|
@@ -613,14 +613,14 @@ def process_webpage_with_QWen(markdown_text, publish_time):
|
|
|
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
|
)
|
|
|
|
|
|
- # 构建针对网页文本的优化提示语
|
|
|
- prompt = """你是酒店行业人事任命信息提取专家。请仔细分析提供的网页Markdown文本内容,精确提取其中的人员任命信息。
|
|
|
+ # 构建针对单个人员网页文本的优化提示语
|
|
|
+ prompt = """你是酒店行业人事任命信息提取专家。请仔细分析提供的网页Markdown文本内容,精确提取其中的单个人员任命信息。
|
|
|
|
|
|
## 重要说明
|
|
|
-1. **多人员处理**: 文本中可能包含多个人的任命信息,通常以数字编号(如**1**、**2**、**3**等)分隔不同人员。请将所有人员信息都提取出来,以JSON数组格式返回。请仔细查看整个文档,不要遗漏任何人员信息。
|
|
|
+1. **单人员处理**: 文本内容通常包含一个人的任命信息,可能包含数字编号(如**1**)作为标识。
|
|
|
2. **照片链接识别**: 人物照片链接通常出现在人物姓名的前面,通过空行分隔。请优先关联距离最近的照片链接。
|
|
|
-3. **字段限制**: 只需要提取指定的9个字段,其他信息忽略。
|
|
|
-4. **完整性要求**: 请确保提取文档中所有被任命的人员信息,包括总经理、副总裁、总监等各级管理人员。每个数字编号下的人员都需要提取。
|
|
|
+3. **字段限制**: 只需要提取指定的8个字段,其他信息忽略。
|
|
|
+4. **准确性要求**: 请确保提取的信息准确无误,包括总经理、副总裁、总监等各级管理人员的信息。
|
|
|
|
|
|
## 提取要求
|
|
|
- 区分中英文内容,分别提取
|
|
@@ -632,10 +632,9 @@ def process_webpage_with_QWen(markdown_text, publish_time):
|
|
|
## 照片链接识别规则
|
|
|
- 照片通常以  格式出现
|
|
|
- 照片链接通常位于人物姓名的前面,通过空行分隔
|
|
|
-- 每个人物对应其前面最近的一张照片
|
|
|
-- 如果照片无法明确对应某个人物,则该人物的pic_url为空字符串
|
|
|
+- 如果照片无法明确对应人物,则pic_url为空字符串
|
|
|
|
|
|
-## 需提取的字段(仅这9个字段)
|
|
|
+## 需提取的字段(仅这8个字段)
|
|
|
1. 中文姓名 (name_zh) - 人物的中文姓名
|
|
|
2. 英文姓名 (name_en) - 人物的英文姓名,如果没有则为空字符串
|
|
|
3. 中文职位/头衔 (title_zh) - 人物的中文职位或头衔
|
|
@@ -646,58 +645,28 @@ def process_webpage_with_QWen(markdown_text, publish_time):
|
|
|
8. 照片链接 (pic_url) - 人物的照片URL链接,根据上述识别规则提取
|
|
|
|
|
|
## 输出格式
|
|
|
-请以严格的JSON格式返回结果,不要添加任何额外解释文字。如果文本中包含多个人员信息,返回JSON数组,每个人员一个JSON对象。如果只有一个人员,也要返回数组格式。
|
|
|
+请以严格的JSON格式返回结果,不要添加任何额外解释文字。返回JSON对象格式(不是数组),包含单个人员的信息。
|
|
|
|
|
|
-单人员示例:
|
|
|
+示例:
|
|
|
```json
|
|
|
-[
|
|
|
- {
|
|
|
- "name_zh": "张三",
|
|
|
- "name_en": "Zhang San",
|
|
|
- "title_zh": "总经理",
|
|
|
- "title_en": "General Manager",
|
|
|
- "hotel_zh": "北京万豪酒店",
|
|
|
- "hotel_en": "Beijing Marriott Hotel",
|
|
|
- "brand_group": "万豪",
|
|
|
- "pic_url": "https://example.com/photo1.jpg"
|
|
|
- }
|
|
|
-]
|
|
|
+{
|
|
|
+ "name_zh": "张三",
|
|
|
+ "name_en": "Zhang San",
|
|
|
+ "title_zh": "总经理",
|
|
|
+ "title_en": "General Manager",
|
|
|
+ "hotel_zh": "北京万豪酒店",
|
|
|
+ "hotel_en": "Beijing Marriott Hotel",
|
|
|
+ "brand_group": "万豪",
|
|
|
+ "pic_url": "https://example.com/photo1.jpg"
|
|
|
+}
|
|
|
```
|
|
|
|
|
|
-多人员示例:
|
|
|
-```json
|
|
|
-[
|
|
|
- {
|
|
|
- "name_zh": "张三",
|
|
|
- "name_en": "Zhang San",
|
|
|
- "title_zh": "总经理",
|
|
|
- "title_en": "General Manager",
|
|
|
- "hotel_zh": "北京万豪酒店",
|
|
|
- "hotel_en": "Beijing Marriott Hotel",
|
|
|
- "brand_group": "万豪",
|
|
|
- "pic_url": "https://example.com/photo1.jpg"
|
|
|
- },
|
|
|
- {
|
|
|
- "name_zh": "李四",
|
|
|
- "name_en": "Li Si",
|
|
|
- "title_zh": "市场总监",
|
|
|
- "title_en": "Marketing Director",
|
|
|
- "hotel_zh": "上海希尔顿酒店",
|
|
|
- "hotel_en": "Shanghai Hilton Hotel",
|
|
|
- "brand_group": "希尔顿",
|
|
|
- "pic_url": "https://example.com/photo2.jpg"
|
|
|
- }
|
|
|
-]
|
|
|
-```
|
|
|
-
|
|
|
-发现有数字编号(**1**、**2**、**3**)分隔,应该提取三个人员的信息,返回包含3个对象的数组。
|
|
|
-
|
|
|
## 特别提醒
|
|
|
-- 请务必扫描整个文档,查找所有数字编号标记的人员信息
|
|
|
-- 每个人员信息都要单独提取成一个JSON对象
|
|
|
-- 最终返回的数组长度应该等于文档中的人员数量
|
|
|
+- 专注于提取单个人员的完整信息
|
|
|
+- 确保提取的信息准确且完整
|
|
|
+- 如果某个字段在文本中不存在,请返回空字符串
|
|
|
|
|
|
-以下是需要分析的网页Markdown文本内容:
|
|
|
+以下是需要分析的单个人员网页Markdown文本内容:
|
|
|
|
|
|
""" + markdown_text
|
|
|
|
|
@@ -719,81 +688,482 @@ def process_webpage_with_QWen(markdown_text, publish_time):
|
|
|
|
|
|
# 解析响应
|
|
|
response_content = completion.choices[0].message.content
|
|
|
- logging.info(f"成功从 Qwen 模型获取网页文本响应: {response_content}")
|
|
|
+ logging.info(f"成功从 Qwen 模型获取单个人员文本响应: {response_content}")
|
|
|
|
|
|
# 直接解析 QWen 返回的 JSON 响应
|
|
|
try:
|
|
|
extracted_data = json.loads(response_content)
|
|
|
- logging.info("成功解析 Qwen 网页文本响应中的 JSON")
|
|
|
+ logging.info("成功解析 Qwen 单个人员文本响应中的 JSON")
|
|
|
except json.JSONDecodeError as e:
|
|
|
error_msg = f"JSON 解析失败: {str(e)}"
|
|
|
logging.error(error_msg)
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
- # 确保返回的是数组格式
|
|
|
- if not isinstance(extracted_data, list):
|
|
|
- # 如果返回的不是数组,包装成数组
|
|
|
- extracted_data = [extracted_data] if extracted_data else []
|
|
|
+ # 确保返回的是单个人员对象,转换为列表格式以保持一致性
|
|
|
+ if isinstance(extracted_data, list):
|
|
|
+ # 如果意外返回数组,取第一个元素
|
|
|
+ if len(extracted_data) > 0:
|
|
|
+ person_data = extracted_data[0]
|
|
|
+ logging.warning("Qwen返回了数组格式,取第一个人员信息")
|
|
|
+ else:
|
|
|
+ logging.error("Qwen返回了空数组")
|
|
|
+ return []
|
|
|
+ elif isinstance(extracted_data, dict):
|
|
|
+ # 正常情况,返回的是单个人员对象
|
|
|
+ person_data = extracted_data
|
|
|
+ else:
|
|
|
+ logging.error(f"Qwen返回了不支持的数据格式: {type(extracted_data)}")
|
|
|
+ return []
|
|
|
|
|
|
- # 确保数组中每个人员对象都包含所有必要字段
|
|
|
+ # 确保人员对象包含所有必要字段
|
|
|
required_fields = [
|
|
|
'name_zh', 'name_en', 'title_zh', 'title_en',
|
|
|
'hotel_zh', 'hotel_en', 'brand_group', 'pic_url'
|
|
|
]
|
|
|
|
|
|
- for person in extracted_data:
|
|
|
- for field in required_fields:
|
|
|
- if field not in person:
|
|
|
- person[field] = ""
|
|
|
-
|
|
|
- # 为每个人员添加career_path字段
|
|
|
- career_entry = {
|
|
|
- 'date': publish_time,
|
|
|
- 'hotel_en': person.get('hotel_en', ''),
|
|
|
- 'hotel_zh': person.get('hotel_zh', ''),
|
|
|
- 'image_path': '',
|
|
|
- 'source': 'webpage_extraction',
|
|
|
- 'title_en': person.get('title_en', ''),
|
|
|
- 'title_zh': person.get('title_zh', '')
|
|
|
- }
|
|
|
-
|
|
|
- person['career_path'] = [career_entry]
|
|
|
- logging.info(f"为人员 {person.get('name_zh', 'Unknown')} 添加了career_path记录: {career_entry}")
|
|
|
+ for field in required_fields:
|
|
|
+ if field not in person_data:
|
|
|
+ person_data[field] = ""
|
|
|
|
|
|
- # 创建解析任务记录
|
|
|
- try:
|
|
|
- # 生成唯一的任务名称:当前日期 + UUID
|
|
|
- current_date = datetime.now().strftime('%Y%m%d')
|
|
|
- task_uuid = str(uuid.uuid4())[:8] # 取UUID的前8位
|
|
|
- task_name = f"{current_date}_{task_uuid}"
|
|
|
-
|
|
|
- # 创建解析任务记录
|
|
|
- parse_task = ParseTaskRepository(
|
|
|
- task_name=task_name,
|
|
|
- task_status='completed', # 解析完成
|
|
|
- task_type='门墩儿新任命',
|
|
|
- task_source='webpage_extraction',
|
|
|
- collection_count=len(extracted_data), # 采集人数
|
|
|
- parse_count=len(extracted_data), # 解析人数
|
|
|
- parse_result=extracted_data, # 解析结果
|
|
|
- created_by='system',
|
|
|
- updated_by='system'
|
|
|
- )
|
|
|
-
|
|
|
- db.session.add(parse_task)
|
|
|
- db.session.commit()
|
|
|
-
|
|
|
- logging.info(f"成功创建解析任务记录: {task_name}, 解析人数: {len(extracted_data)}")
|
|
|
-
|
|
|
- except Exception as db_error:
|
|
|
- db.session.rollback()
|
|
|
- logging.error(f"创建解析任务记录失败: {str(db_error)}", exc_info=True)
|
|
|
- # 不影响主要功能,只记录错误日志
|
|
|
+ # 为人员添加career_path字段
|
|
|
+ career_entry = {
|
|
|
+ 'date': publish_time,
|
|
|
+ 'hotel_en': person_data.get('hotel_en', ''),
|
|
|
+ 'hotel_zh': person_data.get('hotel_zh', ''),
|
|
|
+ 'image_path': '',
|
|
|
+ 'source': 'webpage_extraction',
|
|
|
+ 'title_en': person_data.get('title_en', ''),
|
|
|
+ 'title_zh': person_data.get('title_zh', '')
|
|
|
+ }
|
|
|
|
|
|
- return extracted_data
|
|
|
+ person_data['career_path'] = [career_entry]
|
|
|
+ logging.info(f"为人员 {person_data.get('name_zh', 'Unknown')} 添加了career_path记录: {career_entry}")
|
|
|
+
|
|
|
+ # 返回列表格式以保持与其他函数的一致性
|
|
|
+ return [person_data]
|
|
|
|
|
|
except Exception as e:
|
|
|
error_msg = f"Qwen VL Max 模型网页文本解析失败: {str(e)}"
|
|
|
logging.error(error_msg, exc_info=True)
|
|
|
raise Exception(error_msg)
|
|
|
|
|
|
+
|
|
|
+def batch_process_md(markdown_file_list, publish_time):
|
|
|
+ """
|
|
|
+ 批量处理包含多个人员信息的markdown文件
|
|
|
+
|
|
|
+ Args:
|
|
|
+ markdown_file_list (list): MinIO对象保存地址组成的数组
|
|
|
+ publish_time (str): 发布时间,用于career_path中的date字段
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ dict: 批量处理结果,包含所有人员的解析结果
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ # 参数验证
|
|
|
+ if not markdown_file_list or not isinstance(markdown_file_list, list):
|
|
|
+ return {
|
|
|
+ 'code': 400,
|
|
|
+ 'success': False,
|
|
|
+ 'message': 'markdown_file_list参数必须是非空数组',
|
|
|
+ 'data': None
|
|
|
+ }
|
|
|
+
|
|
|
+ if not publish_time or not isinstance(publish_time, str):
|
|
|
+ return {
|
|
|
+ 'code': 400,
|
|
|
+ 'success': False,
|
|
|
+ 'message': 'publish_time参数必须是非空字符串',
|
|
|
+ 'data': None
|
|
|
+ }
|
|
|
+
|
|
|
+ logging.info(f"开始批量处理 {len(markdown_file_list)} 个markdown文件")
|
|
|
+
|
|
|
+ # 批量处理结果统计
|
|
|
+ batch_results = {
|
|
|
+ 'total_files': len(markdown_file_list),
|
|
|
+ 'processed_files': 0,
|
|
|
+ 'failed_files': 0,
|
|
|
+ 'total_sections': 0,
|
|
|
+ 'total_persons': 0,
|
|
|
+ 'all_results': [],
|
|
|
+ 'file_results': [],
|
|
|
+ 'failed_files_info': []
|
|
|
+ }
|
|
|
+
|
|
|
+ # 逐个处理每个markdown文件
|
|
|
+ for file_index, minio_path in enumerate(markdown_file_list):
|
|
|
+ try:
|
|
|
+ logging.info(f"开始处理第 {file_index + 1} 个文件: {minio_path}")
|
|
|
+
|
|
|
+ # 处理单个文件
|
|
|
+ file_result = process_single_markdown_file(minio_path, publish_time)
|
|
|
+
|
|
|
+ if file_result['success']:
|
|
|
+ batch_results['processed_files'] += 1
|
|
|
+ batch_results['total_sections'] += file_result['data']['total_sections']
|
|
|
+ batch_results['total_persons'] += file_result['data']['total_persons']
|
|
|
+ batch_results['all_results'].extend(file_result['data']['all_results'])
|
|
|
+ batch_results['file_results'].append({
|
|
|
+ 'file_index': file_index + 1,
|
|
|
+ 'minio_path': minio_path,
|
|
|
+ 'result': file_result['data']
|
|
|
+ })
|
|
|
+ logging.info(f"文件 {minio_path} 处理成功,提取 {file_result['data']['total_persons']} 个人员信息")
|
|
|
+ else:
|
|
|
+ batch_results['failed_files'] += 1
|
|
|
+ batch_results['failed_files_info'].append({
|
|
|
+ 'file_index': file_index + 1,
|
|
|
+ 'minio_path': minio_path,
|
|
|
+ 'error': file_result['message']
|
|
|
+ })
|
|
|
+ logging.error(f"文件 {minio_path} 处理失败: {file_result['message']}")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_msg = f"处理文件 {minio_path} 时发生异常: {str(e)}"
|
|
|
+ logging.error(error_msg, exc_info=True)
|
|
|
+ batch_results['failed_files'] += 1
|
|
|
+ batch_results['failed_files_info'].append({
|
|
|
+ 'file_index': file_index + 1,
|
|
|
+ 'minio_path': minio_path,
|
|
|
+ 'error': error_msg
|
|
|
+ })
|
|
|
+
|
|
|
+ # 生成最终结果
|
|
|
+ if batch_results['processed_files'] == batch_results['total_files']:
|
|
|
+ # 全部处理成功
|
|
|
+ return {
|
|
|
+ 'code': 200,
|
|
|
+ 'success': True,
|
|
|
+ 'message': f'所有 {batch_results["total_files"]} 个文件处理成功,共提取 {batch_results["total_persons"]} 个人员信息',
|
|
|
+ 'data': batch_results
|
|
|
+ }
|
|
|
+ elif batch_results['processed_files'] > 0:
|
|
|
+ # 部分处理成功
|
|
|
+ return {
|
|
|
+ 'code': 206, # Partial Content
|
|
|
+ 'success': True,
|
|
|
+ 'message': f'部分处理成功:{batch_results["processed_files"]}/{batch_results["total_files"]} 个文件成功,共提取 {batch_results["total_persons"]} 个人员信息',
|
|
|
+ 'data': batch_results
|
|
|
+ }
|
|
|
+ else:
|
|
|
+ # 全部处理失败
|
|
|
+ return {
|
|
|
+ 'code': 500,
|
|
|
+ 'success': False,
|
|
|
+ 'message': f'所有 {batch_results["total_files"]} 个文件处理失败',
|
|
|
+ 'data': batch_results
|
|
|
+ }
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_msg = f"batch_process_md函数执行失败: {str(e)}"
|
|
|
+ logging.error(error_msg, exc_info=True)
|
|
|
+ return {
|
|
|
+ 'code': 500,
|
|
|
+ 'success': False,
|
|
|
+ 'message': error_msg,
|
|
|
+ 'data': None
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def get_markdown_from_minio(minio_client, minio_path):
|
|
|
+ """
|
|
|
+ 从MinIO获取markdown文件内容
|
|
|
+
|
|
|
+ Args:
|
|
|
+ minio_client: MinIO客户端
|
|
|
+ minio_path (str): MinIO中的文件路径
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ str: 文件内容,如果失败返回None
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ logging.info(f"从MinIO获取文件: {minio_path}")
|
|
|
+
|
|
|
+ # 从MinIO下载文件
|
|
|
+ response = minio_client.get_object(Bucket=minio_bucket, Key=minio_path)
|
|
|
+
|
|
|
+ # 读取文件内容
|
|
|
+ content = response['Body'].read()
|
|
|
+
|
|
|
+ # 解码为字符串
|
|
|
+ if isinstance(content, bytes):
|
|
|
+ # 尝试不同的编码方式
|
|
|
+ try:
|
|
|
+ markdown_content = content.decode('utf-8')
|
|
|
+ except UnicodeDecodeError:
|
|
|
+ try:
|
|
|
+ markdown_content = content.decode('gbk')
|
|
|
+ except UnicodeDecodeError:
|
|
|
+ markdown_content = content.decode('utf-8', errors='ignore')
|
|
|
+ logging.warning(f"文件 {minio_path} 编码检测失败,使用UTF-8忽略错误模式")
|
|
|
+ else:
|
|
|
+ markdown_content = str(content)
|
|
|
+
|
|
|
+ logging.info(f"成功获取文件内容,长度: {len(markdown_content)} 字符")
|
|
|
+ return markdown_content
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logging.error(f"从MinIO获取文件 {minio_path} 失败: {str(e)}", exc_info=True)
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def save_section_to_minio(minio_client, section_content, original_minio_path, section_number):
|
|
|
+ """
|
|
|
+ 将分割后的markdown内容保存到MinIO
|
|
|
+
|
|
|
+ Args:
|
|
|
+ minio_client: MinIO客户端
|
|
|
+ section_content (str): 分割后的markdown内容
|
|
|
+ original_minio_path (str): 原始文件的MinIO路径
|
|
|
+ section_number (str): 分隔符编号
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ str: 新保存文件的MinIO路径,如果失败返回None
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ # 生成新的文件名
|
|
|
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
|
+ unique_id = uuid.uuid4().hex[:8]
|
|
|
+
|
|
|
+ # 从原始路径提取基础信息
|
|
|
+ path_parts = original_minio_path.split('/')
|
|
|
+ if len(path_parts) > 1:
|
|
|
+ directory = '/'.join(path_parts[:-1])
|
|
|
+ original_filename = path_parts[-1]
|
|
|
+ # 移除扩展名
|
|
|
+ base_name = original_filename.rsplit('.', 1)[0] if '.' in original_filename else original_filename
|
|
|
+ else:
|
|
|
+ directory = 'webpage_talent_sections'
|
|
|
+ base_name = 'section'
|
|
|
+
|
|
|
+ # 构建新的文件名
|
|
|
+ new_filename = f"{base_name}_section_{section_number}_{timestamp}_{unique_id}.md"
|
|
|
+ new_minio_path = f"{directory}/{new_filename}"
|
|
|
+
|
|
|
+ logging.info(f"开始保存分割内容到MinIO: {new_minio_path}")
|
|
|
+
|
|
|
+ # 将内容转换为字节流
|
|
|
+ content_bytes = section_content.encode('utf-8')
|
|
|
+ content_stream = BytesIO(content_bytes)
|
|
|
+
|
|
|
+ # 上传到MinIO
|
|
|
+ minio_client.put_object(
|
|
|
+ Bucket=minio_bucket,
|
|
|
+ Key=new_minio_path,
|
|
|
+ Body=content_stream,
|
|
|
+ ContentType='text/markdown',
|
|
|
+ Metadata={
|
|
|
+ 'original_file': original_minio_path,
|
|
|
+ 'section_number': section_number,
|
|
|
+ 'upload_time': datetime.now().isoformat(),
|
|
|
+ 'content_type': 'webpage_talent_section'
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ logging.info(f"分割内容成功保存到MinIO: {new_minio_path}")
|
|
|
+ return new_minio_path
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logging.error(f"保存分割内容到MinIO失败: {str(e)}", exc_info=True)
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def process_single_markdown_file(minio_path, publish_time):
|
|
|
+ """
|
|
|
+ 处理单个markdown文件,从MinIO获取内容并判断是否需要分割
|
|
|
+
|
|
|
+ Args:
|
|
|
+ minio_path (str): MinIO中的文件路径
|
|
|
+ publish_time (str): 发布时间
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ dict: 处理结果
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ # 获取MinIO客户端
|
|
|
+ minio_client = get_minio_client()
|
|
|
+ if not minio_client:
|
|
|
+ return {
|
|
|
+ 'success': False,
|
|
|
+ 'message': '无法连接到MinIO服务器',
|
|
|
+ 'data': None
|
|
|
+ }
|
|
|
+
|
|
|
+ # 从MinIO获取文件内容
|
|
|
+ markdown_content = get_markdown_from_minio(minio_client, minio_path)
|
|
|
+ if not markdown_content:
|
|
|
+ return {
|
|
|
+ 'success': False,
|
|
|
+ 'message': f'无法从MinIO获取文件内容: {minio_path}',
|
|
|
+ 'data': None
|
|
|
+ }
|
|
|
+
|
|
|
+ # 检查是否存在 **1**, **2** 等分隔结构
|
|
|
+ pattern = r'\*\*(\d+)\*\*'
|
|
|
+ matches = re.findall(pattern, markdown_content)
|
|
|
+
|
|
|
+ if not matches:
|
|
|
+ # 如果没有找到分隔符,直接处理整个文件
|
|
|
+ logging.info("未发现数字分隔符,直接处理整个markdown文件")
|
|
|
+ try:
|
|
|
+ result = process_webpage_with_QWen(markdown_content, publish_time)
|
|
|
+
|
|
|
+ # 更新解析结果中的路径信息
|
|
|
+ if result:
|
|
|
+ for person in result:
|
|
|
+ person['pic_url'] = minio_path # 设置原始文件路径
|
|
|
+ if 'career_path' in person and person['career_path']:
|
|
|
+ for career_entry in person['career_path']:
|
|
|
+ career_entry['image_path'] = minio_path # 设置原始文件路径
|
|
|
+
|
|
|
+ return {
|
|
|
+ 'success': True,
|
|
|
+ 'message': '单个markdown文件处理成功',
|
|
|
+ 'data': {
|
|
|
+ 'total_sections': 1,
|
|
|
+ 'processed_sections': 1,
|
|
|
+ 'total_persons': len(result) if result else 0,
|
|
|
+ 'all_results': result,
|
|
|
+ 'section_results': [],
|
|
|
+ 'failed_sections_info': []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ except Exception as e:
|
|
|
+ error_msg = f"处理单个markdown文件失败: {str(e)}"
|
|
|
+ logging.error(error_msg, exc_info=True)
|
|
|
+ return {
|
|
|
+ 'success': False,
|
|
|
+ 'message': error_msg,
|
|
|
+ 'data': None
|
|
|
+ }
|
|
|
+
|
|
|
+ # 发现分隔符,按分隔符拆分内容
|
|
|
+ logging.info(f"发现 {len(matches)} 个数字分隔符: {matches}")
|
|
|
+
|
|
|
+ # 使用正则表达式分割内容
|
|
|
+ sections = re.split(r'\*\*\d+\*\*', markdown_content)
|
|
|
+
|
|
|
+ # 移除第一个空白部分(分隔符前的内容)
|
|
|
+ if sections and not sections[0].strip():
|
|
|
+ sections = sections[1:]
|
|
|
+
|
|
|
+ # 确保分割后的部分数量与分隔符数量匹配
|
|
|
+ if len(sections) != len(matches):
|
|
|
+ logging.warning(f"分割部分数量 ({len(sections)}) 与分隔符数量 ({len(matches)}) 不匹配")
|
|
|
+ # 取较小的数量以确保安全
|
|
|
+ min_count = min(len(sections), len(matches))
|
|
|
+ sections = sections[:min_count]
|
|
|
+ matches = matches[:min_count]
|
|
|
+
|
|
|
+ # 处理结果统计
|
|
|
+ results = {
|
|
|
+ 'total_sections': len(sections),
|
|
|
+ 'processed_sections': 0,
|
|
|
+ 'failed_sections': 0,
|
|
|
+ 'total_persons': 0,
|
|
|
+ 'all_results': [],
|
|
|
+ 'section_results': [],
|
|
|
+ 'failed_sections_info': []
|
|
|
+ }
|
|
|
+
|
|
|
+ # 逐个处理每个markdown片段
|
|
|
+ for i, (section_content, section_number) in enumerate(zip(sections, matches)):
|
|
|
+ try:
|
|
|
+ logging.info(f"开始处理第 {section_number} 部分 (索引: {i})")
|
|
|
+
|
|
|
+ # 清理内容,移除前后空白
|
|
|
+ section_content = section_content.strip()
|
|
|
+
|
|
|
+ if not section_content:
|
|
|
+ logging.warning(f"第 {section_number} 部分内容为空,跳过处理")
|
|
|
+ results['failed_sections'] += 1
|
|
|
+ results['failed_sections_info'].append({
|
|
|
+ 'section_number': section_number,
|
|
|
+ 'index': i,
|
|
|
+ 'error': '部分内容为空'
|
|
|
+ })
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 重新构建完整的markdown片段,包含分隔符标题
|
|
|
+ full_section_content = f"**{section_number}**\n\n{section_content}"
|
|
|
+
|
|
|
+ # 将分割后的内容保存到MinIO
|
|
|
+ section_minio_path = save_section_to_minio(minio_client, full_section_content, minio_path, section_number)
|
|
|
+ if not section_minio_path:
|
|
|
+ logging.warning(f"保存第 {section_number} 部分到MinIO失败,使用原始路径")
|
|
|
+ section_minio_path = minio_path
|
|
|
+
|
|
|
+ # 调用process_webpage_with_QWen处理单个片段
|
|
|
+ section_result = process_webpage_with_QWen(full_section_content, publish_time)
|
|
|
+
|
|
|
+ # 更新解析结果中的路径信息
|
|
|
+ if section_result:
|
|
|
+ for person in section_result:
|
|
|
+ person['pic_url'] = section_minio_path # 设置分割后的文件路径
|
|
|
+ if 'career_path' in person and person['career_path']:
|
|
|
+ for career_entry in person['career_path']:
|
|
|
+ career_entry['image_path'] = section_minio_path # 设置分割后的文件路径
|
|
|
+
|
|
|
+ if section_result:
|
|
|
+ results['processed_sections'] += 1
|
|
|
+ results['total_persons'] += len(section_result)
|
|
|
+ results['all_results'].extend(section_result)
|
|
|
+ results['section_results'].append({
|
|
|
+ 'section_number': section_number,
|
|
|
+ 'index': i,
|
|
|
+ 'persons_count': len(section_result),
|
|
|
+ 'persons': section_result
|
|
|
+ })
|
|
|
+ logging.info(f"第 {section_number} 部分处理成功,提取 {len(section_result)} 个人员信息")
|
|
|
+ else:
|
|
|
+ results['failed_sections'] += 1
|
|
|
+ results['failed_sections_info'].append({
|
|
|
+ 'section_number': section_number,
|
|
|
+ 'index': i,
|
|
|
+ 'error': '未提取到人员信息'
|
|
|
+ })
|
|
|
+ logging.warning(f"第 {section_number} 部分未提取到人员信息")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_msg = f"处理第 {section_number} 部分失败: {str(e)}"
|
|
|
+ logging.error(error_msg, exc_info=True)
|
|
|
+ results['failed_sections'] += 1
|
|
|
+ results['failed_sections_info'].append({
|
|
|
+ 'section_number': section_number,
|
|
|
+ 'index': i,
|
|
|
+ 'error': error_msg
|
|
|
+ })
|
|
|
+
|
|
|
+ # 生成最终结果
|
|
|
+ if results['processed_sections'] == results['total_sections']:
|
|
|
+ # 全部处理成功
|
|
|
+ return {
|
|
|
+ 'success': True,
|
|
|
+ 'message': f'所有 {results["total_sections"]} 个部分处理成功,共提取 {results["total_persons"]} 个人员信息',
|
|
|
+ 'data': results
|
|
|
+ }
|
|
|
+ elif results['processed_sections'] > 0:
|
|
|
+ # 部分处理成功
|
|
|
+ return {
|
|
|
+ 'success': True,
|
|
|
+ 'message': f'部分处理成功:{results["processed_sections"]}/{results["total_sections"]} 个部分成功,共提取 {results["total_persons"]} 个人员信息',
|
|
|
+ 'data': results
|
|
|
+ }
|
|
|
+ else:
|
|
|
+ # 全部处理失败
|
|
|
+ return {
|
|
|
+ 'success': False,
|
|
|
+ 'message': f'所有 {results["total_sections"]} 个部分处理失败',
|
|
|
+ 'data': results
|
|
|
+ }
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_msg = f"process_single_markdown_file函数执行失败: {str(e)}"
|
|
|
+ logging.error(error_msg, exc_info=True)
|
|
|
+ return {
|
|
|
+ 'success': False,
|
|
|
+ 'message': error_msg,
|
|
|
+ 'data': None
|
|
|
+ }
|
|
|
+
|