抽空之余,写个小脚本,获取下上海详细的疫情数据,以作后续的详实数据分析(纯爱好),或者仅仅作为对历史的一种数据样本式的保存也未尝不可,顺便吧,缓解或者平复下情绪。
本文主要是文章 https://blog.csdn.net/yifengchaoran/article/details/124361581 的完结篇,主要用来展示和说明核心的tools文件(文件如何命名其实随意),核心是对文章内的数据进行初步解析、清洗、分组,最终按照固定格式提取。
文本内贴的所有函数,都是在tools.py文件内,因为比较长,所以采用一个一个函数贴,并进行说明。如果读者能一步一步跟着分析(建议打开文章链接并查看源代码,分析具体文章格式,结合本文内的XPATH源代码进行分析,才能对代码有更好的理解),相信对Python知识会有更深的理解。
最后,如果读者不想对代码实现做详细的了解,可以直接下载源代码,在本地直接运行main.py即可,并且里面也包含了笔者搞好的数据(但是需要会shelve才能用)。下载链接:上海疫情数据及源代码实现-Python文档类资源-CSDN下载
开始正题
from lxml import etree
import requests,re,shelve,pathlib
def wash_region_data_item(item):
'''
将文章中提取的每个区上报的数据,格式进行清洗及统一,因为发现16个区上报的数据格式并不太一致
'''
#首先将返回的每个区内的每个上报数据,统一替换符号
if '居住于' in item:
item=item.replace('和',',')
else:
item=item.replace('。',',').replace('、',',').replace(',',',')
item=item.replace('\xa0','').strip()
#然后item内,如果没有,,则程序加上这个符号,便于后面统一处理(使用,对每个区上报的社区数据切割成列表)
if ',' not in item:
item=item+','
return item
#下面这个小不点,主要是用来对4月5日之前的数据进行初步处理
def wash_item(item):
item=item.xpath('span//text()')
item=''.join(item)
return item
在实际分析的时候,发现文章内,每日每个区上报的社区数据,有些包含居住于,有些没有,还有些竟然包含非ascii码的东西,另外。有些有有些没有,所以以上函数主要是对这些乱七八糟的数据进行统一清洗、格式化,便于后面切割。
def wash_region_data_list(region_data_list):
#首先将列表内的每项字符串统一清洗格式化
region_data_list=[wash_region_data_item(item) for item in region_data_list]
#然后再对数据列表连接起来,并重新使用,分割成新的列表
if ':' in ''.join(region_data_list):
data_list=''.join(region_data_list).split(':')
region_data_list=[data_list[0]]+data_list[1].split(',')
elif '于' in ''.join(region_data_list):
data_list=''.join(region_data_list).split('于')
region_data_list=[data_list[0]]+data_list[1].split(',')
else:
region_data_list=[''.join(region_data_list)]
#然后再去除新数据列表内不需要的列表项,因为使用的是pop,为了避免清洗遗漏,故使用了循环
need_wash_flag=True
while need_wash_flag:
for index,data in enumerate(region_data_list):
count=0
#如果为空或包含消毒措施,则移除该列表项
if data == '' or '消毒措施' in data or '来源' in data:
region_data_list.pop(index)
count+=1
if count==0:
need_wash_flag=False
#返回每个区上报的社区名称组成的列表
return region_data_list
同样的,本来提取每个区上报的社区名称然后组成列表返回,对于常规的爬取任务来说,会非常简单,但是因为上海各区数据格式不统一(尤其是在4月5日之前的数据,表现尤为明显,从某种程度上,也反应了这之前管理的混乱和不严谨) ,在对每个社区的名称清洗后再切割后,因为有些区上报社区时有包含居住于,且后面跟着:符号,有些没有,有些甚至直接没有居住于文案,所以以上代码做了一定程度的适配。
下面的while循环,主要是对清洗和切割后的列表,循环去除空项(即'')以及包含消毒措施文案或来源文案的列表项,确保最终返回的列表内,都是上报的社区名称,如果当天某区没有上报的社区数据,则返回的列表为空。
def fetch_date_data(top_title):
#用于从文章顶部整体信息区域,提取当日日期、总确认、总无症状数据
top_title=top_title
current_date=re.search('[0-9]{4,4}年[0-9]{1,2}月[0-9]{1,2}日',top_title).group(0)
total_confirm_count=re.search('确诊病例([0-9]*)例',top_title).group(1)
total_confirm_count=int(total_confirm_count) if total_confirm_count else 0
total_asymptomatic_count=re.search('感染者([0-9]*)例',top_title).group(1)
total_asymptomatic_count=int(total_asymptomatic_count) if total_asymptomatic_count else 0
return (current_date,total_confirm_count,total_asymptomatic_count)
这部分比较简单,结合数据发布文章的内容,然后有一定基础的正则知识,就能比较轻松的看懂,以上函数主要是提取当日发布的全市数据,包括日期信息
def fetch_region_data(region_data_list):
region_data_list=region_data_list
#开始提取该区当日上报的数据,包括该区当日新增确认、新增无症状、新上报的社区名称列表
if '区' in region_data_list[0]:
region_name=re.search('[,,](.*区)',region_data_list[0]).group(1)
#有些区的数据就是不规范,有时候有区有时候没有,烦人
elif '青浦' in region_data_list[0]:
region_name='青浦区'
elif '奉贤' in region_data_list[0]:
#专门为2022年3月28日的奉贤区数据进行适配
region_name='奉贤区'
region_total_confirm=re.search('([0-9]*)例(本土)?(新冠肺炎)?确诊',region_data_list[0])
region_total_confirm=int(region_total_confirm.group(1)) if region_total_confirm else 0
region_total_asymptomatic=re.search('([0-9]*)例(本土)?无症状',region_data_list[0])
region_total_asymptomatic=int(region_total_asymptomatic.group(1)) if region_total_asymptomatic else 0
#只有该区有上报数据时,才记录,否则,为空
if len(region_data_list) >1:
region_community_list=region_data_list[1:]
else:
region_community_list=''
#返回该区的名称、当日总数据、上报的社区名称列表
return(region_name,region_total_confirm,region_total_asymptomatic,region_community_list)
以上函数对4月5日之前和之后的文章数据,做了适配,可以提取某区当日的整体确认数据以及上报的社区名称列表。
其中因为在实际运行中发现,青浦和奉贤区,在上报数据时,有时候带区,有时候不带区,所以单独做了匹配(提取区名称)
下面的代码,同样的,有一定的正则基础知识,即可比较容易的看懂,主要是基于一定的格式,提取每个区当日的整体数据,为了方便理解,每个区上报的整体数据原格式如下:
2022年3月19日,浦东新区新增8例本土确诊病例、127例本土无症状感染者,分别居住于:
def fetch_region_data_before(html):
#用于提取4月5日之前的数据
html=html
regions_covid_data={} #初始化某天的数据字典
#获取当前整体数据,包括日期、总确认、总无症状
if len(html.xpath('//div[@id="ivs_content"]/p[1]/strong')):
top_title=''.join(html.xpath('//div[@id="ivs_content"]/p')[0].xpath('strong//text()'))
else:
top_title=''.join(html.xpath('//div[@id="ivs_content"]/p')[0].xpath('span//text()'))
date_datas=fetch_date_data(top_title)
regions_covid_data['current_date']=date_datas[0]
regions_covid_data['total_confirm_count']=date_datas[1]
regions_covid_data['total_asymptomatic_count']=date_datas[2]
#初始化对应各区的数据字典
regions_covid_data['region_coviods_data']={}
#获取当日每个区的信息,包括区名称、总确诊、总无症状、上报社区列表
#先对文章内 的数据提取并按区分割
total_content_list=[wash_item(item) for item in html.xpath('//div[@id="ivs_content"]/p')]
end_index=None
#为了防止文章中出现各区重复内容,故进行去重判断
for index,item in enumerate(total_content_list[1:]):
if '市卫健委' in item:
end_index=index
regions_content=total_content_list[1:end_index] if end_index else total_content_list[1:]
split_index=[]
for index,item in enumerate(regions_content):
#只是判断月日是否在文本内,因为部分区里面的数据不一定包含年
if date_datas[0].replace('2022年','') in item:
split_index.append(index)
#切割后并按各区存储为列表
region_datas=[]
for i in range(len(split_index)-1):
region_datas.append(regions_content[split_index[i]:split_index[i+1]])
region_datas.append(regions_content[split_index[-1]:])
datas={}
for region_data in region_datas:
region_data_list=wash_region_data_list(region_data)
fetch_datas=fetch_region_data(region_data_list)
region_name=fetch_datas[0]
datas[region_name]={}
datas[region_name]['region_name']=region_name
datas[region_name]['region_total_confirm']=fetch_datas[1]
datas[region_name]['region_total_asymptomatic']=fetch_datas[2]
datas[region_name]['region_community_list']=fetch_datas[3]
#将各区的数据,存储到区数据字段内
regions_covid_data['region_coviods_data']=datas
return regions_covid_data
以上函数用来提取4月5日之前的数据,因为在代码内注释已经表详细了,故此处不再赘述,代码比较长,主要是因为4月5日之前的数据实在有点难洗,如对代码有疑问,可以私信或评论区内询问。
def fetch_region_data_after(html):
html=html
regions_covid_data={} #初始化某天的数据字典
#获取当前整体数据,包括日期、总确认、总无症状
top_title=html.xpath('//section[@data-id="106156"]//span/text()')[0]
date_datas=fetch_date_data(top_title)
regions_covid_data['current_date']=date_datas[0]
regions_covid_data['total_confirm_count']=date_datas[1]
regions_covid_data['total_asymptomatic_count']=date_datas[2]
#初始化对应各区的数据字典
regions_covid_data['region_coviods_data']={}
#然后再提取每个区的具体数据
region_datas=html.xpath('//section[@data-id="72469"]') or html.xpath('//section[@data-id="97598"]')
datas={}
for region_data in region_datas:
#对每个区的数据列表进行清洗
region_data_list=region_data.xpath('section//text()')
region_data_list=wash_region_data_list(region_data_list)
fetch_datas=fetch_region_data(region_data_list)
region_name=fetch_datas[0]
datas[region_name]={}
datas[region_name]['region_name']=region_name
datas[region_name]['region_total_confirm']=fetch_datas[1]
datas[region_name]['region_total_asymptomatic']=fetch_datas[2]
datas[region_name]['region_community_list']=fetch_datas[3]
#将各区的数据,存储到区数据字段内
regions_covid_data['region_coviods_data']=datas
return regions_covid_data
以上是提取4月5日之后的每日各区的完整数据,因为这日之后的数据都是通过微信发布,格式相对统一,代码相对比较简介。
至此,完整代码介绍完毕。