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

好的,各位观众老爷,今天咱们来聊聊 Django REST Framework (DRF) 的自定义渲染器和解析器。这俩玩意儿就像 DRF 的“变形金刚”,能让它处理各种奇葩的非标准数据格式,让你的 API 接口不再挑食,啥都能吃进去,啥都能吐出来!

第一章:渲染器(Renderer)—— API 的“化妆师”

想象一下,你的 API 返回的数据就像一个素颜美女,虽然底子好,但直接展示给用户可能不够惊艳。渲染器的作用就是给数据“化妆”,把它转换成用户需要的格式,比如 JSON, XML, HTML 等等。

1.1 为什么要自定义渲染器?

DRF 已经内置了一堆常用的渲染器,像 JSONRendererBrowsableAPIRenderer 啥的,但世界之大,无奇不有,总有些数据格式是 DRF 搞不定的,比如:

  • 特殊格式的 CSV: 你可能需要自定义 CSV 的分隔符、引号字符等等。
  • 协议缓冲区 (Protocol Buffers): Google 出品的序列化格式,效率高,但需要特定的渲染器。
  • 自定义 XML: 你可能需要按照特定的 XML Schema 生成 XML 数据。

这时候,就需要咱们自己动手,打造一个专属的渲染器了。

1.2 自定义渲染器的基本结构

一个自定义渲染器就是一个 Python 类,继承自 rest_framework.renderers.BaseRenderer,至少需要实现以下两个方法:

  • media_type:指定渲染器处理的 MIME 类型,比如 'application/json''text/csv'
  • render(self, data, accepted_media_type=None, renderer_context=None):核心方法,将数据 data 转换成字符串并返回。

1.3 实战演练:自定义 CSV 渲染器

假设我们需要一个 CSV 渲染器,允许自定义分隔符和引号字符。

from rest_framework.renderers import BaseRenderer
import csv
import io

class CustomCSVRenderer(BaseRenderer):
    media_type = 'text/csv'
    format = 'csv'
    charset = 'utf-8'
    header = None #可自定义的表头
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def render(self, data, accepted_media_type=None, renderer_context=None):
        """
        Renders `data` into CSV, using the separator and quote character
        as specified by the `renderer_context`.
        """
        if data is None:
            return ''

        # 从上下文获取自定义参数,如果没有,则使用默认值
        separator = renderer_context['request'].query_params.get('separator', ',')
        quotechar = renderer_context['request'].query_params.get('quotechar', '"')

        # 表头处理
        if isinstance(data, list) and data:  # 确保 data 是列表且不为空
            if self.header is None: #如果未定义表头,则从数据中提取
                header = list(data[0].keys()) #从第一个数据项中提取键作为表头
            else:
                header = self.header #如果定义了表头,则使用
            buffer = io.StringIO()
            writer = csv.writer(buffer, delimiter=separator, quotechar=quotechar, quoting=csv.QUOTE_MINIMAL)
            writer.writerow(header) #写入表头
            for row in data:
                writer.writerow([row.get(field, '') for field in header]) #写入数据
            return buffer.getvalue().encode(self.charset)
        else: #如果data不是列表,则返回空字符串或者其他合适的错误信息
            return ''.encode(self.charset)

# 使用方法,假设你有一个视图函数
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny

class ExampleView(APIView):
    renderer_classes = [CustomCSVRenderer]
    permission_classes = [AllowAny] # 允许所有用户访问

    def get(self, request):
        data = [
            {'id': 1, 'name': 'Alice', 'age': 30},
            {'id': 2, 'name': 'Bob', 'age': 25},
        ]
        return Response(data)

#配置表头的方式
class ExampleViewWithHeader(APIView):
    renderer_classes = [CustomCSVRenderer]
    permission_classes = [AllowAny]
    def get(self, request):
        data = [
            {'id': 1, 'name': 'Alice', 'age': 30},
            {'id': 2, 'name': 'Bob', 'age': 25},
        ]
        renderer = CustomCSVRenderer() #初始化渲染器
        renderer.header = ['ID', '姓名', '年龄']  # 设置自定义表头
        self.renderer_classes = [type('CustomCSVRendererWithHeader', (CustomCSVRenderer,), {'header': ['ID', '姓名', '年龄']})] #动态创建渲染器,并设置表头
        return Response(data)

