├── README.md └── index.js /README.md: -------------------------------------------------------------------------------- 1 | # xhs 2 | 3 | 通过 JavaScript + xlsx,基于浏览器端实现对小红书的推荐列表进行数据爬取。 4 | 5 | ## AI 解读 6 | 7 | 1. 爬取的数据量为 100 条 8 | 2. 每次操作延迟 2 秒 9 | 3. 每爬取 45 条数据后休息 3 分钟 10 | 4. 爬取的点赞数大于 100 11 | 5. 爬取的数据包括:链接、点赞数、标题、正文、日期、收藏数 12 | 6. 爬取的数据导出为 xlsx 格式的 excel 文件 13 | 14 | ## 调试 15 | 16 | ### 控制台法 17 | 18 | 1. 打开小红书网页 19 | 2. 打开控制台 20 | 3. 复制粘贴代码 21 | 4. 回车 22 | 5. 等待 23 | 24 | ### 油猴 tampermonkey 25 | 26 | 使用油猴 tampermonkey 添加脚本 27 | 28 | ### 功能 29 | 30 | - [x] 拉取推荐列表 31 | - [x] 拉取作品详情 32 | - [x] 导出 Excel 33 | - [x] 防封策略 34 | 35 | ## 提示 36 | 37 | 不支持搜索后爬虫。 38 | 39 | 本项目只提供学习,禁止用于商业项目。 40 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // 爬取的数据量 4 | const MAX_COUNT = 10; 5 | // 操作延迟,单位秒 6 | const HANDLE_DELAY = 2; 7 | // 休息延迟,单位秒 8 | const REST_DELAY = 180; 9 | // 爬取的点赞数 10 | const MIN_LIKES = 100; 11 | 12 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 13 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 14 | return new (P || (P = Promise))(function (resolve, reject) { 15 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 16 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 17 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 18 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 19 | }); 20 | }; 21 | /** 22 | * 通过二进制流下载文件 23 | * 24 | * @param blob 二进制流 25 | * @param fileName 文件名称 26 | */ 27 | const useBlob = (blob, fileName) => { 28 | const link = document.createElement("a"); 29 | const href = URL.createObjectURL(blob); 30 | link.href = href; 31 | link.download = fileName; 32 | document.body.appendChild(link); 33 | link.click(); 34 | document.body.removeChild(link); 35 | URL.revokeObjectURL(link.href); // 释放掉blob对象 36 | }; 37 | /** 38 | * 通过url下载文件,需要后端支持跨域 39 | * 40 | * @param url 文件地址 41 | * @param fileName 文件名称 42 | */ 43 | const useURL = (url, fileName) => { 44 | fetch(url).then((res) => __awaiter(void 0, void 0, void 0, function* () { return useBlob(yield res.blob(), fileName); })); 45 | }; 46 | const download = { 47 | useBlob, 48 | useURL, 49 | }; 50 | 51 | 52 | 53 | 54 | class Xhs { 55 | list = []; 56 | round = 1; 57 | 58 | constructor() { 59 | setTimeout(() => { 60 | this.loop(); 61 | }, HANDLE_DELAY * 1000); 62 | } 63 | 64 | // 睡眠 65 | async sleep(delay) { 66 | return new Promise((resolve, reject) => { 67 | setTimeout(() => { 68 | resolve(); 69 | }, delay * 1000); 70 | }); 71 | } 72 | 73 | // 循环 74 | async loop() { 75 | if (this.list.length >= MAX_COUNT) { 76 | this.exportXlsx(); 77 | return; 78 | } 79 | try { 80 | await this.getList(); 81 | await this.sleep(HANDLE_DELAY); 82 | this.reload(); 83 | this.loop(); 84 | } catch (err) { 85 | console.error(err) 86 | this.exportXlsx(); 87 | } 88 | } 89 | 90 | // 获取列表 91 | async getList() { 92 | const elements = document.querySelectorAll(".note-item"); 93 | for (let i = 0; i < elements.length; i++) { 94 | let item = elements[i]; 95 | // 链接 96 | let link = item.querySelector("a").href; 97 | // 点赞数 98 | let like = item.querySelector(".like-wrapper .count").innerText; 99 | // 标题 100 | let title = item.querySelector(".footer span").innerText; 101 | // 正文 102 | let content = ""; 103 | // 日期 104 | let date = '' 105 | // 收藏数 106 | let collect = '' 107 | 108 | const likeCount = like.indexOf('w') > -1 ? like.replace('w', '') * 10000 : Number(like) 109 | 110 | if (likeCount > MIN_LIKES) { 111 | // 进入详情页 112 | item.querySelectorAll("a")[1].click(); 113 | await this.sleep(HANDLE_DELAY); 114 | 115 | document 116 | .querySelectorAll(".desc") 117 | .forEach((item) => (content += item.innerText)); 118 | date = document.querySelector(".date").innerText; 119 | collect = document.querySelector( 120 | ".collect-wrapper .count" 121 | ).innerText; 122 | 123 | document.querySelector(".close").click(); 124 | await this.sleep(HANDLE_DELAY); 125 | 126 | // 添加到列表 127 | this.list.push({ 128 | title, 129 | like, 130 | collect, 131 | content, 132 | date, 133 | link, 134 | }); 135 | this.log(this.list); 136 | 137 | if (!(this.list.length % 45)) { 138 | this.log(`Resting ${REST_DELAY}s`) 139 | await this.sleep(REST_DELAY) 140 | } 141 | } 142 | } 143 | } 144 | 145 | // 刷新页面 146 | reload() { 147 | document.querySelector(".reload").click(); 148 | } 149 | 150 | // 导出 151 | exportXlsx() { 152 | const xlsx = 153 | "https://cdn.bootcdn.net/ajax/libs/xlsx/0.16.9/xlsx.full.min.js"; 154 | const script = document.createElement("script"); 155 | script.setAttribute("type", "text/javascript"); 156 | script.setAttribute("src", xlsx); 157 | script.onload = () => { 158 | const wb = XLSX.utils.book_new(); 159 | const sheet = XLSX.utils.json_to_sheet(this.list); 160 | sheet["!cols"] = [ 161 | { wch: 40 }, 162 | { wch: 10 }, 163 | { wch: 10 }, 164 | { wch: 80 }, 165 | { wch: 20 }, 166 | { wch: 80 }, 167 | ]; 168 | XLSX.utils.book_append_sheet(wb, sheet, "sheet1"); 169 | XLSX.writeFile(wb, "xhs.xlsx"); 170 | }; 171 | document.getElementsByTagName("head")[0].appendChild(script); 172 | } 173 | 174 | exportJson() { 175 | const json = JSON.stringify(this.list); 176 | const blob = new Blob([json], { type: "text/json" }) 177 | download.useBlob(blob, 'notes.json') 178 | } 179 | 180 | // 日志 181 | log(msg) { 182 | console.log(`Xhs...`, msg) 183 | } 184 | } 185 | 186 | 187 | new Xhs(); --------------------------------------------------------------------------------