天天看點

使用python從線上網頁制作epub(selenium,ebooklib)開始碎碎念

文章目錄

  • 開始
    • 處理過程
    • 1. 抓取文章内容
      • 1. 前置知識點
      • 1.1. web目錄→本地txt目錄
      • 1.1 關鍵代碼(目錄資訊在window内的情況)
      • 1.2. web章節網頁→本地章節txt
    • 2. 對資料進行處理
      • 2. 前置知識點
      • 2.1. 控制換行
      • 2.2. 去除奇奇怪怪的标簽
      • 2.3. 拆分一個檔案内的多個章節
    • 3. 制作epub
      • 3.1. 建立book,填入元資訊
      • 3.2. 添加css
      • 3.3. 添加章節
      • 3.4. 添加目錄和書脊
      • 3.5. 導出
  • 碎碎念
    • 坑1
    • 坑2
    • 坑3

關鍵詞:selenium庫,epub規範,ebooklib庫,re正規表達式,淺淺的爬蟲/前端知識

開始

處理過程

大緻的處理過程為:

  1. 抓取web的目錄頁,擷取每個章節的網址,存為txt檔案。周遊上述txt檔案,抓取web網頁并提取正文内容部分存儲為web檔案夾下txt檔案,避免每次調試程式都重新抓取一遍web網頁,而且selenium蠻慢的。
  2. 周遊web檔案夾,對這些原始資料進行整理和修改,将修改過後的檔案存為modify檔案夾下txt檔案,這些檔案就是即将被制作成epub的章節檔案。
  3. 用ebooklib庫,根據modify檔案夾下的檔案,制作epub
import os
import re
from ebooklib import epub
from selenium import webdriver
           

1. 抓取文章内容

1. 前置知識點

用selenium (Chrome)通路baidu.com并擷取到右上角設定所在框裡的文字

from selenium import webdriver

driver=webdriver.Chrome()
# 通路網站
driver.get("https://www.baidu.com/")
# css選擇器定位,擷取其内文字
word=driver.find_element_by_css_selector('#s-usersetting-top').get_attribute('innerText')
print(word)
           

如何得出來用于定位的#s-usersetting-top:在網頁按f12調出控制台後,點選控制台最左上角方框箭頭,點選你想定位的元素,右鍵控制台内被高亮的标簽——Copy Selector

使用python從線上網頁制作epub(selenium,ebooklib)開始碎碎念

getHTML會擷取該标簽内html内容,和你在控制台看到的一樣

getText隻會擷取标簽内的文字

1.1. web目錄→本地txt目錄

selenium的安裝請各位看其他文章,大多有比較詳細的安裝說明,這裡不再贅述,需要注意下載下傳的驅動與你的浏覽器版本能夠對應上。

這裡以我抓取的網站為例,可以看到目錄網頁的html裡的script标簽包含了每個章節的url資訊(aid),其作用是對浏覽器的window對象注入了一個閉包,調用它會傳回一個json。是以我們直接使用selenium運作控制台指令,來通路這個挂到window上的閉包就能擷取到目錄的資訊。(除了從selenium擷取外,你也可以通過Request請求html文檔,然後用Execjs庫運作閉包裡的函數,或者直接用NodeJS爬)

使用python從線上網頁制作epub(selenium,ebooklib)開始碎碎念

除了上面這種需要js運作時環境的方式之外,大多數平台對目錄資料的存儲方式是在html檔案的body内的一個清單,遇到這種情況則可以使用

driver.find_elements_by_css_selector

方法找到ul内li元素組成的清單,然後for循環周遊清單内元素來得到每章的資訊。

使用python從線上網頁制作epub(selenium,ebooklib)開始碎碎念

1.1 關鍵代碼(目錄資訊在window内的情況)

driver.execute_script

能夠在selenium的控制台運作js代碼,js内return的東西會被這個方法傳回,這裡擷取的是一個章節的json list。然後将每個章節的id拼接上網址儲存在txt裡

這裡我把每個網址後面加上##和網址對應的章節數,友善下一步将下載下傳的檔案命名

zfill(3)可以在字元串左邊補充若幹的0來使得字元串長度等于3

driver.get("url")
article_list=driver.execute_script("return window.__NUXT__.data[0].series.articles")
article_website_list=[]
for art in article_list:
    article_website_list.append(r'https://www.lightnovel.us/cn/detail/'+str(art['aid'])+"##"+str(picknum_from_webtitle(art['title'])).zfill(3))
    # 把爬取的章節id拼接網址後儲存為txt
    with open('{}/bookurls.txt'.format(storage_folders['root']),'w',encoding="utf8") as f:
        f.write("\n".join(article_website_list))
           

