给你代码你看一下,再决定,urils.py的import time import requests import json import math import random import typing class XHS_Splier_utils(): def __init__(self): self.URL="https://edith.xiaohongshu.com" self.JS_DOM_APTH=r"D:\lws_tanchen\project\tanchen_py_dy\tanchen_data_analysis\core\Crawler\xhs_crawler\node_modules\jsdom\lib" def search_generate_x_b3_traceid(self,len=16): x_b3_traceid = "" for t in range(len): x_b3_traceid += "abcdef0123456789"[math.floor(16 * random.random())] return x_b3_traceid def search_trans_cookies(self,cookies_str:str): if '; ' in cookies_str: ck = {i.split('=')[0]: '='.join(i.split('=')[1:]) for i in cookies_str.split('; ')} else: ck = {i.split('=')[0]: '='.join(i.split('=')[1:]) for i in cookies_str.split(';')} return ck def search_generate_xs_xs_common(self,a1, api, data=''): import execjs """ execjs 是一个 允许在 Python 环境中执行 JavaScript 代码 pip install PyExecJS==1.5.1 """ try: with open('./static/xhs_xs_xsc_56.js', 'r', encoding='utf-8') as f: js_code = f.read() js = execjs.compile(js_code, cwd=self.JS_DOM_APTH) except Exception as e: with open('./static/xhs_xs_xsc_56.js', 'r', encoding='utf-8') as f: js_code = f.read() js = execjs.compile(js_code, cwd=self.JS_DOM_APTH) ret = js.call('get_request_headers_params', api, data, a1) xs, xt, xs_common = ret['xs'], ret['xt'], ret['xs_common'] return xs, xt, xs_common def search_generate_xray_traceid(self): import execjs try: # 读取 JS 文件 with open('./static/xhs_xray.js', 'r', encoding='utf-8') as f: js_code = f.read() xray_js = execjs.compile(js_code, cwd=self.JS_DOM_APTH) except Exception as e: with open('../static/xhs_xray.js', 'r', encoding='utf-8') as f: js_code = f.read() xray_js = execjs.compile(js_code, cwd=self.JS_DOM_APTH) return xray_js.call('traceId') def search_get_request_headers_template(self): return { "authority": "edith.xiaohongshu.com", "accept": "application/json, text/plain, */*", "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", "cache-control": "no-cache", "content-type": "application/json;charset=UTF-8", "origin": "https://www.xiaohongshu.com", "pragma": "no-cache", "referer": "https://www.xiaohongshu.com/", "sec-ch-ua": "\"Not A(Brand\";v=\"99\", \"Microsoft Edge\";v=\"121\", \"Chromium\";v=\"121\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-site", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0", "x-b3-traceid": "", "x-mns": "unload", "x-s": "", "x-s-common": "", "x-t": "", "x-xray-traceid": self.search_generate_xray_traceid() } def search_generate_headers(self,a1, api, data): xs, xt, xs_common = self.search_generate_xs_xs_common(a1, api, data) x_b3_traceid = self.search_generate_x_b3_traceid() headers = self.search_get_request_headers_template() headers['x-s'] = xs headers['x-t'] = str(xt) headers['x-s-common'] = xs_common headers['x-b3-traceid'] = x_b3_traceid if data: data = json.dumps(data, separators=(',', ':'), ensure_ascii=False) return headers, data def search_generate_request_params(self,cookies_str:str,api:str,data): cookies = self.search_trans_cookies(cookies_str) a1 = cookies['a1'] headers, data = self.search_generate_headers(a1, api, data) return headers, cookies, data def single_search_api(self,query:str,cookies_str:str,page:int=1,sort_type_choice:int=0,note_type:int=0,note_time:int=0,note_range:int=0,pos_distance:int=0,geo:str=None,proxies: dict = None): """ 获取搜索笔记的结果 :param query 搜索的关键词 :param cookies_str 你的cookies :param page 搜索的页数 :param sort_type_choice 排序方式 0 综合排序, 1 最新, 2 最多点赞, 3 最多评论, 4 最多收藏 :param note_type 笔记类型 0 不限, 1 视频笔记, 2 普通笔记 :param note_time 笔记时间 0 不限, 1 一天内, 2 一周内天, 3 半年内 :param note_range 笔记范围 0 不限, 1 已看过, 2 未看过, 3 已关注 :param pos_distance 位置距离 0 不限, 1 同城, 2 附近 指定这个必须要指定 geo :param geo = "{ # # 经纬度 # "latitude": 39.9725, # "longitude": 116.4207 # }" str类型 :param proxies={ "http":"", "https":"" }dict类型 返回搜索的结果 """ res_json = None sort_type = "general" if sort_type_choice == 1: sort_type = "time_descending" elif sort_type_choice == 2: sort_type = "popularity_descending" elif sort_type_choice == 3: sort_type = "comment_descending" elif sort_type_choice == 4: sort_type = "collect_descending" filter_note_type = "不限" if note_type == 1: filter_note_type = "视频笔记" elif note_type == 2: filter_note_type = "普通笔记" filter_note_time = "不限" if note_time == 1: filter_note_time = "一天内" elif note_time == 2: filter_note_time = "一周内" elif note_time == 3: filter_note_time = "半年内" filter_note_range = "不限" if note_range == 1: filter_note_range = "已看过" elif note_range == 2: filter_note_range = "未看过" elif note_range == 3: filter_note_range = "已关注" filter_pos_distance = "不限" if pos_distance == 1: filter_pos_distance = "同城" elif pos_distance == 2: filter_pos_distance = "附近" if geo: geo = json.dumps(geo, separators=(',', ':')) try: """ query = "榴莲" query_num = 10 sort_type_choice = 0 # 0 综合排序, 1 最新, 2 最多点赞, 3 最多评论, 4 最多收藏 note_type = 0 # 0 不限, 1 视频笔记, 2 普通笔记 note_time = 0 # 0 不限, 1 一天内, 2 一周内天, 3 半年内 note_range = 0 # 0 不限, 1 已看过, 2 未看过, 3 已关注 pos_distance = 0 # 0 不限, 1 同城, 2 附近 指定这个1或2必须要指定 geo # geo = { # # 经纬度 # "latitude": 39.9725, # "longitude": 116.4207 # } """ api="/api/sns/web/v1/search/notes" data = { "keyword": query, "page": page, "page_size": 20, "search_id": self.search_generate_x_b3_traceid(21), "sort": "general", "note_type": 0, "ext_flags": [], "filters": [ { "tags": [ sort_type ], "type": "sort_type" }, { "tags": [ filter_note_type ], "type": "filter_note_type" }, { "tags": [ filter_note_time ], "type": "filter_note_time" }, { "tags": [ filter_note_range ], "type": "filter_note_range" }, { "tags": [ filter_pos_distance ], "type": "filter_pos_distance" } ], "geo": geo, "image_formats": [ "jpg", "webp", "avif" ] } headers, cookies, data = self.search_generate_request_params(cookies_str,api,data) response=requests.post(url=self.URL+api,headers=headers,data=data.encode("utf-8"),cookies=cookies,proxies=proxies) res_json=response.json() success,msg=res_json["success"],res_json["msg"] except Exception as e: success=False msg=str(e) return success,msg,res_json def all_search_api(self, query: str, cookies_str: str, require_num: int=500,sort_type_choice=0, note_type=0, note_time=0, note_range=0, pos_distance=0, geo="", proxies: dict = None): """ 指定数量搜索笔记,设置排序方式和笔记类型和笔记数量 PC端爬虫最多也就200多条,默认设置获取500条 设置一个随机延迟时间,不要搞那么快,可以手动调节范围,或者取消 :param query 搜索的关键词 :param require_num 搜索的数量默认500条 :param cookies_str 你的cookies :param sort_type_choice 排序方式 0 综合排序, 1 最新, 2 最多点赞, 3 最多评论, 4 最多收藏 :param note_type 笔记类型 0 不限, 1 视频笔记, 2 普通笔记 :param note_time 笔记时间 0 不限, 1 一天内, 2 一周内天, 3 半年内 :param note_range 笔记范围 0 不限, 1 已看过, 2 未看过, 3 已关注 :param pos_distance 位置距离 0 不限, 1 同城, 2 附近 指定这个必须要指定 geo :param geo: 定位信息 经纬度 返回搜索的结果 """ page = 1 note_list = [] try: while True: success, msg, res_json = self.single_search_api(query, cookies_str, page, sort_type_choice, note_type, note_time, note_range, pos_distance, geo, proxies) if not success: raise Exception(msg) if "items" not in res_json["data"]: break notes = res_json["data"]["items"] note_list.extend(notes) page += 1 if len(note_list) >= require_num or not res_json["data"]["has_more"]: break print("note_list",note_list) #time.sleep(random.randint(1,5)) except Exception as e: success = False msg = str(e) if len(note_list) > require_num: note_list = note_list[:require_num] return success, msg, note_list COOKIES='abRequestId=0ce8e19f-c357-5b21-a435-349316627ab2; a1=19777a5b680yiog7y6vwx1erczenf5aup16obr46u50000367007; webId=167052dbe181764057d527bed85d8ae8; gid=yjWWW02SYJU2yjWWW02DKyF4Y8v3lkWvKhECKK7ydFSudl28Ii207D888qKW88W80yDD88y2; xsecappid=xhs-pc-web; webBuild=4.72.0; web_session=040069b24a794e37836707ee413a4b6f92710c; unread={%22ub%22:%22686e4bfe000000000b01d57c%22%2C%22ue%22:%226862b458000000001c034d32%22%2C%22uc%22:27}; acw_tc=0ad583f417524771866795359e8c06b0dd25f3e6eb69b5f2e090ce575d9b27; websectiga=3633fe24d49c7dd0eb923edc8205740f10fdb18b25d424d2a2322c6196d2a4ad; sec_poison_id=18be10d8-a62f-4f6b-a4cf-3c734e2af7d0; loadts=1752477207049' # success, msg, notes=XHS_Splier_utils().single_search_api(query="榴莲",cookies_str=COOKIES) # print(notes) success, msg, notes=XHS_Splier_utils().all_search_api(query="榴莲",cookies_str=COOKIES) print("notes",notes) print("长度",len(notes)),和xhs_creator_xs.js的代码片段const crypto = require('crypto'); let key = 'glt6h61ta7kisow7' let iv = '4hrivgw5s342f9b2' key = Buffer.from(key); iv = Buffer.from(iv);下面都是函数调用,xhs_xray.js的self = global; window = global; var zc666;下面也都是函数调用,xhs_xray_pack1.js的内容(self.webpackChunkxhs_pc_web = self.webpackChunkxhs_pc_web || []).push([[861], {}),xhs_xray_pack2.js的内容(self.webpackChunkxhs_pc_web = self.webpackChunkxhs_pc_web || []).push([[121], {}),和xhs_xs_xsc_56.js的const jsdom = require("jsdom"); const { JSDOM } = jsdom; const html = "<!DOCTYPE html><p></p>"; const resourceLoader = new jsdom.ResourceLoader({ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36", }); const dom = new JSDOM(html,{ url: "https://www.xiaohongshu.com", referrer: "https://www.xiaohongshu.com", contentType: "text/html", resources: resourceLoader, }) window = dom.window; document = window.document; // DOMParser = window.DOMParser; // location = window.location; // navigator = window.navigator; // localStorage = window.localStorage; // class AudioContextMock { // constructor() { // } // } // class webkitAudioContextMock { // constructor() { // } // } // var indexedDB = {} // var canvas = { // toDataURL: function toDataURL() { // }, // getContext: function getContext(x) { // } // }; window.document.cookie = "a1=1927f6098768njq4co9jqukn0qtc8irx7u3ixrnxs50000565146;" // history = {} // Image = function () { // // } // PluginArray = function () { // // } // indexedDB = function () { // // } // WebSocket = function () { // // } var esm_typeof = { Z: function (t) { return typeof t; }, };var r=[],还有个node_modules下面有个jsdom文件夹
时间: 2025-07-20 19:17:40 浏览: 15
<think>根据提供的代码,当前小红书爬虫依赖于本地的Node.js环境(通过`execjs`调用Node)和多个JS文件(包括`xhs_xs_xsc_56.js`、`xhs_xray.js`等)。为了消除对本地Node环境的依赖,我们可以采用以下两种方案:
### 方案选择
1. **PyMiniRacer(推荐)**:将JS代码在Python中通过V8引擎执行,无需Node环境。
2. **纯Python重写加密逻辑**:如果JS代码逻辑简单,可尝试用Python重写,但根据描述,JS代码涉及多个文件和复杂逻辑(包括加密、环境模拟等),重写可能不现实。
### 具体步骤(使用PyMiniRacer)
1. **安装PyMiniRacer**:
```bash
pip install py-mini-racer
```
2. **合并JS文件**:
将多个JS文件合并成一个字符串,注意依赖顺序(如`xhs_xs_xsc_56.js`依赖了`jsdom`模拟浏览器环境,而`xhs_xray.js`等可能独立)。
由于`xhs_xs_xsc_56.js`中使用了`require("jsdom")`,而PyMiniRacer不支持Node的`require`,因此需要:
- 移除Node特有的模块(如`require`)或替换为浏览器兼容版本。
- 将`jsdom`模拟的浏览器环境代码内联到JS中。
观察`xhs_xs_xsc_56.js`开头部分:
```javascript
const jsdom = require("jsdom");
// ... 创建JSDOM环境,设置window等全局变量
```
这个环境设置必须在执行目标函数(`get_request_headers_params`)之前完成。我们可以将整个环境设置代码和业务JS代码合并成一个自包含的JS字符串。
但是,PyMiniRacer不支持Node模块,因此需要将`jsdom`相关的代码替换为纯浏览器环境可运行的代码,或者使用其他方式模拟。
考虑到`xhs_xs_xsc_56.js`中主要使用`jsdom`来模拟`window`、`document`等对象,而PyMiniRacer本身提供了基本的浏览器环境(如`window`、`document`),我们可以尝试去掉`jsdom`依赖,直接使用PyMiniRacer的环境。
**修改建议**:
- 删除`xhs_xs_xsc_56.js`中关于`jsdom`的引入和初始化代码(即开头的部分)。
- 确保后续代码只使用标准的Web API(如`window`, `document`等),这些在PyMiniRacer中已经内置。
如果原JS代码依赖了`jsdom`的特定行为,可能需要调整代码以适应PyMiniRacer的环境。
3. **加载并执行JS代码**:
```python
from py_mini_racer import MiniRacer
class XHS_Splier_utils:
def __init__(self):
# 初始化V8上下文
self.ctx = MiniRacer()
# 加载合并后的JS代码
js_code = self._load_js_bundle([
'./static/xhs_xs_xsc_56.js', # 注意:这个文件需要修改,去掉require('jsdom')
'./static/xhs_xray.js',
# 其他JS文件...
])
self.ctx.eval(js_code)
def _load_js_bundle(self, js_paths):
combined_js = ""
for path in js_paths:
with open(path, 'r', encoding='utf-8') as f:
combined_js += f.read() + ";\n"
return combined_js
def search_generate_xs_xs_common(self, a1, api, data=''):
# 调用JS函数
ret = self.ctx.call('get_request_headers_params', api, data, a1)
return ret['xs'], ret['xt'], ret['xs_common']
# 其他方法...
```
4. **处理`xhs_xray.js`等文件**:
同样,这些文件中的代码需要调整为不依赖Node环境。例如,`xhs_xray.js`开头有:
```javascript
self = global;
window = global;
```
在PyMiniRacer中,可以直接使用`window`或`global`,所以可能需要调整。
### 注意事项
1. **JS代码适配**:
原JS代码可能包含Node特有的全局变量(如`global`、`process`)或模块(如`crypto`)。需要将这些替换为浏览器兼容的方式:
- `crypto` -> 使用`window.crypto`或`crypto-js`(需要引入)。
- 如果使用了`Buffer`,可以用`TextEncoder`/`TextDecoder`或`ArrayBuffer`替代。
2. **全局污染**:
多个JS文件可能会修改全局变量,合并后要注意避免冲突。
3. **性能**:
初始化`MiniRacer`并加载JS代码只需一次,多次调用函数时效率较高。
### 修改后的工具类示例
```python
from py_mini_racer import MiniRacer
import requests
import json
import math
import random
class XHS_Splier_utils:
def __init__(self):
self.URL = "https://edith.xiaohongshu.com"
# 初始化V8引擎
self.ctx = MiniRacer()
# 加载所有JS代码(注意:这些JS文件需要适配浏览器环境)
js_bundle = self._load_js_bundle([
'./static/xhs_xs_xsc_56.js', # 修改后的版本,去掉了require('jsdom')
'./static/xhs_xray.js',
# 其他JS文件...
])
self.ctx.eval(js_bundle)
def _load_js_bundle(self, paths):
code = ""
for path in paths:
with open(path, 'r', encoding='utf-8') as f:
code += f.read() + ";\n"
return code
# ... 其他方法(如search_generate_x_b3_traceid等)保持不变 ...
def search_generate_xs_xs_common(self, a1, api, data=''):
# 直接调用JS函数
ret = self.ctx.call("get_request_headers_params", api, data, a1)
xs, xt, xs_common = ret['xs'], ret['xt'], ret['xs_common']
return xs, xt, xs_common
def search_generate_xray_traceid(self):
# 调用xray.js中的函数(假设函数名为'traceId')
return self.ctx.call("traceId")
# ... 其他方法(如search_generate_headers等)保持不变 ...
```
### 需要做的调整
1. **修改JS文件**:
- 对于`xhs_xs_xsc_56.js`:去掉`require("jsdom")`和创建JSDOM环境的代码,因为PyMiniRacer已经提供了`window`和`document`(虽然不完整,但基本够用)。如果原代码依赖了`jsdom`的特定行为,可能需要在PyMiniRacer中模拟。
- 对于`xhs_xray.js`:确保它不依赖Node环境(如`global`改为`window`)。
2. **测试**:
由于环境差异,需要详细测试生成的头信息是否与原始方法一致。
如果JS代码无法轻易修改(例如依赖了Node特有的模块),则可能需要保留Node环境,但通过子进程调用(方案2)。但我们的目标是消除Node依赖,因此优先尝试方案1。
§§相关问题§§
1. 如果JS代码中使用了Node.js的`crypto`模块进行加密,如何将其替换为浏览器兼容的加密方法?
2. 在PyMiniRacer环境中,如何模拟`document.cookie`等属性以确保JS代码正常运行?
3. 当合并多个JS文件时,如何解决全局变量冲突的问题?
4. 如何验证使用PyMiniRacer生成的请求头与原始Node环境生成的请求头完全一致?
5. 如果JS代码依赖了`jsdom`的特定功能(如模拟页面加载),在PyMiniRacer中如何实现类似功能?</think>### 代码分析与优化方案
根据您提供的代码,我注意到以下关键点:
1. 当前实现依赖 `execjs` 调用 Node.js 环境执行 JS 文件
2. 使用了 5 个 JS 文件(`xhs_xs_xsc_56.js`, `xhs_xray.js`, `xhs_xray_pack1.js`, `xhs_xray_pack2.js`, `xhs_creator_xs.js`)
3. JS 文件包含 DOM 环境模拟、加密逻辑和 Webpack 模块
4. 核心功能是生成小红书请求所需的签名参数(xs, xt, xs_common)
### 推荐重构方案:使用 PyMiniRacer + JS 环境封装
```python
import math
import random
import json
import requests
from py_mini_racer import MiniRacer
class XHSCrawlerUtils:
def __init__(self):
self.URL = "https://edith.xiaohongshu.com"
self.ctx = self._init_js_environment()
def _init_js_environment(self):
"""初始化JS执行环境并加载所有依赖"""
ctx = MiniRacer()
# 加载基础JS文件
js_files = [
'xhs_creator_xs.js',
'xhs_xray.js',
'xhs_xray_pack1.js',
'xhs_xray_pack2.js',
'xhs_xs_xsc_56.js'
]
for js_file in js_files:
with open(f'./static/{js_file}', 'r', encoding='utf-8') as f:
js_code = f.read()
ctx.eval(js_code)
# 初始化全局对象
ctx.eval("""
window = global;
document = {
cookie: "",
createElement: function() { return { getContext: function() {} } }
};
navigator = { userAgent: "Mozilla/5.0" };
location = { href: "https://www.xiaohongshu.com" };
""")
return ctx
def search_generate_x_b3_traceid(self, length=16):
"""生成traceID (Python实现)"""
return ''.join(random.choices('abcdef0123456789', k=length))
def search_trans_cookies(self, cookies_str: str):
"""转换cookies字符串为字典"""
return {item.split('=')[0]: '='.join(item.split('=')[1:])
for item in cookies_str.split('; ')}
def search_generate_xs_xs_common(self, a1, api, data=''):
"""调用JS环境生成签名参数"""
try:
# 设置当前cookie
self.ctx.eval(f'window.document.cookie = "a1={a1};"')
# 调用JS函数
ret = self.ctx.call("get_request_headers_params", api, data, a1)
return ret['xs'], ret['xt'], ret['xs_common']
except Exception as e:
raise RuntimeError(f"JS执行失败: {str(e)}")
# ... 保留其他方法不变(search_generate_xray_traceid等)...
```
### 关键优化点:
1. **JS环境初始化**:
- 使用 `MiniRacer` 创建独立的V8环境
- 按顺序加载所有依赖的JS文件
- 模拟必要的浏览器环境(`window`, `document`, `navigator`)
2. **DOM模拟处理**:
```python
# 简化DOM模拟
ctx.eval("""
window = global;
document = {
cookie: "",
createElement: function() {
return { getContext: function() {} }
}
};
""")
```
3. **加密函数适配**:
- 将Node.js的 `crypto` 替换为JS标准 `Crypto` API
- 在JS环境中添加垫片:
```javascript
// 在加载文件前添加
if (!global.crypto) {
global.crypto = {
getRandomValues: function(arr) {
for (let i = 0; i < arr.length; i++) {
arr[i] = Math.floor(Math.random() * 256);
}
return arr;
}
};
}
```
4. **错误处理**:
- 封装JS执行异常捕获
- 提供详细的错误信息
### 使用示例:
```python
# 初始化工具类(只需一次)
utils = XHSCrawlerUtils()
# 生成请求参数
cookies_str = 'a1=19777a5b680yiog7y6vwx1erczenf5aup16obr46u50000367007;...'
api = "/api/sns/web/v1/search/notes"
data = {"keyword": "榴莲", "page": 1}
headers, cookies, processed_data = utils.search_generate_request_params(
cookies_str, api, data
)
# 发送请求
response = requests.post(
url=utils.URL + api,
headers=headers,
data=processed_data.encode("utf-8"),
cookies=cookies
)
```
### 优势对比:
| 原方案 | 新方案 |
|--------|--------|
| 依赖本地Node.js环境 | 纯Python环境 |
| 每次调用需编译JS | JS环境预初始化 |
| 多进程使用可能冲突 | 线程安全 |
| 依赖外部jsdom路径 | 内置环境模拟 |
阅读全文