如何用 KindleEar 推送无 RSS 的网站内容(上篇)

之前书伴曾介绍过利用 Calibre 抓取网站内容制成电子书的方法,可以很方便地生成既美观又实用的期刊样式电子书。Calibre 功能的强大毋庸置疑,不过在实际使用时却有一个明显的不便之处:当你想要定时推送某个内容源时,就必须让运行 Calibre 的电脑一直保持开机状态。因此,很多小伙伴选择使用有着类似功能,并且能免费托管在 Google App Engine 服务器上的开源程序 KindleEar 来解决这个问题。

KindleEar 虽然支持直接订阅 RSS,但可惜很多 RSS 供稿可用性并不高,内置的订阅又无法满足个性化的需求,在这种情况下,就有必要学会自己编写可精准获取目标网站内容的订阅脚本。这里所说的“订阅脚本”也可以理解成为 KindleEar 添加内置订阅,内置的订阅其实也是由一个个定制的订阅脚本组成。

写这篇文章的动机是网友 Jone 发给书伴的一封长信,信中详细描述了自己想要解决的问题:如何在没有编程基础的前提下编写 KindleEar 订阅脚本,并能方便地重新部署到 Google App Engine 上。所以书伴花了几天时间研究了下 KindleEar 订阅脚本,并将经验分享给需要的小伙伴。为了顾及没有任何编程经验的小伙伴,本文会尽可能以普通用户的视角撰写,对涉及代码的部分尽可能做到形象化的说明。

目录

[ 上篇 ]
一、KindleEar 的订阅方式
二、KindleEar 的订阅脚本
三、KindleEar 的调试环境
1、安装 Python 2.7
2、安装 Google Cloud SDK
3、获取 KindleEar 源代码
4、在本地运行 KindleEar
[ 中篇 ]
一、新创建一个订阅脚本
二、订阅脚本的工作原理
三、从网站抽取文章 URL
四、分析 HTML 标签结构
1、分析文章列表的 HTML 标签结构
2、分析文章内容的 HTML 标签结构
五、测试订阅脚本的推送
[ 下篇 ]
一、文章列表的翻页和限定条目
二、文章内容的翻页和细节修改
三、上传到 Google App Engine

由于编写 KindleEar 订阅脚本牵涉到测试环境的配置,导致篇幅较长,所以本文分成了上、中、下三篇。上篇主要是订阅脚本的相关介绍和测试环境的配置步骤,中下篇则是编写订阅脚本的具体步骤。

一、KindleEar 的订阅方式

KindleEar 和 Calibre 一样,支持通过“RSS”或“网页(HTML)”两种方式抓取目标网站上的内容。

KindleEar 对 RSS 和 HTML 有着不同的处理方式。当目标站点提供 RSS 时,它就会用通用的 RSS 处理模块来提取供稿内容生成电子书,也就是前面提到的“自定义 RSS”。直接用 RSS 自然是最方便的,但现实世界的情况总比理想中的复杂,有很多种原因导致我们无法顺利获取 RSS,比如有些网站根本就不提供 RSS,或者提供了 RSS 却只有摘要信息,甚至提供的 RSS 存在格式上的错误无法正常读取等。

RSS 本质上只是一种简单的数据格式,其结构有着相对严格和固定的规范,所以只需要一个通用处理模块就可以应付几乎所有站点的 RSS 供稿。而 HTML 页面就没这么省心了,可以说不同网站的 HTML 标签结构存在着天壤之别,所以当目标站点不提供 RSS 时,就只能为其编写高度定制化的订阅脚本。

说点题外话:可能很多小伙伴会疑惑,为什么很多网站都不提供 RSS 呢?RSS 生来就是为了方便用户追踪网站更新的,从用户角度来看是相当方便——不用访问网站就能获取到网站内容更新,但是这却不可避免地影响到了内容提供者的商业利益。虽然十多年前很多人也为 RSS 做过一些商业化的尝试,但终以失败告终。随着 2013 年 Google 关闭 Google Reader,算是宣告了 RSS 大时代的终结。现在除了一些博客以及尚有情怀的网站外,大都不再提供 RSS 供稿了,即便提供也只是放点摘要信息,最终的目的还是把用户引导到自己的网站上。RSS 并没有消亡,只是因为它给了用户太多自由而不太被商业容忍。

