山东大学项目实训-基于LLM的中文法律文书生成系统(十三)- RAG(2)

今天主要讲langchain向量知识库解析文档的相关内容。

文档解析逻辑

UnstructuredFileLoader

word读取

按照mode="single"来整个文档加载,如下有1个page_content:

from langchain.document_loaders import UnstructuredFileLoader
loader = UnstructuredFileLoader(filepath, mode="single")
print(loader.load())
----------------------------------------
[Document(page_content='这是第一句话,今天天气真好。\n\n这是第二句话,我换行了。\n\n这是第三句话,我顶格换行了。 这是第四句话,我没换行但空格。这是第五句话,啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊不不不不不不不不不不不不不不不不不不不不不不不不不不不不不,哒哒哒哒哒哒;啦啦啦啦啦啦啦啦绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿绿!\n\n    这是第六句话,这里要凑到200个字了,嘻嘻嘻嘻嘻嘻嘻。', metadata={'source': '/home/ph/LLM/Chinese-LangChain-master/docs/added/示例.docx'})]

如果mode="elements"在word上是根据换行来切分,如下有4个page_content:

from langchain.document_loaders import UnstructuredFileLoader
loader = UnstructuredFileLoader(filepath, mode="elements")
print(loader.load())

img

loader.load() 可以加载文档可视

txt读取

上面相同txt文档加载,不会分段,一个page_content。

from langchain.document_loaders import UnstructuredFileLoader
filepath = 'xxx/x.txt'
loader = UnstructuredFileLoader(filepath)
print(loader.load())

img

xlsx读取

加载后,没有按行分段,只有一个Documnet,其中 换行'\n \n \n' 同一行换列'\n '表示

from langchain.document_loaders import UnstructuredFileLoader
filepath = 'xxx/x.xlsx'
loader = UnstructuredFileLoader(filepath)
print(loader.load())

json读取

JSON是一种标准文件格式和数据交换格式,有key:value组成,这里用JSONLoader来解析,指定jq schema来语法(pip install jq)。对json整个文档读取,其中jq_schema='.'表示全部读取,如果是'.[].input'索引其中一个key为input的所有value作为page_content.

import json
from pathlib import Path
file_path='./data/facebook_chat.json'
data = json.loads(Path(file_path).read_text())

loader2 = JSONLoader(
    file_path=json_path,
    jq_schema='.', text_content=False)   #jq_schema='.'表示全部读取  '.[].input'索引其中一个key
  • JSON -> [{"text": ...}, {"text": ...}, {"text": ...}]

jq_schema -> ".[].text"

  • JSON -> {"key": [{"text": ...}, {"text": ...}, {"text": ...}]}

jq_schema -> ".key[].text"

  • JSON -> ["...", "...", "..."]

jq_schema -> ".[]"

详见:JSON | ️ Langchain

PDF读取

PDF标准化为ISO 32000,可以使用PyPDFLoader读取,其中会按页面划分Document, 每个文档包含页面内容和元数据,以及page号码。可以通过页码来检索文档,比如第2页就pages[1].

# !pip install pypdf
from langchain.document_loaders import PyPDFLoader

loader = PyPDFLoader(path)
pages = loader.load_and_split()
print(loader.load())

img

10页分开加载,并且末尾page标号,没有多余原数据

如果用UnstructuredPDFLoader加载PDF,分两种mode,其中'single'表示整个文本全部一起加载Document;‘elements’表示根据标题和描述文本分段加载,也就是段落和标题都要分开。

from langchain.document_loaders import UnstructuredPDFLoader
loader = UnstructuredPDFLoader(path,mode="elements")   # Title and NarrativeText
print(loader.load())

img

csv读取-CSVLoader

CSV文件是一种使用逗号分隔值的定界文本文件。文件的每一行是一个数据记录,每一列通过'\n'来区分。CSVLoader针对csv数据加载,将一行数据读取为一个Document(page_content='xxx')中。

from langchain.document_loaders.csv_loader import CSVLoader
loader = CSVLoader(file_path='xxx/waimai_10k.csv')
data = loader.load()

img

Document(page_content='label: 1\nreview: 很快,好吃,味道足,量大', metadata={'source': '/home/ph/LLM/torchkeras-master/examples/waimai_10k.csv', 'row': 0})

  • 'label: 1\nreview: 很快,好吃,味道足,量大':label是第一列的key, review是第二列的key,类似字典对应

使用source_column参数指定从每一行创建的文档的来源。否则,file_path将作为从 CSV 文件创建的所有文档的来源。

loader = CSVLoader(file_path='xxx/waimai_10k.csv', source_column="label")

data = loader.load()

可以将metadata文件的source来源改为对应列的value,这样更有利于索引问题的链时高效。


