">

笔记9:Django提升篇

摘要:日常学习中对一些知识点进行总结得出该系列文章。学习笔记内容包括前端技术,Django web开发技术,数据库技术如MySQL,MongoDB,PGSQL等等。此外还有一些工具如Dock,ES等等。(本文原创,转载必须注明出处.)

(1)打包应用程序:可重用性

  • 打包 Python 程序需要工具:setuptools 。打包时候建议使用django-appname
  • 1 在你的 Django 项目目录外创建一个名为 django-polls 的文件夹,用于盛放 polls
  • 2 将 polls 目录移入 django-polls 目录。
  • 3 创建一个名为 django-polls/README.rst 的文件,包含以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
=====
Polls
=====

Polls is a simple Django app to conduct Web-based polls. For each
question, visitors can choose between a fixed number of answers.

Detailed documentation is in the "docs" directory.

Quick start
-----------

1. Add "polls" to your INSTALLED_APPS setting like this::

INSTALLED_APPS = [
...
'polls',
]

2. Include the polls URLconf in your project urls.py like this::

path('polls/', include('polls.urls')),

3. Run `python manage.py migrate` to create the polls models.

4. Start the development server and visit http://127.0.0.1:8000/admin/
to create a poll (you'll need the Admin app enabled).

5. Visit http://127.0.0.1:8000/polls/ to participate in the poll.
  • 4 创建一个 django-polls/LICENSE 文件。选择一个非本教程使用的授权协议,但是要足以说明发布代码没有授权证书是 不可能的 。Django 和很多兼容 Django 的应用是以 BSD 授权协议发布的;不过,你可以自己选择一个授权协议。只要确定你选择的协议能够限制未来会使用你的代码的人。
  • 下一步我们将创建 setup.py 用于说明如何构建和安装应用的细节。关于此文件的完整介绍超出了此教程的范围,但是 setuptools docs 有详细的介绍。创建文件 django-polls/setup.py 包含以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import os
from setuptools import find_packages, setup

with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
README = readme.read()

# allow setup.py to be run from any path
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))

setup(
name='django-polls',
version='0.1',
packages=find_packages(),
include_package_data=True,
license='BSD License', # example license
description='A simple Django app to conduct Web-based polls.',
long_description=README,
url='https://www.example.com/',
author='Your Name',
author_email='yourname@example.com',
classifiers=[
'Environment :: Web Environment',
'Framework :: Django',
'Framework :: Django :: X.Y', # replace "X.Y" as appropriate
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', # example license
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
],
)
  • 6 默认包中只包含 Python 模块和包。为了包含额外文件,我们需要创建一个名为 MANIFEST.in 的文件。上一步中关于 setuptools 的文档详细介绍了这个文件。为了包含模板、README.rst 和我们的 LICENSE 文件,创建文件 django-polls/MANIFEST.in 包含以下内容:
1
2
3
4
include LICENSE
include README.rst
recursive-include polls/static *
recursive-include polls/templates *
  • 7 在应用中包含详细文档是可选的,但我们推荐你这样做。创建一个空目录 django-polls/docs 用于未来编写文档。额外添加一行至 django-polls/MANIFEST.in
1
recursive-include docs *
  • 8 试着构建你自己的应用包通过 ptyhon setup.py sdist (在 django-polls``目录内)。这将创建一个名为 ``dist 的目录并构建你自己的应用包, django-polls-0.1.tar.gz
1
C:\Users\Administrator\Desktop\django-polls> python setup.py sdist

(2)使用自己的包名

由于我们把 polls 目录移出了项目,所以它无法工作了。我们现在要通过安装我们的新 django-polls 应用来修复这个问题。

1
C:\Users\Administrator\Desktop\django-polls\dist>pip install --user django-polls-0.1.tar.gz

1560310208431

(3)发布应用,可以邮件,github等等方式上传

(4)模型

(5)查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
>>> from blog.models import Blog
>>> b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
>>> b.save()

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)

>>> all_entries = Entry.objects.all()

Entry.objects.filter(pub_date__year=2006)
Entry.objects.all().filter(pub_date__year=2006)

>>> q1 = Entry.objects.filter(headline__startswith="What")
>>> q2 = q1.exclude(pub_date__gte=datetime.date.today())
>>> q3 = q1.filter(pub_date__gte=datetime.date.today())

>>> one_entry = Entry.objects.get(pk=1)

>>> Entry.objects.all()[5:10] # OFFSET 5 LIMIT 5

>>> Entry.objects.order_by('headline')[0]

SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

Entry.objects.get(headline__exact="Cat bites dog")

>>> Blog.objects.get(name__iexact="beatles blog")

Entry.objects.get(headline__contains='Lennon')

SELECT ... WHERE headline LIKE '%Lennon%';

Blog.objects.filter(entry__authors__isnull=False, entry__authors__name__isnull=True)

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))

>>> print([e.headline for e in Entry.objects.all()])

SELECT * from polls WHERE question LIKE 'Who%'
AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')

>>> e.delete()

>>> Entry.objects.filter(pub_date__year=2005).delete()

e = Entry.objects.get(id=3)
e.authors.all() # Returns all Author objects for this Entry.
e.authors.count()
e.authors.filter(name__contains='John')

a = Author.objects.get(id=5)
a.entry_set.all() # Returns all Entry objects for this Author.

e = Entry.objects.get(id=2)
e.entrydetail # returns the related EntryDetail object
e.entrydetail = ed

(5)聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from django.db import models

class Author(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()

class Publisher(models.Model):
name = models.CharField(max_length=300)

class Book(models.Model):
name = models.CharField(max_length=300)
pages = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
rating = models.FloatField()
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
pubdate = models.DateField()

class Store(models.Model):
name = models.CharField(max_length=300)
books = models.ManyToManyField(Book)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Total number of books.
>>> Book.objects.count()

>>> Book.objects.filter(publisher__name='BaloneyPress').count()

# Average price across all books.
>>> from django.db.models import Avg, Max, Min
>>> Book.objects.aggregate(Avg('price'), Max('price'), Min('price'))

# Each publisher, each with a count of books as a "num_books" attribute.
>>> pubs = Publisher.objects.annotate(num_books=Count('book'))

# The top 5 publishers, in order by number of books.
>>> pubs = Publisher.objects.annotate(num_books=Count('book')).order_by('-num_books')[:5]
>>> pubs[0].num_books

>>> Book.objects.filter(name__startswith="Django").annotate(num_authors=Count('authors'))

>>> Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__gt=1)

>>> Book.objects.annotate(num_authors=Count('authors')).order_by('num_authors')

(6)搜索

1
2
3
4
5
>>> Entry.objects.filter(body_text__search='cheese')

>>> Entry.objects.annotate(
... search=SearchVector('blog__tagline', 'body_text'),
... ).filter(search='cheese')

(7)Manager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from django.db import models

