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’佔用的寬度才頂一個’雨’
處理的思路大概是這樣,天氣的圖標是事先畫好的,寬度都一樣。只需要考慮圖標右邊的信息。因爲信息是左對齊的,所以,只需要算出右邊需要填充多少空格就行了。此消彼長,中文字符佔用多的空間,相應的右邊就少填充些空格。
最後,不管是英文版還是中文版,都可以正常顯示錶格,對齊虛線。
終端是怎麼顯示顏色的?
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},
)