/

使用Puppeteer進行網頁爬蟲

使用Puppeteer進行網頁爬蟲

這篇文章將使用Puppeteer創建一個“JavaScript工作版”,用於彙總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

我們首先創建一個新的文件夾,在文件夾內運行以下命令:

1
npm init -y

然後使用以下命令安裝Puppeteer:

1
npm install puppeteer

現在創建一個app.js文件,在文件頂部引入剛剛安裝的puppeteer庫:

1
const puppeteer = require("puppeteer")

接下來,我們可以使用launch()方法創建一個瀏覽器實例:

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

我們通過將 { headless: false } 配置對象傳遞給launch()方法來顯示Chrome,讓Puppeteer執行操作時我們可以看到發生的情況,這在構建應用程序時很有幫助。

接下來,我們可以使用browser對象上的newPage()方法獲取page對象,然後在page對象上調用goto()方法加載JavaScript工作頁面:

1
2
3
4
5
6
7
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對象來指向頁面的DOM,盡管代碼將在Node.js環境中運行。這是Puppeteer執行的神奇部分。

在這個回調函數內部,我們無法將任何內容打印到控制台,因為它將打印到瀏覽器控制台,而不是Node.js終端。

我們能做的是從中返回一個對象,這樣我們就可以通過page.evaluate()返回的值來訪問它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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')

/* 在頁面內部運行JavaScript */
const data = await page.evaluate(() => {
return ....
})

console.log(data)
await browser.close()
})()

在這個函數內部,我們首先創建一個空數組,然後將要返回的值填充到這個數組中。

我們找到每個工作,它們被包裝在帶有job類的tr HTML元素中,然後我們使用querySelector()getAttribute()從每個工作中獲取數據:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 在頁面內部運行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
})

通過觀察瀏覽器開發工具,我找到了應該使用的確切選擇器:

Screenshot

以下是完整的源代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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")

/* 在頁面內部運行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
})

console.log(data)
await browser.close()
})()

如果執行此代碼,將返回一個包含工作詳細信息的對象數組:

Screenshot

將工作存儲到數據庫中

現在我們準備將這些數據存儲到本地數據庫中。

我們將定期執行Puppeteer腳本,首先刪除所有存儲的工作,然後使用找到的新工作重新填充數據庫。

我們將使用MongoDB。從終端運行以下命令:

1
npm install mongodb

app.js中,我們添加以下邏輯來初始化jobs數據庫及其中的jobs集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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之後才執行該代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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")

/* 在頁面內部運行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
})

console.log(data)
jobs.deleteMany({})
jobs.insertMany(data)
await browser.close()
})()
}
)

在該函數的末尾,我添加了以下內容:

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

首先清空MongoDB表格,然後插入我們的數組。

現在,如果再次嘗試運行node app.js,並使用終端控制台或像TablePlus這樣的應用程序檢查MongoDB數據庫中的內容,您將看到數據存在:

Screenshot

太酷了!現在我們可以設置一個cron job或任何其他自動化方式,每天或每6小時運行此應用程序,以始終獲得新鮮數據。

創建用於顯示工作的Node.js應用程序

現在,我們需要一種方法來顯示這些工作。我們需要一個應用程序。

我們將基於Express和使用Pug的服務器端模板構建一個基於Node.js的應用程序。

創建一個新的文件夾,在文件夾內運行npm init -y

然後安裝Express、MongoDB和Pug:

1
npm install express mongodb pug

首先初始化Express:

1
2
3
4
5
6
7
8
9
10
11
12
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數組中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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"))

大部分這段代碼和我們在網頁爬蟲中使用的代碼是相同的。不同之處在於,現在我們使用find()從數據庫獲取數據:

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

最後,當用戶訪問/端點時,我們渲染一個Pug模板:

1
2
3
4
5
app.get("/", (req, res) => {
res.render("index", {
jobs,
})
})

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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相同的文件夾中,將迭代工作數組以打印我們存儲的詳細信息:

1
2
3
4
5
6
7
html
body
each job in jobs
p
| #{job.company}
br
a(href=`${job.link}`) #{job.position}

以下是結果:

Screenshot