>>> from env_helper import info; info()
页面更新时间: 2024-01-17 15:33:34
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-17-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

4.5. 项目:取得当前的天气数据

检查天气似乎相当简单:打开 Web 浏览器,点击地址栏, 输入天气网站的 URL(或搜索一个,然后点击链接), 等待页面加载,跳过所有的广告等。

其实,如果有一个程序,下载今后几天的天气预报,并以 纯文本打印出来,就可以跳过很多无聊的步骤。该程序利用 第11章介绍的 requests 模块,从网站下载数据。 总的来说,该程序将执行以下操作:

  • 从命令行读取请求的位置。

  • OpenWeatherMap.org下载JSON天气数据。

  • JSON 数据字符串转换成 Python 的数据结构。

  • 打印今天和未来两天的天气。

因此,代码需要完成以下任务:

  • 连接 sys.argv 中的字符串,得到位置。

  • 调用 requests.get() ,下载天气数据。

  • 调用 json.loads(),将JSON数据转换 为Python数据结构。

  • 打印天气预报。

针对这个项目,打开一个新的文件编辑器窗口, 并保存为 quickWeather.py

4.5.1. 第1步:从命令行参数获取位置

该程序的输入来自命令行。让 quickWeather.py 看起来像这样:

#! python3
# quickWeather.py - Prints the weather for a location from the command line.

import json, requests, sys

# Compute location from command line arguments.
if len(sys.argv) < 2:
    print('Usage: quickWeather.py location')
    sys.exit()
location="长春"
# T0D0: Download the JSON data from OpenWeatherMap.org's API.

# T0D0: Load JSON data into a Python variable.

Python 中,命令行参数存储在 sys.argv 列表里。 #! 行和 import 语句之后,程序会检查是否有多个 命令行参数(回想一下, sys.argv 中至少有一个元素 sys.argv[0] ,它包含了Python 脚本的文件名)。 如果该列表中只有一个元素,那么用户没有在命 令行中提供位置,程序向用户提供“Usage(用法)”信息, 然后结束。

命令行参数以空格分隔。命令行参数San Francisco, CA将使 sys.argv中保存['quickWeather.py','San','Francisco', 'CA']。 因此,调用 join()方法,将 sys.argv 中除第一个字符串以 外的字符串连接起来。将连接的字符串存储在变量location 中。

4.5.2. 第2步:下载 JSON 数据

OpenWeatherMap.org提供了 JSON 格式的实时天气信息。 你的程序只需要下载页面 http://api.openweathermap.org/data/2.5/forecast/daily?q=&cnt=3, 其中是想知道天气的城市。 将以下代码添加到 quickWeather.py 中。

>>> #! python3
>>> # quickWeather.py - Prints the weather for a location from the command line.
>>>
>>> import json, requests, sys
>>>
>>> # Compute location from command line arguments.
>>> if len(sys.argv) < 2:
>>>     print('Usage: quickWeather.py location')
>>>     sys.exit()
>>> location="长春"
>>> weatherJsonUrl = "http://wthrcdn.etouch.cn/weather_mini?city=%s" % (location)
>>> response = requests.get(weatherJsonUrl)
>>>
>>> try:
>>>     response.raise_for_status()
>>> except:
>>>     print("网址请求出错")
>>> response = requests.get(weatherJsonUrl)
>>>
>>> weatherData = json.loads(response.text)
>>>
>>> w = weatherData['data']
>>>
>>> # TODO: Load JSON data into a Python variable.
---------------------------------------------------------------------------

gaierror                                  Traceback (most recent call last)

File /usr/lib/python3/dist-packages/urllib3/connection.py:174, in HTTPConnection._new_conn(self)
    173 try:
--> 174     conn = connection.create_connection(
    175         (self._dns_host, self.port), self.timeout, **extra_kw
    176     )
    178 except SocketTimeout:


File /usr/lib/python3/dist-packages/urllib3/util/connection.py:73, in create_connection(address, timeout, source_address, socket_options)
     69     return six.raise_from(
     70         LocationParseError("'%s', label empty or too long" % host), None
     71     )
