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},
)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章