使用 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/json
和multipart/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 模型中 FileField
或 ImageField
的名称。您可以像往常一样使用data
参数将其他数据作为name
或location
传递。
希望这会有所帮助。
【讨论】:
以上是关于使用 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