Quellcode durchsuchen

chore(xiaohongshu): 笔记评论抓取工具脚本(note_id 提取/评论 API/响应拦截)

Sisyphus Agent vor 1 Monat
Ursprung
Commit
fda188c6e0

+ 45 - 0
运营文案/小红书发布/_process_scripts/detail_screenshot.py

@@ -0,0 +1,45 @@
+"""打开笔记详情页,等待充分加载后截图+抓评论"""
+import json, time, websocket, urllib.request, base64
+
+tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:9222/json").read())
+ws_url = None
+for t in tabs:
+    if "creator.xiaohongshu.com" in t.get("url", "") or "xiaohongshu.com" in t.get("url", ""):
+        ws_url = t["webSocketDebuggerUrl"]
+        break
+
+ws = websocket.create_connection(ws_url, timeout=30)
+msg_id = 0
+
+def send_cmd(method, params=None):
+    global msg_id
+    msg_id += 1
+    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
+    while True:
+        resp = json.loads(ws.recv())
+        if resp.get("id") == msg_id:
+            return resp.get("result", {})
+
+send_cmd("Page.enable")
+
+# 打开坦白局详情
+send_cmd("Page.navigate", {"url": "https://www.xiaohongshu.com/explore/6a82ca500000000025004062"})
+time.sleep(12)
+
+# 截图
+result = send_cmd("Page.captureScreenshot", {"format": "png"})
+img_data = result.get("data", "")
+if img_data:
+    with open(r"C:\code\cfc\运营文案\小红书发布\_process_scripts\tanbaiju_detail.png", "wb") as f:
+        f.write(base64.b64decode(img_data))
+    print("Screenshot saved")
+
+# 抓页面文字
+js = """(function() { return document.body.innerText.substring(0, 8000); })()"""
+result = send_cmd("Runtime.evaluate", {"expression": js, "returnByValue": True})
+val = result.get("result", {}).get("value", "")
+with open(r"C:\code\cfc\运营文案\小红书发布\_process_scripts\tanbaiju_detail.txt", "w", encoding="utf-8") as f:
+    f.write(val)
+print("Text saved, length:", len(val))
+
+ws.close()

+ 81 - 0
运营文案/小红书发布/_process_scripts/extract_fiber.py

@@ -0,0 +1,81 @@
+"""从React fiber提取note-info中的note_id"""
+import json, time, websocket, urllib.request
+
+tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:9222/json").read())
+ws_url = None
+for t in tabs:
+    if "creator.xiaohongshu.com" in t.get("url", ""):
+        ws_url = t["webSocketDebuggerUrl"]
+        break
+
+ws = websocket.create_connection(ws_url, timeout=15)
+msg_id = 0
+
+def send_cmd(method, params=None):
+    global msg_id
+    msg_id += 1
+    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
+    while True:
+        resp = json.loads(ws.recv())
+        if resp.get("id") == msg_id:
+            return resp.get("result", {})
+
+# 确保在笔记管理页
+send_cmd("Page.navigate", {"url": "https://creator.xiaohongshu.com/new/note-manager"})
+time.sleep(6)
+
+# 从React fiber提取所有笔记的完整信息
+js = """
+(function() {
+    var results = [];
+    var cards = document.querySelectorAll('.note-card');
+    for (var i = 0; i < cards.length; i++) {
+        var card = cards[i];
+        // 找React fiber key
+        var key = Object.keys(card).find(function(k) { return k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance'); });
+        if (!key) continue;
+        
+        var fiber = card[key];
+        var current = fiber;
+        var noteInfo = null;
+        
+        // 向上遍历找noteInfo
+        for (var j = 0; j < 15; j++) {
+            if (!current) break;
+            var props = current.memoizedProps || current.pendingProps || {};
+            if (props.noteInfo && typeof props.noteInfo === 'object' && props.noteInfo.noteId) {
+                noteInfo = props.noteInfo;
+                break;
+            }
+            // 也检查stateNode
+            if (current.stateNode && current.stateNode.props && current.stateNode.props.noteInfo) {
+                noteInfo = current.stateNode.props.noteInfo;
+                break;
+            }
+            current = current.return;
+        }
+        
+        if (noteInfo) {
+            results.push({
+                noteId: noteInfo.noteId,
+                title: noteInfo.displayTitle || noteInfo.title || '',
+                commentCount: noteInfo.commentCount || 0,
+                likeCount: noteInfo.likedCount || noteInfo.likeCount || 0,
+                shareCount: noteInfo.shareCount || 0,
+                collectCount: noteInfo.collectedCount || noteInfo.collectCount || 0
+            });
+        } else {
+            // fallback: 只拿标题
+            var title = card.querySelector('.note-card__title');
+            results.push({noteId: 'UNKNOWN', title: title ? title.innerText : 'no title'});
+        }
+    }
+    return JSON.stringify(results, null, 2);
+})()
+"""
+result = send_cmd("Runtime.evaluate", {"expression": js, "returnByValue": True})
+val = result.get("result", {}).get("value", "")
+with open(r"C:\code\cfc\运营文案\小红书发布\_process_scripts\note_info.json", "w", encoding="utf-8") as f:
+    f.write(val)
+print("DONE")
+ws.close()