1.2. web章節網頁→本地章節txt

在這部分,通路bookurls.txt裡記錄的每個章節的網址。分割出網址和其對應的章節名字元串(同時以章節名字元串作為爬下來的txt的名稱)。

定位到文章正文所在的html标簽,儲存其中的html格式的内容(由于Epub格式與html相同,這種方法能以網頁觀感保留資訊)。

with open('{}/bookurls.txt'.format(storage_folders['root']),'r',encoding="utf8") as f:
        urls=f.readlines()
for url in urls:
	# 分割url與章節字元串
    weburl,num=url.strip().split("##")
    print("爬取 {}".format(num))
    # 如果檔案已經在web檔案夾裡,則不再進行爬取
    if os.path.exists('{}/{}.txt'.format(storage_folders['web'],num.zfill(3))):
        print("網頁 {} 已經存在".format(num))
        continue
    driver.get(weburl)
    art=driver.find_element_by_id("article-main-contents").get_attribute('innerHTML')
    with open('{}/{}.txt'.format(storage_folders['web'],num.zfill(3)),'w',encoding="utf8") as f:
        f.write(art)
           

2. 對資料進行處理

2. 前置知識點

re正則比對

用re正則比對章節名,提取章節數字那部分和章節名

import re

wordlist=['<p>第六章 七七四十九</p>','說時遲那時快','現在來到了第18章','第一名姓章','<p>第14章 四四十六</p>']

for word in wordlist:
    if re.search(r'<p>第.*?章 .*?</p>',word):
        print(re.sub(r'<p>第(.*?)章 (.*?)</p>',r'\1-\2',word))
# 輸出:
# 六-七七四十九
# 14-四四十六
           

其中,re.search能夠判斷word裡是否存在這個正則pattern,然後re.sub能夠将正則pattern内的括号之外内容去除,保留括号内的内容。

?表示盡可能早的結束比對,正則比對預設會比對到盡可能長的字元串。對于

'<p>第69章 簽字 蓋章 成交</p>'

這種包含章的标題内容,如果pattern内不加

,則結果會是

69章 簽字 蓋-成交

,加了

?

後:

69-簽字 蓋章 成交

建議自己檢查下正則出來的内容,隻要自己想制作的epub沒有出現問題,任何不嚴謹的正則式子都是可以接受的。

2.1. 控制換行

第一步是替換網頁換行符——br标簽,我爬的網頁中br标簽是單标簽但是缺少末尾的/,這在epub中是無法解析的,而且文章中出現太多的多行空白,是以需要處理換行。

使用python從線上網頁制作epub(selenium,ebooklib)開始碎碎念

這裡我直接按br标簽把正文内容分割成清單,然後控制清單裡連續的空白元素的數量不超過2,最後處理結束合并每行時給每行套

<p></p>

标簽

#每個br作為分割點
contentlist=content.split("<br>")
for con in contentlist:
	# 空行檢測
	if blankflag and not con:# 上行和這行空
	    continue
	elif not con:# 這行空
	    blankflag=True
	else:# 這行非空
	    blankflag=False
	# 處理結束合并時給每行套<p></p>标簽
	con="<p>"+con.strip()+"</p>"
	# 後續處理...
           

2.2. 去除奇奇怪怪的标簽

如果内容充滿了奇奇怪怪的各種标簽,如這一話,每行都被左對齊和大字型的span标簽包裹。

使用python從線上網頁制作epub(selenium,ebooklib)開始碎碎念

使用re.sub提取每行的中心文字,删除奇奇怪怪的那些标簽

def editif50(sencontent):
    # 如果是50話,這話版面抽風多出來很多标簽,正則去除
    choufeng=re.compile(r' style="font-size:large"| style="font-family:Arial"')
    res=re.sub(choufeng,r'',sencontent)
    spanspan=re.compile(r'<span><span>(.*?)</span></span>')
    res=re.sub(spanspan,r'\1',res)
    sbdiv=re.compile(r'<div class="inline-align-left">(.*?)</div>')
    res=re.sub(sbdiv,r'\1',res)
    return res
           

2.3. 拆分一個檔案内的多個章節

