如何用 KindleEar 推送無 RSS 的網站內容(中篇) – 書伴

本文詳細介紹了 KindleEar 訂閱腳本的工作原理,并以新聞網站 China Daily 為例,由淺入深詳細說明了如何為該網站編寫定制化的訂閱腳本,編寫好的訂閱腳本可將指定主題頁面的文章內容轉換成電子書。

目錄

[ 上篇 ]
一、KindleEar 的訂閱方式
二、KindleEar 的訂閱腳本
三、KindleEar 的調試環境
1、安裝 App Engine SDK
2、獲取 KindleEar 源代碼
3、在本地運行 KindleEar
[ 中篇 ]
一、新創建一個訂閱腳本
二、訂閱腳本的工作原理
三、從網站抽取文章 URL
四、分析 HTML 標簽結構
1、分析文章列表的 HTML 標簽結構
2、分析文章內容的 HTML 標簽結構
五、測試訂閱腳本的推送
[ 下篇 ]
一、文章列表的翻頁和限定條目
二、文章內容的翻頁和細節修改
三、上傳到 Google App Engine

在開始以下步驟之前,請確保你已經成功在本地運行了 KindleEar 程序,否則,請參考上一篇文章《如何用 KindleEar 推送無 RSS 的網站內容(上篇)》提供的步驟,搭建好運行 KindleEar 的調試環境。

一、新創建一個訂閱腳本

首先我們需要向 KindleEar 添加一個新的內置訂閱,也就是創建一個新的訂閱腳本。具體步驟為:打開代碼編輯器,新建一個空文檔,輸入(或拷貝)如下所示代碼,然后將其保存到 KindleEar 項目的 books 目錄中。注意,文件名的命名隨意,但必須是英文字符,后綴名必須是 .py,如 chinadaily.py。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from base import BaseFeedBook # 繼承基類BaseFeedBook

# 返回此腳本定義的類名
def getBook():
    return ChinaDaily

# 繼承基類BaseFeedBook
class ChinaDaily(BaseFeedBook):
    # 設定生成電子書的元數據
    title = u'China Daily' # 設定標題
    __author__ = u'China Daily' # 設定作者
    description = u'Chinadaily.com.cn is the largest English portal in China. ' # 設定簡介
    language = 'en' # 設定語言

    # 指定要提取的包含文章列表的主題頁面鏈接
    # 每個主題是包含主題名和主題頁面鏈接的元組
    feeds = [
        (u'National affairs', 'http://www.chinadaily.com.cn/china/governmentandpolicy'),
        (u'Society', 'http://www.chinadaily.com.cn/china/society'),
    ]

這段代碼做了 3 件事:導入了 base.py 中的基類 BaseFeedBook 以繼承其中的參數和功能函數;為最終生成的電子書設定了書名、作者、簡介、語言等元數據信息;指定了兩條包含文章列表的主題頁面 URL。

現在我們已經為 KindleEar 添加了一個新的內置訂閱。在網頁瀏覽器中訪問 http://localhost:8080 并登錄賬號,點擊導航上的“我的訂閱”進入訂閱管理頁面,就可以在“未訂閱”列表中看到新添加的訂閱。

如上圖所示,點擊新訂閱條目后面的【訂閱】按鈕將其添加到“已訂閱”列表。如下圖所示,點擊導航上的“高級設置”并進入“現在投遞”頁面,保持新訂閱處于勾選狀態,點擊【推送】按鈕即可手動執行新添加的這個訂閱腳本。只不過目前腳本還沒有實際功能,所以只會生成一條狀態為 nonews 的空日志。

在點擊【推送】按鈕執行訂閱腳本后,可以看到終端(或命令提示符)輸出了以下兩條信息:

INFO     2019-05-12 13:13:37,408 Worker.py:235] No new feeds.
INFO     2019-05-12 13:13:37,425 module.py:861] worker: "GET /worker?u=admin&id=4876402788663296 HTTP/1.1" 200 13

