原文:https://bukapitch.medium.com/scraping-with-puppeteer-eca9a3435408
本文我们将创建一个汇总了 JavaScript 开发者远程工作的“JavaScript 求职板”
以下是项目的实现步骤:
-
创建一个基于 Puppeteer 的 Node.js 爬虫,从 remoteok.io 网站抓取职位信息
-
将职位信息保存到数据库
-
创建一个在我们网站上展示这些职位的 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")
;(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 提供的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 标签里,我们可以使用querySelector
和getAttribute()
获取每一个职位信息:
/* 在页面中运行 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
})
我通过浏览器的开发工具查看页面源码,找到需要的确切选择器。
这是完整的代码:
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 脚本,然后先将原来存储的职位全部删除,再存入新获取到的数据。
我们将使用 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 数据库的内容,你将会看到已存储的数据:
酷!现在我们可以设置一个定时任务或者其它的自动化,每天或每 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}
最终效果如下: