You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

293 lines
12 KiB

5 months ago
import uuid
from langchain_chroma import Chroma
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from qwen_agent.agents import Assistant
import json_repair
from paddlenlp import Taskflow
embeddings = DashScopeEmbeddings(dashscope_api_key="sk-ea89cf04431645b185990b8af8c9bb13")
device_id=0
import re
import time
from docx import Document
import shutil
from docx.opc.pkgreader import _SerializedRelationships, _SerializedRelationship
from docx.opc.oxml import parse_xml
import logging
import logging.config
log_config = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'standard',
'level': logging.INFO,
},
'file': {
'class': 'logging.FileHandler',
'filename': 'Logger.log',
'formatter': 'standard',
'level': logging.INFO,
},
},
'loggers': {
'': {
'handlers': ['console', 'file'],
'level': logging.INFO,
'propagate': True,
},
}
}
logging.config.dictConfig(log_config)
logger = logging.getLogger("checkRepeatText")
def load_from_xml_v2(baseURI, rels_item_xml):
"""
Return |_SerializedRelationships| instance loaded with the
relationships contained in *rels_item_xml*. Returns an empty
collection if *rels_item_xml* is |None|.
"""
srels = _SerializedRelationships()
if rels_item_xml is not None:
rels_elm = parse_xml(rels_item_xml)
for rel_elm in rels_elm.Relationship_lst:
if rel_elm.target_ref in ('../NULL', 'NULL'):
continue
srels._srels.append(_SerializedRelationship(baseURI, rel_elm))
return srels
_SerializedRelationships.load_from_xml = load_from_xml_v2
# 记录程序开始的时间戳
def getOutlineLevel(inputXml):
"""
功能 从xml字段中提取出<w:outlineLvl w:val="number"/>中的数字number
参数 inputXml
返回 number
"""
start_index = inputXml.find('<w:outlineLvl')
end_index = inputXml.find('>', start_index)
number = inputXml[start_index:end_index + 1]
number = re.search("\d+", number).group()
return number
def isTitle(paragraph):
"""
功能 判断该段落是否设置了大纲等级
参数 paragraph:段落
返回 None:普通正文没有大纲级别 0:一级标题 1:二级标题 2:三级标题
"""
# 如果是空行,直接返回None
if paragraph.text.strip() == '':
return None
# 如果该段落是直接在段落里设置大纲级别的,根据xml判断大纲级别
paragraphXml = paragraph._p.xml
if paragraphXml.find('<w:outlineLvl') >= 0:
return getOutlineLevel(paragraphXml)
# 如果该段落是通过样式设置大纲级别的,逐级检索样式及其父样式,判断大纲级别
targetStyle = paragraph.style
while targetStyle is not None:
# 如果在该级style中找到了大纲级别,返回
if targetStyle.element.xml.find('<w:outlineLvl') >= 0:
return getOutlineLevel(targetStyle.element.xml)
else:
targetStyle = targetStyle.base_style
# 如果在段落、样式里都没有找到大纲级别,返回None
return None
#寻找标题名称
def findTitleName(docxPath):
yield '文档相似性检查----检查是否存在详细设计方案'
document = Document(docxPath)
# 逐段读取docx文档的内容
titleWords=[]
firstTitle = 0
secondTitle = 0
sanjiTitle = 0
for paragraph in document.paragraphs:
# 判断该段落的标题级别
# 这里用isTitle()临时代表,具体见下文介绍的方法
text = paragraph.text
if text.strip():#非空判断
level = isTitle(paragraph)
if level=="0":
firstTitle+=1
secondTitle = 0
if(text.find("附件")>=0):
continue
titleWords.append("一级标题:".format(firstTitle)+text)
elif level=="1":
secondTitle+=1
sanjiTitle=0
# words.append("\t"+"{}.{}".format(firstTitle,secondTitle)+text)
# titleWords.append("第{}章的二级标题:".format(firstTitle,firstTitle,secondTitle)+text)
elif level=="2":
sanjiTitle += 1
# words.append("\t"+"{}.{}".format(firstTitle,secondTitle)+text)
# titleWords.append("第{}章的三级标题".format(firstTitle, secondTitle,firstTitle, secondTitle,sanjiTitle) + text)
findTitleName_llm_cfg = {
#'model': 'qwen1.5-72b-chat',
'model':"qwen2-72b",
'model_server': 'http://127.0.0.1:1025/v1', # base_url, also known as api_base
# 'api_key': 'sk-ea89cf04431645b185990b8af8c9bb13',
}
findTitleName_bot = Assistant(llm=findTitleName_llm_cfg,
name='Assistant',
# system_message='1:这样的是一级标题。1.1:这样的是二级标题。1.1.1:这样的是三级标题'
)
prompt='''\n是文档的大纲,一级标题组成,哪一章存在与方案相关的内容
类似详细设计方案,详细服务方案详细建设方案为最相关的优先选择
类似设计方案服务方案建设方案为次相关次级选择
类似方案是最后选择
按照这样的顺序选择最合适的
你只能从这两个答案中选择一个{"name":"一级标题名称","answer":"存在"}{"name":"","answer":"不存在"}不做过多的解释,严格按回答格式作答
'''
# print("\n".join(titleWords)+prompt)
messages = [({'role': 'user', 'content': "\n".join(titleWords)+prompt})]
runList=[]
for rsp in findTitleName_bot.run(messages):
runList.append(rsp)
data = runList[len(runList) - 1][0]["content"]
parsed_data = json_repair.loads(data.replace('`', ''))
logger.info(parsed_data)
if(parsed_data["answer"]=="存在"):
yield parsed_data["name"]
else:
yield "文档相似性检查----未找到与详细设计方案相关内容,无法进行相似性比较"
#获取文档中 详细设计方案 章节的所有内容
def getDocxToText(docxPath,titleName,vector_store_path):
document = Document(docxPath)
# 逐段读取docx文档的内容
levelList=[]
words=[]
addStart = False
levelText=""
i = 0
for paragraph in document.paragraphs:
# 判断该段落的标题级别
# 这里用isTitle()临时代表,具体见下文介绍的方法
text = paragraph.text
if text.strip():#非空判断
if titleName:
level = isTitle(paragraph)
if(addStart and level=="0"):
addStart=False
if(level=="0" and (titleName.find(text)>=0 or text.find(titleName)>=0)):
addStart=True
if level:
levelList.append("{}".format(level)+paragraph.text)
levelText=f"{int(level)+1}级标题-"+text
else:
if addStart:
if(text.startswith("") or text.startswith("注:")):
continue
if(len(text)>30):
i=i+1
words.append("{}".format(levelText)+text)
# 将所有段落文本拼接成一个字符串,并用换行符分隔
if len(words)==0:
raise Exception("checkRepeatText,获取长度为0")
text = '\n'.join(words)
# 将文本写入txt文件
with open("checkRepeatText.txt", 'w', ) as txt_file:
txt_file.write(text)
time.sleep(3)
loader = TextLoader(file_path='checkRepeatText.txt')
docs = loader.load()
# print(docs)
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=10, add_start_index=True,
separators=["\n\n", "\n"])
splits = text_splitter.split_documents(docs)
uuids = []
for i in range(len(splits)):
uuids.append(str(uuid.uuid4()))
logging.info(f"checkRepeatTextuuidLen{len(uuids)}")
vectorstore = Chroma(persist_directory=vector_store_path, embedding_function=embeddings)
vectorstore.add_documents(documents=splits, ids=uuids)
while True:
time.sleep(0.3)
ress = vectorstore.similarity_search(words[0])
if (len(ress) > 0):
break
return words,uuids,vectorstore
# @app.route('/checkRepeatText/<filename>', methods=['GET'])
def checkRepeatText(filename):
yield "文档相似性检查---启动中...."
vector_store_path="vector_store"+str(uuid.uuid4())
for titleName in findTitleName(filename):
yield titleName
if(titleName!="文档相似性检查----未找到与详细设计方案相关内容,无法进行相似性比较"):
try:
yield "文档相似性检查----文档内容解析中"
words,uuids,vectorstore=getDocxToText(filename,titleName,vector_store_path)
except Exception as e:
yield f"文档相似性检查----文档内容获取失败,未找到**{titleName}**相关内容或文档打开失败"
return
# 记录程序开始的时间戳‘
global device_id
similarity = Taskflow("text_similarity",device_id=3)
# device_id+=1
# if(device_id>1):
# device_id=0
reslist = []
count = 0
for i in words:
count += 1
yield f"文档相似性检查--对{titleName}章节,进行文档内容检查中{count}/{len(words)}"
result = vectorstore.similarity_search(i)
textTag = i.split("")[0]
for content in result:
text = content.page_content
tag = text.split("")[0].replace('\n', '')
if (textTag.find(tag) >= 0):
continue
try:
res = similarity([[i[i.find('') + 1:], text[text.find('') + 1:]]])
except Exception as e:
logger.warning("文档相似性检查--发生异常:",e)
logger.warning(i)
logger.warning(text)
if (res[0]["similarity"] > 0.90):
# 判断重复内容是否被放入
if (len(reslist) > 0):
isExist = False
for neirong in reslist:
if i in neirong.values():
isExist = True
break
if not isExist:
# reslist.append({"yuanwen1":i[i.find(':') + 1:],"yuanwen2":text[text.find(':') + 1:],"similarity":res[0]["similarity"]})
reslist.append({"yuanwen1":i.replace("\n",""),"yuanwen2":text.replace("\n",""),"similarity":res[0]["similarity"]})
else:
reslist.append({"yuanwen1":i.replace("\n",""),"yuanwen2":text.replace("\n",""),"similarity":res[0]["similarity"]})
# print(i.split(":")[1] + "\n" + text.split(":")[1])
# vectorstore.delete(ids=uuids)
shutil.rmtree(vector_store_path)
logger.info("已删除")
logger.info(reslist)
resInfo=f"{titleName}章节,发现相似内容:<br>"
if(len(reslist)>0):
for res in reslist:
resInfo+="【在**"+res["yuanwen1"][:res["yuanwen1"].find('')]+"**下包含:"+res["yuanwen1"][res["yuanwen1"].find('') + 1:]+"<br>在**"+res["yuanwen2"][:res["yuanwen2"].find('')]+"**下包含:"+res["yuanwen2"][res["yuanwen2"].find('') + 1:]+"<br>以上两段内容***相似度***:"+'{:.2f}'.format(res['similarity'])+"】<br>"
yield resInfo
logger.info(resInfo)
else:
yield "未发现相似内容"