Flask架構+MongoDB資料庫搭建簡易圖片程式

來源:互聯網
上載者:User

1、前期準備

通過 pip 或 easy_install 安裝了 pymongo 之後, 就能通過 Python 調教 mongodb 了.
接著安裝個 flask 用來當 網頁伺服器.
當然 mongo 也是得安裝的. 對於 Ubuntu 使用者, 特別是使用 Server 12.04 的同學, 安裝最新版要略費些周折, 具體說是

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

如果你跟我一樣覺得讓通過上傳檔案名稱的尾碼判別使用者上傳的什麼檔案完全是捏著山藥當小黃瓜一樣欺騙自己, 那麼最好還準備個 Pillow 庫

pip install Pillow

或 (更適合 Windows 使用者)
   
easy_install Pillow

2、正片

2.1 Flask 檔案上傳

Flask 官網上那個例子居然分了兩截讓人無從吐槽. 這裡先弄個最簡單的, 無論什麼檔案都先弄上來

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 函數中, 使用 flask.request.files[KEY] 擷取上傳檔案對象, KEY 為頁面 form 中 input 的 name 值

因為是在後台輸出內容, 所以測試最好拿純文字檔案來測.

2.2 儲存到 mongodb

如果不那麼講究的話, 最快速基本的儲存方案裡只需要

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('/')

把內容塞進一個  bson.binary.Binary  對象, 再把它扔進 mongodb 就可以了.

現在試試再上傳個什麼檔案, 在 mongo shell 中通過  db.files.find() 就能看到了.

不過 content  這個域幾乎肉眼無法分辨出什麼東西, 即使是純文字檔案, mongo 也會顯示為 Base 64 編碼.

2.3 提供檔案訪問

給定存進資料庫的檔案的 ID (作為 URI 的一部分), 返回給瀏覽器其檔案內容, 如下

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  函數會跳轉到對應的檔案瀏覽頁. 這樣一來, 文字檔內容就可以正常預覽了, 如果不是那麼挑剔分行符號跟連續空格都被瀏覽器吃掉的話.

2.4 當找不到檔案時

有兩種情況, 其一, 資料庫 ID 格式就不對, 這時 pymongo 會拋異常  bson.errors.InvalidId ; 其二, 找不到對象 (!), 這時 pymongo 會返回  None .
簡單起見就這樣處理了

@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

從現在開始要對上傳的檔案嚴格把關了, 文字檔, 狗與剪刀等皆不能上傳.
判斷圖片檔案之前說了我們動真格用 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']

然後試試上傳文字檔肯定虛, 傳圖片檔案才能正常進行. 不對, 也不正常, 因為傳完跳轉之後, 伺服器並沒有給出正確的 mimetype, 所以仍然以預覽文本的方式預覽了一坨二進位亂碼.

要解決這個問題, 得把 MIME 一併存到資料庫裡面去; 並且, 在給出檔案時也正確地傳輸 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)

當然這樣的話原來存進去的東西可沒有 mime 這個屬性, 所以最好先去 mongo shell 用  db.files.drop()  清掉原來的資料.

2.6 根據上傳時間給出 NOT MODIFIED

利用 HTTP 304 NOT MODIFIED 可以儘可能壓榨與利用瀏覽器緩衝和節省頻寬. 這需要三個操作

    記錄檔案最後上傳的時間
    當瀏覽器請求這個檔案時, 向要求標頭裡塞一個時間戳記字串
    當瀏覽器請求檔案時, 從要求標頭中嘗試擷取這個時間戳記, 如果與檔案的時間戳記一致, 就直接 304

體現為代碼是

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)

然後, 得弄個指令碼把資料庫裡面已經有的圖片給加上時間戳記.
順帶吐個槽, 其實 NoSQL DB 在這種環境下根本體現不出任何優勢, 用起來跟 RDB 幾乎沒兩樣.

2.7 利用 SHA-1 排重

與冰箱裡的可樂不同, 大部分情況下你肯定不希望資料庫裡面出現一大波完全一樣的圖片. 圖片, 連同其 EXIFF 之類的資料資訊, 在資料庫中應該是惟一的, 這時使用略強一點的散列技術來檢測是再合適不過了.
達到這個目的最簡單的就是建立一個  SHA-1  惟一索引, 這樣資料庫就會阻止相同的東西被放進去.
在 MongoDB 中表中建立惟一 索引 , 執行 (Mongo 控制台中)

db.files.ensureIndex({sha1: 1}, {unique: true})

如果你的庫中有多條記錄的話, MongoDB 會給報個錯. 這看起來很和諧無害的索引操作被告知資料庫中有重複的取值 null (實際上目前資料庫裡已有的條目根本沒有這個屬性). 與一般的 RDB 不同的是, MongoDB 規定 null, 或不存在的屬性值也是一種相同的屬性值, 所以這些幽靈屬性會導致惟一索引無法建立.
解決方案有三個:

    刪掉現在所有的資料 (一定是測試資料庫才用這種不負責任的方式吧!)
    建立一個 sparse 索引, 這個索引不要求幽靈屬性惟一, 不過出現多個 null 值還是會判定重複 (不管現有資料的話可以這麼搞)
    寫個指令碼跑一次資料庫, 把所有已經存入的資料翻出來, 重新計算 SHA-1, 再存進去

具體做法隨意. 假定現在這個問題已經搞定了, 索引也弄好了, 那麼剩是 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']

在上傳檔案這一環就沒問題了. 不過, 按照上面這個邏輯, 如果上傳了一個已經存在的檔案, 返回  c['_id']  將會是一個不存在的資料 ID. 修正這個問題, 最好是返回  sha1 , 另外, 在訪問檔案時, 相應地修改為用檔案 SHA-1 訪問, 而不是用 ID.

最後修改的結果及本篇完整原始碼如下 :

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

https://github.com/nicolaiarocci/eve

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.