前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用py3fdfs - 踩坑实录 __str__ return non-string (type bytes)

使用py3fdfs - 踩坑实录 __str__ return non-string (type bytes)

作者头像
lesM10
发布2019-08-26 17:09:03
2.2K0
发布2019-08-26 17:09:03
举报

django上传图片 和 用户获得html页面后请求图片 流程

  • 后台运营人员 通过djangoadmin页面,进行(图片)文件的上传
  • django使用 自定义的storage类,把文件上传到fastdfs
  • fastdfs根据文件内容,得到文件名(/group1/M00...)。并 返回文件名(/group1/M00...)django
  • django文件名(/group1/M00...) 存储到数据库表的某个字段内。
  • 网站用户访问某个页面(比如/goods页面),django返回 经过渲染的页面(相关标签 被替换成真实的 数据) 给用户。
  • 用户用经过渲染的页面中的url地址 向 远端的nginx(nginxfastdfs的storage服务器 是部署在一起的) 请求资源文件。
  • nginx返回 资源文件

fastdfs的优点:

  • 使得存储容量的扩展 很方便。
  • 解决了 上传文件时,文件名相同 而文件内容不同 带来的问题。因为fastdfs是根据文件内容 生成 文件名的。
  • fastdfs和nginx结合使用,可提高 网站提供资源的 效率。

在pycharm中导入py3fdfs

代码语言:javascript
复制
# 根据pypi中py3fdfs的示例: 在python3命令行执行下面两句
>>> from fdfs_client.client import *
>>> client = Fdfs_client('/etc/fdfs/client.conf')

在执行client = Fdfs_client('/etc/fdfs/client.conf')时,会报错:TypeError: type object argument after ** must be a mapping, not str 解决方法: 1)根据报错位置,定位到PycharmProjects/dailyfresh/venv/lib/python3.6/site-packages/fdfs_client/client.py。 观察到 如下的代码:

代码语言:javascript
复制
class Fdfs_client(object):
    '''
    Class Fdfs_client implemented Fastdfs client protol ver 3.08.

    It's useful upload, download, delete file to or from fdfs server, etc. It's uses
    connection pool to manage connection to server.
    '''

    def __init__(self, trackers, poolclass=ConnectionPool):
        self.trackers = trackers
        self.tracker_pool = poolclass(**self.trackers)
        self.timeout = self.trackers['timeout']
        return None

发觉Fdfs_client的初始化要传递trackers, 而不是'/etc/fdfs/client.conf'字符串。 接着观察到 文件顶部 有如下代码:

代码语言:javascript
复制
def get_tracker_conf(conf_path='client.conf'):
    cf = Fdfs_ConfigParser()
    tracker = {}
    try:
        cf.read(conf_path)
        timeout = cf.getint('__config__', 'connect_timeout')
        tracker_list = cf.get('__config__', 'tracker_server')
        if isinstance(tracker_list, str):
            tracker_list = [tracker_list]
        tracker_ip_list = []
        for tr in tracker_list:
            tracker_ip, tracker_port = tr.split(':')
            tracker_ip_list.append(tracker_ip)
        tracker['host_tuple'] = tuple(tracker_ip_list)
        tracker['port'] = int(tracker_port)
        tracker['timeout'] = timeout
        tracker['name'] = 'Tracker Pool'
    except:
        raise
    return tracker

def get_tracker_conf(conf_path='client.conf'):不就是 返回一个tracker么,而且其接收的参数是client.conf配置文件的路径def get_tracker_conf(conf_path='client.conf'):函数的作用是:把配置文件client.conf中信息,提取到一个字典tracker中,并返回 该字典tracker

那么我们可以使用如下的代码 来代替(同时也是正确的):

代码语言:javascript
复制
# 由配置文件中的信息 得到 字典trackers 
trackers = get_tracker_conf('/Users/leesam/PycharmProjects/dailyfresh/utils/fdfs/client.conf')
# 把
client = Fdfs_client(trackers)

