Python中使用Flask、MongoDB搭建简易图片服务器

 更新时间:2015年02月04日 09:55:43   投稿:junjie  
这篇文章主要介绍了Python中使用Flask、MongoDB搭建简易图片服务器,本文是一个详细完整的教程,需要的朋友可以参考下

1、前期准备

通过 pip 或 easy_install 安装了 pymongo 之后, 就能通过 Python 调教 mongodb 了.
接着安装个 flask 用来当 web 服务器.

当然 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 也会显示为 Base64 编码.

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 可以尽可能压榨与利用浏览器缓存和节省带宽. 这需要三个操作

1)、记录文件最后上传的时间
2)、当浏览器请求这个文件时, 向请求头里塞一个时间戳字符串
3)、当浏览器请求文件时, 从请求头中尝试获取这个时间戳, 如果与文件的时间戳一致, 就直接 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, 或不存在的属性值也是一种相同的属性值, 所以这些幽灵属性会导致惟一索引无法建立.

解决方案有三个:

1)删掉现在所有的数据 (一定是测试数据库才用这种不负责任的方式吧!)
2)建立一个 sparse 索引, 这个索引不要求幽灵属性惟一, 不过出现多个 null 值还是会判定重复 (不管现有数据的话可以这么搞)
3)写个脚本跑一次数据库, 把所有已经存入的数据翻出来, 重新计算 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

相关文章

  • python 读写excel文件操作示例【附源码下载】

    python 读写excel文件操作示例【附源码下载】

    这篇文章主要介绍了python 读写excel文件操作,结合实例形式分析了Python基于xlutils导入xlrd,xlwt库操作Excel相关实现技巧,并附带源码供读者下载参考,需要的朋友可以参考下
    2019-06-06
  • python3 正则表达式基础廖雪峰

    python3 正则表达式基础廖雪峰

    正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的
    2020-03-03
  • Python如何发送Syslog日志

    Python如何发送Syslog日志

    这篇文章主要介绍了Python如何发送Syslog日志问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09
  • pycharm运行程序时出现Run‘python tests for XXX.py‘问题及解决

    pycharm运行程序时出现Run‘python tests for XXX.py‘问题及

    这篇文章主要介绍了pycharm运行程序时出现Run ‘python tests for XXX.py‘问题及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08
  • django使用LDAP验证的方法示例

    django使用LDAP验证的方法示例

    这篇文章主要介绍了django使用LDAP验证的方法示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-12-12
  • python Scala函数与访问修辞符实例详解

    python Scala函数与访问修辞符实例详解

    这篇文章主要为大家介绍了python Scala函数与访问修辞符实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • python实现数组平移K位问题

    python实现数组平移K位问题

    这篇文章主要介绍了python实现数组平移K位问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • PyTorch中的方法torch.randperm()示例介绍

    PyTorch中的方法torch.randperm()示例介绍

    在 PyTorch 中,torch.randperm(n) 函数用于生成一个从 0 到 n-1 的随机排列的整数序列,这篇文章主要介绍了PyTorch中的方法torch.randperm()介绍,需要的朋友可以参考下
    2024-05-05
  • Pycharm安装python库的方法

    Pycharm安装python库的方法

    这篇文章主要介绍了Pycharm安装python库的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • Python爬虫动态IP代理使用及防止被封的方法

    Python爬虫动态IP代理使用及防止被封的方法

    在进行网络爬虫时,经常会遇到网站的反爬机制,其中之一就是通过IP封禁来限制爬虫的访问,为了规避这种限制,使用动态IP代理是一种有效的方法,本文将介绍在Python爬虫中如何使用动态IP代理,以及一些防止被封的方法,文中有详细的代码讲解,需要的朋友可以参考下
    2023-11-11

最新评论