class PollManager(models.Manager):
def with_counts(self):
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("""
SELECT p.id, p.question, p.poll_date, COUNT(*)
FROM polls_opinionpoll p, polls_response r
WHERE p.id = r.poll_id
GROUP BY p.id, p.question, p.poll_date
ORDER BY p.poll_date DESC""")
result_list = []
for row in cursor.fetchall():
p = self.model(id=row[0], question=row[1], poll_date=row[2])
p.num_responses = row[3]
result_list.append(p)
return result_list

class OpinionPoll(models.Model):
question = models.CharField(max_length=200)
poll_date = models.DateField()
objects = PollManager()

class Response(models.Model):
poll = models.ForeignKey(OpinionPoll, on_delete=models.CASCADE)
person_name = models.CharField(max_length=50)
response = models.TextField()
1
2
3
4
5
6
7
8
9
10
11
class ExtraManager(models.Model):
extra_manager = OtherManager()

class Meta:
abstract = True

class ChildC(AbstractBase, ExtraManager):
# ...
# Default manager is CustomManager, but OtherManager is
# also available via the "extra_manager" attribute.
pass

(8)raw SQL queries

1
2
3
4
class Person(models.Model):
first_name = models.CharField(...)
last_name = models.CharField(...)
birth_date = models.DateField(...)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
>>> for p in Person.objects.raw('SELECT * FROM myapp_person'):
... print(p)

>>> Person.objects.raw('SELECT last_name, birth_date, first_name, id FROM myapp_person')

>>> name_map = {'first': 'first_name', 'last': 'last_name', 'bd': 'birth_date', 'pk': 'id'}
>>> Person.objects.raw('SELECT * FROM some_other_table', translations=name_map)

>>> first_person = Person.objects.raw('SELECT * FROM myapp_person LIMIT 1')[0]

>>> people = Person.objects.raw('SELECT *, age(birth_date) AS age FROM myapp_person')
>>> for p in people:
... print("%s is %s." % (p.first_name, p.age))

>>> lname = 'Doe'
>>> Person.objects.raw('SELECT * FROM myapp_person WHERE last_name = %s', [lname])

from django.db import connection

def my_custom_sql(self):
with connection.cursor() as cursor:
cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [self.baz])
cursor.execute("SELECT foo FROM bar WHERE baz = %s", [self.baz])
row = cursor.fetchone()
return row

cursor.execute("SELECT foo FROM bar WHERE baz = '30%%' AND id = %s", [self.id])

>>> cursor.fetchall()

with connection.cursor() as c:
c.execute(...)

with connection.cursor() as cursor:
cursor.callproc('test_procedure', [1, 'test'])

(9) 数据事务

1
2
3
4
5
6
a.save() # Succeeds, but may be undone by transaction rollback
try:
b.save() # Could throw exception
except IntegrityError:
transaction.rollback()
c.save() # Succeeds, but a.save() may have been undone

(10)多数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DATABASES = {
'default': {
'NAME': 'app_data',
'ENGINE': 'django.db.backends.postgresql',
'USER': 'postgres_user',
'PASSWORD': 's3krit'
},
'users': {
'NAME': 'user_data',
'ENGINE': 'django.db.backends.mysql',
'USER': 'mysql_user',
'PASSWORD': 'priv4te'
}
}
1
2
3
$ ./manage.py migrate --database=users
$ ./manage.py migrate --database=users
$ ./manage.py migrate --database=customers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.contrib import admin

# Specialize the multi-db admin objects for use with specific models.
class BookInline(MultiDBTabularInline):
model = Book

class PublisherAdmin(MultiDBModelAdmin):
inlines = [BookInline]

admin.site.register(Author, MultiDBModelAdmin)
admin.site.register(Publisher, PublisherAdmin)

othersite = admin.AdminSite('othersite')
othersite.register(Publisher, MultiDBModelAdmin)
1
2
3
from django.db import connections
with connections['my_db_alias'].cursor() as cursor:
...

(11) URL调度器

1
2
3
4
5
6
7
8
9
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),
]
1
2
3
4
5
6
7
8
9
from django.urls import path, re_path
from . import views

urlpatterns = [
path('articles/2003/', views.special_case_2003),
re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),
re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<slug>[\w-]+)/$', views.article_detail),
]
1
2
3
4
5
6
from django.urls import re_path

urlpatterns = [
re_path(r'^blog/(page-(\d+)/)?$', blog_articles), # bad
re_path(r'^comments/(?:page-(?P<page_number>\d+)/)?$', comments), # good
]
1
2
3
4
5
6
7
8
from django.urls import include, path

urlpatterns = [
# ... snip ...
path('community/', include('aggregator.urls')),
path('contact/', include('contact.urls')),
# ... snip ...
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# In settings/urls/main.py
from django.urls import include, path

urlpatterns = [
path('<username>/blog/', include('foo.urls.blog')),
]

# In foo/urls/blog.py
from django.urls import path
from . import views

urlpatterns = [
path('', views.blog.index),
path('archive/', views.blog.archive),
]

示例

1
2
3
4
5
6
7
8
from django.urls import path
from . import views

urlpatterns = [
#...
path('articles/<int:year>/', views.year_archive, name='news-year-archive'),
#...
]
1
2
3
4
5
6
7
<a href="{% url 'news-year-archive' 2012 %}">2012 Archive</a>
{# Or with the year in a template context variable: #}
<ul>
{% for yearvar in year_list %}
<li><a href="{% url 'news-year-archive' yearvar %}">{{ yearvar }} Archive</a></li>
{% endfor %}
</ul>
1
2
3
4
5
6
7
8
from django.http import HttpResponseRedirect
from django.urls import reverse

def redirect_to_year(request):
# ...
year = 2006
# ...
return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))

(12)文件上传

forms.py

1
2
3
4
5
from django import forms

class UploadFileForm(forms.Form):
title = forms.CharField(max_length=50)
file = forms.FileField()

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import ModelFormWithFileField

def upload_file(request):
if request.method == 'POST':
form = ModelFormWithFileField(request.POST, request.FILES)
if form.is_valid():
# file is saved
form.save()
return HttpResponseRedirect('/success/url/')
else:
form = ModelFormWithFileField()
return render(request, 'upload.html', {'form': form})
1
2
3
4
def handle_uploaded_file(f):
with open('some/file/name.txt', 'wb+') as destination:
for chunk in f.chunks():
destination.write(chunk)

使用 UploadedFile.chunks() 而不是 read() 是为了确保即使是大文件又不会将我们系统的内存占满。

(13)快捷键函数

  • render(request, template_name, context=None**, content_type=None, status=None,** using=None)
1
2
3
4
5
6
7
from django.shortcuts import render

def my_view(request):
# View code here...
return render(request, 'myapp/index.html', {
'foo': 'bar',
}, content_type='application/xhtml+xml')
  • redirect(to, args, permanent=False**, *kwargs*)
1
2
3
4
5
6
from django.shortcuts import redirect

def my_view(request):
...
obj = MyModel.objects.get(...)
return redirect(obj)

(14)中间件

中间件是 Django 请求/响应处理的钩子框架。它是一个轻量级的、低级的“插件”系统,用于全局改变 Django 的输入或输出。

