一.項目需求
批量上傳圖片,然后批量導入(使用excel)每個圖片對應的屬性(屬性共十個,即對應十個字段,其中外鍵三個)。
二.問題
一次可能上傳成百上千張圖片和對應字段,原來數據庫的設計我將圖片和對應屬性放在一張表中,圖片不可能和對應字段一起批量導入,如果先導入圖片,其他字段必須允許為空,且在導入對應屬性時,會遍歷剛上傳已經存在數據庫中的圖片,然后更新數據庫,增加對應屬性,這樣會給服務器造成很大的壓力,且很有可能出現錯誤(如圖片對應不上,因此很多字段為空或只有圖片,會使很多錯誤很難捕捉。)
三.實踐中的解決方法
1.分成兩張表:
我首先想到將圖片和對應字段分開成兩張表,先上傳圖片,然后在導入對應屬性,然而仔細一想,問題似乎解決得不完善,導入使如何對應圖片id,還是直接對應圖片名,還有是否有可能圖片已經保存到數據庫,但是excel中沒有該圖片的信息,這也會浪費很多的空間,因此此方法還有待提高。
2.使用緩存:
然后我最后想到了緩存,也決定使用該方法批量上傳與導入,思路大概是:上傳圖片先暫時存入緩存(我這里時圖片名為鍵,圖片臨時文件對象為值),設置一定的時效,然后在上傳excel判斷excel的格式及列標題等,這些都對應時,然后將外鍵數據從數據庫取出,一行一行判斷excel中的數據的外鍵是否滿足,以及圖片是否在緩存中,如果條件都滿足,然后這一行數據構成數據庫中的一個Queryset對象存入列表,這樣就將數據驗證完畢,最后驗證完所有的數據后,使用bulk_create()方法批量寫入,或者可以使用get_or_create()方法批量導入(可以去重,但更耗時)。
2.1圖片和excel文件上傳序列化如下:
1 class RockImageSerializer(serializers.Serializer): 2 imgs = serializers.ListField(child=serializers.FileField(max_length=100, 3 ), label="地質薄片圖片", 4 help_text="地質薄片圖片列表", write_only=True) 5 6 def create(self, validated_data): 7 try: 8 imgs = validated_data.get('imgs') 9 notimg_file = [] 10 for img in imgs: 11 img_name = str(img) 12 if not img_name.endswith(('.jpg', '.png', '.bmp', '.JPG', '.PNG', '.BMP')): 13 notimg_file.append(img_name) 14 else: 15 # 將圖片加入緩存 16 cache.set(img_name, img, 60 * 60 * 24) 17 if notimg_file: 18 return {'code': -2, 'msg': '部分未上傳成功,請檢查是否為圖片,失敗文件部分如下:{0}'.format(','.join(notimg_file[:10]))} 19 return {'code': 1} 20 except Exception as e: 21 return {'code': -1} 22 23 def validate_imgs(self, imgs): 24 if imgs: 25 return imgs 26 else: 27 raise serializers.ValidationError('缺失必要的字段或為空') 28 29 30 class SourceSerializer(serializers.Serializer): 31 """ 32 批量上傳序列化(excel) 33 """ 34 source = serializers.FileField(required=True, allow_empty_file=False, 35 error_messages={'empty': '未選擇文件', 'required': '未選擇文件'}, help_text="excel文件批量導入", 36 label="excel文件")
2.2view視圖如下:
1 class ImageViewset(viewsets.GenericViewSet, mixins.CreateModelMixin, mixins.ListModelMixin): 2 parser_classes = (MultiPartParser, FileUploadParser,) 3 authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication) 4 serializer_class = RockImageSerializer 5 queryset = RockImage.objects.all() 6 7 def create(self, request, *args, **kwargs): 8 serializer = self.get_serializer(data=request.data) 9 success_status = serializer.is_valid() 10 if not success_status: 11 errors = serializer.errors 12 first_error = sorted(errors.items())[0] 13 return Response({'code': -1, 'msg': first_error[1]}, 14 status=status.HTTP_400_BAD_REQUEST) 15 serializer_code = self.perform_create(serializer) 16 if serializer_code['code'] == 1: 17 headers = self.get_success_headers(serializer.data) 18 return Response({'code': 1, 'msg': '添加成功'}, status=status.HTTP_201_CREATED, headers=headers) 19 elif serializer_code['code'] == -2: 20 return Response({'code': -2, 'msg': serializer_code['msg']}, status=status.HTTP_400_BAD_REQUEST) 21 else: 22 return Response({'code': -2, 'msg': '圖片上傳過程中發生意外,請稍后重試'}, status=status.HTTP_400_BAD_REQUEST) 23 24 def perform_create(self, serializer): 25 return serializer.save() 26 27 28 class NewRockDetailViewset(viewsets.GenericViewSet, mixins.CreateModelMixin): 29 """ 30 批量上傳字段接口 31 """ 32 authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication) 33 serializer_class = SourceSerializer 34 35 # permission_classes =[SuperPermission] 36 37 def create(self, request, *args, **kwargs): 38 serializer = self.get_serializer(data=request.data) 39 success_status = serializer.is_valid() 40 if not success_status: 41 errors = serializer.errors 42 first_error = sorted(errors.items())[0] 43 return Response({'code': -1, 'msg': first_error[1]}, 44 status=status.HTTP_400_BAD_REQUEST) 45 files = request.FILES.get('source') 46 if files.content_type == 'application/vnd.ms-excel' or files.content_type == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 47 content = [] 48 # 讀取excel文件每次讀取25M 49 for chunk in files.chunks(): 50 content.append(chunk) 51 try: 52 ExcelFile = xlrd.open_workbook(filename=None, file_contents=b''.join(content), 53 encoding_override='gbk') 54 sheet = ExcelFile.sheet_by_index(0) 55 total_rows = sheet.nrows 56 head = sheet.row_values(0) 57 if total_rows <= 1: 58 return Response({'code': -3, 'msg': '數據為空或無列標題'}) 59 if head[0] == "圖片" and head[1] == "地區" and head[2] == "井號" and head[ 60 3] == "年代地層" and head[ 61 4] == "岩石地層" and head[5] == "偏光類型" and head[6] == "岩性" and head[ 62 7] == "深度" and head[8] == "組分特征" and head[9] == "古生物特征" and head[ 63 10] == "岩性特征" and head[11] == "孔縫特征": 64 address = Address.objects.filter(region_type=4).values_list('id', 'region') 65 polarizedtype = PolarizedType.objects.all().values_list('id', 'pol_type') 66 lithological = Lithological.objects.all().values_list('id', 'lit_des') 67 add_count = address.count() 68 pol_count = polarizedtype.count() 69 lit_count = lithological.count() 70 all_counts = [add_count, pol_count, lit_count] 71 add_ids = [] 72 add_datas = [] 73 pol_ids = [] 74 pol_datas = [] 75 lit_ids = [] 76 lit_datas = [] 77 max_num = max(all_counts) 78 for row in range(max_num): 79 if row < add_count: 80 add_ids.append(address[row][0]) 81 add_datas.append(address[row][1]) 82 if row < pol_count: 83 pol_ids.append(polarizedtype[row][0]) 84 pol_datas.append(polarizedtype[row][1]) 85 if row < lit_count: 86 lit_ids.append(lithological[row][0]) 87 lit_datas.append(lithological[row][1]) 88 err_data = [] 89 r_data = [] 90 r_sum = 0 91 for exc_row in range(1, total_rows): 92 row_value = sheet.row_values(exc_row) 93 img = cache.get(row_value[0], None) 94 add_value = row_value[4] 95 pol_value = row_value[5] 96 lit_value = row_value[6] 97 if img and add_value in add_datas and pol_value in pol_datas and lit_value in lit_datas and \ 98 row_value[7]: 99 r_sum += 1 100 r_data.append(Rock(image=img, area_detail_id=int(add_ids[add_datas.index(add_value)]), 101 pol_type_id=int(pol_ids[pol_datas.index(pol_value)]), 102 lit_des_id=int(lit_ids[lit_datas.index(lit_value)]), depth=row_value[7], 103 lit_com=row_value[8], pal_fea=row_value[9], lit_fea=row_value[10], 104 por_fea=row_value[11])) 105 else: 106 err_data.append('第' + str(exc_row) + '行') 107 if r_sum: 108 Rock.objects.bulk_create(r_data) 109 if err_data: 110 return Response({'code': 1, 'msg': '共{0}條數據上傳成功'.format(str(r_sum)), 111 'err_data': '共{0}條數據上傳失敗,部分錯誤數據如下:{1},請查看格式或圖片是否不存在'.format( 112 str(len(err_data)), ','.join(err_data[:10]))}, 113 status=status.HTTP_201_CREATED) 114 else: 115 return Response({'code': 0, 'msg': '共{0}條數據上傳成功'.format(str(r_sum)), 'err_data': '共0條數據失敗'}, 116 status=status.HTTP_201_CREATED) 117 else: 118 return Response({'code': -3, 'msg': '共0條數據上傳成功,請檢查數據格式或圖片未上傳', 119 'err_data': '共{0}條數據上傳失敗'.format(str(len(err_data)))}, 120 status=status.HTTP_400_BAD_REQUEST) 121 else: 122 return Response({'code': -2, 'msg': 'excel列標題格式錯誤'}) 123 except Exception as e: 124 print(e) 125 return Response({'code': -1, 'msg': '無法打開文件'}, status=status.HTTP_400_BAD_REQUEST) 126 127 else: 128 return Response({'code': -1, 'msg': '文件格式不正確'}, 129 status=status.HTTP_400_BAD_REQUEST)
這樣便較好的解決了批量上傳圖片和對應字段的問題,注意:驗證一定要較為全面,還有文件讀寫一定要分片讀(可以利用chunks()方法,可規定大小),防止文件過大,占用大量內存。
