Django REST Framework 自定义渲染器/解析器:处理非标准数据格式

好的,各位观众老爷,欢迎来到今天的“非标准数据格式大作战”讲座!我是你们的老朋友,专门负责解决各种奇葩数据格式问题的编程小能手。今天咱们就来聊聊 Django REST Framework (DRF) 里自定义渲染器和解析器,让咱们的 DRF 不仅能处理 JSON,还能搞定各种你想不到的非标准数据格式。

第一部分:什么是渲染器和解析器?为什么要自定义?

首先,咱们得搞清楚,啥是渲染器,啥是解析器?

  • 解析器 (Parser):顾名思义,就是把客户端发来的数据“解析”成 Python 能够理解的东西。通常情况下,DRF 默认的解析器会把 JSON、表单数据、多部分表单数据解析成 request.data 里的 Python 字典或列表。

  • 渲染器 (Renderer):反过来,渲染器就是把咱们的 Python 对象“渲染”成客户端能看懂的格式,比如 JSON、XML、HTML 等等。渲染器决定了 API 返回的数据格式。

那为啥要自定义呢?难道默认的不好用吗?

当然不是不好用,只是世界太复杂,总有一些奇葩的需求:

  • 历史遗留问题:有些老系统,用的就是一些非标准的数据格式,你总不能让别人为了你一个 API 把整个系统都改了吧?
  • 性能优化:有些场景下,JSON 效率不高,需要用更紧凑的二进制格式。
  • 特殊需求:比如,你需要返回一个 CSV 文件,或者一个 YAML 文件,或者一个 TXT 文件,或者一个……(此处省略一万种奇葩格式)。
  • 对接第三方系统:第三方的接口可能就只支持XML等格式。

总之,为了应对这些情况,我们就需要自定义渲染器和解析器,让 DRF 能够灵活处理各种数据格式。

第二部分:自定义解析器 (Parser)

咱们先从解析器开始。自定义解析器需要继承 rest_framework.parsers.BaseParser 类,并实现 parse() 方法。

from rest_framework import parsers
from rest_framework.exceptions import ParseError

class MyCustomParser(parsers.BaseParser):
    """
    一个自定义的解析器,用来解析特定格式的数据。
    """
    media_type = 'application/my-custom-format' # 指定 Content-Type

    def parse(self, stream, media_type=None, parser_context=None):
        """
        解析传入的流数据。

        Args:
            stream: 一个类文件对象,包含请求体数据。
            media_type: 请求的 Content-Type。
            parser_context: 一个字典,包含解析器的上下文信息。

        Returns:
            解析后的数据,通常是一个 Python 字典或列表。

        Raises:
            ParseError: 如果解析过程中发生错误。
        """
        try:
            # 这里写你的解析逻辑
            # 从 stream 中读取数据
            data = stream.read().decode('utf-8') #假设是utf-8编码
            # 假设我们的数据格式是 "key1=value1&key2=value2"
            result = {}
            for item in data.split('&'):
                key, value = item.split('=')
                result[key] = value
            return result
        except Exception as e:
            raise ParseError(f"解析错误: {e}")

代码解释:

  1. media_type: 这个属性非常重要,它指定了你的解析器能处理的 Content-Type。客户端在发送请求时,必须设置正确的 Content-Type,DRF 才能找到对应的解析器。
  2. parse(): 这个方法是核心,它接收一个 stream 对象,你可以把它当成一个文件来读取数据。media_type 再次传入,以防万一。parser_context 包含一些上下文信息,比如 request 对象、view 对象等等,你可以在这里获取一些额外的信息。
  3. stream.read(): 从流中读取数据,通常是字节流,需要解码成字符串。
  4. result = {}: 创建一个空字典,用来存储解析后的数据。
  5. data.split('&'): 假设我们的数据格式是 key1=value1&key2=value2,所以我们用 & 分割。
  6. key, value = item.split('='): 然后用 = 分割键值对。
  7. result[key] = value: 把键值对存入字典。
  8. raise ParseError: 如果解析过程中发生任何错误,都要抛出一个 ParseError 异常,DRF 会自动处理这个异常,返回一个友好的错误提示。