提示:測試腳本可能出現的錯誤提示都會顯示在終端(或命令提示符)上,我們需要根據這些信息來調試代碼。

其中的 URL 就是點擊【推送】按鈕后請求執行腳本的 URL,為避免在測試時頻繁點擊【推送】按鈕,建議直接在瀏覽器訪問和刷新這個 URL 代替點擊推送按鈕。注意,和訪問 KindleEar 的 8080 端口不同,這個 URL 要使用端口是 8081,其中的 ID 的值是腳本的唯一標識,以你自己命令行上出現的為準:

http://localhost:8081/worker?u=admin&id=6192449487634432

至此,我們就已經創建好可正常運行的訂閱腳本(雖然還抓取不到任何內容),并且還知道怎樣更方便地測試這個腳本。接下來讓我們來了解一下訂閱腳本的工作原理,以及用它抓取網站內容的思路。

二、訂閱腳本的工作原理

之前我們已經為新建的訂閱腳本從模塊 base.py 中導入了名為 BaseFeedBook 的基礎類,這樣新建的腳本就已經繼承了這個基礎類所提供的的各種參數和功能函數,只要我們根據實際情況在新腳本中對其做一些定制和改寫,就可以讓 KindleEar 按照我們的意愿抓取目標網站上的文章內容并轉換成電子書。

提示:其實在模塊 base.py 中還有 WebpageBook、BaseUrlBook 和 BaseComicBook 三個類,它們也繼承了 BaseFeedBook,只不過是針對不同內容類型做了定制。但是在本文中,為了更精細地控制內容的提取,只選用基礎類 BaseFeedBook。

在基礎類 BaseFeedBook 中,除了之前已定義(如書名等)以及之后將會定義的一些參數,還有一些可供調用或改寫的功能函數。其中最重要的函數是 Item(),正是它負責把抓取到的文章內容交給轉換模塊生成電子書的。而 Item() 函數抓取文章內容所需要的 URL 則是另一個功能函數 ParseFeedUrls() 提供的,此函數需要返回一個包含文章 URL 的列表。我們的主要工作就是改寫 ParseFeedUrls()函數,通過分析目標網站文章列表的 HTML 標簽結構,在該函數中編寫一些邏輯完成對文章 URL 的抽取。

ParseFeedUrls() 函數返回列表的結構如下所示。這個列表包含了一些元組,每個元組含有文章的“主題”、“標題”、“鏈接”和“摘要”。KindleEar 生成電子書時會根據這些主題來對文章進行分類。

[
    ('主題A','標題1', 'http://www.sample.com/post-1', None),
    ('主題A','標題2', 'http://www.sample.com/post-2', None),
    ('主題B','標題3', 'http://www.sample.com/post-3', None),
    ('主題B','標題4', 'http://www.sample.com/post-4', None),
    ('主題C','標題5', 'http://www.sample.com/post-5', None),
    ('主題C','標題6', 'http://www.sample.com/post-6', None),
    ...
    ('主題Z','標題n', 'http://www.sample.com/post-n', None),
]

提示:文章元組中的各項參數除了“摘要”之外都是必須的指定的,“摘要”即便不填充內容也要設置成 None 值,不然會出錯。本文的例子不設置摘要,因為一旦設置摘要,Item() 函數會直接把摘要作為文章內容,這顯然不是我們想要的。

Item() 函數在提取文章內容時,默認會自動調用函數 readability() 對文章內容進行清洗,以優化閱讀效果。此函數使用了第三方 Python 庫 readability-lxml,它對頁面內容的處理是全自動的,一般都可以獲得不錯的效果。但是為了更精準地處理頁面內容,本文選用的是另一個函數 readability_by_soup(),以便用 Beautiful Soup 手動處理頁面內容。注意,為了讓 Item() 默認調用 readability_by_soup() 函數,需要把在訂閱腳本中把參數 fulltext_by_readability 的值設為 False,這在后面還會提到。

