文章目录
- 1. 前言
- 2. 数据抓取——分享一个调度服务框架
- 3. 数据处理——基于Numpy的预处理
- 3.1 读取指定编号博主的投票数据
- 3.2 取得指定时刻得票数量前n名的博主序号
- 4. 数据分析——基于matplotlib绘图
- 4.1 绘制指定编号博主的投票曲线
- 4.2 绘制指定编号博主的10分钟投票增量曲线
- 4.3 将多位博主得票数据绘制在一张图上
- 4.4 绘制日增量柱状图,打印markdown格式的位次变化表
- 5. 趋势预测——最小二乘法拟合多项式
1. 前言 万众瞩目的CSDN博客之星总评选投票活动渐入佳境,竞争趋于白热化。入选前200名的博主们火力全开,使出了浑身解数,通过各种渠道拉票。一时间,CSDN刷爆了各大自媒体。无论是吃瓜群众,还是摇旗呐喊、擂鼓助威的亲友团,无不赞叹:CSDN这一波广告创意,真高!
俗话说,外行看热闹,内行看门道。姑且让亲友团、粉丝团飞一会儿,我来给吃瓜群众介绍一下,如何用数据分析的手段,剖析今年的CSDN博客之星总评选,并用机器学习的方式预测后续的竞争结果。本文纯粹出于技术交流之目的,绝无恶意揣测诽谤他人之意图;所谓预测,亦出于游戏之心态,只为博同学们一笑耳。
从17日开始,关心CSDN博客之星总评选的朋友们,可以点击“2019年CSDN博客之星总评选投票走势”查看实时的数据统计结果了。该站点数据来源于本次活动的官方网站,未经任何修改,亦无任何观点倾向,仅供参考。2. 数据抓取——分享一个调度服务框架 做数据分析,首先得有数据。看2019年CSDN博客之星总评选,就要从活动网站上抓取数据。抓数据不难,同学们都会,但要抓历史数据,就需要费功夫了。因为活动网站上没有提供历史数据下载服务,我们只能不断地以固定时间间隔访问网站,并将数据记录下来。这就需要一个长期工作的服务程序。通常,执行定时任务的服务程序,都是基于调度服务的框架。
APScheduler 是我最喜欢的一个用于调度服务的模块,其全称是 Advanced Python Scheduler。这是一个轻量级的 Python 定时任务调度框架,功能非常强大。单说这个模块的话,洋洋洒洒可以写万字以上,但本文的重点不是 APScheduler,所以这里直接分享一个APScheduler 的应用实例。虽然只有区区70余行代码,依然非常实用、健壮。APScheduler 的安装很简单:
python -m pip install apscheduler下面的程序启动之后,每10分钟从2019年CSDN博客之星总评选活动网站抓取一次数据,以博主编号为文件名,保存在和服务程序同级的 data 文件夹下,文件格式如下:
天元浪子【用数据分析的手段,看2019年CSDN博客之星总评选】scheduler_catch.py
2020-01-15 11:10:00,1,10512
2020-01-15 11:20:00,1,10517
2020-01-15 11:30:00,1,10527
… …
# coding:utf-8"""定时数据抓取服务"""import os, re, json, time
from datetime import datetime
import urllib.request
from apscheduler.schedulers.blocking import BlockingSchedulerdef start_service():
"""启动数据抓取调度服务"""scheduler = BlockingScheduler()
scheduler.add_job(TimeJobHandler,
args = (),
trigger = "cron",
second = "0",
minute = "0/10",
hour = "*",
day = "*",
month = "*",
day_of_week = "*",
year = "*",
misfire_grace_time = 60
)scheduler.start()def TimeJobHandler():
url = "http://m234140.nofollow.ax.mvote.cn/action/viewvotewxorderlist.html?voteguid=43ced329-3a4b-0a5d-a13c-f088cf8eafef"
res = urllib.request.urlopen(url)
now = datetime.now()
html = res.read().decode("utf-8")
itemMatch(html, now)def itemMatch(content, now):
cwd = os.getcwd()
path = os.path.join(cwd, "data")
if not os.path.isdir(path):
os.makedirs(path)content = content.replace("\r\n", "").replace("\n", "").replace("\r", "").replace("\"", "'")
p = re.compile("第(\d+)名")
col0 = p.findall(content) # 第N名
p = re.compile("(.*?)", )
col1 = p.findall(content) # 当前名次
p = re.compile("(\d+)票 ")
col2 = p.findall(content) # 当前票数newName = []
for i, name in enumerate(col1):
if "(点此进入个人页)" in name:
ns = name.split(u"(")
name = ns[0]
name = name.strip()
ns = name.split(".")
num = ns[0]
truename = "".join(ns[1:]).strip()
filename = os.path.join(path, num + ".txt")if os.path.isfile(filename):
f = open(filename, 'a')
else:
f = open(filename, 'w')
f.write(truename + "\n")f.write(now.strftime("%Y-%m-%d %X") + "," + col0[i] + "," + col2[i] + "\n")
f.close()if __name__ == '__main__':
start_service()
3. 数据处理——基于Numpy的预处理 数据处理过程中导入用到两个模块,先统一写在这里,后面讲解时就不再处处导入了:
import numpy as np
from datetime import datetime, timedelta
3.1 读取指定编号博主的投票数据
def read_by_no(no, start_dt, end_dt):
"""读取指定编号博主的投票数据"""with open('data/%d.txt'%no, 'r', encoding='utf-8') as fp:
lines = fp.readlines()name = lines[0].strip()
dt_list = list()
rank_list = list()
votes_list = list()
for line in lines[1:]:
dt, rank, votes = line.strip().split(',')
if (not start_dt or start_dt and dt >= start_dt) and (not end_dt or end_dt and dt <= end_dt):
dt_list.append(datetime.strptime(dt, '%Y-%m-%d %H:%M:%S'))
rank_list.append(int(rank))
votes_list.append(int(votes))return name, dt_list, rank_list, votes_list
3.2 取得指定时刻得票数量前n名的博主序号
def get_top(n, end_dt):
"""取得end_dt时刻前n名序号"""ranks = list()
for i in range(1, 202):
with open('data/%d.txt'%i, 'r', encoding='utf-8') as fp:
lines = fp.readlines()for j in range(1, len(lines)):
dt, rank, votes = lines[j].strip().split(',')
if dt > end_dt:
break
dt, rank, votes = lines[j-1].strip().split(',')
ranks.append((i, int(rank)))ranks.sort(key=lambda x:x[1])
return [item[0] for item in ranks][:n]
4. 数据分析——基于matplotlib绘图 绘图函数需要导入matplotlib模块,并设置中文字体。先统一写在这里,后面讲解时就不再处处导入了:
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import matplotlib.ticker as tickerplt.rcParams['font.sans-serif'] = ['FangSong']# 设置默认字体
plt.rcParams['axes.unicode_minus'] = False# 解决保存图像时'-'显示为方块的问题
4.1 绘制指定编号博主的投票曲线
def plot_votes(no, start_dt=None, end_dt=None):
"""绘制指定编号博主的投票曲线"""name, dt_list, rank_list, votes_list = read_by_no(no, start_dt, end_dt)plt.figure('2019年CSDN博客之星总评选统计图表', facecolor='#f4f4f4', figsize=(15, 8))
plt.title('2019年CSDN博客之星总评选%s10分钟得票数量统计曲线'%name, fontsize=20)
plt.grid(linestyle=':') # 辅助网格
plt.plot(dt_list, votes_list) # 绘制数据
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M')) # 格式化时间轴标注
plt.gcf().autofmt_xdate() # 优化标注(自动倾斜)
#plt.savefig('image/得票数量统计曲线.png') # 保存为文件
plt.show()plot_votes(168)
绘制效果如下:
文章图片
4.2 绘制指定编号博主的10分钟投票增量曲线
def plot_delta(no, start_dt=None, end_dt=None):
"""绘制指定编号博主的10分钟投票增量曲线"""name, dt_list, rank_list, votes_list = read_by_no(no, start_dt, end_dt)plt.figure('2019年CSDN博客之星总评选统计图表', facecolor='#f4f4f4', figsize=(15, 8))
plt.title('2019年CSDN博客之星总评选%s10分钟投票增量统计曲线'%name, fontsize=20)
plt.grid(linestyle=':') # 辅助网格
votes_list.insert(0, votes_list[0])
votes_list = np.diff(np.array(votes_list))
plt.plot(dt_list, votes_list, color='g') # 绘制数据
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M')) # 格式化时间轴标注
plt.gcf().autofmt_xdate() # 优化标注(自动倾斜)
#plt.savefig('image/投票增量曲线.png') # 保存为文件
plt.show()sdt = '2020-01-13 00:00:00'
edt = '2020-01-15 00:00:00'
plot_delta(168, start_dt=sdt, end_dt=edt)
绘制效果如下:
文章图片
4.3 将多位博主得票数据绘制在一张图上 函数 get_top(n, end_dt) 返回 end_dt 时刻得票数量排名前 n 位的博主的序号列表。遍历这个列表,我们可以把多位博主的投票曲线或者投票增量曲线,画在同一张图上。这里就不一一给出代码了,直接贴出效果图:
文章图片
文章图片
4.4 绘制日增量柱状图,打印markdown格式的位次变化表 接下来,我们再学习画柱状图,用以分析TOP20的博主们每天的投票增量。
def plot_votes_delta(n, dt):
"""绘制日增量柱状图,打印markdown格式的位次变化表"""last_dt = datetime.strptime(dt, '%Y-%m-%d %H:%M:%S') - timedelta(days=1)
last_dt = last_dt.strftime('%Y-%m-%d %X')
no_list = get_top(n, dt)
names, dv = list(), list()
table_str = '|编号|博主|位次|升降|总得票数|日增票数|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n'
for i in range(n):
no = no_list[i]
name, dt_list, rank_list, votes_list = read_by_no(no, last_dt, dt)
dvotes = votes_list[-1]-votes_list[0]
drank = rank_list[0]-rank_list[-1]if drank > 0:
drank_str = '↑%d'%drank
elif drank < 0:
drank_str = '↓%d'%abs(drank)
else:
drank_str = '-'table_str += '|%d|%s|%d|%s|%d|%d|\n'%(no, name, i+1, drank_str, votes_list[-1], dvotes)
names.append(name)
dv.append(dvotes)print(table_str)color=[(np.random.random(),np.random.random(),np.random.random()) for i in range(n)]
plt.figure('2019年CSDN博客之星总评选统计图表', facecolor='#f4f4f4', figsize=(15, 12))
plt.title('2019年CSDN博客之星总评选TOP%d(截至%s)日增投票数量柱状图'%(n, dt), fontsize=20)
plt.barh(names[::-1], dv[::-1], align="center", height=0.5, alpha=1.0, color=color)
plt.gcf().autofmt_xdate()
plt.savefig('image/日增投票数量柱状图%s.png'%dt[:10])
plt.show()sdt = '2020-01-13 00:00:00'
edt = '2020-01-15 00:00:00'
plot_votes_delta(20, edt)
绘制效果如下:
文章图片
markdown格式的位次变化表:
编号 | 博主 | 位次 | 升降 | 总得票数 | 日增票数 |
---|---|---|---|---|---|
168 | 天元浪子 | 1 | - | 9770 | 1795 |
22 | Eastmount | 2 | - | 8366 | 1062 |
25 | Programer Cat | 3 | - | 7597 | 1357 |
23 | _YourBatman | 4 | ↑3 | 7037 | 1806 |
176 | 小傅哥 | 5 | ↓1 | 6994 | 1513 |
200 | DrogoZhang | 6 | ↓1 | 6803 | 1436 |
127 | Mike__Jiang | 7 | ↓1 | 6741 | 1503 |
57 | lilongsy | 8 | ↑3 | 6166 | 1577 |
201 | 刘望舒 | 9 | - | 6158 | 1197 |
95 | 敖丶丙 | 10 | - | 6033 | 1234 |
21 | 程序猿DD | 11 | ↑1 | 5910 | 1883 |
79 | 沉默王二 | 12 | ↓4 | 5882 | 683 |
76 | 唯有坚持不懈 | 13 | ↑2 | 5037 | 1258 |
82 | 人工智能博士 | 14 | - | 4597 | 776 |
68 | 十步杀一人_千里不留行 | 15 | ↓2 | 4349 | 387 |
20 | 段智华 | 16 | - | 4094 | 915 |
69 | 不脱发的程序猿 | 17 | - | 3748 | 1016 |
66 | lynnlovemin | 18 | - | 3528 | 862 |
103 | Vam的金豆之路 | 19 | - | 3120 | 578 |
70 | 狂野小青年 | 20 | ↑2 | 3072 | 741 |
def predict(no, start_dt, end_dt):
"""趋势预测"""name, dt_list, rank_list, votes_list = read_by_no(168, start_dt, end_dt)
_y = votes_list = np.array(votes_list)
_x = np.arange(_y.shape[0])
x = np.arange(_y.shape[0]+144) # 预测24小时后得票数量g3 = np.poly1d(np.polyfit(_x, _y, 3))
g4 = np.poly1d(np.polyfit(_x, _y, 4))loss3 = np.sum(np.square(g3(_x)-_y))/_y.shape[0]
loss4 = np.sum(np.square(g4(_x)-_y))/_y.shape[0]plt.figure('2019年CSDN博客之星总评选统计图表', facecolor='#f4f4f4', figsize=(15, 8))
plt.title('2019年CSDN博客之星总评选%s24小时趋势预测'%name, fontsize=20)
plt.plot(_x, _y, label='原始数据')
plt.plot(x, g3(x), label='3次多项式,预测24小时后得票数量:%d'%int(g3(x)[-1]))
plt.plot(x, g4(x), label='4次多项式,预测24小时后得票数量:%d'%int(g4(x)[-1]))
plt.plot(_x, g3(_x), label='3次多项式,误差%0.4f'%loss3)
plt.plot(_x, g4(_x), label='4次多项式,误差%0.4f'%loss4)plt.legend()
plt.show()
经过比较,4次多项式拟合结果误差最小。预测结果如下:
文章图片
既然都读到这里了,就给168号博主天元浪子投上5票吧,谢谢!
文章图片