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

在本文的“中篇”,我們已經編寫好了一個可以正常工作的 KindleEar 訂閱腳本,但是用它生成的電子書存在著很多問題,比如沒有設定抓取文章的時間范圍,也沒有處理文章列表和文章內容的翻頁,文章標題還帶有冗余信息。本文將繼續完善訂閱腳本,對這些細節進行處理,讓生成的電子書更加完美。

目錄

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

以下內容分為三部分:首先是文章列表的翻頁和條目限制的處理,然后文章內容頁面的翻頁以及文章標題的處理,最后介紹了本地上傳和 Google Cloud 云端 Shell 上傳兩種上傳 KindleEar 項目的方式。

一、文章列表的翻頁和限定條目

通常我們并不需要抓取網站上的所有文章條目,所以要從“文章數量”或“時間范圍”這兩種緯度限定文章條目。設定條件時,可選擇其一,也可選取兩者不同范圍的交集。本例采用的是后者:先設定抓取 40 篇文章,再在此基礎上保留 1 天之內的文章。由于設定值超過了單頁文章數量,還需要處理列表翻頁。

以下代碼根據以上需求做了修改。從中可以看到,在之前代碼的基礎上,新導入了一個處理時間的模塊,并新增了 2 個參數和 3 個自定義函數。下面我們來詳細解釋一下新增的這些代碼都做了些什么。

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

