ChatGPT編程秀之最小元素的設(shè)計(jì)示例詳解
膨脹的野心與現(xiàn)實(shí)的窘境
上一節(jié)隨著我能抓openai的列表之后,我的野心開始膨脹,既然我們寫了一個(gè)框架,可以開始寫面向各網(wǎng)站的爬蟲了,為什么只面向ChatGPT呢?幾乎所有的平臺(tái)都是這么個(gè)模式,一個(gè)列表,然后逐個(gè)抓取。那我能不能把這個(gè)能力泛化呢?可不可以設(shè)計(jì)一套機(jī)制,讓所有的抓取功能都變得很簡單呢?我抽取一系列的基礎(chǔ)能力,而不管抓哪個(gè)網(wǎng)站只需要復(fù)用這些能力就可以快速的開發(fā)出爬蟲。公司內(nèi)的各種平臺(tái)都是這么想的對(duì)吧?
那么我們就需要進(jìn)行設(shè)計(jì)建模,如果按照正常的面向?qū)ο螅铱赡軙?huì)這么設(shè)計(jì)建模:
看起來很美好不是嗎?是不是可以按照設(shè)計(jì)去寫代碼了?其實(shí)完全是扯淡,魔鬼隱藏在細(xì)節(jié)中,每個(gè)網(wǎng)站都有各種復(fù)雜的HTML、他們可能是簡單的列表,也可能是存在好幾個(gè)iframe,而且你在界面上看到的列表和你真正點(diǎn)開的又不一樣,比如說:
- 有的小說網(wǎng)站,它的列表上假如有N個(gè)列表項(xiàng),但是你真的點(diǎn)擊去之后,你會(huì)發(fā)現(xiàn)有的章節(jié)點(diǎn)擊他只有一半內(nèi)容,再點(diǎn)下一頁的時(shí)候它會(huì)調(diào)到一個(gè)不在列表頁上的展示的頁面,展示后半段內(nèi)容,而你如果只根據(jù)列表鏈接去抓,你會(huì)丟掉這后半段內(nèi)容。
- 有的網(wǎng)站會(huì)在你點(diǎn)了幾個(gè)頁面后隨機(jī)出現(xiàn)一個(gè)按鈕,點(diǎn)擊了才能展開后續(xù)內(nèi)容,防止機(jī)器抓取。你不處理這種情況,直接去抓就抓不全。
- 而有的網(wǎng)站根本就是圖片展示文本內(nèi)容,你得把圖片搞下來,然后OCR識(shí)別,或者插入了各種看不見的文本需要被清洗掉。
而且每個(gè)網(wǎng)站還會(huì)升級(jí)換代,他們一升級(jí)換代,你的抓取方式也要跟著變。 等等等等……而且所有這些要素之間還可以排列組合:
所以最上面的那個(gè)建模只能說過于簡化而沒有用處,起碼,以前是這樣的。
在以前,我們可能會(huì)進(jìn)一步完善這個(gè)設(shè)計(jì),得到一系列復(fù)雜的內(nèi)部子概念、子機(jī)制、子策略,比如:
- 反防抓機(jī)制
- 詳情分頁抓取策略
- 清洗機(jī)制
然后對(duì)這些機(jī)制進(jìn)行組合。
然而這并不會(huì)讓問題變簡單,人們總是低估膠水代碼的復(fù)雜度,最終要么整個(gè)體系非常脆弱,要么就從膠水處開始腐化。
新時(shí)代,新思路
那么在今天,我們有沒有什么新的做法呢?我們從一個(gè)代碼示例開始講起,比如,我這里有一個(gè)抓取某小說網(wǎng)站的代碼:
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++; } }
在實(shí)際工作中這樣的代碼應(yīng)該是比較常見的,由于上述的設(shè)計(jì)沒有什么用處,我們經(jīng)常見到的就是另一個(gè)極端,那就是代碼寫的過于隨意,整個(gè)代碼的實(shí)現(xiàn)變得無法閱讀,當(dāng)我想要做稍微地調(diào)整,比如說我昨天抓了100個(gè),今天接著從101個(gè)往后抓,就要去讀代碼,然后從代碼中看改點(diǎn)什么好讓這個(gè)抓取可以從101往后抓。
那在以前呢,我們就要像上面說的要設(shè)計(jì)比較精密的機(jī)制,而越是精密的機(jī)制,就越不健壯。而且,以我的經(jīng)驗(yàn),你想讓人們使用那么精細(xì)的機(jī)制也不好辦,因?yàn)榇蠖鄶?shù)人的能力并不足以駕馭精細(xì)的機(jī)制。
而在今天,我們可以做的更粗放一些。
首先,我們意識(shí)到有些代碼,準(zhǔn)確的說,是有些變量,是我們經(jīng)常修改的,所以我們?cè)诓桓淖冋w結(jié)構(gòu)的情況下,我們把這些變量提到上面去,變成一個(gè)變量:
//意圖描述 const intention = { list_url:'https://example.com/list-1234.html', batchSize: 50, batchStart: 12, page_waiting_time: 3000, part_fetch:{ //如果全抓取,就注釋掉整個(gè)part_fetch屬性 from:600,//不含該下標(biāo) 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); //一個(gè)批次一存儲(chǔ) } }); } 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++; } }
于是我們把程序分成了兩部分結(jié)構(gòu):
接下來我會(huì)發(fā)現(xiàn),在網(wǎng)站不變的情況下,下面這個(gè)意圖執(zhí)行代碼相當(dāng)?shù)姆€(wěn)定。我經(jīng)常需要做的不管是偏移量的計(jì)算,還是修改抓取目標(biāo)等等,這些都只需要修改上面的意圖描述數(shù)據(jù)結(jié)構(gòu)即可。而且我們可以做進(jìn)一步的封裝,得到下面的代碼(下面的JsDoc也是ChatGPT給我寫的):
/** * @typedef {Object} Intention * @property {string} list_url * @property {integer} batchSize * @property {integer} batchStart * @property {integer} page_waiting_time * @property {PartFetch} part_fetch 如果全抓取,就注釋掉整個(gè)part_fetch屬性 * @property {string} output_folder * * @typedef {Object} PartFetch * @property {integer} from 不含該下標(biāo) * @property {integer} batchStart */ //意圖執(zhí)行 /** * @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); //一個(gè)批次一存儲(chǔ) } }); } 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++; } } }
于是我們就有了一個(gè)穩(wěn)定的接口將意圖的描述和意圖的執(zhí)行徹底分離,隨著我對(duì)我的代碼進(jìn)行了進(jìn)一步的整理后發(fā)現(xiàn),這個(gè)意圖描述結(jié)構(gòu)竟然相當(dāng)?shù)耐ㄓ?,我寫的好多網(wǎng)站的抓取代碼竟然都可以抽取出這樣一個(gè)結(jié)構(gòu)。 于是我們可以進(jìn)一步抽象,到了一種適用于我特定領(lǐng)域的DSL,類似下面的結(jié)構(gòu):
到此為止,我的意圖描述和意圖執(zhí)行徹底解耦,意圖執(zhí)行變成了意圖描述中的一個(gè)屬性,我只需要寫一個(gè)引擎,根據(jù)意圖描述中entrypoint的屬性值,加載對(duì)應(yīng)的函數(shù),然后將意圖數(shù)據(jù)傳給他就可以了,大概的代碼如下:
const intentionString = await fs.readFile(templatePath, 'utf8'); const intention = yaml.load(intentionString); const intention_exec = require(intention.entrypoint); intention_exec(intention, context);
而我們的每一個(gè)意圖執(zhí)行的代碼,可以有自己的不同變化原因,不管是網(wǎng)站升級(jí)了,還是我們要抓下一個(gè)網(wǎng)站了,我們只需要把HTML扔給ChatGPT,他就可以幫我們生成對(duì)應(yīng)的意圖執(zhí)行代碼。哪怕我們想基于一些可以復(fù)用庫函數(shù),比如之前說的反防抓、反詳情頁分頁機(jī)制封裝的庫函數(shù),他也可以給我們生成膠水代碼把這些函數(shù)粘起來(具體的手法我們?cè)诤罄m(xù)的文章里講),所有這一切的變化,都可以用ChatGPT生成代碼這一步解決。那么所謂的在膠水層腐化的問題也就不存在了。
很有趣的是,在我基于該結(jié)構(gòu)的DSL得到一組實(shí)例之后,我很快就開始產(chǎn)生了在DSL這一層的新需求,比如:
- DSL文件的管理需求,因?yàn)槿丝偸呛軕械?,而且我只有業(yè)余時(shí)間寫點(diǎn)這些東西,不能保證自己一直記得哪個(gè)網(wǎng)站對(duì)應(yīng)哪個(gè)文件,然后怎么設(shè)置。
- 我還希望能夠根據(jù)我本地已經(jīng)抓的內(nèi)容和智能生成偏移量
- 我也希望能定時(shí)去查看更新然后生成抓取意圖。
這一切都是很有價(jià)值的需求,而如果我們沒有一個(gè)穩(wěn)定的下層DSL結(jié)構(gòu),我們這些更上層需求也注定是不穩(wěn)定的。
而有了這個(gè)穩(wěn)定的DSL結(jié)構(gòu)后,我們回過頭來看我們的設(shè)計(jì),其實(shí)是在更大的尺度上實(shí)現(xiàn)了面向?qū)ο笤O(shè)計(jì)中的開閉原則,盡管擴(kuò)展需要大量的代碼,而這些代碼卻并不需要人來寫,所以效率依然很高。
總結(jié)一下
在這個(gè)編程秀里面,我們做了什么?我們并沒有做一個(gè)功能,而是面向ChatGPT對(duì)我們的代碼進(jìn)行了一個(gè)設(shè)計(jì)。
- 首先,我們分析了傳統(tǒng)的面向?qū)ο蠼7椒ǖ木窒扌?,指出它過于簡化且無法解決實(shí)際問題。
- 接著,我們提出了新時(shí)代的新思路,通過將意圖描述和意圖執(zhí)行進(jìn)行解耦,使得某一個(gè)場(chǎng)景的開發(fā)變得更加簡單,數(shù)據(jù)結(jié)構(gòu)也更加通用。于是我們得到了在ChatGPT時(shí)代編程的最小元素的標(biāo)準(zhǔn)抽象方式:
- 最后,我們暢想了一下,在我們得到這種穩(wěn)定的數(shù)據(jù)結(jié)構(gòu)后,我們可以再更上層做更多的開發(fā)工作,而因?yàn)榻涌诤芊€(wěn)定,上層的開發(fā)工作也不至于是在浮沙之上建高塔。
這里想再聊深一點(diǎn),說點(diǎn)半題外話,其實(shí)到這里我們可以看出,我們最一開始抽出來的那個(gè)模型,并不是沒有用,只是他在更上層有用。而它把復(fù)雜度壓給了這一層的程序員。這一層的程序員自然是不滿意的。所以所謂的沒有用處其實(shí)是一個(gè)抱怨,背后本質(zhì)上是一種勞動(dòng)者對(duì)于被強(qiáng)迫進(jìn)行繁重勞動(dòng)的不滿。是一種上層的優(yōu)雅和下層的繁重勞動(dòng)之間的矛盾的體現(xiàn)。這個(gè)矛盾是不可調(diào)和的,有人想優(yōu)雅就有人要繁重,而ChatGPT的出現(xiàn)一定程度上轉(zhuǎn)移了這個(gè)矛盾,最繁重的工作給了它,使得開發(fā)者原地變成了管理者,變成得“優(yōu)雅”了。這種優(yōu)雅帶來的是好還是壞,我們還不知道,但我們希望是好的。
好的,那么當(dāng)我們有了最小元素的抽象之后,上一篇文章遺留的問題我們只回答了一半,我們還要進(jìn)一步考慮整個(gè)系統(tǒng)應(yīng)該怎么設(shè)計(jì)架構(gòu)才能更大限度的發(fā)揮ChatGPT的能力,而這是我們后面的內(nèi)容。
以上就是ChatGPT編程秀之最小元素的設(shè)計(jì)示例詳解的詳細(xì)內(nèi)容,更多關(guān)于ChatGPT編程最小元素設(shè)計(jì)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Nodejs實(shí)現(xiàn)的一個(gè)靜態(tài)服務(wù)器實(shí)例
這篇文章主要介紹了Nodejs實(shí)現(xiàn)的一個(gè)靜態(tài)服務(wù)器實(shí)例,本文實(shí)現(xiàn)的靜態(tài)服務(wù)器實(shí)例包含cache功能、壓縮功能等,需要的朋友可以參考下2014-12-12NodeJs生成sitemap站點(diǎn)地圖的方法示例
這篇文章主要介紹了NodeJs生成sitemap站點(diǎn)地圖的方法示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06Windows 系統(tǒng)下安裝和部署Egret的開發(fā)環(huán)境
Egret基于TypeScript開發(fā)的,而TypeScript編譯工具tsc是基于Node.js 開發(fā)的。所以在安裝過程中,我們先需要對(duì)于基礎(chǔ)支持工具進(jìn)行安裝。2014-07-07node.js實(shí)現(xiàn)博客小爬蟲的實(shí)例代碼
這篇文章通過實(shí)例代碼來給大家介紹如何利用node.js實(shí)現(xiàn)博客小爬蟲,有需要的朋友們可以直接運(yùn)用文中給出的實(shí)例代碼來進(jìn)行實(shí)踐學(xué)習(xí),感興趣的朋友們下面來一起看看吧。2016-10-10node終端里如何連接mysql數(shù)據(jù)庫并進(jìn)行sql查詢
這篇文章主要為大家介紹了node終端里如何連接mysql數(shù)據(jù)庫并進(jìn)行sql查詢,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07