This page looks best with JavaScript enabled

一种关于Valine评论实现即时邮件提醒的解决方案

 ·  ☕ 5 min read · 👀... views

0x00 提出背景

自从博客迁移到Hexo上后,这个评论系统可让我没少花功夫折腾。可以了解到现在主流的是使用Valine插件,配合LeanCloud的云端存储来实现一个评论功能,但是这个评论的提醒是个大问题,毕竟不可能每时每刻盯着LeanCloud的数据表吧(尤其像我这么懒的…一个月都不见得会去看一次数据表),这就非常需要评论的邮件提醒了。经过无数的百度后,可以了解到目前大部分的邮件提醒是基于LeanCloud的“忘记密码”功能来实现新评论的邮件提醒。实测发现如果是一个用户对我本人发表的评论进行回复确实是可以收到邮件提醒的,但若是一个用户直接对博文进行评论,貌似是不会收到邮件提醒的(不排除是我的操作问题,但我试了几次确实都没有邮件提醒),而且我觉得绝大多数情况需要的都是别人对博文的评论的提醒,而且LeanCloud的邮件提醒不能快速给出一个被评论的博文链接是你快速跳转回复,甚至要你自己去找是哪个博文被评论了,所以上述那个方案感觉并不太实用。

在寻求解决方案的时候我想到是否可以直接的模拟Hexo对leancloud发出请求来获取到所有的评论信息。这确实是可以的,我了解到对leancloud的查询甚至都已经集成到了一个python库中,以一种类似SQL的方式可以与LeanCloud数据库进行交互。但是这其实是一种学习成本较高的解决方案,作为懒癌晚期的我只想解决这一问题,所以我又去寻求了别的解决方案,就是接下来我要提出的这个方案: 利用LeanCloud提供的Web接口实现一个对该Web接口的实时爬虫,从而实现评论的实时监控

这应该是一个全新的思路,至少我还没有在互联网上看到有这样的解决方案。同时这也是一个折中的方案,以提高一定的代码复杂度为代价来换取一个较低的学习成本就能解决问题的解决方案。因为这是一个实时爬虫,需要脚本全天候运行着,不过这完全不是问题,把它丢服务器上和博客一起运行就可以了。

0x01 准备工作

因为脚本的原理是实时的爬取LeanCloud的web接口页面来获取评论,因此我们首先要开启LeanCloud的Web功能。

进入LeanCloud后端,在 云引擎->设置->Web主机域名 中开启你的Web接口,然后你就会得到类似于 “xxxxxx.leanapp.cn” 这样的接口网址,进入后会是一个登录界面,请确保可以使用管理员账号成功登录

然后需要去申请一个你所使用的邮件服务商的smtp服务授权码,这个非常容易,百度上有大量的教程,直接搜索诸如 “qq邮箱smtp授权码查看” 类似的信息获取此码即可

0x02 leanapp.py

这里没什么好说的,就是一个没什么技术含量的爬虫,直接贴代码了, 注意用的时候看一眼,该修改的配置都标出来了

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
#-*- coding: UTF-8 -*
import requests, re, json, time, hashlib
from lxml import etree
from sendmail import Mail

