使用 Django Rest Framework,我如何上传文件并发送 JSON 有效负载?

Posted

技术标签:

【中文标题】使用 Django Rest Framework,我如何上传文件并发送 JSON 有效负载?【英文标题】:Using Django Rest Framework, how can I upload a file AND send a JSON payload? 【发布时间】:2015-07-22 11:35:15 【问题描述】:

我正在尝试编写一个可以接收文件以及 JSON 有效负载的 Django Rest Framework API 处理程序。我已将 MultiPartParser 设置为处理程序解析器。

但是,我似乎不能两者兼得。如果我将有效负载与文件作为多部分请求一起发送,则 JSON 有效负载在 request.data 中以损坏的方式可用(第一个文本部分直到第一个冒号作为键,其余的是数据)。我可以很好地以标准形式参数发送参数 - 但我的 API 的其余部分接受 JSON 有效负载,我希望保持一致。 request.body 引发 *** RawPostDataException: You cannot access body after reading from request's data stream

时无法读取

例如,请求正文中的文件和此有效负载:"title":"Document Title", "description":"Doc Description" 变为:<QueryDict: u'fileUpload': [<InMemoryUploadedFile: 20150504_115355.jpg (image/jpeg)>, <InMemoryUploadedFile: Front end lead.doc (application/msword)>], u'%22title%22': [u'"Document Title", "description":"Doc Description"']>

有没有办法做到这一点?我可以吃我的蛋糕,保持它而不增加任何体重吗?

编辑: 有人建议这可能是Django REST Framework upload image: "The submitted data was not a file" 的副本。它不是。上传和请求是分段完成的,记住文件和上传就可以了。我什至可以使用标准表单变量来完成请求。但我想看看是否可以在其中获取 JSON 有效负载。

【问题讨论】:

Django REST Framework upload image: "The submitted data was not a file"的可能重复 不,不是。编辑问题以解释原因,尽管除了文件上传位之外,我什至看不到这两个问题之间的相似之处。 需要注意的是,application/jsonmultipart/form-data 是不一样的,它们不能一起使用。而 JSON 默认不支持文件上传,您需要使用自定义文件字段(并对其进行 base64 编码)来获得文件上传支持(这是另一个问题所在)。您不能发送带有多部分数据的 JSON,因为多部分根本无法解析 JSON,而 JSON 也无法解析多部分。 我怀疑凯文,但希望在某个地方有办法做到这一点。就文件大小而言,我不确定 base64 是否会受到太多限制,并且我不想让客户端在发送文件时承担编码文件的责任。那我把api标准做成multipart吧。 ***.com/questions/32017119/…的可能重复 【参考方案1】:

对于需要上传文件和发送一些数据的人来说,没有直接的方法可以让它工作。为此,在 json api 规范中有一个 open issue。我见过的一种可能性是使用multipart/related,如here 所示,但我认为在drf 中实现它非常困难。

最后我实现的是将请求发送为formdata。您可以将每个文件作为 file 并将所有其他数据作为文本发送。 现在,为了将数据作为文本发送,您可以使用一个名为 data 的键并将整个 json 作为字符串值发送。

模型.py

