Last active 8 hours ago

KarelWintersky revised this gist 8 hours ago. Go to revision

1 file changed, 319 insertions

_deepseek_export.py(file created)

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