Binance Square 热度猎杀 作者:Your Name 版权所有 © 2026 Your Name 版本: v12.0 (CDP Live Session + Multi-Topic + Concurrent) --- 概述 从 Binance Square 热门话题排行榜获取话题列表,逐个进入话题页抓取最新帖子,提取社区交易信号、情绪方向和小币提及统计。 使用场景 :在 Polymarket 交易决策前,扫描 Binance Square 社区情绪,辅助判断短期市场方向。 --- 核心依赖 | 组件 | 说明 | |------|------| | Chrome 浏览器 | 已登录的默认配置文件 | | Playwright | | | CDP 端口 | 9222(原生)或 18800(OpenClaw) | --- 工作流程(SOP) 关键 :每个话题页有两个 Tab — 热门 (热度排序)和 最新内容 (实时排序)。脚本默认切换到"最新内容"以获取最新帖子。 Step 1 — 检查 / 启动 Chrome CDP 功能 : 1. 检测 Chrome CDP 是否已在 9222 端口运行 2. 若无,自动启动 Chrome(User Profile + Remote Debugging) 3. 运行 scraper 4. 脚本结束自动关闭 Chrome(加 保留进程…

, line)\n if num:\n n = int(num.group(1))\n if not current['likes']: current['likes'] = n\n elif not current['comments']: current['comments'] = n\n else:\n current['text'] += ' ' + line\n if current and current['text'] and len(current['text']) > 5:\n key = current['text'][:60]\n if key not in seen:\n results.append(current)\n return results\n\nfrom urllib.parse import unquote\n\ndef parse_trends_page(text, html):\n \"\"\"解析 trends 页面,提取话题名称 + slug(需要 innerText + HTML)\"\"\"\n topics = []\n \n # 方式1: 标准格式 #话题名 + 数字 人讨论中/次浏览\n pattern1 = re.compile(r'#([^\\n#]{2,50}?)\\s*\\n?\\s*([\\d,]+)\\s*(?:人讨论中|次浏览)', re.MULTILINE)\n for m in pattern1.finditer(text):\n name = m.group(1).strip()\n count = int(m.group(2).replace(',', ''))\n topics.append((name, count))\n \n # 如果方式1结果少,尝试方式2: 从HTML中直接提取\n # 格式: /zh-CN/square/hashtag/slug 后面跟着话题名\n slug_pattern = re.compile(r'/zh-CN/square/hashtag/([^\"\\'&\\s]+)')\n slugs = slug_pattern.findall(html)\n \n # 方式2补充: 如果 slugs 比 topics 多,用 slug 名作为话题名\n if len(slugs) > len(topics):\n existing_names = {t[0].lower().replace(' ','').replace('-','') for t in topics}\n for slug in slugs:\n # slug 可能是 URL 编码的中文或英文\n slug_name = unquote(slug).replace('-', ' ')\n if slug_name.lower().replace(' ','') not in existing_names:\n topics.append((slug_name, 0))\n existing_names.add(slug_name.lower().replace(' ',''))\n \n # 为每个 topic 匹配 slug(按名称模糊匹配,不用位置)\n result = []\n used_slugs = set()\n for name, count in topics:\n name_clean = unquote(name).lower().replace(' ', '').replace('#', '')\n best_slug = ''\n for s in slugs:\n if s in used_slugs:\n continue\n s_clean = unquote(s).lower().replace(' ', '').replace('-', '')\n # 完全匹配\n if s_clean == name_clean:\n best_slug = s\n break\n if s_clean in name_clean or name_clean in s_clean:\n overlap_len = min(len(s_clean), len(name_clean))\n longer_len = max(len(s_clean), len(name_clean))\n if longer_len == 0 or overlap_len / longer_len >= 0.5:\n best_slug = s\n break\n if not best_slug:\n continue # 匹配不到slug则丢弃该话题\n used_slugs.add(best_slug)\n result.append({'name': name, 'slug': best_slug, 'count': count})\n return result\n\ndef scrape_topic_page_cdp(cdp_url, topic_name, slug, scroll_rounds=2, per_topic=10):\n \"\"\"独立连接CDP,爬取单个话题页(线程安全)\"\"\"\n from playwright.sync_api import sync_playwright\n \n posts = []\n try:\n with sync_playwright() as p:\n browser = p.chromium.connect_over_cdp(cdp_url)\n context = browser.contexts[0] if browser.contexts else None\n if not context:\n browser.close()\n return []\n \n page = context.new_page()\n url = f\"https://www.binance.com/zh-CN/square/hashtag/{slug}\"\n \n try:\n page.goto(url, wait_until='commit', timeout=15000)\n page.wait_for_timeout(4000)\n except Exception as e:\n print(f\" ⚠️ #{topic_name} 导航失败: {e}\", flush=True)\n page.close()\n browser.close()\n return []\n\n # 切换到\"最新内容\" tab\n try:\n latest_tab = page.get_by_text(\"最新内容\", exact=True)\n latest_tab.click()\n page.wait_for_timeout(3000)\n except:\n pass\n\n # 滚动加载\n for i in range(scroll_rounds):\n try:\n page.evaluate(\"window.scrollTo(0, document.body.scrollHeight)\")\n page.wait_for_timeout(2000)\n except:\n break\n\n text = page.inner_text('body')\n raw_posts = parse_posts_from_text(text)\n \n # 按参数截断\n raw_posts = raw_posts[:per_topic]\n\n for item in raw_posts:\n coins = list(set(c for c in item.get('coins', []) if c not in EXCLUDED))\n posts.append({\n 'time': item.get('time', ''),\n 'age_mins': item.get('age_mins', 999),\n 'coins': coins,\n 'text': item.get('text', '').strip(),\n 'likes': item.get('likes', 0) or 0,\n 'comments': item.get('comments', 0) or 0,\n 'topic': topic_name,\n })\n page.close()\n browser.close()\n except Exception as e:\n print(f\" ⚠️ #{topic_name} 异常: {e}\", flush=True)\n \n return posts\n\ndef report(all_posts, max_age_mins, top_n, coin_type, elapsed_secs):\n recent = [p for p in all_posts if p['age_mins'] \u003c= max_age_mins]\n older = [p for p in all_posts if p['age_mins'] > max_age_mins]\n fin_recent = [p for p in recent if is_finance(p['text']) and not is_noise(p['text'])]\n fin_older = [p for p in older if is_finance(p['text']) and not is_noise(p['text'])]\n coin_cnt = Counter(c for p in all_posts for c in p.get('coins', []))\n topics = defaultdict(list)\n for p in all_posts:\n topics[p.get('topic', '')].append(p)\n topic_fin = {t: [p for p in ps if is_finance(p['text']) and not is_noise(p['text'])]\n for t, ps in topics.items()}\n now = datetime.now(timezone(timedelta(hours=8)))\n now_str = now.strftime('%Y-%m-%d %H:%M')\n \n print(f\"\\n{'━'*60}\")\n print(f\"📊 币安 Square v12.0 CDP | {now_str} | ≤{max_age_mins}min | {coin_type}\")\n print(f\"{'━'*60}\")\n print(f\" 总帖子: {len(all_posts)} | 满足: {len(recent)} | 仅计入: {len(older)}\")\n print(f\" 金融帖: {len(fin_recent)} 满足 + {len(fin_older)} 补充 | 小币: {len(coin_cnt)}种\")\n print(f\" 话题: {len(topics)} 个\")\n \n # Topic heat\n if topic_fin:\n sorted_topics = sorted(topic_fin.items(), key=lambda x: len(x[1]), reverse=True)\n print(f\"\\n🔥 Topic 讨论区热度 (金融帖数)\")\n print(f\" 排名 话题 金融帖 总帖\")\n print(f\" ─────────────────────────────────────────────────────\")\n for i, (t, ps) in enumerate(sorted_topics[:10], 1):\n print(f\" {i:>2}. #{t:\u003c32s} {len(ps):>3d}帖 {len(topics[t])}帖\")\n \n # Coin opportunities\n if coin_cnt:\n print(f\"\\n🪙 小币机会 (Top {top_n})\")\n print(f\" 排名 币种 次 信号 摘要\")\n print(f\" ──────────────────────────────────────────────\")\n for i, (c, cnt) in enumerate(coin_cnt.most_common(top_n), 1):\n sig = \"⚡\" if any(is_finance(p['text']) and not is_noise(p['text']) for p in all_posts if c in p.get('coins',[])) else \"💤\"\n sample = next((p['text'][:25] for p in all_posts if c in p.get('coins',[]) and is_finance(p['text']) and not is_noise(p['text'])), \"\")\n print(f\" {i:>2}. ${c:\u003c12s}{cnt:>3d}次 {sig} {sample}\")\n \n if recent:\n print(f\"\\n📌 满足条件帖子 (≤{max_age_mins}min) 共{len(recent)}帖\")\n print(\"━\"*60)\n for p in sorted(recent, key=lambda x: x['age_mins'])[:20]:\n coins_str = ' '.join(f'${c}' for c in p['coins'][:3])\n age = f\"{p['age_mins']:.0f}min\" if p['age_mins'] \u003c 60 else f\"{p['age_mins']//60}h前\"\n fin = \"📈\" if is_finance(p['text']) and not is_noise(p['text']) else \" \"\n print(f\" [{age:>8s}] {fin} {coins_str:\u003c18s} {p['text'][:55]}\")\n \n print(f\"\\n{'━'*60}\")\n print(f\"⏱️ 运行耗时: {elapsed_secs:.1f}s\")\n print(f\"📋 {now_str} | ≤{max_age_mins}min | {len(all_posts)}帖 | {len(fin_recent)}金融帖 | {sum(coin_cnt.values())}次小币\")\n\ndef main():\n ap = argparse.ArgumentParser(description=\"Binance Square Scraper v12.0 CDP Multi-Topic Concurrent\")\n ap.add_argument(\"--min\", type=int, default=60)\n ap.add_argument(\"--top\", type=int, default=10)\n ap.add_argument(\"--coin-type\", choices=[\"all\",\"mainstream\",\"small\",\"meme\"], default=\"all\")\n ap.add_argument(\"--topics\", type=int, default=10, help=\"最多爬取 N 个话题(0=只爬广场)\")\n ap.add_argument(\"--per-topic\", type=int, default=10, help=\"每个话题最多爬 N 帖\")\n ap.add_argument(\"--scrolls\", type=int, default=2, help=\"每话题滚动次数\")\n ap.add_argument(\"--concurrency\", type=int, default=5, help=\"并发话题数(默认5)\")\n ap.add_argument(\"--cdp-port\", type=int, default=9222, help=\"CDP端口(默认9222,OpenClaw用18800)\")\n args = ap.parse_args()\n\n t_start = time.time()\n cdp_url = f\"http://127.0.0.1:{args.cdp_port}\"\n\n from playwright.sync_api import sync_playwright\n\n with sync_playwright() as p:\n try:\n browser = p.chromium.connect_over_cdp(cdp_url)\n except Exception as e:\n print(f\"⚠️ CDP连接失败: {e}\")\n print(\" 请确保 Chrome 已打开 binance.com/zh-CN/square\")\n return\n\n # ── Step 0: 检查登录状态 ──\n ctx = browser.contexts[0]\n check_page = ctx.new_page()\n try:\n check_page.goto('https://www.binance.com/zh-CN/my/dashboard', wait_until='domcontentloaded', timeout=15000)\n time.sleep(2)\n final_url = check_page.url\n body_text = check_page.inner_text('body')[:500]\n if 'login' in final_url.lower() or '登录' in body_text[:200] or 'register' in final_url.lower():\n print(\"⚠️ Binance 未登录或 cookie 已过期(Square 公开内容仍可爬取,但部分功能受限)\")\n print(\"✅ Binance 登录状态正常\", flush=True)\n except Exception as e:\n print(f\"⚠️ 登录检测跳过({e}),继续运行...\", flush=True)\n finally:\n try: check_page.close()\n except: pass\n\n # ── Step 1: 获取 trends 话题列表 ──\n print(\"📥 Step 1: 获取热门话题列表...\", flush=True)\n trends_url = \"https://www.binance.com/zh-CN/square/trends\"\n \n trends_page = None\n for page in browser.contexts[0].pages:\n if 'trends' in page.url:\n trends_page = page\n break\n \n if not trends_page:\n for page in browser.contexts[0].pages:\n if 'square' in page.url.lower():\n try:\n page.goto(trends_url, wait_until='commit', timeout=10000)\n trends_page = page\n break\n except:\n pass\n \n trends_text = \"\"\n trends_html = \"\"\n if trends_page:\n try:\n trends_page.goto(trends_url, wait_until='commit', timeout=10000)\n except:\n pass\n # 多滚动确保加载完整话题列表\n for _ in range(6):\n try:\n trends_page.evaluate(\"window.scrollTo(0, document.body.scrollHeight)\")\n trends_page.wait_for_timeout(1500)\n except:\n break\n trends_text = trends_page.inner_text('body')\n trends_html = trends_page.content()\n \n topics = parse_trends_page(trends_text, trends_html)\n print(f\" ✅ 动态发现 {len(topics)} 个话题\", flush=True)\n for i, t in enumerate(topics):\n print(f\" {i+1}. #{t['name']} ({t['count']:,} 人讨论) slug={t.get('slug','')}\")\n \n crawl_count = min(args.topics, len(topics)) if args.topics > 0 else 0\n print(f\" 📌 将爬取前 {crawl_count} 个话题\", flush=True)\n topics = topics[:crawl_count]\n topics_with_slug = [t for t in topics if t.get('slug')]\n \n # ── Step 2: 爬广场页 ──\n print(f\"\\n📥 Step 2: 爬取广场页...\", flush=True)\n all_posts = []\n \n square_page = None\n for page in browser.contexts[0].pages:\n if 'square' in page.url.lower() and 'trends' not in page.url:\n square_page = page\n break\n \n if square_page:\n try:\n square_page.goto(\"https://www.binance.com/zh-CN/square\", wait_until='commit', timeout=10000)\n except:\n pass\n square_page.wait_for_timeout(4000)\n for _ in range(args.scrolls + 1):\n try:\n square_page.evaluate(\"window.scrollTo(0, document.body.scrollHeight)\")\n square_page.wait_for_timeout(2000)\n except:\n break\n text = square_page.inner_text('body')\n raw = parse_posts_from_text(text)\n for item in raw:\n coins = list(set(c for c in item.get('coins', []) if c not in EXCLUDED))\n filtered = coin_filter(coins, args.coin_type) if args.coin_type != 'all' else coins\n all_posts.append({\n 'time': item.get('time', ''),\n 'age_mins': item.get('age_mins', 999),\n 'coins': filtered,\n 'text': item.get('text', '').strip(),\n 'likes': item.get('likes', 0) or 0,\n 'comments': item.get('comments', 0) or 0,\n 'topic': '广场',\n })\n print(f\" ✅ 广场获取 {len(raw)} 帖\")\n \n browser.close()\n\n # ── Step 3: 并发爬取话题页 ──\n if topics_with_slug:\n print(f\"\\n📥 Step 3: 并发爬取 {len(topics_with_slug)} 个话题页 (并发={args.concurrency})...\", flush=True)\n \n topic_results = {}\n topic_lock = threading.Lock()\n \n def worker(topic):\n posts = scrape_topic_page_cdp(cdp_url, topic['name'], topic['slug'],\n scroll_rounds=args.scrolls, per_topic=args.per_topic)\n # 过滤币种\n for item in posts:\n filtered = coin_filter(item['coins'], args.coin_type) if args.coin_type != 'all' else item['coins']\n item['coins'] = filtered\n with topic_lock:\n topic_results[topic['name']] = posts\n mark = \"✅\" if posts else \"⚠️\"\n print(f\" {mark} #{topic['name']}: +{len(posts)} 帖\", flush=True)\n \n # 分批并发\n for batch_start in range(0, len(topics_with_slug), args.concurrency):\n batch = topics_with_slug[batch_start:batch_start + args.concurrency]\n threads = []\n for topic in batch:\n t = threading.Thread(target=worker, args=(topic,))\n t.start()\n threads.append(t)\n for t in threads:\n t.join(timeout=60)\n batch_ids = [t['name'] for t in batch]\n done = sum(1 for name in batch_ids if name in topic_results)\n print(f\" 批次 [{batch_start+1}-{batch_start+len(batch)}] 完成: {done}/{len(batch)}\", flush=True)\n \n for topic in topics_with_slug:\n all_posts.extend(topic_results.get(topic['name'], []))\n \n skipped = len(topics) - len(topics_with_slug)\n if skipped:\n print(f\" ⏭️ 跳过 {skipped} 个无slug话题\")\n\n elapsed = time.time() - t_start\n\n if not all_posts:\n print(\"⚠️ 未获取到任何数据\")\n return\n\n # ── Step 4: 去重 + 汇报 ──\n seen, unique = set(), []\n for p in all_posts:\n key = p['text'][:60] if p['text'] else ''\n if key and key not in seen:\n seen.add(key); unique.append(p)\n all_posts = unique\n\n report(all_posts, max_age_mins=args.min, top_n=args.top, coin_type=args.coin_type, elapsed_secs=elapsed)\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":19962,"content_sha256":"fb6ec679400a1facfe0a399fcde8501f17b77c1a06d5fb915b0290b75bb542a2"},{"filename":"scripts/square_scraper_live.py","content":"#!/usr/bin/env python3\n\"\"\"\nBinance Square Scraper — v9.0 (CDP Live Session)\nUses Playwright CDP to connect to User's existing Chrome session,\nextracting full content from Binance Square with login state.\n\"\"\"\nimport re, json, argparse, time, ast\nfrom datetime import datetime, timezone, timedelta\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom collections import Counter, defaultdict\n\n# ===== 词表 =====\nMAINSTREAM = {\"BTC\",\"ETH\",\"SOL\",\"BNB\",\"XRP\",\"ADA\",\"DOGE\",\"XLM\",\"USDT\",\n \"BUSD\",\"USDC\",\"DOT\",\"AVAX\",\"LINK\",\"MATIC\",\"SHIB\",\"LTC\"}\nMEME = {\"DOGE\",\"SHIB\",\"PEPE\",\"WIF\",\"BONK\",\"FLOKI\",\"ELON\",\"TRUMP\",\"MAGA\"}\nEXCLUDED = {\n \"BTC\",\"ETH\",\"SOL\",\"BNB\",\"XRP\",\"ADA\",\"DOGE\",\"XLM\",\"USDT\",\"BUSD\",\n \"USDC\",\"DAI\",\"DOT\",\"AVAX\",\"LINK\",\"MATIC\",\"SHIB\",\"LTC\",\"FTT\",\"UNI\",\n \"OKB\",\"USDP\",\"TUSD\",\"AE\",\"LEVER\",\"ONG\",\"SXP\",\"YFI\",\"COMP\",\"MKR\",\n \"ZEC\",\"ENJ\",\"MANA\",\"BAT\",\"AXS\",\"BLZ\",\"CHZ\",\"HBAR\",\"ALGO\",\"VET\",\n \"FTM\",\"THETA\",\"EOS\",\"IOTA\",\"NEO\",\"WAVES\",\"ZIL\",\"KAVA\",\"AUDIO\",\n}\nFIN_KW = [\n \"做多\",\"做空\",\"开仓\",\"平仓\",\"止损\",\"止盈\",\"买入\",\"卖出\",\"合约\",\n \"永续\",\"杠杆\",\"多头\",\"空头\",\"爆仓\",\"强平\",\"建仓\",\"补仓\",\"仓位\",\n \"多单\",\"空单\",\"多空\",\"抄底\",\"逃顶\",\"涨幅\",\"跌幅\",\"暴涨\",\"暴跌\",\n \"盈利\",\"亏损\",\"本金\",\"浮亏\",\"浮盈\",\"反弹\",\"回调\",\"趋势\",\"信号\",\n \"预测\",\"阻力\",\"支撑\",\"突破\",\"牛市\",\"熊市\",\"梭哈\",\"上车\",\"下车\",\n \"做空\",\"看跌\",\"看涨\",\"止损\",\"止盈\",\"买入\",\"卖出\",\"挂单\",\"现价\",\n]\nNOISE_KW = [\n \"女生记住\",\"婚姻\",\"男朋友\",\"高嫁\",\"低嫁\",\"问界\",\"M9\",\"华为\",\"手机\",\n \"特斯拉\",\"抖音\",\"小红书\",\"瑜伽\",\"人民公园\",\"美女\",\"相亲\",\"约会\",\n \"婚礼\",\"装修\",\"考研\",\"考公\",\"留学\",\"移民\",\"工资\",\"裁员\",\"面试\",\n]\n\ndef coin_filter(coins, coin_type):\n if coin_type == \"all\":\n return coins\n elif coin_type == \"mainstream\":\n return [c for c in coins if c in MAINSTREAM]\n elif coin_type == \"small\":\n return [c for c in coins if c not in MAINSTREAM and c not in EXCLUDED]\n elif coin_type == \"meme\":\n return [c for c in coins if c in MEME]\n return coins\n\ndef extract_coins(text):\n return list({c for c in re.findall(r'\\$([A-Z]{2,10})', text) if c not in EXCLUDED})\n\ndef is_noise(text):\n if any(kw in text for kw in NOISE_KW):\n return not any(kw in text for kw in FIN_KW)\n return False\n\ndef is_finance(text):\n return any(kw in text for kw in FIN_KW)\n\ndef score(p):\n recency = max(0, 30 - (p[\"age_mins\"] or 999)) * 2\n return recency + (p.get(\"likes\", 0) + p.get(\"comments\", 0) * 3) / 100 + len(p.get(\"coins\", []))\n\nJS_SCRAPE = \"\"\"\nfunction() {\n var results = [];\n var seen = new Set();\n var timeRegex = /(\\\\d+)\\\\s*(分钟|小时|天)/;\n var lines = document.body.innerText.split('\\\\n');\n var current = null;\n for (var i = 0; i \u003c lines.length; i++) {\n var trimmed = lines[i].trim();\n if (!trimmed || trimmed.length > 200) continue;\n var timeMatch = trimmed.match(timeRegex);\n if (timeMatch) {\n if (current && current.text && current.text.length > 3) {\n var key = current.text.slice(0, 60);\n if (!seen.has(key)) { seen.add(key); results.push(current); }\n }\n var val = parseInt(timeMatch[1]);\n var unit = timeMatch[2];\n current = {\n time: trimmed,\n age_mins: unit==='分钟' ? val : unit==='小时' ? val*60 : val*1440,\n text: '', likes: 0, comments: 0, views: 0\n };\n } else if (current) {\n var numMatch = trimmed.match(/^(\\\\d+(?:,\\\\d+)*)$/);\n if (numMatch) {\n var n = parseInt(numMatch[1].replace(',',''));\n if (!current.likes) current.likes = n;\n else if (!current.comments) current.comments = n;\n else if (!current.views) current.views = n;\n } else {\n current.text += ' ' + trimmed;\n }\n }\n }\n if (current && current.text && current.text.length > 3) {\n var key = current.text.slice(0, 60);\n if (!seen.has(key)) seen.add(key), results.push(current);\n }\n var coinsFn = function(text) {\n var m = text.match(/\\\\$[A-Z]{2,10}/g) || [];\n return [...new Set(m)];\n };\n return results.slice(0, 80).map(function(r) {\n return {\n time: r.time,\n age_mins: r.age_mins,\n coins: coinsFn(r.text).join(','),\n text: r.text.replace(/\\\\$[A-Z]{2,10}/g,'').slice(0, 140),\n likes: r.likes || 0,\n comments: r.comments || 0\n };\n });\n}\n\"\"\"\n\nURLS = {\n \"square\": \"https://www.binance.com/zh-CN/square\",\n \"trending\": \"https://www.binance.com/zh-CN/square/trending\",\n \"hot\": \"https://www.binance.com/zh-CN/square/hot\",\n}\n\ndef _scrape_cdp(url, scroll_rounds=3):\n \"\"\"Connect via CDP to User's Chrome session and scrape Binance Square.\"\"\"\n from playwright.sync_api import sync_playwright\n \n cdp_url = \"http://127.0.0.1:18800\"\n \n with sync_playwright() as p:\n try:\n browser = p.chromium.connect_over_cdp(cdp_url)\n except Exception as e:\n print(f\" ⚠️ CDP连接失败: {e}\")\n return []\n \n try:\n # Find square page\n square_page = None\n for page in browser.contexts[0].pages:\n if 'square' in page.url.lower():\n square_page = page\n break\n \n if not square_page:\n print(\" ⚠️ 未找到Square页面\")\n browser.disconnect()\n return []\n \n # Navigate if needed\n if square_page.url != url:\n try:\n square_page.goto(url, wait_until='commit', timeout=10000)\n square_page.wait_for_timeout(4000)\n except:\n pass\n \n # Scroll to load posts\n for i in range(scroll_rounds):\n square_page.evaluate(\"window.scrollTo(0, document.body.scrollHeight)\")\n square_page.wait_for_timeout(2000)\n \n # Extract\n result = square_page.evaluate(JS_SCRAPE)\n browser.disconnect()\n return result or []\n except Exception as e:\n print(f\" ⚠️ CDP scrape error: {e}\")\n try:\n browser.disconnect()\n except:\n pass\n return []\n\ndef fetch_one_topic(name, slug, per_topic, coin_type, max_age_mins):\n url = f\"https://www.binance.com/zh-CN/square/hashtag/{slug}\"\n posts = _scrape_cdp(url, scroll_rounds=min(3, per_topic // 5 + 2))\n topic_posts = []\n for p in posts:\n if p[\"age_mins\"] > max_age_mins:\n continue\n filtered_coins = coin_filter(p.get(\"coins\", []), coin_type)\n if coin_type != \"all\" and not filtered_coins:\n continue\n p[\"coins\"] = filtered_coins\n p[\"topic\"] = name\n topic_posts.append(p)\n return name, url, topic_posts\n\ndef _parse_result(raw):\n if not raw:\n return []\n if isinstance(raw, str):\n try: raw = ast.literal_eval(raw)\n except: return []\n posts = []\n for item in (raw or []):\n age = item.get(\"age_mins\", 999)\n coins = [c.replace(\"$\",\"\") for c in (item.get(\"coins\",\"\") or \"\").split(\",\") if c]\n posts.append({\n \"time\": item.get(\"time\",\"\"),\n \"age_mins\": age,\n \"coins\": coins,\n \"text\": item.get(\"text\",\"\").strip(),\n \"likes\": item.get(\"likes\", 0) or 0,\n \"comments\": item.get(\"comments\", 0) or 0,\n })\n return posts\n\ndef report(all_posts, max_age_mins, top_n, coin_type):\n now = datetime.now(timezone(timedelta(hours=8)))\n now_str = now.strftime(\"%Y-%m-%d %H:%M\")\n age_label = f\"{max_age_mins}min\"\n \n recent = [p for p in all_posts if p[\"age_mins\"] \u003c= max_age_mins]\n older = [p for p in all_posts if p[\"age_mins\"] > max_age_mins]\n finance_recent = [p for p in recent if is_finance(p[\"text\"]) and not is_noise(p[\"text\"])]\n finance_older = [p for p in older if is_finance(p[\"text\"]) and not is_noise(p[\"text\"])]\n \n all_coins = []\n for p in all_posts:\n for c in p.get(\"coins\", []):\n if coin_filter([c], coin_type):\n all_coins.append(c)\n coin_cnt = Counter(all_coins)\n \n print(f\"\\n{'━'*60}\")\n print(f\"📊 币安 Square v9.0 CDP | {now_str} | ≤{max_age_mins}min | 币种:{coin_type}\")\n print(f\"{'━'*60}\")\n print(f\" 总帖子: {len(all_posts)} | 满足: {len(recent)} | 仅计入: {len(older)}\")\n print(f\" 金融帖: {len(finance_recent)} 满足 + {len(finance_older)} 补充 | 小币: {len(coin_cnt)}种\")\n \n # Topic heat\n topics = defaultdict(list)\n for p in all_posts:\n topics[p.get(\"topic\",\"\")].append(p)\n topic_finance = {t: [p for p in ps if is_finance(p[\"text\"]) and not is_noise(p[\"text\"])]\n for t, ps in topics.items()}\n sorted_topics = sorted(topic_finance.items(), key=lambda x: len(x[1]), reverse=True)\n if sorted_topics:\n print(f\"\\n🔥 Topic 讨论区热度 (金融帖数)\")\n print(f\" 排名 话题 金融帖 概览 \")\n print(f\" ────────────────────────────────────────────────────────────\")\n for i, (t, ps) in enumerate(sorted_topics[:8], 1):\n sample = ps[0][\"text\"][:20] if ps else \"\"\n print(f\" {i:>2}. #{t:\u003c30s} {len(ps)}帖\")\n \n # Coin opportunities\n if coin_cnt:\n print(f\"\\n🪙 小币实时机会 ({coin_type})\")\n print(f\" 排名 币种 次 信号 摘要 \")\n print(f\" ────────────────────────────────────────────────────────────\")\n for i, (c, cnt) in enumerate(coin_cnt.most_common(top_n), 1):\n samples = [p[\"text\"] for p in all_posts if c in p.get(\"coins\",[]) and is_finance(p[\"text\"])]\n sample = samples[0][:30] if samples else \"\"\n sig = \"⚡\" if any(is_finance(p[\"text\"]) and not is_noise(p[\"text\"]) for p in all_posts if c in p.get(\"coins\",[])) else \"💤\"\n print(f\" {i:>2}. ${c:\u003c10s}{cnt:>3d}次 {sig} {sample}\")\n \n # Recent posts\n if recent:\n print(f\"\\n📌 满足条件帖子 (≤{max_age_mins}min) 共{len(recent)}帖\")\n print(\"━\"*60)\n for p in sorted(recent, key=lambda x: x[\"age_mins\"])[:20]:\n coins_str = \" \".join(f\"${c}\" for c in p[\"coins\"][:3])\n age = f\"{p['age_mins']:.0f}min\" if p['age_mins'] \u003c 60 else f\"{p['age_mins']//60}h前\"\n finance_tag = \"📈\" if is_finance(p[\"text\"]) and not is_noise(p[\"text\"]) else \" \"\n print(f\" [{age:>8s}] {finance_tag} {coins_str:\u003c20s} {p['text'][:60]}\")\n \n # Older posts\n older_fin = [p for p in finance_older if coin_filter(p.get(\"coins\",[]), coin_type)]\n if older_fin:\n print(f\"\\n📋 不满足条件(仅计入总数) 共{len(older_fin)}帖:\")\n for p in older_fin[:10]:\n coins_str = \" \".join(f\"${c}\" for c in p[\"coins\"][:3])\n age = f\"{p['age_mins']//60}h前\" if p['age_mins'] >= 60 else f\"{p['age_mins']}min前\"\n print(f\" {age:\u003c8s} {coins_str:\u003c20s} {p['text'][:50]}\")\n \n print(f\"\\n{'━'*60}\")\n print(f\"📋 {now_str} | ≤{max_age_mins}min | {len(all_posts)}帖 | {len(finance_recent)}金融帖 | {sum(coin_cnt.values())}次小币提及\")\n print(f\"{'━'*60}\")\n\ndef main():\n ap = argparse.ArgumentParser(description=\"Binance Square Scraper v9.0 CDP\")\n ap.add_argument(\"--min\", type=int, default=60, help=\"时间精度(分钟)\")\n ap.add_argument(\"--per-topic\", type=int, default=30, help=\"每个Topic最多爬多少帖\")\n ap.add_argument(\"--top\", type=int, default=10, help=\"显示Top N小币\")\n ap.add_argument(\"--coin-type\", choices=[\"all\",\"mainstream\",\"small\",\"meme\"],\n default=\"all\", help=\"主流币/小币/meme/全部\")\n ap.add_argument(\"--topics\", type=int, default=0,\n help=\"爬取Top N个热门话题(0=不爬话题只爬广场)\")\n ap.add_argument(\"--tab\", choices=[\"square\",\"trending\",\"hot\"], default=\"square\")\n args = ap.parse_args()\n\n all_posts = []\n square_url = URLS.get(args.tab, URLS[\"square\"])\n print(f\"📥 爬取广场页(CDP): {square_url}\", flush=True)\n \n posts = _scrape_cdp(square_url, scroll_rounds=3)\n for p in posts:\n p[\"topic\"] = \"广场\"\n all_posts.extend(posts)\n print(f\" ✅ 广场获取{len(posts)}帖\", flush=True)\n\n if args.topics > 0 and posts:\n topics_to_scrape = [\n (\"孙宇晨起诉WLFI\", \"World%20Liberty%20Financial\"),\n (\"Arbitrum冻结ETH\", \"Arbitrum%E5%86%BB%E7%BB%93%E9%BB%98%E5%AE%A2ETH\"),\n (\"wstETH解锁\", \"wstETH%E8%A7%A3%E9%94%81%E6%96%B0%E6%B5%81%E5%8A%A8%E6%80%A7%E9%80%9A%E9%81%93\"),\n (\"Strategy增持BTC\", \"Strategy%E5%A2%9E%E6%8C%81%E6%AF%94%E7%89%B9%E5%B8%81\"),\n (\"山寨币复苏\", \"%E5%B1%B1%E5%AF%A8%E5%B8%81%E5%A4%8D%E8%8B%8F%EF%BC%9F\"),\n ][:args.topics]\n print(f\"\\n📥 爬取{len(topics_to_scrape)}个Topic讨论区 (每Topic最多{args.per_topic}帖)\")\n for name, slug in topics_to_scrape:\n name2, url, tp = fetch_one_topic(name, slug, args.per_topic, args.coin_type, args.min)\n all_posts.extend(tp)\n mark = \"✅\" if tp else \"⚠️\"\n print(f\" {mark} #{name:\u003c28s} +{len(tp)}帖\", flush=True)\n\n if not all_posts:\n print(\"⚠️ 未获取到任何数据\"); return\n\n # 去重\n seen, unique = set(), []\n for p in all_posts:\n key = p[\"text\"][:60] if p[\"text\"] else \"\"\n if key and key not in seen:\n seen.add(key); unique.append(p)\n all_posts = unique\n\n report(all_posts, max_age_mins=args.min, top_n=args.top, coin_type=args.coin_type)\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":14361,"content_sha256":"1575bc2d72d1ee2f61a6538631683ba371f1fed9299d329d54746296d090b0a3"},{"filename":"scripts/square_scraper.py","content":"#!/usr/bin/env python3\n\"\"\"\nBinance Square Scraper — v8.0\n功能:\n 1. 自动识别/指定 Topic 讨论区\n 2. 每个 Topic 爬取数量可配置\n 3. 筛选主流币/小币/全部\n 4. 时间条件筛选(不满足的只计入总数)\n 5. Topic 统计报告\n\n用法:\n python3 square_scraper.py --min 60 --per-topic 30 --coin-type small\n\n参数:\n --min N 只看N分钟内的帖子(不满足的计入总数)\n --per-topic N 每个Topic最多爬N帖(默认30)\n --coin-type all|mainstream|small|meme\n all=全部(默认)\n mainstream=BTC/ETH/SOL/BNB/XRP/ADA/DOGE等\n small=小币(排除主流)\n meme=DOGE/SHIB/PEPE等meme币\n --topics N 爬取Top N个热门话题(默认10,0=不爬话题只爬广场)\n --tab square|trending|hot(广场页,不受topics影响)\n\"\"\"\nimport re, json, argparse, time, ast\nfrom datetime import datetime, timezone, timedelta\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom collections import Counter, defaultdict\n\n# ===== 词表 =====\nMAINSTREAM = {\"BTC\",\"ETH\",\"SOL\",\"BNB\",\"XRP\",\"ADA\",\"DOGE\",\"XLM\",\"USDT\",\n \"BUSD\",\"USDC\",\"DOT\",\"AVAX\",\"LINK\",\"MATIC\",\"SHIB\",\"LTC\"}\nMEME = {\"DOGE\",\"SHIB\",\"PEPE\",\"WIF\",\"BONK\",\"FLOKI\",\"ELON\",\"TRUMP\",\"MAGA\"}\nEXCLUDED = {\n \"BTC\",\"ETH\",\"SOL\",\"BNB\",\"XRP\",\"ADA\",\"DOGE\",\"XLM\",\"USDT\",\"BUSD\",\n \"USDC\",\"DAI\",\"DOT\",\"AVAX\",\"LINK\",\"MATIC\",\"SHIB\",\"LTC\",\"FTT\",\"UNI\",\n \"OKB\",\"USDP\",\"TUSD\",\"AE\",\"LEVER\",\"ONG\",\"SXP\",\"YFI\",\"COMP\",\"MKR\",\n \"ZEC\",\"ENJ\",\"MANA\",\"BAT\",\"AXS\",\"BLZ\",\"CHZ\",\"HBAR\",\"ALGO\",\"VET\",\n \"FTM\",\"THETA\",\"EOS\",\"IOTA\",\"NEO\",\"WAVES\",\"ZIL\",\"KAVA\",\"AUDIO\",\n}\nFIN_KW = [\n \"做多\",\"做空\",\"开仓\",\"平仓\",\"止损\",\"止盈\",\"买入\",\"卖出\",\"合约\",\n \"永续\",\"杠杆\",\"多头\",\"空头\",\"爆仓\",\"强平\",\"建仓\",\"补仓\",\"仓位\",\n \"多单\",\"空单\",\"多空\",\"抄底\",\"逃顶\",\"涨幅\",\"跌幅\",\"暴涨\",\"暴跌\",\n \"盈利\",\"亏损\",\"本金\",\"浮亏\",\"浮盈\",\"反弹\",\"回调\",\"趋势\",\"信号\",\n \"预测\",\"阻力\",\"支撑\",\"突破\",\"牛市\",\"熊市\",\"梭哈\",\"上车\",\"下车\",\n \"做空\",\"看跌\",\"看涨\",\"止损\",\"止盈\",\"买入\",\"卖出\",\"挂单\",\"现价\",\n]\nNOISE_KW = [\n \"女生记住\",\"婚姻\",\"男朋友\",\"高嫁\",\"低嫁\",\"问界\",\"M9\",\"华为\",\"手机\",\n \"特斯拉\",\"抖音\",\"小红书\",\"瑜伽\",\"人民公园\",\"美女\",\"相亲\",\"约会\",\n \"婚礼\",\"装修\",\"考研\",\"考公\",\"留学\",\"移民\",\"工资\",\"裁员\",\"面试\",\n]\n\ndef coin_filter(coins, coin_type):\n if coin_type == \"all\":\n return coins\n elif coin_type == \"mainstream\":\n return [c for c in coins if c in MAINSTREAM]\n elif coin_type == \"small\":\n return [c for c in coins if c not in MAINSTREAM and c not in EXCLUDED]\n elif coin_type == \"meme\":\n return [c for c in coins if c in MEME]\n return coins\n\ndef extract_coins(text):\n return list({c for c in re.findall(r'\\$([A-Z]{2,10})', text) if c not in EXCLUDED})\n\ndef is_noise(text):\n if any(kw in text for kw in NOISE_KW):\n return not any(kw in text for kw in FIN_KW)\n return False\n\ndef is_finance(text):\n return any(kw in text for kw in FIN_KW)\n\ndef score(p):\n recency = max(0, 30 - (p[\"age_mins\"] or 999)) * 2\n return recency + (p.get(\"likes\", 0) + p.get(\"comments\", 0) * 3) / 100 + len(p.get(\"coins\", []))\n\nJS_SCRAPE = \"\"\"\nfunction() {\n var results = [];\n var seen = new Set();\n var timeRegex = /(\\\\d+)\\\\s*(分钟|小时|天)/;\n var lines = document.body.innerText.split('\\\\n');\n var current = null;\n for (var i = 0; i \u003c lines.length; i++) {\n var trimmed = lines[i].trim();\n if (!trimmed || trimmed.length > 200) continue;\n var timeMatch = trimmed.match(timeRegex);\n if (timeMatch) {\n if (current && current.text && current.text.length > 3) {\n var key = current.text.slice(0, 60);\n if (!seen.has(key)) { seen.add(key); results.push(current); }\n }\n var val = parseInt(timeMatch[1]);\n var unit = timeMatch[2];\n current = {\n time: trimmed,\n age_mins: unit==='分钟' ? val : unit==='小时' ? val*60 : val*1440,\n text: '', likes: 0, comments: 0, views: 0\n };\n } else if (current) {\n var numMatch = trimmed.match(/^(\\\\d+(?:,\\\\d+)*)$/);\n if (numMatch) {\n var n = parseInt(numMatch[1].replace(',',''));\n if (!current.likes) current.likes = n;\n else if (!current.comments) current.comments = n;\n else if (!current.views) current.views = n;\n } else {\n current.text += ' ' + trimmed;\n }\n }\n }\n if (current && current.text && current.text.length > 3) {\n var key = current.text.slice(0, 60);\n if (!seen.has(key)) seen.add(key), results.push(current);\n }\n var coinsFn = function(text) {\n var m = text.match(/\\\\$[A-Z]{2,10}/g) || [];\n return [...new Set(m)];\n };\n return results.slice(0, 80).map(function(r) {\n return {\n time: r.time,\n age_mins: r.age_mins,\n coins: coinsFn(r.text).join(','),\n text: r.text.replace(/\\\\$[A-Z]{2,10}/g,'').slice(0, 140),\n likes: r.likes || 0,\n comments: r.comments || 0\n };\n });\n}\n\"\"\"\n\n# ===== 默认 Top 话题列表 =====\nDEFAULT_TOPICS = [\n (\"孙宇晨起诉WLFI\", \"World%20Liberty%20Financial\"),\n (\"Arbitrum冻结ETH\", \"Arbitrum%E5%86%BB%E7%BB%93%E9%BB%91%E5%AE%A2ETH\"),\n (\"wstETH解锁\", \"wstETH%E8%A7%A3%E9%94%81%E6%96%B0%E6%B5%81%E5%8A%A8%E6%80%A7%E9%80%9A%E9%81%93\"),\n (\"Strategy增持BTC\", \"Strategy%E5%A2%9E%E6%8C%81%E6%AF%94%E7%89%B9%E5%B8%81\"),\n (\"美伊冲突\", \"%E7%BE%8E%E4%BC%8A%E5%86%B2%E7%AA%81%E6%8E%A5%E4%B8%8B%E6%9D%A5%E4%BC%9A%E5%A6%82%E4%BD%95%E5%8F%91%E5%B1%95%EF%BC%9F\"),\n (\"RAVE波动\", \"RAVE%E5%89%A7%E7%83%88%E6%B3%A2%E5%8A%A8\"),\n (\"KelpDAO遭攻击\", \"Kelp%20DAO%E9%81%AD%E6%94%BB%E5%87%BB\"),\n (\"山寨币复苏\", \"%E5%B1%B1%E5%AF%A8%E5%B8%81%E5%A4%8D%E8%8B%8F%EF%BC%9F\"),\n (\"加密市场反弹\", \"%E5%8A%A0%E5%AF%86%E5%B8%82%E5%9C%BA%E5%8F%8D%E5%BC%B9\"),\n (\"ARK减持\", \"ARK%20Invest%E5%87%8F%E6%8C%81Circle%E4%B8%8EBullish%E8%82%A1%E7%A5%A8\"),\n (\"OpenAI发布GPT5.5\", \"OpenAI%E5%8F%91%E5%B8%83GPT-5.5\"),\n (\"Aave救助计划\", \"Aave%E5%AE%A3%E5%B8%83%E5%8D%8FUnited%E6%95%91%E5%8A%A9%E8%AE%A1%E5%88%92\"),\n]\n\nURLS = {\n \"square\": \"https://www.binance.com/zh-CN/square\",\n \"trending\": \"https://www.binance.com/zh-CN/square/trending\",\n \"hot\": \"https://www.binance.com/zh-CN/square/hot\",\n}\n\ndef fetch_one_topic(name, slug, per_topic, coin_type, max_age_mins):\n \"\"\"抓取单个话题页\"\"\"\n url = f\"https://www.binance.com/zh-CN/square/hashtag/{slug}\"\n posts = _fetch_posts(url, scroll_rounds=min(4, per_topic // 5 + 2))\n topic_posts = []\n for p in posts:\n if p[\"age_mins\"] > max_age_mins:\n continue\n filtered_coins = coin_filter(p.get(\"coins\", []), coin_type)\n if coin_type != \"all\" and not filtered_coins:\n continue\n p[\"coins\"] = filtered_coins\n p[\"topic\"] = name\n topic_posts.append(p)\n return name, url, topic_posts\n\ndef _scrape_undetected(url, scroll_rounds=4):\n \"\"\"\n Bypass: raise immediately so _fetch_posts falls through to playwright.\n (Selenium Chrome hangs on Binance Square's WebGL-heavy SPA)\n \"\"\"\n raise RuntimeError(\"selenium_hang\")\n\n\ndef _fetch_posts(url, scroll_rounds=4):\n posts = []\n # 1. undetected_chromedriver(绕过反爬,最优先)\n try:\n posts = _scrape_undetected(url, scroll_rounds)\n if posts:\n print(f\" ✅ undetected获取{len(posts)}帖\")\n return posts\n except Exception as e:\n print(f\" ⚠️ undetected失败: {e}\")\n\n # 2. selenium + ChromeDriver\n try:\n posts = _scrape_selenium(url, scroll_rounds)\n if posts:\n print(f\" ✅ selenium获取{len(posts)}帖\")\n return posts\n except: pass\n\n # 3. playwright\n try:\n posts = _scrape_playwright(url, scroll_rounds)\n if posts:\n print(f\" ✅ playwright获取{len(posts)}帖\")\n return posts\n except: pass\n\n return []\n\ndef _scrape_selenium(url, scroll_rounds=4):\n from selenium import webdriver\n from selenium.webdriver.chrome.service import Service\n from selenium.webdriver.chrome.options import Options\n import time as _time\n\n chromedriver_path = '/tmp/chromedriver-linux64/chromedriver'\n chrome_binary = '/usr/bin/google-chrome'\n options = Options()\n options.binary_location = chrome_binary\n options.add_argument('--headless=no')\n options.add_argument('--no-sandbox')\n options.add_argument('--disable-dev-shm-usage')\n options.add_argument('--disable-gpu')\n options.add_argument('--window-size=1400,900')\n options.add_experimental_option('excludeSwitches', ['enable-automation'])\n options.add_experimental_option('useAutomationExtension', False)\n\n try:\n service = Service(chromedriver_path)\n driver = webdriver.Chrome(service=service, options=options)\n except:\n driver = webdriver.Chrome(options=options)\n\n driver.set_page_load_timeout(12)\n\n try:\n driver.get(url)\n except Exception:\n # Page load timed out — stop loading and try to extract what's rendered\n driver.execute_script(\"try{window.stop&&window.stop()}catch(e){try{window.document.execCommand('Stop')}catch(e2){}}\")\n _time.sleep(3)\n for _ in range(scroll_rounds):\n try:\n driver.execute_script(\"window.scrollTo(0, document.body.scrollHeight)\")\n except:\n pass\n _time.sleep(1.5)\n _time.sleep(1)\n try:\n result = driver.execute_script(f\"return ({JS_SCRAPE})()\")\n except:\n result = []\n driver.quit()\n\n return _parse_result(result)\n\ndef _scrape_playwright(url, scroll_rounds=4):\n from playwright.sync_api import sync_playwright\n import time as _time\n with sync_playwright() as p:\n browser = p.chromium.launch(\n headless=True,\n args=[\"--no-sandbox\", \"--disable-dev-shm-usage\", \"--disable-setuid-sandbox\"]\n )\n page = browser.new_page(viewport={\"width\": 1400, \"height\": 900})\n try:\n page.goto(url, timeout=20000, wait_until=\"commit\")\n # Wait for SPA content to render (up to 15s)\n try:\n page.wait_for_selector(\"text=热门话题\", timeout=12000)\n except:\n pass\n page.wait_for_timeout(3000)\n # Trigger lazy-load by scrolling\n for _ in range(scroll_rounds):\n page.evaluate(\"window.scrollTo(0, document.body?.scrollHeight || 0)\")\n page.wait_for_timeout(1500)\n except Exception as e:\n print(f\" ⚠️ playwright error: {e}\")\n browser.close()\n return []\n try:\n result = page.evaluate(JS_SCRAPE)\n except Exception:\n result = []\n browser.close()\n return _parse_result(result)\n\ndef _parse_result(raw):\n if not raw:\n return []\n if isinstance(raw, str):\n try: raw = ast.literal_eval(raw)\n except: return []\n posts = []\n for item in (raw or []):\n age = item.get(\"age_mins\", 999)\n coins = [c.replace(\"$\",\"\") for c in (item.get(\"coins\",\"\") or \"\").split(\",\") if c]\n posts.append({\n \"time\": item.get(\"time\",\"\"),\n \"age_mins\": age,\n \"coins\": coins,\n \"text\": item.get(\"text\",\"\").strip(),\n \"likes\": item.get(\"likes\", 0) or 0,\n \"comments\": item.get(\"comments\", 0) or 0,\n })\n return posts\n\ndef report(all_posts, max_age_mins, top_n, coin_type):\n now = datetime.now(timezone(timedelta(hours=8)))\n now_str = now.strftime(\"%Y-%m-%d %H:%M\")\n age_label = f\"{max_age_mins}min\"\n\n inside = [p for p in all_posts if p[\"age_mins\"] \u003c= max_age_mins]\n outside = [p for p in all_posts if p[\"age_mins\"] > max_age_mins]\n\n fin_inside = [p for p in inside if p[\"text\"] and not is_noise(p[\"text\"]) and is_finance(p[\"text\"])]\n fin_outside = [p for p in outside if p[\"text\"] and not is_noise(p[\"text\"]) and is_finance(p[\"text\"])]\n fin_inside.sort(key=score, reverse=True)\n\n # Topic 统计\n topic_counter = Counter()\n topic_fin = defaultdict(list)\n for p in fin_inside + fin_outside:\n t = p.get(\"topic\", \"广场\")\n topic_counter[t] += 1\n topic_fin[t].append(p)\n\n # 小币统计\n coin_counter = Counter()\n coin_sample = {}\n for p in fin_inside + fin_outside:\n for c in p.get(\"coins\", []):\n if coin_type == \"all\" or (coin_type != \"all\" and c):\n coin_counter[c] += 1\n if c not in coin_sample:\n coin_sample[c] = p[\"text\"][:50]\n top_coins = coin_counter.most_common(top_n * 2)\n\n print(f\"\\n{'━'*60}\", flush=True)\n print(f\"📊 币安 Square v8.0 | {now_str} | ≤{age_label} | 币种:{coin_type}\", flush=True)\n print(f\"{'━'*60}\", flush=True)\n print(f\" 总帖子: {len(all_posts)} | 满足: {len(inside)} | 仅计入: {len(outside)}\", flush=True)\n print(f\" 金融帖: {len(fin_inside)} 满足 + {len(fin_outside)} 补充 | 小币: {len(top_coins)}种\", flush=True)\n\n # Topic 统计\n if topic_counter:\n print(f\"\\n🔥 Topic 讨论区热度 (金融帖数)\", flush=True)\n print(f\" {'排名':>4} {'话题':28s} {'金融帖':>6} {'概览':20s}\", flush=True)\n print(f\" {'─'*60}\", flush=True)\n for i, (topic, cnt) in enumerate(topic_counter.most_common(15), 1):\n sample = fin_outside[0][\"text\"][:20] if topic_fin[topic] else \"\"\n print(f\" {i:4}. #{topic:\u003c26s} {cnt:5d}帖\", flush=True)\n\n # 小币\n if top_coins:\n print(f\"\\n🪙 小币实时机会 ({coin_type})\", flush=True)\n print(f\" {'排名':>4} {'币种':10s} {'次':>3} {'信号':>4} {'摘要':30s}\", flush=True)\n print(f\" {'─'*60}\", flush=True)\n for i, (coin, cnt) in enumerate(top_coins[:top_n], 1):\n sig = \"🔥\" if cnt >= 5 else \"⚡\" if cnt >= 3 else \"💤\"\n print(f\" {i:4}. ${coin:\u003c8s} {cnt:3d}次 {sig} {coin_sample.get(coin,'')[:30]}\", flush=True)\n\n # 满足条件的帖子\n if fin_inside:\n print(f\"\\n{'━'*60}\", flush=True)\n print(f\"📌 满足条件帖子 (≤{age_label}) 共{len(fin_inside)}帖\", flush=True)\n print(f\"{'━'*60}\", flush=True)\n for p in fin_inside[:12]:\n coins_s = \" \".join(f\"${c}\" for c in p[\"coins\"][:5]) or \"—\"\n m = p[\"age_mins\"]\n tstr = f\"{m}min前\" if m \u003c 60 else f\"{m//60}h前\"\n topic = p.get(\"topic\", \"\")\n tag = f\"#{topic}\" if topic else \"\"\n print(f\"\\n [{tstr:>8}] {coins_s} {tag}\", flush=True)\n print(f\" {p['text'][:100]}\", flush=True)\n print(f\" 👍{p['likes']} 💬{p['comments']}\", flush=True)\n\n if fin_outside:\n print(f\"\\n{'─'*56}\", flush=True)\n print(f\"📋 不满足条件(仅计入总数) 共{len(fin_outside)}帖:\", flush=True)\n for p in sorted(fin_outside, key=lambda x: x[\"age_mins\"])[:6]:\n coins_s = \" \".join(f\"${c}\" for c in p[\"coins\"][:4]) or \"—\"\n m = p[\"age_mins\"]\n tstr = f\"{m}min前\" if m \u003c 60 else f\"{m//60}h前\"\n topic = p.get(\"topic\", \"\")\n tag = f\"#{topic}\" if topic else \"\"\n print(f\" {tstr:>6} {coins_s:\u003c18s} {tag} {p['text'][:35]}\", flush=True)\n\n print(f\"\\n{'━'*60}\", flush=True)\n print(f\"📋 {now_str} | ≤{age_label} | {len(all_posts)}帖 | {len(fin_inside)}金融帖 | {sum(coin_counter.values())}次小币提及\", flush=True)\n print(f\"{'━'*60}\", flush=True)\n\n with open(\"/tmp/square_signals.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump({\n \"scan_time\": now_str, \"max_age_mins\": max_age_mins,\n \"coin_type\": coin_type,\n \"total\": len(all_posts), \"inside\": len(inside), \"outside\": len(outside),\n \"topic_stats\": [{\"topic\":t,\"count\":c} for t,c in topic_counter.most_common(20)],\n \"coin_stats\": [{\"coin\":c,\"count\":n} for c,n in top_coins],\n \"fin_posts\": [{\"time\":p[\"time\"],\"age_mins\":p[\"age_mins\"],\"coins\":p[\"coins\"],\n \"text\":p[\"text\"][:200],\"topic\":p.get(\"topic\",\"\"),\n \"likes\":p[\"likes\"],\"comments\":p[\"comments\"]}\n for p in fin_inside[:20]],\n }, f, ensure_ascii=False, indent=2)\n\ndef main():\n ap = argparse.ArgumentParser(description=\"Binance Square Scraper v8.0\")\n ap.add_argument(\"--min\", type=int, default=60, help=\"时间精度(分钟)\")\n ap.add_argument(\"--per-topic\", type=int, default=30, help=\"每个Topic最多爬多少帖\")\n ap.add_argument(\"--top\", type=int, default=10, help=\"显示Top N小币\")\n ap.add_argument(\"--coin-type\", choices=[\"all\",\"mainstream\",\"small\",\"meme\"],\n default=\"all\", help=\"主流币/小币/meme/全部\")\n ap.add_argument(\"--topics\", type=int, default=10,\n help=\"爬取Top N个热门话题(0=不爬话题只爬广场)\")\n ap.add_argument(\"--tab\", choices=[\"square\",\"trending\",\"hot\"], default=\"square\")\n args = ap.parse_args()\n\n all_posts = []\n\n # 1. 爬广场页\n if args.topics == 0:\n square_url = URLS.get(args.tab, URLS[\"square\"])\n print(f\"📥 爬取广场页: {square_url}\", flush=True)\n posts = _fetch_posts(square_url, scroll_rounds=4)\n for p in posts:\n p[\"topic\"] = \"广场\"\n all_posts.extend(posts)\n print(f\" ✅ 广场获取{len(posts)}帖\", flush=True)\n else:\n # 2. 爬广场页\n square_url = URLS.get(args.tab, URLS[\"square\"])\n print(f\"📥 爬取广场页: {square_url}\", flush=True)\n posts = _fetch_posts(square_url, scroll_rounds=3)\n for p in posts:\n p[\"topic\"] = \"广场\"\n all_posts.extend(posts)\n print(f\" ✅ 广场获取{len(posts)}帖\", flush=True)\n\n # 3. 爬各Topic页\n topics_to_scrape = DEFAULT_TOPICS[:args.topics]\n print(f\"\\n📥 爬取{len(topics_to_scrape)}个Topic讨论区 (每Topic最多{args.per_topic}帖)\", flush=True)\n with ThreadPoolExecutor(max_workers=2) as ex:\n futures = {\n ex.submit(fetch_one_topic, name, slug, args.per_topic, args.coin_type, args.min): name\n for name, slug in topics_to_scrape\n }\n for f in as_completed(futures):\n name = futures[f]\n try:\n _, url, tp = f.result(timeout=20)\n all_posts.extend(tp)\n mark = \"✅\" if tp else \"⚠️\"\n print(f\" {mark} #{name:\u003c28s} +{len(tp)}帖\", flush=True)\n except TimeoutError:\n print(f\" ⏱️ #{name}: 超时跳过 (20s)\", flush=True)\n except Exception as e:\n print(f\" ❌ #{name}: {e}\", flush=True)\n\n if not all_posts:\n print(\"⚠️ 未获取到任何数据\"); return\n\n # 去重\n seen, unique = set(), []\n for p in all_posts:\n key = p[\"text\"][:60] if p[\"text\"] else \"\"\n if key and key not in seen:\n seen.add(key); unique.append(p)\n all_posts = unique\n\n report(all_posts, max_age_mins=args.min, top_n=args.top, coin_type=args.coin_type)\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":19634,"content_sha256":"a662345b8b365f27664c30e7f1678f4e4d18cf674dac01a674fedc7e361321cb"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Binance Square 热度猎杀","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"作者:Your Name 版权所有 © 2026 Your Name 版本:","type":"text"},{"text":"v12.0","type":"text","marks":[{"type":"strong"}]},{"text":"(CDP Live Session + Multi-Topic + Concurrent)","type":"text"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"概述","type":"text"}]},{"type":"paragraph","content":[{"text":"从 Binance Square 热门话题排行榜获取话题列表,逐个进入话题页抓取最新帖子,提取社区交易信号、情绪方向和小币提及统计。","type":"text"}]},{"type":"paragraph","content":[{"text":"使用场景","type":"text","marks":[{"type":"strong"}]},{"text":":在 Polymarket 交易决策前,扫描 Binance Square 社区情绪,辅助判断短期市场方向。","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"核心依赖","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"组件","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"说明","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Chrome 浏览器","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"已登录的默认配置文件","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Playwright","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pip install playwright && playwright install chromium","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CDP 端口","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"9222(原生)或 18800(OpenClaw)","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"工作流程(SOP)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Step 1 → 打开 trends 页面 → 解析话题列表(名称 + slug)\nStep 2 → 爬取广场页(实时帖子流)\nStep 3 → 逐个进入话题页 → 点击\"最新内容\"Tab → 滚动加载 → 提取帖子\nStep 4 → 合并去重 → 分析 → 汇报","type":"text"}]},{"type":"paragraph","content":[{"text":"关键","type":"text","marks":[{"type":"strong"}]},{"text":":每个话题页有两个 Tab — ","type":"text"},{"text":"热门","type":"text","marks":[{"type":"strong"}]},{"text":"(热度排序)和 ","type":"text"},{"text":"最新内容","type":"text","marks":[{"type":"strong"}]},{"text":"(实时排序)。脚本默认切换到\"最新内容\"以获取最新帖子。","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1 — 检查 / 启动 Chrome CDP","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 检查是否已运行\ncurl -s --max-time 3 http://localhost:9222/json\n\n# 若无,使用启动脚本(自动检测 + 启动 + 运行 + 清理)\n./run_with_chrome.sh --min 60 --topics 10 --per-topic 20","type":"text"}]},{"type":"paragraph","content":[{"text":"run_with_chrome.sh","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 功能","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"检测 Chrome CDP 是否已在 9222 端口运行","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"若无,自动启动 Chrome(User Profile + Remote Debugging)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"运行 scraper","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"脚本结束自动关闭 Chrome(加 ","type":"text"},{"text":"--keep-alive","type":"text","marks":[{"type":"code_inline"}]},{"text":" 保留进程)","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2 — 运行脚本","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"cd ~/clawd/skills/binance-square/scripts\npython3 square_scraper_cdp.py --min 60 --topics 10 --per-topic 20 --scrolls 2 --concurrency 5","type":"text"}]},{"type":"paragraph","content":[{"text":"参数说明","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"参数","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"默认","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"说明","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--min N","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"60","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"只采集 N 分钟内的帖子","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--topics N","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"10","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"最多爬 N 个话题(0=只爬广场不爬话题)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--per-topic N","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"10","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"每个话题最多爬 N 帖","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--scrolls N","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"每话题滚动加载次数(内容多则加大)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--concurrency N","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"并发话题爬取数","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--top N","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"10","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"输出 Top N 小币统计","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--coin-type","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"all","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"all / mainstream / small / meme","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3 — 分析数据","type":"text"}]},{"type":"paragraph","content":[{"text":"输出指标","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"指标","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"说明","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"总帖子数","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"所有话题 + 广场的帖子合计","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"≤60min 帖","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"近 1 小时内的帖子(核心信号窗口)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"金融帖数","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"含交易关键词(做多/做空/开仓/止损等)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"小币种类","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"非主流币提及次数排名前 N","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Topic 热度","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"各话题金融帖数量排行","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Edge 候选","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"同时含金融关键词 + 小币 + 明确方向","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"信号判定","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"信号强度 = 金融帖数 / 总帖数 × 币种分散度\nEdge 候选 = 金融帖中提及小币 + 方向明确(看涨/看跌)","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 4 — 汇报格式","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"📊 币安 Square v12.0 CDP | {时间} | ≤{N}min | {coin_type}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n总帖子: {N} | 满足: {M} | 仅计入: {K}\n金融帖: {F} 满足 + {O} 补充 | 小币: {C}种 | 话题: {T}个\n\n🔥 Topic 讨论区热度\n 1. #话题名 {N}帖 {M}帖\n\n🪙 小币机会 (Top N)\n 1. $KAT 12次 ⚡ 摘要...\n\n📌 满足条件帖子 (≤60min) 共{M}帖\n [5min前] 📈 $BTC $ETH 趋势反转信号...","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"脚本说明","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"v12.0 CDP Multi-Topic(生产用)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 square_scraper_cdp.py --min 60 --topics 10 --per-topic 20 --scrolls 2 --concurrency 5","type":"text"}]},{"type":"paragraph","content":[{"text":"流程","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"连接 Chrome CDP 会话","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"打开 trends 页面 → 解析所有话题的 slug","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"依次进入每个话题页 → 滚动 2 轮 → 提取 innerText","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"纯 Python 正则解析时间戳/币种/互动数","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"合并去重 → 汇总报告","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"v8.0 headless(备用)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"xvfb-run --auto-servernum python3 square_scraper.py --topics 0","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"无需 Chrome 打开,纯 headless 模式","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"限制","type":"text","marks":[{"type":"strong"}]},{"text":":内容受限,Topic 页会触发 rate limit","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"已知限制","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"问题","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"原因","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"解决方案","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CDP 连接失败","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"浏览器未启动","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"./run_with_chrome.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" 会自动启动","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"话题无 slug","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"trends 页面 HTML 解析失败","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"检查 Chrome 是否已打开 trends 页","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Topic 页限流","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"连续高频请求","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"减少 ","type":"text"},{"text":"--topics","type":"text","marks":[{"type":"code_inline"}]},{"text":" 或加长间隔","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"内容为空","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"未登录 / session 过期","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"重新在 Chrome 登录 Binance","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"文件结构","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"skills/binance-square/\n├── SKILL.md ← 本文件\n└── scripts/\n ├── run_with_chrome.sh ← 启动器(自动检测/启动 Chrome + 运行 scraper)\n ├── square_scraper_cdp.py ← v12.0 CDP Multi-Topic(生产用)\n └── square_scraper.py ← v8.0 headless(备用)","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"许可与版权","type":"text"}]},{"type":"paragraph","content":[{"text":"本工具仅供个人学习研究使用。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"版权所有 © 2026 Your Name。保留所有权利。","type":"text"}]}]},"metadata":{"date":"2026-06-05","author":"@skillopedia","source":{"stars":65,"repo_name":"claude-code-skills","origin_url":"https://github.com/aaaaqwq/claude-code-skills/blob/HEAD/skills/binance-square/SKILL.md","repo_owner":"aaaaqwq","body_sha256":"0795b829869fe60435d082baea80f1207a4836b944e9066b0a863c94ea9e1cbd","cluster_key":"212103632c3053ac618ebe8282c8cfd96c0cf23c5ce0cc332bb3c668445f0186","clean_bundle":{"format":"clean-skill-bundle-v1","source":"aaaaqwq/claude-code-skills/skills/binance-square/SKILL.md","attachments":[{"id":"aad3afdd-a09a-593e-bcdb-b9fc2eaa0ad4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aad3afdd-a09a-593e-bcdb-b9fc2eaa0ad4/attachment.sh","path":"scripts/run_with_chrome.sh","size":3255,"sha256":"1a591621d48d9478915285330f3b327bea5f186caca3fec890fd19d5253b6582","contentType":"application/x-sh; charset=utf-8"},{"id":"ad52914f-bcc6-510e-8360-45fd244dc8af","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ad52914f-bcc6-510e-8360-45fd244dc8af/attachment.py","path":"scripts/square_scraper.py","size":19634,"sha256":"a662345b8b365f27664c30e7f1678f4e4d18cf674dac01a674fedc7e361321cb","contentType":"text/x-python; charset=utf-8"},{"id":"e5fac3a6-9996-52b6-b366-8fef09a3df65","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e5fac3a6-9996-52b6-b366-8fef09a3df65/attachment.py","path":"scripts/square_scraper_cdp.py","size":19962,"sha256":"fb6ec679400a1facfe0a399fcde8501f17b77c1a06d5fb915b0290b75bb542a2","contentType":"text/x-python; charset=utf-8"},{"id":"62b3a6cd-cd8d-52df-9f9f-ca12cfda5deb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/62b3a6cd-cd8d-52df-9f9f-ca12cfda5deb/attachment.20250425","path":"scripts/square_scraper_cdp.py.bak.20250425","size":15039,"sha256":"1e471918636cad0959b29d2763e4e59fea818b692fb5a98c1d4eb40712bd4c63","contentType":"application/octet-stream"},{"id":"b8e95834-1273-54a1-be31-482f6808adf6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b8e95834-1273-54a1-be31-482f6808adf6/attachment.py","path":"scripts/square_scraper_live.py","size":14361,"sha256":"1575bc2d72d1ee2f61a6538631683ba371f1fed9299d329d54746296d090b0a3","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"f3720eebb098affcf55cab2a70df765c8935f539c8f1b62088e8235f7b112391","attachment_count":5,"text_attachments":4,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":1,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/binance-square/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"general","category_label":"General"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"general","import_tag":"clean-skills-v1"}},"renderedAt":1782980072719}

Binance Square 热度猎杀 作者:Your Name 版权所有 © 2026 Your Name 版本: v12.0 (CDP Live Session + Multi-Topic + Concurrent) --- 概述 从 Binance Square 热门话题排行榜获取话题列表,逐个进入话题页抓取最新帖子,提取社区交易信号、情绪方向和小币提及统计。 使用场景 :在 Polymarket 交易决策前,扫描 Binance Square 社区情绪,辅助判断短期市场方向。 --- 核心依赖 | 组件 | 说明 | |------|------| | Chrome 浏览器 | 已登录的默认配置文件 | | Playwright | | | CDP 端口 | 9222(原生)或 18800(OpenClaw) | --- 工作流程(SOP) 关键 :每个话题页有两个 Tab — 热门 (热度排序)和 最新内容 (实时排序)。脚本默认切换到"最新内容"以获取最新帖子。 Step 1 — 检查 / 启动 Chrome CDP 功能 : 1. 检测 Chrome CDP 是否已在 9222 端口运行 2. 若无,自动启动 Chrome(User Profile + Remote Debugging) 3. 运行 scraper 4. 脚本结束自动关闭 Chrome(加 保留进程…