愈來愈多好用的開源專案是用 Python 開發的,對我來說,開源專案最迷人之處莫過於 - 踩到 Bug 自己抓,功能不夠自己加,這麼說來,Python 加減也該會看會改。

最近從某個開源專案認識新東西 - Bottle,一個輕量級的 Python 網站框架,不依賴其他套件,只用 Python 標準程式庫,一支 .py 檔即可運作,跟當初看到 NancyFx 一樣,我對這種小而美的輕巧兵器毫無抵抗力! 便把玩了一下,準備為該專案新增檔案下載功能。

為方便與 Docker 接軌,我在 Linux (CentOS) 上練習。準備開發環境花了點功夫,筆記以下:

  1. CentOS 7 內建的 Python 是 2.7.5,要先升級成 3.x。CentOS 7.7+ 版本可直接安裝:
    cat /etc/redhat-release #檢查 CentOS 版本,確認是 7.7+
    sudo yum update -y
    sudo yum install -y python3
    
  2. CentOS 的 git 也要額外安裝,sudo yum install git 裝的是 1.8.3 版,版本過舊,參考在 CentOS 6/7 以 yum 安裝 git 2IUS 來源安裝 git 2.2.4:
    sudo yum install https://repo.ius.io/ius-release-el7.rpm 
    sudo yum provides git* # 找 IUS Git 套件的名稱,查得 git222
    sudo yum install git222
    

接著我練習用 Bottle 建一個簡單檔案下載網站。開始前要先安裝 Bottle 模組(實際上,它只有一個檔案,bottle.py,直接下載來用也成):

sudo pip3 install bottle 

假想情境是結果檔案會被放在特定目錄下,要寫個簡易下載介面讓使用者可透過瀏覽器下載,因為是家用私人服務,就不花時間寫權限控管,開放匿名存取。我設計了三個 Action,/files 顯示特定目錄下的結果檔案清單,/download/hashId 可下載檔案,/delete/hashId 則是刪除檔案。Bottle 透過 @route("/action") 定義不同網址的回應行為,@route("/action/<varName>") 可從路由取參數。讀檔及刪檔時不使用檔名,而是採取雜湊碼比對,存取範圍嚴格限制在指定目錄內,可降低資安風險。取得檔案清單後轉成 Dictionary,以檔名雜湊碼為 Key,使用白名單方式限定只能下載項目,避免有人在路徑參數動手腳幹壞事。對映到檔案後,再使用 Bottle 提供的 static_file 傳回檔案內容。短短幾行,Python 門外漢搞了一陣子才寫好,大部分的時間花在查 C# Directory.GetFiles()、ToDictionary()、string.Join()、File.Delete() 對映的 Python 寫法。完整程式如下:

from bottle import route, run, template, static_file, abort, redirect
from os import listdir,remove
from os.path import isfile, isdir, join, dirname

filesPath = join(dirname(__file__), 'files')

def toLink(n, f):
    return template('<li><a href="/download/{{n}}" download="{{n}}">{{text}}</a> <a href="/delete/{{n}}" class="del">&times;</a></li>', n=n, text=f)

def getFiles():
    filesDict = dict()
    for f in listdir(filesPath):
        filesDict[str(hash(f))] = f  
    return filesDict

@route('/')
def index():
    return redirect('/files')

@route('/files')
def files():
    html = ['<style>.del {color:red;text-decoration:none}</style><ul>']
    files = getFiles()
    for key in files.keys():
        html.append(toLink(key, files[key]))
    html.append('</ul>')
    return '\n'.join(html)

@route('/download/<n>')
def download(n):
    files = getFiles()
    if n not in files:
        abort(404, 'file not found - ' + n)
    return static_file(files[n], root=filesPath)

@route('/delete/<n>')
def delete(n):
    files = getFiles()
    if n not in files:
        abort(404, 'no such file')
    remove(join(filesPath, files[n]))
    return redirect('/files')

run(host='localhost', port=8080)

比預期順利,程式在 Linux 跑了起來,成功完成讀檔、刪檔操作:

就醬,我寫出人生第一個實用 Python 小網站,默默前進了一步。

但我必須說,武功練得愈深,換兵器就愈痛苦。一來是原本用 C# 彈指可成的功能,換用 Python 寫頻頻卡關,難免心浮氣躁;二則常會想到簡潔性、模組化、效能、安全上的進階議題,初學者沒想那麼多傻傻寫好會動就當 OK (無知有無知的快樂呀),老鳥既然想到就不能無視,但用陌生語言實現這類進階要求格外吃力。就像平常說中文字句斟酌出口成章慣了,改用英文便會卡在某個成語或形容詞不會翻,想用對倒裝補述分詞構句又對文法沒把握,吞吞吐吐擠不出半句。而最要命的是,邊寫心中不斷浮現大哉問 - 人生苦短,明明用 C# 可輕鬆完成,拎杯真的要花時間再學一套功用重複的程式語言嗎?我應該只會將 Python 練到看得懂可以維護修改別人的專案就好,需求複雜到一定程度還是寫 .NET 較有效率,反正跨平台已不是 .NET 的限制,退一百步,還有呼叫外部程式或 WebAPI 方式整合 Python 的大絕可用,呵。

補充:如果對 Python 有興趣想入門的同學,可以試試微軟為初學者製作的 Python 教學影片

Trying to use Python Bottle to write a mini file downloading service in CentOS.


Comments

# by Kevan

晚輩參見真人,在下有禮了

# by Ho.Chun

最後的總結太棒了!!

# by jamesli

看起來跟Flask很像

Post a comment