中间件工厂是一个可调用的程序,它接受 get_response 可调用并返回中间件。中间件是可调用的,它接受请求并返回响应,就像视图一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

或者它可以写成一个类,它的实例是可调用的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

中间件工厂必须接受 get_response 参数。还可以初始化中间件的一些全局状态。记住两个注意事项:

  • Django仅用 get_response 参数初始化您的中间件,因此不能定义 __init__(),因为需要其他参数。
  • 与每次请求调用 __call__() 方法不同,当 Web 服务器启动时,__init__() 只被称为一次
激活中间件

若要激活中间件组件,请将其添加到 Django 设置中的 MIDDLEWARE 列表中。在 MIDDLEWARE 中,每个中间件组件由字符串表示:指向中间件工厂的类或函数名的完整 Python 路径。例如,这里创建的默认值是 django-admin startproject

1
2
3
4
5
6
7
8
9
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',
]

MIDDLEWARE 的顺序很重要,因为中间件会依赖其他中间件。例如:类 AuthenticationMiddleware 在会话中存储经过身份验证的用户;因此,它必须在 SessionMiddleware 后面运行 。中间件。Session中间件。请参阅 Middleware ordering ,用于一些关于 Django 中间件类排序的常见提示。

(15) 会话

1
2
3
4
5
6
7
def post_comment(request, new_comment):
if request.session.get('has_commented', False):
return HttpResponse("You've already commented.")
c = comments.Comment(comment=new_comment)
c.save()
request.session['has_commented'] = True
return HttpResponse('Thanks for your comment!')

This simplistic view logs in a “member” of the site:

1
2
3
4
5
6
7
def login(request):
m = Member.objects.get(username=request.POST['username'])
if m.password == request.POST['password']:
request.session['member_id'] = m.id
return HttpResponse("You're logged in.")
else:
return HttpResponse("Your username and password didn't match.")

…And this one logs a member out, according to login() above:

1
2
3
4
5
6
def logout(request):
try:
del request.session['member_id']
except KeyError:
pass
return HttpResponse("You're logged out.")

(16)表单

16.1 Get与POST

处理表单时只会用到 GETPOST 两种HTTP方法。Django的登录表单使用 POST 方法传输数据,在这个方法中浏览器会封装表单数据,为了传输会进行编码,然后发送到服务端并接收它的响应。相比之下,GET 方法将提交的数据捆绑到一个字符串中,并用它来组成一个URL。该URL包含了数据要发送的地址以及一些键值对应的数据。如果您在Django文档中进行一次搜索,就会看到这点,它会生成一个形似 https://docs.djangoproject.com/search/?q=forms&release=1 的URL。

GETPOST 通常用于不同的目的。

任何可能用于更改系统状态的请求应该使用 POST —— 比如一个更改数据库的请求。GET 应该只被用于不会影响系统状态的请求。

GET 方法也不适合密码表单,因为密码会出现在URL中,于是也会出现在浏览器的历史记录以及服务器的日志中,而且都是以纯文本的形式。它也不适合处理大量的数据或者二进制数据,比如一张图片。在WEB应用的管理表单中使用 GET 请求具有安全隐患:攻击者很容易通过模拟请求来访问系统的敏感数据。 POST 方法通过与其他像Django的 CSRF protection 这样的保护措施配合使用,能对访问提供更多的控制。另一方面, GET 方法适用于诸如网页搜索表单这样的内容,因为这类呈现为一个 GET请求的URL很容易被存为书签、分享或重新提交。

1
2
3
4
5
<form action="/your-name/" method="post">
<label for="your_name">Your name: </label>
<input id="your_name" type="text" name="your_name" value="{{ current_name }}">
<input type="submit" value="OK">
</form>

forms.py

1
2
3
4
from django import forms

class NameForm(forms.Form):
your_name = forms.CharField(label='Your name', max_length=100)

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.http import HttpResponseRedirect
from django.shortcuts import render

from .forms import NameForm

def get_name(request):
# if this is a POST request we need to process the form data
if request.method == 'POST':
# create a form instance and populate it with data from the request:
form = NameForm(request.POST)
# check whether it's valid:
if form.is_valid():
# process the data in form.cleaned_data as required
# ...
# redirect to a new URL:
return HttpResponseRedirect('/thanks/')

# if a GET (or any other method) we'll create a blank form
else:
form = NameForm()

return render(request, 'name.html', {'form': form})

视图将再次创建一个表单实例并使用请求中的数据填充它: form = NameForm(request.POST) 这叫“绑定数据到表单” (现在它是一张 绑定的 表单)。调用表单的 is_valid() 方法;如果不为 True ,我们带着表单返回到模板。这次表单不再为空( 未绑定 ),所以HTML表单将用之前提交的数据进行填充,放到可以根据需要进行编辑和修正的位置。如果 is_valid()True ,我们就能在其 cleaned_data 属性中找到所有通过验证的表单数据。我们可以在发送一个HTTP重定向告诉浏览器下一步去向之前用这些数据更新数据库或者做其他处理。

name.html

1
2
3
4
5
<form action="/your-name/" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit">
</form>

16.2 详解Django Form类

forms.py

1
2
3
4
5
6
7
from django import forms

class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField(widget=forms.Textarea)
sender = forms.EmailField()
cc_myself = forms.BooleanField(required=False)

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.core.mail import send_mail

if form.is_valid():
subject = form.cleaned_data['subject']
message = form.cleaned_data['message']
sender = form.cleaned_data['sender']
cc_myself = form.cleaned_data['cc_myself']

recipients = ['info@example.com']
if cc_myself:
recipients.append(sender)

send_mail(subject, message, sender, recipients)
return HttpResponseRedirect('/thanks/')
1
2
3
4
5
6
7
8
<p><label for="id_subject">Subject:</label>
<input id="id_subject" type="text" name="subject" maxlength="100" required></p>
<p><label for="id_message">Message:</label>
<textarea name="message" id="id_message" required></textarea></p>
<p><label for="id_sender">Sender:</label>
<input type="email" name="sender" id="id_sender" required></p>
<p><label for="id_cc_myself">Cc myself:</label>
<input type="checkbox" name="cc_myself" id="id_cc_myself"></p>

(17)用户认证

Django 自带一个用户验证系统。它负责处理用户账号、组、权限和基于cookie的用户会话。文档的这部分解释了默认的实现如何开箱即用,以及如何扩展和自定义以满足你的项目需求。

(18)验证系统

创建用户

1
2
3
4
5
6
7
8
>>> from django.contrib.auth.models import User
>>> user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword')

# At this point, user is a User object that has already been saved
# to the database. You can continue to change its attributes
# if you want to change other fields.
>>> user.last_name = 'Lennon'
>>> user.save()

创建超级用户

1
$ python manage.py createsuperuser --username=joe --email=joe@example.com

更改密码

1
2
3
4
>>> from django.contrib.auth.models import User
>>> u = User.objects.get(username='john')
>>> u.set_password('new password')
>>> u.save()