+ 90 - 0
运营文案/小红书发布/_process_scripts/extract_note_ids.py

@@ -0,0 +1,90 @@
+"""从笔记管理页DOM提取note_id,然后访问详情看评论"""
+import json, time, websocket, urllib.request
+
+tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:9222/json").read())
+ws_url = None
+for t in tabs:
+    if "creator.xiaohongshu.com" in t.get("url", ""):
+        ws_url = t["webSocketDebuggerUrl"]
+        break
+
+ws = websocket.create_connection(ws_url, timeout=15)
+msg_id = 0
+
+def send_cmd(method, params=None):
+    global msg_id
+    msg_id += 1
+    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
+    while True:
+        resp = json.loads(ws.recv())
+        if resp.get("id") == msg_id:
+            return resp.get("result", {})
+
+# 确保在笔记管理页
+send_cmd("Page.navigate", {"url": "https://creator.xiaohongshu.com/new/note-manager"})
+time.sleep(6)
+
+# 提取所有笔记的note_id(从data属性、链接、或React state中找)
+js = """
+(function() {
+    var results = [];
+    
+    // 方案1: 找所有data-note-id或类似属性
+    document.querySelectorAll('[data-note-id], [data-id], [data-noteid]').forEach(function(el) {
+        results.push({attr: 'data', noteId: el.getAttribute('data-note-id') || el.getAttribute('data-id') || el.getAttribute('data-noteid'), text: (el.innerText||'').substring(0,40)});
+    });
+    
+    // 方案2: 找React内部状态
+    var noteCards = document.querySelectorAll('.note-card');
+    noteCards.forEach(function(card) {
+        // React fiber
+        var key = Object.keys(card).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance'));
+        if (key) {
+            var fiber = card[key];
+            // 向上遍历找memoizedProps中的noteId
+            var current = fiber;
+            for (var i = 0; i < 10; i++) {
+                if (!current) break;
+                var props = current.memoizedProps || {};
+                if (props.noteId || props.note_id || props.id) {
+                    var title = card.querySelector('.note-card__title');
+                    results.push({attr: 'react', noteId: props.noteId || props.note_id || props.id, title: title ? title.innerText : ''});
+                    break;
+                }
+                current = current.return;
+            }
+        }
+    });
+    
+    // 方案3: 找所有包含数字ID的href
+    document.querySelectorAll('a[href]').forEach(function(a) {
+        var href = a.getAttribute('href');
+        if (href && /note[a-z]*\/[a-f0-9]{24}/.test(href)) {
+            results.push({attr: 'href', noteId: href, text: (a.innerText||'').substring(0,40)});
+        }
+    });
+    
+    // 方案4: 找__NEXT_DATA__或全局状态
+    if (window.__NEXT_DATA__) {
+        results.push({attr: 'next_data', keys: Object.keys(window.__NEXT_DATA__).join(',')});
+    }
+    
+    // 方案5: 查看note-card的所有属性
+    var firstCard = document.querySelector('.note-card');
+    if (firstCard) {
+        var attrs = {};
+        for (var i = 0; i < firstCard.attributes.length; i++) {
+            attrs[firstCard.attributes[i].name] = firstCard.attributes[i].value.substring(0, 50);
+        }
+        results.push({attr: 'card_attrs', attrs: attrs});
+    }
+    
+    return JSON.stringify(results, null, 2);
+})()
+"""
+result = send_cmd("Runtime.evaluate", {"expression": js, "returnByValue": True})
+val = result.get("result", {}).get("value", "")
+with open(r"C:\code\cfc\运营文案\小红书发布\_process_scripts\note_ids.txt", "w", encoding="utf-8") as f:
+    f.write(val)
+print("DONE")
+ws.close()

