我正在尝试编写一个Django Rest Framework API处理程序,它可以接收文件以及JSON有效负载。我已将MultiPartParser设置为处理程序解析器。
然而,似乎我不能同时做到这两点。如果我将带有文件的有效负载作为多部分请求发送,则在request.data中以错误的方式提供JSON有效负载(第一个文本部分直到第一个冒号为键,其余为数据)。我可以很好地发送标准表单参数中的参数 - 但我的其余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"的副本。它不是。上传和请求是在multipart中完成的,请记住文件并上传它很好。我甚至可以使用标准表单变量来完成请求。但我想看看我是否可以在那里获得JSON有效载荷。
答案 0 :(得分: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)
答案 1 :(得分:3)
我知道这是一个老线程,但我刚刚遇到过这个问题。我不得不使用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'
}
});
答案 2 :(得分:3)
对于需要上传文件并发送一些数据的人来说,没有直接的方法可以让它发挥作用。 json api规范中有一个open issue。我看到的一种可能性是使用multipart/related
如here所示,但我认为很难在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')
serializers.py - &gt;不需要进行任何特殊更改,因为可写的ManyToMany Field实现,所以不要在此处显示我的序列化程序,因为它太长了。
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)
中的请求示例
答案 3 :(得分:2)
如果您在使用@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
答案 4 :(得分:1)
@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视为文件。因此,我们只需将它们放入data
来解组这些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
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
答案 5 :(得分:0)
如果可以的话,使用多部分帖子和常规视图非常简单。
您将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)
答案 6 :(得分:0)
以下代码对我有用。
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
的名称。您可以使用name
参数像往常一样以location
或data
的形式传递其他数据。
希望这会有所帮助。
答案 7 :(得分:0)
我只想通过修改解析器以接受列表来添加@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