别急,填完这个坑,路的前方 还有好多坑呢。哈哈哈哈啊哈哈。

自定义了 文件存储类 用来将admin管理页面 添加的一条记录 保存到远端fdfs,点击保存按钮时,出现了 如下错误

__str__ return non-string (type bytes)

报错的意思大概是:返回了非字符串的bytes类型。 由于之前 在项目中 只添加了如下的代码,而且 只有2个方法(只有2个返回值)。一个明确返回False,那么 错误 大概是出在 return filename这行。 况且, filename = res.get('Remote file_id')的确是 返回的bytes类型。那么,我们要把其从字节类型转换到字符串类型。 使用decode()函数,把字节类型的 filename转换到字符串类型。 把return filename修改为return filename.decode()即可。

str(value), the type of value is bytes

原因分析: 自己写的文件存储类,返回的是字节型类型的文件名。执行的时候,在django内部的get_prep_value模块 接收到了 该文件名参数,并使用了str(value)进行了封装。所以, 才会报错__str__ returned non-string (type bytes). 由于,报错位置 跟 实际问题的位置 不在一个地方,所以 问题藏得比较隐蔽。

文件存储类的代码如下(注意_save的返回值: 返回字符串类型):

代码语言:javascript
复制
from django.core.files.storage import Storage

from fdfs_client.client import *

class FDFSStorage(Storage):
    '''fastdfs文件存储类'''

    def _open(self, name, mode='rb'):
        '''打开文件时 调用该函数'''
        pass

    # 通过后台管理页面,选文件 并 上传时
    # django会调用_save方法(并给_save方法传递2个参数:name: 所要上传文件的名字,content: (包含文件内容的)File类的实例对象)
    def _save(self, name, content):
        '''保存文件时 调用该函数'''
        # name: 所要上传文件的名字
        # content: File类的实例(包含上传文件内容的File实例对象)

        # 创建一个Fdfs_client对象
        # client = Fdfs_client('./utils/fdfs/client.conf')    #会根据./utils/fdfs/client.conf文件的配置,传给远端的tracker
        trackers = get_tracker_conf('/Users/leesam/PycharmProjects/dailyfresh/utils/fdfs/client.conf')
        client = Fdfs_client(trackers)

        # 上传文件到 fastdfs文件系统 中
        # content.read() 可以从File的实例对象content中 读取 文件内容
        # upload_by_buffer返回内容为 字典。格式如下 注释部分
        res = client.upload_by_buffer(content.read()) # upload_by_buffer 根据文件内容 上传文件

        # dict {
        #
        #     'Group name': group_name,
        #     'Remote file_id': remote_file_id,
        #     'Status': 'Upload successed.',
        #     'Local file name': '',
        #     'Uploaded size': upload_size,
        #     'Storage IP': storage_ip
        #
        # }

        if res.get('Status') != 'Upload successed.':
            # 上传失败
            raise Exception('upload file to fastdfs failed.')

        # 获取 返回的 文件id
        filename = res.get('Remote file_id')

        return filename.decode()

    # django在调用_save之前,会先调用_exists
    # _exists 根据 文件的name,判断 文件 是否存在于 文件系统中。存在:返回True,不存在:返回False
    def exists(self, name):
        '''django 判断 文件名 是否可用'''
        # 因为 文件是存储在 fastdfs文件系统中的,所以 对于django来说:不存在 文件名不可用 的情况
        return False

改进方法:

在setting.py增加以下内容

代码语言:javascript
复制
# 设置django的文件存储类,上传文件时 django会调用 该文件存储类的相关方法
DEFAULT_FILE_STORAGE = 'utils.fdfs.storage.FDFSStorage'

