1. 先说结论:这个命令到底解决了什么问题
我平时有大量阅读英文技术文章的习惯,但真正读起来总是卡在几个点上:页面广告和推荐流干扰太多、想存下来却复制到本地全是乱版、遇到长文读着费劲还得来回切翻译窗口。一开始我也图省事,用浏览器插件或者在线翻译页解决,但每次都要手动复制、粘贴、重新排版,遇到十几页的长文更是折磨。
后来我给自己定了一个小目标:能不能用一个命令行命令一次搞定所有事情——给它一个网址,它自己抓取正文、清理格式、输出中文翻译,并且直接保存成 Markdown 文件。正好手里一直在用 iFlow CLI,它支持注册自定义 Command,于是我就动手做了一个"网页文章下载与翻译工具",这里把完整的实现思路和踩坑记录分享出来。
这个方案非常适合以下几类人参考:需要批量抓取网页正文做资料归档的人、经常阅读外文技术博客但又不想被在线翻译限制字符数的朋友,以及正在研究 iFlow CLI 自定义命令开发、想知道配置细节和调试方法的开发者。哪怕你对 CLI 工具不熟悉,只要跟着下面每一节操作,也能把这套逻辑搬到自己的环境里。
2. 命令的完整工作流设计
2.1 从URL到Markdown的四个处理阶段
在设计这个工具之前,我先把整个需求拆成了几个独立的处理阶段。这样做的好处是:每个阶段都能单独测试、单独替换,哪怕以后换翻译引擎或者改输出格式,也不用把整条链路推倒重来。
整个命令的工作流分为四段:
- 抓取阶段:根据传入的 URL 请求网页内容,拿到 HTML 源码。这里要考虑超时、重定向、User-Agent 伪装、Gzip 压缩等基础问题,稍不小心就会在编码上翻车。
- 解析阶段:从 HTML 中提取文章标题和正文。核心难点在于去除导航栏、侧边栏、页脚、广告、评论等噪声,只保留作者真正写出来的内容。
- 翻译阶段:把正文文本送入翻译引擎,分批处理,等待结果返回,并尽可能保持原文的段落结构。
- 落盘阶段:将翻译后的文本序列化成 Markdown,写入本地文件,同时在终端输出摘要信息,告诉用户文件保存在哪里。
这里插一句我自己的设计心得:最开始我的想法是"一步到位",直接在抓取函数里翻译、翻译函数里写文件,结果调试的时候根本分不清是抓取失败还是翻译失败还是编码问题。后来老老实实拆成四个函数,每个函数只做一件事,调试效率明显上来了。
2.2 技术选型:Python生态的轻量组合
技术选型上,我倾向用最常见的 Python 生态,没有引入重型框架。整个工具就依赖四个库:requests发请求、BeautifulSoup解析 DOM、html2text或者自己写的转换器做 HTML 到 Markdown 的转换、xxhash做文本指纹缓存。如果你不想装这么多,也可以只用 requests + BeautifulSoup,最后用正则清理 HTML 标签。
iFlow CLI 的自定义 Command 并不限定脚本语言,只要注册时指定执行入口即可。我选择 Python 是因为它的字符串处理和文本清洗生态最成熟,写起来效率最高。如果你更熟悉 Node.js,用 fetch + cheerio 的组合也能实现同样的效果,整体逻辑完全一致。
工具的运行模式我定为"单条命令 + 可选参数":
- 必传参数:文章 URL
- 可选参数:目标语言(默认中文)、是否保留英文原文、输出目录、是否生成带元数据的 Markdown
这个设计考虑很直接:大多数 AI 阅读场景只想要一份干净的译文,但资料归档的用户往往希望保留原文和链接信息。参数化设计能让同一套代码适应两种用法。
3. 正文提取模块的实现
3.1 获取页面与编码处理的坑
正文提取是整个工具的基础,如果这一步拿到的是乱码或者残缺内容,后面翻译得再好也没有意义。我先说抓取阶段最容易踩的几个坑。
第一个坑是编码识别。绝大多数网页是 UTF-8 编码,但不少老站点、个人博客用 GBK、GB2312 甚至 Latin-1,如果不做处理直接按 UTF-8 解码,中文会变成一片乱码。我的做法是优先读取 HTML 源码中<meta charset>或<meta http-equiv="Content-Type">里的声明,其次用requests返回的apparent_encoding去兜底。在实测中,apparent_encoding的识别准确率并不算高,所以我会再写一层白名单兜底——遇到常见的 GBK 类页面时直接尝试用gb18030解码。
第二个坑是反爬虫与请求头。有个别文档站会拦截默认的 Python UA。我的处理方式是在请求头中伪造一个常见的浏览器 UA,并加上 Accept、Accept-Language 等字段,同时设置 15 秒超时和最多两次重试。对于需要登录才能查看文章的站点,我暂时没有做 Cookie 注入,只是在报错信息中明确提示用户。
第三个坑是重定向与短链接。很多分享出来的链接是经过短链跳转的,最终页面的 URL 才是真实文章地址。我会记录重定向后的最终 URL,后面生成 Markdown 元数据时以最终 URL 为准。
3.2 基于DOM结构和规则评分提取正文
拿到 HTML 之后,接下来就是提取正文。网上有不少现成的正文提取库,但它们的通病是模型较重,且对中文站点适配一般。我这里采用了一种轻量但极其有效的"规则评分"思路,原理其实很简单:
- 遍历 DOM 中的所有
<p>标签,统计每个<p>的文本长度和其所属父容器。 - 给每个容器打分,评分维度包括:段落数量、段落平均长度、文本中标点密度、链接密度(正文中链接密度低)、是否包含标题标签。
- 选取得分最高的容器作为正文容器,如果得分不足阈值,则回退到取整篇文章的最大文本节点。
这个逻辑和人类判断"哪里是正文"的方式非常接近:一段长文本且配着标题、很少有外链的区块,大概率就是正文。对于绝大多数技术博客,这种方法的准确率在 90% 以上,比我预想的要好。
如果你遇到结构特别复杂的页面(比如多栏目站点),还可以再叠加一层"正文评分阈值":当最高分容器得分不足设定阈值时,直接提示用户"页面结构异常,请手动指定正文容器选择器"。我给工具预留了--selector参数,用户可以手动传入 CSS 选择器来强制指定正文区域。这个功能我在处理某个老博客时真用上了,实测能兜底很多奇怪结构。
3.3 清理与结构化输出
提取到正文容器之后,还不能直接翻译,因为里面还残留着很多<div>嵌套、<span>样式节点、空行和无意义的链接。我的清理流程大致是:
- 移除所有
<script>、<style>、<noscript>节点 - 移除图片节点,但保留
alt文本(如果存在)作为占位提示 - 移除空段落和只有空格的节点
- 将
<br>标签替换为换行符 - 将
<h2>、<h3>、<p>、<li>等关键标签映射为 Markdown 结构
这里有一个细节:直接用html2text库转换虽然省事,但它在处理代码块时经常丢缩进,在处理pre标签时表现不稳定。所以我的做法是:先把 HTML 树里的代码块节点单独摘出来,用专门的转换函数处理,再插回正文流。这样能保证 GitHub 上常见的那种带行号代码块被正确保留为 Markdown 代码块。
最终,正文提取模块的输出是一个包含title、url、author、publish_time、content_markdown、word_count六个字段的字典。这个字典结构贯穿后续所有流程,翻译模块和落盘模块都只依赖它。
4. 翻译模块的接入
4.1 翻译引擎选型与成本估算
翻译引擎的选择直接决定了工具好不好用。考虑到可访问性与成本问题,我接的是兼容主流大模型接口协议的"通用翻译服务",这类服务通常有免费额度,且文档全面、稳定性高。
选择这类服务有几个理由:
- 接口协议通用:换一家服务商时只需改 base_url 和模型名称,不用重写逻辑。
- 上下文理解强:相比传统逐句翻译,整段翻译的语序和术语一致性更好。
- 输入价格便宜:当前模型输入 token 的成本已经降至可忽略不计,单篇一万字的文章翻译费用一般在几分钱级别。
我在代码里预留了--engine参数,默认使用通用兼容接口,也保留切换到本地模型的可能。如果你对数据隐私有更高要求,可以在此基础上接入本地推理服务,只改一个 URL 配置项。
4.2 长文本分批翻译的处理逻辑
翻译阶段最核心的问题是:模型有上下文窗口限制,单次请求不能塞入整篇长文。所以需要把正文切成若干批次,再分批翻译。
我一开始的方案是按字符数硬切,每 1800 个字符切一段。用了两天就发现问题:英文长句在硬切时经常被拦腰截断,翻译出来的句子语义支离破碎。
后来改成按段落边界聚合:
- 把正文拆成段落列表。
- 从第一段开始累计 token 数,当累计值接近 1500 时,把当前已累计的段落作为一批提交。
- 批次之间留 50 个 token 的余量,防止模型输出超长。
这样处理的优势是每个批次的文本在语义上是完整的段落集合,翻译质量比硬切好得多。如果你处理的是中文原文,token 数可以适当放宽,因为中文在 token 化后的比例和英文差异不小。
还要考虑翻译进程的并发与速率限制。免费档的服务通常有 QPS 限制,比如每秒只允许 3 到 5 个请求。我的处理是在批次之间加入可变延迟,避免一次性把批次全部发出去导致限流。实测下来,延迟从 0.3 秒到 1 秒随机取值,既能保证速度,也能稳定过限流。
4.3 专业术语的稳定性处理
翻译技术文章时,最怕的是同一个术语在不同段落被翻成不同的词。比如 "command" 一会儿是"命令"一会儿是"指令",读者看得一头雾水。
我的做法是引入一个术语表映射 + 翻译后替换的双保险机制:
- 在请求翻译之前,先对原文做预处理,把术语统一替换为占位符。例如把
iFlow CLI替换成{{TERM_IFLOW_CLI}},把Command替换成{{TERM_COMMAND}}。 - 翻译完成后,再把占位符替换回术语原文,确保这些词不被翻译引擎改动。
- 术语表维护在一份独立的 JSON 文件中,用户可以自行扩展。
实际体验下来,这个技巧非常实用。像 "CLI"、"API"、"markdown" 这类专业词汇混在中文译文里是正常现象,刻意翻译反而会显得不自然。术语表机制还让我可以把品牌词固定为英文,保证阅读一致性。
为了防止重复处理同一篇文章浪费 token,我还加了内容指纹缓存:对原文取 xxhash 值,如果之前翻译过相同内容,直接读取缓存结果,跳过翻译环节。这对重复执行命令、批量处理文章时节省成本帮助很大。
5. 注册为iFlow Command:配置与调试全记录
5.1 Command配置结构解析
iFlow CLI 的自定义 Command 注册方式非常直观。它通过一个.iflow/commands.json文件来声明命令的元信息,每个命令指向一个可执行脚本。我的配置结构大致如下:
{ "command": "article2md", "description": "下载网页文章并翻译为中文 Markdown 文件", "parameters": [ { "name": "url", "type": "string", "required": true, "description": "文章页面地址" }, { "name": "lang", "type": "string", "default": "zh-CN", "description": "目标语言,默认简中" }, { "name": "output", "type": "string", "default": "./output", "description": "输出目录,默认当前目录下 output 文件夹" }, { "name": "keep-original", "type": "boolean", "default": false, "description": "是否在 Markdown 中保留英文原文" } ], "script": "./scripts/article2md.py", "runner": "python3", "output_mode": "file" }这段配置里有三个设计点值得展开说一下。
第一个是output_mode: "file"。iFlow CLI 支持两种输出模式:一种是命令执行结果直接输出文本到终端;另一种是生成文件后返回文件路径。由于这个工具的核心产物是 Markdown 文件,我选了文件模式,这样终端不会刷出几千行译文,只返回一个保存路径,体验更干净。
第二个是runner字段。我指定为python3,意味着 iFlow 会调用python3 scripts/article2md.py并把参数透传进去。如果你在 Windows 环境,需要改成python或者其他对应的解释器路径。
第三个是parameters的参数名设计。我把布尔参数命名为keep-original而不是original,虽然命令行里多敲了几个字符,但可读性更好,用--keep-original时任何使用者都能立刻明白这个参数的作用。
5.2 参数定义与交互体验设计
参数定义之后,还需要处理命令在终端中的"交互体验"。
我在脚本里做了一层参数校验与提示逻辑:
- URL 必须是以
http://或https://开头的合法链接,否则提示并退出。 - 输出目录不存在时自动创建,而不是报错。
- 翻译引擎未配置时,提示先去环境变量里设置 API Key,并给出示例。
- 如果用户没有传 URL,直接进入交互模式,脚本会通过
input()提示用户输入网址。
交互模式的加入是我后来才想到的。最开始这个工具只支持全参数调用,有次我在终端里忘了带网址,命令直接报错退出,又要重新敲一遍完整命令。后来加了检测:缺少 URL 时进入input()交互,只问一个必填项,其他的用默认值。虽然代码只多几行,但日常使用顺手太多。
另外我还定制了终端的进度输出:每个阶段结束后向 stderr 输出一行带状态标记的日志,例如[1/4] 页面下载完成、[2/4] 正文提取完成、[3/4] 翻译完成、[4/4] 文件已写入。这样用户能清楚看到命令卡在哪一步。实际调试时,这四行日志帮我快速定位了绝大多数问题。
5.3 调试过程的几个关键技巧
在把脚本接入 iFlow CLI 的过程中,我总结了几个调试技巧,这些经验在跑任何自定义 Command 时都适用。
技巧一:先隔离脚本,再接入 CLI。也就是说先把 Python 脚本单独放在终端里用参数跑通,确认输出符合预期之后,再改commands.json接入 iFlow。不要直接改完配置就去测试,否则报错时根本不知道是配置格式问题还是脚本逻辑问题。
技巧二:用--help验证参数解析。参数多了以后,我经常忘记某个参数到底是--output还是--output-dir。所以我为脚本实现了--help分支,终端里输入article2md --help就能看到完整的参数说明,不用反复翻源码。
技巧三:日志与数据分离。所有过程日志一律写入 stderr,只有最终文件路径写入 stdout。这是 Unix 工具设计的经典原则,接 iFlow 这类 CLI 框架时必须遵守。曾有次我把一段 DEBUG 日志打到了 stdout,结果 iFlow 把整段日志当成了返回值,终端显示差点崩掉。
技巧四:善用环境变量。API Key 这类敏感信息不要写进命令配置或脚本源码,统一从环境变量读取。iFlow 本身也支持读取用户级环境变量,所以我在脚本中通过os.getenv("TRANSLATE_API_KEY")读取密钥,缺失时给出明确的提示文案。
6. 实测效果与踩坑记录
6.1 三个典型场景的实测对比
我在本地对三个不同结构的网站做了完整测试,结果如下表:
| 测试目标 | 页面结构 | 正文提取结果 | 翻译质量 | 耗时 |
|---|---|---|---|---|
| 某技术博客单篇教程(英文) | 标准文章页,内容居中,侧边栏较少 | 标题、段落、代码块全部正确提取 | 术语一致性好,句子通顺 | 约 12 秒 |
| 某新闻门户深度报道(英文) | 多栏布局,含大量相关阅读推荐 | 正文完整提取,无侧边栏噪声 | 段落结构保留完整,引用句译得准确 | 约 18 秒 |
| 某个人博客随笔(日文) | 极简风格,仅有正文和评论区 | 正文提取正常,评论区被完全过滤 | 翻译基本流畅,个别语气词有偏差 | 约 9 秒 |
从测试结果来看,规则评分提取法在标准文章页和个人博客上的表现最稳定,在多栏布局的门户网站上也能有效过滤侧边栏。翻译耗时和文章长度正相关,主要瓶颈在分批请求的网络往返,而不是模型处理本身。
我还测试了一个极端情况:某页面正文超过 15000 字。此时翻译阶段会拆成 10 批以上,总耗时达到两三分钟。因为加了缓存机制,第二次运行相同 URL 时直接秒出结果,这一点非常爽。
6.2 高频错误清单与解决方案
运行一段时间后,我把遇到的高频错误整理成了一份自查清单。这里挑几个典型的,大家如果遇到类似报错,可以对照排查。
错误一:[1/4] 页面下载失败: HTTP 403
出现 403 基本就是被服务端拦截了。绝大多数时候是 UA 被识别,我把请求头里的 UA 换成最新版 Chrome UA 字符串后解决。极个别站点还有更强力的防护,这种只能通过--cookies参数手动注入登录态解决。
错误二:[2/4] 正文提取失败: No suitable content block
这个报错表示页面没有找到符合评分阈值的正文容器。我第一次遇到是在一个 PDF 转 HTML 的页面上,正文其实全是图片,自然没有足够的文本段落。后来我补充了逻辑:当正文文本长度少于 500 个字符时,不强行翻译,直接提示"该页面可能以图片为主"。
错误三:翻译返回内容包含大量英文原文
这不是报错,但经常误导人。原因是模型在处理超长段落时偶发漏译。我在后处理里加了一层"漏译检测":如果译文里连续出现超过 80 个英文字符的句子,就把该段落标记为未翻译,再单独送一次小规模补译请求。
错误四:[4/4] 文件写入失败: File name too long
这个是我在 Windows 上测试时踩的坑。文章标题很容易超过 Windows 文件名的 255 字符限制。解决办法是把文件名做哈希截断:标题前 50 个字符 + 8 位短哈希。这个改动不仅解决了上限问题,还避免了不同文章同名导致的覆盖冲突。
6.3 真实使用中的两个意外发现
除了表格里测试的网站,我在实际使用中还发现了一些意料之外的结果。
第一个发现是:这个工具对于没有正文但内容全部在图片里的网站几乎无能为力,但这类网站并不多,所以我没有引入 OCR 的打算。如果你有强需求,可以在解析阶段接入一个基础的图片转文字模型,但这会把工具的依赖库和耗时都拉高一个量级。
第二个发现是:翻译后的 Markdown 文件可以很好地兼容本地知识库软件。我用某笔记软件打开生成的.md文件,正文排版、代码块、标题层级全部正常显示。这让工具的价值从"阅读辅助"直接扩展到"资料沉淀",日常积累外文资料方便了很多。
7. 后续可以扩展的方向
基础功能跑通之后,我一直在考虑这个工具还能往哪些方向扩展。有几个方向已经验证过可行性,可以供大家参考。
第一是批量抓取与队列化。现在一次只能处理一个 URL,但很多时候需求是一篇长文连载或者一个专题下的多篇文章。如果能把多个 URL 放进一个文件,命令通过--batch-file参数循环处理,再配合已有的缓存机制,就能实现全站某个栏目的批量归档。
第二是生成双语对照版本。目前生成的 Markdown 只有译文,如果用户需要中英对照排版,可以在落盘阶段把原文段落和译文段落交错输出。这个功能已经在规划中,实现上只要把--keep-original参数从布尔值改成三种模式:仅译文、原文在上、段落级交错。
第三是定时拉取与更新监测。对于经常更新的文档类页面,可以配合系统的计划任务定期检查页面指纹,发现页面变化就重新抓取翻译。这本质上是一个轻量级的网页监控系统,也是我最想做的下一步。
第四个方向更有意思:把工具作为其他语言模型的工具接口。因为 iFlow CLI 的命令本身就是可被框架调用的,我可以让另一个智能体调用article2md命令来获取网页内容,再基于内容做问答或摘要。这样这个工具就从单机脚本变成了信息管线的一环,价值会更大。
结语:一点个人经验
这个工具的完整实现并不复杂,真正花时间的不是代码,而是那些"看似不需要处理、实际非处理不可"的边界情况。比如编码识别、长文本切割、术语稳定、文件重名、限流退避,这些细节单独拎出来都不起眼,但合在一起才决定了工具能否在日常中长期使用。
我在实际使用中最深的体会是:工具类的项目,最好优先解决自己真实遇到的痛点,而不是一上来追求功能大而全。第一版我只做了"下载正文并翻译",用了一周之后才逐步加了缓存、批处理和术语表。每加一个功能,都是因为某个真实操作刺痛了我,而不是为了炫技。
如果你也想基于 iFlow CLI 做自己的命令,建议从一个尽量小、尽量单功能的需求开始,跑通之后再慢慢加东西。自定义 Command 的养成,本质上是一个不断收敛自己需求的过程。