二、KindleEar 的订阅脚本

在《Calibre 使用教程之抓取网站页面制成电子书》这篇文章中,书伴详细介绍了如何通过编写 Recipe 脚本的方式让 Calibre 抓取指定网站的内容,KindleEar 也提供了类似的功能。不过需要注意的是,虽然 KindleEar 的 MOBI 转换模块提取自 Calibre,但是订阅脚本却与 Calibre 的 Recipe 脚本并不通用,这是因为 KindleEar 并没有直接移植 Calibre 的 Recipe 处理模块,而是将其作为参考重新写了一个处理模块,这导致包括脚本后缀名(KindleEar 是 .py,Calibre 是 .recipe)、相关功能的实现等很多方面都有所不同。因此,你必须遵循 KindleEar 提供的相关功能函数为 KindleEar 编写专用的订阅脚本。

KindleEar 内置的抓取脚都存放在其项目目录下的 books 目录中,脚本的文件名均以英文命名并以 .py 为后缀。每个脚本都继承同目录下名为 base.py 的基类,该基类已对很多种订阅方式做了定义,比如 RSS、HTML 页面、漫画等。我们所创建的订阅脚本就是通过继承这个基类,再根据实际情况改写、定制其中的一些参数和函数,从而实现对目标网站内容的精准抓取。KindleEar 的作者在 base.py 做了大量注释,如果你有一定的编程经验,完全可以根据这些注释说明来理解其中的参数和函数都是如何工作的。

三、KindleEar 的调试环境

由于 KindleEar 的运行依赖于 Google App Engine 环境,无法像用 Calibre 测试 Recipe 脚本那样直接在本地运行,所以为了方便测试编写的 KindleEar 订阅脚本,我们需要在本地搭建可以为 KindleEar 虚拟运行环境的 Google Cloud SDK(Windows 还需要安装 Python 环境和相关的 Python 库)。

不要害怕任何技术性字眼,按照步骤一步步做一般不会有问题。注意不要忽略任何一段文字。

1、安装 Python 2.7

KindleEar 是 Python 程序,所以本地调试时会依赖 Python 环境。macOS 系统和 Linux 系统都预装了 Python,而对于 Windows 系统,如果没有安装 Python,就需要手动安装 2.7 版本的 Python:

* 提示:macOS 系统虽然内置了 Python 环境,但是并不推荐直接使用它,而是推荐使用 Homebrew 安装独立的 Python 环境。Homebrew 是一款包管理器,支持 macOS(或 Linux)系统,它能安装和管理独立于原生系统环境的工具包,可有效避免对原生系统环境产生影响,对于没有包管理器的 macOS 系统(或 Linux 系统中的非 root 用户)来说非常方便。

此外,KindleEar 的运行还依赖一些第三方 Python 库,这些库需要在命令行中用 pip 命令安装。

注意,本文之后的内容经常会用到命令行,所以应记住,当文中说到输入命令时,你需要打开“终端”(Windows 系统则打开“命令提示符”),把相关命令输入(或拷贝)进去,按回车执行。

macOSLinux 用户可直接通过执行下面的命令安装这些第三方 Python 库:

pip install lxml pillow jinja2 pycrypto

* 提示:在 macOS 或 Linux 系统中,如果用的是原生系统的 Python 环境,并且是非 root 用户,则需要附加 sudo 命令。

Windows 用户需要先下载安装微软的 Microsoft Visual C++ Compiler for Python 2.7,因为安装第三方 Python 库的对其有依赖。安装完成后,通过以下命令安装 KindleEar 依赖的第三方 Python 库:

C:\Python\Scripts\pip install lxml pillow jinja2 pycrypto

* 提示:上面这条命令假设你的 Python 是默认安装在 C 盘的,如果指定了其它磁盘,请自行更改路径。

