使用Django Rest框架,如何上传文件并发送JSON负载?

使用Django Rest框架,如何上传文件并发送JSON负载?,json,django,rest,django-rest-framework,multipartform-data,Json,Django,Rest,Django Rest Framework,Multipartform Data,我正在尝试编写一个Django Rest Framework API处理程序,它可以接收文件和JSON负载。我已经将MultiPartParser设置为处理程序解析器 然而,我似乎不能两者兼而有之。如果我以多部分请求的形式发送带有该文件的有效负载,那么JSON有效负载在request.data中以一种混乱的方式可用(第一个文本部分直到第一个冒号作为键,其余部分是数据)。我可以用标准形式的参数发送参数,但我的API的其余部分接受JSON有效负载,我希望保持一致。无法读取request.body,因

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

然而,我似乎不能两者兼而有之。如果我以多部分请求的形式发送带有该文件的有效负载,那么JSON有效负载在request.data中以一种混乱的方式可用(第一个文本部分直到第一个冒号作为键,其余部分是数据)。我可以用标准形式的参数发送参数,但我的API的其余部分接受JSON有效负载,我希望保持一致。无法读取request.body,因为它引发了
***RawPostDataException:从请求的数据流读取后无法访问body

例如,请求正文中的文件和此负载:
{“title”:“Document title”,“description”:“Doc description”}

变成:

有办法做到这一点吗?我能吃蛋糕,留着不发胖吗

编辑:
有人建议,这可能是一份。事实并非如此。上传和请求以多部分方式完成,请记住文件和上传是可以的。我甚至可以用标准表单变量完成请求。但是我想看看是否可以在那里获得JSON负载。

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

序列化程序

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)
看法

示例测试:

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)

我知道这是一条旧线,但我刚刚发现了这个。我不得不使用
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文件上传的我的AngularJS代码是:

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

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

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

Models.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')
py->无需特殊更改,此处不显示我的序列化程序,因为它太长,因为实现了可写的多个字段

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)
postman中的请求示例

编辑:


如果要将每个数据作为键值对发送,请参阅扩展答案。如果这是一个选项,则使用多部分post和常规视图非常简单

将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)

下面的代码适用于我

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\u field\u name'
是Django模型中
文件字段
图像字段
的名称。通过使用
数据
参数,您可以像往常一样将其他数据传递为
名称
位置


希望这有帮助。

@Nithin解决方案可以工作,但本质上它意味着您将JSON作为字符串发送,因此在多部分段中不使用实际的
应用程序/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放在
数据中
即可解组

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
设置.py

REST_FRAMEWORK = {
    ..
    'DEFAULT_PARSER_CLASSES': [
        ..
        'myproject.parsers.MultiPartJSONParser',
    ],
}
现在应该可以了

最后一点是测试。由于Django和REST附带的测试
客户机
不支持多部分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


如果您在
错误类型的行中遇到错误。预期pk值,已收到列表。
,对于@nithin的解决方案,这是因为Django的
QueryDict
正在阻碍-它的具体结构是:

{“列表”:[1,2]}
当被
MultipartJsonParser
解析时

{'list':[[1,2]]}
这会使序列化程序出错

下面是一个处理这种情况的替代方案,特别是JSON的
\u data
键:

来自rest\u框架导入解析器
导入json
类MultiPartJSONParser(parsers.MultiPartParser):
def解析(self、stream、*args、**kwargs):
data=super().parse(流,*args,**kwargs)
json_data_field=data.data.get(“数据”)
如果json_data_字段不是None:
parsed=json.load(json_数据_字段)
可变_数据={}
对于键,parsed.items()中的值:
可变_数据[键]=值
可变_文件={}
对于键,data.files.items()中的值:
如果是键!='_数据':
可变_文件[键]=值
返回parsers.DataAndFiles(可变_数据,可变_文件)
json_data_file=data.files.get(“_data”)
如果json_数据_文件:
parsed=parsers.JSONParser().parse(json_数据_文件)
可变_数据={}
对于键,parsed.items()中的值:
可变_数据[键]=值
可变_文件={}
对于键,data.files.items()中的值:
可变_文件[键]=值
返回parsers.DataAndFiles(可变_数据,可变_文件)
返回数据

我只想通过修改解析器以接受列表来补充@Pithikos的答案,这与DRF在
utils/html\parse\u html\u list中解析序列化程序中的列表的方式一致

class-Mu
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