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 密钥。