前言
學 Rust 也有一段時間了,網上也有不少官方文件的中文翻譯版,但是似乎只有 Rust中文網站 文件一直是最新的,奈何並沒有 PDF 供直接下載,是在是不太方便,為了方便閱讀以及方便後續文件更新,決定用 Python 寫一個爬蟲將網頁下載下來保持為 PDF. 最後完成結果如下:
是的沒錯,將官網樣式也保留下來成功轉為 PDF,接下來分享一下整個爬蟲的過程,最終的爬蟲可以匯出任意 VuePress 搭建的網站為 PDF.
爬蟲
依賴庫的選定
- requests
- BeautifulSoup4
- pdfkit
關於 requests 和 BeautifulSoup4 庫這裡就不做介紹了, 寫過爬蟲的基本上都接觸過, 重點說一下 pdfkit 庫, 毫無疑問,它就是匯出 PDF 的關鍵,簡單說一下它的用法
PdfKit
PdfKit 庫是對 Wkhtmltopdf 工具包的封裝類,所以在使用之前,需要去官網下載相應的安裝包安裝到電腦上, 下載地址
可選: 安裝完成之後可以 Windows 下可以將安裝路徑新增到系統環境變數中
安裝完成之後,說一下 PdfKit 的常用方法,常用方法有三個
from_url
def from_url(url, output_path, options=None, toc=None, cover=None,
configuration=None, cover_first=False):
"""
Convert file of files from URLs to PDF document
:param url: URL or list of URLs to be saved
:param output_path: path to output PDF file. False means file will be returned as string.
:param options: (optional) dict with wkhtmltopdf global and page options, with or w/o '--'
:param toc: (optional) dict with toc-specific wkhtmltopdf options, with or w/o '--'
:param cover: (optional) string with url/filename with a cover html page
:param configuration: (optional) instance of pdfkit.configuration.Configuration()
:param configuration_first: (optional) if True, cover always precedes TOC
Returns: True on success
"""
r = PDFKit(url, 'url', options=options, toc=toc, cover=cover,
configuration=configuration, cover_first=cover_first)
return r.to_pdf(output_path)
從函式名上就很容易理解這個函式的作用,沒錯就是根據 url 下載網頁為 PDF
from_file()
def from_file(input, output_path, options=None, toc=None, cover=None, css=None,
configuration=None, cover_first=False):
"""
Convert HTML file or files to PDF document
:param input: path to HTML file or list with paths or file-like object
:param output_path: path to output PDF file. False means file will be returned as string.
:param options: (optional) dict with wkhtmltopdf options, with or w/o '--'
:param toc: (optional) dict with toc-specific wkhtmltopdf options, with or w/o '--'
:param cover: (optional) string with url/filename with a cover html page
:param css: (optional) string with path to css file which will be added to a single input file
:param configuration: (optional) instance of pdfkit.configuration.Configuration()
:param configuration_first: (optional) if True, cover always precedes TOC
Returns: True on success
"""
r = PDFKit(input, 'file', options=options, toc=toc, cover=cover, css=css,
configuration=configuration, cover_first=cover_first)
return r.to_pdf(output_path)
這個則是從檔案中生成 PDF, 也是我最後選擇的方案,至於為什麼沒有選擇 from_url(),稍後等我分析完,就會明白了.
from_string
def from_string(input, output_path, options=None, toc=None, cover=None, css=None,
configuration=None, cover_first=False):
"""
Convert given string or strings to PDF document
:param input: string with a desired text. Could be a raw text or a html file
:param output_path: path to output PDF file. False means file will be returned as string.
:param options: (optional) dict with wkhtmltopdf options, with or w/o '--'
:param toc: (optional) dict with toc-specific wkhtmltopdf options, with or w/o '--'
:param cover: (optional) string with url/filename with a cover html page
:param css: (optional) string with path to css file which will be added to a input string
:param configuration: (optional) instance of pdfkit.configuration.Configuration()
:param configuration_first: (optional) if True, cover always precedes TOC
Returns: True on success
"""
r = PDFKit(input, 'string', options=options, toc=toc, cover=cover, css=css,
configuration=configuration, cover_first=cover_first)
return r.to_pdf(output_path)
這個方法則是從字串中生成 PDF,很明顯沒有辦法保持網頁樣式,所以不考慮.關於更多 PdfKit 的用法,可以去 wkhtmltopdf文件 檢視
分析目標網頁
依賴庫選定完畢,接下來就是分析目標網頁,開始寫爬蟲的過程了.
測試 PdfKit
PdfKit 自帶一個 from_url 生成 PDF 的功能,如果可以生成合適的 PDF,那我們只需要獲取所有網頁連結就可以了,可以節省很多時間,先測試一下生成的效果
import pdfkit
pdfkit.from_url("https://rustlang-cn.org/office/rust/book/", 'out.pdf', configuration=pdfkit.configuration(
wkhtmltopdf="path/to/wkhtmltopdf.exe"))
匯出結果如下:
從結果不難看出,網頁的樣式儲存下來了,但是側邊欄,頂部和底邊導航欄也都被保留下來了,並且側邊欄還擋住了主要內容,所以使用 from_url 這個方法就被排除了.
最終方案
通過測試,我們得知不能使用 from_url 那麼只能通過使用 from_file 去匯出了, 並且在我們將網頁下載下來儲存到本地之前,我們需要修改網頁內容,移除頂部導航欄,側邊欄,以及底部導航欄
獲取相應元素
現在讓我們先獲取頁面下一頁連結,開啟瀏覽器除錯模式,審查一下網頁元素,不難發現所有下一頁導航,都處於 之下的超連結 中,如下圖:
通過同樣的方法,不難發現頂部導航欄,側邊欄,以及底部導航欄對應的元素,依次為
<div class="navbar"></div>
<div class="sidebar"></div>
<div class="page-edit"></div>
找到對應的元素接著就是獲取連結和銷燬不必要元素
class DownloadVuePress2Pdf:
def get_content_and_next_url(self, content): # content 為網頁內容
# 獲取連結和銷燬不必要元素
navbar = soup.select('.navbar')
if len(navbar):
navbar[0].decompose()
sidebar = soup.select('.sidebar')
if len(sidebar):
sidebar[0].decompose()
page_edit = soup.select('.page-edit')
if len(page_edit):
page_edit[0].decompose()
# 注意下一頁連結在底部導航欄元素中,
# 要先獲取連結後,才能銷燬元素,順序不能顛倒
next_span = soup.select(".next")
if len(next_span):
next_span_href = next_span[0].a['href']
else:
next_span_href = None
page_nav = soup.select('.page-nav')
if len(page_nav):
page_nav[0].decompose()
保持匯出 PDF 樣式
為了使得匯出 PDF 的樣式和網頁一致,我們有倆種方法:
- 根據原始碼在對應目錄建立本地 css 檔案,顯然這種方法不具有普遍性,不能每匯出一個網站,我們就新建一個 css 檔案
- 既然本地的不行,那我們就將網頁中的 css 連結 href 地址指向遠端 css
在上述程式碼中新增如下程式碼:
for link in links:
if not link['href'].startswith("http"):
link['href'] = css_domain + link['href'] # css_domain 為 css 預設域名,需要設定,獲取方式可見下圖
匯出
通過上述的方式,我們將網頁下載下來儲存到本地,全部下載完成之後,最後就是匯出為 PDF 了,通過 from_file() 方法很容易完成匯出這個操作
pdfkit.from_file([檔案列表], "匯出的檔名稱.pdf", options=options, configuration=config)
至此匯出 Rust 官網文件為 PDF 的過程全部完成,效果如開頭展示的那樣
注意: 由於 VuePress 搭建的網站基本上佈局格式一樣, 所以上面的程式碼同樣可以用來匯出其他由 VuePress 構建的網站
完整程式碼
搜尋公眾號 LeeTao,回覆 20190509 即可獲得