| 1 | #!/bin/bash |
| 2 | # export_douyin_cookie.sh |
| 3 | # 导出已登录 Chrome 的抖音 cookie 到 douyin_{8位随机字符}.json |
| 4 | # 使用 curl 直接调用 Chrome DevTools HTTP API |
| 5 | |
| 6 | set -e |
| 7 | |
| 8 | # 解析参数 |
| 9 | USERNAME="" |
| 10 | while [ $# -gt 0 ]; do |
| 11 | case "$1" in |
| 12 | --account) |
| 13 | USERNAME="$2" |
| 14 | shift 2 |
| 15 | ;; |
| 16 | *) |
| 17 | echo "用法: $0 --account <用户名>" |
| 18 | exit 1 |
| 19 | ;; |
| 20 | esac |
| 21 | done |
| 22 | |
| 23 | if [ -z "$USERNAME" ]; then |
| 24 | echo "未指定用户名,将使用随机文件名" |
| 25 | fi |
| 26 | |
| 27 | DEBUG_PORT=9222 |
| 28 | |
| 29 | echo "==========================================" |
| 30 | echo " 抖音 Cookie 导出工具" |
| 31 | echo "==========================================" |
| 32 | echo "" |
| 33 | |
| 34 | # 依赖: curl, python3, websocket-client |
| 35 | |
| 36 | echo "检查 Chrome remote debugging..." |
| 37 | RESPONSE=$(curl -s "http://localhost:${DEBUG_PORT}/json" 2>/dev/null) || true |
| 38 | |
| 39 | if ! echo "$RESPONSE" | grep -q "webSocketDebuggerUrl"; then |
| 40 | echo "Chrome remote debugging 未运行" |
| 41 | echo "请先启动 Chrome: chromium --remote-debugging-port=9222 ..." |
| 42 | exit 1 |
| 43 | fi |
| 44 | |
| 45 | echo "Chrome remote debugging 已运行" |
| 46 | echo "" |
| 47 | echo "获取抖音 Cookie..." |
| 48 | |
| 49 | USERNAME="$USERNAME" python3 << 'PYEOF' |
| 50 | import json |
| 51 | import os |
| 52 | import uuid |
| 53 | import sys |
| 54 | |
| 55 | # Shell 把 USERNAME 作为环境变量传过来, Python 需要显式取 |
| 56 | USERNAME = os.environ.get('USERNAME', '') |
| 57 | |
| 58 | DEBUG_PORT = 9222 |
| 59 | if USERNAME: |
| 60 | COOKIE_FILE = f"cookies/douyin_{USERNAME}.json" |
| 61 | else: |
| 62 | COOKIE_FILE = f"cookies/douyin_{uuid.uuid4().hex[:8]}.json" |
| 63 | |
| 64 | def get_douyin_cookies(): |
| 65 | import urllib.request |
| 66 | import websocket |
| 67 | |
| 68 | # 获取页面列表 |
| 69 | url = f"http://localhost:{DEBUG_PORT}/json" |
| 70 | with urllib.request.urlopen(url, timeout=10) as response: |
| 71 | pages = json.loads(response.read().decode()) |
| 72 | |
| 73 | # 查找抖音创作者页面 |
| 74 | douyin_page = None |
| 75 | for page in pages: |
| 76 | page_url = page.get("url", "") |
| 77 | if "creator.douyin.com" in page_url: |
| 78 | douyin_page = page |
| 79 | break |
| 80 | |
| 81 | if not douyin_page: |
| 82 | print("未找到抖音创作者平台页面") |
| 83 | print("请先在 Chrome 中打开并登录 https://creator.douyin.com") |
| 84 | return False |
| 85 | |
| 86 | page_url = douyin_page["url"] |
| 87 | ws_url = douyin_page.get("webSocketDebuggerUrl", "") |
| 88 | |
| 89 | print(f"找到页面: {page_url}") |
| 90 | |
| 91 | # 连接到目标页面的 WebSocket |
| 92 | print(f"连接页面 WebSocket...") |
| 93 | ws = websocket.create_connection(ws_url, timeout=30) |
| 94 | |
| 95 | # 获取所有 Cookie |
| 96 | ws.send(json.dumps({"id": 1, "method": "Network.getAllCookies"})) |
| 97 | response = json.loads(ws.recv()) |
| 98 | |
| 99 | cookies = response.get("result", {}).get("cookies", []) |
| 100 | |
| 101 | # 过滤抖音相关的 cookie |
| 102 | douyin_cookies = [ |
| 103 | c for c in cookies |
| 104 | if "douyin.com" in c.get("domain", "") or ".douyin.com" in c.get("domain", "") |
| 105 | ] |
| 106 | |
| 107 | print(f"获取到 {len(douyin_cookies)} 个抖音 Cookie") |
| 108 | |
| 109 | if len(douyin_cookies) == 0: |
| 110 | print("未获取到任何抖音 Cookie,可能未登录") |
| 111 | return False |
| 112 | |
| 113 | # 获取 localStorage (origins) |
| 114 | print(f"获取 localStorage...") |
| 115 | local_storage_by_origin = {} |
| 116 | |
| 117 | # 获取页面 frame 树 |
| 118 | ws.send(json.dumps({"id": 2, "method": "Page.getResourceTree"})) |
| 119 | resp = json.loads(ws.recv()) |
| 120 | frames = resp.get("result", {}).get("frameTree", {}).get("childFrames", []) |
| 121 | all_frames = [resp.get("result", {}).get("frameTree", {})] + frames |
| 122 | |
| 123 | for frame in all_frames: |
| 124 | frame_url = frame.get("url", "") |
| 125 | frame_id = frame.get("id", "") |
| 126 | if "douyin.com" in frame_url or "bytedance.com" in frame_url: |
| 127 | # 执行 JavaScript 获取该 frame 的 localStorage |
| 128 | script = """ |
| 129 | (function() { |
| 130 | var result = []; |
| 131 | for (var i = 0; i < localStorage.length; i++) { |
| 132 | var key = localStorage.key(i); |
| 133 | result.push([key, localStorage.getItem(key)]); |
| 134 | } |
| 135 | return result; |
| 136 | })() |
| 137 | """ |
| 138 | ws.send(json.dumps({ |
| 139 | "id": 3, |
| 140 | "method": "Runtime.evaluate", |
| 141 | "params": {"expression": script, "contextId": frame.get("id")} |
| 142 | })) |
| 143 | resp = json.loads(ws.recv()) |
| 144 | result_eval = resp.get("result", {}) |
| 145 | if result_eval.get("result", {}).get("type") == "array": |
| 146 | items = result_eval.get("result", {}).get("value", []) |
| 147 | if items: |
| 148 | origin = frame_url.rsplit("/", 2)[0] + "//" + frame_url.split("/")[2] |
| 149 | local_storage_by_origin[origin] = items |
| 150 | |
| 151 | # 获取主文档的 localStorage |
| 152 | script_main = """ |
| 153 | (function() { |
| 154 | var result = []; |
| 155 | try { |
| 156 | for (var i = 0; i < localStorage.length; i++) { |
| 157 | var key = localStorage.key(i); |
| 158 | result.push([key, localStorage.getItem(key)]); |
| 159 | } |
| 160 | } catch(e) {} |
| 161 | return result; |
| 162 | })() |
| 163 | """ |
| 164 | ws.send(json.dumps({ |
| 165 | "id": 4, |
| 166 | "method": "Runtime.evaluate", |
| 167 | "params": {"expression": script_main} |
| 168 | })) |
| 169 | resp = json.loads(ws.recv()) |
| 170 | result_eval = resp.get("result", {}) |
| 171 | if result_eval.get("result", {}).get("type") == "array": |
| 172 | items = result_eval.get("result", {}).get("value", []) |
| 173 | if items: |
| 174 | origin = page_url.rsplit("/", 2)[0] + "//" + page_url.split("/")[2] |
| 175 | local_storage_by_origin[origin] = items |
| 176 | |
| 177 | ws.close() |
| 178 | |
| 179 | # 构建 JSON |
| 180 | result = { |
| 181 | "cookies": [], |
| 182 | "origins": [] |
| 183 | } |
| 184 | |
| 185 | for c in douyin_cookies: |
| 186 | result["cookies"].append({ |
| 187 | "name": c.get("name", ""), |
| 188 | "value": c.get("value", ""), |
| 189 | "domain": c.get("domain", ""), |
| 190 | "path": c.get("path", "/"), |
| 191 | "expires": c.get("expires", -1), |
| 192 | "httpOnly": c.get("httpOnly", False), |
| 193 | "secure": c.get("secure", True), |
| 194 | "sameSite": c.get("sameSite", "Lax") |
| 195 | }) |
| 196 | |
| 197 | # 添加 origins |
| 198 | for origin, items in local_storage_by_origin.items(): |
| 199 | origin_entry = { |
| 200 | "origin": origin, |
| 201 | "localStorage": [{"name": name, "value": value} for name, value in items] |
| 202 | } |
| 203 | result["origins"].append(origin_entry) |
| 204 | |
| 205 | # 保存 |
| 206 | os.makedirs("cookies", exist_ok=True) |
| 207 | with open(COOKIE_FILE, "w", encoding="utf-8") as f: |
| 208 | json.dump(result, f, indent=2, ensure_ascii=False) |
| 209 | |
| 210 | print(f"Cookie 已保存到: {COOKIE_FILE}") |
| 211 | print("") |
| 212 | |
| 213 | # 打印关键 Cookie |
| 214 | key_cookies = ["sessionid", "uid_tt", "ssid", "ttwid"] |
| 215 | print("关键 Cookie:") |
| 216 | for c in douyin_cookies: |
| 217 | if c.get("name") in key_cookies: |
| 218 | val = c.get("value", "") |
| 219 | if len(val) > 30: |
| 220 | val = val[:30] + "..." |
| 221 | print(f" {c.get('name')}: {val}") |
| 222 | |
| 223 | return True |
| 224 | |
| 225 | if __name__ == "__main__": |
| 226 | if not get_douyin_cookies(): |
| 227 | sys.exit(1) |
| 228 | PYEOF |
| 229 | |
| 230 | echo "" |
| 231 | echo "==========================================" |
| 232 | echo " 完成!文件已导出为$COOKIE_FILE" |
| 233 | echo "==========================================" |