• 【RapidAI】P1 中文文本切割程序


    基本信息

    文件名: chinese_text_splitter.py
    文件地址: E:\Code\Knowledge-QA-LLM\Knowledge-QA-LLM-main\knowledge_qa_llm\text_splitter\chinese_text_splitter.py
    Author: @SWHL@omahs
    CSDN Author: me(脚踏实地的大梦想家)
    Original Code: url


    代码解析

    相关包

    import re
    from pathlib import Path
    from typing import List
    
    • 1
    • 2
    • 3

    re 正则表达式。在文本处理中,正则表达式可用于验证、搜索、提取和替换文本中的特定模式。

    re.sub(pattern, replacement, string, count=0, flags=0)
    # pattern:要被替换的字符串。
    # replacement:要替换的字符串。
    # string:被操作的字符串的名称。
    # count(可选):指定最大替换次数,默认为0,表示替换所有匹配项。
    # flags(可选):可以对字符串加以限制,比如忽略大小写等。
    
    import re
    
    text = "Hello, my name is Alice. Nice to meet you, Alice!"
    new_text = re.sub(r'Alice', 'Bob', text)
    
    >>> Hello, my name is Bob. Nice to meet you, Bob!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Path 负责路径,通过使用 Path 类,可以创建、连接、分解和操作文件系统路径,而无需直接使用字符串拼接或分割。

    # 当前脚本文件的路径
    Path(__file__)
    # resolve()方法会返回规范化的绝对路径,解析出符号链接和相对路径,使其变为绝对路径
    Path(__file__).resolve()
    # root_dir为当前脚本的绝对路径下的上级目录的上级目录
    root_dir = Path(__file__).resolve().parent.parent
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    List 用于表示一个列表类型。

    # 表明函数的参数或返回值应该是一个字符串类型的列表
    List[str]
    
    • 1
    • 2

    获取 yaml 关键文件

    root_dir = Path(__file__).resolve().parent.parent
    config_path = root_dir / "config.yaml"
    config = read_yaml(config_path)
    
    • 1
    • 2
    • 3

    上述代码获取了重要描述文件 config.yaml 文件的路径地址信息,然后通过 read_yaml() 函数读取到其中信息。关于 read_yaml() 函数的代码提取如下:

    # 首先 read_yaml() 函数在 ..util.util.py 文件中
    from ..utils.utils import read_yaml
    
    # 提取出 read_yaml() 函数如下:
    def read_yaml(yaml_path: Union[str, Path]):
        with open(str(yaml_path), "rb") as f:
            data = yaml.load(f, Loader=yaml.Loader)
        return data
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上述代码讲打开 str 字符串类型的地址信息,或者 Path 对象的地址信息;通过代码读取到该地址文件,以二进制的形式返回读取到的结果。


    类的构造函数

    def __init__(
        self,
        pdf: bool = False,
        sentence_size: int = config.get("SENTENCE_SIZE"),
    ):
        self.pdf = pdf
        self.sentence_size = sentence_size
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上述 __init__ 为类 ChineseTextSplitter 的构造函数;
    pdf: bool = False 这是一个布尔型参数,默认为False。它用于表示文本是否来自PDF文档。如果设置为True,则表示文本来自PDF,否则为其他来源。
    sentence_size: int = config.get("SENTENCE_SIZE"): 从 yaml 文件中获取 SENTENCE_SIZE 的值,作为 sentence_size 的默认值。


    切分语句部分

    特殊处理 PDF

    切分语句 split_text 是定义在 ChineseTextSplitter 类中的一个成员方法,用于将输入的文本分割成句子的列表。

    切分语句部分过长,将首先切分开介绍,附录附完整的切分函数代码;

    def split_text(self, text: str) -> List[str]:  ##此处需要进一步优化逻辑
        if self.pdf:
            text = re.sub(r"\n{3,}", r"\n", text)
            text = re.sub("\s", " ", text)
            text = re.sub("\n\n", "", text)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    text 为待分割的文本,字符串格式;
    -> List[str] 是方法的返回类型标注,表示该方法返回一个字符串列表;
    re.sub(r"\n{3,}", "\n", text) 将连续三个以上的换行符替换称为单个换行符;
    re.sub("\s", " ", text) 将单个/连续的空白字符串替换为单个空格;
    text = text.replace("\n\n", "") 将连续两个换行符移除;


    重点切分

    text = re.sub(r"([;;.!?。!?\?])([^”’])", r"\1\n\2", text)
    text = re.sub(r'(\.{6})([^"’”」』])', r"\1\n\2", text)	# \.{6} 代表着连续6个英文点,作为英文中省略号
    text = re.sub(r'(\…{2})([^"’”」』])', r"\1\n\2", text)  # 中文省略号……
    text = re.sub(r'([;;!?。!?\?]["’”」』]{0,2})([^;;!?,。!?\?])', r"\1\n\2", text)	# 其目标与第一个相反,想要筛选出以及引号为结尾的字段。
    text = text.rstrip()  # 段尾如果有多余的\n就去掉它
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上述代码是本文,本函数的重点部分,总结来说,就是运用 re.sub() 函数将句子按照标点的方式分割;详细阐述如下:

    re.sub(r"([;;.!?。!?\?])([^”’])", r"\1\n\2", text) 重点部分拆分:

    1. 第一个捕获组:([;;.!?。!?\?])
      该部分是一个字符类,包含中英文分号,中英文句号,中英文感叹号以及中英文问号。该捕获组的作用为用来匹配句子分隔符。

    2. 第二个捕获组:([^”’])
      该部分是一个否定字符类,表示匹配除了有双引号与有单引号之外的任何字符。
      将第一个捕获组与第二个捕获组的结合,其意义在于筛选出:以第一个捕获组中字符类为结尾,且没有引号在其后的句子。(一定要注意其后,其前是另一种写法)
      e . g . e.g. e.g. 案例见替换模式后下述;

    3. 替换模式:\1\n\2
      如果捕获成功,即满足非引号作为结尾的句子,以字符类结尾,则使用替换模式。将句子通过换行分隔开。切分前的句子为 \1,加入换行符 \n,以及切分后的句子 \2

    e . g . e.g. e.g.

    import re
    
    text = "你好吗?我很好!你想吃什么?“苹果。”她说。"
    text = re.sub(r"([;;.!?。!?\?])([^”’])", r"\1\n\2", text)
    print(text)
    
    # 结果如下:
    >>>你好吗?
    我很好!
    你想吃什么?
    “苹果。”她说。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    去除数组中空字符串

    ls = [i for i in text.split("\n") if i]
    # 将不为空的字符串保留在列表 ls 中。
    
    • 1
    • 2

    若当前空字符串满足单句最大长度要求,则视为完成中文句子切分,返回 ls 数组。


    再度切分后长度

    在去除空元素后,通过调取 yaml 关键信息文件中的 SENTENCE_SIZE 属性信息,获取规定最长的单句文本长度。再根据长度进行判断,若超出规定范围,则需二次切分。

    首先切分

    切分除 。” 结尾的语句(逗号句号搭配引号)

    for ele in ls:
        if len(ele) > self.sentence_size:
            ele1 = re.sub(r'([,,.]["’”」』]{0,2})([^,,.])', r"\1\n\2", ele)
            ele1_ls = ele1.split("\n")
    
    • 1
    • 2
    • 3
    • 4

    其次切分

    切分一个或多个连续的换行符 或 两个或多个连续的空格(后面可能跟随0到2个特定字符)后面紧跟一个非空白字符。然后在这两部分之间插入一个换行符。

    for ele_ele1 in ele1_ls:
        if len(ele_ele1) > self.sentence_size:
            ele_ele2 = re.sub(r'([\n]{1,}| {2,}["’”」』]{0,2})([^\s])', r"\1\n\2", ele_ele1)		# 切分换行以及空格
            ele2_ls = ele_ele2.split("\n")
    
    • 1
    • 2
    • 3
    • 4

    继续切分

    切分查找一个0到2个特定字符后面跟着的空格,然后紧随一个非空格字符。在这两部分之间插入一个换行符。

    然后将会找到 ele2_ls 列表中的元素 ele_ele2,然后用 ele_ele3 字符串中的多个行替换它。

    for ele_ele2 in ele2_ls:
        if len(ele_ele2) > self.sentence_size:
            ele_ele3 = re.sub('( ["’”」』]{0,2})([^ ])', r"\1\n\2", ele_ele2)
            ele2_id = ele2_ls.index(ele_ele2)
    		ele2_ls = (ele2_ls[:ele2_id] + [i for i in ele_ele3.split("\n") if i] + ele2_ls[ele2_id + 1 :])
    
    • 1
    • 2
    • 3
    • 4
    • 5

    替换超长字符

    ele_id = ele1_ls.index(ele_ele1)
    ele1_ls = (ele1_ls[:ele_id] + [i for i in ele2_ls if i] + ele1_ls[ele_id + 1 :])
    
    id = ls.index(ele)
    ls = ls[:id] + [i.strip() for i in ele1_ls if i] + ls[id + 1 :]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    至此为止,数组 ls 中所有字符全部都符号长度标准。


    附录

    附录一:完整代码

    import re
    from pathlib import Path
    from typing import List
    from ..utils.utils import read_yaml
    
    root_dir = Path(__file__).resolve().parent.parent
    config_path = root_dir / "config.yaml"
    config = read_yaml(config_path)
    
    class ChineseTextSplitter:
        def __init__(
            self,
            pdf: bool = False,
            sentence_size: int = config.get("SENTENCE_SIZE"),
        ):
            self.pdf = pdf
            self.sentence_size = sentence_size
    
        def split_text(self, text: str) -> List[str]:  ## 此处需要进一步优化逻辑
            if self.pdf:
                text = re.sub(r"\n{3,}", r"\n", text)
                text = re.sub("\s", " ", text)
                text = re.sub("\n\n", "", text)
    
            text = re.sub(r"([;;.!?。!?\?])([^”’])", r"\1\n\2", text)
            text = re.sub(r'(\.{6})([^"’”」』])', r"\1\n\2", text)
            text = re.sub(r'(\…{2})([^"’”」』])', r"\1\n\2", text)
            text = re.sub(r'([;;!?。!?\?]["’”」』]{0,2})([^;;!?,。!?\?])', r"\1\n\2", text)
            text = text.rstrip()
    
            ls = [i for i in text.split("\n") if i]
            for ele in ls:
                if len(ele) > self.sentence_size:
                    ele1 = re.sub(r'([,,.]["’”」』]{0,2})([^,,.])', r"\1\n\2", ele)
                    ele1_ls = ele1.split("\n")
                    for ele_ele1 in ele1_ls:
                        if len(ele_ele1) > self.sentence_size:
                            ele_ele2 = re.sub(
                                r'([\n]{1,}| {2,}["’”」』]{0,2})([^\s])', r"\1\n\2", ele_ele1
                            )
                            ele2_ls = ele_ele2.split("\n")
                            for ele_ele2 in ele2_ls:
                                if len(ele_ele2) > self.sentence_size:
                                    ele_ele3 = re.sub(
                                        '( ["’”」』]{0,2})([^ ])', r"\1\n\2", ele_ele2
                                    )
                                    ele2_id = ele2_ls.index(ele_ele2)
                                    ele2_ls = (
                                        ele2_ls[:ele2_id]
                                        + [i for i in ele_ele3.split("\n") if i]
                                        + ele2_ls[ele2_id + 1 :]
                                    )
                            ele_id = ele1_ls.index(ele_ele1)
                            ele1_ls = (
                                ele1_ls[:ele_id]
                                + [i for i in ele2_ls if i]
                                + ele1_ls[ele_id + 1 :]
                            )
    
                    id = ls.index(ele)
                    ls = ls[:id] + [i.strip() for i in ele1_ls if i] + ls[id + 1 :]
            return ls
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    附录二:可继续思考问题

    可继续思考的问题:

    1. 是否可以优化上述代码中对于长度的限制;
    2. 为什么要对长度进行限制?长度限制可以调整吗?
    3. 怎样对重复性的ele2,ele1与ele3的限制??

    这些问题我们将在本系列博文最后的部分拓展讨论。

    2023年9月5日
    徐鸿铎 于 西直门

  • 相关阅读:
    ES6详解 快速上手!
    opencv-python图像处理:阈值,滤波,腐蚀,膨胀,梯度
    基于内存的分布式NoSQL数据库Redis(三)常用命令
    【Linux进程间通信】mmap共享存储映射
    (一)Multisim安装与入门
    简论UWB三种定位算法的区别
    定额人工费调整差额的几个解决方案
    前端架构学习,一些知识点记录(二)
    基于Python实现并测试Modularity算法
    Gradle修改镜像库 ,初始启动配置 init.gradle
  • 原文地址:https://blog.csdn.net/weixin_43098506/article/details/132688834