+ 89 - 0
运营文案/小红书发布/_process_scripts/fetch_comments_api.py

@@ -0,0 +1,89 @@
+"""用页面cookie调评论API获取每篇笔记的评论"""
+import json, time, websocket, urllib.request
+
+tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:9222/json").read())
+ws_url = None
+for t in tabs:
+    if "creator.xiaohongshu.com" in t.get("url", ""):
+        ws_url = t["webSocketDebuggerUrl"]
+        break
+
+ws = websocket.create_connection(ws_url, timeout=30)
+msg_id = 0
+
+def send_cmd(method, params=None):
+    global msg_id
+    msg_id += 1
+    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
+    while True:
+        resp = json.loads(ws.recv())
+        if resp.get("id") == msg_id:
+            return resp.get("result", {})
+
+send_cmd("Page.navigate", {"url": "https://creator.xiaohongshu.com/new/note-manager"})
+time.sleep(5)
+
+# 拦截评论API
+send_cmd("Network.enable")
+
+# 导航到坦白局笔记详情(触发评论API加载)
+# 先尝试直接调评论API
+note_ids = [
+    ("坦白局", "6a82ca500000000025004062"),
+    ("D27教育博主", "6a81a53d000000000600738e"),
+]
+
+all_comments = {}
+for name, nid in note_ids:
+    js = f"""
+    (async function() {{
+        try {{
+            // 尝试多种评论API端点
+            var urls = [
+                '/api/galaxy/creator/note/comment?noteId={nid}&cursor=&num=20',
+                '/api/galaxy/v2/creator/note/comment?noteId={nid}&cursor=&num=20',
+                '/api/galaxy/creator/comment/list?noteId={nid}&cursor=&num=20',
+                '/api/galaxy/creator/note/comments?noteId={nid}&cursor=&num=20'
+            ];
+            var results = [];
+            for (var i = 0; i < urls.length; i++) {{
+                try {{
+                    var r = await fetch(urls[i], {{credentials: 'include'}});
+                    var text = await r.text();
+                    if (text.indexOf('"success":true') >= 0 || text.indexOf('"code":0') >= 0) {{
+                        results.push({{url: urls[i], data: text.substring(0, 5000)}});
+                    }}
+                }} catch(e) {{}}
+            }}
+            if (results.length === 0) {{
+                // 也试试edith域名
+                var edith_urls = [
+                    'https://edith.xiaohongshu.com/api/sns/web/comment/page?note_id={nid}&cursor=&top_comment_id=&image_formats=jpg,webp,avif&xsec_token=YBieUL8uRjxi1dmXwcv1fHXDs3jL1VSMg8urb7O07P_uc='
+                ];
+                for (var j = 0; j < edith_urls.length; j++) {{
+                    try {{
+                        var r2 = await fetch(edith_urls[j], {{credentials: 'include'}});
+                        var t2 = await r2.text();
+                        results.push({{url: edith_urls[j], data: t2.substring(0, 5000)}});
+                    }} catch(e) {{}}
+                }}
+            }}
+            return JSON.stringify(results);
+        }} catch(e) {{ return 'ERR:' + e.message; }}
+    }})()
+    """
+    ws.send(json.dumps({"id": 300 + note_ids.index((name, nid)), "method": "Runtime.evaluate", "params": {"expression": js, "returnByValue": True, "awaitPromise": True}}))
+    
+for i in range(len(note_ids)):
+    for _ in range(30):
+        resp = json.loads(ws.recv())
+        rid = resp.get("id", 0)
+        if rid >= 300 and rid < 300 + len(note_ids):
+            val = resp.get("result", {}).get("result", {}).get("value", "")
+            name = note_ids[rid - 300][0]
+            with open(rf"C:\code\cfc\运营文案\小红书发布\_process_scripts\comments_{name}.txt", "w", encoding="utf-8") as f:
+                f.write(val)
+            print(f"Saved comments for {name}")
+            break
+
+ws.close()