---> 73 for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
     74     af, socktype, proto, canonname, sa = res


File /usr/lib/python3.11/socket.py:962, in getaddrinfo(host, port, family, type, proto, flags)
    961 addrlist = []
--> 962 for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
    963     af, socktype, proto, canonname, sa = res


gaierror: [Errno -2] Name or service not known


During handling of the above exception, another exception occurred:


NewConnectionError                        Traceback (most recent call last)

File /usr/lib/python3/dist-packages/urllib3/connectionpool.py:704, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    703 # Make the request on the httplib connection object.
--> 704 httplib_response = self._make_request(
    705     conn,
    706     method,
    707     url,
    708     timeout=timeout_obj,
    709     body=body,
    710     headers=headers,
    711     chunked=chunked,
    712 )
    714 # If we're going to release the connection in ``finally:``, then
    715 # the response doesn't need to know about the connection. Otherwise
    716 # it will also try to release it and we'll have a double-release
    717 # mess.


File /usr/lib/python3/dist-packages/urllib3/connectionpool.py:399, in HTTPConnectionPool._make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    398     else:
--> 399         conn.request(method, url, **httplib_request_kw)
    401 # We are swallowing BrokenPipeError (errno.EPIPE) since the server is
    402 # legitimately able to close the connection after sending a valid response.
    403 # With this behaviour, the received response is still readable.


File /usr/lib/python3/dist-packages/urllib3/connection.py:239, in HTTPConnection.request(self, method, url, body, headers)
    238     headers["User-Agent"] = _get_default_user_agent()
--> 239 super(HTTPConnection, self).request(method, url, body=body, headers=headers)


File /usr/lib/python3.11/http/client.py:1282, in HTTPConnection.request(self, method, url, body, headers, encode_chunked)
   1281 """Send a complete request to the server."""
-> 1282 self._send_request(method, url, body, headers, encode_chunked)


File /usr/lib/python3.11/http/client.py:1328, in HTTPConnection._send_request(self, method, url, body, headers, encode_chunked)
   1327     body = _encode(body, 'body')
-> 1328 self.endheaders(body, encode_chunked=encode_chunked)


File /usr/lib/python3.11/http/client.py:1277, in HTTPConnection.endheaders(self, message_body, encode_chunked)
   1276     raise CannotSendHeader()
-> 1277 self._send_output(message_body, encode_chunked=encode_chunked)


File /usr/lib/python3.11/http/client.py:1037, in HTTPConnection._send_output(self, message_body, encode_chunked)
   1036 del self._buffer[:]
-> 1037 self.send(msg)
   1039 if message_body is not None:
   1040
   1041     # create a consistent interface to message_body


File /usr/lib/python3.11/http/client.py:975, in HTTPConnection.send(self, data)
    974 if self.auto_open:
--> 975     self.connect()
    976 else:


File /usr/lib/python3/dist-packages/urllib3/connection.py:205, in HTTPConnection.connect(self)
    204 def connect(self):
--> 205     conn = self._new_conn()
    206     self._prepare_conn(conn)


File /usr/lib/python3/dist-packages/urllib3/connection.py:186, in HTTPConnection._new_conn(self)
    185 except SocketError as e:
--> 186     raise NewConnectionError(
    187         self, "Failed to establish a new connection: %s" % e
    188     )
    190 return conn


NewConnectionError: <urllib3.connection.HTTPConnection object at 0x7f13f6489d50>: Failed to establish a new connection: [Errno -2] Name or service not known


During handling of the above exception, another exception occurred:


MaxRetryError                             Traceback (most recent call last)

File /usr/lib/python3/dist-packages/requests/adapters.py:489, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    488 if not chunked:
--> 489     resp = conn.urlopen(
    490         method=request.method,
    491         url=url,
    492         body=request.body,
    493         headers=request.headers,
    494         redirect=False,
    495         assert_same_host=False,
    496         preload_content=False,
    497         decode_content=False,
    498         retries=self.max_retries,
    499         timeout=timeout,
    500     )
    502 # Send the request.
    503 else:


File /usr/lib/python3/dist-packages/urllib3/connectionpool.py:788, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    786     e = ProtocolError("Connection aborted.", e)
--> 788 retries = retries.increment(
    789     method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2]
    790 )
    791 retries.sleep()


File /usr/lib/python3/dist-packages/urllib3/util/retry.py:592, in Retry.increment(self, method, url, response, error, _pool, _stacktrace)
    591 if new_retry.is_exhausted():
--> 592     raise MaxRetryError(_pool, url, error or ResponseError(cause))
    594 log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)


MaxRetryError: HTTPConnectionPool(host='wthrcdn.etouch.cn', port=80): Max retries exceeded with url: /weather_mini?city=%E9%95%BF%E6%98%A5 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f13f6489d50>: Failed to establish a new connection: [Errno -2] Name or service not known'))


During handling of the above exception, another exception occurred:


ConnectionError                           Traceback (most recent call last)

Cell In [3], line 12
     10 location="长春"
     11 weatherJsonUrl = "http://wthrcdn.etouch.cn/weather_mini?city=%s" % (location)
---> 12 response = requests.get(weatherJsonUrl)
     14 try:
     15     response.raise_for_status()


File /usr/lib/python3/dist-packages/requests/api.py:73, in get(url, params, **kwargs)
     62 def get(url, params=None, **kwargs):
     63     r"""Sends a GET request.
     64
     65     :param url: URL for the new :class:`Request` object.
   (...)
     70     :rtype: requests.Response
     71     """
---> 73     return request("get", url, params=params, **kwargs)


File /usr/lib/python3/dist-packages/requests/api.py:59, in request(method, url, **kwargs)
     55 # By using the 'with' statement we are sure the session is closed, thus we
     56 # avoid leaving sockets open which can trigger a ResourceWarning in some
     57 # cases, and look like a memory leak in others.
     58 with sessions.Session() as session:
---> 59     return session.request(method=method, url=url, **kwargs)


File /usr/lib/python3/dist-packages/requests/sessions.py:587, in Session.request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    582 send_kwargs = {
    583     "timeout": timeout,
    584     "allow_redirects": allow_redirects,
    585 }
    586 send_kwargs.update(settings)
--> 587 resp = self.send(prep, **send_kwargs)
    589 return resp


File /usr/lib/python3/dist-packages/requests/sessions.py:701, in Session.send(self, request, **kwargs)
    698 start = preferred_clock()
    700 # Send the request
--> 701 r = adapter.send(request, **kwargs)
    703 # Total elapsed time of the request (approximately)
    704 elapsed = preferred_clock() - start


File /usr/lib/python3/dist-packages/requests/adapters.py:565, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    561     if isinstance(e.reason, _SSLError):
    562         # This branch is for urllib3 v1.22 and later.
    563         raise SSLError(e, request=request)
--> 565     raise ConnectionError(e, request=request)
    567 except ClosedPoolError as e:
    568     raise ConnectionError(e, request=request)


ConnectionError: HTTPConnectionPool(host='wthrcdn.etouch.cn', port=80): Max retries exceeded with url: /weather_mini?city=%E9%95%BF%E6%98%A5 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f13f6489d50>: Failed to establish a new connection: [Errno -2] Name or service not known'))

我们从命令行参数中得到了location 。为了生成要访问的网址, 我们利用%s占位符,将 location 中保存的字符串插入 URL 字符串的那个位置。结果保存在 url 中,并将 url 传入 requests.get()requests.get() 调用 返回一个Response 对象,它可以通过调用 raise_for_status() 来检查错误。如果不发生异常, 下载的文本将保存在 response.text 中。

4.5.3. 第3步:加载 JSON 数据并打印天气

response.text成员变量保存了一个JSON格式数据的 大字符串。要将它转换为Python值,就调用json.loads() 函数。JSON数据会像这样:

{'city':{'coord': {'lat': 37.7771, 'Ion': -122.42}, 'country': ' United States of America', 'id': '5391959',
'name': 'San Francisco','population': 0},
'cnt':3,
'cod':200,
'list':[{'clouds': 0,'deg': 233,'dt': 1402344000,'humidity': 58,'pressure': 1012.23, 'speed1: 1.96,
'temp': {'day': 302.29, 'eve': 296.46, 'max': 302.29, 'min':289.77, 'morn': 294.59, 'night': 289.77},
'weather': [{'description':'sky is clear',
'icon':'01d',
--snip--

可以将weatherData传入pprint.pprint,查看这个数据。 你可能要查找http://openweathermap.org/ ,找到关于这些 字段含义的文档。例如,在线文档会告诉你,'day’后面的 302.29是白天的开尔文温度,而不是摄氏或华氏温度。

你想要的天气描述在'nain’'description'之后。 为了整齐地打印出来,在 quickWeather.py 中添加以下代码。

>>> #! python3
>>> # quickWeather.py - Prints the weather for a location from the command line.
>>>
>>> import json, requests, sys
>>>
>>> # Compute location from command line arguments.
>>> if len(sys.argv) < 2:
>>>     print('Usage: quickWeather.py location')
>>>     sys.exit()
>>> location="长春"
>>> weatherJsonUrl = "http://wthrcdn.etouch.cn/weather_mini?city=%s" % (location)
>>> response = requests.get(weatherJsonUrl)
>>>
>>> try:
>>>
>>>     response.raise_for_status()
>>>
>>> except:
>>>
>>>     print("网址请求出错")
>>> response = requests.get(weatherJsonUrl)
>>>
>>> weatherData = json.loads(response.text)
>>>
>>> w = weatherData['data']
>>>
>>> print("地点:%s" % w['city'])
>>>
>>> date_a = []
>>>
>>> highTemp = []
>>>
>>> lowTemp = []
>>>
>>> weather = []
>>>
>>>
>>> for i in range(len(w['forecast'])):
>>>     date_a.append(w['forecast'][i]['date'])
>>>
>>>     highTemp.append(w['forecast'][i]['high'])
>>>
>>>     lowTemp.append(w['forecast'][i]['low'])
>>>
>>>     weather.append(w['forecast'][i]['type'])
>>>
>>>     print('天气地点 %s:' % (location))
>>>     print("日期:" + date_a[i])
>>>
>>>     print("\t温度:最" + lowTemp[i] + '℃~最' + highTemp[i] + '℃')
>>>
>>>     print("\t天气:" + weather[i])
>>>
>>>
>>>     print("\n今日着装:" + w['ganmao'])
>>>
>>>     print("当前温度:" + w['wendu'] + "℃")

请注意,代码将 weatherData['list’]保存在变量 w 中, 这将节省一些打字时间。可以用 w[0]w[1]w[2] 来取得今天、明天和后天天气的字典。这些字典都有 'weather' 键,其中包含一个列表值。你感兴趣的是 第一个列表项(一个嵌套的字典,包含几个键),下标是0。 这里,我们打印出保存在'main''description'键 中的值,用连字符隔开。

如果用命令行参数quickWeather.py San Francisco,CA 运行这个程序,输出看起来是这样的:

Current weather in San Francisco, CA:
Clear - sky is clear

Tomorrow:
Clouds - few clouds

Day after tomorrow:
Clear - sky is clear

4.5.4. 第4步:类似程序的想法

访问气象数据可以成为多种类型程序的基础。 你可以创建类似程序,完成以下任务:

  • 收集几个露营地点或远足路线的天气预报, 看看哪一个天气最好。

  • 如果需要将植物移到室内,安排一个程序定期 检查天气并发送霜冻警报(第15章介绍了定时调度, 第16章介绍了如何发送电子邮件)。

  • 从多个站点获得气象数据,同时显示,或计算并 显示多个天气预报的平均值。