验证用户

1
2
3
4
5
6
from django.contrib.auth import authenticate
user = authenticate(username='john', password='secret')
if user is not None:
# A backend authenticated the credentials
else:
# No backend authenticated the credentials

登出

1
2
3
4
5
from django.contrib.auth import logout

def logout_view(request):
logout(request)
# Redirect to a success page.

限制对未登录用户的访问:装饰器

1
2
3
4
5
from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
...

(19)Django缓存框架

动态网站存在一个基本权衡是——它们是动态的。每次用户请求一个页面,web 服务器需要提供各种各样的计算——从数据库查询到模板渲染再到业务逻辑——最后建立页面呈现给用户。从处理开销的角度来看,这比标准读取文件系统服务安排的开销要高得多。下面是一些伪代码解释了动态网站生成页面的时候,缓存是怎么工作的:

1
2
3
4
5
6
7
given a URL, try finding that page in the cache
if the page is in the cache:
return the cached page
else:
generate the page
save the generated page in the cache (for next time)
return the generated page

19.1 Memcached内存缓存

Memcached 是一个完全基于内存的缓存服务器,是 Django 原生支持的最快、最高效的缓存类型,最初被开发出来用于处理 LiveJournal.com 的高负载,随后由 Danga Interactive 开源。Facebook 和 Wikipedia 等网站使用它来减少数据库访问并显著提高网站性能。

Memcached 以一个守护进程的形式运行,并且被分配了指定数量的 RAM。它所做的就是提供一个快速接口用于在缓存中添加,检索和删除数据。所有数据都直接存储在内存中,因此不会产生数据库或文件系统使用的开销。

在安装 Memcached 本身后,你还需要安装一个 Memcached 绑定。有许多可用的 Python Memcached 绑定,最常见的两个是 python-memcached 和pylibmc

在 Django 中使用 Memcached :

  • 将 BACKEND 设置为 django.core.cache.backends.memcached.MemcachedCache 或者 django.core.cache.backends.memcached.PyLibMCCache (取决于你所选择的 memcached 绑定)
  • 将 LOCATION设置为 ip:port 值,其中 ip 是 Memcached 守护进程的 IP 地址,port 是运行 Memcached 的端口;或者设置为一个 unix:path 值,其中 path 是 Memcached Unix 套接字文件的路径。

在这个示例中,Memcached 使用 python-memcached 绑定,在 localhost (127.0.0.1) 端口 11211 上运行:

1
2
3
4
5
6
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
}
}

在这个示例中, Memcached 可通过本地 Unix 套接字文件 /tmp/memcached.sock 使用 python-memcached 绑定得到:

1
2
3
4
5
6
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': 'unix:/tmp/memcached.sock',
}
}

当使用 pylibmc 绑定时,不要包含 unix:/ 前缀:

1
2
3
4
5
6
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
'LOCATION': '/tmp/memcached.sock',
}
}

Memcached 的一个出色功能是它能够在多个服务器上共享缓存。这意味着您可以在多台计算机上运行 Memcached 守护程序,程序会视这组计算机为单个缓存,而无需在每台机器上复制缓存值。要使用此功能,需要在 LOCATION 中包含所有服务器的地址,可以是分号或者逗号分隔的字符串,也可以是一个列表。

在这个示例中,缓存通过端口 11211 的 IP 地址 172.19.26.240 、 172.19.26.242 运行的 Memcached 实例共享:

1
2
3
4
5
6
7
8
9
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': [
'172.19.26.240:11211',
'172.19.26.242:11211',
]
}
}

在以下示例中,缓存通过在 IP 地址 172.19.26.240(端口号 11211),172.19.26.242(端口号 11212)和 172.19.26.244(端口号 11213)上运行的 Memcached 实例共享:

1
2
3
4
5
6
7
8
9
10
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': [
'172.19.26.240:11211',
'172.19.26.242:11212',
'172.19.26.244:11213',
]
}
}

关于 Memcached 的最后一点是,基于内存的缓存有一个缺点:因为缓存的数据存储在内存中,如果服务器崩溃,那么数据将会丢失。显然,内存不适用于持久数据存储,因此不要依赖基于内存的缓存作为你唯一的数据存储。毫无疑问,没有任何 Django 缓存后端应该被用于持久存储——它们都是适用于缓存的解决方案,而不是存储——我们在这里指出这一点是因为基于内存的缓存是格外临时的。

19.2 数据库缓存

Django 可以在数据库中存储缓存数据。如果你有一个快速、索引正常的数据库服务器,这种缓存效果最好。

用数据库表作为你的缓存后端:

  • BACKEND 设置为 django.core.cache.backends.db.DatabaseCache
  • LOCATION 设置为 数据库表的表名。这个表名可以是没有使用过的任何符合要求的名称。

在这个例子中,缓存表的名称是 my_cache_table

1
2
3
4
5
6
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'my_cache_table',
}
}
  • 创建缓存表

使用数据库缓存之前,必须通过下面的命令创建缓存表:

1
python manage.py createcachetable

这将在数据库中创建一个表,该表的格式与 Django 数据库缓存系统期望的一致。该表的表名取自 LOCATION

如果你正在使用多数据库缓存, createcachetable 会对每个缓存创建一个表。

如果你正在使用多数据库, createcachetable 将遵循数据库路由的 allow_migrate() 方法。

migrate 一样, createcachetable 不会影响已经存在的表,它只创建缺失的表。

要打印即将运行的 SQL,而不是运行它,请使用 createcachetable --dry-run 选项。

  • 多数据库

如果在多数据库中使用缓存,你也需要设置数据库缓存表的路由指令。因为路由的原因,数据库缓存表在 django_cache 应用程序中显示为 CacheEntry 的模型名。这个模型不会出现在模型缓存中,但模型详情可用于路由目的。

比如,下面的路由可以将所有缓存读取操作指向 cache_replica ,并且所有的写操作指向 cache_primary。缓存表将会只同步到 cache_primary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CacheRouter:
"""A router to control all database cache operations"""

def db_for_read(self, model, **hints):
"All cache read operations go to the replica"
if model._meta.app_label == 'django_cache':
return 'cache_replica'
return None

def db_for_write(self, model, **hints):
"All cache write operations go to primary"
if model._meta.app_label == 'django_cache':
return 'cache_primary'
return None

def allow_migrate(self, db, app_label, model_name=None, **hints):
"Only install the cache model on primary"
if app_label == 'django_cache':
return db == 'cache_primary'
return None

如果你没有指定路由指向数据库缓存模型,缓存后端将使用 默认 的数据库。当然,如果没使用数据库缓存后端,则无需担心为数据库缓存模型提供路由指令。

19.3 文件系统缓存

基于文件的后端序列化并保存每个缓存值作为单独的文件。要使用此后端,可将 BACKEND 设置为 "django.core.cache.backends.filebased.FileBasedCache" 并将 LOCATION 设置为一个合适的路径。比如,在 /var/tmp/django_cache 存储缓存数据,使用以下配置:

1
2
3
4
5
6
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/django_cache',
}
}

如果使用 Windows 系统,将驱动器号放在路径开头,如下:

1
2
3
4
5
6
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': 'c:/foo/bar',
}
}

目录路径应该是绝对路径——因此,它应该以文件系统根目录开始。无需担心是否需要以斜杠结尾。

确保这个配置指向的目录存在,并且可由运行 Web 服务器的系统用户读写。继续上面的例子,如果服务器被用户 apache 运行,确保目录 /var/tmp/django_cache 存在并且可被用户 apache 读写。

19.4 本地内存缓存

如果在配置文件中没有指定缓存,那么将默认使用本地内存缓存。如果你想要内存缓存的速度优势,但又没有条件使用 Memcached,那么可以考虑本地内存缓存后端。

1
2
3
4
5
6
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
}
}

LOCATION 被用于标识各个内存存储。如果只有一个 locmem 缓存,你可以忽略 LOCATION 。但是如果你有多个本地内存缓存,那么你至少要为其中一个起个名字,以便将它们区分开。这种缓存使用最近最少使用(LRU)的淘汰策略。

注意,每个进程将有它们自己的私有缓存实例,这意味着不存在跨进程的缓存。这也同样意味着本地内存缓存不是特别节省内存,因此它或许不是生成环境的好选择,不过它在开发环境中表现很好。

19.5 虚拟缓存(用于开发模式)

Django 带有一个实际上不是缓存的 “虚拟” 缓存,它只是实现缓存接口,并不做其他操作。如果你有一个正式网站在不同地方使用了重型缓存,但你不想在开发环境使用缓存,而且不想为这个特殊场景而修改代码的时候,这将非常有用。要激活虚拟缓存,像这样设置 BACKEND

1
2
3
4
5
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}

19.6 使用自定义的缓存后台

虽然 Django 自带一些缓存后端,但有时你也想使用自定义的缓存后端。当使用第三方缓存后端时,使用 Python 导入路径作为 Cache 设置的后端,像这样:

1
2
3
4
5
CACHES = {
'default': {
'BACKEND': 'path.to.backend',
}
}

如果你正在创建自己的后端,你可以使用标准缓存作为参考实现。你在 Django 源代码的 django/core/cache/backends/ 目录找到代码。

注意:除非是令人信服的理由,诸如服务器不支持缓存,否则你应该使用 Django 附带的缓存后端。他们经过了良好的测试并易于使用。

19.7 缓存参数

每个缓存后端可以通过额外的参数来控制缓存行为。这些参数在 CACHES 设置中作为附加键提供。有效参数如下:

  • 缓存:setting:TIMEOUT :用于缓存的默认超时时间(以秒为单位)。这个参数默认为 300 秒(5分钟)。你可以设置 TIMEOUTNone,因此,默认情况下缓存键永不过时。值为 0 会导致键立刻过期(实际上就是不缓存)。

  • OPTIONS :任何选项应该传递到缓存后端。有效选项列表将随着每个后端变化,并且由第三方库缓存支持的后端直接传递它们的选项到底层缓存库。

    实现自有的淘汰策略的缓存后端(比如 locmem, filesystemdatabase 后端)将遵循以下选项:

    • MAX_ENTRIES :删除旧值之前允许缓存的最大条目。默认是 300

    • CULL_FREQUENCY :当达到 MAX_ENTRIES 时被淘汰的部分条目。实际比率为 1 / CULL_FREQUENCY ,当达到 MAX_ENTRIES 时,设置为2就会淘汰一半的条目。这个参数应该是一个整数,默认为3。

      CULL_FREQUENCY 的值为 0 意味着当达到 MAX_ENTRIES 缓存时,整个缓存都会被清空。在一些后端(尤其是 database ),这会使以更多的缓存未命中为代价来更快的进行淘汰。

    Memcached 后端传递 OPTIONS 的内容作为键参数到客户端构造函数,从而允许对客户端行为进行更高级的控制。参见下文:

  • KEY_PREFIX :将自动包含(默认预先添加)到Django 服务器使用的所有缓存键的字符串。

    查看 cache documentation 获取更多信息。

  • VERSION :通过 Django 服务器生成的缓存键的默认版本号。

    查看 cache documentation 获取更多信息。

  • KEY_FUNCTION :一个包含指向函数的路径的字符串,该函数定义将如何前缀、版本和键组成最终的缓存键。

    查看 cache documentation 获取更多信息。

在这个例子中,文件系统后端正被设置成60秒超时时间,并且最大容量是1000条。

1
2
3
4
5
6
7
8
9
10
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/django_cache',
'TIMEOUT': 60,
'OPTIONS': {
'MAX_ENTRIES': 1000
}
}
}

这个的例子是基于 python-memcached 后端的设置,对象大小限制在 2MB :

1
2
3
4
5
6
7
8
9
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'OPTIONS': {
'server_max_value_length': 1024 * 1024 * 2,
}
}
}

这个例子是基于 pylibmc 后端的设置,改设置支持二进制协议、SASL 验证和 ketama行为模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
'LOCATION': '127.0.0.1:11211',
'OPTIONS': {
'binary': True,
'username': 'user',
'password': 'pass',
'behaviors': {
'ketama': True,
}
}
}
}

更多缓存知识访问>> https://docs.djangoproject.com/zh-hans/2.2/topics/cache/

(20)发送邮件

20.1 快速上手,仅需两行代码:

1
2
3
4
5
6
7
8
9
from django.core.mail import send_mail

send_mail(
'Subject here',
'Here is the message.',
'from@example.com',
['to@example.com'],
fail_silently=False,
)

邮件是通过 SMTP 主机和端口发送的,由配置项 EMAIL_HOSTEMAIL_PORT 指定。如果配置了 EMAIL_HOST_USEREMAIL_HOST_PASSWORD ,那么它们将被用来验证 SMTP 服务器。配置项 EMAIL_USE_TLSEMAIL_USE_SSL 控制是否使用安全连接。发送邮件最简单的方式就是使用 django.core.mail.send_mail()

send_mail():send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None, connection=None, html_message=None)

  • 参数 subject, message, from_emailrecipient_list 是必须的。
  • recipient_list: 一个字符串列表,每项都是一个邮箱地址。recipient_list中的每个成员都可以在邮件的 “收件人:” 中看到其他的收件人。
  • fail_silently: 为 Falsesend_mail() 发生错误时抛出 smtplib.SMTPException 。可在 smtplib 文档找到一系列可能的异常,它们都是 SMTPException 的子类。
  • auth_user: 可选的用户名,用于验证登陆 SMTP 服务器。 若未提供,Django 会使用 EMAIL_HOST_USER 指定的值。
  • auth_password: 可选的密码,用于验证登陆 SMTP 服务器。若未提供, Django 会使用 EMAIL_HOST_PASSWORD 指定的值。
  • connection: 可选参数,发送邮件使用的后端。若未指定,则使用默认的后端。查询 邮件后端 文档获取更多细节。
  • html_message: 若提供了 html_message,会使邮件成为 multipart/alternative 的实例, message 的内容类型则是 text/plain ,并且 html_message 的内容类型是 text/html