如果作者把幾個章節寫在同一個網頁内(比如作品介紹和第一話一起更新,或者作者某次更新時在一個網頁内連寫了好幾話内容),就會導緻這幾個章節之間無法像那些一個章節一個網頁的情況各自成為txt檔案。

處理過程為

  1. chap_infolist存儲章節數字,章節名。chap_contentlist存儲章節内容
  2. re.findall找到所有章節名(第X話 XXX),解析章節名,存入infolist
  3. 使用re.split切割章節名,如果開頭有作品介紹,則n個章節應該切出來n+1個段,存入chap_contentlist,其中第一個元素pop出來作為intro,單獨存儲為一個章節。
  4. 最後,把原本的modify檔案夾裡的這一章檔案删除,重新寫入分割後的章節
def split_chapter(filepath):
    # 如果是網頁上的第一話(實際是1-43話)分割一個網頁中的不同章節
    with open(filepath,"r",encoding="utf8") as f:
        fcontent=f.read()
    # 所有章節名行内容
    chaplines=chapter_pattern.findall(fcontent)
    chap_infolist=[]
    for chapline in chaplines:
        resnum=num_pattern.findall(chapline)[0]
        chap_number=get_chap_num(resnum[0])
        chap_name=resnum[1]
        chap_infolist.append({'chap_number':chap_number,'chap_name':chap_name})
    chap_contentlist=chapter_pattern.split(fcontent)
    # 在章節關鍵字出現前的文字作為intro儲存為000.txt
    if(chap_contentlist):
        pre_word=chap_contentlist.pop(0)
        with open(os.path.join(storage_folders["modify"],"{}.txt".format("0".zfill(3))),'w',encoding="utf8") as f:
            f.write('<h2>前言</h2>\n'+pre_word+"\n"+myword)
    if(len(chap_contentlist)!=len(chap_infolist)):
        print("章節分割出錯,檢測到{}個章節名,{}段".format(len(chap_infolist),len(chap_contentlist)))
        print([("比對情況",chap_infolist[i],chap_contentlist[i]) for i in range(min(len(chap_infolist),len(chap_contentlist)))])
        raise IndexError("Length are Not Equal")
    # 删除原本檔案,把每個章節單獨儲存到modify檔案夾
    os.remove(filepath)
    for i in range(len(chap_contentlist)):
        with open(os.path.join(storage_folders["modify"],"{}.txt".format(str(chap_infolist[i]['chap_number']).zfill(3))),'w',encoding="utf8") as f:
            # 此處為最終讀者能在每話開頭看到的标題文字
            f.write('<b>第{}話 {}</b>\n'.format(chap_infolist[i]['chap_number'],chap_infolist[i]['chap_name'])+chap_contentlist[i])
        print("分割章節 {}".format(chap_infolist[i]['chap_number']))
           

3. 制作epub

最關鍵的一步(但是不怎麼難 ),這是ebooklib的文檔

經過前面的處理,我們已經在modify檔案夾内有了每個章節的txt檔案,其内是html形式的文本内容,接下來使用ebooklib庫将其合成為epub

使用python從線上網頁制作epub(selenium,ebooklib)開始碎碎念

pip install ebooklib

安裝庫

3.1. 建立book,填入元資訊

建立book對象,元資訊就是書名,作者,這本書的唯一辨別符(自己定義)等書本資訊

這裡還定義了一個chaplist清單,裡面将會按順序存儲章節

book=epub.EpubBook()
# 書籍元資訊
# chaplist為排序過的所有的章節檔案EpubHtml,在最後會被統一for循環add進book和書脊和目錄裡,是以下文隻用考慮chaplist
chaplist=[]
book.set_identifier("自己定義一串數字")
book.set_title("書名")
book.set_language("cn")
book.add_author("作者名")
book.add_metadata("DC","description","作品描述")
           

3.2. 添加css

css可以定義特殊的樣式,由于我做的是個普通的epub是以沒有用到css

style = 'body { font-family: Times, Times New Roman, serif; }'
nav_css = epub.EpubItem(uid="style_nav",
                    file_name="style/nav.css",
                    media_type="text/css",
                    content=style)
book.add_item(nav_css)
           

3.3. 添加章節

周遊modify檔案夾,将檔案名排序後,按照順序将其添加進chaplist清單,其中的每個元素是EpubHTML對象,也就是Epub格式的每個章節檔案

