Python中使用Flask、MongoDB搭建簡(jiǎn)易圖片服務(wù)器
1、前期準(zhǔn)備
通過 pip 或 easy_install 安裝了 pymongo 之后, 就能通過 Python 調(diào)教 mongodb 了.
接著安裝個(gè) flask 用來當(dāng) web 服務(wù)器.
當(dāng)然 mongo 也是得安裝的. 對(duì)于 Ubuntu 用戶, 特別是使用 Server 12.04 的同學(xué), 安裝最新版要略費(fèi)些周折, 具體說是
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
如果你跟我一樣覺得讓通過上傳文件名的后綴判別用戶上傳的什么文件完全是捏著山藥當(dāng)小黃瓜一樣欺騙自己, 那么最好還準(zhǔn)備個(gè) Pillow 庫(kù)
pip install Pillow
或 (更適合 Windows 用戶)
easy_install Pillow
2、正片
2.1 Flask 文件上傳
Flask 官網(wǎng)上那個(gè)例子居然分了兩截讓人無從吐槽. 這里先弄個(gè)最簡(jiǎn)單的, 無論什么文件都先弄上來
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 ''' <!doctype html> <html> <body> <form action='/upload' method='post' enctype='multipart/form-data'> <input type='file' name='uploaded_file'> <input type='submit' value='Upload'> </form> ''' 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è)試最好拿純文本文件來測(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 中通過 db.files.find() 就能看到了.
不過 content 這個(gè)域幾乎肉眼無法分辨出什么東西, 即使是純文本文件, mongo 也會(huì)顯示為 Base64 編碼.
2.3 提供文件訪問
給定存進(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/<fid>') 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è). 這樣一來, 文本文件內(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)單起見就這樣處理了
@app.route('/f/<fid>') 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)在開始要對(duì)上傳的文件嚴(yán)格把關(guān)了, 文本文件, 狗與剪刀等皆不能上傳.
判斷圖片文件之前說了我們動(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ù)器并沒有給出正確的 mimetype, 所以仍然以預(yù)覽文本的方式預(yù)覽了一坨二進(jìn)制亂碼.
要解決這個(gè)問題, 得把 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/<fid>') 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)然這樣的話原來存進(jìn)去的東西可沒有 mime 這個(gè)屬性, 所以最好先去 mongo shell 用 db.files.drop() 清掉原來的數(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/<fid>') 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ì), 用起來跟 RDB 幾乎沒兩樣.
2.7 利用 SHA-1 排重
與冰箱里的可樂不同, 大部分情況下你肯定不希望數(shù)據(jù)庫(kù)里面出現(xiàn)一大波完全一樣的圖片. 圖片, 連同其 EXIFF 之類的數(shù)據(jù)信息, 在數(shù)據(jù)庫(kù)中應(yīng)該是惟一的, 這時(shí)使用略強(qiáng)一點(diǎn)的散列技術(shù)來檢測(cè)是再合適不過了.
達(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ò). 這看起來很和諧無害的索引操作被告知數(shù)據(jù)庫(kù)中有重復(fù)的取值 null (實(shí)際上目前數(shù)據(jù)庫(kù)里已有的條目根本沒有這個(gè)屬性). 與一般的 RDB 不同的是, MongoDB 規(guī)定 null, 或不存在的屬性值也是一種相同的屬性值, 所以這些幽靈屬性會(huì)導(dǎo)致惟一索引無法建立.
解決方案有三個(gè):
1)刪掉現(xiàn)在所有的數(shù)據(jù) (一定是測(cè)試數(shù)據(jù)庫(kù)才用這種不負(fù)責(zé)任的方式吧!)
2)建立一個(gè) sparse 索引, 這個(gè)索引不要求幽靈屬性惟一, 不過出現(xiàn)多個(gè) null 值還是會(huì)判定重復(fù) (不管現(xiàn)有數(shù)據(jù)的話可以這么搞)
3)寫個(gè)腳本跑一次數(shù)據(jù)庫(kù), 把所有已經(jīng)存入的數(shù)據(jù)翻出來, 重新計(jì)算 SHA-1, 再存進(jìn)去
具體做法隨意. 假定現(xiàn)在這個(gè)問題已經(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)就沒問題了. 不過, 按照上面這個(gè)邏輯, 如果上傳了一個(gè)已經(jīng)存在的文件, 返回 c['_id'] 將會(huì)是一個(gè)不存在的數(shù)據(jù) ID. 修正這個(gè)問題, 最好是返回 sha1 , 另外, 在訪問文件時(shí), 相應(yīng)地修改為用文件 SHA-1 訪問, 而不是用 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/<sha1>') 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 ''' <!doctype html> <html> <body> <form action='/upload' method='post' enctype='multipart/form-data'> <input type='file' name='uploaded_file'> <input type='submit' value='Upload'> </form> ''' 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
相關(guān)文章
Python中內(nèi)置數(shù)據(jù)類型list,tuple,dict,set的區(qū)別和用法
這篇文章主要給大家介紹了Python中內(nèi)置數(shù)據(jù)類型list,tuple,dict,set的區(qū)別和用法,都是非?;A(chǔ)的知識(shí),十分的細(xì)致全面,有需要的小伙伴可以參考下。2015-12-12使用python Telnet遠(yuǎn)程登錄執(zhí)行程序的方法
今天小編就為大家分享一篇使用python Telnet遠(yuǎn)程登錄執(zhí)行程序的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-01-01淺談pytorch 模型 .pt, .pth, .pkl的區(qū)別及模型保存方式
這篇文章主要介紹了淺談pytorch 模型 .pt, .pth, .pkl的區(qū)別及模型保存方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-05-05Python使用Py2neo創(chuàng)建Neo4j的節(jié)點(diǎn)和關(guān)系
Neo4j是一款開源圖數(shù)據(jù)庫(kù),使用Python語言訪問Neo4j可以使用Py2neo。本文介紹了使用Py2neo訪問Neo4j,批量創(chuàng)建節(jié)點(diǎn)和關(guān)系的方法2021-08-08Python Django框架實(shí)現(xiàn)應(yīng)用添加logging日志操作示例
這篇文章主要介紹了Python Django框架實(shí)現(xiàn)應(yīng)用添加logging日志操作,結(jié)合實(shí)例形式分析了Django框架中添加Python內(nèi)建日志模塊相關(guān)操作技巧,需要的朋友可以參考下2019-05-05快速了解Python開發(fā)中的cookie及簡(jiǎn)單代碼示例
這篇文章主要介紹了快速了解Python開發(fā)中的cookie及簡(jiǎn)單代碼示例,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01關(guān)于你不想知道的所有Python3 unicode特性
我的讀者知道我是一個(gè)喜歡痛罵Python3 unicode的人。這次也不例外。我將會(huì)告訴你用unicode有多痛苦和為什么我不能閉嘴。我花了兩周時(shí)間研究Python3,我需要發(fā)泄我的失望。在這些責(zé)罵中,仍然有有用的信息,因?yàn)樗涛覀內(nèi)绾蝸硖幚鞵ython3。如果沒有被我煩到,就讀一讀吧2014-11-11