另外,KindleEar 還給清洗內容的函數內分別安插了兩個函數:preprocess()soupprocessex() 。前者可在處理頁面內容的原始 HTML 代碼前對其做一些預處理(處理完需要返回處理的內容),而后者則可對處理完成的頁面內容的 Beautiful Soup 對象再做一些后處理(只負責處理過程無需返回內容)。

現在我們知道了 KindleEar 訂閱腳本抓取網站內容的大體運作流程,下面就讓我們來小試身手吧。

三、從網站抽取文章 URL

下面我來完善一下之前寫的代碼,增加一些必要的參數,并將函數 ParseFeedUrls() 加進去。下面是編寫好的完整代碼,每一行都有詳細注釋。后面還會解釋這些新添的的代碼都做了些什么。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from base import BaseFeedBook # 繼承基類BaseFeedBook
from lib.urlopener import URLOpener # 導入請求URL獲取頁面內容的模塊
from bs4 import BeautifulSoup # 導入BeautifulSoup處理模塊

# 返回此腳本定義的類名
def getBook():
    return ChinaDaily

# 繼承基類BaseFeedBook
class ChinaDaily(BaseFeedBook):
    # 設定生成電子書的元數據
    title = u'China Daily' # 設定標題
    __author__ = u'China Daily' # 設定作者
    description = u'Chinadaily.com.cn is the largest English portal in China. ' # 設定簡介
    language = 'en' # 設定語言

    coverfile = 'cv_chinadaily.jpg' # 設定封面圖片
    mastheadfile = 'mh_chinadaily.gif' # 設定標頭圖片

    # 指定要提取的包含文章列表的主題頁面鏈接
    # 每個主題是包含主題名和主題頁面鏈接的元組
    feeds = [
        (u'National affairs', 'http://www.chinadaily.com.cn/china/governmentandpolicy'),
        (u'Society', 'http://www.chinadaily.com.cn/china/society'),
    ]

    page_encoding = 'utf-8' # 設定待抓取頁面的頁面編碼
    fulltext_by_readability = False # 設定手動解析網頁

    # 設定內容頁需要保留的標簽
    keep_only_tags = [
        dict(name='span', class_='info_l'),
        dict(name='div', id='Content'),
    ]

    # 提取每個主題頁面下所有文章URL
    def ParseFeedUrls(self):
        urls = [] # 定義一個空的列表用來存放文章元組
        # 循環處理fees中兩個主題頁面
        for feed in self.feeds:
            # 分別獲取元組中主題的名稱和鏈接
            topic, url = feed[0], feed[1]
            # 請求主題鏈接并獲取相應內容
            opener = URLOpener(self.host, timeout=self.timeout)
            result = opener.open(url)
            # 如果請求成功,并且頁面內容不為空
            if result.status_code == 200 and result.content:
                # 將頁面內容轉換成BeatifulSoup對象
                soup = BeautifulSoup(result.content, 'lxml')
                # 找出當前頁面文章列表中所有文章條目
                items = soup.find_all(name='span', class_='tw3_01_2_t')
                # 循環處理每個文章條目
                for item in items:
                    title = item.a.string # 獲取文章標題
                    link = item.a.get('href') # 獲取文章鏈接
                    link = BaseFeedBook.urljoin(url, link) # 合成文章鏈接
                    urls.append((topic, title, link, None)) # 把文章元組加入列表
            # 如果請求失敗通知到日志輸出中
            else:
                self.log.warn('Fetch article failed(%s):%s' % 
                    (URLOpener.CodeMap(result.status_code), url))
        # 返回提取到的所有文章列表
        return urls

在之前創建的訂閱腳本基礎上,我們在代碼頭部新導入了 URLOpenerBeautifulSoup 兩個模塊,前者是用來請求頁面 URL 獲取響應內容的,后者則是用來解析響應內容以便提取文章內容數據的。