文档分割

如果经过上面文档加载成Document这种,使用loader.load_and_split(text_splitter)text_splitter.split_documents(loader)

如果是单纯的字符串string,使用text_splitter.split_text(text)

CharacterTextSplitter按字符拆分

这是最简单的方法。它根据字符(默认为 "\n\n")进行拆分,并通过字符数来衡量块的长度。

with open('state_of_the_union.txt') as f:
    state_of_the_union = f.read()
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(        
    separator = "\n\n",
    chunk_size = 1000,
    chunk_overlap  = 200,
    length_function = len,
)
texts = text_splitter.create_documents([state_of_the_union])

text_splitter_module = importlib.import_module('langchain.text_splitter')
TextSplitter = getattr(text_splitter_module, "CharacterTextSplitter")
等同于
from langchain.text_splitter import CharacterTextSplitter

你现在要加载word文档并分割:将字符分割CharacterTextSplitter作为text_splitter划分标准,可以划分docx文件:

from langchain.document_loaders import UnstructuredFileLoader
from langchain.text_splitter import CharacterTextSplitter
loader = UnstructuredFileLoader('xxx/xx.docx', mode="elements")   
docs_all = loader.load()      # 这里加载文档。
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)    # 换行划分\n\n
docs = text_splitter.split_documents(docs_all)    

docs[mode="elements"]分割后打印出来(本来换行就分割好了),不过我注意到word里面如果有英文的逗号并且产生半个占位符会将它分割开来:

img

也可以自定义分割的类ChineseTextSplitter:

class ChineseTextSplitter(CharacterTextSplitter):
    def __init__(self, pdf: bool = False, **kwargs):
        super().__init__(**kwargs)
        self.pdf = pdf

    def split_text(self, text: str) -> List[str]:
        if self.pdf:  # 如果传入是pdf
            text = re.sub(r"\n{3,}", "\n", text)  # 将连续出现的3个以上换行符替换为单个换行符,从而将多个空行缩减为一个空行。
            text = re.sub('\s', ' ', text)  # 将文本中的所有空白字符(例如空格、制表符、换行符等)替换为单个空格
            text = text.replace("\n\n", "")  # 将文本中的连续两个换行符替换为空字符串
        sent_sep_pattern = re.compile(
            '([﹒﹔﹖﹗.。!?]["’”」』]{0,2}|(?=["‘“「『]{1,2}|$))')  # 用于匹配中文文本中的句子分隔符,例如句号、问号、感叹号等
        sent_list = []
        for ele in sent_sep_pattern.split(text):
            if sent_sep_pattern.match(ele) and sent_list:
                sent_list[-1] += ele
            elif ele:
                sent_list.append(ele)  
        return sent_list
--------------------------------------------
textSplitter = ChineseTextSplitter(True)
loader = UnstructuredFileLoader(filepath, mode="elements")
docs= loader.load_and_split(textSplitter)   # 这里进入.split_text()

RecursiveCharacterTextSplitter

RecursiveCharacterTextSplitter为递归字符分割器,它由一个字符列表参数化。它尝试按顺序在它们上进行切割,直到块变得足够小。默认列表是["\n\n", "\n", " ", ""]。这样做的效果是尽可能保持所有段落(然后句子,然后单词)在一起,因为它们在语义上通常是最相关的文本片段。

  • chunk_size:每一个分片的最大大小
  • chunk_overlap:相邻的块之间的最大重叠。有一些重叠可以很好地保持块之间的一些连续性(类似于一个滑动窗口)。
  • separators:定义分割文本时使用的字符顺序,优先顺序为段落分隔符、句子分隔符和空格等。
from langchain.text_splitter import RecursiveCharacterTextSplitter

r_splitter = RecursiveCharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=0,
    separators=["\n"]     # 自定义切分
)

test = """a\nbcefg\nhij\nk"""
print(len(test))
text = r_splitter.split_text(test)      # test没进过加载器

这里打印出来test长度为13,'\n'长度为1。我们自定义按照'\n'切分,但chunk_size很影响结果,优先以分割符为准,如果分割后的长度超过chunk_size没办法就不管他,但如果两块分割后的求和可以小于chunk_size则将它们合并,尽量保持语义的完整。比如:

  • chunk_size=2时切分为['a', '\nbcefg', '\nhij', '\nk']
  • chunk_size=5时切分为['a', '\nbcefg', '\nhij', '\nk']
  • chunk_size=6时切分为['a', '\nbcefg', 'hij\nk'] -> 这里可以合并‘hij\nk’因为长度不超过chunk_size
  • chunk_size=10时切分为['a\nbcefg', 'hij\nk']
  • chunk_size=13时切分为['a\nbcefg\nhij\nk']

