import axios from 'axios'; import qs from 'querystring'; import { get_xs } from './jsvmp/xhs'; import fs from 'fs'; import { getXCommon, getSearchId, SearchSortType, SearchNoteType } from './helper.js'; import { ErrorEnum, DataFetchError, IPBlockError, SignError, NeedVerifyError } from './exception'; const camelToUnderscore = (key) => { return key.replace(/([A-Z])/g, '_$1').toLowerCase(); }; const transformJsonKeys = (jsonData) => { const dataDict = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData; const dictNew = {}; for (const [key, value] of Object.entries(dataDict)) { const newKey = camelToUnderscore(key); if (!value) { dictNew[newKey] = value; } else if (typeof value === 'object' && !Array.isArray(value)) { dictNew[newKey] = transformJsonKeys(value); } else if (Array.isArray(value)) { dictNew[newKey] = value.map((item) => (item && typeof item === 'object' ? transformJsonKeys(item) : item)); } else { dictNew[newKey] = value; } } return dictNew; }; class XhsClient { /** * Constructor for XhsClient * @param {Object} options - Configuration options * @param {string} options.cookie - Cookie string for authentication * @param {string} options.userAgent - User agent string for requests * @param {number} options.timeout - Request timeout in milliseconds * @param {string} options.proxies - Proxy settings */ constructor({ cookie = null, userAgent = null, timeout = 10000, proxies = null } = {}) { this.proxies = proxies; this.timeout = timeout; this._host = 'https://edith.xiaohongshu.com'; this._creatorHost = 'https://creator.xiaohongshu.com'; this._customerHost = 'https://customer.xiaohongshu.com'; this.home = 'https://www.xiaohongshu.com'; this.userAgent = userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36'; this.axiosInstance = axios.create({ timeout: this.timeout, headers: { 'user-agent': this.userAgent, 'Content-Type': 'application/json', }, }); if (cookie) { this.cookie = cookie; } } // Getter for cookie get cookie() { return this.axiosInstance.defaults.headers.Cookie; } // Setter for cookie set cookie(cookie) { this.axiosInstance.defaults.headers.Cookie = cookie; } // Getter for cookieDict get cookieDict() { const cookieStr = this.axiosInstance.defaults.headers.Cookie; return cookieStr ? qs.parse(cookieStr.replace(/; /g, '&')) : {}; } _preHeaders(url, data = null) { let a1 = this.cookieDict.a1; let b1 = ''; let x_s_result = get_xs(url, data, this.cookie); const X_S = x_s_result['X-s']; const X_t = x_s_result['X-t'].toString(); const X_S_COMMON = getXCommon(a1, b1, X_S, X_t); // this.axiosInstance.defaults.headers['X-s'] = X_S; // this.axiosInstance.defaults.headers['X-t'] = X_t; // this.axiosInstance.defaults.headers['X-s-common'] = X_S_COMMON; return { headers: { 'X-s': X_S, 'X-t': X_t, 'X-s-common': X_S_COMMON, }, }; } getCookieMap() { const cookie = this.cookie; let cookieDict = {}; if (cookie) { const cookieArray = cookie.split(';'); cookieArray.forEach((item) => { const [key, value] = item.split('='); const trimKey = key.trim(); if (trimKey) { const _value = value ? value.trim() : ''; cookieDict[trimKey] = _value; } }); return cookieDict; } return {}; } /** * Get X-S and X-T * @param {*} url * @param {*} data * @param {*} cookie * @returns */ get_xs(url, data, cookie) { return get_xs(url, data, cookie); } async request(method, url, config = {}) { try { const response = await this.axiosInstance({ method, url, ...config }); if (!response.data) return response; // console.log('response', response) if (response.status === 471 || response.status === 461) { const verifyType = response.headers['verifytype']; const verifyUuid = response.headers['verifyuuid']; throw new NeedVerifyError(`出现验证码,请求失败,Verifytype: ${verifyType},Verifyuuid: ${verifyUuid}`, response, verifyType, verifyUuid); } const data = response.data; if (data.success) { return data.data || data.success; } else if (data.code === ErrorEnum.IP_BLOCK.code) { throw new IPBlockError(ErrorEnum.IP_BLOCK.msg, response); } else if (data.code === ErrorEnum.SIGN_FAULT.code) { throw new SignError(ErrorEnum.SIGN_FAULT.msg, response); } else { throw new DataFetchError(data, response); } } catch (error) { if (error.response && (error.response.status === 471 || error.response.status) === 461) { // Handle verification error const verifyType = error.response.headers['verifytype']; const verifyUuid = error.response.headers['verifyuuid']; throw new NeedVerifyError(`出现验证码,请求失败,Verifytype: ${verifyType},Verifyuuid: ${verifyUuid}`, error.response, verifyType, verifyUuid); } throw error; } } /** * * @param {*} uri * @param {*} params * @param {Object} config * @param {*} [config.sign] - Whether to sign the request * @param {boolean} [config.isCreator] - Whether the request is for a creator * @param {boolean} [config.isCustomer] - Whether the request is for a customer * @param {*} [config.headers] - XSEC token for authentication * @returns */ async get(uri, params = null, config = {}) { if (params) { uri = `${uri}?${qs.stringify(params)}`; } if (config.sign) { await config.sign(uri, data, config); } else { const { headers } = this._preHeaders(uri, null); config = { ...config, headers: { ...config.headers, ...headers } }; } let isCreator = config?.isCreator ?? false; let isCustomer = config?.isCustomer ?? false; let endpoint = this._host; if (isCustomer) { endpoint = this._customerHost; } else if (isCreator) { endpoint = this._creatorHost; } return this.request('GET', `${endpoint}${uri}`, config); } /** * * @param {*} uri * @param {*} data * @param {Object} config * @param {*} [config.sign] - Whether to sign the request * @param {boolean} [config.isCreator] - Whether the request is for a creator * @param {boolean} [config.isCustomer] - Whether the request is for a customer * @param {*} [config.headers] - XSEC token for authentication * @returns */ async post(uri, data = null, config = {}) { let jsonStr = data ? JSON.stringify(data) : null; let endpoint = this._host; if (config.sign) { await config.sign(uri, data, config); } else { const { headers } = this._preHeaders(uri, data); config = { ...config, headers: { ...config.headers, ...headers } }; } let isCreator = config?.isCreator ?? false; let isCustomer = config?.isCustomer ?? false; if (isCustomer) { endpoint = this._customerHost; } else if (isCreator) { endpoint = this._creatorHost; } if (data) { return this.request('POST', `${endpoint}${uri}`, { ...config, data: jsonStr, headers: { ...config.headers, 'Content-Type': 'application/json', }, }); } return this.request('POST', `${endpoint}${uri}`, { ...config, data }); } /** * 获取笔记详情 * 注意: 需要xsec_token * @param {string} noteId * @returns */ async getNoteById(noteId, xsecToken, xsecSource = 'pc_feed') { if (!xsecToken) { throw new Error('xsecToken is required'); } const data = { source_note_id: noteId, image_scenes: ['CRD_WM_WEBP'], xsec_token: xsecToken, xsec_source: xsecSource, }; const uri = '/api/sns/web/v1/feed'; try { const res = await this.post(uri, data); return res.items[0].note_card; } catch (error) { console.error('Error fetching note:', error); throw error; } } async getNoteByIdFromHtml(noteId, xsecToken, xsecSource = 'pc_feed') { const url = `https://www.xiaohongshu.com/explore/${noteId}?xsec_token=${xsecToken}&xsec_source=${xsecSource}`; let html = ''; try { const response = await this.axiosInstance.get(url, { headers: { 'user-agent': this.userAgent, referer: 'https://www.xiaohongshu.com/', }, }); html = response.data; const stateMatch = html.match(/window.__INITIAL_STATE__=({.*})<\/script>/); if (stateMatch) { const state = stateMatch[1].replace(/undefined/g, '""'); if (state !== '{}') { const noteDict = transformJsonKeys(JSON.parse(state)); return noteDict.note.note_detail_map[noteId].note; } } if (html.includes(ErrorEnum.IP_BLOCK.value)) { throw new IPBlockError(ErrorEnum.IP_BLOCK.value); } throw new DataFetchError(html); } catch (error) { console.error('Error fetching note:', error); fs.writeFileSync('a.html', html); throw error; } } async getSelfInfo() { const uri = '/api/sns/web/v1/user/selfinfo'; return this.get(uri); } async getSelfInfoV2() { const uri = '/api/sns/web/v2/user/me'; return this.get(uri); } async getUserInfo(userId) { const uri = '/api/sns/web/v1/user/otherinfo'; const params = { target_user_id: userId, }; return this.get(uri, params); } /** * * @param {string} keyword 关键词 * @param {number} page 页码 * @param {number} pageSize 分页查询的数量 * @param {string} sort 搜索的类型,分为: general, popularity_descending, time_descending * @param {number} noteType 笔记类型 * @returns */ async getNoteByKeyword(keyword, page = 1, pageSize = 20, sort = SearchSortType.GENERAL, noteType = SearchNoteType.ALL) { const uri = '/api/sns/web/v1/search/notes'; const data = { keyword: keyword, page: page, page_size: pageSize, search_id: getSearchId(), sort: sort.value, note_type: noteType.value, image_formats: ['jpg', 'webp', 'avif'], ext_flags: [], }; return this.post(uri, data); } /** * 获取笔记评论 * @param {string} noteId 笔记id * @param {string} cursor 分页查询的下标,默认为"" * @returns */ async getNoteComments(noteId, cursor = '') { const uri = '/api/sns/web/v2/comment/page'; const params = { note_id: noteId, cursor: cursor, }; return this.get(uri, params); } /** * 获取用户笔记 * @param {*} userId * @param {*} cursor * @returns */ async getUserNotes(userId, cursor = '') { const uri = '/api/sns/web/v1/user_posted'; const params = { cursor: cursor, num: 30, user_id: userId, image_scenes: 'FD_WM_WEBP', }; return this.get(uri, params); } /** * 获取账号@我通知 * @param {*} num * @param {*} cursor * @returns */ async getMentionNotifications(num = 20, cursor = '') { const uri = '/api/sns/web/v1/you/mentions'; const params = { num: num, cursor: cursor }; return this.get(uri, params); } /** * 获取点赞通知 * @param {*} num * @param {*} cursor * @returns */ async getLikeNotifications(num = 20, cursor = '') { const uri = '/api/sns/web/v1/you/likes'; const params = { num: num, cursor: cursor }; return this.get(uri, params); } /** * 获取关注通知 * @param {*} num * @param {*} cursor * @returns */ async getFollowNotifications(num = 20, cursor = '') { const uri = '/api/sns/web/v1/you/connections'; const params = { num: num, cursor: cursor }; return this.get(uri, params); } async getUserInfoFromHtml(userId) { const url = `https://www.xiaohongshu.com/user/profile/${userId}`; try { const response = await this.axiosInstance.get(url, { headers: { 'user-agent': this.userAgent, referer: 'https://www.xiaohongshu.com/', }, }); const html = response.data; const stateMatch = html.match(/window.__INITIAL_STATE__=({.*})<\/script>/); if (stateMatch) { const state = stateMatch[1].replace(/"undefined"/g, '"_"').replace(/\bundefined\b/g, '""'); if (state !== '{}') { const parsedState = JSON.parse(state); const userBasicInfo = transformJsonKeys(parsedState).user.user_page_data.basic_info; return userBasicInfo; } } return response.data; } catch (error) { console.error('Error fetching user info:', error); throw error; } } } export { XhsClient };