使用Python爬取COVID-19疫情数据

2020-04-27 14:14:17 浏览数 (1)

视频内容

新型冠状病毒感染的肺炎疫情爆发后,对人们的生活产生很大的影响。当前感染人数依然在不断变化。每天国家卫健委和各大新闻媒体都会公布疫情的数据,包括累计确诊人数、现有确诊人数等。

本案例使用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中存放着我们需要的数据,因此我们取出数据。

代码语言:javascript复制
data = data_json['data'] # 取出json中的数据
data.keys()

dict_keys(['chinaTotal', 'chinaDayList', 'lastUpdateTime', 'areaTree'])

数据中总共有四个键,每个键存储着不同的内容:

接下来我们分别获取实时数据和历史数据。

键名称

数据内容

chinaTotal

全国当日数据

chinaDayList

全国历史数据

lastUpdateTime

更新时间

areaTree

世界各地实时数据

3. 实时数据爬取

3.1 全国各省实时数据爬取

首先我们抓取全国各省的实时数据,在areaTree键值对中,存放着世界各地的实时数据,areaTree是一个列表,每一个元素都是一个国家的数据,每一个元素的children是各国家省份的数据。 我们首先找到中国各省的实时数据,如下图所示。

代码语言:javascript复制
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

各省下一级数据

代码语言:javascript复制
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是各国家省份的数据,现在我们提取世界各国实时数据。

代码语言:javascript复制
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中每个键值的含义:

代码语言:javascript复制
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中提取每个国家的实时数据。

代码语言:javascript复制
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存放着中国的历史数据,我们将其取出。

代码语言:javascript复制
data.keys()

dict_keys(['chinaTotal', 'chinaDayList', 'lastUpdateTime', 'areaTree'])

代码语言:javascript复制
chinaDayList = data['chinaDayList'] # 取出chinaDayList

chinaDayList中键值含义:

键名称

数据内容

date

日期

today

当日数据

total

累计数据

lastUpdateTime

更新时间

代码语言:javascript复制
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}}

可以看到todaytotal中也嵌套着字典,因此直接使用定义好的方法从chinaDayList中提取全国历史数据。

代码语言:javascript复制
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中存放着数据。

代码语言:javascript复制
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对应.

代码语言:javascript复制
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中有国家的编号和名称,可以用它来生成键值对。

代码语言:javascript复制
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爬虫获得我们得到了疫情的数据,包括中国各省和世界各国的实时数据,还包括中国整体、中国各省、世界各国的历史数据。通过这样一篇数据采集的案例让同学们学习到爬虫的基本方法,掌握这些方法能够提升数据采集的能力,并且可以使用在以后的学习和工作中。

注意:因为爬虫的对象是网页上的数据,如果这些网页的数据内容有所变动可能会导致采集无效。

0 人点赞