본 작성자는 아직 1년차도 못채운 햇병아리 코더입니다.
(사내에 장고를 다루는 사람이 저 혼자라서) 기술한 내용중에 틀린 내용이 있을 수도 있습니다.
예제랍시고 올려놓은 코드가 아주 허접할 수도 있습니다.
미숙한 제게 조언을 주실 수 있다면 댓글 부탁드립니다. 겸허히 배우겠습니다.
개발 대상 사양은 모두 아래를 공통적으로 사용합니다.
[개발 OS : Windows10]
[설치 OS : CentOS 8 with gnomeUI]
[Python: 3.8]
[Mongodb : 4.4.1]
[PostgreSQL : 12.4]
[Elasticsearch : 7.9.3]
[Tensorflow : 2.4.1]
[scikit-learn : 0.23.2]
<수정이력>
: 2021-04-13 => 코드 위 파일명 어노테이션 누락분 수정.
------------------------------------------------------------------
지난시간에 Custom Field와 Embeded Field로 이미지 업로드와 자동 텍스트 추출을 구현했습니다.
이번에는 이어서 다른 방법으로 같은 내용을 구현해볼까합니다.
마찬가지로 이미지를 업로드 받으면, 이 이미지를 "알아서 잘" 요리해서 추출한 텍스트와 함께 집어 넣어야 합니다.
자바 환경에서 코딩을 하다가 파이썬을 다루면 가끔 문서가 너무 불친절한게 아닌가 싶을때가 있습니다. 특히나 시간에 쫓기고 있을때는 더더욱 그렇죠.
그래서 문서가 부족할때, 꼭 클래스 정의에 가보면 주석으로 문서를 만들어놓는 경우가 있어 확인해보면 큰 도움이 됩니다. (자바라고 안그런건 아니지만 문서보다 더 빨리 알려줄 선배들이 있다.)
class ModelSerializer(Serializer):
"""
A `ModelSerializer` is just a regular `Serializer`, except that:
* A set of default fields are automatically populated.
* A set of default validators are automatically populated.
* Default `.create()` and `.update()` implementations are provided.
The process of automatically determining a set of serializer fields
based on the model fields is reasonably complex, but you almost certainly
don't need to dig into the implementation.
If the `ModelSerializer` class *doesn't* generate the set of fields that
you need you should either declare the extra/differing fields explicitly on
the serializer class, or simply use a `Serializer` class.
"""
serializer_field_mapping = {
models.AutoField: IntegerField,
models.BigIntegerField: IntegerField,
models.BooleanField: BooleanField,
models.CharField: CharField,
models.CommaSeparatedIntegerField: CharField,
models.DateField: DateField,
models.DateTimeField: DateTimeField,
models.DecimalField: DecimalField,
models.DurationField: DurationField,
models.EmailField: EmailField,
models.Field: ModelField,
models.FileField: FileField,
models.FloatField: FloatField,
models.ImageField: ImageField,
models.IntegerField: IntegerField,
models.NullBooleanField: BooleanField,
models.PositiveIntegerField: IntegerField,
models.PositiveSmallIntegerField: IntegerField,
models.SlugField: SlugField,
models.SmallIntegerField: IntegerField,
models.TextField: CharField,
models.TimeField: TimeField,
models.URLField: URLField,
models.UUIDField: UUIDField,
models.GenericIPAddressField: IPAddressField,
models.FilePathField: FilePathField,
}
if hasattr(models, 'JSONField'):
serializer_field_mapping[models.JSONField] = JSONField
if postgres_fields:
serializer_field_mapping[postgres_fields.HStoreField] = HStoreField
serializer_field_mapping[postgres_fields.ArrayField] = ListField
serializer_field_mapping[postgres_fields.JSONField] = JSONField
serializer_related_field = PrimaryKeyRelatedField
serializer_related_to_field = SlugRelatedField
serializer_url_field = HyperlinkedIdentityField
serializer_choice_field = ChoiceField
# The field name for hyperlinked identity fields. Defaults to 'url'.
# You can modify this using the API setting.
#
# Note that if you instead need modify this on a per-serializer basis,
# you'll also need to ensure you update the `create` method on any generic
# views, to correctly handle the 'Location' response header for
# "HTTP 201 Created" responses.
url_field_name = None
# Default `create` and `update` behavior...
def create(self, validated_data):
"""
We have a bit of extra checking around this in order to provide
descriptive messages when something goes wrong, but this method is
essentially just:
return ExampleModel.objects.create(**validated_data)
If there are many to many fields present on the instance then they
cannot be set until the model is instantiated, in which case the
implementation is like so:
example_relationship = validated_data.pop('example_relationship')
instance = ExampleModel.objects.create(**validated_data)
instance.example_relationship = example_relationship
return instance
The default implementation also does not handle nested relationships.
If you want to support writable nested relationships you'll need
to write an explicit `.create()` method.
"""
raise_errors_on_nested_writes('create', self, validated_data)
ModelClass = self.Meta.model
# Remove many-to-many relationships from validated_data.
# They are not valid arguments to the default `.create()` method,
# as they require that the instance has already been saved.
info = model_meta.get_field_info(ModelClass)
many_to_many = {}
for field_name, relation_info in info.relations.items():
if relation_info.to_many and (field_name in validated_data):
many_to_many[field_name] = validated_data.pop(field_name)
try:
instance = ModelClass._default_manager.create(**validated_data)
except TypeError:
tb = traceback.format_exc()
msg = (
'Got a `TypeError` when calling `%s.%s.create()`. '
'This may be because you have a writable field on the '
'serializer class that is not a valid argument to '
'`%s.%s.create()`. You may need to make the field '
'read-only, or override the %s.create() method to handle '
'this correctly.\nOriginal exception was:\n %s' %
(
ModelClass.__name__,
ModelClass._default_manager.name,
ModelClass.__name__,
ModelClass._default_manager.name,
self.__class__.__name__,
tb
)
)
raise TypeError(msg)
# Save many-to-many relationships after the instance is created.
if many_to_many:
for field_name, value in many_to_many.items():
field = getattr(instance, field_name)
field.set(value)
return instance
def update(self, instance, validated_data):
raise_errors_on_nested_writes('update', self, validated_data)
info = model_meta.get_field_info(instance)
# Simply set each attribute on the instance, and then save it.
# Note that unlike `.create()` we don't need to treat many-to-many
# relationships as being a special case. During updates we already
# have an instance pk for the relationships to be associated with.
m2m_fields = []
for attr, value in validated_data.items():
if attr in info.relations and info.relations[attr].to_many:
m2m_fields.append((attr, value))
else:
setattr(instance, attr, value)
instance.save()
# Note that many-to-many fields are set after updating instance.
# Setting m2m fields triggers signals which could potentially change
# updated instance and we do not want it to collide with .update()
for attr, value in m2m_fields:
field = getattr(instance, attr)
field.set(value)
return instance
# Determine the fields to apply...
어셈블러까지 들어갈게 아니라면 딱 여기서 멈추고, 동작을 핵심을 살펴보겠습니다.
매개변수 이름이 validated_data 라는 점에서 어느 정도 내부 처리 순서가 보이는 군요.
class CreateModelMixin:
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
def get_success_headers(self, data):
try:
return {'Location': str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}
아니나 다를까, ModelViewSet이 상속받는 CreateModelMixin에서 그 단편이 보입니다.
View에서 Serializer의 create를 호출하는 구조로 보이네요. 그리고 그 전에 validation을 진행하는군요
serializer.save()는 수많은 예외처리와 함께 serializer의 update와 create를 분기해주는 역할을 합니다.
만약 serializer를 커스텀해서 사용하려면 간단하게 이 2개의 메서드를 오버라이딩 해주면 되겠군요.
데이터 모델은 중간과정을 생략하고 최종본만 간략하게 만들면 될 것 같습니다.
<ocrapp/models.py>
class UserImages(models.Model):
_id = models.ObjectIdField()
image_title = models.TextField(null=False, default="a.jpg")
image = models.ImageField(null=False, upload_to="uploaded_pictures")
image_context = models.TextField(null=False, default="일방통행")
class Meta:
db_table = "user_images"
이번의 context 에는 간단하게 OCR로 추출한 텍스트 정보를 뭉뚱그려 집어넣겠습니다.
OCR 추출 코드도 다시 바뀌여야 겠지요.
<ocrapp/ocr_method/py_tesseract_ocr.py>
def tesseract_ocr_extract_from_file_only_text(image_file):
image = cv2.imdecode(np.frombuffer(image_file.read(), np.uint8), cv2.COLOR_BGR2RGB)
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = pytesseract.image_to_data(rgb, output_type=pytesseract.Output.DICT)
return " ".join([text for text in results["text"] if text])
기타 모든 정보를 무시하고 텍스트만 하나의 문자열로 합치기로 합니다.
자 이제 시리얼 라이저를 커스텀 할 차례입니다.
validated_data 는 단순한 dict형의 객체입니다. 여기에는 모델에 선언된 필드의 이름을 키값으로 가지는 요소가 포함되어야 합니다.
이때의 validated_data에 원하는 값을 넣어주면 Request에는 없는 필드도 만들어서 넣어줄 수 있게 됩니다.
<ocrapp/serializer.py>
class UserImageSerializer(serializers.ModelSerializer):
class Meta:
model = UserImages
fields = "__all__"
def create(self, validated_data):
validated_data["image_context"] = tesseract_ocr_extract_from_file_only_text(validated_data["image"])
validated_data["image_title"] = validated_data["image"].name
return super(UserImageSerializer, self).create(validated_data)
def update(self, instance, validated_data):
validated_data["image_context"] = tesseract_ocr_extract_from_file_only_text(validated_data["image"])
validated_data["image_title"] = validated_data["image"].name
return super(UserImageSerializer, self).update(instance, validated_data)
그런데 문제가 하나 있습니다. 큰 문제는 아니고, ImageField의 특징 때문에 발생하는 문제인데, 이 이미지 필드는
url을 돌려줍니다. 받을때는 이미지 파일을 받습니다. 직관적이지 않아서 네이밍이 조금 그렇네요.
요청시 image 파라미터에는 파일이 들어가고,
응답시 image 파라미터에는 url이 돌아오게 됩니다.
전혀 직관적이지 않아서 좀 문제가 있네요.
다음시간에는 view의 create 메서드와 validation 을 수정해서 더 개선을 해보도록 하겠습니다.
'Django and Mongo' 카테고리의 다른 글
Django REST API & Djongo - 4. CustomField & Embedded Model (0) | 2021.03.22 |
---|---|
Django REST API & Djongo - 3. Image Upload & Download (0) | 2021.03.15 |
Django REST API & Djongo - 2. ObjectID (0) | 2021.03.06 |
Django REST API & Djongo - 1. Introduce (0) | 2021.03.06 |