Django进阶教程

Django进阶教程

Queryset特性及高级查询技巧

什么是QuerySet

QuerySet是Django提供的强大的数据库接口(API)。正是因为通过它,我们可以使用filter, exclude, get等方法进行数据库查询,而不需要使用原始的SQL语言与数据库进行交互。从数据库中查询出来的结果一般是一个集合,这个集合叫就做 queryset。

如果你还不知道如何使用Django提供的数据接口(API)对数据库进行最基本的增删改查,请先阅读下面这篇文章:

  • https://pythondjango.cn/django/basics/6-models-queryset-API/

Django的QuerySet是惰性的

Django的QuerySet是惰性的, 那么它到底是什么意思呢?

下例中article_list试图从数据库查询一个标题含有django的全部文章列表。

article_list = Article.objects.filter(title__contains="django")

但是当我们定义article_list的时候,Django的数据接口QuerySet并没有对数据库进行任何查询。无论你加多少过滤条件,Django都不会对数据库进行查询。只有当你需要对article_list做进一步运算时(比如打印出查询结果,判断是否存在,统计结果长度),Django才会真正执行对数据库的查询(见下例1)。这个过程被称为queryset的执行(evaluation)。Django这样设计的本意是尽量减少对数据库的无效操作,比如查询了结果而不用是对计算资源的很大浪费。

# example 1
for article in article_list:
    print(article.title)
    

Django的QuerySet自带缓存(Cache)

在例1中,当你遍历article_list时,所有匹配的记录会从数据库获取。这些结果会载入内存并保存在queryset内置的cache中。这样如果你再次遍历或读取这个article_list时,Django就不需要重复查询了,这样也可以减少对数据库的查询。

下例中例2比例3要好,因为在你打印文章标题后,Django不仅执行了查询,还把查询到的article_list放在了缓存里, 因此这个article_list是可以复用的。例3就不行了。

# Example 2: Good
article_list = Article.objects.filter(title__contains="django")
for article in article_list:
    print(article.title)

# Example 3: Bad
for article in Article.objects.filter(title__contains="django"):
    print(article.title)

用if也会导致queryset的执行

不知道你注意到上述例2中有个问题没有?万一article_list是个空数据集呢? 虽然for....in...用到空集合上也不会出现raise什么错误,但专业优秀的我们怎么能允许这样的低级事情发生呢?最好的做法就是在loop前加个if判断(例4)。因为django会对执行过的queryset进行缓存(if也会导致queryset执行, 缓存article_list),所以我们在遍历article_list时不用担心Django会对数据库进行二次查询。

# Example 4: Good
article_list = Article.objects.filter(title__contains="django")
if article_list:
    for article in article_list:
        print(article.title)
else:
    print("No records")

但有时我们只希望了解查询的结果是否存在,而不需要使用整个数据集,这时if触发整个queryset的缓存变成了一件坏事情。哎,程序员要担心的事情着不少。这时你可以用exists()方法。与if判断不同,exists只会检查查询结果是否存在,返回True或False,而不会缓存article_list(见例5)。

# Example 5: Good
article_list = Article.objects.filter(title__contains="django")
if article_list.exists():
    print("Records found.")
else:
    print("No records")

注意: 判断查询结果是否存在到底用if还是exists取决于你是否希望缓存查询数据集复用,如果是用if,反之用exists

统计查询结果数量优选count方法

len()count()均能统计查询结果的数量。一般来说count更快,因为它是从数据库层面直接获取查询结果的数量,而不是返回整个数据集,而len会导致queryset的执行,需要将整个queryset载入内存后才能统计其长度。但事情也没有绝对,如果数据集queryset已经在缓存里了,使用len更快,因为它不需要跟数据库再次打交道。

下面三个例子中,只有例7最差,尽量不要用。

# Example 6: Good
count = Article.objects.filter(title__contains="django").count()

# Example 7:Bad
count = Article.objects.filter(title__contains="django").len()

# Example 8: Good
article_list = Article.objects.filter(title__contains="django")
if article_list:
    print("{} records found.".format(article_list.len()))

当queryset非常大时,数据请按需去取

当查询到的queryset的非常大时,会大量占用内存(缓存)。我们可以使用valuesvalue_list方法按需提取数据。比如例1中我们只需要打印文章标题,这时我们完全没有必要把每篇文章对象的全部信息都提取出来载入到内存中。我们可以做如下改进, 查询数据库时只提取title出来(例9)。

# Example 9: Good
article_list = Article.objects.filter(title__contains="django").values('title')
if article_list:
    print(article.title)

article_list = Article.objects.filter(title__contains="django").values_list('id', 'title')
if article_list:
    print(article.title)

注意: values和values_list分别以字典和元组形式返回查询结果,不再是queryset类型数据。

我们还可以使用deferonly这两个查询方法来实现按需查询数据。除此以外,我们还可以使用iterator()方法可以优化程序对内存的使用,其工作原理是不对queryset进行缓存,而是采用迭代方法逐一返回查询结果,但这有时会增加数据库的访问次数,新手一般也驾驭不了。我这里就不细讲了。

更新数据库部分字段请用update方法

如果需要对数据库中的某条已有数据或某些字段进行更新,更好的方式是用update,而不是save方法。我们现在可以对比下面两个案例。例10中需要把整个Article对象的数据(标题,正文……)先提取出来,缓存到内存中,变更信息后再写入数据库。而例11直接对标题做了更新,不需要把整个文章对象的数据载入内存,显然更高效。尽管单篇文章占用内存不多,但是万一用户非常多呢,那么占用的内存加起来也是很恐怖的。

# Example 10: Bad
article = Article.objects.get(id=10)
Article.title = "Django"
article.save()

# Example 11: Good
Article.objects.filter(id=10).update(title='Django')

update方法还会返回已更新条目的数量,这点也非常有用。当然事情也没有绝对,save方法对于单个模型的更新还是很有优势的,比如save(commit=False), article.author = request.user等等事情update都做不来。

批量创建或更新数据请用bulk_create或bulk_update

在Django中向数据库中插入或更新多条数据时,每使用save或create方法保存一条就会执行一次SQL。而Django提供的bulk_createbulk_update方法可以一次SQL添加或更新多条数据,效率要高很多,如下所示:

# 内存生成多个对象实例
articles  = [Article(title="title1", body="body1"), Article(title="title2", body="body2"), Article(title="title3", body="body3")]

# 执行一次SQL插入数据
Article.objects.bulk_create(articles)

专业地使用explain方法

Django 2.1中QuerySet新增了explain方法,可以统计一个查询所消耗的执行时间。这可以帮助程序员更好地优化查询结果。

print(Blog.objects.filter(title='My Blog').explain(verbose=True))

# outputt
Seq Scan on public.blog  (cost=0.00..35.50 rows=10 width=12) (actual time=0.004..0.004 rows=10 loops=1)
  Output: id, title
  Filter: (blog.title = 'My Blog'::bpchar)
Planning time: 0.064 ms
Execution time: 0.058 ms

小结

Django QuerySet的惰性和缓存特性对于减少数据库的访问次数非常有用。你需要根据不同应用场景选择合适的方法(比如exists, count, update, values) 来减少数据库的访问,减少查询结果占用的内存空间从而提升网站的性能。希望本文总结的一些高效使用queryset技巧对你学习Django和Web开发有所帮助。

Django多种缓存配置方式

  1. 什么是缓存Cache?
  2. 为什么要使用缓存Cache?
  3. 缓存Cache的应用场景
  4. Django缓存设置
    1. Memcached缓存
    2. Redis缓存
    3. 数据库缓存
    4. 文件系统缓存
    5. 本地内存缓存
    6. Dummy缓存
  5. 测试缓存是否设置成功
  6. Django项目中使用缓存
    1. 在视图View中使用
    2. 路由URLConf中使用
    3. 模板中使用缓存
  7. 自定义缓存和清除缓存
  8. 小结

什么是缓存Cache?

缓存是一类可以更快的读取数据的介质统称,也指其它可以加快数据读取的存储方式。一般用来存储临时数据,常用介质的是读取速度很快的内存。一般来说从数据库多次把所需要的数据提取出来,要比从内存或者硬盘等一次读出来付出的成本大很多。对于中大型网站而言,使用缓存减少对数据库的访问次数是提升网站性能的关键之一。

为什么要使用缓存Cache?

当用户请求到达Django的视图后,视图会先从数据库读取数据传递给模板进行渲染,返回给用户看到的网页。如果用户每次请求都从数据库读取数据并渲染,将极大降低性能,不仅服务器压力大,而且客户端也无法即时获得响应。如果能将数据库中读取的数据动态生成的网页放到速度更快的缓存中,每次有请求过来,先检查缓存中是否有对应的资源,如果有,直接从缓存中取出来返回响应,节省读取数据和渲染的时间,不仅能大大提高系统性能,还能提高用户体验。

我们来看一个实际的博客例子。每当我们访问首页/index/时,下面视图就会从数据库中读取文章列表,并与模板结合动态地生成网页。大多数情况下,我们的博客不会更新得那么频繁,所以文章列表和首页都是不变的。这样用户在一定时间内多次访问首页时每次都从数据库重新读取同样的数据再进行渲染是一种很大的浪费。

from django.shortcuts import render
from .models import Article

def index(request):
    # 读取数据库等并渲染到网页
    article_list = Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})

服务器端使用缓存Cache就可以帮我们解决这个问题。当第一个用户首次访问博客首页时,我们将从数据库中读取的数据或动态生成的网页存储到缓存里(常用的是内存,这取决于你的设置)。当这个用户或其它更多用户在一定时间内多次请求访问首页时, Django先检查缓存里用户请求的数据或网页是否已经存在,如果存在,直接从缓存中读取相关内容,展示给用户。如果数据不存在或缓存已过期,则重新读取数据建立缓存。这就是cache_page这个装饰器的作用,如下所示:

from django.shortcuts import render
from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # 这里指缓存 15 分钟
def index(request):
    article_list = Article.objects.all()
    return render(request, 'index.html', {'article_list': article_list})

缓存Cache的应用场景

缓存主要适用于对页面实时性要求不高的页面。存放在缓存的数据,通常是频繁访问而又不会经常修改的数据。我们来举几个应用例子:

  • 个人博客:假设用户平均一天更新一篇文章,那么可以设置1天的全站缓存,一天后会刷新。
  • 购物网站:商品的描述信息几乎不会变化,而商品的购买数量需要根据用户情况实时更新。我们可以只选择缓存商品描述信息。
  • 缓存网页片段:比如缓存网页导航菜单和脚部(Footer)。
  • 热点信息: 比如短时间内新闻点击排行,这些热点数据不需要存入到关系型数据库里,放到缓存里即可。

Django缓存设置

Django中提供了多种缓存方式,如果要使用缓存,需要先在settings.py中进行配置,然后应用。根据缓存介质的不同,你需要设置不同的缓存后台Backend。在生产环境中最常用的缓存是Memcached和Redis。在开发环境中可以使用本地内存缓存进行测试。

Memcached缓存

Memcached是一个高性能的分布式内存对象缓存系统,是Django原生支持的最快最有效的缓存系统。Memcached的优点是速度快,属于分布式缓存,支持同时在多台服务器上运行 (Django会把它们当成一个大缓存),缺点是不支持数据持久化,服务器重启后缓存数据就没了。

第一步:安装Memcached

  • windows系统:官网下载,解压安装即可。
  • Linux系统:Ubuntu系统需要使用sudo apt-get install libevent ibevent-dev安装Memcached依赖环境,再使用sudo apt-get install memcached安装memcached。

如何安装参考菜鸟网教程:https://www.runoob.com/memcached/memcached-tutorial.html

**第二步:启动Memcached **

# Linux系统-前台启动
/usr/local/memcached/bin/memcached -p 11211 -m 64m -vv
# Linux系统-作为后台服务启动
/usr/local/memcached/bin/memcached -p 11211 -m 64m -d

第三步:pip安装python-memcached

Python操作memcached数据库需要安装python-memcachedpylibmc, 推荐前者。

pip install pyhon-memcached

第四步:将memcached设为Django缓存后台

# 本地缓存,使用localhost
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

# 使用unix soket通信
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': 'unix:/tmp/memcached.sock',
    }
}   

# 分布式缓存,多台服务器,支持配置权重。
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': [
            '172.19.26.240:11211',
            '172.19.26.242:11211',
        ]
        # 我们也可以给缓存机器加权重,权重高的承担更多的请求,如下:
        'LOCATION': [
            ('172.19.26.240:11211',5),
            ('172.19.26.242:11211',1),
        ]
    }
 }

Redis缓存

Redis 是当今速度最快的内存型非关系型(NoSQL)型数据库。Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等多种数据结构的存储。与memcached相比,Redis不仅支持支持缓存数据在硬盘上的持久化,还支持master-slave模式的数据备份,有明显的优点。

第一步:安装Redis

  • windows系统:官网下载,解压安装即可, 记得勾上加入环境变量。
  • Linux系统:Ubuntu系统可使用sudo apt-get install redis-server安装。

第二步:启动Redis服务

# Windows系统:cmd进入redis安装目录,启动redis服务
redis-server.exe redis.windows.conf

# Linux系统:进入redis安装目录启动redis服务
redis-server /etc/redis/redis.conf 

# 打开redis交互命令行,用于测试(可选)
redis-cli.exe -h 127.0.0.1 -p 6379 # windows系统下另打开一个窗口
redis-cli # linux系统

注意:默认情况下,访问Redis服务器是不需要密码的,为了让其他服务器使用同增加安全性我们建议设置Redis服务器的访问密码。

由于redis默认绑定本机的,所以第一步取消该设置:

#编辑配置文件
sudo vim /etc/redis/redis.conf

用vim打开该配置文件后,注释掉下面这行:

# bind 127.0.0.1

然后设置登录密码。由于配置文件较长,命令模式下输入/requirepass foobared快速搜索该配置项:

#找到下面这一行并去除注释,未修改之前:
#requirepass foobared

#修改成:
requirepass your_pwd #设置新的密码

修改后使用redis-server restart重启服务器使配置生效。以后从其它服务器访问redis时携带你设置的密码即可:

redis-cli -a your_pwd -h hostip

第三步:pip安装django-redis

Redis安装好并且启动后,你还需要通过pip安装django-redis才能在Django中操作redis数据库。

pip install django-redis

第四步:将Redis设为Django缓存后台

CACHES = {
    
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://your_host_ip:6379', # redis所在服务器或容器ip地址
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
             "PASSWORD": "your_pwd", # 你设置的密码
        },
    },
}

你还可以在settings.py设置缓存默认过期时间(非必须)。

REDIS_TIMEOUT=24*60*60
CUBES_REDIS_TIMEOUT=60*30
NEVER_REDIS_TIMEOUT=365*24*60*60

数据库缓存

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
        'LOCATION': 'my_cache_table',
    }

使用数据库缓存前需要先使用如下命令创建缓存数据表:

python manage.py createcachetable

文件系统缓存

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': '/var/tmp/django_cache',#这个是文件夹的路径
        #'LOCATION': 'c:\foo\bar',# windows下的示例
    }
}

本地内存缓存

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake' # 名字随便定
    }
}

Dummy缓存

不做任何实际缓存,仅用于测试目的。

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
    }
}

测试缓存是否设置成功

在你修改完settings.py中关于缓存的配置后,你一定想知道Django缓存是否设置成功。你可以输入下面命令打开Python的命令交互窗口:

python manage.py shell

然后逐条输入以下命令进行测试。如果无任何报错,说明你缓存设置成功。

from django.core.cache import cache  #引入缓存模块

cache.set('k1', '555', 60*1)   #写入key为k1,值为555的缓存,有效期1分钟
cache.has_key('k1')#判断key为k1是否存在
cache.get('k1')   #获取key为k1的缓存结果

Django项目中使用缓存

当你做好有关缓存(Cache)的设置后,在Django项目中你可以有四种方式使用Cache。

  • 全站缓存
  • 在视图View中使用
  • 在路由URLConf中使用
  • 在模板中使用

### 全站缓存

全站缓存(per-site)是依赖中间件实现的,也是Django项目中使用缓存最简单的方式。这种缓存方式仅适用于静态网站或动态内容很少的网站。

# 缓存中间件,添加顺序很重要
MIDDLEWARE = [
    'django.middleware.cache.UpdateCacheMiddleware',     # 新增
    'django.middleware.common.CommonMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',  # 新增
]

# 其它设置
CACHE_MIDDLEWARE_ALIAS = 'default'  # 缓存别名
CACHE_MIDDLEWARE_SECONDS = '600'    # 缓存时间
CACHE_MIDDLEWARE_KEY_PREFIX = ''    # 缓存别名前缀

在视图View中使用

此种缓存方式依赖@cache_page这个装饰器,仅适合内容不怎么变化的单个视图页面。

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request):
    ...

路由URLConf中使用

同样@cache_page这个装饰器,只不过在urls.py中使用。

from django.views.decorators.cache import cache_page

urlpatterns = [
    path('articles/<int:id>/', cache_page(60 * 15)(my_view)),
]

模板中使用缓存

@cache_page缓存整个页面不同,模板缓存的颗粒度更细,可以用来缓存内容不怎么变化的 HTML 片段。具体的使用方式如下,首先加载 cache 过滤器,然后使用模板标签语法把需要缓存的片段包围起来即可。

{% load cache %}
{% cache 500 sidebar request.user.username %}
    .. sidebar for logged in user ..
{% endcache %}

自定义缓存和清除缓存

实际缓存应用中,Django提供的缓存中间件、装饰器或者模板cache标签的颗粒度还是不够细,有时候你需要在视图中自定义数据缓存,如下所示:

from django.core.cache import cache

def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    objects = cache.get('cached_objects')

    if objects is None:
        objects = MyModel.objects.all()
        cache.set('cached_objects', objects)

    context['objects'] = objects

    return context

当你的模型有所变化(比如删除或更新)时,你还需及时地清除老的缓存,这个可以通过Django的信号机制实现。

from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver

@receiver(post_delete, sender=MyModel)
def cache_post_delete_handler(sender, **kwargs):
     cache.delete('cached_objects')

@receiver(post_save, sender=MyModel)
def cache_post_save_handler(sender, **kwargs):
    cache.delete('cached_objects')

小结

本章总结了Django项目中如何设置缓存后台以及如何在视图和模板中使用缓存,提高网站性能和用户体验。

Django权限详解

  1. 什么是权限?
  2. Django Admin中的权限分配
  3. 查看用户的权限
  4. 新增自定义权限
    1. 方法1. 在Model的meta属性中添加权限
    2. 方法2. 使用ContentType程序化创建权限
    3. 方法1. 使用user_permissions.add方法增加权限
    4. 方法2. 通过用户组(group)给用户增加权限
    5. 方法3. 通过remove或如clear方法移除权限
  5. 注意权限的缓存机制
  6. 用户权限的验证
    1. 视图中验证
    2. 模板中验证
    3. 用户组(Group)
  7. Django自带权限机制的不足
  8. Django-guardian的使用
    1. 安装与配置
    2. 权限分配
      1. 为用户分配权限
      2. 为用户组分配权限
      3. 通过信号分配权限
    3. 删除权限
    4. 权限验证
      1. has_perm方法
      2. get_perms方法
      3. get_objects_for_user方法
      4. ObjectPermissionChecker
      5. 使用装饰器
      6. 模板中校验
    5. 与Django-admin的集成
    6. 使用定制User模型
  9. 小结