+ 96 - 0
运营文案/小红书发布/_process_scripts/find_global_state.py

@@ -0,0 +1,96 @@
+"""暴力提取:从window.__INITIAL_STATE__或Vue根组件拿笔记ID"""
+import json, time, websocket, urllib.request
+
+tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:9222/json").read())
+ws_url = None
+for t in tabs:
+    if "creator.xiaohongshu.com" in t.get("url", ""):
+        ws_url = t["webSocketDebuggerUrl"]
+        break
+
+ws = websocket.create_connection(ws_url, timeout=15)
+msg_id = 0
+
+def send_cmd(method, params=None):
+    global msg_id
+    msg_id += 1
+    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
+    while True:
+        resp = json.loads(ws.recv())
+        if resp.get("id") == msg_id:
+            return resp.get("result", {})
+
+send_cmd("Page.navigate", {"url": "https://creator.xiaohongshu.com/new/note-manager"})
+time.sleep(6)
+
+# 暴力搜索window对象中所有包含noteId的属性
+js = """
+(function() {
+    var results = [];
+    
+    // 搜索window上的全局状态
+    var keys = ['__INITIAL_STATE__', '__NUXT__', '__APP_DATA__', '__PRELOADED_STATE__'];
+    for (var i = 0; i < keys.length; i++) {
+        if (window[keys[i]]) {
+            results.push({source: keys[i], type: typeof window[keys[i]]});
+        }
+    }
+    
+    // 搜索Vue实例
+    var app = document.querySelector('#app') || document.querySelector('[id]');
+    if (app && app.__vue__) {
+        results.push({source: 'vue_root', type: 'found'});
+        // 遍历vue data
+        try {
+            var vueData = JSON.stringify(app.__vue__.$data || {}).substring(0, 2000);
+            results.push({source: 'vue_data', data: vueData});
+        } catch(e) {
+            results.push({source: 'vue_data_error', msg: e.message});
+        }
+    }
+    
+    // 搜索所有__vue__实例
+    var vueEls = document.querySelectorAll('*');
+    var vueCount = 0;
+    for (var j = 0; j < vueEls.length && vueCount < 3; j++) {
+        if (vueEls[j].__vue__) {
+            vueCount++;
+            try {
+                var d = vueEls[j].__vue__.$data;
+                var str = JSON.stringify(d);
+                if (str.indexOf('noteId') >= 0 || str.indexOf('note_id') >= 0) {
+                    results.push({source: 'vue_with_noteId', data: str.substring(0, 3000)});
+                }
+            } catch(e) {}
+        }
+    }
+    
+    // 搜索React root
+    var rootEl = document.getElementById('root') || document.getElementById('app');
+    if (rootEl) {
+        var rKey = Object.keys(rootEl).find(function(k) { return k.startsWith('__reactContainer') || k.startsWith('__reactFiber'); });
+        if (rKey) {
+            results.push({source: 'react_root', key: rKey});
+        }
+    }
+    
+    // 最后手段:搜索所有script标签中的JSON数据
+    document.querySelectorAll('script').forEach(function(s) {
+        var text = s.textContent || '';
+        if (text.indexOf('noteId') >= 0 || text.indexOf('note_id') >= 0) {
+            var match = text.match(/"noteId"\s*:\s*"([a-f0-9]+)"/g);
+            if (match) {
+                results.push({source: 'script_tag', noteIds: match.slice(0, 10)});
+            }
+        }
+    });
+    
+    return JSON.stringify(results, null, 2);
+})()
+"""
+result = send_cmd("Runtime.evaluate", {"expression": js, "returnByValue": True})
+val = result.get("result", {}).get("value", "")
+with open(r"C:\code\cfc\运营文案\小红书发布\_process_scripts\global_state.txt", "w", encoding="utf-8") as f:
+    f.write(val)
+print("DONE")
+ws.close()

