自制RAG工具:docx文档读取工具

自制RAG工具:docx文档读取工具

  • 1. 介绍
  • 2. 源码
    • 2.1 chunk
    • 2.2 DocReader
  • 3. 使用方法
    • 3.1 文档格式设置
    • 3.2 代码使用方法

1. 介绍

在RAG相关的工作中,经常会用到读取docx文档的功能,为了更好地管理文档中的各个分块,以提供更高质量的prompt给LLM,我在去年实现了一个轻量好用的docx管理工具。

主要应用到python模块docx。安装依赖:

python-docx                              1.0.1

2. 源码

代码结构非常简单,仅有两个类构成。以及需要引用的部分:

import docx
from uuid import uuid4
from typing import *

ZH2NUM = {'一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '十': 10}

2.1 chunk

chunk类指的是文档中一个分块,考虑到LLM的长度限制问题和成本问题,通常需要对文档进行分块处理,尤其是对于篇幅很长的文档,需要在文档内部再做一次召回。

class Chunk:
    """
    文本块
    ---------------
    ver: 2023-11-02
    by: changhongyu
    """
    def __init__(self, id_: str, level: int, content: str, children: List = None, max_depth: int = 99):
        """
        :param id_: 此文本块的唯一id
        :param level: 此文本块的层级
        :param content: 此文本块的内容
        :param children: 此文本块的所有下一级文本块
        :param max_depth: 允许存在的文本块最大层级数,由DocReader控制
        """
        self.id = id_
        self.level = level
        self.content = content
        self.children = children
        self.max_depth = max_depth
        if not self.children:
            self.children = []
        self.path_to_this_chunk = None
        self.title_path = None

    def __len__(self):
        return len(self.content)

    def __str__(self):
        msg = ''
        msg += f'[{self.level}]'
        if self.level >= 99:
            msg += '  ' * self.max_depth
        else:
            msg += '  ' * (self.level - 1)
        if len(self.content) < 20:
            msg += self.content
        else:
            msg += self.content[:20]
            msg += '...'
        msg += '\n'
        for child in self.children:
            msg += str(child)
        return msg

2.2 DocReader

文档读取器,直接传入一篇文档的地址以实例化。

class DocxReader:
    """
    读取一篇docx文档,并转化为结构化格式
    ---------------
    ver: 2023-11-02
    by: changhongyu
    """
    def __init__(self, doc_path: str, doc_name: str = None, filter_none_text: bool = True):
        """
        :param doc_path: 文件路径
        :param doc_name: 文档名称,如空则用路径为名
        :param filter_none_text: 过滤掉非文本的内容
        """
        self.doc = docx.Document(doc_path)
        self.doc_name = doc_name if doc_name else doc_path
        max_depth = max(self.style2level(para.style.name) for para in self.doc.paragraphs)
        self.chunks = [self.para2chunk(para, max_depth=max_depth) for para in self.doc.paragraphs if para.text]
        if filter_none_text:
            self.chunks = [chunk for chunk in self.chunks if chunk.level <= 10]
        # 合并level==10的内容部分
        self.chunks = self.combine_chunks()
        self.id2chunks = {chunk.id: chunk for chunk in self.chunks}
        self.doc_tree = Chunk(self.doc_name, 0, self.doc_name, self.build_tree())
        for chunk in self.chunks:
            _ = self.get_path_to(chunk.id)

    def __len__(self):
        return len(self.chunks)

    def __getitem__(self, idx):
        return self.id2chunks[idx]

    @classmethod
    def style2level(cls, style_name):
        if '级' in style_name:
            num = style_name.split('级')[0]
            if num in ZH2NUM:
                return ZH2NUM[num]
            else:
                return int(num)
        if '标题' in style_name:
            return int(style_name.strip().split('标题')[-1])
        elif '正文' in style_name or 'Normal' in style_name:
            return 10
        elif 'Heading' in style_name:
            return int(style_name.split('Heading')[-1])
        elif '图' in style_name or 'Caption' in style_name:
            return 11
        else:
            return 12

    def para2chunk(self, para, max_depth):
        return Chunk(self.doc_name + '**' + str(uuid4()), self.style2level(para.style.name), para.text, [], max_depth)

    def combine_chunks(self):
        """
        将同一级目录下的数据合并成大一点的分块
        只对内容块进行操作,不对标题块进行操作
        """
        combined_chunks = []
        prev_level = -1
        cur_chunk = None

        for chunk in self.chunks:
            if chunk.level < 10:
                if cur_chunk is not None:
                    # 如果当前存在分块,则将其添加
                    combined_chunks.append(cur_chunk)
                    cur_chunk = None
                # 如果是标题,则无需对这个分块进行处理
                combined_chunks.append(chunk)
            elif chunk.level == 10:
                # 如果是内容,则判断上一个分块是否也是内容
                if prev_level == 10:
                    # 如果是连续的内容,则合并
                    cur_chunk.content += f'\n{chunk.content}'
                else:
                    # 如果是第一次遇到内容,则创建要给新的
                    cur_chunk = Chunk(self.doc_name + '**' + str(uuid4()), 10, chunk.content, [], 10)
            else:
                continue
            prev_level = chunk.level
        return combined_chunks

    def build_tree(self):
        """
        建立树状结构
        """
        def build_subtree(chunks_):
            if not chunks_:
                return []
            root = chunks_[0]
            remaining_nodes = chunks_[1:]

            # 找到当前节点的子节点
            child_nodes = []
            while remaining_nodes and remaining_nodes[0].level > root.level:
                child_nodes.append(remaining_nodes.pop(0))

            # 递归构建子树
            root.children = build_subtree(child_nodes)

            return [root] + build_subtree(remaining_nodes)

        return build_subtree(self.chunks)

    def get_path_to(self, id_: str):
        """
        给定一个文本块的id,获取从根节点到该块的路径
        """
        def trace_back(chunk: Chunk, cur_path: List, target_id: str):
            if chunk is None:
                return

            # 如果找到了直接返回
            cur_path.append(chunk)
            if chunk.id == target_id:
                return cur_path

            # dfs子树
            for child in chunk.children:
                path_ = trace_back(child, cur_path, target_id)
                if path_ is not None:
                    return path_

            # 如果没有找到,回溯
            cur_path.pop()
            return

        # 先检查这个路径有没有已经计算过
        if self[id_].path_to_this_chunk is not None:
            return self[id_].path_to_this_chunk

        path_chunks = trace_back(self.doc_tree, [], id_)
        for i in range(1, len(path_chunks)):
            # 跟新路径中的所有chunk的路径
            if path_chunks[i].path_to_this_chunk is None:
                path_chunks[i].path_to_this_chunk = [chunk.id for chunk in path_chunks[: i]]
        path_id = [chunk.id for chunk in path_chunks]
        title_path = '_'.join(chunk.content for chunk in path_chunks if chunk.level < 10)
        if not title_path:
            title_path = self.doc_name
        self[id_].path_to_this_chunk = path_id
        self[id_].title_path = title_path
        return path_id

3. 使用方法

3.1 文档格式设置

这个工具并不是任何文档都能使用的,由于是基于docx模块来确定每个段落的层级的,所以需要在文档中,把文档格式设置成正确的格式。这个过程目前没有想到特别好的自动化实现的方法。最好是在编辑文档的时候就注意一下格式规范。

以WPS为例,需要在这里选中对应的格式:
格式选择

3.2 代码使用方法

本工具的功能主要包括如下:

(1)创建

doc_item = DocxReader('./测试.docx', doc_name='测试')

(2)打印结构

print(doc_item.doc_tree)

# [0]测试
# [1]标题1
# [2]  标题1-1
# [3]    标题1-1-1
# [10]                  111内容内容内容内容
# [2]  标题1-2
# [3]    标题1-2-1
# [10]                  121内容内容内容内容
# [3]    标题1-2-2

(3)id索引

doc_item.id2chunks

# {'测试**a7f1dba1-89fc-4861-b9b4-2035c1d136f7': <__main__.Chunk at 0x7f235145d750>,
#  '测试**5dc92bc5-e856-47e7-a67e-8fff6b496614': <__main__.Chunk at 0x7f235145d6f0>,
#  '测试**7dec18a1-b50d-47d8-808d-250f16f240e9': <__main__.Chunk at 0x7f235145c4c0>,
#  '测试**2ab5291e-b2a1-4d04-a19b-37e9fceb56f9': <__main__.Chunk at 0x7f235145cfa0>,
#  '测试**e3eb2029-6493-40df-803a-5c80f2ce7040': <__main__.Chunk at 0x7f235145cee0>,
#  '测试**4d081a9e-b689-47aa-a75a-6786f90a111e': <__main__.Chunk at 0x7f235145c460>,
#  '测试**07dc5c3f-7fd0-4cce-8204-1fe7da847e68': <__main__.Chunk at 0x7f235145c0d0>,
#  '测试**f0bcc14a-7bdd-4899-8691-fca284772a9b': <__main__.Chunk at 0x7f235145c160>}
 
doc_item.id2chunks['测试**a7f1dba1-89fc-4861-b9b4-2035c1d136f7'].children
# [<__main__.Chunk at 0x7f235145d6f0>, <__main__.Chunk at 0x7f235145cee0>]

doc_item.id2chunks['测试**a7f1dba1-89fc-4861-b9b4-2035c1d136f7'].children[0].content
# '标题1-1'

(4)按照所有父级标题拼接当前标题
这个功能是为了给当前文本内容(正文部分,level=10),生成一个综合了之前所有标题信息的当前标题,用于向量化检索。

doc_item.chunks[4].title_path
# '测试_标题1_标题1-2'

(5)获取路径
类似于4中拼接标题,也可以获取从根节点到当前chunk的路径,返回结果是路径中所有id构成的列表:

doc_item.chunks[4].path_to_this_chunk
# ['测试',
#  '测试**a7f1dba1-89fc-4861-b9b4-2035c1d136f7',
#  '测试**e3eb2029-6493-40df-803a-5c80f2ce7040']

(6)更多自定义扩展应用
使用者可以根据自己的实际需求进一步开发该工具的使用,例如获取某个层级的所有分块:

level_2 = [chunk for chunk in doc_item.chunks if chunk.level == 2]
level_2
# [<__main__.Chunk at 0x7f235145d6f0>, <__main__.Chunk at 0x7f235145cee0>]

以上就是本文的全部内容,如果对你有所帮助,记得点一个免费的赞。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/593681.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

伺服电机初识

目录 一、伺服电机的介绍二、伺服电机的基本原理三、伺服电机的技术特点四、伺服电机的分类五、实际产品介绍1、基本技术规格&#xff1a;2、MD42电机硬件接口3、通讯协议介绍3.1 通讯控制速度运行3.2 通讯控制位置运行3.3 通讯控制转矩运行 4、状态灯与报警信息 一、伺服电机的…

MyScaleDB:SQL+向量驱动大模型和大数据新范式

大模型和 AI 数据库双剑合璧&#xff0c;成为大模型降本增效&#xff0c;大数据真正智能的制胜法宝。 大模型&#xff08;LLM&#xff09;的浪潮已经涌动一年多了&#xff0c;尤其是以 GPT-4、Gemini-1.5、Claude-3 等为代表的模型你方唱罢我登场&#xff0c;成为当之无愧的风口…

富文本编辑器CKEditor4简单使用-07(处理浏览器不支持通过工具栏粘贴问题 和 首行缩进的问题)

富文本编辑器CKEditor4简单使用-07&#xff08;处理浏览器不支持通过工具栏粘贴问题 和 首行缩进的问题&#xff09; 1. 前言——CKEditor4快速入门2. 默认情况下的粘贴2.1 先看控制粘贴的3个按钮2.1.1 工具栏粘贴按钮2.1.2 存在的问题 2.2 不解决按钮问题的情况下2.2.1 使用ct…

Linux——基础IO2

引入 之前在Linux——基础IO(1)中我们讲的都是(进程打开的文件)被打开的文件 那些未被打开的文件呢&#xff1f; 大部分的文件都是没有被打开的文件&#xff0c;这些文件在哪保存&#xff1f;磁盘(SSD) OS要不要管理磁盘上的文件&#xff1f;(如何让OS快速定位一个文件) 要…

设计模式之拦截过滤器模式

想象一下&#xff0c;在你的Java应用里&#xff0c;每个请求就像一场冒险旅程&#xff0c;途中需要经过层层安检和特殊处理。这时候&#xff0c;拦截过滤器模式就化身为你最可靠的特工团队&#xff0c;悄无声息地为每一个请求保驾护航&#xff0c;确保它们安全、高效地到达目的…

Endnote X9 20 21如何把中文引用的et al 换(变)成 等

描述 随着毕业的临近&#xff0c;我在写论文时可能会遇到在引用的中文参考文献中出现“et al”字样。有的学校事比较多&#xff0c;非让改成等等&#xff0c;这就麻烦了。 本身人家endnote都是老美的软件&#xff0c;人家本身就是针对英文文献&#xff0c;你现在让改成等等&a…

JavaScript的操作符运算符

前言&#xff1a; JavaScript的运算符与C/C一致 算数运算符&#xff1a; 算数运算符说明加-减*乘%除/取余 递增递减运算符&#xff1a; 运算符说明递增1-- 递减1 补充&#xff1a; 令a1&#xff0c;b1 运算a b ab12ab22ab--10a--b00 比较(关系)运算符&#xff1a; 运算…

【ChatGPT with Date】使用 ChatGPT 时显示消息时间的插件

文章目录 1. 介绍2. 使用方法2.1 安装 Tampermonkey2.2 安装脚本2.3 使用 3. 配置3.1 时间格式3.2 时间位置3.3 高级配置(1) 生命周期钩子函数(2) 示例 4. 反馈5. 未来计划6. 开源协议7. 供给开发者自定义修改脚本的文档7.1 项目组织架构7.2 定义新的 Component(1) 定义一个新的…

提示找不到msvcr110.dll怎么办,分享多种靠谱的解决方法

当用户在操作计算机时遇到系统提示“找不到msvcr110.dll&#xff0c;无法继续执行代码”这一错误信息&#xff0c;这个问题会导致软件无法启动运行。本文将介绍计算机找不到msvcr110.dll的5种详细的解决方法&#xff0c;帮助读者解决这个问题。 一&#xff0c;关于msvcr110.dll…

《十六》QT TCP协议工作原理和实战

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍如何运用QTcpSocket组件实现基于TCP的网络通信…

论文| Where Is Your Place, Visual Place Recognition?

论文| Where Is Your Place, Visual Place Recognition&#xff1f;

1.pytorch加载收数据(B站小土堆)

数据的加载主要有两个函数&#xff1a; 1.dataset整体收集数据&#xff1a;提供一种方法去获取数据及其label&#xff0c;告诉我们一共有多少数据&#xff08;就是自开始把要的数据和标签都收进来&#xff09; 2.dataloader&#xff0c;后面传入模型时候&#xff0c;每次录入数…

某站戴师兄——Excel学习笔记

1、拿到源数据第一件事——备份工作表&#xff0c;隐藏 Ctrlshift键L打开筛选 UV (Unique visitor)去重 是指通过互联网访问、浏览这个网页的自然人。访问网站的一台电脑客户端为一个访客。00:00-24:00内相同的客户端只被计算一次。一天内同个访客多次访问仅计算一个UV。 PV …

【C++】详解STL的容器之一:list

目录 简介 初识list 模型 list容器的优缺点 list的迭代器 常用接口介绍 获取迭代器 begin end empty size front back insert push_front pop_front push_back pop_back clear 源代码思路 节点设计 迭代器的设计 list的设计 begin() end() 空构造 ins…

【编程题-错题集】chika 和蜜柑(排序 / topK)

牛客对于题目链接&#xff1a;chika和蜜柑 (nowcoder.com) 一、分析题目 排序 &#xff1a;将每个橘⼦按照甜度由高到低排序&#xff0c;相同甜度的橘子按照酸度由低到高排序&#xff0c; 然后提取排序后的前 k 个橘子就好了。 二、代码 1、看题解之前AC的代码 #include <…

企业计算机服务器中了halo勒索病毒怎么处理,halo勒索病毒解密流程

随着网络技术的不断发展&#xff0c;网络在企业生产运营过程中发挥着重大作用&#xff0c;很多企业利用网络开展各项工作业务&#xff0c;网络也大大提高了企业的生产效率&#xff0c;但随之而来的网络数据安全问题成为众多企业关心的主要话题。近日&#xff0c;云天数据恢复中…

机械臂标准DH建模及正运动学分析(以IRB4600型工业机械臂为例)

1. 前言 对于工业机械臂而言&#xff0c;运动学是不考虑力学特性的情况下对机械臂的几何参数与其位置、速度、加速度等运动特性的关系研究。DH建模是运动学的基础&#xff0c;全称为Denavit-Hartenberg建模方法&#xff0c;是一种广泛应用于机器人运动学中的建模技术。该方法通…

Python爬虫:XPath解析爬取豆瓣电影Top250示例

一、示例的函数说明&#xff1a; 函数processing()&#xff1a;用于处理字符串中的空白字符&#xff0c;并拼接字符串。 主函数程序入口&#xff1a;每页显示25部影片&#xff0c;实现循环&#xff0c;共10页。通过format方法替换切换的页码的url地址。然后调用实现爬虫程序的…

Unity Animation--动画剪辑

Unity Animation--动画剪辑 动画剪辑 动画剪辑是Unity动画系统的核心元素之一。Unity支持从外部来源导入动画&#xff0c;并提供创建动画剪辑的能力使用“动画”窗口在编辑器中从头开始。 外部来源的动画 从外部来源导入的动画剪辑可能包括&#xff1a; 人形动画 运动捕捉…

[力扣]——387.字符串中的第一个唯一字符

. - 力扣&#xff08;LeetCode&#xff09; class Solution {public int firstUniqChar(String s) {int[] count new int[256];// 统计每个字符出现的次数for(int i 0; i < s.length(); i){count[s.charAt(i)];}// 找第一个只出现一次的字符for(int i 0; i < s.lengt…
最新文章