├── .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 | [![Star History Chart](https://api.star-history.com/svg?repos=saifeiLee/xhs-js&type=Date)](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; --------------------------------------------------------------------------------