# 设置 fastdfs文件系统 使用的 client.conf文件路径
FDFS_CLIENT_CONF = './utils/fdfs/client.conf'
# 设置 fastdfs存储服务器上 nginx使用的IP和端口号
FDFS_STORAGE_URL = 'http://10.211.55.15:8888/'

定义自己的storage类:

代码语言:javascript
复制
from django.core.files.storage import Storage

from fdfs_client.client import *

from django.conf import settings

class FDFSStorage(Storage):
    '''fastdfs文件存储类'''
    def  __init__(self, client_conf=None, base_url=None):
        if client_conf == None:
            client_conf = settings.FDFS_CLIENT_CONF
        self.client_conf = client_conf

        if base_url == None:
            base_url = settings.FDFS_STORAGE_URL
        self.base_url = base_url


    def _open(self, name, mode='rb'):
        '''打开文件时 调用该函数'''
        # 用不到 打开文件,所以省略
        pass

    # 通过后台管理页面,选文件 并 上传时
    # django会调用_save方法(并给_save方法传递2个参数:name: 所要上传文件的名字,content: (包含文件内容的)File类的实例对象)
    def _save(self, name, content):
        '''保存文件时 调用该函数'''
        # name: 所要上传文件的名字
        # content: File类的实例(包含上传文件内容的File实例对象)
        # 返回值: fastdfs中 存储文件时 使用的文件名(被保存到 数据库的表 中)

        # 创建一个Fdfs_client对象
        # client = Fdfs_client('./utils/fdfs/client.conf')    #会根据./utils/fdfs/client.conf文件的配置,传给远端的tracker
        # trackers = get_tracker_conf('/Users/leesam/PycharmProjects/dailyfresh/utils/fdfs/client.conf')
        trackers = get_tracker_conf(self.client_conf)
        client = Fdfs_client(trackers)

        # 上传文件到 fastdfs文件系统 中
        # content.read() 可以从File的实例对象content中 读取 文件内容
        # upload_by_buffer返回内容为 字典。格式如下 注释部分
        res = client.upload_by_buffer(content.read()) # upload_by_buffer 根据文件内容 上传文件

        # dict {
        #
        #     'Group name': group_name,
        #     'Remote file_id': remote_file_id,
        #     'Status': 'Upload successed.',
        #     'Local file name': '',
        #     'Uploaded size': upload_size,
        #     'Storage IP': storage_ip
        #
        # }

        if res.get('Status') != 'Upload successed.':
            # 上传失败
            raise Exception('upload file to fastdfs failed.')

        # 获取 返回的 文件id
        filename = res.get('Remote file_id')
        # 只能返回str类型, filename为bytes类型(需要转换类型,不然会报错)
        return filename.decode()

    # django在调用_save之前,会先调用_exists
    # _exists 根据 文件的name,判断 文件 是否存在于 文件系统中。存在:返回True,不存在:返回False
    def exists(self, name):
        '''django 判断 文件名 是否可用'''
        # 因为 文件是存储在 fastdfs文件系统中的,所以 对于django来说:不存在 文件名不可用 的情况
        # 因为 fastdfs是根据文件内容 得到文件名的(不存在文件名相同 文件内容不同,因而 无法存储的问题)
        return False

    def url(self, name):
        '''返回 访问文件name 所需的url路径'''
        # django调用url方法时,所传递的 name参数:数据库 表中所存的 文件名字符串(即是,fastdfs中存储文件时 使用的文件名)
        return self.base_url + name

compare:

代码语言:javascript
复制
# 存储类必须是:deconstructible,以便在迁移中的字段上使用它时可以序列化。 
# 只要你的字段有自己的参数:serializable,
#你可以使用django.utils.deconstruct.deconstructible类装饰器(这是Django在FileSystemStorage上使用的)
@deconstructible
class FdfsStorage(Storage):
    def __init__(self, option=None):
        if not option:
            self.option = settings.CUSTOM_STORAGE_OPTIONS
        else:
            self.option = option
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019.07.30 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档