weatpy源码剖析

golang写的命令行天气预报wego,其github上居然有几千个star,于是就想着用python来写写看。

写完后发现还挺有意思的。

基本思路

调用forecast.io的api,根据其返回的数据处理后显示在终端。

golang

因为wego是golang写的,花了一天时间专门了解golang,足够看懂wego代码了。

后端、前端可切换

Template Method模式.
不管是什么backend,只要实现fetch方法。
不管是什么frontend,只要实现render方法。

class Backend(object):
    def fetch(self, arg_ns):
        raise NotImplementedError


class Frontend(object):
    def render(self, data, unit):
        raise NotImplementedError

...

def main():
    arg_ns = get_arg_namespace()
    be = ALL_BACKENDS[arg_ns.backend]
    r = be.fetch(arg_ns)
    fe = ALL_FRONTENDS[arg_ns.frontend]
    fe.render(r, unit=arg_ns.unit)

network

发请求调用api毫无疑问用requests, 当然,也可以用标准库urllib。

r = requests.get(url)
if r.status_code == 200:
    r.json()

requests的接口很优雅。

命令行解析

标准库argparse实在是太强大了,我完全没有考虑用docopt的想法。

    parser = argparse.ArgumentParser(description='weatpy for weather forecast')
    parser.set_defaults(**defaults)
    parser.add_argument('location', help='LOCATION to be queried (default "22.5333,114.1333")')
    parser.add_argument('-v', action='count')
    parser.add_argument('-b', '--backend', help='BACKEND to be used. (default "forecast.io")')
    parser.add_argument('-f', '--frontend', help='FRONTEND to be used. (default "ascii-art-table")')
    parser.add_argument('-d', '--numdays', type=int, help='NUMBER of days of weather forecast to be displayed (default 3)')
    parser.add_argument('-u', '--unit', help='UNITSYSTEM to use for output. Choices are: metric, imperial, si (default "metric")')
    parser.add_argument('--api-key', help='the api KEY to use')
    parser.add_argument('--lang', help='LANGUAGE to request from forecast.io (default zh)')
    arg_namespace = parser.parse_args()

使用ssh,应该有使用过ssh -vvv address, 即根据不同的v参数个数,打出不同级别的log。可以-v或-vv或-vvv, 怎么用argparse实现这个功能呢?

argparse有action='count'这个action,可以进行计数。

怎么实现优先读取命令行参数,再读取配置文件~/.weatpyrc呢?
思路是解析文件配置后传到parser去 parser.set_defaults(**defaults)

这里遇到的一个问题是python的标准库ConfigParser是严格要求配置文件要有section的,否则会报MissingSectionHeaderError错误。
而很多unix风格的配置文件是没有section的。于是这里做了一下处理。

class FakeGlobalSectionHead(object):
    def __init__(self, fp):
        self.fp = fp
        self.sechead = '[global]\n'

    def readline(self):
        if self.sechead:
            try:
                return self.sechead
            finally:
                self.sechead = None
        else:
            return self.fp.readline()

...
cp = ConfigParser.ConfigParser()
cp.readfp(FakeGlobalSectionHead(open(CONFIG_FILE)))
defaults = dict(cp.items('global'))

...      
parser.set_defaults(**defaults)

数据结构

python的namedtuple看起来有点像golang的struct,然而namedtuple的实例是只读的。基本就只是给元组加了个名字而已,功能很弱。

于是用一个自定义的DataObject对象来放复杂数据,用__slots__来限制属性。

字符画前端

要把一天中早上、中午、傍晚、深夜4个阶段的天气预报给拼成一个表格。
一不小心,一言不合线就乱了,就对不齐了。
对于纯ascii字符,python自带的字符串格式化或者.format()函数是可以解决问题了。然而有中文就对不齐了,还有°C之类的字符,还要考虑颜色字符如’\033[0;32mhello\033[0m’。

处理代码在AatFrontend.aatpad()。

在这里需要明确的一点是,“字节串长度跟字符串长度是2个不同的概念;不同的字符在终端显示的宽度不一定相同”
比如字符’a’和’雨’,

  • ‘a’用1个字节,’雨’在utf-8下用3个字节
  • ‘a’是1个字符,’雨’是一个字符
  • 在我的iterm2终端下,2个’a’占用的宽度才顶一个’雨’

处理的思路大概是这样,天气的图标是事先画好的,宽度都一样。只需要考虑图标右边的信息。因为信息是左对齐的,所以,只需要算出右边需要填充多少空格就行了。此消彼长,中文字符占用多的空间,相应的右边就少填充些空格。

最后,不管是英文版还是中文版,都可以正常显示表格,对齐虚线。

终端是怎么显示颜色的?

参见 《飘逸的python - 彩色你的控制台》

json前端

怎么把一个嵌套的python对象给转成json呢?

自定义JSONEncoder,重写default()方法。

class ComplexEncoder(json.JSONEncoder):
    DATE_FORMAT = "%Y-%m-%d"
    TIME_FORMAT = "%H:%M:%S"

    def default(self, obj):
        if hasattr(obj, 'to_json'):
            return obj.to_json()
        elif isinstance(obj, datetime.datetime):
            return obj.strftime("%s %s" % (self.DATE_FORMAT, self.TIME_FORMAT))
        else:
            return json.JSONEncoder.default(self, obj)


class JSONFrontend(iface.Frontend):
    def render(self, data, unit):
        print json.dumps(data, cls=ComplexEncoder, indent=4, ensure_ascii=False)

当然,还需要类提供to_json()方法。 这里放在父类DataObject里,其它子类都自动继承了这个方法,不用每个子类都写一遍。


class DataObject(object):
    ...
    def to_json(self):
        return {attr: getattr(self, attr) for attr in self.__slots__}

编写setup.py

怎么实现安装后可以直接在终端执行weatpy而不用python weatpy.py呢?

在setup.py里面指定

setup(
    ...
    entry_points={
        'console_scripts': ['weatpy = weatpy.main:main']
    },
    ...
)

怎么在安装的时候自动创建~/.weatpyrc配置文件呢?

在setup.py中可以写自定义脚本,当然也可以用来创建文件了。

from distutils.command.build_py import build_py

class my_build_py(build_py):
    def run(self):
        # honor the --dry-run flag
        if not self.dry_run:
            if not os.path.exists(CONFIG_FILE):
                print 'creating %s' % CONFIG_FILE
                with open(CONFIG_FILE, 'w') as f:
                    f.write(...)

        # distutils uses old-style classes, so no super()
        build_py.run(self)

setup(
    ...
    cmdclass={'build_py': my_build_py},
)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章