思考问题域

我要写一个爬虫,把ChatGPT上我的数据都爬下来,首要想想咱们的问题域,我想到几个问题:

  • 不能用HTTP请求去爬,如果我直接用HTTP请求去抓的话,一个我要花太多精力在登录上了,而我的数据又不多,另一个,现在都是单页引用,你HTTP爬下来的底子就不对啊。
    • 所以最好是自动化测验的那种方法,发动浏览器去爬。
  • 可是我又不能保证一次把代码写成功,反复登录的话,会被网站封号,就几个数据,不值当的。

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

ChatGPT编程秀:从一个爬虫开始

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

ChatGPT编程秀:从一个爬虫开始

从这种相似性能够看出,我需求一个类似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年代应该成为一种习气,它会从两个方面带来优点:

  1. 能够下降输入的文本数量,防止ChatGPT犯错。由于很多人都知道,ChatGPT能够快速写出一些小程序,可是长一点的总是会出错,很多人到这里就抛弃了,但其实,咱们会发现如果咱们能把问题分化到它恰好擅长的领域咱们就能够最大极限的利用它的优势,规避它的劣势。人类历史上,蒸汽机车创造的时分,它必定不如马耐颠,但为了充沛理由他的优势,人们为它铺了铁轨。直到今日为了发挥机动车的效能,咱们仍是要筑路铺轨,可是咱们并不觉得有什么不对,从这个角度来讲,咱们也不该只盯着ChatGPT的缺点看,扬长避短才是正路。
  2. 缩短反馈环,进步功率。从全体功率角度来讲,只要反馈环的缩短才是真实进步了功率,某一步的快速完成并不真实进步功率。所谓反馈环的缩短在咱们的上下文里便是”我想到怎样编码完成使命 -> 编码 -> 测验 -> 得知代码履行失败->我又想到怎样编码完成使命”的这个循环,咱们不能假定代码编写一次成功,所以这个环越短,咱们的功率就越高。在这个比如里我想到了我不能一次写对,所以我就先做了REPL,这便是所谓磨刀不误砍柴工。可是道理咱们都懂,在有ChatGPT之前,磨刀这个事他总是误砍柴工的,可是在今日,你能够用几个问题就得到一个趁手的工具,开始你的作业,所以不要着急冲进去作业,先做个工具或许是新年代的好习气。

下一篇,咱们将进入这样一个场景:我根据这个结构,我写了很多爬虫代码,我该怎样组织和办理这些代码呢?我需不需求一个精妙设计的内部结构和规范来组织我的代码呢?