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 没有试验,估计对应的是 NoneTrue/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')
>>>

这时我发现一个问题:每次查询不同城市我都要输入 apikeycity 三个参数,而实际上只有 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 在欢迎界面获得当前城市天气信息

results matching ""

    No results matching ""