804 lines
42 KiB
Python
804 lines
42 KiB
Python
|
import json
|
|||
|
import os
|
|||
|
import re
|
|||
|
import regex
|
|||
|
import time
|
|||
|
from concurrent.futures import ThreadPoolExecutor
|
|||
|
from flask_app.general.doubao import generate_full_user_query
|
|||
|
from flask_app.general.通义千问long import qianwen_plus
|
|||
|
from flask_app.general.通用功能函数 import process_string_list
|
|||
|
from collections import OrderedDict
|
|||
|
from docx import Document
|
|||
|
from flask_app.general.insert_del_pagemark import insert_mark
|
|||
|
from flask_app.general.format_change import pdf2docx
|
|||
|
|
|||
|
|
|||
|
# 只读取前附表中的最后一列(省钱,但容易漏内容)
|
|||
|
def read_docx_last_column(truncate_file):
|
|||
|
# 尝试打开文档
|
|||
|
try:
|
|||
|
doc = Document(truncate_file)
|
|||
|
except Exception as e:
|
|||
|
print(f"Error opening file: {e}")
|
|||
|
return []
|
|||
|
|
|||
|
last_column_values = []
|
|||
|
|
|||
|
# 读取文档中的所有表格
|
|||
|
if not doc.tables:
|
|||
|
print("No tables found in the document.")
|
|||
|
return last_column_values
|
|||
|
|
|||
|
# 遍历文档中的每个表格
|
|||
|
for table in doc.tables:
|
|||
|
# 获取表格的最后一列
|
|||
|
for row in table.rows:
|
|||
|
last_cell = row.cells[-1] # 获取最后一个单元格
|
|||
|
# 去除内容前后空白并删除文本中的所有空格
|
|||
|
cleaned_text = last_cell.text.strip().replace(' ', '')
|
|||
|
last_column_values.append(cleaned_text)
|
|||
|
|
|||
|
return last_column_values
|
|||
|
|
|||
|
# 完整读取文件中所有表格(适合pdf转docx价格便宜的情况,优先推荐,内容完整)
|
|||
|
def read_tables_from_docx(file_path):
|
|||
|
# print(file_path)
|
|||
|
# 尝试打开文档
|
|||
|
try:
|
|||
|
doc = Document(file_path)
|
|||
|
except Exception as e:
|
|||
|
print(f"Error opening file: {e}")
|
|||
|
return []
|
|||
|
|
|||
|
# 初始化列表来保存符合条件的单元格内容
|
|||
|
cell_contents = []
|
|||
|
|
|||
|
# 读取文档中的所有表格
|
|||
|
if not doc.tables:
|
|||
|
print("No tables found in the document.")
|
|||
|
return []
|
|||
|
|
|||
|
# 遍历文档中的每个表格
|
|||
|
for table_idx, table in enumerate(doc.tables):
|
|||
|
# 遍历表格中的每一行
|
|||
|
for row_idx, row in enumerate(table.rows):
|
|||
|
# 遍历每一行中的单元格
|
|||
|
for cell in row.cells:
|
|||
|
cell_text = cell.text.strip() # 去除单元格内容前后空白
|
|||
|
if len(cell_text) > 8: # 检查文字数量是否大于8
|
|||
|
cell_contents.append(cell_text)
|
|||
|
|
|||
|
# 返回符合条件的单元格内容
|
|||
|
return cell_contents
|
|||
|
|
|||
|
|
|||
|
#处理跨页的段落
|
|||
|
def preprocess_paragraphs(paragraphs):
|
|||
|
processed = [] # 初始化处理后的段落列表
|
|||
|
index = 0
|
|||
|
flag = False # 初始化标志位
|
|||
|
|
|||
|
# 定义两个新的正则表达式模式
|
|||
|
pattern_numbered = re.compile(r'^\s*([一二三四五六七八九十]{1,2})\s*、\s*')
|
|||
|
pattern_parentheses = re.compile(r'^\s*[((]\s*([一二三四五六七八九十]{1,2})\s*[))]\s*')
|
|||
|
|
|||
|
# 定义列表项的模式
|
|||
|
list_item_pattern = re.compile(
|
|||
|
r'^\s*('
|
|||
|
r'[(\(]\d+[)\)]|' # 匹配:(1) 或 (1)
|
|||
|
r'[A-Za-z]\.\s*|' # 匹配:A. 或 b.
|
|||
|
r'[一二三四五六七八九十]+、|' # 匹配:一、二、三、
|
|||
|
r'第[一二三四五六七八九十百零]+[章节部分节]|' # 匹配:第x章,第x部分,第x节
|
|||
|
r'[A-Za-z]\d+(?:\.\d+)*[\s\.、.)\)]?|' # 匹配:A1.2 等
|
|||
|
r'\d+(?:\.\d+)+[\s\.、.)\)]?(?!\s*[号条款节章项例页段部步点年月日时分秒个元千万])|' # 匹配:数字序号如1.1 1.1.1
|
|||
|
r'(?=\d+\s(?!\s*[号条款节章项例页段部步点年月日时分秒个元千万]))|' # 数字后空格,空格后非指定关键字
|
|||
|
r'(?=\d+[、..])(?!\s*[号条款节章项例页段部步点年月日时分秒个元千万])' # 数字后直接跟顿号或点号
|
|||
|
r')'
|
|||
|
)
|
|||
|
|
|||
|
# 新增的正则表达式,用于匹配以数字序号开头的段落
|
|||
|
pattern_numeric_header = re.compile(
|
|||
|
r'^(?<![a-zA-Z((])(\d+(?:\.\d+)+)\s*(.*)' # 匹配如 '12.1 内容'
|
|||
|
)
|
|||
|
pattern_numeric_header_fallback = re.compile(
|
|||
|
r'^(\d+\.)\s*(.+)$' # 匹配如 '12. 内容'
|
|||
|
)
|
|||
|
|
|||
|
# 是否存在连续超过指定长度的空白字符序列: 排除遇到表格、填空的情况
|
|||
|
def has_long_spaces(text, max_space_count=5):
|
|||
|
return any(len(space) > max_space_count for space in re.findall(r'\s+', text))
|
|||
|
|
|||
|
# 正则表达式用于检测页面标记
|
|||
|
pattern_marker = re.compile(r'\$\$index_mark_\d+\$\$')
|
|||
|
|
|||
|
# 辅助函数:查找上一个非空且非标记的段落
|
|||
|
def find_prev_text(current_index):
|
|||
|
for i in range(current_index - 1, -1, -1):
|
|||
|
try:
|
|||
|
text = paragraphs[i].text.strip()
|
|||
|
except AttributeError:
|
|||
|
continue # 如果段落对象没有 text 属性,跳过
|
|||
|
if text and not pattern_marker.search(text):
|
|||
|
return text, i
|
|||
|
return '', -1
|
|||
|
|
|||
|
# 辅助函数:查找下一个非空且非标记的段落
|
|||
|
def find_next_text(current_index):
|
|||
|
for i in range(current_index + 1, len(paragraphs)):
|
|||
|
try:
|
|||
|
text = paragraphs[i].text.strip()
|
|||
|
except AttributeError:
|
|||
|
continue # 如果段落对象没有 text 属性,跳过
|
|||
|
# 跳过空白段落和页面标记
|
|||
|
if not text or pattern_marker.search(text):
|
|||
|
continue
|
|||
|
# 跳过匹配排除模式的段落
|
|||
|
if (pattern_numbered.match(text) or pattern_parentheses.match(text)) and len(text) < 8:
|
|||
|
continue
|
|||
|
return text, i
|
|||
|
return '', -1
|
|||
|
|
|||
|
while index < len(paragraphs):
|
|||
|
try:
|
|||
|
current_text = paragraphs[index].text.strip() # 去除当前段落的前后空白
|
|||
|
except AttributeError:
|
|||
|
# 如果段落对象没有 text 属性,跳过该段落
|
|||
|
index += 1
|
|||
|
continue
|
|||
|
|
|||
|
# 检查当前段落是否为页面标记
|
|||
|
if pattern_marker.search(current_text):
|
|||
|
# 动态查找前一个非空段落
|
|||
|
prev_text, prev_index = find_prev_text(index)
|
|||
|
# 动态查找后一个非空段落
|
|||
|
next_text, next_index = find_next_text(index)
|
|||
|
|
|||
|
# 应用现有的合并逻辑
|
|||
|
if prev_text and next_text and not has_long_spaces(prev_text) and not has_long_spaces(next_text):
|
|||
|
if not prev_text.endswith(('。', '!', '?')): # ',', ',', 先注释了,如果逗号,可能还没结束。
|
|||
|
# 检查后一个段落是否为列表项
|
|||
|
if not list_item_pattern.match(next_text) and len(prev_text) > 30:
|
|||
|
# 合并前后段落
|
|||
|
merged_text = prev_text + ' ' + next_text # 为了可读性添加空格
|
|||
|
if prev_index < len(paragraphs):
|
|||
|
# 移除 processed 中的前一个段落
|
|||
|
if processed and processed[-1] == prev_text:
|
|||
|
processed.pop()
|
|||
|
# 添加合并后的文本
|
|||
|
processed.append(merged_text)
|
|||
|
|
|||
|
# 跳过标记以及前后所有空白段落,直到 next_index
|
|||
|
index = next_index + 1
|
|||
|
continue # 继续下一个循环
|
|||
|
|
|||
|
# 如果不满足合并条件,跳过标记及其周围的空白段落
|
|||
|
# 计算下一个需要处理的索引
|
|||
|
# 从当前 index 向下,跳过所有连续的空白段落和标记
|
|||
|
skip_index = index + 1
|
|||
|
while skip_index < len(paragraphs):
|
|||
|
try:
|
|||
|
skip_text = paragraphs[skip_index].text.strip()
|
|||
|
except AttributeError:
|
|||
|
skip_index += 1
|
|||
|
continue # 如果段落对象没有 text 属性,跳过
|
|||
|
if skip_text == '' or pattern_marker.search(skip_text):
|
|||
|
skip_index += 1
|
|||
|
else:
|
|||
|
break
|
|||
|
index = skip_index
|
|||
|
continue # 继续下一个循环
|
|||
|
|
|||
|
# 检查当前段落是否匹配任一排除模式
|
|||
|
if (pattern_numbered.match(current_text) or pattern_parentheses.match(current_text)) and len(current_text) < 8:
|
|||
|
# 如果匹配,则跳过当前段落,不添加到processed列表中
|
|||
|
index += 1
|
|||
|
continue
|
|||
|
|
|||
|
# 检查是否为以数字序号开头的段落
|
|||
|
match = pattern_numeric_header.match(current_text)
|
|||
|
if not match:
|
|||
|
match = pattern_numeric_header_fallback.match(current_text)
|
|||
|
|
|||
|
if match:
|
|||
|
# 当前段落以数字序号开头,直接添加到 processed
|
|||
|
processed.append(current_text)
|
|||
|
flag = True # 设置标志位,准备处理下一个段落
|
|||
|
index += 1
|
|||
|
continue
|
|||
|
else:
|
|||
|
if flag:
|
|||
|
if not list_item_pattern.match(current_text):
|
|||
|
if processed:
|
|||
|
# **新增逻辑开始**
|
|||
|
next_non_empty_text, next_non_empty_index = find_next_text(index)
|
|||
|
is_next_numbered = False
|
|||
|
if next_non_empty_text:
|
|||
|
is_next_numbered = bool(
|
|||
|
pattern_numeric_header.match(next_non_empty_text) or
|
|||
|
pattern_numeric_header_fallback.match(next_non_empty_text)
|
|||
|
)
|
|||
|
|
|||
|
if is_next_numbered and len(processed[-1]) > 30:
|
|||
|
# 只有在下一个段落以数字序号开头且上一个段落长度大于30时,才将当前段落追加到上一个段落
|
|||
|
processed[-1] = processed[-1] + ' ' + current_text
|
|||
|
else:
|
|||
|
# 否则,不追加,而是作为新的段落添加
|
|||
|
processed.append(current_text)
|
|||
|
# **新增逻辑结束**
|
|||
|
else:
|
|||
|
# **新增处理:匹配 list_item_pattern 的段落也应被保存**
|
|||
|
processed.append(current_text)
|
|||
|
# 无论是否追加,都将 flag 重置
|
|||
|
flag = False
|
|||
|
index += 1
|
|||
|
continue
|
|||
|
else:
|
|||
|
# flag 为 False,直接添加到 processed
|
|||
|
processed.append(current_text)
|
|||
|
index += 1
|
|||
|
continue
|
|||
|
|
|||
|
return processed
|
|||
|
|
|||
|
#如果当前段落有序号,则向下匹配直接遇到相同的序号样式
|
|||
|
#如果当前段落无序号,则向下匹配序号,把若干同类的序号都摘出来。
|
|||
|
def extract_text_with_keywords(doc_path, keywords, follow_up_keywords):
|
|||
|
|
|||
|
if isinstance(keywords, str):
|
|||
|
keywords = [keywords]
|
|||
|
|
|||
|
doc = Document(doc_path)
|
|||
|
extracted_paragraphs = OrderedDict()
|
|||
|
continue_collecting = False
|
|||
|
current_section_pattern = None
|
|||
|
active_key = None
|
|||
|
|
|||
|
def match_keywords(text, patterns):
|
|||
|
# 首先检查关键词是否匹配
|
|||
|
for pattern in patterns:
|
|||
|
if re.search(pattern, text, re.IGNORECASE):
|
|||
|
return True
|
|||
|
return False
|
|||
|
|
|||
|
def extract_from_text(text, current_index):
|
|||
|
nonlocal continue_collecting, current_section_pattern, active_key
|
|||
|
if text == "":
|
|||
|
return current_index
|
|||
|
|
|||
|
if continue_collecting:
|
|||
|
if current_section_pattern and re.match(current_section_pattern, text):
|
|||
|
continue_collecting = False
|
|||
|
active_key = None
|
|||
|
else:
|
|||
|
if active_key is not None:
|
|||
|
extracted_paragraphs[active_key].append(text)
|
|||
|
return current_index
|
|||
|
|
|||
|
if match_keywords(text, keywords):
|
|||
|
active_key = text
|
|||
|
extracted_paragraphs[active_key] = [text]
|
|||
|
if match_keywords(text, follow_up_keywords):
|
|||
|
continue_collecting = True
|
|||
|
section_number = re.match(r'^(\d+([..]\d+)*)\s*[..]?', text) # 修改后的正则,支持 '数字 、' 和 '数字.'
|
|||
|
if section_number: #当前匹配的行前有序号,那么就匹配到下个相似序号为止停止收集
|
|||
|
current_section_number = section_number.group(1)
|
|||
|
level_count = current_section_number.count('.')
|
|||
|
# 获取章节的各级部分
|
|||
|
parts = current_section_number.split('.')
|
|||
|
# Pattern to match current level, e.g., 3.4.5 添加负向前瞻以防止匹配四级或更高层级
|
|||
|
pattern = r'^' + (r'\d+\s*[..]\s*') * level_count + r'\d+' + r'(?!\s*[..]\s*\d+)'
|
|||
|
matched_patterns = [pattern] # start with the full pattern
|
|||
|
|
|||
|
# for i in range(1, 6): #同级,与matched_patterns = [pattern]重复了,故注释
|
|||
|
# # 复制 parts 列表以避免修改原列表
|
|||
|
# new_parts = parts. copy()
|
|||
|
# new_parts[-1] = str(int(new_parts[-1]) + i)
|
|||
|
# # 使用不同的分隔符
|
|||
|
# next_pattern = r'^' + r'\s*[..]\s*'.join(new_parts)
|
|||
|
# matched_patterns.append(next_pattern)
|
|||
|
|
|||
|
# Parent section (if applicable)
|
|||
|
if len(parts) > 1:
|
|||
|
for i in range(1, 6): #考虑原文档的书写不规范,跳序号的情况,目前设置了范围<5
|
|||
|
parent_section_parts = parts[:-1].copy()
|
|||
|
parent_section_parts[-1] = str(int(parent_section_parts[-1]) + i)
|
|||
|
parent_pattern = r'^' + r'\s*[..]\s*'.join(parent_section_parts)+ r'(?!\s*[..]\s*\d+)'
|
|||
|
matched_patterns.append(parent_pattern)
|
|||
|
|
|||
|
# 添加对 '数字 、' 格式的支持
|
|||
|
digit_comma_pattern = r'^\d+\s*、'
|
|||
|
matched_patterns.append(digit_comma_pattern)
|
|||
|
|
|||
|
# 获取当前顶级章节编号
|
|||
|
current_top_level_num = int(current_section_number.split('.')[0])
|
|||
|
for i in range(1, 6):
|
|||
|
next_top_level_num = current_top_level_num + i
|
|||
|
next_top_level_pattern = r'^' + str(next_top_level_num) + r'\s*[..]'
|
|||
|
# 检查是否已经包含了该模式,避免重复
|
|||
|
if next_top_level_pattern not in matched_patterns:
|
|||
|
matched_patterns.append(next_top_level_pattern)
|
|||
|
|
|||
|
# Combine the patterns
|
|||
|
combined_pattern = r'(' + r')|('.join(matched_patterns) + r')'
|
|||
|
current_section_pattern = re.compile(combined_pattern)
|
|||
|
|
|||
|
else:
|
|||
|
found_next_number = False
|
|||
|
current_section_pattern = None
|
|||
|
|
|||
|
while current_index < len(processed_paragraphs) - 1:
|
|||
|
current_index += 1
|
|||
|
next_text = processed_paragraphs[current_index].strip()
|
|||
|
# 添加对空白行的处理
|
|||
|
if not next_text:
|
|||
|
continue # 跳过空白行,进入下一个循环
|
|||
|
if not found_next_number:
|
|||
|
# 修改后的正则,支持 '数字 、' 格式
|
|||
|
next_section_number = re.match(r'^([A-Za-z0-9]+(?:[..][A-Za-z0-9]+)*)|([((]\s*\d+\s*[))])|(\d+\s*、)',
|
|||
|
next_text)
|
|||
|
if next_section_number:
|
|||
|
found_next_number = True
|
|||
|
if next_section_number.group(1):
|
|||
|
section_parts = next_section_number.group(1).split('.')
|
|||
|
dynamic_pattern = r'^' + r'[..]'.join(
|
|||
|
[r'[A-Za-z0-9]+' for _ in section_parts]) + r'\b'
|
|||
|
elif next_section_number.group(2):
|
|||
|
dynamic_pattern = r'^[\(\(]\s*\d+\s*[\)\)]'
|
|||
|
elif next_section_number.group(3):
|
|||
|
dynamic_pattern = r'^\d+\s*、'
|
|||
|
current_section_pattern = re.compile(dynamic_pattern)
|
|||
|
if current_section_pattern and re.match(current_section_pattern, next_text):
|
|||
|
extracted_paragraphs[active_key].append(next_text)
|
|||
|
else:
|
|||
|
continue_collecting = False
|
|||
|
active_key = None
|
|||
|
break
|
|||
|
|
|||
|
return current_index
|
|||
|
|
|||
|
processed_paragraphs = preprocess_paragraphs(doc.paragraphs)
|
|||
|
index = 0
|
|||
|
while index < len(processed_paragraphs):
|
|||
|
# print(processed_paragraphs[index].strip())
|
|||
|
index = extract_from_text(processed_paragraphs[index].strip(), index)
|
|||
|
# print("--------------")
|
|||
|
index += 1
|
|||
|
|
|||
|
return extracted_paragraphs
|
|||
|
|
|||
|
"""
|
|||
|
eg:
|
|||
|
text_list = ["这是第一句。 1. 接下来是第二句! (3) 最后一句。"]
|
|||
|
new_text_list = ["这是第一句。", "1. 接下来是第二句!", "(3) 最后一句。"]
|
|||
|
"""
|
|||
|
def preprocess_text_list(text_list):
|
|||
|
new_text_list = []
|
|||
|
# 正则表达式匹配中文字符或标点后的空格,该空格后紧跟字母、数字或带括号的数字
|
|||
|
split_pattern = re.compile(r'(?<=[\u4e00-\u9fff。;!??!;])(?=\s+[a-zA-Z\d]|\s+\([1-9]\d*\)|\s+\([1-9]\d*\))') #。;!??!;
|
|||
|
for text in text_list:
|
|||
|
# 使用正则表达式检查并拆分元素
|
|||
|
parts = split_pattern.split(text)
|
|||
|
new_text_list.extend(part.strip() for part in parts if part.strip()) # 添加非空字符串检查
|
|||
|
|
|||
|
return new_text_list
|
|||
|
|
|||
|
def clean_dict_datas(extracted_contents, keywords, excludes): # 让正则表达式提取到的东西格式化
|
|||
|
all_texts1 = []
|
|||
|
all_texts2 = []
|
|||
|
# 定义用于分割句子的正则表达式,包括中文和西文的结束标点
|
|||
|
split_pattern = r'(?<=[。!?\!\?])'
|
|||
|
|
|||
|
for key, text_list in extracted_contents.items():
|
|||
|
if len(text_list) == 1:
|
|||
|
for data in text_list:
|
|||
|
# print(data)
|
|||
|
# 检查是否包含任何需要排除的字符串
|
|||
|
if any(exclude in data for exclude in excludes):
|
|||
|
continue # 如果包含任何排除字符串,跳过这个数据
|
|||
|
# 去掉开头的序号,eg:1 | (1) |(2) | 1. | 2.(全角点)| 3、 | 1.1 | 2.3.4 | A1 | C1.1 | 一、
|
|||
|
pattern = r'^\s*(?:[((]\d+[)))]|[A-Za-z]?\d+(?:\.\s*\d+)*[\s\.、.)\)]+|[一二三四五六七八九十]+、|[A-Z][))\.、.]?\s*)'
|
|||
|
data = re.sub(pattern, '', data).strip()
|
|||
|
keyword_match = re.search(keywords, data)
|
|||
|
if keyword_match:
|
|||
|
# 从关键词位置开始查找结束标点符号
|
|||
|
start_pos = keyword_match.start()
|
|||
|
# 截取从关键词开始到后面的内容
|
|||
|
substring = data[start_pos:]
|
|||
|
# 按定义的结束标点分割
|
|||
|
sentences = re.split(split_pattern, substring, 1)
|
|||
|
if len(sentences) > 0 and sentences[0]:
|
|||
|
# 只取第一句,保留标点
|
|||
|
cleaned_text = data[:start_pos] + sentences[0] # eg:经采购人允许,潜在投标人可进入项目现场进行考察,但潜在投标人不得因此使采购人承担有关责任和蒙受损失。潜在投标人应自行承担现场考察的全部费用、责任和风险。
|
|||
|
# 经采购人允许,潜在投标人可进入项目现场进行考察,但潜在投标人不得因此使采购人承担有关责任和蒙受损失。
|
|||
|
else:
|
|||
|
cleaned_text = data # 如果没有标点,使用整个字符串
|
|||
|
else:
|
|||
|
# 如果没有找到关键词,保留原文本
|
|||
|
cleaned_text = data
|
|||
|
# 删除空格
|
|||
|
cleaned_text_no_spaces = cleaned_text.replace(' ', '').replace(' ', '')
|
|||
|
# 如果长度大于8,则添加到结果列表
|
|||
|
if len(cleaned_text_no_spaces) > 8:
|
|||
|
all_texts1.append(cleaned_text_no_spaces)
|
|||
|
|
|||
|
else:
|
|||
|
# print(text_list)
|
|||
|
# print("*********")
|
|||
|
# new_text_list = preprocess_text_list(text_list)
|
|||
|
# 用于处理结构化文本,清理掉不必要的序号,并将分割后的段落合并,最终形成更简洁和格式化的输出。
|
|||
|
pattern = r'^\s*(?:[((]\d+[)))]|[A-Za-z]?\d+(?:\.\s*\d+)*[\s\.、.)\)]+|[一二三四五六七八九十]+、|[A-Z][))]\s+|[A-Z]\.\s*)'
|
|||
|
data = re.sub(pattern, '', text_list[0]).strip() # 去除序号
|
|||
|
# 将修改后的第一个元素和剩余的元素连接起来
|
|||
|
text_list[0] = data # 更新列表中的第一个元素
|
|||
|
joined_text = "\n".join(text_list) # 如果列表中有多个元素,则连接它们
|
|||
|
# 删除空格
|
|||
|
joined_text_no_spaces = joined_text.replace(' ', '').replace(' ', '')
|
|||
|
all_texts2.append(joined_text_no_spaces) # 将每个列表的内容添加到 all_texts 中
|
|||
|
|
|||
|
return all_texts1, all_texts2 # all_texts1要额外用gpt all_text2直接返回结果
|
|||
|
|
|||
|
|
|||
|
#从表格中提取数据
|
|||
|
def extract_table_with_keywords(data, keywords, follow_up_keywords,flag=False):
|
|||
|
"""遍历列表中的每个元素,查找并返回包含关键词的句子列表,并根据是否存在后续关键词分别存储到两个列表中。"""
|
|||
|
sentences1 = [] # 保存没有后续关键词的情况
|
|||
|
sentences2 = [] # 保存有后续关键词的情况
|
|||
|
|
|||
|
# 编译关键词的正则表达式,提高匹配性能
|
|||
|
keywords_pattern = re.compile(keywords, re.IGNORECASE)
|
|||
|
follow_up_patterns = [re.compile(fu, re.IGNORECASE) for fu in follow_up_keywords]
|
|||
|
|
|||
|
# 检查是否包含 '无效报价' 的关键词
|
|||
|
check_invalid_bidding = bool(re.search(r'无\s*效\s*报\s*价', keywords, re.IGNORECASE))
|
|||
|
|
|||
|
# 定义用于提取括号内容的正则表达式,支持中英文括号
|
|||
|
bracket_pattern = re.compile(r'[((][^(()))]+[))]')
|
|||
|
|
|||
|
# 遍历列表中的每个字符串元素
|
|||
|
for item in data:
|
|||
|
# 只有在 keywords 包含 '无效报价' 时,才检查 "无效报价"
|
|||
|
if check_invalid_bidding and re.search(r'无\s*效\s*报\s*价', item, re.IGNORECASE):
|
|||
|
sentences1.append(item.strip())
|
|||
|
continue
|
|||
|
|
|||
|
# 先检查 item 是否包含任意关键词,如果不包含,则跳过分割
|
|||
|
if not keywords_pattern.search(item):
|
|||
|
continue
|
|||
|
|
|||
|
# 1. 先提取并替换括号内容
|
|||
|
bracket_contents = []
|
|||
|
|
|||
|
def replace_bracket_content(match):
|
|||
|
bracket_contents.append(match.group(0)) # 保存括号内容
|
|||
|
return f"<BRACKET_{len(bracket_contents) - 1}>" # 使用占位符替换括号内容
|
|||
|
|
|||
|
item_with_placeholders = bracket_pattern.sub(replace_bracket_content, item)
|
|||
|
|
|||
|
# 2. 分割句子,保证句子完整性(按标点符号和序号分割)
|
|||
|
split_sentences = regex.split(
|
|||
|
r'(?<=[。!?!?\?])|' # 在中文句号、感叹号、问号或分号后面分割
|
|||
|
r'(?=\d+(?:[..]\d+)+)(?!\s*[号条款节章项例页段部步点年月日时分秒个元千万])|' # 在类似1.1 1.1.1 的数字序号前分割
|
|||
|
r'(?<![+\-×÷*/]\s*|\d)(?=\d+\s(?!\s*[号条款节章项例页段部步点年月日时分秒个元千万]))|' # 数字后面跟空格且空格后面不是指定关键字,且前面不是运算符和空格或数字
|
|||
|
r'(?<![+\-×÷*/]\s*|\d)(?=\d+[、..])(?!\s*[号条款节章项例页段部步点年月日时分秒个元千万])|' # 数字后直接跟顿号、半角点号或全角点号,且前面不是运算符和空格或数字
|
|||
|
r'(?=[A-Za-z][..]\s*)|' # 在字母加点(如A.、a.)前分割
|
|||
|
r'(?=[A-Za-z]+\s*\d+\s*(?:[..]\s*\d+)*)|' # 在可选字母加数字或多级编号前分割
|
|||
|
r'(?=[一二三四五六七八九十]+、)', # 在中文数字加顿号(如一、二、)前分割
|
|||
|
item_with_placeholders
|
|||
|
)
|
|||
|
|
|||
|
# 3. 还原括号内容
|
|||
|
split_sentences = [re.sub(r"<BRACKET_(\d+)>", lambda m: bracket_contents[int(m.group(1))], s) for s in
|
|||
|
split_sentences]
|
|||
|
|
|||
|
# 接下来是处理包含和不包含后续关键词的情况
|
|||
|
i = 0
|
|||
|
# 清洗模式
|
|||
|
clean_pattern = r'^\s*(?:[((]\s*\d+\s*[)))]|[A-Za-z]?\d+(?:\.\s*\d+)*[\s\.、.)\)]+|[一二三四五六七八九十]+、|[A-Z][))\.、.]?\s*)'
|
|||
|
while i < len(split_sentences):
|
|||
|
sentence = split_sentences[i].strip()
|
|||
|
|
|||
|
# 如果匹配关键词
|
|||
|
if keywords_pattern.search(sentence):
|
|||
|
# 检查是否存在后续关键词
|
|||
|
follow_up_present = any(fp.search(sentence) for fp in follow_up_patterns)
|
|||
|
if follow_up_present:
|
|||
|
# 如果存在后续关键词,则从当前位置开始截取
|
|||
|
start_index = i
|
|||
|
end_index = start_index
|
|||
|
found_next_section = False
|
|||
|
for j in range(start_index + 1, len(split_sentences)):
|
|||
|
if re.match(r'\d+[..]\d+([..]\d+)?', split_sentences[j].strip()):
|
|||
|
end_index = j
|
|||
|
found_next_section = True
|
|||
|
break
|
|||
|
if found_next_section:
|
|||
|
full_text = ' '.join(split_sentences[start_index:end_index]).strip()
|
|||
|
else:
|
|||
|
full_text = ' '.join(split_sentences[start_index:]).strip()
|
|||
|
|
|||
|
full_text = re.sub(clean_pattern, '', full_text).replace(' ', '').strip()
|
|||
|
sentences2.append(full_text) # 存储有后续关键词的情况
|
|||
|
i = end_index if found_next_section else len(split_sentences)
|
|||
|
else:
|
|||
|
# 没有后续关键词的情况
|
|||
|
if flag:
|
|||
|
# 当 flag=True 时,简化逻辑,直接添加清洗后的句子
|
|||
|
# 清洗文本
|
|||
|
cleaned_sentence = re.sub(clean_pattern, '', sentence).replace('\n', '').replace(' ', '').strip()
|
|||
|
if len(cleaned_sentence) > 8:
|
|||
|
sentences1.append(cleaned_sentence)
|
|||
|
else:
|
|||
|
# 如果 \n 换行符前面满足匹配,如 '无效投标\n',那么不删该 \n
|
|||
|
sentence = re.sub(fr'({keywords})(\s*\n)', r'\1[换行标记]', sentence)
|
|||
|
|
|||
|
# 清洗文本
|
|||
|
cleaned_sentence = re.sub(clean_pattern, '', sentence).replace('\n', '').replace(' ', '').strip()
|
|||
|
|
|||
|
# 恢复保留的换行符
|
|||
|
cleaned_sentence = cleaned_sentence.replace('[换行标记]', '\n')
|
|||
|
|
|||
|
# 检查匹配次数和是否需要切分
|
|||
|
matches = list(keywords_pattern.finditer(sentence))
|
|||
|
if len(matches) >= 2: # 如果匹配了两处及以上
|
|||
|
split_points = []
|
|||
|
for match in matches:
|
|||
|
start, end = match.span()
|
|||
|
print(sentence[end:end + 6])
|
|||
|
if sentence[end:end + 6] == "[换行标记]":
|
|||
|
split_points.append(end)
|
|||
|
|
|||
|
if len(split_points) >= 1: # 至少有一个有效切分点
|
|||
|
split_index = split_points[0] # 取第一个切分点
|
|||
|
part1 = sentence[:split_index].replace("[换行标记]", "\n").strip()
|
|||
|
part2 = sentence[split_index:].replace("[换行标记]", "\n").strip()
|
|||
|
# 对 part1 和 part2 进行清洗
|
|||
|
part1 = re.sub(clean_pattern, '', part1).replace('\n', '').replace(' ', '').strip()
|
|||
|
part2 = re.sub(clean_pattern, '', part2).replace('\n', '').replace(' ', '').strip()
|
|||
|
sentences1.append(part1) # 将前半部分加入结果
|
|||
|
sentences1.append(part2) # 将后半部分加入结果
|
|||
|
else:
|
|||
|
# 如果没有足够的有效切分点,直接保留完整句子
|
|||
|
if len(cleaned_sentence) > 8:
|
|||
|
sentences1.append(cleaned_sentence)
|
|||
|
else:
|
|||
|
# 如果只有一个匹配点或不足两处匹配
|
|||
|
if len(cleaned_sentence) > 8:
|
|||
|
sentences1.append(cleaned_sentence)
|
|||
|
|
|||
|
i += 1
|
|||
|
else:
|
|||
|
i += 1
|
|||
|
return sentences1, sentences2 # 返回两个列表
|
|||
|
|
|||
|
def extract_values_if_contains(data, includes):
|
|||
|
"""
|
|||
|
递归检查字典中的值是否包含列表 'includes' 中的内容。
|
|||
|
如果包含,将这些值添加到一个列表中并返回。
|
|||
|
|
|||
|
参数:
|
|||
|
data (dict): 字典或从 JSON 解析得到的数据。
|
|||
|
includes (list): 包含要检查的关键词的列表。
|
|||
|
|
|||
|
返回:
|
|||
|
list: 包含满足条件的值的列表。
|
|||
|
"""
|
|||
|
included_values = [] # 初始化结果列表
|
|||
|
|
|||
|
# 定义递归函数来处理嵌套字典
|
|||
|
def recursive_search(current_data):
|
|||
|
if isinstance(current_data, dict):
|
|||
|
for key, value in current_data.items():
|
|||
|
if isinstance(value, dict):
|
|||
|
# 如果值是字典,递归搜索
|
|||
|
recursive_search(value)
|
|||
|
elif isinstance(value, str):
|
|||
|
# 如果值是字符串,检查是否包含任何 includes 中的关键词
|
|||
|
if any(include in value for include in includes):
|
|||
|
included_values.append(value)
|
|||
|
elif isinstance(current_data, list):
|
|||
|
for item in current_data:
|
|||
|
# 如果是列表,递归每个元素
|
|||
|
recursive_search(item)
|
|||
|
|
|||
|
# 开始递归搜索
|
|||
|
recursive_search(data)
|
|||
|
|
|||
|
return included_values
|
|||
|
|
|||
|
def handle_query(file_path, user_query, output_file, result_key, keywords):
|
|||
|
try:
|
|||
|
excludes = ["说明表", "重新招标", "否决所有", "否决投标的条件", "本人保证:", "我方"]
|
|||
|
follow_up_keywords = [
|
|||
|
r'情\s*形\s*之\s*一',
|
|||
|
r'情\s*况\s*之\s*一',
|
|||
|
r'下\s*列(?!\s*公式)', # 增加负向前瞻,排除“下列公式”
|
|||
|
r'以\s*下(?!\s*公式)', # 增加负向前瞻,排除“以下公式”
|
|||
|
r'其\s*他.*?情\s*形\s*[::]',
|
|||
|
r'包\s*括'
|
|||
|
]
|
|||
|
extracted_contents = extract_text_with_keywords(file_path, [keywords], follow_up_keywords) # 字典结果
|
|||
|
all_texts1, all_texts2 = clean_dict_datas(extracted_contents, keywords, excludes) # 列表
|
|||
|
# print(all_texts2)
|
|||
|
# table_data_list=read_docx_last_column(file_path) #从投标人须知前附表中提取信息生成列表data,每个元素为'一行信息'
|
|||
|
table_data_list = read_tables_from_docx(file_path)
|
|||
|
# print(table_data_list)
|
|||
|
all_tables1, all_tables2 = extract_table_with_keywords(table_data_list, keywords, follow_up_keywords)
|
|||
|
qianwen_txt = all_texts1 + all_tables1
|
|||
|
# Proceed only if there is content to write
|
|||
|
selected_contents = [] # 使用列表保持顺序
|
|||
|
seen_contents = set() # 使用集合跟踪已添加的内容以去重
|
|||
|
|
|||
|
if qianwen_txt:
|
|||
|
with open(output_file, 'w', encoding='utf-8') as file:
|
|||
|
counter = 1
|
|||
|
for content in qianwen_txt:
|
|||
|
# 使用内容的前25个字符作为去重的依据
|
|||
|
key = content[:25] # 提取前25个字符
|
|||
|
if key not in seen_contents: # 如果前30个字符未出现过
|
|||
|
file.write(f"{counter}. {content}\n")
|
|||
|
file.write("..............." + '\n')
|
|||
|
seen_contents.add(key) # 标记前30个字符为已写入
|
|||
|
counter += 1
|
|||
|
|
|||
|
# 生成用户查询
|
|||
|
user_query = generate_full_user_query(output_file, user_query)
|
|||
|
model_ans = qianwen_plus(user_query) # 豆包模型返回结果
|
|||
|
# file_id = upload_file(output_file)
|
|||
|
# model_ans = qianwen_long(file_id, user_query)
|
|||
|
num_list = process_string_list(model_ans) # 处理模型返回的序号
|
|||
|
print(result_key + "选中的序号:" + str(num_list))
|
|||
|
|
|||
|
for index in num_list:
|
|||
|
if 1 <= index <= len(qianwen_txt):
|
|||
|
content = qianwen_txt[index - 1]
|
|||
|
# 直接添加到 selected_contents,因为前面已经按前30字符去重
|
|||
|
selected_contents.append(content)
|
|||
|
|
|||
|
# 无论 qianwen_txt 是否为空,都添加 all_texts2 和 all_tables2 的内容
|
|||
|
for item in all_texts2 + all_tables2:
|
|||
|
# 同样使用前25个字符判断去重
|
|||
|
key = item[:25] # 提取前30个字符
|
|||
|
if key not in seen_contents:
|
|||
|
selected_contents.append(item)
|
|||
|
seen_contents.add(key)
|
|||
|
|
|||
|
# 如果 selected_contents 不为空,则返回结果,否则返回空字符串
|
|||
|
if selected_contents:
|
|||
|
res = {result_key: list(selected_contents)}
|
|||
|
else:
|
|||
|
res = {result_key: ""}
|
|||
|
return res
|
|||
|
except Exception as e:
|
|||
|
print(f"handle_query 在处理 {result_key} 时发生异常: {e}")
|
|||
|
return {result_key: ""}
|
|||
|
|
|||
|
# 你是一个文本助手,文本内的信息以'...............'分割,你负责准确筛选所需的信息并返回,每块信息要求完整,不遗漏,你不得擅自进行总结或删减。
|
|||
|
# 以上是从招标文件中摘取的内容,文本内之间的信息以'...............'分割,请你根据该内容回答:否决投标或拒绝投标或无效投标或使投标失效的情况有哪些?文本中可能存在无关的信息,请你准确筛选符合的信息并将它的序号返回。请以[x,x,x]格式返回给我结果,x为符合的信息的序号。
|
|||
|
# 以上是原文内容,文本内的信息以'...............'分割,请你根据该信息回答:否决投标或拒绝投标或无效投标或使投标失效的情况有哪些?文本中可能存在无关的信息,请你准确筛选所需的信息并返回。最终结果以json列表格式返回给我,键名为'否决和无效投标情形',你的回答完全忠于原文内容,且回答内容与原文内容一致,要求完整与准确,不能擅自总结或者概括。",
|
|||
|
|
|||
|
#"以上是从招标文件中摘取的内容,文本内之间的信息以'...............'分割,每条信息规定了各方不得存在的情形或是禁止投标的情形,在这些信息中,我作为投标方,需要关注和我相关的信息,请你筛选主语是投标人或中标人或供应商或联合体投标各方或磋商小组的信息,不要返回主语是招标人或采购人或评标委员会的信息,请你筛选所需的信息并将它的序号返回。请以[x,x,x]格式返回给我结果,示例返回为[1,4,6],若情况不存在,返回[]。",
|
|||
|
#"以上是从招标文件中摘取的内容,文本内之间的信息以'...............'分割,每条信息规定了各方不得存在的情形,请回答:在这些信息中,主语是投标人或中标人或供应商或联合体投标各方或磋商小组的信息有哪些?不要返回主语是招标人或采购人或评标委员会的信息,请你筛选所需的信息并将它的序号返回。请以[x,x,x]格式返回给我结果,示例返回为[1,4,6],若情况不存在,返回[]。",
|
|||
|
|
|||
|
|
|||
|
def combine_find_invalid(invalid_docpath, output_dir):
|
|||
|
os.makedirs(output_dir, exist_ok=True)
|
|||
|
queries = [
|
|||
|
(
|
|||
|
r'否\s*决|'
|
|||
|
r'无\s*效\s*投\s*标|'
|
|||
|
r'无\s*效\s*文\s*件|'
|
|||
|
r'(?:文\s*件|投\s*标|响\s*应)\s*[\u4e00-\u9fa5]?\s*(?:无|失)\s*效|'
|
|||
|
r'无\s*效\s*响\s*应|'
|
|||
|
r'无\s*效\s*报\s*价|'
|
|||
|
r'无\s*效\s*标|'
|
|||
|
r'视\s*为\s*无\s*效|'
|
|||
|
r'被\s*拒\s*绝|'
|
|||
|
r'将\s*拒\s*绝|'
|
|||
|
r'予\s*以\s*拒\s*绝',
|
|||
|
"""以下是从招标文件中摘取的内容,文本中序号分明,各信息之间以...............分割。
|
|||
|
任务目标:
|
|||
|
从文本中筛选所有描述否决投标,拒绝投标,投标、响应无效或类似表述的情况,并返回对应的序号。
|
|||
|
要求与指南:
|
|||
|
文本中可能存在无关的信息,请准确筛选符合条件的信息,并将符合条件的信息的序号返回。
|
|||
|
输出格式:
|
|||
|
以 [x, x, x] 的形式返回,x 为符合条件的信息的序号,为自然数。
|
|||
|
如果文本中没有符合条件的信息,请返回 []。
|
|||
|
特殊情况:
|
|||
|
如果某序号的内容明显分为几部分且一部分内容符合筛选条件,但其他部分明显是无关内容,请返回符合部分的字符串内容代替序号。
|
|||
|
示例输出,仅供格式参考:
|
|||
|
[1,3,4,6]
|
|||
|
文本内容:{full_text}
|
|||
|
""",
|
|||
|
os.path.join(output_dir, "temp1.txt"),
|
|||
|
"否决和无效投标情形"
|
|||
|
),
|
|||
|
(
|
|||
|
r'废\s*标',
|
|||
|
"""以下是从招标文件中摘取的内容,文本中序号分明,文本内之间的信息以'...............'分割。
|
|||
|
任务目标:
|
|||
|
请根据以下内容,筛选出 废标项的情况 (明确描述导致 废标 的情况)并返回对应的序号。
|
|||
|
要求与指南:
|
|||
|
文本中可能存在无关的信息,请准确筛选符合条件的信息,并将符合条件的信息的序号返回。
|
|||
|
输出格式:
|
|||
|
返回结果以 [x, x, x] 的形式,其中 x 为符合条件的信息的序号,为自然数。
|
|||
|
如果文本中没有任何符合条件的废标情况,请返回 []。
|
|||
|
示例输出,仅供格式参考:
|
|||
|
[1,3,4,6]
|
|||
|
文本内容:{full_text}
|
|||
|
""",
|
|||
|
os.path.join(output_dir, "temp2.txt"),
|
|||
|
"废标项"
|
|||
|
),
|
|||
|
(
|
|||
|
r'不\s*得(?!\s*(分|力))|禁\s*止\s*投\s*标',
|
|||
|
"""以下是从招标文件中摘取的内容,文本中序号分明,文本内的条款以'...............'分割。条款规定了各方不得存在的情形。请根据以下要求进行筛选:
|
|||
|
**投标相关主体与非投标相关主体的定义**:
|
|||
|
投标相关主体:包括但不限于“投标人”、“中标人”、“供应商”、“联合体投标各方”、“响应人”、“应答人”或其他描述投标方的词语。
|
|||
|
非投标相关主体:包括但不限于“招标人”、“采购人”、“评标委员会”或其他描述非投标方的词语。
|
|||
|
**筛选要求**:
|
|||
|
1. **仅筛选**明确描述投标相关主体禁止情形或不得存在的情形的条款,不包含笼统或未具体说明情形的条款。例如:
|
|||
|
若条款内容包含'投标人不得存在的其他关联情形'这样的笼统描述,而未说明具体的情形,则无需添加该条款。
|
|||
|
2. **排除**仅描述非投标相关主体行为限制或禁止情形的条款,例如“招标人不得泄露信息”或“评标委员会不得收受贿赂”,则无需返回。
|
|||
|
3. 若条款同时描述了对投标相关主体与非投标相关主体的行为限制、禁止情形,也需返回。
|
|||
|
4. **特殊情况**:如果条款中包含“磋商小组”、”各方“等既能指代投标相关主体又能指代非投标相关主体的词汇:
|
|||
|
若在语境中其指代或包含投标相关主体,则应将其考虑在内;否则,排除该条款。
|
|||
|
|
|||
|
**输出格式**:
|
|||
|
返回结果以 [x, x, x] 的形式,其中 x 为符合条件的条款的序号,为自然数。
|
|||
|
如果没有符合条件的条款,返回 `[]`。
|
|||
|
**示例**:
|
|||
|
- **符合条件**:
|
|||
|
- `1. 投标人不得...` → 包含,返回序号 1。
|
|||
|
- `3. 联合体投标各方不得...` → 包含,返回序号 3。
|
|||
|
- **不符合条件**:
|
|||
|
- `2. 采购人不得...` → 主语为“采购人”,排除。
|
|||
|
-示例输出: [1,3]
|
|||
|
请根据上述筛选要求,阅读以下文本内容,并返回符合条件的条款序号,
|
|||
|
|
|||
|
文本内容:{full_text}
|
|||
|
""",
|
|||
|
os.path.join(output_dir, "temp3.txt"),
|
|||
|
"不得存在的情形"
|
|||
|
)
|
|||
|
]
|
|||
|
results = []
|
|||
|
|
|||
|
# 使用线程池来并行处理查询
|
|||
|
with ThreadPoolExecutor() as executor:
|
|||
|
futures = []
|
|||
|
for keywords, user_query, output_file, result_key in queries:
|
|||
|
future = executor.submit(handle_query, invalid_docpath, user_query, output_file, result_key, keywords)
|
|||
|
futures.append((future, result_key)) # 保持顺序
|
|||
|
time.sleep(0.5) # 暂停0.5秒后再提交下一个任务
|
|||
|
|
|||
|
for future, result_key in futures:
|
|||
|
try:
|
|||
|
result = future.result()
|
|||
|
except Exception as e:
|
|||
|
print(f"线程处理 {result_key} 时出错: {e}")
|
|||
|
result = {result_key: ""}
|
|||
|
results.append(result)
|
|||
|
combined_dict = {}
|
|||
|
for d in results:
|
|||
|
combined_dict.update(d)
|
|||
|
|
|||
|
print("无效标与废标done...")
|
|||
|
return {"无效标与废标项": combined_dict}
|
|||
|
|
|||
|
if __name__ == '__main__':
|
|||
|
start_time = time.time()
|
|||
|
# truncate_json_path = "C:\\Users\\Administrator\\Desktop\\货物标\\output4\\tmp2\\竞争性谈判文件(3)_tobidders_notice_part1\\truncate_output.json"
|
|||
|
# truncate_file="C:\\Users\\Administrator\\Desktop\\货物标\\output4\\招标文件(实高电子显示屏)_tobidders_notice_part1.docx"
|
|||
|
# clause_path = "D:\\flask_project\\flask_app\\static\\output\\output1\\77a48c63-f39f-419b-af2a-7b3dbf41b70b\\clause1.json"
|
|||
|
# doc_path="C:\\Users\\Administrator\\Desktop\\货物标\\zbfilesdocx\\磋商文件(1).docx"
|
|||
|
# doc_path = r'C:\Users\Administrator\Desktop\new招标文件\tmp\2024-贵州-贵州省罗甸县 2024 年度广州市协作资金龙坪镇、边阳镇产业路硬化建设项目.docx'
|
|||
|
pdf_path = r'C:\Users\Administrator\Desktop\货物\test\磋商采购文件-恩施市森林火灾风险普查样品检测服务_invalid.pdf'
|
|||
|
|
|||
|
output_dir = r"D:\flask_project\flask_app\static\output\output1\f91db70d-8d96-44a5-b840-27d2f1ecbe95\tmp"
|
|||
|
# invalid_added = insert_mark(pdf_path)
|
|||
|
# invalid_added_docx = pdf2docx(invalid_added)
|
|||
|
invalid_added_docx=r'D:\flask_project\flask_app\static\output\output1\8a662477-a954-4b84-b9c2-d68ebd4f537b\invalid_added.docx'
|
|||
|
results = combine_find_invalid(invalid_added_docx, output_dir)
|
|||
|
end_time = time.time()
|
|||
|
print("Results:", json.dumps(results, ensure_ascii=False, indent=4))
|
|||
|
print("Elapsed time:", str(end_time - start_time))
|