+ 58 - 0
运营文案/小红书发布/_process_scripts/get_response_body.py

@@ -0,0 +1,58 @@
+"""获取已拦截的API响应体"""
+import json, time, websocket, urllib.request
+
+tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:9222/json").read())
+ws_url = None
+for t in tabs:
+    if "creator.xiaohongshu.com" in t.get("url", ""):
+        ws_url = t["webSocketDebuggerUrl"]
+        break
+
+ws = websocket.create_connection(ws_url, timeout=30)
+msg_id = 0
+
+def send_cmd(method, params=None):
+    global msg_id
+    msg_id += 1
+    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
+    while True:
+        resp = json.loads(ws.recv())
+        if resp.get("id") == msg_id:
+            return resp.get("result", {})
+
+# 启用Network
+send_cmd("Network.enable")
+
+# 导航到笔记管理
+send_cmd("Page.navigate", {"url": "https://creator.xiaohongshu.com/new/note-manager"})
+
+# 收集响应ID
+galaxy_responses = []
+start = time.time()
+while time.time() - start < 10:
+    try:
+        ws.settimeout(0.5)
+        msg = json.loads(ws.recv())
+        if msg.get("method") == "Network.responseReceived":
+            url = msg.get("params", {}).get("response", {}).get("url", "")
+            req_id = msg.get("params", {}).get("requestId", "")
+            if "galaxy" in url and "note" in url:
+                galaxy_responses.append({"id": req_id, "url": url})
+    except:
+        continue
+
+print(f"Found {len(galaxy_responses)} galaxy note API responses")
+
+# 获取每个响应的body
+for item in galaxy_responses:
+    try:
+        result = send_cmd("Network.getResponseBody", {"requestId": item["id"]})
+        body = result.get("body", "")
+        with open(r"C:\code\cfc\运营文案\小红书发布\_process_scripts\galaxy_resp.txt", "w", encoding="utf-8") as f:
+            f.write("URL: " + item["url"] + "\n\n" + body[:10000])
+        print(f"Saved response from {item['url']}")
+        print(body[:500])
+    except Exception as e:
+        print(f"Error getting body for {item['url']}: {e}")
+
+ws.close()

+ 84 - 0
运营文案/小红书发布/_process_scripts/intercept_responses.py