from datetime import datetime # 導入時間處理模塊datetime
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'),
    ]

    max_articles_per_feed = 40 # 設定每個主題下要最多可抓取的文章數量
    oldest_article = 1 # 設定文章的時間范圍。小于等于365則單位為天,否則單位為秒,0為不限制。

    # 提取每個主題頁面下所有文章URL
    def ParseFeedUrls(self):
        urls = [] # 定義一個空的列表用來存放文章元組
        # 循環處理fees中兩個主題頁面
        for feed in self.feeds:
            # 分別獲取元組中主題的名稱和鏈接
            topic, url = feed[0], feed[1]
            # 把抽取每個主題頁面文章鏈接的任務交給自定義函數ParsePageContent()
            self.ParsePageContent(topic, url, urls, count=0)
        print urls
        exit(0)
        # 返回提取到的所有文章列表
        return urls

    # 該自定義函數負責單個主題下所有文章鏈接的抽取,如有翻頁則繼續處理下一頁
    def ParsePageContent(self, topic, url, urls, count):
        # 請求主題頁面鏈接并獲取其內容
        result = self.GetResponseContent(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) # 合成文章鏈接
                count += 1 # 統計當前已處理的文章條目
                # 如果處理的文章條目超過了設定數量則中止抽取
                if count > self.max_articles_per_feed:
                    break
                # 如果文章發布日期超出了設定范圍則忽略不處理
                if self.OutTimeRange(item):
                    continue
                # 將符合設定文章數量和時間范圍的文章信息作為元組加入列表
                urls.append((topic, title, link, None))

            # 如果主題頁面有下一頁,且已處理的文章條目未超過設定數量,則繼續抓取下一頁
            next = soup.find(name='a', string='Next')
            if next and count  0 and updated:
            # 將文章發布時間字符串轉換成日期對象
            updated = datetime.strptime(updated, '%Y-%m-%d %H:%M')
            delta = current - updated # 當前時間減去文章發布時間
            # 將設定的時間范圍轉換成秒,小于等于365則單位為天,否則則單位為秒
            if self.oldest_article > 365:
                threshold = self.oldest_article # 以秒為單位的直接使用秒
            else:
                threshold = 86400 * self.oldest_article # 以天為單位的轉換為秒
            # 如果文章發布時間超出設定時間范圍返回True
            if (threshold 

首先我們從 Python 標準庫中導入了一個時間處理模塊 datetime,這在驗證文章時間范圍時需要用到。

然后新增了 max_articles_per_feedoldest_article 兩個參數,前者用來設定從每個主題抓取的文章數量,后者則是用來設定要保留多久時間之內更新的文章。這兩個參數的值都是數字。設置文章的時間范圍時需要注意:如果設定的數值小于等于 365 單位是天,否則單位為秒,0 表示不限制時間范圍。

最后添加了三個自定義函數,分別是 ParsePageContent()、OutTimeRange()GetResponseContent()。其中函數 ParsePageContent() 的邏輯是從之前的 ParseFeedUrls() 函數中拆出來再被其調用的,為的是遞歸處理列表翻頁,里面還新增了對文章數量和發布時間的判斷,以便按照設定條件過濾文章。

根據翻頁鏈接的 HTML 標簽結構,代碼中通過查找含有 Next 字符的 a 標簽來確定當前列表是否有下一頁,如果有的話就繼續調用 ParsePageContent() 提取下一頁內容,直到達到設定的抓取數量為止。

▲ 翻頁鏈接顯示效果和標簽結構

▲ 翻頁鏈接結構說明

其它兩個函數的功能比較簡單:OutTimeRange() 用來判斷傳入文章的發布時間是否超出了設定范圍,然后把結果返回給調用函數使用;GetResponseContent() 用來請求傳入的頁面鏈接,然后把響應內容返回給調用它的函數,這主要是為了方便之后復用,因為下面處理文章內容翻頁時也需要請求頁面內容。

二、文章內容的翻頁和細節修改

處理完文章列表的翻頁,我們再來處理內容的一些細節:內容頁的翻頁和移除內容標題上的冗余信息。

下面是完善后的代碼,也是本文最終完成的代碼。新增的代碼主要調用了基類中的 processtitle()preprocess() 兩個函數對文章內容做預處理,前者用來處理文章標題,后者用來處理文章內容。

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

from datetime import datetime # 導入時間處理模塊datetime
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'),
    ]

    max_articles_per_feed = 40 # 設定每個主題下要最多可抓取的文章數量
    oldest_article = 1 # 設定文章的時間范圍。小于等于365則單位為天,否則單位為秒,0為不限制。

    # 提取每個主題頁面下所有文章URL
    def ParseFeedUrls(self):
        urls = [] # 定義一個空的列表用來存放文章元組
        # 循環處理fees中兩個主題頁面
        for feed in self.feeds:
            # 分別獲取元組中主題的名稱和鏈接
            topic, url = feed[0], feed[1]
            # 把抽取每個主題頁面文章鏈接的任務交給自定義函數ParsePageContent()
            self.ParsePageContent(topic, url, urls, count=0)
        # print urls
        # exit(0)
        # 返回提取到的所有文章列表
        return urls

    # 該自定義函數負責單個主題下所有文章鏈接的抽取,如有翻頁則繼續處理下一頁
    def ParsePageContent(self, topic, url, urls, count):
        # 請求主題頁面鏈接并獲取其內容
        result = self.GetResponseContent(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) # 合成文章鏈接
                count += 1 # 統計當前已處理的文章條目
                # 如果處理的文章條目超過了設定數量則中止抽取
                if count > self.max_articles_per_feed:
                    break
                # 如果文章發布日期超出了設定范圍則忽略不處理
                if self.OutTimeRange(item):
                    continue
                # 將符合設定文章數量和時間范圍的文章信息作為元組加入列表
                urls.append((topic, title, link, None))

            # 如果主題頁面有下一頁,且已處理的文章條目未超過設定數量,則繼續抓取下一頁
            next = soup.find(name='a', string='Next')
            if next and count  0 and updated:
            # 將文章發布時間字符串轉換成日期對象
            updated = datetime.strptime(updated, '%Y-%m-%d %H:%M')
            delta = current - updated # 當前時間減去文章發布時間
            # 將設定的時間范圍轉換成秒,小于等于365則單位為天,否則則單位為秒
            if self.oldest_article > 365:
                threshold = self.oldest_article # 以秒為單位的直接使用秒
            else:
                threshold = 86400 * self.oldest_article # 以天為單位的轉換為秒
            # 如果文章發布時間超出設定時間范圍返回True
            if (threshold 

函數 Items() 抓取文章內容時,是從頁面 <title> 標簽中獲取文章標題的,但是 China Daily 的文章標題都附加了一個重復的尾巴,類似 XXXXX - Chinadaily.com.cn,所以我們需要調用一個現成的預處理函數 processtitle() 把這個尾巴刪掉,在函數中我們只需要簡單地用 replace() 函數將其替換為空即可。

在函數 readability_by_soup() 清洗頁面內容前,我們可以調用另一個現成的預處理函數 preprocess() 對原始的頁面內容做些處理,在這里我們就是通過調用此函數來處理含內容頁面翻頁的。在本例中,含翻頁的文章頁面雖然不常見,但確實存在,比如“China, Thailand conclude joint naval training”這篇文章,四幅圖片被放進了四頁,如果不對其做相應處理,推送后就只能看到這篇文章的第一張圖片。

自定義函數 SplitJointPagination() 用來遞歸處理文章頁面的翻頁。當此函數被 preprocess() 調用時,會查找傳入的頁面是否有下一頁,如果有就讀取下一頁內容,直至把所有翻頁內容拼接在一起返回。

至此就完成了為 China Daily 網站定制的訂閱腳本。因為該網站所有板塊的 HTML 標簽結構幾乎是相同的,所以你可以在 feeds 參數中增加你喜歡的其它主題頁面鏈接。不過要注意,Google App Engine 對資源的使用有限制,而且 Gmail 發信對附件的推送也有 20MB 的限制,不建議一次性抓取過多內容。

三、上傳到 Google App Engine

訂閱腳本編寫完成之后就可以上傳到正式的 Google App Engine(GAE)環境上使用了。如果你想要在本地上傳,可參照《KindleEar 搭建教程:推送 RSS 訂閱到 Kindle》這篇文章提供的“手動上傳”步驟操作。當然也可以采用另一種方式,即先把修改的源碼 Push 到 Github,再用 GAE 的云端 Shell 上傳。

上傳 KindleEar 源碼前,建議檢查項目中 app.yaml 和 modul-worker.yaml 兩個文件的 application 參數,以及 config.py 中的 SRC_EMAILDOMAIN 參數,確保都已經改成了你自己賬號信息。在本地測試推送時,本文示例曾修改過文 config.py 中的 SRC_EMAIL 這個參數,上傳前要改回你的 Gmail 郵箱。

1、本地上傳注意事項

如果你身在墻內,本地上傳前需要在 Google App Engine SDK 的文件 appcfg.py 中添加可用代理。

在 Windows 系統中,可以在 Google App Engin SDK 安裝目錄找到文件 appcfg.py。默認位置如下:

C:Program Files (x86)Googlegoogle_appengineappcfg.py

在 macOS 系統中,需要進入“應用程序”目錄,找到并右鍵點擊 GoogleAppEngineLauncher 應用,在彈出的菜單中點擊“顯示包內內容”,即可在文件列表中找到 appcfg.py 文件。默認位置如下:

Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/appcfg.py

用代碼編輯器打開 appcfg.py 文件,并找到下面這行代碼,代理相關的代碼要添加在它的前面。

"""Convenience wrapper for starting an appengine tool."""

下面提供了 SOCKS5 和 HTTP 兩種協議的網絡代理添加方法,可根據你所用的代理工具選擇:

import socks
import socket
socket.socket = socks.socksocket
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", 8788)

▲ 添加 SOCKS5 代理

import os
proxy = 'http://127.0.0.1:8787'
os.environ['http_proxy'] = proxy
os.environ['https_proxy'] = proxy

▲ 添加 HTTP 代理

提示:使用 HTTP 代理時,在 macOS 和 Linux 系統中也可以在終端直接用 export 命令配置環境變量 http[s]_proxy。

配置好代理后,在終端(或命令提示符)上用 cd 命令定位到 KindleEar 項目,執行如下命令上傳:

appcfg.py update ./app.yaml ./module-worker.yaml
appcfg.py update ./

注意,第一次上傳會出現一個驗證鏈接,如果是上傳源碼和訪問鏈接的是同一個電腦,直接訪問鏈接驗證即可。否則,需要添加參數 --noauth_local_webserver,打開鏈接獲取并輸入驗證碼進行驗證。

2、云端 Shell 上傳注意事項

通過云端 Shell 上傳,也需要你的瀏覽器能夠通過可用代理上網,以便正常訪問 Google Cloud 服務。

從 Github 拉取源碼上傳過程比較簡單。確保你修改的 KindleEar 源代碼已成功 Push 到 Github,然后進入 Google App Engine 控制臺,點擊右上角的“Shell 圖標”激活云端 Shell,依次執行如下命令上傳:

git clone https://github.com/YOURNANME/KindleEar.git
cd KindleEar
appcfg.py update ./app.yaml ./module-worker.yaml
appcfg.py update ./

上傳成功就可以登錄 KindleEar 添加和推送新訂閱了。如果新添加的訂閱沒出現,就表示沒上傳成功,建議仔細檢查自己的操作步驟(可根據終端或命令提示符上出現的錯誤提示排查上傳失敗的原因)。

▲ 訂閱腳本最終推送效果

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

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

評論搶沙發

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

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

支付寶掃一掃打賞

微信掃一掃打賞

捕鸟达人修改金币