什么是权限?

权限是能够约束用户行为和控制页面显示内容的一种机制。一个完整的权限应该包含3个要素: 用户,对象和权限,即什么用户对什么对象有什么样的权限。

假设我们有一个应用叫blog,其包含一个叫Article(文章)的模型。那么一个超级用户一般会有如下4种权限,而一个普通用户可能只有1种或某几种权限,比如只能查看文章,或者能查看和创建文章但是不能修改和删除。

  • 查看文章(view)
  • 创建文章(add)
  • 更改文章(change)
  • 删除文章(delete)

我们在Django的管理后台(admin)中是可以很轻易地给用户分配权限的。

Django Admin中的权限分配

Django中的用户权限分配,主要通过Django自带的Admin界面进行维护的。当你编辑某个user信息时, 你可以很轻易地在User permissions栏为其设置对某些模型查看, 增加、更改和删除的权限(如下图所示)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gP3gBofx-1655293377679)(https://pythondjango.cn/django/advanced/8-permissions.assets/image-20210419145028877.png)]

Django的权限permission本质是djang.contrib.auth中的一个模型, 其与User的user_permissions字段是多对多的关系。当我们在INSTALLED_APP里添加好auth应用之后,Django就会为每一个你安装的app中的模型(Model)自动创建4个可选的权限:view, add,change和delete。(注: Django 2.0前没有view权限)。随后你可以通过admin将这些权限分配给不同用户。

查看用户的权限

权限名一般有app名(app_label),权限动作和模型名组成。以blog应用为例,Django为Article模型自动创建的4个可选权限名分别为:

  • 查看文章(view): blog.view_article
  • 创建文章(add): blog.add_article
  • 更改文章(change): blog.change_article
  • 删除文章(delete): blog.delete_article

在前例中,我们已经通过Admin给用户A(user_A)分配了创建文章和修改文章的权限。我们现在可以使用user.has_perm()方法来判断用户是否已经拥有相应权限。下例中应该返回True。

user_A.has_perm('blog.add_article')
user_A.has_perm('blog.change_article')

如果我们要查看某个用户所在用户组的权限或某个用户的所有权限(包括从用户组获得的权限),我们可以使用get_group_permissions()get_all_permissions()方法。

user_A.get_group_permissions()
user_A.get_all_permissions()

新增自定义权限

有时django创建的4种可选权限满足不了我们的要求,这时我们需要自定义权限。实现方法主要有两种。下面我们将分别使用2种方法给Article模型新增了两个权限,一个是publish_article, 一个是comment_article

方法1. 在Model的meta属性中添加权限

class Article(models.Model):
    ...
    class Meta:
        permissions = (
            ("publish_article", "Can publish article"),
            ("comment_article", "Can comment article"),
        )

方法2. 使用ContentType程序化创建权限

from blog.models import Article
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType

content_type = ContentType.objects.get_for_model(article)
permission1 = Permission.objects.create(
    codename='publish_article',
    name='Can publish articles',
    content_type=content_type,
)

permission2 = Permission.objects.create(
    codename='comment_article',
    name='Can comment articles',
    content_type=content_type,
)

当你使用python manage.py migrate命令后,你会发现Django admin的user permissions栏又多了两个可选权限。

## 手动分配权限

如果你不希望总是通过admin来给用户设置权限,你还可以通过视图函数手动给用户分配权限。这里也有两种实现方法。

方法1. 使用user_permissions.add方法增加权限

myuser.user_permissions.add(permission1, permission2, ...)

方法2. 通过用户组(group)给用户增加权限

mygroup.permissions.add(permission1, permission2, ...)

方法3. 通过remove或如clear方法移除权限

如果你希望在代码中移除一个用户的权限,你可以使用removeclear方法。

myuser.user_permissions.remove(permission1, permission2, ...)
myuser.user_permissions.clear()

注意权限的缓存机制

Django会缓存每个用户对象,包括其权限user_permissions。当你在代码中手动改变一个用户的权限后,你必须重新获取该用户对象,才能获取最新的权限。比如下例在代码中给用户手动增加了change_blogpost的权限,如果不重新载入用户,那么将显示用户还是没有change_blogpost的权限。

from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404

from myapp.models import BlogPost

def user_gains_perms(request, user_id):
    user = get_object_or_404(User, pk=user_id)
    # any permission check will cache the current set of permissions
    user.has_perm('myapp.change_blogpost') 
    
content_type = ContentType.objects.get_for_model(BlogPost)
permission = Permission.objects.get(
    codename='change_blogpost',
    content_type=content_type,
)
user.user_permissions.add(permission)
 
# Checking the cached permission set
user.has_perm('myapp.change_blogpost')  # False
 
# Request new instance of User
# Be aware that user.refresh_from_db() won't clear the cache.
user = get_object_or_404(User, pk=user_id)
 
# Permission cache is repopulated from the database
user.has_perm('myapp.change_blogpost')  # True

用户权限的验证

我们前面讲解了用户权限的创建和设置,现在我们将进入关键一环,用户权限的验证。我们在分配好权限后,我们还需要在视图views.py和模板里验证用户是否具有相应的权限,否则前面设置的权限形同虚设。这就是为什么我们前面很多django实战案例里,没有给用户分配某个模型的add和change权限,用户还是还能创建和编辑对象的原因。

视图中验证

在视图中你当然可以使用user.has_perm方法对一个用户的权限进行直接验证。当然一个更好的方法是使用@permission_required这个装饰器。

permission_required(perm, login_url=None, raise_exception=False)

你如果指定了login_url, 用户会被要求先登录。如果你设置了raise_exception=True, 会直接返回403无权限的错误,而不会跳转到登录页面。使用方法如下所示:

from django.contrib.auth.decorators import permission_required

@permission_required('polls.can_vote')
def my_view(request):
    ...

如果你使用基于类的视图(Class Based View), 而不是函数视图,你需要混入PermissionRequiredMixin这个类或使用method_decorator装饰器,如下所示:

from django.contrib.auth.mixins import PermissionRequiredMixin

class MyView(PermissionRequiredMixin, View):
    permission_required = 'polls.can_vote'
    # Or multiple of permissions:
    permission_required = ('polls.can_open', 'polls.can_edit')
    
from django.utils.decorators import method_decorator
from django.core.urlresolvers import reverse_lazy
from django.contrib.auth.decorators import user_passes_test

@method_decorator(user_passes_test(lambda u: Group.objects.get(name='admin') in u.groups.all()),name='dispatch')
class ItemDelete(DeleteView):
    model = Item
    success_url = reverse_lazy('items:index')

模板中验证

在模板中验证用户权限主要需要学会使用perms这个全局变量。perms对当前用户的user.has_module_permsuser.has_perm方法进行了封装。当我们需要判断当前用户是否拥有blog应用下的所有权限时,我们可以使用:

{{ perms.blog }}

我们如果判断当前用户是否拥有blog应用下发表文章讨论的权限,则使用:

{{ perms.blog.comment_article }}

这样结合template的if标签,我们可以通过判断当前用户所具有的权限,显示不同的内容了.

{% if perms.blog %}
    <p>You have permission to do something in this blog app.</p>
	{% if perms.blog.add_article %}
       <p>You can add articles.</p>
    {% endif %}

    {% if perms.blog.comment_article %}
        <p>You can comment articles!</p>
    {% endif %}
{% else %}
    <p>You don't have permission to do anything in the blog app.</p>
{% endif %}

用户组(Group)

用户组(Group)和User模型是多对多的关系。其作用在权限控制时可以批量对用户的权限进行管理和分配,而不用一个一个用户分配,节省工作量。将一个用户加入到一个Group中后,该用户就拥有了该Group所分配的所有权限。例如,如果一个用户组editors有权限change_article, 那么所有属于editors组的用户都会有这个权限。

将用户添加到用户组或者给用户组(group)添加权限,一般建议直接通过django admin进行。如果你希望手动给group添加或删除权限,你可以使用如下方法:

mygroup.permissions = [permission_list]
mygroup.permissions.add(permission1, permission2, ...)
mygroup.permissions.remove(permission1, permission2, ...)
mygroup.permissions.clear()

如果你要将某个用户移除某个用户组,可以使用如下方法:

myuser.groups.remove(group1, group2, ...) 
myuser.groups.clear()

Django自带权限机制的不足

Django自带的权限机制是针对模型的,这就意味着一个用户如果对Article模型有change的权限,那么该用户获得对所有文章对象进行修改的权限。如果我们希望实现对单个文章对象的权限管理,我们需要借助于第三方库比如django-guardian。具体扩展方式见下文。

Django-guardian的使用

安装与配置

pip install django-guardian

安装完成后,我们可以将django-guardian加入到我们的项目。首先在settings里将guardian加入到INSTALLED_APPS

INSTALLED_APPS = ( 
    # ... 
    'guardian',
)

然后加入到身份验证后端AUTHENTICATION_BACKENDS

AUTHENTICATION_BACKENDS = ( 
    'django.contrib.auth.backends.ModelBackend', 
    'guardian.backends.ObjectPermissionBackend', # 添加
) 

注意:一旦我们将django-guardian配置进我们的项目,当我们调用migrate命令将会创建一个匿名用户的实例(名为AnonymousUser )。guardian的匿名用户与Django的匿名用户不同。Django匿名用户在数据库中没有条目,但是Guardian匿名用户有,这意味着以下代码将返回意外的结果。

request.user.is_anonymous = True

如果你希望关闭匿名用户面向对象的权限,可以设置ANONYMOUS_USER_NAME=None

权限分配

加入我们有如下一个Task模型,我们自定义了一条assign_task的权限。

class Task(models.Model):
    summary = models.CharField(max_length=32)
    content = models.TextField()
    reported_by = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        permissions = (
            ('assign_task', 'Assign task'),
        )
为用户分配权限
# 创建用户,本来权限为False
>>> from django.contrib.auth.models import User
>>> boss = User.objects.create(username='Big Boss')
>>> joe = User.objects.create(username='joe')
>>> task = Task.objects.create(summary='Some job', content='', reported_by=boss)
>>> joe.has_perm('assign_task', task)
False

# 使用gaurdian的assign_perm方法新增taks对象的权限
>>> from guardian.shortcuts import assign_perm
>>> assign_perm('assign_task', joe, task)
>>> joe.has_perm('assign_task', task)
True
为用户组分配权限

同样使用assign_perm方法即可。

>>> from django.contrib.auth.models import Group
>>> group = Group.objects.create(name='employees')
>>> assign_perm('change_task', group, task)
>>> joe.has_perm('change_task', task)
False
>>> # Well, joe is not yet within an *employees* group
>>> joe.groups.add(group)
>>> joe.has_perm('change_task', task)
True
通过信号分配权限

下例展示了通过信号为新增用户分配了编辑个人资料的权限。

@receiver(post_save, sender=User)
def user_post_save(sender, **kwargs):
    """
    Create a Profile instance for all newly created User instances. We only
    run on user creation to avoid having to check for existence on each call
    to User.save.
    """
    user, created = kwargs["instance"], kwargs["created"]
    if created and user.username != settings.ANONYMOUS_USER_NAME:
        from profiles.models import Profile
        profile = Profile.objects.create(pk=user.pk, user=user, creator=user)
        assign_perm("change_user", user, user)
        assign_perm("change_profile", user, profile)

删除权限

使用remove_perm方法即可删除一个用户或用户组的权限。

>>> from guardian.shortcuts import remove_perm
>>> remove_perm('change_site', joe, site)
>>> joe = User.objects.get(username='joe')
>>> joe.has_perm('change_site', site)
False

权限验证

HAS_PERM方法
# 分配对象权限前
>>> site = Site.objects.get_current()
>>> joe.has_perm('sites.change_site', site)

# 分配对象权限后
>>> from guardian.shortcuts import assign_perm
>>> assign_perm('sites.change_site', joe, site)
<UserObjectPermission: example.com | joe | change_site>
>>> joe = User.objects.get(username='joe')
>>> joe.has_perm('sites.change_site', site)
True
GET_PERMS方法
>>> from guardian.shortcuts import get_perms
>>>
>>> joe = User.objects.get(username='joe')
>>> site = Site.objects.get_current()
>>>
>>> 'change_site' in get_perms(joe, site)
True
GET_OBJECTS_FOR_USER方法

该方法可以获取用户具有权限操作的对象列表

from guardian.shortcuts import get_objects_for_user

def user_dashboard(request, template_name='projects/dashboard.html'):
    projects = get_objects_for_user(request.user, 'projects.view_project')
OBJECTPERMISSIONCHECKER

该方法可以缓存用户对一个对象的全部权限,减少数据库查询次数。

>>> from guardian.core import ObjectPermissionChecker
>>> checker = ObjectPermissionChecker(joe) # 检查joe的对象权限并缓存
>>> checker.has_perm('change_site', site)
True
>>> checker.has_perm('add_site', site) # 无需再次查询数据库
False
使用装饰器

标准的permission_required的装饰器不能用来检查对象权限,guardian提供了自己的装饰器。

>>> from guardian.decorators import permission_required_or_403
>>> from django.http import HttpResponse
>>>
>>> @permission_required_or_403('auth.change_group',
>>>     (Group, 'name', 'group_name'))
>>> def edit_group(request, group_name):
>>>     return HttpResponse('some form')
模板中校验

模板中校验对象级别权限时需要用到guardian提供的get_obj_perms模板标签。

{% load guardian_tags %}

模板中可以使用如下方法获取一个用户或用户组对一个对象的权限。

{% get_obj_perms user/group for obj as "context_var" %}

示例代码如下所示:

{% get_obj_perms request.user for article as "article_perms" %}
{% if "delete_article" in flatpage_perms %}
    <a href="/pages/delete?target={{ article.url }}">Remove article</a>
{% endif %}

与Django-admin的集成

使用GuardedModelAdmin,而不是Django的ModelAdmin

from django.contrib import admin
from posts.models import Post
from guardian.admin import GuardedModelAdmin

class PostAdmin(GuardedModelAdmin):
    prepopulated_fields = {"slug": ("title",)}
    list_display = ('title', 'slug', 'created_at')
    search_fields = ('title', 'content')
    ordering = ('-created_at',)
    date_hierarchy = 'created_at'

admin.site.register(Post, PostAdmin)

使用定制User模型

如果你使用定制User模型,建议设置GUARDIAN_MONKEY_PATCH = False并将其继承GuardianUserMixin, 如下所示:

class User(GuardianUserMixin, AbstractUser):
    name = models.CharField(blank=True, max_length=50)

小结

本文详细总结了Django的权限管理机制,包括权限的分配、删除与校验,并详细介绍了如何使用Django-Guardian实现对象级别的权限控制。

Django中间件原理及示例

什么是中间件及其应用场景

*中间件(middleware)是一个镶嵌到Django的request(请求)/response(响应)处理机制中的一个钩子(hooks) 框架。它是一个可以修改Django全局输入或输出的一个底层插件系统。*

上面这段话是Django官方文档中对于中间件的介绍,非常抽象难懂。小编我来尝试用浅显的语言再介绍一遍吧。我们首先要了解下Django的request/response处理机制,然后再看看中间件在整个处理机制中的角色及其工作原理。

HTTP Web服务器工作原理一般都是接收用户发来的请求(request), 然后给出响应(response)。Django也不例外,其一般工作方式是接收request请求和其它参数,交由视图(view)处理,然后给出它的响应(response): 渲染过的html文件或json格式的数据。然而在实际工作中Django并不是接收到request对象后,马上交给视图函数或类(view)处理,也不是在view执行后立马把response返回给用户。**一个请求在达到视图View处理前需要先经过一层一层的中间件处理,经过View处理后的响应也要经过一层一层的中间件处理才能返回给用户 **。

中间件(Middleware)在整个Django的request/response处理机制中的角色如下所示:

HttpRequest -> Middleware -> View -> Middleware -> HttpResponse

中间件常用于权限校验、限制用户请求、打印日志、改变输出内容等多种应用场景,比如:

  • 禁止特定IP地址的用户或未登录的用户访问我们的View视图函数
  • 对同一IP地址单位时间内发送的请求数量做出限制
  • 在View视图函数执行前传递额外的变量或参数
  • 在View视图函数执行前或执行后把特定信息打印到log日志
  • 在View视图函数执行后对response数据进行修改后返回给用户

注意:装饰器也经常用于用户权限校验。但与装饰器不同,中间件对Django的输入或输出的改变是全局的。比如@login_required装饰器仅作用于单个视图函数。如果你希望实现全站只有登录用户才能访问,编写一个中间件是一个更好的解决方案。

Django自带中间件

当你创建一个新Django项目时,你会发现settings.py里的MIDDLEWARE列表已经注册了一些Django自带的中间件,每个中间件都负责一个特定的功能。

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',
]

每个中间件的功能如下,小编建议都保留:

  • SecurityMiddleware:为request/response提供了几种安全改进;
  • SessionMiddleware:开启session会话支持;
  • CommonMiddleware:基于APPEND_SLASH和PREPEND_WWW的设置来重写URL,如果APPEND_SLASH设为True,并且初始URL 没有以斜线结尾以及在URLconf 中没找到对应定义,这时形成一个斜线结尾的新URL;
  • CsrfViewMiddleware:添加跨站点请求伪造的保护,通过向POST表单添加一个隐藏的表单字段,并检查请求中是否有正确的值;
  • AuthenticationMiddleware:在视图函数执行前向每个接收到的user对象添加HttpRequest属性,表示当前登录的用户,无它用不了request.user
  • MessageMiddleware:开启基于Cookie和会话的消息支持
  • XFrameOptionsMiddleware:对点击劫持的保护

除此以外, Django还提供了压缩网站内容的GZipMiddleware,根据用户请求语言返回不同内容的LocaleMiddleware和给GET请求附加条件的ConditionalGetMiddleware。这些中间件都是可选的。

Django的中间件执行顺序