我們還添加了一些參數。其中 coverfile 用來設定電子書的“封面圖片”, mastheadfile 是用來設定期刊樣式電子書特有的“標頭圖片”的。制作這兩張圖片時,其尺寸和格式可參考 KindleEar 項目 images 目錄中已有的圖片,制作好的圖片也保存在這個目錄。注意,參數值需要圖片的文件名,不需要額外指定路徑,因為 KindleEar 默認圖片都在 images 目錄下。本例用的是如下所示兩張圖片,你也可另存使用。

▲ 封面圖片:cv_chinadaily.jpg

▲ 標頭圖片: mh_chinadaily.gif

然后就是 page_encodingfulltext_by_readability 兩個參數,前者的作用是設定待抓取頁面的編碼類型。一般現代的 WEB 頁面使用的都是“UTF-8”,但也有一些網站使用了其它編碼,具體可在頁面源代碼中查找 <meta> 標簽中 charset 的值。后者是前面提到過,是開啟用 Beautiful Soup 手動清洗內容的。

還有一個 keep_only_tags 參數,它告訴清洗內容的函數,需要保留文章頁面中的哪些內容元素,從而排除掉其它不需要的元素。該參數的值是一個字典容器 dict(),里面一般可設定兩種類型的鍵值,一個是元素的標簽名,即代碼中的 name,另一個是前者的選擇器,即代碼中的 class_(或 id)。這種參數其實就是供 Beautiful Soup 的 find_all()find() 方法解析內容用的(詳細介紹參考其文檔說明)。

最后添加了這個新建訂閱腳本最核心的函數 ParseFeedUrls(),下面我們來詳細解釋一下它在做什么。

四、分析 HTML 標簽結構

在解釋函數 ParseFeedUrls() 之前,先讓我們來分析一下“文章列表”和“文章內容”的 HTML 標簽結構。

1、分析文章列表的 HTML 標簽結構

首先是文章列表的標簽結構。用 Chrome 訪問 China Daily 的 Society 板塊,可以看到如下圖所示有規律的文章列表。注意,上方的幾個方塊只是置頂文章,其實也是從列表中挑選出來的,所以不用管它。

▲ 文章列表顯示效果

在頁面上右鍵并點擊菜單上的“檢查”調出開發者工具,即可輕松查看文章列表的代碼結構。

▲ 文章列表標簽結構

在這個代碼結構中可以看出我們所需要文章數據存放在重復出現的 span.tw3_01_2_t 標簽中,文章標題在其子標簽 a 中,文章鏈接是這個 a 標簽的 href 屬性值,文章日期在子標簽 b 標簽中。如下圖所示:

▲ 文章列表結構說明

2、分析文章內容的 HTML 標簽結構

和查看文章列表的標簽結構一樣,我們也可以用同樣的方式在文章內容頁面找超出我們所需要的數據:文章信息存放在類名為 .info_lspan 標簽中,文章內容存放在 idContentdiv 標簽中。

▲ 文章內容顯示效果

▲ 文章內容標簽結構

▲ 文章內容結構說明

在分析示例網站 China Daily 網站時,你可能已經發現,它所有主題頁面的文章列表和文章內容的標簽結構都是相同的,這也是我們能在 feeds 列表中添加多個主題頁面鏈接,并對其進行統一處理的原因。

搞清文章列表和文章內容的標簽結構后就可以輕松解析它們了?;剡^頭看函數 ParseFeedUrls() 做了些什么。它先循環處理 feeds 列表中的每個主題頁面的 URL,然后用新導入的函數 URLOpener() 請求當前處理的 URL,成功獲取到響應后,把響應的 HTML 代碼轉換成 Beautiful Soup 對象準備解析。

接著用 find_all() 方法從 Beautiful Soup 對象中找到所有文章條目,并循環處理這些條目,依次把每篇文章的“標題”和“鏈接”都制成元組,然后再把制成的元組追加到之前預定義好的 urls 列表中。

所有循環運行完畢即可得到一個完整的含有所有文章信息的 urls 列表,最后用關鍵字 return 將其返回供函數 Item() 使用。至此函數 ParseFeedUrls() 就完成了它的工作,我們的腳本也能正常使用了。

