Connect to Real World
输入城市名,返回该城市最新的天气数据; 输入指令,打印帮助文档(一般使用 h 或 help); 输入指令,退出程序的交互(一般使用 quit 或 exit); 在退出程序之前,打印查询过的所有城市。
前两周都是先写完代码,再写教程大纲,最后每天抽空填补内容。好处是思路连贯,步骤条理清晰。可是因为自己拙计的写作水平,每次教程都在最后一天填补完,心中还有缺憾。
这次换个套路:来试试写教程的过程中编写程序代码。
STEP1. 选择一个在线服务
首先没有花太多精力选择服务。教材要教你举一反三,随便找一个免费的来。
在搜索首页选择了和风天气,提供 JSON 格式 API,免费可查 3000 次/天。
这次需要的实时天气接口在文档目录很容易就找到了,描述是这样的
GET 接口地址/now
city:
required
string
城市名称 city可通过城市中英文名称、ID和IP地址进行,例如city=北京,city=beijing,city=CN101010100,city=60.194.130.1
key:
required
string
用户认证key
lang:
string
多语言,默认为中文,可选参数
说实话这个 API 描述对新手不是很友好。但若了解基本的 HTTP 知识,那么文档也已经讲明要点了。
接口要求用 HTTP GET 方法请求,接口地址在文档可以找到,用户认证 key 注册后在控制台中获得,我要做一次请求需要组合这样一个 URL
https://free-api.heweather.com/v5/now?city=丹东&key=6a1f4bcddc354930a996ccbe784e6507
你打开浏览器在地址栏输入 URL 就能看到搜索结果了(如果很不幸你遇到的 API 要求 POST 方法——当然这是不合理的——用地址栏不行,那就要用工具或者写个脚本来测试了。)
{"HeWeather5":[{"basic":{"city":"丹东","cnty":"中国","id":"CN101070601","lat":"40.110000","lon":"124.357000","update":{"loc":"2017-01-23 20:51","utc":"2017-01-23 12:51"}},"now":{"cond":{"code":"100","txt":"晴"},"fl":"-12","hum":"34","pcpn":"0","pres":"1034","tmp":"-9","vis":"10","wind":{"deg":"346","dir":"北风","sc":"5-6","spd":"32"}},"status":"ok"}]}
一坨是吧... 这就是标准 JSON 格式了。JSON 是基于 js 标准制定的一种轻量级数据交换格式,目前是 web 开发最主要的数据通信格式。这里推荐一个插件 JSON Viewer,可以在浏览器中格式化 JSON 数据,很适合调试 API 用。
STEP2. Requests: HTTP for Humans
我在浏览器中证明了这个借口是可用的,现在我要在 CLI 里验证了。
Python 自带的 http 库据说相当难用,先不研究,直接考虑第三方库—— Requests
根据官方指示,安装
$ pip install requests
>>> import requests
>>> requests
<module 'requests' from '/Users/gogu/.pyenv/versions/3.6.0/lib/python3.6/site-packages/requests/__init__.py'>
安装成功,模块可以引用了。按 quick start 提供的例子来测试看看
>>> r = requests.get('https://free-api.heweather.com/v5/now?city=丹东&key=6a1f4bcddc354930a996ccbe784e6507')
>>> r
<Response [200]>
>>> r.raw
<requests.packages.urllib3.response.HTTPResponse object at 0x10d1b2470>
>>> r.text
'{"HeWeather5":[{"basic":{"city":"丹东","cnty":"中国","id":"CN101070601","lat":"40.110000","lon":"124.357000","update":{"loc":"2017-01-23 22:51","utc":"2017-01-23 14:51"}},"now":{"cond":{"code":"100","txt":"晴"},"fl":"-14","hum":"36","pcpn":"0","pres":"1034","tmp":"-10","vis":"10","wind":{"deg":"342","dir":"北风","sc":"5-6","spd":"26"}},"status":"ok"}]}'
>>> r.json()
{'HeWeather5': [{'basic': {'city': '丹东', 'cnty': '中国', 'id': 'CN101070601', 'lat': '40.110000', 'lon': '124.357000', 'update': {'loc': '2017-01-23 22:51', 'utc': '2017-01-23 14:51'}}, 'now': {'cond': {'code': '100', 'txt': '晴'}, 'fl': '-14', 'hum': '36', 'pcpn': '0', 'pres': '1034', 'tmp': '-10', 'vis': '10', 'wind': {'deg': '342', 'dir': '北风', 'sc': '5-6', 'spd': '26'}}, 'status': 'ok'}]}
很顺利,requests.get
返回一个 HTTPResponse 对象。拥有属性 text
,内容和浏览器测试一致;拥有方法 json
,把 text
的 json 字符串转换成 Python 对象,object 对应 dict,array 对应 list,number 和 string 都转成了 str。null 和 true/false 没有试验,估计对应的是 None
和 True
/False
。
STEP3. 模块拆分
现在我们有了在线 API 服务和获取数据的方法 requests.get
,可以着手设计代码图纸了。
我本来还希望再次复用 w1 的代码,并保持 w1 和 w2 自身功能完整可运行。但是这次由于数据源产生变化,导致主要逻辑都不能复用了,不如重写。现在回过头想,如果当初把输出的部分也做的更抽象就好了。所以这次单独做一个负责在 CLI 输出的模块。
规划初步如下:
Module weather
[FUNCTIONS]
query(api, key, city)
Return the city weather information.
save_record(history, record)
Save a record to history, return the history.
Module cli
[FUNCTIONS]
print_log(command=None, weather_info={}, history=[])
Exec the command or parse the weatherInfo,then print the result to cli.
print_welcome()
Print welcome info to cli.
get_input()
Get input line.
check_command()
Check if the command is valid.
[DATA]
HELP_INFO = (
'- 输入城市名,返回该城市最新的天气数据;\n'
'- 输入指令,打印帮助文档(一般使用 h 或 help)\n'
'- 输入指令,退出程序的交互(一般使用 quit 或 exit)\n'
'- 在退出程序之前,打印查询过的所有城市\n')
CITY_WEATHER_INFO = (
'{city} 今日天气: {weather}\n'
'体感温度:{fl},湿度:{hum}%,\n'
'{windDir},{windSc}级\n'
'更新时间:{time}\n')
SIMPLE_WEATHER_INFO = '{city} 今日天气: {weather},更新时间:{time}'
HISTORY_INFO = '你查询过如下城市',
WELCOME = '进入天气查询程序,欢迎使用\n',
INPUT_HINT = '请输入指令或您要查询的城市名: ',
NO_HISTORY = '没有查询记录\n',
GOODBYE_INFO = '拜拜\n',
NO_HINT = '没有找到该城市,或输入指令无效\n',
Module main
[DATA]
API_ADDRESS = 'https://free-api.heweather.com/v5/now'
KEY = '6a1f4bcddc354930a996ccbe784e6507'
然后 main.py 大致是这样
import weather
import cli
history = []
if __name__ == '__main__':
cli.print_log('welcome')
while True:
command = cli.get_input().lower()
if (cli.check_command(command)):
cli.print_log(command=command, history=history)
else:
weather_info = weather.query(command)
weather.save_record(history, weather_info)
cli.print_log(weather_info=weather_info)
if (command in ['q', 'quit']):
break
整个设计的过程并不是线性的,预想是 weather 和 cli 分开,填充了预想中 main 可能用到的函数,途中沉迷 cli 中的字符串模板细节。最后写 main 的时候就发现预想不周,来回改了几次函数功能和名称。然后发现 history 完全被我忘掉了,又再次改了 main 和 weather 的草图。
STEP4. 模块实现
本次实现模块的重点在于 main 之外的模块不互相依赖,不产生额外作用,全部由 main
模块调配。
按照草图首先实现 weather
。
def query(api, key, city):
'''Return the city weather information.'''
data = requests.get(api, params={'key': key, 'city': city}) \
.json().get('HeWeather5')[0]
if data.get('status') == 'ok':
basic = data.get('basic')
now = data.get('now')
wind = now.get('wind')
return {
'city': basic.get('city'),
'time': basic.get('update').get('loc'),
'weather': now.get('cond').get('txt'),
'fl': now.get('fl'),
'hum': now.get('hum'),
'windDir': wind.get('dir'),
'windSc': wind.get('sc'),
}
else:
return None
进行「最小步骤调试」,注意尽量覆盖各种情况
>>> import weather
>>> API_ADDRESS = 'https://free-api.heweather.com/v5/now'
>>> KEY = '6a1f4bcddc354930a996ccbe784e6507'
>>> weather.query(API_ADDRESS, KEY, 'dandong')
{'city': '丹东', 'time': '2017-01-26 22:59', 'weather': '阵雪', 'fl': '-4', 'hum': '41', 'windDir': '北风', 'windSc': '6-7'}
>>> weather.query(API_ADDRESS, KEY, 'nowhere')
>>>
这时我发现一个问题:每次查询不同城市我都要输入 api
,key
和 city
三个参数,而实际上只有 city
这个参数变了,有没有办法保存这前两个参数呢?
如果使用面向对象的方式,就可以在把这两个参数保存在对象属性上,可是我还是不想投入面向对象。JavaScript 里有闭包的方式,我看到 Python 也是有的。可以返回一个函数来获取其词法作用域中的存储的值(不知 Python 中这样理解对不对)。
def buildQuery(api, key):
'''Build and return a query function.'''
return lambda city: query(api, key, city)
>>> query = weather.buildQuery(API_ADDRESS, KEY)
>>> query('dandong')
{'city': '丹东', 'time': '2017-01-26 22:59', 'weather': '阵雪', 'fl': '-4', 'hum': '41', 'windDir': '北风', 'windSc': '6-7'}
>>> query('dalian')
{'city': '大连', 'time': '2017-01-26 22:59', 'weather': '多云', 'fl': '-7', 'hum': '52', 'windDir': '北风', 'windSc': '6-7'}
接着一个个实现预先定义的函数,每个函数都单独测试(途中也有和图纸不同的改动...)
STEP5. 体验增强
TODO: 根据 IP 在欢迎界面获得当前城市天气信息