1. 项目概述与核心价值
如果你正在开发一个基于ChatGPT的Telegram机器人,并且希望AI回复的消息能像在Markdown编辑器里一样,拥有加粗、斜体、代码块甚至“剧透”隐藏等丰富的格式,那么你很可能已经遇到了一个头疼的问题:Telegram Bot API只支持有限的HTML标签,而ChatGPT等大模型默认输出的是标准的Markdown语法。直接发送过去,用户看到的只会是一堆带着星号、下划线的“乱码”。这个名为botfather-dev/formatter-chatgpt-telegram的项目,就是为了精准解决这个“最后一公里”的格式转换难题而生的。
简单来说,它是一个轻量级的Python解析器,专门负责将ChatGPT等模型生成的、符合Telegram风格的Markdown文本,转换成Telegram Bot API能够识别和渲染的HTML。它不仅仅是一个简单的字符串替换工具,其设计充分考虑了实际应用场景中的复杂性,比如嵌套样式、代码块的完整性、特殊字符的转义,以及对Telegram独有功能(如可折叠引用块、剧透文本)的支持。对于任何希望提升机器人消息可读性和交互体验的开发者而言,这个工具都能省去大量手动处理格式的繁琐工作,让开发者可以更专注于业务逻辑本身。
2. 核心功能与设计思路拆解
2.1 为什么需要专门的格式转换器?
在深入代码之前,我们先理解一下问题的根源。Telegram Bot API的sendMessage方法支持一个名为parse_mode的参数,可以设置为HTML或MarkdownV2。然而,这两种模式都有其局限性:
- HTML模式:支持一组有限的标签(如
<b>,<i>,<u>,<s>,<code>,<pre>),但要求标签必须正确嵌套和闭合。ChatGPT输出的Markdown通常不严格遵循HTML规范,直接当作HTML发送会导致格式错乱或解析失败。 - MarkdownV2模式:Telegram有一套自己定义的Markdown语法,与CommonMark标准存在差异。例如,下划线是
__text__而非_text_。让ChatGPT完全适配这套私有语法既困难又不通用。
因此,最稳健的方案是:让ChatGPT使用一个“超集”语法(即兼容标准Markdown并扩展了Telegram特性),然后在服务端将这个“超集Markdown”精准、安全地转换为Telegram HTML。这正是本项目telegram_format函数的核心使命。
2.2 功能特性深度解析
项目README中列举的功能点,每一个都对应着实际开发中的痛点。我们来逐一拆解其重要性:
文本样式转换:这是基础功能,将
**text**、*text*、__text__、~~text~~分别转换为<b>、<i>、<u>、<s>标签。关键在于处理嵌套样式,例如**bold and *italic***需要正确转换为<b>bold and <i>italic</i></b>,这要求解析器有状态或使用递归/栈来处理标签的打开和关闭顺序,而不是简单的正则替换。Telegram特色功能支持:
- 剧透文本(Spoiler):语法为
||text||,转换为<span class="tg-spoiler">text</span>。这是Telegram的独有功能,标准Markdown没有。在知识问答、谜题揭秘等场景中非常有用。 - 可折叠引用块(Expandable Blockquotes):语法为
**> text,转换为<blockquote expandable>text</blockquote>。这对于组织长文回复、隐藏可选细节(如代码示例、冗长解释)至关重要,能保持消息界面的整洁。
- 剧透文本(Spoiler):语法为
代码块处理:这是复杂度最高的部分之一。原因在于:
- 语言标识:Markdown代码块
```python\ncode\n```需要被转换为<pre><code class="language-python">code</code></pre>,以支持语法高亮(依赖客户端)。 - 内容保护:代码块内部的任何Markdown符号(如
**、*)都不应该被解析为格式。因此,解析器必须先提取并保护代码块,在对正文进行格式转换后,再将处理好的代码块HTML插回原位。这通常通过临时占位符(如{code_block_0})来实现。 - 自动补全闭合符:一个非常贴心的设计是自动补全缺失的闭合反引号。ChatGPT在流式输出时,有时会先输出开头的
```python,内容还在生成中,结尾的 ```` ``````` 可能尚未输出或丢失。解析器能检测并补全,避免了因格式错误导致的整个消息解析失败。
- 语言标识:Markdown代码块
链接与转义:
- 链接
[text](URL)的转换相对直接。 - HTML字符转义至关重要。如果用户输入或AI回复中包含
<、>、&等字符,必须将其转换为<、>、&,否则它们会被错误地解析为HTML标签的一部分,导致格式破坏甚至安全问题(如XSS注入,虽然在Telegram上下文风险较低,但仍是良好实践)。
- 链接
2.3 架构设计:一种稳健的转换策略
从使用描述可以推断,该解析器很可能采用了“分阶段处理”的管道架构,这是一种非常稳健的策略:
- 预处理与净化阶段:首先,确保输入文本的“基础卫生”。例如,自动补全未闭合的代码块反引号。这一步能防止后续阶段因格式错误而崩溃。
- 代码块提取与隔离阶段:使用正则表达式匹配所有
```language\n...\n```格式的代码块。将它们从主文本中提取出来,转换为目标HTML格式,并存储到一个列表中。同时在原位置替换为一个唯一的、不会与正文冲突的占位符(如__CODE_BLOCK_0__)。这样,主文本中就只剩下普通段落、样式和链接了。 - 核心Markdown转换阶段:对移除了代码块的主文本,按顺序应用一系列转换规则。这里的顺序很重要!通常需要先处理更复杂、边界更明确的元素(如链接
[...](...)),再处理简单的样式符号(如**、*),并且要小心正则表达式的贪婪与非贪婪匹配,以正确处理嵌套。同时,这个阶段会完成HTML特殊字符的转义。 - 代码块回填阶段:遍历主文本,将所有的占位符
__CODE_BLOCK_N__替换回之前存储的、已转换好的HTML代码块字符串。 - 后处理与验证阶段:(可选但推荐)检查生成的HTML标签是否嵌套正确、是否全部闭合。可以做一个简单的栈检查,确保每一个
<b>都有对应的</b>。
这种架构的优点是模块清晰,每个阶段职责单一,易于调试和扩展。例如,如果需要支持新的Markdown语法(比如脚注),只需要在第三阶段增加相应的转换规则即可。
3. 集成与实操指南
3.1 安装与基础使用
安装非常简单,正如项目所示:
pip install chatgpt-md-converter这里假设包已发布到PyPI。如果是从GitHub仓库直接安装,可能需要使用pip install git+https://github.com/botfather-dev/formatter-chatgpt-telegram.git。
基础使用就是调用一个函数:
from chatgpt_md_converter import telegram_format markdown_text = """ 嘿!这是一个**加粗**的提醒,还有一段 `inline code`。 下面是关键答案:||答案是42||。 详细解释如下: **> 点击展开技术细节 > 这里包含了大量的实现原理... > ...和额外的说明。 """ html_output = telegram_format(markdown_text) print(html_output) # 输出可以直接用于 telebot 或 python-telegram-bot 的 send_message(..., parse_mode='HTML')3.2 在主流机器人框架中的集成示例
让我们看看如何将其集成到两个最流行的Python Telegram Bot框架中。
场景一:与python-telegram-bot集成
python-telegram-bot是一个功能强大、设计优雅的框架。
import logging from telegram import Update from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes from chatgpt_md_converter import telegram_format # 假设你有一个调用OpenAI API的函数 from your_ai_module import get_chatgpt_response # 启用日志 logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO) logger = logging.getLogger(__name__) async def chat_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): user_message = update.message.text logger.info(f"用户 {update.effective_user.id} 发送: {user_message}") try: # 1. 获取AI的原始Markdown回复 raw_markdown_reply = await get_chatgpt_response(user_message) # 2. 使用本工具转换为Telegram HTML formatted_html = telegram_format(raw_markdown_reply) # 3. 发送消息,指定 parse_mode='HTML' await update.message.reply_text(formatted_html, parse_mode='HTML') except Exception as e: logger.error(f"处理消息时出错: {e}") await update.message.reply_text("抱歉,处理您的请求时出现了问题。") def main(): # 替换为你的Bot Token application = Application.builder().token("YOUR_BOT_TOKEN").build() application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, chat_handler)) application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == '__main__': main()关键点:在reply_text方法中设置parse_mode='HTML'是让转换生效的最后一步。
场景二:与pyTelegramBotAPI(telebot) 集成
telebot以其简洁直观的API而受欢迎。
import telebot from chatgpt_md_converter import telegram_format from your_ai_module import get_chatgpt_response bot = telebot.TeleBot("YOUR_BOT_TOKEN") @bot.message_handler(func=lambda message: True) def handle_message(message): try: # 获取AI回复 raw_reply = get_chatgpt_response(message.text) # 格式转换 formatted_reply = telegram_format(raw_reply) # 发送HTML消息 bot.send_message(message.chat.id, formatted_reply, parse_mode='HTML') except Exception as e: bot.send_message(message.chat.id, f"出错了: {str(e)}") if __name__ == '__main__': bot.infinity_polling()注意:无论使用哪个框架,核心步骤都是三步:获取原始Markdown -> 用
telegram_format转换 -> 以HTML模式发送。务必确保不要在已经转换后的HTML字符串上重复设置parse_mode,否则Telegram会尝试再次解析,导致格式混乱。
3.3 流式输出(Streaming)场景下的特殊处理
如果你的机器人支持类似ChatGPT的“打字机”式流式输出,那么格式转换会面临挑战。你不能等AI完全生成完再转换发送,那样会失去流式体验。但每收到一个片段就转换一次,又可能因为片段截断了Markdown语法(如一个**在片段头,另一个在后续片段尾)而导致中间状态格式错误。
策略建议:
- 缓冲区(Buffer)管理:维护一个文本缓冲区。每次收到流式片段,就将其追加到缓冲区。
- 尝试增量转换:对当前的整个缓冲区内容调用
telegram_format。由于该函数能自动补全闭合符,即使缓冲区末尾是一个未闭合的**或```,它也能生成一份当前“最佳猜测”的、标签闭合的HTML。 - 差异发送:比较本次转换后的HTML与上一次发送的HTML的差异,只将新增的、有效的字符发送给用户。这需要精细的差分算法,一个简化版的方法是每次发送当前缓冲区的完整转换结果(对于短消息可行),但会重复传输大量字符。
- 最终修正:当流式传输完成时,你对完整的文本进行一次最终的、准确的格式转换,并替换掉最后一条“进行中”的消息。这样可以确保最终呈现的格式是100%正确的。
这是一个高级特性,实现起来较复杂。对于大多数应用,非流式的“生成完再发送”体验已经足够好。
4. 提示工程:教会AI使用Telegram Markdown
项目文档中给出的Prompt模板是项目的精髓之一。ChatGPT等模型并不天然知道||spoiler||或**>的语法。你必须明确地教导它。
核心Prompt示例:
你可以将以下内容作为系统消息(System Prompt)的一部分,或者在每次对话的初始指令中发送:
你是一个Telegram聊天机器人。请使用以下特定的Markdown格式来组织你的回复,以便它们能在Telegram中正确显示为富文本: ### 格式规范: 1. **加粗**:使用 `**文字**` 2. *斜体*:使用 `*文字*` 或 `_文字_` 3. <u>下划线</u>:使用 `__文字__` (注意是双下划线) 4. ~~删除线~~:使用 `~~文字~~` 5. 行内代码:使用 `` `代码` `` 6. 代码块:使用三个反引号,并可选指定语言: ```python print("Hello") ``` 7. 链接:使用 `[链接文本](https://example.com)` 8. **剧透(隐藏内容)**:使用双竖线包裹:`||这是隐藏内容||`。用户需要点击才能查看。适用于答案、惊喜、敏感信息。 9. **可折叠引用块(长内容收纳)**:用于收纳长篇解释、额外细节或可选内容。 * 第一行以 `**>` 开头,后面跟标题。 * 后续每一行内容都以单独的 `> ` 开头。 * 示例: **> 详细实现步骤 > 第一步,先安装依赖。 > 第二步,配置环境变量。 ### 内容组织建议: - 对关键结论、警告使用**加粗**。 - 对技术术语、变量名使用`行内代码`。 - 对长代码示例使用```代码块```。 - 当直接给出用户可能不想立刻看到的答案(如谜底、练习答案)时,请使用||剧透||包裹。 - 当解释非常冗长,或提供非必要的补充信息时,请使用**> 可折叠引用块**将其收纳起来,保持主回复的简洁。 请严格按照以上格式要求输出。为什么这个Prompt有效?
- 结构化清晰:分门别类地列出了所有语法,并提供了Telegram特有的(剧透、可折叠引用)示例。
- 提供了使用场景:告诉AI在什么情况下该用什么格式(如“关键结论用加粗”、“答案用剧透”),这能引导AI做出更符合预期的格式决策。
- 示例具体:可折叠引用块的格式容易写错,明确的示例至关重要。
实操心得:在实际使用中,即使提供了Prompt,AI偶尔仍会“忘记”或使用错误格式。一个加强记忆的技巧是,在用户提问后,你可以用机器人身份在回复前悄悄加上一句格式提醒(用户不可见),或者在前几轮对话中主动使用这些格式来示范。此外,对于极其重要的格式(如剧透答案),可以在转换函数之后添加一个校验逻辑,如果发现答案部分没有剧透标签,可以自动为其包裹一层。
5. 性能考量与进阶优化
项目提供的性能基准数据很有参考价值。对于单次转换,即使是“长混合文本”也只需要约0.5毫秒,这意味着每秒可以处理超过2000次请求,对于绝大多数机器人应用来说都绰绰有余。性能瓶颈更可能出现在网络I/O(调用OpenAI API)或数据库查询上。
然而,在高并发或处理极长文本(如整篇文章)时,仍有优化空间:
正则表达式优化:项目的核心是正则表达式。确保使用的正则表达式是预编译(
re.compile)的,而不是在每次函数调用中临时编译。查看项目源码,如果发现类似re.sub(r'\*\*(.*?)\*\*', ...)在函数内部,可以将其提取为模块级的常量。# 优化前(在函数内,每次调用都编译) def telegram_format(text): text = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', text) ... # 优化后(预编译) BOLD_PATTERN = re.compile(r'\*\*(.*?)\*\*') def telegram_format(text): text = BOLD_PATTERN.sub(r'<b>\1</b>', text) ...处理超长文本:Telegram单条消息有长度限制(约4096个UTF-8字符)。如果AI回复超过了这个限制,你需要先分割消息,再分别转换和发送。注意,分割点不能破坏Markdown语法结构(比如在代码块中间、或在一个未闭合的加粗标签中间分割)。一个简单策略是按段落(
\n\n)分割,并检查分割点是否在代码块或特殊标签内。异步处理:如果你的机器人框架支持异步(如
python-telegram-bot的异步版本),确保telegram_format函数是同步的、不会阻塞事件循环的。由于它是CPU密集型操作,如果处理量巨大,可以考虑将其放入线程池中执行,避免阻塞异步主循环。缓存策略:如果机器人有大量重复或相似的回复(例如,标准化的帮助信息、常见问题解答),可以对转换后的HTML结果进行缓存。这样,当相同或高度相似的Markdown输入再次出现时,可以直接返回缓存中的HTML,节省CPU时间。
6. 常见问题排查与调试技巧
即使使用了转换器,在实际部署中你可能还是会遇到格式显示不正常的问题。下面是一个排查清单:
问题1:消息完全无格式,显示纯文本(包括星号、反引号)。
- 原因:最可能的原因是
parse_mode='HTML'没有设置或设置错误。 - 排查:
- 检查发送消息的代码,确认
parse_mode参数已正确传入。 - 打印出
telegram_format函数的输出,确认它确实生成了如<b>这样的标签,而不是原始的**。 - 在Telegram官方Web版或桌面客户端测试,有时官方移动App的缓存可能导致显示延迟或错误。
- 检查发送消息的代码,确认
问题2:格式错乱,标签被当作普通文本显示。
- 原因:HTML标签没有被正确转义,或者转换后的HTML结构无效(如标签未闭合、嵌套错误)。
- 排查:
- 检查输入文本中是否包含
<或>字符。转换函数必须将它们转义为<和>。你可以在输入中特意加入这些字符测试。 - 将转换后的HTML片段粘贴到一个简单的HTML文件中,用浏览器打开,检查浏览器是否报解析错误。这能快速定位标签嵌套问题。
- 检查是否对同一段文本重复调用了
telegram_format函数,导致<b>被再次转换,变成<b>。
- 检查输入文本中是否包含
问题3:代码块不显示或格式不对。
- 原因:代码块提取和回填逻辑出错,或者语言标识符被错误处理。
- 排查:
- 输入一个包含简单代码块的文本,逐步调试,查看代码块被提取后,占位符是否正确,回填时是否匹配。
- 检查输出中代码块是否被包裹在
<pre><code class=\"language-xxx\">...</code></pre>中。如果没有class,语法高亮可能失效。 - 确保代码块内的换行符
\n被保留。HTML中需要用<br>或保留在<pre>标签内才能换行,<pre>标签会保留空白符。
问题4:剧透(Spoiler)或可折叠引用不生效。
- 原因:Telegram客户端版本过旧,或者生成的HTML标签或class不正确。
- 排查:
- 剧透文本的HTML是
<span class="tg-spoiler">,检查输出是否完全一致,class名称不能错。 - 可折叠引用是
<blockquote expandable>,检查expandable属性是否存在。 - 在Telegram的最新官方客户端上测试。某些第三方客户端可能不支持这些新特性。
- 剧透文本的HTML是
问题5:性能突然下降。
- 原因:输入了异常长的文本,或者正则表达式遇到了灾难性回溯。
- 排查:
- 记录输入文本的长度。如果经常处理万字长文,需要考虑前面提到的分割策略。
- 检查输入中是否有大量、复杂的嵌套样式(如十层以上的加粗嵌套),这可能会增加正则匹配的复杂度。不过,对于AI生成的文本,这种情况很少见。
调试技巧:
- 单元测试是王道:为你的集成代码编写单元测试,覆盖各种边界情况:空字符串、纯文本、各种样式嵌套、包含特殊字符的代码块、未闭合的标记等。使用项目自带的测试文件(如果有)作为参考。
- 使用中间状态输出:如果问题复杂,可以临时修改转换函数,让它打印出每个主要处理阶段(如提取代码块后、转换样式后)的文本,帮助你定位问题发生在哪个环节。
- 对比在线工具:可以找一个在线的Markdown转HTML工具(输出标准HTML)作为粗略参考,但记住最终标准是Telegram的HTML子集。
这个formatter-chatgpt-telegram项目就像一座精心设计的桥梁,一头连接着强大但格式“自由散漫”的大模型,另一头连接着要求严格但表现力丰富的Telegram客户端。用好它,不仅能提升机器人的专业感和用户体验,更能让你从繁琐的文本处理杂务中解脱出来。