使用 Puppeteer 抓取网页【译】

lxf2023-04-12 20:10:01
原文:https://bukapitch.medium.com/scraping-with-puppeteer-eca9a3435408

本文我们将创建一个汇总了 JavaScript 开发者远程工作的“JavaScript 求职板”

以下是项目的实现步骤:

  1. 创建一个基于 Puppeteer 的 Node.js 爬虫,从 remoteok.io 网站抓取职位信息

  2. 将职位信息保存到数据库

  3. 创建一个在我们网站上展示这些职位的 Node.js 应用

    注意:这个网站只是个例子。我不推荐你爬取它,因为你可以通过它提供的官方 API 使用这些数据。这里只是解释 Puppeteer 如何与一个知名网站搭配使用,以及向你展示如何使用它。

让我们开始吧!

为 JavaScript 职位创建一个爬虫

我们将从 remoteok.io(一个很棒的远程工作网站)抓取职位信息。

这个网站提供了许多不同类型的职位,JavaScript 工作被收集在“JavaScript”标签下,在撰写本文时,remoteok.io/remote-java… 这个页面下的工作都是有效的。

我说“在撰写本文时”是因为有一个很重要的共识:这个网站的内容随时可能更改。我们不保证任何事情。网站的任何改变都可能使我们的爬虫应用无法工作。这不是一个 API,而是两方之间的约定。

因此,根据我的经验,爬虫程序需要更多的维护工作。但是有时候我们没有其它选择来完成特定任务,所以它们仍然是我们可以使用的有效工具。

启动 Puppeteer

首先新建文件夹,在里面运行

npm init -y

安装 Puppeteer

npm install puppeteer

现在创建app.js文件。在顶部导入刚安装的puppeteer

const puppeteer = require("puppeteer")

然后我们使用lauch()方法创建一个浏览器实例

;(async () => {
  const browser = await puppeteer.launch({ headless: false })
})()

配置项中传入{ headless: false },我们就可以通过 Chrome 看到 Puppeteer 正在执行的操作,这在搭建应用程序时是非常有用的。

接下来使用browser对象newPage()方法获取page对象,然后再调用page对象的goto()方法下载 JavaScript 职位页面:

const puppeteer = require('puppeteer');(async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.goto("https://remoteok.io/remote-javascript-jobs
")
})()

在终端运行node app.js,将会启动一个 Chromium 实例,并下载我们传入的页面:

使用 Puppeteer 抓取网页【译】

从页面获取职位信息

现在我们需要想办法从页面中获取职位详情。

为此,我们将使用 Puppeteer 提供的page.evaluate()函数。

在回调函数里面基本上过渡到了浏览器,因此我们可以使用指向这个页面文档document对象。即使代码是运行在 Node.js 环境中。这就是 Puppeteer 有点神奇的地方。

回调函数里我们无法向控制台打印任何东西,因为会打印到浏览器的控制台,而不是 Node.js 终端。

我们能做的就是返回一个对象,通过这个对象访问到page.evaluate()的返回值:

const puppeteer = require('puppeteer');(async () => {
  const browser = await puppeteer.launch({ headless: false })
  const page = await browser.newPage()
  await page.goto('https://remoteok.io/remote-javascript-jobs')  /* Run javascript inside the page */
  const data = await page.evaluate(() => {
    return ....something...
  })  console.log(data)
  await browser.close()
})()

首先我们在这个函数中创建一个空数组,再填充希望返回的数据。

我们发现每一个职位都嵌套在带有job类名的tr HTML 标签里,我们可以使用querySelectorgetAttribute()获取每一个职位信息:

/* 在页面中运行 javascript */
const data = await page.evaluate(() => {
  const list = []
  const items = document.querySelectorAll("tr.job")  for (const item of items) {
    list.push({
      company: item.querySelector(".company h3").innerHTML,
      position: item.querySelector(".company h2").innerHTML,
      link: "https://remoteok.io" + item.getAttribute("data-href"),
    })
  }  return list
})

我通过浏览器的开发工具查看页面源码,找到需要的确切选择器。

使用 Puppeteer 抓取网页【译】

这是完整的代码:

const puppeteer = require("puppeteer");(async () => {
  const browser = await puppeteer.launch({ headless: false })
  const page = await browser.newPage()
  await page.goto("https://remoteok.io/remote-javascript-jobs")  /* Run javascript inside the page */
  const data = await page.evaluate(() => {
    const list = []
    const items = document.querySelectorAll("tr.job")    for (const item of items) {
      list.push({
        company: item.querySelector(".company h3").innerHTML,
        position: item.querySelector(".company h2").innerHTML,
        link: "https://remoteok.io" + item.getAttribute("data-href"),
      })
    }    return list
  })  console.log(data)
  await browser.close()
})()

如果你运行它,将会得到一个包含了职位详情的对象数组: 使用 Puppeteer 抓取网页【译】

将职位存到数据库中

现在我们准备将这些数据存储到本地数据库中。

