HTML 转 Markdown 接口技术解析:异步接入流程、参数设计与轮询实践
1. 背景与适用场景
在内容迁移、文档整理与数据采集等工程中,经常需要把网页或富文本中的 HTML 片段转换成结构化的 Markdown 文本。典型场景包括:把 CMS 导出的文章正文清洗为 Markdown 入库、将第三方页面内容转成可编辑文档、在静态站点生成流水线中把抓取结果沉淀为 .md 文件、以及为检索增强(RAG)准备干净的语料。
这类转换的难点在于 HTML 标签层级深、嵌套复杂,手写解析器难以覆盖表格、代码块、列表、图片与链接等元素。本文以阿里云云市场的一个 HTML 转 Markdown 接口为例,讲透「提交任务 → 轮询结果 → 取回 Markdown」的异步接入方式,以及多语言调用、错误排查与工程化落地的通用做法。文中的接入思路可迁移到任意采用 APPCODE 鉴权、异步任务模型的网关接口。

2. 接口概览
该接口以 API 形式交付,网关主机固定,所有操作均为 POST,返回 JSON。
| 项目 | 说明 |
|---|---|
| 网关主机 | https://htmlmark.market.alicloudapi.com |
| 请求方式 | POST |
| 返回格式 | JSON |
| 鉴权方式 | 简单身份认证:Authorization: APPCODE <appcode>;亦支持 AppKey & AppSecret 签名认证 |
| 操作集合 | 提交转换任务 /html2md、查询任务结果 /task_detail、查询历史任务 /task_history |
鉴权头示例:
Authorization: APPCODE 你的APPCODE。下文所有示例均使用 APPCODE 方式,密钥通过环境变量注入,不写死在代码里。
异步模型说明:转换服务采用「先提交、后查询」的两段式设计。/html2md 接收 HTML 源码并返回一个 task_id;随后用 task_id 调用 /task_detail 获取转换后的 Markdown 文本与任务状态。这种模型避免了大文档转换时的长连接超时。
3. 请求参数
3.1 提交转换任务 POST /html2md
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| html | string | 是 | 待转换的 HTML 源码 |
| ret_type | string | 否 | 返回格式:json(默认值)或 markdown |
请求体为表单格式(application/x-www-form-urlencoded),无 Header 与 Query 参数。
3.2 查询任务结果 POST /task_detail
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| task_id | string | 是 | 提交任务时返回的任务 ID |
3.3 查询历史任务 POST /task_history
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | string | 否 | 查询页码,用于分页拉取历史任务列表 |
以上三个操作的入参均为字符串类型;
html与task_id为必填项,其余为可选。

4. 返回结构
4.1 提交任务返回
{
"remark": "请求成功",
"task_id": "e6e723cf4",
"ret_code": 0
}
4.2 查询结果返回
{
"remark": "请求成功",
"md_text": "# 标题\n\n转换后的 Markdown 内容……",
"ret_code": 0,
"task_name": "测试页面",
"task_status": "success"
}
task_status 的取值与含义:
| 取值 | 含义 |
|---|---|
| not_exit | 任务不存在 |
| waiting | 任务执行中 |
| success | 任务执行成功 |
| fail | 任务执行失败 |
task_status同时出现在 HTTP 返回头中;当返回格式为 Markdown 时,也可从返回头直接获取状态。
4.3 历史任务返回
历史接口返回带网关包装结构:
{
"showapi_res_id": "",
"showapi_res_error": "",
"showapi_res_code": 0,
"showapi_res_body": {
"allNum": 5,
"contentlist": [
{
"task_id": "e6e723cf4",
"task_name": "测试任务",
"task_status": "success"
}
]
}
}

5. 错误码与排查
接口在业务层通过 ret_code 表达结果(0 表示成功,非 0 表示失败),网关层通过 HTTP 状态码表达鉴权与限流结果。常见排查路径:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| HTTP 401 | APPCODE 缺失或错误 | 检查 Authorization 头格式与密钥值 |
| HTTP 403 | 签名认证失败或权限不足 | 核对 AppKey/AppSecret 与签名算法 |
| HTTP 400 | 请求体缺失必填参数 | 确认 html / task_id 已正确传参 |
| HTTP 429 | 触发频控 | 降低并发、加入退避重试 |
| HTTP 500/502 | 网关或服务临时异常 | 幂等重试,记录请求标识便于回溯 |
ret_code != 0 |
业务处理失败 | 参考 remark 字段定位,必要时重新提交 |
调用次数仅在 HTTP 响应码为 200 时扣减,非 200 不消耗额度。
6. 频控与合规
调用按次数结算,详细标准以平台公示为准。工程上需注意以下边界:
- 频控:网关对单账号有并发与 QPS 约束,批量转换时应做客户端限流(令牌桶),避免触发 429。
- 数据安全:传入的
html可能包含用户生成的网页内容,调用前应做来源校验,避免把含敏感信息的页面外发到第三方服务。 - 最小必要:只传入需要转换的正文片段,剥离无关脚本、样式与追踪标签,既减少体积也降低信息泄露面。
- 结果处置:转换得到的 Markdown 落地后应做合规留存与脱敏,不长期缓存原始 HTML。

