diff --git a/flask_app/general/无效标和废标公共代码.py b/flask_app/general/无效标和废标公共代码.py index 34c7c1f..160aa4a 100644 --- a/flask_app/general/无效标和废标公共代码.py +++ b/flask_app/general/无效标和废标公共代码.py @@ -576,7 +576,6 @@ def combine_find_invalid(file_path, output_dir): print("无效标与废标done...") return {"无效标与废标项": combined_dict} -# TODO:无效标目前以整个docx文档作为输入,可能导致后面两章不必要的信息也导入。 无效投标至少>8个字 if __name__ == '__main__': start_time = time.time() # truncate_json_path = "C:\\Users\\Administrator\\Desktop\\货物标\\output4\\tmp2\\竞争性谈判文件(3)_tobidders_notice_part1\\truncate_output.json" diff --git a/flask_app/old_version/截取pdf_old.py b/flask_app/old_version/截取pdf_old.py index 51280ee..4770c4b 100644 --- a/flask_app/old_version/截取pdf_old.py +++ b/flask_app/old_version/截取pdf_old.py @@ -514,8 +514,6 @@ def truncate_pdf_specific_engineering(pdf_path, output_folder, selections, uniqu logger.error(f"Error in truncate_pdf_specific_engineering: {e}") return [""] * len(selections) # 返回与 selections 数量相同的空字符串列表 - -#TODO:zbtest8 zbtest18有问题 后期需要完善,截取需要截两次,第一次严格第二次宽松 if __name__ == "__main__": input_path = "C:\\Users\\Administrator\\Desktop\\招标文件\\new_test\\zbtest8.pdf" # input_path="C:\\Users\\Administrator\\Desktop\\fsdownload\\68549b0b-e892-41a9-897c-c3694535ee61\\ztbfile.pdf" diff --git a/flask_app/routes/货物标解析main.py b/flask_app/routes/货物标解析main.py index f519eee..fb04ce1 100644 --- a/flask_app/routes/货物标解析main.py +++ b/flask_app/routes/货物标解析main.py @@ -278,6 +278,7 @@ def goods_bid_main(output_folder, file_path, file_type, unique_id): #TODO:同系统下的多个货物,记录一下数量 #TODO:设备前面带星,而不是要求前面带星。 +#TODO:重置一下投标文件格式提取那部分的代码 #商务标这里改为列表最里层 #good_list 金额 截取上下文 if __name__ == "__main__": diff --git a/flask_app/test_case/__init__.py b/flask_app/test_case/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask_app/test_case/test_combine_and_update_results.py b/flask_app/test_case/test_combine_and_update_results.py new file mode 100644 index 0000000..2878d96 --- /dev/null +++ b/flask_app/test_case/test_combine_and_update_results.py @@ -0,0 +1,416 @@ +def combine_and_update_results(original_data, updates): + def normalize_key(key): + """ + 规范化键名: + - 替换全角点号为半角点号。 + - 删除所有空格(包括半角和全角)。 + """ + # 替换全角点号(.、。)为半角点号(.) + key = key.replace('.', '.').replace('。', '.') + # 删除所有空格(半角空格和全角空格) + key = key.replace(' ', '').replace('\u3000', '') + return key + + def normalize_original_data(d): + """ + 递归规范化原始数据字典的键。 + """ + if not isinstance(d, dict): + return d + normalized = {} + for k, v in d.items(): + nk = normalize_key(k) + normalized[nk] = normalize_original_data(v) + return normalized + + def normalize_update_value(value): + """ + 递归规范化更新字典中嵌套的字典的键。 + """ + if isinstance(value, dict): + return {normalize_key(k): normalize_update_value(v) for k, v in value.items()} + else: + return value + + def recursive_update(data, key, value): + """ + 递归更新嵌套字典。 + """ + keys = key.split('.') + for k in keys[:-1]: + data = data.setdefault(k, {}) + if isinstance(value, dict) and isinstance(data.get(keys[-1], None), dict): + data[keys[-1]] = {**data.get(keys[-1], {}), **value} + else: + data[keys[-1]] = value + + # 1. 规范化原始数据字典的键 + original_data = normalize_original_data(original_data) + + # 2. 规范化更新字典的键 + normalized_updates = {} + for key, value in updates.items(): + nk = normalize_key(key) + nv = normalize_update_value(value) + normalized_updates[nk] = nv + + # 3. 执行递归更新 + for key, value in normalized_updates.items(): + recursive_update(original_data, key, value) + + return original_data + + +# 测试用例1:复杂情况 +def test_complex_case(): + original_data = { + 'user.name': 'Alice', + 'user。details': { + 'age': 30, + 'address line': '123 Main St', # 全角空格 + 'preferences': { + 'color': 'blue', + 'food': 'sushi' + } + }, + 'status': 'active', + 'metrics': { + 'score': 85, + 'rank': 5 + }, + 'list_field': [1, 2, 3] + } + + updates = { + 'user. name': 'Bob', # 更新user.name + 'user.details.age': 31, # 更新嵌套字段 + 'user.details.addressline': '456 Elm St', # 更新地址,键中有空格被移除 + 'user.details.preferences.hobby': 'cycling', # 添加新的嵌套字段 + 'status': 'inactive', # 更新顶级字段 + 'metrics.score': {'current': 90, 'max': 100}, # 更新为字典 + 'metrics.new_metric': 50, # 添加新的顶级嵌套字段 + 'new_field': 'new_value', # 添加新的顶级字段 + 'list_field': [4, 5] # 更新列表字段 + } + + expected_result = { + 'user': { + 'name': 'Bob', + 'details': { + 'age': 31, + 'addressline': '456 Elm St', + 'preferences': { + 'color': 'blue', + 'food': 'sushi', + 'hobby': 'cycling' + } + } + }, + 'status': 'inactive', + 'metrics': { + 'score': {'current': 90, 'max': 100}, + 'rank': 5, + 'new_metric': 50 + }, + 'list_field': [4, 5], + 'new_field': 'new_value' + } + + result = combine_and_update_results(original_data, updates) + assert result == expected_result, f"测试复杂情况失败。\n预期: {expected_result}\n实际: {result}" + print("测试复杂情况通过。") + + +# 测试用例2:更新为空 +def test_empty_updates(): + original_data = { + 'key1': 'value1', + 'key2': { + 'subkey': 'subvalue' + } + } + updates = {} + expected_result = { + 'key1': 'value1', + 'key2': { + 'subkey': 'subvalue' + } + } + + result = combine_and_update_results(original_data, updates) + assert result == expected_result, f"测试更新为空失败。\n预期: {expected_result}\n实际: {result}" + print("测试更新为空通过。") + + +# 测试用例3:原始数据为空 +def test_empty_original(): + original_data = {} + updates = { + 'new.key': 'new value', + 'nested。key.sub key': 123 + } + expected_result = { + 'new.key': 'newvalue', + 'nested.key': { + 'subkey': 123 + } + } + + result = combine_and_update_results(original_data, updates) + assert result == expected_result, f"测试原始数据为空失败。\n预期: {expected_result}\n实际: {result}" + print("测试原始数据为空通过。") + + +# 测试用例4:更新中包含非字典类型的值 +def test_non_dict_values(): + original_data = { + 'a.b': { + 'c.d': 1 + }, + 'list.field': [1, 2, 3] + } + updates = { + 'a.b.c.d': {'new': 2}, + 'list.field': 'not a list anymore' + } + expected_result = { + 'a': { + 'b': { + 'c': { + 'd': {'new': 2} + } + } + }, + 'list.field': 'not a list anymore' + } + + result = combine_and_update_results(original_data, updates) + assert result == expected_result, f"测试更新中包含非字典类型的值失败。\n预期: {expected_result}\n实际: {result}" + print("测试更新中包含非字典类型的值通过。") + + +# 测试用例5:键中包含全角空格 +def test_full_width_spaces(): + original_data = { + 'key with fullwidth spaces': 'value1', + 'nested.key with.spaces': { + 'sub key': 'subvalue' + } + } + updates = { + 'key with fullwidth spaces': 'updated_value', + 'nested.key with spaces.new subkey': 'new_subvalue' + } + expected_result = { + 'keywithfullwidthspaces': 'updated_value', + 'nested': { + 'keywithspaces': { + 'subkey': 'new_subvalue' + } + } + } + + result = combine_and_update_results(original_data, updates) + assert result == expected_result, f"测试键中包含全角空格失败。\n预期: {expected_result}\n实际: {result}" + print("测试键中包含全角空格通过。") + + +# 测试用例6:复杂嵌套和不同数据类型 +def test_nested_and_various_types(): + original_data = { + 'config.settings': { + 'resolution': '1080p', + 'volume': 75, + 'controls': { + 'jump': 'space', + 'crouch': 'ctrl' + } + }, + 'user.preferences': { + 'theme': 'dark', + 'notifications': True + } + } + + updates = { + 'config.settings.resolution': '4K', + 'config.settings.controls.run': 'shift', # 添加新控制 + 'config.settings.volume': 80, # 更新音量 + 'user.preferences.theme': 'light', # 更新主题 + 'user.preferences.language': 'en-US', # 添加新偏好 + 'new.section.setting': 'enabled' # 添加新部分 + } + + expected_result = { + 'config': { + 'settings': { + 'resolution': '4K', + 'volume': 80, + 'controls': { + 'jump': 'space', + 'crouch': 'ctrl', + 'run': 'shift' + } + } + }, + 'user': { + 'preferences': { + 'theme': 'light', + 'notifications': True, + 'language': 'en-US' + } + }, + 'new.section': { + 'setting': 'enabled' + } + } + + result = combine_and_update_results(original_data, updates) + assert result == expected_result, f"测试复杂嵌套和不同数据类型失败。\n预期: {expected_result}\n实际: {result}" + print("测试复杂嵌套和不同数据类型通过。") + + +# 测试用例7:键仅包含空格和点号 +def test_keys_only_spaces_and_dots(): + original_data = { + '. ..': 'value1', + '  ': 'value2', # 仅全角空格 + 'nested.key': { + '.sub.key.': 'subvalue' + } + } + updates = { + '...': 'updated_value1', + 'nested.key..sub.key.': 'updated_subvalue' + } + expected_result = { + '..': 'updated_value1', + '': 'value2', # 全角空格被删除后键为空字符串 + 'nested': { + 'key': { + 'subkey': 'updated_subvalue' + } + } + } + + result = combine_and_update_results(original_data, updates) + assert result == expected_result, f"测试键仅包含空格和点号失败。\n预期: {expected_result}\n实际: {result}" + print("测试键仅包含空格和点号通过。") + + +# 测试用例8:更新覆盖整个嵌套结构 +def test_overwrite_nested_structure(): + original_data = { + 'settings': { + 'display': { + 'brightness': 70, + 'contrast': 50 + }, + 'sound': { + 'volume': 80 + } + } + } + updates = { + 'settings.display': 'default', # 覆盖整个display字典 + 'settings.sound.volume': 90 # 更新音量 + } + expected_result = { + 'settings': { + 'display': 'default', + 'sound': { + 'volume': 90 + } + } + } + + result = combine_and_update_results(original_data, updates) + assert result == expected_result, f"测试覆盖整个嵌套结构失败。\n预期: {expected_result}\n实际: {result}" + print("测试覆盖整个嵌套结构通过。") + + +# 测试用例9:更新包含列表中的字典 +def test_update_with_list_of_dicts(): + original_data = { + 'users': [ + {'name': 'Alice', 'age': 30}, + {'name': 'Bob', 'age': 25} + ], + 'settings': { + 'theme': 'dark' + } + } + updates = { + 'users': [ + {'name': 'Alice', 'age': 31}, # 更新第一个用户的年龄 + {'name': 'Bob', 'age': 26}, # 更新第二个用户的年龄 + {'name': 'Charlie', 'age': 22} # 添加新用户 + ], + 'settings.theme': 'light' # 更新主题 + } + expected_result = { + 'users': [ + {'name': 'Alice', 'age': 31}, + {'name': 'Bob', 'age': 26}, + {'name': 'Charlie', 'age': 22} + ], + 'settings': { + 'theme': 'light' + } + } + + result = combine_and_update_results(original_data, updates) + assert result == expected_result, f"测试更新包含列表中的字典失败。\n预期: {expected_result}\n实际: {result}" + print("测试更新包含列表中的字典通过。") + + +# 测试用例10:键重复但规范化后不同 +def test_duplicate_keys_after_normalization(): + original_data = { + 'key': { + 'one':'value1', + 'two':'value2' + } + } + updates = { + 'key.one': 'updated_value', + 'keytwo': 'updated_value3' + } + expected_result = { + 'keyone': 'updated_value', # 'key.one' 和 'key.one ' 规范化后合并为 'keyone' + 'keytwo': 'updated_value3' # 'key two' 规范化后为 'keytwo' + } + + result = combine_and_update_results(original_data, updates) + assert result == expected_result, f"测试键重复但规范化后不同失败。\n预期: {expected_result}\n实际: {result}" + print("测试键重复但规范化后不同通过。") + + +# 主函数,运行所有测试用例 +def run_all_tests(): + test_functions = [ + # test_complex_case, + # test_empty_updates, + # test_empty_original, + # test_non_dict_values, + # test_full_width_spaces, + # test_nested_and_various_types, + # test_keys_only_spaces_and_dots, + # test_overwrite_nested_structure, + # test_update_with_list_of_dicts, + test_duplicate_keys_after_normalization + ] + + for test_func in test_functions: + try: + test_func() + except AssertionError as e: + print(e) + except Exception as ex: + print(f"{test_func.__name__} 运行时发生错误: {ex}") + + print("所有测试用例已执行完毕。") + + +if __name__ == '__main__': + run_all_tests() diff --git a/flask_app/test_case/test_extract_matching_keys.py b/flask_app/test_case/test_extract_matching_keys.py new file mode 100644 index 0000000..6ea2579 --- /dev/null +++ b/flask_app/test_case/test_extract_matching_keys.py @@ -0,0 +1,92 @@ +from flask_app.货物标.技术参数要求提取后处理函数 import extract_matching_keys + + +def test_extract_matching_keys(): + # 定义测试数据 + data = { + "fruits": ["apple", "banana"], + "fruits-1": ["orange"], + "vegetables": ["carrot"], + "系统功能": ["feature1"], # 特殊键,应被特殊处理或排除 + "grains": { + "whole": ["rice", "wheat"], + "whole-1": ["barley"], + "refined": ["white rice"] + }, + "misc": { + "fruits": ["strawberry"], + "fruits-1": ["blueberry"], + "系统 功能": ["feature2"], # 特殊键,带空格 + "dairy": ["milk", "cheese"] + }, + "beverages": { + "alcoholic": { + "beer": ["lager", "ale"], + "wine": ["red", "white"] + }, + "non-alcoholic": ["juice", "soda"] + }, + "snacks": [ + { + "chips": ["potato", "tortilla"], + "nuts": ["almonds", "cashews"] + }, + { + "chips-1": ["kettle", "baked"], + "candies": ["chocolate", "gummy"] + } + ] + } + + good_list = ["fruits", "vegetables", "grains", "chips"] + + special_keys = ["系统功能", "dairy"] # 假设 'dairy' 也是特殊键 + + # 预期输出 + expected_output = { + "fruits-a": ["apple", "banana"], + "fruits-b": ["orange"], + "vegetables": ["carrot"], + "grains-a": ["rice", "wheat"], + "grains-b": ["barley"], + "grains": {"refined": ["white rice"]}, + "misc": {}, # 'fruits' and 'fruits-1' inside 'misc' should be processed + "chips-a": ["potato", "tortilla"], + "chips-b": ["kettle", "baked"] + } + + # 注意:根据您的函数逻辑,特殊键会被排除,且嵌套的 'fruits' 会被处理 + # 这里我们需要根据函数实际行为调整预期输出 + # 让我们根据函数逻辑重新定义预期输出 + + # 函数会生成新的键名,对于重复的 'fruits' 会添加后缀 + # 'grains' 内的 'whole' 和 'whole-1' 也会被处理为 'whole-a', 'whole-b' + # 'chips' 和 'chips-1' 会被处理为 'chips-a', 'chips-b' + # 'dairy' 是特殊键,应被排除 + # '系统功能' 和 '系统 功能' 是特殊键,应被排除 + + expected_output_correct = { + "fruits-a": ["apple", "banana"], + "fruits-b": ["orange"], + "vegetables": ["carrot"], + "grains-a": ["rice", "wheat"], + "grains-b": ["barley"], + "chips-a": ["potato", "tortilla"], + "chips-b": ["kettle", "baked"] + } + + # 运行函数 + result = extract_matching_keys(data, good_list, special_keys) + + # 打印结果 + print("测试用例: 提取匹配键并处理各种情况") + print("输入数据:", data) + print("good_list:", good_list) + print("special_keys:", special_keys) + print("\n预期输出:", expected_output_correct) + print("实际输出:", result) + print("\n测试通过:", result == expected_output_correct) + +# 运行测试 +if __name__ == "__main__": + test_extract_matching_keys() diff --git a/flask_app/test_case/test_generate_key_paths.py b/flask_app/test_case/test_generate_key_paths.py new file mode 100644 index 0000000..08c0049 --- /dev/null +++ b/flask_app/test_case/test_generate_key_paths.py @@ -0,0 +1,352 @@ +import json +import re +from collections import defaultdict +from copy import deepcopy + + +def generate_key_paths(data): + """ + 处理输入的字典,生成 key_paths, grouped_paths 和 good_list,并根据条件修改原始字典。 + + 参数: + data (dict): 输入的嵌套字典。 + + 返回: + tuple: 包含 key_paths, grouped_paths 和 good_list 的元组。 + """ + # 编译用于匹配后缀的正则表达式模式 + pattern = re.compile(r'(.+)-\d+$') + + # 初始化结果列表和字典 + key_paths = [] + grouped_counts = defaultdict(int) # 用于记录每个 grouped_path 的数量 + good_list = [] + + def recurse(current_dict, path): + """ + 递归遍历字典,处理 key_paths 和 grouped_paths,并收集 good_list 和 grouped_counts。 + + 参数: + 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: + base = key + base_names[key] = base + base_name_count[base] = base_name_count.get(base, 0) + 1 + + # 第二遍遍历,根据基名的出现次数分类 + 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_path = '.'.join(path + [base]) + grouped_counts[grouped_path] += 1 + + # 执行键名的重命名,同时保持原有顺序 + 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, []) + + 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_counts 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 转换为包含数量的字典列表 + grouped_paths = [{path: grouped_counts[path]} for path in 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 + + # 对整个数据结构进行递归重命名 + return rename_keys_recursive(data) +def test_generate_key_paths(): + data0={ + "显示系统": { + "LED全彩显示屏": [], + "控制盒及电源": [], + "大屏播控系统": [], + "配电柜(含PLC)": [], + "钢结构底座及铝型材支架": [], + "电缆及信号线缆": [], + "控制终端": [], + "50寸液晶电视机": [], + "50寸电视机地面推车": [], + "高清监视器": [] + }, + "摄像系统": { + "高清摄像机": [], + "摄像机三脚架": [], + "摄像机壁装架": [], + "摄像机控制键盘": [] + }, + "视频处理系统": { + "高清视频拼控矩阵(16*16)": [], + "分量信号接口器": [], + "高清四画面分割器": [] + }, + "发言系统": { + "数字会议发言主机": [], + "方形短杆代表话筒": [], + "专用连接线缆": [], + "手持无线话筒": [] + }, + "视频会议系统": { + "多点控制器": [], + "多串口控制服务器": [], + "综合会议管理调度平台": [], + "高清会议终端(主会场)": [], + "高清会议终端(分会场)": [], + "65寸电视机移动推车(9楼)": [], + "65寸液晶电视机(分会场)": [], + "控制平板及软件": [], + "鹅颈话筒": [] + }, + "辅助系统": { + "时序电源": [], + "多媒体地插盒": [], + "线材辅料": [], + "墙体拆除及修复": [] + } + } + # 定义测试数据 + data = { + "fruits": [], + "fruits-1": [], + "vegetables": { + "root": [], + "root-1": [], + "leafy": [] + }, + "grains": { + "交换机":[], + "whole grains": [], # 键名包含空格 + "whole-grains-1": [] # 同一基名,带后缀 + }, + "misc": { + "system functions": [], # 特殊键,包含空格 + "system functions-1": [], # 特殊键,带后缀 + "fruits-1": [], + "fruits-2": [], + "fruits-3": [] + }, + "beverages": [] + } + # 运行函数 + key_paths, grouped_paths, good_list, data_copy = generate_key_paths(data0) + # 打印结果 + print("实际 data_copy:") + print(json.dumps(data_copy,ensure_ascii=False,indent=4)) + modi=rename_keys(data_copy) + print(json.dumps(modi, ensure_ascii=False, indent=4)) + + print(key_paths) + print(grouped_paths) + print(good_list) + +user_query_template = """请根据货物标中采购要求部分的内容,告诉我\"{}\"的技术参数或采购要求是什么。请以 JSON 格式返回结果,键名为\"{}\",键值为一个列表,列表中包含若干描述\"{}\"的技术参数或采购要求的字符串,请按原文内容回答,保留三角▲、五角★和序号,不可擅自增删内容,尤其是不可擅自添加序号。 + +要求与指南: +1. 如果该货物没有相关采购要求或技术参数要求,键值应为空列表[]。 +2. 如果存在嵌套结构,且原文为Markdown 的表格语法,如'摄像机|有效像素|≥900W像素', 请不要返回该Markdown语法,而是使用冒号':'将相关信息拼接在一起,生成一条完整且清晰的技术参数(或采购要求)描述,作为列表中的一个字符串。如"摄像机:有效像素:≥900W像素"。 +3. 字符串中的内容为具体的技术参数要求或采购要求,请不要返回诸如'(1)高清录像功能'这种标题性质且不能体现要求的内容。 +4. 你的键值应该全面,对于同一个单元格内的数据,尽量全面,不要遗漏,对于单元格内以序号分隔的各条参数要求,请分别添加进键值(即字符串列表)中。 + +### 示例输出1如下: +{{ + "摄像机控制键盘": [ + "1、▲支持串行 RS232/RS422 和 IP 混合控制,允许在一个控制器上使用 RS232/RS422/IP 控制单个系统中的摄像机;", + "2、支持 2 组 RS422 串口 VISCA 协议菊花链控制 2x7 台摄像机。", + "★能够自动对焦,提供检测报告" + ] +}} + +### 示例输出2如下(包含嵌套结构): +{{ + "摄像机": [ + "摄像机:有效像素:≥900W像素", + "摄像机:最低照度:彩色≤0.001lx", + "协议:routes 接口开放:具备;▲支持标准 ONVIF 协议与第三方厂家设备进行互联;支持 GB/T28181;应提供 SDK" + ] +}} +""" + +user_query_template_two="""请根据货物标中采购要求部分的内容,告诉我\"{}\"的技术参数或采购要求是什么。由于该货物存在 {} 种不同的采购要求或技术参数,请逐一列出,并以 JSON 格式返回结果。请以'货物名-编号'区分多种型号,编号为从 1 开始的自然数,依次递增,即第一个键名为\"{}-1\", 键值为一个列表,列表中包含若干描述\"{}\"的技术参数(或采购要求)的字符串,请按原文内容回答,保留三角▲、五角★和序号(若有),不可擅自增添内容。 +请注意以下特殊情况: +要求与指南: +1. 如果该货物没有相关采购要求或技术参数要求,键值应为空列表。 +2. 如果存在嵌套结构,且原文为Markdown 的表格语法,如'摄像机|有效像素|≥900W像素', 请不要返回该Markdown语法,而是使用冒号':'将相关信息拼接在一起,生成一条完整且清晰的技术参数(或采购要求)描述,作为列表中的一个字符串。如"摄像机:有效像素:≥900W像素"。 +3. 字符串中的内容为具体的技术参数要求或采购要求,请不要返回诸如'(1)高清录像功能'这种标题性质且不能体现要求的内容。 +4. 你的键值应该全面,对于同一个单元格内的数据,尽量全面,不要遗漏,对于单元格内以序号分隔的各条参数要求,请分别添加进键值(即字符串列表)中。 + +### 示例输出1如下: +{{ + "交换机-1": [ + "★1、支持固化千兆电口≥8 个,固化千兆光口≥2 个,桌面型设备;", + "2、支持静态链路聚合" + ], + "交换机-2": [ + "1、交换容量≥52Gbps,包转发率≥38.69Mpps,", + "2、提供国家强制性产品认证证书及测试报告(3C)", + "★能实现信号控制独立传输" + ] +}} + +### 示例输出2如下(包含嵌套结构): +{{ + "摄像机-1": [ + "摄像机:有效像素:≥900W像素", + "摄像机:最低照度:彩色≤0.001lx", + "协议:routes 接口开放:具备;▲支持标准 ONVIF 协议与第三方厂家设备进行互联;支持 GB/T28181;应提供 SDK" + ], + "摄像机-2": [ + "支持夜视", "支持云存储" + ] +}} + """ +def test_generate_ques_two(grouped_paths): + queries=[] + for grouped_dict in grouped_paths: + for grouped_key, grouped_key_cnt in grouped_dict.items(): + # 将键中的 '.' 替换为 '下的' + modified_grouped_key = grouped_key.replace('.', '下的') + # 使用修改后的键填充第一个占位符,原始键填充第二个占位符 + # 如果需要使用 full_text,可以取消注释并提供相应的实现 + # full_text = read_txt_to_string(processed_filepath) + # new_query = user_query_template_two.format(modified_grouped_key, grouped_key, modified_grouped_key, full_text) + + # 根据您的需求,生成新的查询字符串 + new_query = user_query_template_two.format(modified_grouped_key, grouped_key_cnt,grouped_key, modified_grouped_key) + queries.append(new_query) + print(new_query) + +def test_generate_ques_one(key_paths): + queries=[] + for key in key_paths: + # 将键中的 '.' 替换为 '下的' + modified_key = key.replace('.', '下的') + # 使用修改后的键填充第一个占位符,原始键填充第二个占位符 + # full_text = read_txt_to_string(processed_filepath) + # new_query = user_query_template.format(modified_key, key, modified_key,full_text) #转豆包后取消注释 + new_query = user_query_template.format(modified_key, key, modified_key) + queries.append(new_query) + print(new_query) + +if __name__ == "__main__": + # test_generate_key_paths() + # grouped_paths=[{'fruits': 2}, {'vegetables.root': 2}, {'misc.system functions': 2}, {'misc.fruits': 3}] + # key_paths=['显示系统.LED全彩显示屏', '显示系统.控制盒及电源', '显示系统.大屏播控系统', '显示系统.配电柜(含PLC)', '显示系统.钢结构底座及铝型材支架', '显示系统.电缆及信号线缆', '显示系统.控制终端', '显示系统.50寸液晶电视机', '显示系统.50寸电视机地面推车', '显示系统.高清监视器', '摄像系统.高清摄像机', '摄像系统.摄像机三脚架', '摄像系统.摄像机壁装架', '摄像系统.摄像机控制键盘', '视频处理系统.高清视频拼控矩阵(16*16)', '视频处理系统.分量信号接口器', '视频处理系统.高清四画面分割器', '发言系统.数字会议发言主机', '发言系统.方形短杆代表话筒', '发言系统.专用连接线缆', '发言系统.手持无线话筒', '视频会议系统.多点控制器', '视频会议系统.多串口控制服务器', '视频会议系统.综合会议管理调度平台', '视频会议系统.高清会议终端(主会场)', '视频会议系统.高清会议终端(分会场)', '视频会议系统.65寸电视机移动推车(9楼)', '视频会议系统.65寸液晶电视机(分会场)', '视频会议系统.控制平板及软件', '视频会议系统.鹅颈话筒', '辅助系统.时序电源', '辅助系统.多媒体地插盒', '辅助系统.线材辅料', '辅助系统.墙体拆除及修复'] + key_paths=['显示系统.LED全彩显示屏', '显示系统.控制盒及电源', '显示系统.大屏播控系统'] + # test_generate_ques(grouped_paths) + test_generate_ques_one(key_paths) \ No newline at end of file diff --git a/flask_app/test_case/test_restructure_data.py b/flask_app/test_case/test_restructure_data.py new file mode 100644 index 0000000..5c60538 --- /dev/null +++ b/flask_app/test_case/test_restructure_data.py @@ -0,0 +1,213 @@ +from flask_app.货物标.技术参数要求提取后处理函数 import restructure_data + +#注意!这里的期待输出有误 +def test_restructure_data(): + print("开始测试 restructure_data 函数...\n") + + # 测试用例 1: 所有顶层键的值都是列表(二层结构) + data1 = { + "fruits": ["apple", "banana", "cherry"], + "vegetables": ["carrot", "lettuce", "spinach"], + "grains": ["rice", "wheat", "barley"] + } + expected1 = data1.copy() + result1 = restructure_data(data1) + print("测试用例 1: 所有顶层键的值都是列表(二层结构)") + print("输入:", data1) + print("预期输出:", expected1) + print("实际输出:", result1) + print("测试通过:", result1 == expected1, "\n") + + # 测试用例 2: 所有顶层键的值都是字典,且已经是三层结构 + data2 = { + "fruits": { + "citrus": ["orange", "lemon"], + "berries": ["strawberry", "blueberry"] + }, + "vegetables": { + "root": ["carrot", "beet"], + "leafy": ["lettuce", "spinach"] + } + } + expected2 = data2.copy() + result2 = restructure_data(data2) + print("测试用例 2: 所有顶层键的值都是字典,且已经是三层结构") + print("输入:", data2) + print("预期输出:", expected2) + print("实际输出:", result2) + print("测试通过:", result2 == expected2, "\n") + + # 测试用例 3: 混合的两层和三层结构 + data3 = { + "fruits": ["apple", "banana"], + "vegetables": { + "root": ["carrot", "beet"], + "leafy": ["lettuce", "spinach"] + }, + "grains": ["rice", "wheat"] + } + expected3 = { + "fruits": {"fruits": ["apple", "banana"]}, + "vegetables": { + "root": ["carrot", "beet"], + "leafy": ["lettuce", "spinach"] + }, + "grains": {"grains": ["rice", "wheat"]} + } + result3 = restructure_data(data3) + print("测试用例 3: 混合的两层和三层结构") + print("输入:", data3) + print("预期输出:", expected3) + print("实际输出:", result3) + print("测试通过:", result3 == expected3, "\n") + + # 测试用例 4: 超过三层嵌套 + data4 = { + "fruits": { + "citrus": { + "tropical": ["orange", "lemon"] + }, + "berries": ["strawberry", "blueberry"] + }, + "vegetables": ["carrot", "lettuce"] + } + expected4 = { + "fruits.citrus": { + "tropical": ["orange", "lemon"] + }, + "fruits.berries": { + "berries": ["strawberry", "blueberry"] + }, + "vegetables": { + "vegetables": ["carrot", "lettuce"] + } + } + result4 = restructure_data(data4) + print("测试用例 4: 超过三层嵌套") + print("输入:", data4) + print("预期输出:", expected4) + print("实际输出:", result4) + print("测试通过:", result4 == expected4, "\n") + + # 测试用例 5: 超过四层嵌套 + data5 = { + "animals": { + "mammals": { + "primates": { + "humans": ["Alice", "Bob"], + "monkeys": ["Charlie", "Dave"] + }, + "carnivores": ["Lion", "Tiger"] + }, + "birds": ["Eagle", "Parrot"] + }, + "plants": ["Oak", "Pine"] + } + expected5 = { + "animals.mammals.primates": { + "humans": ["Alice", "Bob"], + "monkeys": ["Charlie", "Dave"] + }, + "animals.mammals.carnivores": ["Lion", "Tiger"], + "animals.birds": ["Eagle", "Parrot"], + "plants": {"plants": ["Oak", "Pine"]} + } + result5 = restructure_data(data5) + print("测试用例 5: 超过四层嵌套") + print("输入:", data5) + print("预期输出:", expected5) + print("实际输出:", result5) + print("测试通过:", result5 == expected5, "\n") + + # 测试用例 6: 空字典 + data6 = {} + expected6 = {} + result6 = restructure_data(data6) + print("测试用例 6: 空字典") + print("输入:", data6) + print("预期输出:", expected6) + print("实际输出:", result6) + print("测试通过:", result6 == expected6, "\n") + + # 测试用例 7: 顶层键的值类型无效 + data7 = { + "fruits": "apple", + "vegetables": ["carrot", "lettuce"] + } + print("测试用例 7: 顶层键的值类型无效") + print("输入:", data7) + try: + restructure_data(data7) + print("预期结果: 引发 ValueError") + print("实际输出: 无异常") + print("测试通过: False\n") + except ValueError as ve: + print("预期结果: 引发 ValueError") + print("实际输出: 引发 ValueError:", ve) + print("测试通过:", "数据格式异常" in str(ve), "\n") + + # 测试用例 8: 嵌套字典中存在无效的值类型 + data8 = { + "fruits": { + "citrus": "orange", + "berries": ["strawberry", "blueberry"] + }, + "vegetables": ["carrot", "lettuce"] + } + print("测试用例 8: 嵌套字典中存在无效的值类型") + print("输入:", data8) + try: + restructure_data(data8) + print("预期结果: 引发 ValueError") + print("实际输出: 无异常") + print("测试通过: False\n") + except ValueError as ve: + print("预期结果: 引发 ValueError") + print("实际输出: 引发 ValueError:", ve) + print("测试通过:", "数据格式异常" in str(ve), "\n") + + # 测试用例 9: 复杂的嵌套结构 + data9 = { + "fruits": { + "citrus": ["orange", "lemon"], + "berries": { + "summer": ["strawberry", "blueberry"], + "winter": ["cranberry"] + } + }, + "vegetables": ["carrot", "lettuce"], + "grains": { + "whole": ["rice", "wheat"], + "refined": ["white rice"] + } + } + expected9 = { + "fruits.berries": { + "summer": ["strawberry", "blueberry"], + "winter": ["cranberry"] + }, + "fruits.citrus": { + "citrus": ["orange", "lemon"] + }, + "vegetables": { + "vegetables": ["carrot", "lettuce"] + }, + "grains.whole": { + "whole": ["rice", "wheat"] + }, + "grains.refined": { + "refined": ["white rice"] + } + } + result9 = restructure_data(data9) + print("测试用例 9: 复杂的嵌套结构") + print("输入:", data9) + print("预期输出:", expected9) + print("实际输出:", result9) + print("测试通过:", result9 == expected9, "\n") + + print("所有测试用例已完成。") + +# 运行测试 +if __name__ == "__main__": + test_restructure_data() diff --git a/flask_app/test_case/采购要求数据.py b/flask_app/test_case/采购要求数据.py new file mode 100644 index 0000000..7c576f8 --- /dev/null +++ b/flask_app/test_case/采购要求数据.py @@ -0,0 +1,342 @@ +data1={ + "采购需求": { + "显示系统": { + "★LED全彩显示屏": [], + "控制盒及电源": [], + "大屏播控系统": [], + "配电柜(含PLC)": [], + "钢结构底座及铝型材支架": [], + "电缆及信号线缆": [], + "控制终端": [], + "50寸液晶电视机": [], + "50寸电视机地面推车": [], + "高清监视器": [] + }, + "摄像系统": { + "★高清摄像机": [], + "摄像机三角架": [], + "摄像机壁装架": [], + "摄像机控制键盘": [] + }, + "视频处理系统": { + "★高清视频拼控矩阵(16*16)": [], + "分量信号接口器": [], + "高清四画面分割器": [] + }, + "发言系统": { + "数字会议发言主机": [], + "方形短杆代表话筒": [], + "专用连接线缆": [], + "手持无线话筒": [] + }, + "视频会议系统": { + "★多点控制器": [], + "★多串口控制服务器": [], + "★综合会议管理调度平台": [], + "★高清会议终端(主会场)": [], + "★高清会议终端(分会场)": [], + "65寸电视机移动推车(9楼)": [], + "65寸液晶电视机(分会场)": [], + "控制平板及软件": [], + "鹅颈话筒": [] + }, + "辅助系统": { + "时序电源": [], + "多媒体地插盒": [], + "线材辅料": [], + "墙体拆除及修复": [] + } + } +} + +data2={ + "采购需求": { + "显示系统": { + "★LED全彩显示屏": [ + "1、显示尺寸:6m±0.3(W)× 1.5m±0.2(H),单屏分辨率≥3744 × 1040;", + "2 、像素间距≤1.53mm;", + "3 、亮度≥450nits, 色温: 3000-15000K 可调, 对比度: 5000:1;", + "4 、峰值功耗≤440W, 平均功耗≤146W, 带有智能(黑屏) 节电功 能, 开启智能节电功能比没开启节能 40%以上;", + "5 、水平可视角度≥160 ° , 垂直可视角度≥140 ° ;", + "6 、亮度均匀性≥97%, 刷新率≥3840 Hz , 发光点中心距偏差<3%;", + "7 、色域覆盖率≥100%, 低亮高灰: 100%亮度时, 16 bits 灰度,20% 亮度时, 12bits 灰度;", + "8 、铝底壳材质, 无风扇散热结构;", + "9 、模组电源接口采用4P 接插头, 免工具维护, 同时有防呆设计, 预防接错电源线短路而导致的烧毁模组行为,采用集成 HUB 接收卡 控制, 支持通讯状态监控;", + "10 、冗余备份, 支持双电网供电, 当其中一路交流电网跳闸后, 另 外一路电网继续供电, 实现不间断供电, 支持热备份, 当其中一块 电源失效后, 另一块电源继续工作, 从而实现不间断供电;", + "11 、屏体发光模组采用 4.5 VDC 的安全电压供电;", + "12 、彩色信号处理位数≥16bit;", + "13 、具备故障自诊及排查功能;", + "14 、 图像有降噪 、增强 、运动补偿 、色坐标变换处理 、钝 化处理无 几何失真和线性失真现象 、消除鬼影拖尾, 无“毛毛虫 ”“鬼影 ” 跟随现象;", + "15 、防护等级符合 IP6X, 显示屏具有防潮 、防尘 、防腐蚀 、防电磁 干扰 、防静电等功能, 并具有过流、短路 、过压 、欠压保护等功能;", + "16 、工作噪音声压等级一米处≤7.8 dB (A),盐雾符合 10 级要求, PCB 阻燃等级达到 UL 94 V-0 级要求, 通过 9 级烈度地震模拟实验。" + ], + "控制盒及电源": [ + "高清发送盒 2 台 、接收卡 27 张 、 电源 26 台。" + ], + "大屏播控系统": [ + "1、具有多用户多权限管理功能,支持多用户同时登录客户端,每个用户根据自身不同权限管理显示屏;", + "2、系统对输入信号源进行预监视,实现在播控前预先查看的功能。" + ], + "配电柜(含PLC)": [ + "1 、10 KW 容量,施耐德元器件,含 PLC 控制系统,可实时获取屏幕背部烟雾及温度数据, 可执行远程开关机操作; 数字量 输入: 12 路 24 V DC 输入;数字量输出: 10 路继电输出;模拟量输 入:4 个电压 输入,4 个电流输入;通讯口: 1 个 RS232,2 个 RS485, 1 个以太网; 程序容量: 256K 片内 Flash 内存; 功能: 远程监控 、 电话监视 、温 度监控 、消防监控;", + "2、本配电柜具备过压、过流、欠压、短路、断路以及漏电保护措施。" + ], + "钢结构底座及铝型材支架": [ + "1 、主体钢架结构及定制型材;", + "2 、确保楼层承受力许可,按需加固楼层地面;", + "3 、钢结构。" + ], + "电缆及信号线缆": [ + "配套所需控制网线 、高清视频线缆 、 电源线缆等适配。" + ], + "控制终端": [ + "1 、处理器: 八核心 16 线程;", + "2 、显卡: 8G/DDR6/PCI Express 4.0 16X;", + "3 、 内存: ≥8G DDR4 内存;", + "4 、硬盘: SSD 固态硬盘(容量≥480G);", + "5 、接口: 音频/网络/HDMI 接口/9 针串口;", + "6 、显示器: 21.5 英寸。" + ], + "50寸液晶电视机": [ + "1 、屏幕尺寸: 50 英寸;", + "2 、背光类型: LED;", + "3 、屏幕分辨率: 超高清 4K (3840 ×2 160);", + "4 、支持 HDR 显示;", + "5 、接口: USB2.0 ×2 、HDMI2.0 ×3;", + "6 、 网络连接方式: 无线/网线。" + ], + "50寸电视机地面推车": [ + "1 、全钢结构, 满足 50 寸显示屏承重安装要求;", + "2 、承载: 50Kg;", + "3 、高度: 650mm;", + "4 、可以调节观看角度。" + ], + "高清监视器": [ + "1 、27 寸全屏高清监视器;", + "2 、 IPS 技术炫彩硬屏, 4K 高清分辨率;", + "3 、 内置音箱;", + "4 、亮度: 350cd/m2;", + "5 、接口: DP × 1, HDMI ×2, USB ×2, 音频/耳机 × 1。" + ] + }, + "摄像系统": { + "★高清摄像机": [ + "1 、成像器件: 1/2.8 Exmor CMOS;", + "2 、镜头: 30 倍光学(f=4.3mm to 129mm );", + "3 、水平视角: 63.7 ° ;", + "4、视频输出格式:1080P/60,1 080P/50,1080P/30,1080P/25,1080i/60, 720p/60;", + "5 、视频输出: 3G-SDI, HDMI, CVBS,IP (可同步输出 );", + "6 、真双输出: IP 和 SDI 视频格式可以独立设置;", + "7 、控制方式: RS232 / RS422 / RS485, IP/Onvif/Visca-over-IP, IP 控制软件, 红外遥控器;", + "8 、IP 最高 1080p60, 支持 H.264/H.265/MJPEG;", + "9 、支持 Tally 灯;", + "10 、支持独立 PoE+(IEEE 802.3 at) 和 DC 12V 电源;", + "11 、扩展存储: Micro SD,最高支持 128GB。" + ], + "摄像机三角架": [ + "1 、铝合金材质, 承重 2-10Kg;", + "2 、满足高清摄像机承重 、尺寸要求。" + ], + "摄像机壁装架": [ + "1 、钣金 2.0mm 优质冷轧钢板;", + "2 、满足高清摄像机壁挂安装承重 、尺寸要求;", + "3 、具有放置摄像机电源空间。" + ], + "摄像机控制键盘": [ + "1 、支持串行 RS232/RS422 和 IP 混合控制, 允许在一个控制器上使用 RS232/RS422/IP 控制单个系统中的摄像机;", + "2 、支持 2 组 RS422 串口 VISCA 协议菊花链控制 2x7 台摄像机;", + "3 、通过手柄或单独的 Seesaw 控制杆控制变焦,通过专用旋钮及按钮, 可直接设置摄像机的光圈 、快门 、增益 、 白平 衡 、 自动曝光等 级等参数, 无需通过菜单设置;", + "4 、使用 IP 控制,控制器可自动搜索系统中的 IP 摄像机, 快速分配 摄像机 IP 地址 。支持 Onvif 、CGI 、VISCA over IP;", + "5 、单个网络上不限制控制器, 控制 255 台 IP 摄像机;", + "6 、多达 256 个预置位,带有图像参数记忆功能及 8 条 轨迹记忆存储, 方便快速调用摄像机的运动 (需摄像机支持 );", + "7 、6 个可选 ASSIGN 功能键, 可以为 ASSIGN 按钮分配附加功能;", + "8 、多色按键/旋钮照明指示摄像机当前状态;", + "9 、独立双电源供电: DC 12V ,POE。" + ] + }, + "视频处理系统": { + "★高清视频拼控矩阵(16*16)": [ + "1 、8U 切换主箱体, 支持输入 13 槽, 输出 4.5 槽, 支持 8 路高分采集, 支持冗余电源, 标配 1 个电源模块; 本项目配置输入接 口 16 路和 1 张字幕卡, 输出接口 16 路;", + "2 、设备应为纯硬件 FPGA 架构, CrossPoint 全总线交换技术, 背板 等效带宽;", + "3 、单张板卡支持 4 通道输入或输出, 紧凑型机箱,模拟视频单板卡 支持 16 路同时输入, 单卡支持 2 种信号源任意组合;", + "4、输入输出板卡可热插拔,输入板卡热插拔恢复时间 <2s,输出板 卡热插拔恢复时间<8s;", + "5 、开机时间≤10s, 启动电源至输出最总画面的时间间隔;", + "6 、平均故障时间间隔 ( MTBF ) 不小于 96000 小时, 保证设备能够 稳定运行;", + "7 、最大单机背板信号处理带宽不小于 720Gbps;", + "8、对各个输入通道采用纯硬件处理技术,采用独享带宽方式为每个 输入通道分配带宽, 切换过程中对其他信号无影响, 实现了对输入 通道的实时处理;", + "9、支持集中采集 DVI、VGA、CVBS、HDBaseT、HDMI、SDI、YPbPr 、 光纤等 2K 信号,Dual-link DVI、HDMI 1.4、DisplayPort 等 4K 信号;", + "10 、支持 DVI 、HDBaseT 、HDMI 、SDI 、光纤 、CVBS 、Ypbp r 等常见的 2K 信号输出, Dual-link DVI 、HDMI 1.4 等 4K 信号输出;", + "11、设备可实现任意一路画面的任意比例缩放、漫游、 跨屏 、叠加、 开窗;", + "12 、设备支持图像无缝实时切换功能, 无缝切换时间<20 ms ;", + "13 、支持场景保存及快速调用, 支持场景轮巡, 适应于不同的应用 场景;", + "14 、支持信号源预监功能, 支持浏览所有输入信号源的实时预览画 面;", + "15 、支持大屏图像回显, 可显示整面拼接墙的显示图像;", + "16 、支持设置拼接屏的拼缝补偿, 可精确到 1 个像素;", + "17 、支持 RRTA 分辨率实时全兼容技术, 单台 设备应支持同时控制 4 组不同分辨率的大屏幕显示;", + "18 、设备具备静态底图功能, 设备支持超大分辨率底图显示, 横纵 分辨率最大 65535 像素。" + ], + "分量信号接口器": [ + "用于现有视频会议专业对接高清矩阵接口器" + ], + "高清四画面分割器": [ + "画面预览使用, 具有画中画 、独立单画面放大功能。" + ] + }, + "发言系统": { + "数字会议发言主机": [ + "1 、标准挂载单元数量: 4 路总线接口, 单路可连接 32 个 , 最多系统可挂载 128 个会议单元, 且最远线路长度可高达 100 米;", + "2、主机面板彩屏显示系统菜单,通过设置可设 定 1/2/4/6 发言数量;", + "3 、支持先入先出模式, 后入后出模式, 限制模式, 电脑/主席允许 模式, 自由讨论模式;", + "4 、可直接控制最多三个摄像球, 完成视频会议功能;", + "5、多种输入输出接口主输入、卡座输入和前置输出、辅助输出及录音输出接口;", + "6 、带有 RS 232 视频控制输出 口, 可以直接输出派尔高-P, 派尔高 -D, VISCA 控制协议, 控制最大 3 个摄像机, 完成摄像自动跟踪;", + "7 、 内置 4 切 1 视频切换器, 用于摄像机的视频接连;", + "8 、可以响应处理话筒的会议中服务的请求;", + "9 、 内置签到表决功能, 可以配合话筒进行签到表决;", + "10 、 内置 DSP 自适应音频处理器,可以最大可能的抑制声回输。" + ], + "方形短杆代表话筒": [ + "1 、超大静音开关设计;", + "2 、会议操作系统,全新的触摸操控技术, 2.8 英寸的彩色触摸屏幕;", + "3 、超短全金属短咪杆设计;", + "4 、高灵敏度咪芯设计,拾音距离≥80 cm ;", + "5 、红色雾面指示灯设计, 指示发言状态;", + "6 、支持视像跟踪;", + "7、配合主机, 可以实现先入先出,后入后出, 限制模式,主席允许模式, 自由讨论模式;", + "8、话筒的身份可以自行设定,可以通过主机设置改变话筒身份,在 代表, 主席, VIP 自由切换, 让使用更灵活多样, 满足 高端需求;", + "9 、长距离传输对音质不会有影响; 具备超强的抗手机 RF 干扰性。" + ], + "专用连接线缆": [ + "主机与话筒专用连接线缆, 长度≥30m。" + ], + "手持无线话筒": [ + "1 、含一台接收机, 两个无线手持话筒发射器;", + "2 、频率响应: 50Hz-18KHz;", + "3 、有效使用距离≥100 米;", + "4 、信噪比≥105dB(1KHz-A);", + "5 、灵敏度: -105dBm(12dB S/N AD)。" + ] + }, + "视频会议系统": { + "★多点控制器": [ + "1 、遵循 H.323 、H.320 、SIP 标准协议;", + "2 、支持 H.265, H.264 HP, H.264 编解码标准。 3 、支持不低于 25 分屏高清多画面;", + "4 、最大线路速率: 8M;", + "5、视频抗丢包能力:支持高至 60%丢包率情况下 , 图像流畅无马赛 克;音频抗 IP 网络丢包能力:支持高至 75%丢包 率情况下,声音清 晰流畅; 会议抗 IP 网络丢包能力:支持高至 70%丢包率情况下,会 议仍可正常召开。 以上 5 项参数需提供第三方检测机构检验 报告。" + ], + "★多串口控制服务器": [ + "1 、具有高速数据处理能力, 内嵌高速嵌入式 CPU ;", + "2 、提供 16 路一控多 、多控一;", + "3 、具有多种转发机制, 支持 IP 、串口间双向转发机制;", + "4、控制会议矩阵、会议摄像机外围设备串口设备,实现对会议系统设备的控制;" + ], + "★综合会议管理调度平台": [ + "1、含硬件终端和视频会议专用软件,用于控制会议、矩阵、会议摄像机,实现与省厅、随州市综合会议管理调度平台对接、融合,互联互通;", + "2、统一调度管理平台,根据业务需要,可互为控制、互为备份;", + "3、可以与原有的主控平台互为操作、实现控制备份,保证会议正常召开,需在设计方案中详细阐明如何实现;", + "4、实现对会议设备的整合控制,采用一键拖拉式操作,软件界面友好、操作管理简易、直观;", + "5、可在综合会议管理平台实现四画面预览各分会场及中心视频信号;", + "6、提供软件著作权证书。" + ], + "★高清会议终端(主会场)": [ + "1 、体系标准: ITU-T H.323 、H.320 、IETF SIP/专有平台协议;", + "2 、 网 络 传 输 协 议: TCP/IP, UDP, RTP/RTC P/ RTSP, HTTP, DHCP/PPPOE/ NAT, IGMP;", + "3 、视频编解码协议: H.265, H.264 HP, H.264 、H.2 63 、MPEG-4;", + "4 、音频编解码协议: G.711a/G.711u 、G.722/G.723/G.7 19/AAC-LC;", + "5 、视频码率: 128Kbps~ 16Mbps, 支持 16M( E1*8 );", + "6 、音频码率: 24Kbps~384Kbps;", + "7 、 内 容 分 辨 率 : 典 型 : 动 态 (HDMI/VGA) 1 080P60 , 静 态 (VGA)1600*1200@60 Up to 60fps;", + "8 、音频指标: AEC/ANS/AGC 等;", + "9 、音频特性: 唇音同步, 静音与哑音控制;", + "10 、接口指标: 遥控接口 NEC 码制标准红外接口,支持遥控器直接 控制终端;", + "11 、通信控制接 口: RJ45 × 2(RS232 x 1, RS48 5 x1), 摄像机控制 RJ45-232 接口通用路由器己置线;另预留 RS232 x 2;", + "12 、 网 络 接 口 : LAN , RJ45 x 4 , E1 (4x E1) , CC4/miniBNC x 810/100/1000Mbps2/4/6/8Mbps;", + "13、视频输出接口:3G-SDIx1、HDMIx1,DVI-Ix1,CVBSx1,VGAx1, DVI -I 可以配置选择 DVI / HDMI / VGA / YPrPb 任何一种接口模式输入 图像, 高标 、清接口可同时输出;", + "14、音频接口:音频输出输出采用“凤凰接口 ”, LINE IN, MAIN (3pin) × 1,XLR IN (3pin) × 1,XLR 用于 MIC 输入时支持,+48V 幻象供 电, LINE OUT,MAIN (3pin) × 1;", + "15 、遵循 H.323 、H.320 、SIP 协议标准;", + "16 、视频抗丢包能力: 支持高至 60%丢包率情况下, 图像流畅 、无 卡顿、无马赛克;音频抗 IP 网络丢包能力:支 持高至 75%丢包率情 况下,声音清晰流畅; 会议抗 IP 网络丢包能力:支持高至 70%丢包 率情况下, 会议可正常召开。" + ], + "★高清会议终端(分会场)": [ + "内置 1080P 高清摄像机, 12 倍光学变焦,72 度广角镜头,DVI-I 接口辅流输入,2 路 1080P@30fps 高清视频, 6M 速率,双 HDMI 接口输出,双屏双显、单屏双显、单屏三显、单屏四显 ,含终端控制软件。" + ], + "65寸电视机移动推车(9楼)": [ + "1 、全钢结构, 满足 70 寸电视承重安装要求;", + "2 、承载: 200Kg;", + "3 、轮子带自锁刹车功能。" + ], + "65寸液晶电视机(分会场)": [ + "1 、屏幕尺寸: 65 英寸; 含挂架及安装;", + "2 、背光类型: LED;", + "3 、屏幕分辨率: 超高清 4K (3840 ×2 160);", + "4 、支持 HDR 显示;", + "5 、CPU: Cortex A55 四核;", + "6 、接口: USB2.0 ×2 、HDMI2.0 ×2;", + "7 、 网络连接方式: 无线/网线。" + ], + "控制平板及软件": [ + "10.2 寸无线触摸屏, 含控制软件, 实现远程一键式控制 、视频会议调度。" + ], + "鹅颈话筒": [ + "1 、采样率: 48kHz;", + "2 、频响: 20Hz – 20kHz;", + "3 、灵敏度: 38 ±2dB;", + "4 、拾音距离: 20-50CM;含接头 、线缆, 线缆 长度≥3.5m;", + "5 、支持终端远程供电, 无需外接电源。" + ] + }, + "辅助系统": { + "时序电源": [ + "1 、具有 12 路 1KW 电源;", + "2 、具有电压表指示, 支持串口控制;", + "3 、采用触点闭合控制功能;", + "4 、具有过压 、过流保护。" + ], + "多媒体地插盒": [ + "1 、具有至少 1 路 HDMI 、 1 路电源 、2 路网络接口模块;", + "2 、采用优质接插件。" + ], + "线材辅料": [ + "采用专用线材、材料、接口、各种辅料等。" + ], + "墙体拆除及修复": [ + "对大屏安装区域墙体 、天花进行拆除及修复。" + ] + }, + "货物列表": [ + "★LED全彩显示屏", + "控制盒及电源", + "大屏播控系统", + "配电柜(含PLC)", + "钢结构底座及铝型材支架", + "电缆及信号线缆", + "控制终端", + "50寸液晶电视机", + "50寸电视机地面推车", + "高清监视器", + "★高清摄像机", + "摄像机三角架", + "摄像机壁装架", + "摄像机控制键盘", + "★高清视频拼控矩阵(16*16)", + "分量信号接口器", + "高清四画面分割器", + "数字会议发言主机", + "方形短杆代表话筒", + "专用连接线缆", + "手持无线话筒", + "★多点控制器", + "★多串口控制服务器", + "★综合会议管理调度平台", + "★高清会议终端(主会场)", + "★高清会议终端(分会场)", + "65寸电视机移动推车(9楼)", + "65寸液晶电视机(分会场)", + "控制平板及软件", + "鹅颈话筒", + "时序电源", + "多媒体地插盒", + "线材辅料", + "墙体拆除及修复" + ] + } +} diff --git a/flask_app/货物标/技术参数要求提取.py b/flask_app/货物标/技术参数要求提取.py index 102658a..e3b8fae 100644 --- a/flask_app/货物标/技术参数要求提取.py +++ b/flask_app/货物标/技术参数要求提取.py @@ -65,14 +65,14 @@ def generate_key_paths(data): # 编译用于匹配后缀的正则表达式模式 pattern = re.compile(r'(.+)-\d+$') - # 初始化结果列表 + # 初始化结果列表和字典 key_paths = [] - grouped_set = set() # 使用集合来避免重复 + grouped_counts = defaultdict(int) # 用于记录每个 grouped_path 的数量 good_list = [] def recurse(current_dict, path): """ - 递归遍历字典,处理 key_paths 和 grouped_paths,并收集 good_list。 + 递归遍历字典,处理 key_paths 和 grouped_paths,并收集 good_list 和 grouped_counts。 参数: current_dict (dict): 当前遍历的字典。 @@ -109,8 +109,9 @@ def generate_key_paths(data): # 继续递归处理 recurse(value, path + [base]) else: - # 记录分组路径,确保唯一性 - grouped_set.add('.'.join(path + [base])) + # 记录分组路径,并统计数量 + grouped_path = '.'.join(path + [base]) + grouped_counts[grouped_path] += 1 # 执行键名的重命名,同时保持原有顺序 if keys_to_rename: @@ -141,16 +142,6 @@ def generate_key_paths(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) @@ -159,7 +150,7 @@ def generate_key_paths(data): else: base = key current_path = '.'.join(path + [base]) - if current_path in grouped_set and current_path not in collected: + if current_path in grouped_counts and current_path not in collected: collected.append(current_path) value = current_dict[key] if isinstance(value, dict): @@ -167,7 +158,9 @@ def generate_key_paths(data): collected_grouped_paths = [] collect_grouped_paths(data_copy, [], collected_grouped_paths) - grouped_paths = collected_grouped_paths + + # 将 grouped_paths 转换为包含数量的字典列表 + grouped_paths = [{path: grouped_counts[path]} for path in collected_grouped_paths] return key_paths, grouped_paths, good_list, data_copy def rename_keys(data): @@ -177,7 +170,7 @@ def rename_keys(data): def rename_keys_recursive(current_dict): """ - 递归地重命名字典中的键,确保同一层级下具有相同基名的键被正确编号。 + 递归地重名字典中的键,确保同一层级下具有相同基名的键被正确编号。 """ if not isinstance(current_dict, dict): return current_dict @@ -222,13 +215,15 @@ def rename_keys(data): 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 + + # 对整个数据结构进行递归重命名 + return rename_keys_recursive(data) def combine_and_update_results(original_data, updates): + """ + 先规范化original和updates中的字典,防止空格的情况导致匹配不上无法更新 + """ def normalize_key(key): """ 规范化键名: @@ -364,6 +359,9 @@ def get_technical_requirements(file_path,invalid_path,processed_filepath): 5. 系统功能:若采购的某系统提及总体系统功能,则在系统值中添加'系统功能'二级键,不展开具体内容。 6. 完整性:确保不遗漏系统内的货物,也不添加未提及的内容。若'采购清单'中未提取的货物(或系统)名称在形如'主要设备功能指标'的标题下有详细参数指标要求,请将该货物名也添加至返回中。 +特殊情况: +若采购的货物或系统或模块名称前存在三角▲、五角★,注意是名称前而非具体的技术参数或采购要求前,在返回名称时请保留前面的▲或★符号,如'★高清摄像机'。 + 输出格式: 1.JSON格式,最外层键名为'采购需求'。 2.层次关系用嵌套键值对表示。 @@ -428,8 +426,8 @@ def get_technical_requirements(file_path,invalid_path,processed_filepath): processed_data=truncate_system_keys(cleaned_res['采购需求']) key_paths, grouped_paths, good_list, data_copy= generate_key_paths(processed_data) # 提取需要采购的货物清单 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像素"。 @@ -454,8 +452,8 @@ def get_technical_requirements(file_path,invalid_path,processed_filepath): ] }} """ - user_query_template_two="""请根据货物标中采购要求部分的内容,告诉我\"{}\"的技术参数或采购要求是什么。由于该货物存在多种不同的采购要求或技术参数,请逐一列出,并以 JSON 格式返回结果。请以'货物名-编号'区分多种型号,编号为从 1 开始的自然数,依次递增,即第一个键名为\"{}-1\", 键值为一个列表,列表中包含若干描述\"{}\"的技术参数(或采购要求)的字符串,请按原文内容回答,保留三角▲、五角★和序号(若有),不可擅自增添内容。 -请注意以下特殊情况: + user_query_template_two="""请根据货物标中采购要求部分的内容,告诉我\"{}\"的技术参数或采购要求是什么。由于该货物存在 {} 种不同的采购要求或技术参数,请逐一列出,并以 JSON 格式返回结果。请以'货物名-编号'区分多种型号,编号为从 1 开始的自然数,依次递增,即第一个键名为\"{}-1\";键值为一个列表,列表中包含若干描述\"{}\"的技术参数(或采购要求)的字符串,请按原文内容回答,保留三角▲、五角★和序号(若有),不可擅自增删内容,尤其是不可擅自添加序号。 + 要求与指南: 1. 如果该货物没有相关采购要求或技术参数要求,键值应为空列表。 2. 如果存在嵌套结构,且原文为Markdown 的表格语法,如'摄像机|有效像素|≥900W像素', 请不要返回该Markdown语法,而是使用冒号':'将相关信息拼接在一起,生成一条完整且清晰的技术参数(或采购要求)描述,作为列表中的一个字符串。如"摄像机:有效像素:≥900W像素"。 @@ -498,14 +496,17 @@ def get_technical_requirements(file_path,invalid_path,processed_filepath): queries.append(new_query) # 处理 grouped_paths 中的项,应用 user_query_template_two - for grouped_key in grouped_paths: - # 将键中的 '.' 替换为 '下的' - modified_grouped_key = grouped_key.replace('.', '下的') - # 使用修改后的键填充第一个占位符,原始键填充第二个占位符 - # full_text = read_txt_to_string(processed_filepath) - # new_query = user_query_template_two.format(modified_grouped_key, grouped_key, modified_grouped_key, full_text) - new_query = user_query_template_two.format(modified_grouped_key, grouped_key, modified_grouped_key) - queries.append(new_query) + for grouped_dict in grouped_paths: + for grouped_key, grouped_key_cnt in grouped_dict.items(): + # 将键中的 '.' 替换为 '下的' + modified_grouped_key = grouped_key.replace('.', '下的') + # 使用修改后的键填充第一个占位符,原始键填充第二个占位符 + # 如果需要使用 full_text,可以取消注释并提供相应的实现 + # full_text = read_txt_to_string(processed_filepath) + # new_query = user_query_template_two.format(modified_grouped_key, grouped_key, modified_grouped_key, full_text) + # 根据您的需求,生成新的查询字符串 + new_query = user_query_template_two.format(modified_grouped_key, grouped_key_cnt,grouped_key, modified_grouped_key) + queries.append(new_query) results = multi_threading(queries, "", file_id, 2) #通义 # results = multi_threading(queries, "", "", 3) #豆包 technical_requirements = [] @@ -553,8 +554,8 @@ def test_all_files_in_folder(input_folder, output_folder): if __name__ == "__main__": start_time=time.time() # truncate_file="C:\\Users\\Administrator\\Desktop\\fsdownload\\469d2aee-9024-4993-896e-2ac7322d41b7\\ztbfile_procurement.docx" - truncate_docfile=r"C:\Users\Administrator\Desktop\货物标\output1\招标文件(107国道)_procurement.docx" - truncate_file=r'C:\Users\Administrator\Desktop\fsdownload\e702f1e6-095d-443d-bb7d-ef2e42037cb1\ztbfile_procurement.pdf' + truncate_docfile=r"C:\Users\Administrator\Desktop\货物标\output1\6_2定版视频会议磋商文件_procurement.docx" + truncate_file=r'C:\Users\Administrator\Desktop\货物标\output1\6.2定版视频会议磋商文件_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" diff --git a/flask_app/货物标/技术参数要求提取后处理函数.py b/flask_app/货物标/技术参数要求提取后处理函数.py index 7f6fe61..4b72567 100644 --- a/flask_app/货物标/技术参数要求提取后处理函数.py +++ b/flask_app/货物标/技术参数要求提取后处理函数.py @@ -133,6 +133,14 @@ def restructure_data(data): - 如果所有顶层键的值都是列表(两层结构),直接返回原数据。 - 如果存在混合的两层和三层结构,或更深层级,则将所有数据统一为三层结构。 """ + def get_max_depth(d, current_depth=1): + """ + 计算字典的最大嵌套深度。 + """ + if not isinstance(d, dict) or not d: + return current_depth + return max(get_max_depth(v, current_depth + 1) for v in d.values()) + # 检查是否所有顶层键的值都是列表(即两层结构) all_two_layers = all(isinstance(value, list) for value in data.values()) @@ -150,7 +158,7 @@ def restructure_data(data): # 如果存在更深层级,展开至三层 for sub_key, sub_value in value.items(): if isinstance(sub_value, dict): - # 这里假设需要展开到三层,将其保留为三层 + # 保留为三层 structured_data[sub_key] = sub_value elif isinstance(sub_value, list): # 将两层结构转换为三层结构 @@ -165,7 +173,14 @@ def restructure_data(data): structured_data[key] = {key: value} else: raise ValueError(f"键'{key}'的数据格式异常: {type(value)}") - return structured_data + + # 检查重构后的数据深度 + max_depth = get_max_depth(structured_data) + if max_depth > 3: + # 递归调用以进一步重构 + return restructure_data(structured_data) + else: + return structured_data # 定义获取所有以':'结尾的前缀的函数 @@ -202,7 +217,7 @@ def remove_common_prefixes(string_list): else: new_string_list.append(s) return new_string_list -#TODO:目前对于超过四层的数据,无法平坦化为3层 + if __name__ == "__main__": # 示例数据 sample_data = { diff --git a/flask_app/货物标/提取采购需求main.py b/flask_app/货物标/提取采购需求main.py index c846c0c..360dab8 100644 --- a/flask_app/货物标/提取采购需求main.py +++ b/flask_app/货物标/提取采购需求main.py @@ -62,7 +62,6 @@ def fetch_procurement_reqs(procurement_path, invalid_path): # TODO:技术要求可以在技术参数之后执行,把完整的技术参数输入,问大模型,除了上述内容还有哪些,这样的话把技术标和其他的区分开。 -# TODO:0362 if __name__ == "__main__": start_time = time.time()