Pandas爬取历史天气数据
1. 前言
1.1 基本介绍
Pandas是一款开放源码的BSD许可的Python库,为Python编程语言提供了高性能,易于使用的数据结构和数据分析工具。
Pandas用于广泛的领域,包括金融,经济,统计,分析等学术和商业领域。Series 和 DataFrame 是Pandas 中最主要的数据结构,使用Pandas 就是使用 Series 和 DataFrame 来构造原始数据。
本文爬取历史天气数据主要是基于 Pandas 的 read_html 方法。
该方法非常简单明了,就是解析网页中的表格(因为展现历史数据,表格是一个很清晰的表示方法),然后将网页中的所有表格返回回来,其他内容则略过。
访问的历史天气源则是【天气后报】 http://www.tianqihoubao.com/
页面也是比较简洁的。
历史天气页面则是以月份为分隔,将每天的天气历史天气数据展示在表格中。
1.2 运行环境
- 操作系统: win10
- python版本:3.7.0
- Anaconda:3.5.1
- pandas版本:0.23.4(最新0.24.2)
2. 代码详解
2.1 read_html()
pandas read_html() 方法参数比较简单,可以将网址、html文件或者字符串作为输入,内置的解析方法会将网页内容进行解析。
说到解析网页,在文档中发现了一个意外惊喜。
对常见的解析器(lxml, bs4, html5lib)的优缺点进行了分析~
header,index_col,skiprows 等等都是 pandas 的常见参数,因此不作赘述,可以在文末的参考网址中查看官方文档或者参数详解文档。
2.2 代码分解
首先从网址构成看,不同的历史数据就只是城市和月份的不同,因此构建网址只需要改变这两个位置的字符串就可以了;再看数据内容,数据被很规整的放置在 table 当中,这个解析的工作就交给 read_html() , 再将内容输出到 excel,就简单了。
引入模块
1 import calendar
2 import datetime
3 import os
4 import random
5 import re
6 import time
7 from pprint import pprint
8 import numpy as np
9 import pandas as pd
10 from dateutil.parser import parse
这里创建了一个自定义的时间区间函数,方便取得自然月份的区间,就可以得到两个端点月份的日期(即起止月份)
12 def get_month_period(month_begin=1, month_end=0):
13 '''
14 获得自然月份间隔时间段, 默认取前一个自然月
15 :param month_begin: 几个月前的第一天
16 :param month_end: 几个月前的结束第一天
17 :return: e.g(2018,4,1 ,2018,5,1)
18 '''
19 now = datetime.datetime.now()
20 day = datetime.datetime.strptime(datetime.datetime.strftime(
21 now.replace(day=1), "%Y-%m-%d"), "%Y-%m-%d")
22
23 def get_day(shijian, zhouqi):
24 for i in range(zhouqi):
25 last_month_last_day = shijian - datetime.timedelta(days=1)
26 cc = calendar.monthrange(last_month_last_day.year, last_month_last_day.month)
27 last_month_first_day = shijian - datetime.timedelta(days=cc[1])
28 shijian = last_month_first_day
29 i += 1
30 return (last_month_first_day, last_month_last_day)
31
32 begin = get_day(day, month_begin)[0]
33 end = get_day(day, month_end + 1)[1] + datetime.timedelta(days=1)
34 return begin, end
然后就是定义获取天气数据的方法
36 def get_weather_data(city='hangzhou', time_func_name=get_month_period, *args):
37 begin, end = time_func_name(*args)
38 print(begin, end)
39 # 获得需要爬取的日期区间
40 date_list = [date.strftime("%Y%m") for date in pd.date_range(begin, end, freq='M')]
41 # 构建url
42 url_list = ["http://www.tianqihoubao.com/lishi/{}/month/{}.html".format(city, date) for date in date_list]
43 pprint(url_list)
44 # 合并后的天气信息文件
45 filepath = os.path.join(os.path.abspath(os.getcwd()), 'data',
46 "weather-{}-{}-{}.xlsx".format(city, date_list[0], date_list[-1]))
47 if os.path.exists(filepath):
48 weather_data = pd.read_excel(filepath)
49 else:
50 # 抓取天气信息
51 weather_data = pd.DataFrame(columns=["日期", "天气状况", "气温", "风力风向"])
52 for index, url in enumerate(url_list):
53 weatherDataFilePath = os.path.join(os.path.abspath(os.getcwd()), 'data',
54 "weather-{}-{}.xlsx".format(city, date_list[index]))
55 # print(weatherDataFilePath)
56 try:
57 weather_df = pd.read_excel(weatherDataFilePath, header=0)
58 # 不完整月份的天气数据补充
59 current_date = datetime.datetime.strptime(date_list[index], '%Y%m')
60 if weather_df.shape[0] < calendar.monthrange(current_date.year, current_date.month)[1]:
61 weather_df = pd.DataFrame(pd.read_html(url, encoding='GBK', header=0)[0])
62 weather_df.to_excel(weatherDataFilePath, index=None)
63 except Exception:
64 weather_df = pd.DataFrame(pd.read_html(url, encoding='GBK', header=0)[0])
65 weather_df.to_excel(weatherDataFilePath, index=None)
66 # 随机等待 [1-10]秒 发送请求
67 time.sleep(random.randint(1, 10))
68
69 weather_data = pd.concat([weather_data, weather_df], ignore_index=True)
70
71 weather_data.to_excel(filepath, index=None)
72 return weather_data, filepath
这里的逻辑也很简单,确定好想要的时间区间和城市,根据网址的结构规则,构建出来所有页面的 URL ,再将它们传入 read_html() 即可
运行时我们将起止时间和构建的 URL 打印出来(这里测试了爬取杭州近3个月的天气数据)
这里虽然网站没有定义 robots 文件,但是为了良性地访问数据,我们还是设置了随机停顿 1-10 秒
观察天气数据的格式,日期需要调整格式,天气情况、气温都需要拆分,风力风向则不仅需要拆分还需要数值转化
使用正则表达式,我们将使其转化为简洁易处理的格式
86 def clean_weather_data(df, filepath, remove=True):
87 '''使用正则表达式清洗天气数据'''
88 ptianqi = re.compile('w+')
89 pwendu = re.compile('d+')
90 pfengli = re.compile('(w+)s+(d*W+d+)')
91 df['主天气状况'] = df.loc[:, '天气状况'].apply(lambda x: ptianqi.findall(x)[0])
92 df['次天气状况'] = df.loc[:, '天气状况'].apply(lambda x: ptianqi.findall(x)[1])
93 df['主风向'] = df.loc[:, '风力风向'].apply(lambda x: pfengli.findall(x)[0][0])
94 df['主风力'] = df.loc[:, '风力风向'].apply(lambda x: pfengli.findall(x)[0][1])
95 df['主风力'] = df.loc[:, '主风力'].apply(lambda x: clean_fengli(x))
96 df['次风向'] = df.loc[:, '风力风向'].apply(lambda x: pfengli.findall(x)[1][0])
97 df['次风力'] = df.loc[:, '风力风向'].apply(lambda x: pfengli.findall(x)[1][1])
98 df['次风力'] = df.loc[:, '次风力'].apply(lambda x: clean_fengli(x))
99 df['最高温度'] = df.loc[:, '气温'].apply(lambda x: pwendu.findall(x)[0])
100 df['最低温度'] = df.loc[:, '气温'].apply(lambda x: pwendu.findall(x)[1])
101 df["日期"] = df["日期"].apply(lambda x: parse("-".join(re.match('(d+)w*(d{2})w*(d{2,})', x).groups())))
102 if remove:
103 os.remove(filepath)
104 df.drop(columns=["天气状况", "气温", "风力风向"], inplace=True)
105 # 存储所有清洗好的天气数据
106 df.to_excel(filepath.replace('weather-', 'weatherCleaned-'), index=False)
107 return df # [日期 主天气状况 次天气状况 主风向 主风力 次风向 次风力 最高温度 最低温度]
天气情况、气温、风向都使用模式匹配的方式将 dataframe中一列的结果转化为了两列。 因为风力和风向放在了一起,并且从数据中我们发现风力存在 3 种不同的格式(对应于 pattern1,pattern2,pattern3),因此单独写了一个方法来处理风力的数据。
74 def clean_fengli(x):
75 '''正则表达式清洗风力数据的格式'''
76 pattern1 = re.compile('(d+)(W+)(d+)') # 1-2, 1~2
77 pattern2 = re.compile('(d*)(W+)(d+)') # <2 <=2
78 pattern3 = re.compile('(d+)') # 2
79 if re.match(pattern1, x):
80 return np.mean((int(re.match(pattern1, x).groups()[0]), int(re.match(pattern1, x).groups()[2])))
81 elif re.match(pattern2, x):
82 return int(re.match(pattern2, x).group()[1]) - 0.5
83 else:
84 return int(re.match(pattern3, x).group(0))
对于区间型的风力我们区平均值,单边的则将风力调低 0.5 级,整数的则原始值。
基于以上处理,我们就基本上得到了格式简明易处理的天气数据了,最终调用一下。
109 if __name__ == '__main__':
110 weather_data, filepath = get_weather_data('hangzhou', get_month_period, 3)
111 clean_weather_data(weather_data, filepath, remove=True)
就拿到了所有的天气数据啦!
3. 后续改进
3.1 天气预报API
历史天气数据毕竟只是参考数据,我们还是希望能够拿到未来的数据,对于预报类的天气数据就需要api 来调用了,看了下觉得YY天气的接口还不错。可以拿到比天气后报更多的天气相关的信息。
3.2 日期区间函数优化
总觉得获取自然月区间的函数方法有点别扭,后续会找找好的简洁的方式对该方法改写一下~
本文完整代码:
https://github.com/firewang/lingweilingyu/blob/master/weatherCrawler/weatherCrawler.py
参考网址:
- http://pandas.pydata.org/pandas-docs/stable/user_guide/io.html
- http://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#io-read-html
- http://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#io-html-gotchas
- https://www.cnblogs.com/litufu/articles/8721207.html
- https://www.cnblogs.com/litufu/articles/8721659.html
- http://www.yytianqi.com/api.html
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法