用Puppeteer进行刮削

140 阅读6分钟

在这篇文章中,我们将创建一个 "JavaScript Job Board",聚合JavaScript开发人员的远程工作。

以下是完成我们项目的步骤。

  1. 创建一个使用Puppeteer构建的Node.js搜刮器,从remoteok.io网站获取工作机会
  2. 将作业存储到数据库中
  3. 创建一个Node.js应用程序,在我们自己的网站上显示这些工作。

需要注意的是。我只是把这个网站作为一个例子。我并不是推荐你去搜刮它,因为如果你想使用它的数据,它有一个官方的API。这只是为了解释Puppeteer如何与一个大家都知道的网站一起工作,并向你展示如何在实践中使用它。

开始吧!

为JavaScript作业创建一个搜刮器

我们将从remoteok.io,一个伟大的远程工作网站上搜刮JavaScript工作。

该网站提供了许多不同类型的工作。JavaScript工作被列在 "JavaScript "标签下,在撰写本文时,它们都在这个页面上:https://remoteok.io/remote-javascript-jobs

我说 "在写的时候 "是因为这是一个重要的认识:网站可能随时会改变。我们不能保证任何东西。通过搜刮,网站的任何变化都可能使我们的应用程序停止工作。这不是一个API,它是两方之间的一种合同。

所以,根据我的经验,刮削应用程序需要更多的维护。但有时我们没有其他选择来完成一个特定的任务,所以它们仍然是我们可以利用的有效工具。

设置Puppeteer

我们先创建一个新的文件夹,在该文件夹内运行

npm init -y

然后使用Puppeteer安装

npm install puppeteer

现在创建一个app.js 文件。在顶部,要求我们刚刚安装的puppeteer 库。

const puppeteer = require("puppeteer")

然后我们可以使用launch() 方法来创建一个浏览器实例。

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

我们通过{ headless: false } 配置对象,在Puppeteer执行操作时显示Chrome,这样我们就可以看到正在发生的事情,这在构建应用程序时很有帮助。

接下来我们可以使用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实例将启动,加载我们告诉它要加载的页面。

![](Screen Shot 2020-09-06 at 08.56.43.png)

从该页面获取工作机会

现在,我们需要找出一种方法,从页面上获取工作的详细信息。

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

在其回调函数中,我们基本上过渡到了浏览器,所以我们可以使用现在指向页面DOM的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()
})()

在该函数中,我们首先创建一个空数组,将其填入我们想要返回的值。

tr 我们找到每个工作,它被包裹在一个job 类的HTML元素中,然后我们使用querySelector()getAttribute() 从每个工作中获取数据。

/* 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
})

我通过使用浏览器的devtools查看页面源代码,发现哪些是要使用的确切选择器。

![](Screen Shot 2020-09-06 at 10.37.04.png)

这里是完整的源代码。

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

如果你运行这个,你会得到一个对象数组,每个对象都包含作业的详细信息。

![](Screen Shot 2020-09-06 at 10.35.28.png)

在数据库中存储作业

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

我们将在一段时间内运行Puppeteer脚本,首先删除所有存储的作业,然后用发现的新作业重新填充数据库。

我们将使用MongoDB。从终端,运行。

npm install mongodb

然后在app.js ,我们添加这个逻辑来初始化jobs 数据库,以及它里面的一个jobs 集合。

const puppeteer = require("puppeteer")
const mongo = require("mongodb").MongoClient

const url = "mongodb://localhost:27017"
let db, jobs

mongo.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").MongoClient

const url = "mongodb://localhost:27017"
let db, jobs

mongo.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数据库内容,你会看到数据正在出现。

![](Screen Shot 2020-09-06 at 10.56.39.png)

很好!我们现在可以设置一个cron job或任何其他自动化程序,每天或每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").MongoClient

const url = "mongodb://localhost:27017"
let db, jobsCollection, jobs

mongo.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").MongoClient

const url = "mongodb://localhost:27017"
let db, jobsCollection, jobs

mongo.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 存放在同一文件夹中,将对作业数组进行迭代,以打印我们存储的细节。

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

这就是结果。

![](Screen Shot 2020-09-06 at 11.27.20.png)