Python爬取疫情实战:Flask搭建web/Echarts可视化大屏/MySQL数据库/Linux项目部署与任务定时调度
展示地址
总的来说这次的项目还是遇到了不少坑,有的地方调了好久。
首先这个项目就是要会使用爬虫,能去各大巨头那拿到数据,先复习一下
urllib的使用
from urllib import request
header = {
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36"
}
url = "http://www.dianping.com"
req = request.Request(url, headers = header)
res = request.urlopen(req) #获取响应
print(res.info()) #响应头
print(res.getcode()) #状态码 2xx:正常 3xx:发生了重定向 4xx:访问资源有问题 5xx:服务器出错
print(res.geturl()) #返回响应地址
html = res.read()
# print(html)
html = html.decode("utf-8")
print(html)
requests的使用
import requests
header = {
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36"
}
url = "http://www.baidu.com"
res = requests.get(url, headers = header)
print(res.encoding)
print(res.headers) # 里面如果没有 Content-Type, encoding = utf-8,否则如果有charset 则以设置为准, 否则 为 IOS-8859-1
print(res.url)
res.encoding = "utf-8"
print(res.text)
print(res.status_code)
Beautifulsoup的简单使用
import re
import requests
from bs4 import BeautifulSoup
url = "http://wsjkw.sc.gov.cn/scwsjkw/gzbd/fyzt.shtml"
res = requests.get(url)
res.encoding = "utf-8"
html = res.text
soup = BeautifulSoup(html)
a = soup.find("a")
print(a)
print(a.attrs["href"])
url_new = "http://wsjkw.sc.gov.cn" + a.attrs["href"]
res = requests.get(url_new)
res.encoding = "utf-8"
soup = BeautifulSoup(res.text)
content = soup.find_all("p")
print(content)
text = content[1].text
patten = "新增新型冠状病毒肺炎确诊病例(\d+)例"
print(text)
res = re.search(patten, text)
print(res)
# .*后面的 ? 表示非贪心匹配
patten = "新增新型冠状病毒肺炎确诊病例(\d+).*?出院病例(\d+).*?疑似病例(\d+)?.*?死亡病例(\d+)?"
# print(text)
res = re.search(patten, text)
print(res)
# print(res.groups())
爬取腾讯的疫情数据
爬取这个数据最重要的就是解析数据包的结构,说实话这个结构包有一点复杂,花了我不少时间
import urllib.request as rq
import pymysql #链接数据库
import requests
from bs4 import BeautifulSoup
import json
import time
import traceback #追踪异常
url_today = "https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5"
url_history = "https://view.inews.qq.com/g2/getOnsInfo?name=disease_other"
headers = {
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36"
}
def get_tencent_data():
"""
return 返回历史数据和当日详细数据
"""
# 获取历史数据
r = requests.get(url_history, headers)
r.encoding = "utf-8"
res = json.loads(r.text) # json字符串转成字典
data_history = json.loads(res["data"]) #获取数据
'''
数据包结构如下
data_history
cityStatis
chinaDayList
confirm
suspect
dead
heal
nowConfirm
nowSevere
importedCase
deadRate
healRate
date
y
noInfect
localConfirm
noInfectH5
localConfirmH5
local_acc_confirm
chinaDayAddList
confirm
suspect
dead
heal
importedCase
infect
localinfectionadd
localConfirmadd
deadRate
healRate
date
y
provinceCompare
nowConfirmStatis
statisGradeCityDetail
dailyNewAddHistory
dailyHistory
wuhanDayList
articleList
'''
history_data = {}
for every_data in data_history["chinaDayList"]:
date = every_data["y"] + "." + every_data["date"]
tup = time.strptime(date, "%Y.%m.%d")
date = time.strftime("%Y-%m-%d", tup) # 改变时间格式,不然插入数据库会报错,数据库是datetime类型
confirm = every_data["confirm"]
suspect = every_data["suspect"]
dead = every_data["dead"]
heal = every_data["heal"]
# nowConfirm = every_data["nowConfirm"]
history_data[date] = {"confirm":confirm, "suspect":suspect, "dead":dead, "heal":heal}
for every_data_add in data_history["chinaDayAddList"]:
date_add = every_data["y"] + "." + every_data_add["date"]
tup_add = time.strptime(date_add, "%Y.%m.%d")
date_add = time.strftime("%Y-%m-%d", tup_add) # 改变时间格式,不然插入数据库会报错,数据库是datetime类型
confirm_add = every_data_add["confirm"]
suspect_add = every_data_add["suspect"]
dead_add = every_data_add["dead"]
heal_add = every_data_add["heal"]
# g更新,update函数可以添加新的键值对
print(date_add)
if str(date_add) in history_data.keys():
history_data[date_add].update({"confirm_add":confirm_add, "suspect_add":suspect_add, "dead_add":dead_add, "heal_add":heal_add})
#获取当日数据
r = requests.get(url_today, headers)
r.encoding = "utf-8"
res = json.loads(r.text) # json字符串转成字典
data_today = json.loads(res["data"]) #获取数据
'''
数据包结构如下
data_today
lastUpdateTime 上次更新时间
chinaTotal 中国总计
chinaAdd 中国新增
isShowAdd 是否展示
showAddSwitch 展示信息开关
areaTree :
name 中国数据
today
total
children :
-name 省级数据
-today
-total
-children:
-name 市级数据
-today
-total
'''
details = []
update_time = data_today["lastUpdateTime"]
data_country = data_today["areaTree"]
data_province = data_country[0]["children"]
for pro_infos in data_province:
province = pro_infos["name"]
for city_infos in pro_infos["children"]:
city = city_infos["name"]
confirm = city_infos["total"]["confirm"]
confirm_add = city_infos["today"]["confirm"]
heal = city_infos["total"]["heal"]
dead = city_infos["total"]["dead"]
details.append([update_time, province, city, confirm, confirm_add, heal, dead])
return history_data, details
链接数据库,将数据库可持久化
爬取百度热搜方法一致,不再赘述
# 连接数据库
def get_conn():
#return:链接, 游标
#获取链接
conn = pymysql.connect(host = "ip",
port = 3306,
user = "root",
password = "123456",
db = "cov"
)
# 创建游标:
cursor = conn.cursor()
return conn, cursor
def close_conn(conn, cursor):
if cursor:
cursor.close()
if conn:
conn.close()
def update_details():
cursor = None
conn = None
try:
detail_data = get_tencent_data()[1] # 0是字典数据 1是列表数据
conn, cursor = get_conn()
sql = "insert into details(update_time,province,city,confirm,confirm_add,heal,dead) values(%s,%s,%s,%s,%s,%s,%s)"
sql_query = 'select %s=(select update_time from details order by id desc limit 1)' #对比当前最大时间戳,拿到最后一条数据
cursor.execute(sql_query, detail_data[0][0])
if not cursor.fetchone()[0]:
print(f"{time.asctime()}开始更新最新数据")
for item in detail_data:
cursor.execute(sql, item)
conn.commit() # 提交事务 update delete insert操作
print(f"{time.asctime()}更新最新数据完毕")
else:
print(f"{time.asctime()}已是最新数据!")
except:
traceback.print_exc()
finally:
close_conn(conn, cursor)
def update_history():
cursor = None
conn = None
try:
dic = get_tencent_data()[0]
print(f"{time.asctime()}开始更新历史数据")
conn, cursor = get_conn()
sql = "insert into history values(%s,%s,%s,%s,%s,%s,%s,%s,%s)"
sql_query = "select confirm from history where ds=%s"
for key, value in dic.items():
# item 格式 {'2020-01-13': {'confirm': 41, 'suspect': 0, 'heal': 0, 'dead': 1}
if not cursor.execute(sql_query, key):
cursor.execute(sql, [key, value.get("confirm"), value.get("confirm_add"), value.get("suspect"),
value.get("suspect_add"), value.get("heal"), value.get("heal_add"),
value.get("dead"), value.get("dead_add")])
conn.commit()
print(f"{time.asctime()}历史数据更新完毕")
except:
traceback.print_exc()
finally:
close_conn(conn, cursor)
if __name__ == '__main__':
update_details()
update_history()
前端页面和后端路由的实现
路由使用flask框架可以简单实现
from flask import Flask
app = Flask(__name__)
@app.route('/') #路由
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run(0.0.0.0)
然后前端使用ajax刷新页面,比如时间的版块就可以用定时调度(每秒)这个实现
function gettime() {
$.ajax({
url: "/time",
timeout: 10000, //超时时间设置为10秒;
success: function(data) {
$("#time").html(data)
},
error: function(xhr, type, errorThrown) {
}
});
}
Echarts可视化大屏
这个可以通过Echarts官网找到对应的示例进行修改,然后从数据库读取数据实现
示例如下:需要Echarts.min.js和China.js
var ec_center = echarts.init(document.getElementById('c2'), "dark");
var mydata = [{'name': '上海', 'value': 318}, {'name': '云南', 'value': 162}];
var ec_center_option = {
title: {
text: '',
subtext: '',
x: 'left'
},
tooltip: {
trigger: 'item'
},
//左侧小导航图标
visualMap: {
show: true,
x: 'left',
y: 'bottom',
textStyle: {
fontSize: 12,
},
splitList: [{ start: 1,end: 9 },
{start: 10, end: 99 },
{ start: 100, end: 999 },
{ start: 1000, end: 9999 },
{ start: 10000 }],
color: ['#8A3310', '#C64918', '#E55B25', '#F2AD92', '#F9DCD1']
},
//配置属性
series: [{
name: '累计确诊人数',
type: 'map',
mapType: 'china',
roam: false, //拖动和缩放
itemStyle: {
normal: {
borderWidth: .5, //区域边框宽度
borderColor: '#009fe8', //区域边框颜色
areaColor: "#ffefd5", //区域颜色
},
emphasis: { //鼠标滑过地图高亮的相关设置
borderWidth: .5,
borderColor: '#4b0082',
areaColor: "#fff",
}
},
label: {
normal: {
show: false, //省份名称
color: 'black',
fontSize: 12,
},
emphasis: {
show: true,
fontSize: 12,
}
},
data: [] //mydata //数据
}]
};
ec_center.setOption(ec_center_option);
将项目部署到服务器上
首先,推荐使用docker进行部署,这样比较安全,不会影响到服务器上的其它项目。
首先就是mysql,docker有专门的mysql镜像,我一开始想在一个docker内部装,死活装不上
后来查了一些资料发现,一般mysql是单独开一个docker部署的,于是我按照网上的教程部署好了mysql,将本地数据移植过去,开始进行试验。
这里强调一点,一定要记得去云服务器上打开对应的端口,不然外部无法访问。然后,我很顺利地链接上了数据库。
接下来,由于我不熟悉ftps,我只能去寻求软件来从Windows传输文件到linux服务器。开始我也找了许多软件,要不就是收费,要不就是不好用
收费的破解也有点麻烦,总之我没能按照网上教程破解成功。最后我突然想到我们金仓数据库上用的MobaXterm就有这个功能,我拿过来一实验,果然可以。
于是我赶紧拿我的Django镜像创建了一个docker,端口映射10000:22,准备把本地的代码文件传上去跑起来。传过去了之后,我立马 chmod +x app.py,准备运行,结果,报错了。
稍微一看,我没有加标注信息,于是加上,后来又发现没下包,于是把包也安装上。
但是,woc,又报错了
Python——/usr/bin/env: ‘python(3)\r’: No such file or directory
异常原因是:
DOS系统下和Linux系统下对于换行键的表示不同。
在windows下,用连续的'\r'和'\n'两个字符进行换行。'\r'为回车符,'\n'为换行符,比如原来的'aaabbb'更改为'aaa \n bbb'后输出的结果为:aaa 换行 bbb。
解决方式:
看了很多网上的教程,说用VIM编辑器进行修改,由于我不是很常用VIM编辑器,尝试了几次并没有成功。后来发现可以用dos2unix这个包进行转换。
首先通过apt-get命令安装dos2unix的包,然后通过dos2unix这个命令即可完成转换。
sudo apt-get install dos2unix
dos2unix app.py
好,这下不报错了,开始运行!
确实,程序跑起来了,然后我app.run()直接跑的,下面默认地址是127.0.0.1,端口是5000
我通过我自己电脑浏览器去访问我的服务器ip:5000
结果啥也没访问到
我以为我搞错了,然后取访问ip:10000
嗯,服务器返回了无效的页面,我百思不得其解
于是我搜了一下,改成了app.run(host='0.0.0.0', port=10000)
继续访问,woc!还是访问不了。
又是网上查资料查了几个小时,还是没找出原因
于是我退回到我的服务器上(原本在服务器的docker容器内)
用最简单的flask代码搭了个hello world页面
app.run(host='0.0.0.0', port=10000),然后进行测试
woc!可以访问。
后来仔细检查发现,我创建docker镜像的时候,只开了一个端口而且映射到了22的ssh端口,根本没有开方其它端口
最后,我重新建立了一个docker,把东西重新导了过去(操作有点low),终于,可以访问了
然后记得将代码改成部署到生产环境
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return"Hello!"
if __name__ =="__main__":
from waitress import serve
serve(app, host="0.0.0.0", port=8080)
将爬虫部署到服务器上并且定时执行
我将我本地的爬虫传到服务器后,安装了对应的包
然后,遇到的大问题就是selenium的问题,我原来是用selenium写的爬取百度热搜
但是当这个放到服务器上去之后,就跑不了了,为什么?因为我没有安装Chrome
网上搜了一下,要用yum安装,我用的是Ubuntu,不支持yum,试了几下没弄成,我只好作罢
还好,我发现用最普通的方式也能爬到信息,于是我重写了这一部分,没能解决这个坑
最后,就是使用crontab定时执行爬虫就行。
补充
这里对于echarts再补充一点
按照上述情况将程序部署好之后,确实是能正常访问了,但是一旦页面缩放,问题就来了
图形要么就重叠,要么就出现了空缺位置,显然是无法接受的,因为练窗口化都会出现这个问题
然后我也找了一些同学来访问网页,发现有时候还会出现页面一直抖动的情况
解决方式:
抖动的话,在页面的css文件中加入
html, body {height:100%;overflow:auto;margin: 0;}
html{overflow-y:scroll;}
echarts图表自适应div大小
你可以在每个图表的setOption后
添加
window.addEventListener('resize', function () {myChart.resize();})