@@ -0,0 +1,84 @@
+"""拦截页面加载时的API响应,获取笔记数据"""
+import json, time, websocket, urllib.request
+
+tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:9222/json").read())
+ws_url = None
+for t in tabs:
+    if "creator.xiaohongshu.com" in t.get("url", ""):
+        ws_url = t["webSocketDebuggerUrl"]
+        break
+
+ws = websocket.create_connection(ws_url, timeout=30)
+msg_id = 0
+responses = {}
+
+def send_cmd(method, params=None):
+    global msg_id
+    msg_id += 1
+    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
+    while True:
+        resp = json.loads(ws.recv())
+        if resp.get("id") == msg_id:
+            return resp.get("result", {})
+
+# 启用网络和响应监听
+send_cmd("Network.enable")
+
+# 导航到笔记管理页
+send_cmd("Page.navigate", {"url": "https://creator.xiaohongshu.com/new/note-manager"})
+
+# 收集所有网络响应
+start = time.time()
+while time.time() - start < 15:
+    try:
+        ws.settimeout(1)
+        msg = json.loads(ws.recv())
+        if msg.get("method") == "Network.responseReceived":
+            url = msg.get("params", {}).get("response", {}).get("url", "")
+            req_id = msg.get("params", {}).get("requestId", "")
+            if "galaxy" in url and ("note" in url or "comment" in url):
+                responses[req_id] = url
+        elif msg.get("method") == "Network.loadingFinished":
+            req_id = msg.get("params", {}).get("requestId", "")
+            if req_id in responses:
+                # 获取响应体
+                send_cmd("Network.getResponseBody", {"requestId": req_id})
+    except:
+        continue
+
+# 尝试读取已收集的响应
+time.sleep(1)
+
+# 如果没抓到响应体,用Runtime.evaluate直接读XHR
+js = """
+(async function() {
+    // 直接用已知的cookie请求v2 API
+    try {
+        var r = await fetch('/api/galaxy/v2/creator/note/user/posted?tab=0&page=0', {
+            credentials: 'include',
+            headers: {
+                'Accept': 'application/json',
+                'X-Requested-With': 'XMLHttpRequest'
+            }
+        });
+        var text = await r.text();
+        return text.substring(0, 8000);
+    } catch(e) {
+        return 'ERR: ' + e.message;
+    }
+})()
+"""
+ws.send(json.dumps({"id": 200, "method": "Runtime.evaluate", "params": {"expression": js, "returnByValue": True, "awaitPromise": True}}))
+for _ in range(30):
+    resp = json.loads(ws.recv())
+    if resp.get("id") == 200:
+        val = resp.get("result", {}).get("result", {}).get("value", "")
+        with open(r"C:\code\cfc\运营文案\小红书发布\_process_scripts\v2_response.txt", "w", encoding="utf-8") as f:
+            f.write(val)
+        print("Saved v2 response")
+        break
+
+ws.close()
+print(f"Intercepted {len(responses)} galaxy API URLs")
+for rid, url in responses.items():
+    print(f"  {rid}: {url}")

+ 41 - 0
运营文案/小红书发布/_process_scripts/open_detail.py

@@ -0,0 +1,41 @@
+"""打开每篇笔记的前台详情页,抓取评论"""
+import json, time, websocket, urllib.request
+
+tabs = json.loads(urllib.request.urlopen("http://127.0.0.1:9222/json").read())
+ws_url = None
+for t in tabs:
+    if "creator.xiaohongshu.com" in t.get("url", ""):
+        ws_url = t["webSocketDebuggerUrl"]
+        break
+
+ws = websocket.create_connection(ws_url, timeout=30)
+msg_id = 0
+
+def send_cmd(method, params=None):
+    global msg_id
+    msg_id += 1
+    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
+    while True:
+        resp = json.loads(ws.recv())
+        if resp.get("id") == msg_id:
+            return resp.get("result", {})
+
+# 坦白局笔记详情页
+notes = [
+    ("坦白局", "6a82ca500000000025004062"),
+    ("D27教育博主", "6a81a53d000000000600738e"),
+]
+
+for name, nid in notes:
+    url = f"https://www.xiaohongshu.com/explore/{nid}"
+    send_cmd("Page.navigate", {"url": url})
+    time.sleep(8)
+    
+    js = """(function() { return document.body.innerText.substring(0, 6000); })()"""
+    result = send_cmd("Runtime.evaluate", {"expression": js, "returnByValue": True})
+    val = result.get("result", {}).get("value", "")
+    with open(rf"C:\code\cfc\运营文案\小红书发布\_process_scripts\detail_{name}.txt", "w", encoding="utf-8") as f:
+        f.write("URL: " + url + "\n\n" + val)
+    print(f"Saved detail for {name}")
+
+ws.close()