小红书爬虫代码
一. 在要保存代码的目录下新建 main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| const playwright = require("playwright"); const axios = require("axios"); const fs = require("fs");
(async () => { const browser = await playwright.chromium.launch({ headless: false }); const context = await browser.newContext(); const page = await context.newPage();
await page.goto("https://www.xiaohongshu.com/explore");
try { let arr = [];
for (let i = 0; i < 100; i++) { await page.evaluate((idx) => { window.scrollTo(0, 800 * idx); }, i); await page.waitForSelector("section", { timeout: 10000 }); const content = await page.$$eval("section", (els) => { return els.map((item) => { return { title: item.querySelector("a.title span").innerHTML, author: item.querySelector("a.author span").innerHTML, like: item.querySelector(".like-wrapper.like-active span.count") .innerHTML, img: item.querySelector("a.cover img").src, }; }); }); arr = arr.concat(content); console.log(content); console.log( i, "======================================================================" ); } writeToJsonFile(arr, "content.json");
arr.forEach((item) => { if (!fs.existsSync("./imgs")) { fs.mkdirSync("./imgs"); } downloadImage( item.img, `./imgs/${item.img.split("/")[item.img.split("/").length - 1]}.png` ); }); } catch (error) { console.log("error", error); }
await browser.close(); })();
function writeToJsonFile(data, filename) { const jsonData = JSON.stringify(data, null, 2); fs.writeFile(filename, jsonData, "utf8", (err) => { if (err) { console.error("Error writing to JSON file: ", err); return; } console.log(`Data written to ${filename}`); }); }
async function downloadImage(imageUrl, outputPath) { try { const response = await axios({ url: imageUrl, method: "GET", responseType: "stream", });
const writer = fs.createWriteStream(outputPath);
response.data.pipe(writer);
return new Promise((resolve, reject) => { writer.on("finish", resolve); writer.on("error", reject); }); } catch (error) { console.error("Error downloading image:", error); throw error; } }
|
二. 代码详解
第一次使用时需要运行:
会在该目录中新建 package.json 文件,用于存储在代码中需要的依赖,否则会找不到包
1> 引入依赖
playwright: 用于自动化浏览器操作,如打开网页、滚动页面、获取页面元素等。
axios: 用于发起 HTTP 请求,这里主要用于下载图片。
fs: Node.js 的文件系统模块,用于读写文件。
如果没有对应模块可以通过npm下载对应模块
1 2 3
| const playwright = require("playwright"); const axios = require("axios"); const fs = require("fs");
|
2> 脚本主体
使用 async 函数来执行异步操作,这是处理网络请求和浏览器自动化操作的常见做法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| (async () => { const browser = await playwright.chromium.launch({ headless: false }); const context = await browser.newContext(); const page = await context.newPage();
await page.goto("https://www.xiaohongshu.com/explore");
try { let arr = [];
for (let i = 0; i < 100; i++) { await page.evaluate((idx) => { window.scrollTo(0, 800 * idx); }, i); await page.waitForSelector("section", { timeout: 10000 }); const content = await page.$$eval("section", (els) => { return els.map((item) => { return { title: item.querySelector("a.title span").innerHTML, author: item.querySelector("a.author span").innerHTML, like: item.querySelector(".like-wrapper.like-active span.count") .innerHTML, img: item.querySelector("a.cover img").src, }; }); }); arr = arr.concat(content); console.log(content); console.log( i, "======================================================================" ); } writeToJsonFile(arr, "content.json");
arr.forEach((item) => { if (!fs.existsSync("./imgs")) { fs.mkdirSync("./imgs"); } downloadImage( item.img, `./imgs/${item.img.split("/")[item.img.split("/").length - 1]}.png` ); }); } catch (error) { console.log("error", error); }
await browser.close(); })();
|
a. 初始化
1 2 3
| const browser = await playwright.chromium.launch({ headless: false }); const context = await browser.newContext(); const page = await context.newPage();
|
- 使用 playwright.chromium.launch({ headless: false }) 启动一个非无头(即显示浏览器界面)的 Chromium 浏览器实例,方便调试。
- 创建一个新的浏览器上下文和页面,并导航到小红书的“探索”页面。
b. 抓取内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| try { let arr = []
for (let i = 0 ; i < 100 ; i++){ await page.evaluate((idx) => { window.scrollTo(0, 800*idx) },i); await page.waitForSelector('section', { timeout: 10000 }); const content = await page.$$eval('section', els => { return els.map(item => { return { "title":item.querySelector('a.title span').innerHTML, "author":item.querySelector('a.author span').innerHTML, "like":item.querySelector('.like-wrapper.like-active span.count').innerHTML, "img":item.querySelector('a.cover img').src, }; }) }); arr = arr.concat(content) console.log(content); console.log(i,"======================================================================"); }
|
- 脚本通过循环来模拟用户滚动页面的行为。在每次循环中,它使用 page.evaluate 方法在浏览器上下文中执行 JavaScript 代码来滚动页面(每次滚动 800 像素)。
- 使用 page.waitForSelector 等待页面上出现 section 元素(这里假设 section 元素包含了需要抓取的数据)
- 使用 page.$$eval 方法抓取页面上所有 section 元素内的数据。这个方法在浏览器上下文中执行一个函数,该函数接收一个元素数组作为参数,并返回一个处理后的数组。在这个例子中,它返回了一个包含帖子标题、作者、点赞数和图片链接的对象数组。
- 将每次循环抓取的数据合并到一个数组中,并在控制台中打印出来。
c. 写入 JSON 文件和下载图片
1 2 3 4 5 6 7 8 9 10 11 12
| writeToJsonFile(arr, "content.json");
arr.forEach((item) => { if (!fs.existsSync("./imgs")) { fs.mkdirSync("./imgs"); } downloadImage( item.img, `./imgs/${item.img.split("/")[item.img.split("/").length - 1]}.png` ); });
|
- 使用自定义的 writeToJsonFile 函数将抓取的数据数组写入到 content.json 文件中。这个函数内部使用 JSON.stringify 将数据转换为 JSON 字符串,并使用 fs.writeFile 将其写入文件。
- 遍历数据数组,对每个图片链接使用自定义的 downloadImage 函数(尽管函数体未在脚本中给出,但可以假设它使用 Axios 或其他 HTTP 客户端来下载图片)。脚本首先检查本地 ./imgs 目录是否存在,如果不存在则创建它。然后,将图片下载到该目录下,文件名使用 URL 的最后一部分(假设是文件名),并添加 .png 扩展名
d. 错误处理和资源清理
1 2 3 4 5 6 7
| } catch (error) { console.log('error', error); }
await browser.close(); })();
|
- 使用 try…catch 结构来捕获并处理可能出现的错误。
- 在脚本的最后,使用 await browser.close() 关闭浏览器实例,以释放资源。
3> 脚本主体中 writeToJsonFile 和 downloadImage 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| function writeToJsonFile(data, filename) { const jsonData = JSON.stringify(data, null, 2); fs.writeFile(filename, jsonData, "utf8", (err) => { if (err) { console.error("Error writing to JSON file: ", err); return; } console.log(`Data written to ${filename}`); }); }
async function downloadImage(imageUrl, outputPath) { try { const response = await axios({ url: imageUrl, method: "GET", responseType: "stream", });
const writer = fs.createWriteStream(outputPath);
response.data.pipe(writer);
return new Promise((resolve, reject) => { writer.on("finish", resolve); writer.on("error", reject); }); } catch (error) { console.error("Error downloading image:", error); throw error; } }
|
a. writeToJsonFile 函数
- 参数接收:函数接收两个参数,data(要写入文件的数据)和 filename(文件的名称,包括路径)。
- 数据格式化:使用 JSON.stringify(data, null, 2) 将 data 对象转换为格式化的 JSON 字符串。这里的 null 和 2 分别指定了 replacer(在此处不使用,因此为 null)和缩进级别(2 表示每级缩进两个空格)。
- 写入文件:调用 fs.writeFile 方法将格式化的 JSON 字符串写入到指定的文件中。fs.writeFile 是 Node.js 文件系统模块中的一个异步方法,它接受文件路径、要写入的数据、字符编码(此处为 ‘utf8’)和一个回调函数作为参数。
- 错误处理:如果在写入文件过程中发生错误,回调函数将被调用,并接收到一个错误对象 err。如果 err 存在,则打印错误信息并退出函数。否则,打印成功消息,表明数据已被写入文件。
b. downloadImage 函数
- 参数接收:函数接收两个参数,imageUrl(图片的 URL)和 outputPath(图片下载后保存的路径)。
- HTTP 请求:使用 axios 发起一个异步的 GET 请求到 imageUrl。通过设置 responseType: ‘stream’,axios 返回一个包含响应流的对象,而不是将响应体作为字符串或 Buffer 返回。
- 创建可写流:使用 fs.createWriteStream 创建一个可写流,该流将数据写入到 outputPath 指定的文件中。
- 管道传输:使用 .pipe() 方法将响应流(response.data)中的数据管道传输到可写流(writer)。这样,当数据从响应流中流出时,它会自动被写入到文件中。
- 写入完成监听:创建一个新的 Promise 对象,并在其上监听可写流的 ‘finish’ 和 ‘error’ 事件。当所有数据都被写入文件并且文件描述符被关闭时,’finish’ 事件被触发,Promise 被解析。如果在写入过程中发生错误,’error’ 事件被触发,Promise 被拒绝。
- 错误处理:如果在请求或写入过程中发生错误,则捕获该错误,打印错误信息,并重新抛出错误(这可能导致调用者需要处理这个错误)。
三. 运行
可改参数:
1 2 3 4
| for (let i = 0 ; i < 100 ; i++){ await page.evaluate((idx) => { window.scrollTo(0, 800*idx) },i);
|
需要翻多少页改成多少即可(main.js 中 19 行)
进入代码所在根目录下运行:
运行之后会自动弹出小红书,并且会自动创建 content.json 和 imgs 文件夹来储存对应爬取数据