#!/usr/bin/env python3 """ Export DeepSeek shared conversation to Markdown. Usage (just run it — venv is created automatically): python deepseek_export.py [-o output.md] [--json] [--txt] [--token TOKEN] Examples: python deepseek_export.py https://chat.deepseek.com/share/nvy7v2ps1r6e2wyoyj python deepseek_export.py nvy7v2ps1r6e2wyoyj -o my_chat.md python deepseek_export.py nvy7v2ps1r6e2wyoyj --json -o chat.json python deepseek_export.py nvy7v2ps1r6e2wyoyj --txt First run will create .venv/ and install requests via uv. """ # ── auto-venv bootstrap ───────────────────────────────────────────────────── import os import subprocess import sys from pathlib import Path _SCRIPT_DIR = Path(__file__).resolve().parent _VENV_DIR = _SCRIPT_DIR / ".venv" _VENV_PYTHON = _VENV_DIR / ("Scripts/python.exe" if os.name == "nt" else "bin/python") def _ensure_venv(): """Create venv + install deps if missing, then re-exec inside it.""" if sys.prefix != sys.base_prefix: return # already inside a venv if not _VENV_PYTHON.exists(): print("[boot] Creating .venv …", flush=True) subprocess.check_call(["uv", "venv", str(_VENV_DIR)], cwd=_SCRIPT_DIR) print("[boot] Installing dependencies …", flush=True) subprocess.check_call( ["uv", "pip", "install", "--python", str(_VENV_PYTHON), "requests"], cwd=_SCRIPT_DIR, ) os.execv(str(_VENV_PYTHON), [str(_VENV_PYTHON), *sys.argv]) _ensure_venv() # ── end bootstrap ──────────────────────────────────────────────────────────── import argparse import json import re from datetime import datetime, timezone import requests # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- SHARE_URL_RE = re.compile( r"(?:https?://chat\.deepseek\.com)?/share/(?P[A-Za-z0-9_-]+)" ) def extract_share_id(raw: str) -> str: m = SHARE_URL_RE.search(raw) if m: return m.group("id") if re.fullmatch(r"[A-Za-z0-9_-]{6,}", raw): return raw raise ValueError(f"Cannot extract share id from: {raw}") HEADERS = { "accept": "application/json, text/plain, */*", "accept-language": "en-US,en;q=0.9", "referer": "https://chat.deepseek.com/", "user-agent": ( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36" ), } # --------------------------------------------------------------------------- # Strategy 1 — plain requests (works when WAF is not aggressive) # --------------------------------------------------------------------------- def fetch_via_requests(share_id: str, token: str | None = None) -> dict: s = requests.Session() s.headers.update(HEADERS) s.get("https://chat.deepseek.com/", timeout=15) if token: s.headers["authorization"] = f"Bearer {token}" url = f"https://chat.deepseek.com/api/v0/share/content?share_id={share_id}" r = s.get(url, timeout=15) r.raise_for_status() data = r.json() if data.get("code") != 0: raise RuntimeError(f"API error: {data.get('msg', data)}") return data # --------------------------------------------------------------------------- # Strategy 2 — Playwright headless browser (bypasses WAF reliably) # --------------------------------------------------------------------------- def fetch_via_playwright(share_id: str, token: str | None = None) -> dict: try: from playwright.sync_api import sync_playwright except ImportError: raise RuntimeError( "playwright not installed. Run:\n" " uv pip install playwright && uv run playwright install chromium" ) captured = {} def on_response(response): if "/api/v0/share/content" in response.url: try: captured["data"] = response.json() except Exception: pass with sync_playwright() as p: browser = p.chromium.launch(headless=True) ctx = browser.new_context( user_agent=HEADERS["user-agent"], viewport={"width": 1280, "height": 800}, ) page = ctx.new_page() page.on("response", on_response) if token: ctx.add_cookies([{ "name": "ds_session_id", "value": token, "domain": "chat.deepseek.com", "path": "/", }]) page.goto( f"https://chat.deepseek.com/share/{share_id}", wait_until="networkidle", timeout=30000, ) page.wait_for_timeout(3000) browser.close() if not captured.get("data"): raise RuntimeError("Playwright could not capture the share API response") data = captured["data"] if data.get("code") != 0: raise RuntimeError(f"API error: {data.get('msg', data)}") return data # --------------------------------------------------------------------------- # Main fetch (tries requests first, falls back to playwright) # --------------------------------------------------------------------------- def fetch_share(share_id: str, token: str | None = None) -> dict: try: print("[1/2] Trying direct API…", end=" ", flush=True) data = fetch_via_requests(share_id, token) print("OK") return data except Exception as e: print(f"failed ({e})") print("[2/2] Falling back to Playwright…", end=" ", flush=True) try: data = fetch_via_playwright(share_id, token) print("OK") return data except Exception as e: print(f"failed ({e})") raise RuntimeError( "Both strategies failed. If WAF is blocking you, install Playwright:\n" " uv pip install playwright && uv run playwright install chromium\n" "Or pass your token with --token." ) # --------------------------------------------------------------------------- # Formatting # --------------------------------------------------------------------------- def _ts(ts) -> str: if not ts: return "" try: return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") except Exception: return str(ts) def to_markdown(data: dict) -> str: biz = data.get("data", {}).get("biz_data", {}) messages = biz.get("messages", []) or biz.get("chat_messages", []) title = biz.get("title") or "DeepSeek Conversation" model = biz.get("model_type") or biz.get("model") or "" parts = [f"# {title}\n"] if model: parts.append(f"**Model:** {model}") parts.append("") if not messages: parts.append("_No messages found._\n") return "\n".join(parts) for msg in messages: role = (msg.get("role") or "").upper() content = msg.get("content") or msg.get("message") or "" if not content and not role: continue thinking = msg.get("thinking_content") or msg.get("reasoning_content") or "" elapsed = msg.get("thinking_elapsed_secs") search = msg.get("search_results") or [] if role == "USER": header = "You" elif role in ("ASSISTANT", "AI"): header = "DeepSeek" elif role == "SYSTEM": header = "System" else: header = role.title() parts.append(f"## {header}\n") if thinking: label = f"Thinking ({elapsed}s)" if elapsed else "Thinking" parts.append(f"
{label}\n\n{thinking}\n\n
\n") if search: refs = [] for item in search: t = item.get("title") or "" u = item.get("url") or "" if u: refs.append(f"- [{t}]({u})" if t else f"- {u}") if refs: parts.append("**Sources:**\n" + "\n".join(refs) + "\n") if content: parts.append(f"{content}\n") parts.append("---\n") return "\n".join(parts) def to_plain_text(data: dict) -> str: biz = data.get("data", {}).get("biz_data", {}) messages = biz.get("messages", []) or biz.get("chat_messages", []) title = biz.get("title") or "DeepSeek Conversation" parts = [f"=== {title} ===\n"] for msg in messages: role = (msg.get("role") or "").upper() content = msg.get("content") or msg.get("message") or "" thinking = msg.get("thinking_content") or msg.get("reasoning_content") or "" if role == "USER": parts.append(f"[User]\n{content}\n") elif role in ("ASSISTANT", "AI"): if thinking: parts.append(f"[Thinking]\n{thinking}\n") parts.append(f"[DeepSeek]\n{content}\n") else: parts.append(f"[{role}]\n{content}\n") return "\n".join(parts) # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def main(): ap = argparse.ArgumentParser(description="Export a DeepSeek shared conversation") ap.add_argument("share", help="Share URL or share ID") ap.add_argument("-o", "--output", help="Output file path (default: auto-named)") ap.add_argument("--json", action="store_true", help="Dump raw JSON instead of Markdown") ap.add_argument("--txt", action="store_true", help="Dump plain text") ap.add_argument("--token", help="Bearer token for auth (optional, needed for private shares)") args = ap.parse_args() share_id = extract_share_id(args.share) print(f"Share ID: {share_id}") data = fetch_share(share_id, args.token) if args.output: out = Path(args.output) elif args.json: out = Path(f"export_{share_id}.json") elif args.txt: out = Path(f"export_{share_id}.txt") else: out = Path(f"export_{share_id}.md") if args.json: content = json.dumps(data, indent=2, ensure_ascii=False) elif args.txt: content = to_plain_text(data) else: content = to_markdown(data) out.write_text(content, encoding="utf-8") print(f"Saved to {out} ({len(content)} bytes)") if __name__ == "__main__": main()