1、前期準(zhǔn)備
通過(guò) pip 或 easy_install 安裝了 pymongo 之后, 就能通過(guò) Python 調(diào)教 mongodb 了.
接著安裝個(gè) flask 用來(lái)當(dāng) web 服務(wù)器.
當(dāng)然 mongo 也是得安裝的. 對(duì)于 Ubuntu 用戶, 特別是使用 Server 12.04 的同學(xué), 安裝最新版要略費(fèi)些周折, 具體說(shuō)是
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list sudo apt-get update sudo apt-get install mongodb-10gen
如果你跟我一樣覺(jué)得讓通過(guò)上傳文件名的后綴判別用戶上傳的什么文件完全是捏著山藥當(dāng)小黃瓜一樣欺騙自己, 那么最好還準(zhǔn)備個(gè) Pillow 庫(kù)
pip install Pillow
或 (更適合 Windows 用戶)
easy_install Pillow
2、正片
2.1 Flask 文件上傳
Flask 官網(wǎng)上那個(gè)例子居然分了兩截讓人無(wú)從吐槽. 這里先弄個(gè)最簡(jiǎn)單的, 無(wú)論什么文件都先弄上來(lái)
import flask app = flask.Flask(__name__) app.debug = True @app.route('/upload', methods=['POST']) def upload(): f = flask.request.files['uploaded_file'] print f.read() return flask.redirect('/') @app.route('/') def index(): return '''''' if __name__ == '__main__': app.run(port=7777)
注: 在 upload 函數(shù)中, 使用 flask.request.files[KEY] 獲取上傳文件對(duì)象, KEY 為頁(yè)面 form 中 input 的 name 值
因?yàn)槭窃诤笈_(tái)輸出內(nèi)容, 所以測(cè)試最好拿純文本文件來(lái)測(cè).
2.2 保存到 mongodb
如果不那么講究的話, 最快速基本的存儲(chǔ)方案里只需要
import pymongo import bson.binary from cStringIO import StringIO app = flask.Flask(__name__) app.debug = True db = pymongo.MongoClient('localhost', 27017).test def save_file(f): content = StringIO(f.read()) db.files.save(dict( content= bson.binary.Binary(content.getvalue()), )) @app.route('/upload', methods=['POST']) def upload(): f = flask.request.files['uploaded_file'] save_file(f) return flask.redirect('/')
把內(nèi)容塞進(jìn)一個(gè)? bson.binary.Binary? 對(duì)象, 再把它扔進(jìn) mongodb 就可以了.
現(xiàn)在試試再上傳個(gè)什么文件, 在 mongo shell 中通過(guò)? db.files.find() 就能看到了.
不過(guò) content? 這個(gè)域幾乎肉眼無(wú)法分辨出什么東西, 即使是純文本文件, mongo 也會(huì)顯示為 Base64 編碼.
2.3 提供文件訪問(wèn)
給定存進(jìn)數(shù)據(jù)庫(kù)的文件的 ID (作為 URI 的一部分), 返回給瀏覽器其文件內(nèi)容, 如下
def save_file(f): content = StringIO(f.read()) c = dict(content=bson.binary.Binary(content.getvalue())) db.files.save(c) return c['_id'] @app.route('/f/') def serve_file(fid): f = db.files.find_one(bson.objectid.ObjectId(fid)) return f['content'] @app.route('/upload', methods=['POST']) def upload(): f = flask.request.files['uploaded_file'] fid = save_file(f) return flask.redirect( '/f/' + str(fid))
上傳文件之后,? upload? 函數(shù)會(huì)跳轉(zhuǎn)到對(duì)應(yīng)的文件瀏覽頁(yè). 這樣一來(lái), 文本文件內(nèi)容就可以正常預(yù)覽了, 如果不是那么挑剔換行符跟連續(xù)空格都被瀏覽器吃掉的話.
2.4 當(dāng)找不到文件時(shí)
有兩種情況, 其一, 數(shù)據(jù)庫(kù) ID 格式就不對(duì), 這時(shí) pymongo 會(huì)拋異常? bson.errors.InvalidId ; 其二, 找不到對(duì)象 (!), 這時(shí) pymongo 會(huì)返回? None .
簡(jiǎn)單起見(jiàn)就這樣處理了
@app.route('/f/') def serve_file(fid): import bson.errors try: f = db.files.find_one(bson.objectid.ObjectId(fid)) if f is None: raise bson.errors.InvalidId() return f['content'] except bson.errors.InvalidId: flask.abort(404)
2.5 正確的 MIME
從現(xiàn)在開(kāi)始要對(duì)上傳的文件嚴(yán)格把關(guān)了, 文本文件, 狗與剪刀等皆不能上傳.
判斷圖片文件之前說(shuō)了我們動(dòng)真格用 Pillow
from PIL import Image allow_formats = set(['jpeg', 'png', 'gif']) def save_file(f): content = StringIO(f.read()) try: mime = Image.open(content).format.lower() if mime not in allow_formats: raise IOError() except IOError: flask.abort(400) c = dict(content=bson.binary.Binary(content.getvalue())) db.files.save(c) return c['_id']
然后試試上傳文本文件肯定虛, 傳圖片文件才能正常進(jìn)行. 不對(duì), 也不正常, 因?yàn)閭魍晏D(zhuǎn)之后, 服務(wù)器并沒(méi)有給出正確的 mimetype, 所以仍然以預(yù)覽文本的方式預(yù)覽了一坨二進(jìn)制亂碼.
要解決這個(gè)問(wèn)題, 得把 MIME 一并存到數(shù)據(jù)庫(kù)里面去; 并且, 在給出文件時(shí)也正確地傳輸 mimetype
def save_file(f): content = StringIO(f.read()) try: mime = Image.open(content).format.lower() if mime not in allow_formats: raise IOError() except IOError: flask.abort(400) c = dict(content=bson.binary.Binary(content.getvalue()), mime=mime) db.files.save(c) return c['_id'] @app.route('/f/') def serve_file(fid): try: f = db.files.find_one(bson.objectid.ObjectId(fid)) if f is None: raise bson.errors.InvalidId() return flask.Response(f['content'], mimetype='image/' + f['mime']) except bson.errors.InvalidId: flask.abort(404)
當(dāng)然這樣的話原來(lái)存進(jìn)去的東西可沒(méi)有 mime 這個(gè)屬性, 所以最好先去 mongo shell 用? db.files.drop()? 清掉原來(lái)的數(shù)據(jù).
2.6 根據(jù)上傳時(shí)間給出 NOT MODIFIED
利用 HTTP 304 NOT MODIFIED 可以盡可能壓榨與利用瀏覽器緩存和節(jié)省帶寬. 這需要三個(gè)操作
1)、記錄文件最后上傳的時(shí)間
2)、當(dāng)瀏覽器請(qǐng)求這個(gè)文件時(shí), 向請(qǐng)求頭里塞一個(gè)時(shí)間戳字符串
3)、當(dāng)瀏覽器請(qǐng)求文件時(shí), 從請(qǐng)求頭中嘗試獲取這個(gè)時(shí)間戳, 如果與文件的時(shí)間戳一致, 就直接 304
體現(xiàn)為代碼是
import datetime def save_file(f): content = StringIO(f.read()) try: mime = Image.open(content).format.lower() if mime not in allow_formats: raise IOError() except IOError: flask.abort(400) c = dict( content=bson.binary.Binary(content.getvalue()), mime=mime, time=datetime.datetime.utcnow(), ) db.files.save(c) return c['_id'] @app.route('/f/') def serve_file(fid): try: f = db.files.find_one(bson.objectid.ObjectId(fid)) if f is None: raise bson.errors.InvalidId() if flask.request.headers.get('If-Modified-Since') == f['time'].ctime(): return flask.Response(status=304) resp = flask.Response(f['content'], mimetype='image/' + f['mime']) resp.headers['Last-Modified'] = f['time'].ctime() return resp except bson.errors.InvalidId: flask.abort(404)
然后, 得弄個(gè)腳本把數(shù)據(jù)庫(kù)里面已經(jīng)有的圖片給加上時(shí)間戳.
順帶吐個(gè)槽, 其實(shí) NoSQL DB 在這種環(huán)境下根本體現(xiàn)不出任何優(yōu)勢(shì), 用起來(lái)跟 RDB 幾乎沒(méi)兩樣.
2.7 利用 SHA-1 排重
與冰箱里的可樂(lè)不同, 大部分情況下你肯定不希望數(shù)據(jù)庫(kù)里面出現(xiàn)一大波完全一樣的圖片. 圖片, 連同其 EXIFF 之類的數(shù)據(jù)信息, 在數(shù)據(jù)庫(kù)中應(yīng)該是惟一的, 這時(shí)使用略強(qiáng)一點(diǎn)的散列技術(shù)來(lái)檢測(cè)是再合適不過(guò)了.
達(dá)到這個(gè)目的最簡(jiǎn)單的就是建立一個(gè)? SHA-1? 惟一索引, 這樣數(shù)據(jù)庫(kù)就會(huì)阻止相同的東西被放進(jìn)去.
在 MongoDB 中表中建立惟一 索引 , 執(zhí)行 (Mongo 控制臺(tái)中)
db.files.ensureIndex({sha1: 1}, {unique: true})
如果你的庫(kù)中有多條記錄的話, MongoDB 會(huì)給報(bào)個(gè)錯(cuò). 這看起來(lái)很和諧無(wú)害的索引操作被告知數(shù)據(jù)庫(kù)中有重復(fù)的取值 null (實(shí)際上目前數(shù)據(jù)庫(kù)里已有的條目根本沒(méi)有這個(gè)屬性). 與一般的 RDB 不同的是, MongoDB 規(guī)定 null, 或不存在的屬性值也是一種相同的屬性值, 所以這些幽靈屬性會(huì)導(dǎo)致惟一索引無(wú)法建立.
解決方案有三個(gè):
1)刪掉現(xiàn)在所有的數(shù)據(jù) (一定是測(cè)試數(shù)據(jù)庫(kù)才用這種不負(fù)責(zé)任的方式吧!)
2)建立一個(gè) sparse 索引, 這個(gè)索引不要求幽靈屬性惟一, 不過(guò)出現(xiàn)多個(gè) null 值還是會(huì)判定重復(fù) (不管現(xiàn)有數(shù)據(jù)的話可以這么搞)
3)寫(xiě)個(gè)腳本跑一次數(shù)據(jù)庫(kù), 把所有已經(jīng)存入的數(shù)據(jù)翻出來(lái), 重新計(jì)算 SHA-1, 再存進(jìn)去
具體做法隨意. 假定現(xiàn)在這個(gè)問(wèn)題已經(jīng)搞定了, 索引也弄好了, 那么剩是 Python 代碼的事情了.
import hashlib def save_file(f): content = StringIO(f.read()) try: mime = Image.open(content).format.lower() if mime not in allow_formats: raise IOError() except IOError: flask.abort(400) sha1 = hashlib.sha1(content.getvalue()).hexdigest() c = dict( content=bson.binary.Binary(content.getvalue()), mime=mime, time=datetime.datetime.utcnow(), sha1=sha1, ) try: db.files.save(c) except pymongo.errors.DuplicateKeyError: pass return c['_id']
在上傳文件這一環(huán)就沒(méi)問(wèn)題了. 不過(guò), 按照上面這個(gè)邏輯, 如果上傳了一個(gè)已經(jīng)存在的文件, 返回? c['_id']? 將會(huì)是一個(gè)不存在的數(shù)據(jù) ID. 修正這個(gè)問(wèn)題, 最好是返回? sha1 , 另外, 在訪問(wèn)文件時(shí), 相應(yīng)地修改為用文件 SHA-1 訪問(wèn), 而不是用 ID.
最后修改的結(jié)果及本篇完整源代碼如下 :
import hashlib import datetime import flask import pymongo import bson.binary import bson.objectid import bson.errors from cStringIO import StringIO from PIL import Image app = flask.Flask(__name__) app.debug = True db = pymongo.MongoClient('localhost', 27017).test allow_formats = set(['jpeg', 'png', 'gif']) def save_file(f): content = StringIO(f.read()) try: mime = Image.open(content).format.lower() if mime not in allow_formats: raise IOError() except IOError: flask.abort(400) sha1 = hashlib.sha1(content.getvalue()).hexdigest() c = dict( content=bson.binary.Binary(content.getvalue()), mime=mime, time=datetime.datetime.utcnow(), sha1=sha1, ) try: db.files.save(c) except pymongo.errors.DuplicateKeyError: pass return sha1 @app.route('/f/') def serve_file(sha1): try: f = db.files.find_one({'sha1': sha1}) if f is None: raise bson.errors.InvalidId() if flask.request.headers.get('If-Modified-Since') == f['time'].ctime(): return flask.Response(status=304) resp = flask.Response(f['content'], mimetype='image/' + f['mime']) resp.headers['Last-Modified'] = f['time'].ctime() return resp except bson.errors.InvalidId: flask.abort(404) @app.route('/upload', methods=['POST']) def upload(): f = flask.request.files['uploaded_file'] sha1 = save_file(f) return flask.redirect('/f/' + str(sha1)) @app.route('/') def index(): return ''' ''' if __name__ == '__main__': app.run(port=7777)
3、REF
Developing RESTful Web APIs with Python, Flask and MongoDB
http://www.slideshare.net/nicolaiarocci/developing-restful-web-apis-with-python-flask-and-mongodb
https://github.com/nicolaiarocci/eve
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
