博客访问量自动监测脚本

Apr 5,2020   9268 words   34 min

Tags: Web

1.起因

这个脚本从名字上看就知道与博客的访问量有关,但它并不是用来统计博客的浏览量。博客的浏览统计与记录在很久以前的这篇博客里已经说过了,后台基于百度统计、51LA和Google Analytics,前台展示基于不蒜子。但上面的这些统计方法都有一个很大的缺点,就是无法进行长期的历史记录。例如百度统计和51LA,都只能够保存近期30天的访问记录信息。而不蒜子作为极简网页计数工具则彻底不具有历史记录功能。因此研究如何能够长期保存博客的访问趋势是一个很有用的课题。另一个目前存在的问题是博客的UV和PV每增加10000,我都会在网站大事记里同步更新,但目前这个过程是纯手工的。也就意味着说我需要时不时地关注网站的浏览量,如果发现新增过万,则手动添加一条记录,比较繁琐。因此本篇博客的脚本主要就是为了解决上面提到的两个问题,使得浏览量统计尽可能自动化。

2.功能设计与需求分析

其实要实现的功能流程是十分清晰的。首先服务器端每过一段时间(如一天)就运行一次脚本。运行脚本时,首先脚本会下载网站首页的内容,然后解析找到不蒜子提供的UV、PV信息,将其与上次获取到的数据进行比较,如果过万了则说明访问量到了新的阶段。为了能够自动提醒,当过万的时候会将相关信息以邮件形式发送到我的邮箱,包含变化时间和内容相关信息。这样在收到邮件后就可以再添加条目了,从而避免人工定期查看。当访问量有变化时会发送邮件提醒我。另外每次运行脚本时也会记录当前的访问量到文件中,便于以后统计分析。

3.技术路线与实现方法

根据上面提到的功能与需求,需要用到的核心技术大体可以分为以下几个方面。

(1)网页JS动态内容解析

在之前这篇博客中介绍过如何利用Python的urllib来爬虫下载网络上的资源。但这种方法有它的局限性,那就是只能下载静态资源,对于动态加载的资源则无能为力,例如网页上利用JS动态加载出来的资源等等。本任务需要获取的是博客主页下方由不蒜子的JS脚本动态加载出来的访问数据。如果采用之前的方法,没有办法获得到这些数据。因此需要新的方法来处理动态内容。对于没有界面的服务器而言,需要用到的就是Selenium与PhantomJS的组合。Selenium是一个分布式的自动化测试工具,中文官网是这里,它可以用于加载动态网页内容。而PhantomJS可以理解为是一个浏览器,官网是这里。通过Selenium调用PhantomJS这个浏览器从而实现对于动态网页内容的获取。

对于Selenium相对比较好安装,对于Python的接口,直接利用PIP即可pip install selenium。对于PhantomJS按照以下命令安装即可。

# 安装相关依赖
yum -y install wget fontconfig

# 下载获取phantomjs,注意这里是64位版本的
wget -P /tmp/ https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2

# 解压,如果出现解压错误可能是缺少bzip2,yum install bzip2
tar xjf /tmp/phantomjs-2.1.1-linux-x86_64.tar.bz2 -C /usr/local/

# 重命名一下文件夹
mv /usr/local/phantomjs-2.1.1-linux-x86_64 /usr/local/phantomjs

# 建立个软链接,方便使用
ln -s /usr/local/phantomjs/bin/phantomjs /usr/bin/

另外对于PhantomJS,现在已经过时了,虽然还可以使用。Selenium目前推荐使用headless的Chrome或Firefox。在使用PhantomJS时控制台会弹出过时提醒,如果想不让它弹出,可以修改Python路径下的site-packages\selenium\webdriver\phantomjs\webdriver.py文件中的49和50行,如下,将这两行注释掉就不会再弹出过期提醒了。

(2)文件的存取

Python的文件存取其实比较简单,没有什么特别好说的,直接用open()函数实现即可。对于文件是否存在的判断可以使用os模块中的os.path.exists()函数实现。对于路径分隔符,如果想通用的话可以用os.path.sep

(3)邮件自动发送

