前言#
成為打工人後,沒有那麼多的時間去刷掘金了,很多時候只能在週末才有時間去優哉遊哉的躺在床上刷著熱榜,但這個時候我們往往會錯過掘金熱榜的歷史記錄(熱榜的內容一直在變化),這種情況或許會導致我們錯過一些很不錯的掘金文章,那麼有沒有什麼辦法可以讓我們只需要一行命令就可以記錄下掘金的歷史熱榜呢?這個時候就需要介紹一下Puppeteer了
簡單介紹#
Puppeteer 是一個由 Google 開發和維護的 Node.js 庫,它提供了一個高級的 API,用於通過控制一個 Headless Chrome(無界面的 Chrome 瀏覽器)實例來進行網頁的自動化操作。它可以用於執行各種任務,包括網頁截圖、生成 PDF、爬取數據、自動化表單填寫和交互等。
這裡再重點說一下 Headless Chrome,也就是無頭瀏覽器(之後的代碼會涉及這個概念),無頭瀏覽器是指沒有可見用戶界面的網絡瀏覽器。它可以在後台運行,執行網頁操作和瀏覽行為,但沒有圖形界面顯示,相較於傳統的網絡瀏覽器,通常會給我們一個用戶可以看見的界面,用戶通過這些界面進行交互,實現前後端的邏輯交互,如輸入 URL 進行鏈接跳轉,又或者是在登錄註冊的時候點擊提交。但無頭瀏覽器就可以讓這些操作在後台自動化運行,並不會一次次的彈窗,方便開發人員可以通過編寫腳本的方式來自動化的執行各種操作,如:
- Web 應用程序中的測試自動化
- 拍攝網頁截圖
- 對 JavaScript 庫運行自動化測試
- 收集網站數據
- 自動化網頁交互
接下來我就來演示一下如何快速的上手 Puppeteer 並且能夠僅僅依靠一行命令就可以獲取到掘金的熱榜文章信息吧
配置 Puppeteer#
這裡的配置實際上只是跟著官方文檔的快速上手走一下罷了,我這裡就簡單的過一下
這裡我們可以選擇直接安裝Puppeteer
npm i puppeteer
這裡要注意一點,這裡我們可以通過創建一個puppeteer.config.cjs
文件來對 puppeteer 進行配置:
const {join} = require('path');
/**
* @type {import("puppeteer").Configuration}
*/
module.exports = {
// Changes the cache location for Puppeteer.
cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};
我們先創建完這個配置文件後運行安裝的命令的話,我們可以看見多了一個.cache
文件夾,我們點開查看會發現裡面存儲了很多二進制文件,這也就涉及到一個提高啟動速度的優化方案了。.cache
文件會在我們第一次使用Puppeteer
的時候自動下載適合我們當前操作系統的 Chrome 瀏覽器二進制文件,這樣一來就避免了後續啟動的時候 Puppeteer 還需要重新下載所需文件,提高了啟動速度
初上手 Puppeteer#
這時候我們創建一個test.js
文件,輸入以下內容,我將逐行進行解釋:
//引入 Puppeteer 庫,使我們可以使用其中的功能,這裡使用ESM的語法也是可以的
//import puppeteer from 'puppeteer';
const puppeteer = require('puppeteer');
(async () => {
//啟動Chrome瀏覽器實例,目前Puppeteer默認為無頭瀏覽器的啟動模式
const browser = await puppeteer.launch();//const browser = await puppeteer.launch({headless: true});
//創建一個新的頁面對象
const page = await browser.newPage();
//代碼導航到指定的 URL,模擬我們在輸入URl進行跳轉這一操作
await page.goto('https://example.com');
//對當前頁面進行截圖操作,注意:Chrome團隊為了讓不同的設備上顯示的內容一致,默認的瀏覽器窗口呈現大小為800x600
await page.screenshot({path: 'example.png'});
//關掉Chrome
await browser.close();
})();
運行node .\test.js
命令後得到這張圖便說明你已經成功了:
很好,現在你已經初上手了 Puppeteer,並且掌握了最基本的操作,接下來便是實現我們上述所說的需求了
功能實現#
首先我們要知道掘金熱榜上,文章標題和文章的鏈接究竟是在哪,具體點說,不是讓我們明白它們的位置,而是讓Puppeteer
知道他的位置,這裡我們在熱榜部分打開控制台,這裡我們使用選擇器語法:Page.$$()
,該方法可以在瀏覽器裡運行document.querySelectorAll
方法,輸入:$$('a')
,得到的是一大堆 a 標籤,但這很明顯與我們預期的不符
這時候我們就需要縮小他的範圍,將鼠標放在熱榜上,單擊右鍵 “檢查”
這時候我們就可以在控制台上快速定位到這一部分的內容了,這時候我們修改選擇器內容:$$('.hot-list>a')
,這時候我們就獲取到了鏈接內容,這時候我們想要獲取到標題,原理也就一樣了,再對其進行稍微的處理:$$('.article-title').map(x=>x.innerText)
,便可以得到掘金熱榜的標題了
坑點#
如果此時我們直接運行代碼,極大概率得到的是[]
,這裡就涉及很重要的一點了,網頁加載延遲
這裡我們取消掉默認的無頭瀏覽器模式,修改代碼為:
const browser = await puppeteer.launch({ headless: false })
這時候我們再運行代碼,就會發現網頁在還沒有加載完全的情況下就結束了我們的操作,這個時候我們就需要設置waitUntil
或延遲加載該腳本了,修改代碼:
await page.goto("https://juejin.cn/hot/articles", {
waitUntil: "domcontentloaded",
});
await page.waitForTimeout(2000);
但這裡我們翻看文檔會發現,這裡會提示我們page.waitForTimeout
已過時,更推薦我們使用Frame.waitForSelector
,它會等待與給定選擇器匹配的元素出現在幀中才運行代碼,相比較與直接延遲執行代碼來講更加的高效,這裡先暫時這樣,之後我附上完整的代碼
補全並優化功能#
當我們能夠成功檢測到內容後,我們需要的便是將其保存到本地,這時候便引入 Node.js 的文件系統模塊,將檢測到的文件內容進行寫入:
import puppeteer from "puppeteer";
import fs from "fs";
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://juejin.cn/hot/articles', {
waitUntil: "domcontentloaded"
});
await page.waitForTimeout(2000);
let hotList = await page.$$eval(".article-title[data-v-cfcb8fcc]", (title) => {
return title.map((x) => x.innerText);
});
console.log(hotList);
// 將文章標題保存到文本文件
fs.writeFile('titles.txt', hotList.join('\n'), (err) => {
if (err) throw err;
console.log('文章標題已保存到titles.txt文件');
});
await browser.close();
})();
此時便獲取到了文章的所有標題,但是光有標題可不夠,週末的時候想刷點文章如果還需要手動的輸入的話那多麻煩,這時候就需要將文章的標題和鏈接一起存入,調用closest("a").href
來獲取鏈接:
const articleList = await page.$$eval(
".article-title[data-v-cfcb8fcc] a",
(articles) => {
return articles.map((article) => ({
title: article.innerText,
link: article.href,
}));
}
);
console.log(articleList);
// 將文章標題和鏈接保存到文本文件
const formattedData = articleList.map(
(article) => `${article.title} - ${article.link}`
);
fs.writeFile("articles.txt", formattedData.join("\n"), (err) => {
if (err) throw err;
console.log("文章標題和鏈接已保存到articles.txt文件");
});
大功告成!但這時候我們發現第二天我們重新運行這個腳本的時候,把前一天的文件給覆蓋了,這可不行啊,那就按照日期分類,將文章的熱榜按照不同的天數進行分類,這裡我們再添加上先前所說的等待與給定選擇器匹配的元素出現在幀中後的功能。最終得到:
import puppeteer from "puppeteer";
import fs from "fs";
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto("https://juejin.cn/hot/articles", {
waitUntil: "domcontentloaded",
});
//處理文件夾名
const currentDate = new Date().toLocaleDateString();
const fileName = `${currentDate.replace(/\//g, "-")}.txt`;
await page.waitForSelector(".article-title[data-v-cfcb8fcc]");
const articleList = await page.$$eval(
".article-title[data-v-cfcb8fcc]",
(articles) => {
return articles.map((article) => ({
title: article.innerText,
link: article.closest("a").href,
}));
}
);
console.log(articleList);
const formattedData = articleList.map(
(article) => `${article.title} - ${article.link}`
);
fs.writeFile(fileName, formattedData.join("\n"), (err) => {
if (err) throw err;
console.log(`文章標題和鏈接已保存到文件: ${fileName}`);
});
await browser.close();
})();
運行代碼後變得到了這樣的內容:
總結#
Puppeteer
作為一個由 Google 團隊開發並維護的 Node.js 庫,極大程度上方便了我們進行各種自動化的操作,想像一下,之後你只需要運行一行簡單的 node 命令即可存儲當前的熱榜文章和信息,豈不美哉🐱
然而爬蟲這一方案僅僅是他眾多功能中最微不足道的一點,正如官方所說,它還可以進行自動化表單提交、UI 測試、捕獲站點的時間線、對 SPA 進行爬蟲以達到預呈現的效果(這點有機會再出一篇關於前端首屏優化的文章😽)