Python中使用Flask、MongoDB搭建简易图片服务器
主要介绍了Python中使用Flask、MongoDB搭建简易图片服务器,本文是一个详细完整的教程,需要的朋友可以参考下
1、前期准备
通过 pip 或 easy_install 安装了 pymongo 之后, 就能通过 Python 调教 mongodb 了.
接着安装个 flask 用来当 web 服务器.
当然 mongo 也是得安装的. 对于 Ubuntu 用户, 特别是使用 Server 12.04 的同学, 安装最新版要略费些周折, 具体说是
|
1
2
3
4
|
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.listsudo apt-get updatesudo apt-get install mongodb-10gen |
如果你跟我一样觉得让通过上传文件名的后缀判别用户上传的什么文件完全是捏着山药当小黄瓜一样欺骗自己, 那么最好还准备个 Pillow 库
或 (更适合 Windows 用户)
2、正片
2.1 Flask 文件上传
Flask 官网上那个例子居然分了两截让人无从吐槽. 这里先弄个最简单的, 无论什么文件都先弄上来
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import flaskapp = 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
如果不那么讲究的话, 最快速基本的存储方案里只需要
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import pymongoimport bson.binaryfrom cStringIO import StringIOapp = flask.Flask(__name__)app.debug = Truedb = pymongo.MongoClient('localhost', 27017).testdef 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 的一部分), 返回给浏览器其文件内容, 如下
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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 .
简单起见就这样处理了
|
1
2
3
4
5
6
7
8
9
10
|
@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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from PIL import Imageallow_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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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
体现为代码是
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import datetimedef 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 控制台中)
如果你的库中有多条记录的话, MongoDB 会给报个错. 这看起来很和谐无害的索引操作被告知数据库中有重复的取值 null
(实际上目前数据库里已有的条目根本没有这个属性). 与一般的 RDB 不同的是, MongoDB 规定 null,
或不存在的属性值也是一种相同的属性值, 所以这些幽灵属性会导致惟一索引无法建立.
解决方案有三个:
1)删掉现在所有的数据 (一定是测试数据库才用这种不负责任的方式吧!)
2)建立一个 sparse 索引, 这个索引不要求幽灵属性惟一, 不过出现多个 null 值还是会判定重复 (不管现有数据的话可以这么搞)
3)写个脚本跑一次数据库, 把所有已经存入的数据翻出来, 重新计算 SHA-1, 再存进去
具体做法随意. 假定现在这个问题已经搞定了, 索引也弄好了, 那么剩是 Python 代码的事情了.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import hashlibdef 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.
最后修改的结果及本篇完整源代码如下 :
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
import hashlibimport datetimeimport flaskimport pymongoimport bson.binaryimport bson.objectidimport bson.errorsfrom cStringIO import StringIOfrom PIL import Imageapp = flask.Flask(__name__)app.debug = Truedb = pymongo.MongoClient('localhost', 27017).testallow_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
Python中使用Flask、MongoDB搭建简易图片服务器的更多相关文章
- Python中Opencv和PIL.Image读取图片的差异对比
近日,在进行深度学习进行推理的时候,发现不管怎么样都得不出正确的结果,再仔细和正确的代码进行对比了后发现原来是Python中不同的库读取的图片数组是有差异的. image = np.array(Ima ...
- Windows下搭建Nginx图片服务器
在项目最开始,上传图片的时候,服务器先保存原图再使用ImageMagick生成上传图片缩略图,这种方法有很多缺点,例如生成的缩略图的大小是固定的,不能动态请求指定大小的缩略图. 虽然有非常多的图片云存 ...
- FastDFS搭建单机图片服务器(二)
防丢失转载:https://blog.csdn.net/MissEel/article/details/80856194 根据 分布式文件系统 - FastDFS 在 CentOS 下配置安装部署 和 ...
- FastDFS搭建单机图片服务器(一)
防丢失转载:https://blog.csdn.net/MissEel/article/details/80856194 根据 分布式文件系统 - FastDFS 在 CentOS 下配置安装部署 和 ...
- 搭建Nginx图片服务器
搭建Nginx图片服务器 Part-I 安装Nginx 安装PCRE 下载 ngx_cache_purge 并解压,用来清除缓存 下载Nginx并解压 cd nginx-1.7.7 编译,--pref ...
- 第一次项目上Linux服务器(八:——搭建Nginx图片服务器)
一.准备工作 安装Nginx,参考历史博客 二.搭建Nginx图片服务器 1.效果 例如:图片通过xftp服务上传到/home/java/upload目录下,我想通过访问Nginx服务器来访问xftp ...
- 【图片服务器】搭建Nginx图片服务器
一.安装Nginx 二.安装vsftpd 三.开始搭建Nginx图片服务器 1.效果 例如:图片通过ftp服务上传到/home/ftpuser/www/images目录下,我想通过访问Nginx服务器 ...
- Flask+mongodb 实现简易个人博客
最近学习完了<flask-web开发>,实现了一个简易的个人博客网站,由flask+mongodb+bootstrap做成, 这个软件是在阅读<Flask-Web开发>后写的一 ...
- 从无到有,用Nodejs+express+mongodb搭建简易登陆系统
前端处理server表示很蛋疼,初学Node,虽然感觉异常强大,但是学起来还是有些吃力的,Node是工具,它不是万能的,搭建一个系统还是需要借助其他一些工具,对于我这个没怎么接触server的前端来说 ...
随机推荐
- 6月10日-IOS应用-日记本
嗯,经过这几天的学习,我的第一个IOS应用,日记本算是学习完毕了,下面写一篇日记,记录所学到的知识和需要继续学习的地方. 1,首先是ViewController,必须添加两个协议UITableView ...
- 监测scroll
$(window).scroll(function() { var scrollValue = $(this).scrollTop(); var h=200+scrollValue; $('.yui3 ...
- The reference to entity "characterEncoding" must end with the ';' delimiter
数据源配置时加上编码转换格式后出问题了: The reference to entity "characterEncoding" must end with the ';' del ...
- android应用Dialog跳转到Activity
public void back(Context context){ intent = new Intent(); intent.setClass(context, IBookActivity.cla ...
- cocos2d-x游戏开发系列教程-超级玛丽07-CMGameMap(三)-按键处理
在地图初始化好了之后,就开始移动马里奥吧,我在windows下,是使用键盘来移动马里奥的 w是跳,d是前进,a是后退,那么在程序里是怎么来获取这个按键的呢? 普通的windows程序,在按键之后,会有 ...
- Linux的环境变量总结
请见Github博客:http://wuxichen.github.io/Myblog/linux/2014/10/11/LinuxEnvironmentVariables.html
- pxe网络安装操作系统 原理与详细过程
摘要:在实际工作中,我们经常会遇到这样的情况:想要安装Linux但是计算机不带光驱或软驱,或者是笔记本配置的非标准的软驱和光驱,如1394接口,USB接口等,在Linux安装时所引导的Linux内核一 ...
- C# RSA在服务上使用出现拒绝方法错误的解决方法
在做一个快钱接口的时候,遇到了.net RSA加密无法在一台win2008服务器上运行正常,更换到Win2003服务器后出现问题,具体表现如下: “/”应用程序中的服务器错误. ----------- ...
- Android 启动APP时黑屏白屏的三个解决方案(转载)
你会很奇怪,为什么有些app启动时,会出现一会儿的黑屏或者白屏才进入Activity的界面显示,但是有些app却不会如QQ手机端,的确这里要做处理一下.这里先了解一下为什么会出现这样的现象,其实很简单 ...
- 【Hibernate】Illegal attempt to associate a collection with two open sessions
今天在用Hibernate对对象进行修改操作的时候报了这个错. 之前一直没什么错误,但是今天修改了一下表结构,增加了一个OneToMany的映射. 所以在我获取对象,重新set一个变量之后就报了这个错 ...