diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 4cccfe3..6ee30e3 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -11,6 +11,8 @@ + + \ No newline at end of file diff --git a/flask_app/general/little_zbparse.py b/flask_app/general/little_zbparse.py index cfd56c3..0f74af8 100644 --- a/flask_app/general/little_zbparse.py +++ b/flask_app/general/little_zbparse.py @@ -36,7 +36,7 @@ def little_parse_goods(output_folder, file_path): dict: 包含 '基础信息' 的字典。 """ # 截取特定的货物 PDF 文件 - selections = [4, 5] # 仅处理 selection 4 和 5 + selections = [4, 1] # 仅处理 selection 1和4 #公告+投标人须知 files = truncate_pdf_specific_goods(file_path, output_folder,selections) if not files: raise ValueError("未找到截取后的文件。") diff --git a/flask_app/testdir/插入位置.json b/flask_app/general/static/插入位置.json similarity index 92% rename from flask_app/testdir/插入位置.json rename to flask_app/general/static/插入位置.json index cac22cf..b482f23 100644 --- a/flask_app/testdir/插入位置.json +++ b/flask_app/general/static/插入位置.json @@ -36,7 +36,8 @@ ], "关键字": [ "法定代表", - "法人代表" + "法人代表", + "法定代表人身份证" ] }, "法定代表人授权人身份证": { @@ -93,9 +94,8 @@ "供应商认为有必要提供的其他资料及方案" ], "关键字": [ - "实力", - "资质证书", - "证明材料", + "企业实力", + "企业资质证书", "安全生产许可证", "认证证书" ] @@ -110,14 +110,14 @@ "企业经营业绩" ], "关键字": [ - "业绩", + "企业业绩", "类似项目", "案例", "项目情况", "施工项目" ] }, - "财务审计报告": { + "财务信息(财务审计报告)": { "章节": [ "提供依法缴纳税收和社会保障资金的相关材料", "具有依法缴纳税收和社会保障资金的良好记录", @@ -131,12 +131,10 @@ "关键字": [ "财务报告", "财务状况", - "资格审查资料", - "资信证明", "财务会计报表" ] }, - "缴纳税收证明": { + "财务信息(缴纳税收证明)": { "章节": [ "提供依法缴纳税收和社会保障资金的相关材料", "具有依法缴纳税收和社会保障资金的良好记录", @@ -150,11 +148,10 @@ ], "关键字": [ "缴纳税收", - "用户评审的证明材料", - "资格审查资料" + "税收缴纳证明" ] }, - "缴纳社保证明": { + "财务信息(公司缴纳社保证明)": { "章节": [ "投标人资质证明文件", "按照“供应商资格要求”规定提交的相关证明材料", @@ -165,7 +162,9 @@ ], "关键字": [ "社会保障资金", - "资格审查资料" + "社保资料", + "社会保险证明", + "社保证明" ] } } \ No newline at end of file diff --git a/flask_app/general/判断截取位置.py b/flask_app/general/判断截取位置.py index df9365e..4c52ff4 100644 --- a/flask_app/general/判断截取位置.py +++ b/flask_app/general/判断截取位置.py @@ -1,76 +1,172 @@ # -*- encoding:utf-8 -*- +import ast import json -from flask_app.general.json_utils import clean_json_string -from flask_app.general.多线程提问 import multi_threading,read_questions_from_file -from flask_app.general.通义千问long import upload_file,qianwen_long +import re +import os +import sys +from flask_app.general.多线程提问 import multi_threading +from flask_app.general.通义千问long import upload_file -# 从json文件中读取数据 -with open('../testdir/test.json', 'r', encoding='utf-8') as f: - data_dict = json.load(f) -# 定义目标名称列表 -target_names = [ - "营业执照", - # "开户信息", - "法定代表人身份证", - # "法定代表人授权人身份证", - "人员证书", - "人员社保资料", - # "劳动合同", - "企业证书", - "企业业绩", - "财务审计报告", - "缴纳税收证明", - "公司缴纳社保证明" -] - -# 定义user_query模板 -def generate_user_query(target, chapters, keywords): - #章节名格式通常是如'三、联合体协议书'这样的序号+标题。现在我需要将{target}贴在该章节的最后面,但是在下一章之前,目前我需要定位到插入的位置, - template = f"""这是投标文件模板,作为投标人,我需要把不同的投标材料填充到对应位置,请你根据该文件回答:{target}应该插入在该文件哪块地方?你可能需要查找以下关键词出现的地方:{', '.join([f"'{chapter}'" for chapter in chapters])},并确认插入的小节,然后在该小节的末尾内容之后插入{target},请你返回给我插入位置的上下文内容,上文为该小节的末尾内容,下文为下一小节的开头内容,上下文字数请限制在20字以内(即你只需返回最末尾的内容以及最开头的内容,不需考虑语义)。你的回答以json格式返回,外层键名为'{target}',嵌套键名分别是'上文','下文',上下文内容应完全与原文保持一致,不得擅自删减总结,若插入位置不明确,那么嵌套键名'上文'和'下文'的键值为'未知',输出格式示例如下: -{{ -{target}:{{ -"上文":"上文测试投标人: (盖单位章) - 年 月 日", -"下文":"下文测试章节名 -(招标人名称):测试" -}} -}} -""" - template2=f"""该文件为投标文件格式要求,请你根据该招标文件回答:{target}应该附在哪个地方?你可能需要查找以下章节出现的地方:{', '.join([f"'{chapter}'" for chapter in chapters])};或者可能匹配的关键字:{', '.join([f"'{kw}'" for kw in keywords])},并确定所在章节。我需要将{target}贴在该章节的最后面,目前我需要定位到插入的位置,请你返回给我插入位置的上下文,下文应该是下一章的章节名或开头内容,字数限制在30字以内,以json格式返回,键名分别是'上文','下文',上下文格式内容应完全与原文保持一致,不得擅自删减总结,示例输出如下: - {{ -"上文":"上文相关内容 -测试", -"下文":"四、下文章节名 -(招标人名称):测试" -}} +def load_data(json_path): """ - return template + 从指定的JSON文件中加载数据。 -# 生成user_query_list -user_query_list = [] + Args: + json_path (str): JSON文件的路径。 -for target in target_names: - if target in data_dict: - chapters = data_dict[target]["章节"] - keywords = data_dict[target]["关键字"] - user_query = generate_user_query(target, chapters, keywords) - user_query_list.append({ - "target": target, - "query": user_query - }) - else: - print(f"警告:'{target}'未在数据字典中找到相关信息。") + Returns: + dict: 加载的JSON数据字典。 + """ + try: + with open(json_path, 'r', encoding='utf-8') as f: + data = json.load(f) + return data + except FileNotFoundError: + print(f"错误:文件未找到 - {json_path}") + sys.exit(1) + except json.JSONDecodeError as e: + print(f"错误:解析JSON文件时出错 - {e}") + sys.exit(1) -#TODO:先对文件打标记, -# 将生成的查询添加到queries列表 -queries = [item['query'] for item in user_query_list] -truncate_file="C:\\Users\\Administrator\\Desktop\\招标文件\\招标test文件夹\\zbtest19\\zbtest19_214-320.pdf" -# 上传文件并获取file_id -file_id = upload_file(truncate_file) -# 使用multi_threading并行处理查询 -results = multi_threading(queries, "", file_id, 2) -# 清理返回结果并输出 -baseinfo_list = [clean_json_string(res) for _, res in results] if results else [] -for i in baseinfo_list: - print(json.dumps(i, ensure_ascii=False, indent=4)) + +def define_target_names(): + """ + 定义目标名称列表。 + + Returns: + list: 目标名称列表。 + """ + return [ + "营业执照", + # "开户信息", + "法定代表人身份证", + # "法定代表人授权人身份证", + "人员证书", + "人员社保资料", + # "劳动合同", + "企业证书", + "企业业绩", + "财务信息(财务审计报告)", + "财务信息(缴纳税收证明)", + "财务信息(公司缴纳社保证明)" + ] + + +def generate_user_query(target, chapters, keywords): + """ + 根据目标、章节和关键词生成用户查询模板。 + + Args: + target (str): 目标名称。 + chapters (list): 相关章节列表。 + keywords (list): 相关关键词列表。 + + Returns: + str: 生成的用户查询字符串。 + """ + template3 = f"""这是投标文件模板,作为投标人,我需要把不同的投标材料填充到对应位置,请你根据该文件回答:{target}应该插入在该文件哪个地方?你可能需要查找以下关键词出现的地方:{', '.join([f"'{kw}'" for kw in keywords])},并确认插入的位置。我已在原文中打上若干待插入位置的标记,形如'[$$第17个可插入位置$$]',它的标记与它上面的小节内容关联。你需要返回给我{target}应该插入位置的标记序号,即'[$$第17个可插入位置$$]'中的'17',而不是页码,若有多个位置需要插入,可以返回多个序号,你的回答以数组返回,如[17, 19],若插入位置不明确,那么返回[-1]。 +""" + return template3 + + +def generate_user_queries(target_names, data_dict): + """ + 为每个目标生成对应的用户查询。 + + Args: + target_names (list): 目标名称列表。 + data_dict (dict): 数据字典。 + + Returns: + list: 包含目标和查询的字典列表。 + """ + user_queries = [] + for target in target_names: + if target in data_dict: + chapters = data_dict[target].get("章节", []) + keywords = data_dict[target].get("关键字", []) + query = generate_user_query(target, chapters, keywords) + user_queries.append({ + "target": target, + "query": query + }) + else: + print(f"警告:'{target}'未在数据字典中找到相关信息。") + return user_queries + + +def process_string_list(string_list): + """ + 处理字符串列表,提取方括号内的内容并转换为实际列表。 + + Args: + string_list (str): 包含方括号的字符串。 + + Returns: + list: 解析后的列表内容。 + """ + match = re.search(r'\[(.*?)\]', string_list) + if match: + content_inside = match.group(1).strip() + if content_inside: + items = [item.strip() for item in content_inside.split(',')] + if all(item.isdigit() for item in items): + formatted_list = [int(item) for item in items] + else: + formatted_list = items + return formatted_list + return [] + + +def main(): + # 定义JSON文件路径 + # json_path = "flask_app/general/static/插入位置.json" + json_path = "D:\\flask_project\\flask_app\\general\\static\\插入位置.json" + # 加载数据 + data_dict = load_data(json_path) + + # 定义目标名称 + target_names = define_target_names() + + # 生成用户查询列表 + user_query_list = generate_user_queries(target_names, data_dict) + + if not user_query_list: + print("没有生成任何用户查询。") + sys.exit(0) + + # 提取查询 + queries = [item['query'] for item in user_query_list] + + # 定义文件路径 + format_part = "C:\\Users\\Administrator\\Desktop\\outzb2 (2).pdf" + + # 检查文件是否存在 + if not os.path.isfile(format_part): + print(f"错误:文件未找到 - {format_part}") + sys.exit(1) + + # 上传文件并获取file_id + file_id = upload_file(format_part) + + if not file_id: + print("错误:文件上传失败。") + sys.exit(1) + + # 使用多线程并行处理查询 + results = multi_threading(queries, "", file_id,2) + + if not results: + print("错误:未收到任何处理结果。") + sys.exit(1) + + # 清理返回结果 + baseinfo_list = [process_string_list(res) for _, res in results] + + # 输出结果 + for info in baseinfo_list: + print(f'{target_names}:{info}') + +if __name__ == "__main__": + main() diff --git a/flask_app/general/读取文件/按页读取pdf.py b/flask_app/general/读取文件/按页读取pdf.py index 92462af..d6b82f5 100644 --- a/flask_app/general/读取文件/按页读取pdf.py +++ b/flask_app/general/读取文件/按页读取pdf.py @@ -153,7 +153,7 @@ if __name__ == '__main__': # file_path = 'C:\\Users\\Administrator\\Desktop\\货物标\\output4\\2-招标文件(2020年广水市中小学教师办公电脑系统及多媒体“班班通”设备采购安装项目)_tobidders_notice_part2.pdf' # file_path = 'C:\\Users\\Administrator\\Desktop\\货物标\\output4\\磋商文件_tobidders_notice_part2.pdf' # file_path = 'C:\\Users\\Administrator\\Desktop\\货物标\\截取test\\交警支队机动车查验监管系统项目采购_tobidders_notice_part1.pdf' - file_path="C:\\Users\\Administrator\\Desktop\\招标文件\\招标test文件夹\\\zbtest12.pdf" + file_path="C:\\Users\\Administrator\\Desktop\\fsdownload\\68549b0b-e892-41a9-897c-c3694535ee61\\ztbfile.pdf" # ress = extract_common_header(file_path) # print(ress) res=extract_text_by_page(file_path) diff --git a/flask_app/main/start_up.py b/flask_app/main/start_up.py index bf8dcce..4f690e8 100644 --- a/flask_app/main/start_up.py +++ b/flask_app/main/start_up.py @@ -387,6 +387,7 @@ def process_and_stream(file_url, zb_type): yield f"data: {json.dumps(response, ensure_ascii=False)}\n\n" + logger.info("开始后处理:保存文件+发送提取之后的数据") # **保存 combined_data 到 output_folder 下的 'final_result.json'** output_json_path = os.path.join(output_folder, 'final_result.json') extracted_info_path=os.path.join(output_folder, 'extracted_result.json') diff --git a/flask_app/main/工程标解析main.py b/flask_app/main/工程标解析main.py index d2b64db..e424955 100644 --- a/flask_app/main/工程标解析main.py +++ b/flask_app/main/工程标解析main.py @@ -26,7 +26,7 @@ logger=None # 创建全局线程池 executor = ThreadPoolExecutor() -def preprocess_files(output_folder, downloaded_file_path, file_type): +def preprocess_files(output_folder, downloaded_file_path, file_type,unique_id): logger.info("starting 文件预处理...") logger.info("output_folder..." + output_folder) @@ -45,7 +45,7 @@ def preprocess_files(output_folder, downloaded_file_path, file_type): return None # 调用截取PDF多次 - truncate_files = truncate_pdf_multiple(pdf_path, output_folder) + truncate_files = truncate_pdf_multiple(pdf_path, output_folder,unique_id) # 处理各个部分 truncate0_docpath = pdf2docx(truncate_files[0]) # 投标人须知前附表转docx diff --git a/flask_app/main/截取pdf.py b/flask_app/main/截取pdf.py index 998af4d..a88a97c 100644 --- a/flask_app/main/截取pdf.py +++ b/flask_app/main/截取pdf.py @@ -3,6 +3,14 @@ import re # 导入正则表达式库 import os # 用于文件和文件夹操作 from flask_app.general.merge_pdfs import merge_pdfs import concurrent.futures +import logging +def get_global_logger(unique_id): + if unique_id is None: + return logging.getLogger() # 获取默认的日志器 + logger = logging.getLogger(unique_id) + return logger + +logger=None def clean_page_content(text, common_header): # 首先删除抬头公共部分 if common_header: # 确保有公共抬头才进行替换 @@ -392,7 +400,7 @@ def merge_selected_pdfs(output_folder, truncate_files, output_path, base_file_na return "" -def truncate_pdf_multiple(input_path, output_folder): +def truncate_pdf_multiple(input_path, output_folder, unique_id="123"): """ 处理 PDF 文件,选择 selection 1-6 的部分,并合并结果。 @@ -403,6 +411,8 @@ def truncate_pdf_multiple(input_path, output_folder): Returns: list: 截取的文件路径列表,包括合并后的文件路径(如果有)。 """ + global logger + logger = get_global_logger(unique_id) base_file_name = os.path.splitext(os.path.basename(input_path))[0] # 纯文件名 truncate_files = [] selections = range(1, 7) # 选择 1 到 6 @@ -422,9 +432,10 @@ def truncate_pdf_multiple(input_path, output_folder): valid_files = [f for f in files if f] truncate_files.extend(valid_files) else: + logger.error(f"Selection {selection}: 截取失败,已添加空字符串。") truncate_files.append("") # 截取失败时添加空字符串 except Exception as e: - print(f"Selection {selection} 生成了一个异常: {e}") + logger.error(f"Selection {selection} 生成了一个异常: {e}") truncate_files.append("") # 发生异常时添加空字符串 if any(f for f in truncate_files if f): # 检查是否有有效的文件路径 @@ -432,18 +443,18 @@ def truncate_pdf_multiple(input_path, output_folder): merged_result = merge_selected_pdfs(output_folder, truncate_files, merged_output_path, base_file_name) if merged_result: truncate_files.append(merged_result) - print(f"已生成合并文件: {merged_output_path}") + logger.info(f"merged_baseinfo: 已生成合并文件: {merged_output_path}") else: truncate_files.append("") # 如果merged_result未生成,添加空字符串 - print("未生成合并文件,因为没有找到需要合并的 PDF 文件。") + logger.warning("merged_baseinfo: 未生成合并文件,因为没有找到需要合并的 PDF 文件。") else: truncate_files.append("") # 如果没有文件需要合并,也添加空字符串 - print(f"没有文件需要合并 for {input_path}") + logger.warning(f"merged_baseinfo: 没有文件需要合并 for {input_path}") return truncate_files -def truncate_pdf_specific_engineering(pdf_path, output_folder, selections): +def truncate_pdf_specific_engineering(pdf_path, output_folder, selections, unique_id="123"): """ 处理 PDF 文件,选择指定的 selections,并合并结果。 @@ -451,11 +462,14 @@ def truncate_pdf_specific_engineering(pdf_path, output_folder, selections): pdf_path (str): 要处理的 PDF 文件路径。 output_folder (str): 截取后的文件保存文件夹路径。 selections (list): 需要截取的部分(例如 [4, 5])。 + unique_id (str): 用于日志记录的唯一标识符。 Returns: list: 截取的文件路径列表,包括合并后的文件路径(如果有)。 """ try: + global logger + logger = get_global_logger(unique_id) base_file_name = os.path.splitext(os.path.basename(pdf_path))[0] truncate_files = [] @@ -474,10 +488,10 @@ def truncate_pdf_specific_engineering(pdf_path, output_folder, selections): valid_files = [f for f in files if f] truncate_files.extend(valid_files) else: + logger.error(f"Selection {selection}: 截取失败,已添加空字符串。") truncate_files.append("") # 截取失败时添加空字符串 - print(f"截取 selection {selection} 失败,已添加空字符串。") except Exception as e: - print(f"Selection {selection} 生成了一个异常: {e}") + logger.error(f"Selection {selection} 生成了一个异常: {e}") truncate_files.append("") # 发生异常时添加空字符串 if any(f for f in truncate_files if f): # 检查是否有有效的文件路径 @@ -485,26 +499,27 @@ def truncate_pdf_specific_engineering(pdf_path, output_folder, selections): merged_result = merge_selected_pdfs(output_folder, truncate_files, merged_output_path, base_file_name) if merged_result: truncate_files.append(merged_result) - print(f"已生成合并文件: {merged_output_path}") + logger.info(f"merged_specific: 已生成合并文件: {merged_output_path}") else: truncate_files.append("") # 如果 merged_result 未生成,添加空字符串 - print("未生成合并文件,因为没有找到需要合并的 PDF 文件。") + logger.warning("merged_specific: 未生成合并文件,因为没有找到需要合并的 PDF 文件。") else: truncate_files.append("") # 如果没有文件需要合并,也添加空字符串 - print(f"没有文件需要合并 for {pdf_path}") + logger.warning(f"merged_specific: 没有文件需要合并 for {pdf_path}") return truncate_files except Exception as e: - print(f"Error in truncate_pdf_specific_engineering: {e}") - return [] # 返回空列表表示失败 + # 假设 selections 的长度是已知的,可以通过传递参数或其他方式获取 + logger.error(f"Error in truncate_pdf_specific_engineering: {e}") + return [""] * len(selections) # 返回与 selections 数量相同的空字符串列表 # TODO:需要完善二次请求。目前invalid一定能返回 前附表 须知正文如果为空的话要额外处理一下,比如说就不进行跳转(见xx表) 开评定标这里也要考虑 如果评分表为空,也要处理。 #TODO:zbtest8 zbtest18有问题 后期需要完善,截取需要截两次,第一次严格第二次宽松 if __name__ == "__main__": - input_path = "C:\\Users\\Administrator\\Desktop\\招标文件\\招标test文件夹\\zbtest1.pdf" # 可以是单个PDF文件路径或文件夹路径 - # input_path="C:\\Users\\Administrator\\Desktop\\fsdownload\\e378e002-45ff-440d-a65a-9974b5015472\\ztbfile.pdf" + input_path = "C:\\Users\\Administrator\\Desktop\\招标文件\\招标test文件夹\\zbtest4_evaluation_method.pdf" # 可以是单个PDF文件路径或文件夹路径 + # input_path="C:\\Users\\Administrator\\Desktop\\fsdownload\\68549b0b-e892-41a9-897c-c3694535ee61\\ztbfile.pdf" # input_path = "C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles\\2-招标文件.pdf" output_folder="C:\\Users\\Administrator\\Desktop\\招标文件\\output6" files=truncate_pdf_multiple(input_path,output_folder) diff --git a/flask_app/testdir/test3.py b/flask_app/testdir/test3.py index abfedcb..e82dfbe 100644 --- a/flask_app/testdir/test3.py +++ b/flask_app/testdir/test3.py @@ -1,228 +1,480 @@ -import json -import re -from collections import defaultdict - - -def process_data_based_on_key(data): - exclude_word = ["包", "未知", "评分因素"] - # 获取字典的键列表 - keys = list(data.keys()) - # 检查键的数量是否为1并且 exclude_word 中的任何词包含在 keys[0] 中 - if len(keys) == 1 and any(word in keys[0] for word in exclude_word): - # 返回内层的字典 - return data[keys[0]] - - # 如果条件不满足,则返回原始字典 - return data - -def parse_json_with_duplicates(raw_string): +def post_process_baseinfo(base_info): """ - 解析具有重复键的 JSON 字符串,将所有重复的键值对存储为列表。 + 在 'base_info' 任务完成后执行的函数。 + 确保在缺少某些键时,返回 good_list=[]。 - Args: - json_string (str): 需要解析的 JSON 字符串。 + 参数: + - base_info (dict): 原始的 base_info 数据。 - Returns: - dict: 解析后的字典,重复的键对应的值为列表。 + 返回: + - tuple: (处理后的 base_info, good_list) + """ + try: + pure_base_info = base_info.get("基础信息", {}) + # 尝试提取 '货物列表',若中间某个键不存在,返回 good_list=[] + procurement_reqs = pure_base_info.get('采购要求', {}) + technical_requirements = procurement_reqs.get('技术要求', {}) + good_list = technical_requirements.pop('货物列表', []) # 如果 '货物列表' 不存在,返回 [] - eg:输入:"综合实力": { - "评分": "2分", - "要求": "投标人具备电子与智能化工程专业承包二级资质及以上证书得 2分,不能够提供不得分(开标时需提供原件)。" + # 删除 '货物列表' 后更新原始的 base_info + if '技术要求' in procurement_reqs and not technical_requirements: + # 若技术要求为空,删除该键 + procurement_reqs.pop('技术要求') + + if '采购要求' in pure_base_info and not procurement_reqs: + # 若采购要求为空,删除该键 + pure_base_info.pop('采购要求') + + # 更新基础信息 + base_info['基础信息'] = pure_base_info + + return base_info, good_list + except Exception as e: + return base_info, [] # 返回空列表 +data={ + "基础信息": { + "招标人/代理信息": { + "招标人": "广水市公路管理局", + "招标人联系方式": { + "名称": "广水市公路管理局", + "联系电话": "17362698785", + "地址": "随州市广水市四贤路 7号应山办事处南门" }, - "综合实力": { - "评分": "2分", - "要求": "投标人具有建筑机电安装工程专业承包三级资质或以上资质得 2分,否则不得分。(证书开标原件备查)。" + "招标代理机构": "湖北众恒永业工程项目管理有限公司广水分公司", + "招标代理机构联系方式": { + "名称": "湖北众恒永业工程项目管理有限公司广水分公司", + "联系电话": "13997896775", + "地址": "广水市名都花园 22栋" + }, + "项目联系方式": { + "名称": "闵杨、喻亚", + "联系电话": "13997896775、15337331616" } - 输出:"综合实力": [ - { - "评分": "2分", - "要求": "投标人具备电子与智能化工程专业承包二级资质及以上证书得 2分,不能够提供不得分(开标时需提供原件)。" - }, - { - "评分": "2分", - "要求": "投标人具有建筑机电安装工程专业承包三级资质或以上资质得 2分,否则不得分。(证书开标原件备查)。" - }] - """ - - def custom_object_pairs_hook(pairs): - d = defaultdict(list) - for key, value in pairs: - # 如果值是字典或列表,递归处理 - if isinstance(value, dict): - value = process_dict(value) - elif isinstance(value, list): - value = process_list(value) - d[key].append(value) - # 将有多个值的键转换为列表,单个值的键保持原样 - return {key: (values if len(values) > 1 else values[0]) for key, values in d.items()} - - def process_dict(d): - """ - 递归处理字典,确保所有重复键的值为列表。 - - Args: - d (dict): 需要处理的字典。 - - Returns: - dict: 处理后的字典。 - """ - return custom_object_pairs_hook(d.items()) - - def process_list(l): - """ - 递归处理列表,确保列表中的所有字典也被处理。 - - Args: - l (list): 需要处理的列表。 - - Returns: - list: 处理后的列表。 - """ - return [process_dict(item) if isinstance(item, dict) else item for item in l] - - """输入字符串,提取 { 和 } 之间的内容,并将其解析为字典""" - if not raw_string.strip(): - return {} - match = re.search(r'\{[\s\S]*\}', raw_string) - if match: - try: - json_string = match.group(0) - return json.loads(json_string, object_pairs_hook=custom_object_pairs_hook) - except json.JSONDecodeError as e: - print(f"json_utils: extract_content_from_json: JSON decode error: {e}") - return {} - else: - print("json_utils: extract_content_from_json: No valid JSON content found.") - return {} - -def combine_technical_and_business(data, target_values): - extracted_data = {} # 根级别存储所有数据 - technical_found = False - business_found = False - - def extract_nested(data, parent_key='', is_technical=False, is_business=False): - nonlocal technical_found, business_found - if isinstance(data, dict): - for key, value in data.items(): - current_key = f"{parent_key}.{key}" if parent_key else key - - # 检查是否为技术标的内容 - if any(target in key for target in target_values): - if not is_technical: - extracted_data[key] = value - technical_found = True - continue - - # 默认其他所有内容都归为商务标 - else: - if not is_business: - if '商务评分' not in extracted_data: - extracted_data['商务评分'] = {} - extracted_data['商务评分'][key] = value - business_found = True - continue - - if isinstance(value, dict) or isinstance(value, list): - extract_nested(value, current_key, is_technical, is_business) - - elif isinstance(data, list): - for index, item in enumerate(data): - extract_nested(item, f"{parent_key}[{index}]", is_technical, is_business) - - extract_nested(data) - - if not technical_found: - extracted_data['技术评分'] = '' - if not business_found: - extracted_data['商务评分'] = '' - - return extracted_data -raw_data=""" -{ - "一包": { - "技术评分": { - "主要监理岗位的职责": { - "评分": "4分", - "要求": "1、总监理工程师的职责全面、清晰、合理得 1.2-2分;一般的1.2分。2、其他主要监理人员及岗位的职责全面、清晰、合理得 1.2-2分;一般的 1.2分。" + }, + "项目信息": { + "项目名称": "107国道交通信号灯及安全防护设施采购项目", + "项目编号": "HBZHYY-2021-001", + "项目概况": "107国道交通信号灯及安全防护设施采购项目", + "项目基本情况": { + "1": "政府采购计划编号:2020-09-000560", + "2": "项目编号:HBZHYY-2021-001", + "3": "项目名称:107国道交通信号灯及安全防护设施采购项目", + "4": "采购方式:公开招标", + "5": "预算金额:RMB10071358.07元", + "6": "最高限价:RMB10070194.68元(一包:RMB6230708.59元、二包:RMB2175838.09元、三包:RMB1663648元)", + "7": { + "7.1": "项目概况及内容:一包:107国道交通信号灯及安全防护设施的监控抓拍系统、电子警察、信号灯、专用配套设备等。二包:107国道广水段标示标牌及标线等。三包:107国道霞家河超限检测站抓拍、超重系统等。(以上具体要求见招标文件第三章项目技术、服务及商务要求。)", + "7.2": "项目地点:107国道路段。", + "7.3": "投标人只能对此项目三个包中的一个包进行报名,若同时对两个包或两个包以上进行报名,则所有报名无效。" }, - "备注": "注:若不满足“与公安部、省公安厅、随州市公安局高清视频会议系统无缝对接互联互通”的要求,则本项技术部分(50分)不得分。" + "8": "合同履行期限:合同签订后 6个月内。", + "9": "本项目不接受联合体投标。", + "10": "是否可采购进口产品:否。" }, - "商务评分": { - "控制系统内主板": { - "评分": "10分", - "要求": "所投电梯控制系统内主板为制造商原厂原品牌制造生产且为进口部件得 10分。(提供进口部件报关单及原产地证明扫描件加盖公章,否则不得分)" + "招标控制价": "RMB10070194.68元", + "投标竞争下浮率": "未知", + "分包": "不允许", + "是否接受联合体投标": "否" + }, + "采购要求": { + "技术要求": { + "交通信号灯": { + "单灯技术要求": "1、道路交通信号灯单灯必须符合国家标准GB14887-2011《道路交通信号灯》全部技术规定,并通过公安部交通安全产品质量监督检测中心的检测。", + "光源要求": "2、道路交通信号灯光源必须采用户外型超亮度发光二极管(LED),使用寿命不少于50000小时。", + "材质要求": "3、信号灯灯具材质为铝质金属材料。机动车信号灯灯芯透镜尺寸采用¢400mm规格,人行信号灯灯芯透镜尺寸采用¢300mm,相同规格的灯芯可以互换。信号灯外观应与目前广水市使用的信号灯外观相一致。", + "供电电路要求": "4、信号灯需采用恒流供电电路,单体信号灯功率¢400mm规格不超过20VA;¢300mm规格不超过 15VA。", + "倒计时器要求": "1、道路交通信号倒计时器安装在信号灯的上方或右方,2位数码显示倒九秒提示。当机动车信号灯采用悬臂式杆件时,机动车信号灯倒计时器规格为 800×600,当采用立柱式杆件时,倒计时器规格为 400×400。人行信号灯倒计时显示器透光面为Φ300mm或 300mm ×300mm。倒计时显示器的光学性能、工作条件、机械强度、电气性能均符合国家安全行业标准GA/T508-2004《道路交通信号倒计时显示器》的全部技术规定。", + "倒计时器运行要求": "2、倒计时器运行中遇到电磁、静电、电网等干扰时不能有死机现象。", + "倒计时器测试要求": "3、倒计时器必须经过过电压、过电流测试,具有防雷击功能。", + "倒计时器触发方式": "4、倒计时器应为学习型,并支持脉冲触发、黄灯触发及通讯式触发。", + "信号灯杆技术要求": "1、信号灯杆所属的立柱、法兰盘、地脚螺栓、螺母、垫片、加强筋等金属构件及悬臂、支撑臂、拉杆、抱箍座、夹板等附件的防腐性能应符合GB/T18226《公路交通工程钢构件防腐技术条件》的规定。", + "信号灯杆材质要求": "2、信号灯杆应采用圆形或多棱形经热镀锌处理的钢管制造;悬臂式灯杆悬臂杆与支撑杆使用圆形或多棱形的变截面型材制作,悬臂与灯杆连接端宜焊接固定法兰盘,悬臂下应留有进出线孔。", + "信号灯杆表面处理": "3、信号灯杆制作后须经过防锈处理,底层喷涂富锌防锈底漆,外层喷涂银灰色瓷漆。", + "信号灯杆底部要求": "4、信号灯灯杆距路面约 300-350mm处留有拉线孔和拉线孔门。孔门盖应设有防盗措施,孔内设置接地端子座,并与接地线可靠接驳。", + "信号灯杆顶部要求": "5、立柱式灯杆顶部安装灯具处应留有出线孔,并配备橡胶护套、电缆线回水弯挂钩,灯杆顶部应安装塑料或经防腐处理的内套式金属防水管帽。", + "悬臂式灯杆拉杆要求": "6、悬臂式灯杆拉杆宜使用圆钢制作,一端配有可调距离的螺旋扣,直径和长度根据悬臂长度确定。", + "信号灯杆法兰盘要求": "7、信号灯杆杆体底部应焊接固定法兰盘,法兰盘与杆体之间应均匀焊接加强筋。", + "人行信号灯设计要求": "8、一体式人行信号灯采用整灯嵌入式设计、结构紧凑、方便拆装、便于维护。杆体前后金属面板喷塑处理,两侧铝型材包边。" }, - "制造商技术实力": [ - { - "评分": "3分", - "要求": "一级证书得3分,二级证书得1分,其他不得分" + "交通监控视频子系统": { + "高清视频抓拍像机": { + "有效像素": "≥900W像素", + "最低照度": "彩色≤0.001lx", + "传感器类型": "≥1英寸全局曝光COMS/GMOS/GS COMS", + "电子快门": "至少满足 1/25s至 1/100,000s,可调", + "视频压缩标准": "至少支持H264、H265等", + "视频分辨率": "≥4096×2160,向下可设置", + "视频流帧率": "≥25fps,至少双视频流", + "图片压缩方式": "JPEG", + "图像分辨率": "≥4096(H)×2160(V)", + "强光抑制": "具备", + "API接口开放": "具备;支持标准ONVIF协议与第三方厂家设备进行互联;支持GB/T28181;应提供 SDK", + "通讯接口": "≥1个 RJ45,10M/100M/1000M自适应以太网电口;≥1个 RS-485接口", + "前端存储卡": "嵌入式,支持断网时本地存储,裸容量≥128GB", + "具备其他功能": "应具备I/O触发、RS-485触发、视频触发,支持电源同步;支持频闪式卜光装置和脉冲式补光装置同步补光", + "具备其他功能2": "具备AI深度学习算法和GPU芯片", + "光圈、聚焦": "手动光圈;焦距应可根据车道宽度及抓拍距离进行调整设置,要求中心成像圆内解像力不小于 900万像素,边缘解像力不低于中心 60%,镜头的成像尺寸应与摄像机成像靶面尺寸相等或略大。镜头应配置电动偏振镜,具备根据环境自动切换功能", + "防护罩类型": "室外型防护罩,含底座;具备隔热防潮、防水、防尘、防腐、防震等功能;具有加热器、支架、遮阳罩等,具有良好的密封性和恒温等功能。", + "防护等级": "≥IP66" }, - { - "评分": "2分", - "要求": "行业销量排名连续前 2 名,得 2 分,第 4-6 名得 0.5 分,其他不得分" + "补光灯": { + "配置方式": "每个车道配置≥1台;补光装置必须与高清摄像设备高度匹配", + "LED管芯": "采用超高亮大功率白光LED管芯,灯珠数量不少于 20颗", + "功耗": "30W/车道≤单台平均功耗≤50W/车道,可调", + "峰值光照度(基准轴)": "≤300lx", + "峰值光照度(补光区)": "应大于等于基准轴上有效光照度的 50%", + "平均光照度": "≤50lx", + "闪光频率": "≥50Hz;补光装置应能与集成式高清摄像设备同步", + "有效补光距离": "≥25m", + "最大点亮时间": "≤4ms(可调节)", + "控制方式": "电平,同步触发方式", + "安全性": "在保证瞬时亮度的前提下,必须进行防炫目处理,不会造成光污染", + "使用寿命": "≥50000小时", + "防护等级": "不低于IP65", + "其它": "应配置光栅装置或遮光阻断装置" + }, + "光纤收发器": { + "1路千兆以太网": "1路千兆以太网。", + "单模单纤": "单模单纤,光接口为FC接口,满足实际传输距离要求。", + "10/100/1000M自适应以太网接口": "10/100/1000M自适应以太网接口。", + "工作环境适应性强": "工作环境适应性强,满足全天候使用的要求。", + "耐压": "耐压≥300V。" + }, + "交换机": { + "交换容量": "≥6Gbps,包转发率≥7Mpps;MAC地址列表≥8K;应具备线性转发能力;", + "以太网接口": "应至少具有 8个 10/100/1000M自适应以太网接口和至少 2个 1000M光口;", + "以太网光端口传输距离": "应不小于 40km,以太网光模块为单芯双向光模块,光口接口类型为LC型接口,光模块连接单模光纤;", + "支持协议": "至少支持 IEEE802.1p、IEEE802.1q、IEEE802.3、IEEE802.3u、IEEE802.3z、IEEE802.3x等协议;", + "VLAN功能": "应具有 IEEE802.1Q VLAN,应实现信号控制独立传输,至少支持 4个 VLAN划分;", + "路由协议": "应至少支持三层动态路由协议;", + "QOS功能": "应具有 IEEE802.1p_QOS功能;", + "组播功能": "应具有 IGMP静态组播、端口聚合、端口镜像等功能;", + "防护等级": "IP40以上等级防护;", + "设计要求": "应采用无风扇设计;", + "网管功能": "应具有网管功能。" + }, + "终端服务器": { + "结构形式": "采用嵌入式架构;", + "操作系统": "嵌入式操作系统;", + "接入路数": "提供≥12路高清摄像机视频存储、过车记录存储、图片存储、数据上传、视频流转发等;", + "主机存储": "内置 SATA接口;配置存储容量≥16T硬盘 ;", + "网络接口": "提供≥8个 RJ45 1000M网络接口;", + "其他接口": "提供 RS232、RS485、外置 USB接口、VGA接口等;", + "访问操作": "支持 Web操作访问;", + "接口协议": "至少支持 ONVIF,GB/T28181等;", + "API接口开放支持": "支持标准ONVIF协议与第三方厂家设备进行互通;支持 GB/T 28181。", + "支持套牌车检测": "可将抓拍图片与本地历史数据进行车辆特征比对分析,检测出套牌车辆,同时给出告警提示" } - ] - }, - "投标报价评审": { - "投标报价是否出现违反计价规范": { - "评分": "合格/不合格", - "要求": "A:投标报价未违反计价规范的,评审意见为“合格”;B:投标报价违反计价规范的,评审意见为“不合格”" + }, + "交通诱导子系统": { + "交通诱导屏": { + "主要特性": "1)温度范围-40~70℃ 2)光带宽度>130mm 3)通迅接口:RS485、网络通讯、3G网络 4)视角:水平:110°~120°垂直:55° 5)发光亮度:6500-7000 cd/m2 6)平整度≤0.5mm 7)使用寿命≥10万小时 8)平均故障时间≥1万小时 9)电源采用 n+1高可靠容错的开关电源系统 10)显示尺寸:长 3.84米×高 2.56米=9.83平方米 11)机箱要求:冷轧钢板,机箱为内外两层,内箱体为全封闭、全天候、防风雨型,符合IP65防护等级 12)抗风等级:40m/s", + "技术参数": "1)物理点间距:10mm2)物理密度:10000点/m23)发光点颜色:1R1G 4)基色:纯红+纯绿 5)模组尺寸:320mm*160mm 6)刷新频率:≥800HZ 7)工作电压:AC220V±10%,50Hz(三相五线制)8)平均功耗:350W/m2 9)最大功耗:≤700W/ m2" + } + }, + "电子警察子系统": { + "高清视频抓拍像机": { + "摄像机.有效像素": "≥900W像素", + "摄像机.最低照度": "彩色≤0.001lx", + "摄像机.传感器类型": "≥1英寸全局曝光COMS/GMOS/GS COMS", + "摄像机.电子快门": "至少满足 1/25s至 1/100,000s,可调", + "视频图像.视频压缩标准": "至少支持H264、H265等", + "视频图像.视频分辨率": "≥4096×2160,向下可设置", + "视频图像.视频流帧率": "≥25fps,至少双视频流", + "视频图像.图片压缩方式": "JPEG", + "视频图像.图像分辨率": "≥4096(H)×2160(V)", + "视频图像.强光抑制": "具备", + "协议.API接口开放": "具备;支持标准ONVIF协议与第三方厂家设备进行互联;支持GB/T28181;应提供 SDK", + "接口.通讯接口": "≥1个 RJ45,10M/100M/1000M自适应以太网电口;≥1 个 RS-485接口", + "接口.前端存储卡": "嵌入式,支持断网时本地存储,裸容量≥128GB", + "功能.具备其他功能": "应具备I/O触发、RS-485触发、视频触发,支持电源同步;支持频闪式卜光装置和脉冲式补光装置同步补光", + "功能.具备其他功能.具备AI深度学习算法和GPU芯片": "具备AI深度学习算法和GPU芯片", + "高清工业级镜头.光圈、聚焦": "手动光圈;焦距应可根据车道宽度及抓拍距离进行调整设置,要求中心成像圆内解像力不小于 900万像素,边缘解像力不低于中心 60%,镜头的成像尺寸应与摄像机成像靶面尺寸相等或略大。镜头应配置电动偏振镜,具备根据环境自动切换功能", + "防护罩.防护罩类型": "室外型防护罩,含底座;具备隔热防潮、防水、防尘、防腐、防震等功能;具有加热器、支架、遮阳罩等,具有良好的密封性和恒温等功能。", + "防护罩.防护等级": "≥IP66" + }, + "补光灯": { + "配置方式": "每个车道配置≥1台;补光装置必须与高清摄像设备高度匹配", + "LED管芯": "采用超高亮大功率白光LED管芯,灯珠数量不少于 20颗", + "功耗": "30W/车道≤单台平均功耗≤50W/车道,可调", + "峰值光照度(基准轴)": "≤300lx", + "峰值光照度(补光区)": "应大于等于基准轴上有效光照度的 50%", + "平均光照度": "≤50lx", + "闪光频率": "≥50Hz;补光装置应能与集成式高清摄像设备同步", + "有效补光距离": "≥25m", + "最大点亮时间": "≤4ms(可调节)", + "控制方式": "电平,同步触发方式", + "安全性": "在保证瞬时亮度的前提下,必须进行防炫目处理,不会造成光污染", + "使用寿命": "≥50000小时", + "防护等级": "不低于IP65", + "其它": "应配置光栅装置或遮光阻断装置" + }, + "光纤收发器": { + "1路千兆以太网": "1路千兆以太网。", + "单模单纤": "单模单纤,光接口为FC接口,满足实际传输距离要求。", + "10/100/1000M自适应以太网接口": "10/100/1000M自适应以太网接口。", + "工作环境适应性强": "工作环境适应性强,满足全天候使用的要求。", + "耐压": "耐压≥300V。" + }, + "交换机": { + "交换容量": "≥6Gbps,包转发率≥7Mpps;MAC地址列表≥8K;应具备线性转发能力;", + "以太网接口": "应至少具有 8个 10/100/1000M自适应以太网接口和至少 2个 1000M光口;", + "以太网光端口传输距离": "应不小于 40km,以太网光模块为单芯双向光模块,光口接口类型为LC型接口,光模块连接单模光纤;", + "支持协议": "至少支持 IEEE802.1p、IEEE802.1q、IEEE802.3、IEEE802.3u、IEEE802.3z、IEEE802.3x等协议;", + "VLAN功能": "应具有 IEEE802.1Q VLAN,应实现信号控制独立传输,至少支持 4个 VLAN划分;", + "动态路由协议": "应至少支持三层动态路由协议;", + "QOS功能": "应具有 IEEE802.1p_QOS功能;", + "组播功能": "应具有 IGMP静态组播、端口聚合、端口镜像等功能;", + "防护等级": "IP40以上等级防护;", + "设计要求": "应采用无风扇设计;", + "网管功能": "应具有网管功能。" + }, + "终端服务器": { + "结构形式": "采用嵌入式架构;", + "操作系统": "嵌入式操作系统;", + "接入路数": "提供≥12路高清摄像机视频存储、过车记录存储、图片存储、数据上传、视频流转发等;", + "主机存储": "内置 SATA接口;配置存储容量≥16T硬盘 ;", + "网络接口": "提供≥8个 RJ45 1000M网络接口;", + "其他接口": "提供 RS232、RS485、外置 USB接口、VGA接口等;", + "访问操作": "支持 Web操作访问;", + "接口协议": "至少支持 ONVIF,GB/T28181等;", + "API接口开放支持": "支持标准ONVIF协议与第三方厂家设备进行互通;支持 GB/T 28181。", + "支持套牌车检测": "可将抓拍图片与本地历史数据进行车辆特征比对分析,检测出套牌车辆,同时给出告警提示" + } + }, + "交通卡口子系统": { + "高清视频抓拍像机": { + "摄像机.有效像素": "≥900W像素", + "摄像机.最低照度": "彩色≤0.001lx", + "摄像机.传感器类型": "≥1英寸全局曝光COMS/GMOS/GS COMS", + "摄像机.电子快门": "至少满足 1/25s至 1/100,000s,可调", + "视频图像.视频压缩标准": "至少支持H264、H265等", + "视频图像.视频分辨率": "≥4096×2160,向下可设置", + "视频图像.视频流帧率": "≥25fps,至少双视频流", + "视频图像.图片压缩方式": "JPEG", + "视频图像.图像分辨率": "≥4096(H)×2160(V)", + "视频图像.强光抑制": "具备", + "协议.API接口开放": "具备;支持标准ONVIF协议与第三方厂家设备进行互联;支持GB/T28181;应提供 SDK", + "接口.通讯接口": "≥1个 RJ45,10M/100M/1000M自适应以太网电口;≥1 个 RS-485接口", + "接口.前端存储卡": "嵌入式,支持断网时本地存储,裸容量≥128GB", + "功能.具备其他功能": "应具备I/O触发、RS-485触发、视频触发,支持电源同步;支持频闪式卜光装置和脉冲式补光装置同步补光", + "功能.具备其他功能.1": "具备AI深度学习算法和GPU芯片", + "高清工业级镜头.光圈、聚焦": "手动光圈;焦距应可根据车道宽度及抓拍距离进行调整设置,要求中心成像圆内解像力不小于 900万像素,边缘解像力不低于中心 60%,镜头的成像尺寸应与摄像机成像靶面尺寸相等或略大。镜头应配置电动偏振镜,具备根据环境自动切换功能", + "防护罩.防护罩类型": "室外型防护罩,含底座;具备隔热防潮、防水、防尘、防腐、防震等功能;具有加热器、支架、遮阳罩等,具有良好的密封性和恒温等功能。", + "防护罩.防护等级": "≥IP66" + }, + "补光灯": { + "配置方式": "每个车道配置≥1台;补光装置必须与高清摄像设备高度匹配", + "LED管芯": "采用超高亮大功率白光LED管芯,灯珠数量不少于 20颗", + "功耗": "30W/车道≤单台平均功耗≤50W/车道,可调", + "峰值光照度(基准轴)": "≤300lx", + "峰值光照度(补光区)": "应大于等于基准轴上有效光照度的 50%", + "平均光照度": "≤50lx", + "闪光频率": "≥50Hz;补光装置应能与集成式高清摄像设备同步", + "有效补光距离": "≥25m", + "最大点亮时间": "≤4ms(可调节)", + "控制方式": "电平,同步触发方式", + "安全性": "在保证瞬时亮度的前提下,必须进行防炫目处理,不会造成光污染", + "使用寿命": "≥50000小时", + "防护等级": "不低于IP65", + "其它": "应配置光栅装置或遮光阻断装置" + }, + "光纤收发器": { + "1路千兆以太网": "1路千兆以太网。", + "单模单纤": "单模单纤,光接口为FC接口,满足实际传输距离要求。", + "10/100/1000M自适应以太网接口": "10/100/1000M自适应以太网接口。", + "工作环境适应性强": "工作环境适应性强,满足全天候使用的要求。", + "耐压": "耐压≥300V。" + }, + "交换机": { + "交换容量": "≥6Gbps,包转发率≥7Mpps;MAC地址列表≥8K;应具备线性转发能力;", + "以太网接口": "应至少具有 8个 10/100/1000M自适应以太网接口和至少 2个 1000M光口;", + "以太网光端口传输距离": "应不小于 40km,以太网光模块为单芯双向光模块,光口接口类型为LC型接口,光模块连接单模光纤;", + "支持协议": "至少支持 IEEE802.1p、IEEE802.1q、IEEE802.3、IEEE802.3u、IEEE802.3z、IEEE802.3x等协议;", + "VLAN功能": "应具有 IEEE802.1Q VLAN,应实现信号控制独立传输,至少支持 4个 VLAN划分;", + "路由协议": "应至少支持三层动态路由协议;", + "QOS功能": "应具有 IEEE802.1p_QOS功能;", + "组播功能": "应具有 IGMP静态组播、端口聚合、端口镜像等功能;", + "防护等级": "IP40以上等级防护;", + "设计要求": "应采用无风扇设计;", + "网管功能": "应具有网管功能。" + }, + "终端服务器": { + "结构形式": "采用嵌入式架构;", + "操作系统": "嵌入式操作系统;", + "接入路数": "提供≥12路高清摄像机视频存储、过车记录存储、图片存储、数据上传、视频流转发等;", + "主机存储": "内置 SATA接口;配置存储容量≥16T硬盘 ;", + "网络接口": "提供≥8个 RJ45 1000M网络接口;", + "其他接口": "提供 RS232、RS485、外置 USB接口、VGA接口等;", + "访问操作": "支持 Web操作访问;", + "接口协议": "至少支持 ONVIF,GB/T28181等;", + "API接口开放支持": "支持标准ONVIF协议与第三方厂家设备进行互通;支持 GB/T 28181。", + "支持套牌车检测": "可将抓拍图片与本地历史数据进行车辆特征比对分析,检测出套牌车辆,同时给出告警提示" + } + }, + "区间测速子系统": { + "高清视频抓拍像机": { + "1.摄像机.有效像素": "≥900W像素", + "1.摄像机.最低照度": "彩色≤0.001lx", + "1.摄像机.传感器类型": "≥1英寸全局曝光COMS/GMOS/GS COMS", + "1.摄像机.电子快门": "至少满足 1/25s至 1/100,000s,可调", + "2.视频图像.视频压缩标准": "至少支持H264、H265等", + "2.视频图像.视频分辨率": "≥4096×2160,向下可设置", + "2.视频图像.视频流帧率": "≥25fps,至少双视频流", + "3.视频图像.图片压缩方式": "JPEG", + "3.视频图像.图像分辨率": "≥4096(H)×2160(V)", + "3.视频图像.强光抑制": "具备", + "4.协议.API接口开放": "具备;支持标准ONVIF协议与第三方厂家设备进行互联;支持GB/T28181;应提供 SDK", + "5.接口.通讯接口": "≥1个 RJ45,10M/100M/1000M自适应以太网电口;≥1 个 RS-485接口", + "5.接口.前端存储卡": "嵌入式,支持断网时本地存储,裸容量≥128GB", + "5.功能.具备其他功能": "具备AI深度学习算法和GPU芯片", + "6.高清工业级镜头.光圈、聚焦": "手动光圈;焦距应可根据车道宽度及抓拍距离进行调整设置,要求中心成像圆内解像力不小于 900万像素,边缘解像力不低于中心 60%,镜头的成像尺寸应与摄像机成像靶面尺寸相等或略大。镜头应配置电动偏振镜,具备根据环境自动切换功能", + "7.防护罩.防护罩类型": "室外型防护罩,含底座;具备隔热防潮、防水、防尘、防腐、防震等功能;具有加热器、支架、遮阳罩等,具有良好的密封性和恒温等功能。", + "7.防护罩.防护等级": "≥IP66" + }, + "补光灯": { + "配置方式": "每个车道配置≥1台;补光装置必须与高清摄像设备高度匹配", + "LED管芯": "采用超高亮大功率白光LED管芯,灯珠数量不少于 20颗", + "功耗": "30W/车道≤单台平均功耗≤50W/车道,可调", + "峰值光照度(基准轴)": "≤300lx", + "峰值光照度(补光区)": "应大于等于基准轴上有效光照度的 50%", + "平均光照度": "≤50lx", + "闪光频率": "≥50Hz;补光装置应能与集成式高清摄像设备同步", + "有效补光距离": "≥25m", + "最大点亮时间": "≤4ms(可调节)", + "控制方式": "电平,同步触发方式", + "安全性": "在保证瞬时亮度的前提下,必须进行防炫目处理,不会造成光污染", + "使用寿命": "≥50000小时", + "防护等级": "不低于IP65", + "其它": "应配置光栅装置或遮光阻断装置" + }, + "光纤收发器": { + "1路千兆以太网": "1路千兆以太网。", + "单模单纤": "单模单纤,光接口为FC接口,满足实际传输距离要求。", + "10/100/1000M自适应以太网接口": "10/100/1000M自适应以太网接口。", + "工作环境适应性强": "工作环境适应性强,满足全天候使用的要求。", + "耐压": "耐压≥300V。" + }, + "交换机": { + "交换容量": "≥6Gbps", + "包转发率": "≥7Mpps", + "MAC地址列表": "≥8K", + "线性转发能力": "应具备线性转发能力", + "以太网接口": "应至少具有 8个 10/100/1000M自适应以太网接口和至少 2个 1000M光口", + "以太网光端口传输距离": "应不小于 40km,以太网光模块为单芯双向光模块,光口接口类型为LC型接口,光模块连接单模光纤", + "支持协议": "至少支持 IEEE802.1p、IEEE802.1q、IEEE802.3、IEEE802.3u、IEEE802.3z、IEEE802.3x等协议", + "VLAN": "应具有 IEEE802.1Q VLAN,应实现信号控制独立传输,至少支持 4个 VLAN划分", + "三层动态路由协议": "应至少支持三层动态路由协议", + "QOS功能": "应具有 IEEE802.1p_QOS功能", + "IGMP静态组播": "应具有 IGMP静态组播、端口聚合、端口镜像等功能", + "防护等级": "IP40以上等级防护", + "无风扇设计": "应采用无风扇设计", + "网管功能": "应具有网管功能" + }, + "终端服务器": { + "结构形式": "采用嵌入式架构;", + "操作系统": "嵌入式操作系统;", + "接入路数": "提供≥8路高清摄像机视频存储、过车记录存储、图片存储、数据上传、视频流转发等;", + "主机存储": "内置 SATA接口;配置存储容量≥16T硬盘 ;", + "网络接口": "提供≥8个 RJ45 1000M网络接口;", + "其他接口": "提供 RS232、RS485、外置 USB接口、VGA接口等;", + "访问操作": "支持 Web操作访问;", + "接口协议": "至少支持 ONVIF,GB/T28181等;", + "API接口开放支持": "支持标准ONVIF协议与第三方厂家设备进行互通;支持 GB/T 28181。" + } + }, + "后台接入服务器": { + "1、机架式服务器": "1、机架式服务器;", + "2、处理器": "2、处理器:至少配置 2颗单颗 10核 CPU,单核 CPU主频≥2.0GHz;", + "3、内存": "3、内存:配置 128GB DDR4内存;", + "4、硬盘": "4、硬盘:配置 6块 1.2T SAS硬盘,转速≥10Krpm;", + "5、RAID": "5、RAID:配置 RAID控制器,支持 RAID0、1、10、1E、5、50、60等;", + "6、网络": "6、网络:配置双千兆网卡;", + "7、电源": "7、电源:双电源;" + }, + "存储服务器": { + "1": "4U 48盘位磁盘阵列;", + "2": "单设备配置 64位多核处理器;", + "3": "16GB缓存;", + "4": "冗余电源;", + "5": "支持 SATA硬盘;", + "6": "2个千兆网口;", + "7": "1个系统 SSD盘;", + "8": "支持视音频、图片、直接写入,支持视频高速预览、回放、下载,支持云内容灾备份,支持一体化运维,支持GB/T28181-2011、Onvif、RTSP、H265、SVAC等标准视频协议。" + }, + "硬盘": { + "容量": "4T", + "尺寸": "3.5寸", + "接口": "SATA" + }, + "室外挂箱": { + "安装方式": "采用悬挂式安装方式,悬挂于杆件立柱,高度应确保机箱下边缘距离地面净高 2.5 米以上。设备机箱安装后不得侵入机动车道建筑界限以内,不得影响车辆正常通行。设备机箱应安装牢固;", + "表面处理": "设备机箱表面应经过考漆处理,应具备防锈蚀、防盐雾、防霉菌能力;", + "内部空间": "机箱内部空间应足够大,能确保设备、装置的合理摆放,设有存放用户手册、说明书、接线图、维修记录等资料的存储盒,并有适当空间预留。机箱空间应有利于机箱内各设备单元的散热、安装、使用和维修,同时应提供设备辅助散热措施,提高系统环境适应能力;", + "防护等级": "设备机箱的结构应能防雨并能降低灰尘及有害物质的侵入,机箱门盖应有溢水槽,机箱门内侧应配备密封条,机箱顶部应具有防积水措施。机箱防护等级应达到IP55以上;", + "机械强度": "机箱结构应具有足够的机械强度,应能承受正常条件下可预料到的运输、安装、搬运、维护等过程中的操作;", + "门的设计": "机箱门的最大开启角度应大于 120°。机箱门锁应采用保险柜天地锁式的结构设计防止被非法打开,门锁至少可对上、下及左右侧中的一侧进行缩栓式保护,应具备较强的设备防砸、防盗能力。机箱应具有防盗报警功能,机箱在非正常状态下开启时能够报警提示。机箱门接缝处有耐久且有弹性的密封垫,密封垫连续设置,无间断接口。机柜门锁上后,无松动、变形现象;", + "电源保护": "机箱内设应置有具备稳压、过载、漏电、短路保护功能的电源开关和防雷保护功能的电源浪涌保护器。在熔断器和电源开关等处应有警告标志。机箱内合适位置配备接地铜排,接地铜排的截面应不小于 100 mm2,接地端子应进行防腐处理。并应设置接地标志;接地铜排应保证良好接地,接地线截面积应不小于 16mm2;", + "电源插座": "机箱内应配备不少于 2路单相 2孔扁圆电源插座、2路单相 3孔扁圆电源插座;", + "电源开关": "应具有稳压、短路、过载、漏电保护;电源保护响应时间应为纳秒级;开关的额定电压、额定电流值应满足设备正常运行的要求;机械寿命应不少于 20000次;具有良好的散热性能。", + "额外要求": "室外大机箱需满足上述使用外,还应可安装前端管理主机。" + }, + "交通管道": { + "过街管道": "主要垂直于道路中线埋设,可开挖路段采用 DN90镀锌钢管,不宜开挖路段根据现场情况采用拖拉管(DN90 PE管)。路口必须在各方向埋设双管。", + "非过街管道": "采用 PE双壁波纹管。", + "干线(纵向)路段": "两接线井之间最大距 50m。", + "管道埋设深度": "机动车道和非机动车道下管道埋设深度(管顶至路面)应≥0.7m,人行道及绿化带下管道埋设深度(管顶至路面)应≥0.5m。如不能达到上述埋设深度要求应采取混凝土包封或采用钢管等保护措施。", + "管道敷设的坡度": "应为 0.3%-0.4%,不得小于 0.25%。", + "其他敷设要求": "应参照《市政公用工程细部构造做法(湖北省)》中《交通管道预埋断面图》做法。", + "交通管道开挖后": "应按原有道路或绿化进行恢复,管线尽量敷设于人行道上,不影响后续的绿化园林施工。" } - } + }, + "商务要求": "", + "服务要求": "", + "其他要求": "" + }, + "关键时间/内容": { + "投标文件递交截止日期": "2021年月日点分(北京时间)", + "投标文件递交地点": "广水市公共资源交易中心五楼号开标室", + "开标时间": "未知", + "开标地点": "广水市公共资源交易中心五楼号开标室", + "澄清招标文件的截止时间": "未知", + "投标有效期": "提交投标文件截止之日起 60日历日", + "信息公示媒介": [ + { + "名称": "中国湖北政府采购网", + "网址": "http://www.ccgp-hubei.gov.cn/" + }, + { + "名称": "中国广水网", + "网址": "http://www.zggsw.gov.cn/" + } + ] + }, + "保证金相关": { + "是否提交履约保证金": "未知", + "质量保证金": "未知" + }, + "其他信息": { + "投标费用承担": "投标人应承担所有与准备和参加投标有关的费用。不论投标的结果如何,分散采购机构和采购人均无义务和责任承担这些费用。", + "招标代理服务费": { + "收费标准": "服务费按国家规定收费标准按国家发展改革委关于印发《招标代理服务收费管理暂行办法》的通知(计价格[2002]1980 号)执行。", + "支付方": "中标方", + "支付时间": "政府采购代理缴纳中标服务费" + }, + "是否退还投标文件": "否", + "投标预备会": "不召开", + "偏离": { + "商务要求响应、偏离说明表": "第七章投标文件格式(参考)附件十二", + "商务要求“★”号条款响应、偏离说明表": "第七章投标文件格式(参考)附件十三", + "技术、服务要求响应、偏离说明表": "第七章投标文件格式(参考)附件十五", + "技术、服务要求“★”号条款响应、偏离说明表": "第七章投标文件格式(参考)附件十六", + "偏离项的具体要求": "未知" + }, + "踏勘现场": "不组织", + "货物列表": {} } -} -""" -def reorganize_data(input_dict, include=None): - """ - 重组输入字典,将“技术评分”和“商务评分”提升为最外层键, - 并将包含在 include 列表中的包名的数据嵌套在相应的评分类别下。 - - 如果 input_dict 的顶层键不包含任何 include 列表中的项,则返回原始字典。 - - :param input_dict: 原始输入字典 - :param include: 包名列表,例如 ['一包', '二包', '三包'] - :return: 重组后的字典 - """ - if include is None: - include = [] - - # 检查是否有任何顶层键包含在 include 列表中 - has_include = any(key in include for key in input_dict.keys()) - - if not has_include: - # 没有包含任何指定的包名,直接返回原始字典 - return input_dict - - # 初始化新的字典结构 - reorganized = { - "技术评分": {}, - "商务评分": {} } - - # 遍历每一个包(如 "一包", "二包") - for package, categories in input_dict.items(): - # 处理技术评分 - if "技术评分" in categories: - reorganized["技术评分"][package] = categories["技术评分"] - - # 处理商务评分 - if "商务评分" in categories: - reorganized["商务评分"][package] = categories["商务评分"] - - return reorganized -include = ['一包', '二包', '三包', '四包', '五包'] -target_values = ['技术', '设计', '实施'] -cleaned_evaluation_res = parse_json_with_duplicates(raw_data) #处理重复键名的情况 -result_data = process_data_based_on_key(cleaned_evaluation_res) -updated_jsons = {} -if any(key for key in result_data if any(included in key for included in include)): - # 有匹配的项,处理这些项 - for key in result_data: - if any(item in key for item in include): - inner_dict = result_data[key] - updated_jsons[key] = combine_technical_and_business(inner_dict, target_values) -else: - # 没有匹配的项,对整个字典运行 - updated_jsons = combine_technical_and_business(result_data, target_values) -res=reorganize_data(updated_jsons,include) -print(json.dumps(res, ensure_ascii=False, indent=4)) \ No newline at end of file +} +res1,res2=post_process_baseinfo(data) +print(res1) \ No newline at end of file diff --git a/flask_app/货物标/截取pdf货物标版.py b/flask_app/货物标/截取pdf货物标版.py index 440ddd7..829ebc9 100644 --- a/flask_app/货物标/截取pdf货物标版.py +++ b/flask_app/货物标/截取pdf货物标版.py @@ -1,9 +1,18 @@ +import logging + from PyPDF2 import PdfReader, PdfWriter import re # 导入正则表达式库 import os # 用于文件和文件夹操作 from flask_app.general.format_change import docx2pdf from flask_app.general.merge_pdfs import merge_and_cleanup,merge_pdfs import concurrent.futures +def get_global_logger(unique_id): + if unique_id is None: + return logging.getLogger() # 获取默认的日志器 + logger = logging.getLogger(unique_id) + return logger +logger = None + def clean_page_content(text, common_header): # 首先删除抬头公共部分 if common_header: # 确保有公共抬头才进行替换 @@ -179,26 +188,27 @@ def extract_pages(pdf_path, output_folder, begin_pattern, begin_page, end_patter common_header = extract_common_header(pdf_path) pdf_document = PdfReader(pdf_path) exclusion_pattern = None - total_pages = len(pdf_document.pages)-1 # 获取总页数 + total_pages = len(pdf_document.pages) - 1 # 获取总页数 + if output_suffix == "tobidders_notice": exclusion_pattern = re.compile(r'文件的构成|文件的组成|须对应|需对应|须按照|需按照|须根据|需根据') - start_page, mid_page, end_page = extract_pages_tobidders_notice(pdf_document, begin_pattern, end_pattern, - begin_page, common_header, - exclusion_pattern) - if start_page is None or mid_page is None or end_page is None: + start_page, mid_page, end_page = extract_pages_tobidders_notice( + pdf_document, begin_pattern, begin_page, common_header, exclusion_pattern + ) + + if start_page is None or end_page is None or mid_page is None: print(f"first: {output_suffix} 未找到起始或结束页在文件 {pdf_path} 中!尝试备用提取策略。") return extract_pages_twice_tobidders_notice(pdf_path, output_folder, output_suffix, common_header) - path1 = save_extracted_pages(pdf_document, start_page, mid_page, pdf_path, output_folder, - "tobidders_notice_part1") - path2 = save_extracted_pages(pdf_document, mid_page, end_page, pdf_path, output_folder, - "tobidders_notice_part2") + + path1 = save_extracted_pages(pdf_document, start_page, mid_page, pdf_path, output_folder, "tobidders_notice_part1") + path2 = save_extracted_pages(pdf_document, mid_page, end_page, pdf_path, output_folder, "tobidders_notice_part2") return path1, path2 + else: # 原有的处理逻辑保持不变 if output_suffix == "qualification1": 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) + start_page, end_page = extract_pages_generic(pdf_document, begin_pattern, end_pattern, begin_page, common_header, exclusion_pattern, output_suffix) # 针对 selection = 6 的特殊处理 if output_suffix == "format": if start_page is None: @@ -213,12 +223,13 @@ def extract_pages(pdf_path, output_folder, begin_pattern, begin_page, end_patter print(f"first: {output_suffix} 未找到起始或结束页在文件 {pdf_path} 中!尝试备用提取策略。") return extract_pages_twice(pdf_path, output_folder, output_suffix, common_header) elif output_suffix == "qualification1": - truncate_pdf_main(pdf_path, output_folder, 2, "qualification3") #合并'资格审查'章节和'评标办法'章节 + truncate_pdf_main(pdf_path, output_folder, 2, "qualification3") # 合并'资格审查'章节和'评标办法'章节 return save_extracted_pages(pdf_document, start_page, end_page, pdf_path, output_folder, output_suffix) except Exception as e: print(f"Error processing {pdf_path}: {e}") return "" + def get_patterns_for_procurement(): begin_pattern = re.compile( r'^第[一二三四五六七八九十百千]+(?:章|部分).*?(?:服务|项目|商务).*?要求|' @@ -294,46 +305,105 @@ def get_patterns_for_notice(): # break # return start_page, mid_page, end_page -def extract_pages_tobidders_notice(pdf_document, begin_pattern, end_pattern, begin_page, common_header, - exclusion_pattern): - def run_extraction(use_multiline=False): +def extract_pages_tobidders_notice(pdf_document, begin_pattern, begin_page, common_header, exclusion_pattern): + def run_extraction(): start_page = None mid_page = None end_page = None + chapter_type = None # 用于存储“章”或“部分” + for i, page in enumerate(pdf_document.pages): text = page.extract_text() or "" cleaned_text = clean_page_content(text, common_header) - # print(cleaned_text) + if exclusion_pattern and re.search(exclusion_pattern, cleaned_text) and mid_page is not None: continue - if start_page is None and re.search(begin_pattern, cleaned_text) and i > begin_page: - start_page = i - if start_page is not None and mid_page is None: - mid_pattern = r'^\s*[((]?\s*[一1]\s*[))]?\s*[、..]*\s*(说\s*明|总\s*则)' - flags = re.MULTILINE if use_multiline else 0 - if re.search(mid_pattern, cleaned_text, flags): + + if start_page is None: + match = re.search(begin_pattern, cleaned_text) + if match and i > begin_page: + start_page = i + matched_text = match.group(0) # 获取整个匹配的文本 + if '章' in matched_text: + chapter_type = '章' + elif '部分' in matched_text: + chapter_type = '部分' + else: + chapter_type = None # 未匹配到“章”或“部分” + + if chapter_type: + # 根据 chapter_type 动态生成 end_pattern + end_pattern = re.compile( + rf'^第[一二三四五六七八九十百千]+?(?:{chapter_type})\s*[\u4e00-\u9fff]+', + re.MULTILINE + ) + # print(f"动态生成的 end_pattern: {end_pattern.pattern}") # 打印生成的 end_pattern + + # 根据 chapter_type 动态生成 additional_mid_pattern + if chapter_type == '章': + additional_mid_pattern = r'^第[一二三四五六七八九十百千]+?(?:部分)' + elif chapter_type == '部分': + additional_mid_pattern = r'^第[一二三四五六七八九十百千]+?(?:章)' + else: + additional_mid_pattern = '' + + # 定义基础的 mid_pattern + base_mid_pattern = r'^\s*(?:[((]\s*[一1]?\s*[))]\s*[、..]*|[一1][、..]+|[、..]+)\s*(说\s*明|总\s*则)' + + # 合并基础模式和额外模式 + if additional_mid_pattern: + combined_mid_pattern = re.compile( + rf'(?:{base_mid_pattern})|(?:{additional_mid_pattern})', + re.MULTILINE + ) + else: + combined_mid_pattern = re.compile( + rf'{base_mid_pattern}', + re.MULTILINE + ) + # print(f"生成的 combined_mid_pattern: {combined_mid_pattern.pattern}") # 打印 combined_mid_pattern + else: + # 如果未匹配到“章”或“部分”,使用默认的 end_pattern 和 mid_pattern + end_pattern = re.compile( + r'^第[一二三四五六七八九十百千]+(?:章|部分)\s*[\u4e00-\u9fff]+', + re.MULTILINE + ) + print(f"使用默认的 end_pattern: {end_pattern.pattern}") # 打印默认的 end_pattern + + # 定义基础的 mid_pattern + base_mid_pattern = r'^\s*(?:[((]\s*[一1]?\s*[))]\s*[、..]*|[一1][、..]+|[、..]+)\s*(说\s*明|总\s*则)' + combined_mid_pattern = re.compile( + rf'{base_mid_pattern}', + re.MULTILINE + ) + print( + f"使用默认的 combined_mid_pattern: {combined_mid_pattern.pattern}") # 打印默认的 combined_mid_pattern + + continue + + if start_page is not None and mid_page is None and combined_mid_pattern: + if re.search(combined_mid_pattern, cleaned_text): mid_page = i - if start_page is not None and re.search(end_pattern, cleaned_text): - if mid_page is None: - if i > start_page: - end_page = i - break - else: - if i > mid_page: - end_page = i - break + if start_page is not None and mid_page is not None and chapter_type: + if re.search(end_pattern, cleaned_text): + if mid_page is None: + if i > start_page: + end_page = i + break + else: + if i > mid_page: + end_page = i + break + return start_page, mid_page, end_page - # 第一次运行 + # 运行提取 start_page, mid_page, end_page = run_extraction() - # 如果有任何一个值为 None,使用 re.MULTILINE 重新运行 - if start_page is None or mid_page is None or end_page is None: - start_page, mid_page, end_page = run_extraction(use_multiline=True) - return start_page, mid_page, end_page + def extract_pages_twice_tobidders_notice(pdf_path, output_folder, output_suffix, common_header): begin_pattern = re.compile( r'^第[一二三四五六七八九十百千]+(?:章|部分)\s*(?:(?:投标人?|磋商|供应商|谈判供应商|磋商供应商)须知)+' @@ -341,7 +411,7 @@ def extract_pages_twice_tobidders_notice(pdf_path, output_folder, output_suffix, end_pattern = re.compile( r'^第[一二三四五六七八九十百千]+(?:章|部分)\s*([\u4e00-\u9fff]+)' # 捕获中文部分 ) - exclusion_words = ["合同", "评标", "开标"] # 在这里添加需要排除的关键词 + exclusion_words = ["合同", "评标", "开标","评审","采购","资格"] # 在这里添加需要排除的关键词 pdf_document = PdfReader(pdf_path) exclusion_pattern = re.compile(r'文件的构成|文件的组成|须对应|需对应|须按照|需按照|须根据|需根据') @@ -453,6 +523,11 @@ def extract_pages_twice(pdf_path, output_folder, output_suffix, common_header): def save_extracted_pages(pdf_document, start_page, end_page, pdf_path, output_folder, output_suffix): try: + # 检查 start_page 和 end_page 是否为 None + if start_page is None or end_page is None: + print("Error: start_page 或 end_page 为 None") + return "" + base_file_name = os.path.splitext(os.path.basename(pdf_path))[0] output_pdf_path = os.path.join(output_folder, f"{base_file_name}_{output_suffix}.pdf") @@ -492,16 +567,20 @@ def merge_selected_pdfs(output_folder, truncate_files, output_path, base_file_na - truncate_files (list): 包含 PDF 文件路径的列表。 - output_path (str): 合并后的 PDF 文件保存路径。 - base_file_name (str): 用于匹配文件名的基础名称。 + - logger (logging.Logger): 日志记录器对象。 + + 返回: + - str: 如果合并成功,返回 output_path;否则,返回空字符串 ""。 """ # 1. 获取 output_folder 中所有文件 try: all_output_files = os.listdir(output_folder) except FileNotFoundError: print(f"输出文件夹 '{output_folder}' 未找到。") - return + return "" except PermissionError: print(f"没有权限访问输出文件夹 '{output_folder}'。") - return + return "" # 2. 定义要选择的文件后缀及合并顺序,包括 before 文件 desired_suffixes = [ @@ -538,26 +617,31 @@ def merge_selected_pdfs(output_folder, truncate_files, output_path, base_file_na if not all_pdfs_to_merge: print("没有找到要合并的 PDF 文件。") - return + return "" # 调用 merge_pdfs 函数进行合并 merge_pdfs(all_pdfs_to_merge, output_path) print(f"已成功合并 PDF 文件到 '{output_path}'。") + # 检查合并后的文件是否存在且不为空 + if os.path.exists(output_path) and os.path.getsize(output_path) > 0: + return output_path + else: + print(f"合并失败,没有生成 '{output_path}'。") + return "" + def truncate_pdf_main(input_path, output_folder, selection, output_suffix="default"): try: - if selection == 1: - # 更新的正则表达式以匹配"第x章"和"第x部分",考虑到可能的空格和其他文字 + if selection == 1: #招标公告 + begin_page = 0 begin_pattern = re.compile( - r'^第[一二三四五六七八九十百千]+(?:章|部分).*?(?:服务|项目|商务).*?要求|' - r'^第[一二三四五六七八九十百千]+(?:章|部分).*?采购.*|' - r'^第[一二三四五六七八九十百千]+(?:章|部分).*?需求.*' + r'^第[一二三四五六七八九十百千]+(?:章|部分).*?(?:公告|邀请书|邀请函).*' ) - begin_page = 3 end_pattern = re.compile( - r'^第[一二三四五六七八九十百千]+(?:章|部分)\s*[\u4e00-\u9fff]+' + # r'^(?:第[一二三四五六七八九十百千]+(?:章|部分)\s*(?:投标人|磋商|供应商|谈判供应商|磋商供应商)须知+|(?:一\s*、\s*)?(?:投标人|磋商|供应商)须知前附表)' + r'^第[一二三四五六七八九十百千]+(?:章|部分)\s*[\u4e00-\u9fff]+', re.MULTILINE ) - local_output_suffix = "procurement" + local_output_suffix = "notice" elif selection == 2: begin_pattern = re.compile( r'^第[一二三四五六七八九十百千]+(?:章|部分).*?(磋商|谈判|评标|评定|评审)(方法|办法).*' @@ -582,20 +666,23 @@ def truncate_pdf_main(input_path, output_folder, selection, output_suffix="defau r'^(?:第[一二三四五六七八九十百千]+(?:章|部分)\s*(?:投标人?|磋商|供应商|谈判供应商|磋商供应商)须知+|(?:一\s*、\s*)?(?:投标人?|磋商|供应商)须知前附表)', re.MULTILINE ) - end_pattern = re.compile( - r'^第[一二三四五六七八九十百千]+(?:章|部分)\s*[\u4e00-\u9fff]+', re.MULTILINE - ) + end_pattern=None + # end_pattern = re.compile( + # r'^第[一二三四五六七八九十百千]+(?:章|部分)\s*[\u4e00-\u9fff]+', re.MULTILINE + # ) local_output_suffix = "tobidders_notice" - elif selection == 5: # 招标公告 - begin_page = 0 + elif selection == 5: #采购需求 + # 更新的正则表达式以匹配"第x章"和"第x部分",考虑到可能的空格和其他文字 begin_pattern = re.compile( - r'^第[一二三四五六七八九十百千]+(?:章|部分).*?(?:公告|邀请书|邀请函).*' + r'^第[一二三四五六七八九十百千]+(?:章|部分).*?(?:服务|项目|商务).*?要求|' + r'^第[一二三四五六七八九十百千]+(?:章|部分).*?采购.*|' + r'^第[一二三四五六七八九十百千]+(?:章|部分).*?需求.*' ) + begin_page = 3 end_pattern = re.compile( - # r'^(?:第[一二三四五六七八九十百千]+(?:章|部分)\s*(?:投标人|磋商|供应商|谈判供应商|磋商供应商)须知+|(?:一\s*、\s*)?(?:投标人|磋商|供应商)须知前附表)' - r'^第[一二三四五六七八九十百千]+(?:章|部分)\s*[\u4e00-\u9fff]+',re.MULTILINE + r'^第[一二三四五六七八九十百千]+(?:章|部分)\s*[\u4e00-\u9fff]+' ) - local_output_suffix = "notice" + local_output_suffix = "procurement" elif selection==6: #投标文件格式 begin_page=5 begin_pattern=re.compile( @@ -607,7 +694,7 @@ def truncate_pdf_main(input_path, output_folder, selection, output_suffix="defau local_output_suffix = "format" else: print("无效的选择:请选择1-5") - return None + return [''] # 如果传入的 output_suffix 是 'default',则使用本地生成的 output_suffix if output_suffix == "default": @@ -617,10 +704,12 @@ def truncate_pdf_main(input_path, output_folder, selection, output_suffix="defau return process_input(input_path, output_folder, begin_pattern, begin_page, end_pattern, output_suffix) or "" except Exception as e: print(f"Error in truncate_pdf_main: {e}") - return "" # 返回空字符串 + return [''] # 返回空字符串 -def truncate_pdf_multiple(pdf_path, output_folder): +def truncate_pdf_multiple(pdf_path, output_folder,unique_id="123"): + global logger + logger = get_global_logger(unique_id) base_file_name = os.path.splitext(os.path.basename(pdf_path))[0] truncate_files = [] @@ -642,20 +731,24 @@ def truncate_pdf_multiple(pdf_path, output_folder): if files: truncate_files.extend(files) except Exception as e: - print(f"Selection {selection} 生成了一个异常: {e}") + logger.error(f"Selection {selection} 生成了一个异常: {e}") - if truncate_files: - merged_output_path = os.path.join(output_folder, f"{base_file_name}_merged_baseinfo.pdf") - merge_selected_pdfs(output_folder, truncate_files, merged_output_path, base_file_name) - truncate_files.append(merged_output_path) - print(f"已生成合并文件: {merged_output_path}") + # 定义合并后的输出路径 + merged_output_path = os.path.join(output_folder, f"{base_file_name}_merged_baseinfo.pdf") + # 调用 merge_selected_pdfs 并获取返回值 + merged_path = merge_selected_pdfs(output_folder, truncate_files, merged_output_path, base_file_name) + if merged_path: + # 合并成功,添加合并后的文件路径 + truncate_files.append(merged_path) + logger.info(f"已生成合并文件: {merged_output_path}") else: - print(f"没有文件需要合并 for {pdf_path}") - + # 合并失败,添加空字符串 + truncate_files.append("") + logger.warning(f"合并失败,没有生成合并文件 for {pdf_path}") return truncate_files #小解析,只需要前三章内容 -def truncate_pdf_specific_goods(pdf_path, output_folder, selections): +def truncate_pdf_specific_goods(pdf_path, output_folder, selections,unique_id="123"): """ 处理 PDF 文件,选择指定的 selections,并合并结果。 @@ -667,6 +760,8 @@ def truncate_pdf_specific_goods(pdf_path, output_folder, selections): Returns: list: 截取的文件路径列表,包括合并后的文件路径(如果有)。 """ + global logger + logger = get_global_logger(unique_id) base_file_name = os.path.splitext(os.path.basename(pdf_path))[0] truncate_files = [] @@ -686,32 +781,37 @@ def truncate_pdf_specific_goods(pdf_path, output_folder, selections): elif isinstance(files, str): truncate_files.append(files) except Exception as e: - print(f"Selection {selection} 生成了一个异常: {e}") + logger.error(f"Selection {selection} 生成了一个异常: {e}") - if truncate_files: - merged_output_path = os.path.join(output_folder, f"{base_file_name}_merged_specific.pdf") - merge_selected_pdfs(output_folder, truncate_files, merged_output_path, base_file_name) - truncate_files.append(merged_output_path) - print(f"已生成合并文件: {merged_output_path}") + # 定义合并后的输出路径 + merged_output_path = os.path.join(output_folder, f"{base_file_name}_merged_baseinfo.pdf") + # 调用 merge_selected_pdfs 并获取返回值 + merged_path = merge_selected_pdfs(output_folder, truncate_files, merged_output_path, base_file_name) + if merged_path: + # 合并成功,添加合并后的文件路径 + truncate_files.append(merged_path) + logger.info(f"已生成合并文件: {merged_output_path}") else: - print(f"没有文件需要合并 for {pdf_path}") + # 合并失败,添加空字符串 + truncate_files.append("") + logger.warning(f"合并失败,没有生成合并文件 for {pdf_path}") return truncate_files # TODO:交通智能系统和招标(1)(1)文件有问题 包头 绍兴 资格审查文件可能不需要默认与"evaluation"同一章 无效投标可能也要考虑 “more”的情况,类似工程标 if __name__ == "__main__": - input_path = "C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles\\招标文件(广水市教育局封闭管理项目二次).pdf" + # input_path = "C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles\\zbtest4_evaluation_method.pdf" # input_path = "C:\\Users\\Administrator\\Desktop\\fsdownload\\b151fcd0-4cd8-49b4-8de3-964057a9e653\\ztbfile.pdf" - # input_path="C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles" + input_path="C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles" # 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="C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles\\新建文件夹" # files = truncate_pdf_multiple(input_path, output_folder) - selections = [4, 5] - files=truncate_pdf_specific_goods(input_path,output_folder,selections) - print(files) - # selection = 6# 例如:1 - 商务技术服务要求, 2 - 评标办法, 3 - 资格审查后缀有qualification1或qualification2(与评标办法一致) 4.投标人须知前附表part1 投标人须知正文part2 5-公告 - # generated_files = truncate_pdf_main(input_path, output_folder, selection) + # selections = [1,4] + # files=truncate_pdf_specific_goods(input_path,output_folder,selections) + # print(files) + selection = 4# 例如: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/货物标/货物标解析main.py b/flask_app/货物标/货物标解析main.py index 26da875..d2be35f 100644 --- a/flask_app/货物标/货物标解析main.py +++ b/flask_app/货物标/货物标解析main.py @@ -54,11 +54,11 @@ def preprocess_files(output_folder, file_path, file_type): # 处理各个部分 invalid_docpath = docx_path # docx截取无效标部分 - procurement_path = truncate_files[0] # 商务技术服务要求 + procurement_path = truncate_files[5] # 商务技术服务要求 evaluation_method_path = truncate_files[1] # 评标办法 qualification_path = truncate_files[2] # 资格审查 tobidders_notice_path = truncate_files[4] # 投标人须知正文 - notice_path = truncate_files[5] + notice_path = truncate_files[0] #招标公告 merged_baseinfo_path = truncate_files[6] # 合并封面+招标公告+投标人须知前附表+须知正文 clause_path = convert_clause_to_json(tobidders_notice_path, output_folder) # 投标人须知正文条款pdf->json @@ -204,14 +204,12 @@ def goods_bid_main(output_folder, file_path, file_type, unique_id): collected_good_list = good_list # Store good_list for later use yield json.dumps({'base_info': transform_json_values(base_info)}, ensure_ascii=False) # 如果是 evaluation_standards,拆分技术标和商务标 - if key == 'evaluation_standards': + elif key == 'evaluation_standards': technical_standards = result["technical_standards"] commercial_standards = result["commercial_standards"] # 分别返回技术标和商务标 - yield json.dumps({'technical_standards': transform_json_values(technical_standards)}, - ensure_ascii=False) - yield json.dumps({'commercial_standards': transform_json_values(commercial_standards)}, - ensure_ascii=False) + yield json.dumps({'technical_standards': transform_json_values(technical_standards)},ensure_ascii=False) + yield json.dumps({'commercial_standards': transform_json_values(commercial_standards)},ensure_ascii=False) else: # 处理其他任务的结果 yield json.dumps({key: transform_json_values(result)}, ensure_ascii=False)