最近在看一位大佬寫的原始碼解析部落格,平時上下班用手機看不太得勁,但是平板又沒有網絡卡,所以就想搞個離線pdf版,方便通勤時間學習閱讀。

所以,問題來了: 怎麼把線上網頁內容轉成pdf?

這位大佬的部落格是用gitbook寫的,我先上網搜了下工具,發現大多是將自己本地gitbook轉pdf,只有一個開源工具是用python爬取的線上gitbook,但是一看issues,中文亂碼、空白頁、看不到圖等等問題都沒解決,遂放棄……

經過我不懈的搜尋,終於找到了一個可以直接把網頁儲存成pdf工具:phantomjs

phantomjs是一個無介面的,可指令碼程式設計的 WebKit 瀏覽器引擎,也就是俗稱的“無頭瀏覽器”,常用於爬取網頁資訊。

下載地址:https://phantomjs.org/download.html

我用的是win系統,下載後在bin目錄下找到.exe檔案,然後新建如下js指令碼:

// html2pdf.js
var page = require('webpage').create();
var system = require('system'); if (system.args.length === 1) {
console.log('Usage: loadspeed.js <some URL>');
//這行程式碼很重要。凡是結束必須呼叫。否則phantomjs不會停止
phantom.exit();
}
page.settings.loadImages = true; //載入圖片
page.settings.resourceTimeout = 30000;//超過10秒放棄載入
//截圖設定,
//page.viewportSize = {
// width: 1000,
// height: 3000
//};
var address = system.args[1];
var name = system.args[2];
page.open(address, function (status) { function checkReadyState() {//等待載入完成將頁面生成pdf
setTimeout(function () {
var readyState = page.evaluate(function () {
return document.readyState;
}); if ("complete" === readyState) { page.paperSize = { width: '297mm', height: '500mm', orientation: 'portrait', border: '1cm' };
var timestamp = Date.parse(new Date());
var pdfname = name;
var outpathstr = "C:/Users/Desktop/pdfs/" + pdfname + ".pdf"; // 輸出路徑
page.render(outpathstr);
console.log("生成成功");
console.log("$" + outpathstr + "$");
phantom.exit();
} else {
checkReadyState();
}
}, 1000);
}
checkReadyState();
});

控制檯cd進入bin目錄,命令列執行:

phantomjs html2pdf.js http://xxx.xx.xxx(部落格所在地址)

即可將該網頁轉成一個pdf。

這時候問題又來了:

  1. phantomjs不能自動擷取下一頁;
  2. 每一個網頁都只能生成一個pdf,最後還要找工具把所有pdf合併成一個;
  3. 儲存的實際上是網頁的截圖,對於側邊欄、頂部欄、底部等我不需要的內容也會儲存到頁面中,不能很好地適配。

思考和觀察得出解決辦法:

  • 這個部落格的網址除了域名統一外並沒有其他規律,只能手動維護一個url列表,然後通過指令碼遍歷來解決第 1 個問題;
  • pdf合併工具網上有現成的,第 2 個問題也能解決;
  • phantomjs是可以對dom進行操作的,但是有個問題,頁面裡如果有非同步請求的資源,比如圖片,就需要延遲截圖時間,否則會出現很多空白區域,具體解決辦法可以參見這篇部落格:使用phantomjs操作DOM並對頁面進行截圖需要注意的幾個問題

問題雖然是能解決,但是過於麻煩,而且這個dom操作並不能在截圖的時候去掉多餘內容。

經過上面一系列騷操作,我受到了啟發,從而有了一個全新的思路:

通過dom操作,把整個部落格的內容都爬到一個html檔案中,再把這個html檔案轉成pdf。

話不多說,直接開擼。

為了避開瀏覽器同源網路策略,我基於之前搭建的node+express本地服務,並引入外掛cheerio(用於dom操作)、html-pdf(用於將網頁轉成pdf)來實現。

首先,觀察需要爬取的dom元素的特點:

我需要爬取的內容如圖所示



這部分的內容可以通過.theme-default-content樣式獲取到:

https.get(url, function (res) {
var str = "";
//繫結方法,獲取網頁資料
res.on("data", function (chunk) {
str += chunk;
})
//資料獲取完畢
res.on("end", function () {
//沿用JQuery風格,定義$
var $ = cheerio.load(str);
//獲取的資料陣列
var arr = $(".theme-default-content");
var main = "";
if (arr && arr.length > 0) {
main = arr[0]
}
})

通過這段程式碼得到的main就是我們要獲取的主體dom。

其次,觀察圖片資源的url:



這裡用的是相對路徑,所以需要對圖片路徑進行處理:

// 將上面得到的main轉為字串便於處理
main = $.html(main)
// 對圖片路徑進行補全
main = main.replace(/src=\"\/img/g, "src=\"" + prefixUrl + "/img")

觀察下一頁的url地址,都是在一個樣式名為nextspan標籤內:



獲取下一頁內容的程式碼如下:

var $ = cheerio.load(str);
var arr = $(".next");
var nextData = "";
if (arr && arr.length > 0) {
nextData = arr[0]
if (nextData && nextData.children && nextData.children[0] && nextData.children[0].attribs) {
// 下一頁地址:prefixUrl + nextData.children[0].attribs.href
} else {
// 沒有下一頁
}
}

最後還需要把html轉成pdf:

function htmlTopdf() {
var html = fs.readFileSync(path.resolve(__dirname, htmlFileName), 'utf8');
var options = { format: 'A4' };
pdf.create(html, options).toFile(path.resolve(__dirname, pdfFileName), function (err, res) {
if (err) return console.log("error message", err);
console.log(res);
});
}

總結一下實現思路

  1. 通過dom操作抓取到主體內容,並對其中的圖片等資源進行處理,然後儲存到html檔案中;
  2. 找到下一頁的url,將下一頁的主體內容繼續拼到html檔案後;
  3. 最後將html轉成pdf儲存。

上面的程式碼不是通用的,但是如果要抓取其他網頁的話,思路基本都是這三步。

Demo已開源:https://github.com/youzouzou/node-crawler/blob/main/routes/index.js

npm install後開啟http://localhost:3009/ 即可生成pdf。