我们将不时的运行 Puppeteer 脚本,然后先将原来存储的职位全部删除,再存入新获取到的数据。

我们将使用 MongoDB,在终端运行:

npm install mongodb

然后在app.js 中添加初始化数据库jobs和集合jobs的逻辑:

const puppeteer = require("puppeteer")
const mongo = require("mongodb").MongoClientconst url = "mongodb://localhost:27017"
let db, jobsmongo.connect(
  url,
  {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  },
  (err, client) => {
    if (err) {
      console.error(err)
      return
    }
    db = client.db("jobs")
    jobs = db.collection("jobs")    //....
  }
)

现在将函数的//....注释替换为爬取数据的代码。当连接 MongoDB 后将运行这部分代码。

const puppeteer = require("puppeteer")
const mongo = require("mongodb").MongoClientconst url = "mongodb://localhost:27017"
let db, jobsmongo.connect(
  url,
  {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  },
  (err, client) => {
    if (err) {
      console.error(err)
      return
    }
    db = client.db("jobs")
    jobs = db.collection("jobs")
    ;(async () => {
      const browser = await puppeteer.launch({ headless: false })
      const page = await browser.newPage()
      await page.goto("https://remoteok.io/remote-javascript-jobs")      /* Run javascript inside the page */
      const data = await page.evaluate(() => {
        const list = []
        const items = document.querySelectorAll("tr.job")        for (const item of items) {
          list.push({
            company: item.querySelector(".company h3").innerHTML,
            position: item.querySelector(".company h2").innerHTML,
            link: "https://remoteok.io" + item.getAttribute("data-href"),
          })
        }        return list
      })      console.log(data)
      jobs.deleteMany({})
      jobs.insertMany(data)
      await browser.close()
    })()
  }
)

我在函数的末尾添加了这段

jobs.deleteMany({})
jobs.insertMany(data)

先清空 MongoDB 的表, 再插入获取到的数据。

现在如果在运行node app.js,然后使用终端控制台或者 TablePlus 等应用检查 MongoDB 数据库的内容,你将会看到已存储的数据: 使用 Puppeteer 抓取网页【译】 酷!现在我们可以设置一个定时任务或者其它的自动化,每天或每 6 小时运行这个应用程序以获取最新的数据。

创建 Node.js 应用可视化职位

现在我们需要一种方式来可视化这些职位。我们需要一个应用程序。

我们将搭建一个基于 express 和服务端模版 Pug 的 Node.js 应用

新建一个文件夹,在里面运行npm init -y

然后安装 Express,MongoDB 和 Pug:

npm install express mongodb pug

首先初始化 Express:

const express = require("express")
const path = require("path")const app = express()
app.set("view engine", "pug")
app.set("views", path.join(__dirname, "."))app.get("/", (req, res) => {
  //...
})app.listen(3000, () => console.log("Server ready"))

然后初始化 MongoDB,将获得的职位信息保存到jobs数组中:

const express = require("express")
const path = require("path")const app = express()
app.set("view engine", "pug")
app.set("views", path.join(__dirname, "."))const mongo = require("mongodb").MongoClientconst url = "mongodb://localhost:27017"
let db, jobsCollection, jobsmongo.connect(
  url,
  {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  },
  (err, client) => {
    if (err) {
      console.error(err)
      return
    }
    db = client.db("jobs")
    jobsCollection = db.collection("jobs")
    jobsCollection.find({}).toArray((err, data) => {
      jobs = data
    })
  }
)app.get("/", (req, res) => {
  //...
})app.listen(3000, () => console.log("Server ready"))

大部分代码与在 Puppeteer 脚本中插入数据的代码是也样的。不同的是现在我们使用find()来获取数据库中的数据:

jobsCollection.find({}).toArray((err, data) => {
  jobs = data
})

最后,当用户访问/时,渲染一个 Pug 模版:

app.get("/", (req, res) => {
  res.render("index", {
    jobs,
  })
})

以下是完整的app.js文件:

const express = require("express")
const path = require("path")const app = express()
app.set("view engine", "pug")
app.set("views", path.join(__dirname, "."))const mongo = require("mongodb").MongoClientconst url = "mongodb://localhost:27017"
let db, jobsCollection, jobsmongo.connect(
  url,
  {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  },
  (err, client) => {
    if (err) {
      console.error(err)
      return
    }
    db = client.db("jobs")
    jobsCollection = db.collection("jobs")
    jobsCollection.find({}).toArray((err, data) => {
      jobs = data
    })
  }
)app.get("/", (req, res) => {
  res.render("index", {
    jobs,
  })
})app.listen(3000, () => console.log("Server ready"))

index.pug文件与app.js位于同一个文件夹中,会遍历 jobs 数组并将详情打印出来:

html
  body
    each job in jobs
      p
      | #{job.company}
      br
      a(href=`${job.link}`) #{job.position}

最终效果如下: 使用 Puppeteer 抓取网页【译】