使用MarkdownIt库拆分Markdown文本

使用MarkdownIt库拆分Markdown文本

在处理大模型的返回结果过程中,有些时候大模型返回的文本过长,甚至超过Telegram消息的长度限制。

这个时候,就需要对消息进行拆分。

但是,不能简单地根据分片长度或者换行符(\n)暴力拆分,因为这样可能会破坏Markdown的格式,特别是当返回中有代码片段(code fence) 时。因为代码中存在换行符,故会被拆成2部分,导致代码片段的闭合符```影响后续的文本格式。

本例是一个拆分长`Markdown并保留原来格式的示例。

其利用了MarkdownIt库,这个库能够将Markdown文本解析为token,每个token都有基本的属性,观察属性,可以总结规律。

基本的拆分原则是:

  1. 不在type为open的token处拆分
  2. 不对ulli进行拆分
  3. 不对code fence进行拆分

以下是不完美代码示例:


  1from markdown_it import MarkdownIt
  2# from markdown_it.renderer import RendererProtocol
  3from typing import List
  4
  5#  利用 markdown-it 库解析 markdown 文本
  6
  7# 示例用法
  8markdown_text = (
  9    """
 10好的,下面我将用一个简单的 Java 示例来解释饿汉式单例模式的缺点。
 11
 12首先,我们来看一个典型的饿汉式单例模式的实现:
 13
 14```java
 15public class EagerSingleton {
 16
 17    private static final EagerSingleton instance = new EagerSingleton();
 18
 19    private EagerSingleton() {
 20        // 私有构造函数,防止外部实例化
 21        System.out.println("EagerSingleton is initialized."); // 用于观察初始化时机
 22    }
 23
 24    public static EagerSingleton getInstance() {
 25        return instance;
 26    }
 27
 28    public void doSomething() {
 29        System.out.println("Doing something...");
 30    }
 31
 32    public static void main(String[] args) {
 33        EagerSingleton.getInstance().doSomething();
 34    }
 35}
 36```
 37
 38在这个例子中,`instance` 静态变量在类加载时就被立即初始化了。 即使你的程序在启动时并不需要使用这个单例,它也会被创建。 这就是饿汉式的主要缺点:
 39
 40**缺点:资源浪费**
 41
 42*   **过早初始化:** 无论是否需要,单例实例都会在类加载时被创建。 如果这个单例对象的创建过程比较耗时,或者占用的资源较多,而程序在某些情况下根本不需要用到它,那么就会造成不必要的资源浪费。
 43*   **无法延迟加载:** 饿汉式无法实现延迟加载(lazy loading)。 延迟加载指的是只有在真正需要使用对象时才创建它。
 44
 45**示例说明:**
 46
 47在上面的代码中,`System.out.println("EagerSingleton is initialized.");` 这行代码会在类加载时立即执行,表明单例对象被创建。 即使你只运行程序,但没有调用 `getInstance()` 方法,单例对象仍然会被创建。
 48
 49**何时不适合使用饿汉式:**
 50
 51*   当单例对象的创建非常耗时或占用大量资源时。
 52    
 53    *   这是2级列表
 54    *   This is 2nd class List
 55       
 56*   当无法确定程序启动时是否一定会用到该单例对象时。
 57
 58**总结:**
 59
 60饿汉式单例模式实现简单,线程安全,但在某些情况下可能会造成资源浪费。 如果你确定程序启动时一定会用到该单例对象,并且创建过程不复杂,那么饿汉式是一个不错的选择。 否则,可以考虑使用懒汉式或其他单例模式的变体,以实现延迟加载。
 61
 62为了更清楚地说明问题,可以考虑以下场景:
 63
 64假设 `EagerSingleton` 类需要连接到一个数据库,而这个数据库连接的建立需要花费较长时间。 如果程序在启动后的一段时间内并不需要访问数据库,那么使用饿汉式就会导致数据库连接过早建立,浪费资源。
 65
 66希望这个解释能够帮助你理解饿汉式单例模式的缺点。
 67
 68    """
 69)
 70
 71markdown_text_2 = (
 72    """
 73为了更准确地满足你的需求,请告诉我你对哪个方向的 Python 应用更感兴趣,例如 Web 开发、数据分析、人工智能等。这样我可以为你提供更具针对性的学习建议和资源。
 74非常乐意为您整理一份Python学习大纲。以下是一个更精简、更侧重实用性的学习路径,适合希望快速上手并应用于实际项目的学习者:
 75
 76**第一阶段:Python快速入门**
 77
 781.  **基础语法**
 79    *   变量、数据类型(字符串、数字、布尔值、列表、字典)
 80    *   运算符、表达式
 81    *   输入输出
 822.  **流程控制**
 83    *   条件语句(if/else)
 84    *   循环语句(for/while)
 853.  **函数**
 86    *   定义函数、调用函数
 87    *   参数传递
 88    *   返回值
 894.  **常用数据结构**
 90    *   列表(List):增删改查、切片
 91    *   字典(Dictionary):键值对操作
 925.  **模块**
 93    *   导入模块(import)
 94    *   常用标准库(如`os`, `datetime`, `random`)
 95
 96**第二阶段:面向对象编程**
 97
 981.  **类与对象**
 99    *   定义类、创建对象
100    *   属性和方法
101    *   `self`关键字
1022.  **继承**
103    *   单继承
104    *   方法重写
1053.  **简单实践**
106    *   编写简单的类来解决实际问题
107
108**第三阶段:常用库与应用**
109
1101.  **数据处理**
111    *   Pandas:数据读取、清洗、分析
1122.  **Web开发**
113    *   Flask:搭建简单Web应用
1143.  **爬虫**
115    *   Requests:发送HTTP请求
116    *   Beautiful Soup:解析HTML
1174.  **数据库**
118    *   SQLite:基本数据库操作
119
120**第四阶段:项目实践**
121
1221.  **选择项目**
123    *   根据兴趣选择小项目(如:简单爬虫、Web应用、数据分析)
1242.  **完成项目**
125    *   从头到尾完成项目,遇到问题查阅资料
1263.  **代码优化**
127    *   学习代码规范,优化代码结构
128
129**学习资源**
130
131*   **在线平台**:
132    *   Codecademy, Coursera, Udemy
133*   **书籍**:
134    *   《Python Crash Course》
135    *   《Automate the Boring Stuff with Python》
136*   **官方文档**:
137    *   Python官方网站
138
139**学习建议**
140
141*   **动手实践**:边学边练,多写代码
142*   **解决问题**:遇到问题积极搜索、提问
143*   **持续学习**:Python生态丰富,不断学习新库和技术
144
145这个大纲更注重实用性和快速上手,可以帮助您在较短时间内掌握Python核心技能,并应用于实际项目中。如果您对某个领域(如Web开发、数据分析)特别感兴趣,可以深入学习相关库和框架。
146
147为了更好地帮助您,请告诉我您对哪个方向的Python应用更感兴趣?例如,Web开发、数据分析、自动化脚本等。这样我可以为您提供更具体的学习建议和资源。
148    """
149)
150
151#  markdown_text 和markdown_text_2解析结果是一样的
152md = MarkdownIt("commonmark", {"html": False, "typographer": True})
153tokens = md.parse(markdown_text_2)
154
155# for i, token in enumerate(tokens):
156#     print(f"token{i}({token.type}):\n {token.content}")
157    
158chunks = []
159chunk = ""
160max_chunk_length = 10000
161last_token_tag = ""
162
163for token in tokens:
164    last_token_tag = token.tag
165    """
166    分片长度限制 20 但是要保留完整的markdown格式。
167    chunk 总是以 open开头 close结束
168    code fence只有一个token 其他内容每一行至少有3个token(open inline close)
169    列表项内容至少外层还有2个token包围(list_item_open list_item_close)
170    列表顶层还有2个token(bullet_list_open bullet_list_close) 
171    保证列表的完整性
172    保证code fence的完整性
173    """
174    if token.type.endswith("_open"):
175        if token.tag == 'ul':
176            chunk += "\n"
177        elif token.tag == 'li':
178            if token.level > 1:
179                chunk += "\t" + token.info + token.markup + " "
180            else:
181                chunk += token.info + token.markup + " "
182        elif last_token_tag == 'li':
183            continue
184        else:
185            # 判断是否结束token?
186            if len(chunk) > max_chunk_length:
187                # 开新的chunk
188                chunks.append(chunk)
189                chunk = ""
190
191    elif token.type == "fence":
192        chunk += token.markup + token.info + "\n" + token.content + token.markup + "\n\n"
193    elif token.type.endswith("_close"):
194        if token.tag == 'ul':
195            chunk += "\n"
196    else:
197        chunk += token.content
198        # nested ul li
199        if token.level > 1:
200            chunk += "\n"
201        else:
202            chunk += "\n\n"
203else:
204    chunks.append(chunk)
205
206print(f'chunks size: {len(chunks)}')
207for i, c in enumerate(chunks):
208    print(f'chunk[{i}]: {c}')