import json import os import re import regex from PyPDF2 import PdfReader from flask_app.general.截取pdf通用函数 import clean_page_content, extract_common_header def compare_headings(current, new): """ 比较两个标题的层次关系,并确保新标题比当前标题大且最高位数字差值不超过5。 因为默认新的序号是比旧的序号大的,同时也要防止一些特别大的序号在行首,如'2025年xxx' 错误地将'2025'匹配成序号了,事实上它只是正文的一部分。 参数: current (str): 当前标题,例如 "1.2.3" new (str): 新标题,例如 "1.3" 返回: bool: 如果新标题大于当前标题且最高位数字差值不超过3,则返回 True,否则返回 False """ # 使用过滤来确保只处理非空且为数字的部分 current_nums = [int(num) for num in current.split('.') if num.isdigit()] new_nums = [int(num) for num in new.split('.') if num.isdigit()] # 确保新标题的最高位数字不超过当前标题的最高位数字 + 3 if new_nums: if len(new_nums) > 0 and len(current_nums) > 0: if new_nums[0] > current_nums[0] + 3: return False # 比较数字序列以确定标题的层次关系 for c, n in zip(current_nums, new_nums): if n > c: return True elif n < c: return False # 如果新标题有更多层次,认为是新的子章节 return len(new_nums) > len(current_nums) def should_add_newline(content, keywords, max_length=20): #如果拼接后的字符串 content_str 中包含任何一个在 keywords 中的关键词,或者 content_str 的长度小于或等于 max_length,则返回 True,表示应该添加换行符。 content_str = ''.join(content).strip() return any(keyword in content_str for keyword in keywords) or len(content_str) <= max_length def handle_content_append(current_content, line_content, append_newline, keywords, in_special_section): if append_newline: if should_add_newline(current_content, keywords): #current_content:['费用承担'] line_content:'投标人准备和参加投标活动发生的费用自理。' current_content.append('\n') # 添加换行符 #current_content:['费用承担', '\n'] append_newline = False current_content.append(line_content) #current_content:['费用承担', '\n', '投标人准备和参加投标活动发生的费用自理。'] if in_special_section: current_content.append('\n') return append_newline def parse_text_by_heading(text): keywords = ['包含', '以下'] data = {} current_key = None current_key_chinese = None current_value_chinese = None current_content = [] append_newline = False skip_subheadings = False last_main_number = None temp_title = None # 临时存储以点号开头但不带数字的标题 # 定义所有需要的正则表达式模式 pattern_numbered = re.compile(r'^\s*([一二三四五六七八九十]{1,2})\s*、\s*') # 一、 pattern_parentheses = re.compile(r'^\s*[((]\s*([一二三四五六七八九十]{1,2})\s*[))]\s*') # (一) pattern_letter_initial = re.compile(r'^([A-Z])[..、]?\s*(.*)$') # 初始扫描时匹配 A内容 或 A. 内容 # pattern_arabic = re.compile(r'^(\d+、)\s*(.+)$') # 1、内容 initial_heading_pattern = None special_section_keywords = [r'文件.*(组成|构成|包括|包含)'] #注意,只考虑对文件组成/构成换行,因为一般一行能展示的下,如“投标函”;暂不考虑'下列情形',因为添加换行会导致语义的不连贯。 in_special_section = False # 标志是否在特殊章节中 lines = text.split('\n') def get_current_number(key_chinese): chinese_to_number = { #便于比较中文序号大小 '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '十': 10, '十一': 11, '十二': 12, '十三': 13, '十四': 14, '十五': 15 } if not key_chinese: return -1 return chinese_to_number.get(key_chinese, -1) def check_year_pattern(line): # 检查是否为年份模式,如 '2014年' line_no_spaces = line.replace(' ', '') return re.match(r'^\d{4}\s*年', line_no_spaces) is not None def is_heading(line): # 检查是否匹配任何标题模式 patterns = [ r'^(? current_key_num and new_key_num - current_key_num <= 5)): # 处理临时标题与新主标题的关联 if temp_title: main_number = new_key_candidate.split('.')[0] data[f"{main_number}."] = temp_title temp_title = None # 重置临时标题 # 开始新的标题 if current_key_chinese is not None: data[current_key_chinese] = current_value_chinese if current_key is not None: content_string = ''.join(current_content).strip() data[current_key] = data.get(current_key, '') + content_string current_key = new_key_candidate current_content = [line_content] append_newline = True last_main_number = new_key_candidate.rstrip('.') else: # 将当前行视为当前标题的内容 append_newline = handle_content_append(current_content, line_stripped, append_newline, keywords, in_special_section) else: #前面的都没匹配上,那么当前处理行可能是大标题'一、总则' 或者是普通正文部分'应通知采购代理机构补全或更换,否则风险自负。' if has_initial_heading_patterns and not skip_subheadings and not in_special_section: # 匹配上任意一种大标题形式:'一、xx' '(一)、xx' 'A.xx' 且不处于'特殊章节' ,继续执行后面的代码 numbered_match = pattern_numbered.match(line_stripped) # 一、 parentheses_match = pattern_parentheses.match(line_stripped) # (一) if i < 5: pattern_letter = pattern_letter_initial else: pattern_letter = re.compile(r'^([A-Z])[..、]\s*(.*)$') letter_match = pattern_letter.match(line_stripped) # A. 内容 # arabic_match = pattern_arabic.match(line_stripped) #1、内容 # 判断当前行是否匹配了任何标题模式 if numbered_match or parentheses_match or letter_match: # 如果初始标题模式尚未设置,则记录当前匹配的标题模式 if initial_heading_pattern is None: if numbered_match: initial_heading_pattern = 'numbered' elif parentheses_match: initial_heading_pattern = 'parentheses' elif letter_match: initial_heading_pattern = 'letter' # 确定当前匹配的标题模式 if numbered_match: current_heading_pattern = 'numbered' elif parentheses_match: current_heading_pattern = 'parentheses' elif letter_match: current_heading_pattern = 'letter' # 如果当前标题模式与初始标题模式一致,创建新的键值对 if current_heading_pattern == initial_heading_pattern: new_key_chinese = None new_value = None # 保存之前的 key 的内容 if current_key_chinese is not None: data[current_key_chinese] = current_value_chinese if current_key is not None: content_string = ''.join(current_content).strip() # 如果已经有内容,添加换行符 if data.get(current_key): data[current_key] = data.get(current_key) + '\n' + content_string else: data[current_key] = content_string current_content = [] # 处理匹配到的标题 if current_heading_pattern == 'numbered': new_key_chinese = numbered_match.group(1) new_value = line_stripped[numbered_match.end():].lstrip('..、,').replace(' ', '') elif current_heading_pattern == 'parentheses': new_key_chinese = parentheses_match.group(1) new_value = line_stripped[parentheses_match.end():].lstrip('..、,').replace(' ', '') elif current_heading_pattern == 'letter': letter_key, letter_value = letter_match.groups() letter_to_chinese = { 'A': '一', 'B': '二', 'C': '三', 'D': '四', 'E': '五', 'F': '六', 'G': '七', 'H': '八', 'I': '九', 'J': '十', 'K': '十一', 'L': '十二', 'M': '十三', 'N': '十四', 'O': '十五' } new_key_chinese = letter_to_chinese.get(letter_key, letter_key) new_value = letter_value.lstrip('..、,').replace(' ', '') # 比较数字大小 current_number = get_current_number(current_key_chinese) if current_key_chinese else -1 new_number = get_current_number(new_key_chinese) if new_number > current_number or current_key_chinese is None: # 设置新的键值对 current_key_chinese = new_key_chinese current_value_chinese = new_value current_key = None else: # 如果新数字小于等于当前数字,将该行视为内容 if line_stripped: append_newline = handle_content_append(current_content, line_stripped, append_newline, keywords, in_special_section) else: # 当前标题模式与初始模式不一致,将该行视为内容 if line_stripped: append_newline = handle_content_append(current_content, line_stripped, append_newline, keywords, in_special_section) else: # 未匹配到任何标题模式,将该行视为内容 if line_stripped: append_newline = handle_content_append(current_content, line_stripped, append_newline, keywords, in_special_section) else: #该情况下,所有内容都作为当前标题的内容,添加到current_content中 if line_stripped: append_newline = handle_content_append(current_content, line_stripped, append_newline, keywords, in_special_section) # 最后保存最后一个 key 对应的内容 if current_key is not None: content_string = ''.join(current_content).strip() data[current_key] = data.get(current_key, '') + content_string if current_key_chinese is not None: data[current_key_chinese] = current_value_chinese if temp_title: # 处理任何未关联的临时标题 data["未分类标题"] = temp_title # 您可以根据需要选择合适的键名 return data def extract_text_from_pdf(file_path, end_pattern, start_pattern_1, start_pattern_2): # start_pattern_2可能为0,从匹配位置的下一行开开始加入,而非匹配到的当前行,应付'三、招投标须知正文'的情况 # 从PDF文件中提取文本 common_header = extract_common_header(file_path) pdf_document = PdfReader(file_path) all_pages_text = [] start_index = None # 处理所有页面 for i, page in enumerate(pdf_document.pages): page_text = page.extract_text() if page.extract_text() else "" cleaned_text = clean_page_content(page_text, common_header) # 在第一页查找开始位置 if i == 0 and start_index is None: # 按行拆分文本 lines = cleaned_text.splitlines() for idx, line in enumerate(lines): # 只处理 start_pattern_1 和 start_pattern_2 start_match_1 = regex.search(start_pattern_1, line) start_match_2 = None if start_pattern_2: start_match_2 = regex.search(start_pattern_2, line) # 如果匹配第一个表达式 if start_match_1: start_index = cleaned_text.find(line) # 找到匹配行的起始位置 cleaned_text = cleaned_text[start_index:] # 从匹配行开始截取 break # 如果匹配第二个表达式,并且 start_pattern_2 不为 None elif start_match_2: start_index = cleaned_text.find(line) # 找到匹配行的起始位置 # 从匹配到的行的下一行开始截取 cleaned_text = cleaned_text[cleaned_text.find(lines[idx + 1]):] if idx + 1 < len(lines) else "" break # 在最后一页查找结束位置 if i == len(pdf_document.pages) - 1: matches = list(regex.finditer(end_pattern, cleaned_text, regex.MULTILINE)) if matches: end_index = matches[-1].start() cleaned_text = cleaned_text[:end_index] lines = cleaned_text.split('\n') for j, line in enumerate(lines): if j == 0: lines[j] = '##' + line cleaned_text = '\n'.join(lines) all_pages_text.append(cleaned_text) # 合并所有页面的文本 full_text = "\n".join(all_pages_text) return full_text def convert_clause_to_json(file_path, output_folder, type=1): try: if not os.path.exists(file_path): print(f"The specified file does not exist: 返回空的clause_path") return "" if type == 2: start_pattern_1 = r'.*(?