返回值会是成功发送的信息的数量(只能是 01 ,因为同时只能发送一条消息)。

20.2 批量发送邮件

django.core.mail.send_mass_mail() 用于批量发送邮件。

send_mass_mail():(datatuple, fail_silently=False, auth_user=None, auth_password=None, connection=None)

datatuple 是一个元组,形式如下:

1
(subject, message, from_email, recipient_list)

datatuple 参数的每个元素会生成一份独立的邮件内容。就像 send_mail() 中的一样, recipient_list 中的每个收件人会在邮件的 “收件人:” 中看到其他收件人的地址一样.举个例子,以下代码会向两个不同的收件人列表发送两封不同的邮件,却复用了同一条连接:返回值是成功发送的消息的数量。

1
2
3
message1 = ('Subject here', 'Here is the message', 'from@example.com', ['first@example.com', 'other@example.com'])
message2 = ('Another Subject', 'Here is another message', 'from@example.com', ['second@test.com'])
send_mass_mail((message1, message2), fail_silently=False)

20.3 显示全部收件人与独立收件人

以下发送了一封邮件给 john@example.comjane@example.com,他们都出现在 “收件人:”:

1
2
3
4
5
6
send_mail(
'Subject',
'Message.',
'from@example.com',
['john@example.com', 'jane@example.com'],
)

以下分别发送了一封邮件给 john@example.comjane@example.com,他们收到了独立的邮件:

1
2
3
4
5
datatuple = (
('Subject', 'Message.', 'from@example.com', ['john@example.com']),
('Subject', 'Message.', 'from@example.com', ['jane@example.com']),
)
send_mass_mail(datatuple)

20.4 防止头注入

Header injection 是一个开发漏洞,攻击者可以利用它在邮件头插入额外信息,以控制脚本生成的邮件中的 “收件人:” 和 “发件人:” 内容。Django 的邮件函数包含了以上所有的反头注入功能,通过在头中禁止新的行。如果 subjectfrom_emailrecipient_list 包含了新行(不管是 Unix,Windows 或 Mac 格式中的哪一种),邮件函数(比如 send_mail() )都会抛出一个 django.core.mail.BadHeaderErrorValueError 的子类),这会中断邮件发送。你需要在将参数传给邮件函数前确保数据的有效性和合法性。

如果邮件的 内容 的开始部分包含了邮件头信息,这些头信息只会作为邮件内容原样打印。以下是一个实例视图,从请求的 POST 数据中获取 subjectmessagefrom_email,并将其发送至 admin@example.com ,成功后再重定向至 “/contact/thanks/“

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.core.mail import BadHeaderError, send_mail
from django.http import HttpResponse, HttpResponseRedirect

def send_email(request):
subject = request.POST.get('subject', '')
message = request.POST.get('message', '')
from_email = request.POST.get('from_email', '')
if subject and message and from_email:
try:
send_mail(subject, message, from_email, ['admin@example.com'])
except BadHeaderError:
return HttpResponse('Invalid header found.')
return HttpResponseRedirect('/contact/thanks/')
else:
# In reality we'd use a form class
# to get proper validation errors.
return HttpResponse('Make sure all fields are entered and valid.')

关于发送邮件的单元测试资料,参见测试文档中 Email services 章节。

(21)分页

21.1 分页基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> from django.core.paginator import Paginator
>>> objects = ['john', 'paul', 'george', 'ringo']
>>> p = Paginator(objects, 2)
>>> p.count
4
>>> p.num_pages
2

>>> page2 = p.page(2)
>>> page2.object_list
['george', 'ringo']

>>> page2.has_next()
False
>>> page2.has_previous()
True
>>> page2.has_other_pages()
True
>>> page2.previous_page_number()
1
>>> page2.start_index() # The 1-based index of the first item on this page
3
>>> page2.end_index() # The 1-based index of the last item on this page
4

21.2 在视图中使用 Paginator

The view function looks like this:

1
2
3
4
5
6
7
8
9
10
from django.core.paginator import Paginator
from django.shortcuts import render

def listing(request):
contact_list = Contacts.objects.all()
paginator = Paginator(contact_list, 25) # Show 25 contacts per page

page = request.GET.get('page')
contacts = paginator.get_page(page)
return render(request, 'list.html', {'contacts': contacts})

In the template list.html, you’ll want to include navigation between pages along with any interesting information from the objects themselves:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{% for contact in contacts %}
{# Each "contact" is a Contact model object. #}
{{ contact.full_name|upper }}<br>
...
{% endfor %}

<div class="pagination">
<span class="step-links">
{% if contacts.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ contacts.previous_page_number }}">previous</a>
{% endif %}

<span class="current">
Page {{ contacts.number }} of {{ contacts.paginator.num_pages }}.
</span>

{% if contacts.has_next %}
<a href="?page={{ contacts.next_page_number }}">next</a>
<a href="?page={{ contacts.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</div>

21.3 相关方法与属性

  • Page.has_next()
  • Page.has_previous()
  • Page.has_other_pages()
  • Page.next_page_number()
  • Page.previous_page_number()
  • Page.start_index()
  • Page.end_index()
  • Page.object_list:此页上的对象列表。
  • Page.number:此页的基于 1 的页码。
  • Page.paginator:关联的 Paginator 对象。

(22)性能与优化

22.1 性能优化介绍

清楚地理解你所说的“绩效”是什么很重要,因为它不仅仅是一个指标。提高速度可能是程序最明显的目标,但有时可能会寻求其他性能改进,例如降低内存消耗或减少对数据库或网络的要求。一个领域的改进通常会提高另一个领域的性能,但并不总是如此;有时甚至会牺牲另一个领域的性能。例如,一个程序速度的提高可能会导致它使用更多的内存。更糟糕的是,如果速度提高太过内存不足,以致于系统开始耗尽内存,那么你所做的弊大于利。还有其他的权衡。你自己的时间是一个宝贵的资源,比CPU时间更宝贵。一些改进可能太难实现,或者可能影响代码的可移植性或可维护性。并非所有的性能改进都值得付出努力。所以,你需要知道你的目标是什么样的性能改进,你也需要知道你有一个很好的理由去瞄准那个方向——而且你需要:

django-debug-toolbar https://github.com/jazzband/django-debug-toolbar/ 是一个非常方便的工具,它可以深入了解您的代码正在做什么以及花费了多少时间。特别是它可以显示您的页面生成的所有SQL查询,以及每个查询所用的时间。第三方面板也可用于工具栏,可以(例如)报告缓存性能和模板呈现时间。

22.2 性能优化的几个方面

  • 内存与索引
  • 数据库查询优化
  • 中间件优化
  • 访问时间
  • 缓存
  • 禁用 DEBUG = False

(23)WSGI部署Django

23.1 使用 WSGI 进行部署

