Трансляция charset в объектах request и response в Django
В одном из Django-приложений, которое я разрабатываю, возникла ситуация, когда очень хочется, чтобы сайт общался с клиентами в кодировке UTF-8.
При этом стандартной кодировкой системы (шаблоны, данные в базе данных), в которой работает это приложение, является Windows-1251. Конвертировать данные в базе и т.п. - not an option, т.к. с этими данными работает еще куча уже написанного софта, который так привык. Соответственно, во избежание глюков с кодировкой, желательно, чтобы DEFAULT_CHARSET был Windows-1251, и все внутренние операции со строками и базой были в этой кодировке.
Что делаем? Пишем middleware.
# module proj.middleware.translationmiddleware from django.conf import settings from django.http import * from urllib import quote, unquote import re CHARSET_RE = re.compile(r'(.+; charset=)(.+)', re.IGNORECASE) SKIP_CONTENT_FOR = (HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseNotModified, HttpResponseNotAllowed) class TranslationMiddleware(object): def __init__(self): self.trCset = getattr(settings, 'TRANSLATE_CHARSET', 'UTF-8') self.defCset = settings.DEFAULT_CHARSET def tr(self, data, _from, _to): if _to == _from: return data return data.decode(_from).encode(_to) def process_request(self, request): GET = request.GET.copy() POST = request.POST.copy() for dic in (GET, POST): for key in dic: newlist = [] for val in dic.getlist(key): newlist.append(self.tr(val, self.trCset, self.defCset)) dic.setlist(key, newlist) dic._mutable = False request.GET, request.POST = GET, POST if hasattr(request, '_request'): del(request._request) for key in request.COOKIES: request.COOKIES[key] = self.tr(request.COOKIES[key], self.trCset, self.defCset) def process_response(self, request, response): klass = response.__class__ if klass not in SKIP_CONTENT_FOR: new_response = klass() new_response.content = self.tr(response.content, self.defCset, self.trCset) else: new_response = response new_response._charset = self.trCset for key in response.headers: if key == 'Content-Type': ctype = CHARSET_RE.sub(r'\1%s' % self.trCset, response.headers[key]) new_response[key] = ctype elif key == 'Location': value = unquote(response.headers[key]) value = self.tr(value, self.defCset, self.trCset) new_response[key] = quote(value, RESERVED_CHARS) else: new_response[key] = self.tr(response.headers[key], self.defCset, self.trCset) for key in response.cookies: value = self.tr(response.cookies[key].value, self.defCset, self.trCset) kwargs = {} for var in ('max_age', 'path', 'domain', 'secure', 'expires'): kwargs[var] = response.cookies[key].get(var.replace('_', '-'), None) new_response.set_cookie(key, value, **kwargs) return new_response
В данном случае мне нужно было транслировать из и в UTF-8, но это можно переопределить в settings.py проекта, установив переменную TRANSLATE_CHARSET. Значение кодировки для "внутреннего пользования" и для трансляции из и во внешний мир устанавливаются при инициализации объекта middleware.
Middleware определяет методы process_request и process_response, для транслирования кодировки в данных пришедшего запроса и для перекодировки ответа клиенту, соответственно.
К моменту запуска process_request мы уже имеем готовый объект request с заполненными объектами request.GET и request.POST. Эти объекты - неизменяемые экземпляры класса QueryDict, так что сначала делаются их копии, которые можно изменять. После трансляции данных в нужную внутреннюю кодировку, новым объектам тоже ставится флаг неизменяемости, и они присваиваются вместо старых request.GET и request.POST. request.FILES, конечно, не трогаем, как и request.raw_post_data — эти данные остаются в оригинальной кодировке, так что если будет нужно что-то получать из них, с кодировкой нужно будет разбираться отдельно. Дополнительно, удаляем атрибут request._request, если он присутствует (это если уже был доступ к атрибуту request.REQUEST), чтобы не осталось ссылок на старые GET и POST.
Также транслируем cookies. request.COOKIES — обычный dict, так что его не копируем, а изменяем по месту.
process_response осуществляет обратную операцию - перекодировку ответа сервера. Создается новый объект new_response того же класса, что и оригинальный response, перекодируем его содержимое, если класс response это поддерживает (у объекта есть содержимое, а не только заголовки); если этот объект не должен содержать контента, то новый объект не создается, работаем со старым.
Также перекодируем cookies и HTTP-заголовки. Как правило, заголовки не содержат ничего кроме ASCII, но это может быть и редирект - заголовок Location, который вполне может содержать не-ASCII-данные, закодированные URL-кодировкой. В этом случае нам надо раскодировать его обратно перед трансляцией в другую кодировку, а после перекодировки - закодировать обратно функцией urllib.quote.
process_response возвращает объект new_response, из которого потом и строится ответ сервера клиенту.
В файле settings.py нашего проекта в начало списка MIDDLEWARE_CLASSES вставляем путь к нашему middleware - "proj.middleware.translationmiddleware.TranslationMiddleware" (мы ставим его первым, для того, чтобы его process_request отработал прежде других middleware и обработки запроса, а process_response — в конце обработки, после всех других middleware, непосредственно перед выдачей ответа), рестарт приложения, voila! Страницы отдаются в нужной "внешней" кодировке, а данные запросов перекодируются во "внутреннюю" на лету.
Данный код опробован в Django, работающего по FastCGI. Я не пробовал его под mod_python, но, полагаю, разницы быть не должно.
Источники:
https://www.ragbag.ru/2006/11/21/django_translation_middleware