如何使用自定义解析器?

有两种方法:

  • 在 View 级别指定:

    from rest_framework.views import APIView
    from rest_framework.response import Response
    
    class MyView(APIView):
        parser_classes = [MyCustomParser]
    
        def post(self, request):
            data = request.data  # 这里面的数据就是经过 MyCustomParser 解析后的数据
            return Response({'message': '收到数据', 'data': data})
  • 在全局设置中指定:

    settings.py 文件中,修改 REST_FRAMEWORK 设置:

    REST_FRAMEWORK = {
        'DEFAULT_PARSER_CLASSES': [
            'path.to.MyCustomParser',
            'rest_framework.parsers.JSONParser',  # 别忘了加上默认的 JSON 解析器
        ]
    }

    注意,全局设置会影响所有的 API 视图。

第三部分:自定义渲染器 (Renderer)

自定义渲染器和自定义解析器类似,也需要继承一个基类 rest_framework.renderers.BaseRenderer,并实现 render() 方法。

from rest_framework import renderers

class MyCustomRenderer(renderers.BaseRenderer):
    """
    一个自定义的渲染器,用来渲染特定格式的数据。
    """
    media_type = 'application/my-custom-format' # 指定 Accept
    format = 'custom' # 指定 format后缀

    def render(self, data, media_type=None, renderer_context=None):
        """
        渲染数据。

        Args:
            data: 要渲染的数据,通常是一个 Python 字典或列表。
            media_type: 请求的 Accept 头部。
            renderer_context: 一个字典,包含渲染器的上下文信息。

        Returns:
            渲染后的数据,通常是一个字节流。
        """
        try:
            # 这里写你的渲染逻辑
            # 假设我们的数据格式是 "key1=value1&key2=value2"
            result = []
            for key, value in data.items():
                result.append(f"{key}={value}")
            output = '&'.join(result)
            return output.encode('utf-8')  # 必须返回字节流
        except Exception as e:
            return str(e).encode('utf-8')

代码解释:

  1. media_type: 这个属性也很重要,它指定了你的渲染器能处理的 Accept 头部。客户端在发送请求时,可以设置 Accept 头部,告诉服务器它希望接收的数据格式。
  2. format: 这个属性指定了 URL 中的 format 参数。比如,如果你的 API 是 http://example.com/api/data.custom,那么 DRF 会自动使用 MyCustomRenderer 来渲染数据。
  3. render(): 这个方法是核心,它接收要渲染的 datamedia_type 再次传入,以防万一。renderer_context 包含一些上下文信息,比如 request 对象、view 对象、response 对象等等,你可以在这里获取一些额外的信息。
  4. output.encode('utf-8'): 必须把渲染后的数据编码成字节流,才能返回给客户端。

如何使用自定义渲染器?

和解析器一样,也有两种方法:

  • 在 View 级别指定:

    from rest_framework.views import APIView
    from rest_framework.response import Response
    
    class MyView(APIView):
        renderer_classes = [MyCustomRenderer]
    
        def get(self, request):
            data = {'key1': 'value1', 'key2': 'value2'}
            return Response(data)  # 这里面的数据会被 MyCustomRenderer 渲染
  • 在全局设置中指定:

    settings.py 文件中,修改 REST_FRAMEWORK 设置:

    REST_FRAMEWORK = {
        'DEFAULT_RENDERER_CLASSES': [
            'path.to.MyCustomRenderer',
            'rest_framework.renderers.JSONRenderer',  # 别忘了加上默认的 JSON 渲染器
        ]
    }

第四部分:更复杂的例子:CSV 渲染器

咱们来一个更实用的例子:CSV 渲染器。这个渲染器可以把 Python 列表或字典渲染成 CSV 文件。

import csv
from rest_framework import renderers