这个好处就是尽量让分割后的能合并成不超过chunk_size的连续语句,如果希望段和段也要关联,则增大chunk_overlap。

SpacyTextSplitter

NLTK 的另一种替代方案是使用 Spacy文本分割器。

需要先下载en_core_web_smzh_core_web_sm离线安装包,在环境中安装:

pip install en_core_web_sm-2.3.0.tar.gz -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install zh_core_web_sm-3.6.0-py3-none-any.whl -i https://pypi.tuna.tsinghua.edu.cn/simple

块大小如何被测量:通过传递的长度函数(默认为字符数),并且列举了三种文档分割器加载loader的写法:

with open('../../../state_of_the_union.txt') as f:
    loader = f.read()
from langchain.text_splitter import SpacyTextSplitter
text_splitter = TextSplitter(
    pipeline="zh_core_web_sm",
    chunk_size=200,
    chunk_overlap=0,
)
# 有三种
texts = text_splitter.split_text(loader)
texts = text_splitter.split_documents(loader)
texts = loader.load_and_split(text_splitter)

项目代码解读

for file in file_list:
    ext_name = os.path.splitext(file)[-1]
    if ext_name == ".pptx":
        loader = UnstructuredPowerPointLoader(file)
    elif ext_name == ".docx":
        loader = UnstructuredWordDocumentLoader(file)
    elif ext_name == ".pdf":
        loader = UnstructuredPDFLoader(file)
    else:
        loader = UnstructuredFileLoader(file)

    doc = loader.load()
    doc[0].page_content = self.filter_space(doc[0].page_content)
    doc = text_splitter.split_documents(doc)
    docs.extend(doc)

这里根据文件扩展名选择合适的加载器加载文件内容:

UnstructuredPowerPointLoader、UnstructuredWordDocumentLoader、UnstructuredPDFLoader、UnstructuredFileLoader:

分别用于加载PPTX、DOCX、PDF和其他文件。

调用loader.load()加载文档内容,并通过filter_space方法过滤空白字符,使用text_splitter.split_documents(doc)方法将文档按指定字符分割成块,并添加到docs列表中。

if self.vector_db is None:
    self.files = ", ".join([item.split("/")[-1] for item in file_list])
    emb = self.docs2embedding([x.page_content for x in docs])
    self.vector_db = faiss.IndexFlatL2(self.embeddings_size)
    self.vector_db.add(np.array(emb))
    self.string_db = docs
else:
    self.files = self.files + ", " + ", ".join([item.split("/")[-1] for item in file_list])
    emb = self.docs2embedding([x.page_content for x in docs])
    self.vector_db.add(np.array(emb))
    self.string_db += docs
如果self.vector_db为空

表示向量数据库尚未初始化,将文件名列表转换为字符串并存储在self.files中,调用docs2embedding方法将文档内容转换为特征向量。

使用faiss.IndexFlatL2创建一个新的L2距离向量索引,并添加特征向量。

将分割后的文档存储在self.string_db中。

self.vector_db已存在

,表示需要更新现有数据库,此时更新self.files,将新文件名追加到现有文件列表中,内容转换为特征向量并添加到现有向量数据库中,并将新文档追加到self.string_db中。

完整代码:

def init_vector_db_from_documents(self, file_list: List[str]):
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=325, chunk_overlap=6,
                                                   separators=["\n\n", "\n", "。", "!", ",", " ", ""])
    docs = []
    for file in file_list:
        ext_name = os.path.splitext(file)[-1]
        if ext_name == ".pptx":
            loader = UnstructuredPowerPointLoader(file)
        elif ext_name == ".docx":
            loader = UnstructuredWordDocumentLoader(file)
        elif ext_name == ".pdf":
            loader = UnstructuredPDFLoader(file)
        else:
            loader = UnstructuredFileLoader(file)
	doc = loader.load()
    doc[0].page_content = self.filter_space(doc[0].page_content)
    doc = text_splitter.split_documents(doc)
    docs.extend(doc)

# 文件解析失败
if len(docs) == 0:
    return False

if self.vector_db is None:
    self.files = ", ".join([item.split("/")[-1] for item in file_list])
    emb = self.docs2embedding([x.page_content for x in docs])
    self.vector_db = faiss.IndexFlatL2(self.embeddings_size)
    self.vector_db.add(np.array(emb))
    self.string_db = docs
else:
    self.files = self.files + ", " + ", ".join([item.split("/")[-1] for item in file_list])
    emb = self.docs2embedding([x.page_content for x in docs])
    self.vector_db.add(np.array(emb))
    self.string_db += docs
return True
posted @ 2024-05-31 09:00  H1S96  阅读(207)  评论(0)    收藏  举报