数据处理

本文对应源代码在此处,如需复现可下载运行源代码。

为构建我们的本地知识库,我们需要对以多种类型存储的本地文档进行处理,读取本地文档并通过前文描述的 Embedding 方法将本地文档的内容转化为词向量来构建向量数据库。在本节中,我们以一些实际示例入手,来讲解如何对本地文档进行处理。

一、源文档选取

我们选用 Datawhale 一些经典开源课程作为示例,具体包括:

  1. from langchain.document_loaders.pdf import PyMuPDFLoader
  2. # 创建一个 PyMuPDFLoader Class 实例,输入为待加载的 pdf 文档路径
  3. loader = PyMuPDFLoader("../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf")
  4. # 调用 PyMuPDFLoader Class 的函数 load 对 pdf 文件进行加载
  5. pdf_pages = loader.load()

文档加载后储存在 pages 变量中:

  • page 的变量类型为 List
  • 打印 pages 的长度可以看到 pdf 一共包含多少页
  1. print(f"载入后的变量类型为:{type(pdf_pages)},", f"该 PDF 一共包含 {len(pdf_pages)} 页")
  1. 载入后的变量类型为:<class 'list'>, PDF 一共包含 196

page 中的每一元素为一个文档,变量类型为 langchain_core.documents.base.Document, 文档变量类型包含两个属性

  • page_content 包含该文档的内容。
  • meta_data 为文档相关的描述性数据。
  1. pdf_page = pdf_pages[1]
  2. print(f"每一个元素的类型:{type(pdf_page)}.",
  3. f"该文档的描述性数据:{pdf_page.metadata}",
  4. f"查看该文档的内容:\n{pdf_page.page_content}",
  5. sep="\n------\n")
  1. 每一个元素的类型:<class 'langchain_core.documents.base.Document'>.
  2. ------
  3. 该文档的描述性数据:{'source': './data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'file_path': './data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'page': 1, 'total_pages': 196, 'format': 'PDF 1.5', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'creator': 'LaTeX with hyperref', 'producer': 'xdvipdfmx (20200315)', 'creationDate': "D:20230303170709-00'00'", 'modDate': '', 'trapped': ''}
  4. ------
  5. 查看该文档的内容:
  6. 前言
  7. “周志华老师的《机器学习》
  8. (西瓜书)是机器学习领域的经典入门教材之一,周老师为了使尽可能多的读
  9. 者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述,但是这对那些想深究公式推
  10. 导细节的读者来说可能“不太友好”
  11. ,本书旨在对西瓜书里比较难理解的公式加以解析,以及对部分公式补充
  12. 具体的推导细节。
  13. 读到这里,大家可能会疑问为啥前面这段话加了引号,因为这只是我们最初的遐想,后来我们了解到,周
  14. 老师之所以省去这些推导细节的真实原因是,他本尊认为“理工科数学基础扎实点的大二下学生应该对西瓜书
  15. 中的推导细节无困难吧,要点在书里都有了,略去的细节应能脑补或做练习”
  16. 。所以...... 本南瓜书只能算是我
  17. 等数学渣渣在自学的时候记下来的笔记,希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二
  18. 下学生”
  19. 使用说明
  20. 南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的,所以南瓜书的最佳使用方法是以西瓜书
  21. 为主线,遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书;
  22. 对于初学机器学习的小白,西瓜书第1 章和第2 章的公式强烈不建议深究,简单过一下即可,等你学得
  23. 有点飘的时候再回来啃都来得及;
  24. 每个公式的解析和推导我们都力(zhi) 争(neng) 以本科数学基础的视角进行讲解,所以超纲的数学知识
  25. 我们通常都会以附录和参考文献的形式给出,感兴趣的同学可以继续沿着我们给的资料进行深入学习;
  26. 若南瓜书里没有你想要查阅的公式,
  27. 或者你发现南瓜书哪个地方有错误,
  28. 请毫不犹豫地去我们GitHub
  29. Issues(地址:https://github.com/datawhalechina/pumpkin-book/issues)进行反馈,在对应版块
  30. 提交你希望补充的公式编号或者勘误信息,我们通常会在24 小时以内给您回复,超过24 小时未回复的
  31. 话可以微信联系我们(微信号:at-Sm1les
  32. 配套视频教程:https://www.bilibili.com/video/BV1Mh411e7VU
  33. 在线阅读地址:https://datawhalechina.github.io/pumpkin-book(仅供第1 版)
  34. 最新版PDF 获取地址:https://github.com/datawhalechina/pumpkin-book/releases
  35. 编委会
  36. 主编:Sm1lesarchwalkerjbb0523
  37. 编委:juxiaoMajingminMrBigFanshanryYe980226
  38. 封面设计:构思-Sm1les、创作-林王茂盛
  39. 致谢
  40. 特别感谢awyd234
  41. feijuan
  42. Ggmatch
  43. Heitao5200
  44. huaqing89
  45. LongJH
  46. LilRachel
  47. LeoLRH
  48. Nono17
  49. spareribssunchaothuStevenLzq 在最早期的时候对南瓜书所做的贡献。
  50. 扫描下方二维码,然后回复关键词“南瓜书”
  51. ,即可加入“南瓜书读者交流群”
  52. 版权声明
  53. 本作品采用知识共享署名-非商业性使用-相同方式共享4.0 国际许可协议进行许可。

2. MD 文档

我们可以以几乎完全一致的方式读入 markdown 文档:

  1. from langchain.document_loaders.markdown import UnstructuredMarkdownLoader
  2. loader = UnstructuredMarkdownLoader("../../data_base/knowledge_db/prompt_engineering/1. 简介 Introduction.md")
  3. md_pages = loader.load()

读取的对象和 PDF 文档读取出来是完全一致的:

  1. print(f"载入后的变量类型为:{type(md_pages)},", f"该 Markdown 一共包含 {len(md_pages)} 页")
  1. 载入后的变量类型为:<class 'list'>, Markdown 一共包含 1
  1. md_page = md_pages[0]
  2. print(f"每一个元素的类型:{type(md_page)}.",
  3. f"该文档的描述性数据:{md_page.metadata}",
  4. f"查看该文档的内容:\n{md_page.page_content[0:][:200]}",
  5. sep="\n------\n")
  1. 每一个元素的类型:<class 'langchain_core.documents.base.Document'>.
  2. ------
  3. 该文档的描述性数据:{'source': './data_base/knowledge_db/prompt_engineering/1. 简介 Introduction.md'}
  4. ------
  5. 查看该文档的内容:
  6. 第一章 简介
  7. 欢迎来到面向开发者的提示工程部分,本部分内容基于吴恩达老师的《Prompt Engineering for Developer》课程进行编写。《Prompt Engineering for Developer》课程是由吴恩达老师与 OpenAI 技术团队成员 Isa Fulford 老师合作授课,Isa 老师曾开发过受欢迎的 ChatGPT 检索插件,并且在教授 LLM Larg

三、数据清洗

我们期望知识库的数据尽量是有序的、优质的、精简的,因此我们要删除低质量的、甚至影响理解的文本数据。
可以看到上文中读取的pdf文件不仅将一句话按照原文的分行添加了换行符\n,也在原本两个符号中间插入了\n,我们可以使用正则表达式匹配并删除掉\n

  1. import re
  2. pattern = re.compile(r'[^\u4e00-\u9fff](\n)[^\u4e00-\u9fff]', re.DOTALL)
  3. pdf_page.page_content = re.sub(pattern, lambda match: match.group(0).replace('\n', ''), pdf_page.page_content)
  4. print(pdf_page.page_content)
  1. 前言
  2. “周志华老师的《机器学习》(西瓜书)是机器学习领域的经典入门教材之一,周老师为了使尽可能多的读
  3. 者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述,但是这对那些想深究公式推
  4. 导细节的读者来说可能“不太友好”,本书旨在对西瓜书里比较难理解的公式加以解析,以及对部分公式补充
  5. 具体的推导细节。”
  6. 读到这里,大家可能会疑问为啥前面这段话加了引号,因为这只是我们最初的遐想,后来我们了解到,周
  7. 老师之所以省去这些推导细节的真实原因是,他本尊认为“理工科数学基础扎实点的大二下学生应该对西瓜书
  8. 中的推导细节无困难吧,要点在书里都有了,略去的细节应能脑补或做练习”。所以...... 本南瓜书只能算是我
  9. 等数学渣渣在自学的时候记下来的笔记,希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二
  10. 下学生”。
  11. 使用说明
  12. 南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的,所以南瓜书的最佳使用方法是以西瓜书
  13. 为主线,遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书;• 对于初学机器学习的小白,西瓜书第1 章和第2 章的公式强烈不建议深究,简单过一下即可,等你学得
  14. 有点飘的时候再回来啃都来得及;• 每个公式的解析和推导我们都力(zhi) 争(neng) 以本科数学基础的视角进行讲解,所以超纲的数学知识
  15. 我们通常都会以附录和参考文献的形式给出,感兴趣的同学可以继续沿着我们给的资料进行深入学习;• 若南瓜书里没有你想要查阅的公式,
  16. 或者你发现南瓜书哪个地方有错误,
  17. 请毫不犹豫地去我们GitHub
  18. Issues(地址:https://github.com/datawhalechina/pumpkin-book/issues)进行反馈,在对应版块
  19. 提交你希望补充的公式编号或者勘误信息,我们通常会在24 小时以内给您回复,超过24 小时未回复的
  20. 话可以微信联系我们(微信号:at-Sm1les);
  21. 配套视频教程:https://www.bilibili.com/video/BV1Mh411e7VU
  22. 在线阅读地址:https://datawhalechina.github.io/pumpkin-book(仅供第1 版)
  23. 最新版PDF 获取地址:https://github.com/datawhalechina/pumpkin-book/releases
  24. 编委会
  25. 主编:Sm1lesarchwalkerjbb0523
  26. 编委:juxiaoMajingminMrBigFanshanryYe980226
  27. 封面设计:构思-Sm1les、创作-林王茂盛
  28. 致谢
  29. 特别感谢awyd234feijuanGgmatchHeitao5200huaqing89LongJHLilRachelLeoLRHNono17spareribssunchaothuStevenLzq 在最早期的时候对南瓜书所做的贡献。
  30. 扫描下方二维码,然后回复关键词“南瓜书”,即可加入“南瓜书读者交流群”
  31. 版权声明
  32. 本作品采用知识共享署名-非商业性使用-相同方式共享4.0 国际许可协议进行许可。

进一步分析数据,我们发现数据中还有不少的和空格,我们的简单实用replace方法即可。

  1. pdf_page.page_content = pdf_page.page_content.replace('•', '')
  2. pdf_page.page_content = pdf_page.page_content.replace(' ', '')
  3. print(pdf_page.page_content)
  1. 前言
  2. “周志华老师的《机器学习》(西瓜书)是机器学习领域的经典入门教材之一,周老师为了使尽可能多的读
  3. 者通过西瓜书对机器学习有所了解,所以在书中对部分公式的推导细节没有详述,但是这对那些想深究公式推
  4. 导细节的读者来说可能“不太友好”,本书旨在对西瓜书里比较难理解的公式加以解析,以及对部分公式补充
  5. 具体的推导细节。”
  6. 读到这里,大家可能会疑问为啥前面这段话加了引号,因为这只是我们最初的遐想,后来我们了解到,周
  7. 老师之所以省去这些推导细节的真实原因是,他本尊认为“理工科数学基础扎实点的大二下学生应该对西瓜书
  8. 中的推导细节无困难吧,要点在书里都有了,略去的细节应能脑补或做练习”。所以......本南瓜书只能算是我
  9. 等数学渣渣在自学的时候记下来的笔记,希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二
  10. 下学生”。
  11. 使用说明
  12. 南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的,所以南瓜书的最佳使用方法是以西瓜书
  13. 为主线,遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书;对于初学机器学习的小白,西瓜书第1章和第2章的公式强烈不建议深究,简单过一下即可,等你学得
  14. 有点飘的时候再回来啃都来得及;每个公式的解析和推导我们都力(zhi)争(neng)以本科数学基础的视角进行讲解,所以超纲的数学知识
  15. 我们通常都会以附录和参考文献的形式给出,感兴趣的同学可以继续沿着我们给的资料进行深入学习;若南瓜书里没有你想要查阅的公式,
  16. 或者你发现南瓜书哪个地方有错误,
  17. 请毫不犹豫地去我们GitHub
  18. Issues(地址:https://github.com/datawhalechina/pumpkin-book/issues)进行反馈,在对应版块
  19. 提交你希望补充的公式编号或者勘误信息,我们通常会在24小时以内给您回复,超过24小时未回复的
  20. 话可以微信联系我们(微信号:at-Sm1les);
  21. 配套视频教程:https://www.bilibili.com/video/BV1Mh411e7VU
  22. 在线阅读地址:https://datawhalechina.github.io/pumpkin-book(仅供第1版)
  23. 最新版PDF获取地址:https://github.com/datawhalechina/pumpkin-book/releases
  24. 编委会
  25. 主编:Sm1lesarchwalkerjbb0523
  26. 编委:juxiaoMajingminMrBigFanshanryYe980226
  27. 封面设计:构思-Sm1les、创作-林王茂盛
  28. 致谢
  29. 特别感谢awyd234feijuanGgmatchHeitao5200huaqing89LongJHLilRachelLeoLRHNono17spareribssunchaothuStevenLzq在最早期的时候对南瓜书所做的贡献。
  30. 扫描下方二维码,然后回复关键词“南瓜书”,即可加入“南瓜书读者交流群”
  31. 版权声明
  32. 本作品采用知识共享署名-非商业性使用-相同方式共享4.0国际许可协议进行许可。

上文中读取的md文件每一段中间隔了一个换行符,我们同样可以使用replace方法去除。

  1. md_page.page_content = md_page.page_content.replace('\n\n', '\n')
  2. print(md_page.page_content)
  1. 第一章 简介
  2. 欢迎来到面向开发者的提示工程部分,本部分内容基于吴恩达老师的《Prompt Engineering for Developer》课程进行编写。《Prompt Engineering for Developer》课程是由吴恩达老师与 OpenAI 技术团队成员 Isa Fulford 老师合作授课,Isa 老师曾开发过受欢迎的 ChatGPT 检索插件,并且在教授 LLM Large Language Model 大语言模型)技术在产品中的应用方面做出了很大贡献。她还参与编写了教授人们使用 Prompt OpenAI cookbook。我们希望通过本模块的学习,与大家分享使用提示词开发 LLM 应用的最佳实践和技巧。
  3. 网络上有许多关于提示词(Prompt 本教程中将保留该术语)设计的材料,例如《30 prompts everyone has to know》之类的文章,这些文章主要集中在 ChatGPT Web 界面上,许多人在使用它执行特定的、通常是一次性的任务。但我们认为,对于开发人员,大语言模型(LLM 的更强大功能是能通过 API 接口调用,从而快速构建软件应用程序。实际上,我们了解到 DeepLearning.AI 的姊妹公司 AI Fund 的团队一直在与许多初创公司合作,将这些技术应用于诸多应用程序上。很兴奋能看到 LLM API 能够让开发人员非常快速地构建应用程序。
  4. 在本模块,我们将与读者分享提升大语言模型应用效果的各种技巧和最佳实践。书中内容涵盖广泛,包括软件开发提示词设计、文本总结、推理、转换、扩展以及构建聊天机器人等语言模型典型应用场景。我们衷心希望该课程能激发读者的想象力,开发出更出色的语言模型应用。
  5. 随着 LLM 的发展,其大致可以分为两种类型,后续称为基础 LLM 和指令微调(Instruction TunedLLM。基础LLM是基于文本训练数据,训练出预测下一个单词能力的模型。其通常通过在互联网和其他来源的大量数据上训练,来确定紧接着出现的最可能的词。例如,如果你以“从前,有一只独角兽”作为 Prompt ,基础 LLM 可能会继续预测“她与独角兽朋友共同生活在一片神奇森林中”。但是,如果你以“法国的首都是什么”为 Prompt ,则基础 LLM 可能会根据互联网上的文章,将回答预测为“法国最大的城市是什么?法国的人口是多少?”,因为互联网上的文章很可能是有关法国国家的问答题目列表。
  6. 与基础语言模型不同,指令微调 LLM 通过专门的训练,可以更好地理解并遵循指令。举个例子,当询问“法国的首都是什么?”时,这类模型很可能直接回答“法国的首都是巴黎”。指令微调 LLM 的训练通常基于预训练语言模型,先在大规模文本数据上进行预训练,掌握语言的基本规律。在此基础上进行进一步的训练与微调(finetune),输入是指令,输出是对这些指令的正确回复。有时还会采用RLHFreinforcement learning from human feedback,人类反馈强化学习)技术,根据人类对模型输出的反馈进一步增强模型遵循指令的能力。通过这种受控的训练过程。指令微调 LLM 可以生成对指令高度敏感、更安全可靠的输出,较少无关和损害性内容。因此。许多实际应用已经转向使用这类大语言模型。
  7. 因此,本课程将重点介绍针对指令微调 LLM 的最佳实践,我们也建议您将其用于大多数使用场景。当您使用指令微调 LLM 时,您可以类比为向另一个人提供指令(假设他很聪明但不知道您任务的具体细节)。因此,当 LLM 无法正常工作时,有时是因为指令不够清晰。例如,如果您想问“请为我写一些关于阿兰·图灵( Alan Turing )的东西”,在此基础上清楚表明您希望文本专注于他的科学工作、个人生活、历史角色或其他方面可能会更有帮助。另外您还可以指定回答的语调, 来更加满足您的需求,可选项包括专业记者写作,或者向朋友写的随笔等。
  8. 如果你将 LLM 视为一名新毕业的大学生,要求他完成这个任务,你甚至可以提前指定他们应该阅读哪些文本片段来写关于阿兰·图灵的文本,这样能够帮助这位新毕业的大学生更好地完成这项任务。本书的下一章将详细阐释提示词设计的两个关键原则:清晰明确和给予充足思考时间。

四、文档分割

由于单个文档的长度往往会超过模型支持的上下文,导致检索得到的知识太长超出模型的处理能力,因此,在构建向量知识库的过程中,我们往往需要对文档进行分割,将单个文档按长度或者按固定的规则分割成若干个 chunk,然后将每个 chunk 转化为词向量,存储到向量数据库中。

在检索时,我们会以 chunk 作为检索的元单位,也就是每一次检索到 k 个 chunk 作为模型可以参考来回答用户问题的知识,这个 k 是我们可以自由设定的。

Langchain 中文本分割器都根据 chunk_size (块大小)和 chunk_overlap (块与块之间的重叠大小)进行分割。

image.png

  • chunk_size 指每个块包含的字符或 Token (如单词、句子等)的数量

  • chunk_overlap 指两个块之间共享的字符数量,用于保持上下文的连贯性,避免分割丢失上下文信息

Langchain 提供多种文档分割方式,区别在怎么确定块与块之间的边界、块由哪些字符/token组成、以及如何测量块大小

  • RecursiveCharacterTextSplitter(): 按字符串分割文本,递归地尝试按不同的分隔符进行分割文本。
  • CharacterTextSplitter(): 按字符来分割文本。
  • MarkdownHeaderTextSplitter(): 基于指定的标题来分割markdown 文件。
  • TokenTextSplitter(): 按token来分割文本。
  • SentenceTransformersTokenTextSplitter(): 按token来分割文本
  • Language(): 用于 CPP、Python、Ruby、Markdown 等。
  • NLTKTextSplitter(): 使用 NLTK(自然语言工具包)按句子分割文本。
  • SpacyTextSplitter(): 使用 Spacy按句子的切割文本。
  1. '''
  2. * RecursiveCharacterTextSplitter 递归字符文本分割
  3. RecursiveCharacterTextSplitter 将按不同的字符递归地分割(按照这个优先级["\n\n", "\n", " ", ""]),
  4. 这样就能尽量把所有和语义相关的内容尽可能长时间地保留在同一位置
  5. RecursiveCharacterTextSplitter需要关注的是4个参数:
  6. * separators - 分隔符字符串数组
  7. * chunk_size - 每个文档的字符数量限制
  8. * chunk_overlap - 两份文档重叠区域的长度
  9. * length_function - 长度计算函数
  10. '''
  11. #导入文本分割器
  12. from langchain.text_splitter import RecursiveCharacterTextSplitter
  1. # 知识库中单段文本长度
  2. CHUNK_SIZE = 500
  3. # 知识库中相邻文本重合长度
  4. OVERLAP_SIZE = 50
  1. # 使用递归字符文本分割器
  2. text_splitter = RecursiveCharacterTextSplitter(
  3. chunk_size=CHUNK_SIZE,
  4. chunk_overlap=OVERLAP_SIZE
  5. )
  6. text_splitter.split_text(pdf_page.page_content[0:1000])
  1. ['前言\n“周志华老师的《机器学习》(西瓜书)是机器学习领域的经典入门教材之一,周老师为了使尽可能多的读\n者通过西瓜书对机器学习有所了解,所以在书中对部分公式的推导细节没有详述,但是这对那些想深究公式推\n导细节的读者来说可能“不太友好”,本书旨在对西瓜书里比较难理解的公式加以解析,以及对部分公式补充\n具体的推导细节。”\n读到这里,大家可能会疑问为啥前面这段话加了引号,因为这只是我们最初的遐想,后来我们了解到,周\n老师之所以省去这些推导细节的真实原因是,他本尊认为“理工科数学基础扎实点的大二下学生应该对西瓜书\n中的推导细节无困难吧,要点在书里都有了,略去的细节应能脑补或做练习”。所以......本南瓜书只能算是我\n等数学渣渣在自学的时候记下来的笔记,希望能够帮助大家都成为一名合格的“理工科数学基础扎实点的大二\n下学生”。\n使用说明\n南瓜书的所有内容都是以西瓜书的内容为前置知识进行表述的,所以南瓜书的最佳使用方法是以西瓜书\n为主线,遇到自己推导不出来或者看不懂的公式时再来查阅南瓜书;对于初学机器学习的小白,西瓜书第1章和第2章的公式强烈不建议深究,简单过一下即可,等你学得',
  2. '有点飘的时候再回来啃都来得及;每个公式的解析和推导我们都力(zhi)争(neng)以本科数学基础的视角进行讲解,所以超纲的数学知识\n我们通常都会以附录和参考文献的形式给出,感兴趣的同学可以继续沿着我们给的资料进行深入学习;若南瓜书里没有你想要查阅的公式,\n或者你发现南瓜书哪个地方有错误,\n请毫不犹豫地去我们GitHub的\nIssues(地址:https://github.com/datawhalechina/pumpkin-book/issues)进行反馈,在对应版块\n提交你希望补充的公式编号或者勘误信息,我们通常会在24小时以内给您回复,超过24小时未回复的\n话可以微信联系我们(微信号:at-Sm1les);\n配套视频教程:https://www.bilibili.com/video/BV1Mh411e7VU\n在线阅读地址:https://datawhalechina.github.io/pumpkin-book(仅供第1版)\n最新版PDF获取地址:https://github.com/datawhalechina/pumpkin-book/releases\n编委会',
  3. '编委会\n主编:Sm1les、archwalk']
  1. split_docs = text_splitter.split_documents(pdf_pages)
  2. print(f"切分后的文件数量:{len(split_docs)}")
  1. 切分后的文件数量:720
  1. print(f"切分后的字符数(可以用来大致评估 token 数):{sum([len(doc.page_content) for doc in split_docs])}")
  1. 切分后的字符数(可以用来大致评估 token 数):308931

注:如何对文档进行分割,其实是数据处理中最核心的一步,其往往决定了检索系统的下限。但是,如何选择分割方式,往往具有很强的业务相关性——针对不同的业务、不同的源数据,往往需要设定个性化的文档分割方式。因此,在本章,我们仅简单根据 chunk_size 对文档进行分割。对于有兴趣进一步探索的读者,欢迎阅读我们第三部分的项目示例来参考已有的项目是如何进行文档分割的。

本文对应源代码在此处,如需复现可下载运行源代码。