Version 1.0.1
[python-thefuckingweather.git] / thefuckingweather.py
blob2ec0c040358c25322160c01726749e7d74341adb
1 # coding: utf-8
3 # Python API for The Fucking Weather, version 1.0.1
4 # Copyright (C) 2009 Ian Weller <ian@ianweller.org>
5 # http://ianweller.org/thefuckingweather
7 # This program is free software. It comes without any warranty, to
8 # the extent permitted by applicable law. You can redistribute it
9 # and/or modify it under the terms of the Do What The Fuck You Want
10 # To Public License, Version 2, as published by Sam Hocevar. See
11 # http://sam.zoy.org/wtfpl/COPYING for more details.
13 """Scrapes data from www.thefuckingweather.com for a given location."""
15 from optparse import OptionParser
16 import re
17 import urllib
18 import urllib2
20 RE_WEATHER = """<div style="float: left;"><span class="small">(.*)</span></div>
21 <iframe id="fbook" .*>.*</iframe>
22 <div id="content"><div class="large" >(\d*)&deg;\?!<br />
23 <br />(.*)</div><div id="remark"><br />
24 <span>(.*)</span></div>"""
26 RE_FORECAST = """<div class="boxhead">
27 <h2>THE FUCKING FORECAST</h2>
28 </div>
29 <div class="boxbody">
30 <table><tr>
31 <td>DAY:</td>
32 <td class="center"><strong>(.{3})</strong></td>
33 <td class="center"><strong>(.{3})</strong></td>
34 </tr>
35 <tr>
36 <td>HIGH:</td><td class="center hot">(\d*)</td>\
37 <td class="center hot">(\d*)</td>
38 </tr>
39 <tr>
40 <td>LOW:</td><td class="center cold">(\d*)</td>\
41 <td class="center cold">(\d*)</td>
42 </tr>
43 <tr>
44 <td>FORECAST:</td><td class="center">(.*)</td><td class="center">(.*)</td></tr>
45 </table>
46 </div>"""
48 DEGREE_SYMBOL = "°"
51 class LocationError(StandardError):
52 """
53 The website reported a "WRONG FUCKING ZIP" error, which could mean either
54 the server has no clue what to do with your location or that it messed up.
55 """
57 def __init__(self):
58 StandardError.__init__(self, "WRONG FUCKING ZIP returned from website")
61 class ParseError(StandardError):
62 """
63 Something is wrong with the regexps or the site owner updated his template.
64 """
66 def __init__(self):
67 StandardError.__init__(
68 self, """Couldn't parse the website.
69 RE: %s
71 Please report what you did to get this error and this full Python traceback
72 to ian@ianweller.org. Thanks!""" % RE_WEATHER)
75 def get_weather(location, celsius=False):
76 """
77 Retrieves weather and forecast data for a given location.
79 Data is presented in a dict with three main elements: "location" (the
80 location presented by TFW), "current" (current weather data) and "forecast"
81 (a forecast of the next two days, with highs, lows, and what the weather
82 will be like).
84 "current" is a dictionary with three elements: "temperature" (an integer),
85 "weather" (a list of descriptive elements about the weather, e.g., "ITS
86 FUCKING HOT", which may be coupled with something such as "AND THUNDERING";
87 this element is named as such because it always begins with "ITS FUCKING")
88 and "remark" (a string printed by the server which is meant to be witty but
89 is often not. each to their own, I guess).
91 "forecast" is a dictionary with two elements, 0 and 1 (both integers). Each
92 of these is a dictionary which contains the keys "day" (a three-letter
93 string consisting of the day of week), "high" and "low" (integers
94 representing the relative extreme temperature of the day) and "weather" (a
95 basic description of the weather, such as "Scattered Thunderstorms").
97 The default is for temperatures to be in Fahrenheit. If you're so inclined,
98 you can pass True as a second variable and get temperatures in Celsius.
100 If you need a degree symbol, you can use thefuckingweather.DEGREE_SYMBOL,
101 for your convenience.
103 # Retrieve yummy HTML
104 query = {"zipcode": location}
105 if celsius:
106 query["CELSIUS"] = "yes"
107 query_string = urllib.urlencode(query)
108 url = "http://www.thefuckingweather.com/?" + query_string
109 data = urllib2.urlopen(url).read()
110 # Check for an error report
111 if re.search("WRONG FUCKING ZIP", data):
112 raise LocationError()
113 # No error, so parse current weather data
114 return_val = {"current": {}, "forecast": {0: {}, 1: {}}}
115 weather_search = re.search(RE_WEATHER, data)
116 if not weather_search:
117 raise ParseError()
118 return_val["location"] = weather_search.group(1)
119 return_val["current"]["temperature"] = int(weather_search.group(2))
120 return_val["current"]["weather"] = weather_search.group(3).split(
121 "<br />")
122 return_val["current"]["remark"] = weather_search.group(4)
123 # Now parse the forecast data
124 forecast_search = re.search(RE_FORECAST, data)
125 if not forecast_search:
126 raise ParseError()
127 return_val["forecast"][0]["day"] = forecast_search.group(1)
128 return_val["forecast"][0]["high"] = int(forecast_search.group(3))
129 return_val["forecast"][0]["low"] = int(forecast_search.group(5))
130 return_val["forecast"][0]["weather"] = forecast_search.group(7)
131 return_val["forecast"][1]["day"] = forecast_search.group(2)
132 return_val["forecast"][1]["high"] = int(forecast_search.group(4))
133 return_val["forecast"][1]["low"] = int(forecast_search.group(6))
134 return_val["forecast"][1]["weather"] = forecast_search.group(8)
135 # I'm gonna have to jump!
136 return return_val
139 def main():
141 This function is run when the python file is run from the command line. It
142 prints content formatted somewhat like www.thefuckingweather.com. You can
143 use the -c (--celsius) switch to return temperatures in Celsius.
145 usage = "usage: %prog [-c] location"
146 parser = OptionParser(usage=usage)
147 parser.add_option("-c", "--celsius", dest="celsius", help="return temp"+\
148 "eratures in Celsius (Fahrenheit without this switch",
149 action="store_true", default=False)
150 (options, args) = parser.parse_args()
151 if len(args) == 1:
152 weather = get_weather(args[0], options.celsius)
153 weather_tuple = (weather["location"],
154 weather["current"]["temperature"],
155 DEGREE_SYMBOL,
156 "\n".join(weather["current"]["weather"]),
157 weather["current"]["remark"],
158 weather["forecast"][0]["day"],
159 weather["forecast"][0]["high"],
160 weather["forecast"][0]["low"],
161 weather["forecast"][0]["weather"],
162 weather["forecast"][1]["day"],
163 weather["forecast"][1]["high"],
164 weather["forecast"][1]["low"],
165 weather["forecast"][1]["weather"])
166 print """\
167 (%s)
168 %d%s?! %s
171 Forecast
172 Today (%s)
173 High: %d
174 Low: %d
175 Weather: %s
176 Tomorrow (%s)
177 High: %d
178 Low: %d
179 Weather: %s""" % weather_tuple
180 else:
181 parser.print_help()
183 if __name__ == "__main__":
184 main()