用python编写网页更新提醒程序
本学期的很多课程依赖课程网页发布通知、课件、作业、OJ。因为不想每天手动上这堆网站查看更改,我用python写了一个程序来自动检测和提醒课程网站的更改。
这篇文章将讲一下如何订阅我已经写好的提醒服务、如何写一个自己的提醒服务。
项目代码已经开源到GitHub。
订阅已有服务
注意:本服务仅针对南京大学人工智能学院20级
订阅:发送任意邮件到 subscribe20ai@caomingjun.com
取消订阅:发送任意邮件到 cancel20ai@caomingjun.com
一旦订阅,程序会在网页更改时通过邮箱 notifier20ai@caomingjun.com 向您发送信息。
如果没有出现bug,订阅或取消订阅后,你可以在5分钟内收到自动回复邮件。在收到自动回复邮件之前,请不要进行任何操作(比如订阅后在没有收到回复的情况下取消订阅),虽然这种行为不会导致程序出现异常,但是会导致你不知道自己订阅了没有。
重复订阅、不在订阅列表中的邮箱取消订阅不会收到回复。
程序每3分钟访问一次受监视网页,如果发生更改会以邮件提醒。因此最大延迟为3分钟。
需要任何帮助(或者发现程序出现奇怪的举动),请联系 support@caomingjun.com,这个邮箱由人类管理。
目前受监视的网页列表为:
如果有任何遗漏欢迎发送邮件到 support@caomingjun.com 指出。
准备工作
本地还是服务器?
我的程序用到了一台腾讯云的轻量应用服务器以实现实时监测,这也可以由其他的服务器或者任何24小时开机的计算机完成。
如果你愿意牺牲实时性,改为定时(或者在个人电脑开机启动时)检查网页的更改,也可以不需要服务器。这样需要对程序做一些修改(主要是简化),这些修改会在文末提出。
需求分析
我们需要实现以下几个功能模块:
- 读取网页数据并分析与之前数据的区别
- 保存网页数据以便下一次比较
- 发送邮件
- 接受邮件并将发件人加入或移出订阅列表(如果你只是写给自己用,可以忽略)
我希望在更改配置(发件和收件邮箱、监视网页列表)时保持程序运行,并且在程序中断(更新维护或者程序报错)时保存数据,所以我把数据存储在数据库中,这里使用 MongoDB(这不是关系数据库)。如果不需要这些功能,你也可以不使用数据库而改用文件(如 json)甚至直接将数据保存在程序的变量中。
依赖
- 一台服务器(可选)
- 在服务器上安装宝塔面板并在其应用商店中安装python项目管理器(如果你不想要图形化界面,可以不装这两个)
- 安装 MongoDB(如果不使用数据库,可以不装)
- 在本地安装 MongoDB Compass(如果不使用数据库或者不想用图形化界面管理数据库,可以不装)
- 在运行程序的机器上安装
python3
,我的版本是3.7.9
- 在项目文件夹下新建文件夹(随便什么名字,我的是
myMdmail
)将 GitHub 项目 mdmail 的这个文件夹下的所有内容下载到新建的文件夹中。不直接安装的原因是我准备对它进行更改 - 还有若干 python 包,在编写程序过程中会提到,到时候再安装。(其实是我懒得整理并列在这里)
数据库交互
建立数据库
安装好 MongoDB 后,可以直接用 MongoDB Compass 管理数据库,连接方式非常简单,在后者的连接处输入 mongodb://8.8.8.8:27017/
即可(将 8.8.8.8
更改为你的服务器的公网IP)
为了数据库的安全性,通常会为数据库建立账户并将数据库改为只能通过账户访问。具体方式请 STFW 。
首先建立一个数据库,建议用项目的名字命名,比如 20AI_reminder
。
我把用户列表、监视网站列表、基本配置放进了数据库,因此在 20AI_reminder
下创建以下 Collection
:
websites
,监视网站列表,包括其名称、URL、读取类型(后面会提到)、内容(程序保存以便下次比较的网页数据)。如果是网盘链接,还要存储密码。users
,用户列表,仅包括用户邮箱。config
,程序配置,包括发件和收件邮箱等。整个Collection
只有一个行。
python与数据库交互
MongoDB 提供了 pymongo
包来实现 python 与 MongoDB 的交互。教程见菜鸟教程。
这个部分应当包括:
- 获取程序配置信息
- 获取监视网站列表
- 获取用户列表
- 增加和删除用户
- 更新网站内容
- 刷新数据库内容
建议把数据库交互写成一个单独的 .py
文件,其他各文件都导入它,以实现配置信息在文件间的共享。
获取网页
这个模块会访问网页,以Markdown格式(比HTML代码更加用户友好)输出更改。
我们需要获取多个网页的内容,包括普通的HTML网页、RSS、网盘,因此要为不同类型的网页设计不同的函数。
这些函数统一以网站(一个字典,包括其名称、URL、读取类型、内容)作为输入,输出发送给用户的更改提示。如果没有更改,输出空字符串。如果网页发生更改,函数还要调用数据库交互的模块以更新网页内容。
注意网络连接可能超时,一定要设置一定的重试次数。
普通网页
可以使用 markdown
包的函数 markdown.Markdown
,它可以很轻松地将HTML代码转换为Markdown。
在使用过程中,我发现在转换部分网页时, markdown.Markdown
的输出只有一行(所有换行都消失了),所以为这类网页单独设计了一个函数,逐行将HTML代码传入 markdown.Markdown
来进行转换。
获取网页的Markdown之后,与数据库中存储的数据进行比较,如果不同,调用 difflib.unified_diff
以输出 diff
格式的文本。
diff
格式的文本并不是非常用户友好,但是也还凑合。或许以后会改。
RSS
RSS的获取简单得多。feedparser
包提供了 feedparser.parse
函数来解析RSS,STFW并使用即可。
然后就是进行比较了,我只对每一项的标题进行比较,输出的是非常用户友好的文本。
南大网盘
这部分比较复杂,因为涉及到了密码的验证。
先定义 token
,这是为链接中的一部分,比如分享链接为 https://box.nju.edu.cn/d/12345/
,那么 token
为 12345
整个获取过程如下:
首先 GET https://box.nju.edu.cn/d/<token>/
得到 sfcsrftoken
(在cookies中)
再从上面的返回HTML中找到 csrfmiddlewaretoken
(通过 regex
包进行正则表达式匹配)
然后以 sfcsrftoken
为cookies;body为 csrfmiddlewaretoken
、token
(在网址中)、password
为内容进行请求。
body例子:
csrfmiddlewaretoken=1234&token=1234&password=1234
得到两个cookies:(新的)sfcsrftoken
和 sessionid
以这两个cookies向 https://box.nju.edu.cn/api/v2.1/share-links/<token>/dirents/?thumbnail_size=48&path=%2F
发出请求并获得一个JSON文件。这个JSON包括了分享的文件夹根目录下的文件(文件夹)列表。
我们可以使用 request
包提供的 request.Session
类,它会自动帮我们处理cookies。
获取JSON后进行比较并输出即可。我的程序中这部分输出的文本也是非常用户友好的。
发送邮件
在网站发生更改、有新订阅者、有人取消订阅时,程序都需要发送邮件。
由于无法直接发送Markdown格式邮件,我们要先把Markdown转为HTML。
发送邮件采用更改过的 mdmail
模块,我们前面已经下载了原版的 mdmail
,接下来对它进行更改。
首先在 api.py
中更改 EmailContent
类的初始化函数,让它支持显示代码:
1 |
|
然后,由于邮件是群发的,为了保护用户的隐私,我不希望让程序在发送邮件时显示收件人,所以使用邮件的密送功能(BCC),这样收到邮件的人就不知道还有谁订阅了服务。mdmail
使用了 emails
包来发送邮件,但是我没有找到如何用 emails
发送只密送的邮件(可以发送密送,但是不能没有普通收件人),所以我将其改为使用 smtplib
。这需要对 api.py
中的函数 send
作更改:
1 |
|
读取邮件
程序需要从订阅和取消邮箱中读取邮件以增删用户。这部分使用 imaplib
完成。
需要注意:
imaplib.IMAP4_SSL
在初始化时可能出现连接错误,需要设置重试次数(其实就是异常处理)- 要考虑在程序的一个运行周期内有人进行多次操作的情况(比如订阅后没有收到回复就取消),因此要根据收到邮件的时间进行排序
示例代码如下,这个函数读入收件邮箱、密码、操作类型(添加或删除)并输出一个包含操作类型、发件人、时间的元组组成的列表。外层函数会合并两个列表,进行排序并逐个操作。
1 |
|
尾声
运行周期与异常处理
主程序使用一个 try
-except
包裹,except
将异常通过邮件发送给程序管理员(就是你自己)。
运行周期通过 crontab
实现。
部署
在服务器上安装 python 和需要的包,使用 crontab 定时运行。如果需要,也可以为它创建对应的虚拟环境。
本地版本
如果你只想在本地手动或者开机启动时运行一次网页更改检测,你可以:
- 不写数据库部分,而是用文件储存数据
- 不写订阅和取消部分(包括读取邮件),因为只有你一个人用
- 不写发送邮件部分,而是采用命令行输出
- 不写运行周期,甚至不写异常处理,因为运行的时候你在看着输出
升级
提醒方式升级
你可以把提醒方式改为使用微信公众号。
我暂时没有做这部分功能,而且由于非技术原因(腾讯政策),做好了也很可能不会正式上线,而是仅小范围使用。
获取网页 meta property
我未来给自己做小说更新提醒程序的时候要用到,这里是教程。
鸣谢
感谢 OrangeX4 参与程序的测试工作并提出建议
感谢在网上发布各种开源程序包和教程的不认识的大佬们
用python编写网页更新提醒程序
https://blog.caomingjun.com/using-python-to-notify-website-changes/