0%

利用GPT-4智能编写Node.js爬虫:一键备份孩子成长瞬间的照片和视频

最近,我一直在研究AIGC领域的各种软件。伴随着飞速的技术迭代,我发现自己竟有些应接不暇。前几天,我突然产生了一个想法:为什么不尝试用GPT来编写一些程序呢?但我一直纠结于到底要写什么。就在昨天,我突然想起了一个一直想做却总是找不到时间的项目,我认为这正是一个利用GPT技术的绝佳机会。

这个项目其实很简单。自从孩子出生以来,我就一直使用XX小屋这款APP来存储和分享孩子的照片和视频。时光荏苒,不知不觉已经积累了7年的时光。我一直想要备份这些照片和视频,但APP本身并不支持数据导出功能。因此,我决定编写一个爬虫程序来将这些数据下载到本地。

既然决定要动手,那就开始吧!目前,我还没有找到一种适合让GPT分析网络请求的方法,所以我决定自己来处理API的分析。通过研究网页版应用,我发现整个过程还算简单,主要涉及到两个API。

第一个API是/events/updates,它可以用于获取用户的所有照片信息。照片会按照事件进行分组,此接口有两个有用的参数:一个是babyId(孩子的ID),另一个是since(类似分页的参数,是一个时间戳)。通常,我们从0开始,然后获取100条数据。接着,将最后一条数据的updated_at_in_ts属性作为参数再次调用接口。此外,返回的数据中还有一个next属性,如果为true,则表示还有数据;如果为false,则表示所有数据已经返回。

第二个API是/moments/new,它可以用于获取阿里云OSS的参数。用户的图片和视频数据是存储在阿里云OSS上的,并且采用私有方式进行读写。因此,我们需要使用阿里云的临时授权来下载这些数据。这个接口正是用来获取临时授权的,获取到授权后,我们可以在前端生成加密URL,从而得到可下载的链接。

API分析完成后,接下来就是编写代码。由于我的前端经验较丰富,对JavaScript比较熟悉,所以我决定使用Node.js来开发这个程序。这次,我计划完全采用GPT-4来生成代码,于是我开始撰写第一个Prompt。

使用nodejs开发一个能够自动采集图片或视频路径并下载图片的程序要求如下经过分析采集流程如下

  1. 使用登录后的cookie调用API,cookie的内容设为一个变量,由用户输入
  2. 一些需要的变量 baby_id 由用户输入
  3. GET方式访问API https://xxxx/events/updates?style=best_12&skip_invisible=true&include_rt=true&since=0&baby_id=xxxx,其中style、> skip_invisible、include_rt为固定参数,baby_id为之前设置的变量,since从0开始,然后获取这次请求结果的最后一条数据的updated_at_in_ts属性的值
  4. 接口每次返回的数据为100条,循环调用接口,直到接口返回数据中的next属性为false为止
  5. 接口返回的数据格式如下
    1
    2
    3
    {
    // 这里放上示例数据,文章中省略
    }
  6. 解析接口数据,将数据简化并存入一个json文件文件格式如下,
    1
    2
    3
    {
    // 这里放上示例数据,文章中省略
    }
  7. 获取OSS加密参数GET 方式调用API https://xxx/moments/new?force_aliyun=true 返回结果如下
    1
    2
    3
    {
    // 这里放上示例数据,文章中省略
    }
  8. 使用加密参数加上第6步的json里的URL,生成下载链接,并开始下载图片和视频
  9. 下载目录规则如下{months}{days}{content||index}.文件后缀Please write in Chinese language.

ChatGPT给我返回了一段完整的代码

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
const axios = require("axios");
const fs = require("fs");

const getJSON = async (url, cookie, baby_id, since) => {
const response = await axios.get(url, {
headers: {
Cookie: cookie,
},
params: {
style: "best_12",
skip_invisible: "true",
include_rt: "true",
since: since,
baby_id: baby_id,
},
});

return response.data;
};

