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

原文发布于微信公众号 - 零维领域(lingweilingyu)

原文发表时间:2019-05-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券