class leanapp:
    def __init__(self):
        self.login_url = "https://xxxxxxxxx.leanapp.cn/login"    # 你的LeanCloud Web接口地址
        self.comment_url = "https://xxxxxxxxx.leanapp.cn/comments"  # 你的LeanCloud Web接口登陆后所显示的地址
        self.username = "xxxx@qq.com"      # LeanCloud Web接口管理员用户名
        self.password = "xxxxxxxxxxx"  # 管理员密码
        self.my_name = "My_name"                # 填写你回复评论时用的昵称,以避免你回复别人的评论时也发送邮件提醒
        self.sender = "xxxx@qq.com"        # 邮件提醒发件人,需与授权码相对
        self.receivers = ['xxxx@qq.com']   # 邮件提醒收信人

        self.session = requests.Session()
        self.set = set()
        self.cnt = -1

    def login(self, username, password):
        data = {
            'username': username,
            'password': password
        }
        res = self.session.post(self.login_url, data=data, headers=headers)
        # print(res.text)
        if res.status_code == 200:
            print("Login Success! ")

    def getComments(self):
        res = self.session.get(self.comment_url, headers=headers)
        if res.status_code != 200:
            print(time.asctime(time.localtime(time.time())) + "status_code Error:", res.status_code)
            return 0
        
        html = etree.HTML(res.text)
        all_rawcomments = html.xpath('//li[@class="vcard"]')
        # print(all_rawcomments)

        for comment_node in all_rawcomments:
            comment = {
                'name': comment_node.xpath('.//div[@class="vhead"]/a[1]/text()')[0].strip(),
                'time': comment_node.xpath('.//span[@class="vtime"]/text()')[0],
                'content': "".join(comment_node.xpath('.//div[@class="vcomment"]//p/text()')),
                'url': comment_node.xpath('.//div[@class="vhead"]/a[2]/@href')[0]
            }
            print(comment)
            md5 = hashlib.md5()
            md5.update(str(comment).encode())
            comment_md5 = md5.hexdigest()
            if self.cnt != -1 and (comment_md5 in self.set) == False:
                self.set.add(comment_md5)
                self.sendMail(comment)
            self.set.add(comment_md5)

        self.cnt = len(self.set)
        print(time.asctime(time.localtime(time.time())), "当前共有", len(all_rawcomments), "条评论\n")
        if len(all_rawcomments) == 0:
            raise CookieError()
    
    def sendMail(self, comment):
        print("发现新评论 开始发送邮件!")
        # print(comment)
        if comment["name"] == self.my_name:
            print("是本人回复 略过提醒")
            return 0
        sender = self.sender
        receivers = self.receivers    # 收件人列表
        subject = '新评论通知'  # 发送的主题
        content = mail_board.format(comment["time"], comment["name"], comment["content"], comment["url"])
        sender_name = self.my_name       # 发件人姓名
        receivers_name = self.receivers   # 收件人姓名

        mail = Mail(sender, receivers)
        mail.send(subject, content, sender_name, receivers_name)

class CookieError(Exception):
    def __str__(self):
        return "Cookie失效 尝试重新登录"

def WriteError(filename, error_text):
    try:
        with open(filename, 'w+') as fp:
            print(time.asctime(time.localtime(time.time())), file=fp)
            print(error_text,file=fp)
    except Exception as e:
        print("Catch Error when Write Error")
        print(e)
        exit(1)

headers = {'User-Agent': 'User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'}
mail_board = '''
您的博客在 {} 时收到 {} 的新评论:
{}
快速访问:{}
'''

if  __name__ == '__main__':
    while True:
        try:
            instance = leanapp()
            instance.login(instance.username, instance.password)
            while True:
                instance.getComments()
                time.sleep(15)
        except Exception as e:
            WriteError("Error.txt", e)
            del instance

0x03 sendmail.py

这里主要要修改的就是Mail类构造函数里的授权码和邮件服务器的地址,这个Mail类就是发邮件的板子了,所以单独写成一个文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#coding:utf-8
import smtplib
from email.mime.text import MIMEText
from email.header import Header

class Mail:
    def __init__(self, sender, receivers):
        # 第三方 SMTP 服务

        self.mail_host="smtp.qq.com"       #设置服务器:这个是qq邮箱服务器
        self.mail_pass="*********"           #授权码  需到服务提供商处获取
        self.sender = sender      #你的邮箱地址 
        self.receivers = receivers  # 收件人的邮箱地址,可设置为你的QQ邮箱或者其他邮箱,可多个

    def send(self, subject, content, sender_name, receivers_name):

        message = MIMEText(content, 'plain', 'utf-8')

        message['From'] = Header(sender_name, 'utf-8')   
        message['To'] =  Header(receivers_name[0], 'utf-8')     # 收件人名字,但只能传字符串,因此取列表第一个
        message['Subject'] = Header(subject, 'utf-8') 
        try:
            smtpObj = smtplib.SMTP_SSL(self.mail_host, 465) 
            smtpObj.login(self.sender,self.mail_pass)  
            smtpObj.sendmail(self.sender, self.receivers, message.as_string())
            smtpObj.quit()
            print('邮件发送成功')
        except smtplib.SMTPException as e:
            print('邮件发送失败')



if  __name__ == '__main__':
    sender = '********'    # 发件人邮箱
    receivers = ['********']    # 收件人列表
    subject = 'test subject'  # 发送的主题
    content = 'This is test content'  # 发送的内容
    sender_name = "*****"       # 发件人姓名
    receivers_name = ['********']   # 收件人姓名

    mail = Mail(sender, receivers)
    mail.send(subject, content, sender_name, receivers_name)

0x04 后记

用这个脚本也有好长一段时间了,感觉还稳定,但是我知道这个脚本不管是从写法上还是设计上都有诸多不合理的地方,所以我觉得这里最主要的是提供一种解决思路。

目前看来类似于LeanCloud这样全栈一体化的平台越来越多。相信还会有很多平台如LeanCloud一样,利用官方接口未必就是解决问题最简单的方式,Web接口用的好甚至能更快捷的获取到目标数据。

Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer