├── .husky
└── pre-push
├── .npmignore
├── config.js
├── rollup.config.js
├── README.md
├── package.json
├── LICENSE
├── src
├── exception.js
├── help.js
└── index.js
├── example
└── index.js
├── .gitignore
└── test
└── xhs.test.js
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | npm run test
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | example
3 | test
4 | .gitignore
5 | rollup.config.js
6 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | const cookie = "abRequestId=6a42c3f7-5f4c-572e-9847-bc733cc61073; a1=18f74e3b0e2m6jaraiqcaajn8bmz3765wou17xhp030000156562; webId=6cf889e2d027fb9172e3e69efc3394dc; gid=yYiW4dqDYiEJyYiW4dqD8lCxdJ6KV0F03AS07U80VIYD6yq8uqWK2W888y2K2KJ8K8JJKiYi; x-user-id-creator.xiaohongshu.com=5ef20f0f000000000100483a; customerClientId=965656639764280; webBuild=4.42.2; web_session=040069799389ab765b502e9d04354b24650967; websectiga=f3d8eaee8a8c63016320d94a1bd00562d516a5417bc43a032a80cbf70f07d5c0; sec_poison_id=6a5505f5-8628-484e-8da1-1adabffc9b6c; xsecappid=xhs-pc-web; acw_tc=0fe5eea74116d0477ddaca964f4a2909bd60b2fe9316259edf475dcf9ec4dea1; unread={%22ub%22:%22672f19e5000000003c016ce0%22%2C%22ue%22:%2267337b810000000019016c7d%22%2C%22uc%22:23}"
2 |
3 | module.exports = {
4 | cookie
5 | }
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | // import commonjs from '@rollup/plugin-commonjs';
2 | // import resolve from '@rollup/plugin-node-resolve';
3 | const commonjs = require('@rollup/plugin-commonjs');
4 | const resolve = require('@rollup/plugin-node-resolve');
5 |
6 | module.exports = [
7 | {
8 | input: 'src/index.js',
9 | output: {
10 | file: 'dist/index.esm.js',
11 | format: 'esm'
12 | },
13 | plugins: [
14 | resolve(),
15 | commonjs()
16 | ],
17 | external: ['axios', 'jsdom', 'querystring']
18 | },
19 | {
20 | input: 'src/index.js',
21 | output: {
22 | file: 'dist/index.cjs.js',
23 | format: 'cjs',
24 | exports: 'auto'
25 | },
26 | plugins: [
27 | resolve(),
28 | commonjs()
29 | ],
30 | external: ['axios', 'jsdom', 'querystring']
31 | }
32 | ];
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
xhs-js
2 |
3 | 小红书逆向api的js实现.
4 | 🌟**点个star🌟再走吧~**🌟
5 |
6 | ## 免责声明
7 |
8 | 本项目出于学习目的,旨在了解爬虫和反爬虫的机制,请勿用于非法用途,否则后果自负。
9 |
10 | 示例可参考:
11 | - [example/index.js](./example/index.js)
12 | - [test/xhs.test.js](./test/xhs.test.js)
13 |
14 |
15 | ## 本地测试
16 |
17 | ```
18 | npm install
19 | # 替换test/xhs.test.js中的cookie
20 | npm run test
21 |
22 | ```
23 |
24 | ## API
25 |
26 | - getNoteById: 获取笔记详情
27 | - getNoteByIdFromHtml: 从html中获取笔记详情
28 | - getSelfInfo: 获取账号个人信息
29 | - getUserInfo: 获取指定用户信息
30 | - getNoteByKeyword: 根据关键词搜索笔记
31 | - getNoteComments: 获取笔记评论
32 | - getUserNotes: 获取用户笔记
33 | - getMentionNotifications: 获取用户@通知
34 | - getLikeNotifications: 获取用户点赞通知
35 | - getFollowNotifications: 获取用户关注通知
36 | - getUserInfoFromHtml: 从html中获取用户信息
37 |
38 | ## Star History
39 |
40 | [](https://star-history.com/#saifeiLee/xhs-js&Date)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xhs-js",
3 | "version": "0.0.1",
4 | "description": "基于小红书web端进行的API封装,Javascript版本",
5 | "main": "dist/index.cjs.js",
6 | "module": "dist/index.esm.js",
7 | "exports": {
8 | ".": {
9 | "require": "./dist/index.cjs.js",
10 | "import": "./dist/index.esm.js"
11 | }
12 | },
13 | "files": [
14 | "dist"
15 | ],
16 | "scripts": {
17 | "build": "rollup -c",
18 | "prepublishOnly": "npm run build",
19 | "test": "jest",
20 | "prepare": "husky install"
21 | },
22 | "keywords": [
23 | "xhs",
24 | "xhs-api",
25 | "crawl"
26 | ],
27 | "author": "",
28 | "license": "MIT",
29 | "dependencies": {
30 | "axios": "^1.7.7",
31 | "jsdom": "^25.0.0",
32 | "querystring": "^0.2.1"
33 | },
34 | "devDependencies": {
35 | "@rollup/plugin-commonjs": "^26.0.1",
36 | "@rollup/plugin-node-resolve": "^15.2.3",
37 | "husky": "^9.1.6",
38 | "jest": "^29.7.0",
39 | "rollup": "^4.21.2"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 ReaJason
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/exception.js:
--------------------------------------------------------------------------------
1 | // ErrorTuple and ErrorEnum
2 | const ErrorEnum = {
3 | IP_BLOCK: { code: 300012, msg: "网络连接异常,请检查网络设置或重启试试" },
4 | NOTE_ABNORMAL: { code: -510001, msg: "笔记状态异常,请稍后查看" },
5 | NOTE_SECRETE_FAULT: { code: -510001, msg: "当前内容无法展示" },
6 | SIGN_FAULT: { code: 300015, msg: "浏览器异常,请尝试关闭/卸载风险插件或重启试试!" },
7 | SESSION_EXPIRED: { code: -100, msg: "登录已过期" }
8 | };
9 |
10 | // Custom error classes
11 | class DataFetchError extends Error {
12 | constructor(message) {
13 | super(message);
14 | this.message = message;
15 | this.name = "DataFetchError";
16 | }
17 | }
18 |
19 | class IPBlockError extends Error {
20 | constructor(message) {
21 | super(message);
22 | this.message = message;
23 | this.name = "IPBlockError";
24 | }
25 | }
26 |
27 | class SignError extends Error {
28 | constructor(message) {
29 | super(message);
30 | this.message = message;
31 | this.name = "SignError";
32 | }
33 | }
34 |
35 | class NeedVerifyError extends Error {
36 | constructor(message, verifyType = null, verifyUuid = null) {
37 | super(message);
38 | this.message = message;
39 | this.name = "NeedVerifyError";
40 | this.verifyType = verifyType;
41 | this.verifyUuid = verifyUuid;
42 | }
43 | }
44 |
45 | module.exports = {
46 | ErrorEnum,
47 | DataFetchError,
48 | IPBlockError,
49 | SignError,
50 | NeedVerifyError
51 | };
52 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | const XhsClient = require('../src/index.js')
2 | // const XhsClient = require('../dist/index.cjs.js')
3 | const cookie = require('../config.js').cookie
4 | const client = new XhsClient({
5 | cookie: cookie
6 | });
7 | function test_getNoteById() {
8 | const noteUrl = "https://www.xiaohongshu.com/explore/6733744a000000003c01675b?xsec_token=AB57n_T7V4_y7vHdEkH9igS6-y4BwN7uOHNWJW6PK0GNs=&xsec_source=pc_feed"
9 | // parse noteId, xsec_token, xsec_source from noteUrl
10 | const noteId = noteUrl.split('/explore/')[1].split('?')[0];
11 | const xsecToken = noteUrl.split('xsec_token=')[1].split('&')[0];
12 | const xsecSource = noteUrl.split('xsec_source=')[1];
13 | client.getNoteById(noteId, xsecToken, xsecSource).then(res => {
14 | console.log('笔记数据:', res)
15 | })
16 | }
17 | function test_getNoteByIdFromHtml() {
18 | const client = new XhsClient({
19 | cookie: cookie
20 | });
21 | const noteId = '66d90590000000001f01fe31';
22 | client.getNoteByIdFromHtml(noteId).then(res => {
23 | console.log('笔记数据:', res)
24 | })
25 | }
26 |
27 | // test_getNoteByIdFromHtml()
28 | async function testSearchNote() {
29 | const keyword = '旅游';
30 | const client = new XhsClient({
31 | cookie: cookie
32 | });
33 | const result = await client.getNoteByKeyword(
34 | keyword,
35 | );
36 | console.log(result)
37 | }
38 | // testSearchNote()
39 | test_getNoteById()
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 | .DS_Store
--------------------------------------------------------------------------------
/test/xhs.test.js:
--------------------------------------------------------------------------------
1 | const XhsClient = require('../src/index.js')
2 | const { SearchSortType, SearchNoteType } = require('../src/help.js');
3 |
4 | // Replace with a valid cookie
5 | const cookie = require('../config.js').cookie
6 |
7 | const client = new XhsClient({ cookie });
8 |
9 | // Increase the timeout for each test to 30 seconds
10 | jest.setTimeout(30000);
11 |
12 | describe('XhsClient', () => {
13 | test('getNoteById', async () => {
14 | const noteUrl = "https://www.xiaohongshu.com/explore/6733744a000000003c01675b?xsec_token=AB57n_T7V4_y7vHdEkH9igS6-y4BwN7uOHNWJW6PK0GNs=&xsec_source=pc_feed"
15 | // parse noteId, xsec_token, xsec_source from noteUrl
16 | const noteId = noteUrl.split('/explore/')[1].split('?')[0];
17 | const xsecToken = noteUrl.split('xsec_token=')[1].split('&')[0];
18 | const xsecSource = noteUrl.split('xsec_source=')[1];
19 | const result = await client.getNoteById(noteId, xsecToken, xsecSource);
20 | expect(result).toBeDefined();
21 | expect(result.note_id).toBe(noteId);
22 | });
23 |
24 | test('getNoteByIdFromHtml', async () => {
25 | const noteUrl = "https://www.xiaohongshu.com/explore/6733744a000000003c01675b?xsec_token=AB57n_T7V4_y7vHdEkH9igS6-y4BwN7uOHNWJW6PK0GNs=&xsec_source=pc_feed"
26 | // parse noteId, xsec_token, xsec_source from noteUrl
27 | const noteId = noteUrl.split('/explore/')[1].split('?')[0];
28 | const xsecToken = noteUrl.split('xsec_token=')[1].split('&')[0];
29 | const xsecSource = noteUrl.split('xsec_source=')[1];
30 | const result = await client.getNoteByIdFromHtml(noteId, xsecToken, xsecSource);
31 | expect(result).toBeDefined();
32 | expect(result.note_id).toBe(noteId);
33 | });
34 |
35 | test('getSelfInfo', async () => {
36 | const result = await client.getSelfInfo();
37 | expect(result).toBeDefined();
38 | expect(result.basic_info).toBeDefined();
39 | });
40 |
41 | test('getSelfInfoV2', async () => {
42 | const result = await client.getSelfInfoV2();
43 | expect(result).toBeDefined();
44 | });
45 |
46 | test('getUserInfo', async () => {
47 | const userId = '5ef20f0f000000000100483a'; // Replace with a valid user ID
48 | const result = await client.getUserInfo(userId);
49 | expect(result).toBeDefined();
50 | expect(result.basic_info).toBeDefined();
51 | });
52 |
53 | test('getNoteByKeyword', async () => {
54 | const keyword = '旅游';
55 | const result = await client.getNoteByKeyword(
56 | keyword,
57 | 1,
58 | 20,
59 | SearchSortType.GENERAL,
60 | SearchNoteType.ALL
61 | );
62 | expect(result).toBeDefined();
63 | expect(result.has_more).toBeDefined();
64 | expect(Array.isArray(result.items)).toBe(true);
65 | });
66 |
67 | test('getNoteComments', async () => {
68 | const noteId = '66adced2000000002701ca09';
69 | const result = await client.getNoteComments(noteId);
70 | expect(result).toBeDefined();
71 | expect(result.comments).toBeDefined();
72 | });
73 |
74 | test('getUserNotes', async () => {
75 | const userId = '5968981d6a6a6922a67e3035'; // Replace with a valid user ID
76 | const result = await client.getUserNotes(userId);
77 | // console.log('user notes:', result)
78 | expect(result).toBeDefined();
79 | expect(result.notes).toBeDefined();
80 | });
81 |
82 | test("getMentionNotifications", async () => {
83 | const result = await client.getMentionNotifications();
84 | expect(result).toBeDefined();
85 | expect(result.message_list).toBeDefined();
86 | });
87 |
88 | test('getLikeNotifications', async () => {
89 | const result = await client.getLikeNotifications();
90 | expect(result).toBeDefined();
91 | expect(result.message_list).toBeDefined();
92 | })
93 |
94 | test('getFollowNotifications', async () => {
95 | const result = await client.getFollowNotifications();
96 | expect(result).toBeDefined();
97 | expect(result.message_list).toBeDefined();
98 | });
99 |
100 | test('getUserInfoFromHtml', async () => {
101 | const userId = '66ddd49f000000001d020d91';
102 | const result = await client.getUserInfoFromHtml(userId);
103 | expect(result).toBeDefined();
104 | expect(result.nickname).toBeDefined();
105 | });
106 | });
107 |
108 |
--------------------------------------------------------------------------------
/src/help.js:
--------------------------------------------------------------------------------
1 | const lookup = [
2 | "Z",
3 | "m",
4 | "s",
5 | "e",
6 | "r",
7 | "b",
8 | "B",
9 | "o",
10 | "H",
11 | "Q",
12 | "t",
13 | "N",
14 | "P",
15 | "+",
16 | "w",
17 | "O",
18 | "c",
19 | "z",
20 | "a",
21 | "/",
22 | "L",
23 | "p",
24 | "n",
25 | "g",
26 | "G",
27 | "8",
28 | "y",
29 | "J",
30 | "q",
31 | "4",
32 | "2",
33 | "K",
34 | "W",
35 | "Y",
36 | "j",
37 | "0",
38 | "D",
39 | "S",
40 | "f",
41 | "d",
42 | "i",
43 | "k",
44 | "x",
45 | "3",
46 | "V",
47 | "T",
48 | "1",
49 | "6",
50 | "I",
51 | "l",
52 | "U",
53 | "A",
54 | "F",
55 | "M",
56 | "9",
57 | "7",
58 | "h",
59 | "E",
60 | "C",
61 | "v",
62 | "u",
63 | "R",
64 | "X",
65 | "5",
66 | ]
67 |
68 | function encodeUtf8(e) {
69 | const b = [];
70 | const m = encodeURIComponent(e).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
71 | let w = 0;
72 |
73 | while (w < m.length) {
74 | const T = m[w];
75 | if (T === "%") {
76 | const E = m.slice(w + 1, w + 3);
77 | const S = parseInt(E, 16);
78 | b.push(S);
79 | w += 2;
80 | } else {
81 | b.push(T.charCodeAt(0));
82 | }
83 | w += 1;
84 | }
85 |
86 | return b;
87 | }
88 |
89 | function mrc(e) {
90 | const ie = [
91 | 0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685,
92 | 2657392035, 249268274, 2044508324, 3772115230, 2547177864, 162941995,
93 | 2125561021, 3887607047, 2428444049, 498536548, 1789927666, 4089016648,
94 | 2227061214, 450548861, 1843258603, 4107580753, 2211677639, 325883990,
95 | 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755,
96 | 2366115317, 997073096, 1281953886, 3579855332, 2724688242, 1006888145,
97 | 1258607687, 3524101629, 2768942443, 901097722, 1119000684, 3686517206,
98 | 2898065728, 853044451, 1172266101, 3705015759, 2882616665, 651767980,
99 | 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705,
100 | 3099436303, 671266974, 1594198024, 3322730930, 2970347812, 795835527,
101 | 1483230225, 3244367275, 3060149565, 1994146192, 31158534, 2563907772,
102 | 4023717930, 1907459465, 112637215, 2680153253, 3904427059, 2013776290,
103 | 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719,
104 | 3865271297, 1802195444, 476864866, 2238001368, 4066508878, 1812370925,
105 | 453092731, 2181625025, 4111451223, 1706088902, 314042704, 2344532202,
106 | 4240017532, 1658658271, 366619977, 2362670323, 4224994405, 1303535960,
107 | 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733,
108 | 3554079995, 1131014506, 879679996, 2909243462, 3663771856, 1141124467,
109 | 855842277, 2852801631, 3708648649, 1342533948, 654459306, 3188396048,
110 | 3373015174, 1466479909, 544179635, 3110523913, 3462522015, 1591671054,
111 | 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443,
112 | 3233442989, 3988292384, 2596254646, 62317068, 1957810842, 3939845945,
113 | 2647816111, 81470997, 1943803523, 3814918930, 2489596804, 225274430,
114 | 2053790376, 3826175755, 2466906013, 167816743, 2097651377, 4027552580,
115 | 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225,
116 | 1852507879, 4275313526, 2312317920, 282753626, 1742555852, 4189708143,
117 | 2394877945, 397917763, 1622183637, 3604390888, 2714866558, 953729732,
118 | 1340076626, 3518719985, 2797360999, 1068828381, 1219638859, 3624741850,
119 | 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135,
120 | 1181335161, 3412177804, 3160834842, 628085408, 1382605366, 3423369109,
121 | 3138078467, 570562233, 1426400815, 3317316542, 2998733608, 733239954,
122 | 1555261956, 3268935591, 3050360625, 752459403, 1541320221, 2607071920,
123 | 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877,
124 | 83908371, 2512341634, 3803740692, 2075208622, 213261112, 2463272603,
125 | 3855990285, 2094854071, 198958881, 2262029012, 4057260610, 1759359992,
126 | 534414190, 2176718541, 4139329115, 1873836001, 414664567, 2282248934,
127 | 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795,
128 | 376229701, 2685067896, 3608007406, 1308918612, 956543938, 2808555105,
129 | 3495958263, 1231636301, 1047427035, 2932959818, 3654703836, 1088359270,
130 | 936918000, 2847714899, 3736837829, 1202900863, 817233897, 3183342108,
131 | 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449,
132 | 601450431, 3009837614, 3294710456, 1567103746, 711928724, 3020668471,
133 | 3272380065, 1510334235, 755167117,
134 | ]
135 | let o = -1
136 |
137 | function rightWithoutSign(num, bit = 0) {
138 | return (num >>> bit) | 0;
139 | }
140 |
141 | for (let n = 0; n < 57; n++) {
142 | o = ie[(o & 255) ^ e.charCodeAt(n)] ^ rightWithoutSign(o, 8);
143 | }
144 | return o ^ -1 ^ 3988292384;
145 | }
146 |
147 | function encodeChunk(e, t, r) {
148 | const m = [];
149 | for (let b = t; b < r; b += 3) {
150 | const n = (16711680 & (e[b] << 16)) +
151 | ((e[b + 1] << 8) & 65280) +
152 | (e[b + 2] & 255);
153 | m.push(tripletToBase64(n));
154 | }
155 | return m.join('');
156 | }
157 |
158 | function tripletToBase64(e) {
159 | return (
160 | lookup[63 & (e >> 18)] +
161 | lookup[63 & (e >> 12)] +
162 | lookup[(e >> 6) & 63] +
163 | lookup[e & 63]
164 | );
165 | }
166 | function b64Encode(e) {
167 | const P = e.length;
168 | const W = P % 3;
169 | const U = [];
170 | const z = 16383;
171 | let H = 0;
172 | const Z = P - W;
173 |
174 | while (H < Z) {
175 | U.push(encodeChunk(e, H, Math.min(H + z, Z)));
176 | H += z;
177 | }
178 |
179 | if (W === 1) {
180 | const F = e[P - 1];
181 | U.push(lookup[F >> 2] + lookup[(F << 4) & 63] + "==");
182 | } else if (W === 2) {
183 | const F = (e[P - 2] << 8) + e[P - 1];
184 | U.push(lookup[F >> 10] +
185 | lookup[63 & (F >> 4)] +
186 | lookup[(F << 2) & 63] +
187 | "=");
188 | }
189 |
190 | return U.join("");
191 | }
192 |
193 |
194 | module.exports = {
195 | getXCommon,
196 | encodeUtf8,
197 | mrc,
198 | encodeChunk
199 | };
200 |
201 | function getXCommon(a1="", b1 = "", xS = undefined, xT = undefined) {
202 | const common = {
203 | s0: 5, // getPlatformCode
204 | s1: "",
205 | x0: "1", // localStorage.getItem("b1b1")
206 | x1: "3.7.8-2", // version
207 | x2: "Windows",
208 | x3: "xhs-pc-web",
209 | x4: "4.27.7",
210 | x5: a1, // cookie of a1
211 | x6: xT,
212 | x7: xS,
213 | x8: b1, // localStorage.getItem("b1")
214 | x9: mrc(xT + xS + b1),
215 | x10: 1, // getSigCount
216 | };
217 | const encodeStr = encodeUtf8(JSON.stringify(common));
218 | const x_s_common = b64Encode(encodeStr);
219 | return x_s_common;
220 | }
221 |
222 | function getSearchId() {
223 | const e = BigInt(Date.now()) << 64n;
224 | const t = Math.floor(Math.random() * 2147483647);
225 | return base36encode(e + BigInt(t));
226 | }
227 |
228 | function base36encode(num) {
229 | return num.toString(36).toUpperCase();
230 | }
231 | const SearchSortType = Object.freeze({
232 | // default
233 | GENERAL: { value: "general" },
234 | // most popular
235 | MOST_POPULAR: { value: "popularity_descending" },
236 | // Latest
237 | LATEST: { value: "time_descending" }
238 | });
239 |
240 | const SearchNoteType = Object.freeze({
241 | // default
242 | ALL: { value: 0 },
243 | // only video
244 | VIDEO: { value: 1 },
245 | // only image
246 | IMAGE: { value: 2 }
247 | });
248 |
249 |
250 |
251 | module.exports = {
252 | getXCommon,
253 | encodeUtf8,
254 | mrc,
255 | encodeChunk,
256 | getSearchId,
257 | SearchSortType,
258 | SearchNoteType,
259 | }
260 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const qs = require('querystring');
3 | const { get_xs } = require('./jsvmp/xhs');
4 | const {
5 | getXCommon,
6 | getSearchId,
7 | SearchSortType,
8 | SearchNoteType
9 | } = require('./help');
10 | const {
11 | ErrorEnum,
12 | DataFetchError,
13 | IPBlockError,
14 | SignError,
15 | NeedVerifyError
16 | } = require('./exception');
17 |
18 | const camelToUnderscore = (key) => {
19 | return key.replace(/([A-Z])/g, "_$1").toLowerCase();
20 | };
21 |
22 | const transformJsonKeys = (jsonData) => {
23 | const dataDict = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData;
24 | const dictNew = {};
25 | for (const [key, value] of Object.entries(dataDict)) {
26 | const newKey = camelToUnderscore(key);
27 | if (!value) {
28 | dictNew[newKey] = value;
29 | } else if (typeof value === 'object' && !Array.isArray(value)) {
30 | dictNew[newKey] = transformJsonKeys(value);
31 | } else if (Array.isArray(value)) {
32 | dictNew[newKey] = value.map(item =>
33 | item && typeof item === 'object' ? transformJsonKeys(item) : item
34 | );
35 | } else {
36 | dictNew[newKey] = value;
37 | }
38 | }
39 | return dictNew;
40 | };
41 |
42 | class XhsClient {
43 | constructor({
44 | cookie = null,
45 | userAgent = null,
46 | timeout = 10000,
47 | proxies = null,
48 | } = {}) {
49 | this.proxies = proxies;
50 | this.timeout = timeout;
51 | this._host = "https://edith.xiaohongshu.com";
52 | this._creatorHost = "https://creator.xiaohongshu.com";
53 | this._customerHost = "https://customer.xiaohongshu.com";
54 | this.home = "https://www.xiaohongshu.com";
55 | 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";
56 |
57 | this.axiosInstance = axios.create({
58 | timeout: this.timeout,
59 | headers: {
60 | 'user-agent': this.userAgent,
61 | 'Content-Type': 'application/json',
62 | },
63 | });
64 |
65 | if (cookie) {
66 | this.cookie = cookie;
67 | }
68 | }
69 |
70 | // Getter for cookie
71 | get cookie() {
72 | return this.axiosInstance.defaults.headers.Cookie;
73 | }
74 |
75 | // Setter for cookie
76 | set cookie(cookie) {
77 | this.axiosInstance.defaults.headers.Cookie = cookie;
78 | }
79 |
80 | // Getter for cookieDict
81 | get cookieDict() {
82 | const cookieStr = this.axiosInstance.defaults.headers.Cookie;
83 | return cookieStr ? qs.parse(cookieStr.replace(/; /g, '&')) : {};
84 | }
85 |
86 | _preHeaders(url, data = null) {
87 | let a1 = this.cookieDict.a1;
88 | let b1 = ""
89 | let x_s_result = get_xs(url, data, this.cookie);
90 | const X_S = x_s_result['X-s']
91 | const X_t = x_s_result['X-t'].toString()
92 | const X_S_COMMON = getXCommon(a1, b1, X_S, X_t)
93 | this.axiosInstance.defaults.headers['X-s'] = X_S
94 | this.axiosInstance.defaults.headers['X-t'] = X_t
95 | this.axiosInstance.defaults.headers['X-s-common'] = X_S_COMMON
96 | }
97 |
98 | async request(method, url, config = {}) {
99 | try {
100 | const response = await this.axiosInstance({ method, url, ...config });
101 | if (!response.data) return response;
102 | // console.log('response', response)
103 | if (response.status === 471 || response.status === 461) {
104 | const verifyType = response.headers['verifytype'];
105 | const verifyUuid = response.headers['verifyuuid'];
106 | throw new NeedVerifyError(`出现验证码,请求失败,Verifytype: ${verifyType},Verifyuuid: ${verifyUuid}`, response, verifyType, verifyUuid);
107 | }
108 |
109 | const data = response.data;
110 | if (data.success) {
111 | return data.data || data.success;
112 | } else if (data.code === ErrorEnum.IP_BLOCK.code) {
113 | throw new IPBlockError(ErrorEnum.IP_BLOCK.msg, response);
114 | } else if (data.code === ErrorEnum.SIGN_FAULT.code) {
115 | throw new SignError(ErrorEnum.SIGN_FAULT.msg, response);
116 | } else {
117 | throw new DataFetchError(data, response);
118 | }
119 | } catch (error) {
120 | if (error.response && (error.response.status === 471 || error.response.status) === 461) {
121 | // Handle verification error
122 | const verifyType = error.response.headers['verifytype'];
123 | const verifyUuid = error.response.headers['verifyuuid'];
124 | throw new NeedVerifyError(`出现验证码,请求失败,Verifytype: ${verifyType},Verifyuuid: ${verifyUuid}`, error.response, verifyType, verifyUuid);
125 | }
126 | throw error;
127 | }
128 | }
129 |
130 | async get(uri, params = null, isCreator = false, isCustomer = false, config = {}) {
131 | let finalUri = uri;
132 | if (params) {
133 | finalUri = `${uri}?${qs.stringify(params)}`;
134 | }
135 | this._preHeaders(finalUri, null);
136 | let endpoint = this._host;
137 | if (isCustomer) {
138 | endpoint = this._customerHost;
139 | } else if (isCreator) {
140 | endpoint = this._creatorHost;
141 | }
142 | return this.request('GET', `${endpoint}${finalUri}`, config);
143 | }
144 |
145 | async post(uri, data = null, isCreator = false, isCustomer = false, config = {}) {
146 | let jsonStr = data ? JSON.stringify(data) : null;
147 | this._preHeaders(uri, data);
148 | let endpoint = this._host;
149 | if (isCustomer) {
150 | endpoint = this._customerHost;
151 | } else if (isCreator) {
152 | endpoint = this._creatorHost;
153 | }
154 | if (data) {
155 | return this.request('POST', `${endpoint}${uri}`, {
156 | ...config,
157 | data: jsonStr,
158 | headers: {
159 | ...config.headers,
160 | 'Content-Type': 'application/json',
161 | }
162 | });
163 | }
164 | return this.request('POST', `${endpoint}${uri}`, { ...config, data });
165 | }
166 |
167 | /**
168 | * 获取笔记详情
169 | * 注意: 需要xsec_token
170 | * @param {string} noteId
171 | * @returns
172 | */
173 | async getNoteById(noteId, xsecToken, xsecSource = "pc_feed") {
174 | if (!xsecToken) {
175 | throw new Error("xsecToken is required");
176 | }
177 | const data = {
178 | source_note_id: noteId,
179 | image_scenes: ["CRD_WM_WEBP"],
180 | xsec_token: xsecToken,
181 | xsec_source: xsecSource
182 | };
183 | const uri = "/api/sns/web/v1/feed";
184 |
185 | try {
186 | const res = await this.post(uri, data);
187 | return res.items[0].note_card;
188 | } catch (error) {
189 | console.error("Error fetching note:", error);
190 | throw error;
191 | }
192 | }
193 |
194 | async getNoteByIdFromHtml(noteId, xsecToken, xsecSource = "pc_feed") {
195 | const url = `https://www.xiaohongshu.com/explore/${noteId}?xsec_token=${xsecToken}&xsec_source=${xsecSource}`;
196 | try {
197 | const response = await this.axiosInstance.get(url, {
198 | headers: {
199 | 'user-agent': this.userAgent,
200 | 'referer': 'https://www.xiaohongshu.com/'
201 | }
202 | });
203 |
204 | const html = response.data;
205 | const stateMatch = html.match(/window.__INITIAL_STATE__=({.*})<\/script>/);
206 |
207 | if (stateMatch) {
208 | const state = stateMatch[1].replace(/undefined/g, '""');
209 | if (state !== "{}") {
210 | const noteDict = transformJsonKeys(JSON.parse(state));
211 | return noteDict.note.note_detail_map[noteId].note;
212 | }
213 | }
214 |
215 | if (html.includes(ErrorEnum.IP_BLOCK.value)) {
216 | throw new IPBlockError(ErrorEnum.IP_BLOCK.value);
217 | }
218 |
219 | throw new DataFetchError(html);
220 | } catch (error) {
221 | console.error("Error fetching note:", error);
222 | throw error;
223 | }
224 | }
225 |
226 | async getSelfInfo() {
227 | const uri = "/api/sns/web/v1/user/selfinfo";
228 | return this.get(uri);
229 | }
230 |
231 | async getSelfInfoV2() {
232 | const uri = "/api/sns/web/v2/user/me";
233 | return this.get(uri);
234 | }
235 |
236 | async getUserInfo(userId) {
237 | const uri = '/api/sns/web/v1/user/otherinfo'
238 | const params = {
239 | "target_user_id": userId
240 | }
241 | return this.get(uri, params);
242 | }
243 |
244 | /**
245 | *
246 | * @param {string} keyword 关键词
247 | * @param {number} page 页码
248 | * @param {number} pageSize 分页查询的数量
249 | * @param {string} sort 搜索的类型,分为: general, popularity_descending, time_descending
250 | * @param {number} noteType 笔记类型
251 | * @returns
252 | */
253 | async getNoteByKeyword(
254 | keyword,
255 | page = 1,
256 | pageSize = 20,
257 | sort = SearchSortType.GENERAL,
258 | noteType = SearchNoteType.ALL
259 | ) {
260 | const uri = "/api/sns/web/v1/search/notes";
261 | const data = {
262 | keyword: keyword,
263 | page: page,
264 | page_size: pageSize,
265 | search_id: getSearchId(),
266 | sort: sort.value,
267 | note_type: noteType.value,
268 | image_formats: ["jpg", "webp", "avif"],
269 | ext_flags: [],
270 | };
271 |
272 | return this.post(uri, data);
273 | }
274 |
275 | /**
276 | * 获取笔记评论
277 | * @param {string} noteId 笔记id
278 | * @param {string} cursor 分页查询的下标,默认为""
279 | * @returns
280 | */
281 | async getNoteComments(noteId, cursor = "") {
282 | const uri = "/api/sns/web/v2/comment/page"
283 | const params = {
284 | "note_id": noteId,
285 | "cursor": cursor,
286 | }
287 | return this.get(uri, params);
288 | }
289 |
290 | /**
291 | * 获取用户笔记
292 | * @param {*} userId
293 | * @param {*} cursor
294 | * @returns
295 | */
296 | async getUserNotes(userId, cursor = "") {
297 | const uri = "/api/sns/web/v1/user_posted"
298 | const params = {
299 | "cursor": cursor,
300 | "num": 30,
301 | "user_id": userId,
302 | "image_scenes": "FD_WM_WEBP"
303 | }
304 | return this.get(uri, params);
305 | }
306 |
307 | /**
308 | * 获取账号@我通知
309 | * @param {*} num
310 | * @param {*} cursor
311 | * @returns
312 | */
313 | async getMentionNotifications(num = 20, cursor = "") {
314 | const uri = "/api/sns/web/v1/you/mentions"
315 | const params = { "num": num, "cursor": cursor }
316 | return this.get(uri, params);
317 | }
318 |
319 | /**
320 | * 获取点赞通知
321 | * @param {*} num
322 | * @param {*} cursor
323 | * @returns
324 | */
325 | async getLikeNotifications(num = 20, cursor = "") {
326 | const uri = "/api/sns/web/v1/you/likes"
327 | const params = { "num": num, "cursor": cursor }
328 | return this.get(uri, params);
329 | }
330 |
331 | /**
332 | * 获取关注通知
333 | * @param {*} num
334 | * @param {*} cursor
335 | * @returns
336 | */
337 | async getFollowNotifications(num = 20, cursor = "") {
338 | const uri = "/api/sns/web/v1/you/connections"
339 | const params = { "num": num, "cursor": cursor }
340 | return this.get(uri, params);
341 | }
342 |
343 | async getUserInfoFromHtml(userId) {
344 | const url = `https://www.xiaohongshu.com/user/profile/${userId}`
345 | try {
346 | const response = await this.axiosInstance.get(url, {
347 | headers: {
348 | 'user-agent': this.userAgent,
349 | 'referer': 'https://www.xiaohongshu.com/'
350 | }
351 | });
352 | const html = response.data;
353 | const stateMatch = html.match(/window.__INITIAL_STATE__=({.*})<\/script>/);
354 | if (stateMatch) {
355 | const state = stateMatch[1].replace(/"undefined"/g, '"_"').replace(/\bundefined\b/g, '""');
356 | if (state !== "{}") {
357 | const parsedState = JSON.parse(state);
358 | const userBasicInfo = transformJsonKeys(parsedState).user.user_page_data.basic_info;
359 | return userBasicInfo;
360 | }
361 | }
362 | return response.data;
363 | } catch (error) {
364 | console.error("Error fetching user info:", error);
365 | throw error;
366 | }
367 | }
368 | }
369 |
370 |
371 |
372 |
373 |
374 | module.exports = XhsClient;
--------------------------------------------------------------------------------