草庐IT

ChatGPT编程秀:做一个简单爬虫程序

q56731523 2023-08-29 原文

随着ChatGPT的大火,越来越多的人习惯于用ChatGPT搞一些有趣的事。对于一个资深的爬虫程序来说,体验下ChatGPT做爬虫程序也是很有意思的事情。

首先想想我们的问题域,我想到几个问题:

不能用HTTP请求去爬,如果我直接用HTTP请求去抓的话,一个我要花太多精力在登录上了,而我的数据又不多,另一个,现在都是单页引用,你HTTP爬下来的根本就不对啊。

所以最好是自动化测试的那种方式,启动浏览器去爬。

但是我又不能保证一次把代码写成功,反复登录的话,会被网站封号,就几个数据,不值当的。

所以总的来说我需要一个这样的流程:

从流程上我们是不是可以看出,这个流程跟我们用WebConsole试验一段代码的过程很像?

从这种相似性可以看出,我需要一个类似WebConsole的东西来实现我要的效果,这个东西学名叫REPL(Read–eval–print loop),不过你不知道这个名字也无所谓,不影响。

而且还不止,我需要从文件读入我的代码,毕竟没有代码高亮,我可写不好程序。从文件读入的话,我就可以用vscode给我提供代码高亮,这岂不是美滋滋。

想到这,如果是以前,我已经一个头两个大了,我只是想抓点东西,你让我搞个REPL,这玩意听着就不科学,我一天就晚上有一个小时的时间写点代码,这玩意太复杂了,乍一看也不是我业余时间能写出来的。

但是现在,我们有了ChatGPT,那么我接下来可以用ChatGPT开始写,简单做一个任务分解:

1、写一个框架,可以最大限度的帮我们提高开发效率

2、在这个框架上,一个场景一个场景的开发爬虫

用ChatGPT写一个爬虫

1、先写一个框架

首先,我可能不知道这个东西叫REPL,但是我知道控制台,那我就这么问:

如何用nodejs做一个命令行交互控制台,执行输入的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);
});

但是这个代码还不够我用的,我需要文件输入,所以我可以问:

如果是读入文件作为输入呢?

然后我得到了下面的代码:

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);
}

第一反应,这个不是async/await模式调用的呀,我后面代码写起来会很麻烦,于是我就说:

我要用fs/promises库实现上面逻辑怎么写呢?

然后我就得到了下面的代码:

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);
}

这个点算是令我满意了,但马上我又意识到不对劲,我需要执行完之后 hang 在那里等待下一次输入的,他这执行完就退出了,于是我又改进了的问题:

我要实现的效果是,启动后在命令行里打印一个“>”然后等待输入,我敲击回车,程序就会读入代码中写死的目标文件,并执行其中的js,然后重新打印一个“>“等待下一次输入,无限循环。

然后我得到了下面的代码:

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);
});

注意,我再第一次的时候忘记了写”无限循环“,所以他写的代码就只执行两次,仔细一看,确实这样实现也符合我说的逻辑,他没有猜测我要无限循环,所以在使用的时候要注意,有时候没得到你想要的结果可能是你没说清楚。

那么看起来到此为止就差不多了,没想到也不复杂。那我测试一下。

我建立了一个下面的工程结构:

.
├── index.js
└── input
    └── input.js

把文件名写死为input/input.js
执行 node index.js

彩蛋:其中有一次执行的代码在后续执行发现错误

发现下面错误:

  /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用错了,不过没关系,直接拷给他:

上面的代码执行报错: SyntaxError: await is only valid in async functions and the top level bodies of modules

于是得到了修改后的代码

2、在这个框架上,开发爬虫

那么接下来,我可以试着抓取一下列表了,这部分GPT3.5就不能胜任了,我切换到GPT4,然后问:

我用puppeteer打开一个网页,可以直接用document DOM API操作,然后返回数据,请抓取这个网页的列表,HTML如下:

下面省略是openAI的页面代码,但是呢,因为他太大了,我不得不手动做了点删减,比如我列表很长,我就减到只剩3个元素,其他无关的元素的代码我都删掉。

经过几次询问,他给出了我代码:

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('你的网址'); // 将'你的网址'替换为您要打开的页面的实际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、回到任务1的问题域

真到执行的时候又发现新问题,这个puppeteer我希望他一直开着,就不能在input.js里启动,我需要在REPL代码里就执行,所以我需要稍微修改下代码,我说:

我需要在控制台启动的时候用puppeteer启动一个浏览器,然后每次回车可以让文件中的代码可以通过puppeteer访问启动的这个浏览器

然后得到了代码:

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();

这代码还是不令我满意,我又接连问了下面几个问题:

我需要browser是GUI可见的
把path/to/target/file.js 改为 input/input.js
我需要在进入input/input.js之前直接启动一个page,里直接访问page而不是browser
这行代码: return eval(async function (page) { ${fileContent} })(page);
报错:
xxxx 能不能不用eval?
报错: /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

最后得到了我可以执行的代码。不过实际执行中还出现了防抓机器人的问题,经过一些列的查找解决了这个问题,为了突出重点,这里就不贴解决过程了,最终代码如下:

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、最后回到具体的爬虫代码

而既然浏览器一直开着了,那我们需要执行的代码其实只有两个了:

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);
})();

当然实际上fetch_list.js有点问题,因为openai做了防抓程序,我们可能很难搞到列表项的链接,不过这个也不难,我们用名字匹配挨个点就好了嘛,反正也不多。

比如下面这样:

(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}".`);
    }
})();

说句题外话,上面的代码很有意思,似乎它为了防止点某个具体元素不管用,竟然点击了一个区域。

接下来如果我们想备份我们的每一个thread就可以在这个基础上,让ChatGPT继续给我们写实现完成即可,这里就不继续展开了,大家可以自己完成。

回顾一下,我们做了什么,得到了什么?

首先,我们对问题域做了分析,把目标网站和工作者我本人以及时间限制等约束都纳入了问题域进行了分析,得到了一个方案,然后通过类比发现我们的方案其实就是做一个有特定上下文的REPL,然后用这个REPL再去干具体的事。

接着,我们基于这个方案做了任务分解,粗略分成了做一个REPL和实现具体的抓取代码两部分。
接着我们靠ChatGPT把些任务实现,在实现的过程中,我们发现自己对问题域的细节了解不够,于是我们又迭代了我们的任务列表。可以说方案没有大的变化,实现上做了很多调整。

最终,我们就靠ChatGPT把这个REPL给做了出来,为了写一个这样的小功能,我们做了个框架,颇有点为了这点醋才包的这顿饺子的味道了。这要是在以前的时代,是一个巨大的浪费,但其实先做一个框架的思路在ChatGPT时代应该成为一种习惯,它会从两个方面带来好处:

可以降低输入的文本数量,避免ChatGPT犯错。因为很多人都知道,ChatGPT可以快速写出一些小程序,但是长一点的总是会出错,很多人到这里就放弃了,但其实,我们会发现如果我们能把问题分解到它恰好擅长的领域我们就可以最大限度的利用它的优势,规避它的劣势。

人类历史上,蒸汽机车发明的时候,它肯定不如马耐颠,但为了充分理由他的优势,人们为它铺了铁轨。直到今天为了发挥机动车的效力,我们还是要修路铺轨,但是我们并不觉得有什么不对,从这个角度来讲,我们也不该只盯着ChatGPT的缺点看,扬长避短才是正道。

缩短反馈环,提高效率。从整体效率角度来讲,只有反馈环的缩短才是真正提高了效率,某一步的快速完成并不真正提高效率。所谓反馈环的缩短在我们的上下文里就是”我想到怎么编码完成任务 -> 编码 -> 测试 -> 得知代码执行失败->我又想到怎么编码完成任务"的这个循环,我们不能假设代码编写一次成功,所以这个环越短,我们的效率就越高。

在这个例子里我想到了我不能一次写对,所以我就先做了REPL,这就是所谓磨刀不误砍柴工。但是道理大家都懂,在有ChatGPT之前,磨刀这个事他总是误砍柴工的,但是在今天,你可以用几个问题就得到一个趁手的工具,开始你的工作,所以不要着急冲进去工作,先做个工具可能是新时代的好习惯。

下一篇,我们将进入这样一个场景:我基于这个框架,我写了很多爬虫代码,我该怎么组织和管理这些代码呢?我需不需要一个精妙设计的内部框架和规范来组织我的代码呢?

有关ChatGPT编程秀:做一个简单爬虫程序的更多相关文章

  1. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

    我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

  2. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  3. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  4. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  5. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  6. ruby - 在 Ruby 中编写命令行实用程序 - 2

    我想用ruby​​编写一个小的命令行实用程序并将其作为gem分发。我知道安装后,Guard、Sass和Thor等某些gem可以从命令行自行运行。为了让gem像二进制文件一样可用,我需要在我的gemspec中指定什么。 最佳答案 Gem::Specification.newdo|s|...s.executable='name_of_executable'...endhttp://docs.rubygems.org/read/chapter/20 关于ruby-在Ruby中编写命令行实用程序

  7. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  8. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  9. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  10. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

随机推荐