2、安装 Google Cloud SDK

Google Cloud SDK 的安装请参考《KindleEar 搭建教程:推送 RSS 订阅到 Kindle》这篇文章的第二部分“二、上传应用”中的“方法二:手动上传”中的第 1、2 小节提供的详细步骤。

如果你是使用 Homebrew 安装 Google Cloud SDK 的,请注意,默认情况下 dev_appserver.py 这个命令可能不会被添加到环境变量 $PATH 中,所以无法直接在“终端”或“命令提示符”中运行,因此,如果你也遇到了这个问题,可执行以下命令手动将这个命令的软链接添加到可执行程序目录中。

ln -s /usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/dev_appserver.py /usr/local/bin/dev_appserver.py

3、获取 KindleEar 源代码

首先需要获取一份 KindleEar 源代码到本地。你可以通过下面的链接下载 ZIP 包解压缩备用:

如果你的电脑已经安装了 Git 工具,也可以通过 Git 的 Clone 命令将源代码拉取到本地:

git clone https://github.com/cdhigh/KindleEar.git

为了之后上传方便,建议去 GitHub 注册一个账户,然后把 KindleEar 项目 Fork 一份到自己的账户下,这样,在添加好编写的订阅脚本后,可以先将源代码 Push 到你 Fork 的项目中,再按照 KindleEar 搭建教程中的“自动上传”方式利用 Google App Engine 的云端 Shell 重新部署你 Fork 的 KindleEar 项目。

4、在本地运行 KindleEar

接下来就是让 KindleEar 在本机运行起来了。注意,在这里我们不使用带界面的 Google App Engine Launcher,而只使用它附带的命令行工具。默认情况下,Windows 系统可以直接在命令提示符中使用这些命令,而 macOS 系统需要先打开界面版的 Google App Engine Launcher,点击软件的菜单中的“Make Symlinks…”创建命令软链接才能使用命令。Linux 系统需要添加 PATH 变量才能使用命令。

打开终端(Windows 系统打开命令提示符)并定位到 KindleEar 的项目目录。假设项目在系统桌面上。

macOS 系统需要输入类似以下命令定位到 KindleEar 项目目录(注意替换 YOURNAME):

cd /Users/YOURNAME/Desktop/KindleEar

Windows 系统则需要输入类似以下命令定位到 KindleEar 项目目录(注意替换 YOURNAME):

cd C:\Users\YOURNAME\Desktop\KindleEar

定位到 KindleEar 项目目录后,输入以下命令让 KindleEar 运行起来(注意有两个空格):

dev_appserver.py ./app.yaml ./module-worker.yaml

在 Windows 系统下,有小伙伴反馈了两个问题。一个是输入 dev_appserver.py 无法成功执行命令,而是被记事本打开了,这是因为你强制设定了 .py 文件的打开方式,需要将其打开方式恢复成 python:随便找一个 .py 为后缀的文件,通过右键属性更改该文件的默认打开方式为你所安装的 python.exe。

另一个是在 Windows 7 系统下,输入 dev_appserver.py 提示 error: too few arguments 错误,这是由于某种原因 Python 无法读取命令后的参数导致的,这需要通过修改注册表来修复。在命令提示符中输入 regedit 打开注册表,依次展开 HKEY_CLASSES_ROOT\Applications\python.exe\shell\open\command,如果发现 command 的值不是 "C:\Python27\python.exe" "%1" %* 就改成这个(通常是缺少了 %*)。

当你看到终端(或命令提示符)上出现如下所示的输出,就说明 KindleEar 已经在本机正常运行了:

INFO     2019-05-11 13:51:41,358 sdk_update_checker.py:231] Checking for updates to the SDK.
INFO     2019-05-11 13:51:44,383 sdk_update_checker.py:247] Update check failed: 
INFO     2019-05-11 13:51:44,613 api_server.py:275] Starting API server at: http://localhost:49342
INFO     2019-05-11 13:51:44,625 dispatcher.py:256] Starting module "default" running at: http://localhost:8080
INFO     2019-05-11 13:51:44,667 dispatcher.py:256] Starting module "worker" running at: http://localhost:8081
INFO     2019-05-11 13:51:44,672 admin_server.py:150] Starting admin server at: http://localhost:8000
INFO     2019-05-11 13:51:46,928 instance.py:294] Instance PID: 37115

打开浏览器(推荐用 Chrome),输入 http://localhost:8080 即可访问运行在本机上的 KindleEar 程序,输入默认的用户名和密码 admin 即可登入控制界面。至此,KindleEar 的调试环境便准备好了。

本文的下篇将会以 China Daily 为例,由浅入深详细说明如何编写 KindleEar 的订阅脚本。编写好的脚本可抓取指定板块下指定数量和日期的新闻条目,并将其整合到同一本电子书中,其中还包括对内容页、分页等细节的处理。最后,把测试成功的订阅脚本上传部署到 Google App Engine 的生产环境上。

▲ China Daily 网站抓取效果

为方便编写代码,建议先备好一款代码编辑器,推荐 Sublime TextVisual Studio Code。如果你对 KindleEar 订阅脚本有什么疑问,或者发现本教程存在的谬误或不详尽之处,欢迎留言。

你可继续阅读:《如何用 KindleEar 推送无 RSS 的网站内容(中篇)

有帮助,分享给其他小伙伴:

发表评论

标注为 * 的是必填项。您填写的邮箱地址将会被保密。如果是在本站首次留言,审核后才能显示。
若提问,请务必描述清楚该问题的前因后果,提供尽可能多的对分析该问题有帮助的线索。

小伙伴们发表了 71 条评论

  1. 您好
    我在打开http://localhost:8080时控制台出现以下内容

    E:\KindleEar-master\KindleEar-master>dev_appserver.py ./app.yaml ./module-worker.yaml
    INFO     2020-10-18 13:47:35,727 devappserver2.py:289] Skipping SDK update check.
    WARNING  2020-10-18 13:47:36,453 simple_search_stub.py:1198] Could not read search indexes from c:\users\admini~1\appdata\local\temp\appengine.kindleear\search_indexes
    INFO     2020-10-18 13:47:36,457 api_server.py:282] Starting API server at: http://localhost:2134
    INFO     2020-10-18 13:47:36,470 dispatcher.py:267] Starting module "default" running at: http://localhost:8080
    INFO     2020-10-18 13:47:36,502 dispatcher.py:267] Starting module "worker" running at: http://localhost:8081
    INFO     2020-10-18 13:47:36,505 admin_server.py:150] Starting admin server at: http://localhost:8000
    INFO     2020-10-18 13:47:38,992 instance.py:294] Instance PID: 10924
    WARNING  2020-10-18 05:47:51,750 sandbox.py:1105] The module msvcrt is whitelisted for local dev only. If your application relies on msvcrt, it is likely that it will not function properly in production.
    WARNING  2020-10-18 05:47:51,813 sandbox.py:1105] The module _winreg is whitelisted for local dev only. If your application relies on _winreg, it is likely that it will not function properly in production.
    WARNING  2020-10-18 05:47:52,762 __init__.py:40] Book 'Aisixiang' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,769 __init__.py:40] Book 'Dapenti' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,776 __init__.py:40] Book 'Economist' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,782 __init__.py:40] Book 'FTChinese' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,788 __init__.py:40] Book 'Lifeweek' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,793 __init__.py:40] Book 'nfzm' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,805 __init__.py:40] Book 'TEDxBohaiBay' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,813 __init__.py:40] Book 'Xueqiu' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,828 __init__.py:40] Book 'ZhihuDaily' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,884 __init__.py:40] Book 'biquge.DaWang' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,891 __init__.py:40] Book 'biquge.JianLing' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,898 __init__.py:40] Book 'biquge.JianTu' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,904 __init__.py:40] Book 'biquge.NiLiu' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,911 __init__.py:40] Book 'biquge.XiuZhen' import failed : No module named lxml.etree
    WARNING  2020-10-18 05:47:52,921 __init__.py:40] Book 'cn3k5.WenDing' import failed : No module named lxml.etree
    ERROR    2020-10-18 05:47:53,032 wsgi.py:269]
    Traceback (most recent call last):
      File "D:\Program Files (x86)\Google\Cloud SDK\google-cloud-sdk\platform\google_appengine\google\appengine\runtime\wsgi.py", line 240, in Handle
        handler = _config_handle.add_wsgi_middleware(self._LoadHandler())
      File "D:\Program Files (x86)\Google\Cloud SDK\google-cloud-sdk\platform\google_appengine\google\appengine\runtime\wsgi.py", line 311, in _LoadHandler
        handler, path, err = LoadObject(self._handler)
      File "D:\Program Files (x86)\Google\Cloud SDK\google-cloud-sdk\platform\google_appengine\google\appengine\runtime\wsgi.py", line 96, in LoadObject
        __import__(cumulative_path)
      File "E:\KindleEar-master\KindleEar-master\apps\module_front.py", line 47, in 
        from apps.View import *
      File "E:\KindleEar-master\KindleEar-master\apps\View\__init__.py", line 14, in 
        module = loader.find_module(name).load_module(name)
    INFO     2020-10-18 13:47:53,061 module.py:865] default: "GET / HTTP/1.1" 500 -
      File "D:\Program Files (x86)\Google\Cloud SDK\google-cloud-sdk\platform\bundledpython2\lib\pkgutil.py", line 246, in load_module
        mod = imp.load_module(fullname, self.file, self.filename, self.etc)
      File "E:\KindleEar-master\KindleEar-master\apps\View\Adv.py", line 14, in 
        from PIL import Image
    ImportError: No module named PIL

    但是在控制台查看安装了哪些包时提示了一下内容

    C:\Users\Administrator>pip list
    DEPRECATION: Python 2.7 reached the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 is no longer maintained. pip 21.0 will drop support for Python 2.7 in January 2021. More details about Python 2 support in pip can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support pip 21.0 will remove support for this functionality.
    Package    Version
    ---------- -------
    Jinja2     2.11.2
    lxml       4.6.0
    MarkupSafe 1.1.1
    Pillow     6.2.2
    pip        20.2.3
    pycrypto   2.6.1
    setuptools 41.2.0

    不知道为什么已经安装了这些包还提示错误?

  2. 请问出现以下错误是怎么回事啊。

    C:\Users\vipde>cd D:\KindleEar
    C:\Users\vipde>dev_appserver.py .\app.yaml .\module-worker.yaml
    Traceback (most recent call last):
      File "D:\google cp\google-cloud-sdk\platform\google_appengine\dev_appserver.py", line 96, in 
        _run_file(__file__, globals())
      File "D:\google cp\google-cloud-sdk\platform\google_appengine\dev_appserver.py", line 90, in _run_file
        execfile(_PATHS.script_file(script_name), globals_)
      File "D:\google cp\google-cloud-sdk\platform\google_appengine\google\appengine\tools\devappserver2\devappserver2.py", line 613, in 
        main()
      File "D:\google cp\google-cloud-sdk\platform\google_appengine\google\appengine\tools\devappserver2\devappserver2.py", line 601, in main
        dev_server.start(options)
      File "D:\google cp\google-cloud-sdk\platform\google_appengine\google\appengine\tools\devappserver2\devappserver2.py", line 277, in start
        env_variables=parsed_env_variables)
      File "D:\google cp\google-cloud-sdk\platform\google_appengine\google\appengine\tools\devappserver2\application_configuration.py", line 940, in __init__
        env_variables)
      File "D:\google cp\google-cloud-sdk\platform\google_appengine\google\appengine\tools\devappserver2\application_configuration.py", line 139, in __init__
        self._config_path)
      File "D:\google cp\google-cloud-sdk\platform\google_appengine\google\appengine\tools\devappserver2\application_configuration.py", line 501, in _parse_configuration
        with open(configuration_path) as f:
    IOError: [Errno 2] No such file or directory: '.\\app.yaml'