代码解释:

  • media_typeformat 定义了 MIME 类型和格式名。
  • render() 方法从 renderer_context 中获取分隔符和引号字符,如果用户没有指定,则使用默认值。
  • csv.writer 用于生成 CSV 数据。
  • 使用 io.StringIO 作为内存文件,避免直接操作磁盘文件。
  • 返回的字符串需要编码成 UTF-8。

1.4 如何使用自定义渲染器?

有两种方法:

  • 在视图类中指定:renderer_classes 属性中添加你的自定义渲染器。
  • 在全局设置中指定:settings.py 中配置 DEFAULT_RENDERER_CLASSES,将你的自定义渲染器添加到列表中。

例如:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'my_app.renderers.CustomCSVRenderer',
        'rest_framework.renderers.JSONRenderer',
    ]
}

1.5 高级技巧

  • 处理二进制数据: 有些数据格式(比如图片)是二进制的,你需要使用 open('image.jpg', 'rb').read() 读取二进制数据,然后直接返回。
  • 使用模板引擎: 对于复杂的 HTML 渲染,可以使用 Django 的模板引擎,将数据传递给模板进行渲染。
  • 根据请求头选择渲染器: 你可以根据 accepted_media_type 参数的值,选择不同的渲染器。

第二章:解析器(Parser)—— API 的“胃”

解析器就像 API 的“胃”,负责将客户端发送的请求数据(比如 JSON、XML、表单数据)解析成 Python 对象,供视图函数使用。

2.1 为什么要自定义解析器?

和渲染器一样,DRF 内置的解析器已经很强大了,但总有些情况需要我们自己动手:

  • 处理自定义的 JSON 格式: 有些客户端可能发送不标准的 JSON 数据,比如字段名大小写不一致,或者缺少某些字段。
  • 处理 XML 数据: XML 数据格式复杂,需要特定的解析器来处理。
  • 处理二进制数据: 客户端可能上传图片、音频等二进制文件,需要解析器来处理。

2.2 自定义解析器的基本结构

一个自定义解析器就是一个 Python 类,继承自 rest_framework.parsers.BaseParser,至少需要实现以下两个方法:

  • media_type:指定解析器处理的 MIME 类型,比如 'application/json''text/xml'
  • parse(self, stream, media_type=None, parser_context=None):核心方法,从输入流 stream 中读取数据,并解析成 Python 对象并返回。

2.3 实战演练:自定义 JSON 解析器

假设我们需要一个 JSON 解析器,能够将字段名统一转换成小写。

from rest_framework.parsers import JSONParser
from rest_framework import exceptions
import json

class LowercaseJSONParser(JSONParser):
    media_type = 'application/json'

    def parse(self, stream, media_type=None, parser_context=None):
        """
        Parses the incoming bytestream as JSON and returns the resulting data.
        """
        try:
            data = stream.read().decode('utf-8')
            data = json.loads(data)
            # 将字段名转换为小写
            def lowercase_keys(obj):
                if isinstance(obj, dict):
                    return {k.lower(): lowercase_keys(v) for k, v in obj.items()}
                elif isinstance(obj, list):
                    return [lowercase_keys(elem) for elem in obj]
                else:
                    return obj

            return lowercase_keys(data)

        except ValueError as exc:
            raise exceptions.ParseError('JSON parse error - %s' % str(exc))

# 使用方法,假设你有一个视图函数
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny

class ExampleView(APIView):
    parser_classes = [LowercaseJSONParser]
    permission_classes = [AllowAny] # 允许所有用户访问

    def post(self, request):
        data = request.data
        return Response(data)

代码解释:

  • media_type 定义了 MIME 类型。
  • parse() 方法首先读取输入流,然后使用 json.loads() 解析成 Python 对象。
  • lowercase_keys() 函数递归地将字典和列表中的字段名转换为小写。
  • 如果解析出错,则抛出 exceptions.ParseError 异常。

