胀大的野心与实际的困境
上一节跟着我能抓openai的列表之后,我的野心开端胀大,既然咱们写了一个结构,能够开端写面向各网站的爬虫了,为什么只面向ChatGPT呢?几乎一切的平台都是这么个形式,一个列表,然后逐个抓取。那我能不能把这个才干泛化呢?可不能够规划一套机制,让一切的抓取功用都变得很简略呢?我抽取一系列的基础才干,而不管抓哪个网站只需求复用这些才干就能够快速的开宣布爬虫。公司内的各种平台都是这么想的对吧?
那么咱们就需求进行规划建模,假如按照正常的面向对象,我可能会这么规划建模:
看起来很美好不是吗?是不是能够按照规划去写代码了?其实完全是扯淡,魔鬼隐藏在细节中,每个网站都有各种杂乱的HTML、他们可能是简略的列表,也可能是存在好几个iframe,并且你在界面上看到的列表和你真实点开的又不一样,比方说:
- 有的小说网站,它的列表上假如有N个列表项,可是你真的点击去之后,你会发现有的章节点击他只有一半内容,再点下一页的时分它会调到一个不在列表页上的展示的页面,展示后半段内容,而你假如只依据列表链接去抓,你会丢掉这后半段内容。
- 有的网站会在你点了几个页面后随机呈现一个按钮,点击了才干展开后续内容,防止机器抓取。你不处理这种状况,直接去抓就抓不全。
- 而有的网站根本便是图片展示文本内容,你得把图片搞下来,然后OCR辨认,或者插入了各种看不见的文本需求被清洗掉。
- 并且每个网站还会晋级换代,他们一晋级换代,你的抓取方式也要跟着变。 等等等等……并且一切这些要素之间还能够排列组合:
所以最上面的那个建模只能说过于简化而没有用途,起码,以前是这样的。
在以前,咱们可能会进一步完善这个规划,得到一系列杂乱的内部子概念、子机制、子战略,比方:
- 反防抓机制
- 详情分页抓取战略
- 清洗机制
然后对这些机制进行组合。
然而这并不会让问题变简略,人们总是低估胶水代码的杂乱度,最终要么整个体系十分软弱,要么就从胶水处开端堕落。
新年代,新思路
那么在今日,咱们有没有什么新的做法呢?咱们从一个代码示例开端讲起,比方,我这儿有一个抓取某小说网站的代码:
const fs = require('fs/promises');
async function main() {
const novel_section_list_url = 'https://example.com/list-1234.html';
await driver.goto(novel_section_list_url);
const novelSections = await driver.evaluate(() => {
let title = getNovelTitle(document)
let section_list = getNovelSectionList(document);
return {
title, section_list
}
function getNovelTitle(document) {
return document.querySelector("h1.index_title").textContent;
}
function getNovelSectionList(document) {
let result = [];
document.querySelectorAll("ul.section_list>li>a").forEach(item => {
const { href } = item;
const name = item.textContent;
result.push({ href, name });
});
return result;
}
});
console.log(novelSections.section_list.length);
const batchSize = 50;
const title = novelSections.title;
let section_list = novelSections.section_list;
if (intention.part_fetch) {
section_list = novelSections.section_list.slice(600, 750);
}
await batchProcess(section_list, batchSize, async (one_batch, batchNumber) => {
await download_one_batch_novel_content(one_batch, driver);
async function download_one_batch_novel_content(one_batch, driver) {
let one_text_file_content = "";
for (section of one_batch) {
await driver.goto(section.href);
await driver.waitForTimeout(3000);
const section_text = await driver.evaluate(() => {
return "\n\n" + document.querySelector("h1.chapter_title").textContent
+ "\n"
+ document.querySelector("#chapter_content").textContent;
});
one_text_file_content += section_text;
}
await fs.writeFile(`./output/example/${title}-${batchNumber}.txt`, one_text_file_content);
}
});
}
main().then(() => { });
async function batchProcess(list, batchSize, asyncFn) {
const listCopy = [...list];
const batches = [];
while (listCopy.length > 0) {
batches.push(listCopy.splice(0, batchSize));
}
let batchNumber = 12;
for (const batch of batches) {
await asyncFn(batch, batchNumber);
batchNumber++;
}
}
在实际作业中这样的代码应该是比较常见的,由于上述的规划没有什么用途,咱们常常见到的便是另一个极端,那便是代码写的过于随意,整个代码的完成变得无法阅读,当我想要做略微地调整,比方说我昨日抓了100个,今日接着从101个往后抓,就要去读代码,然后从代码中看改点什么好让这个抓取能够从101往后抓。
那在以前呢,咱们就要像上面说的要规划比较精细的机制,而越是精细的机制,就越不强健。并且,以我的经历,你想让人们运用那么精细的机制也不好办,因为大多数人的才干并不足以驾驭精细的机制。
而在今日,咱们能够做的更粗豪一些。
首先,咱们意识到有些代码,准确的说,是有些变量,是咱们常常修正的,所以咱们在不改变全体结构的状况下,咱们把这些变量说到上面去,变成一个变量:
//目的描绘
const intention = {
list_url:'https://example.com/list-1234.html',
batchSize: 50,
batchStart: 12,
page_waiting_time: 3000,
part_fetch:{ //假如全抓取,就注释掉整个part_fetch特点
from:600,//不含该下标
to:750
},
output_folder: "./output/example"
}
const fs = require('fs/promises');
const driver = require('../util/driver.js');
async function main() {
const novel_section_list_url = intention.list_url;
await driver.goto(novel_section_list_url);
const novelSections = await driver.evaluate(() => {
let title = getNovelTitle(document)
let section_list = getNovelSectionList(document);
return {
title, section_list
}
function getNovelTitle(document) {
return document.querySelector("h1.index_title").textContent;
}
function getNovelSectionList(document) {
let result = [];
document.querySelectorAll("ul.section_list>li>a").forEach(item => {
const { href } = item;
const name = item.textContent;
result.push({ href, name });
});
return result;
}
});
console.log(novelSections.section_list.length);
const batchSize = intention.batchSize;
const title = novelSections.title;
let section_list = novelSections.section_list;
if (intention.part_fetch) {
section_list = novelSections.section_list.slice(intention.part_fetch.from, intention.part_fetch.to);
}
await batchProcess(section_list, batchSize, async (one_batch, batchNumber) => {
await download_one_batch_novel_content(one_batch, driver);
async function download_one_batch_novel_content(one_batch, driver) {
let one_text_file_content = "";
for (section of one_batch) {
await driver.goto(section.href);
await driver.waitForTimeout(intention.page_waiting_time);
const section_text = await driver.evaluate(() => {
return "\n\n" + document.querySelector("h1.chapter_title").textContent
+ "\n"
+ document.querySelector("#chapter_content").textContent;
});
one_text_file_content += section_text;
}
await fs.writeFile(`${intention.output_folder}/${title}-${batchNumber}.txt`, one_text_file_content); //一个批次一存储
}
});
}
main().then(() => { });
async function batchProcess(list, batchSize, asyncFn) {
const listCopy = [...list];
const batches = [];
while (listCopy.length > 0) {
batches.push(listCopy.splice(0, batchSize));
}
let batchNumber = intention.batchStart;
for (const batch of batches) {
await asyncFn(batch, batchNumber);
batchNumber++;
}
}
于是咱们把程序分成了两部分结构:
接下来我会发现,在网站不变的状况下,下面这个目的履行代码适当的安稳。我常常需求做的不管是偏移量的计算,仍是修正抓取目标等等,这些都只需求修正上面的目的描绘数据结构即可。并且咱们能够做进一步的封装,得到下面的代码(下面的JsDoc也是ChatGPT给我写的):
/**
* @typedef {Object} Intention
* @property {string} list_url
* @property {integer} batchSize
* @property {integer} batchStart
* @property {integer} page_waiting_time
* @property {PartFetch} part_fetch 假如全抓取,就注释掉整个part_fetch特点
* @property {string} output_folder
*
* @typedef {Object} PartFetch
* @property {integer} from 不含该下标
* @property {integer} batchStart
*/
//目的履行
/**
* @param {Intention} intention
*/
module.exports = (intention, context) => {
Object.assign(this, context);
const {fs,console} = context;
async function main() {
const novel_section_list_url = intention.list_url;
await driver.goto(novel_section_list_url);
const novelSections = await driver.evaluate(() => {
let title = getNovelTitle(document)
let section_list = getNovelSectionList(document);
return {
title, section_list
}
function getNovelTitle(document) {
return document.querySelector("h1.index_title").textContent;
}
function getNovelSectionList(document) {
let result = [];
document.querySelectorAll("ul.section_list>li>a").forEach(item => {
const { href } = item;
const name = item.textContent;
result.push({ href, name });
});
return result;
}
});
console.log(novelSections.section_list.length);
const batchSize = intention.batchSize;
const title = novelSections.title;
// const section_list = novelSections.section_list.slice(0, 3);
let section_list = novelSections.section_list;
if (intention.part_fetch) {
section_list = novelSections.section_list.slice(intention.part_fetch.from, intention.part_fetch.to);
}
await batchProcess(section_list, batchSize, async (one_batch, batchNumber) => {
await download_one_batch_novel_content(one_batch, driver);
async function download_one_batch_novel_content(one_batch, driver) {
let one_text_file_content = "";
for (section of one_batch) {
await driver.goto(section.href);
await driver.waitForTimeout(intention.page_waiting_time);
const section_text = await driver.evaluate(() => {
return "\n\n" + document.querySelector("h1.chapter_title").textContent
+ "\n"
+ document.querySelector("#chapter_content").textContent;
});
one_text_file_content += section_text;
}
await fs.writeFile(`${intention.output_folder}/${title}-${batchNumber}.txt`, one_text_file_content); //一个批次一存储
}
});
}
main().then(() => { });
async function batchProcess(list, batchSize, asyncFn) {
const listCopy = [...list];
const batches = [];
while (listCopy.length > 0) {
batches.push(listCopy.splice(0, batchSize));
}
let batchNumber = intention.batchStart;
for (const batch of batches) {
await asyncFn(batch, batchNumber);
batchNumber++;
}
}
}
于是咱们就有了一个安稳的接口将目的的描绘和目的的履行彻底分离,跟着我对我的代码进行了进一步的收拾后发现,这个目的描绘结构居然适当的通用,我写的好多网站的抓取代码居然都能够抽取出这样一个结构。 于是咱们能够进一步笼统,到了一种适用于我特定范畴的DSL,类似下面的结构:
到此为止,我的目的描绘和目的履行彻底解耦,目的履行变成了目的描绘中的一个特点,我只需求写一个引擎,依据目的描绘中entrypoint的特点值,加载对应的函数,然后将目的数据传给他就能够了,大约的代码如下:
const intentionString = await fs.readFile(templatePath, 'utf8');
const intention = yaml.load(intentionString);
const intention_exec = require(intention.entrypoint);
intention_exec(intention, context);
而咱们的每一个目的履行的代码,能够有自己的不同改变原因,不管是网站晋级了,仍是咱们要抓下一个网站了,咱们只需求把HTML扔给ChatGPT,他就能够帮咱们生成对应的目的履行代码。哪怕咱们想根据一些能够复用库函数,比方之前说的反防抓、反详情页分页机制封装的库函数,他也能够给咱们生成胶水代码把这些函数粘起来(具体的办法咱们在后续的文章里讲),一切这一切的改变,都能够用ChatGPT生成代码这一步解决。那么所谓的在胶水层堕落的问题也就不存在了。
很风趣的是,在我根据该结构的DSL得到一组实例之后,我很快就开端产生了在DSL这一层的新需求,比方:
- DSL文件的管理需求,因为人总是很懒的,并且我只有业余时间写点这些东西,不能保证自己一向记得哪个网站对应哪个文件,然后怎样设置。
- 我还期望能够依据我本地现已抓的内容和智能生成偏移量
- 我也期望能定时去检查更新然后生成抓取目的。
这一切都是很有价值的需求,而假如咱们没有一个安稳的基层DSL结构,咱们这些更上层需求也注定是不安稳的。
而有了这个安稳的DSL结构后,咱们回过头来看咱们的规划,其实是在更大的尺度上完成了面向对象规划中的开闭原则,虽然扩展需求很多的代码,而这些代码却并不需求人来写,所以效率仍然很高。
总结一下
在这个编程秀里边,咱们做了什么?咱们并没有做一个功用,而是面向ChatGPT对咱们的代码进行了一个规划。
- 首先,咱们剖析了传统的面向对象建模办法的局限性,指出它过于简化且无法解决实际问题。
- 接着,咱们提出了新年代的新思路,通过将目的描绘和目的履行进行解耦,使得某一个场景的开发变得愈加简略,数据结构也愈加通用。于是咱们得到了在ChatGPT年代编程的最小元素的规范笼统方式:
- 最终,咱们畅想了一下,在咱们得到这种安稳的数据结构后,咱们能够再更上层做更多的开发作业,而因为接口很安稳,上层的开发作业也不至于是在浮沙之上建高塔。
这儿想再聊深一点,说点半题外话,其实到这儿咱们能够看出,咱们最一开端抽出来的那个模型,并不是没有用,仅仅他在更上层有用。而它把杂乱度压给了这一层的程序员。这一层的程序员自然是不满意的。所以所谓的没有用途其实是一个诉苦,背面本质上是一种劳作者关于被逼迫进行深重劳作的不满。是一种上层的优雅和基层的深重劳作之间的对立的体现。这个对立是不可谐和的,有人想优雅就有人要深重,而ChatGPT的呈现一定程度上转移了这个对立,最深重的作业给了它,使得开发者原地变成了管理者,变成得“优雅”了。这种优雅带来的是好仍是坏,咱们还不知道,但咱们期望是好的。
好的,那么当咱们有了最小元素的笼统之后,上一篇文章遗留的问题咱们只回答了一半,咱们还要进一步考虑整个体系应该怎样规划架构才干更大极限的发挥ChatGPT的才干,而这是咱们后面的内容。