ChatGPT編程秀:從一個(gè)爬蟲開始

思考問題域

我要寫一個(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)一段代碼的過程很像腮考?

01-02-web-console-working-process.png

從這種相似性可以看出,我需要一個(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ù)分解:

  1. 寫一個(gè)框架,可以最大限度的幫我們提高開發(fā)效率
  2. 在這個(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
  • 于是得到了修改后的代碼

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è)方面帶來好處:

  1. 可以降低輸入的文本數(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)長避短才是正道混稽。
  2. 縮短反饋環(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ī)范來組織我的代碼呢?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末僵蛛,一起剝皮案震驚了整個(gè)濱河市尚蝌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌充尉,老刑警劉巖飘言,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異驼侠,居然都是意外死亡姿鸿,警方通過查閱死者的電腦和手機(jī)谆吴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苛预,“玉大人句狼,你說我怎么就攤上這事∪饶常” “怎么了腻菇?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長昔馋。 經(jīng)常有香客問我筹吐,道長,這世上最難降的妖魔是什么秘遏? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任丘薛,我火速辦了婚禮,結(jié)果婚禮上邦危,老公的妹妹穿的比我還像新娘洋侨。我一直安慰自己,他們只是感情好铡俐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布凰兑。 她就那樣靜靜地躺著,像睡著了一般审丘。 火紅的嫁衣襯著肌膚如雪吏够。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天滩报,我揣著相機(jī)與錄音锅知,去河邊找鬼。 笑死脓钾,一個(gè)胖子當(dāng)著我的面吹牛售睹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播可训,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼昌妹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了握截?” 一聲冷哼從身側(cè)響起飞崖,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谨胞,沒想到半個(gè)月后固歪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡胯努,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年牢裳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了逢防。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蒲讯,死狀恐怖忘朝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伶椿,我是刑警寧澤辜伟,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布氓侧,位于F島的核電站脊另,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏约巷。R本人自食惡果不足惜偎痛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望独郎。 院中可真熱鬧踩麦,春花似錦、人聲如沸氓癌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贪婉。三九已至反粥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間疲迂,已是汗流浹背才顿。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留尤蒿,地道東北人郑气。 一個(gè)月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像腰池,于是被迫代替她去往敵國和親尾组。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容