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