class CSVRenderer(renderers.BaseRenderer):
    media_type = 'text/csv'
    format = 'csv'
    charset = 'utf-8'

    def render(self, data, media_type=None, renderer_context=None):
        """
        渲染数据为 CSV 格式。
        """
        if data is None:
            return b''

        if not isinstance(data, list):
            data = [data]  # 确保数据是列表

        # 获取 response 对象,设置 Content-Disposition 头部
        response = renderer_context['response']
        response['Content-Disposition'] = 'attachment; filename="data.csv"'

        # 获取 CSV 写入器
        writer = csv.writer(response)

        # 写入头部
        header = data[0].keys() if data else []  # 假设所有字典的键相同
        writer.writerow(header)

        # 写入数据
        for row in data:
            writer.writerow(row.values())

        return response

代码解释:

  1. media_type = 'text/csv': 指定 Content-Typetext/csv
  2. format = 'csv': 指定 format 参数为 csv
  3. charset = 'utf-8': 指定字符集为 utf-8
  4. response = renderer_context['response']: 从 renderer_context 中获取 response 对象。
  5. response['Content-Disposition'] = 'attachment; filename="data.csv"': 设置 Content-Disposition 头部,告诉浏览器这是一个附件,文件名为 data.csv
  6. writer = csv.writer(response): 创建一个 CSV 写入器,把数据写入 response 对象。
  7. header = data[0].keys() if data else []: 获取 CSV 头部,也就是字典的键。
  8. writer.writerow(header): 写入头部。
  9. writer.writerow(row.values()): 写入数据。
  10. return response: 返回 response 对象。注意,这里直接返回 response 对象,而不是字节流。因为 csv.writer 已经把数据写入了 response 对象。

如何使用 CSV 渲染器?

和前面一样,也有两种方法:

  • 在 View 级别指定:

    from rest_framework.views import APIView
    from rest_framework.response import Response
    
    class MyView(APIView):
        renderer_classes = [CSVRenderer]
    
        def get(self, request):
            data = [
                {'name': '张三', 'age': 18},
                {'name': '李四', 'age': 20},
            ]
            return Response(data)
  • 在全局设置中指定:

    settings.py 文件中,修改 REST_FRAMEWORK 设置:

    REST_FRAMEWORK = {
        'DEFAULT_RENDERER_CLASSES': [
            'path.to.CSVRenderer',
            'rest_framework.renderers.JSONRenderer',
        ]
    }

第五部分:一些小技巧和注意事项

  • Content Negotiation (内容协商):DRF 会根据客户端的 Accept 头部和 URL 中的 format 参数,自动选择合适的渲染器。
  • 优先级format 参数的优先级高于 Accept 头部。
  • 异常处理:在 parse()render() 方法中,一定要做好异常处理,避免程序崩溃。
  • 测试:写完自定义渲染器和解析器后,一定要写测试用例,确保它们能够正确工作。
  • DRF 提供的工具类: DRF 提供了一些现成的渲染器和解析器的基类,可以简化你的开发工作。 比如TemplateHTMLRenderer可以很方便的返回html模版。

第六部分:常见问题和解决方案

  • 解析器不生效? 检查 Content-Type 是否正确,以及解析器是否被正确注册。
  • 渲染器不生效? 检查 Accept 头部是否正确,以及渲染器是否被正确注册。
  • 中文乱码? 确保使用正确的字符集,比如 utf-8
  • 数据类型不匹配? 确保解析后的数据类型和视图期望的数据类型一致。

第七部分:总结

自定义渲染器和解析器是 DRF 的一个强大特性,可以让你灵活处理各种非标准数据格式。虽然写起来稍微麻烦一点,但是一旦掌握了,就能解决很多实际问题。

希望今天的讲座对大家有所帮助!记住,编程就像做菜,只有掌握了各种食材和调料,才能做出美味佳肴。而自定义渲染器和解析器,就是咱们 DRF 的秘密武器!

表格总结:

概念 作用 需要继承的类 核心方法 重要属性
解析器 把客户端数据解析成 Python 对象 rest_framework.parsers.BaseParser parse() media_type
渲染器 把 Python 对象渲染成客户端可读的格式 rest_framework.renderers.BaseRenderer render() media_type, format

好了,今天的课程就到这里,各位下课! 希望大家在实际项目能够活学活用!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注