const getAllData = async (cookie, baby_id) => {
let since = 0;
let next = true;
let allData = [];

while (next) {
const data = await getJSON("<https://xxx/events/updates>", cookie, baby_id, since);

allData = allData.concat(data.list);
since = data.list[data.list.length - 1].updated_at_in_ts;
next = data.next;
}

return allData;
};

const extractData = (allData) => {
const extractedData = [];

allData.forEach((item) => {
item.layout_detail.forEach((detail) => {
if (detail.type === "picture" || detail.type === "video") {
extractedData.push({
months: item.months,
days: item.days,
type: detail.type,
content: detail.content,
[detail.type === "picture" ? "picture" : "original_video_path"]: detail[detail.type === "picture" ? "picture" : "original_video_path"],
});
}
});
});

return extractedData;
};

const saveDataToJSON = (extractedData) => {
fs.writeFileSync("data.json", JSON.stringify(extractedData, null, 2));
};

const getOSSParams = async () => {
const response = await axios.get("<https://xxx/moments/new>", {
params: {
force_aliyun: "true",
},
});

return response.data;
};

const downloadImagesAndVideos = async (extractedData, ossParams) => {
for (const item of extractedData) {
const url = item[item.type === "picture" ? "picture" : "original_video_path"];
const response = await axios.get(url, {
responseType: "stream",
});

const dir = `${item.months}/${item.days}`;
fs.mkdirSync(dir, { recursive: true });

const filename = `${dir}/${item.content || index}.${url.split(".").pop()}`;
response.data.pipe(fs.createWriteStream(filename));
}
};

(async () => {
const cookie = "your_cookie_here";
const baby_id = xxx;

const allData = await getAllData(cookie, baby_id);
const extractedData = extractData(allData);
saveDataToJSON(extractedData);

const ossParams = await getOSSParams();
await downloadImagesAndVideos(extractedData, ossParams);
})();

这段代码已经基本90%可用,但是还缺少OSS下载的相关代码。于是,我追问了几个问题,让GPT补充完整了代码。最终的代码如下:

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// download.js
const axios = require("axios");
const fs = require("fs-extra");
const OSS = require('ali-oss');
const path = require('path');
const emojiRegex = require('emoji-regex');
const config = require("./config.json");
const allLocalData = require("./data.json");

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const getAllData = async (cookie, babyId, since = 0, retries = 3) => {
try {

await delay(500); // 添加 500ms 延迟

const response = await axios.get(
`https://shiguangxiaowu.cn/events/updates?style=best_12&skip_invisible=true&include_rt=true&since=${since}&baby_id=${babyId}`,
{
headers: {
Cookie: cookie,
},
}
);

const data = response.data;
const lastItem = data.list[data.list.length - 1];
const nextSince = lastItem.updated_at_in_ts;

if (data.next) {
const nextPageData = await getAllData(cookie, babyId, nextSince);
return data.list.concat(nextPageData);
} else {
return data.list;
}
} catch (error) {
if (retries > 0) {
console.warn(`获取数据失败,正在重试... 剩余重试次数:${retries}`);
return getAllData(cookie, babyId, since, retries - 1);
} else {
console.error(`获取数据失败:${error.message}`);
return [];
}
}
};

const extractData = (allData) => {
const extractedData = [];
allData.forEach((item) => {
item?.layout_detail?.forEach((detail) => {
if (detail.type === "picture" || detail.type === "video") {
const urlKey = detail.type === "picture" ? "picture" : "video_path";
const url = detail[urlKey].replace(/^(?:https?:\/\/)[^/]+\/?/, "");

extractedData.push({
months: item.months,
days: item.days,
type: detail.type,
content: detail.content,
[urlKey]: url,
});
}
});
});

return extractedData;
};

const saveDataToJSON = (extractedData) => {
fs.writeFileSync("data.json", JSON.stringify(extractedData, null, 2));
};

const getOSSParams = async (cookie) => {
const response = await axios.get("https://shiguangxiaowu.cn/moments/new", {
params: {
force_aliyun: "true",
},
headers: {
Cookie: cookie,
},
});

return response.data;
};