class Posts(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    caption = models.TextField(max_length=1000)
    media = models.ImageField(blank=True, default="", upload_to="posts/")
    tags = models.ManyToManyField('Tags', related_name='posts')

serializers.py -> 不需要特殊更改,由于可写的 ManyToMany 字段实现,这里没有显示我的序列化器,因为它太长了。

views.py

class PostsViewset(viewsets.ModelViewSet):
    serializer_class = PostsSerializer
    parser_classes = (MultipartJsonParser, parsers.JSONParser)
    queryset = Posts.objects.all()
    lookup_field = 'id'

您将需要如下所示的自定义解析器来解析 json。

utils.py

from django.http import QueryDict
import json
from rest_framework import parsers

class MultipartJsonParser(parsers.MultiPartParser):

    def parse(self, stream, media_type=None, parser_context=None):
        result = super().parse(
            stream,
            media_type=media_type,
            parser_context=parser_context
        )
        data = 
        # find the data field and parse it
        data = json.loads(result.data["data"])
        qdict = QueryDict('', mutable=True)
        qdict.update(data)
        return parsers.DataAndFiles(qdict, result.files)

邮递员中的请求示例

编辑:

如果您想将每个数据作为键值对发送,请参阅this 扩展答案

【讨论】:

这个答案真的帮助了我多部分 json + 文件上传。特别是我给了我一个想法,如何处理由于嵌套字典被转换为字符串而导致的问题。我必须使用我的自定义解析器将它们转换回正确的 json。 如果媒体和数据字段是外键(多个),你能上传邮递员的图片吗? 这对我不起作用。嵌套的序列化器字段(在我的例子中为“位置”)稍后会被删除,API 会发送“需要标签字段”的响应。我的解析器中确实有“位置”字段,只是没有传递给我认为的验证功能。 如果您在尝试此操作后仍然收到“必填字段”错误。你应该看看这个***.com/questions/68555559/…【参考方案2】:

我知道这是一个旧线程,但我只是遇到了这个。我必须使用MultiPartParser 才能将我的文件和额外数据放在一起。这是我的代码的样子:

# views.py
class FileUploadView(views.APIView):
    parser_classes = (MultiPartParser,)

    def put(self, request, filename, format=None):
        file_obj = request.data['file']
        ftype    = request.data['ftype']
        caption  = request.data['caption']
        # ...
        # do some stuff with uploaded file
        # ...
        return Response(status=204)

我使用 ng-file-upload 的 AngularJS 代码是:

file.upload = Upload.upload(
  url: "/api/picture/upload/" + file.name,
  data: 
    file: file,
    ftype: 'final',
    caption: 'This is an image caption'
  
);

【讨论】:

【参考方案3】:

我发送 JSON 和图像来创建/更新产品对象。下面是一个适用于我的创建 APIView。

序列化器

class ProductCreateSerializer(serializers.ModelSerializer):
    class Meta:
         model = Product
        fields = [
            "id",
            "product_name",
            "product_description",
            "product_price",
          ]
    def create(self,validated_data):
         return Product.objects.create(**validated_data)

查看

from rest_framework  import generics,status
from rest_framework.parsers import FormParser,MultiPartParser

class ProductCreateAPIView(generics.CreateAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductCreateSerializer
    permission_classes = [IsAdminOrIsSelf,]
    parser_classes = (MultiPartParser,FormParser,)

    def perform_create(self,serializer,format=None):
        owner = self.request.user
        if self.request.data.get('image') is not None:
            product_image = self.request.data.get('image')
            serializer.save(owner=owner,product_image=product_image)
        else:
            serializer.save(owner=owner)

示例测试:

def test_product_creation_with_image(self):
    url = reverse('products_create_api')
    self.client.login(username='testaccount',password='testaccount')
    data = 
        "product_name" : "Potatoes",
        "product_description" : "Amazing Potatoes",
        "image" : open("local-filename.jpg","rb")
    
    response = self.client.post(url,data)
    self.assertEqual(response.status_code,status.HTTP_201_CREATED)

【讨论】:

你也可以展示你的序列化器吗?您使用哪个字段作为图像键? 这不是发送 JSON。 你能不能也显示图像序列化器,目标是哪个字段【参考方案4】:

@Nithin 解决方案有效,但本质上这意味着您将 JSON 作为字符串发送,因此在多部分段内不使用实际的 application/json

我们想要的是让后端接受以下格式的数据

------WebKitFormBoundaryrga771iuUYap8BB2
Content-Disposition: form-data; name="file"; filename="1x1_noexif.jpeg"
Content-Type: image/jpeg


------WebKitFormBoundaryrga771iuUYap8BB2
Content-Disposition: form-data; name="myjson"; filename="blob"
Content-Type: application/json

"hello":"world"
------WebKitFormBoundaryrga771iuUYap8BB2
Content-Disposition: form-data; name="isDownscaled"; filename="blob"
Content-Type: application/json

false
------WebKitFormBoundaryrga771iuUYap8BB2--

MultiPartParser 使用上述格式,但会将这些 json 视为文件。因此,我们只需将这些 json 放入 data 即可解组它们。

parsers.py

from rest_framework import parsers

class MultiPartJSONParser(parsers.MultiPartParser):
    def parse(self, stream, *args, **kwargs):
        data = super().parse(stream, *args, **kwargs)

        # Any 'File' found having application/json as type will be moved to data
        mutable_data = data.data.copy()
        unmarshaled_blob_names = []
        json_parser = parsers.JSONParser()
        for name, blob in data.files.items():
            if blob.content_type == 'application/json' and name not in data.data:
                mutable_data[name] = json_parser.parse(blob)
                unmarshaled_blob_names.append(name)
        for name in unmarshaled_blob_names:
            del data.files[name]
        data.data = mutable_data

        return data

settings.py

REST_FRAMEWORK = 
    ..
    'DEFAULT_PARSER_CLASSES': [
        ..
        'myproject.parsers.MultiPartJSONParser',
    ],

现在应该可以了。

最后一点是测试。由于 Django 和 REST 附带的测试 client 不支持多部分 JSON,因此我们通过包装任何 JSON 数据来解决这个问题。

import io
import json

def JsonBlob(obj):
    stringified = json.dumps(obj)
    blob = io.StringIO(stringified)
    blob.content_type = 'application/json'
    return blob

def test_simple(client, png_3x3):
    response = client.post(f'http://localhost/files/', 
            'file': png_3x3,
            'metadata': JsonBlob('lens': 'Sigma 35mm'),
        , format='multipart')
    assert response.status_code == 200

【讨论】:

在 utils/html#parse_html_list 中传递 Json blob 中的数据列表。我将尝试在另一个答案中列出您的解析器,因为我无法在评论中这样做。但首先让我感谢您的回答。非常有帮助!【参考方案5】:

如果您在使用@nithin 的解决方案时遇到Incorrect type. Expected pk value, received list. 的错误,那是因为Django 的QueryDict 妨碍了它——它专门针对use a list for each entry in the dictionary 构建,因此:

 "list": [1, 2] 

当被MultipartJsonParser 解析时产生

 'list': [[1, 2]] 

这会让你的序列化器出错。

这是处理这种情况的替代方法,特别是您的 JSON 需要 _data 键:

from rest_framework import parsers
import json

class MultiPartJSONParser(parsers.MultiPartParser):
    def parse(self, stream, *args, **kwargs):
        data = super().parse(stream, *args, **kwargs)
        json_data_field = data.data.get('_data')
        if json_data_field is not None:
            parsed = json.loads(json_data_field)
            mutable_data = 
            for key, value in parsed.items():
                mutable_data[key] = value
            mutable_files = 
            for key, value in data.files.items():
                if key != '_data':
                    mutable_files[key] = value
            return parsers.DataAndFiles(mutable_data, mutable_files)

        json_data_file = data.files.get('_data')
        if json_data_file:
            parsed = parsers.JSONParser().parse(json_data_file)
            mutable_data = 
            for key, value in parsed.items():
                mutable_data[key] = value
            mutable_files = 
            for key, value in data.files.items():
                mutable_files[key] = value
            return parsers.DataAndFiles(mutable_data, mutable_files)

        return data

【讨论】:

【参考方案6】:

如果可以选择,使用多部分帖子和常规视图非常简单。

您将 json 作为字段发送,将文件作为文件发送,然后在一个视图中处理。

这是一个简单的 python 客户端和一个 Django 服务器:

客户端 - 发送多个文件和任意 json 编码对象:

import json
import requests

payload = 
    "field1": 1,
    "manifest": "special cakes",
    "nested": "arbitrary":1, "object":[1,2,3],
    "hello": "word" 

filenames = ["file1","file2"]
request_files = 
url="example.com/upload"

for filename in filenames:
    request_files[filename] = open(filename, 'rb')

r = requests.post(url, data='json':json.dumps(payload), files=request_files)

服务器 - 使用 json 并保存文件:

@csrf_exempt
def upload(request):
    if request.method == 'POST':
        data = json.loads(request.POST['json']) 
        try:
            manifest = data['manifest']
            #process the json data

        except KeyError:
            HttpResponseServerError("Malformed data!")

        dir = os.path.join(settings.MEDIA_ROOT, "uploads")
        os.makedirs(dir, exist_ok=True)

        for file in request.FILES:
            path = os.path.join(dir,file)
            if not os.path.exists(path):
                save_uploaded_file(path, request.FILES[file])           

    else:
        return HttpResponseNotFound()

    return HttpResponse("Got json data")


def save_uploaded_file(path,f):
    with open(path, 'wb+') as destination:
        for chunk in f.chunks():
            destination.write(chunk)

【讨论】:

【参考方案7】:

我只想通过修改解析器以接受列表来添加到@Pithikos 的答案,这与 DRF 在utils/html#parse_html_list 中的序列化程序中解析列表的方式一致

class MultiPartJSONParser(parsers.MultiPartParser):
    def parse(self, stream, *args, **kwargs):
        data = super().parse(stream, *args, **kwargs)

        # Any 'File' found having application/json as type will be moved to data
        mutable_data = data.data.copy()
        unmarshaled_blob_names = []
        json_parser = parsers.JSONParser()
        for name, blob in data.files.items():
            if blob.content_type == 'application/json' and name not in data.data:
                parsed = json_parser.parse(blob)
                if isinstance(parsed, list):
                    # need to break it out into [0], [1] etc
                    for idx, item in enumerate(parsed):
                        mutable_data[name+f"[str(idx)]"] = item
                else:
                    mutable_data[name] = parsed
                unmarshaled_blob_names.append(name)
        for name in unmarshaled_blob_names:
            del data.files[name]
        data.data = mutable_data

        return data

【讨论】:

【参考方案8】:

以下代码对我有用。

from django.core.files.uploadedfile import SimpleUploadedFile
import requests
from typing import Dict

with open(file_path, 'rb') as f:
    file = SimpleUploadedFile('Your-Name', f.read())

    data: Dict[str,str]
    files: Dict[str,SimpleUploadedFile] = 'model_field_name': file

    requests.put(url, headers=headers, data=data, files=files)
    requests.post(url, headers=headers, data=data, files=files)

'model_field_name' 是 Django 模型中 FileFieldImageField 的名称。您可以像往常一样使用data 参数将其他数据作为namelocation 传递。

希望这会有所帮助。

【讨论】:

以上是关于使用 Django Rest Framework,我如何上传文件并发送 JSON 有效负载?的主要内容,如果未能解决你的问题,请参考以下文章

Django Rest Framework 和 django Rest Framework simplejwt 两因素身份验证

18-Django REST framework-使用Django开发REST 接口

如何在 django-rest-framework 中为 API 使用 TokenAuthentication

Django-rest-framework 和 django-rest-framework-jwt APIViews and validation Authorization headers

django rest framework中文介绍

使用 django-rest-framework-simplejwt 注册后返回令牌