五、測試訂閱腳本的推送

最后我們需要測試一下這個訂閱腳本的推送。測試前,你需要先準備好一個可用的 SMTP 服務器,這里以 163 郵箱為例。準備好之后,在終端(或命令提示符上)按 Ctrl + C 退出 Google App Engine(如果還在運行的話)。然后在原來的基礎上,增加下面這些參數,中文部分換成你自己的郵箱賬戶信息:

dev_appserver.py 
--smtp_host=smtp.163.com 
--smtp_port=25 
--smtp_user=郵箱用戶名@163.com 
--smtp_password=郵箱授權碼 
--smtp_allow_tls=False 
./app.yaml ./module-worker.yaml

注意,Windows 的命令提示符不支持用反斜杠對命令進行換行,所以需要把命令寫進同一行:

dev_appserver.py --smtp_host=smtp.163.com --smtp_port=25 --smtp_user=郵箱用戶名@163.com --smtp_password=郵箱授權碼 --smtp_allow_tls=False ./app.yaml ./module-worker.yaml

還要修改 KindleEar 項目里的 config.py 文件,將其中的 SRC_EMAIL 參數值暫時改成上面所用的郵箱。

現在,進入 KindleEar 的“設置”頁面,把“Kindle郵箱”設置成你的 Kindle 郵箱或任意普通郵箱(注意把上面所用的郵箱加入認可列表),然后刷新測試鏈接(或進入 KindleEar 的“高級設置”頁面,點擊“現在投遞”上的【推送】按鈕),就可以運行訂閱腳本了。不出意外的話,你會在終端看到如下的輸出:

INFO     2019-05-14 15:15:31,133 resources.py:49] Serializing resources...
INFO     2019-05-14 15:15:31,144 mobioutput.py:149] Creating MOBI 6 output
INFO     2019-05-14 15:15:31,932 manglecase.py:34] Applying case-transforming CSS...
INFO     2019-05-14 15:15:31,944 parse_utils.py:302] Forcing toc.html into XHTML namespace
INFO     2019-05-14 15:15:33,267 mail_stub.py:170] MailService.Send
  From: [email protected]
  To: [email protected]
  Subject: KindleEar 2019-05-14_23-15
  Body:
    Content-type: text/plain
    Data length: 22
  Attachment:
    File name: China Daily(2019-05-14_23-15).mobi
    Data length: 110878
INFO     2019-05-14 15:15:34,306 module.py:861] worker: "GET /worker?u=admin&id=6192449487634432 HTTP/1.1" 200 40

稍后,你填寫的 Kindle 郵箱(或普通郵箱)就能收到你編寫的腳本所生成的電子書了。如下圖所示:

▲ 訂閱腳本推送效果

不過,到目前為止,我們生成的電子書還不夠完美。比如,文章內容中含有重復的網站名,文章數量總是 20 篇,沒有按照時間進行過濾,列表翻頁沒有處理,文章內容有分頁的情況也沒有處理……

本來書伴預計兩篇文章就可以把本文寫完,但寫到這兒發現長度超出了預期,所以只能把本文分成上、中、下三篇了。本篇已讓 KindleEar 訂閱腳本正常運行了,下篇我們再來處理那些不完美的細節。

如果你對本教程有什么疑問,或者發現內容存在謬誤或不詳盡之處,歡迎留言。

你可繼續閱讀:《如何用 KindleEar 推送無 RSS 的網站內容(下篇)

未經允許不得轉載:螞蟻搬書 » 如何用 KindleEar 推送無 RSS 的網站內容(中篇) – 書伴
微信公眾號:螞蟻搬書
關注我們,分享kindle電子書資源
12000人已關注
分享到:
贊(0) 打賞

評論搶沙發

  • 昵稱 (必填)
  • 郵箱 (必填)
  • 網址

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

微信掃一掃打賞

捕鸟达人修改金币