当你在settings.py注册中间件时一定要要考虑中间件的执行顺序,中间件在request到达view之前是从上向下执行的,在view执行完后返回response过程中是从下向上执行的,如下图所示。举个例子,如果你自定义的中间件有依赖于request.user,那么你自定义的中间件一定要放在AuthenticationMiddleware的后面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-whgPqWbo-1655293377680)(https://pythondjango.cn/django/advanced/middleware.assets/image-20210322171209455.png)]

自定义中间件

自定义中间件你首先要在app所属目录下新建一个文件middleware.py, 添加好编写的中间件代码,然后在项目settings.py中把它添加到MIDDLEWARE列表进行注册,添加时一定要注意顺序。

Django提供了两种编写自定义中间件的方式:函数和类,基本框架如下所示:

函数

def simple_middleware(get_response):
    # 一次性设置和初始化
    def middleware(request):
        # 请求在到达视图前执行的代码
        response = get_response(request)
        # 响应在返回给客户端前执行的代码
        return response
    return middleware

当请求从浏览器发送到服务器视图时,将执行response = get_response(request)该行之前的所有代码。当响应从服务器返回到浏览器时,将执行response = get_response(request)此行之后的所有内容。

那么这条分界线respone = get_response(request)做什么的?简而言之,它将调用列表中的下一个中间件。如果这是最后一个中间件,则将调用该视图。

*示例*

我们现在以函数编写一个名为timeit_middleware的中间件,打印出执行每个请求所花费的时间,代码如下所示:

import time

def timeit_middleware(get_response):
    
    def middleware(request):
        start = time.time()
        response = get_response(request)
        end = time.time()
        print("请求花费时间: {}秒".format(end - start))
        return response

    return 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',
    'blog.middleware.timeit_middleware', # 新增
]

执行效果

每当Django处理一个请求时,终端(terminal)就会打印出请求花费时间,如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8vqcAk6k-1655293377680)(https://pythondjango.cn/django/advanced/middleware.assets/image-20210322212937585.png)]

使用类

class SimpleMiddleware:
    def __init__(self, get_response):
        # 一次性设置和初始化
        self.get_response = get_response
        
    def __call__(self, request):
        # 视图函数执行前的代码
        response = self.get_response(request)
        # 视图函数执行后的代码
        return response

*示例*

我们现在以类来编写一个名为LoginRequiredMiddleware的中间件,实现全站要求登录,但是登录页面和开放白名单上的urls除外。代码如下所示:

from django.shortcuts import redirect
from django.conf import settings

class LoginRequiredMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.login_url = settings.LOGIN_URL
        # 开放白名单,比如['/login/', '/admin/']
        self.open_urls = [self.login_url] + getattr(settings, 'OPEN_URLS', [])

    def __call__(self, request):        
        if not request.user.is_authenticated and request.path_info not in self.open_urls:
            return redirect(self.login_url + '?next=' + request.get_full_path())
        
        response = self.get_response(request) 
        return response

小知识: request.path_info用于获取当前请求的相对路径,如/articles/,而request.get_full_path()用于获取当前请求完整的相对路径,包括请求参数,如/articles/?page=2。使用request.get_full_path()时别忘了加括号哦,否则返回的是uwsgi请求对象,不是字符串。

注册中间件

修改settings.py, 注册中间件,并添加 LOGIN_URLOPEN_URLS

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',
    'blog.middleware.timeit_middleware',
    'blog.middleware.LoginRequiredMiddleware',
]

LOGIN_URL = "/admin/login/"
OPEN_URLS = ["/admin/"]

查看效果

添加完中间件后,你访问任何非LOGIN_URL和OPEN_URLS里的urls,都需要你先进行登录,如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mYzEgn0b-1655293377681)(https://pythondjango.cn/django/advanced/middleware.assets/image-20210322222204474.png)]

其它中间件钩子函数

Django还提供了其它三个中间件钩子函数,分别在执行视图函数,处理异常和进行模板渲染时调用。

process_view(request, view_func, view_args, view_kwargs)

该方法有四个参数

  • request是HttpRequest对象。
  • view_func是Django即将使用的视图函数。 (它是实际的函数对象,而不是函数的名称作为字符串。
  • view_args是将传递给视图的位置参数的列表。
  • view_kwargs是将传递给视图的关键字参数的字典。 view_args和view_kwargs都不包含第一个视图参数(request)。

Django会在调用视图函数之前调用process_view方法。它应该返回None或一个HttpResponse对象。 如果返回None,Django将继续处理这个请求,执行任何其他中间件的process_view方法,然后在执行相应的视图。 如果它返回一个HttpResponse对象,Django不会调用适当的视图函数。 它将执行中间件的process_response方法并将应用到该HttpResponse并返回结果。

process_exception(self, request, exception)

该方法两个参数:

  • 一个HttpRequest对象
  • 一个exception是视图函数异常产生的Exception对象。

这个方法只有在视图函数中出现异常了才执行,它返回的值可以是一个None也可以是一个HttpResponse对象。如果是HttpResponse对象,Django将调用模板和中间件中的process_response方法,并返回给浏览器,否则将默认处理异常。如果返回一个None,则交给下一个中间件的process_exception方法来处理异常。该方法常用于发生异常时通知管理员或将其日志的形式记录下来。

process_template_response(self, request, response)

该方法两个参数:

  • 一个HttpRequest对象
  • 一个response是TemplateResponse对象(由视图函数或者中间件产生)。

该方法是在视图函数执行完成后立即执行,但是它有一个前提条件,那就是视图函数返回的对象有一个render()方法(或者表明该对象是一个TemplateResponse对象)。该方法常用于向模板注入变量或则直接改变模板。

如何使用这3个钩子函数?

在函数或类的中间件中应该如何使用上面3个钩子函数呢? 具体实现方式如下:

函数

from django.http import HttpResponse

def timeit_middleware(get_response):
    
    def middleware(request):
        response = get_response(request)
        return response
    
    def process_view(request, view_func, view_args, view_kwargs)
        return None or HttpResponse(xx)
 
    def process_exception(self, request, exception):
        return None or HttpResponse(xx)
    
    def process_template_response(self, request, response)
        return ...
    
    middleware.process_view = process_view
    middleware.process_exception = process_exception
    middleware.process_template_response = process_template_response
 
    return middleware

class MyClassMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
 
    def __call__(self, request):
        return self.get_response(request)
    
    def process_view(request, view_func, view_args, view_kwargs)
        return None or HttpResponse(xx)
 
    def process_exception(self, request, exception):
        return None or HttpResponse(xx)
        # 例子: 打印出异常
        return HttpResponse(<h1>str(exception)</h1)
    
    # 该方法仅对TemplateResponse输入有用,对render方法失效
    def process_template_response(self, request, response)
        response.context_data['title'] = 'New title'
        return response

小结

本文介绍了Django中间件(Middleware)的工作原理,执行顺序及如何自定义中间件。了解中间件一定要先对Django的request/response处理过程非常了解。当你希望在视图函数执行请求前或执行请求后添加额外的功能,且这种功能是全局性的(针对所有的request或view或response), 那么使用中间件是最好的实现方式。

Django全局上下文处理器

全局上下文处理器(Context Processors)应用场景

Django内置的全局上下文处理器

如何自定义全局上下文处理器

全局变量与本地变量的优先级

小结

全局上下文处理器(Context Processors)应用场景

当你需要向所有模板传递一个可以被全局使用的变量时。在编写Django视图函数时,我们一般会在视图函数中以Python字典(dict)形式向模板中传递需要被调用或使用的变量并指定渲染模板。通常情况下,我们向模板的传递的字典变量与模板是一一对应的关系。有时我们还需要向模板传递全局变量,即每个模板都需要使用到的变量(比如站点名称, 博客的最新文章列表)。

如果每个视图函数分别去查询数据库,然后向每个模板传递这些变量,不仅造成代码冗余,而且会造成对数据库的重复查询。一个更好的解决方案就是使用自定义的上下文处理器(Context Processors)给模板传递全局变量,一次查询全局使用,完美解决了这些问题。

Django内置的全局上下文处理器

你或许没有自定义过自己的全局上下文处理器(Context Processors),但你一定使用过Django内置的全局上下文处理器(Context Processors)。举个例子,虽然你没有向某个模板中传递过权限perms对象,你却可以在所有模板中随时调用它(如下所示)。同样可以在模板中全局使用的变量还有request和user对象。

为什么?因为Django的settings.py里已经包含了django.template.context_processors.requestdjango.contrib.auth.context_processors.auth这两个全局上下文处理器。如果你把他们移除, 看看还能不能在模板中调用 userperms?

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'templates')
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [ # 以下包含了4个默认的全局上下文处理器
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'myapp.custom_context_processors.xxx',  # 自定义全局上下文处理器
            ],
        },
    },
]

Django一般包含了上述4个默认全局上下文处理器,它们作用如下所示:

  • django.template.context_processors.debug:在模板里面可以直接使用settings的DEBUG参数以及强大的sql_queries:它本身是一个字典,其中包括当前页面执行SQL查询所需的时间
  • django.template.context_processors.request:在模板中可以直接使用request对象
  • django.contrib.auth.context_processors.auth:在模板里面可以直接使用user,perms对象。
  • django.contrib.messages.context_processors.messages:在模板里面可以直接使用message对象。

另外Django还提供了几个全局上下文处理器:

  • django.template.context_processors.i18n:在模板里面可以直接使用settings的LANGUAGES和LANGUAGE_CODE
  • django.template.context_processors.media:可以在模板里面使用settings的MEDIA_URL参数
  • django.template.context_processors.csrf : 给模板标签 csrf_token提供值
  • django.template.context_processors.tz: 可以在模板里面使用 TIME_ZONE参数。

如何自定义全局上下文处理器

自定义的全局上下文处理器本质上是个函数,使用它必须满足3个条件:

  1. 传入参数必须有request对象
  2. 返回值必须是个字典
  3. 使用前需要在settings的context_processors里申明。

我们通常会把自定义的上下文处理器函数放在单独命名的context_processors.py里,这个python文件可以放在project目录下,也可以放在某个app的目录下。

接下来我们来看一个具体例子。我们需要向所有模板传递一个叫site_name的全局变量以便在所有模板中直接使用 site_name输出站点名称,我们可以在blog(应用名)的目录下新建context_processors.py,新增如下代码:

# blog/context_processors.py

from django.conf import settings
def global_site_name(request):
    return {'site_name': settings.SITE_NAME,}

然后可以在settings.py里声明:

'context_processors': [ # 以下包含了4个默认的全局上下文处理器
    'django.template.context_processors.debug',
    'django.template.context_processors.request',
    'django.contrib.auth.context_processors.auth',
    'django.contrib.messages.context_processors.messages',
    'blog.context_processors.global_site_name',  # 自定义全局上下文处理器
]

全局变量与本地变量的优先级

全局上下文处理器提供的变量优先级高于单个视图函数给单个模板传递的变量。这意味着全局上下文处理器提供的变量可能会覆盖你视图函数中自定义的本地变量,因此请注意避免本地变量名与全局上下文处理器提供的变量名称重复。这些变量名包括perms, user和debug等等。

如果你希望单个视图函数定义的变量名覆盖全局变量,请使用以下强制模式:

from django.template import RequestContext

high_priority_context = RequestContext(request)
high_priority_context.push({"my_name": "Adrian"})

小结

本文总结了什么是Django的全局上下文处理器(Context Processors),它的应用场景及如何自定义使用自己的全局上下文处理器,希望大家喜欢。

Django信号机制及示例

  1. 信号的工作机制
  2. 信号的应用场景
  3. 两个简单例子
  4. Django常用内置信号
  5. 如何放置信号监听函数代码
  6. 自定义信号
    1. 第一步:自定义信号
    2. 第二步:触发信号
    3. 第三步:将监听函数与信号相关联
  7. 小结

信号的工作机制

Django 中的信号工作机制依赖如下三个主要要素:

  • 发送者(sender):信号的发出方,可以是模型,也可以是视图。当某个操作发生时,发送者会发出信号。
  • 信号(signal):发送的信号本身。Django内置了许多信号,比如模型保存后发出的post_save信号。
  • 接收者(receiver):信号的接收者,其本质是一个简单的回调函数。将这个函数注册到信号上,当特定的事件发生时,发送者发送信号,回调函数就会被执行。

信号的应用场景

信号主要用于Django项目内不同事件的联动,实现程序的解耦。比如当模型A有变动时,模型B与模型C收到发出的信号后同步更新。又或当一个数据表数据有所改变时,监听这个信号的函数可以及时清除已失效的缓存。另外通知也是一个信号常用的场景,比如有人刚刚回复了你的贴子,可以通过信号进行推送。

注意:Django中信号监听函数不是异步执行,而是同步执行,所以需要异步执行耗时的任务时(比如发送邮件或写入文件),不建议使用Django自带的信号。

两个简单例子

假如我们有一个Profile模型,与User模型是一对一的关系。我们希望创建User对象实例时自动创建Profile对象实例,而更新User对象实例时不创建新的Profile对象实例。这时我们就可以自定义 create_user_profilesave_user_profile两个监听函数,同时监听sender (User模型)发出的post_save信号。由于post_save可同时用于模型的创建和更新,我们用if created这个判断来加以区别。

from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
 
class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    birth_date = models.DateField(null=True, blank=True)

# 监听User模型创建    
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
   if created:
       Profile.objects.create(user=instance)

# 监听User模型更新  
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

我们再来看一个使用信号清除缓存的例子。当模型A被更新或被删除时,会分别发出post_savepost_delete的信号,监听这两个信号的receivers函数会自动清除缓存里的A对象列表。

from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver

@receiver(post_save, sender=ModelA)
def cache_post_save_handler(sender, **kwargs):
    cache.delete('cached_a_objects')
    
@receiver(post_delete, sender=ModelA)
def cache_post_delete_handler(sender, **kwargs):
     cache.delete('cached_a_objects')

注意:有时为了防止信号多次发送,可以通过dispatch_uid给receiver函数提供唯一标识符。

@receiver(post_delete, sender=ModelA, dispatch_uid = "unique_identifier")

Django常用内置信号

前面例子我们仅仅使用了post_savepost_delete信号。Django还内置了其它常用信号:

  • pre_save & post_save: 在模型调用 save()方法之前或之后发送。
  • pre_init& post_init: 在模型调用_init_方法之前或之后发送。
  • pre_delete & post_delete: 在模型调用delete()方法或查询集调用delete() 方法之前或之后发送。
  • m2m_changed: 在模型多对多关系改变后发送。
  • request_started & request_finished: Django建立或关闭HTTP 请求时发送。

这些信号都非常有用。举个例子:使用pre_save信号可以在将用户的评论存入数据库前对其进行过滤,或则检测一个模型对象的字段是否发生了变更。

注意:监听pre_savepost_save信号的回调函数不能再调用save()方法,否则回出现死循环。另外Django的update方法不会发出pre_savepost_save的信号。

如何放置信号监听函数代码

在之前案例中,我们将Django信号的监听函数写在了models.py文件里。当一个app的与信号相关的自定义监听函数很多时,此时models.py代码将变得非常臃肿。一个更好的方式把所以自定义的信号监听函数集中放在app对应文件夹下的signals.py文件里,便于后期集中维护。

假如我们有个account的app,包含了User和Profile模型,我们首先需要在account文件夹下新建signals.py,如下所示:

# account/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import User, Profile

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
  if created:
      Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

接下来我们需要修改account文件下apps.py__init__.py,以导入创建的信号监听函数。

# apps.py
from django.apps import AppConfig
 
class AccountConfig(AppConfig):
    name = 'account'
 
    def ready(self):
        import account.signals
        
# account/__init__.py中增加如下代码:
default_app_config = 'account.apps.AccountConfig'

自定义信号

Django的内置信号在大多数情况下能满足我们的项目需求,但有时我们还需要使用自定义的信号。在Django项目中使用自定义信号也比较简单,分三步即可完成。

第一步:自定义信号

每个自定义的信号,都是Signal类的实例。这里我们首先在app目录下新建一个signals.py文件,创建一个名为my_signal的信号,它包含有msg这个参数,这个参数在信号触发的时候需要传递。当监听函数收到这个信号时,会得到msg参数的值。

from django.dispatch import Signal

my_signal = Signal(providing_args=['msg'])

第二步:触发信号

视图中进行某个操作时可以使用send方法触发自定义的信号,并设定msg的值。

from . import signals
# Create your views here.

def index(request):
    signals.my_signal.send(sender=None, msg='Hello world')
    return render(request, template_name='index.html')

第三步:将监听函数与信号相关联

from django.dispatch import Signal, Receiver

my_signal = Signal(providing_args=['msg'])

@receiver(my_signal)
def my_signal_callback(sender, **kwargs):
    print(kwargs['msg']) # 打印Hello world!

这样每当用户访问/index/视图时,Django都会发出my_signal的信号(包含msg这个参数)。回调函数收到这个信号后就会打印出msg的值来。

小结

在本文里我们总结了Django信号(signals)的工作机制及应用场景,介绍了如何在Django项目中使用信号实现事件的联动。最后我们还总结了Django常用内置信号以及如何自定义信号。Django信号还有非常多的应用场景等着你去发现。

自定义Django-admin管理命令

创建文件夹布局

编写命令代码

实际应用场景

  1. 案例1: 检查数据库连接是否已就绪
  2. 案例2:周期性发送邮件

小结

创建文件夹布局

自定义的Django-admin管理命令本质上是一个python脚本文件,它的存放路径必须遵循一定的规范,一般位于app/management/commands目录。整个文件夹的布局如下所示:

app01/
    __init__.py
    models.py
    management/
        __init__.py
        commands/
            __init__.py
            _private.py # 以下划线开头文件不能用作管理命令
            my_commands.py # 这个就是自定义的管理命令脚本,文件名即为命令名
    tests.py
    views.py

注意

  • managementcommands每个目录下都必须有个__init__.py空文件,表明这是一个python包。另外以下划线开头的文件名不能用作管理命令脚本。
  • management/commands目录可以位于任何一个app的目录下,Django都能找到它。
  • 一般建议每个python脚本文件对应一条管理命令。

编写命令代码

每一个自定义的管理命令本质是一个Command类, 它继承了Django的Basecommand或其子类, 主要通过重写handle()方法实现自己的业务逻辑代码,而add_arguments()则用于帮助处理命令行的参数,如果运行命令时不需要额外参数,可以不写这个方法。

from django.core.management.base import BaseCommand

class Command(BaseCommand):
    # 帮助文本, 一般备注命令的用途及如何使用。
    help = 'Some help texts'

    # 处理命令行参数,可选
    def add_arguments(self, parser):
       pass

    # 核心业务逻辑
    def handle(self, *args, **options):
        pass

我们现在来看一个最简单的例子,希望定义一个名为hello_world的命令。这样当我们运行python manage.py hello_world命令时,控制台会打印出Hello World!字样。在app/management/commands目录下新建hello_world.py, 添加如下代码:

from django.core.management.base import BaseCommand

class Command(BaseCommand):
    # 帮助文本, 一般备注命令的用途及如何使用。
    help = "Print Hello World!"

    # 核心业务逻辑
    def handle(self, *args, **options):
        self.stdout.write('Hello World!') **注意**:当你使用管理命令并希望在控制台输出指定信息时,你应该使用`self.stdout`和`self.stderr`方法,而不能直接使用python的`print`方法。另外,你不需要在消息的末尾加上换行符,它将被自动添加。