2.4 如何使用自定义解析器?

和渲染器一样,有两种方法:

  • 在视图类中指定:parser_classes 属性中添加你的自定义解析器。
  • 在全局设置中指定:settings.py 中配置 DEFAULT_PARSER_CLASSES,将你的自定义解析器添加到列表中。

例如:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PARSER_CLASSES': [
        'my_app.parsers.LowercaseJSONParser',
        'rest_framework.parsers.JSONParser',
    ]
}

2.5 高级技巧

  • 处理文件上传: 使用 stream.read() 读取文件内容,然后根据文件类型进行处理。
  • 验证数据:parse() 方法中可以对数据进行验证,如果数据不合法,则抛出异常。
  • 使用第三方库: 可以使用第三方库来解析 XML、YAML 等数据格式。

第三章:渲染器和解析器的配合

渲染器和解析器就像一对好基友,一个负责将数据转换成客户端需要的格式,一个负责将客户端发送的数据转换成 Python 对象。它们可以一起工作,让你的 API 接口更加灵活和强大。

3.1 例子:处理自定义的 XML 格式

假设我们需要一个 API 接口,能够接收和返回自定义的 XML 格式。

首先,我们需要一个自定义的 XML 渲染器:

from rest_framework.renderers import BaseRenderer
import xml.etree.ElementTree as ET
from xml.dom import minidom

class CustomXMLRenderer(BaseRenderer):
    media_type = 'application/xml'
    format = 'xml'
    charset = 'utf-8'

    def render(self, data, accepted_media_type=None, renderer_context=None):
        if data is None:
            return ''.encode(self.charset)

        root = ET.Element('root')
        self._to_xml(root, data)

        xml_string = ET.tostring(root, encoding=self.charset)
        # 美化XML输出
        dom = minidom.parseString(xml_string)
        return dom.toprettyxml(indent="  ", encoding=self.charset)

    def _to_xml(self, parent, data):
        if isinstance(data, dict):
            for key, value in data.items():
                element = ET.SubElement(parent, key)
                self._to_xml(element, value)
        elif isinstance(data, list):
            for item in data:
                element = ET.SubElement(parent, 'item')
                self._to_xml(element, item)
        else:
            parent.text = str(data)

然后,我们需要一个自定义的 XML 解析器:

from rest_framework.parsers import BaseParser
from rest_framework import exceptions
import xml.etree.ElementTree as ET

class CustomXMLParser(BaseParser):
    media_type = 'application/xml'

    def parse(self, stream, media_type=None, parser_context=None):
        try:
            xml_string = stream.read().decode('utf-8')
            root = ET.fromstring(xml_string)
            return self._to_dict(root)
        except ET.ParseError as exc:
            raise exceptions.ParseError('XML parse error - %s' % str(exc))

    def _to_dict(self, element):
        data = {}
        for child in element:
            if len(child):
                data[child.tag] = self._to_dict(child)
            else:
                data[child.tag] = child.text
        return data

最后,在视图函数中使用它们:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny

class ExampleView(APIView):
    renderer_classes = [CustomXMLRenderer]
    parser_classes = [CustomXMLParser]
    permission_classes = [AllowAny]

    def get(self, request):
        data = {
            'message': 'Hello, world!',
            'items': [
                {'id': 1, 'name': 'Alice'},
                {'id': 2, 'name': 'Bob'},
            ]
        }
        return Response(data)

    def post(self, request):
        data = request.data
        return Response(data)

总结

DRF 的自定义渲染器和解析器是两个非常强大的工具,它们可以让你处理各种非标准的数据格式,让你的 API 接口更加灵活和强大。掌握了它们,你就能够轻松应对各种奇葩的需求,成为 API 开发高手!

一些建议:

  • 在自定义渲染器和解析器之前,先看看 DRF 是否已经提供了相应的支持。
  • 尽量使用标准的数据格式,避免自定义过于复杂的数据格式。
  • 编写清晰的代码和注释,方便自己和他人阅读和维护。
  • 多做实验,多踩坑,才能真正掌握这些工具。

好了,今天的讲座就到这里,感谢大家的观看!

发表回复

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