使用Puppeteer進行網頁爬蟲
這篇文章將使用Puppeteer創建一個“JavaScript工作版”,用於彙總JavaScript開發人員的遠程工作。
以下是完成這個項目的步驟:
- 使用Puppeteer創建基於Node.js的網頁爬蟲,從remoteok.io網站獲取工作信息
- 將工作信息存儲到數據庫中
- 創建基於Node.js的應用程序,將這些工作信息顯示在自己的網站上
注意:我只是將此網站作為示例,並不建議您進行網頁爬蟲,因為該網站有官方API可供使用。我只是用它來解釋Puppeteer如何與人人皆知的網站配合工作,以及如何在實踐中使用它。
讓我們開始吧!
為JavaScript工作創建網頁爬蟲
我們將從remoteok.io這個很棒的遠程工作網站上爬取JavaScript工作。
此網站上有許多不同類型的工作。JavaScript工作在“JavaScript”標籤下列出,在撰寫本文的時候,可以在此頁面上找到:https://remoteok.io/remote-javascript-jobs
我之所以說“在撰寫本文的時候”是因為這是一個重要的認識:網站可能隨時更改。我們無法確保任何事情。使用網頁爬蟲時,網站的任何更改都可能使我們的應用程序停止工作。這不是一個API,像是兩個方之間的合同。
根據我的經驗,網頁爬蟲應用程序需要更多的維護工作。但有時我們別無選擇,只能使用它來完成特定的任務,所以它們仍然是我們可以使用的有效工具。
設置Puppeteer
我們首先創建一個新的文件夾,在文件夾內運行以下命令:
然後使用以下命令安裝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 })
|
通過觀察瀏覽器開發工具,我找到了應該使用的確切選擇器:
以下是完整的源代碼:
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() })()
|
如果執行此代碼,將返回一個包含工作詳細信息的對象數組:
將工作存儲到數據庫中
現在我們準備將這些數據存儲到本地數據庫中。
我們將定期執行Puppeteer腳本,首先刪除所有存儲的工作,然後使用找到的新工作重新填充數據庫。
我們將使用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數據庫中的內容,您將看到數據存在:
太酷了!現在我們可以設置一個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}
|
以下是結果: