如何用 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
在之前创建的订阅脚本基础上,我们在代码头部新导入了 URLOpener 和 BeautifulSoup 两个模块,前者是用来请求页面 URL 获取响应内容的,后者则是用来解析响应内容以便提取文章内容数据的。
我们还添加了一些参数。其中 coverfile
用来设定电子书的“封面图片”, mastheadfile
是用来设定期刊样式电子书特有的“标头图片”的。制作这两张图片时,其尺寸和格式可参考 KindleEar 项目 images 目录中已有的图片,制作好的图片也保存在这个目录。注意,参数值需要图片的文件名,不需要额外指定路径,因为 KindleEar 默认图片都在 images 目录下。本例用的是如下所示两张图片,你也可另存使用。
▲ 封面图片:cv_chinadaily.jpg
▲ 标头图片: mh_chinadaily.gif
然后就是 page_encoding
和 fulltext_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_l
的 span
标签中,文章内容存放在 id
为 Content
的 div
标签中。
▲ 文章内容显示效果
▲ 文章内容标签结构
▲ 文章内容结构说明
在分析示例网站 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: YOUREMAILNAME@163.com
To: YOUREMAILNAME@kindle.cn
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 的网站内容(下篇)》
© 「书伴」原创文章,转载请注明出处及原文链接:https://bookfere.com/post/752.html
“Kindle推送”相关阅读
- 为什么推送到 Kindle 的 KF8 标准 MOBI 电子书不显示封面
- Kindle 个人文档服务 2022 年 8 月起不再支持推送 MOBI 格式
- Kindle 怎么导入电子书(图解多种电子书导入方式)
- 如何单个或批量删除 Kindle 云端的电子书
- Calibre 使用教程之通过邮箱一键推送 Kindle 电子书
- 解决 Calibre 推送“500 Error: bad syntax”错误
- 如何快速无损修复推送失败的 EPUB 格式电子书文件
- BookDrop:用 Dropbox 自动同步电子书到 Kindle
- Readability:定时或立即把长文推送到 Kindle
- 如何利用 Sigil 和 EpubCheck 插件检查和修复 EPUB 文件
- 中亚微信推送服务 Send to Kindle 全新升级
- EpubPress:把打开的多个网页转成一本电子书
- Kindle 推送小技巧:“网易闪电邮”右键推送电子书
- 通过 Send to Kindle 发送的文档已支持 KFX 增强排版功能
- 亚马逊 Kindle 个人文档服务已原生支持推送 EPUB 格式
生成的文章是什么格式的哇
是 MOBI 格式。
你好,请教下,ubuntu18.04X64 本地运行 KindleEar手动推送正常,自动推送没有反应,是什么原因呢?怎么处理好?
貌似 SDK 不支持定时任务。
哦,可以用linux的定时任务替代不?
如果能找到执行的接口应该是可以的,但是貌似定时任务是需要用户权限的。
哦,在dockerhub找到个镜像unixfox/kindleear,看dockerfile应该是可以成功的,也看到加有定时任务,部署成功了,但网页看日志又没有自动推送,有什么好的排查办法不?
你好,已经能收到推送了,但我想问一下这几条代码错误要怎样处理
WARNING 2019-08-18 07:09:08,349 sandbox.py:1111] 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 2019-08-18 07:09:33,132 sandbox.py:1111] The module _ctypes is whitelisted for local dev only. If your application relies on _ctypes, it is likely that it will not function properly in production.
这是在做什么操作的时候出现的提示?
登录kindleEar后,终端就给出现这些代码,会显示在INFO的那几条代码之间
这个 Warning 信息只有 Windows 系统才会出现,对应用是没有影响的,因为 production 环境是 GAE,不存在这种只有 Windows 系统才有的模块。如果不想看到这个提示,可以编辑这个文件:
找到
WIN_ONLY_WHITELISTS = ['_winreg', '_ctypes', 'msvcrt']
这一行,将其值改为空数组[]
(个人建议用#
将其注释掉,然后再重新添加一行WIN_ONLY_WHITELISTS = []
)。这样命令提示符就没有提示了。请问,我的是是么情况?
raise SMTPSenderRefused(code, resp, from_addr)
SMTPSenderRefused: (553, ‘Mail from must equal authorized user’, )
WARNING 2019-08-18 04:52:34,711 BaseHandler.py:172] sendmail to 13112301690@kindle.cn failed:SMTPSenderRefused(553, ‘Mail from must equal authorized user’, ).
INFO 2019-08-18 12:52:34,984 module.py:861] worker: “GET /worker?u=admin&id=5891733057437696 HTTP/1.1” 200 40
SMTP 服务器拒绝的原因是发信邮箱和发信人名称不一致。你需要确认终端上的
--smtp_user
设定值和 config.py 文件中SRC_EMAIL
参数的值一致。假设你使用的是 163 邮箱,如果你的发信邮箱是 example@163.com,那么发信人名称也应该是 example@163.com,而不是 example,更不能是其它邮箱地址。请问上中篇的步骤都完成后命令提示符仍显示no new feeds和invalid syntax要怎么办啊?
如果出现了 invalid syntax 说明你编写的脚本有语法错误,命令提示符上应该有详细的错误提示,你需要根据提示修正你的代码。
抓到了内容但是send failed是怎么回事是啊?这里显示的错误信息是SMTPHeloError(500,’Error:bad syntax’)是不是又是语法错了?如果错了我要往哪方面更正呢?
还有我这里总显示WARNING …]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
还有一个_ctypes的一样的WARNING,这两个是什么意思啊?影响seed的send吗?
看起来是 SMTP 设置的问题。建议把提示信息贴完整一些,不然不好判断问题所在。
谢谢您上面的解答,现在可以成功send了但还接收不到,我把报告复制下来了:
WARNING 2019-07-19 14:40:50,553 makeoeb.py:47] read file ‘images\cv_chinadaily.jpg’ failed : [Errno 2] No such file or directory: u’images\\cv_chinadaily.jpg’
WARNING 2019-07-19 14:40:50,553 resources.py:101] Failed to generate thumbnail
INFO 2019-07-19 22:40:51,733 mail_stub.py:170] MailService.Send
INFO 2019-07-19 22:40:51,779 mail_stub.py:379] You are not currently sending out real email. If you have sendmail installed you can use it by using the server with –enable_sendmail
INFO 2019-07-19 22:40:51,888 module.py:861] worker: “GET /worker?id=5760616295825408&u=admin HTTP/1.1” 200 40
错误提示你不能发送真正的邮件,你没有设置 SMTP 参数吗?建议仔细阅读本文第五部分。
我想知道,相对于使用kindle ear源码订阅网站,是不是使用rsshub制作路由,再使用自定义订阅更方便灵活,两者之间的优缺点是什么?
如果第三方工具转制的 RSS 供稿可用且能满足自己的阅读需求,那直接用“自定义 RSS”功能订阅就行了,优点是省事儿,缺点是无法精确控制内容的输出,遇到 RSS 供稿不可用时就无法使用了。自制订阅脚本可以对网站内容的抓取进行更细颗粒度地控制,好处是可以根据自己的意愿直接从网站抓取内容转换成电子书,而不用先把内容转换成 RSS,再用 KindleEar 转换成电子书,缺点(主要对没有编程经验的人而言)是需要自己写代码实现。
建议出一篇KindleEar推送微信公号的
本文适用所有含文章列表的页面。对于微信公众号,可以用“搜狗微信”提供的公众号搜索功能找到公众号的文章列表。