利用Python爬取教程並轉為PDF格式,隨看隨學!
作為一名程式員,經常要搜一些教程,有的教程是線上的,不提供離線版本,這就有些侷限了。那麼同樣作為一名程式設計師,遇到問題就應該解決它,今天就來將線上教程儲存為PDF以供查閱。
1、網站介紹
2、準備工作
2.1軟體安裝2.2庫安裝
3、爬取內容
3.1獲取教程名稱3.2獲取目錄及對應網址3.3獲取章節內容3.4儲存pdf3.5合併pdf
1、網站介紹
之前再搜資料的時候經常會跳轉到如下圖所示的線上教程:

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">01.教程樣式</figcaption>
註釋:加群696541369獲取python入門20天完整學習筆記和100道基礎練習題及答案以及入門書籍視訊原始碼等資料
包括一些github的專案也紛紛將教程連結指向這個網站。經過一番查詢,該網站是一個可以建立、託管和瀏覽文件的網站,其網址為: ofollow,noindex">https://readthedocs.org 。在上面可以找到很多優質的資源。
該網站雖然提供了下載功能,但是有些教程並沒有提供PDF格式檔案的下載,如圖:

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">02.下載</figcaption>
該教程只提供了 HTML格式檔案的下載,還是不太方便查閱,那就讓我們動手將其轉成PDF吧!
2、準備工作
2.1 軟體安裝
由於我們是要把html轉為pdf,所以需要手動 wkhtmltopdf 。Windows平臺直接在 http://wkhtmltopdf.org/downloads.html 下載穩定版的 wkhtmltopdf 進行安裝,安裝完成之後把該程式的執行路徑加入到系統環境 $PATH 變數中,否則 pdfkit 找不到 wkhtmltopdf 就出現錯誤 “No wkhtmltopdf executable found”。Ubuntu 和 CentOS 可以直接用命令列進行安裝
$ sudo apt-get install wkhtmltopdf# ubuntu$ sudo yum intsall wkhtmltopdf # centos
2.2 庫安裝
pip install requests # 用於網路請求
pip install beautifulsoup4 # 用於操作html
pip install pdfkit # wkhtmltopdf 的Python封裝包
pip install PyPDF2 # 用於合併pdf
3、爬取內容
本文的目標網址為: http://python3-cookbook.readthedocs.io/zh_CN/latest/ 。
3.1 獲取教程名稱
頁面的左邊一欄為目錄,按F12調出開發者工具並按以下步驟定位到目錄元素:
① 點選開發者工具左上角" 選取頁面元素 "按鈕;
② 用滑鼠點選左上角教程名稱處。
通過以上步驟即可定位到目錄元素,用圖說明:

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">03.尋找教程名稱</figcaption>
從圖看到我們需要的教程名稱包含在<div class="wy-side-nav-search"></div>之間的a標籤裡。假設我們已經獲取到了網頁內容為html,可以使用以下程式碼獲取該內容:
book_name = soup.find('div',class_='wy-side-nav-search').a.text
3.2 獲取目錄及對應網址
使用與 2.1 相同的步驟來獲取:

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">04.定位目錄及網址</figcaption>
從圖看到我們需要的目錄包含在<div class="section"></div>之間,<li class="toctree-l1"></li>標籤裡為一級目錄及網址;<li class="toctree-l2"></li>標籤裡為二級目錄及網址。當然這個url是相對的url,前面還要拼接http://python3-cookbook.readthedocs.io/zh_CN/latest/。
使用BeautifulSoup進行資料的提取:
# 全域性變數base_url = 'http://python3-cookbook.readthedocs.io/zh_CN/latest/'book_name = ''chapter_info = []def parse_title_and_url(html): """ 解析全部章節的標題和url :param html: 需要解析的網頁內容 :return None """ soup = BeautifulSoup(html, 'html.parser') # 獲取書名 book_name = soup.find('div', class_='wy-side-nav-search').a.text menu = soup.find_all('div', class_='section') chapters = menu[0].div.ul.find_all('li', class_='toctree-l1') for chapter in chapters: info = {} # 獲取一級標題和url # 標題中含有'/'和'*'會儲存失敗 info['title'] = chapter.a.text.replace('/', '').replace('*', '') info['url'] = base_url + chapter.a.get('href') info['child_chapters'] = [] # 獲取二級標題和url if chapter.ul is not None: child_chapters = chapter.ul.find_all('li') for child in child_chapters: url = child.a.get('href') # 如果在url中存在'#',則此url為頁面內連結,不會跳轉到其他頁面 # 所以不需要儲存 if '#' not in url: info['child_chapters'].append({ 'title': child.a.text.replace('/', '').replace('*', ''), 'url': base_url + child.a.get('href'), }) chapter_info.append(info)
程式碼中定義了兩個全域性變數來儲存資訊。章節內容儲存在 chapter_info 列表裡,裡面包含了層級結構,大致結構為:
[ {'title':'first_level_chapter','url':'www.xxxxxx.com','child_chapters': [ {'title':'second_level_chapter','url':'www.xxxxxx.com', } ... ] } ...]
3.3 獲取章節內容
還是同樣的方法定位章節內容:

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;">05.獲取章節內容</figcaption>
程式碼中我們通過itemprop這個屬性來定位,好在一級目錄內容的元素位置和二級目錄內容的元素位置相同,省去了不少麻煩。
html_template ="""<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"></head><body>{content}</body></html>"""defget_content(url):""" 解析URL,獲取需要的html內容 :param url: 目標網址 :return: html """html = get_one_page(url) soup = BeautifulSoup(html,'html.parser') content = soup.find('div', attrs={'itemprop':'articleBody'}) html = html_template.format(content=content)returnhtml
3.4 儲存pdf
defsave_pdf(html, filename):""" 把所有html檔案儲存到pdf檔案 :param html: html內容 :param file_name: pdf檔名 :return: """options = {'page-size':'Letter','margin-top':'0.75in','margin-right':'0.75in','margin-bottom':'0.75in','margin-left':'0.75in','encoding':"UTF-8",'custom-header': [ ('Accept-Encoding','gzip') ],'cookie': [ ('cookie-name1','cookie-value1'), ('cookie-name2','cookie-value2'), ],'outline-depth':10, } pdfkit.from_string(html, filename, options=options)defparse_html_to_pdf():""" 解析URL,獲取html,儲存成pdf檔案 :return: None """try:forchapterinchapter_info: ctitle = chapter['title'] url = chapter['url']# 資料夾不存在則建立(多級目錄) dir_name = os.path.join(os.path.dirname(__file__), 'gen', ctitle) if not os.path.exists(dir_name): os.makedirs(dir_name) html = get_content(url) padf_path = os.path.join(dir_name, ctitle + '.pdf') save_pdf(html, os.path.join(dir_name, ctitle + '.pdf')) children = chapter['child_chapters'] if children: for child in children: html = get_content(child['url']) pdf_path = os.path.join(dir_name, child['title'] + '.pdf') save_pdf(html, pdf_path) except Exception as e: print(e)
3.5 合併pdf
經過上一步,所有章節的pdf都儲存下來了,最後我們希望留一個pdf,就需要合併所有pdf並刪除單個章節pdf。
fromPyPDF2importPdfFileReader, PdfFileWriterdef merge_pdf(infnList, outfn):""" 合併pdf :param infnList: 要合併的PDF檔案路徑列表 :param outfn: 儲存的PDF檔名 :return: None """pagenum =0pdf_output = PdfFileWriter()forpdfininfnList:# 先合併一級目錄的內容 first_level_title = pdf['title'] dir_name = os.path.join(os.path.dirname( __file__), 'gen', first_level_title) padf_path = os.path.join(dir_name, first_level_title + '.pdf') pdf_input = PdfFileReader(open(padf_path, 'rb')) # 獲取 pdf 共用多少頁 page_count = pdf_input.getNumPages() for i in range(page_count): pdf_output.addPage(pdf_input.getPage(i)) # 新增書籤 parent_bookmark = pdf_output.addBookmark( first_level_title, pagenum=pagenum) # 頁數增加 pagenum += page_count # 存在子章節 if pdf['child_chapters']: for child in pdf['child_chapters']: second_level_title = child['title'] padf_path = os.path.join(dir_name, second_level_title + '.pdf') pdf_input = PdfFileReader(open(padf_path, 'rb')) # 獲取 pdf 共用多少頁 page_count = pdf_input.getNumPages() for i in range(page_count): pdf_output.addPage(pdf_input.getPage(i)) # 新增書籤 pdf_output.addBookmark( second_level_title, pagenum=pagenum, parent=parent_bookmark) # 增加頁數 pagenum += page_count # 合併 pdf_output.write(open(outfn, 'wb')) # 刪除所有章節檔案 shutil.rmtree(os.path.join(os.path.dirname(__file__), 'gen'))
本來PyPDF2庫中有一個類PdfFileMerger專門用來合併pdf,但是在合併過程中會丟擲異常,網上有人也遇到同樣的問題,解決辦法是修改庫原始碼,本著“不動庫原始碼”的理念,毅然選擇了上面這種比較笨的辦法,程式碼還是比較好理解的。
經過以上幾個步驟,我們想要的pdf檔案已經生成,一起來欣賞一下勞動成果:
