From 823a5f0a46de06530e967395ad2bd6738d6f0f22 Mon Sep 17 00:00:00 2001 From: zy123 <646228430@qq.com> Date: Sat, 23 Nov 2024 15:38:52 +0800 Subject: [PATCH] =?UTF-8?q?=2011.23=20=E4=BF=AE=E6=94=B9=E5=95=86=E5=8A=A1?= =?UTF-8?q?=E8=A6=81=E6=B1=82bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flask_app/main/工程标解析main.py | 1 - flask_app/货物标/商务服务其他要求提取.py | 46 +- flask_app/货物标/截取pdf货物标版.py | 16 +- flask_app/货物标/技术参数要求提取.py | 401 +++++++++--------- .../货物标/技术参数要求提取后处理函数.py | 28 +- flask_app/货物标/提取采购需求main.py | 16 +- 6 files changed, 267 insertions(+), 241 deletions(-) diff --git a/flask_app/main/工程标解析main.py b/flask_app/main/工程标解析main.py index 74bf962..1a630f1 100644 --- a/flask_app/main/工程标解析main.py +++ b/flask_app/main/工程标解析main.py @@ -229,7 +229,6 @@ def engineering_bid_main(output_folder, downloaded_file_path, file_type, unique_ yield json.dumps({'error': f'Error processing {key}: {str(exc)}'}, ensure_ascii=False) #TODO:基本信息,判断是否这里,打勾逻辑取消了。 -#TODO:缩进 if __name__ == "__main__": start_time = time.time() output_folder = "C:\\Users\\Administrator\\Desktop\\招标文件\\new_test1" diff --git a/flask_app/货物标/商务服务其他要求提取.py b/flask_app/货物标/商务服务其他要求提取.py index 60c59f4..f69851a 100644 --- a/flask_app/货物标/商务服务其他要求提取.py +++ b/flask_app/货物标/商务服务其他要求提取.py @@ -2,10 +2,12 @@ import json import re from PyPDF2 import PdfReader + +from flask_app.general.doubao import read_txt_to_string from flask_app.general.json_utils import combine_json_results,clean_json_string from flask_app.general.通义千问long import upload_file,qianwen_long_stream from flask_app.货物标.截取pdf货物标版 import extract_common_header, clean_page_content - +from flask_app.general.doubao import doubao_model #正则表达式判断原文中是否有商务、服务、其他要求 def find_exists(truncate_file, required_keys): @@ -102,7 +104,7 @@ def generate_queries(truncate_file, required_keys): # print(query_base) return queries -def generate_user_query_template(required_keys): +def generate_user_query_template(required_keys,processed_filepath): import textwrap import json @@ -111,11 +113,11 @@ def generate_user_query_template(required_keys): # 定义每个键对应的示例内容 example_content1 = { - "技术要求": ["相关技术要求以及服务要求1", "相关技术要求以及服务要求2"], + "技术要求": ["相关技术要求1", "相关技术要求2"], "服务要求": ["服务要求1", "服务要求2"], "商务要求": { - "★产品质保期": ["所投 LED 整屏不低于 3 年,健身器材整套不低于 2 年"], - "售后服务方案": ["包含产品配送、安装保障方案"] + "子因素名1": ["商务要求1"], + "子因素名2": ["商务要求2"] }, "其他要求": { "子因素名1": ["关于项目采购的其他要求1...", "关于项目采购的其他要求2..."], @@ -133,7 +135,7 @@ def generate_user_query_template(required_keys): "子因素名1": ["相关服务要求1", "相关服务要求2"], "子因素名2": ["相关服务要求3", "相关服务要求4"] }, - "商务要求": ["所投 LED 整屏不低于 3 年,健身器材整套不低于 2 年", "包含产品配送、安装保障方案"], + "商务要求": ["商务要求1", "商务要求2"], "其他要求": ["关于项目采购的其他要求1..."], "技术、服务要求": { "子因素名1": ["相关技术、服务要求内容1"], @@ -167,7 +169,7 @@ def generate_user_query_template(required_keys): outer_keys_str = ', '.join([f"'{key}'" for key in sorted_keys]) # 使用三引号定义多行字符串,便于编辑和维护 - prompt_instruction = textwrap.dedent(f"""请你根据该货物类招标文件中的采购要求部分内容,请告诉我该项目采购的{keys_str}分别是什么,请以json格式返回结果,默认情况下外层键名是{outer_keys_str},键值为字符串列表,每个字符串表示具体的一条要求,内容需要与原文保持一致,不可擅自总结删减。 + prompt_instruction = textwrap.dedent(f"""请你根据该货物类招标文件中的采购要求部分内容(技术、服务及商务要求部分内容),请告诉我该项目采购的{keys_str}分别是什么,请以json格式返回结果,默认情况下外层键名是{outer_keys_str},键值为字符串列表,每个字符串表示具体的一条要求,请按原文内容回答,保留三角▲、五角星★和序号(若有),不要擅自增添内容。 要求与指南: 1. 默认情况无需嵌套,键值为字符串列表;若存在嵌套结构,嵌套键名是原文中该要求下相应子标题,最多一层嵌套。 @@ -177,10 +179,11 @@ def generate_user_query_template(required_keys): a. 一个对象(字典),其键为子因素名,值为字符串列表。 b. 一个字符串列表,表示具体的一条条要求。若只有一条要求,也用字符串列表表示。 - 最多只允许一层嵌套。 - 3. 请优先定位正文部分的大标题'xx要求',在其之后提取'xx要求'相关内容,由于要求的位置比较集中,请尽量避免在全文各处寻找。 - 4. 在提取技术要求或技术、服务要求时(若有),你无需从采购清单或表格中提取货物名以及参数要求,你仅需定位到原文中相应位置(正文部分、而非表格中)并提取正文内容,通常一类要求写在一块大标题下,否则,键值为空列表。 - 5. 若无相关要求,键值为[] - """) + 3. 请优先定位正文部分的大标题'xx要求',在其之后提取'xx要求'相关内容, + 4. 若章节开头位置或者采购清单中除了需要采购的货物、数量、单位之外,还有带三角▲或五角星★的描述内容(如工期要求、质保要求等商务要求),请将该部分内容提取出来,添加在键名为'商务要求'的字典的键值部分,注意请不要返回Markdown语法,必要时使用冒号':'将相关信息拼接在一起。 + 5. 在提取技术要求或技术、服务要求时(若有),你无需从采购清单或表格中提取货物名以及参数要求,你仅需定位到原文中大标题'技术要求'或'技术、服务要求'部分提取正文内容,若内容全在表格中,键值为空列表[]。 + 6. 若无相关要求,键值为[] + """ ) # 过滤 example_content1 和 example_content2 以仅包含 sorted_keys def filter_content(example_content, keys): @@ -192,7 +195,8 @@ def generate_user_query_template(required_keys): # 将过滤后的示例转换为格式化的 JSON 字符串 json_example1_str = json.dumps(filtered_example_content1, indent=4, ensure_ascii=False) json_example2_str = json.dumps(filtered_example_content2, indent=4, ensure_ascii=False) - + # 从文件中读取内容 + # full_text = read_txt_to_string(processed_filepath) # 完整的用户查询模板,包含两份示例输出 user_query_template = f""" {prompt_instruction} @@ -201,27 +205,31 @@ def generate_user_query_template(required_keys): {json_example1_str} 示例 2: {json_example2_str} -""" +""" + # 文本内容:{full_text} return user_query_template -def get_business_requirements(procurement_path): +def get_business_requirements(procurement_path,processed_filepath): file_id=upload_file(procurement_path) required_keys = ["技\s*术\s*要\s*求","商\s*务\s*要\s*求", "服\s*务\s*要\s*求", "其\s*他\s*要\s*求"] contained_keys=find_exists(procurement_path,required_keys) print(contained_keys) # queries = generate_queries(truncate_file, contained_keys) - user_query=generate_user_query_template(contained_keys) - business_requirements=qianwen_long_stream(file_id,user_query) + user_query=generate_user_query_template(contained_keys,processed_filepath) + # print(user_query) + model_res=qianwen_long_stream(file_id,user_query) + # model_res=doubao_model(user_query) # Combine and fill missing keys with default values - final_res = clean_json_string(business_requirements) + final_res = clean_json_string(model_res) # final_res.update({key: final_res.get(key, "") for key in required_keys}) return final_res #TODO:改为先判断,再摘取 if __name__ == "__main__": # truncate_file = "C:\\Users\\Administrator\\Desktop\\fsdownload\\e4be098d-b378-4126-9c32-a742b237b3b1\\ztbfile_procurement.docx" - truncate_file=r"C:\Users\Administrator\Desktop\货物标\output1\2-招标文件(广水市教育局封闭管理)_procurement.pdf" + truncate_file=r"C:\Users\Administrator\Desktop\fsdownload\5901b181-b55f-4107-9f30-c85d607b1fa0\ztbfile_procurement.pdf" + processed_filepath="" # file_id = upload_file(truncate_file) - res=get_business_requirements(truncate_file) + res=get_business_requirements(truncate_file,"") print(json.dumps(res, ensure_ascii=False, indent=4)) diff --git a/flask_app/货物标/截取pdf货物标版.py b/flask_app/货物标/截取pdf货物标版.py index dbe568c..2079ec9 100644 --- a/flask_app/货物标/截取pdf货物标版.py +++ b/flask_app/货物标/截取pdf货物标版.py @@ -123,7 +123,7 @@ def extract_pages(pdf_path, output_folder, begin_pattern, begin_page, end_patter else: # 原有的处理逻辑保持不变 - if output_suffix == "qualification1": + if output_suffix == "qualification1" or output_suffix=="procurement": exclusion_pattern = re.compile(r'文件的构成|文件的组成|须对应|需对应|须按照|需按照|须根据|需根据') start_page, end_page = extract_pages_generic(pdf_document, begin_pattern, end_pattern, begin_page, common_header, exclusion_pattern, output_suffix) # 针对 selection = 6 的特殊处理 @@ -832,16 +832,16 @@ def truncate_pdf_specific_goods(pdf_path, output_folder, selections,unique_id="1 #ztbfile.pdf少资格评审 包头少符合性评审 if __name__ == "__main__": # input_path = "C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles" - input_path = r"C:\Users\Administrator\Desktop\货物标\zbfiles\2-招标文件.pdf" + input_path = r"C:\Users\Administrator\Desktop\new招标文件\货物标" # input_path="C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles\\zbtest4_evaluation_method.pdf" # input_path = "C:\\Users\\Administrator\\Desktop\\货物标\\output1\\2-招标文件_procurement.pdf" # input_path="C:\\Users\\Administrator\\Desktop\\fsdownload\\a091d107-805d-4e28-b8b2-0c7327737238\\ztbfile.pdf" # output_folder = "C:\\Users\\Administrator\\Desktop\\fsdownload\\a091d107-805d-4e28-b8b2-0c7327737238\\tmp" output_folder=r"C:\Users\Administrator\Desktop\new招标文件\output5" # files = truncate_pdf_multiple(input_path, output_folder) - selections = [3,5] - files=truncate_pdf_specific_goods(input_path,output_folder,selections) - print(files) - # selection = 5# 例如:1 - 公告, 2 - 评标办法, 3 - 资格审查后缀有qualification1或qualification2(与评标办法一致) 4.投标人须知前附表part1 投标人须知正文part2 5-采购需求 - # generated_files = truncate_pdf_main(input_path, output_folder, selection) - # print(generated_files) \ No newline at end of file + # selections = [3,5] + # files=truncate_pdf_specific_goods(input_path,output_folder,selections) + # print(files) + selection = 5# 例如:1 - 公告, 2 - 评标办法, 3 - 资格审查后缀有qualification1或qualification2(与评标办法一致) 4.投标人须知前附表part1 投标人须知正文part2 5-采购需求 + generated_files = truncate_pdf_main(input_path, output_folder, selection) + print(generated_files) \ No newline at end of file diff --git a/flask_app/货物标/技术参数要求提取.py b/flask_app/货物标/技术参数要求提取.py index 8b12319..c6bedcd 100644 --- a/flask_app/货物标/技术参数要求提取.py +++ b/flask_app/货物标/技术参数要求提取.py @@ -3,6 +3,8 @@ import json import os import re import time +from collections import defaultdict +from copy import deepcopy from flask_app.general.file2markdown import convert_pdf_to_markdown from flask_app.general.format_change import pdf2docx @@ -14,137 +16,180 @@ from flask_app.general.doubao import doubao_model, generate_full_user_query, pdf from flask_app.货物标.技术参数要求提取后处理函数 import postprocess, all_postprocess -def generate_key_paths(data, parent_key='', good_list=None, seen=None): +def generate_key_paths(data): """ - 生成嵌套字典中的键路径,并提取最内层的键名。 - 同时,提取特定模式的键(如 '交换机-1', '交换机-2')的父路径。 - 如果同一层级下只有'交换机-1'但没有'交换机-2',则视为错误输入,将键名中的后缀'-1'删除,直接修改为'交换机',保持顺序。 + 处理输入的字典,生成 key_paths, grouped_paths 和 good_list,并根据条件修改原始字典。 参数: - data (dict): 输入的字典数据 - parent_key (str): 上级键路径,用于递归调用 - good_list (list): 用于存储去重后的最内层键名 - seen (set): 用于跟踪已添加到 good_list 的元素 + data (dict): 输入的嵌套字典。 返回: - tuple: 包含键路径列表、最内层键名列表、分组路径列表以及 no_keys_added 的元组 - (key_paths, good_list, grouped_paths, no_keys_added) + tuple: 包含 key_paths, grouped_paths 和 good_list 的元组。 """ - if good_list is None: - good_list = [] - if seen is None: - seen = set() + # 编译用于匹配后缀的正则表达式模式 + pattern = re.compile(r'(.+)-\d+$') + # 初始化结果列表 key_paths = [] - grouped_paths = set() # 使用集合避免重复路径 - no_keys_added = True # 默认假设没有添加任何键 + grouped_set = set() # 使用集合来避免重复 + good_list = [] - # Step 1: Collect keys that match the pattern - pattern = re.compile(r'(.+)-\d+$') # 匹配形如 '交换机-1', '交换机-2' 的键 - prefix_groups = {} - other_keys = [] + def recurse(current_dict, path): + """ + 递归遍历字典,处理 key_paths 和 grouped_paths,并收集 good_list。 - for key in list(data.keys()): # 使用 list(data.keys()) 防止修改字典时出错 - clean_key = key.replace(" ", "") - match = pattern.match(clean_key) - if match: - prefix = match.group(1) - if prefix not in prefix_groups: - prefix_groups[prefix] = [] - prefix_groups[prefix].append(key) - else: - other_keys.append(key) - - # Step 2: Handle grouped keys - for prefix, keys in prefix_groups.items(): - current_prefix_path = f"{parent_key}.{prefix}" if parent_key else prefix - if len(keys) > 1: - # 多个键匹配同一前缀:添加到 grouped_paths - grouped_paths.add(current_prefix_path) - if prefix not in seen: - good_list.append(prefix) - seen.add(prefix) - no_keys_added = False - else: - # 只有一个键匹配:重命名键,删除后缀,并保持顺序 - old_key = keys[0] - new_key = prefix # 删除后缀后的新键名 - value = data.pop(old_key) # 移除旧键并获取其值 - - # 插入新键以保持顺序 - # 由于 Python 3.7+ 字典保持插入顺序,我们需要重新插入键 - # 创建一个新的临时字典来保持顺序 - temp_dict = {} - for k in data: - temp_dict[k] = data[k] - if k == parent_key or (parent_key and k.startswith(parent_key + ".")): - # 在适当的位置插入新键 - continue - data[new_key] = value # 添加新键 - - key_path = f"{parent_key}.{new_key}" if parent_key else new_key - key_paths.append(key_path) - if prefix not in seen: - good_list.append(prefix) - seen.add(prefix) - no_keys_added = False - # 注意:由于直接修改字典,键的顺序可能会受到影响 - - # Step 3: Handle other keys - for key in other_keys: - value = data[key] - current_key = f"{parent_key}.{key}" if parent_key else key - - if isinstance(value, dict): - if value: - # 递归调用,并获取子路径、子 good_list、子分组路径以及子 no_keys_added - sub_key_paths, _, sub_grouped_paths, sub_no_keys_added = generate_key_paths( - value, current_key, good_list, seen - ) - key_paths.extend(sub_key_paths) - grouped_paths.update(sub_grouped_paths) # 合并子分组路径到当前分组路径 - # 更新 no_keys_added - no_keys_added = no_keys_added and sub_no_keys_added + 参数: + current_dict (dict): 当前遍历的字典。 + path (list): 当前路径的键列表。 + """ + # 第一遍遍历,统计每个基名的出现次数 + base_name_count = {} + base_names = {} + for key in current_dict.keys(): + match = pattern.match(key) + if match: + base = match.group(1) else: - # 空字典视为叶子节点 - clean_key = key.replace(" ", "") - key_paths.append(current_key.replace(" ", "")) - if clean_key not in seen: - good_list.append(clean_key) # 去掉空格后添加 - seen.add(clean_key) + base = key + base_names[key] = base + base_name_count[base] = base_name_count.get(base, 0) + 1 - # 更新 no_keys_added - no_keys_added = False - elif isinstance(value, list): - # 列表类型视为叶子节点,无论是否为空 - key_paths.append(current_key.replace(" ", "")) - clean_key = key.replace(" ", "") - if clean_key not in seen: - good_list.append(clean_key) # 去掉空格后添加 - seen.add(clean_key) - # 更新 no_keys_added - no_keys_added = False - elif value in {"未知", "", "/"}: - # 特定值视为叶子节点 - key_paths.append(current_key.replace(" ", "")) - clean_key = key.replace(" ", "") - if clean_key not in seen: - good_list.append(clean_key) # 去掉空格后添加 - seen.add(clean_key) - # 更新 no_keys_added - no_keys_added = False - else: - # 其他情况视为叶子节点 - key_paths.append(current_key.replace(" ", "")) - clean_key = key.replace(" ", "") - if clean_key not in seen: - good_list.append(clean_key) # 去掉空格后添加 - seen.add(clean_key) - # 更新 no_keys_added - no_keys_added = False - # Step 4: 删除 key_paths 中包含在 grouped_paths 中的元素 - key_paths = [path for path in key_paths if path not in grouped_paths] #排除同一层下既有'xx',又有'xx-1'的情况 - return key_paths, good_list, grouped_paths, no_keys_added + # 第二遍遍历,根据基名的出现次数分类 + keys_to_rename = {} + for key, base in base_names.items(): + if base_name_count[base] == 1: + # 检查是否是最内层(值为列表) + value = current_dict[key] + if isinstance(value, list): + current_path = '.'.join(path + [base]) + key_paths.append(current_path) + # 收集 good_list,保持顺序且不重复 + if base not in good_list: + good_list.append(base) + # 如果原键名有后缀,需要记录以便后续重命名 + if key != base: + keys_to_rename[key] = base + elif isinstance(value, dict): + # 继续递归处理 + recurse(value, path + [base]) + else: + # 记录分组路径,确保唯一性 + grouped_set.add('.'.join(path + [base])) + + # 执行键名的重命名,同时保持原有顺序 + if keys_to_rename: + new_ordered_dict = {} + for key in current_dict.keys(): + if key in keys_to_rename: + new_key = keys_to_rename[key] + new_ordered_dict[new_key] = current_dict[key] + else: + new_ordered_dict[key] = current_dict[key] + current_dict.clear() + current_dict.update(new_ordered_dict) + + # 对于基名重复的键,继续递归(如果值是字典) + for key, base in base_names.items(): + if base_name_count[base] > 1: + value = current_dict[key] + if isinstance(value, dict): + recurse(value, path + [base]) + elif isinstance(value, list): + # 如果值是列表,仍需收集基名到 good_list + if base not in good_list: + good_list.append(base) + + # 深拷贝数据以避免修改原始输入 + data_copy = deepcopy(data) + + # 开始递归遍历 + recurse(data_copy, []) + + # 转换 grouped_set 为列表,并排序以保持一致性 + # 为了保持 grouped_paths 的顺序与它们首次出现的顺序一致, + # 我们需要在递归过程中记录它们的顺序,而不是简单地排序。 + # 因此,修改 grouped_set 为列表并在添加时检查重复。 + grouped_paths = [] + for path in data.keys(): + pass # This is a placeholder if needed for order + + # 实际上,由于 grouped_set 已经去重且不保顺序, + # 我们需要重新遍历数据_copy 来收集 grouped_paths 按顺序 + def collect_grouped_paths(current_dict, path, collected): + for key in current_dict.keys(): + match = pattern.match(key) + if match: + base = match.group(1) + else: + base = key + current_path = '.'.join(path + [base]) + if current_path in grouped_set and current_path not in collected: + collected.append(current_path) + value = current_dict[key] + if isinstance(value, dict): + collect_grouped_paths(value, path + [base], collected) + + collected_grouped_paths = [] + collect_grouped_paths(data_copy, [], collected_grouped_paths) + grouped_paths = collected_grouped_paths + + return key_paths, grouped_paths, good_list, data_copy +def rename_keys(data): + """ + 对整个数据结构进行重命名处理。 + """ + + def rename_keys_recursive(current_dict): + """ + 递归地重命名字典中的键,确保同一层级下具有相同基名的键被正确编号。 + """ + if not isinstance(current_dict, dict): + return current_dict + + key_order = list(current_dict.keys()) + base_name_dict = defaultdict(list) + + # 辅助函数:提取基名(去除可能的 -数字 后缀) + def get_base_name(key): + if '-' in key: + parts = key.rsplit('-', 1) + if parts[1].isdigit(): + return parts[0] + return key + + # 将键按基名分组 + for key in key_order: + base = get_base_name(key) + base_name_dict[base].append(key) + + new_dict = {} + for key in key_order: + base = get_base_name(key) + keys = base_name_dict[base] + if len(keys) > 1: + # 如果存在同基名的多个键,则进行重命名 + if base not in new_dict: + # 按原始顺序对需要重命名的键进行排序 + sorted_keys = sorted(keys, key=lambda x: key_order.index(x)) + for idx, original_key in enumerate(sorted_keys, start=1): + new_key = f"{base}-{idx}" + # 如果值是字典,递归处理 + if isinstance(current_dict[original_key], dict): + new_dict[new_key] = rename_keys_recursive(current_dict[original_key]) + else: + new_dict[new_key] = current_dict[original_key] + else: + # 如果没有重复的基名,保持原名 + if isinstance(current_dict[key], dict): + new_dict[key] = rename_keys_recursive(current_dict[key]) + else: + new_dict[key] = current_dict[key] + + return new_dict + for top_key, top_value in data.items(): + if isinstance(top_value, dict): + data[top_key] = rename_keys_recursive(top_value) + return data def combine_and_update_results(original_data, updates): @@ -226,7 +271,7 @@ def combine_and_update_results(original_data, updates): # """ #文件内容以markdown格式组织,其中表格部分(若有)以html语法组织, -def get_technical_requirements(file_path,invalid_path): +def get_technical_requirements(file_path,invalid_path,processed_filepath): docx_file_path=pdf2docx(file_path) file_id=upload_file(docx_file_path) first_query_template="该文件是否说明了采购需求,即需要采购哪些货物?如果有,请回答'是',否则,回答'否'" #防止截取失败 @@ -297,7 +342,7 @@ def get_technical_requirements(file_path,invalid_path): 2. 采购目标:采购目标通常有硬件(如设备、货物)和软件(如系统软件、应用APP),一次采购活动可能同时包含这两种类型。对于工程类的施工、建设采购需求,无需提取。 3. 非清单形式处理:若未出现采购清单,则从表格或文字中摘取采购信息。 4. 系统归属:一些采购活动可能将采购目标划分为若干系统和货物,每个系统可能包含若干货物,则将这些货物名称作为该系统的二级键;系统可以只包含总体'系统功能'而无货物。 - 5. 软件需求:对于软件应用或系统软件需求,仅需列出系统模块构成(若有),并作为系统键值的一部分,无需在模块下再细分功能。 + 5. 软件需求:对于软件应用或系统软件需求,仅需列出系统模块构成(若有),并作为该系统键值的一部分,无需在模块下再细分功能。 6. 系统功能:若采购的某系统提及总体系统功能,则在系统值中添加'系统功能'二级键,不展开具体内容。 7. 完整性:确保不遗漏系统内的货物,也不添加未提及的内容。 @@ -359,19 +404,16 @@ def get_technical_requirements(file_path,invalid_path): else: # processed_filepath = convert_pdf_to_markdown(file_path) # 转markdown格式 # processed_filepath=r"C:\Users\Administrator\Desktop\货物标\extract_files\107国道.txt" - processed_filepath = pdf2txt(file_path) # 纯文本提取 user_query=generate_full_user_query(processed_filepath,prompt_template2) model_res=doubao_model(user_query) # model_res = qianwen_long(file_id,prompt_template1) print(model_res) cleaned_res = clean_json_string(model_res) #转字典 - normal_paths,good_list,grouped_paths,no_keys_added= generate_key_paths(cleaned_res['采购需求']) # 提取需要采购的货物清单 key_list:交通监控视频子系统.高清视频抓拍像机 ... grouped_paths是同一系统下同时有'交换机-1'和'交换机-2',提取'交换机' ,输出eg:{'交通标志.标志牌铝板', '交通信号灯.交换机'} - if no_keys_added: - ffinal_res = postprocess(cleaned_res) - else: - # user_query_template = "请你根据该货物标中采购要求部分的内容,请你给出\"{}\"的技术参数(或采购要求),请以json格式返回结果,外层键名为\"{}\", 键值对中的键是你对该要求的总结,而值需要完全与原文保持一致,不可擅自总结删减。" - user_query_template = """请根据货物标中采购要求部分的内容,告诉我\"{}\"的技术参数或采购要求是什么。请以 JSON 格式返回结果,键名为\"{}\",键值为一个列表,列表中包含若干描述\"{}\"的技术参数或采购要求的字符串,请按原文内容回答,保留三角▲、五角★和序号,不可擅自增添内容,尤其是不可擅自添加序号。 + key_paths, grouped_paths, good_list, data_copy= generate_key_paths(cleaned_res['采购需求']) # 提取需要采购的货物清单 key_list:交通监控视频子系统.高清视频抓拍像机 ... grouped_paths是同一系统下同时有'交换机-1'和'交换机-2',提取'交换机' ,输出eg:{'交通标志.标志牌铝板', '交通信号灯.交换机'} + modified_data=rename_keys(data_copy) + # user_query_template = "请你根据该货物标中采购要求部分的内容,请你给出\"{}\"的技术参数(或采购要求),请以json格式返回结果,外层键名为\"{}\", 键值对中的键是你对该要求的总结,而值需要完全与原文保持一致,不可擅自总结删减。" + user_query_template = """请根据货物标中采购要求部分的内容,告诉我\"{}\"的技术参数或采购要求是什么。请以 JSON 格式返回结果,键名为\"{}\",键值为一个列表,列表中包含若干描述\"{}\"的技术参数或采购要求的字符串,请按原文内容回答,保留三角▲、五角★和序号,不可擅自增添内容,尤其是不可擅自添加序号。 要求与指南: 1. 如果该货物没有相关采购要求或技术参数要求,键值应为空列表。 2. 如果存在嵌套结构,且原文为Markdown 的表格语法,如'摄像机|有效像素|≥900W像素', 请不要返回该Markdown语法,而是使用冒号':'将相关信息拼接在一起,生成一条完整且清晰的技术参数(或采购要求)描述,作为列表中的一个字符串。如"摄像机:有效像素:≥900W像素"。 @@ -395,7 +437,7 @@ def get_technical_requirements(file_path,invalid_path): ] }} """ - user_query_template_two="""请根据货物标中采购要求部分的内容,告诉我\"{}\"的技术参数或采购要求是什么。由于该货物存在多种不同的采购要求或技术参数,请逐一列出,并以 JSON 格式返回结果。请以'货物名-编号'区分多种型号,编号为从 1 开始的自然数,依次递增,即第一个键名为\"{}-1\", 键值为一个列表,列表中包含若干描述\"{}\"的技术参数(或采购要求)的字符串,请按原文内容回答,保留三角▲、五角★和序号(若有),不可擅自增添内容。 + user_query_template_two="""请根据货物标中采购要求部分的内容,告诉我\"{}\"的技术参数或采购要求是什么。由于该货物存在多种不同的采购要求或技术参数,请逐一列出,并以 JSON 格式返回结果。请以'货物名-编号'区分多种型号,编号为从 1 开始的自然数,依次递增,即第一个键名为\"{}-1\", 键值为一个列表,列表中包含若干描述\"{}\"的技术参数(或采购要求)的字符串,请按原文内容回答,保留三角▲、五角★和序号(若有),不可擅自增添内容。 请注意以下特殊情况: 要求与指南: 1. 如果该货物没有相关采购要求或技术参数要求,键值应为空列表。 @@ -426,38 +468,38 @@ def get_technical_requirements(file_path,invalid_path): ] }} """ - queries = [] - for key in normal_paths: - # 将键中的 '.' 替换为 '下的' - modified_key = key.replace('.', '下的') - # 使用修改后的键填充第一个占位符,原始键填充第二个占位符 - new_query = user_query_template.format(modified_key, key, modified_key) - queries.append(new_query) + queries = [] + for key in key_paths: + # 将键中的 '.' 替换为 '下的' + modified_key = key.replace('.', '下的') + # 使用修改后的键填充第一个占位符,原始键填充第二个占位符 + new_query = user_query_template.format(modified_key, key, modified_key) + queries.append(new_query) - # 处理 grouped_paths 中的项,应用 user_query_template_two - for grouped_key in grouped_paths: - # 将键中的 '.' 替换为 '下的' - modified_grouped_key = grouped_key.replace('.', '下的') - # 使用修改后的键填充第一个占位符,原始键填充第二个占位符 - new_query = user_query_template_two.format(modified_grouped_key, grouped_key, modified_grouped_key) - queries.append(new_query) - results = multi_threading(queries, "", file_id, 2) - technical_requirements = [] - if not results: - print("errror!未获得大模型的回答!") - else: - # 打印结果 - for question, response in results: - technical_requirements.append(response) - # print(response) - technical_requirements_combined_res = combine_json_results(technical_requirements) + # 处理 grouped_paths 中的项,应用 user_query_template_two + for grouped_key in grouped_paths: + # 将键中的 '.' 替换为 '下的' + modified_grouped_key = grouped_key.replace('.', '下的') + # 使用修改后的键填充第一个占位符,原始键填充第二个占位符 + new_query = user_query_template_two.format(modified_grouped_key, grouped_key, modified_grouped_key) + queries.append(new_query) + results = multi_threading(queries, "", file_id, 2) + technical_requirements = [] + if not results: + print("errror!未获得大模型的回答!") + else: + # 打印结果 + for question, response in results: + technical_requirements.append(response) + # print(response) + technical_requirements_combined_res = combine_json_results(technical_requirements) - """根据所有键是否已添加处理技术要求""" - # 更新原始采购需求字典 - final_res=combine_and_update_results(cleaned_res['采购需求'], technical_requirements_combined_res) - ffinal_res=all_postprocess(final_res) - # final_res = postprocess(cleaned_res) - ffinal_res["货物列表"] = good_list + """根据所有键是否已添加处理技术要求""" + # 更新原始采购需求字典 + final_res=combine_and_update_results(modified_data, technical_requirements_combined_res) + ffinal_res=all_postprocess(final_res) + # final_res = postprocess(cleaned_res) + ffinal_res["货物列表"] = good_list # 输出最终的 JSON 字符串 return {"采购需求":ffinal_res} @@ -496,53 +538,18 @@ def test_all_files_in_folder(input_folder, output_folder): # "业务视频应用": { # "视频回放": {} # }, -#TODO:{ 顺序变了 -# "采购需求": { -# "高清数字枪机-1": [], -# "枪机支架-1": [], -# "高清数字半球机-1": [], -# "网络硬盘录像机-1": [], -# "监硬控硬盘-1": [], -# "交换机-1": [], -# "交换机-2": [], -# "监视器-1": [], -# "电源线-1": [], -# "网线-1": [], -# "水晶头-1": [], -# "PVC线槽-1": [], -# "辅料-1": [], -# "安装调试-1": [] -# } -# } -# { -# "采购需求": { -# "交换机-1": [], -# "交换机-2": [], -# "高清数字枪机": [], -# "枪机支架": [], -# "高清数字半球机": [], -# "网络硬盘录像机": [], -# "监硬控硬盘": [], -# "监视器": [], -# "电源线": [], -# "网线": [], -# "水晶头": [], -# "PVC线槽": [], -# "辅料": [], -# "安装调试": [] -# } -# } if __name__ == "__main__": start_time=time.time() # truncate_file="C:\\Users\\Administrator\\Desktop\\fsdownload\\469d2aee-9024-4993-896e-2ac7322d41b7\\ztbfile_procurement.docx" - truncate_file=r"C:\Users\Administrator\Desktop\fsdownload\db79e9e0-830e-442c-8cb6-1d036215f8ff\ztbfile_procurement.pdf" + truncate_file=r"C:\Users\Administrator\Desktop\货物标\output1\陕西省公安厅交通警察总队高速公路交通安全智能感知巡查系统项目 (1)_procurement.pdf" # invalid_path="D:\\flask_project\\flask_app\\static\\output\\output1\\e7dda5cb-10ba-47a8-b989-d2993d34bb89\\ztbfile.pdf" # truncate_file="D:\\flask_project\\flask_app\\static\\output\\output1\\e7dda5cb-10ba-47a8-b989-d2993d34bb89\\ztbfile_procurement.docx" # output_folder="C:\\Users\\Administrator\\Desktop\\货物标\\output1\\tmp" # file_id = upload_file(truncate_file) invalid_path="C:\\Users\\Administrator\\Desktop\\fsdownload\\a110ed59-00e8-47ec-873a-bd4579a6e628\\ztbfile.pdf" # file_id=upload_file(truncate_file) - res=get_technical_requirements(truncate_file,invalid_path) + processed_filepath = pdf2txt(truncate_file) + res=get_technical_requirements(truncate_file,invalid_path,processed_filepath) json_string = json.dumps(res, ensure_ascii=False, indent=4) print(json_string) # # input_folder = "C:\\Users\\Administrator\\Desktop\\货物标\\output1" diff --git a/flask_app/货物标/技术参数要求提取后处理函数.py b/flask_app/货物标/技术参数要求提取后处理函数.py index 4591ba4..7f6fe61 100644 --- a/flask_app/货物标/技术参数要求提取后处理函数.py +++ b/flask_app/货物标/技术参数要求提取后处理函数.py @@ -5,6 +5,9 @@ from collections import OrderedDict from collections import defaultdict #传输技术参数需求的时候后处理 def extract_matching_keys(data, good_list, special_keys=None, parent_key=''): + import re + from collections import defaultdict + def get_suffix(n): """ 根据数字n返回对应的字母后缀。 @@ -23,9 +26,10 @@ def extract_matching_keys(data, good_list, special_keys=None, parent_key=''): if isinstance(data, dict): for key, value in data.items(): + clean_key = key.replace(" ", "") # 去除键中的空格 if isinstance(value, list): - if key not in special_keys and any(pattern.match(key) for pattern in patterns): - counter[key] += 1 + if clean_key not in special_keys and any(pattern.match(clean_key) for pattern in patterns): + counter[clean_key] += 1 elif isinstance(value, dict): count_matching_keys(value, patterns, special_keys, counter) elif isinstance(data, list): @@ -44,21 +48,22 @@ def extract_matching_keys(data, good_list, special_keys=None, parent_key=''): if isinstance(data, dict): for key, value in data.items(): + clean_key = key.replace(" ", "") # 去除键中的空格 if isinstance(value, list): # 处理值为列表的键 - if any(pattern.match(key) for pattern in patterns): - new_key = generate_key(key, parent_key, key_counter, suffix_map, special_keys) + if any(pattern.match(clean_key) for pattern in patterns): + new_key = generate_key(clean_key, parent_key, key_counter, suffix_map, special_keys) filtered_data[new_key] = value elif isinstance(value, dict): # 继续递归处理嵌套字典 - new_parent_key = key if parent_key == '' else f"{parent_key}的{key}" + new_parent_key = clean_key if parent_key == '' else f"{parent_key}的{clean_key}" process_data(value, patterns, special_keys, key_counter, suffix_map, - filtered_data, new_parent_key) + filtered_data, new_parent_key) elif isinstance(data, list): for item in data: if isinstance(item, (dict, list)): process_data(item, patterns, special_keys, key_counter, suffix_map, - filtered_data, parent_key) + filtered_data, parent_key) def generate_key(key, parent_key, key_counter, suffix_map, special_keys): """生成新的键名""" @@ -71,9 +76,13 @@ def extract_matching_keys(data, good_list, special_keys=None, parent_key=''): return key if special_keys is None: - special_keys = [] + special_keys = ["系统功能"] # 默认值为 ["系统功能"] - patterns = [re.compile(r'^' + re.escape(g) + r'(?:-\d+)?$') for g in good_list] + # 去除 good_list 中的空格 + clean_good_list = [g.replace(" ", "") for g in good_list] + + # 构建匹配的正则表达式 + patterns = [re.compile(r'^' + re.escape(g) + r'(?:-\d+)?$') for g in clean_good_list] # 先统计所有匹配键的出现次数,仅统计值为列表的键 key_counter = count_matching_keys(data, patterns, special_keys) @@ -89,6 +98,7 @@ def extract_matching_keys(data, good_list, special_keys=None, parent_key=''): return filtered_data + def postprocess(data): """递归地转换字典中的值为列表,如果所有键对应的值都是'/', '{}' 或 '未知'""" def convert_dict(value): diff --git a/flask_app/货物标/提取采购需求main.py b/flask_app/货物标/提取采购需求main.py index fb961b9..9a6801d 100644 --- a/flask_app/货物标/提取采购需求main.py +++ b/flask_app/货物标/提取采购需求main.py @@ -1,13 +1,14 @@ import concurrent.futures import json import time + +from flask_app.general.doubao import pdf2txt from flask_app.货物标.技术参数要求提取 import get_technical_requirements from flask_app.general.通义千问long import upload_file from flask_app.货物标.商务服务其他要求提取 import get_business_requirements #获取采购清单 - def fetch_procurement_reqs(procurement_path, invalid_path): # procurement_docpath = pdf2docx(procurement_path) # 采购需求docx # 定义默认的 procurement_reqs 字典 @@ -24,13 +25,13 @@ def fetch_procurement_reqs(procurement_path, invalid_path): return DEFAULT_PROCUREMENT_REQS.copy() try: - + processed_filepath = pdf2txt(procurement_path) # 纯文本提取 # 使用 ThreadPoolExecutor 并行处理 get_technical_requirements 和 get_business_requirements with concurrent.futures.ThreadPoolExecutor() as executor: # 提交任务给线程池 - future_technical = executor.submit(get_technical_requirements, procurement_path, invalid_path) + future_technical = executor.submit(get_technical_requirements, procurement_path, invalid_path,processed_filepath) time.sleep(0.5) # 保持原有的延时 - future_business = executor.submit(get_business_requirements, procurement_path) + future_business = executor.submit(get_business_requirements, procurement_path,processed_filepath) # 获取并行任务的结果 technical_requirements = future_technical.result() @@ -56,13 +57,14 @@ def fetch_procurement_reqs(procurement_path, invalid_path): # 在出错时返回默认的包含空字符串的字典 return DEFAULT_PROCUREMENT_REQS.copy() -#TODO:技术要求可以在技术参数之后执行,把完整的技术参数输入,问大模型,除了上述内容还有哪些 + +#TODO:技术要求可以在技术参数之后执行,把完整的技术参数输入,问大模型,除了上述内容还有哪些,这样的话把技术标和其他的区分开。 if __name__ == "__main__": start_time=time.time() output_folder = "C:\\Users\\Administrator\\Desktop\\货物标\\货物标output" # file_path="C:\\Users\\Administrator\\Desktop\\货物标\\output1\\2-招标文件(2020年广水市中小学教师办公电脑系统及多媒体“班班通”设备采购安装项目)_procurement.pdf" - procurement_path = "C:\\Users\\Administrator\\Desktop\\fsdownload\\db79e9e0-830e-442c-8cb6-1d036215f8ff\\ztbfile_procurement.pdf" - procurement_docpath="C:\\Users\\Administrator\\Desktop\\fsdownload\\db79e9e0-830e-442c-8cb6-1d036215f8ff\\ztbfile_procurement.docx" + procurement_path = r"C:\Users\Administrator\Desktop\fsdownload\5901b181-b55f-4107-9f30-c85d607b1fa0\ztbfile_procurement.pdf" + procurement_docpath=r"C:\Users\Administrator\Desktop\fsdownload\5901b181-b55f-4107-9f30-c85d607b1fa0" invalid_path="C:\\Users\\Administrator\\Desktop\\fsdownload\\db79e9e0-830e-442c-8cb6-1d036215f8ff\\ztbfile.pdf" res=fetch_procurement_reqs(procurement_path,invalid_path) print(json.dumps(res, ensure_ascii=False, indent=4))