服务器端邮件的自动发送是个看似简单但其实比较复杂的问题。对于CentOS而言,直接的办法是用mail指令。但其实在真正的实践中问题一大堆,在经过多次尝试之后还是没有成功,所以最终放弃了这个办法。实际采用的是Python中的相关模块实现邮件的发送,具体而言是stmplibemail两个模块。当然还需要在邮箱中开启STMP的服务。例如以QQ邮箱为例,需要在设置里选择开启POP3/IMAP/SMTP服务,开启后会得到一个授权码,这个授权码在后面会有用到。 关于如何用代码发送邮件,见代码部分。

(4)脚本定期执行

脚本每执行一次就会获取一次访问数据,很显然需要脚本的重复执行才可以实现期望的功能。但这里的重复执行和之前这篇博客里实现的功能有所区别。那篇博客里服务器的角色相对被动,需要一直等待,响应接入的连接。而这里服务器是主动的,每隔一段时间主动获取数据。因此并不需要像那篇博客那样一直运行。这里有两种方案,一种是直接用while循环,每次循环sleep()指定时间,从而实现定期执行。另一种方案是用CentOS的crontab服务,相较于前者它可以提供更丰富的功能实现。如果没有安装的话,利用yum install crontabs安装即可。它和前者的区别是前者其实是脚本一直在运行,只是相当于卡在了sleep()函数,而后者则是真正的停止了。一直在后台运行的是crontab服务,每到指定的时间就会唤醒脚本执行一遍。crontab服务一些常用的指令如下:

# 注意crontab的服务名称叫crond
# 启动服务
service crond start
# 终止服务
service crond stop
# 重启服务
service crond restart
# 重新加载参数
service crond reload
# 查看服务运行状态
serivce crond status

可以通过crontab -e进行执行命令编辑,可以像vim一样编辑。它也有一些它的特定格式,详细格式可以参考这里。简单的格式说明如下:

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name  command to be executed

例如本任务需要的是每天都执行一次,因此写成如下格式:

30 23 * * * python /root/blogMonitor/update.py

这样每天的晚上11点半就会自动执行update.py这个Python脚本。另外需要说明的两点:

1.crontab服务在后台执行命令,因此它并不会在终端或控制台中有任何输出。可以通过tailf /var/log/cron查看日志文件,从而观察执行情况。 另外你可能会有疑问,那如果执行的脚本里就有print语句那会怎么样呢?答案是还是不会在控制台中输出,但是你会收到一封邮件。例如在我的服务器上,这个邮件被放在了/var/spool/mail文件夹下,名字叫root,没有后缀名,直接用文本编辑器打开可以看到如下内容。 没错,这里保存的就是脚本在控制台输出的内容(如果没有输出则不会收到任何邮件)。每次执行都会收到一封这样的邮件(都在root这个文件里)。

2.上面说了这么多,你可能另一个疑问是那到底怎么自动运行呢?答案就是启动crond服务即可。换句话说就是先用crontab -e配置好要执行的命令,然后用service crond start启动服务,命令就会按照你的设置自动执行了。如果需要停止,直接service crond stop终止服务即可。另外,虽说服务器一般不会重启,但如果担心,可以将crontab添加到开机启动,直接systemctl enable crond.service就可以了。这样就算重启,还是会自动运行,监测不会断。

(5)任务的后台运行

由于是通过SSH连接的远程服务器,因此只要连接一断,正在执行的程序就会挂掉。因此显然不能直接运行。解决这个问题可以利用之前这篇博客说过的screen命令。但由于这里使用的是crontab,它是系统的服务,只要打开,它并不会因为连接的断开而终止,因此倒也不需要担心这个问题。但如果是采用脚本内循环的方式,则必须要考虑这个问题了。

4.核心代码

完整代码贴出来,如下,也并不是很多。里面涉及到的一些容错机制都在注释中介绍了。

# coding=utf-8
from selenium import webdriver  # 用于实现对动态JS内容的加载
import time  # 用于获取当前时间
import os  # 用于文件操作相关
import sys  # 用于获取脚本路径相关
import smtplib  # 用于邮件的发信动作
from email.mime.text import MIMEText  # 用于构建邮件内容
from email.header import Header

# 下载网页内容
def download(http_url):
    driver = webdriver.PhantomJS()
    driver.get(http_url)
    return driver

# 解析UV
def getUV(content):
    if content.find("busuanzi_value_site_uv") == -1:
        return -1
    else:
        index_start_uv = content.find("busuanzi_value_site_uv") + len("busuanzi_value_site_uv") + 2
        index_end_uv = content.find("</span> visitors")
        num_str = content[index_start_uv:index_end_uv]
        try:
            return int(num_str)
        except:
            return -1

# 解析PV
def getPV(content):
    if content.find("busuanzi_value_site_pv") == -1:
        return -1
    else:
        index_start_pv = content.find("busuanzi_value_site_pv") + len("busuanzi_value_site_pv") + 2
        index_end_pv = content.find("</span> times")
        num_str = content[index_start_pv:index_end_pv]
        try:
            return int(num_str)
        except:
            return -1

# 读取上一次保存的数据
def readLastFlag():
    dir_path = sys.argv[0][:sys.argv[0].rfind(os.path.sep)]
    file_path = dir_path + os.path.sep + "lastflag.txt"

    if os.path.exists(file_path):
        file_lf = open(file_path, 'r')
        last_flag_uv = int(file_lf.readline().strip())
        last_flag_pv = int(file_lf.readline().strip())
        file_lf.close()
    else:
        last_flag_uv = 0
        last_flag_pv = 0
    return last_flag_uv, last_flag_pv

# 保存上一次的数据
def writeFlag(uv, pv):
    dir_path = sys.argv[0][:sys.argv[0].rfind(os.path.sep)]
    file_path = dir_path + os.path.sep + "lastflag.txt"
    file_lf = open(file_path, 'w')
    file_lf.write(str(uv) + "\n")
    file_lf.write(str(pv) + "\n")
    file_lf.close()

# 为了方便自动更新,生成并保存指定格式的数据
def writeItem(str_content):
    dir_path = sys.argv[0][:sys.argv[0].rfind(os.path.sep)]
    file_path = dir_path + os.path.sep + "items.txt"
    file_lf = open(file_path, 'a')
    file_lf.write(str_content + "\n")
    file_lf.close()

# 发送邮件
def sendEmail(type_flag, message):
    # 用于构建邮件头
    # 发信方的信息:发信邮箱,QQ邮箱授权码(不是密码)
    from_addr = 'zhaoxuhui1993@qq.com'
    password = 'xxxxxxxxxxxx'

    # 收信方邮箱
    to_addr = 'zhaoxuhui1993@qq.com'

    # 发信服务器
    smtp_server = 'smtp.qq.com'

    # 邮箱正文内容,第一个参数为内容,第二个参数为格式(plain 为纯文本),第三个参数为编码
    msg = MIMEText(message, 'plain', 'utf-8')

    # 邮件头信息
    msg['From'] = Header(from_addr)
    msg['To'] = Header(to_addr)
    if type_flag is 'uv':
        msg['Subject'] = Header('Blog UV Update!')
    elif type_flag is 'pv':
        msg['Subject'] = Header('Blog PV Update!')
    elif type_flag is 'uv_error':
        msg['Subject'] = Header('Blog UV Update Error!')
    elif type_flag is 'pv_error':
        msg['Subject'] = Header('Blog PV Update Error!')
    elif type_flag is 'status_error':
        msg['Subject'] = Header('Blog UV&PV Update Error!')

    # 开启发信服务,这里使用的是加密传输
    server = smtplib.SMTP_SSL(smtp_server)
    server.connect(smtp_server, 465)
    # 登录发信邮箱
    server.login(from_addr, password)
    # 发送邮件
    server.sendmail(from_addr, to_addr, msg.as_string())
    # 关闭服务器
    server.quit()

# 保存每次获取到的访问数据,记录历史,方便以后统计
def recordHistory(time, uv, pv):
    dir_path = sys.argv[0][:sys.argv[0].rfind(os.path.sep)]
    hist_file = open(dir_path + os.path.sep + 'history.txt', 'a')
    hist_file.write(time + "\t" + "UV:" + str(uv) + "\tPV:" + str(pv) + "\n")
    hist_file.close()