WSGI,PythonWeb服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是Python应用程序或框架和Web服务器之间的一种接口,已经被广泛接受, 它已基本达成它的可移植性方面的目标。WSGI是作为Web服务器与Web应用程序或应用框架之间的一种低级别的接口,以提升可移植Web应用开发的共同点。WSGI是基于现存的[[CGI]]标准而设计的。

  • application对象

用 WSGI 部署的关键是 application callable,应用服务器用它与你的代码交互。 application callable 一般以一个位于 Python 模块中,名为 application 的对象的形式提供,且对服务器可见。startproject 命令创建了文件 <project_name>/wsgi.py,其中包含了 application callable。Django 开发服务器和生产环境的 WSGI 部署都用到了它。

WSGI 服务器从其配置中获取 application callable 的路径。Django 的默认服务器( runserver 命令),从配置项 WSGI_APPLICATION 中获取。默认值是 <project_name>.wsgi.application,指向 <project_name>/wsgi.py 中的 application callable。

  • 配置setting模块

wsgi.py 默认将其设置为 mysite.settingsmysite 即工程名字。这就是 runserver 默认的发现默认配置行为。

注解:由于环境变量是进程级的,所以如果在同一进程运行多个 Django 站点将出错。这在使用 mod_wsgi 时会出现。要避免此问题,为每个站点在后台进程使用 mod_wsgi 的后台模式,或者在 wsgi.py 中通过 os.environ["DJANGO_SETTINGS_MODULE"]= "mysite.settings" 重写来自环境变量的值。

  • 应用WSGI中间件

要应用 WSGI 中间层,你只需简单包裹应用对象。举个例子,你可以在 wsgi.py 末尾添加以下代码:

1
2
from helloworld.wsgi import HelloWorldApplication
application = HelloWorldApplication(application)

如果你想将 Django 应用于一个 WSGI 应用或其它框架联合起来,可以用自定义 WSGI 应用替换 Django 的 WSGI 应用,前者会在稍晚时候将任务委托给 WSGI 应用。

23.2 使用 Apache 和 mod_wsgi 托管 Django

mod_wsgi 是一个 Apache 模块,它可以管理任何 Python WSGI 应用,包括 Django。Django 支持所有支持 mod_wsgi 的 Apache 版本。官方 mod_wsgi 文档 介绍了如何使用 mod_wsgi 的全部细节。你可能更喜欢从 安装和配置文档 开始。

安装并激活 mod_wsgi 后,编辑 Apache 服务器的 httpd.conf 文件,并添加以下内容。若你正在使用的 Apache 版本号早于 2.4,用 Allow from all 替换 Require allgranted,并在其上添加一行 Order deny,allow

1
2
3
4
5
6
7
8
9
WSGIScriptAlias / /path/to/mysite.com/mysite/wsgi.py
WSGIPythonHome /path/to/venv
WSGIPythonPath /path/to/mysite.com

<Directory /path/to/mysite.com/mysite>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
  • WSGIScriptAlias 行的第一项是你所期望的应用所在的基础 URL 路径( / 根 url),第二项是 “WSGI 文件” 的位置——一般位于项目包之内(本例中是 mysite)。这告诉 Apache 用该文件中定义的 WSGI 应用响应指定 URL 下的请求。
  • 如果你在某个 virtualenv 内为应用安装项目的 Python 依赖,将该 virtualenv 的路径添加至 WSGIPythonHome 。参考 mod_wsgi virtualenv 指引 获取更多细节。
  • WSGIPythonPath 行确保你的项目包能从 Python path 导入;换句话说, importmysite 能正常工作。
  • <Directory> 片段仅确保 Apache 能访问 wsgi.py 文件。
  • 下一步,我们需要确认 wsgi.py 文件包含一个 WSGI 应用对象。从 Django 1.4 起, startproject 会自动创建;换而言之,你无需手动创建。查阅 WSGI 概述文档 获取你需要配置的默认内容,以及其它可配置项。

注意1:

如果多个 Django 站点运行在同一 mod_wsgi 进程,它们会共用最先启动的站点配置。能通过以下修改改变行为:

1
2
3
> os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings")
>
>

>

wsgi.py 中也这么改:

1
2
3
> os.environ["DJANGO_SETTINGS_MODULE"] = "{{ project_name }}.settings"
>
>

>

或通过 使用 mod_wsgi 的后台模式 确保每个站点都运行于独立的后台进程。

注意2:

为文件上传修复 UnicodeEncodeError

上传名称包含非 ASCII 字符的文件时,若抛出 UnicodeEncodeError,确认 Apache 是否被正确配置,能接受非 ASCII 文件名:

1
2
3
4
> export LANG='en_US.UTF-8'
> export LC_ALL='en_US.UTF-8'
>
>

>

常见的配置文件路径是 /etc/apache2/envvars

参考 Unicode 参考指引的 Files 章节获取细节信息。

  • 使用 mod_wsgi 后台模式

为了创建必要的后台进程组并在其中运行 Django 实例,你需要添加合适的 WSGIDaemonProcessWSGIProcessGroup 指令。上述配置在你使用后台模式时需要点小修改,即你不能使用 WSGIPythonPath;作为替换,你要在 WSGIDaemonProcess 中添加 python-path 选项,例如:

1
2
WSGIDaemonProcess example.com python-home=/path/to/venv python-path=/path/to/mysite.com
WSGIProcessGroup example.com

