小红书数据挖掘

小红书爬虫代码

一. 在要保存代码的目录下新建 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 }); // 设为 false 以便调试
const context = await browser.newContext();
const page = await context.newPage();

// 导航到目标页面
await page.goto("https://www.xiaohongshu.com/explore");

// 等待并抓取 #exploreFeeds 元素内容
try {
let arr = [];
// await page.waitForSelector('section', { timeout: 10000 });

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) => {
// item.img.split("/")[item.img.split("/").length-1]
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();
})();

// 写入json文件的写法
function writeToJsonFile(data, filename) {
const jsonData = JSON.stringify(data, null, 2); // 将数据转换为格式化的 JSON 字符串
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 {
// 发起 HTTP 请求获取图片数据
const response = await axios({
url: imageUrl,
method: "GET",
responseType: "stream",
});

// 创建可写流以将图片数据写入到文件
const writer = fs.createWriteStream(outputPath);

// 将响应流管道到可写流
response.data.pipe(writer);

// 监听 'finish' 事件,当写入完成时触发
return new Promise((resolve, reject) => {
writer.on("finish", resolve);
writer.on("error", reject);
});
} catch (error) {
console.error("Error downloading image:", error);
throw error;
}
}

二. 代码详解

第一次使用时需要运行:

1
npm init -y

会在该目录中新建 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 }); // 设为 false 以便调试
const context = await browser.newContext();
const page = await context.newPage();

// 导航到目标页面
await page.goto("https://www.xiaohongshu.com/explore");

// 等待并抓取 #exploreFeeds 元素内容
try {
let arr = [];
// await page.waitForSelector('section', { timeout: 10000 });

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) => {
// item.img.split("/")[item.img.split("/").length-1]
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 }); // 设为 false 以便调试
const context = await browser.newContext();
const page = await context.newPage();
  1. 使用 playwright.chromium.launch({ headless: false }) 启动一个非无头(即显示浏览器界面)的 Chromium 浏览器实例,方便调试。
  2. 创建一个新的浏览器上下文和页面,并导航到小红书的“探索”页面。

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
// 等待并抓取 #exploreFeeds 元素内容
try {
let arr = []
// await page.waitForSelector('section', { timeout: 10000 });

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,"======================================================================");
}
  1. 脚本通过循环来模拟用户滚动页面的行为。在每次循环中,它使用 page.evaluate 方法在浏览器上下文中执行 JavaScript 代码来滚动页面(每次滚动 800 像素)。
  2. 使用 page.waitForSelector 等待页面上出现 section 元素(这里假设 section 元素包含了需要抓取的数据)
  3. 使用 page.$$eval 方法抓取页面上所有 section 元素内的数据。这个方法在浏览器上下文中执行一个函数,该函数接收一个元素数组作为参数,并返回一个处理后的数组。在这个例子中,它返回了一个包含帖子标题、作者、点赞数和图片链接的对象数组。
  4. 将每次循环抓取的数据合并到一个数组中,并在控制台中打印出来。

c. 写入 JSON 文件和下载图片

1
2
3
4
5
6
7
8
9
10
11
12
writeToJsonFile(arr, "content.json");

arr.forEach((item) => {
// item.img.split("/")[item.img.split("/").length-1]
if (!fs.existsSync("./imgs")) {
fs.mkdirSync("./imgs");
}
downloadImage(
item.img,
`./imgs/${item.img.split("/")[item.img.split("/").length - 1]}.png`
);
});
  1. 使用自定义的 writeToJsonFile 函数将抓取的数据数组写入到 content.json 文件中。这个函数内部使用 JSON.stringify 将数据转换为 JSON 字符串,并使用 fs.writeFile 将其写入文件。
  2. 遍历数据数组,对每个图片链接使用自定义的 downloadImage 函数(尽管函数体未在脚本中给出,但可以假设它使用 Axios 或其他 HTTP 客户端来下载图片)。脚本首先检查本地 ./imgs 目录是否存在,如果不存在则创建它。然后,将图片下载到该目录下,文件名使用 URL 的最后一部分(假设是文件名),并添加 .png 扩展名

d. 错误处理和资源清理

1
2
3
4
5
6
7
 } catch (error) {
console.log('error', error);
}

// 关闭浏览器
await browser.close();
})();
  1. 使用 try…catch 结构来捕获并处理可能出现的错误。
  2. 在脚本的最后,使用 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
// 写入json文件的写法
function writeToJsonFile(data, filename) {
const jsonData = JSON.stringify(data, null, 2); // 将数据转换为格式化的 JSON 字符串
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 {
// 发起 HTTP 请求获取图片数据
const response = await axios({
url: imageUrl,
method: "GET",
responseType: "stream",
});

// 创建可写流以将图片数据写入到文件
const writer = fs.createWriteStream(outputPath);

// 将响应流管道到可写流
response.data.pipe(writer);

// 监听 'finish' 事件,当写入完成时触发
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 函数

  1. 参数接收:函数接收两个参数,data(要写入文件的数据)和 filename(文件的名称,包括路径)。
  2. 数据格式化:使用 JSON.stringify(data, null, 2) 将 data 对象转换为格式化的 JSON 字符串。这里的 null 和 2 分别指定了 replacer(在此处不使用,因此为 null)和缩进级别(2 表示每级缩进两个空格)。
  3. 写入文件:调用 fs.writeFile 方法将格式化的 JSON 字符串写入到指定的文件中。fs.writeFile 是 Node.js 文件系统模块中的一个异步方法,它接受文件路径、要写入的数据、字符编码(此处为 ‘utf8’)和一个回调函数作为参数。
  4. 错误处理:如果在写入文件过程中发生错误,回调函数将被调用,并接收到一个错误对象 err。如果 err 存在,则打印错误信息并退出函数。否则,打印成功消息,表明数据已被写入文件。

b. downloadImage 函数

  1. 参数接收:函数接收两个参数,imageUrl(图片的 URL)和 outputPath(图片下载后保存的路径)。
  2. HTTP 请求:使用 axios 发起一个异步的 GET 请求到 imageUrl。通过设置 responseType: ‘stream’,axios 返回一个包含响应流的对象,而不是将响应体作为字符串或 Buffer 返回。
  3. 创建可写流:使用 fs.createWriteStream 创建一个可写流,该流将数据写入到 outputPath 指定的文件中。
  4. 管道传输:使用 .pipe() 方法将响应流(response.data)中的数据管道传输到可写流(writer)。这样,当数据从响应流中流出时,它会自动被写入到文件中。
  5. 写入完成监听:创建一个新的 Promise 对象,并在其上监听可写流的 ‘finish’ 和 ‘error’ 事件。当所有数据都被写入文件并且文件描述符被关闭时,’finish’ 事件被触发,Promise 被解析。如果在写入过程中发生错误,’error’ 事件被触发,Promise 被拒绝。
  6. 错误处理:如果在请求或写入过程中发生错误,则捕获该错误,打印错误信息,并重新抛出错误(这可能导致调用者需要处理这个错误)。

三. 运行

可改参数:

1
2
3
4
for (let i = 0 ; i < 100 ; i++){
await page.evaluate((idx) => {
window.scrollTo(0, 800*idx)
},i);

需要翻多少页改成多少即可(main.js 中 19 行)

进入代码所在根目录下运行:

1
node main

运行之后会自动弹出小红书,并且会自动创建 content.json 和 imgs 文件夹来储存对应爬取数据