Code analysis summary
作成日: 2024年12月14日
作成日: 2024年12月14日
分析代码
import requests
import asyncio
import re
import json
from typing import Callable, Any, Optional, Dict, List
from urllib.parse import urlparse
EmitterType = Optional[Callable[[dict], Any]]
SendCitationType = Callable[[str, str, str], None]
SendStatusType = Callable[[str, bool], None]
def get_send_citation(event_emitter: EmitterType) -> SendCitationType:
async def send_citation(url: str, title: str, content: str):
if event_emitter is None:
return
await event_emitter(
{
"type": "citation",
"data": {
"document": [content],
"metadata": [{"source": url, "html": False}],
"source": {"name": title},
},
}
)
return send_citation
def get_send_status(event_emitter: EmitterType) -> SendStatusType:
async def send_status(status_message: str, done: bool):
if event_emitter is None:
return
await event_emitter(
{
"type": "status",
"data": {"description": status_message, "done": done},
}
)
return send_status
class Tools:
def init(self):
pass
textasync def pipe( self, user_message: str, model_id: str, messages: List[Dict[str, Any]], body: Dict[str, Any], __event_emitter__: EmitterType = None, ) -> None: pattern = r"(?:https?://)?linux\.do/t/topic/\d+(?:/\d+)?(?:\?.*)?" urls = re.findall(pattern, user_message) if not urls: return None send_status = get_send_status(__event_emitter__) send_citation = get_send_citation(__event_emitter__) for url in urls: full_url = url if url.startswith("http") else f"https://{url}" # 解析 URL parsed_url = urlparse(full_url) if not parsed_url.scheme: parsed_url = parsed_url._replace(scheme="https") full_url = parsed_url.geturl() if not parsed_url.netloc: error_msg = f"Invalid URL: {full_url}. No host supplied." await send_status(error_msg, True) for msg in reversed(messages): if msg["role"] == "user": msg["content"] = msg["content"].replace(url, error_msg) break continue # 构建 JSON URL json_url = ( f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}.json" ) try: await send_status(f"正在读取 {full_url} 的内容", False) # 添加一个小延迟,确保初始状态能被显示 await asyncio.sleep(0.1) response = requests.get(json_url) response.raise_for_status() markdown = parse_to_markdown(response.text) await send_status("解析完成,正在发送内容", False) # 添加一个小延迟,确保状态更新能被显示 await asyncio.sleep(0.1) for msg in reversed(messages): if msg["role"] == "user": msg["content"] = msg["content"].replace(url, markdown) break await send_status("内容发送成功", True) await send_citation(full_url, "Linux.do 帖子内容", markdown) except requests.RequestException as e: error_msg = f"无法获取 {full_url} 的数据。错误信息: {str(e)}" await send_status(error_msg, True) for msg in reversed(messages): if msg["role"] == "user": msg["content"] = msg["content"].replace(url, error_msg) break except Exception as e: error_msg = f"处理 {full_url} 时出错。错误信息: {str(e)}" await send_status(error_msg, True) for msg in reversed(messages): if msg["role"] == "user": msg["content"] = msg["content"].replace(url, error_msg) break body["messages"] = messages return None
def parse_to_markdown(json_data: str) -> str:
try:
data = json.loads(json_data)
title = data.get("title", "未标题")
posts = data.get("post_stream", {}).get("posts", [])
textif not posts: return "未找到帖子内容。" md = f"# {title}\n\n---\n\n" for post in posts: username = post.get("username", "未知用户") display_username = post.get("display_username") or username content = post.get("cooked", "") post_number = post.get("post_number", 0) if post_number == 1: md += f"**作者:** {display_username}\n\n" content = re.sub(r'data-download-href=".*?"', "", content) md += f"**内容:**\n\n{content}\n\n---\n\n## 评论\n\n" else: md += f"**{display_username}:**\n\n{content}\n\n---\n\n" return md except Exception as e: return f"解析 JSON 时出错: {str(e)}"
这段代码主要用于处理用户消息中包含特定格式的URL(如 linux.do/t/topic/数字),通过获取这些URL对应的JSON数据,将其解析为Markdown格式,并替换原始消息中的URL。以下是对代码的详细分析:
URL 提取与验证:
https://linux.do/t/topic/12345)。数据获取与解析:
.json)。消息更新与事件通知:
pythonEmitterType = Optional[Callable[[dict], Any]] SendCitationType = Callable[[str, str, str], None] SendStatusType = Callable[[str, bool], None]
EmitterType:可选的事件发射器,接受一个字典参数。SendCitationType:发送引用信息的函数类型,接受三个字符串参数。SendStatusType:发送状态信息的函数类型,接受一个字符串和一个布尔值参数。get_send_citationpythondef get_send_citation(__event_emitter__: EmitterType) -> SendCitationType: async def send_citation(url: str, title: str, content: str): if __event_emitter__ is None: return await __event_emitter__( { "type": "citation", "data": { "document": [content], "metadata": [{"source": url, "html": False}], "source": {"name": title}, }, } ) return send_citation
send_citation 函数,用于发送引用信息。get_send_statuspythondef get_send_status(__event_emitter__: EmitterType) -> SendStatusType: async def send_status(status_message: str, done: bool): if __event_emitter__ is None: return await __event_emitter__( { "type": "status", "data": {"description": status_message, "done": done}, } ) return send_status
send_status 函数,用于发送状态更新。Tools 类及其 pipe 方法pythonclass Tools: def __init__(self): pass async def pipe( self, user_message: str, model_id: str, messages: List[Dict[str, Any]], body: Dict[str, Any], __event_emitter__: EmitterType = None, ) -> None: ...
pipe 方法是核心功能实现,接受用户消息、模型ID、消息列表、请求体以及可选的事件发射器作为参数。pythonpattern = r"(?:https?://)?linux\.do/t/topic/\d+(?:/\d+)?(?:\?.*)?" urls = re.findall(pattern, user_message) if not urls: return None
linux.do/t/topic/数字 格式的URL。pythonsend_status = get_send_status(__event_emitter__) send_citation = get_send_citation(__event_emitter__)
send_status 和 send_citation 函数,用于后续的状态和引用信息发送。pythonfor url in urls: full_url = url if url.startswith("http") else f"https://{url}" ...
https)。pythonparsed_url = urlparse(full_url) if not parsed_url.scheme: parsed_url = parsed_url._replace(scheme="https") full_url = parsed_url.geturl() if not parsed_url.netloc: error_msg = f"Invalid URL: {full_url}. No host supplied." await send_status(error_msg, True) for msg in reversed(messages): if msg["role"] == "user": msg["content"] = msg["content"].replace(url, error_msg) break continue
urlparse 解析URL,确保其包含协议和主机名。pythonjson_url = f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}.json" try: await send_status(f"正在读取 {full_url} 的内容", False) await asyncio.sleep(0.1) response = requests.get(json_url) response.raise_for_status() markdown = parse_to_markdown(response.text) await send_status("解析完成,正在发送内容", False) await asyncio.sleep(0.1) for msg in reversed(messages): if msg["role"] == "user": msg["content"] = msg["content"].replace(url, markdown) break await send_status("内容发送成功", True) await send_citation(full_url, "Linux.do 帖子内容", markdown)
.json)。requests.get 获取JSON数据,并检查响应状态。parse_to_markdown 函数将JSON数据解析为Markdown。pythonexcept requests.RequestException as e: error_msg = f"无法获取 {full_url} 的数据。错误信息: {str(e)}" await send_status(error_msg, True) for msg in reversed(messages): if msg["role"] == "user": msg["content"] = msg["content"].replace(url, error_msg) break except Exception as e: error_msg = f"处理 {full_url} 时出错。错误信息: {str(e)}" await send_status(error_msg, True) for msg in reversed(messages): if msg["role"] == "user": msg["content"] = msg["content"].replace(url, error_msg) break
requests 相关的异常和其他通用异常,发送错误状态,并在用户消息中替换URL为错误信息。pythonbody["messages"] = messages return None
parse_to_markdown 函数pythondef parse_to_markdown(json_data: str) -> str: try: data = json.loads(json_data) title = data.get("title", "未标题") posts = data.get("post_stream", {}).get("posts", []) if not posts: return "未找到帖子内容。" md = f"# {title}\n\n---\n\n" for post in posts: username = post.get("username", "未知用户") display_username = post.get("display_username") or username content = post.get("cooked", "") post_number = post.get("post_number", 0) if post_number == 1: md += f"**作者:** {display_username}\n\n" content = re.sub(r'data-download-href=".*?"', "", content) md += f"**内容:**\n\n{content}\n\n---\n\n## 评论\n\n" else: md += f"**{display_username}:**\n\n{content}\n\n---\n\n" return md except Exception as e: return f"解析 JSON 时出错: {str(e)}"
post_number == 1),作为作者的主要内容展示,其他帖子作为评论展示。代码中存在异步与同步代码的混用,可能导致性能瓶颈或不可预期的行为:
send_citation 和 send_status 是异步函数,需要使用 await 调用。requests.get,这会阻塞事件循环,降低异步性能。aiohttp,替代 requests,以充分利用异步优势。pythonimport aiohttp # 示例修改 async with aiohttp.ClientSession() as session: async with session.get(json_url) as response: response.raise_for_status() json_text = await response.text()
当前的正则表达式:
pythonpattern = r"(?:https?://)?linux\.do/t/topic/\d+(?:/\d+)?(?:\?.*)?"
http 或 https 协议。linux.do/t/topic/ 后跟一串数字,后面可选地跟一个斜杠和另一个数字,以及可选的查询参数。pythontasks = [] for url in urls: tasks.append(process_url(url, ...)) # 定义一个异步处理单个URL的函数 await asyncio.gather(*tasks)
这段代码实现了从用户消息中提取特定格式的URL,获取其JSON数据并解析为Markdown,再将结果反馈给用户的功能。总体结构清晰,但在异步与同步代码的混用、正则表达式的准确性、性能优化和安全性等方面还有改进空间。通过使用异步HTTP库、优化正则表达式、并发处理URL以及加强安全验证,可以提升代码的性能、可靠性和安全性。
下面是对 parse_to_markdown 函数以及相关上下文进行更深度的分析和改进建议:
pythondef parse_to_markdown(json_data: str) -> str: try: data = json.loads(json_data) title = data.get("title", "未标题") posts = data.get("post_stream", {}).get("posts", []) if not posts: return "未找到帖子内容。" md = f"# {title}\n\n---\n\n" for post in posts: username = post.get("username", "未知用户") display_username = post.get("display_username") or username content = post.get("cooked", "") post_number = post.get("post_number", 0) if post_number == 1: # 第一篇帖是主题内容 md += f"**作者:** {display_username}\n\n" # 移除特定的下载链接属性 content = re.sub(r'data-download-href=".*?"', "", content) md += f"**内容:**\n\n{content}\n\n---\n\n## 评论\n\n" else: # 后续是评论 md += f"**{display_username}:**\n\n{content}\n\n---\n\n" return md except Exception as e: return f"解析 JSON 时出错: {str(e)}"
该函数的主要职责是:
post_stream.posts 列表包含主题帖和回复)。try/except 捕获异常,并在失败时返回可读的错误信息。HTML内容的处理:
content 字段来自 Discourse 的 JSON 数据,其 cooked 字段通常是已渲染好的 HTML 内容。如果将 HTML 直接嵌入 Markdown 中,可能导致渲染效果不理想或产生安全问题。
改进建议:
markdownify 库)将 cooked 内容转换为真正的 Markdown 文本,以获得更一致、纯文本化的输出。对特殊属性的清理不完整:
当前仅对 data-download-href 属性进行了简单替换清理,但其他HTML标签和属性可能依然存在。
改进建议:
BeautifulSoup 等 HTML 解析库,更精确地清理不需要的标签或属性。cooked 中存在图片、链接、表格等内容,可以根据需要进行更丰富的转换。异常处理的细粒度:
目前对所有异常均返回同样的提示信息 解析 JSON 时出错: ...。这对于开发调试虽然够用,但对最终用户不够友好。
改进建议:
并发和性能问题:
当前代码结构虽然简单明了,但如果在高并发或需要频繁调用下,性能可能会受到影响(例如外部网络请求部分)。不过这属于上游环节(获取JSON数据的部分),parse_to_markdown 本身主要是本地解析和字符串处理,性能问题不大。
改进建议(可选):
国际化和本地化支持:
函数中部分文本(如 "未标题", "未找到帖子内容", "作者", "内容", "评论")是中文硬编码。如果服务面向国际用户,可能需要多语言支持。
改进建议:
Markdown格式进一步优化:
目前Markdown中对标题、作者、内容和评论使用了特定的格式,但如果希望展示得更美观或有更多层级结构,可以进一步丰富Markdown格式。例如:
parse_to_markdown 函数能较好地实现从JSON数据到Markdown文本的初步转换,结构清晰且易懂。但在HTML处理、安全性、国际化和用户体验等方面仍有改进空间。
通过对HTML进行更好的转换、完善错误处理和丰富Markdown格式,可以使得输出内容在实际使用场景中更可靠、更美观,也更易于阅读和后期维护。
给出修改建议
下面是针对 parse_to_markdown 函数及其相关逻辑提出的修改建议,包括具体的代码改进方向和理由:
使用 HTML 到 Markdown 转换库
当前的 content 是 HTML 格式的 cooked 内容,直接嵌入 Markdown 会带来渲染不一致的问题。建议使用一个 Python 库(如 markdownify)将 HTML 转换为 Markdown。这可以减少对 HTML 标签的手动处理。
示例代码:
pythonfrom markdownify import markdownify as md_convert def parse_to_markdown(json_data: str) -> str: try: data = json.loads(json_data) title = data.get("title", "未标题") posts = data.get("post_stream", {}).get("posts", []) if not posts: return "未找到帖子内容。" md = f"# {title}\n\n---\n\n" for post in posts: username = post.get("username", "未知用户") display_username = post.get("display_username") or username content_html = post.get("cooked", "") post_number = post.get("post_number", 0) # 使用 HTML -> Markdown 转换 content_md = md_convert(content_html) if post_number == 1: md += f"**作者:** {display_username}\n\n" md += f"**内容:**\n\n{content_md}\n\n---\n\n## 评论\n\n" else: md += f"**{display_username}:**\n\n{content_md}\n\n---\n\n" return md except json.JSONDecodeError as e: return f"解析 JSON 时出错:JSON格式不正确。错误信息: {str(e)}" except Exception as e: return f"处理数据时出错:{str(e)}"
完善异常处理
将 except Exception 拆分为更具体的异常,例如 json.JSONDecodeError 来处理 JSON 解析失败的情况,给出更明确的错误信息。
已在上述代码示例中演示。
细化内容过滤
如果想要进一步清理 HTML 中的特定属性,可在转换前使用 BeautifulSoup 来移除特定标签或属性,而不仅仅是正则替换。
示例(在转换前处理 HTML):
pythonfrom bs4 import BeautifulSoup def clean_html(html: str) -> str: soup = BeautifulSoup(html, "html.parser") # 移除 data-download-href 属性 for tag in soup.find_all(attrs={"data-download-href": True}): del tag["data-download-href"] return str(soup) def parse_to_markdown(json_data: str) -> str: try: data = json.loads(json_data) title = data.get("title", "未标题") posts = data.get("post_stream", {}).get("posts", []) if not posts: return "未找到帖子内容。" md = f"# {title}\n\n---\n\n" for post in posts: username = post.get("username", "未知用户") display_username = post.get("display_username") or username content_html = post.get("cooked", "") # 在转换之前清理HTML cleaned_html = clean_html(content_html) # 然后转换为Markdown content_md = md_convert(cleaned_html) post_number = post.get("post_number", 0) if post_number == 1: md += f"**作者:** {display_username}\n\n" md += f"**内容:**\n\n{content_md}\n\n---\n\n## 评论\n\n" else: md += f"**{display_username}:**\n\n{content_md}\n\n---\n\n" return md except json.JSONDecodeError as e: return f"解析 JSON 时出错:JSON格式不正确。错误信息: {str(e)}" except Exception as e: return f"处理数据时出错:{str(e)}"
国际化与定制化文本
将中文硬编码文本提取至配置文件或变量中,以满足未来的国际化需求或用户自定义。
示例:
python# 定义一个配置字典或读取配置文件 I18N = { "no_posts": "未找到帖子内容。", "no_title": "未标题", "author_label": "作者", "content_label": "内容", "comments_label": "评论" } def parse_to_markdown(json_data: str, i18n=I18N) -> str: try: data = json.loads(json_data) title = data.get("title", i18n["no_title"]) posts = data.get("post_stream", {}).get("posts", []) if not posts: return i18n["no_posts"] md = f"# {title}\n\n---\n\n" for post in posts: username = post.get("username", "未知用户") display_username = post.get("display_username") or username content_html = post.get("cooked", "") cleaned_html = clean_html(content_html) content_md = md_convert(cleaned_html) post_number = post.get("post_number", 0) if post_number == 1: md += f"**{i18n['author_label']}:** {display_username}\n\n" md += f"**{i18n['content_label']}:**\n\n{content_md}\n\n---\n\n## {i18n['comments_label']}\n\n" else: md += f"**{display_username}:**\n\n{content_md}\n\n---\n\n" return md except json.JSONDecodeError as e: return f"解析 JSON 时出错:JSON格式不正确。错误信息: {str(e)}" except Exception as e: return f"处理数据时出错:{str(e)}"
可扩展的格式增强
可以根据需要在评论前添加时间戳、帖子链接或其他元信息。也可对内容中的特定模式(如图片链接、@用户、#主题)进行进一步标记化处理。
通过上述修改建议,可以:
这些改进将使得 parse_to_markdown 函数更加健壮、可扩展和易于维护。