此时当你进入项目文件夹运行python manage.py hello_world命令时,你将得到如下输出结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vxNVUNDW-1655293377681)(https://pythondjango.cn/django/advanced/11-django-admin-commands.assets/image-20210422151023064.png)]

现在我们来增加点难度,来通过命令行给hello_world命令传递参数,以实现运行python manage.py helloworld John命令时 打印出Hello World! John

现在修改我们的hello_world.py, 添加add_arguments方法,该方法的作用是给自定义的handle方法添加1个或多个参数。

from django.core.management.base import BaseCommand

class Command(BaseCommand):
    # 帮助文本, 一般备注命令的用途及如何使用。
    help = "Print Hello World!"

    # 给命令添加一个名为name的参数
    def add_arguments(self, parser):
        parser.add_argument('name')

    # 核心业务逻辑,通过options字典接收name参数值,拼接字符串后输出
    def handle(self, *args, **options):
        msg = 'Hello World ! '+ options['name']
        self.stdout.write(msg) 此时当你再次运行`python manage.py hello_world John`命令时,你将得到如下输出结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2KNh8R7A-1655293377681)(https://pythondjango.cn/django/advanced/11-django-admin-commands.assets/image-20210422152903910.png)]

如果你直接运行命令而不携带参数,将会报错,如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zVNbXS9K-1655293377682)(https://pythondjango.cn/django/advanced/11-django-admin-commands.assets/image-20210422152824530.png)]

实际应用场景

前面的案例过于简单,我们现在来看两个自定义管理命令的实际应用案例。

案例1: 检查数据库连接是否已就绪

无论你使用常规方式还是Docker在生产环境中部署Django项目,你需要确保数据库连接已就绪后才进行数据库迁移(migrate)的命令(Docker-compose的depends选项并不能确保这点),否则Django应用程序会出现报错。

这时你可以自定义一个wait_for_db的命令,如下所示:

# app/management/commands/wait_for_db.py

import time
from django.db import connections
from django.db.utils import OperationalError
from django.core.management import BaseCommand


class Command(BaseCommand):
    help = 'Run data migrations until db is available.'

    def handle(self, *args, **options):
        self.stdout.write('Waiting for database...')
        db_conn = None
        while not db_conn:
            try:
                # 尝试连接
                db_conn = connections['default']
            except OperationalError:
                # 连接失败,就等待1秒钟
                self.stdout.write('Database unavailable, waiting 1 second...')
                time.sleep(1)

        self.stdout.write(self.style.SUCCESS('Database available!'))

定义好这个命令后每次在运行python manage.py migrate 命令前先运行python manage.py wait_for_db即可。

案例2:周期性发送邮件

如果你是网站管理员,你肯定希望知道每天有多少新用户已注册,这时你可以自定义一条mail_admin的管理命令,将每天新注册用户数量以邮件形式发给自己,如下所示:

# app/management/commands/mail_admin.py

#-*- coding:utf-8 -*-
from datetime import timedelta, time, datetime
from django.core.mail import mail_admins
from django.core.management import BaseCommand
from django.utils import timezone
from django.contrib.auth import get_user_model

User = get_user_model()

today = timezone.now()
yesterday = today - timedelta(1)


class Command(BaseCommand):
    help = "Send The Daily Count of New Users to Admins"

    def handle(self, *args, **options):
        # 获取过去一天注册用户数量
        user_count =User.objects.filter(date_joined__range=(yesterday, today)).count()
        
        # 当注册用户数量多余1个,才发送邮件给管理员
        if user_count >= 1:
            message = "You have got {} user(s) in the past 24 hours".format(user_count)

            subject = (
                f"New user count for {today.strftime('%Y-%m-%d')}: {user_count}"
            )

            mail_admins(subject=subject, message=message, html_message=None)

            self.stdout.write("E-mail was sent.")
        else:
            self.stdout.write("No new users today.")

如果你在终端运行python manage.py mail_admin 命令,你将得到如下输出结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a4uP8K6J-1655293377682)(https://pythondjango.cn/django/advanced/11-django-admin-commands.assets/image-20210422163523157.png)]

注意:真正发送邮件成功需要设置Email后台及管理员,测试环境下可以使用如下简单配置:

EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DEFAULT_FROM_EMAIL = "noreply@example.com"
ADMINS = [("大江狗", "yunbo.shi@example.com"), ]

但是如果每天都要进入终端运行这个命令实在太麻烦了,我们完全可以使用Linux的crontab服务或Celery-Beat将其设成周期性定时任务task,这时只需要调用Django的call_command方法即可。

# app/tasks.py, 可以任一app目录下新建task
from celery import shared_task
from django.core.management import call_command

@shared_task
def mail_admin():
    call_command("mail_admin", )

关于Django项目中如何使用Celery执行异步和周期性任务,请参加Django进阶-异步和周期任务篇。

小结

本文总结了如何自定义Django-admin管理命令并提供了两个应用场景,更多好的使用案例等你去发现哦。

Django项目中使用Celery执行异步和周期性任务

  1. Celery的工作原理
  2. 安装项目依赖文件
  3. Celery配置
  4. 测试Celery是否工作正常
  5. 编写任务
  6. 异步调用任务
  7. 查看任务执行状态及结果
  8. 设置定时和周期性任务
    1. 配置文件添加任务
    2. Django Admin添加周期性任务
    3. 通过Crontab设置定时任务
    4. 启动任务调度器beat
  9. Flower监控任务执行状态
  10. Celery高级用法与注意事项
    1. 给任务设置最大重试次数
    2. 不同任务交由不同Queue处理
    3. 忽略不想要的结果
    4. 避免启动同步子任务
    5. Django的模型对象不应该作为参数传递
    6. 使用on_commit函数处理事务
  11. 小结

Celery的工作原理

Celery是一个高效的基于分布式消息传递的作业队列。它主要通过消息(messages)传递任务,通常使用一个叫Broker(中间人)来协调client(任务的发出者)和worker(任务的处理者)。 clients发出消息到队列中,broker将队列中的信息派发给 Celery worker来处理。Celery本身不提供消息服务,它支持的消息服务(Broker)有RabbitMQ和Redis。小编一般推荐Redis,因为其在Django项目中还是首选的缓存后台。

安装项目依赖文件

本项目使用了最新Django和Celery版本。因为本项目使用Redis做消息队列的broker,所以还需要安装redis (Windows下安装和启动redis参见菜鸟教程)。另外如果你要设置定时或周期性任务,还需要安装django-celery-beat

# pip安装必选
Django==3.2
celery==5.0.5
redis==3.5.3

# 可选,windows下运行celery 4以后版本,还需额外安装eventlet库
eventlet 

# 推荐安装, 需要设置定时或周期任务时安装,推荐安装
django-celery-beat==2.2.0

# 视情况需要,需要存储任务结果时安装,视情况需要
django-celery-results==2.0.1

# 视情况需要,需要监控celery运行任务状态时安装
folower==0.9.7

Celery配置

在正式使用celerydjango-celery-beat之前,你需要做基础的配置。假如你的Django项目文件夹布局如下所示,你首先需要在myproject/myproject目录下新增celery.py并修改__init__.py

- myproject/
  - manage.py
  - project/
    - __init__.py # 修改这个文件
    - celery.py # 新增这个文件
    - asgi.py
    - settings.py
    - urls.py
    - wsgi.py

新建celery.py,添加如下代码:

import os
from celery import Celery

# 设置环境变量
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

# 实例化
app = Celery('myproject')

# namespace='CELERY'作用是允许你在Django配置文件中对Celery进行配置
# 但所有Celery配置项必须以CELERY开头,防止冲突
app.config_from_object('django.conf:settings', namespace='CELERY')

# 自动从Django的已注册app中发现任务
app.autodiscover_tasks()

# 一个测试任务
@app.task(bind=True)
def debug_task(self):
    print(f'Request: {self.request!r}')

修改__init__.py,如下所示:

from .celery import app as celery_app
__all__ = ('celery_app',)

接下来修改Django项目的settings.py,添加Celery有关配置选项,如下所示:

# 最重要的配置,设置消息broker,格式为:db://user:password@host:port/dbname
# 如果redis安装在本机,使用localhost
# 如果docker部署的redis,使用redis://redis:6379
CELERY_BROKER_URL = "redis://127.0.0.1:6379/0"

# celery时区设置,建议与Django settings中TIME_ZONE同样时区,防止时差
# Django设置时区需同时设置USE_TZ=True和TIME_ZONE = 'Asia/Shanghai'
CELERY_TIMEZONE = TIME_ZONE

其它Celery常用配置选项包括:

# 为django_celery_results存储Celery任务执行结果设置后台
# 格式为:db+scheme://user:password@host:port/dbname
# 支持数据库django-db和缓存django-cache存储任务状态及结果
CELERY_RESULT_BACKEND = "django-db"
# celery内容等消息的格式设置,默认json
CELERY_ACCEPT_CONTENT = ['application/json', ]
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'

# 为任务设置超时时间,单位秒。超时即中止,执行下个任务。
CELERY_TASK_TIME_LIMIT = 5

# 为存储结果设置过期日期,默认1天过期。如果beat开启,Celery每天会自动清除。
# 设为0,存储结果永不过期
CELERY_RESULT_EXPIRES = xx

# 任务限流
CELERY_TASK_ANNOTATIONS = {'tasks.add': {'rate_limit': '10/s'}}

# Worker并发数量,一般默认CPU核数,可以不设置
CELERY_WORKER_CONCURRENCY = 2

# 每个worker执行了多少任务就会死掉,默认是无限的
CELERY_WORKER_MAX_TASKS_PER_CHILD = 200

完整配置选项见:

  • https://docs.celeryproject.org/en/stable/userguide/configuration.html#std-setting-result_expires

注意:

  • 在Django中正式编写和执行自己的异步任务前,一定要先测试redis和celery是否安装好并配置成功。
  • 一个无限期阻塞的任务会使得工作单元无法再做其他事情,建议给任务设置超时时间。

测试Celery是否工作正常

首先你要启动redis服务。windows进入redis所在目录(比如C:\redis),使用redis-server.exe启动redis。Linux下使用./redis-server redis.conf启动,也可修改redis.conf将daemonize设置为yes, 确保守护进程开启。

启动redis服务后,你要先进入项目所在文件夹运行python manage.py runserver命令启动Django服务器(无需创建任何app),然后再打开一个终端terminal窗口输入celery命令,启动worker。

# Linux下测试,启动Celery
Celery -A myproject worker -l info

# Windows下测试,启动Celery
Celery -A myproject worker -l info -P eventlet

# 如果Windows下Celery不工作,输入如下命令
Celery -A myproject worker -l info --pool=solo

如果你能看到[tasks]下所列异步任务清单如debug_task,以及最后一句celery@xxxx ready, 说明你的redis和celery都配置好了,可以开始正式工作了。

-------------- celery@DESKTOP-H3IHAKQ v4.4.2 (cliffs)
--- ***** -----
-- ******* ---- Windows-10-10.0.18362-SP0 2020-04-24 22:02:38

- *** --- * ---
- ** ---------- [config]
- ** ---------- .> app:         myproject:0x456d1f0
- ** ---------- .> transport:   redis://127.0.0.1:6379/0
- ** ---------- .> results:     redis://localhost:6379/0
- *** --- * --- .> concurrency: 4 (eventlet)
  -- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
  --- ***** -----
   -------------- [queues]
                .> celery           exchange=celery(direct) key=celery


[tasks]
  . myproject.celery.debug_task

[2020-04-24 22:02:38,484: INFO/MainProcess] Connected to redis://127.0.0.1:6379/0
[2020-04-24 22:02:38,500: INFO/MainProcess] mingle: searching for neighbors
[2020-04-24 22:02:39,544: INFO/MainProcess] mingle: all alone
[2020-04-24 22:02:39,572: INFO/MainProcess] pidbox: Connected to redis://127.0.0.1:6379/0.
[2020-04-24 22:02:39,578: WARNING/MainProcess] c:\users\missenka\pycharmprojects\django-static-html-generator\venv\lib\site-packages\celery\fixups\django.py:203: UserWarning: Using sett
ings.DEBUG leads to a memory
            leak, never use this setting in production environments!
  leak, never use this setting in production environments!''')
[2020-04-24 22:02:39,579: INFO/MainProcess] celery@DESKTOP-H3IHAKQ ready.

编写任务

Celery配置完成后,我们就可以编写任务了。Django项目中所有需要Celery执行的异步或周期性任务都放在tasks.py文件里,该文件可以位于project目录下,也可以位于各个app的目录下。专属于某个Celery实例化项目的task可以使用@app.task装饰器定义,各个app目录下可以复用的task建议使用@shared_task定义。

两个示例如下所示:

# myproject/tasks.py
# 专属于myproject项目的任务
app = Celery('myproject')
@ app.task
def test():
    pass

# app/tasks.py, 可以复用的task
from celery import shared_task
import time

@shared_task
def add(x, y):
    time.sleep(2)
    return x + y

上面我们定义一个名为add的任务,它接收两个参数,并返回计算结果。为了模拟耗时任务,我们中途让其sleep 2秒。现在已经定义了一个耗时任务,我们希望在Django的视图或其它地方中以异步方式调用执行它,应该怎么做呢? 下面我们将给出答案。

注意

  1. 使用celery定义任务时,避免在一个任务中调用另一个异步任务,容易造成阻塞。
  2. 当我们使用@app.task装饰器定义我们的异步任务时,那么这个任务依赖于根据项目名myproject生成的Celery实例。然而我们在进行Django开发时为了保证每个app的可重用性,我们经常会在每个app文件夹下编写异步任务,这些任务并不依赖于具体的Django项目名。使用@shared_task 装饰器能让我们避免对某个项目名对应Celery实例的依赖,使app的可移植性更强。

异步调用任务

Celery提供了2种以异步方式调用任务的方法,delayapply_async方法,如下所示:

# 方法一:delay方法
task_name.delay(args1, args2, kwargs=value_1, kwargs2=value_2)

# 方法二: apply_async方法,与delay类似,但支持更多参数
task.apply_async(args=[arg1, arg2], kwargs={key:value, key:value})

我们接下来看一个具体的例子。我们编写了一个Django视图函数,使用delay方法调用add任务。

# app/views.py
from .tasks import add

def test_celery(request):
    add.delay(3, 5)
    return HttpResponse("Celery works")

# app/urls.py
urlpatterns = [
    re_path(r'^test/$', views.test_celery, name="test_celery")
]

当你通过浏览器访问/test/链接时,你根本感受不到2s的延迟,页面可以秒开,同时你会发现终端的输出如下所示,显示任务执行成功。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YovTIuSO-1655293377682)(https://pythondjango.cn/django/advanced/12-sync-periodic-tasks-with-celery.assets/image-20210423125753033.png)]

我们现在再次使用apply_async方法调用add任务,不过还要打印初任务的id (task.id)和状态status。Celery会为每个加入到队列的任务分配一个独一无二的uuid, 你可以通过task.status获取状态和task.result获取结果。注意:apply_async传递参数的方式与delay方法不同。

# app/views.py
from .tasks import add

def test_celery(request):
    result = add.apply_async(args=[3, 5])
    return HttpResponse(result.task_id + ' : ' + result.status)

Django返回响应结果如下所示。这是在预期之内的,因为Django返回响应时任务还未执行完毕。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c2lCgh7T-1655293377683)(https://pythondjango.cn/django/advanced/12-sync-periodic-tasks-with-celery.assets/image-20210423140239672.png)]

那么问题来了,这个异步任务执行了,返回了个计算结果(8),那么我们系统性地了解任务状态并获取这个执行结果呢? 答案是django-celery-results

查看任务执行状态及结果

通过pip安装django-celery-results后,需要将其加入到INSTALLED_APPS并使用migrate命令迁移创建数据表。以下几项配置选项是与这个库相关的。

# 支持数据库django-db和缓存django-cache存储任务状态及结果
# 建议选django-db
CELERY_RESULT_BACKEND = "django-db"
# celery内容等消息的格式设置,默认json
CELERY_ACCEPT_CONTENT = ['application/json', ]
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'

安装配置完成后,进入Django admin后台,你就可以详细看到每个任务的id、名称及状态。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PKEljZCo-1655293377683)(https://pythondjango.cn/django/advanced/12-sync-periodic-tasks-with-celery.assets/image-20210423142735615.png)]

点击单个任务id,你可以看到有关这个任务的更多信息,比如传递的参数和返回结果,如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eWX2i4sX-1655293377683)(https://pythondjango.cn/django/advanced/12-sync-periodic-tasks-with-celery.assets/image-20210423142900021.png)]

除了在Django admin后台中查看任务状态和结果,你还可以在视图中通过AsyncResult方法获取任务执行状态和结果,它需要接收一个任务的task_id(通常为uuid格式)。

from celery.result import AsyncResult
# 调用异步任务
async_task = add.apply_async(args=[3, 5])
# 获取任务状态和结果
AsyncResult(async_task.task_id).status
AsyncResult(async_task.task_id).result

设置定时和周期性任务

借助于装django-celery-beat后, 你可以将任一Celery任务设置为定时任务或周期性任务。使用它你只需要通过pip安装它,并加入INSTALLED_APPS里去。

django-celery-beat提供了两种添加定时或周期性任务的方式,一是直接在settings.py中添加,二是通过Django admin后台添加。

配置文件添加任务

同一任务可以设置成不同的调用周期,给它们不同的任务名就好了。

from datetime import timedelta
CELERY_BEAT_SCHEDULE = {
    "add-every-30s": {
        "task": "app.tasks.add",
        'schedule': 30.0, # 每30秒执行1次
        'args': (3, 8) # 传递参数-
    },
    "add-every-day": {
        "task": "app.tasks.add",
        'schedule': timedelta(hours=1), # 每小时执行1次
        'args': (3, 8) # 传递参数-
    },
}

Django Admin添加周期性任务

先在settings.py中将任务调度器设为DatabaseScheduler

CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'

然后进入Periodic Task表添加和修改周期性任务即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-27jolPPp-1655293377683)(https://pythondjango.cn/django/advanced/12-sync-periodic-tasks-with-celery.assets/image-20210423144902917.png)]

通过Crontab设置定时任务

如果你希望在特定的时间(某月某周或某天)执行一个任务,你可以通过crontab设置定时任务,如下例所示:

CELERY_BEAT_SCHEDULE = {
    # 每周一早上7点半执行
    'add-every-monday-morning': {
        'task': 'app.tasks.add',
        'schedule': crontab(hour=7, minute=30, day_of_week=1),
        'args': (7, 8),
    },
}

更多Crontab定义案例如下所示:

例子含义
crontab()每分
crontab(minute=0, hour=0)每天午夜
crontab(minute=0, hour='*/3')能被3整除的小时数,3,6,9点等等
crontab(minute=0,``hour='0,3,6,9,12,15,18,21')与前面相同,指定小时
crontab(minute='*/15')每15分钟
crontab(day_of_week='sunday')星期日每分钟
crontab(minute='*',``hour='*', day_of_week='sun')同上
crontab(minute='*/10',``hour='3,17,22', day_of_week='thu,fri')每10分钟运行一次, 但仅限于周四或周五的 3-4 am, 5-6 pm, 和10-11 pm.
crontab(minute=0, hour='*/2,*/3')可以被2或3整除的小时数,除了 1am, 5am, 7am, 11am, 1pm, 5pm, 7pm, 11pm
crontab(minute=0, hour='*/5')可以被5整除的小时
crontab(minute=0, hour='*/3,8-17')8am-5pm之间可以被3整除的小时
crontab(0, 0, day_of_month='2')每个月的第2天
crontab(0, 0,``day_of_month='2-30/2')每月的偶数日
crontab(0, 0,``day_of_month='1-7,15-21')每月的第一和第三周
crontab(0, 0, day_of_month='11',``month_of_year='5')每年的5月11日
crontab(0, 0,``month_of_year='*/3')每个季度首个月份每天

Crontab也可以通过Django Admin添加,然后与任务进行绑定。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zfm58qA3-1655293377684)(https://pythondjango.cn/django/advanced/12-sync-periodic-tasks-with-celery.assets/image-20210423151514956.png)]

如果你变换了时区timezone,比如从’UTC’变成了’Asia/Shanghai’,需重置周期性任务,这非常重要。

 # 调整timezone后重置任务
$ python manage.py shell
>>> from django_celery_beat.models import PeriodicTask
>>> PeriodicTask.objects.update(last_run_at=None)

前面我们只是添加了定时或周期性任务,我们还需要启动任务调度器beat分发定时和周期任务给Celery的worker。

启动任务调度器beat

多开几个终端,一个用来启动任务调度器beat,另一个启动celery worker,你的任务就可以在后台执行啦。

# 开启任务调度器
Celery -A myproject beat

# Linux下开启Celery worker
Celery -A myproject worker -l info

# windows下开启Celery worker
Celery -A myproject worker -l info -P eventlet

# windows下如果报Pid错误
Celery -A myproject worker -l info --pool=solo

Flower监控任务执行状态

除了django_celery_results, 你可以使用flower监控后台任务执行状态。它提供了一个可视化的界面,在测试环境中非常有用。

pip install flower

安装好后,你有如下两种方式启动服务器。启动服务器后,打开http://localhost:5555即可查看监控情况。

# 从terminal终端启动, proj为项目名
$ flower -A proj --port=5555  
# 从celery启动
$ celery flower -A proj --address=127.0.0.1 --port=5555

Celery高级用法与注意事项

给任务设置最大重试次数

定义任务时可以通过max_retries设置最大重试次数,并调用self.retry方法调用。因为要调用self这个参数,定义任务时必须设置bind=True

@shared_task(bind=True, max_retries=3)
def send_batch_notifications(self):
   try:
       something_raising()
       raise Exception('Can\'t send email.')
   except Exception as exc:
       self.retry(exc=exc, countdown=5)
   send_mail(
       subject='Batch email notifications',
       message='Test email',
       from_email='no-reply@example.com',
       recipient_list=['john@example.com']
   )

不同任务交由不同Queue处理

不同的任务所需要的资源和时间不一样的。为了防止一些非常占用资源或耗时的任务阻塞任务队列导致一些简单任务也无法执行,可以将不同任务交由不同的Queue处理。下例定义了两个Queue队列,default执行普通任务,heavy_tasks执行重型任务。

CELERY_TASK_DEFAULT_QUEUE = 'default'
CELERY_TASK_DEFAULT_ROUTING_KEY = 'default'
CELERY_QUEUES = (
   Queue('default', Exchange('default'), routing_key='default'),
   Queue('heavy_tasks', Exchange('heavy_tasks'), routing_key='heavy_tasks'),
)
CELERY_TASK_ROUTES = {
   'myapp.tasks.heave_tasks': 'heavy_tasks'
}

忽略不想要的结果

如果你不在意任务的返回结果,可以设置 ignore_result 选项,因为存储结果耗费时间和资源。你还可以可以通过 task_ignore_result 设置全局忽略任务结果。

@app.task(ignore_result=True)
def my_task():
    something()

避免启动同步子任务

让一个任务等待另外一个任务的返回结果是很低效的,并且如果工作单元池被耗尽的话这将会导致死锁。

# 话例子
@app.task
def update_page_info(url):
    page = fetch_page.delay(url).get()
    info = parse_page.delay(url, page).get()
    store_page_info.delay(url, info)
 
@app.task
def fetch_page(url):
    return myhttplib.get(url)
 
@app.task
def parse_page(url, page):
    return myparser.parse_document(page)
 
@app.task
def store_page_info(url, info):
    return PageInfo.objects.create(url, info)

# 好例子

def update_page_info(url):
    # fetch_page -> parse_page -> store_page
    chain = fetch_page.s(url) | parse_page.s() | store_page_info.s(url)
    chain()
 
 
@app.task()
def fetch_page(url):
    return myhttplib.get(url)
 
 
@app.task()
def parse_page(page):
    return myparser.parse_document(page)
 
 
@app.task(ignore_result=True)
def store_page_info(info, url):
    PageInfo.objects.create(url=url, info=info)

在好例子里,我们将不同的任务签名链接起来创建一个任务链,三个子任务按顺序执行

Django的模型对象不应该作为参数传递

Django 的模型对象不应该作为参数传递给任务。几乎总是在任务运行时从数据库获取对象是最好的,因为老的数据会导致竞态条件。假象有这样一个场景,你有一篇文章,以及自动展开文章中缩写的任务:

class Article(models.Model):
    title = models.CharField()
    body = models.TextField()
 
@app.task
def expand_abbreviations(article):
    article.body.replace('Old text', 'New text')
    article.save()

首先,作者创建一篇文章并保存,这时作者点击一个按钮初始化一个缩写展开任务:

>>> article = Article.objects.get(id=102)
>>> expand_abbreviations.delay(article)

现在,队列非常忙,所以任务在2分钟内都不会运行。与此同时,另一个作者修改了这篇文章,当这个任务最终运行,因为老版本的文章作为参数传递给了这个任务,所以这篇文章会回滚到老的版本。修复这个竞态条件很简单,只要参数传递文章的 id 即可,此时可以在任务中重新获取这篇文章:

@app.task
def expand_abbreviations(article_id):
    article = Article.objects.get(id=article_id)
    article.body.replace('MyCorp', 'My Corporation')
    article.save()

使用on_commit函数处理事务

我们再看另外一个celery中处理事务的例子。这是在数据库中创建一个文章对象的 Django 视图,此时传递主键给任务。它使用 commit_on_success 装饰器,当视图返回时该事务会被提交,当视图抛出异常时会进行回滚。

from django.db import transaction
 
@transaction.commit_on_success
def create_article(request):
    article = Article.objects.create()
    expand_abbreviations.delay(article.pk)

如果在事务提交之前任务已经开始执行会产生一个竞态条件;数据库对象还不存在。解决方案是使用 on_commit 回调函数来在所有事务提交成功后启动任务。

from django.db.transaction import on_commit
 
def create_article(request):
    article = Article.objects.create()
    on_commit(lambda: expand_abbreviations.delay(article.pk))

小结

本文详细演示了如何在Django项目中集成Celery设置执行异步、定时和周期性任务,并提供了一些高级使用案例和注意事项,希望对你有所帮助。

Django日志管理

  1. 日志基础
  2. Django的日志模块
  3. settings.py推荐日志配置
  4. 其它日志管理工具
    1. loguru
    2. sentry
  5. 小结

日志基础

日志与我们的软件程序密不可分。它记录了程序的运行情况,可以给我们调试程序和故障排查提供非常有用的信息。每一条日志信息记录了一个事件的发生。具体而言,它包括了:

  • 事件发生时间
  • 事件发生位置
  • 事件的严重程度–日志级别
  • 事件内容

日志的级别又分为:

  • DEBUG:用于调试目的的低级系统信息
  • INFO:一般系统信息
  • WARNING:描述已发生的小问题的警告信息。
  • ERROR:描述已发生的主要问题的错误信息。
  • CRITICAL:描述已发生的严重问题的信息。

在Django项目中,我们可以针对日志的不同级别设置不同的处理方式。比如INFO级别及以上的日志我们写入到log文件里保存,ERROR级别及以上的日志我们直接通过邮件发送给系统管理员。

Django的日志模块

Django的日志模块其实就是python的logging模块。它由4部分组成:

  • Logger 记录仪:生成和记录每条日志信息及级别
  • Handler处理程序: 根据日志信息级别交由相应处理程序处理(比如生成文件或发送邮件)
  • Filters 过滤器:日志交由处理程序处理前需要满足的过滤条件(比如Debug=True或False)
  • Formaters 格式化程序:决定每条日志的打印输出格式,可以有完整版的,也有简单版的

一个logger记录仪的例子如下所示。当程序运行出现错误时,它生成了一条级别为error的日志信息。这条记录产生后就会交由Handler处理。

import logging
# 获得logger实例
logger = logging.getLogger(__name__)
def my_view(request, arg1, arg2):
    ...
    if error_happens:
        # 如发生错误,记录错误信息
        logger.error('Something went wrong!')

Debug=True时,日志信息默认在console输出。现在我们还需要在django配置文件里配置日志(logging)相关内容,使得当Debug=False时,日志信息会输出到日志文件里或发送给系统管理员。

settings.py推荐日志配置

以下基本配置信息在django cookiecutter推荐使用的logging配置信息上做了修改,可适合大部分项目使用。如果真的希望发送和接收到邮件还需在settings.py正确配置电子邮箱Email。

# 给ADMINS发送邮件需要配置
ADMINS = (
 ('admin_name','your@gmail.com'),
)
MANAGERS = ADMINS

# 创建log文件的文件夹
LOG_DIR = os.path.join(BASE_DIR, "logs")
if not os.path.exists(LOG_DIR): 
    os.mkdir(LOG_DIR) 

# 基本配置,可以复用的
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False, # 禁用已经存在的logger实例
    "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
    "formatters": { # 定义了两种日志格式
        "verbose": { # 详细
            "format": "%(levelname)s %(asctime)s %(module)s "
            "%(process)d %(thread)d %(message)s"
        },
        'simple': { # 简单
            'format': '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d]%(message)s'
        },
    },
    "handlers": { # 定义了三种日志处理方式
        "mail_admins": { # 只有debug=False且Error级别以上发邮件给admin
            "level": "ERROR",
            "filters": ["require_debug_false"],
            "class": "django.utils.log.AdminEmailHandler",
        },
        'file': { # 对INFO级别以上信息以日志文件形式保存
            'level': "INFO", 
            'class': 'logging.handlers.RotatingFileHandler',  # 滚动生成日志,切割
            'filename': os.path.join(LOG_DIR,'django.log'),  # 日志文件名
            'maxBytes': 1024 * 1024 * 10,  # 单个日志文件最大为10M
            'backupCount': 5,  # 日志备份文件最大数量
            'formatter': 'simple', # 简单格式
            'encoding': 'utf-8', # 放置中文乱码
        },
        "console": { # 打印到终端console
            "level": "DEBUG",
            "class": "logging.StreamHandler",
            "formatter": "verbose",
        },
    },
    "root": {"level": "INFO", "handlers": ["console"]},
    "loggers": {
        "django.request": { # Django的request发生error会自动记录
            "handlers": ["mail_admins"],
            "level": "ERROR",
            "propagate": True,  # 向不向更高级别的logger传递
        },
        "django.security.DisallowedHost": { # 对于不在 ALLOWED_HOSTS 中的请求不发送报错邮件
            "level": "ERROR",
            "handlers": ["console", "mail_admins"],
            "propagate": True,
        },
    },
}

以上配置中大家最需要了解的就是Python提供的RotatingFileHandler, 其作用是滚动生成日志文件,当单个日志的文件大小达到上限时,会生成新的日志文件。当总的日志文件数量超过日志备份最大数量时删除老的日志文件。

其它日志管理工具

在前面日志配置中,我们使用了Python自带的logging模块, 另外两个常用的日志管理工具是logurusentry。我们将简单演示如何使用。

loguru

pip install loguru

安装好后在Django项目中可以直接在视图中使用,省去复杂的配置,非常便捷。它定义了日志文件名、每条记录的格式、日志文件的轮替以及过滤级别。

from loguru import logger
 
logger.add("django.log", format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}", rotation="100 MB", filter="", level="INFO", encoding='utf-8')
def my_view(request, arg1, arg2):
    ...
    if error_happens:
        logger.error("Something went wrong")

sentry

Sentry为多种语言以及各种框架(包括Django)提供了SDK。只需几行配置,sentry就会监控你的程序运行,自动收集错误和异常以及上下文数据,发送到sentry的服务器上,开发者可以通过sentry的web端实时查看错误和异常。

第一步:安装sentry-sdk

pip install --upgrade sentry-sdk

第二步:注册登录sentry,创建Django项目,获取一个公共密钥PublicKey地址,第三步会用到。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dk4Ow0C5-1655293377684)(https://pythondjango.cn/django/advanced/logging.assets/image-20210328103852219.png)]

第三步:修改settings.py,如下所示:

import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

sentry_sdk.init(
    dsn="https://examplePublicKey@o0.ingest.sentry.io/0", # 你的PublicKey
    integrations=[DjangoIntegration()],

    # Set traces_sample_rate to 1.0 to capture 100%
    # of transactions for performance monitoring.
    # We recommend adjusting this value in production,
    traces_sample_rate=1.0,

    # If you wish to associate users to errors (assuming you are using
    # django.contrib.auth) you may enable sending PII data.
    send_default_pii=True,

    # By default the SDK will try to use the SENTRY_RELEASE
    # environment variable, or infer a git commit
    # SHA as release, however you may want to set
    # something more human-readable.
    # release="myapp@1.0.0",
)

配置就这么简单,你以后都可以通过sentry的web端直接查看Django项目运行的错误和异常了。

小结

日志对于捕捉生产环境里程序的错误和异常非常重要。本文介绍了日志的基础知识以及如何在Django项目中配置日志,比如logging, loguru和sentry。你最喜欢哪个呢?

Django国际化

展示效果

第一步 修改settings.py配置

第二步 修改项目的urls.py

第三步 在.py, .html和.txt文件中标记需要翻译的字符串

第四步 生成.po和.mo编译消息文件

Windows用户注意事项

小结

展示效果

本例展示效果如下,用户选择不同语言,网站即显示翻译过后的内容, 包括footer部分的文本。本例基于Python 3.7和Django 3.0开发,前端boostrap 4, 无需额外的第三方库支持。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QNCHb00z-1655293377684)(https://pythondjango.cn/django/advanced/internationalization.assets/2.gif)]

到此你只需记住一点:我们实现了中英对照翻译,但这个翻译不是浏览器翻译的,Django也不会帮你翻译。这个需要你自己事先手动翻译好,存放在专门翻译文件中,Django只是事后调用而已。

第一步 修改settings.py配置

假设你已经创建了一个myproject的项目,那么为了支持国际化/多语种,你首先应做两件事:

  • 在myproject目录下新建locale文件夹,用于保存翻译消息文件(.po和.mo格式的)
  • 修改配置文件settings.py

settings.py主要设置如下所示:

from django.utils.translation import ugettext_lazy as _

# 默认语言
LANGUAGE_CODE = 'en-us'

# 设置I18n和L10N为True
USE_I18N = True
USE_L10N = True

# 指定支持语言。这里为了简化只支持简体中文和英文
LANGUAGES = (
    ('en', _('English')),
    ('zh-hans', _('Simplified Chinese')),
)

# 用于存放django.po和django.mo编译过的翻译文件
PROJECT_ROOT = os.path.dirname(os.path.realpath(__name__))
LOCALE_PATHS = (
    os.path.join(PROJECT_ROOT, 'locale'),
)

在这里我们使用了ugettext_lazy这个方法,它的作用是在.py文件文件中标记需要翻译的字符串,对其进行惰性参照存储,而不是对字符串进行真正的翻译。我们经常会在models.py中定义字段时用到它,也会在views.py中用到它,指定需要翻译的字符串。然而在模板html文件中我们并不能直接使用ugettext_lazy这个方法,而是使用{% trans "string" %}, {% blocktrans%} {%endblocktrans %}这两个标签来标记需要翻译的字符串。

注意:这两个标签也不是对字符串进行真正的翻译,只是标记而已。使用这两个模板标签前需要在模板的最开始地方加入{% load i18n %}

最后别忘了加入LocaleMiddleware这个中间件。它的位置也很重要,应于SessionMiddleware之后,CommonMiddleware之前。

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware', # 新增多语支持
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

小知识

i18n是国际化(Internationalization)的缩写。i 和 n 之间有 18 个字母,简称 I18N,。l10n是本地化(localization)的缩写。l 和 n 之间有 10 个字母,简称 L10N。

第二步 修改项目的urls.py

本步动作是为了增加对国际化i18n的支持,具体代码如下所示(注意:这里是项目目录下urls.py, 不是app下urls.py)。i18n_patterns的作用是让每个url前面自动加上所选语言的代码,比如/en/, /zh-hans/等等。

from django.contrib import admin
from django.urls import path, include
from django.conf.urls.i18n import i18n_patterns


urlpatterns = [
    path('i18n/', include('django.conf.urls.i18n')),
]


urlpatterns += i18n_patterns(
    path('admin/', admin.site.urls),
    path('', include('myapp.urls')),
)

实际上现在你已经可以通过访问Django自带的admin来检验你的国际化设置是否成功了。分别访问http://127.0.0.1:8000/en/admin/和http://127.0.0.1:8000/zh-hans/admin/你将看到不同语言版本,如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PaJn53N1-1655293377685)(https://pythondjango.cn/django/advanced/internationalization.assets/1.gif)]

第三步 在.py, .html和.txt文件中标记需要翻译的字符串

Django支持在python文件,html和txt文本文件中标记需要翻译的字符串。在python文件中使用ugettext_lazy方法或则简写的_,在html和txt文件中使用transblocktrans标签, 使用前还需在文件开头加入 {% load i18n %}。重要的话多说几遍也无妨。

本例中我们不需要使用到模型,只需开发一个url,所以只需要一个视图(index)和模板(index.html)。相应内容如下所示:

# myapp/urls.py
from django.urls import path
from . import views

app_name = 'myapp'
urlpatterns = (
    path('', views.index, name='index'),
)

在视图中我们使用ugettext_lazy方法标记了一个需要翻译的字符串”Welcome to China”。

# myapp/views.py
from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _

# Create your views here.
def index(request):
    context = {'msg': _("Welcome to China")}
    return render(request, 'myapp/index.html', context)

模板文件较长,请全文复制,解读在后面。模板路径为myapp/templates/myapp/index.html

<!DOCTYPE html>
{% load static %}
{% load i18n %}
{% load core_tags_filters %}
<html lang="en">
  <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   <meta name="description" content="">
   <title>Django房产网</title>
   <link rel="canonical" href="https://getbootstrap.com/docs/4.5/examples/">
   <!-- Bootstrap core CSS -->
   <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
      <style>
   body {
      overflow-x: hidden; /* Prevent scroll on narrow devices */
     font-family: 'Inter', Arial, sans-serif;
     background: #F6F8FF;
     padding-top: 56px;
   }
</style>
  </head>
  <body>
     <nav class="navbar navbar-expand-md fixed-top navbar-light box-shadow bg-white">
     <div class="container">
       <a class="navbar-brand align-items-md-center" href="index.html">Django双语示例</a>
           <form class="form-inline ml-3-md" action="{% url 'set_language' %}" method="post">
               {% csrf_token %}
               <div class="input-group">
                    <input name="next" type="hidden" value="{{ redirect_to }}" />
                    <select name="language" class="form-control">
                        {% get_current_language as LANGUAGE_CODE %}
                        {% get_available_languages as LANGUAGES %}
                        {% get_language_info_list for LANGUAGES as languages %}
                        {% for language in languages %}
                            <option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
                                {{ language.name_local }}
                            </option>
                        {% endfor %}
                    </select>
                   <div class="input-group-append">
                    <button type="submit" class="btn btn-inline btn-sm bg-warning">
                        {% trans "Select" %}
                    </button>
                   </div>
               </div>
        </form>
     </div>
    </nav>
  {% block content %}
<div class="py-4 px-3 bg-light">
     <div class="container">
         {% get_current_language as LANGUAGE_CODE %}
         <h4>{% trans 'Current language code' %}: {{ LANGUAGE_CODE }}</b> </h4>
         <p><small>{% trans "Welcome to our page" %}</small></p>
         <hr/>
         <p>{% blocktrans %} {{ msg }} {% endblocktrans %}</p>
     </div>
</div>
{% endblock %}


<footer class="bd-footer bg-light">
    <div class="container pt-3 pb-2 px-3 px-md-2">
      <ul class="bd-footer-links list-unstyled text-muted list-inline pb-2">
        <li class="list-inline-item">
        <small>&#169; Example.com</small>
        </li>
        <li class="list-inline-item right"><a href="https://twitter.com/getbootstrap"><small>{% trans 'Privacy and Cookie' %}</small></a></li>
        <li class="list-inline-item right"><a href="/docs/4.5/examples/"><small>{% trans 'Terms and Conditions' %}</small></a></li>
      </ul>
   </div>
</footer>
  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
</html> 

模板文件中最重要的一段代码如下所示。

{% block content %}
<div class="py-4 px-3 bg-light">
     <div class="container">
    {% get_current_language as LANGUAGE_CODE %}
         <h4>{% trans 'Current language code' %}: {{ LANGUAGE_CODE }}</b> </h4>
         <p><small>{% trans "Welcome to our page" %}</small></p>
         <hr/>
         <p>{% blocktrans %} {{ msg }} {% endblocktrans %}</p>
     </div>
</div>
{% endblock %} 

{% load i18n %}以后,你可以使用get_current_language标签获得当前语言。我们还在模板中分别使用{% trans "string" %}{% blocktrans%} {%endblocktrans %}标签来标记了两个需要翻译的字符串,一个是模板中已存在的字符串,一个是视图函数传递过来的变量。

另外模板中还有一段通过session用于切换语言的标准代码,大家可以随时拿去用。

<form class="form-inline ml-3-md" action="{% url 'set_language' %}" method="post">
      {% csrf_token %}
      <div class="input-group">
           <input name="next" type="hidden" value="{{ redirect_to }}" />
             <select name="language" class="form-control">
                {% get_current_language as LANGUAGE_CODE %}
                    {% get_available_languages as LANGUAGES %}
                     {% get_language_info_list for LANGUAGES as languages %}
                      {% for language in languages %}
                        <option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
                            {{ language.name_local }}
                         </option>
                       {% endfor %}
                    </select>
                   <div class="input-group-append">
                    <button type="submit" class="btn btn-inline btn-sm bg-warning">
                 {% trans "Select" %}
                    </button>
                   </div>
               </div>
</form>

此时你启动测试服务器,应该能看到页面已经能正确显示。但是当你切换不同语言时,页面显示的内容还是一样的,这是因为我们还没对那些字符串进行翻译啊。

第四步 生成.po和.mo编译消息文件

现在到了关键步骤了,也是最后一步。我们需要对前面标记的字符串进行手动翻译,并生成编译过后的消息文件供Django使用。新手最容易犯的错误就是以为trans标签能实现自动翻译,以为国际化就到此为止了。整个过程一共有3步, 请仔细阅读别走错。

第一步: 进入项目文件夹,使用django-admin makemessages -l zh_HANS命令提取所有前面标记需要翻译的字符串。该命令会自动生成一个名为django.po的文件,地址如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qv0YnG7I-1655293377685)(https://pythondjango.cn/django/advanced/internationalization.assets/4.png)]

注意:这里是zh_HANS是下划线的形式,而不是语言代码简写zh-hans

django.po文件刚开始内容如下所示。你可以看到需要翻译的字符串,但翻译内容为空。

#: .\house\templates\house\index.html:27
msgid "Welcome to our page"
msgstr ""


#: .\house\templates\house\index.html:29 .\house\templates\house\index1.html:65
#, python-format
msgid " %(msg)s "
msgstr ""


#: .\house\templates\house\index1.html:48
msgid "Select"
msgstr ""


#: .\house\templates\house\index1.html:60
#, fuzzy
msgid "Welcome to our website"
msgstr ""

第二步:修改django.po文件,添加手动翻译的字符串。

#: .\house\templates\house\index.html:27
msgid "Welcome to our page"
msgstr "欢迎来到我们主页"

#: .\house\templates\house\index.html:29 .\house\templates\house\index1.html:65
#, python-format
msgid " %(msg)s "
msgstr ""

#: .\house\templates\house\index1.html:48
msgid "Select"
msgstr "选择"

#: .\house\templates\house\index1.html:60
#, fuzzy
msgid "Welcome to our website"
msgstr "欢迎来到我们网站"

第三步:使用python manage.py compilemessages命令生成翻译编译文件。

该命令会生成一个django.mo的文件,内容如下所示。这个就是Django最后需要调用的翻译文件,里面包含了翻译过后的字符串列表。

Report-Msgid-Bugs-To: 
PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
Last-Translator: FULL NAME <EMAIL@ADDRESS>
Language-Team: LANGUAGE <LL@li.org>
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plural-Forms: nplurals=1; plural=0;
 当前语言代码 英语 隐私与Cookie 选择 简体中文 服务条款与协议 欢迎来到中国 欢迎来到我们主页

现在你重新启动服务器就应该可以看到文初展示的效果了,神不神奇?

Windows用户注意事项

windows系统下使用makemessagescompilemessages命令时会出现错误,这是因为windows缺少基于GNU的gettext模块。安装方式如下:

  1. https://mlocati.github.io/articles/gettext-iconv-windows.html下载相应版本,安装或解压缩到C盘或D盘, 比如C:\Program Files (x86)\gettext
  2. 把gettex下的bin地址,比如C:\Program Files (x86)\gettext\bin加入到系统PATH的环境变量(在控制面板>系统>高级>环境变量中添加)。
  3. 如果pycharm的terminal中运行两个命令有问题,请直接在windows的cmd窗口运行。

Linux系统如果缺少可以使用sudo apt-get install gettext安装。

小结

本文以实例展示了如何让Django开发的网站支持多语种,其中技能你get到了吗? 你是不是也曾经以为ugettext_lazy方法, transblocktrans标签会自动帮你翻译文本呢

展示效果

本例展示效果如下,用户选择不同语言,网站即显示翻译过后的内容, 包括footer部分的文本。本例基于Python 3.7和Django 3.0开发,前端boostrap 4, 无需额外的第三方库支持。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uvT1RztK-1655293377685)(https://pythondjango.cn/django/advanced/internationalization.assets/2.gif)]

到此你只需记住一点:我们实现了中英对照翻译,但这个翻译不是浏览器翻译的,Django也不会帮你翻译。这个需要你自己事先手动翻译好,存放在专门翻译文件中,Django只是事后调用而已。

第一步 修改settings.py配置

假设你已经创建了一个myproject的项目,那么为了支持国际化/多语种,你首先应做两件事:

  • 在myproject目录下新建locale文件夹,用于保存翻译消息文件(.po和.mo格式的)
  • 修改配置文件settings.py

settings.py主要设置如下所示:

from django.utils.translation import ugettext_lazy as _

# 默认语言
LANGUAGE_CODE = 'en-us'

# 设置I18n和L10N为True
USE_I18N = True
USE_L10N = True

# 指定支持语言。这里为了简化只支持简体中文和英文
LANGUAGES = (
    ('en', _('English')),
    ('zh-hans', _('Simplified Chinese')),
)

# 用于存放django.po和django.mo编译过的翻译文件
PROJECT_ROOT = os.path.dirname(os.path.realpath(__name__))
LOCALE_PATHS = (
    os.path.join(PROJECT_ROOT, 'locale'),
)

在这里我们使用了ugettext_lazy这个方法,它的作用是在.py文件文件中标记需要翻译的字符串,对其进行惰性参照存储,而不是对字符串进行真正的翻译。我们经常会在models.py中定义字段时用到它,也会在views.py中用到它,指定需要翻译的字符串。然而在模板html文件中我们并不能直接使用ugettext_lazy这个方法,而是使用{% trans "string" %}, {% blocktrans%} {%endblocktrans %}这两个标签来标记需要翻译的字符串。

注意:这两个标签也不是对字符串进行真正的翻译,只是标记而已。使用这两个模板标签前需要在模板的最开始地方加入{% load i18n %}

最后别忘了加入LocaleMiddleware这个中间件。它的位置也很重要,应于SessionMiddleware之后,CommonMiddleware之前。

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware', # 新增多语支持
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

小知识

i18n是国际化(Internationalization)的缩写。i 和 n 之间有 18 个字母,简称 I18N,。l10n是本地化(localization)的缩写。l 和 n 之间有 10 个字母,简称 L10N。

第二步 修改项目的urls.py

本步动作是为了增加对国际化i18n的支持,具体代码如下所示(注意:这里是项目目录下urls.py, 不是app下urls.py)。i18n_patterns的作用是让每个url前面自动加上所选语言的代码,比如/en/, /zh-hans/等等。

from django.contrib import admin
from django.urls import path, include
from django.conf.urls.i18n import i18n_patterns


urlpatterns = [
    path('i18n/', include('django.conf.urls.i18n')),
]


urlpatterns += i18n_patterns(
    path('admin/', admin.site.urls),
    path('', include('myapp.urls')),
)

实际上现在你已经可以通过访问Django自带的admin来检验你的国际化设置是否成功了。分别访问http://127.0.0.1:8000/en/admin/和http://127.0.0.1:8000/zh-hans/admin/你将看到不同语言版本,如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JVWyGFyP-1655293377685)(https://pythondjango.cn/django/advanced/internationalization.assets/1.gif)]

第三步 在.py, .html和.txt文件中标记需要翻译的字符串

Django支持在python文件,html和txt文本文件中标记需要翻译的字符串。在python文件中使用ugettext_lazy方法或则简写的_,在html和txt文件中使用transblocktrans标签, 使用前还需在文件开头加入 {% load i18n %}。重要的话多说几遍也无妨。

本例中我们不需要使用到模型,只需开发一个url,所以只需要一个视图(index)和模板(index.html)。相应内容如下所示:

# myapp/urls.py
from django.urls import path
from . import views

app_name = 'myapp'
urlpatterns = (
    path('', views.index, name='index'),
)

在视图中我们使用ugettext_lazy方法标记了一个需要翻译的字符串”Welcome to China”。

# myapp/views.py
from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _

# Create your views here.
def index(request):
    context = {'msg': _("Welcome to China")}
    return render(request, 'myapp/index.html', context)

模板文件较长,请全文复制,解读在后面。模板路径为myapp/templates/myapp/index.html

<!DOCTYPE html>
{% load static %}
{% load i18n %}
{% load core_tags_filters %}
<html lang="en">
  <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   <meta name="description" content="">
   <title>Django房产网</title>
   <link rel="canonical" href="https://getbootstrap.com/docs/4.5/examples/">
   <!-- Bootstrap core CSS -->
   <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
      <style>
   body {
      overflow-x: hidden; /* Prevent scroll on narrow devices */
     font-family: 'Inter', Arial, sans-serif;
     background: #F6F8FF;
     padding-top: 56px;
   }
</style>
  </head>
  <body>
     <nav class="navbar navbar-expand-md fixed-top navbar-light box-shadow bg-white">
     <div class="container">
       <a class="navbar-brand align-items-md-center" href="index.html">Django双语示例</a>
           <form class="form-inline ml-3-md" action="{% url 'set_language' %}" method="post">
               {% csrf_token %}
               <div class="input-group">
                    <input name="next" type="hidden" value="{{ redirect_to }}" />
                    <select name="language" class="form-control">
                        {% get_current_language as LANGUAGE_CODE %}
                        {% get_available_languages as LANGUAGES %}
                        {% get_language_info_list for LANGUAGES as languages %}
                        {% for language in languages %}
                            <option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
                                {{ language.name_local }}
                            </option>
                        {% endfor %}
                    </select>
                   <div class="input-group-append">
                    <button type="submit" class="btn btn-inline btn-sm bg-warning">
                        {% trans "Select" %}
                    </button>
                   </div>
               </div>
        </form>
     </div>
    </nav>
  {% block content %}
<div class="py-4 px-3 bg-light">
     <div class="container">
         {% get_current_language as LANGUAGE_CODE %}
         <h4>{% trans 'Current language code' %}: {{ LANGUAGE_CODE }}</b> </h4>
         <p><small>{% trans "Welcome to our page" %}</small></p>
         <hr/>
         <p>{% blocktrans %} {{ msg }} {% endblocktrans %}</p>
     </div>
</div>
{% endblock %}


<footer class="bd-footer bg-light">
    <div class="container pt-3 pb-2 px-3 px-md-2">
      <ul class="bd-footer-links list-unstyled text-muted list-inline pb-2">
        <li class="list-inline-item">
        <small>&#169; Example.com</small>
        </li>
        <li class="list-inline-item right"><a href="https://twitter.com/getbootstrap"><small>{% trans 'Privacy and Cookie' %}</small></a></li>
        <li class="list-inline-item right"><a href="/docs/4.5/examples/"><small>{% trans 'Terms and Conditions' %}</small></a></li>
      </ul>
   </div>
</footer>
  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
</html> 

模板文件中最重要的一段代码如下所示。

{% block content %}
<div class="py-4 px-3 bg-light">
     <div class="container">
    {% get_current_language as LANGUAGE_CODE %}
         <h4>{% trans 'Current language code' %}: {{ LANGUAGE_CODE }}</b> </h4>
         <p><small>{% trans "Welcome to our page" %}</small></p>
         <hr/>
         <p>{% blocktrans %} {{ msg }} {% endblocktrans %}</p>
     </div>
</div>
{% endblock %} 

{% load i18n %}以后,你可以使用get_current_language标签获得当前语言。我们还在模板中分别使用{% trans "string" %}{% blocktrans%} {%endblocktrans %}标签来标记了两个需要翻译的字符串,一个是模板中已存在的字符串,一个是视图函数传递过来的变量。

另外模板中还有一段通过session用于切换语言的标准代码,大家可以随时拿去用。

<form class="form-inline ml-3-md" action="{% url 'set_language' %}" method="post">
      {% csrf_token %}
      <div class="input-group">
           <input name="next" type="hidden" value="{{ redirect_to }}" />
             <select name="language" class="form-control">
                {% get_current_language as LANGUAGE_CODE %}
                    {% get_available_languages as LANGUAGES %}
                     {% get_language_info_list for LANGUAGES as languages %}
                      {% for language in languages %}
                        <option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
                            {{ language.name_local }}
                         </option>
                       {% endfor %}
                    </select>
                   <div class="input-group-append">
                    <button type="submit" class="btn btn-inline btn-sm bg-warning">
                 {% trans "Select" %}
                    </button>
                   </div>
               </div>
</form>

此时你启动测试服务器,应该能看到页面已经能正确显示。但是当你切换不同语言时,页面显示的内容还是一样的,这是因为我们还没对那些字符串进行翻译啊。

第四步 生成.po和.mo编译消息文件

现在到了关键步骤了,也是最后一步。我们需要对前面标记的字符串进行手动翻译,并生成编译过后的消息文件供Django使用。新手最容易犯的错误就是以为trans标签能实现自动翻译,以为国际化就到此为止了。整个过程一共有3步, 请仔细阅读别走错。

第一步: 进入项目文件夹,使用django-admin makemessages -l zh_HANS命令提取所有前面标记需要翻译的字符串。该命令会自动生成一个名为django.po的文件,地址如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZuEloiQW-1655293377686)(https://pythondjango.cn/django/advanced/internationalization.assets/4.png)]

注意:这里是zh_HANS是下划线的形式,而不是语言代码简写zh-hans

django.po文件刚开始内容如下所示。你可以看到需要翻译的字符串,但翻译内容为空。

#: .\house\templates\house\index.html:27
msgid "Welcome to our page"
msgstr ""


#: .\house\templates\house\index.html:29 .\house\templates\house\index1.html:65
#, python-format
msgid " %(msg)s "
msgstr ""


#: .\house\templates\house\index1.html:48
msgid "Select"
msgstr ""


#: .\house\templates\house\index1.html:60
#, fuzzy
msgid "Welcome to our website"
msgstr ""

第二步:修改django.po文件,添加手动翻译的字符串。

#: .\house\templates\house\index.html:27
msgid "Welcome to our page"
msgstr "欢迎来到我们主页"

#: .\house\templates\house\index.html:29 .\house\templates\house\index1.html:65
#, python-format
msgid " %(msg)s "
msgstr ""

#: .\house\templates\house\index1.html:48
msgid "Select"
msgstr "选择"

#: .\house\templates\house\index1.html:60
#, fuzzy
msgid "Welcome to our website"
msgstr "欢迎来到我们网站"

第三步:使用python manage.py compilemessages命令生成翻译编译文件。

该命令会生成一个django.mo的文件,内容如下所示。这个就是Django最后需要调用的翻译文件,里面包含了翻译过后的字符串列表。

Report-Msgid-Bugs-To: 
PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
Last-Translator: FULL NAME <EMAIL@ADDRESS>
Language-Team: LANGUAGE <LL@li.org>
Language: 
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plural-Forms: nplurals=1; plural=0;
 当前语言代码 英语 隐私与Cookie 选择 简体中文 服务条款与协议 欢迎来到中国 欢迎来到我们主页

现在你重新启动服务器就应该可以看到文初展示的效果了,神不神奇?

Windows用户注意事项

windows系统下使用makemessagescompilemessages命令时会出现错误,这是因为windows缺少基于GNU的gettext模块。安装方式如下:

  1. https://mlocati.github.io/articles/gettext-iconv-windows.html下载相应版本,安装或解压缩到C盘或D盘, 比如C:\Program Files (x86)\gettext
  2. 把gettex下的bin地址,比如C:\Program Files (x86)\gettext\bin加入到系统PATH的环境变量(在控制面板>系统>高级>环境变量中添加)。
  3. 如果pycharm的terminal中运行两个命令有问题,请直接在windows的cmd窗口运行。

Linux系统如果缺少可以使用sudo apt-get install gettext安装。

小结

本文以实例展示了如何让Django开发的网站支持多语种,其中技能你get到了吗? 你是不是也曾经以为ugettext_lazy方法, transblocktrans标签会自动帮你翻译文本呢?

Docker部署Django

  1. Docker及Docker-Compose的安装
  2. Django + Uwsgi + Nginx + MySQL + Redis组合容器示意图
  3. Docker-compose部署Django项目布局树形图
  4. 第一步:编写docker-compose.yml文件
  5. 第二步:编写Web (Django+Uwsgi)镜像和容器所需文件
  6. 第三步:编写Nginx镜像和容器所需文件
  7. 第四步:编写Db (MySQL)容器配置文件
  8. 第五步:编写Redis 容器配置文件
  9. 第六步:修改Django项目settings.py
  10. 第七步:使用docker-compose 构建镜像并启动容器组服务
  11. 第八步:排错
    1. Nginx容器排错
    2. Web容器排错
    3. 数据库db容器排错
  12. 小结

Docker及Docker-Compose的安装

学习本教程前首先我们要在Linux服务器上安装Docker及Docker-Compose。菜鸟教程上总结了Docker在各个平台和系统上的安装,大家可以参考。这里总结了下Docker及Docker-compose在阿里云Ubuntu系统上的安装过程。步骤看似很多且复杂,但大家只需要一步一步copy和paste命令就行了,整个安装过程很流畅。

# 以Ubuntu为例
# Step 1: 移除之前docker版本并更新更新 apt 包索引
sudo apt-get remove docker docker-engine docker.io
sudo apt-get update

# Step 2: 安装 apt 依赖包,用于通过HTTPS来获取仓库
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common

# Step 3: 添加 Docker 的官方 GPG 密钥
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -

# Step 4: 设置docker稳定版仓库,这里使用了阿里云仓库
sudo add-apt-repository "deb [arch=amd64] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"
sudo apt-get update

# Step 5: 安装免费的docker Community版本docker-ce
sudo apt-get -y install docker-ce
# sudo apt-get install -y docker-ce=<VERSION> #该命令可以选择docker-ce版本

# Step 6: 查看docker版本及运行状态
sudo docker -v
sudo systemctl status docker

# Step 7:本步非必需。使用阿里云设置Docker镜像加速,注意下面链接请使用阿里云给自己的URL
sudo mkdir -p /etc/docker 
sudo tee /etc/docker/daemon.json <<-'EOF' 
{  "registry-mirrors": ["https://ua3456xxx.mirror.aliyuncs.com"] } 
EOF 
sudo systemctl daemon-reload 
sudo systemctl restart docker

# Step 8: 以ubuntu为例,下载docker-compose
$ sudo curl -L https://github.com/docker/compose/releases/download/1.17.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose

# Step 9: 给予docker-compose可执行权限
$ sudo chmod +x /usr/local/bin/docker-compose

# Step 10: 查看docker-compose版本
$ docker-compose --version

Django + Uwsgi + Nginx + MySQL + Redis组合容器示意图

本例中我们将使用docker-compose编排并启动4个容器,这个更接近于实际生成环境下的部署。

  1. Django + Uwsgi容器:核心应用程序,处理动态请求
  2. MySQL 容器:数据库服务
  3. Redis 容器:缓存服务
  4. Nginx容器:反向代理服务并处理静态资源请求

这四个容器的依赖关系是:Django+Uwsgi 容器依赖 Redis 容器和 MySQL 容器,Nginx 容器依赖Django+Uwsgi容器。为了方便容器间的相互访问和通信,我们使用docker-compose时可以给每个容器取个别名,这样访问容器时就可以直接使用别名访问,而不使用Docker临时给容器分配的IP了。

这四个容器的别名及通信端口如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MvgrH5dT-1655293377686)(https://pythondjango.cn/django/advanced/12-internationalization.assets/image-20210419144633551.png)]

Docker-compose部署Django项目布局树形图

我们新建了一个compose文件夹,专门存放用于构建其它容器镜像的Dockerfile及配置文件。compose文件夹与django项目的根目录myproject同级。这样做的好处是不同的django项目可以共享compose文件夹。

myproject_docker # 项目根目录
├── compose # 存放各项容器服务的Dockerfile和配置文件
│   ├── mysql
│   │   ├── conf
│   │   │   └── my.cnf # MySQL配置文件
│   │   └── init
│   │       └── init.sql # MySQL启动脚本
│   ├── nginx
│   │   ├── Dockerfile # 构建Nginx镜像所的Dockerfile
│   │   ├── log # 挂载保存nginx容器内日志log目录
│   │   ├── nginx.conf # Nginx配置文件
│   │   └── ssl # 如果需要配置https需要用到
│   ├── redis
│   │   └── redis.conf # redis配置文件
│   └── uwsgi # 挂载保存django+uwsgi容器内uwsgi日志
├── docker-compose.yml # 核心编排文件
└── myproject # 常规Django项目目录
    ├── Dockerfile # 构建Django+Uwsgi镜像的Dockerfile
    ├── apps # 存放Django项目的各个apps
    ├── manage.py
    ├── myproject # Django项目配置文件
    │   ├── asgi.py
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── pip.conf # 非必需。pypi源设置成国内,加速pip安装
    ├── requirements.txt # Django项目依赖文件
    ├── .env # 环境变量文件
    ├── start.sh # 启动Django+Uwsgi容器后要执行的脚本
    ├── media # 用户上传的媒体资源,如果没有需手动创建
    ├── static #搜集项目的静态文件夹,如果没有需手动创建
    └── uwsgi.ini # uwsgi配置文件

下面我们开始正式部署。

第一步:编写docker-compose.yml文件

修改过的docker-compose.yml的核心内容如下。我们定义了4个数据卷,用于挂载各个容器内动态生成的数据,比如MySQL的存储数据,redis生成的快照、django+uwsgi容器中收集的静态文件以及用户上传的媒体资源。这样即使删除容器,容器内产生的数据也不会丢失。

我们还定义了3个网络,分别为nginx_network(用于nginx和web容器间的通信),db_network(用于db和web容器间的通信)和redis_network(用于redis和web容器间的通信)。

整个编排里包含4项容器服务,别名分别为redis, db, nginxweb,接下来我们将依次看看各个容器的Dockerfile和配置文件。

version: "3"

volumes: # 自定义数据卷
  db_vol: #定义数据卷同步存放容器内mysql数据
  redis_vol: #定义数据卷同步存放redis数据
  media_vol: #定义数据卷同步存放web项目用户上传到media文件夹的数据
  static_vol: #定义数据卷同步存放web项目static文件夹的数据

networks: # 自定义网络(默认桥接), 不使用links通信
  nginx_network:
    driver: bridge
  db_network:
    driver: bridge
  redis_network: 
    driver: bridge

services:
  redis:
    image: redis:latest
    command: redis-server /etc/redis/redis.conf # 容器启动后启动redis服务器
    networks:
      - redis_network
    volumes:
      - redis_vol:/data # 通过挂载给redis数据备份
      - ./compose/redis/redis.conf:/etc/redis/redis.conf # 挂载redis配置文件
    ports:
      - "6379:6379"
    restart: always # always表容器运行发生错误时一直重启

  db:
    image: mysql
    env_file:  
      - ./myproject/.env # 使用了环境变量文件
    networks:  
      - db_network
    volumes:
      - db_vol:/var/lib/mysql:rw # 挂载数据库数据, 可读可写
      - ./compose/mysql/conf/my.cnf:/etc/mysql/my.cnf # 挂载配置文件
      - ./compose/mysql/init:/docker-entrypoint-initdb.d/ # 挂载数据初始化sql脚本
    ports:
      - "3306:3306" # 与配置文件保持一致
    restart: always

  web:
    build: ./myproject
    expose:
      - "8000"
    volumes:
      - ./myproject:/var/www/html/myproject # 挂载项目代码
      - static_vol:/var/www/html/myproject/static # 以数据卷挂载容器内static文件
      - media_vol:/var/www/html/myproject/media # 以数据卷挂载容器内用户上传媒体文件
      - ./compose/uwsgi:/tmp # 挂载uwsgi日志
    networks:
      - nginx_network
      - db_network  
      - redis_network 
    depends_on:
      - db
      - redis
    restart: always
    tty: true
    stdin_open: true

  nginx:
    build: ./compose/nginx
    ports:
      - "80:80"
      - "443:443"
    expose:
      - "80"
    volumes:
      - ./compose/nginx/nginx.conf:/etc/nginx/conf.d/nginx.conf # 挂载nginx配置文件
      - ./compose/nginx/ssl:/usr/share/nginx/ssl # 挂载ssl证书目录
      - ./compose/nginx/log:/var/log/nginx # 挂载日志
      - static_vol:/usr/share/nginx/html/static # 挂载静态文件
      - media_vol:/usr/share/nginx/html/media # 挂载用户上传媒体文件
    networks:
      - nginx_network
    depends_on:
      - web
    restart: always

第二步:编写Web (Django+Uwsgi)镜像和容器所需文件

构建Web镜像(Django+Uwsgi)的所使用的Dockerfile如下所示:

# 建立 python 3.9环境
FROM python:3.9

# 安装netcat
RUN apt-get update && apt install -y netcat

# 镜像作者大江狗
MAINTAINER DJG

# 设置 python 环境变量
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# 可选:设置镜像源为国内
COPY pip.conf /root/.pip/pip.conf

# 容器内创建 myproject 文件夹
ENV APP_HOME=/var/www/html/myproject
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME

# 将当前目录加入到工作目录中(. 表示当前目录)
ADD . $APP_HOME

# 更新pip版本
RUN /usr/local/bin/python -m pip install --upgrade pip

# 安装项目依赖
RUN pip install -r requirements.txt

# 移除\r in windows
RUN sed -i 's/\r//' ./start.sh

# 给start.sh可执行权限
RUN chmod +x ./start.sh

# 数据迁移,并使用uwsgi启动服务
ENTRYPOINT /bin/bash ./start.sh

本Django项目所依赖的requirements.txt内容如下所示:

# django
django==3.2
# uwsgi
uwsgi==2.0.18
# mysql
mysqlclient==1.4.6
# redis
django-redis==4.12.1
redis==3.5.3
# for images
Pillow==8.2.0 

start.sh启动脚本文件内容如下所示。最重要的是最后一句,使用uwsgi.ini配置文件启动Django服务。

#!/bin/bash
# 从第一行到最后一行分别表示:
# 1. 等待MySQL服务启动后再进行数据迁移。nc即netcat缩写
# 2. 收集静态文件到根目录static文件夹,
# 3. 生成数据库可执行文件,
# 4. 根据数据库可执行文件来修改数据库
# 5. 用 uwsgi启动 django 服务
# 6. tail空命令防止web容器执行脚本后退出
while ! nc -z db 3306 ; do
    echo "Waiting for the MySQL Server"
    sleep 3
done

python manage.py collectstatic --noinput&&
python manage.py makemigrations&&
python manage.py migrate&&
uwsgi --ini /var/www/html/myproject/uwsgi.ini&&
tail -f /dev/null

exec "$@"

uwsgi.ini配置文件如下所示:

[uwsgi]

project=myproject
uid=www-data
gid=www-data
base=/var/www/html

chdir=%(base)/%(project)
module=%(project).wsgi:application
master=True
processes=2

socket=0.0.0.0:8000
chown-socket=%(uid):www-data
chmod-socket=664

vacuum=True
max-requests=5000

pidfile=/tmp/%(project)-master.pid
daemonize=/tmp/%(project)-uwsgi.log

#设置一个请求的超时时间(秒),如果一个请求超过了这个时间,则请求被丢弃
harakiri = 60
post buffering = 8192
buffer-size= 65535
#当一个请求被harakiri杀掉会,会输出一条日志
harakiri-verbose = true

#开启内存使用情况报告
memory-report = true

#设置平滑的重启(直到处理完接收到的请求)的长等待时间(秒)
reload-mercy = 10

#设置工作进程使用虚拟内存超过N MB就回收重启
reload-on-as= 1024

第三步:编写Nginx镜像和容器所需文件

构建Nginx镜像所使用的Dockerfile如下所示:

# nginx镜像compose/nginx/Dockerfile

FROM nginx:latest

# 删除原有配置文件,创建静态资源文件夹和ssl证书保存文件夹
RUN rm /etc/nginx/conf.d/default.conf \
&& mkdir -p /usr/share/nginx/html/static \
&& mkdir -p /usr/share/nginx/html/media \
&& mkdir -p /usr/share/nginx/ssl

# 设置Media文件夹用户和用户组为Linux默认www-data, 并给予可读和可执行权限,
# 否则用户上传的图片无法正确显示。
RUN chown -R www-data:www-data /usr/share/nginx/html/media \
&& chmod -R 775 /usr/share/nginx/html/media

# 添加配置文件
ADD ./nginx.conf /etc/nginx/conf.d/

# 关闭守护模式
CMD ["nginx", "-g", "daemon off;"]

Nginx的配置文件如下所示:

# nginx配置文件
# compose/nginx/nginx.conf

upstream django {
    ip_hash;
    server web:8000; # Docker-compose web服务端口
}

# 配置http请求,80端口
server {
    listen 80; # 监听80端口
    server_name 127.0.0.1; # 可以是nginx容器所在ip地址或127.0.0.1,不能写宿主机外网ip地址

    charset utf-8;
    client_max_body_size 10M; # 限制用户上传文件大小

    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log warn;

    location /static {
        alias /usr/share/nginx/html/static; # 静态资源路径
    }

    location /media {
        alias /usr/share/nginx/html/media; # 媒体资源,用户上传文件路径
    }

    location / {
        include /etc/nginx/uwsgi_params;
        uwsgi_pass django;
        uwsgi_read_timeout 600;
        uwsgi_connect_timeout 600;
        uwsgi_send_timeout 600;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_redirect off;
        proxy_set_header X-Real-IP  $remote_addr;
       # proxy_pass http://django;  # 使用uwsgi通信,而不是http,所以不使用proxy_pass。
    }
}

第四步:编写Db (MySQL)容器配置文件

启动MySQL容器我们直接使用官方镜像即可,不过我们需要给MySQL增加配置文件。

# compose/mysql/conf/my.cnf
[mysqld]
user=mysql
default-storage-engine=INNODB
character-set-server=utf8
secure-file-priv=NULL # mysql 8 新增这行配置
default-authentication-plugin=mysql_native_password  # mysql 8 新增这行配置

port            = 3306 # 端口与docker-compose里映射端口保持一致
#bind-address= localhost #一定要注释掉,mysql所在容器和django所在容器不同IP

basedir         = /usr
datadir         = /var/lib/mysql
tmpdir          = /tmp
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
skip-name-resolve  # 这个参数是禁止域名解析的,远程访问推荐开启skip_name_resolve。

[client]
port = 3306
default-character-set=utf8

[mysql]
no-auto-rehash
default-character-set=utf8

我们还需设置MySQL服务启动时需要执行的脚本命令, 注意这里的用户名和password必需和myproject目录下.env文件中的环境变量保持一致。

# compose/mysql/init/init.sql
Alter user 'dbuser'@'%' IDENTIFIED WITH mysql_native_password BY 'password';
GRANT ALL PRIVILEGES ON myproject.* TO 'dbuser'@'%';
FLUSH PRIVILEGES;

.env文件内容如下所示:

MYSQL_ROOT_PASSWORD=123456
MYSQL_USER=dbuser
MYSQL_DATABASE=myproject
MYSQL_PASSWORD=password

第五步:编写Redis 容器配置文件

启动redis容器我们直接使用官方镜像即可,不过我们需要给redis增加配置文件。大部分情况下采用默认配置就好了,这里我们只做出了如下几条核心改动:

# compose/redis/redis.conf
# Redis 5配置文件下载地址
# https://raw.githubusercontent.com/antirez/redis/5.0/redis.conf

# 请注释掉下面一行,变成#bind 127.0.0.1,这样其它机器或容器也可访问
bind 127.0.0.1

# 取消下行注释,给redis设置登录密码。这个密码django settings.py会用到。
requirepass yourpassword

第六步:修改Django项目settings.py

在你准备好docker-compose.yml并编排好各容器的Dockerfile及配置文件后,请先不要急于使用Docker-compose命令构建镜像和启动容器。这时还有一件非常重要的事情要做,那就是修改Django的settings.py, 提供mysql和redis服务的配置信息。最重要的几项配置如下所示:

# 生产环境设置 Debug = False
Debug = False

# 设置ALLOWED HOSTS
ALLOWED_HOSTS = ['your_server_IP', 'your_domain_name']

# 设置STATIC ROOT 和 STATIC URL
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = "/static/"

# 设置MEDIA ROOT 和 MEDIA URL
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = "/media/"

# 设置数据库。这里用户名和密码必需和docker-compose.yml里mysql环境变量保持一致
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'myproject', # 数据库名
        'USER':'dbuser', # 你设置的用户名 - 非root用户
        'PASSWORD':'password', # # 换成你自己密码
        'HOST': 'db', # 注意:这里使用的是db别名,docker会自动解析成ip
        'PORT':'3306', # 端口
    }
}

# 设置redis缓存。这里密码为redis.conf里设置的密码
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://redis:6379/1", #这里直接使用redis别名作为host ip地址
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "PASSWORD": "yourpassword", # 换成你自己密码
        },
    }
}

第七步:使用docker-compose 构建镜像并启动容器组服务

现在我们可以使用docker-compose命名构建镜像并启动容器组了。

# 进入docker-compose.yml所在文件夹,输入以下命令构建镜像
sudo docker-compose build
# 查看已生成的镜像
sudo docker images
# 启动容器组服务
sudo docker-compose up

如果一切顺利,此时你应该可以看到四个容器服务都已经成功运行了。此时打开你的浏览器,输入你服务器的ip地址或域名指向地址,你就应该可以看到网站已经上线啦。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7SaBZnXD-1655293377686)(https://pythondjango.cn/django/advanced/12-internationalization.assets/image-20210419144719353.png)]

第八步:排错

初学者使用Docker或Docker-compose部署会出现各种各样的错误,本文教你如何排错。

Nginx容器排错

容器已启动运行,网站打不开,最有用的是查看Nginx的错误日志error.log。由于我们对容器内Nginx的log进行了挂载,你可以在宿主机的/compose/nginx/log目录里直接查看相关日志。

# 进入nginx日志目录,一个access.log, 一个error.log
cd compose/nginx/log
# 查看日志文件
sudo cat error.log 

绝大部分网站打不开,Nginx日志显示nginx: connect() failed (111: Connection refused) while connecting to upstream或Nginx 502 gateway的错误都不是因为nginx自身的原因,而是Web容器中Django程序有问题或则uwsgi配置文件有问题。

在进入Web容器排错前,你首先要检查下Nginx转发请求的方式(proxy_passuwsgi_pass)以及转发端口与uwsgi里面的监听方式以及端口是否一致。

uWSGI和Nginx之间有3种通信方式unix socket,TCP socket和http如果Nginx以proxy_pass方式转发请求,uwsgi需要使用http协议进行通信。如果Nginx以uwsgi_pass转发请求,uwsgi建议配置socket进行通信。

Web容器排错

Web容器也就是Django+UWSGI所在的容器,是最容易出现错误的容器。如果Nginx配置没问题,你应该进入web容器查看运行脚本命令时有没有报错,并检查uwsgi的运行日志。uwsgi的日志非常有用,它会记录Django程序运行时发生了哪些错误或异常。一旦发生了错误,uwsgi的进程虽然不会停止,但也无法正常工作,自然也就不能处理nginx转发的动态请求从而出现nginx报错了。

# 查看web容器日志
$ docker-compose logs web
# 进入web容器执行启动命令,查看有无报错
$ docker-compose exec web /bin/bash start.sh
# 或则进入web柔情其,逐一执行python manage.py命令
$ docker-compose exec web /bin/bash 
# 进入web容器,查看uwsgi是否正常启动
$ ps aux | grep uwsgi 
# 进入uwsgi日志所在目录,查看Django项目是否有报错
cd /tmp

另外一个常发生的错误是 docker-compose生成的web容器执行脚本命令后立刻退出(exited with code 0), 这时的解决方案是在docker-compose.yml中包含以下2行, 另外脚本命令里加入tail -f /dev/null是容器服务持续运行。

stdin_open: true
tty: true

有时web容器会出现不能连接到数据库的报错,这时需要检查settings.py中的数据库配置信息是否正确(比如host为db),并检查web容器和db容器是否通过db_network正常通信(比如进入db容器查看数据表是否已经生成)。在进行数据库迁移时web容器还会出现if table exists or failed to open the referenced table ‘users_user’, inconsistent migration history的错误, 可以删除migrations目录下文件并进入MySQL容器删除django_migrations数据表即可。

数据库db容器排错

我们还需要经常进入数据库容器查看数据表是否已生成并删除一些数据,这时可以使用如下命令:

# 进入db容器
$ docker-compose exec db /bin/bash
# 登录
mysql -u username -p;
# 选择数据库
USE dbname;
# 显示数据表
SHOW tables;
# 清空数据表
DELETE from tablename;
# 删除数据表
DROP TABLE tablename;

小结

本文详细地介绍了如何使用docker-compose工具分八步在生产环境下部署Django + Uwsgi + Nginx + MySQL + Redis。过程看似很复杂,但很多Dockerfile,项目布局及docker-compose.yml都是可以复用的。花时间学习并练习本章内容是非常值得的,一但你学会了,基本上可以10分钟内完成一个正式Django项目的部署,而且可以保证在任何一台Linux机器上顺利地运行。本文最后的排错经验更会助你一臂之力。

Django性能优化大全

  1. 性能优化指标
  2. 数据库查询优化
    1. 利用Queryset的惰性和缓存,避免重复查询
    2. 一次查询所有需要的关联模型数据
    3. 仅查询需要用到的数据
    4. 使用分页,限制最大页数
    5. 数据库设置优化
  3. 缓存
    1. 视图缓存
    2. 使用@cached_property装饰器缓存计算属性
    3. 缓存临时性数据比如sessions
    4. 模版缓存
  4. 静态文件

性能优化指标

在对一个Web项目进行性能优化时,我们通常需要考虑如下几个指标:

  • 响应时间
  • 最大并发连接数
  • 代码的行数
  • 函数调用次数
  • 内存占用情况
  • CPU占比

其中响应时间(服务器从接收用户请求,处理该请求并返回结果所需的总的时间)通常是最重要的指标,因为过长的响应时间会让用户厌倦等待,转投其它网站或APP。当你的用户数量变得非常庞大,如何提高最大并发连接数,减少内存消耗也将变得非常重要。

在开发环境中,我们一般建议使用django-debug-toolbardjango-silk来进行性能监测分析。它们提供了每次用户请求的响应时间,并告诉你程序执行过程哪个环节(比如SQL查询)最消耗时间。

对于中大型网站或Web APP而言,最影响网站性能的就是数据库查询部分了。一是反复从数据库读写数据很消耗时间和计算资源,二是当返回的查询数据集queryset非常大时还会占据很多内存。我们先从这部分优化做起。

数据库查询优化

利用Queryset的惰性和缓存,避免重复查询

充分利用Django的QuerySet的惰性和自带缓存特性,可以帮助我们减少数据库查询次数。比如下例中例1比例2要好。因为在你打印文章标题后,Django不仅执行了数据库查询,还把查询到的article_list放在了缓存里,下次可以在其它地方复用,而例2就不行了。

# 例1: 利用了缓存特性 - Good
article_list = Article.objects.filter(title__contains="django")
for article in article_list:
    print(article.title)

# 例2: Bad
for article in Article.objects.filter(title__contains="django"):
    print(article.title)

但有时我们只希望了解查询的结果是否存在或查询结果的数量,这时可以使用exists()count()方法,如下所示。这样就不会浪费资源查询一个用不到的数据集,还可以节省内存。

# 例3: Good
article_list = Article.objects.filter(title__contains="django")
if article_list.exists():
    print("Records found.")
else:
    print("No records")
    
# 例4: Good
count = Article.objects.filter(title__contains="django").count()

一次查询所有需要的关联模型数据

假设我们有一个文章(Article)模型,其与类别(Category)是单对多的关系(ForeignKey), 与标签(Tag)是多对多的关系(ManyToMany)。我们需要编写一个article_list的函数视图,以列表形式显示文章清单及每篇文章的类别和标签,你的模板文件可能如下所示:

<ul>
{% for article in articles %}
    <li>{{ article.title }} </li>
    <li>{{ article.category.name }}</li>
    <li>
        {% for tag in article.tags.all %}
           {{ tag.name }},
        {% endfor %}
    </li>
{% endfor %}
</ul>

在模板里每进行一次for循环获取关联对象category和tag的信息,Django就要单独进行一次数据库查询,造成了极大资源浪费。我们完全可以使用select_related方法和prefetch_related方法一次性从数据库获取单对多和多对多关联模型数据,这样在模板中遍历时Django也不会执行数据库查询了。

# 仅获取文章数据 - Bad
def article_list(request):
    articles = Article.objects.all()
    return render(request, 'blog/article_list.html',{'articles': articles, })

# 一次性提取关联模型数据 - Good
def article_list(request):
    articles = Article.objects.all().select_related('category').prefecth_related('tags')
    return render(request, 'blog/article_list.html', {'articles': articles, })

仅查询需要用到的数据

默认情况下Django会从数据库中提取所有字段,但是当数据表有很多列很多行的时候,告诉Django提取哪些特定的字段就非常有意义了。假如我们数据库中有100万篇文章,需要循环打印每篇文章的标题。如果按例4操作,我们会将每篇文章对象的全部信息都提取出来载入到内存中,不仅花费更多时间查询,还会大量占用内存,而最后只用了title这一个字段,这是完全没有必要的。我们完全可以使用valuesvalue_list方法按需提取数据,比如只获取文章的id和title,节省查询时间和内存(例6-例8)。

# 例子5: Bad
article_list = Article.objects.all()
if article_list:
    print(article.title)

# 例子6: Good - 字典格式数据
article_list = Article.objects.values('id', 'title')
if article_list:
    print(article.title)

# 例子7: Good - 元组格式数据
article_list = Article.objects.values_list('id', 'title')
if article_list:
    print(article.title)
    
# 例子8: Good - 列表格式数据
article_list = Article.objects.values_list('id', 'title', flat=True)
if article_list:
    print(article.title)

除此以外,Django项目还可以使用deferonly这两个查询方法来实现这一点。第一个用于指定哪些字段不要加载,第二个用于指定只加载哪些字段。

使用分页,限制最大页数

事实前面代码可以进一步优化,比如使用分页仅展示用户所需要的数据,而不是一下子查询所有数据。同时使用分页时也最好控制最大页数。比如当你的数据库有100万篇文章时,每页即使展示100篇,也需要1万页展示给你的用户,这是完全没有必要的。你可以完全只展示前200页的数据,如下所示:

LIMIT = 100 * 200

data = Articles.objects.all()[:(LIMIT + 1)]
if len(data) > LIMIT:
    raise ExceededLimit(LIMIT)

return data

数据库设置优化

如果你使用单个数据库,你可以采用如下手段进行优化:

  • 建立模型时能用CharField确定长度的字段尽量不用不用TextField, 可节省存储空间;
  • 可以给搜索频率高的字段属性,在定义模型时使用索引(db_index=True);
  • 持久化数据库连接。

没有持久化连接,Django每个请求都会与数据库创建一个连接,直到请求结束,关闭连接。如果数据库不在本地,每次建立和关闭连接也需要花费一些时间。设置持久化连接时间,仅需要添加CONN_MAX_AGE参数到你的数据库设置中,如下所示:

DATABASES = {
    ‘default’: {
        ‘ENGINE’: ‘django.db.backends.postgresql_psycopg2’,
        ‘NAME’: ‘postgres’,
        ‘CONN_MAX_AGE’: 60, # 60秒
    }
}

当然CONN_MAX_AGE也不宜设置过大,因为每个数据库并发连接数有上限的(比如mysql默认的最大并发连接数是100个)。如果CONN_MAX_AGE设置过大,会导致mysql 数据库连接数飙升很快达到上限。当并发请求数量很高时,CONN_MAX_AGE应该设低点,比如30s, 10s或5s。当并发请求数不高时,这个值可以设得长一点,比如60s或5分钟。

当你的用户非常多、数据量非常大时,你可以考虑读写分离、主从复制、分表分库的多数据库服务器架构。这种架构上的布局是对所有web开发语言适用的,并不仅仅局限于Django,这里不做进一步展开了。

缓存

缓存是一类可以更快的读取数据的介质统称,也指其它可以加快数据读取的存储方式。一般用来存储临时数据,常用介质的是读取速度很快的内存。一般来说从数据库多次把所需要的数据提取出来,要比从内存或者硬盘等一次读出来付出的成本大很多。对于中大型网站而言,使用缓存减少对数据库的访问次数是提升网站性能的关键之一。

视图缓存

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request):
    ...

使用@cached_property装饰器缓存计算属性

对于不经常变动的计算属性,可以使用@cached_property装饰器缓存结果。

缓存临时性数据比如sessions

Django的sessions默认是存在数据库中的,这样的话每一个请求Django都要使用sql查询会话数据,然后获得用户对象的信息。对于临时性的数据比如sessions和messages,最好将它们放到缓存里,也可以减少SQL查询次数。

SESSION_ENGINE = 'django.contrib.sessions.backends.cache'

模版缓存

默认情况下Django每处理一个请求都会使用模版加载器都会去文件系统搜索模板,然后渲染这些模版。你可以通过使用cached.Loader开启模板缓存加载。这时Django只会查找并且解析你的模版一次,可以大大提升模板渲染效率。

TEMPLATES = [{
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'DIRS': [BASE_DIR / 'templates'],
    'OPTIONS': {
        'loaders': [
            ('django.template.loaders.cached.Loader', [
                'django.template.loaders.filesystem.Loader',
                'django.template.loaders.app_directories.Loader',
                'path.to.custom.Loader',
            ]),
        ],
    },
}]

注意:不建议在开发环境中(Debug=True)时开启缓存加载,因为修改模板后你不能及时看到修改后的效果。

另外模板文件中建议使用with标签缓存视图传来的数据,便于下一次时使用。对于公用的html片段,也建议使用缓存。

{% load cache %}
{% cache 500 sidebar request.user.username %}
    .. sidebar for logged in user ..
{% endcache %}

静态文件

压缩 HTML、CSS 和 JavaScript等静态文件可以节省带宽和传输时间。Django 自带的压缩工具有GzipMiddleware 中间件和 spaceless 模板 Tag。使用Python压缩静态文件会影响性能,一个更好的方法是通过 Apache、Nginx 等服务器来对输出内容进行压缩。例如Nginx服务器支持gzip压缩,同时可以通过expires选项设置静态文件的缓存时间。

更多关于Nginx的配置见:

  • https://pythondjango.cn/python/tools/5-nginx-configuration/

`

在模板里每进行一次for循环获取关联对象category和tag的信息,Django就要单独进行一次数据库查询,造成了极大资源浪费。我们完全可以使用select_related方法和prefetch_related方法一次性从数据库获取单对多和多对多关联模型数据,这样在模板中遍历时Django也不会执行数据库查询了。

# 仅获取文章数据 - Bad
def article_list(request):
    articles = Article.objects.all()
    return render(request, 'blog/article_list.html',{'articles': articles, })

# 一次性提取关联模型数据 - Good
def article_list(request):
    articles = Article.objects.all().select_related('category').prefecth_related('tags')
    return render(request, 'blog/article_list.html', {'articles': articles, })

仅查询需要用到的数据

默认情况下Django会从数据库中提取所有字段,但是当数据表有很多列很多行的时候,告诉Django提取哪些特定的字段就非常有意义了。假如我们数据库中有100万篇文章,需要循环打印每篇文章的标题。如果按例4操作,我们会将每篇文章对象的全部信息都提取出来载入到内存中,不仅花费更多时间查询,还会大量占用内存,而最后只用了title这一个字段,这是完全没有必要的。我们完全可以使用valuesvalue_list方法按需提取数据,比如只获取文章的id和title,节省查询时间和内存(例6-例8)。

# 例子5: Bad
article_list = Article.objects.all()
if article_list:
    print(article.title)

# 例子6: Good - 字典格式数据
article_list = Article.objects.values('id', 'title')
if article_list:
    print(article.title)

# 例子7: Good - 元组格式数据
article_list = Article.objects.values_list('id', 'title')
if article_list:
    print(article.title)
    
# 例子8: Good - 列表格式数据
article_list = Article.objects.values_list('id', 'title', flat=True)
if article_list:
    print(article.title)

除此以外,Django项目还可以使用deferonly这两个查询方法来实现这一点。第一个用于指定哪些字段不要加载,第二个用于指定只加载哪些字段。

使用分页,限制最大页数

事实前面代码可以进一步优化,比如使用分页仅展示用户所需要的数据,而不是一下子查询所有数据。同时使用分页时也最好控制最大页数。比如当你的数据库有100万篇文章时,每页即使展示100篇,也需要1万页展示给你的用户,这是完全没有必要的。你可以完全只展示前200页的数据,如下所示:

LIMIT = 100 * 200

data = Articles.objects.all()[:(LIMIT + 1)]
if len(data) > LIMIT:
    raise ExceededLimit(LIMIT)

return data

数据库设置优化

如果你使用单个数据库,你可以采用如下手段进行优化:

  • 建立模型时能用CharField确定长度的字段尽量不用不用TextField, 可节省存储空间;
  • 可以给搜索频率高的字段属性,在定义模型时使用索引(db_index=True);
  • 持久化数据库连接。

没有持久化连接,Django每个请求都会与数据库创建一个连接,直到请求结束,关闭连接。如果数据库不在本地,每次建立和关闭连接也需要花费一些时间。设置持久化连接时间,仅需要添加CONN_MAX_AGE参数到你的数据库设置中,如下所示:

DATABASES = {
    ‘default’: {
        ‘ENGINE’: ‘django.db.backends.postgresql_psycopg2’,
        ‘NAME’: ‘postgres’,
        ‘CONN_MAX_AGE’: 60, # 60秒
    }
}

当然CONN_MAX_AGE也不宜设置过大,因为每个数据库并发连接数有上限的(比如mysql默认的最大并发连接数是100个)。如果CONN_MAX_AGE设置过大,会导致mysql 数据库连接数飙升很快达到上限。当并发请求数量很高时,CONN_MAX_AGE应该设低点,比如30s, 10s或5s。当并发请求数不高时,这个值可以设得长一点,比如60s或5分钟。

当你的用户非常多、数据量非常大时,你可以考虑读写分离、主从复制、分表分库的多数据库服务器架构。这种架构上的布局是对所有web开发语言适用的,并不仅仅局限于Django,这里不做进一步展开了。

缓存

缓存是一类可以更快的读取数据的介质统称,也指其它可以加快数据读取的存储方式。一般用来存储临时数据,常用介质的是读取速度很快的内存。一般来说从数据库多次把所需要的数据提取出来,要比从内存或者硬盘等一次读出来付出的成本大很多。对于中大型网站而言,使用缓存减少对数据库的访问次数是提升网站性能的关键之一。

视图缓存

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request):
    ...

使用@cached_property装饰器缓存计算属性

对于不经常变动的计算属性,可以使用@cached_property装饰器缓存结果。

缓存临时性数据比如sessions

Django的sessions默认是存在数据库中的,这样的话每一个请求Django都要使用sql查询会话数据,然后获得用户对象的信息。对于临时性的数据比如sessions和messages,最好将它们放到缓存里,也可以减少SQL查询次数。

SESSION_ENGINE = 'django.contrib.sessions.backends.cache'

模版缓存

默认情况下Django每处理一个请求都会使用模版加载器都会去文件系统搜索模板,然后渲染这些模版。你可以通过使用cached.Loader开启模板缓存加载。这时Django只会查找并且解析你的模版一次,可以大大提升模板渲染效率。

TEMPLATES = [{
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'DIRS': [BASE_DIR / 'templates'],
    'OPTIONS': {
        'loaders': [
            ('django.template.loaders.cached.Loader', [
                'django.template.loaders.filesystem.Loader',
                'django.template.loaders.app_directories.Loader',
                'path.to.custom.Loader',
            ]),
        ],
    },
}]

注意:不建议在开发环境中(Debug=True)时开启缓存加载,因为修改模板后你不能及时看到修改后的效果。

另外模板文件中建议使用with标签缓存视图传来的数据,便于下一次时使用。对于公用的html片段,也建议使用缓存。

{% load cache %}
{% cache 500 sidebar request.user.username %}
    .. sidebar for logged in user ..
{% endcache %}

静态文件

压缩 HTML、CSS 和 JavaScript等静态文件可以节省带宽和传输时间。Django 自带的压缩工具有GzipMiddleware 中间件和 spaceless 模板 Tag。使用Python压缩静态文件会影响性能,一个更好的方法是通过 Apache、Nginx 等服务器来对输出内容进行压缩。例如Nginx服务器支持gzip压缩,同时可以通过expires选项设置静态文件的缓存时间。

更多关于Nginx的配置见:

  • https://pythondjango.cn/python/tools/5-nginx-configuration/
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小松_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值