功能描述:
干净的小说更新推送
fork https://github.com/cooljacket/NovelUpdate
更新内容:
日志打印添加日期
在使用的时候请注意要:
修改发送列表
开发环境
python3(注意只支持python3) + ubuntu 16.10 + Crontab
其中Crontab是ubuntu上的一个定时任务机制,用来设置定时指定这个脚本的。用法很简单,请自行搜索。
- 在shell中输入命令crontab -e(首次运行时需要选择编辑器,建议选自己熟悉的,比如vim);
- 然后在里面输入
* * * * * python3 /home/jacket/NovelUpdate/main.py >> /home/jacket/NovelUpdate/data/log.txt
,保存即可。 注意上面的路径需要根据自己的实际来改,还有我把输出重定向导出为日志,如果不需要也可以去掉。 - ps,上面的命令是每分钟都做一次,这样子日志很快就很大了!所以我还定制了每天的日志清除任务
值得注意的是,随着检测更新的任务的增多,我发现有些是需要实时的(比如喜欢看的小说、必须看的通知),而有些的实时性要求很低(比如一些技术博客),所以我在crontab中区分了这两种任务,最后的话,我的crontab脚本这样写:
# 每分钟都检测一遍“实时的检测任务”
* * * * * python3 /home/jacket/NovelUpdate/RealTimeChecker.py >> /home/jacket/NovelUpdate/data/RealTimeLog.txt
# 每隔十分钟才检测一遍“非实时的检测任务”
0,10,20,30,40,50 * * * * python3 /home/jacket/NovelUpdate/NotRealTimeChecker.py >> /home/jacket/NovelUpdate/data/NotRealTimeLog.txt
# 每天的中午12点清除log文件
* 12 * * * rm /home/jacket/NovelUpdate/data/RealTimeLog.txt /home/jacket/NovelUpdate/data/NotRealTimeLog.txt
程序逻辑
一开始我写爬取百度小说贴吧的程序,没有模块化设计,全部逻辑写在一个类里,过了几个月回头看,太丑了,扩展性超级差。所以就按照MVC架构来思考模块化:
Model
数据层分三部分:
- 存储已经爬取到的数据;
- 获取指定页面的目标数据(用正则表达式匹配出目标内容);
- 支持查找,以便逻辑层判断数据是否为更新。
对于第一点,其实正规一点应该使用数据库的,不过考虑到一本小说最多也就2000章,杀鸡不需要牛刀,所以使用文件系统来存储也很简单维护。所以我是将历史的更新存储到一个数据库db_update里,然后将新获取到的数据与历史数据对比,如果没有匹配的,说明就是更新了。
对于第二点,设计了一个SinglePageSpider来封装urllib库的请求操作,直接传入网址就可以获取到目标数据,所谓的目标数据是通过编写正则表达式来匹配页面得到的!
第三点,简单匹配,也可以考虑做hash等。
View
这里其实是:
- 根据更新内容(来自Model层),生成邮件内容;
- 可靠发送邮件。
生成邮件内容这个简单,如何做到“可靠发送邮件”呢?(如果不可靠,这个爬虫的乐趣就少很多了) 其实关键是“可靠”,为什么会不可靠呢?发邮件不是一发就出去了吗? 这个涉及到我使用的免费的新浪邮箱的个人邮箱服务,很多限制,有时网络不好,有时邮箱限制发送的次数、时间间隔等等,都会导致失败。
我把发送失败的邮件保存到数据库中,然后在下一次激活的时候会发送,直到它们被发送成功为止!由于我目前设定crontab是每分钟都执行一次爬虫程序,所以邮件发送的延迟还是很小的。
Control
逻辑层把前面两层组织起来了:
- 给Model层指定页面去爬取数据,并判断是否为更新;
- 若是更新,则扔给“可靠邮件发送器”发送邮件通知。
关于发送失败情况的进一步讨论
很明显发送失败了,是不能将其内容写入数据库db_update里的,因为这样下次就会把这些内容判断为不是更新的内容,从而忽略掉它们。 那应该怎么办呢?
逻辑上这应该是一个原子操作:发送成功+写入数据库,或者发送失败+不写入数据库。 但是不写入数据库的话,要丢弃掉这些数据,然后等下一次爬取的时候再尝试发送?
这样确实可以,但是有个问题——就常识来说,邮件服务器的故障一般是持续的(作最坏的打算),不会说马上一两分钟就解决。那万一它这个故障持续了挺久的,并且那些更新了却没有发送成功的数据被后来更新的数据给挤掉了(因为主页一般只有2-3个置顶贴),那这部分数据就真的丢失了!!! 所以不能丢弃掉这些数据,但是逻辑上又不能写入数据库db_update里,那应该怎么办呢?
我的做法是——还是实际上写入数据库db_update里,但是会另外建立一个数据库fail来存储这些发送失败的数据,在每次检测的时候也检测是否有发送失败的数据并尝试重新发送,直到发送成功才将fail里的数据清空。这样子相当于逻辑上是没有写入到db_update里的,但同时又保证了不会有因为邮件服务器故障时间的长短而导致数据丢失的问题!!!
好吧,说人话,还是从MVC架构的角度来考虑,逻辑层从数据层拿到更新的数据之后,传递给View层,View就负责可靠地将这些内容发送邮件成功,这样职责分明,逻辑层不必管View层是怎么实现的,而View专心发送好邮件就好了。
代码结构
main.py
这是主程序,在里面可自定义发送的邮箱列表、想要爬取的贴吧地址、执行多种不同的监测更新模块。
SimpleDB.py
这是我自己封装的一个简易数据库抽象类,抽象了这个爬虫对于数据处理的接口,目前支持的操作有:建表、插入、查找、获取整张表数据、删除整张表的数据的操作。
SimpleDBUsingSqlite3.py
继承SimpleDB并实现其接口,使用sqlite3实现。
SimpleDBUsingFS.py
继承SimpleDB并实现其接口,底层其实是用了文件系统来做(效率低)。
SinglePageSpider.py
这是对python3的urllib的请求操作的上层封装,很容易获取单个页面的内容。
send_email.py
这是一个发送邮件的功能函数,使用smtp协议发送邮件,需要自行申请或搭建一个邮件服务器来实现,当然,也可以换成另外的通讯模式,因为发送邮件是一个黑盒子,里面的功能逻辑可以随便改,对其它模块没有任何影响!!!(前提是返回值、参数等的约定保持一致)
SendMailReliablly.py
这是基于send_email.py提供的api封装的一个“可靠邮件发送器”!一旦发送失败,它会将未发送成功的邮件内容保存进数据库中,等待下次激活时发送。而这个重新发送的过程对于用户来说是透明的,完全由程序自己控制的,所以我称之为“可靠放心的”!
监测模块UpdateMonitorBaseClass.py
至此,所有的模块对于整个爬虫监测系统来说,都是可重用的!想要迁移到新的业务需求上时,只需要简单使用我封装好的监测模块即可!如果有特殊的业务需求(下面有例子),也可以通过简单继承基类和重写函数就可以达到,可重用性超级高(个人觉得)!!!!
比如,想监测我们学院官网通知的更新,只需要知道官网网址,以及匹配通知链接的正则表达式,然后简单调用:
# 参数的含义分别是:发送列表,标题,要爬取的页面url,匹配内容的正则表达式
sdcs = UpdateMonitorBaseClass(email_send_list,
'学院官网',
'http://sdcs.sysu.edu.cn/',
'<a href="(http://sdcs.sysu.edu.cn/\?p=\d*)" title="([^"]*)"[^<]*</a>',
'条通知'
)
sdcs.checkUpdate()
优化与深入
1. 每次监测更新是线性遍历比较
线性遍历不一定慢,因为只要匹配到有,就可以退出了!!!假设我们逆序遍历文件里的行,那么一切就搞定了,很快! 举个例子,我们已经爬取到的数据是:
tie1
tie2
tie3
tie4
...
tie1000
最新的是tie1000,那么假设没有更新,我们还是只爬取到tie1000,如果从头到尾顺序遍历过来的话,那么比较次数是1000次,而如果逆序遍历,只需要比较一次就可以了!!!
可以控制文件指针来实现,不过我还在想有没有更好的办法(因为控制文件指针移动来读取每行数据太麻烦啦)
2. 数据库更换
其实python里内置了sqlite(效率应该可以比现在高挺多,可以用索引之类的来快速查找),哈哈,不过自己造轮子的感觉很爽!!! 我将这个爬虫里对Model层的接口需求抽象出来了,然后分别用文件系统、sqlite3来实现这些接口,然后就随心所欲调用了,只需要将想使用的class传给UpdateMonitorBaseClass的构造函数即可!比如:
googleBlog = UpdateMonitorBaseClass(
email_send_list,
'Google blog',
'http://developers.googleblog.cn/',
"<a href='(http://developers.googleblog.cn/[^']*)' itemprop='url' title='([^']*)'>[^<]*</a>",
dbClass = SimpleDBUsingSqlite3,
tips = '篇博客'
)