7. 多语言接入示例
以下示例统一使用网关主机常量与 APPCODE 鉴权。请在实际工程中从环境变量读取密钥。
7.1 curl
# 第一步:提交转换任务
curl -X POST 'https://htmlmark.market.alicloudapi.com/html2md' \
-H 'Authorization: APPCODE 你的APPCODE' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'html=<h1>标题</h1><p>正文内容</p>' \
-d 'ret_type=json'
# 第二步:用返回的 task_id 查询结果
curl -X POST 'https://htmlmark.market.alicloudapi.com/task_detail' \
-H 'Authorization: APPCODE 你的APPCODE' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'task_id=e6e723cf4'
7.2 Python
import os
import time
import requests
HOST = "https://htmlmark.market.alicloudapi.com"
APPCODE = os.environ["APPCODE"]
def submit(html: str) -> str:
r = requests.post(
f"{HOST}/html2md",
headers={
"Authorization": f"APPCODE {APPCODE}"},
data={
"html": html, "ret_type": "json"},
timeout=10,
)
r.raise_for_status()
return r.json()["task_id"]
def poll(task_id: str, tries: int = 10, interval: float = 1.0) -> str:
for _ in range(tries):
r = requests.post(
f"{HOST}/task_detail",
headers={
"Authorization": f"APPCODE {APPCODE}"},
data={
"task_id": task_id},
timeout=10,
)
r.raise_for_status()
body = r.json()
if body.get("task_status") == "success":
return body["md_text"]
if body.get("task_status") == "fail":
raise RuntimeError(body.get("remark", "task failed"))
time.sleep(interval)
raise TimeoutError("task still waiting")
if __name__ == "__main__":
tid = submit("<h1>标题</h1><p>正文</p>")
print(poll(tid))
7.3 Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;
public class Html2Md {
static final String HOST = "https://htmlmark.market.alicloudapi.com";
static final String APPCODE = System.getenv("APPCODE");
static String post(String path, Map<String, String> form) throws Exception {
String body = form.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.reduce((a, b) -> a + "&" + b).orElse("");
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(HOST + path))
.header("Authorization", "APPCODE " + APPCODE)
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> resp = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return resp.body();
}
public static void main(String[] args) throws Exception {
System.out.println(post("/html2md", Map.of("html", "<h1>标题</h1>")));
}
}
7.4 PHP
<?php
$host = "https://htmlmark.market.alicloudapi.com";
$appcode = getenv("APPCODE");
$ch = curl_init("$host/html2md");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: APPCODE $appcode", "Content-Type: application/x-www-form-urlencoded"],
CURLOPT_POSTFIELDS => http_build_query(["html" => "<h1>标题</h1><p>正文</p>"]),
CURLOPT_RETURNTRANSFER => true,
]);
$resp = curl_exec($ch);
curl_close($ch);
echo $resp;
7.5 Node.js
const HOST = "https://htmlmark.market.alicloudapi.com";
const APPCODE = process.env.APPCODE;
async function submit(html) {
const r = await fetch(`${
HOST}/html2md`, {
method: "POST",
headers: {
Authorization: `APPCODE ${
APPCODE}`, "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
html, ret_type: "json" }),
});
const j = await r.json();
return j.task_id;
}
submit("<h1>标题</h1>").then(console.log);

8. 接入工程实践
- 重试与退避:网络抖动或 429/5xx 时采用指数退避重试,单次请求设置超时(如 10s),避免线程长期阻塞。
- 轮询节奏:提交后先短间隔轮询,状态为
waiting时按退避拉长间隔;命中fail立即终止并告警。 - 幂等与去重:为每次转换携带稳定业务标识,转换结果按
task_id做本地缓存,避免重复提交相同内容。 - 结果缓存:HTML 内容不变时 Markdown 结果可复用。可依据内容哈希做 TTL 缓存(如 24h),降低重复调用。
- 密钥安全:APPCODE 从环境变量或密钥管理读取,禁止提交到代码仓库;服务间调用走内网时同样避免明文落盘。
- 批量限流:多文档批量转换时用令牌桶控制并发,配合退避平滑处理 429。

9. 技术 FAQ
Q1:提交后一直返回 waiting 怎么办?
先确认 task_id 正确,再按退避节奏轮询;若长时间不结束可能是源 HTML 过大或含异常结构,可简化标签后重试。
Q2:task_status 为 fail 如何排查?
读取 remark 字段的描述信息,常见为源 HTML 解析失败或内容为空,修正入参后重新提交。
Q3:查询时提示任务不存在?not_exit 表示 task_id 无效或已过期,核对提交阶段返回的任务 ID,确认未被截断。
Q4:返回头里的状态和响应体里的状态不一致?
以响应体 task_status 为准;返回头状态仅作快速判断,最终应解析 JSON 体。
Q5:频繁调用出现限流?
网关对单账号有频控,应在客户端做令牌桶限流与退避重试,避免突发并发。
Q6:ret_type 选 json 还是 markdown?
默认 json 会返回包装结构(含 md_text 与状态);markdown 直接返回文本,按业务消费方式选择。
10. 小结
本文围绕一个 HTML 转 Markdown 接口,梳理了异步任务模型的接入方式:通过 /html2md 提交 HTML 源码拿到 task_id,再用 /task_detail 轮询取回 md_text。文中给出的多语言示例、错误排查表与重试/缓存/限流等实践,同样适用于采用 APPCODE 鉴权与异步任务模型的其它网关接口。落地时只需把密钥管理、频控与结果缓存这三件事做扎实,即可在批量内容处理场景中稳定运行。