ChatGPT编程秀之最小元素的设计示例详解

目录
  • 膨胀的野心与现实的窘境
  • 新时代,新思路
  • 总结一下

膨胀的野心与现实的窘境

上一节随着我能抓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的能力,而这是我们后面的内容。

以上就是ChatGPT编程秀之最小元素的设计示例详解的详细内容,更多关于ChatGPT编程最小元素设计的资料请关注我们其它相关文章!

(0)

相关推荐

  • ChatGPT前端编程秀之别拿编程语言不当语言

    目录 TDD第一步就卡住了 破门而入,针对性反馈 总结一下 TDD第一步就卡住了 写完小工具,这一篇回来我们接着写我们的程序.再看一眼我们的程序运行视图: 带着TDD思路,我进入了 ejs_and_yaml_dsl_loader 这个模块,这块因为我切的不是很好,所以这代码有点难写,不过没关系,正好我们实际工作大部分的场景都是这样的.看看我们在这里能玩出点什么来. 那么这次的需求呢是这个样子的,我们需要把ejs模版引擎渲染出的yaml转换为json,那么我们这个功能会非常复杂,所以我们没有以上来

  • 从一个爬虫开始ChatGPT的编程秀

    目录 思考问题域 用ChatGPT写一个爬虫 1. 先写一个框架 2. 在这个框架上,开发爬虫 3. 回到任务1的问题域 4. 最后回到具体的爬虫代码 回顾一下,我们做了什么,得到了什么? 思考问题域 我要写一个爬虫,把ChatGPT上我的数据都爬下来,首先想想我们的问题域,我想到几个问题: 不能用HTTP请求去爬,如果我直接 用HTTP请求去抓的话,一个我要花太多精力在登录上了,而我的数据又不多,另一个,现在都是单页引用,你HTTP爬下来的根本就不对啊. 所以最好是自动化测试的那种方式,启动浏

  • ChatGPT编程秀之跨越认知边界

    目录 作者说 碰到了认知边界 跨越认知边界 总结一下 作者说 最近要忙了,日更的日子要到头了.后面每一篇讲的点就小一点吧,大的点等后面有空了再写.大家见谅. 碰到了认知边界 我的有的朋友跟我说,用ChatGPT编程需要你至少要跟他对等水平,因为现阶段我们还不能做到完全不需要关心它写出来的代码,当你要读懂它写的代码的时候,就必须能力对等.还有的朋友跟我说,ChatGPT的不能超过你的认知水平,你的认知水平的上限决定了它的表现,比如你认知水平不行,导致自己不能分解任务的时候,那么你用ChatGPT也

  • ChatGPT如何写好Prompt编程示例详解

    目录 引言 好的 prompt 具有的设计原则 编写良好prompt的四种基础模式 编写一个合格的prompt的要点 让AI扮演角色 明确提供要执行的任务 给出完成任务的步骤 围绕任务提供上下文 陈述具体目标,给出具体要求 要求格式化输出 明确指定语言风格 让AI站在人物的角度,而非上帝视角 马上给出具体的样例 小结 引言 现在已经产生了一种新职业:Prompt Engineer(提示指令工程师),可见 Prompt 是多么重要,且编写不易. ChatGPT的产出,一半决定于它的实力,一半决定于

  • 适合面向ChatGPT编程的架构示例详解

    目录 新的需求 领域知识 架构设计 管道架构 分层架构 类分层神经网络的架构 总结一下 新的需求 我们前面爬虫的需求呢,有些平台说因为引起争议,所以不让发,好吧,那我们换个需求,本来那个例子也不好扩展了.最近AI画图也是比较火的,那么我们来试试做个程序帮我们生成AI画图的prompt. 首先讲一下AI话题的prompt的关键要素,大部分的AI画图都是有一个个由逗号分割的关键字,也叫subject,类似下面这样: a cute cat, smile, running, look_at_viewer

  • ChatGPT编程秀之最小元素的设计示例详解

    目录 膨胀的野心与现实的窘境 新时代,新思路 总结一下 膨胀的野心与现实的窘境 上一节随着我能抓openai的列表之后,我的野心开始膨胀,既然我们写了一个框架,可以开始写面向各网站的爬虫了,为什么只面向ChatGPT呢?几乎所有的平台都是这么个模式,一个列表,然后逐个抓取.那我能不能把这个能力泛化呢?可不可以设计一套机制,让所有的抓取功能都变得很简单呢?我抽取一系列的基础能力,而不管抓哪个网站只需要复用这些能力就可以快速的开发出爬虫.公司内的各种平台都是这么想的对吧? 那么我们就需要进行设计建模

  • C语言编程题杨氏矩阵算法快速上手示例详解

    目录 题目概要 一.解题思路 二.具体代码 题目概要 有一个数字矩阵,矩阵的每行从左到右都是递增的,矩阵从上到下都是递增的,请编写程序在这样的矩阵中查找某个数字是否存在? 一.解题思路 对于查找一个数组中元素是否存在,很多同学第一想法就是从头到尾遍历一遍.这样的想法优点是代码简单且无脑容易上手,但是这样的缺点也很明显,比如是m *n的数组,你从头到尾遍历,最坏情况要找m *n次.题目给的相关条件比如从左向右递增,从上向下递增你也完全没有使用,这样的暴力求解显然不是我们想看到的 我们来介绍一种方法

  • python编程中简洁优雅的推导式示例详解

    目录 1. 列表推导式 增加条件语句 多重循环 更多用法 2. 字典推导式 3. 集合推导式 4. 元组推导式 Python语言有一种独特的推导式语法,相当于语法糖的存在,可以帮助你在某些场合写出较为精简酷炫的代码.但没有它,也不会有太多影响.Python语言有几种不同类型的推导式. 1. 列表推导式 列表推导式是一种快速生成列表的方式.其形式是用方括号括起来的一段语句,如下例子所示: lis = [x * x for x in range(1, 10)] print(lis) 输出 [1, 4

  • java编程创建型设计模式工厂方法模式示例详解

    目录 1.什么是工厂方法模式? 2.案例实现 3.JDK中的工厂方法模式 1.什么是工厂方法模式? 工厂方法模式设计方案:  将披萨项目的实例化功能抽象成抽象方法,在不同的口味点餐子类中具体实现. 工厂方法模式:  定义了一个创建对象的抽象方法,由子类决定要实例化的类.工厂方法模式将对象的实例化推迟到子类. 何时使用?  不同条件下创建不用实例时.方法是让子类实现工厂接口. 2.案例实现 假如说,我们现在有这样一个需求:客户在点披萨时,可以点不同口味的披萨,比如北京的奶酪pizza.北京的胡椒p

  • Java设计模式之原型设计示例详解

    目录 简单说一下(定义) 稍微夸一下(优缺点) 顺便提一下(适用场景) 着重讲一下(深.浅克隆) 多多用一下(结构.代码实现) 简单说一下(定义) 什么是原型模式:原型模式是用于创建重复的对象,同时又能保证性能.用一个已经创建的实例作为原型,通过复制该原型对象来创建一个或者多个和原型相同或者相似的新对象 举例说明:我们都玩过打飞机的游戏,敌军的飞机可谓是数不胜数,但是如果每出一架敌机都要重新实例化的话,那么自然我们的功能很复杂.所以这个时候我们的原型模式就派上用场了,只实例化一架飞机出来,其他的

  • Blender Python编程实现程序化建模生成超形示例详解

    目录 正文 什么是超形(Supershapes, Superformula) 二维超形 n1 = n2 = n3 = 1 n1 = n2 = n3 = 0.3 其他特别情况 例子 1 例子 2 例子 3 例子 4 例子 5 奇异的形状 三维超形 Blender 生成超形 详细代码和注释如下 正文 Blender 并不是唯一一款允许你为场景编程和自动化任务的3D软件; 随着每一个新版本的推出,Blender 正逐渐成为一个可靠的 CG 制作一体化解决方案,从使用油脂铅笔的故事板到基于节点的合成.

  • Flutter 异步编程之单线程下异步模型图文示例详解

    目录 一. 本专栏图示概念规范 1. 任务概念规范 2. 任务的状态 3. 时刻与时间线 4.同步与异步 二.理解单线程中的异步任务 1. 任务的分配 2.异步任务特点 3. 异步任务完成与回调 三. Dart 语言中的异步 1.编程语言中与异步模型的对应关系 2.Dart 编程中的异步任务 3.当前任务分析 四.异步模型的延伸 1. 单线程异步模型的局限性 2. 多线程与异步的关系 3. Dart 中如何解决单线程异步模型的局限性 一. 本专栏图示概念规范 本专栏是对 异步编程 的系统探索,会

  • jquery移除、绑定、触发元素事件使用示例详解

    复制代码 代码如下: unbind(type [,data])     //data是要移除的函数$('#btn').unbind("click"); //移除click$('#btn').unbind(); //移除所有 对于只需要触发一次的,随后就要立即解除绑定的情况,用one() 复制代码 代码如下: $('#btn').one("click",function(){.......}); 触发操作trigger() 方法触发被选元素的指定事件类型. 复制代码

  • Python函数的默认参数设计示例详解

    在Python教程里,针对默认参数,给了一个"重要警告"的例子: def f(a, L=[]): L.append(a) return L print(f(1)) print(f(2)) print(f(3)) 默认值只会执行一次,也没说原因.会打印出结果: [1] [1, 2] [1, 2, 3] 因为学的第一门语言是Ruby,所以感觉有些奇怪. 但肯定的是方法f一定储存了变量L. 准备知识:指针 p指向不可变对象,比如数字.则相当于p指针指向了不同的内存地址. p指向的是可变对象,

  • Java并发编程包中atomic的实现原理示例详解

    线程安全: 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类时线程安全的. 线程安全主要体现在以下三个方面: 原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作 可见性:一个线程对主内存的修改可以及时的被其他线程观察到 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序 引子 在多线程的场景中,我们需要保证数据安全,就会考虑同步的

随机推荐