Python 中的字节
Python 中的字节其实是一个不会被经常讨论的东西, 大家用 python 通常是为了写一些胶水程序, 或者做深度学习研究等。
而 bytes 这个概念显然对于 python 来说过于底层了, 但是不巧, 最近在处理一些网络传输的需求时, 我们不得不开始面对这个问题。
flask 获取图片
对于不同类型的数据, 我们是通过不同的请求提交给 flask 的.
如果需要传输的内容只是 json, 那么可以放在 request body 里面提交, 如果是图像这种二进制文件, 则需要通过 form 进行提交.
实际上在网络传输的过程中, 这些文件也都是通过字节流的形式发送到服务器的, 但是 werkzeug 和 flask 对于接收到的文件进行了封装.
1 | from flask import request |
具体来看, uploaded_file
这个文件的类别为 werkzeug.datastructures.FileStorage
, 看起来并不是一个 bytearray.
不过我想那么多, 就直接调用 FileStorage.save
的方法, 将其保存在本地, 一直到这里都没有出现任何问题.
上传到存储桶
为了统一管理产品中的各种对象文件, 我们用 Minio 做了一个存储桶, 并且打算把接受到用户提交的图片给上传到桶里。
Minio 提供了 put_object
的方法, 引用 minio-py 的描述知道, put_object
对于 data
的要求是要实现 read()
的方法, 然后返回对象的 bytes.
put_object(bucket_name, object_name, data, length, content_type=”application/octet-stream”, metadata=None, sse=None, progress=None, part_size=0, num_parallel_uploads=3, tags=None, retention=None, legal_hold=False)
| Param | Type | Description |
| —- | —- | —- |
| bucket_name | str | Name of the bucket. |
| object_name | str | Object name in the bucket. |
| data | object | An object having callable read() returning bytes object. |
调用 put_object
方法, 代码如下. 此时出现的问题是, 提交到 Minio 的对象是空的, 没有图片内容。
但是如果把 uploaded_file.save(f"./static/xxx.png")
这个代码去掉, 向 Minio 提交的内容就是正常的了。
1 | from flask import request |
原因是什么, 为什么把对象保存到本地之后, 再向 Minio 提交就会出错呢。
操作字节流
原因在于我们调用 FileStorage.save
方法之后, 实际上改变了 FileStorage
的底层。
概括的说, FileStorage
对象仍然以 bytesarray 的形式处于内存中, 这些字节对象都用变量 _pos
来记录当前位置的地址。
当 save
方法被调用后, 可以读取到 bytesarray 从 _pos
开始往后的字节。
同时 _pos
也被移动了, 再次调用 save
方法的时候 _pos
已经不在原来的位置, 所以无法得到有效的结果。
更加具体的分析如下。
我们先对 uploaded_file
进行更深入的剖析:
variable | class | Method |
---|---|---|
uploaded_file |
werkzeug.datastructures.FileStorage |
… |
uploaded_file.stream |
tempfile.SpooledTemporaryFile |
read , seek , tell , write , flush , … |
uploaded_file.stream._file |
_io.BytesIO |
read , seek , tell , write , flush , … |
可以看出 FileStorage
提供了 Minio put_object
所要求的 read
方法。
FileStorage.stream._file
的类为 _io.BytesIO
, 我们去看 cpython 对于 io.read
的实现 _pyio#L922-L941 的实现。
1 | class BytesIO(BufferedIOBase): |
可以发现 read
方法实际读取了 _buffer
从当前位置 _pos
到文件结尾的所有内容。而 read
调用结束之后, _pos
指向了新的位置, 所以当我们试图再次调用 uploaded_file.stream
的 read
方法将不会得到任何有效的内容。
因此我们需要在下次读取的时候, 重新把 _pos
的位置调回到 buffer 的开头, 可以通过上面代码中的 io.seek
方法 _pyio.py#L967-L986 把 _pos
复位。
最后新的代码为
1 | from flask import request |
总结
这是一个非常小的 debug 的过程, 但是它揭示了 python 的另外一面。
我们平时还是把 python 当作脚本语言用的, 对于字节的操作算是它稍微底层一点的知识。
对比 c++ 这样直接操作内存的语言, python 能够封装到绝大多数人都很难接触到它对字节的管理, 不能说不是巨大的成功。