新型冠状病毒感染的肺炎疫情爆发后,对人们的生活产生很大的影响。当前感染人数依然在不断变化。每天国家卫健委和各大新闻媒体都会公布疫情的数据,包括累计确诊人数、现有确诊人数等。
本案例使用Python开发网络爬虫,对新冠肺炎的疫情数据(包括当日的实时数据和历史数据)进行采集。
1. 选择数据源
自新冠肺炎(covid-19)疫情爆发以来,这场疫情几乎影响了每个人的生活,为了对疫情做数据分析,需要采集疫情的数据,本篇案例就基于python爬虫进行数据采集。
首先我们需要找到合适的数据源,卫健委和各媒体每天都会报道新冠肺炎的疫情数据,如下图所示,因此我们可以考虑将这些网页作为数据源。
以北京市和湖南省为例查看卫健委的数据,结果发现,两地卫健委的数据各式并不统一,北京市卫健委数据为文本,而湖南省卫健委数据为图片,并且由于各卫健委的网络地址还需要再花时间去寻找,因此卫健委的数据并不便于采集。
- 北京市卫健委疫情通报地址:
http://wjw.beijing.gov.cn/wjwh/ztzl/xxgzbd/gzbdyqtb/index_1.html
- 湖南省卫健委疫情通报地址:
http://wjw.hunan.gov.cn/wjw/qwfb/yqfkgz_list.html
我们考虑将新闻媒体的播报平台作为数据源,以网易的疫情播报平台为例,如下图所示可以看到它的数据内容非常丰富,不仅包括国内的数据还包括国外的数据,且作为大平台,公信度也比较高。因此我们选择网易的疫情实时动态播报平台作为数据源,其地址如下:
https://wp.m.163.com/163/page/news/virus_report/index.html?_nw_=1&_anw_=1
我们基于网易的实时播报平台寻找数据,由于它是一个实时的动态平台,因此数据一般在Network标签下可以找到,以Chrome浏览器为例展示寻找数据步骤:
- 访问网易实时疫情播报平台
https://wp.m.163.com/163/page/news/virus_report/index.html?_nw_=1&_anw_=1
- 在页面任意位置右键点击检查
- 进入Network标签下的XHR,此时可能会提示刷新,按下“Ctrl R”即可
在网页的检查页面内,左下角红框所示的部分是我们找到的数据源,基于这些地址进行爬取,我们获取数据的目标如下:
2. 初步探索
通过比对,我们发现在第二个地址中存放着关于疫情的数据,因此我们先对这个地址进行爬虫。
接下来找到其地址,点击headers后进行查看,在url中?后边为参数,可以不用设置,因此我们需要请求的地址为:https://c.m.163.com/ug/api/wuhan/app/data/list-total
,并且可以看到请求方法为get,同时查看自己浏览器的user-agent,使用requests请求时,设置user-agent伪装浏览器。
下来开始请求,首先导入使用的包,使用request进行网页请求,使用pandas保存数据。
代码语言:javascript复制import requests
import pandas as pd
import time
pd.set_option('max_rows',500)
设置请求头,伪装为浏览器
代码语言:javascript复制headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}
发起请求,将找到的第一个数据源作为请求目标。
代码语言:javascript复制url = 'https://c.m.163.com/ug/api/wuhan/app/data/list-total' # 定义要访问的地址
r = requests.get(url, headers=headers) # 使用requests发起请求
代码语言:javascript复制print(r.status_code) # 查看请求状态
200
代码语言:javascript复制print(type(r.text))
print(len(r.text))
<class 'str'> 301443
可以看到返回后的内容是一个几十万长度的字符串,由于字符串格式不方便进行分析,并且在网页预览中发现数据为类似字典的json格式,所以我们将其转为json格式。
代码语言:javascript复制import json
data_json = json.loads(r.text)
data_json.keys()
dict_keys(['reqId', 'code', 'msg', 'data', 'timestamp'])
可以看出在data
中存放着我们需要的数据,因此我们取出数据。
data = data_json['data'] # 取出json中的数据
data.keys()
dict_keys(['chinaTotal', 'chinaDayList', 'lastUpdateTime', 'areaTree'])
数据中总共有四个键,每个键存储着不同的内容:
接下来我们分别获取实时数据和历史数据。
键名称 | 数据内容 |
---|---|
chinaTotal | 全国当日数据 |
chinaDayList | 全国历史数据 |
lastUpdateTime | 更新时间 |
areaTree | 世界各地实时数据 |
3. 实时数据爬取
3.1 全国各省实时数据爬取
首先我们抓取全国各省的实时数据,在areaTree
键值对中,存放着世界各地的实时数据,areaTree
是一个列表,每一个元素都是一个国家的数据,每一个元素的children
是各国家省份的数据。 我们首先找到中国各省的实时数据,如下图所示。
data_province = data['areaTree'][2]['children'] # 取出中国各省的实时数据
代码语言:javascript复制type(data_province)
list
data_province是列表格式,每个元素是一个省的实时数据,并且为字典格式,每个省的键名称全部相同。
代码语言:javascript复制data_province[0].keys() # 查看每个省键名称
dict_keys(['today', 'total', 'extData', 'name', 'id', 'lastUpdateTime', 'children'])
各省数据键值含义如下:
键名称 | 数据内容 |
---|---|
today | 各省当日数据 |
total | 各省当日累计数据 |
extData | 无任何数据 |
name | 各省名称 |
id | 各省行政编号 |
lastUpdateTime | 更新时间 |
children | 各省下一级数据 |
for i in range(len(data_province)): # 遍历查看各省名称、更新时间
print(data_province[i]['name'],data_province[i]['lastUpdateTime'])
if i == 5:
break
湖北 2020-03-25 08:15:00 广东 2020-03-25 08:44:58 河南 2020-03-25 08:23:28 浙江 2020-03-25 09:01:13 湖南 2020-03-25 08:25:33 安徽 2020-03-25 08:38:12
用DataFrame以字典格式生成数据的例子,传入一个列表,列表每一个元素都是字典。
代码语言:javascript复制test_dict = [{'a':1,
'b':2,
'c':3,},
{'a':111,
'b':222}]
pd.DataFrame(test_dict)
代码语言:javascript复制pd.DataFrame(data_province).head() # 直接生成数据效果并不理想
不能直接生成DataFrame是因为数据中嵌套着字典,例如湖北省数据如下:标红线表示带有嵌套字典,篮筐内没有嵌套字典。
逐个拆解数据,我们已经了解children为下一地级数据,我们只分析到省单位,因此各省的children数据不采集;extData为空值,也不采集,最后具体采集方法如下:
不需要采集的数据:children、extData。 需要采集的数据:由于数据中today和total嵌套着字典,因此不能直接获取,对于id、lastUpdateTime、name、可以直接取出为一个数据,today为一个数据,total为一个数据,最后三个数据合并为一个数据。
代码语言:javascript复制# 获取id、lastUpdateTime、name
info = pd.DataFrame(data_province)[['id','lastUpdateTime','name']]
info.head()
列表推导式例子
代码语言:javascript复制l1 = [1,1,1,2,2,2]
代码语言:javascript复制[i 1 for i in l1 ]
[2, 2, 2, 3, 3, 3]
代码语言:javascript复制# 获取today中的数据
today_data = pd.DataFrame([province['today'] for province in data_province ])
today_data.head()
代码语言:javascript复制['today_' i for i in today_data.columns]
['today_confirm', 'today_dead', 'today_heal', 'today_severe', 'today_storeConfirm', 'today_suspect']
代码语言:javascript复制today_data.columns = ['today_' i for i in today_data.columns] # 由于today中键名和total键名相同,因此需要修改列名称
代码语言:javascript复制today_data.head()
代码语言:javascript复制# 获取total中的数据
total_data = pd.DataFrame([province['total'] for province in data_province ])
total_data.columns = ['total_' i for i in total_data.columns]
total_data.head()
代码语言:javascript复制pd.concat([info,total_data,today_data],axis=1).head() # 将三个数据合并
代码语言:javascript复制# 将提取数据的方法封装为函数
def get_data(data,info_list):
info = pd.DataFrame(data)[info_list] # 主要信息
today_data = pd.DataFrame([i['today'] for i in data ]) # 生成today的数据
today_data.columns = ['today_' i for i in today_data.columns] # 修改列名
total_data = pd.DataFrame([i['total'] for i in data ]) # 生成total的数据
total_data.columns = ['total_' i for i in total_data.columns] # 修改列名
return pd.concat([info,total_data,today_data],axis=1) # info、today和total横向合并最终得到汇总的数据
代码语言:javascript复制today_province = get_data(data_province,['id','lastUpdateTime','name'])
today_province.head()
代码语言:javascript复制def save_data(data,name): # 定义保存数据方法
file_name = name '_' time.strftime('%Y_%m_%d',time.localtime(time.time())) '.csv'
data.to_csv(file_name,index=None,encoding='utf_8_sig')
print(file_name ' 保存成功!')
代码语言:javascript复制time.strftime('%Y_%m_%d',time.localtime(time.time()))
'2020_03_25'
代码语言:javascript复制save_data(today_province,'today_province')
today_province_2020_03_25.csv 保存成功!
3.2 世界各国实时数据爬取
之前已经了解到在json数据data
中的areaTree
是列表格式,每个元素都是一个国家的实时数据,每个元素的children
是各国家省份的数据,现在我们提取世界各国实时数据。
areaTree = data['areaTree'] # 取出areaTree
代码语言:javascript复制areaTree[0] # 查看第一个国家的数据
{'children': [], 'extData': {'confirmNote': None, 'deadNote': None, 'healNote': None, 'incrConfirmNote': None, 'incrSevereNote': None, 'suspectNote': None}, 'id': '9577772', 'lastUpdateTime': '2020-03-25 00:00:31', 'name': '突尼斯', 'today': {'confirm': None, 'dead': None, 'heal': None, 'severe': None, 'storeConfirm': None, 'suspect': None}, 'total': {'confirm': 114, 'dead': 3, 'heal': 0, 'severe': 0, 'suspect': 0}}
areaTre
中每个键值的含义:
for i in range(len(areaTree)): # 查看各国家名称和更新时间
print(areaTree[i]['name'],areaTree[i]['lastUpdateTime'])
if i == 5:
break
突尼斯 2020-03-25 00:00:31 塞尔维亚 2020-03-25 03:57:40 中国 2020-03-25 09:52:07 日本 2020-03-24 15:26:25 泰国 2020-03-25 00:00:30 新加坡 2020-03-25 00:00:30
从areaTree
中提取每个国家的实时数据。
today_world = get_data(areaTree,['id','lastUpdateTime','name'])
today_world.head()
代码语言:javascript复制save_data(today_world,'today_world')
today_world_2020_03_25.csv 保存成功!
4.历史数据爬取
4.1 全国历史数据爬取
在数据data
中,总共有四个键,其中chinaDayList
存放着中国的历史数据,我们将其取出。
data.keys()
dict_keys(['chinaTotal', 'chinaDayList', 'lastUpdateTime', 'areaTree'])
代码语言:javascript复制chinaDayList = data['chinaDayList'] # 取出chinaDayList
chinaDayList中键值含义:
键名称 | 数据内容 |
---|---|
date | 日期 |
today | 当日数据 |
total | 累计数据 |
lastUpdateTime | 更新时间 |
type(chinaDayList) # 查看chinaDayList的格式
list
代码语言:javascript复制chinaDayList[0]
{'date': '2020-01-20', 'lastUpdateTime': None, 'today': {'confirm': 291, 'dead': 6, 'heal': 25, 'severe': 0, 'storeConfirm': None, 'suspect': 27}, 'total': {'confirm': 291, 'dead': 6, 'heal': 25, 'severe': 0, 'suspect': 54}}
可以看到today
、total
中也嵌套着字典,因此直接使用定义好的方法从chinaDayList
中提取全国历史数据。
alltime_China = get_data(chinaDayList,['date','lastUpdateTime'])
alltime_China.head()
代码语言:javascript复制save_data(alltime_China,'alltime_China')
alltime_China_2020_03_25.csv 保存成功!
4.2 全国各省历史数据爬取
找到第二个数据地址https://c.m.163.com/ug/api/wuhan/app/data/list-by-area-code?areaCode=420000
,在页面对比发现是湖北省的历史数据。
先以其中一个省为例,先尝试获取其历史数据,其他的省可以使用同样的方法。
代码语言:javascript复制url = 'https://c.m.163.com/ug/api/wuhan/app/data/list-by-area-code?areaCode=420000' # 定义数据地址
r = requests.get(url, headers=headers) # 进行请求
data_json = json.loads(r.text) # 获取json数据
代码语言:javascript复制data_json.keys()
dict_keys(['reqId', 'code', 'msg', 'data', 'timestamp'])
在list
中存放着数据。
data_json['data']['list'][0]
{'date': '2020-01-20', 'lastUpdateTime': None, 'today': {'confirm': 270, 'dead': 6, 'heal': 25, 'severe': None, 'storeConfirm': None, 'suspect': 0}, 'total': {'confirm': 270, 'dead': 6, 'heal': 25, 'severe': 0, 'suspect': 0}}
可以看出一个省一天的的数据格式如上,这和之前的数据结构一样,因此可以使用之前的方法得到数据。
代码语言:javascript复制data_test = get_data(data_json['data']['list'],['date'])
data_test['name'] = '湖北省'
data_test.head()
通过上述方法得到了湖北省的历史数据,想要得到每个省的历史数据怎么做呢?
在湖北省历史数据的地址中,我们发现参数aeraCode=420000
,而这刚好和全国各省实时数据today_province
中的id
对应.
today_province[['id','name']].head()
为了进一步确认,在百度上查找全国各省的行政代码,结果发现和数据today_province
中的id
这一列一致,因此id
这一列就是各省的行政代码。
因此为了得到每个省的历史数据,我们只需要将各省的行政代码作为参数传入这个地址即可,如下所示:
https://c.m.163.com/ug/api/wuhan/app/data/list-by-area-code?areaCode=各省行政代码
例如广东省历史数据的地址为:https://c.m.163.com/ug/api/wuhan/app/data/list-by-area-code?areaCode=440000
湖南省历史数据的地址为:https://c.m.163.com/ug/api/wuhan/app/data/list-by-area-code?areaCode=430000
但是数据中并没有显示省的名称,因此需要写入每个省的名称。
为了便于写入各省的名称,我们需要生成一个各省行政代码和省名称对应的字典。
以一个简单的数据为例,展示生成字典的方法。
代码语言:javascript复制a = ['1','2','3','4']
b = ['q','w','e','r']
for i,j in zip(a, b):
print(i,j)
1 q 2 w 3 e 4 r
代码语言:javascript复制{ i:j for i,j in zip(a, b)}
{'1': 'q', '2': 'w', '3': 'e', '4': 'r'}
代码语言:javascript复制province_dict = {num:name for num,name in zip(today_province['id'],today_province['name'])}
代码语言:javascript复制# 查看前五个内容
count = 0
for i in province_dict:
print(i,province_dict[i])
count = 1
if count == 5:
break
420000 湖北 440000 广东 410000 河南 330000 浙江 430000 湖南
每一个省的列名是相同的,因此多个省的数据合并起来就可以存入一个数据中,数据合并演示的例子如下:
代码语言:javascript复制df1 = pd.DataFrame([{'a':1,'b':2,'c':3,},{'a':111,'b':222}])
df1
代码语言:javascript复制df2 = pd.DataFrame([{'a':9,'b':8,'c':7,},{'a':345,'c':789}])
df2
代码语言:javascript复制df1 = pd.concat([df1,df2],axis=0)
df1
按照上述方法,得到每一个省的数据后,进行合并。
代码语言:javascript复制start = time.time()
for province_id in province_dict: # 遍历各省编号
try:
# 按照省编号访问每个省的数据地址,并获取json数据
url = 'https://c.m.163.com/ug/api/wuhan/app/data/list-by-area-code?areaCode=' province_id
r = requests.get(url, headers=headers)
data_json = json.loads(r.text)
# 提取各省数据,然后写入各省名称
province_data = get_data(data_json['data']['list'],['date'])
province_data['name'] = province_dict[province_id]
# 合并数据
if province_id == '420000':
alltime_province = province_data
else:
alltime_province = pd.concat([alltime_province,province_data])
print('-'*20,province_dict[province_id],'成功',
province_data.shape,alltime_province.shape,
',累计耗时:',round(time.time()-start),'-'*20)
# 设置延迟等待
time.sleep(10)
except:
print('-'*20,province_dict[province_id],'wrong','-'*20)
代码语言:javascript复制save_data(alltime_province,'alltime_province')
alltime_province_2020_03_25.csv 保存成功!
4.3 世界各国历史数据爬取
接着找到下一个数据地址,对比后发现是意大利数据,首先尝试爬取意大利的历史数据,其他国家的数据可以用同样的方法爬取。
代码语言:javascript复制url_italy = 'https://c.m.163.com/ug/api/wuhan/app/data/list-by-area-code?areaCode=15' # 意大利的数据地址
r = requests.get(url_italy, headers=headers) # 进行访问
italy_json = json.loads(r.text) # 导出json数据
代码语言:javascript复制italy_json.keys()
dict_keys(['reqId', 'code', 'msg', 'data', 'timestamp'])
代码语言:javascript复制italy_json['data'].keys()
dict_keys(['list'])
结合页面观察,发现各国家的历史数据结构和各省的类似。
代码语言:javascript复制italy_json['data']['list'][0] # 查看数据内容
{'date': '2020-01-31', 'lastUpdateTime': None, 'today': {'confirm': 2, 'dead': 0, 'heal': 0, 'severe': None, 'storeConfirm': None, 'suspect': 0}, 'total': {'confirm': 2, 'dead': 0, 'heal': 0, 'severe': 0, 'suspect': 0}}
每个国家的数据格式和之前各省的几乎一样,因此使用定义好的方法生成数据,然后把国家名称写入数据。
代码语言:javascript复制data_italy = get_data(italy_json['data']['list'],['date']) # 生成数据
data_italy['name'] = '意大利' # 写入国家名称
data_italy.head()
因为原始数据中没有国家名称,为了得到每个国家的名称,需要生成国家编号和国家名称的键值对,这样就可以存储国家名称,在之前的世界各国实时数据today_world
中有国家的编号和名称,可以用它来生成键值对。
today_world[['id','name']].head()
代码语言:javascript复制country_dict = {key:value for key,value in zip(today_world['id'], today_world['name'])}
代码语言:javascript复制# 查看前五个内容
count = 0
for i in country_dict:
print(i,country_dict[i])
count = 1
if count == 5:
break
9577772 突尼斯 9507896 塞尔维亚 0 中国 1 日本 2 泰国
通过每个国家的编号访问每个国家历史数据的地址,然后获取每一个国家的历史数据。
代码语言:javascript复制start = time.time()
for country_id in country_dict: # 遍历每个国家的编号
try:
# 按照编号访问每个国家的数据地址,并获取json数据
url = 'https://c.m.163.com/ug/api/wuhan/app/data/list-by-area-code?areaCode=' country_id
r = requests.get(url, headers=headers)
json_data = json.loads(r.text)
# 生成每个国家的数据
country_data = get_data(json_data['data']['list'],['date'])
country_data['name'] = country_dict[country_id]
# 数据叠加
if country_id == '9577772':
alltime_world = country_data
else:
alltime_world = pd.concat([alltime_world,country_data])
print('-'*20,country_dict[country_id],'成功',country_data.shape,alltime_world.shape,
',累计耗时:',round(time.time()-start),'-'*20)
time.sleep(10)
except:
print('-'*20,country_dict[country_id],'wrong','-'*20)
代码语言:javascript复制save_data(alltime_world,'alltime_world')
5.总结
本篇案例的主要内容是新冠肺炎疫情的数据采集,首先我们寻找合适的数据源,最终选择网易的实时疫情播报平台作为数据源,然后逐步地解析找到我们需要的数据,最后通过python爬虫获得我们得到了疫情的数据,包括中国各省和世界各国的实时数据,还包括中国整体、中国各省、世界各国的历史数据。通过这样一篇数据采集的案例让同学们学习到爬虫的基本方法,掌握这些方法能够提升数据采集的能力,并且可以使用在以后的学习和工作中。
注意:因为爬虫的对象是网页上的数据,如果这些网页的数据内容有所变动可能会导致采集无效。