const downloadImagesAndVideos = async (items, ossParams, basePath) => {
const ossClient = new OSS({
region: 'oss-cn-shenzhen',
accessKeyId: ossParams.access_key_id,
accessKeySecret: ossParams.access_key_secret,
stsToken: ossParams.sts_token,
bucket: 'timehut-cn-sz',
endpoint: 'https://oss-cn-shenzhen.aliyuncs.com',
refreshSTSTokenInterval: 30 * 60 * 1000, // 每30分钟刷新一次
refreshSTSToken: async function () {
// 在这里重新获取新的OSS参数
const newOSSParams = await getOSSParams(cookie);
return {
accessKeyId: newOSSParams.access_key_id,
accessKeySecret: newOSSParams.access_key_secret,
stsToken: newOSSParams.sts_token
};
}
});
const total = items.length;
for (const [index, item] of items.entries()) {
const dirPath = `${basePath}/${item.months}/${item.days}`;
await fs.ensureDir(dirPath); // 确保目录存在,如果不存在则创建目录

const filePath = `${dirPath}/${item.content ? removeEmojisAndNewlines(item.content) : index}`;

let fileUrl, extension;
if (item.type === 'picture') {
fileUrl = item.picture;
extension = path.extname(fileUrl);
} else if (item.type === 'video') {
fileUrl = item.video_path;
extension = path.extname(fileUrl);
}

const localPath = `${filePath}${extension}`;

if (fs.existsSync(localPath)) {
console.log(`(${index + 1}/${total}) 文件已存在,跳过下载: ${localPath}`);
continue;
}

try {
const result = await ossClient.get(fileUrl, localPath);
console.log(`(${index + 1}/${total}) 下载${item.type === 'picture' ? '图片' : '视频'}成功: ${localPath}`);
} catch (error) {
console.log(`(${index + 1}/${total}) 下载失败:`, error);
}
}
};
// 清除文件名中的表情符号和换行符
function removeEmojisAndNewlines(text) {
const regex = emojiRegex();
return text.replace(regex, '').replace(/[\r\n]+/g, '');
}

(async () => {
const cookie = config.cookie;
const baby_id = config.baby_id;
const basePath = config.basePath;
const useLocalData = config.useLocalData;

if (!useLocalData || allLocalData.length === 0) {
const allData = await getAllData(cookie, baby_id);
const extractedData = extractData(allData);
saveDataToJSON(extractedData);
}

const data = useLocalData ? allLocalData : extractedData;
const ossParams = await getOSSParams(cookie);
await downloadImagesAndVideos(data, ossParams, basePath);
})();

1
2
3
4
5
6
7
// config.json
{
"cookie": "xxx",
"baby_id": xxx,
"basePath": "Photos",
"useLocalData": false
}

在实际过程中,我通过追问进行了几次优化:

  1. 调用接口速度过快导致失败。为解决这个问题,我增加了延时,并增加了重试机制。
  2. OSS调用增加了refreshSTSToken机制。
  3. 对已经下载过的图片进行跳过,避免重复下载。
  4. 增加了下载目录的设置,并将配置放到了单独的配置页面。
  5. 增加了使用本地数据的方法。

经过大约一个小时的调试,我成功地将30多GB的照片和视频下载到了本地。整个开发调试过程耗时约2个小时。在整个过程中,我仅仅阅读了代码,仅在几个简单的地方进行了修改(我实在懒得再写Prompt了)。基本上,我没有参与编码工作,而是让GPT出色地完成了任务。

通过这次开发过程我总结了几个经验

  1. 尽量把需求和处理逻辑写清楚考虑清楚,然后让GPT写,效果会比较好,比较贴近你的需求
  2. 需求文笔不一定要多好,但是描述要尽量清晰
  3. 需求部分可以让GPT协助拓展,有时候可能补充一些你没有想到的点
  4. GPT4效果比GPT3.5效果要好很多