Dernière activité 9 hours ago

Export DeepSeek shared conversation to Markdown

KarelWintersky a révisé ce gist 9 hours ago. Aller à la révision

1 file changed, 375 insertions

_deepseek_export.py(fichier créé)

@@ -0,0 +1,375 @@
1 + #!/usr/bin/env python3
2 + """
3 + Export DeepSeek shared conversation to Markdown.
4 +
5 + Requires uv — the fast Python package installer.
6 + Install: curl -LsSf https://astral.sh/uv/install.sh | sh
7 +
8 + Usage (just run it — venv is created automatically):
9 + python deepseek_export.py <share_url_or_id> [-o output.md] [--json] [--txt] [--token TOKEN]
10 +
11 + Examples:
12 + python deepseek_export.py https://chat.deepseek.com/share/nvy7v2ps1r6e2wyoyj
13 + python deepseek_export.py nvy7v2ps1r6e2wyoyj -o my_chat.md
14 + python deepseek_export.py nvy7v2ps1r6e2wyoyj --json -o chat.json
15 + python deepseek_export.py nvy7v2ps1r6e2wyoyj --txt
16 +
17 + First run will create .venv/ and install requests via uv.
18 + """
19 +
20 + # ── auto-venv bootstrap ─────────────────────────────────────────────────────
21 + import os
22 + import shutil
23 + import subprocess
24 + import sys
25 + from pathlib import Path
26 +
27 + _SCRIPT_DIR = Path(__file__).resolve().parent
28 + _VENV_DIR = _SCRIPT_DIR / ".venv"
29 + _VENV_PYTHON = _VENV_DIR / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
30 +
31 + _USAGE_TEXT = (
32 + "Export DeepSeek shared conversations to Markdown/JSON/text.\n"
33 + "Run with -h for full help."
34 + )
35 +
36 +
37 + def _find_uv() -> str | None:
38 + return shutil.which("uv")
39 +
40 +
41 + def _install_uv() -> bool:
42 + print("[boot] uv is not installed.", flush=True)
43 + answer = input("Install uv now? [Y/n] ").strip().lower()
44 + if answer and answer != "y":
45 + print("Aborted. Install uv manually: curl -LsSf https://astral.sh/uv/install.sh | sh")
46 + return False
47 + print("[boot] Installing uv …", flush=True)
48 + try:
49 + subprocess.check_call(
50 + ["curl", "-LsSf", "https://astral.sh/uv/install.sh", "|", "sh"],
51 + cwd=_SCRIPT_DIR,
52 + )
53 + except Exception:
54 + # curl ... | sh doesn't work via subprocess.check_call, use shell
55 + subprocess.check_call("curl -LsSf https://astral.sh/uv/install.sh | sh", shell=True)
56 + if not _find_uv():
57 + print("[boot] uv installation failed. Install manually: curl -LsSf https://astral.sh/uv/install.sh | sh")
58 + return False
59 + return True
60 +
61 +
62 + def _ensure_venv():
63 + """Create venv + install deps if missing, then re-exec inside it."""
64 + if sys.prefix != sys.base_prefix:
65 + return # already inside a venv
66 +
67 + print(_USAGE_TEXT, flush=True)
68 + print(flush=True)
69 +
70 + uv = _find_uv()
71 + if not uv:
72 + if not _install_uv():
73 + sys.exit(1)
74 + uv = _find_uv()
75 + if not uv:
76 + sys.exit(1)
77 +
78 + if not _VENV_PYTHON.exists():
79 + print("[boot] Creating .venv …", flush=True)
80 + subprocess.check_call([uv, "venv", str(_VENV_DIR)], cwd=_SCRIPT_DIR)
81 + print("[boot] Installing dependencies …", flush=True)
82 + subprocess.check_call(
83 + [uv, "pip", "install", "--python", str(_VENV_PYTHON), "requests"],
84 + cwd=_SCRIPT_DIR,
85 + )
86 +
87 + os.execv(str(_VENV_PYTHON), [str(_VENV_PYTHON), *sys.argv])
88 +
89 +
90 + _ensure_venv()
91 + # ── end bootstrap ────────────────────────────────────────────────────────────
92 +
93 + import argparse
94 + import json
95 + import re
96 + from datetime import datetime, timezone
97 +
98 + import requests
99 +
100 + # ---------------------------------------------------------------------------
101 + # Helpers
102 + # ---------------------------------------------------------------------------
103 +
104 + SHARE_URL_RE = re.compile(
105 + r"(?:https?://chat\.deepseek\.com)?/share/(?P<id>[A-Za-z0-9_-]+)"
106 + )
107 +
108 +
109 + def extract_share_id(raw: str) -> str:
110 + m = SHARE_URL_RE.search(raw)
111 + if m:
112 + return m.group("id")
113 + if re.fullmatch(r"[A-Za-z0-9_-]{6,}", raw):
114 + return raw
115 + raise ValueError(f"Cannot extract share id from: {raw}")
116 +
117 +
118 + HEADERS = {
119 + "accept": "application/json, text/plain, */*",
120 + "accept-language": "en-US,en;q=0.9",
121 + "referer": "https://chat.deepseek.com/",
122 + "user-agent": (
123 + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
124 + "(KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
125 + ),
126 + }
127 +
128 +
129 + # ---------------------------------------------------------------------------
130 + # Strategy 1 — plain requests (works when WAF is not aggressive)
131 + # ---------------------------------------------------------------------------
132 +
133 + def fetch_via_requests(share_id: str, token: str | None = None) -> dict:
134 + s = requests.Session()
135 + s.headers.update(HEADERS)
136 + s.get("https://chat.deepseek.com/", timeout=15)
137 +
138 + if token:
139 + s.headers["authorization"] = f"Bearer {token}"
140 +
141 + url = f"https://chat.deepseek.com/api/v0/share/content?share_id={share_id}"
142 + r = s.get(url, timeout=15)
143 + r.raise_for_status()
144 + data = r.json()
145 +
146 + if data.get("code") != 0:
147 + raise RuntimeError(f"API error: {data.get('msg', data)}")
148 + return data
149 +
150 +
151 + # ---------------------------------------------------------------------------
152 + # Strategy 2 — Playwright headless browser (bypasses WAF reliably)
153 + # ---------------------------------------------------------------------------
154 +
155 + def fetch_via_playwright(share_id: str, token: str | None = None) -> dict:
156 + try:
157 + from playwright.sync_api import sync_playwright
158 + except ImportError:
159 + raise RuntimeError(
160 + "playwright not installed. Run:\n"
161 + " uv pip install playwright && uv run playwright install chromium"
162 + )
163 +
164 + captured = {}
165 +
166 + def on_response(response):
167 + if "/api/v0/share/content" in response.url:
168 + try:
169 + captured["data"] = response.json()
170 + except Exception:
171 + pass
172 +
173 + with sync_playwright() as p:
174 + browser = p.chromium.launch(headless=True)
175 + ctx = browser.new_context(
176 + user_agent=HEADERS["user-agent"],
177 + viewport={"width": 1280, "height": 800},
178 + )
179 + page = ctx.new_page()
180 + page.on("response", on_response)
181 +
182 + if token:
183 + ctx.add_cookies([{
184 + "name": "ds_session_id",
185 + "value": token,
186 + "domain": "chat.deepseek.com",
187 + "path": "/",
188 + }])
189 +
190 + page.goto(
191 + f"https://chat.deepseek.com/share/{share_id}",
192 + wait_until="networkidle",
193 + timeout=30000,
194 + )
195 + page.wait_for_timeout(3000)
196 + browser.close()
197 +
198 + if not captured.get("data"):
199 + raise RuntimeError("Playwright could not capture the share API response")
200 + data = captured["data"]
201 + if data.get("code") != 0:
202 + raise RuntimeError(f"API error: {data.get('msg', data)}")
203 + return data
204 +
205 +
206 + # ---------------------------------------------------------------------------
207 + # Main fetch (tries requests first, falls back to playwright)
208 + # ---------------------------------------------------------------------------
209 +
210 + def fetch_share(share_id: str, token: str | None = None) -> dict:
211 + try:
212 + print("[1/2] Trying direct API…", end=" ", flush=True)
213 + data = fetch_via_requests(share_id, token)
214 + print("OK")
215 + return data
216 + except Exception as e:
217 + print(f"failed ({e})")
218 +
219 + print("[2/2] Falling back to Playwright…", end=" ", flush=True)
220 + try:
221 + data = fetch_via_playwright(share_id, token)
222 + print("OK")
223 + return data
224 + except Exception as e:
225 + print(f"failed ({e})")
226 + raise RuntimeError(
227 + "Both strategies failed. If WAF is blocking you, install Playwright:\n"
228 + " uv pip install playwright && uv run playwright install chromium\n"
229 + "Or pass your token with --token."
230 + )
231 +
232 +
233 + # ---------------------------------------------------------------------------
234 + # Formatting
235 + # ---------------------------------------------------------------------------
236 +
237 + def _ts(ts) -> str:
238 + if not ts:
239 + return ""
240 + try:
241 + return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
242 + except Exception:
243 + return str(ts)
244 +
245 +
246 + def to_markdown(data: dict) -> str:
247 + biz = data.get("data", {}).get("biz_data", {})
248 + messages = biz.get("messages", []) or biz.get("chat_messages", [])
249 +
250 + title = biz.get("title") or "DeepSeek Conversation"
251 + model = biz.get("model_type") or biz.get("model") or ""
252 +
253 + parts = [f"# {title}\n"]
254 + if model:
255 + parts.append(f"**Model:** {model}")
256 + parts.append("")
257 +
258 + if not messages:
259 + parts.append("_No messages found._\n")
260 + return "\n".join(parts)
261 +
262 + for msg in messages:
263 + role = (msg.get("role") or "").upper()
264 + content = msg.get("content") or msg.get("message") or ""
265 + if not content and not role:
266 + continue
267 +
268 + thinking = msg.get("thinking_content") or msg.get("reasoning_content") or ""
269 + elapsed = msg.get("thinking_elapsed_secs")
270 + search = msg.get("search_results") or []
271 +
272 + if role == "USER":
273 + header = "You"
274 + elif role in ("ASSISTANT", "AI"):
275 + header = "DeepSeek"
276 + elif role == "SYSTEM":
277 + header = "System"
278 + else:
279 + header = role.title()
280 +
281 + parts.append(f"## {header}\n")
282 +
283 + if thinking:
284 + label = f"Thinking ({elapsed}s)" if elapsed else "Thinking"
285 + parts.append(f"<details><summary>{label}</summary>\n\n{thinking}\n\n</details>\n")
286 +
287 + if search:
288 + refs = []
289 + for item in search:
290 + t = item.get("title") or ""
291 + u = item.get("url") or ""
292 + if u:
293 + refs.append(f"- [{t}]({u})" if t else f"- {u}")
294 + if refs:
295 + parts.append("**Sources:**\n" + "\n".join(refs) + "\n")
296 +
297 + if content:
298 + parts.append(f"{content}\n")
299 + parts.append("---\n")
300 +
301 + return "\n".join(parts)
302 +
303 +
304 + def to_plain_text(data: dict) -> str:
305 + biz = data.get("data", {}).get("biz_data", {})
306 + messages = biz.get("messages", []) or biz.get("chat_messages", [])
307 + title = biz.get("title") or "DeepSeek Conversation"
308 +
309 + parts = [f"=== {title} ===\n"]
310 + for msg in messages:
311 + role = (msg.get("role") or "").upper()
312 + content = msg.get("content") or msg.get("message") or ""
313 + thinking = msg.get("thinking_content") or msg.get("reasoning_content") or ""
314 + if role == "USER":
315 + parts.append(f"[User]\n{content}\n")
316 + elif role in ("ASSISTANT", "AI"):
317 + if thinking:
318 + parts.append(f"[Thinking]\n{thinking}\n")
319 + parts.append(f"[DeepSeek]\n{content}\n")
320 + else:
321 + parts.append(f"[{role}]\n{content}\n")
322 + return "\n".join(parts)
323 +
324 +
325 + # ---------------------------------------------------------------------------
326 + # CLI
327 + # ---------------------------------------------------------------------------
328 +
329 + class _ArgumentParser(argparse.ArgumentParser):
330 + def error(self, message):
331 + self.print_usage(sys.stderr)
332 + sys.stderr.write("\n")
333 + sys.stderr.write(f"{self.prog}: error: {message}\n")
334 + sys.exit(2)
335 +
336 +
337 + def main():
338 + ap = _ArgumentParser(
339 + description="Export a DeepSeek shared conversation",
340 + epilog="Example: %(prog)s https://chat.deepseek.com/share/nvy7v2ps1r6e2wyoyj",
341 + )
342 + ap.add_argument("link", help="Share URL or share ID")
343 + ap.add_argument("-o", "--output", help="Output file path (default: auto-named)")
344 + ap.add_argument("--json", action="store_true", help="Dump raw JSON instead of Markdown")
345 + ap.add_argument("--txt", action="store_true", help="Dump plain text")
346 + ap.add_argument("--token", help="Bearer token for auth (optional, needed for private shares)")
347 + args = ap.parse_args()
348 +
349 + share_id = extract_share_id(args.link)
350 + print(f"Share ID: {share_id}")
351 +
352 + data = fetch_share(share_id, args.token)
353 +
354 + if args.output:
355 + out = Path(args.output)
356 + elif args.json:
357 + out = Path(f"export_{share_id}.json")
358 + elif args.txt:
359 + out = Path(f"export_{share_id}.txt")
360 + else:
361 + out = Path(f"export_{share_id}.md")
362 +
363 + if args.json:
364 + content = json.dumps(data, indent=2, ensure_ascii=False)
365 + elif args.txt:
366 + content = to_plain_text(data)
367 + else:
368 + content = to_markdown(data)
369 +
370 + out.write_text(content, encoding="utf-8")
371 + print(f"Saved to {out} ({len(content)} bytes)")
372 +
373 +
374 + if __name__ == "__main__":
375 + main()
Plus récent Plus ancien