AI 工具

用 DeepL Write API 构建文本改写客户端

DeepL Write 的 /v2/write/rephrase 会在同语种内纠错并改写。这篇实现官方请求形状的客户端,并附 mock,便于无 Pro 密钥时练手。

BINBHO · 图文教程

DeepL Write 的 /v2/write/rephrase 会在同语种内纠错并改写。这篇实现官方请求形状的客户端,并附 mock,便于无 Pro 密钥时练手。

先看清成品

  • mock 下 business 风格改写成功。
  • 无效密钥打官方 free 主机返回 403。
  • 1 项 pytest 通过。

合规提醒:DeepL Write API 目前需要 API Pro(以官方说明为准),Free 计划可能不可用。密钥用 Authorization: DeepL-Auth-Key ...,勿提交到公开仓库。Write 是同语种润色,不是翻译;不要用它处理你无权处理的保密文本(注意 Pro 数据保护条款)。

运行说明:无 DeepL Pro 密钥。A/B/C 均用本地 mock 验证;对 api-free 用无效密钥实测 Write 返回 HTTP 403 鉴权失败。2026-10-23(Asia/Shanghai)pytest 通过。正式调用基址一般为 https://api.deepl.com 。

做完之后你会得到什么?

成果 说明
deepl_write_client.py rephrase 客户端
mock_deepl_write.py 本地假服务
test_98a.py 测试

需要准备哪些环境?

项目 说明
Python 3.10+
密钥 DEEPL_API_KEY;Write 需 Pro

关键事实

事实 来源
POST /v2/write/rephrase,text 为数组;可选 writing_style 或 tone(互斥) 官方 Improve text / Write Quickstart
鉴权:Authorization: DeepL-Auth-Key <key> 官方 Auth
Write 尚未包含在 API Free(以官方 Quickstart 为准) 官方 Write Quickstart

文件结构

98-deepl-write-A-src/
├── deepl_write_client.py
├── mock_deepl_write.py
└── test_98a.py

第 1 步:客户端

#!/usr/bin/env python3
"""DeepL Write client for /v2/write/rephrase (Pro API; Free plan may not include Write)."""
from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.error
import urllib.request


def rephrase(
    *,
    base: str,
    api_key: str,
    texts: list[str],
    target_lang: str = "en-US",
    writing_style: str | None = None,
    tone: str | None = None,
) -> dict:
    if writing_style and tone:
        raise ValueError("writing_style and tone are mutually exclusive")
    payload: dict = {"text": texts, "target_lang": target_lang}
    if writing_style:
        payload["writing_style"] = writing_style
    if tone:
        payload["tone"] = tone
    req = urllib.request.Request(
        f"{base.rstrip('/')}/v2/write/rephrase",
        data=json.dumps(payload).encode(),
        headers={
            "Authorization": f"DeepL-Auth-Key {api_key}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            return json.loads(resp.read().decode())
    except urllib.error.HTTPError as e:
        raise SystemExit(f"HTTP {e.code}: {e.read().decode(errors='replace')}") from e


def main(argv=None) -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("text", nargs="?", default="I could relly use sum help with edits on thiss text !")
    ap.add_argument("--base", default=os.environ.get("DEEPL_BASE_URL", "https://api.deepl.com"))
    ap.add_argument("--key", default=os.environ.get("DEEPL_API_KEY", ""))
    ap.add_argument("--lang", default="en-US")
    ap.add_argument("--style", default=None)
    ap.add_argument("--tone", default=None)
    args = ap.parse_args(argv)
    if not args.key:
        print("设置 DEEPL_API_KEY(Write 需 API Pro,以官网为准)", file=sys.stderr)
        return 2
    r = rephrase(
        base=args.base,
        api_key=args.key,
        texts=[args.text],
        target_lang=args.lang,
        writing_style=args.style,
        tone=args.tone,
    )
    for i, imp in enumerate(r.get("improvements") or []):
        print(f"[{i}] {imp.get('text')}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

第 2 步:Mock

#!/usr/bin/env python3
"""Mock DeepL Write /v2/write/rephrase and /v2/write/correct."""
from __future__ import annotations

import json
import re
from http.server import BaseHTTPRequestHandler, HTTPServer

API_KEY = "DeepL-Auth-Key deepl-test-key"
STYLES = {"academic", "business", "casual", "default", "simple",
          "prefer_academic", "prefer_business", "prefer_casual", "prefer_simple"}
TONES = {"confident", "default", "diplomatic", "enthusiastic", "friendly",
         "prefer_confident", "prefer_diplomatic", "prefer_enthusiastic", "prefer_friendly"}


def improve(text: str, mode: str, style: str | None, tone: str | None) -> str:
    t = text
    # trivial spelling fixes for demo
    repl = {
        "relly": "really",
        "sum help": "some help",
        "thiss": "this",
        "imprve": "improve",
        "a example": "an example",
    }
    for a, b in repl.items():
        t = re.sub(a, b, t, flags=re.I)
    if mode == "rephrase":
        if style == "business" or (style or "").endswith("business"):
            t = t.replace("could really use some help", "would appreciate assistance")
        if tone and "diplomatic" in tone:
            t = "It seems that " + t[0].lower() + t[1:] if t else t
        if not any(k in t.lower() for k in ("really", "appreciate", "seems")):
            t = t.rstrip("!") + " (clarified)."
    return t


class H(BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def _auth_ok(self) -> bool:
        return self.headers.get("Authorization") == API_KEY

    def _send(self, code: int, obj: dict):
        b = json.dumps(obj).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(b)))
        self.end_headers()
        self.wfile.write(b)

    def do_POST(self):
        if not self._auth_ok():
            self._send(403, {"message": "Wrong API key"})
            return
        n = int(self.headers.get("Content-Length", 0))
        raw = self.rfile.read(n)
        if len(raw) > 10 * 1024:
            self._send(413, {"message": "Request body too large (demo 10KiB)"})
            return
        body = json.loads(raw or b"{}")
        texts = body.get("text") or []
        if not isinstance(texts, list) or not texts:
            self._send(400, {"message": "text must be a non-empty array"})
            return
        style = body.get("writing_style")
        tone = body.get("tone")
        if style and tone:
            self._send(400, {"message": "writing_style and tone are mutually exclusive"})
            return
        if style and style not in STYLES:
            self._send(400, {"message": f"invalid writing_style: {style}"})
            return
        if tone and tone not in TONES:
            self._send(400, {"message": f"invalid tone: {tone}"})
            return
        if self.path == "/v2/write/rephrase":
            mode = "rephrase"
        elif self.path == "/v2/write/correct":
            mode = "correct"
            if style or tone:
                self._send(400, {"message": "correct does not accept writing_style or tone"})
                return
        else:
            self.send_error(404)
            return
        target = body.get("target_lang") or "en-US"
        improvements = [
            {
                "text": improve(t, mode, style, tone),
                "detected_source_language": "en",
                "target_language": target,
            }
            for t in texts
        ]
        self._send(200, {"improvements": improvements})


def main():
    import argparse

    ap = argparse.ArgumentParser()
    ap.add_argument("--port", type=int, default=8980)
    args = ap.parse_args()
    HTTPServer(("127.0.0.1", args.port), H).serve_forever()


if __name__ == "__main__":
    main()

第 3 步:测试

import subprocess
import sys
import time
from pathlib import Path

HERE = Path(__file__).resolve().parent


def test_rephrase_mock():
    port = 18980
    proc = subprocess.Popen(
        [sys.executable, str(HERE / "mock_deepl_write.py"), "--port", str(port)],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    try:
        time.sleep(0.3)
        r = subprocess.run(
            [
                sys.executable,
                str(HERE / "deepl_write_client.py"),
                "I could relly use sum help with edits on thiss text !",
                "--base",
                f"http://127.0.0.1:{port}",
                "--key",
                "deepl-test-key",
                "--style",
                "business",
            ],
            capture_output=True,
            text=True,
            check=True,
        )
        assert "really" in r.stdout.lower() or "appreciate" in r.stdout.lower()
    finally:
        proc.terminate()
        proc.wait(timeout=5)

第 4 步:运行

.                                                                        [100%]
1 passed in 0.36s
[0] I would appreciate assistance with edits on this text !
HTTP 403
{"message":"Authentication failed, provided API key is invalid. You can find more info in our docs: https://developers.deepl.com/docs/getting-started/auth"}

怎么检查结果?

  • 输出 improvements 文本。
  • style 与 tone 不要同时传。

调试示例

Free 密钥调 Write。 官方写明 Write 需 API Pro;用 free 主机 + 无效/不匹配密钥会看到鉴权错误。不要把 403 误当成「端点不存在」。

常见问题

现象 原因 处理
400 style 语言不支持 改用 prefer_ 前缀或换 target_lang
中文 支持情况 查 GET /v3/languages?resource=write
和翻译搞混 不同产品 Write 不改语种

模板规则

  • Pro 与定价以官网为准。
  • 保密文本遵守数据处理条款。
  • 互斥参数不要同传。

项目资源

用途 地址
Write Quickstart(官方) https://developers.deepl.com/docs/translate/write-quickstart
Improve text(官方) https://developers.deepl.com/api-reference/improve-text/request-text-improvement
Auth(官方) https://developers.deepl.com/docs/getting-started/auth
DeepL Write 网页 https://www.deepl.com/write

先 mock 跑通请求,再上 Pro 密钥。