思考問題域
我要寫一個(gè)爬蟲瘸右,把ChatGPT上我的數(shù)據(jù)都爬下來猪瞬,首先想想我們的問題域膀懈,我想到幾個(gè)問題:
- 不能用HTTP請求去爬仅仆,如果我直接用HTTP請求去抓的話器赞,一個(gè)我要花太多精力在登錄上了,而我的數(shù)據(jù)又不多墓拜,另一個(gè)港柜,現(xiàn)在都是單頁引用,你HTTP爬下來的根本就不對啊。
- 所以最好是自動化測試的那種方式夏醉,啟動瀏覽器去爬爽锥。
- 但是我又不能保證一次把代碼寫成功,反復(fù)登錄的話畔柔,會被網(wǎng)站封號氯夷,就幾個(gè)數(shù)據(jù),不值當(dāng)?shù)摹?/li>
所以總的來說我需要一個(gè)這樣的流程:
從流程上我們是不是可以看出靶擦,這個(gè)流程跟我們用WebConsole試驗(yàn)一段代碼的過程很像腮考?
從這種相似性可以看出,我需要一個(gè)類似WebConsole的東西來實(shí)現(xiàn)我要的效果玄捕,這個(gè)東西學(xué)名叫REPL(Read–eval–print loop)踩蔚,不過你不知道這個(gè)名字也無所謂,不影響枚粘。
而且還不止寂纪,我需要從文件讀入我的代碼,畢竟沒有代碼高亮赌结,我可寫不好程序捞蛋。從文件讀入的話,我就可以用vscode給我提供代碼高亮柬姚,這豈不是美滋滋拟杉。
想到這,如果是以前量承,我已經(jīng)一個(gè)頭兩個(gè)大了搬设,我只是想抓點(diǎn)東西,你讓我搞個(gè)REPL撕捍,這玩意聽著就不科學(xué)拿穴,我一天就晚上有一個(gè)小時(shí)的時(shí)間寫點(diǎn)代碼奋献,這玩意太復(fù)雜了袄膏,乍一看也不是我業(yè)余時(shí)間能寫出來的。
但是現(xiàn)在壶笼,我們有了ChatGPT狮腿,那么我接下來可以用ChatGPT開始寫腿宰,簡單做一個(gè)任務(wù)分解:
- 寫一個(gè)框架,可以最大限度的幫我們提高開發(fā)效率
- 在這個(gè)框架上缘厢,一個(gè)場景一個(gè)場景的開發(fā)爬蟲
用ChatGPT寫一個(gè)爬蟲
1. 先寫一個(gè)框架
首先吃度,我可能不知道這個(gè)東西叫REPL,但是我知道控制臺贴硫,那我就這么問:
如何用nodejs做一個(gè)命令行交互控制臺椿每,執(zhí)行輸入的js代碼
然后我就得到了下面的代碼:
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '>> '
});
rl.prompt();
rl.on('line', (line) => {
try {
const result = eval(line);
console.log(result);
} catch (err) {
console.error(err);
}
rl.prompt();
});
rl.on('close', () => {
console.log('Exiting...');
process.exit(0);
});
但是這個(gè)代碼還不夠我用的,我需要文件輸入,所以我可以問:
如果是讀入文件作為輸入呢间护?
然后我得到了下面的代碼:
const fs = require('fs');
const fileName = 'path/to/file.js';
const fileContent = fs.readFileSync(fileName, 'utf8');
try {
const result = eval(fileContent);
console.log(result);
} catch (err) {
console.error(err);
}
第一反應(yīng)删壮,這個(gè)不是async/await模式調(diào)用的呀,我后面代碼寫起來會很麻煩兑牡,于是我就說:
我要用fs/promises庫實(shí)現(xiàn)上面邏輯怎么寫呢央碟?
然后我就得到了下面的代碼:
const fs = require('fs').promises;
const fileName = 'path/to/file.js';
const fileContent = await fs.readFile(fileName, 'utf8');
try {
const result = eval(fileContent);
console.log(result);
} catch (err) {
console.error(err);
}
這個(gè)點(diǎn)算是令我滿意了,但馬上我又意識到不對勁均函,我需要執(zhí)行完之后 hang 在那里等待下一次輸入的亿虽,他這執(zhí)行完就退出了,于是我又改進(jìn)了的問題:
我要實(shí)現(xiàn)的效果是苞也,啟動后在命令行里打印一個(gè)“>”然后等待輸入洛勉,我敲擊回車,程序就會讀入代碼中寫死的目標(biāo)文件如迟,并執(zhí)行其中的js收毫,然后重新打印一個(gè)“>“等待下一次輸入,無限循環(huán)殷勘。
然后我得到了下面的代碼:
const readline = require('readline');
const fs = require('fs').promises;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '> '
});
let filePath = 'path/to/target/file.js';
rl.prompt();
rl.on('line', async () => {
try {
const fileContent = await fs.readFile(filePath, 'utf8');
const result = eval(fileContent);
console.log(result);
} catch (err) {
console.error(err);
}
rl.prompt();
});
rl.on('SIGINT', () => {
console.log('Exiting...');
rl.close();
process.exit(0);
});
注意此再,我再第一次的時(shí)候忘記了寫”無限循環(huán)“,所以他寫的代碼就只執(zhí)行兩次玲销,仔細(xì)一看输拇,確實(shí)這樣實(shí)現(xiàn)也符合我說的邏輯,他沒有猜測我要無限循環(huán)贤斜,所以在使用的時(shí)候要注意策吠,有時(shí)候沒得到你想要的結(jié)果可能是你沒說清楚。
那么看起來到此為止就差不多了瘩绒,沒想到也不復(fù)雜猴抹。那我測試一下。
- 我建立了一個(gè)下面的工程結(jié)構(gòu):
.
├── index.js
└── input
└── input.js
- 把文件名寫死為
input/input.js
- 執(zhí)行
node index.js
彩蛋:其中有一次執(zhí)行的代碼在后續(xù)執(zhí)行發(fā)現(xiàn)錯(cuò)誤锁荔,
-
發(fā)現(xiàn)下面錯(cuò)誤:
``` /Volumes/MyLab/chatgpt-show/crawler/workspace/v1/index.js:15 const input = await new Promise(resolve => rl.question('', resolve)); ^^^^^ SyntaxError: await is only valid in async functions and the top level bodies of modules at internalCompileFunction (node:internal/vm:73:18) at wrapSafe (node:internal/modules/cjs/loader:1149:20) at Module._compile (node:internal/modules/cjs/loader:1190:27) at Module._extensions..js (node:internal/modules/cjs/loader:1280:10) at Module.load (node:internal/modules/cjs/loader:1089:32) at Module._load (node:internal/modules/cjs/loader:930:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) at node:internal/main/run_main_module:23:47 ```
-
原來是await用錯(cuò)了蟀给,不過沒關(guān)系,直接拷給他:
- 上面的代碼執(zhí)行報(bào)錯(cuò):
SyntaxError: await is only valid in async functions and the top level bodies of modules
- 上面的代碼執(zhí)行報(bào)錯(cuò):
于是得到了修改后的代碼
2. 在這個(gè)框架上堕战,開發(fā)爬蟲
那么接下來坤溃,我可以試著抓取一下列表了拍霜,這部分GPT3.5就不能勝任了嘱丢,我切換到GPT4,然后問:
我用puppeteer打開一個(gè)網(wǎng)頁祠饺,可以直接用document DOM API操作越驻,然后返回?cái)?shù)據(jù),請抓取這個(gè)網(wǎng)頁的列表,HTML如下:
\
下面是openAI的頁面代碼缀旁,但是呢记劈,因?yàn)樗罅耍也坏貌皇謩幼隽它c(diǎn)刪減并巍,比如我列表很長目木,我就減到只剩3個(gè)元素,其他無關(guān)的元素的代碼我都刪掉懊渡。
經(jīng)過幾次詢問刽射,他給出了我代碼:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('你的網(wǎng)址'); // 將'你的網(wǎng)址'替換為您要打開的頁面的實(shí)際URL
const list = await page.evaluate(() => {
const anchors = document.querySelectorAll('a');
const items = [];
for (const anchor of anchors) {
const itemText = anchor.querySelector('div.flex-1.text-ellipsis.max-h-5.overflow-hidden.break-all.relative');
if (itemText) {
const itemTitle = itemText.textContent.trim();
const itemLink = anchor.href;
items.push({ title: itemTitle, link: itemLink });
}
}
return items;
});
console.log(list);
await browser.close();
})();
3. 回到任務(wù)1的問題域
真到執(zhí)行的時(shí)候又發(fā)現(xiàn)新問題,這個(gè)puppeteer我希望他一直開著剃执,就不能在input.js里啟動誓禁,我需要在REPL代碼里就執(zhí)行,所以我需要稍微修改下代碼肾档,我說:
我需要在控制臺啟動的時(shí)候用puppeteer啟動一個(gè)瀏覽器摹恰,然后每次回車可以讓文件中的代碼可以通過puppeteer訪問啟動的這個(gè)瀏覽器
然后得到了代碼:
async function main() {
const readline = require('readline');
const fs = require('fs').promises;
const puppeteer = require('puppeteer-core');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '> '
});
const browser = await puppeteer.launch({
executablePath: '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
});
let filePath = 'path/to/target/file.js';
rl.prompt();
async function executeWithBrowser(browser) {
const fileContent = await fs.readFile(filePath, 'utf8');
return eval(`async function (browser) { ${fileContent} }`)(browser);
}
rl.on('line', async () => {
try {
const result = await executeWithBrowser(browser);
console.log(result);
} catch (err) {
console.error(err);
}
rl.prompt();
});
rl.on('SIGINT', async () => {
console.log('Exiting...');
await browser.close();
rl.close();
process.exit(0);
});
}
main();
這代碼還是不令我滿意,我又接連問了下面幾個(gè)問題:
- 我需要browser是GUI可見的
- 把path/to/target/file.js 改為 input/input.js
- 我需要在進(jìn)入input/input.js之前直接啟動一個(gè)page怒见,里直接訪問page而不是browser
- 這行代碼:
return eval(async function (page) { ${fileContent} }
)(page);
報(bào)錯(cuò):
xxxx
能不能不用eval俗慈? - 報(bào)錯(cuò):
/Volumes/MyLab/chatgpt-show/crawler/workspace/v1/index.js:11
const browser = await puppeteer.launch({
^^^^^
SyntaxError: await is only valid in async functions and the top level bodies of modules
最后得到了我可以執(zhí)行的代碼。不過實(shí)際執(zhí)行中還出現(xiàn)了防抓機(jī)器人的問題遣耍,經(jīng)過一些列的查找解決了這個(gè)問題姜盈,為了突出重點(diǎn),這里就不貼解決過程了配阵,最終代碼如下:
const readline = require('readline');
const fs = require('fs').promises;
// const puppeteer = require('puppeteer-core');
const puppeteer = require('puppeteer-extra')
// add stealth plugin and use defaults (all evasion techniques)
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '> '
});
const browser = await puppeteer.launch({
executablePath: '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome',
headless: false,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security']
});
const page = await browser.newPage();
let filePath = 'input/input.js';
rl.prompt();
async function executeWithPage(page) {
const fileContent = await fs.readFile(filePath, 'utf8');
const func = new Function('page', fileContent);
return func(page);
}
rl.on('line', async () => {
try {
const result = await executeWithPage(page);
console.log(result);
} catch (err) {
console.error(err);
}
rl.prompt();
});
rl.on('SIGINT', async () => {
console.log('Exiting...');
await browser.close();
rl.close();
process.exit(0);
});
})();
4. 最后回到具體的爬蟲代碼
而既然瀏覽器一直開著了馏颂,那我們需要執(zhí)行的代碼其實(shí)只有兩個(gè)了:
- goto_chatgpt.js
(async () => {
await page.goto('https://chat.openai.com/chat/');
})();
- fetch_list.js
(async () => {
const list = await page.evaluate(() => {
const anchors = document.querySelectorAll('a');
const items = [];
for (const anchor of anchors) {
const itemText = anchor.querySelector('div.flex-1.text-ellipsis.max-h-5.overflow-hidden.break-all.relative');
if (itemText) {
const itemTitle = itemText.textContent.trim();
const itemLink = anchor.href;
items.push({ title: itemTitle, link: itemLink });
}
}
return items;
});
console.log(list);
})();
當(dāng)然實(shí)際上fetch_list.js有點(diǎn)問題,因?yàn)閛penai做了防抓程序棋傍,我們可能很難搞到列表項(xiàng)的鏈接救拉,不過這個(gè)也不難,我們用名字匹配挨個(gè)點(diǎn)就好了嘛瘫拣,反正也不多亿絮。
比如下面這樣:
(async () => {
const targetTitle = 'AI Replacing Human';
const targetSelector = await page.evaluateHandle((targetTitle) => {
const anchors = document.querySelectorAll('a');
for (const anchor of anchors) {
const itemText = anchor.querySelector('div.flex-1.text-ellipsis.max-h-5.overflow-hidden.break-all.relative');
if (itemText && itemText.textContent.trim() === targetTitle) {
return anchor;
}
}
return null;
}, targetTitle);
if (targetSelector) {
const box = await targetSelector.boundingBox();
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
console.log(`Clicked the link with title "${targetTitle}".`);
} else {
console.log(`No link found with title "${targetTitle}".`);
}
})();
說句題外話,上面的代碼很有意思麸拄,似乎它為了防止點(diǎn)某個(gè)具體元素不管用派昧,竟然點(diǎn)擊了一個(gè)區(qū)域。
接下來如果我們想備份我們的每一個(gè)thread就可以在這個(gè)基礎(chǔ)上拢切,讓ChatGPT繼續(xù)給我們寫實(shí)現(xiàn)完成即可蒂萎,這里就不繼續(xù)展開了,大家可以自己完成淮椰。
回顧一下五慈,我們做了什么纳寂,得到了什么?
- 首先泻拦,我們對問題域做了分析毙芜,把目標(biāo)網(wǎng)站和工作者我本人以及時(shí)間限制等約束都納入了問題域進(jìn)行了分析,得到了一個(gè)方案争拐,然后通過類比發(fā)現(xiàn)我們的方案其實(shí)就是做一個(gè)有特定上下文的REPL腋粥,然后用這個(gè)REPL再去干具體的事。
- 接著架曹,我們基于這個(gè)上下文做了任務(wù)分解灯抛,粗略分成了做一個(gè)REPL和實(shí)現(xiàn)具體的抓取代碼兩部分。
- 接著我們靠ChatGPT把些任務(wù)實(shí)現(xiàn)音瓷,在實(shí)現(xiàn)的過程中对嚼,我們發(fā)現(xiàn)自己對問題域的細(xì)節(jié)了解不夠,于是我們又迭代了我們的任務(wù)列表绳慎∽菔可以說方案沒有大的變化,實(shí)現(xiàn)上做了很多調(diào)整杏愤。
最終靡砌,我們就靠ChatGPT把這個(gè)REPL給做了出來,為了寫一個(gè)這樣的小功能珊楼,我們做了個(gè)框架通殃,頗有點(diǎn)為了這點(diǎn)醋才包的這頓餃子的味道了。這要是在以前的時(shí)代厕宗,是一個(gè)巨大的浪費(fèi)画舌,但其實(shí)先做一個(gè)框架的思路在ChatGPT時(shí)代應(yīng)該成為一種習(xí)慣,它會從兩個(gè)方面帶來好處:
- 可以降低輸入的文本數(shù)量已慢,避免ChatGPT犯錯(cuò)曲聂。因?yàn)楹芏嗳硕贾溃珻hatGPT可以快速寫出一些小程序佑惠,但是長一點(diǎn)的總是會出錯(cuò)朋腋,很多人到這里就放棄了,但其實(shí)膜楷,我們會發(fā)現(xiàn)如果我們能把問題分解到它恰好擅長的領(lǐng)域我們就可以最大限度的利用它的優(yōu)勢旭咽,規(guī)避它的劣勢。人類歷史上赌厅,蒸汽機(jī)車發(fā)明的時(shí)候穷绵,它肯定不如馬耐顛,但為了充分理由他的優(yōu)勢察蹲,人們?yōu)樗伭髓F軌请垛。直到今天為了發(fā)揮機(jī)動車的效力催训,我們還是要修路鋪軌洽议,但是我們并不覺得有什么不對宗收,從這個(gè)角度來講,我們也不該只盯著ChatGPT的缺點(diǎn)看亚兄,揚(yáng)長避短才是正道混稽。
- 縮短反饋環(huán),提高效率审胚。從整體效率角度來講匈勋,只有反饋環(huán)的縮短才是真正提高了效率,某一步的快速完成并不真正提高效率膳叨。所謂反饋環(huán)的縮短在我們的上下文里就是”我想到怎么編碼完成任務(wù) -> 編碼 -> 測試 -> 得知代碼執(zhí)行失敗->我又想到怎么編碼完成任務(wù)"的這個(gè)循環(huán)洽洁,我們不能假設(shè)代碼編寫一次成功,所以這個(gè)環(huán)越短菲嘴,我們的效率就越高饿自。在這個(gè)例子里我想到了我不能一次寫對,所以我就先做了REPL龄坪,這就是所謂磨刀不誤砍柴工昭雌。但是道理大家都懂,在有ChatGPT之前健田,磨刀這個(gè)事他總是誤砍柴工的烛卧,但是在今天,你可以用幾個(gè)問題就得到一個(gè)趁手的工具妓局,開始你的工作总放,所以不要著急沖進(jìn)去工作,先做個(gè)工具可能是新時(shí)代的好習(xí)慣好爬。
下一篇间聊,我們將進(jìn)入這樣一個(gè)場景:我基于這個(gè)框架,我寫了很多爬蟲代碼抵拘,我該怎么組織和管理這些代碼呢哎榴?我需不需要一個(gè)精妙設(shè)計(jì)的內(nèi)部框架和規(guī)范來組織我的代碼呢?