原文地址:
https://docs.djangoproject.com/en/2.0/topics/http/
https://docs.djangoproject.com/en/2.0/ref/request-response/
文章目录
1. 关于HTTP请求和响应
原文地址:https://docs.djangoproject.com/en/2.0/ref/request-response/
当请求一个页面时,Django会创建出一个HttpRequest对象,该对象包含该请求的元数据。然后Django加载合适的(处理该请求的)view(视图处理函数),并将该对象作为视图函数的第一个参数传给它。视图处理函数会返回一个HttpResponse对象。
1.1 HttpRequest对象
1.1.1 属性
该对象包含的属性包括:
(如下这些数据应该是只读的)
- HttpRequest.scheme:字符串,表示请求的模式(http或是https)
- HttpRequest.body:byte string,表示原始的http request body;
- HttpRequest.path:字符串,表示完整的请求路径,不包括http scheme或域,例如:
/music/bands/the_beatles/
- HttpRequest.path_info:
- HttpRequest.method:字符串,表示请求的方法,例如“GET”,“POST”
- HttpRequest.encoding:
- HttpRequest.content_type:
- HttpRequest.content_params:
- HttpRequest.GET:一个类字典的对象,包含类所有的HTTP GET参数。
- HttpRequest.POST:一个类字典对象,包含类所有的HTTP POST参数,包括form表单数据。如果你想获取原始的、非form表单数据,可以通过HttpRequest.body数据获取。对于一个POST方法,可能HttpRequest.POST为空(由于没有包含任何表单数据),因此,如果你想判定请求的方法,不要使用HttpRequest.POST来检查,而是应该使用
request.method == "POST"
这种形式判定。 - HttpRequest.COOKIES:字典,包含所有的key,value(都是字符串)
- HttpRequest.FILES:一个类字典对象,包含类所有的上传文件。FILES中的key是
<input type="file" name="" />.
中的name。value为一个UploadedFile对象。详情见:https://docs.djangoproject.com/en/2.0/topics/files/ 。只有当请求的方法为POST且 提交的表单 form 包含enctype="multipart/form-data"
属性时,FILES才会包含值,否则为一个空的类字典对象。 - HttpRequest.META:
- HttpRequest.resolver_match:
被应用代码设置的属性:(Django不会设置这些属性,但是如果你的应用设置了这些属性,则会使用这些值)
- HttpRequest.current_app:
- HttpRequest.urlconf:
被中间层设置的属性:
- HttpRequest.session:被SessionMiddleware设置的;
- HttpRequest.site:被CurrentSiteMiddleware设置的;
- HttpRequest.user:被AuthenticaionMiddleware设置的;
1.1.2 方法
方法包括:
- HttpRequest.get_host():
- HttpRequest.get_port():
- HttpRequest.get_full_path():
- HttpRequest.build_absolute_uri:
- HttpRequest.get_signed_cookie:
- HttpRequest.is_secure():
- HttpRequest.is_ajax():
- HttpRequest.read(size=None)/HttpRequest.readline()/HttpRequest.readlines()
HttpRequest.__iter__()
:
1.2 QueryDict对象
在HttpRequest对象中,GET,POST属性都是django.http.QueryDict
类型对象。该对象可以处理一个key 绑定多个值的情况(由于html 中存在select multiple这种多个value的情况)。在request.POST,request.GET中的django.http.QueryDict
都是不可变的,如果要获取一个可变的版本,可通过QueryDict.copy()
获取。https://docs.djangoproject.com/en/2.0/ref/request-response/#django.http.QueryDict.copy
QueryDict实现了所有的标准的字典的方法(它就是一个dictionary的子类)。但是有些的行为可能会存在一点差异(和标准的字典相比)。例如:
QueryDict.__init__(query_string=None, mutable=False, encoding=None)
:注意request.POST,request.GET中的对象是不可变的,如果你想构造可变的版本,则可传入:mutable=True。
使用示例:QueryDict('a=1&a=2&c=3')
QueryDict.__getitem__(key)
:返回给定key的value,如果该key对应多个value,则返回最后一个。如果不存在key ,则抛出异常。QueryDict.__setitem__(key, value)
:QueryDict.__contains__(key)
:QueryDict.get(key, default=None)
:QueryDict.update(other_dict)
:将新的值附加到当前列表中(而不是进行替换)。例如:
>>> q = QueryDict('a=1', mutable=True)
>>> q.update({'a': '2'})
>>> q.getlist('a')
['1', '2']
>>> q['a'] # returns the last
'2'
- QueryDict.items():
- QueryDict.values():
- QueryDict.copy():
- QueryDict.getlist(key, default=None):
- QueryDict.setlist(key, list_):
- QueryDict.appendlist(key, item):
- 还有其他的,懒得粘了
1.3 HttpResponse 对象
HttpResponse是完全由你构造,填充了。
典型的用法是通过一个string来构造httpresponse,例如:
>>> from django.http import HttpResponse
>>> response = HttpResponse("Here's the text of the Web page.")
>>> response = HttpResponse("Text only, please.", content_type="text/plain")
另外,httpresponse是一个 file-like对象,你可以通过文件读写方法填充内容。
>>> response = HttpResponse()
>>> response.write("<p>Here's the text of the Web page.</p>")
>>> response.write("<p>Here's another paragraph.</p>")
另外,也可以通过iterator来填充内容。HttpResponse可以消费iterator,然后就内容保存为一个string。如果你想将你的response 通过streaming的方式传递给client,可以使用 StreamingHttpResponse。
1.3.1 设置响应头部
可想操作字典一样,设置头部,例如:
>>> response = HttpResponse()
>>> response['Age'] = 120
>>> del response['Age']
1.3.2 告知浏览器,响应是一个文件附件
to tell the browser to treat the response as a file attachment,通过content_type参数、Content-Disposition头部。例如:
>>> response = HttpResponse(my_data, content_type='application/vnd.ms-excel')
>>> response['Content-Disposition'] = 'attachment; filename="foo.xls"'
1.3.3 HttpResponse属性
包括:
- HttpResponse.content:
- HttpResponse.charset:
- HttpResponse.status_code:
- HttpResponse.reason_phrase:
- HttpResponse.streaming:
- HttpResponse.closed:
1.3.4 HttpResponse方法
HttpResponse.__init__(content='', content_type=None, status=200, reason=None, charset=None)
:HttpResponse.__setitem__(header, value)
:HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False)
:HttpResponse.write(content)
:- 其他方法,不粘贴了
1.3.5 HttpResponse的子类
包括:
- HttpResponseRedirect:
- HttpResponsePermanentRedirect:
- HttpResponseNotModified:
- HttpResponseBadRequest:
- HttpResponseNotFound:
- HttpResponseForbidden:
- 其他,不粘贴了
1.4 JsonResponse 对象
HttpResponse的子类,继承了父类的大多数行为,但是,默认情况下,Content-Type 头部为application/json,内容是json-化的对象。
1.5 StreamingHttpResponse 对象与FileResponse 对象
StreamingHttpResponse是HttpResponse的子类。
2.django 对请求的路由处理
当用户请求一个基于Django的页面时,Django通过如下方式找到处理该请求的方法/类(或是没有找到,抛出异常):
- Django先确定将要使用的URLConf模块(通常来说,就是setting.py中指定的ROOT_URLCONF(该值一般指定来要使用的URL映射文件))。但是新来的请求HttpRequest对象自己携带来urlconf属性的话,就会使用该值。
- 根据ROOT_URLCONF,加载对应的python模块,在模块中寻找urlpatterns变量,该变量的值是一个
django.urls.path()
或/和django.urls.re_path()
变量的列表; - Django按顺序检查urlpatterns变量中的每项值,一旦找到一个匹配的URL,就停止,然后Django导入、调用对应的方法。调用时,传入:HttpRequest实例、URL匹配时匹配到的位置参数、其他调用传入的附加参数;
示例:
from django.urls import path
from . import views
urlpatterns = [
path('articles/2003/', views.special_case_2003),
path('articles/<int:year>/', views.year_archive),
path('articles/<int:year>/<int:month>/', views.month_archive),
path('articles/<int:year>/<int:month>/<slug:slug>/', views.article_detail),
]
对于请求 /articles/2005/03/:匹配第三条, Django会调用views.month_archive(request, year=2005, month=3)
/articles/2003/ :匹配第一条,Django调用views.special_case_2003(request)
/articles/2003 :没有匹配;
/articles/2003/03/building-a-django-site/ :匹配最后一条,调用:views.article_detail(request, year=2003, month=3, slug="building-a-django-site").
在写URL匹配规则时,可以使用路径转化器,默认的转化器包括:
- str :匹配任意非空自负串,包括/。
- int :匹配0或是其他任意的正数,返回一个int值。
- slug :Matches any slug string consisting of ASCII letters or numbers, plus the hyphen and underscore characters. For example, building-your-1st-django-site.
- uuid : 匹配格式化的uuid。为了避免多个URL匹配到同一个页面,必须包括短横杠(dash),且字符必须是小写的。例如:075194d3-6885-417e-a8a8-6c931e272f00. Returns a UUID instance.
- path :匹配任意非空串,包括/。
自定义路径转化器,见:https://docs.djangoproject.com/en/2.0/topics/http/urls/
3. 请求函数的编写
一个处理请求的函数(view function),就是一个简单的python函数,接受一个web请求,然后返回一个web响应。响应可以是html内容,或是一个重定向,或是404错误,或是xml文档或是一个图片,或是其他任意的东西。函数代码也可以放在任意的地方,只要是在python path下。但是一般建议(约定上)放在views.py中。
示例:
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
html = "<html><body>It is now %s.</body></html>" % now
return HttpResponse(html)
4. 视图装饰器
Django提供了多个装饰器,用于支持不同的Http特征。
4.1 对于http 请求方法的限制过滤
位于django.views.decorators.http
中的装饰器可以提供基于http方法的限制,限制对view 函数的调用。当条件不满足时,这些装饰器会返回django.http.HttpResponseNotAllowed
错误。装饰器包括:
require_http_methods(request_method_list)
:接受特定的方法请求。例如:
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET", "POST"])
def my_view(request):
# I can assume now that only GET or POST requests make it this far
# ...
pass
- require_GET():
- require_POST()
- require_safe():只允许get,head方法。
4.2 Conditional view processing
位于django.views.decorators.http
中的装饰器可以控制对于特定视图的处理caching行为。例如:
- condition(etag_func=None, last_modified_func=None):
- etag(etag_func):
- last_modified(last_modified_func):
这些装饰器可以用于产生ETag,Last-Modified 头部,详细请求见:https://docs.djangoproject.com/en/2.0/topics/conditional-view-processing/
4.3 GZip 压缩
位于Django.views.decorators.gzip
中的装饰器可以控制内容压缩行为。装饰器包括:
- gzip_page():如果浏览器允许gzip压缩的话,该装饰器会压缩内容。
4.4 Vary headers
在django.views.decorators.vary
包中的装饰器可以用来控制caching行为(基于特定的头部)。装饰器包括:
- vary_on_cookie(func):
- vary_on_headers:Vary头部定义了:在构造cache key时,那个request header 应该考虑。
详细见:https://docs.djangoproject.com/en/2.0/topics/cache/#using-vary-headers
4.5 Caching
位于django.views.decorators.cache
中的装饰器控制了服务端、客户端的cache,装饰器包括:
cache_control(**kwargs)
:该装饰器控制http 响应中的Cache-Control
头部。never_cache(view_func)
:该装饰器控制http 响应中增加Cache-Control: max-age=0, no-cache, no-store, must-revalidate
头部,表示不进行caching。
5. 上传文件
当Django处理文件上传时,文件数据存放在request.FILES
变量中。本章主要描述文件如何存储到磁盘或内存中,并如何自定义行为。
对于上传文件的处理:
5.1 示例一:读取
from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import UploadFileForm
# Imaginary function to handle an uploaded file.
from somewhere import handle_uploaded_file
def upload_file(request):
if request.method == 'POST':
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
handle_uploaded_file(request.FILES['file'])
return HttpResponseRedirect('/success/url/')
else:
form = UploadFileForm()
return render(request, 'upload.html', {'form': form})
def handle_uploaded_file(f):
with open('some/file/name.txt', 'wb+') as destination:
for chunk in f.chunks():
destination.write(chunk)
5.2 示例二:保存到model 中
保存到model中,可以使用FileField字段类型。当调用form.save函数时,文件数据将会保存到upload_to参数指定的位置
from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import UploadFileForm
from .models import ModelWithFileField
def upload_file(request):
if request.method == 'POST':
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
instance = ModelWithFileField(file_field=request.FILES['file'])
instance.save()
return HttpResponseRedirect('/success/url/')
else:
form = UploadFileForm()
return render(request, 'upload.html', {'form': form})
5.3 upload handler
当用户上传一个文件时,Django将文件数据传递给upload handler。 upload handler在setting 中的FILE_UPLOAD_HANDLERS变量指定,其初始配置是:
["django.core.files.uploadhandler.MemoryFileUploadHandler",
"django.core.files.uploadhandler.TemporaryFileUploadHandler"]
Django对于文件的默认处理是:小文件放到内存中,大文件放到磁盘中。
你可以自定义自己的文件处理器,例如增加用户级配置管理,压缩或是将文件存储到其他地方(而非本地)。详见:https://docs.djangoproject.com/en/2.0/ref/files/uploads/#custom-upload-handlers
5.4 upload handler上传数据存储位置
在你对上传的文件做处理前,Django需要将文件存储在某个地方,方便你后续业务的处理。
默认情况下,当文件小于2.5M时,Django会将文件存储在内存中。
如果文件过大,Django会将文件存放到一个临时文件中(位于系统的临时目录中)。在类unix系统中,零时文件类似于:/tmp/tmpzfp6I6.upload。
https://docs.djangoproject.com/en/2.0/ref/settings/#file-upload-settings
6. Django 快捷函数
django.shortcuts 包中提供了一些便捷功能,来增加MVC各层之间的交互功能。例如:
render(request, template_name, context=None, content_type=None, status=None, using=None)
:html 文件渲染,将参数填入模版到中。- redirect(to, permanent=False, *args, **kwargs):重定向;
- get_object_or_404():在model manager上调用get 方法,如果模型不存在,则返回http404;
- get_list_or_404():通过model manager获取过滤后的结果,如果结果为空,则返回http 404;
7. 通用视图
https://docs.djangoproject.com/en/2.0/topics/http/generic-views/
8. 中间层(middleware)
https://docs.djangoproject.com/en/2.0/topics/http/middleware/
Django 的中间层是一种框架,hook into 用户Django 请求/响应处理过程种。它的特点是轻量级的、低层次的‘插件’系统,会全局改变Django的输入输出。
每个中间层组件都完成某种特定的功能,例如AuthenticationMiddleware中间件通过session,将用户与请求绑定。
本章主要描述中间件如何工作且怎么激活中间件、怎么编写自己的中间件。当然,Django也提供来一些内置的中间见,详见:https://docs.djangoproject.com/en/2.0/ref/middleware/
8.1 编写自己的中间件
中间件工厂是一个可调用对象,接受一个get_response 可调用对象,返回一个中间件。
中间件是一个可调用对象,然后返回一个response(和view一样)
示例:
def simple_middleware(get_response):
# One-time configuration and initialization.
def middleware(request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
return middleware
或是,也可以是一个类实例是可调用的类,例如:
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
Django提供的get_response可调用对象可以是真正的view,或是链上的下一个中间件。当前的中间件不需要get_response具体是那种。
上述只是一个简化的实例。对于中间件链上的最后一个中间件的get_response实参不会是真正实际的view,而是一个包装器方法,该方法来源于view middleware处理器。
注意:
- 中间件工厂方法只接受一个参数,即get_response。因此,在
__init__()
函数中,不能有其他的参数; - 对于每次请求,
__call__()
方法都会被调用一次,但是__init__()
只有在服务器启动时调用一次。
在启动阶段,动态标记那个中间件为“unused":通过在__init__()
函数中,抛出MiddlewareNotUsed
异常,则Django会将该中间件从链上剔除。
8.2 激活使用中间件
为了启用某个中间件,在MIDDLEWARE 列表(settings.py)中声明你要使用的中间件列表。
在MIDDLEWARE中,每个中间件都表示为一个字符串(该串为中间件工厂的全路径,或是函数名)。例如:(默认配置)
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
在Django项目中,中间件不是必须的, MIDDLEWARE变量可为空,但是,建议你至少使用CommonMiddleware中间件。
8.2.1 中间件的执行顺序和分层
在调用视图函数之前,Django将依次按顺序调用MIDDLEWARE中定义的中间件。一层层调用中间件(像洋葱模型一样)。如果中间的某个中间件做short-cut短路处理,直接返回类response,而不是调用它的get_response,则后续的中间件是看不到请求的。
8.3 中间件hooks
Django中间件必须至少包含以下方法之一:process_request,process_response,process_view 和 process_exception。 这些是由WSGI处理程序收集的方法,然后按列出的顺序调用。
8.3.1process_view
(1)函数声明:
process_view(request, view_func, view_args, view_kwargs)
(2)参数说明:
request:表示HttpRequst 对象。
view_func: 表示Django将要使用的python函数;
view_args,view_kargs:表示传递给view_func的参数。
(3)返回值
process_view函数在Django调用view之间调用。该函数应该返回None或是HttpResponse对象。
如果返回None,则Django会继续处理该请求,执行其他中间件的process_view函数、view函数。
如果返回HttpResponse对象,则Django就不会往后处理了,直接返回给response。
8.3.2 process_exception
当view函数抛出异常时,就会调用中间件的process_exception函数。
8.3.3 process_template_response
(1)函数声明:process_template_response(request, response)
(2)函数参数:
request:http request对象;
response:TemplateResponse对象。该对象由Django view或是其他middleware返回。
process_template_response是在view函数被调用处理完后被调用。如果response实例有render()方法,则表明该response是一个TemplateResponse 对象,或者说等同于一个TemplateResponse 对象。
(3)返回值
该函数必须返回一个实现了render方法的response对象。它可以通过修改response.template_name 或response.context_data ,修改传入的response对象。或是创建一个全新的TemplateResponse对象或是一个类TemplateResponse对象。
你无需显示的对response进行渲染(通过render方法),当所有的template response 中间件都被调用后,response会自动的渲染一次。
process_template_response方法的执行顺序和中间件定义的顺序相反。
各个钩子函数的调用时机如下图所示:
8.4 处理流式响应
和httpresponse不同的是StreamingHttpResponse 没有content属性。因此,中间件不能假设所有的response 都有一个content属性。当需要获取content时,你需要对response进行测试,如下:
if response.streaming:
response.streaming_content = wrap_streaming_content(response.streaming_content)
else:
response.content = alter_content(response.content)
9. Sessions
https://docs.djangoproject.com/en/2.0/topics/http/sessions/
Django 完全支持匿名session。默认将session数据存储在服务器端,并将cookies中数据的发送和接收过程抽象处理啊。Cookies包含类一个session ID(而不是数据本身)(除非你想将session数据存放到cookie中,详见https://docs.djangoproject.com/en/2.0/topics/http/sessions/#cookie-session-backend)
9.1 启用/禁用session
session是基于中间件实现的。为了允许session机制,你应该做如下配置:
- 编辑setting中的MIDDLEWARE值,使得包含如下串:
django.contrib.sessions.middleware.SessionMiddleware
。通过Django-admin startproject 配置的项目默认包含了该值。
如果你不想使用session,则可在MIDDLEWARE中移除SessionMiddleware中间件串,并在INSTALLED_APPS变量中,移除django.contrib.sessions。
9.2 配置session引擎
默认情况下,Django将session数据存储在数据库中(对应的model为:django.contrib.sessions.models.Session)。但是这可以配置。
如果你想将session存储到数据库中,你需要在INSTALLED_APPS中配置django.contrib.sessions。在配置好后,运行manage.py migrate来安装存储session数据的单个数据表。
9.2.1 将session存储到cache中
将session存储到cache中可以提高存取性能。
在将session存储到cache前,首先检查是否配置来cache。详细见:https://docs.djangoproject.com/en/2.0/topics/cache/
注意:本地cache存储不是线程安全的,因此可能不适合在生成环境下配置本地cache。但是可以配置到像memcache这种cache中。
如果你在CACHES中定义了多个cache,则Django会使用默认的cache。如果要使用其他的cache,将ESSION_CACHE_ALIAS配置成你要使用的cache。
当你配置后cache后,你可以进行如下配置:
- 设置
SESSION_ENGINE
为django.contrib.sessions.backends.cache
,配置简单的cache存储。Session将会直接存储到你的cache中。但是,当cache 存储空间满或是服务器重启后,session数据可能不会进行持久化处理; - 为了进行持久化,设置
SESSION_ENGINE
为django.contrib.sessions.backends.cached_db
。这是,会进行写直达(write through)。每次写cache时,也会写入到数据库中。如果数据不在cache中,则会直接到数据库中读取。如果你要使用改种模式,则还需要继续进行配置,详见:https://docs.djangoproject.com/en/2.0/topics/http/sessions/#using-database-backed-sessions
9.2.2 将session存储到文件中
将session存储到文件中,将设置SESSION_ENGINE
为django.contrib.sessions.backends.file
。
文件式的存储还需要进行配置 SESSION_FILE_PATH,配置文件路径,详见:https://docs.djangoproject.com/en/2.0/ref/settings/#std:setting-SESSION_FILE_PATH
9.2.3 将session存储到cookie中
将session存储到cookie中,将设置SESSION_ENGINE
为django.contrib.sessions.backends.signed_cookies
。
9.3 在view中使用session
当启用了SessionMiddleware
后,每个HttpRequest 对象(view的第一个参数)就会有session属性(类字典的对象),然后你可以在view中,通过request.session的方式读写session的值。
9.3.1 backends.base.SessionBase类
该类是所有session对象的基类。该类具有如下方法、及方法的使用示例:
__getitem__(key)
:使用方式,例如:fav_color = request.session['fav_color']
__setitem__(key, value)
:使用方式,例如:request.session['fav_color'] = 'blue'
__delitem__(key)
:可以使用del request.session['fav_color']
,但是当key不存在时,会抛出异常;__contains__(key)
:'fav_color' in request.session
get(key, default=None)
:fav_color = request.session.get('fav_color', 'red')
pop(key, default=__not_given)
:fav_color = request.session.pop('fav_color', 'blue')
keys()
:items()
:set_expiry(value)
:flush()
:删除情况session数据;get_expiry_date()
:- 其他
9.3.2 session的序列化
默认情况下,Django将session组成成json格式。你可以通过SESSION_SERIALIZER
设置来指定使用特定的序列化格式,详见:https://docs.djangoproject.com/en/2.0/topics/http/sessions/#custom-serializers
9.4 在view外使用session
可以通过SessionStore 在 view外使用session。例如:
>>> from django.contrib.sessions.backends.db import SessionStore
>>> s = SessionStore()
>>> # stored as seconds since epoch since datetimes are not serializable in JSON.
>>> s['last_login'] = 1376587691
>>> s.create()
>>> s.session_key
'2b1189a188b44ad18c35e113ac6ceead'
>>> s = SessionStore(session_key='2b1189a188b44ad18c35e113ac6ceead')
>>> s['last_login']
1376587691
如果你使用db 作为session后端引擎,则可通过model的方式访问操作session。session 相关的model定义在django/contrib/sessions/models.py
中。使用示例:
>>> from django.contrib.sessions.models import Session
>>> s = Session.objects.get(pk='2b1189a188b44ad18c35e113ac6ceead')
>>> s.expire_date
datetime.datetime(2005, 8, 20, 13, 35, 12)