如果你想在子目录中开放你的项目(本例中 https://example.com/mysite),你可在上述配置中添加 WSGIScriptAlias

1
WSGIScriptAlias /mysite /path/to/mysite.com/mysite/wsgi.py process-group=example.com

参考官方 mod_wsgi 文档获取 配置后台模式的细节

23.3 Apache 利用 Django 的用户数据库进行验证

使用 Apache 时,保持多个身份认证数据同步是一个常见的问题,你可以让 Apache 直接使用 Django 的 验证系统。这要求 Apache 版本 >= 2.2,且 mod_wsgi >= 2.0。例如这样:

  • 仅为已授权的用户直接从 Apache 提供 static/media 文件。
  • 仅为有特定权限的 Django 用户提供 Subversion 仓库访问。
  • 允许某些用户连接到 mod_dav 创建的 WebDAV 共享。

确保你已按照 Apache 配合 mod_wsgi 文档正确安装并激活了 mod_wsgi。然后,编辑 Apache 配置,添加只允许授权用户查看的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
WSGIScriptAlias / /path/to/mysite.com/mysite/wsgi.py
WSGIPythonPath /path/to/mysite.com

WSGIProcessGroup %{GLOBAL}
WSGIApplicationGroup %{GLOBAL}

<Location "/secret">
AuthType Basic
AuthName "Top Secret"
Require valid-user
AuthBasicProvider wsgi
WSGIAuthUserScript /path/to/mysite.com/mysite/wsgi.py
</Location>

WSGIAuthUserScript 指令告诉 mod_wsgi 在指定 wsgi 脚本中执行 check_password 函数,并传递从提示符获取的用户名和密码。在本例中, WSGIAuthUserScriptWSGIScriptAlias 一样,后者 由 django-admin startproject 创建,定义了应用。

最后,编辑 WSGI 脚本 mysite.wsgi,通过导入 check_password 函数,将 Apache 的认证授权机制接续在你站点的授权机制之后:

1
2
3
4
5
6
7
8
import os

os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings'

from django.contrib.auth.handlers.modwsgi import check_password

from django.core.handlers.wsgi import WSGIHandler
application = WSGIHandler()

/secret/ 开头的请求现在会要求用户认证。

mod_wsgi 可达性控制机制文档 提供了其它授权机制和方法的更多细节和信息。

利用 mod_wsgi 和 Django 用户组(groups)进行授权

mod_wsgi 也提供了将组成员限制至特定路径的功能。

在本例中,Apache 配置应该看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WSGIScriptAlias / /path/to/mysite.com/mysite/wsgi.py

WSGIProcessGroup %{GLOBAL}
WSGIApplicationGroup %{GLOBAL}

<Location "/secret">
AuthType Basic
AuthName "Top Secret"
AuthBasicProvider wsgi
WSGIAuthUserScript /path/to/mysite.com/mysite/wsgi.py
WSGIAuthGroupScript /path/to/mysite.com/mysite/wsgi.py
Require group secret-agents
Require valid-user
</Location>

要支持 WSGIAuthGroupScript 指令,同样的 WSGI 脚本 mysite.wsgi 必须也导入 groups_for_user 函数,函数会返回用户所属用户组的列表。

1
from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user

/secret 的请求现在也会要求用户是 “secret-agents” 用户组的成员。

23.4 如何使用 Gunicorn 托管 Django

Gunicorn (‘Green Unicorn’) 是一个 UNIX 下的纯 Python WSGI 服务器。它没有其它依赖,容易安装和使用。安装 gunicorn 非常简单,只要执行 pip install gunicorn 即可。

安装 Gunicorn 之后,可以使用 gunicorn 命令启动 Gunicorn 服务进程。最简模式下,只需要把包含了 WSGI 应用对象的 application 模块位置告诉 gunicorn,就可以启动了。因此对于典型的 Django 项目,像这样来调用 gunicorn:

1
gunicorn myproject.wsgi

这样会创建一个进程,包含了一个监听在 127.0.0.1:8000 的线程。前提是你的项目在 Python path 中,要满足这个条件,最简单的方法是在 manage.py 文件所在的目录中运行这条命令。

23.5 如何用 uWSGI 托管 Django

uWSGI 是一个快速的,自我驱动的,对开发者和系统管理员友好的应用容器服务器,完全由 C 编写。uWSGI 百科介绍了几种 安装流程。Pip (Python 包管理器)能让你仅用一行代码就安装任意版本的 uWSGI。例子:

1
2
3
4
5
# Install current stable version.
$ pip install uwsgi

# Or install LTS (long term support).
$ pip install https://projects.unbit.it/downloads/uwsgi-lts.tar.gz

假设你有个叫做 mysite 的顶级项目包,期中包含一个模板 mysite/wsgi.py,模块包含一个 WSGI application 对象。如果你使用的是较新的 Django,这就是你运行 django-admin startproject mysite (使用你的项目名替换 mysite)后得到的目录结构。若该文件不存在,你需要创建它。参考文档 如何使用 WSGI 进行部署 看看你需要配置的默认内容,以及你还能添加什么。

Django 指定的参数如下:

  • chdir:需要包含于 Python 的导入路径的目录的路径——例如,包含 mysite 包的目录。
  • module:要使用的 WSGI 模块——可能是 startproject 创建的 mysite.wsgi的模块。
  • env:至少要包括 DJANGO_SETTINGS_MODULE
  • home: 可选的路径,指向你工程的 virtualenv。

示例 ini 配置文件:

1
2
3
4
5
6
7
8
[uwsgi]
chdir=/path/to/your/project
module=mysite.wsgi:application
master=True
pidfile=/tmp/project-master.pid
vacuum=True
max-requests=5000
daemonize=/var/log/uwsgi/yourproject.log

示例 ini 配置文件语法:

1
uwsgi --ini uwsgi.ini

(24)Django FAQ0

  • 发音:Django 发音为 JANG,使用 FANG 来押韵,字母 “D “是不发声的.

    我们也记录了一段 发音的音频片段.

  • 稳定性:相当稳定。使用 Django 搭建的网站能承受每秒 50000 次点击的流量峰值。

  • 扩展性:可以在任何级别添加硬件——数据库服务器,缓存服务器或 Web /应用程序服务器。

  • MVC 框架:在我们对 MVC 的解释中,“视图”描述了呈现给用户的数据。数据看起来怎么样并不重要,重要的是哪些数据被呈现。 Django “MTV “ 框架—即 “模型(Model) “、 “模板(Template)” 和 “视图(View). “视图(view)”是 Python 中针对一个特定 URL 的回调函数,此回调函数描述了需要展示的数据。展示效果就是模板。在 Django 里面,一个视图(view)描述了哪些数据会被展示。那控制器(Controller)在什么位置?在 Django 中,会根据 Django 的 URL 配置,将请求分发到适当的视图(view)。

  • 数据库:官方文档推荐PostgreSQL

  • 版本选择:Django2.1,2.2支持python3.5+。Django 的第三方插件可以自由设置他们的版本要求。生产中使用 Django,你应该使用稳定版本。

  • 使用图片和文件字段。在模型中使用 FileFieldImageField ,你还需要完成如下步骤:

    1. 在你的 setting 文件中,你需要定义:setting: MEDIA_ROOT 作为 Django 存储上传文件目录的完整路径。(为了提高性能,这些文件不会储存在数据库中)定义: setting: MEDIA_URL 作为该目录的基本公共 URL, 确保该目录能够被 Web 服务器的账户写入。
    2. 在你的模型中添加 FileField 或者 ImageField ,可以通过定义:attr:~django.db.models.FileField.upload_to 在 MEDIA_ROOT 中明确一个子目录用来上传文件。
    3. 所有将被储存在数据库中的文件路径相同(相对于:setting: MEDIA_ROOT)。你很想用由 Django 提供的:attr:~django.db.models.fields.files.FieldFile.url,比如, 如果:class:~django.db.models.ImageField 被叫做mug_shot, 你就可以得到` 图片模板的绝对路径。

技术交流共享QQ群

机器学习和自然语言QQ群:436303759

机器学习和自然语言(QQ群号:436303759)是一个研究深度学习、机器学习、自然语言处理、数据挖掘、图像处理、目标检测、数据科学等AI相关领域的技术群。其宗旨是纯粹的AI技术圈子、绿色的交流环境。本群禁止有违背法律法规和道德的言谈举止。群成员备注格式:城市-自命名。微信订阅号:datathinks

白宁超 wechat
扫一扫关注微信公众号,机器学习和自然语言处理,订阅号datathinks!