weatherimage

   

Github 有位叫 schachmat 的用户用 go 写了一个天气预报小工具 wego,效果非常棒,于是抱着好玩的心态用 Python 山寨一个类似工具 wepy。实现不难,主要注意两点:

  • Weather API:选择非常多,个人推荐 openweathermap.org
  • Terminal color:参考了原作者的配色方案,以绿黄为主。

   

shanghai

   

源码如下:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import httplib
import json

import copy
import sys
import time


OK_STATUS = (200, 201, 202, 204)

WIND_ICON = {
    "N":   "\033[1m↓\033[0m",
    "NE":  "\033[1m↙\033[0m",
    "E":   "\033[1m←\033[0m",
    "SE":  "\033[1m↖\033[0m",
    "S":   "\033[1m↑\033[0m",
    "SW":  "\033[1m↗\033[0m",
    "W":   "\033[1m→\033[0m",
    "NW":  "\033[1m↘\033[0m"
}

WEATHER_ICON = {
    "unknown": (
        "    .-.      ",
        "     __)     ",
        "    (        ",
        "     `-’     ",
        "      •      "),
    "sunny": (
        "\033[38;5;226m    \\   /    \033[0m",
        "\033[38;5;226m     .-.     \033[0m",
        "\033[38;5;226m  ― (   ) ―  \033[0m",
        "\033[38;5;226m     `-’     \033[0m",
        "\033[38;5;226m    /   \\    \033[0m"),
    "partlyCloudy": (
        "\033[38;5;226m   \\  /\033[0m      ",
        "\033[38;5;226m _ /\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m   \\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "             "),
    "cloudy": (
        "             ",
        "\033[38;5;250m     .--.    \033[0m",
        "\033[38;5;250m  .-(    ).  \033[0m",
        "\033[38;5;250m (___.__)__) \033[0m",
        "             "),
    "veryCloudy": (
        "             ",
        "\033[38;5;240;1m     .--.    \033[0m",
        "\033[38;5;240;1m  .-(    ).  \033[0m",
        "\033[38;5;240;1m (___.__)__) \033[0m",
        "             "),
    "lightShowers": (
        "\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "\033[38;5;111m     ‘ ‘ ‘ ‘ \033[0m",
        "\033[38;5;111m    ‘ ‘ ‘ ‘  \033[0m"),
    "heavyShowers": (
        "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;240;1m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;240;1m(___(__) \033[0m",
        "\033[38;5;21;1m   ‚‘‚‘‚‘‚‘  \033[0m",
        "\033[38;5;21;1m   ‚’‚’‚’‚’  \033[0m"),
    "lightSnowShowers": (
        "\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "\033[38;5;255m     *  *  * \033[0m",
        "\033[38;5;255m    *  *  *  \033[0m"),
    "heavySnowShowers": (
        "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;240;1m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;240;1m(___(__) \033[0m",
        "\033[38;5;255;1m    * * * *  \033[0m",
        "\033[38;5;255;1m   * * * *   \033[0m"),
    "lightSleetShowers": (
        "\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "\033[38;5;111m     ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m* \033[0m",
        "\033[38;5;255m    *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘  \033[0m"),
    "thunderyShowers": (
        "\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "\033[38;5;228;5m    ⚡\033[38;5;111;25m‘ ‘\033[38;5;228;5m⚡\033[38;5;111;25m‘ ‘ \033[0m",
        "\033[38;5;111m    ‘ ‘ ‘ ‘  \033[0m"),
    "thunderyHeavyRain": (
        "\033[38;5;240;1m     .-.     \033[0m",
        "\033[38;5;240;1m    (   ).   \033[0m",
        "\033[38;5;240;1m   (___(__)  \033[0m",
        "\033[38;5;21;1m  ‚‘\033[38;5;228;5m⚡\033[38;5;21;25m‘‚\033[38;5;228;5m⚡\033[38;5;21;25m‚‘   \033[0m",
        "\033[38;5;21;1m  ‚’‚’\033[38;5;228;5m⚡\033[38;5;21;25m’‚’   \033[0m"),
    "thunderySnowShowers": (
        "\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "\033[38;5;255m     *\033[38;5;228;5m⚡\033[38;5;255;25m *\033[38;5;228;5m⚡\033[38;5;255;25m * \033[0m",
        "\033[38;5;255m    *  *  *  \033[0m"),
    "lightRain": (
        "\033[38;5;250m     .-.     \033[0m",
        "\033[38;5;250m    (   ).   \033[0m",
        "\033[38;5;250m   (___(__)  \033[0m",
        "\033[38;5;111m    ‘ ‘ ‘ ‘  \033[0m",
        "\033[38;5;111m   ‘ ‘ ‘ ‘   \033[0m"),
    "heavyRain": (
        "\033[38;5;240;1m     .-.     \033[0m",
        "\033[38;5;240;1m    (   ).   \033[0m",
        "\033[38;5;240;1m   (___(__)  \033[0m",
        "\033[38;5;21;1m  ‚‘‚‘‚‘‚‘   \033[0m",
        "\033[38;5;21;1m  ‚’‚’‚’‚’   \033[0m"),
    "lightSnow": (
        "\033[38;5;250m     .-.     \033[0m",
        "\033[38;5;250m    (   ).   \033[0m",
        "\033[38;5;250m   (___(__)  \033[0m",
        "\033[38;5;255m    *  *  *  \033[0m",
        "\033[38;5;255m   *  *  *   \033[0m"),
    "heavySnow": (
        "\033[38;5;240;1m     .-.     \033[0m",
        "\033[38;5;240;1m    (   ).   \033[0m",
        "\033[38;5;240;1m   (___(__)  \033[0m",
        "\033[38;5;255;1m   * * * *   \033[0m",
        "\033[38;5;255;1m  * * * *    \033[0m"),
    "lightSleet": (
        "\033[38;5;250m     .-.     \033[0m",
        "\033[38;5;250m    (   ).   \033[0m",
        "\033[38;5;250m   (___(__)  \033[0m",
        "\033[38;5;111m    ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m*  \033[0m",
        "\033[38;5;255m   *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘   \033[0m"),
    "fog": (
        "             ",
        "\033[38;5;251m _ - _ - _ - \033[0m",
        "\033[38;5;251m  _ - _ - _  \033[0m",
        "\033[38;5;251m _ - _ - _ - \033[0m",
        "             ")
}

WEATHER_MAPE = {
    800: 'sunny',
    801: 'partlyCloudy',
    802: 'cloudy',
    803: 'veryCloudy',
    804: 'veryCloudy',
    620: 'lightSnowShowers',
    621: 'lightSnowShowers',
    622: 'heavySnowShowers',
    612: 'lightSleetShowers',
    211: 'thunderyShowers',
    201: 'thunderyHeavyRain',
    202: 'thunderyHeavyRain',
    500: 'lightRain',
    501: 'lightRain',
    521: 'lightRain',
    502: 'heavyRain',
    503: 'heavyRain',
    504: 'heavyRain',
    522: 'heavyRain',
    600: 'lightSnow',
    601: 'lightSnow',
    602: 'heavySnow',
    611: 'lightSleet',
    200: 'fog',
}

TEMP_COLOR = {
    0: '27',
    1: '39',
    2: '51',
    3: '49',
    4: '47',
    5: '82',
    6: '190',
    7: '154',
    8: '214',
    9: '220',
}

WIND_COLOR = {
    0: '82',
    1: '118',
    2: '154',
    3: '190',
    4: '226',
    5: '220',
    6: '214',
    7: '208',
    8: '202',
}


class WeatherFormat(object):
    def __init__(self, data):
        weather = data['weather'][0]
        self.id = weather['id']
        self.main = weather['main']

        if len(weather['description']) >= 16:
            self.description = weather['description'][0:16]
        else:
            self.description = weather['description']

        if self.id in WEATHER_MAPE:
            self.weather_icon = copy.deepcopy(WEATHER_ICON[WEATHER_MAPE[self.id]])
        else:
            self.weather_icon = copy.deepcopy(WEATHER_ICON['unknown'])

        self.dt = data['dt']
        self.date = time.asctime(time.localtime(self.dt))[0:10]
        self.data = data

    def get_wind_icon(self):

        if self.wind_deg >= 337.5 or self.wind_deg < 22.5:
            wind_icon = WIND_ICON['N']
        elif self.wind_deg >= 22.5 and self.wind_deg < 67.5:
            wind_icon = WIND_ICON['NE']
        elif self.wind_deg >= 67.5 and self.wind_deg < 112.5:
            wind_icon = WIND_ICON['E']
        elif self.wind_deg >= 112.5 and self.wind_deg < 157.5:
            wind_icon = WIND_ICON['SE']
        elif self.wind_deg >= 157.5 and self.wind_deg < 202.5:
            wind_icon = WIND_ICON['S']
        elif self.wind_deg >= 202.5 and self.wind_deg < 247.5:
            wind_icon = WIND_ICON['SW']
        elif self.wind_deg >= 247.5 and self.wind_deg < 292.5:
            wind_icon = WIND_ICON['W']
        elif self.wind_deg >= 292.5 and self.wind_deg < 337.5:
            wind_icon = WIND_ICON['NW']

        return wind_icon

    def color_wind(self, wind_speed):
        if int(wind_speed * 1.2) >= 8:
            color = WIND_COLOR[8]
        else:
            color = WIND_COLOR[int(wind_speed * 1.2)]

        return "\033[38;5;" + color + "m" + str(wind_speed) + "\033[0m"

    def color_temp(self, temp):
        temp += 15
        if temp < 0:
            color = TEMP_COLOR[0]
        elif temp > 56:
            color = TEMP_COLOR[9]
        else:
            color = TEMP_COLOR[int(temp / 7)]

        return "\033[38;5;" + color + "m" + str(temp - 15) + "\033[0m"

    def color_date(self, date):
        return "\033[38;5;202m" + date + "\033[0m"

    def color_main(self, main):
        return "\033[1;5;32m" + main + "\033[0m"

    def format_daily(self):

        temp = self.data['temp']
        self.temp_min = temp['min']
        self.temp_max = temp['max']
        self.pressure = self.data['pressure']
        self.humidity = self.data['humidity']
        self.wind_deg = self.data['deg']
        self.wind_speed = self.data['speed']
        self.wind_icon = self.get_wind_icon()

        wind_ret = self.wind_icon + "  " + self.color_wind(self.wind_speed) + " m/s"
        temp_ret = self.color_temp(self.temp_min) + " - " + self.color_temp(self.temp_max) + ' °C'

        ret = list(self.weather_icon)
        ret[0] += self.color_main(self.main)
        ret[1] += self.description
        ret[2] += wind_ret
        ret[3] += temp_ret
        ret[4] += 'Humidity: ' + str(self.humidity)
        ret.append(self.color_date(self.date))

        length = [len(self.main) + 17,
                  len(self.description) + 17,
                  len('  ' + str(self.wind_speed) + ' m/s') + 18,
                  len(str(self.temp_min) + " - " + str(self.temp_max) + ' °C') + 16,
                  len('Humidity: ' + str(self.humidity)) + 17]

        ret.append(length)

        return ret


class OpenWeatherMap(object):
    def __init__(self, q=None):
        # City name.
        self.q = q or 'beijing'
        self.city = None
        self.country = None
        self.APPID = None
        self.host = 'api.openweathermap.org'
        self.today_path = '/data/2.5/forecast/daily?units=metric&cnt=15&q=' + self.q

    def http_request(self, path):
        try:
            conn = httplib.HTTPConnection(self.host)
            conn.request('GET', path)
        except Exception as ex:
            print_exit_error(ex)

        res = conn.getresponse()
        if res.status in OK_STATUS:
            body = res.read()
        else:
            msg = "Network Problem: please try again later ^_^."
            print_exit_error(msg)

        conn.close()

        return body

    def get_weather_data(self):
        resp = self.http_request(self.today_path)

        if not resp:
            print_exit_error("Response data is None.")

        data = json.loads(resp)

        if data['cod'] == "404":
            print_exit_error("Sorry, the city is not found.")

        self.city = data['city']['name']
        self.country = data['city']['country']
        data = data['list']

        return data


class PrintWeather(object):
    def __init__(self, data):
        self.data = data
        self.lines = [
            "┌──────────────────────────────┬──────────────────────────────┬──────────────────────────────┬──────────────────────────────┐",
            "│                              │                              |                              │                              │",
            "├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤",
            "|                              |                              |                              |                              |",
            "|                              |                              |                              |                              |",
            "|                              |                              |                              |                              |",
            "|                              |                              |                              |                              |",
            "|                              |                              |                              |                              |",
            "└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘"]

    def prepare_date(self):
        d_line = ""
        for i in range(4):
            d_line += "│          " + self.data[i][5] + "          "
        d_line += "|"
        self.lines[1] = d_line

    def prepare_weather(self):
        for i in range(5):
            w_line = ""
            for j in range(4):
                if self.data[j][6][i] <= 34:
                    w_line += "|" + self.data[j][i] + \
                        " " * (34 - self.data[j][6][i])
                else:
                    w_line += "|" + self.data[j][i][0:34]
            w_line += "|"
            self.lines[i + 3] = w_line

    def prepare_all(self):
        self.prepare_date()
        self.prepare_weather()


def print_city_info(city, country):
    city_info = "\n\033[38;5;202mWeather for City: \033[0m"
    city_info += "\033[1;5;32m" + city + " " + country + "\033[0m\n"
    print(city_info)


def print_exit_error(msg):
    msg = "\033[31m" + msg + "\033[0m"
    print(msg)
    exit(-1)


if '__main__' == __name__:
    if len(sys.argv) == 1:
        weather = OpenWeatherMap()
    else:
        weather = OpenWeatherMap(sys.argv[1])

    format_data = []
    data = weather.get_weather_data()
    #(Note) we only print 12 days weather.
    for d in data[0:12]:
        daily_data = WeatherFormat(d)
        format_data.append(daily_data.format_daily())

    print_city_info(weather.city, weather.country)
    for i in range(3):
        print_weather = PrintWeather(format_data[4*i: 4*i + 4])
        print_weather.prepare_all()
        for j in range(9):
            print(print_weather.lines[j])