9.29投标人须知提取指定内容,结构化处理
This commit is contained in:
parent
d0049861fe
commit
03a268422b
@ -1,98 +1,56 @@
|
||||
# -*- encoding:utf-8 -*-
|
||||
import json
|
||||
import re
|
||||
|
||||
def post_process(value):
|
||||
# 如果传入的是非字符串值,直接返回原值
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
|
||||
# 定义可能的分割模式及其正则表达式
|
||||
patterns = [
|
||||
(r'\d+、', r'(?=\d+、)'), # 匹配 '1、'
|
||||
(r'[((]\d+[))]', r'(?=[((]\d+[))])'), # 匹配 '(1)' 或 '(1)'
|
||||
(r'\d+\.', r'(?=\d+\.)'), # 匹配 '1.'
|
||||
(r'[一二三四五六七八九十]、', r'(?=[一二三四五六七八九十]、)'), # 匹配 '一、'、'二、' 等
|
||||
(r'[一二三四五六七八九十]\.', r'(?=[一二三四五六七八九十]\.)') # 匹配 '一.'、'二.' 等
|
||||
]
|
||||
|
||||
# 初始化用于保存最早匹配到的模式及其位置
|
||||
first_match = None
|
||||
first_match_position = len(value) # 初始值设为文本长度,确保任何匹配都会更新它
|
||||
|
||||
# 遍历所有模式,找到第一个出现的位置
|
||||
for search_pattern, split_pattern_candidate in patterns:
|
||||
match = re.search(search_pattern, value)
|
||||
if match:
|
||||
# 如果这个匹配的位置比当前记录的更靠前,更新匹配信息
|
||||
if match.start() < first_match_position:
|
||||
first_match = split_pattern_candidate
|
||||
first_match_position = match.start()
|
||||
|
||||
# 如果找到了最早出现的匹配模式,使用它来分割文本
|
||||
if first_match:
|
||||
blocks = re.split(first_match, value)
|
||||
else:
|
||||
# 如果没有匹配的模式,保留原文本
|
||||
blocks = [value]
|
||||
|
||||
processed_blocks = []
|
||||
for block in blocks:
|
||||
if not block:
|
||||
continue
|
||||
# 计算中英文字符总数,如果大于50,则加入列表
|
||||
if block and len(re.findall(r'[\u4e00-\u9fff\w]', block)) >= 50:
|
||||
processed_blocks.append(block.strip())
|
||||
else:
|
||||
# 如果发现有块长度小于50,返回原数据
|
||||
print(block)
|
||||
return value
|
||||
|
||||
# 如果所有的块都符合条件,返回分割后的列表
|
||||
return processed_blocks
|
||||
|
||||
# 递归地处理嵌套结构
|
||||
def process_nested_data(data):
|
||||
# 递归遍历字典,处理最内层的字符串
|
||||
if isinstance(data, dict):
|
||||
# 如果当前项是字典,继续递归遍历其键值对
|
||||
result = {}
|
||||
for key, value in data.items():
|
||||
result[key] = process_nested_data(value) # 递归处理子项
|
||||
return result
|
||||
elif isinstance(data, list):
|
||||
# 如果是列表,直接返回列表,保持原样
|
||||
def is_numeric_key(key):
|
||||
# 这个正则表达式匹配由数字、点、括号中的数字或单个字母(小写或大写)组成的字符串,
|
||||
# 字母后跟数字,或数字后跟字母,单个字母后跟点,但不能是字母-数字-字母的组合
|
||||
pattern = r'^[\d.]+$|^\(\d+\)$|^(\d+)$|^[a-zA-Z]$|^[a-zA-Z]\d+$|^\d+[a-zA-Z]$|^[a-zA-Z]\.$'
|
||||
return re.match(pattern, key) is not None
|
||||
def process_dict(data):
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
else:
|
||||
# 到达最内层,处理非字典和非列表的元素(字符串)
|
||||
return post_process(data)
|
||||
|
||||
# 测试数据
|
||||
# data = {
|
||||
# '开标': {
|
||||
# '开标': '1、黄石市公共资源电子交易平台远程不见面开标系统(以下简称:不见面开标系统)可以通过黄石市公共资源交易中心网站,黄石市工程建设电子交易系统(网址(https://gcjs.hsztbzx.com/login)进入不见面开投标人测试见面开标系统对投标人投标人测试见面开标系统对投标人标大厅,2、不见面开标系统对投标人测试测试见面开标系统对投标人测试见面开标系统对投标人测试见面开标系统对投标人测试见面开标系统对投标人测试见面投标人测试见面开标系统对投标人开标系统对投标人测试',
|
||||
# '开标程序': [
|
||||
# '主持人按下列程序在“电子交易平台”的“开标大厅”进行在线开标:(1)宣布开标纪律;(2)公布主持人、招标人代表、监标人等有关人员姓名;(3)公布在投标截止时间前投标文件的递交情况;(4)公布投标保证金递交情况;(5)投标人根据提示在投标人须知前附表规定的时间内解密投标文件;(6)读取已解密的投标文件的内容;(7)公布投标人名称、标段名称、投标保证金的递交情况、投标报价、质量目标、工期及其他内容,并生成开标记录;(8)开标结束。',
|
||||
# '在本章第5.2.1(5)目规定的时间内,非因“电子交易平台”原因造成投标文件未解密的,视为投标人撤回投标文件。已解密的投标文件少于三个的,招标失败;已解密的投标文件不少于三个,开标继续进行。'
|
||||
# ],
|
||||
# '开标异议': [
|
||||
# '投标人对开标有异议的,应当在开标过程中提出;招标人当场对异议作出答复,并记入开标记录。异议与答复应通过“开标大厅”在“异议与答复”菜单以书面形式进行。',
|
||||
# '投标人异议成立的,招标人将及时采取纠正措施,或者提交评标委员会评审确认;投标人异议不成立的,招标人将当场给予解释说明。'
|
||||
# ],
|
||||
# '特殊情况的处置': [
|
||||
# '1、黄石市公共资源电子交易平台远程不见面开标系统(以下简称:不见面开标系统)可以通过黄石市公共资源交易中心网站,黄石市工程建设电子交易系统(网址(https://gcjs.hsztbzx.com/login)进入不见面开标大厅,2、不见面开标系统对投标人测试测试见面开标系统对投标人测试见面开标系统对投标人测试见见面开标系统对投标人测试见面开标系面开标系统对投',
|
||||
# '因“电子交易平台”系统故障导致无法正常开标的,招标人将暂停开标,待系统恢复正常后继续开标。',
|
||||
# '“电子交易平台”系统故障是指下列情形:(1)系统服务器发生故障,无法访问或无法使用系统;(2)系统的软件或数据库出现错误,不能进行正常操作;(3)系统发现有安全漏洞,有潜在的泄密危险;(4)出现断电、断网事故;(5)其他无法保证招投标过程正常进行的情形。'
|
||||
# ],
|
||||
# '投标报价不参加评标基准价计算的情况': '若招标人发现投标文件出现投标人须知前附表中规定的投标报价不参加评标基准价计算的情况,经监标人确认后招标人将如实记录,其投标报价不参与评标基准价的计算,并提交评标委员会评审。'
|
||||
# }
|
||||
# }
|
||||
result = {}
|
||||
numeric_keys = []
|
||||
non_numeric_keys = {}
|
||||
|
||||
for key, value in data.items():
|
||||
if is_numeric_key(key):
|
||||
numeric_keys.append((key, value))
|
||||
else:
|
||||
non_numeric_keys[key] = value
|
||||
|
||||
if numeric_keys:
|
||||
result['items'] = [process_dict(item[1]) for item in sorted(numeric_keys)]
|
||||
|
||||
for key, value in non_numeric_keys.items():
|
||||
if isinstance(value, list):
|
||||
processed_list = []
|
||||
for item in value:
|
||||
if isinstance(item, dict):
|
||||
# 处理字典中只有一个键值对的情况
|
||||
if len(item) == 1:
|
||||
processed_item = process_dict(list(item.values())[0])
|
||||
else:
|
||||
processed_item = process_dict(item)
|
||||
else:
|
||||
processed_item = process_dict(item)
|
||||
|
||||
# 如果处理后的项是只包含一个元素的列表,则展平它
|
||||
if isinstance(processed_item, list) and len(processed_item) == 1:
|
||||
processed_item = processed_item[0]
|
||||
|
||||
processed_list.append(processed_item)
|
||||
|
||||
result[key] = processed_list
|
||||
else:
|
||||
result[key] = process_dict(value)
|
||||
|
||||
if len(result) == 1 and 'items' in result:
|
||||
return result['items']
|
||||
|
||||
return result
|
||||
data={
|
||||
'开标': '1、黄石市公共资源电子交易平台远程不见面开标系统(以下简称:不见面开标系统)可以通过黄石市公共资源交易中心网站,黄石市工程建设电子交易系统(网址(https://gcjs.hsztbzx.com/login)进入不见面开投标人测试见面开标系统对投标人投标人测试见面开标系统对投标人标大厅,2、不见面开标系统对投标人测试测试见面开标系统对投标人测试见面开标系统对投标人测试见面开标系统对投标人测试见面开标系统对投标人测试见面投标人测试见面开标系统对投标人开标系统对投标人测试'
|
||||
|
||||
}
|
||||
|
||||
|
||||
# 调用函数处理嵌套数据结构
|
||||
result = process_nested_data(data)
|
||||
print(json.dumps(result,ensure_ascii=False,indent=4))
|
||||
|
||||
process_dict()
|
@ -123,6 +123,7 @@ def transform_json(data):
|
||||
|
||||
return remove_single_item_lists(result)
|
||||
|
||||
#主要是处理键值中若存在若干序号且每个序号块的内容>=50字符的时候,用列表表示。
|
||||
def post_process(value):
|
||||
# 如果传入的是非字符串值,直接返回原值
|
||||
if not isinstance(value, str):
|
||||
@ -174,6 +175,9 @@ def post_process(value):
|
||||
|
||||
# 递归地处理嵌套结构
|
||||
def process_nested_data(data):
|
||||
# 先检查是否所有值都是 ""、"/" 或空列表
|
||||
if isinstance(data, dict) and all(v == "" or v == "/" or (isinstance(v, list) and not v) for v in data.values()):
|
||||
return list(data.keys())
|
||||
# 递归遍历字典,处理最内层的字符串
|
||||
if isinstance(data, dict):
|
||||
# 如果当前项是字典,继续递归遍历其键值对
|
||||
@ -197,7 +201,7 @@ def extract_from_notice(clause_path, type):
|
||||
elif type == 3:
|
||||
target_values = ["重新招标、不再招标和终止招标", "重新招标", "不再招标", "终止招标"]
|
||||
elif type == 4:
|
||||
target_values = ["开标"] #测试
|
||||
target_values = ["评标"] #测试
|
||||
else:
|
||||
raise ValueError("Invalid type specified. Use 1 for '投标文件, 投标' or 2 for '开标, 评标, 定标'or 3 for '重新招标'")
|
||||
with open(clause_path, 'r', encoding='utf-8') as file:
|
||||
@ -210,8 +214,8 @@ def extract_from_notice(clause_path, type):
|
||||
|
||||
#TODO: 再审视一下zbtest20的处理是否合理
|
||||
if __name__ == "__main__":
|
||||
file_path = 'C:\\Users\\Administrator\\Desktop\\招标文件\\招标test文件夹\\tmp\\clause1.json'
|
||||
# file_path='C:\\Users\\Administrator\\Desktop\\招标文件\\招标test文件夹\\clause1.json'
|
||||
file_path = 'C:\\Users\\Administrator\\Desktop\\货物标\\output4\\tmp1\\clause1.json'
|
||||
# file_path='C:\\Users\\Administrator\\Desktop\\招标文件\\招标test文件夹\\tmp\\clause1.json'
|
||||
try:
|
||||
res = extract_from_notice(file_path, 4) # 可以改变此处的 type 参数测试不同的场景
|
||||
res2=json.dumps(res,ensure_ascii=False,indent=4)
|
||||
|
@ -1,141 +0,0 @@
|
||||
import json
|
||||
import docx
|
||||
import re
|
||||
import os
|
||||
from PyPDF2 import PdfReader
|
||||
from flask_app.main.截取pdf import clean_page_content, extract_common_header
|
||||
|
||||
def extract_text_from_docx(file_path):
|
||||
doc = docx.Document(file_path)
|
||||
return '\n'.join([para.text for para in doc.paragraphs])
|
||||
|
||||
|
||||
def extract_text_from_pdf(file_path):
|
||||
# 从PDF文件中提取文本
|
||||
common_header = extract_common_header(file_path)
|
||||
pdf_document = PdfReader(file_path)
|
||||
text = ""
|
||||
# 遍历每一页
|
||||
for page in pdf_document.pages:
|
||||
# 提取当前页面的文本
|
||||
page_text = page.extract_text() if page.extract_text() else ""
|
||||
# 清洗页面文本
|
||||
page_text = clean_page_content(page_text, common_header)
|
||||
# 将清洗后的文本添加到总文本中
|
||||
text += page_text + "\n"
|
||||
return text
|
||||
|
||||
def extract_section(text, start_pattern, end_phrases):
|
||||
# 查找开始模式
|
||||
start_match = re.search(start_pattern, text)
|
||||
if not start_match:
|
||||
return "" # 如果没有找到匹配的开始模式,返回空字符串
|
||||
start_index = start_match.end() # 从匹配的结束位置开始
|
||||
|
||||
# 初始化结束索引为文本总长度
|
||||
end_index = len(text)
|
||||
|
||||
# 遍历所有结束短语,查找第一个出现的结束短语
|
||||
for phrase in end_phrases:
|
||||
match = re.search(phrase, text[start_index:], flags=re.MULTILINE)
|
||||
if match:
|
||||
end_index = start_index + match.start() # 更新结束索引为匹配到的开始位置
|
||||
break # 找到第一个匹配后立即停止搜索
|
||||
|
||||
# 提取并返回从开始模式后到结束模式前的内容
|
||||
return text[start_index:end_index]
|
||||
|
||||
def should_add_newline(content, keywords, max_length=20):
|
||||
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):
|
||||
if append_newline:
|
||||
if should_add_newline(current_content, keywords):
|
||||
current_content.append('\n') # 添加换行符
|
||||
append_newline = False
|
||||
current_content.append(line_content)
|
||||
return append_newline
|
||||
|
||||
#对二级标题如x.x进行额外处理:如果当前处理内容包含keywords中的内容,则必须保留换行符/如果当前内容字数大于20,不保留换行。
|
||||
def parse_text_by_heading(text):
|
||||
keywords = ['包含', '以下']
|
||||
data = {}
|
||||
current_key = None
|
||||
current_content = []
|
||||
append_newline = False
|
||||
|
||||
lines = text.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
line_stripped = line.strip()
|
||||
# 匹配形如 '1.1'、'2.2.3' 等至少包含一个点的标题,并确保其前后没有字母或括号
|
||||
match = re.match(r'^(?<![a-zA-Z((])(\d+(?:\.\d+)+)\s*(.*)', line_stripped)
|
||||
if not match:
|
||||
match = re.match(r'^(\d+\.)\s*(.+)$', line_stripped)
|
||||
|
||||
if match:
|
||||
new_key, line_content = match.groups()
|
||||
line_content = line_content.lstrip('.')
|
||||
# 检查是否应该更新当前键和内容
|
||||
if current_key is not None:
|
||||
# 将之前的内容保存到data中,保留第一个换行符,后续的换行符转为空字符
|
||||
content_string = ''.join(current_content).strip()
|
||||
data[current_key] = content_string.replace(' ', '')
|
||||
current_key = new_key
|
||||
current_content = [line_content]
|
||||
# 只有当标题级别为两级(如 1.1)时,才设置 append_newline 为 True
|
||||
append_newline = len(new_key.split('.')) == 2
|
||||
else:
|
||||
if line_stripped:
|
||||
append_newline = handle_content_append(current_content, line_stripped, append_newline, keywords)
|
||||
|
||||
if current_key is not None:
|
||||
# 保存最后一部分内容
|
||||
content_string = ''.join(current_content).strip()
|
||||
data[current_key] = content_string.replace(' ', '')
|
||||
|
||||
return data
|
||||
|
||||
def convert_to_json(file_path, start_word, end_phrases):
|
||||
if file_path.endswith('.docx'):
|
||||
text = extract_text_from_docx(file_path)
|
||||
elif file_path.endswith('.pdf'):
|
||||
text = extract_text_from_pdf(file_path)
|
||||
else:
|
||||
raise ValueError("Unsupported file format")
|
||||
# 提取从 start_word 开始到 end_phrases 结束的内容
|
||||
text = extract_section(text, start_word, end_phrases)
|
||||
# print(text)
|
||||
parsed_data = parse_text_by_heading(text)
|
||||
return parsed_data
|
||||
|
||||
def convert_clause_to_json(input_path, output_folder, type=1):
|
||||
if not os.path.exists(input_path):
|
||||
print(f"The specified file does not exist: {input_path}")
|
||||
return ""
|
||||
if type == 1:
|
||||
start_word = "第四章"
|
||||
end_phrases = [
|
||||
"第五章"
|
||||
]
|
||||
else:
|
||||
start_word = r'第[一二三四五六七八九十]+章\s*招标公告|第一卷|招标编号:|招标编号:'
|
||||
end_phrases = [r'第[一二三四五六七八九十]+章\s*投标人须知', r'投标人须知前附表']
|
||||
result = convert_to_json(input_path, start_word, end_phrases)
|
||||
file_name = "clause1.json" if type == 1 else "clause2.json"
|
||||
output_path = os.path.join(output_folder, file_name)
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, indent=4, ensure_ascii=False)
|
||||
print(f"投标人须知正文条款提取成json文件: The data has been processed and saved to '{output_path}'.")
|
||||
return output_path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# file_path = 'D:\\flask_project\\flask_app\\static\\output\\cfd4959d-5ea9-4112-8b50-9e543803f029\\ztbfile_tobidders_notice.pdf'
|
||||
file_path = 'C:\\Users\\Administrator\\Desktop\\货物标\\output3\\2-招标文件(广水市教育局封闭管理)_qualification1.pdf'
|
||||
output_folder = 'C:\\Users\\Administrator\\Desktop\\货物标\\output3\\tmp'
|
||||
try:
|
||||
output_path = convert_clause_to_json(file_path, output_folder)
|
||||
print(f"Final JSON result saved to: {output_path}")
|
||||
except ValueError as e:
|
||||
print("Error:", e)
|
@ -10,7 +10,6 @@ from flask_app.main.形式响应评审 import update_json_data,extract_matching_
|
||||
from flask_app.main.json_utils import extract_content_from_json
|
||||
#这个字典可能有嵌套,你需要遍历里面的键名,对键名作判断,而不是键值,具体是这样的:如果处于同一层级的键的数量>1并且键名全由数字或点号组成。那么就将这些序号键名全部删除,重新组织成一个字典格式的数据,你可以考虑用字符串列表来保持部分平级的数据
|
||||
#对于同级的键,如果数量>1且键名都统一,那么将键名去掉,用列表保持它们的键值
|
||||
#对于同一个字典中,可能存在若干键值对,若它们的键值都是""或者"/" 你就将它们的键值删去,它们的键名用字符串列表保存
|
||||
|
||||
def is_numeric_key(key):
|
||||
# 这个正则表达式匹配由数字、点、括号中的数字或单个字母(小写或大写)组成的字符串,
|
||||
@ -40,7 +39,7 @@ def preprocess_dict(data):
|
||||
if isinstance(data, dict):
|
||||
if len(data) > 1:
|
||||
# 检查是否所有值都是 "" 或 "/"
|
||||
if all(v == "" or v == "/" for v in data.values()):
|
||||
if all(v == "" or v == "/" or (isinstance(v, list) and not v)for v in data.values()):
|
||||
return list(data.keys())
|
||||
else:
|
||||
processed = {}
|
||||
@ -56,6 +55,7 @@ def preprocess_dict(data):
|
||||
return [preprocess_dict(item) for item in data]
|
||||
else:
|
||||
return data
|
||||
|
||||
def process_dict(data):
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
@ -98,10 +98,13 @@ def process_dict(data):
|
||||
|
||||
if len(result) == 1 and 'items' in result:
|
||||
return result['items']
|
||||
# 检查如果所有键对应的值都是空列表,则将键名转换成列表项
|
||||
if all(isinstance(v, list) and not v for v in result.values()):
|
||||
return list(result.keys())
|
||||
|
||||
return result
|
||||
|
||||
|
||||
#查找引用的序号
|
||||
def find_chapter_clause_references(data, parent_key=""):
|
||||
exclude_list=["格式要求"]
|
||||
result = []
|
||||
|
Loading…
x
Reference in New Issue
Block a user