if __name__ == '__main__':
    uv = -1 # 初始uv值
    pv = -1 # 初始pv值
    max_try_time = 15    # 最大尝试次数
    sleep_time = 30 # 每次尝试的时间间隔
    log_str = ""
    run_date = time.strftime("%Y.%m.%d", time.localtime())
    run_time = time.strftime("%Y.%m.%d %H:%M:%S", time.localtime())
    for i in range(max_try_time):
        # 下载网页
        driver = download("http://zhaoxuhui.top")
        content = driver.page_source
        driver.quit()

        # 获取访问数据并进行记录
        uv = getUV(content)
        pv = getPV(content)
        # 如果没有网络或没有下载成功会输出一个空网页<html><head></head><body></body></html>
        # 因此需要进一步判断,如果不包含指定内容再尝试,否则跳出循环
        if uv != -1 and pv != -1:
            break
        else:
            # 为了降低频率和增加成功率,每次间隔指定时间
            time.sleep(sleep_time)
            log_str += "Attempt " + (i+1).__str__() + ": "
            if uv == -1:
                log_str += " get UV failed."
            if pv == -1:
                log_str += " get PV failed."
            log_str += "\n"
    # 如果达到最大次数还不行则下载失败,直接退出脚本,并发送错误提示邮件
    if uv == -1 or pv == -1:
        err_msg = "Error Time: " + run_time + "\n"
        err_flag = ""
        if uv == -1 and pv == -1:
            err_msg += "After attemping " + max_try_time.__str__() + " times,we still cannot get the UV and PV data!"
            err_flag = "status_error"
        elif uv == -1 and pv != -1:
            err_msg += "After attemping " + max_try_time.__str__() + " times,we still cannot get the UV data!"
            err_flag = "uv_error"
        elif uv != -1 and pv == -1:
            err_msg += "After attemping " + max_try_time.__str__() + " times,we still cannot get the PV data!"
            err_flag = "pv_error"
        err_msg += "\nRemember to check the data for " + run_date + " !"
        err_msg += "\nDetailed Info:\n"
        err_msg += log_str
        # 如果有问题发邮件提醒,退出本次执行的脚本
        sendEmail(err_flag, err_msg)
        exit()
    # 历史文件中追加记录
    recordHistory(run_date, uv, pv)

    # 获取上次的数据
    last_flag_uv, last_flag_pv = readLastFlag()
    write_flag = False

    # 由于是每增加10000才添加记录,所以除以10000
    flag_uv = uv / 10000
    flag_pv = pv / 10000

    # UV的数据比对
    if flag_uv != last_flag_uv:
        # 格式化的字符串
        uv_fmt_str = "\t- **" + run_date + "**" + " 网站累计访客数(UV)突破" + str(
            flag_uv * 10000)
        # 常规字符串用于阅读
        uv_str = run_date + " 网站累计访客数(UV)突破" + str(
            flag_uv * 10000)
        # 输出格式化字符串
        writeItem(uv_fmt_str)
        # 发送邮件
        sendEmail('uv', uv_str + "\n" + uv_fmt_str)
        write_flag = True
    
    # PV的数据比对
    if flag_pv != last_flag_pv:
        pv_fmt_str = "\t- **" + run_date + "**" + " 网站累计浏览量(PV)突破" + str(
            flag_pv * 10000)
        pv_str = run_date + " 网站累计浏览量(PV)突破" + str(
            flag_pv * 10000)
        writeItem(pv_fmt_str)
        sendEmail('pv', pv_str + "\n" + pv_fmt_str)
        write_flag = True

    # flag为true,说明数据有变化,将新的数据写入文件
    if write_flag:
        writeFlag(flag_uv, flag_pv)

5.测试效果

为了测试,将每天执行一次改成了每两分钟执行一次,浏览数据也并非真实。在启动crond服务后,观察日志如下。 可以看到我们指定的命令每隔两分钟运行一次,运行了多次。同时打开邮箱可以看到多了两封新的邮件。 之所以有两封邮件是因为UV和PV的更新是分开提醒的。而运行了多次只收到两封是因为第一次运行的时候当前获取到的结果和之前的不一样,触发了提醒,而后续虽然也运行了脚本,但是变化并没有过万,所以没有提醒。也可以打开历史记录文件history.txt查看每次获取到的访问信息,如下。

这样,通过以上的脚本就可以解决了一开始的两个问题。我也再不需要人工监测博客的访问了,只需要等待接收提醒邮件即可,终于可以偷懒了。

6.参考资料

  • [1] https://www.jianshu.com/p/61ec46473884
  • [2] https://blog.csdn.net/yxwb1253587469/article/details/52233562
  • [3] https://blog.csdn.net/LeoPhilo/article/details/89074232
  • [4] https://blog.csdn.net/yi247630676/article/details/84135565
  • [5] https://segmentfault.com/a/1190000019459125
  • [6] https://blog.csdn.net/emtit2008/article/details/82287796

本文作者原创,未经许可不得转载,谢谢配合

返回顶部