diff --git a/flask_app/general/static/插入位置.json b/flask_app/general/static/插入位置.json index b482f23..4482b6a 100644 --- a/flask_app/general/static/插入位置.json +++ b/flask_app/general/static/插入位置.json @@ -10,8 +10,7 @@ ], "关键字": [ "营业执照", - "自然人的身份证明", - "独立承担民事责任的能力" + "自然人的身份证明" ] }, "开户信息": { @@ -117,7 +116,7 @@ "施工项目" ] }, - "财务信息(财务审计报告)": { + "财务审计报告": { "章节": [ "提供依法缴纳税收和社会保障资金的相关材料", "具有依法缴纳税收和社会保障资金的良好记录", @@ -134,7 +133,7 @@ "财务会计报表" ] }, - "财务信息(缴纳税收证明)": { + "缴纳税收证明": { "章节": [ "提供依法缴纳税收和社会保障资金的相关材料", "具有依法缴纳税收和社会保障资金的良好记录", @@ -151,7 +150,7 @@ "税收缴纳证明" ] }, - "财务信息(公司缴纳社保证明)": { + "公司缴纳社保证明": { "章节": [ "投标人资质证明文件", "按照“供应商资格要求”规定提交的相关证明材料", diff --git a/flask_app/general/判断截取位置.py b/flask_app/general/判断截取位置.py index 4c52ff4..e4855c2 100644 --- a/flask_app/general/判断截取位置.py +++ b/flask_app/general/判断截取位置.py @@ -1,12 +1,101 @@ # -*- encoding:utf-8 -*- -import ast import json import re import os import sys -from flask_app.general.多线程提问 import multi_threading -from flask_app.general.通义千问long import upload_file +from pathlib import Path +from openai import OpenAI +import concurrent.futures +import queue +import time +def qianwen_long(file_id, user_query): + print("call qianwen-long...") + """ + Uses a previously uploaded file to generate a response based on a user query. + """ + client = OpenAI( + api_key=os.getenv("DASHSCOPE_API_KEY"), + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" + ) + + # Generate a response based on the file ID + completion = client.chat.completions.create( + model="qwen-long", + top_p=0.5, + temperature=0.4, + messages=[ + { + 'role': 'system', + 'content': f'fileid://{file_id}' + }, + { + 'role': 'user', + 'content': user_query + } + ], + stream=False + ) + + # Return the response content + return completion.choices[0].message.content + +def llm_call(question, knowledge_name,file_id, result_queue, ans_index, llm_type): + + if llm_type==2: + print(f"qianwen_long! question:{question}") + qianwen_res = qianwen_long(file_id,question) + result_queue.put((ans_index,(question,qianwen_res))) + return +def multi_threading(queries, knowledge_name="", file_id="", llm_type=1): + if not queries: + return [] + + print("多线程提问:starting multi_threading...") + result_queue = queue.Queue() + + # 使用 ThreadPoolExecutor 管理线程 + with concurrent.futures.ThreadPoolExecutor(max_workers=15) as executor: + # 逐个提交任务,每提交一个任务后休眠1秒 + future_to_query = {} + for index, query in enumerate(queries): + future = executor.submit(llm_call, query, knowledge_name, file_id, result_queue, index, llm_type) + future_to_query[future] = index + time.sleep(1) # 每提交一个任务后等待1秒 + + # 收集每个线程的结果 + for future in concurrent.futures.as_completed(future_to_query): + index = future_to_query[future] + try: + future.result() # 捕获异常或确认任务完成 + except Exception as exc: + print(f"Query {index} generated an exception: {exc}") + # 确保在异常情况下也向 result_queue 添加占位符 + result_queue.put((index, None)) + + # 从队列中获取所有结果并按索引排序 + results = [None] * len(queries) + while not result_queue.empty(): + index, result = result_queue.get() + results[index] = result + # 检查是否所有结果都是 None + if all(result is None for result in results): + return [] + # 过滤掉None值 + results = [r for r in results if r is not None] + # 返回一个保证是列表的结构 + return results + +def upload_file(file_path): + """ + Uploads a file to DashScope and returns the file ID. + """ + client = OpenAI( + api_key=os.getenv("DASHSCOPE_API_KEY"), + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" + ) + file = client.files.create(file=Path(file_path), purpose="file-extract") + return file.id def load_data(json_path): """ @@ -47,9 +136,9 @@ def define_target_names(): # "劳动合同", "企业证书", "企业业绩", - "财务信息(财务审计报告)", - "财务信息(缴纳税收证明)", - "财务信息(公司缴纳社保证明)" + "财务审计报告", + "缴纳税收证明", + "公司缴纳社保证明" ] @@ -65,9 +154,14 @@ def generate_user_query(target, chapters, keywords): Returns: str: 生成的用户查询字符串。 """ - template3 = f"""这是投标文件模板,作为投标人,我需要把不同的投标材料填充到对应位置,请你根据该文件回答:{target}应该插入在该文件哪个地方?你可能需要查找以下关键词出现的地方:{', '.join([f"'{kw}'" for kw in keywords])},并确认插入的位置。我已在原文中打上若干待插入位置的标记,形如'[$$第17个可插入位置$$]',它的标记与它上面的小节内容关联。你需要返回给我{target}应该插入位置的标记序号,即'[$$第17个可插入位置$$]'中的'17',而不是页码,若有多个位置需要插入,可以返回多个序号,你的回答以数组返回,如[17, 19],若插入位置不明确,那么返回[-1]。 + template3 = f"""这是投标文件模板,作为投标人,我需要把不同的投标材料填充到对应位置,请你根据该文件回答:{target}应该插入在该文件哪个地方?你可能需要查找以下关键词出现的地方:{', '.join([f"'{kw}'" for kw in keywords])},并确认插入的位置。我已在原文中打上若干待插入位置的标记,形如'[$$第5个待插入位置$$]',它的标记与它上面的小节内容关联。你需要返回给我{target}应该插入位置的标记序号,即'[$$第5个待插入位置$$]'中的'5',而不是页码,若有多个位置需要插入,可以返回多个序号,你的回答以数组返回,如[17, 19],若插入位置不明确,那么返回[-1]。 """ - return template3 + template2 = f"""你是一个专业撰写投标文件的专家,这是投标文件模板,作为投标人,你需要把不同的投标材料填充到对应位置,请你根据该文件回答:{target}应该插入在该文件哪个地方?你可能需要查找以下关键词出现的地方:{', '.join([f"'{kw}'" for kw in keywords])},或者根据文中'备注'或'附'内的信息确认插入的位置。目前原文中各章节末尾已打上待插入位置的标记,标记格式为'[$$当前章节名:第x个待插入位置$$]'形如'[$$四、投标保证金:第4个待插入位置$$]',每个标记与它紧跟着的上面的这一章内容关联。你需要返回给我{target}应该插入位置的标记序号,即'[$$四、投标保证金:第4个待插入位置$$]'中的'4',而不是当前页码,若有多个位置需要插入,可以返回多个序号,你的回答以数组返回,如[4, 5],若插入位置不明确,那么返回[-1]。 + """ + template1 = f"""这是投标文件模板,作为投标人,我需要把不同的投标材料填充到对应位置,请你根据该文件回答:{target}应该插入在该文件哪个地方?你可能需要查找以下关键词出现的地方:{', '.join([f"'{kw}'" for kw in keywords])},并确认插入的位置。我已在原文中各章节末尾打上待插入位置的标记,标记格式为'[$$当前章节名:第x个待插入位置$$]'形如'[$$四、投标保证金:第4个待插入位置$$]',每个标记与它紧跟着的上面的这一章内容关联。你需要返回给我{target}应该插入位置的标记数字序号,即'[$$四、投标保证金:第4个待插入位置$$]'中的'4',而不是当前页码,若有多个位置需要插入,可以返回多个序号,你的回答以数组返回,如[4, 5],若插入位置不明确,那么返回[-1]。 + """ + + return template1 def generate_user_queries(target_names, data_dict): @@ -140,8 +234,8 @@ def main(): queries = [item['query'] for item in user_query_list] # 定义文件路径 - format_part = "C:\\Users\\Administrator\\Desktop\\outzb2 (2).pdf" - + format_part = "C:\\Users\\Administrator\\Desktop\\bid_format.pdf" + # format_part="C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles\\新建文件夹\\bid_format.docx" # 检查文件是否存在 if not os.path.isfile(format_part): print(f"错误:文件未找到 - {format_part}") @@ -165,8 +259,8 @@ def main(): baseinfo_list = [process_string_list(res) for _, res in results] # 输出结果 - for info in baseinfo_list: - print(f'{target_names}:{info}') + for i, info in enumerate(baseinfo_list): + print(f'{target_names[i]}:{info}') if __name__ == "__main__": main() diff --git a/flask_app/general/截取文件格式.py b/flask_app/general/截取文件格式.py index bf0b032..b093ed7 100644 --- a/flask_app/general/截取文件格式.py +++ b/flask_app/general/截取文件格式.py @@ -293,8 +293,8 @@ def truncate_pdf_main(input_path, output_folder, selection,begin_page=10): if __name__ == "__main__": # 定义输入和输出路径 - input_path = "C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles" - # input_path="C:\\Users\\Administrator\\Desktop\\招标文件\\招标test文件夹\\zbtest13.pdf" + # input_path = "C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles" + input_path="C:\\Users\\Administrator\\Desktop\\招标文件\\招标test文件夹\\zbtest2.pdf" output_folder = "C:\\Users\\Administrator\\Desktop\\货物标\\zbfiles\\新建文件夹" selection = 1 # 1 - 投标文件格式 # 执行截取 diff --git a/flask_app/main/截取pdf.py b/flask_app/main/截取pdf.py index a88a97c..289ec7d 100644 --- a/flask_app/main/截取pdf.py +++ b/flask_app/main/截取pdf.py @@ -1,16 +1,18 @@ +import re +import os from PyPDF2 import PdfReader, PdfWriter -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 +logger = None + def clean_page_content(text, common_header): # 首先删除抬头公共部分 if common_header: # 确保有公共抬头才进行替换 @@ -21,15 +23,12 @@ def clean_page_content(text, common_header): # 删除页码 eg:89/129 这个代码分三步走可以把89/129完全删除 text = re.sub(r'^\s*\d+\s*(?=\D)', '', text) # 删除开头的页码,仅当紧跟非数字字符时 - text = re.sub(r'\s+\d+\s*$', '', text) # 删除结尾的页码 - text = re.sub(r'\s*\/\s*\d+\s*', '', text) # 删除形如 /129 的页码 + text = re.sub(r'\s+\d+\s*$', '', text) # 删除结尾的页码 + text = re.sub(r'\s*\/\s*\d+\s*', '', text) # 删除形如 /129 的页码 text = re.sub(r'\s*[—-]\s*\d+\s*[—-]\s*', '', text) # 删除形如 '—2—' 或 '-2-' 的页码 return text -#PYPDF2库 def extract_common_header(pdf_path): - from PyPDF2 import PdfReader - pdf_document = PdfReader(pdf_path) headers = [] total_pages = len(pdf_document.pages) @@ -66,22 +65,7 @@ def extract_common_header(pdf_path): return '\n'.join(common_headers) - -def save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page,common_header): - """ - 从原始PDF截取指定范围的页面并保存到新的PDF文件中。 - 如果 output_suffix 是 'notice',则额外保存 start_page 之前的页面。 - - 参数: - pdf_path (str): 原始PDF文件路径。 - output_folder (str): 输出文件夹路径。 - output_suffix (str): 输出文件的后缀,用于区分不同的提取。 - start_page (int): 起始页码(0基)。 - end_page (int): 结束页码(0基)。 - - 返回: - str: 保存的PDF文件路径。如果提取失败,返回空字符串。 - """ +def save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page, common_header): try: # 获取文件基本名称 base_file_name = os.path.splitext(os.path.basename(pdf_path))[0] @@ -138,98 +122,135 @@ def save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, en print(f"Error in save_pages_to_new_pdf: {e}") return "" # 返回空字符串 - -def extract_pages_twice(pdf_path, output_folder, output_suffix): - common_header = extract_common_header(pdf_path) - last_begin_index = 0 - begin_pattern = re.compile(r'第[一二三四五六七八九十]+章\s*招标公告|第一卷|投标邀请书') - pdf_document = PdfReader(pdf_path) - for i, page in enumerate(pdf_document.pages): - text = page.extract_text() - if text: - cleaned_text = clean_page_content(text,common_header) - # 检查“第一章”开始的位置 - if begin_pattern.search(cleaned_text): - last_begin_index = i # 更新最后匹配的索引,页码从0开始 - if output_suffix == "qualification": - common_pattern = r'^(?:附录(?:一)?[::]|附件(?:一)?[::]|附表(?:一)?[::])' - # end_pattern = r'^(第[一二三四五六七八九十]+章\s*投标人须知|评标办法|评标办法前附表)' - end_pattern = re.compile( - common_pattern + r'(?!.*(?:资质|能力|信誉)).*$|' # 排除资质、能力、信誉的描述 - r'^(第[一二三四五六七八九十]+章\s*评标办法|评标办法前附表|投标人须知)', # 新增的匹配项 - re.MULTILINE - ) +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[last_begin_index:], start=last_begin_index): - text = page.extract_text() - if text: - cleaned_text = clean_page_content(text,common_header) - # 确定起始页,需在last_begin_index之后 - if ("资格审查" in cleaned_text or "资质条件" in cleaned_text): - if re.search(common_pattern, cleaned_text, re.MULTILINE): - if start_page is None: - start_page = i # 确保起始页不小于章节的开始页码 - # 确定结束页 - if start_page is not None and re.search(end_pattern, cleaned_text): - if i > start_page: - end_page = i - break # 找到结束页后退出循环 + for i, page in enumerate(pdf_document.pages): + text = page.extract_text() or "" + cleaned_text = clean_page_content(text, common_header) - if start_page is None or end_page is None: - print(f"{output_suffix} twice: 未找到起始或结束页在文件 {pdf_path} 中!") - return "" - else: - return save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page,common_header) - elif output_suffix == "invalid": - pdf_document = PdfReader(pdf_path) - total_pages = len(pdf_document.pages) - # 计算总页数的三分之二 - total = int(total_pages * 2 / 3) - start_page = last_begin_index - end_page = min(90, total) - return save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page,common_header) + if exclusion_pattern and re.search(exclusion_pattern, cleaned_text) and mid_page is not None: + continue + 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 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() + + return start_page, mid_page, end_page def extract_pages(pdf_path, output_folder, begin_pattern, begin_page, end_pattern, output_suffix): - common_header=extract_common_header(pdf_path) # 打开PDF文件 + common_header = extract_common_header(pdf_path) pdf_document = PdfReader(pdf_path) start_page = None end_page = None + # 遍历文档的每一页,查找开始和结束短语的位置 for i in range(len(pdf_document.pages)): page = pdf_document.pages[i] text = page.extract_text() if text: - cleaned_text = clean_page_content(text,common_header) - # print(cleaned_text) + cleaned_text = clean_page_content(text, common_header) if re.search(begin_pattern, cleaned_text) and i > begin_page: - if output_suffix == "invalid" and start_page: # 仅当提取Invalid的时候,判断初始页码是第一个匹配到的页码,因为招标编号可能存在多个,后面的覆盖前面 + if output_suffix == "invalid" and start_page: continue else: start_page = i if start_page is not None and re.search(end_pattern, cleaned_text): - # 如果output_suffix是"qualification",调整条件检查 condition = i > start_page if condition: is_invalid_condition = output_suffix == "invalid" and i > 30 # 这边默认无效投标至少有30页 if is_invalid_condition or output_suffix != "invalid": end_page = i - break + break # 找到结束页后退出循环 - # 确保找到了起始和结束页面 + # 移除对 extract_pages_twice 的调用 if start_page is None or end_page is None: - if output_suffix == "qualification" or output_suffix =="invalid": - return extract_pages_twice(pdf_path, output_folder, output_suffix) - else: - print(f"{output_suffix} first: 未找到起始或结束页在文件 {pdf_path} 中!") - return "" + print(f"{output_suffix} first: 未找到起始或结束页在文件 {pdf_path} 中!") + return [] else: - return save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page,common_header) - + return save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page, common_header) def process_input(input_path, output_folder, begin_pattern, begin_page, end_pattern, output_suffix): # 确保输出文件夹存在 @@ -241,87 +262,204 @@ def process_input(input_path, output_folder, begin_pattern, begin_page, end_patt for file in os.listdir(input_path): if file.endswith(".pdf"): pdf_path = os.path.join(input_path, file) - output_pdf_path = extract_pages(pdf_path, output_folder, begin_pattern, begin_page, end_pattern, - output_suffix) + output_pdf_path = extract_pages(pdf_path, output_folder, begin_pattern, begin_page, end_pattern, output_suffix) if output_pdf_path and os.path.isfile(output_pdf_path): generated_files.append(output_pdf_path) return generated_files elif os.path.isfile(input_path) and input_path.endswith(".pdf"): # 处理单个PDF文件 - output_pdf_path = extract_pages(input_path, output_folder, begin_pattern, begin_page, end_pattern, - output_suffix) + output_pdf_path = extract_pages(input_path, output_folder, begin_pattern, begin_page, end_pattern, output_suffix) if output_pdf_path and os.path.isfile(output_pdf_path): return [output_pdf_path] # 以列表形式返回,以保持一致性 else: print("提供的路径既不是文件夹也不是PDF文件。") return [] + def truncate_pdf_main(input_path, output_folder, selection): - try: + # Define a list to hold all possible (begin_pattern, end_pattern) pairs for the given selection + pattern_pairs = [] + output_suffix = "" + + # Define pattern pairs for each selection + if selection == 1: + # Selection 1: 投标人须知前附表 + pattern_pairs = [ + ( + re.compile(r'第[一二三四五六七八九十]+章\s*投标人须知'), + re.compile( + r'第[一二三四五六七八九十]+章\s*评标办法|^评标办法前附表|^附录(?:一)?[::]|^附件(?:一)?[::]|^附表(?:一)?[::]', + re.MULTILINE) + ) + ] + output_suffix = "tobidders_notice_table" + + elif selection == 2: + # Selection 2: 评标办法 + pattern_pairs = [ + ( + re.compile(r'^第[一二三四五六七八九十]+章\s*评标办法'), + re.compile(r'^评标办法正文|评标办法') + ), + ( + re.compile(r'第[一二三四五六七八九十]+章\s*评标办法'), # Alternative begin pattern + re.compile(r'评标办法正文|评标办法') # Alternative end pattern + ) + ] + output_suffix = "evaluation_method" + + elif selection == 3: + # Selection 3: 投标人须知正文 + pattern_pairs = [ + ( + re.compile(r'投标人须知正文'), + re.compile( + r'^第[一二三四五六七八九十]+章\s*评标办法|^评标办法前附表|^附录(?:一)?[::]|^附件(?:一)?[::]|^附表(?:一)?[::]', + re.MULTILINE) + ) + ] + output_suffix = "tobidders_notice" + + elif selection == 4: + # Selection 4: 资格审查条件 + pattern_pairs = [ + ( + re.compile(r'^(?:附录(?:一)?[::]|附件(?:一)?[::]|附表(?:一)?[::]).*(?:资质|能力|信誉).*$', + re.MULTILINE), + re.compile( + r'^(?:附录(?:一)?[::]|附件(?:一)?[::]|附表(?:一)?[::])(?!.*(?:资质|能力|信誉)).*$|' # Exclude specific terms + r'^(第[一二三四五六七八九十]+章\s*评标办法|评标办法前附表)', # Additional end patterns + re.MULTILINE + ) + ) + ] + output_suffix = "qualification" + + elif selection == 5: + # Selection 5: 招标公告 + pattern_pairs = [ + ( + re.compile(r'^第[一二三四五六七八九十]+章\s*(招标公告|.*邀请.*)|^第一卷|^投标邀请书'), + re.compile(r'第[一二三四五六七八九十]+章\s*投标人须知') + ) + ] + output_suffix = "notice" + + elif selection == 6: + # Selection 6: 无效标 + pattern_pairs = [ + ( + re.compile(r'第[一二三四五六七八九十]+章\s*(招标公告|.*邀请.*)|第一卷|招标编号:|招标编号:'), + re.compile(r'第[一二三四五六七八九十]+章\s*合同|[::]清标报告|^第二卷', re.MULTILINE) + ) + ] + output_suffix = "invalid" + + else: + print("无效的选择:请选择1-6") + return [""] + + # Iterate through each pattern pair and attempt extraction + for idx, (begin_pattern, end_pattern) in enumerate(pattern_pairs, start=1): + print(f"Selection {selection}: 尝试使用模式对 {idx}/{len(pattern_pairs)}") + # Determine begin_page based on selection if selection == 1: - # Configure patterns and phrases for "投标人须知前附表" - begin_pattern = re.compile(r'第[一二三四五六七八九十]+章\s*投标人须知') begin_page = 3 - end_pattern = re.compile(r'投标人须知正文') - output_suffix = "tobidders_notice_table" elif selection == 2: - # Configure patterns and phrases for "评标办法" - begin_pattern = re.compile( - r'第[一二三四五六七八九十]+章\s*评标办法') # 考虑到这种情况 '第三章 第三章 第三章 第三章 评标办法 评标办法 评标办法 评标办法' begin_page = 10 - end_pattern = re.compile(r'评标办法正文|评标办法') - output_suffix = "evaluation_method" - elif selection == 3: - # Configure patterns and phrases for "投标人须知正文" - begin_pattern = re.compile(r'投标人须知正文') + elif selection in [3, 4]: begin_page = 5 - end_pattern = re.compile( - r'^第[一二三四五六七八九十]+章\s*评标办法|^评标办法前附表|^附录(?:一)?[::]|^附件(?:一)?[::]|^附表(?:一)?[::]', - re.MULTILINE) - output_suffix = "tobidders_notice" - elif selection == 4: - # 配置用于 "资格审查条件" 的正则表达式模式和短语 + else: + begin_page = 0 + if selection == 1: + output_paths=extract_pages_tobidders_notice() + output_paths = process_input( + input_path, + output_folder, + begin_pattern, + begin_page=begin_page, + end_pattern=end_pattern, + output_suffix=output_suffix + ) + + # Check if extraction was successful + if output_paths and any(os.path.isfile(f) for f in output_paths): + print(f"Selection {selection}: 使用模式对 {idx} 成功提取。") + return output_paths # Return immediately upon successful extraction + else: + print(f"Selection {selection}: 使用模式对 {idx} 失败。尝试下一个模式对。") + + # If all pattern pairs failed, attempt a fallback + print(f"Selection {selection}: 所有模式对都未匹配成功。尝试执行 extract_pages_twice 或返回空。") + fallback_result = extract_pages_twice(input_path, output_folder, output_suffix) + if fallback_result and any(os.path.isfile(f) for f in fallback_result): + return fallback_result + else: + print(f"{output_suffix} first: 未找到起始或结束页在文件 {input_path} 中!") + return [''] + +def extract_pages_twice(pdf_path, output_folder, output_suffix): + """ + 处理 PDF 文件,作为 truncate_pdf_main 的后备方法。 + 此函数将在所有模式对都失败后调用。 + """ + try: + common_header = extract_common_header(pdf_path) + last_begin_index = 0 + begin_pattern = re.compile(r'第[一二三四五六七八九十]+章\s*招标公告|第一卷|投标邀请书') + pdf_document = PdfReader(pdf_path) + for i, page in enumerate(pdf_document.pages): + text = page.extract_text() + if text: + cleaned_text = clean_page_content(text, common_header) + if begin_pattern.search(cleaned_text): + last_begin_index = i # 更新最后匹配的索引,页码从0开始 + + if output_suffix == "qualification": common_pattern = r'^(?:附录(?:一)?[::]|附件(?:一)?[::]|附表(?:一)?[::])' - begin_pattern = re.compile(common_pattern + r'.*(?:资质|能力|信誉).*$', re.MULTILINE) - begin_page = 5 end_pattern = re.compile( - common_pattern + r'(?!.*(?:资质|能力|信誉)).*$|' # 原有的模式 - r'^(第[一二三四五六七八九十]+章\s*评标办法|评标办法前附表)', # 新增的匹配项 + common_pattern + r'(?!.*(?:资质|能力|信誉)).*$|' # 排除资质、能力、信誉的描述 + r'^(第[一二三四五六七八九十]+章\s*评标办法|评标办法前附表|投标人须知)', re.MULTILINE ) - output_suffix = "qualification" - elif selection == 5: - # 配置用于 "招标公告" 的正则表达式模式和短语 - begin_pattern = re.compile(r'^第[一二三四五六七八九十]+章\s*(招标公告|.*邀请.*)|^第一卷|^投标邀请书') - begin_page = 0 - end_pattern = re.compile(r'第[一二三四五六七八九十]+章\s*投标人须知') - output_suffix = "notice" - elif selection == 6: - # 配置用于 "无效标" 的正则表达式模式和短语 - begin_pattern = re.compile(r'第[一二三四五六七八九十]+章\s*(招标公告|.*邀请.*)|第一卷|招标编号:|招标编号:') - begin_page = 0 - end_pattern = re.compile(r'第[一二三四五六七八九十]+章\s*合同|[::]清标报告|^第二卷', re.MULTILINE) - output_suffix = "invalid" + start_page = None + end_page = None + + # 从章节开始后的位置进行检查 + for i, page in enumerate(pdf_document.pages[last_begin_index:], start=last_begin_index): + text = page.extract_text() + if text: + cleaned_text = clean_page_content(text, common_header) + # 确定起始页,需在last_begin_index之后 + if ("资格审查" in cleaned_text or "资质条件" in cleaned_text): + if re.search(common_pattern, cleaned_text, re.MULTILINE): + if start_page is None: + start_page = i # 确保起始页不小于章节的开始页码 + # 确定结束页 + if start_page is not None and re.search(end_pattern, cleaned_text): + if i > start_page: + end_page = i + break # 找到结束页后退出循环 + + if start_page is None or end_page is None: + print(f"{output_suffix} twice: 未找到起始或结束页在文件 {pdf_path} 中!") + return [] + else: + return save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page, common_header) + elif output_suffix == "invalid": + pdf_document = PdfReader(pdf_path) + total_pages = len(pdf_document.pages) + # 计算总页数的三分之二 + total = int(total_pages * 2 / 3) + start_page = last_begin_index + end_page = min(90, total) + return save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page, common_header) else: - print("无效的选择:请选择1-6") - return [""] - # 调用 process_input 进行截取 - output_paths = process_input(input_path, output_folder, begin_pattern, begin_page, end_pattern, output_suffix) - if not output_paths: - return [""] # 截取失败时返回空字符串 - return output_paths + print(f"{output_suffix} twice: 未定义的输出后缀。") + return [] except Exception as e: - print(f"Error in truncate_pdf_main for selection {selection}: {e}") - return [""] # 捕获异常时返回空字符串 - - - -def truncate_pdf_multiple_old(input_path, output_folder): - truncate_files = [] - for selection in range(1, 5): - files = truncate_pdf_main(input_path, output_folder, selection) - truncate_files.extend(files) - return truncate_files + print(f"Error in extract_pages_twice: {e}") + return [] def merge_selected_pdfs(output_folder, truncate_files, output_path, base_file_name): """ @@ -399,7 +537,6 @@ def merge_selected_pdfs(output_folder, truncate_files, output_path, base_file_na print(f"合并 PDF 文件时出错: {e}") return "" - def truncate_pdf_multiple(input_path, output_folder, unique_id="123"): """ 处理 PDF 文件,选择 selection 1-6 的部分,并合并结果。 @@ -453,7 +590,6 @@ def truncate_pdf_multiple(input_path, output_folder, unique_id="123"): return truncate_files - def truncate_pdf_specific_engineering(pdf_path, output_folder, selections, unique_id="123"): """ 处理 PDF 文件,选择指定的 selections,并合并结果。 @@ -514,19 +650,19 @@ 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:需要完善二次请求。目前invalid一定能返回 前附表 须知正文如果为空的话要额外处理一下,比如说就不进行跳转(见xx表) 开评定标这里也要考虑 如果评分表为空,也要处理。 #TODO:zbtest8 zbtest18有问题 后期需要完善,截取需要截两次,第一次严格第二次宽松 +#投标人须知前附表改为货物标一样的 if __name__ == "__main__": - input_path = "C:\\Users\\Administrator\\Desktop\\招标文件\\招标test文件夹\\zbtest4_evaluation_method.pdf" # 可以是单个PDF文件路径或文件夹路径 + input_path = "C:\\Users\\Administrator\\Desktop\\招标文件\\招标test文件夹\\zbtest3.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) + # files=truncate_pdf_multiple(input_path,output_folder) # selections = [5, 1] # 仅处理 selection 5、1 和 3 # files=truncate_pdf_specific_engineering(input_path,output_folder,selections) - print(files) - # selection = 6 # 例如:1 - 投标人须知前附表, 2 - 评标办法, 3 - 投标人须知正文 4-资格审查条件 5-招标公告 6-无效标 - # generated_files = truncate_pdf_main(input_path, output_folder, selection) + # print(files) + selection = 1 # 例如:1 - 投标人须知前附表, 2 - 评标办法, 3 - 投标人须知正文 4-资格审查条件 5-招标公告 6-无效标 + generated_files = truncate_pdf_main(input_path, output_folder, selection) # print(generated_files) # print("生成的文件:", generated_files) \ No newline at end of file diff --git a/flask_app/main/截取pdf_old.py b/flask_app/main/截取pdf_old.py new file mode 100644 index 0000000..a88a97c --- /dev/null +++ b/flask_app/main/截取pdf_old.py @@ -0,0 +1,532 @@ +from PyPDF2 import PdfReader, PdfWriter +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: # 确保有公共抬头才进行替换 + for header_line in common_header.split('\n'): + if header_line.strip(): # 只处理非空行 + # 替换首次出现的完整行 + text = re.sub(r'^' + re.escape(header_line.strip()) + r'\n?', '', text, count=1) + + # 删除页码 eg:89/129 这个代码分三步走可以把89/129完全删除 + text = re.sub(r'^\s*\d+\s*(?=\D)', '', text) # 删除开头的页码,仅当紧跟非数字字符时 + text = re.sub(r'\s+\d+\s*$', '', text) # 删除结尾的页码 + text = re.sub(r'\s*\/\s*\d+\s*', '', text) # 删除形如 /129 的页码 + text = re.sub(r'\s*[—-]\s*\d+\s*[—-]\s*', '', text) # 删除形如 '—2—' 或 '-2-' 的页码 + return text + +#PYPDF2库 +def extract_common_header(pdf_path): + from PyPDF2 import PdfReader + + pdf_document = PdfReader(pdf_path) + headers = [] + total_pages = len(pdf_document.pages) + + # 确定要读取的页数和起始页 + if total_pages == 2: + pages_to_read = 2 + start_page = 0 + else: + pages_to_read = 3 + middle_page = total_pages // 2 + start_page = max(0, middle_page - 1) + + for i in range(start_page, min(start_page + pages_to_read, total_pages)): + page = pdf_document.pages[i] + text = page.extract_text() or "" + if text: + # 只取每页的前三行 + first_lines = text.strip().split('\n')[:3] + headers.append(first_lines) + + if len(headers) < 2: + return "" # 如果没有足够的页来比较,返回空字符串 + + # 寻找每一行中的公共部分,按顺序保留 + common_headers = [] + for lines in zip(*headers): + # 提取第一行的词汇顺序 + first_words = lines[0].split() + # 筛选所有页面都包含的词汇,保持顺序 + common_line = [word for word in first_words if all(word in line.split() for line in lines[1:])] + if common_line: + common_headers.append(' '.join(common_line)) + + return '\n'.join(common_headers) + + +def save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page,common_header): + """ + 从原始PDF截取指定范围的页面并保存到新的PDF文件中。 + 如果 output_suffix 是 'notice',则额外保存 start_page 之前的页面。 + + 参数: + pdf_path (str): 原始PDF文件路径。 + output_folder (str): 输出文件夹路径。 + output_suffix (str): 输出文件的后缀,用于区分不同的提取。 + start_page (int): 起始页码(0基)。 + end_page (int): 结束页码(0基)。 + + 返回: + str: 保存的PDF文件路径。如果提取失败,返回空字符串。 + """ + try: + # 获取文件基本名称 + 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") + + # 读取PDF文件 + pdf_document = PdfReader(pdf_path) + total_pages = len(pdf_document.pages) + + # 检查起始和结束页码是否有效 + if start_page < 0 or end_page >= total_pages or start_page > end_page: + print(f"无效的页面范围: {start_page} 到 {end_page}") + return "" + + # 如果 output_suffix 是 'notice',保存 start_page 之前的页面(若遇到'目录',则截取到'目录') + if output_suffix == 'notice' and start_page > 0: + before_pdf_path = os.path.join(output_folder, f"{base_file_name}_before.pdf") + before_doc = PdfWriter() + toc_page = -1 + + # 查找目录页 + for page_num in range(min(start_page, total_pages)): + page_text = pdf_document.pages[page_num].extract_text() + cleaned_text = clean_page_content(page_text, common_header) + if re.search(r'目\s*录', cleaned_text, re.MULTILINE): + toc_page = page_num + break + + # 确定截取的页数 + pages_to_extract = toc_page + 1 if toc_page != -1 else start_page + + # 提取页面 + for page_num in range(pages_to_extract): + before_doc.add_page(pdf_document.pages[page_num]) + with open(before_pdf_path, 'wb') as f_before: + before_doc.write(f_before) + print(f"已保存页面从 0 到 {pages_to_extract} 为 {before_pdf_path}") + + # 提取指定范围的页面 + output_doc = PdfWriter() + for page_num in range(start_page, end_page + 1): + output_doc.add_page(pdf_document.pages[page_num]) + + # 保存新的PDF文件 + with open(output_pdf_path, 'wb') as f_output: + output_doc.write(f_output) + + print(f"{output_suffix} 已截取并保存页面从 {start_page + 1} 到 {end_page + 1} 为 {output_pdf_path}") + return output_pdf_path + + except Exception as e: + print(f"Error in save_pages_to_new_pdf: {e}") + return "" # 返回空字符串 + + +def extract_pages_twice(pdf_path, output_folder, output_suffix): + common_header = extract_common_header(pdf_path) + last_begin_index = 0 + begin_pattern = re.compile(r'第[一二三四五六七八九十]+章\s*招标公告|第一卷|投标邀请书') + pdf_document = PdfReader(pdf_path) + for i, page in enumerate(pdf_document.pages): + text = page.extract_text() + if text: + cleaned_text = clean_page_content(text,common_header) + # 检查“第一章”开始的位置 + if begin_pattern.search(cleaned_text): + last_begin_index = i # 更新最后匹配的索引,页码从0开始 + if output_suffix == "qualification": + common_pattern = r'^(?:附录(?:一)?[::]|附件(?:一)?[::]|附表(?:一)?[::])' + # end_pattern = r'^(第[一二三四五六七八九十]+章\s*投标人须知|评标办法|评标办法前附表)' + end_pattern = re.compile( + common_pattern + r'(?!.*(?:资质|能力|信誉)).*$|' # 排除资质、能力、信誉的描述 + r'^(第[一二三四五六七八九十]+章\s*评标办法|评标办法前附表|投标人须知)', # 新增的匹配项 + re.MULTILINE + ) + start_page = None + end_page = None + + # 从章节开始后的位置进行检查 + for i, page in enumerate(pdf_document.pages[last_begin_index:], start=last_begin_index): + text = page.extract_text() + if text: + cleaned_text = clean_page_content(text,common_header) + # 确定起始页,需在last_begin_index之后 + if ("资格审查" in cleaned_text or "资质条件" in cleaned_text): + if re.search(common_pattern, cleaned_text, re.MULTILINE): + if start_page is None: + start_page = i # 确保起始页不小于章节的开始页码 + # 确定结束页 + if start_page is not None and re.search(end_pattern, cleaned_text): + if i > start_page: + end_page = i + break # 找到结束页后退出循环 + + if start_page is None or end_page is None: + print(f"{output_suffix} twice: 未找到起始或结束页在文件 {pdf_path} 中!") + return "" + else: + return save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page,common_header) + elif output_suffix == "invalid": + pdf_document = PdfReader(pdf_path) + total_pages = len(pdf_document.pages) + # 计算总页数的三分之二 + total = int(total_pages * 2 / 3) + start_page = last_begin_index + end_page = min(90, total) + return save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page,common_header) + + +def extract_pages(pdf_path, output_folder, begin_pattern, begin_page, end_pattern, output_suffix): + common_header=extract_common_header(pdf_path) + # 打开PDF文件 + pdf_document = PdfReader(pdf_path) + start_page = None + end_page = None + # 遍历文档的每一页,查找开始和结束短语的位置 + for i in range(len(pdf_document.pages)): + page = pdf_document.pages[i] + text = page.extract_text() + if text: + cleaned_text = clean_page_content(text,common_header) + # print(cleaned_text) + if re.search(begin_pattern, cleaned_text) and i > begin_page: + if output_suffix == "invalid" and start_page: # 仅当提取Invalid的时候,判断初始页码是第一个匹配到的页码,因为招标编号可能存在多个,后面的覆盖前面 + continue + else: + start_page = i + if start_page is not None and re.search(end_pattern, cleaned_text): + # 如果output_suffix是"qualification",调整条件检查 + condition = i > start_page + if condition: + is_invalid_condition = output_suffix == "invalid" and i > 30 # 这边默认无效投标至少有30页 + if is_invalid_condition or output_suffix != "invalid": + end_page = i + break + + # 确保找到了起始和结束页面 + if start_page is None or end_page is None: + if output_suffix == "qualification" or output_suffix =="invalid": + return extract_pages_twice(pdf_path, output_folder, output_suffix) + else: + print(f"{output_suffix} first: 未找到起始或结束页在文件 {pdf_path} 中!") + return "" + else: + return save_pages_to_new_pdf(pdf_path, output_folder, output_suffix, start_page, end_page,common_header) + + +def process_input(input_path, output_folder, begin_pattern, begin_page, end_pattern, output_suffix): + # 确保输出文件夹存在 + if not os.path.exists(output_folder): + os.makedirs(output_folder) + if os.path.isdir(input_path): + generated_files = [] + # 遍历文件夹内的所有PDF文件 + for file in os.listdir(input_path): + if file.endswith(".pdf"): + pdf_path = os.path.join(input_path, file) + output_pdf_path = extract_pages(pdf_path, output_folder, begin_pattern, begin_page, end_pattern, + output_suffix) + if output_pdf_path and os.path.isfile(output_pdf_path): + generated_files.append(output_pdf_path) + return generated_files + elif os.path.isfile(input_path) and input_path.endswith(".pdf"): + # 处理单个PDF文件 + output_pdf_path = extract_pages(input_path, output_folder, begin_pattern, begin_page, end_pattern, + output_suffix) + if output_pdf_path and os.path.isfile(output_pdf_path): + return [output_pdf_path] # 以列表形式返回,以保持一致性 + else: + print("提供的路径既不是文件夹也不是PDF文件。") + return [] + +def truncate_pdf_main(input_path, output_folder, selection): + try: + if selection == 1: + # Configure patterns and phrases for "投标人须知前附表" + begin_pattern = re.compile(r'第[一二三四五六七八九十]+章\s*投标人须知') + begin_page = 3 + end_pattern = re.compile(r'投标人须知正文') + output_suffix = "tobidders_notice_table" + elif selection == 2: + # Configure patterns and phrases for "评标办法" + begin_pattern = re.compile( + r'第[一二三四五六七八九十]+章\s*评标办法') # 考虑到这种情况 '第三章 第三章 第三章 第三章 评标办法 评标办法 评标办法 评标办法' + begin_page = 10 + end_pattern = re.compile(r'评标办法正文|评标办法') + output_suffix = "evaluation_method" + elif selection == 3: + # Configure patterns and phrases for "投标人须知正文" + begin_pattern = re.compile(r'投标人须知正文') + begin_page = 5 + end_pattern = re.compile( + r'^第[一二三四五六七八九十]+章\s*评标办法|^评标办法前附表|^附录(?:一)?[::]|^附件(?:一)?[::]|^附表(?:一)?[::]', + re.MULTILINE) + output_suffix = "tobidders_notice" + elif selection == 4: + # 配置用于 "资格审查条件" 的正则表达式模式和短语 + common_pattern = r'^(?:附录(?:一)?[::]|附件(?:一)?[::]|附表(?:一)?[::])' + begin_pattern = re.compile(common_pattern + r'.*(?:资质|能力|信誉).*$', re.MULTILINE) + begin_page = 5 + end_pattern = re.compile( + common_pattern + r'(?!.*(?:资质|能力|信誉)).*$|' # 原有的模式 + r'^(第[一二三四五六七八九十]+章\s*评标办法|评标办法前附表)', # 新增的匹配项 + re.MULTILINE + ) + output_suffix = "qualification" + elif selection == 5: + # 配置用于 "招标公告" 的正则表达式模式和短语 + begin_pattern = re.compile(r'^第[一二三四五六七八九十]+章\s*(招标公告|.*邀请.*)|^第一卷|^投标邀请书') + begin_page = 0 + end_pattern = re.compile(r'第[一二三四五六七八九十]+章\s*投标人须知') + output_suffix = "notice" + elif selection == 6: + # 配置用于 "无效标" 的正则表达式模式和短语 + begin_pattern = re.compile(r'第[一二三四五六七八九十]+章\s*(招标公告|.*邀请.*)|第一卷|招标编号:|招标编号:') + begin_page = 0 + end_pattern = re.compile(r'第[一二三四五六七八九十]+章\s*合同|[::]清标报告|^第二卷', re.MULTILINE) + output_suffix = "invalid" + else: + print("无效的选择:请选择1-6") + return [""] + # 调用 process_input 进行截取 + output_paths = process_input(input_path, output_folder, begin_pattern, begin_page, end_pattern, output_suffix) + if not output_paths: + return [""] # 截取失败时返回空字符串 + return output_paths + except Exception as e: + print(f"Error in truncate_pdf_main for selection {selection}: {e}") + return [""] # 捕获异常时返回空字符串 + + + +def truncate_pdf_multiple_old(input_path, output_folder): + truncate_files = [] + for selection in range(1, 5): + files = truncate_pdf_main(input_path, output_folder, selection) + truncate_files.extend(files) + return truncate_files + +def merge_selected_pdfs(output_folder, truncate_files, output_path, base_file_name): + """ + 合并 output_folder 中以 {base_file_name}_before.pdf 结尾的 PDF 文件, + 以及 truncate_files 中以指定后缀结尾的文件,按照指定顺序合并。 + + 参数: + - output_folder (str): 包含以 {base_file_name}_before.pdf 结尾的 PDF 文件的文件夹路径。 + - truncate_files (list): 包含 PDF 文件路径的列表。 + - output_path (str): 合并后的 PDF 文件保存路径。 + - base_file_name (str): 用于匹配文件名的基础名称。 + + 返回: + - str: 合并后的 PDF 文件路径,如果没有合并则返回 ""。 + """ + # 1. 获取 output_folder 中所有文件 + try: + all_output_files = os.listdir(output_folder) + except FileNotFoundError: + print(f"输出文件夹 '{output_folder}' 未找到。") + return "" + except PermissionError: + print(f"没有权限访问输出文件夹 '{output_folder}'。") + return "" + + # 2. 定义要选择的文件后缀及合并顺序,包括 before 文件 + desired_suffixes = [ + f'{base_file_name}_before.pdf', + f'{base_file_name}_notice.pdf', + f'{base_file_name}_tobidders_notice_table.pdf' + ] + + all_pdfs_to_merge = [] + + for suffix in desired_suffixes: + if suffix == f'{base_file_name}_before.pdf': + # 从 output_folder 中选择以 {base_file_name}_before.pdf 结尾的文件 + matching_files = [ + os.path.join(output_folder, f) + for f in all_output_files + if f.endswith(suffix) + ] + else: + # 从 truncate_files 中选择以指定后缀结尾的文件 + matching_files = [f for f in truncate_files if f.endswith(suffix)] + + if matching_files: + # 如果找到多个匹配的文件,按名称排序并添加 + matching_files_sorted = sorted(matching_files) + all_pdfs_to_merge.extend(matching_files_sorted) + for f in matching_files_sorted: + print(f"选中文件: {f}") + else: + print(f"没有找到以 '{suffix}' 结尾的文件。") + + print(f"总共将要合并的 PDF 文件数量: {len(all_pdfs_to_merge)}") + + if not all_pdfs_to_merge: + print("没有找到要合并的 PDF 文件。") + return "" + + # 过滤掉不存在或为空的文件路径 + all_pdfs_to_merge = [f for f in all_pdfs_to_merge if os.path.isfile(f)] + + if not all_pdfs_to_merge: + print("没有有效的 PDF 文件需要合并。") + return "" + + # 调用 merge_pdfs 函数进行合并 + try: + merge_pdfs(all_pdfs_to_merge, output_path) + print(f"已成功合并 PDF 文件到 '{output_path}'。") + return output_path + except Exception as e: + print(f"合并 PDF 文件时出错: {e}") + return "" + + +def truncate_pdf_multiple(input_path, output_folder, unique_id="123"): + """ + 处理 PDF 文件,选择 selection 1-6 的部分,并合并结果。 + + Args: + input_path (str): 要处理的 PDF 文件路径。 + output_folder (str): 截取后的文件保存文件夹路径。 + + 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 + + # 使用 ThreadPoolExecutor 进行多线程处理 + with concurrent.futures.ThreadPoolExecutor(max_workers=len(selections)) as executor: + # 提交所有任务并保持 selection 顺序 + future_to_selection = {selection: executor.submit(truncate_pdf_main, input_path, output_folder, selection) for selection in selections} + + # 按 selection 顺序收集结果 + for selection in selections: + future = future_to_selection.get(selection) + try: + files = future.result() + if files and any(f for f in files): + # 过滤空字符串 + 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: + logger.error(f"Selection {selection} 生成了一个异常: {e}") + truncate_files.append("") # 发生异常时添加空字符串 + + if any(f for f in truncate_files if f): # 检查是否有有效的文件路径 + merged_output_path = os.path.join(output_folder, f"{base_file_name}_merged_baseinfo.pdf") + merged_result = merge_selected_pdfs(output_folder, truncate_files, merged_output_path, base_file_name) + if merged_result: + truncate_files.append(merged_result) + logger.info(f"merged_baseinfo: 已生成合并文件: {merged_output_path}") + else: + truncate_files.append("") # 如果merged_result未生成,添加空字符串 + logger.warning("merged_baseinfo: 未生成合并文件,因为没有找到需要合并的 PDF 文件。") + else: + truncate_files.append("") # 如果没有文件需要合并,也添加空字符串 + logger.warning(f"merged_baseinfo: 没有文件需要合并 for {input_path}") + + return truncate_files + + +def truncate_pdf_specific_engineering(pdf_path, output_folder, selections, unique_id="123"): + """ + 处理 PDF 文件,选择指定的 selections,并合并结果。 + + Args: + 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 = [] + + # 使用 ThreadPoolExecutor 进行多线程处理 + with concurrent.futures.ThreadPoolExecutor(max_workers=len(selections)) as executor: + # 提交所有任务并保持 selection 顺序 + future_to_selection = {selection: executor.submit(truncate_pdf_main, pdf_path, output_folder, selection) for selection in selections} + + # 按 selection 顺序收集结果 + for selection in selections: + future = future_to_selection.get(selection) + try: + files = future.result() + if files and any(f for f in files): + # 过滤空字符串 + 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: + logger.error(f"Selection {selection} 生成了一个异常: {e}") + truncate_files.append("") # 发生异常时添加空字符串 + + if any(f for f in truncate_files if f): # 检查是否有有效的文件路径 + merged_output_path = os.path.join(output_folder, f"{base_file_name}_merged_specific.pdf") + merged_result = merge_selected_pdfs(output_folder, truncate_files, merged_output_path, base_file_name) + if merged_result: + truncate_files.append(merged_result) + logger.info(f"merged_specific: 已生成合并文件: {merged_output_path}") + else: + truncate_files.append("") # 如果 merged_result 未生成,添加空字符串 + logger.warning("merged_specific: 未生成合并文件,因为没有找到需要合并的 PDF 文件。") + else: + truncate_files.append("") # 如果没有文件需要合并,也添加空字符串 + logger.warning(f"merged_specific: 没有文件需要合并 for {pdf_path}") + + return truncate_files + + except Exception as e: + # 假设 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文件夹\\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) + # selections = [5, 1] # 仅处理 selection 5、1 和 3 + # files=truncate_pdf_specific_engineering(input_path,output_folder,selections) + print(files) + # selection = 6 # 例如:1 - 投标人须知前附表, 2 - 评标办法, 3 - 投标人须知正文 4-资格审查条件 5-招标公告 6-无效标 + # generated_files = truncate_pdf_main(input_path, output_folder, selection) + # print(generated_files) + # print("生成的文件:", generated_files) \ No newline at end of file diff --git a/flask_app/货物标/评分标准提取main.py b/flask_app/货物标/评分标准提取main.py index 0514316..4beba32 100644 --- a/flask_app/货物标/评分标准提取main.py +++ b/flask_app/货物标/评分标准提取main.py @@ -217,7 +217,7 @@ def combine_evaluation_standards(truncate_file): # user_query = "根据该文档中的评标办法前附表或者评分标准表,请你列出该文件的技术评分,商务评分,投标报价评审标准以及它们对应的具体评分要求,外层键名分别为'技术评分','商务评分','投标报价'。如果评分内容不是这3个,则返回文档中给定的评分内容以及它的评分要求,都以json的格式返回结果,如果该采购活动有多个包,则最外层键名为对应的包名。请不要回答有关资格审查的内容" user_query = ( """ - 根据该文档中的评标办法前附表,请你列出该文件的技术评分,商务评分,投标报价评审以及它们对应的具体评分要求,请以json格式返回结果,请在这三大块评分中分别用若干键值对表示具体要求,请精确到具体的评审项,内层的键名为'评分'及'要求',若这三大块评分中存在其他信息,则在相应评分大块中新增键名'备注'存放该信息,键值为具体的要求,否则不需要。如果评分内容(因素)不是这3个,则返回文档中给定的评分内容(因素)以及它们的具体评分要求,如果该采购活动有多个包,则最外层键名为对应的包名,否则不需要。不要回答有关资格审查的内容,若存在未知信息,填充'未知'。以下为示例输出: + 根据该文档中的评标办法前附表,请你列出该文件的技术评分,商务评分,投标报价评审以及它们对应的具体评分要求,请以json格式返回结果,请在这三大块评分中分别用若干键值对表示具体要求,请精确到具体的评审项,内层的键名为'评分'及'要求',若这三大块评分中存在其他信息,则在相应评分大块内部新增键名'备注'存放该信息,键值为具体的要求,否则不需要。如果评分内容(因素)不是这3个,则返回文档中给定的评分内容(因素)以及它们的具体评分要求,如果该采购活动有多个包,则最外层键名为对应的包名,否则不需要。不要回答有关资格审查的内容,若存在未知信息,填充'未知'。以下为示例输出: { "一包": { "技术评分": { @@ -255,7 +255,7 @@ def combine_evaluation_standards(truncate_file): ) # 执行第二个查询 evaluation_res = qianwen_long(file_id, user_query) - + print(evaluation_res) # 清理和处理响应 cleaned_evaluation_res = parse_json_with_duplicates(evaluation_res) #处理重复键名的情况 result_data = process_data_based_on_key(cleaned_evaluation_res) #处理不知名外键的情况