# 添加章節
    for root, dirs, files in os.walk(storage_folders['modify']):
        files.sort()
        # 分析所有章節是否連續,這邊直接轉int比較長度了
        if len(files)>0:
            if files[0]=="000.txt":# 有intro
                if len(files)-1!=int(files[-1].split(".")[0]):
                    print("章節出現缺失,請檢查!")
            else:
                if len(files)!=int(files[-1].split(".")[0]):
                    print("章節出現缺失,請檢查!")
        else:
            raise Exception("無章節txt")
        # 按檔案添加 001.txt
        for file in files:
            fpath=os.path.join(root,file)
            with open(fpath,'r',encoding="utf8") as f:
                fcontent=f.read()
            chaplist.append(epub.EpubHtml(
                title="第{}話".format(file.split('.')[0]),
                file_name='{}.xhtml'.format(file.split('.')[0]),
                lang='cn',
                content=fcontent
                ))
        break
           

3.4. 添加目錄和書脊

前面說的chaplist裡存儲了每個章節對象,但這時它們隻是一個python清單,還沒有存儲到book對象裡,通過add_item就能把每個章節對象放入book這個壓縮包裡,在這之後每個章節才是可通路的

toc目錄,可以了解為閱讀軟體裡點選目錄按鈕之後會蹦出來的那個清單,這在epub裡面是一個檔案,定義了目錄的順序以及點選每個标題跳轉到哪個檔案。

spine書脊,可以了解為真實世界裡的書本的裝訂順序,如果不用目錄跳轉,單純從第一頁翻到最後一頁,我們需要書脊來知道這一章的最後一頁翻完之後是哪一章的第一頁。

add_item,目錄和書脊的不同:

add_item往book這個壓縮包裡放入了某個檔案使其可被通路,書脊定義了一頁一頁翻閱epub的呈現順序,目錄提供了一種快速閱覽書本章節和跳轉的頁面。

—如果某個HTML或CSS沒有被add_item進book,則無論如何在epub檔案裡都看不到它們

—如果book.toc中删除其他隻保留第一章,讀者仍然可以一頁一頁翻到最後一章。

—如果book.spine删除其他隻保留第一章,則讀者隻可閱覽第一章内容,目錄中的所有章節隻有第一章可以正常跳轉

#把所有chapters導入book裡,并添加目錄和書脊
for chap in chaplist:
    book.add_item(chap)
book.toc=chaplist
book.spine=chaplist
# 添加預設的 NCX and Nav file
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
           

3.5. 導出

# 儲存
epub.write_epub("./final.epub",book)
print("書本 {} 已導出".format(bookname))
           

Bibi-快速閱覽你的epub,不到1MB的本地靜态檔案,解壓zip後打開index.html即可

碎碎念

正文結束,下面是一些碎碎念

為啥會做這個:最近在水群的時候刷到了一個漫畫截圖,誰不愛看薄紗牛頭人呢 。

于是就迅速找到漫畫然後找到小說翻譯帖,看到小說翻譯帖的epub樓層年久失修(某度雲連結挂掉 )而且已經是三年前的東西了。看到翻譯君趁着漫畫出來繼續三年前的翻譯,于是想着拿python做一個不用怎麼手動操作的程式,這樣等翻譯完了直接跑一遍程式就行 (如果不多出來什麼新的奇奇怪怪的标簽的話 )。

為啥用selenium而不是request庫:selenium比較直覺,并且正如1.1節裡說的,爬目錄需要js運作時環境,是以個人感覺使用selenium比request+解析script要友善的多。當然1.2節的工作使用request是比較快速的,不過考慮到爬取的内容被另存為txt,也不需要多次爬取,我就繼續用selenium了,後續可以考慮這部分做成request然後多線程。

踩過的坑:

坑1

selenium使用execute_script在控制台運作腳本,需要在腳本裡return,才能在python裡獲得傳回值,

article_list=driver.execute_script("return window.__NUXT__.data[0].series.articles")

。否則傳回值是Undefined

坑2

ebooklib建立的HTML對象,一定要記得挂到到book對象裡,

book.add_item()

,然後書脊spine和目錄toc才能找到正确的HTML對象,否則會報錯

TypeError: Argument must be bytes or unicode, got 'NoneType'

坑3

re.sub中把某句子替換成r’\1’,則如果未比對到pattern的話,sub的傳回值會是原本的句子。是以在那之前要先search一下,如果存在,再sub。

禁止轉載原文,如有需要可以連結本文位址