12. Django 第三方功能应用

2024-06-10_022252

12. 第三方功能应用

因为Django具有很强的可扩展性, 所以延伸了第三方功能应用.
通过本章的学习, 读者能够在网站开发过程中快速实现API接口开发, 验证码生成与使用, 站内搜索引擎, 
第三方网站实现用户注册, 异步任务和定时任务, 即时通信等功能.

12.1 Django Rest Framework框架

API接口简称API, 它与网站路由的实现原理相同.
当用户使用GET或者POST方式访问API时, API以JSON或字符串的数据内容返回给用户,
网站的路由返回的是HTML网页信息, 这与API返回的数据格式有所不同.

12.1.1 DRF的安装与配置

开发网站的API可以在视图函数中使用响应类JsonResponse实现, 它将字典格式的数据作为响应内容.
使用响应类JsonResponse开发API需要根据用户请求信息构建字典格式的数据内容,
数据构建的过程可能涉及模型的数据查询, 数据分页处理等业务逻辑, 这种方式开发API很容易造成代码冗余, 不利于功能的变更和维护.

为了简化API的开发过程, 我们可以使用Django Rest Framework框架实现API开发.
使用框架开发不仅能减少代码冗余, 还可以规范代码的编写格式, 
这对企业级开发来说很有必要, 毕竟每个开发人员的编程风格存在一定的差异, 开发规范化可以方便其他开发人员查看和修改.
在使用Django Rest Framework之前, 首先安装Django Rest Framework框架, 
建议使用pip完成安装, 安装指令如下: pip install djangorestframework .
清华源: pip install -i https://pypi.tuna.tsinghua.edu.cn/simple djangorestframework .

image-20240529182609653

框架安装成功后, 通过简单的例子来讲述如何在Django中配置Django Rest Framework的功能.
以MyDjango为例, 首先在MyDjango的settings.py中设置功能配置, 功能配置如下:
# MyDjango的settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'index',
    # 添加Django Rest Framework框架
    'rest_framework'
]

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'index.apps.IndexConfig',
    'rest_framework',
]

# Django Rest Framework框架设置信息
# 分页设置
REST_FRAMEWORK = {
    # 默认分页类
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    # 每页显示多少条数据
    'PAGE_SIZE': 2
}

image-20240529183158429

上述配置信息用于实现Django Rest Framework的功能配置, 配置说明如下: 
(1) 在INSTALLED_APPS中添加API框架的功能配置, 这样能使Django在运行过程中自动加载Django Rest Framework的功能.
(2) 配置属性REST_FRAMEWORK以字典的形式表示, 用于设置Django Rest Framework的分页功能.
完成settings.py的配置后, 下一步定义项目的数据模型.
在index的models.py中分别定义模型PersonInfo和Vocation, 代码如下:
from django.db import models


# 人员信息
class PersonInfo(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20)
    age = models.IntegerField()
    hire_date = models.DateField()  # 雇用日期

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = '人员信息'


# 职业信息
class Vocation(models.Model):
    id = models.AutoField(primary_key=True)
    job = models.CharField(max_length=20)
    title = models.CharField(max_length=20)
    salary = models.IntegerField(null=True, blank=True)
    # 外键字段
    person_id = models.ForeignKey(PersonInfo, on_delete=models.Case)

    def __str__(self):
        return self.id

    class Meta:
        verbose_name = '职业信息'

将定义好的模型执行数据迁移, 在项目的db.sqlite3数据库文件中生成数据表.
# 数据迁移:
PS D:\MyDjango> python manage.py makemigrations
Migrations for 'index':
  index\migrations\0001_initial.py
    - Create model PersonInfo
    - Create model Vocation
PS D:\MyDjango> python manage.py migrate

image-20240529191205684

对数据表index_personinfo和index_vocation添加数据内容, 如图12-1所示.

image-20240529191750028

12-1 数据表index_personinfo和index_vocation
-- 向index_personinfo插入数据:
INSERT INTO "index_personinfo" ("id", "name", "age", "hire_date") VALUES (1, '张三', 18, '2018-09-18');
INSERT INTO "index_personinfo" ("id", "name", "age", "hire_date") VALUES (2, '李四', 19, '2018-10-18');
INSERT INTO "index_personinfo" ("id", "name", "age", "hire_date") VALUES (3, '王五', 20, '2018-10-01');
INSERT INTO "index_personinfo" ("id", "name", "age", "hire_date") VALUES (4, '赵六', 21, '2018-05-01');

-- 向index_vocation插入数据:
INSERT INTO "index_vocation" 
("id", "job", "title", "salary", "person_id_id") VALUES (1, '软件工程师', 'Python开发', 10000, 2);
INSERT INTO "index_vocation"
("id", "job", "title", "salary", "person_id_id") VALUES (2, '测试工程师', '软件测试', 8000, 4);
INSERT INTO "index_vocation"
("id", "job", "title", "salary", "person_id_id") VALUES (3, '行政管理', '人事行政', 5000, 1);
INSERT INTO "index_vocation" 
("id", "job", "title", "salary", "person_id_id") VALUES (4, '前端工程师', 'Vue开发', 9000, 3);

12.1.2 序列化类Serializer

项目环境搭建完成后, 我们将使用Django Rest Framework快速开发API.
首先在项目应用index中创建serializers.py文件, 该文件用于定义Django Rest Framework的序列化类;
然后定义序列化类MySerializer, 代码如下:
# index的MySerializer.py
from rest_framework import serializers
from .models import PersonInfo, Vocation

# 定义serializer类
# 设置模型Vocation的字段person_id的下拉内容
id_list = PersonInfo.objects.values_list('id', flat=True)


class MySerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)  # 只读
    job = serializers.CharField(max_length=100)
    title = serializers.CharField(max_length=100)
    salary = serializers.CharField(max_length=100)

    # person_id = serializers.ChoiceField(choices=id_list, default=1)
    # 模型Vocation的字段person_id是外键字段, 它指向模型PersonInfo
    # 因此, 外键字段可以使用PrimaryKeyRelatedField (主键相关字段)
    person_id = serializers.PrimaryKeyRelatedField(queryset=id_list)

    # 重写create函数, 将API数据保存到数据表index_vocation中
    def create(self, validate_data):
        return Vocation.objects.create(**validate_data)

    # 重写update函数, 将API数据更新到数据表index_vocation中
    def update(self, instance, validated_data):
        return instance.update(**validated_data)

自定义序列化类MySerializer继承父类Serializer, 
父类Serializer是由Django Rest Framework定义的, 它的定义过程与表单类Form十分相似.
在PyCharm里打开父类Serializer的源码文件, 分析序列化类Serializer的定义过程, 如图12-2所示.

image-20240529214630611

12-2 Serializer的定义过程
从图12-2得知,Serializer继承父类BaseSerializer, 设置元类SerializerMetaclass, (老版本装饰器add_metaclass设置元类).
我们以流程图的形式说明Serializer的继承关系, 如图12-3所示.

image-20240529215236421

12-3 Serializer的继承关系
自定义序列化类MySerializer的字段对应模型Vocation的字段,
序列化字段的数据类型可以在Django Rest Framework的源码文件fields.py中找到定义过程,
它们都继承父类Field, 序列化字段的数据类型与表单字段的数据类型相似, 此处不再详细讲述.

在定义序列化字段的时候, 每个序列化字段允许设置参数信息,
我们分析父类Field的初始化参数, 它们适用于所有序列化字段的参数设置, 参数说明如下:
 read_only: 设置序列化字段的只读属性.
 write_only: 设置序列化字段的编辑属性.
 required: 设置序列化字段的数据是否为空, 默认值为True.●default:设置序列化字段的默认值。
 initial: 设置序列化字段的初始值.
 source: 为序列化字段指定一个模型字段来源, (email='user.email').
 label: 用于生成label标签的网页内容.
 help_text: 设置序列化字段的帮助提示信息.
 style: 以字典格式表示, 控制模板引擎如何渲染序列化字段.
 error_messages: 设置序列化字段的错误信息, 以字典格式表示, 包含null, blank, invalid, invalid_choice, unique等键值.
 validators: 与表单类的validators相同, 这是自定义数据验证规则, 以列表格式表示, 列表元素为数据验证的函数名.
 allow_null: 设置序列化字段是否为None, 若为True, 则序列化字段的值允许为None.
自定义序列化类MySerializer还定义了关系字段name, 重写了父类BaseSerializer的create和update函数.
关系字段可以在源码文件relations.py中找到定义过程, 每个关系字段都有代码注释说明, 本节不再重复讲述.
下一步使用序列化类MySerializer实现API开发, 在index的urls.py中分别定义路由my_def和路由my_class, 路由信息的代码如下:
# index的urls.py
from django.urls import path
from .views import *
urlpatterns = [
    # 视图函数
    path('', vocationDef, name='my_def'),
    # 视图类
    path('my_class/', vocationClass.as_view(), name='my_class'),
]

image-20240530114802150

路由my_def对应视图函数vocation_def, 它以视图函数的方式使用MySerializer实现模型Vocation的API接口;
路由my_class对应视图类VocationClass, 它以视图类的方式使用MySerializer实现模型Vocation的API接口.
因此, 视图函数vocationDef和视图类vocationClass的定义过程如下:
# index的views.py
from .models import PersonInfo, Vocation
from .serializers import MySerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status  # 响应状态码
from rest_framework.pagination import PageNumberPagination
from rest_framework.decorators import api_view


@api_view(['GET', 'POST'])  # 接收GET与POST请求
def vocation_def(request):
    if request.method == 'GET':
        q = Vocation.objects.all()
        # 分页查询, 需要在settings.py中设置REST_FRAMEWORK属性
        pg = PageNumberPagination()
        p = pg.paginate_queryset(queryset=q, request=request)
        # 将分页后的数据传递给MySerializer, 生成JSON数据对象
        serializer = MySerializer(instance=p, many=True)
        # 返回对象Response由Django Rest Framework实现
        return Response(serializer.data)

    elif request.method == 'POST':
        # 获取请求数据
        data = request.data
        # 外键字段person_id, 期望的是一个 PersonInfo 模型的实例, 而不是一个整数.
        # Cannot assign "num": "Vocation.person_id" must be a "PersonInfo" instance.
        pk = data['person_id']
        data['person_id'] = PersonInfo.objects.filter(id=pk).first()
        # 查询数据是否存在, 存在则返回一个模型对象, 否则值为0
        instance = Vocation.objects.filter(id=data.get('id', 0))

        if instance:
            # 修改数据
            MySerializer().update(instance, data)
        else:
            # 创建数据
            MySerializer().create(data)
        return Response('Done!', status=status.HTTP_201_CREATED)


class VocationClass(APIView):
    # GET请求
    def get(self, request):
        q = Vocation.objects.all()
        # 分页查询, 需要在settings.py中设置REST_FRAMEWORK属性
        pg = PageNumberPagination()
        p = pg.paginate_queryset(queryset=q, request=request, view=self)
        # 将分页后的数据传递给MySerializer, 生成JSON数据对象
        serializer = MySerializer(instance=p, many=True)
        # 返回对象Response由Django Rest Framework实现
        return Response(serializer.data)

    # POST请求
    def post(self, request):
        data = request.data
        pk = data['person_id']
        data['person_id'] = PersonInfo.objects.filter(id=pk).first()
        instance = Vocation.objects.filter(id=data.get('id', 0))

        if instance:
            # 修改数据
            MySerializer().update(instance, data)
        else:
            # 创建数据
            MySerializer().create(data)

        return Response('Done!', status=status.HTTP_201_CREATED)

image-20240530114950127

视图函数vocation_def和视图类VocationClass实现的功能是一致的, 
若使用视图函数开发API接口, 则必须对视图函数使用装饰器api_view;
若使用视图类, 则必须继承父类APIView, 这是Django Rest Framework明确规定的.
上述的视图函数vocation_def和视图类VocationClass对GET请求和POST请求进行不同的处理.

当用户在浏览器上访问路由my_def或路由my_class的时候,
视图函数vocation_def或视图类VocationClass将接收GET请求, 该请求的处理过程说明如下:
(1) 视图函数vocation_def或视图类VocationClass查询模型Vocation所有数据, 并将数据进行分页处理.
(2) 分页功能由Django Rest Framework的PageNumberPagination实现,
    它是在Django内置分页功能的基础上进行封装的, 分页属性设置在settings.py的REST_FRAMEWORK中.
(3) 分页后的数据传递给序列化类MySerializer, 转化成JSON数据, 最后由Django Rest Framework框架的Response完成用户响应.

运行MyDjango, 分别访问路由my_def和路由my_class, 发现两者返回的网页内容是一致的.
如果在路由地址中设置请求参数page, 就可以获取某分页的数据信息, 如图12-4所示.

image-20240530000737766

12-4 运行结果
用户向路由my_def或路由my_class发送POST请求时, 视图函数vocation_def或视图类VocationClass的处理过程说明如下:
(1) 视图函数vocationDef或视图类vocationClass获取请求参数,
    将请求参数id作为模型字段id的查询条件, 在模型Vocation中进行数据查询.
(2) 如果存在查询对象, 就说明模型Vocation已存在相应的数据信息, 当前POST请求将视为修改模型Vocation的已有数据.
(3) 如果不存在查询对象, 就把当前请求的数据信息添加在模型Vocation中.

在图12-4的网页正下方找到Content文本框, 以模型Vocation的字段编写单个JSON数据,
单击'POST'按钮即可实现数据的新增或修改, 如图12-5所示.
# 修改数据:
{
    "id": 4,
    "job": "前端工程师",
    "title": "Vue开发",
    "salary": "6666",
    "person_id": 3
}
# 新增数据:
{
    "id": 5,
    "job": "xx开发",
    "title": "xx开发",
    "salary": "666",
    "person_id": 3
}

image-20240530113811680

12-5 新增或修改数据

image-20240530113835279

12.1.3 模型序列化类ModelSerializer

序列化类Serializer可以与模型结合使用, 从而实现模型的数据读写操作.
但序列化类Serializer定义的字段必须与模型字段相互契合, 否则在使用过程中很容易提示异常信息.
为了简化序列化类Serializer的定义过程, Django Rest Framework定义了模型序列化类ModelSerializer,
它与模型表单ModelForm的定义和使用十分相似.
12.1.2小节的MyDjango为例, 将自定义的MySerializer改为VocationSerializer,
序列化类VocationSerializer继承父类ModelSerializer, 它能与模型Vocation完美结合, 无须开发者定义序列化字段.
在index的serializers.py中定义VocationSerializer, 代码如下:
# index的serializers.py
from rest_framework import serializers
from .models import Vocation


class VocationSerializer(serializers.ModelSerializer):
    class Meta:
        # 使用的模型
        model = Vocation
        # 可序列化的字段, __all__表示所有
        fields = '__all__'
        # fields = ['id', 'job', 'title', 'salary', 'person_id']

image-20240530205518328

分析VocationSerializer得知, 属性model将模型Vocation与ModelSerializer进行绑定;
属性fields用于设置哪些模型字段转化为序列化字段, 属性值__all__代表模型所有字段转化为序列化字段,
如果只设置部分模型字段, 属性fields的值就可以使用元组或列表表示, 元组或列表的每个元素代表一个模型字段.
下一步重新定义视图函数vocation_def和视图类VocationClass, 使用模型序列化类VocationSerializer实现模型Vocation的API接口.
视图函数vocation_def和视图类VocationClass的代码如下:
# index的views.py
from .models import PersonInfo, Vocation
from .serializers import VocationSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status  # 响应状态码
from rest_framework.pagination import PageNumberPagination
from rest_framework.decorators import api_view


@api_view(['GET', 'POST'])  # 接收GET与POST请求
def vocation_def(request):
    if request.method == 'GET':
        q = Vocation.objects.all()
        # 分页查询, 需要在settings.py中设置REST_FRAMEWORK属性
        pg = PageNumberPagination()
        p = pg.paginate_queryset(queryset=q, request=request)
        # 将分页后的数据传递给VocationSerializer, 生成JSON数据对象
        serializer = VocationSerializer(instance=p, many=True)
        # 返回对象Response由Django Rest Framework实现
        return Response(serializer.data)

    elif request.method == 'POST':
        # 获取请求数据
        pk = request.data.get('id', 0)
        # 判断请求参数id在模型Vocation中是否存在
        # 若存在, 则执行数据修改; 否则新增数据
        operation = Vocation.objects.filter(id=pk).first()
        # 数据验证
        serializer = VocationSerializer(data=request.data)
        if serializer.is_valid():
            if operation:
                data = request.data
                pk = data['person_id']
                data['person_id'] = PersonInfo.objects.filter(id=pk).first()
                serializer.update(operation, data)  # 传递更新的模型和数据
            else:
                # 保存到数据库
                serializer.save()

            # 返回对象Response由Django Rest Framework实现
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_404_NOT_FOUND)


class VocationClass(APIView):
    # GET请求
    def get(self, request):
        q = Vocation.objects.all()
        # 分页查询, 需要在settings.py中设置REST_FRAMEWORK属性
        pg = PageNumberPagination()
        p = pg.paginate_queryset(queryset=q, request=request, view=self)
        # 将分页后的数据传递给VocationSerializer, 生成JSON数据对象
        serializer = VocationSerializer(instance=p, many=True)
        # 返回对象Response由Django Rest Framework实现
        return Response(serializer.data)

    # POST请求
    def post(self, request):
        # 获取请求数据
        pk = request.data.get('id', 0)
        # 判断请求参数id在模型Vocation中是否存在
        # 若存在, 则执行数据修改; 否则新增数据
        operation = Vocation.objects.filter(id=pk).first()
        # 数据验证
        serializer = VocationSerializer(data=request.data)
        if serializer.is_valid():
            if operation:
                data = request.data
                pk = data['person_id']
                data['person_id'] = PersonInfo.objects.filter(id=pk).first()
                serializer.update(operation, data)  # 传递更新的模型和数据
            else:
                # 保存到数据库
                serializer.save()

            # 返回对象Response由Django Rest Framework实现
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_404_NOT_FOUND)

image-20240530205658969

视图函数vocation_def和视图类VocationClass的业务逻辑与12.1.2小节的业务逻辑是相同的,
而对于POST请求的处理过程, 本节与12.1.2小节实现的功能一致, 但使用的函数方法有所不同.
启动项目, 访问: 127.0.0.1:8000/ .
# 修改数据:
{
    "id": 4,
    "job": "前端工程师",
    "title": "Vue开发",
    "salary": "7777",
    "person_id": 3
}

2024-05-30_121913

# 新增数据:
{
    "id": 6,
    "job": "xx开发",
    "title": "xx开发",
    "salary": "777",
    "person_id": 3
}

image-20240530122041175

查看数据库.

image-20240530122140997

12.1.4 序列化的嵌套使用

在开发过程中, 我们需要对多个JSON数据进行嵌套使用,
比如将模型PersonInfo和Vocation的数据组合存放在同一个JSON数据中, 两个模型的数据通过外键字段person_id进行关联.

模型之间必须存在数据关系才能实现数据嵌套, 数据关系可以是一对一, 一对多或多对多的.
不同的数据关系对数据嵌套的读写操作会有细微的差异.

12.1.3小节的MyDjango为例, 在index的serializers.py中定义PersonInfoSerializer和VocationSerializer.
分别对应模型PersonInfo和Vocation, 模型序列化类的定义如下:
# index的serializers.py
from rest_framework import serializers
from .models import Vocation, PersonInfo


class PersonInfoSerializer(serializers.ModelSerializer):
    class Meta:
        # 使用的模型
        model = PersonInfo
        # 展示的字段
        fields = '__all__'


# 定义ModelSerializer类
class VocationSerializer(serializers.ModelSerializer):
    person_id = PersonInfoSerializer()

    class Meta:
        # 使用的模型
        model = Vocation
        # 展示的字段
        fields = '__all__'

    def create(self, validated_data):
        # 从validated_data中获取模型PersonInfo的数据
        person_id = validated_data.get('person_id', '')  # person_id是一个嵌套字典
        pk = person_id.get('id', 0)
        p = PersonInfo.objects.filter(id=pk).first()
        # 根据pk判断模型PersonInfo是否存在数据对象,
        # 若存在数据对象, 则只对Vocation新增数据
        # 若不存在, 则先对模型PersonInfo新增数据
        # 再对模型Vocation新增数据
        if not p:
            p = PersonInfo.objects.create(**person_id)

        data = validated_data
        data['person_id'] = p

        v = Vocation.objects.create(**data)
        return v

    def update(self, instance, validated_data):
        # 从validated_data中获取模型PersonInfo的数据
        person_id = validated_data.get('person_id', '')
        pk = person_id.get('id', 0)
        p = PersonInfo.objects.filter(id=pk).first()
        # 判断外键person_id是否存在模型PersonInfo
        if p:
            # 若存在, 则先更新模型PersonInfo的数据
            PersonInfo.objects.filter(id=pk).update(**person_id)
            # 再更新模型Vocation的数据
            data = validated_data
            data['person_id'] = pk  # 将嵌套字典换为外键值或PersonInfo对象
            v_pk = validated_data.get('id', '')
            v = Vocation.objects.filter(id=v_pk).update(**data)
            return v

image-20240530160908868

从上述代码看到, 序列化类VocationSerializer对PersonInfoSerializer进行实例化并赋值给变量person_id,
而属性fields的person_id代表模型Vocation的外键字段person_id, 同时也是变量person_id.
换句话说, 序列化字段person_id代表模型Vocation的外键字段person_id, 而变量person_id作为序列化字段person_id的数据内容.

变量person_id的名称必须与模型外键字段名称或者序列化字段名称一致, 
否则模型外键字段或者序列化字段无法匹配变量person_id, 从而提示异常信息.

序列化类VocationSerializer还重写了数据的新增函数create和修改函数update,
因为不同数据关系的数据读写方式各不相同, 并且不同的开发需求可能导致数据读写方式有所不同.
新增函数create和修改函数update的业务逻辑较为相似, 业务逻辑说明如下:
(1) 从用户的请求参数(函数参数validated_data)获取模型PersonInfo的主键id, 根据主键id查询模型PersonInfo是否已存在数据对象.
(2) 如果存在模型PersonInfo的数据对象, 那么update函数首先修改模型PersonInfo的数据, 然后修改模型Vocation的数据.
(3) 如果不存在模型PersonInfo的数据对象, 那么create函数在模型PersonInfo中新增数据, 然后在模型Vocation中新增数据.
    确保模型Vocation的外键字段name不为空, 使两个模型之间构成一对多的数据关系.
下一步对视图函数vocation_def和视图类VocationClass的代码进行调整, 代码的业务逻辑与12.1.3小节的相同, 代码调整如下:
# index的views.py
from .models import Vocation
from .serializers import VocationSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status  # 响应状态码
from rest_framework.pagination import PageNumberPagination
from rest_framework.decorators import api_view


@api_view(['GET', 'POST'])  # 接收GET与POST请求
def vocation_def(request):
    if request.method == 'GET':
        q = Vocation.objects.all().order_by('id')  # 按id排序
        # 分页查询, 需要在settings.py中设置REST_FRAMEWORK属性
        pg = PageNumberPagination()
        p = pg.paginate_queryset(queryset=q, request=request)
        # 将分页后的数据传递给VocationSerializer, 生成JSON数据对象
        serializer = VocationSerializer(instance=p, many=True)
        # 返回对象Response由Django Rest Framework实现
        return Response(serializer.data)

    elif request.method == 'POST':
        # 获取请求数据
        pk = request.data.get('id', 0)
        # 判断请求参数id在模型Vocation中是否存在
        # 若存在, 则执行数据修改; 否则新增数据
        operation = Vocation.objects.filter(id=pk).first()
        # 数据验证
        serializer = VocationSerializer(data=request.data)
        if serializer.is_valid():
            if operation:
                serializer.update(operation, request.data)  # 传递更新的模型和数据
            else:
                # 保存到数据库
                serializer.save()

            # 返回对象Response由Django Rest Framework实现
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_404_NOT_FOUND)


class VocationClass(APIView):
    # GET请求
    def get(self, request):
        q = Vocation.objects.all().order_by('id')  # 按id排序
        # 分页查询, 需要在settings.py中设置REST_FRAMEWORK属性
        pg = PageNumberPagination()
        p = pg.paginate_queryset(queryset=q, request=request, view=self)
        # 将分页后的数据传递给VocationSerializer, 生成JSON数据对象
        serializer = VocationSerializer(instance=p, many=True)
        # 返回对象Response由Django Rest Framework实现
        return Response(serializer.data)

    # POST请求
    def post(self, request):
        # 获取请求数据
        pk = request.data.get('id', 0)
        # 判断请求参数id在模型Vocation中是否存在
        # 若存在, 则执行数据修改; 否则新增数据
        operation = Vocation.objects.filter(id=pk).first()
        # 数据验证
        serializer = VocationSerializer(data=request.data)
        if serializer.is_valid():
            if operation:
                serializer.update(operation, request.data)  # 传递更新的模型和数据
            else:
                # 保存到数据库
                serializer.save()

            # 返回对象Response由Django Rest Framework实现
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_404_NOT_FOUND)

image-20240530205754133

最后运行MyDjango, 在浏览器上访问: 127.0.0.1:8000, 模型Vocation的每行数据嵌套了模型PersonInfo的某行数据,
两个模型的数据嵌套主要由模型Vocation的外键字段name实现关联, 如图12-6所示.

2024-05-30_155759

# 创建数据
{
    "id": 7,
    "person_id": {
        "id": 5,
        "name": "王七",
        "age": 22,
        "hire_date": "2018-10-18"
    },
    "job": "xx开发",
    "title": "xx开发",
    "salary": 8888
}

image-20240530160552005

image-20240530160640639

12-6数据嵌套

12.2 验证码生成与使用

现在很多网站都采用验证码功能, 这是反爬虫常用的策略之一.
目前常用的验证码类型如下:
 字符验证码: 在图片上随机产生数字, 英文字母或汉字, 一般有4位或者6位验证码字符.
 图片验证码: 图片验证码采用字符验证码的技术, 不再使用随机的字符, 而是让用户识别图片, 比如12306的验证码.
 GIF动画验证码: 由多张图片组合而成的动态验证码, 使得识别器不容易辨识哪一张图片是真正的验证码图片.
 极验验证码: 2012年推出的新型验证码, 采用行为式验证技术,
  通过拖曳滑块完成拼图的形式实现验证, 是目前比较有创意的验证码, 安全性具有新的突破.
 手机验证码: 通过短信的形式发送到用户手机上面的验证码, 一般为6位的数字.
 语音验证码: 属于手机端验证的一种方式.
 视频验证码: 视频验证码是验证码中的新秀, 在视频验证码中, 将随机数字, 
  字母和中文组合而成的验证码动态嵌入MP4, FLV等格式的视频中, 增大破解难度.

12.2.1 Django Simple Captcha的安装与配置

如果想在Django中实现验证码功能, 那么可以使用PIL模块生成图片验证码, 但不建议使用这种实现方式.
除此之外, 还可以通过第三方应用Django Simple Captcha实现, 
验证码的生成过程由该应用自动执行, 开发者只需考虑如何应用到Django项目中即可.
Django Simple Captcha使用pip安装, 安装指令如下: pip install django-simple-captcha .
清华源: pip install -i https://pypi.tuna.tsinghua.edu.cn/simple django-simple-captcha .
安装成功后, 下一步讲述如何在Django中使用Django Simple Captcha生成网站验证码.
以MyDjango项目为例, 首先创建项目应用user, 并在项目应用user中创建forms.py文件;
然后在templates文件夹中放置user.html文件, 目录结构如图12-7所示.

image-20240530211159811

12-7 目录结构
本节将实现带验证码的用户登录功能, 因此在settings.py中需要配置INSTALLED_APPS, TEMPLATES和DATABASES, 配置信息如下:
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'user.apps.UserConfig',
    # 添加验证码功能
    'captcha',
]

image-20240531020055410

配置属性INSTALLED_APPS添加了captcha, 这是将Django Simple Captcha的功能引入MyDjango项目.
Django Simple Captcha设有多种验证码的生成方式, 如设置验证码的内容, 图片噪点和图片大小等,
这些功能设置可以在Django Simple Captcha的源码文件(site-packages\captcha\conf\settings.py)中查看.
本节只说明验证码的常用功能设置, 在MyDjango的settings.py中添加以下属性:
# Django Simple Captcha 的基本配置
# 设置验证码的显示顺序
# 一个验证码识别包含文本输入框, 隐藏域和验证图片
# CAPTCHA_OUTPUT_FORMAT(验证码输出格式)是设置三者的显示顺序
CAPTCHA_OUTPUT_FORMAT = '%(text_field)% %(hidden_field)s %(image)s'
# 设置图片噪点
CAPTCHA_NOISE_FUNCTIONS = (
    # 设置样式
    'captcha.helpers.noise_null',
    # 设置干扰线
    'captcha.helpers.noise_arcs',
    # 设置干扰点
    'captcha.helpers.noise_dots'
)

# 图片大小
CAPTCHA_IMAGE_SIZE = (100, 25)
# 设置图片背景颜色
CAPTCHA_BACkGROUND_COLOR = "#ffffff"
# 图片中的文字为随机英文字母
# CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge'
# 图片中的文字为英文单词
# CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.word_challenge'
# 图片中的文字为数学表达式
# CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.math_challenge'
# 设置字符的个数
CAPTCHA_LENGTH = 4
# 设置超时(minutes)
CAPTCHA_TIMEOUT = 1

image-20240531211709207

上述配置主要设置验证码的显示顺序, 图片噪点, 图片大小, 背景颜色和验证码内容, 具体的配置以及配置说明可以查看源代码及注释.
完成上述配置后, 下一步执行数据迁移, 因为验证码需要依赖数据表才能得以实现.

通过python manage.py migrate指令完成数据迁移, 然后查看项目所生成的数据表,
发现新增数据表captcha_captchastore, 如图12-8所示.
# 数据库迁移
PS D:\MyDjango> python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, captcha, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  ...

image-20240531122018344

12-8 数据表captcha_captchastore

12.2.2 使用验证码实现用户登录

完成Django Simple Captcha与MyDjango的功能搭建, 下一步在用户登录页面实现验证码功能.
我们将用户登录页面划分为多个不同的功能, 详细说明如下:
 用户登录页面: 由表单生成, 表单类在项目应用user的forms.py中定义.
 登录验证: 触发POST请求, 用户信息以及验证功能由Django内置的Auth认证系统实现.
 验证码动态刷新: 由Ajax向Captcha功能应用发送GET请求完成动态刷新.
 验证码动态验证: 由Ajax向Django发送GET请求完成验证码验证.
根据上述功能进行分析, 整个用户登录过程由MyDjango的urls.py和项目应用user的forms.py, urls.py, views.py和user.html共同实现.
首先在项目应用user的forms.py中定义用户登录表单类, 代码如下:
# user的forms.py
# 定义用户登录表单类
from django import forms
from captcha.fields import CaptchaField
class CaptchaTestForm(forms.Form):
    username = forms.CharField(label='用户名')
    password=forms.CharField(label='密码', widget=forms.PasswordInput)
    captcha = CaptchaField()

image-20240531211750019

从表单类CaptchaTestForm可以看到, 字段captcha是由Django Simple Captcha定义的CaptchaField对象,
该对象在生成HTML网页信息时, 将自动生成文本输入框, 隐藏控件和验证码图片.
下一步在MyDjango的urls.py和项目应用user的urls.py中定义用户登录页面的路由信息, 代码如下:
# MyDjango的urls.py
from django.urls import path, include

urlpatterns = [
    path('', include(('user.urls', 'user'), namespace='user')),
    # 导入Django Simple Captcha的路由, 生成图片地址
    path('captcha/', include('captcha.urls'))
]

image-20240604235902606

# D:\Python\Python38\Lib\site-packages\captcha\urls.py
from django.urls import re_path

from captcha import views


urlpatterns = [
    re_path(
        r"image/(?P<key>\w+)/$",
        views.captcha_image,
        name="captcha-image",
        kwargs={"scale": 1},
    ),
    re_path(
        r"image/(?P<key>\w+)@2/$",
        views.captcha_image,
        name="captcha-image-2x",
        kwargs={"scale": 2},
    ),
    re_path(r"audio/(?P<key>\w+).wav$", views.captcha_audio, name="captcha-audio"),
    re_path(r"refresh/$", views.captcha_refresh, name="captcha-refresh"),
]
views.captcha_image: 这个视图函数可能是用来生成和提供验证码图片的.
它接收一个key参数, 这个key(hashkey字段的值)用于从数据库中检索特定的验证码.
此外, 它还接收一个额外的scale参数, 这个参数是用来控制文字的大小的.
当scale为1, 生成正常大小的文字; 当scale为2, 生成两倍大小的文字.
( 注意: 图片尺寸不会改变, 只是文字大小改变!!!
http://127.0.0.1:8000/captcha/image/c81aecfa4b4d8d92d1c7da68caf3cb30e8f51a6a/   文字正常大小
http://127.0.0.1:8000/captcha/image/c81aecfa4b4d8d92d1c7da68caf3cb30e8f51a6a@2/ 文字正常大小*2 
)

views.captcha_audio: 这个视图函数可能是用来生成和提供验证码音频的.
它也接收一个key参数, 用于检索特定的验证码.
音频验证码对于视觉障碍的用户特别有用, 因为它们可以通过听音频中的字符来输入验证码.

views.captcha_refresh: 刷新验证码. 在web表单中, 当用户需要一个新的验证码时(例如, 他们可能认为当前的验证码难以识别),
他们可能会点击一个刷新按钮, 这个按钮会触发对captcha-refresh URL的请求.
视图函数接收到这个请求后, 可能会生成一个新的验证码, 并可能将其与原来的表单字段相关联, 或者将新的验证码的图片或音频返回给用户.
# user的urls.py
from django.urls import path
from .views import login, ajax_val

urlpatterns = [
    # 用户登录界面
    path('', login, name='login'),
    # 验证码验证API接口
    path('ajax_val', ajax_val, name='ajax_val')
]

image-20240604235840136

MyDjango的urls.py中分别引入了Django Simple Captcha的urls.py和项目应用user的urls.py.
前者是为验证码图片提供路由地址以及为Ajax动态刷新验证码提供API接口;
后者设置用户登录页面的路由地址以及为Ajax动态校对验证码提供API接口.
项目应用user的路由login和ajax_val分别指向视图函数login和ajax_val,
因此在user的views.py中分别定义视图函数loginView和ajax_val, 代码如下:
# user的views.py
from django.shortcuts import render
from django.contrib.auth.models import User
from django.contrib.auth import login as auth, authenticate
from .forms import CaptchaTestForm


# 用户登录
def login(request):
    if request.method == 'POST':
        form = CaptchaTestForm(request.POST)
        # 验证表单数据
        if form.is_valid():
            u = form.cleaned_data.get('username')
            p = form.cleaned_data.get('password')
            if User.objects.filter(username=u):
                user = authenticate(username=u, password=p)
                if user:
                    if user.is_active:
                        auth(request, user)
                        tips = '登录成功!'
                else:
                    tips = '账号密码错误, 请重新输入!'
            else:
                tips = '用户不存在, 请注册!'

    # GET请求
    else:
        form = CaptchaTestForm()

    return render(request, 'user.html', locals())


# ajax接口, 实现动态验证码
from django.http import JsonResponse
from captcha.models import CaptchaStore


def ajax_val(request):
    if request.is_ajax():
        # 用户输入的验证码结果
        r = request.GET.get('response')
        # 隐藏的value值
        h = request.GET.get('hashkey')
        cs = CaptchaStore.objects.filter(response=r, hashkey=h)  # 双重验证
        # 若存在cs, 则验证成功, 否则验证失败

        if cs:
            json_data = {'status': 1}
        else:
            json_data = {'status': 0}

        return JsonResponse(json_data)
    else:
        json_data = {'status': 0}
        return JsonResponse(json_data)

image-20240604235813937

可以使用Django的中间件来添加自己的is_ajax()方法.
# user的middleware.py
from django.utils.deprecation import MiddlewareMixin


class AjaxMiddleware(MiddlewareMixin):
    def process_request(self, request):
        def is_ajax():
            return request.headers.get('X-Requested-With') == 'XMLHttpRequest' or \
                request.content_type == 'application/json'

        request.is_ajax = is_ajax

image-20240604235451854

# MyDjango的settings.py
MIDDLEWARE = [
    # ...
    'user.middleware.AjaxMiddleware',  
]

2024-06-04_235535

在以上代码中, AjaxMiddleware中间件会给每个request对象添加一个is_ajax属性, 可以像以前一样使用 request.is_ajax().
视图函数login根据不同的请求方式执行不同的处理, 如果用户发送GET请求,
视图函数loginView就使用表单类CaptchaTestForm生成带验证码的用户登录页面;
如果用户发送POST请求, 视图函数login就将请求参数传递给表单类CaptchaTestForm, 
通过表单的实例化对象调用Auth认证系统, 完成用户登录过程.
视图函数ajax_val判断用户是否通过AJAX方式发送HTTP请求, 判断方法由请求对象request调用函数is_ajax.
如果当前请求由AJAX发送, 视图函数ajax_val就获取请求参数response和hashkey,
并将请求参数与Django Simple Captcha的模型CaptchaStore进行匹配, 若匹配成功, 则说明用户输入的验证码是正确的, 否则是错误的.

最后在模板文件user.html中编写用户登录页面的HTML代码和AJAX的请求过程,
AJAX的请求过程分别实现验证码的动态刷新和动态验证. 模板文件user.html的代码如下:
<!-- template的user.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Django</title>
    <script src="http://apps.bdimg.com/libs/jquery/2.1.1/jquery.min.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/mobi.css@3.1.1/dist/mobi.min.css">
</head>
<body>
<div class="flex-center">
    <div class="container">
        <div class="flex-center">
            <div class="unit-1-2 unit-1-on-mobile">
                <h1>MyDjango Verification</h1>
                {% if tips %}
                    <div>{{ tips }}</div>
                {% endif %}

                <form action="" class="form" method="post">
                    {% csrf_token %}
                    <div>用户名称:{{ form.username }}</div>
                    <div>用户密码:{{ form.password }}</div>
                    <div>验证码: {{ form.captcha }}</div>
                    <button type="submit" class="btn btn-primary btn-block">确定</button>
                </form>
            </div>
        </div>

    </div>
</div>

<script>
    $(function () {
        {#  ajax刷新验证码  #}
        $('.captcha').click(function () {  // 绑定点击事件
            console.log('click');

            // 发送 AJAX GET 请求到指定的 URL 并返回一个 JSON 对象
            $.getJSON('/captcha/refresh/', function (result) {  // 点击验证码图片刷新验证码
                $('.captcha').attr('src', result['image_url']);  // 图片地址
                $('#id_captcha_0').val(result['key']);  // 为验证码后面的隐藏框设置值, 为哈希键

            });
        });

        {#  ajax动态验证码验证  #}
        $('#id_captcha_1').blur(function () {  // 输入验证码后, 当该输入框失去焦点就会触发函数
            // #idcaptcha_1为数据框的id
            json_date = {
                // 获取输入框和隐藏字段id_captcha_0的数值
                'hashkey': $('#id_captcha_0').val(),  // 对于数据表的hashkey字段
                'response': $('#id_captcha_1').val(),  // response字段为小写的验证码
                // challenge 字段为大写的验证码

            }

            $.getJSON('/ajax_val', json_date, function (data) {
                // 验证码状态
                $('#captcha_status').remove()  // 移除验证码校验结果
                // 若status返回1, 则验证码正确
                // 若status返回0, 则验证码错误
                if (data['status']) {
                    $('#id_captcha_1').after(  // 在验证码输入框后添加span标签, 展示验证结果
                        '<span id="captcha_status"> *验证码正确!</spam>')
                } else {
                    $('#id_captcha_1').after(
                        '<span id="captcha_status"> *验证码错误!</spam>')
                }

            })
        })
    })
</script>
</body>
</html>

image-20240604235003776

<!-- form表单生成的验证码控件 -->
<div>验证码: 
    <!-- 图片验证码 -->
    <img src="/captcha/image/hashkey字段值/" alt="captcha" class="captcha">
    <!-- 隐藏控件 -->
    <input type="hidden" name="captcha_0" value="hashkey字段值" required="" id="id_captcha_0" autocomplete="off">
    <!-- 验证码输入框 -->
    <input type="text" name="captcha_1" required="" id="id_captcha_1" autocapitalize="off" 
           autocomplete="off" autocorrect="off" spellcheck="false">
</div>

2024-06-05_192102

至此, 我们已完成网站验证码功能的开发.
运行MyDjango, 访问127.0.0.1:8000即可看到带验证码的用户登录页面.
单击图片验证码将触发AJAX请求, Django动态刷新验证码图片, 并在数据表captcha_captchastore中创建验证码信息, 如图12-9所示.

2024-06-04_234151

12-9 动态刷新验证码
在PyCharm里使用createsuperuser指令创建超级管理员账号(账号和密码皆为admin),
并在用户登录页面完成用户登录, 如果在验证码文本框输入正确的验证码, 那么单击页面某空白处将触发AJAX请求,
Django动态验证验证码是否正确, 如图12-10所示.
PS D:\MyDjango> python manage.py createsuperuser
Username (leave blank to use 'blue'): admin
Email address: (回车)
Password: (admin)
Password (again): (admin)
The password is too similar to the username.
This password is too short. It must contain at least 8 characters.
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

image-20240605000051096

image-20240605000221989

12-10 动态校对验证码

12.3 站内搜索引擎

站内搜索是网站常用的功能之一, 其作用是方便用户快速查找站内数据以便查阅.
对于一些初学者来说, 站内搜索可以使用SQL模糊查询实现, 
从某个角度来说, 这种实现方式只适用于个人小型网站, 对于企业级的开发, 站内搜索是由搜索引擎实现的.

Django Haystack是一个专门提供搜索功能的Django第三方应用, 它支持Solr, Elasticsearch, Whoosh和Xapian等多种搜索引擎,
配合著名的中文自然语言处理库jieba分词可以实现全文搜索系统.

12.3.1 Django Haystack的安装与配置

本节在Whoosh搜索引擎和jieba分词的基础上使用Django Haystack实现网站搜索引擎.
因此, 在安装Django Haystack的过程中, 需要自行安装Whoosh搜索引擎和jieba分词, 具体的pip安装指令如下:
pip install django-haystack  
pip install whoosh 
pip install jieba  

# 使用清华源安装:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple django-haystack 
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple whoosh 
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple whoosh jieba 
django-haystack: 提供了全文搜索, 即时搜索和地理空间搜索等功能.
pip install whoosh: 用于Python的全文搜索引擎库, 它提供了高效的文本搜索功能.
pip install jieba: 用于中文文本的分词库. 
分词是将连续的文本切分成具有意义的词或短语的过程, 这在中文自然语言处理中非常重要, 因为中文的单词之间没有明显的分隔符(如空格).
完成上述模块的安装后, 接着在MyDjango中搭建项目环境.
在项目应用index中添加文件search_indexes.py和whoosh_cn_backend.py, 
在项目的根目录创建文件夹static和templates, static存放CSS样式文件, 
templates存放模板文件search.html和搜索引擎文件product_text.txt, 其中product_text.txt需要存放在特定的文件夹里.
整个MyDjango的目录结构如图12-11所示.

image-20240605220133189

12-11 MyDjango的目录结构
MyDjango的项目环境创建了多个文件和文件夹, 每个文件与文件夹负责实现不同的功能, 详细说明如下:
(1) search_indexes.py: 定义模型的索引类, 使模型的数据能被搜索引擎搜索.
(2) whoosh_cn_backend.py: 自定义的Whoosh搜索引擎文件.
    由于Whoosh不支持中文搜索, 因此重新定义Whoosh搜索引擎文件, 将jieba分词器添加到搜索引擎中, 使得它具有中文搜索功能.
(3) static: 存放模板文件search.html的网页样式common.css和search.css.
(4) search.html: 搜索页面的模板文件, 用于生成网站的搜索页面.
(5) product_text.txt: 搜索引擎的索引模板文件, 模板文件命名以及路径有固定格式,
   /templates/search/indexes/项目应用的名称/模型名称(小写)_text.txt.
完成MyDjango的环境搭建后, 下一步在settings.py中配置站内搜索引擎Django Haystack.
在INSTALLED_APPS中引入Django Haystack以及设置该应用的功能配置, 具体的配置信息如下:
# MyDajango的settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'index.apps.IndexConfig',
    # 配置haystack
    'haystack',
]

# 配置haystack
HAYSTACK_CONNECTIONS = {
    'default': {
        # 设置搜索引擎, 文件是index的whoosh_cn_backend.py
        'ENGINE': 'index.whoosh_cn_backend.WhooshEngine',
        'PATH': str(BASE_DIR / 'whoosh_index'),
        'INCLUDE_SPELLING': True
    }
}

# 设置每页显示的数据量
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 4
# 当数据库改变时, 会自动更新搜索引擎, 非常方便
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

image-20240605221425847

除了Django Haystack的功能配置之外, 还需要配置项目的静态资源文件夹static.
# MyDajango的settings.py
STATIC_URL = 'static/'
# 资源集合
STATICFILES_DIRS = [
    # 设置根目录的静态资源文件夹static
    BASE_DIR / 'static',
]

image-20240605222209147

观察上述配置可以发现, 配置属性HAYSTACK_CONNECTIONS的ENGINE指向项目应用index的whoosh_cn_backend.py文件的WhooshEngine类,
该类的属性backend和query分别指向WhooshSearchBackend和WhooshSearchQuery, 这是Whoosh搜索引擎的定义过程.
在Python的安装目录中可以找到Whoosh源码文件whoosh_backend.py, 如图12-12所示.

image-20240605222540491

12-12 源码文件whoosh_backend.py
我们在项目应用index的whoosh_cn_backend.py中重新定义WhooshEngine, 类属性backend指向自定义的MyWhooshSearchBackend,
然后重写MyWhooshSearchBackend的类方法build_schema, 在类方法build_schema的功能上加入jieba分词, 使其支持中文搜索.
因此, 在index的whoosh_cn_backend.py中定义MyWhooshSearchBackend和WhooshEngine, 定义过程如下:
# index的whoosh_cn_backend.py

# 文件来自haystack包, 路径为Python\Lib\site-packages\haystack\backends\whoosh_backend.py
# 导入from jieba.analyse import ChineseAnalyzer包, 添加中文搜索功能
# 将schema_fields[field_class.index_fieldname] = TEXT....的内容修改为:
# schema_fields[field_class.index_fieldname] = TEXT(stored=True, 
# analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)

from haystack.backends.whoosh_backend import *
from jieba.analyse import ChineseAnalyzer  # 中文分析仪


class MyWhooshSearchBackend(WhooshSearchBackend):
    def build_schema(self, fields):
        schema_fields = {
            ID: WHOOSH_ID(stored=True, unique=True),
            DJANGO_CT: WHOOSH_ID(stored=True),
            DJANGO_ID: WHOOSH_ID(stored=True),
        }
        # 获取在Haystack中硬编码的键的数量.
        # 我们将使用这个数量(可能)在后续出现错误时提供更优雅的失败处理方式。
        initial_key_count = len(schema_fields)
        content_field_name = ''

        for field_name, field_class in fields.items():
            if field_class.is_multivalued:
                if field_class.indexed is False:
                    schema_fields[field_class.index_fieldname] = IDLIST(
                        stored=True, field_boost=field_class.boost)
                else:
                    schema_fields[field_class.index_fieldname] = KEYWORD(
                        stored=True, commas=True, scorable=True,
                        field_boost=field_class.boost)
            elif field_class.field_type in ['date', 'datetime']:
                schema_fields[field_class.index_fieldname] = DATETIME(
                    stored=field_class.stored, sortable=True)
            elif field_class.field_type == 'integer':
                schema_fields[field_class.index_fieldname] = NUMERIC(
                    stored=field_class.stored, numtype=int,
                    field_boost=field_class.boost)
            elif field_class.field_type == 'float':
                schema_fields[field_class.index_fieldname] = NUMERIC(
                    stored=field_class.stored, numtype=float,
                    field_boost=field_class.boost)
            elif field_class.field_type == 'boolean':
                # 自 1.8.2 起,BOOLEAN 不支持字段提升.
                schema_fields[field_class.index_fieldname] = BOOLEAN(
                    stored=field_class.stored)
            elif field_class.field_type == 'ngram':
                schema_fields[field_class.index_fieldname] = NGRAM(
                    minsize=3, maxsize=15, stored=field_class.stored,
                    field_boost=field_class.boost)
            elif field_class.field_type == 'edge_ngram':
                schema_fields[field_class.index_fieldname] = NGRAMWORDS(
                    minsize=2, maxsize=15, at='start',
                    stored=field_class.stored,
                    field_boost=field_class.boost)
            else:
                schema_fields[field_class.index_fieldname] = TEXT(
                    stored=True, analyzer=ChineseAnalyzer(),
                    field_boost=field_class.boost, sortable=True)

            if field_class.document is True:
                content_field_name = field_class.index_fieldname
                schema_fields[field_class.index_fieldname].spelling = True

        # 失败比依靠后端在没有字段的情况下死亡更优雅
        # 被找到.
        if len(schema_fields) <= initial_key_count:
            # 在任何search_indexes中均未找到字段. 请在尝试搜索之前更正此问题.
            raise SearchBackendError(
                "No fields were found in any search_indexes. Please correct this before attempting to search.")

        return (content_field_name, Schema(**schema_fields))


# 重新定义搜索引擎
class WhooshEngine(BaseEngine):
    # 将搜索引擎指向自定义的MyWhooshSearchBackend
    backend = MyWhooshSearchBackend
    query = WhooshSearchQuery

image-20240605224524680

下一步在index的models.py中定义模型Product, 模型设有4个字段, 主要记录产品的基本信息, 如产品名称, 重量和描述等.
模型Product的定义过程如下:
# index的models.py
from django.db import models


# 创建产品信息表
class Product(models.Model):
    id = models.AutoField('序号', primary_key=True)
    name = models.CharField('名称', max_length=50)
    weight = models.CharField('重量', max_length=20)
    describe = models.CharField('描述', max_length=500)

    # 设置返回值
    def __str__(self):
        return self.name

image-20240605225750283

然后在PyCharm的Terminal中为MyDjango项目执行数据迁移.
# 数据迁移
PS D:\MyDjango> python manage.py makemigrations
Migrations for 'index':
  index\migrations\0001_initial.py
    - Create model Product
PS D:\MyDjango> python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, index, sessions
Running migrations:
 ...

image-20240605231342746

使用Navicat Premium打开MyDjango的db.sqlite3数据库文件, 在数据表index_product中添加产品信息, 如图12-13所示.
苹果: 200g, 苹果是常见的水果之一, 具有丰富的营养价值, 如维生素C和膳食纤维. 苹果的品种多样, 如红富士, 嘎啦苹果等.
香蕉: 150g, 香蕉是全球广泛种植的水果, 以其独特的甜味和软糯的口感受到人们喜爱. 香蕉富含钾元素, 对维持人体正常生理功能有重要作用.
橙子: 150g, 橙子富含维生素C, 有助于提高免疫力. 橙子的品种繁多, 如脐橙, 血橙等, 每种都有其独特的风味和营养价值.
葡萄: 15g, 葡萄是一种广泛种植的水果, 含有多种维生素和矿物质, 葡萄除了鲜食外, 还可以用于制作葡萄酒, 葡萄干等食品.
菠萝: 1000g, 菠萝是一种热带水果, 具有独特的香甜味道和丰富的营养价值. 菠萝中的菠萝蛋白酶有助于消化, 对人体有益.
草莓: 33g, 草莓是一种小而多汁的水果, 富含维生素C和抗氧化物质. 草莓的口感鲜美, 常用于制作甜点, 果汁等食品.
西瓜: 5000g, 西瓜是夏季常见的水果, 具有清凉解暑的作用. 西瓜含有大量水分和糖分, 能迅速补充人体所需的能量和水分.
芒果: 100g, 芒果是一种热带水果, 以其独特的香甜味道和丰富的营养价值受到人们喜爱. 芒果富含维生素A和C, 有助于保护眼睛和皮肤健康.
樱桃: 20g, 樱桃是一种小而甜美的水果, 富含维生素C和铁元素. 樱桃的口感鲜美, 常用于制作甜点, 果酱等食品.
荔枝: 28g, 荔枝是一种热带水果, 以其甜美的味道和独特的香气受到人们喜爱. 荔枝富含维生素C和多种矿物质, 对人体有益.
INSERT INTO index_product (name, weight, describe) VALUES  
('苹果', '200g', 
'苹果是常见的水果之一, 具有丰富的营养价值, 如维生素C和膳食纤维. 苹果的品种多样, 如红富士, 嘎啦苹果等.'),  
('香蕉', '150g', 
'香蕉是全球广泛种植的水果, 以其独特的甜味和软糯的口感受到人们喜爱. 香蕉富含钾元素, 对维持人体正常生理功能有重要作用.'),  
('橙子', '150g', 
'橙子富含维生素C,
有助于提高免疫力. 橙子的品种繁多, 如脐橙, 血橙等, 每种都有其独特的风味和营养价值.'),  
('葡萄', '15g', 
'葡萄是一种广泛种植的水果, 含有多种维生素和矿物质, 葡萄除了鲜食外, 还可以用于制作葡萄酒, 葡萄干等食品.'),  
('菠萝', '1000g', 
'菠萝是一种热带水果, 具有独特的香甜味道和丰富的营养价值. 菠萝中的菠萝蛋白酶有助于消化, 对人体有益.'),  
('草莓', '33g',
'草莓是一种小而多汁的水果, 富含维生素C和抗氧化物质. 草莓的口感鲜美, 常用于制作甜点, 果汁等食品.'),  
('西瓜', '5000g', 
'西瓜是夏季常见的水果, 具有清凉解暑的作用. 西瓜含有大量水分和糖分, 能迅速补充人体所需的能量和水分.'),  
('芒果', '100g', 
'芒果是一种热带水果, 以其独特的香甜味道和丰富的营养价值受到人们喜爱. 芒果富含维生素A和C, 有助于保护眼睛和皮肤健康.'),  
('樱桃', '20g', 
'樱桃是一种小而甜美的水果, 富含维生素C和铁元素. 樱桃的口感鲜美, 常用于制作甜点, 果酱等食品.'),  
('荔枝', '28g', 
'荔枝是一种热带水果, 以其甜美的味道和独特的香气受到人们喜爱. 荔枝富含维生素C和多种矿物质, 对人体有益.');

image-20240606001445874

12-13 数据表index_product

12.3.2 使用搜索引擎实现产品搜索

完成settings.py, whoosh_cn_backend.py和模型Product的配置和定义后, 我们可以在MyDjango中实现站内搜索引擎的功能开发.
首先创建搜索引擎的索引, 创建索引主要能使搜索引擎快速找到符合条件的数据, 
索引就像是书本的目录, 可以为读者快速地查找内容, 在这里也是同样的道理.
当数据量非常大的时候, 要从这些数据中找出所有满足搜索条件的数据是不太可能的,
并且会给服务器带来极大的负担, 所以我们需要为指定的数据添加一个索引.

索引是在search_indexes.py中定义的, 然后由指令执行创建过程.
以模型Product为例, 在search_indexes.py中定义该模型的索引类, 代码如下:
# index的search_indexes.py
from haystack import indexes
from .models import Product


# 类名必须为模型名+Index
# 比如模型Product,索引类为ProductIndex
class ProductIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)

    # 设置模型
    def get_model(self):
        return Product

    # 设置查询范围
    def index_queryset(self, using=None):
        return self.get_model().objects.all()

image-20240606001918799

从上述代码来看, 在定义模型的索引类ProductIndex时, 类的定义要求以及定义说明如下:
(1) 定义索引类的文件名必须为search_indexes.py, 不得修改文件名, 否则程序无法创建索引.
(2) 模型的索引类的类名格式必须为"模型名+Index", 每个模型对应一个索引类, 如模型Product的索引类为ProductIndex.
(3) 字段text设置document=True, 代表搜索引擎使用此字段的内容作为索引.
    (在Haystack中, 当定义多个搜索索引(即多个继承自haystack.indexes.SearchIndex的类),
     Haystack期望每个索引类都有一个特殊的字段, 这个字段用于存储文档的主要内容, 以便进行全文搜索.
     这个字段通常被命名为text, 并且它被标记为document=True.)
(4) use_template=True使用索引模板文件, 可以理解为在索引中设置模型的查询字段,
    如设置Product的describe字段, 这样可以通过describe的内容检索Product的数据.
(5) 类函数get_model是将该索引类与模型Product进行绑定, 类函数index_queryset用于设置索引的查询范围.
由上述分析得知, use_template=True代表搜索引擎使用索引模板文件进行搜索, 索引模板文件的路径是固定不变的,
路径格式为/templates/search/indexes/项目应用名称/模型名称(小写)_text.txt,
如MyDjango的templates/search/indexes/index/product_text.txt.
我们在索引模板文件product_text.txt中设置模型Product的字段name和describe作为索引的检索字段,
因此在索引模板文件product_text.txt中编写以下代码:
# templates/search/indexes/index/product_text.txt
{{ object.name }}
{{ object.describe }}

image-20240606002358626

上述设置是对模型Product的字段name和describe建立索引, 当搜索引擎进行搜索时,
Django根据搜索条件对这两个字段进行全文检索匹配, 然后将匹配结果排序并返回.
现在只是定义了搜索引擎的索引类和索引模板文件, 下一步根据索引类和索引模板文件创建搜索引擎的索引文件.
在PyCharm的Terminal中运行: python manage.py rebuild_index , 指令即可完成索引文件的创建,
MyDjango的根目录自动创建whoosh_index文件夹, 该文件夹中含有索引文件, 如图12-14所示.
PS D:\MyDjango> python manage.py rebuild_index
WARNING: This will irreparably remove EVERYTHING from your search index in connection 'default'.
Your choices after this are to restore from backups or rebuild via the `rebuild_index` command.
Are you sure you wish to continue? [y/N] y
Removing all documents from your index because you said so.
All documents removed.
Indexing 10 products
Building prefix dict from the default dictionary ...
Dumping model to file cache C:\Windows\TEMP\jieba.cache
Loading model cost 1.448 seconds.
Prefix dict has been built successfully.
当运行: python manage.py rebuild_index 命令时, 正在尝试重建 Django 项目中使用的搜索索引.
该命令会清空(或删除)当前的索引, 然后重新创建它, 以便从数据库或其他数据源中重新填充数据.

输出中, 会有一个警告信息, 提示这个操作会不可逆地删除所有在默认连接('default')上的搜索索引内容.
这是非常重要的, 因为一旦索引被清空, 除非你有备份, 否则你将无法恢复它.

如果确定要继续并清空索引, 应该输入 y并按回车键. 如果你不想继续, 只需按回车键或输入任何非 y 的字符.

image-20240606003058570

12-14 whoosh_index文件夹
最后在MyDjango中实现模型Product的搜索功能, 在MyDjango的urls.py和index的urls.py中定义路由haystack, 代码如下:
# MyDjango的urls.py
from django.urls import path, include

urlpatterns = [
    path('', include(('index.urls', 'index'), namespace='index')),
]

image-20240606003701996

# index的urls.py
from django.urls import path
from .views import MySearch

urlpatterns = [
    # 搜索引擎
    path('', MySearch(), name='haystack'),
]

image-20240606003958417

路由haystack指向视图类MySearch, 视图类MySearch继承Django Haystack定义的视图类SearchView,
它与Django内置视图类的定义过程十分相似, 本书就不再深入分析视图类SearchView的定义过程, 
我们在index的views.py中定义视图MySearch, 并且重写父类的方法get(),
这是自定义视图类MySearch接收HTTP的GET请求的响应内容, 实现代码如下:
from django.core.paginator import *
from django.shortcuts import render
from django.conf import settings
from .models import Product
from haystack.generic_views import SearchView


# 视图以通用视图实现
class MySearch(SearchView):
    # 模板文件
    template_name = 'search.html'

    # get请求
    def get(self, request, *args, **kwargs):
        if not self.request.GET.get('q', ):
            product = Product.objects.all().order_by('id')  # 获取所有数据并排序
            per = settings.HAYSTACK_SEARCH_RESULTS_PER_PAGE  # 每页4条记录
            p = Paginator(product, per)  # 分页器,

            try:
                # 没有提交page的值, 则返回第一页的数据
                num = int(self.request.GET.get('page', 1))
                page_obj = p.page(num)
            except PageNotAnInteger:
                # 如果参数page不是整形, 则返回第一页数据
                page_obj = p.page(1)

            except EmptyPage:
                # 访问页数大于总页数, 则返回第一页的数据
                page_obj = p.page(p.num_pages)

            return render(request, self.template_name, locals())

        #  请求中携带q时, 处理 GET 请求并实例化表单的空白版本
        else:
            return super().get(*args, request, *args, **kwargs)

修改一下, 页码使用内置的get_page方法获取.
# index的views.py
from django.core.paginator import *
from django.shortcuts import render
from django.conf import settings
from .models import Product
from haystack.generic_views import SearchView


# 视图以通用视图实现
class MySearch(SearchView):
    # 模板文件
    template_name = 'search.html'

    # get请求
    def get(self, request, *args, **kwargs):
        if not self.request.GET.get('q', ):
            product = Product.objects.all().order_by('id')  # 获取所有数据并排序
            per = settings.HAYSTACK_SEARCH_RESULTS_PER_PAGE  # 每页4条记录
            p = Paginator(product, per)  # 分页器,

            num = int(self.request.GET.get('page', 1))
            # 没有提交page的值, 则返回第一页的数据
            # 如果参数page不是整形, 则返回第一页数据
            # 访问页数大于总页数, 则返回第一页的数据
            page_obj = p.get_page(num)

            return render(request, self.template_name, locals())

        #  请求中携带q时, 处理 GET 请求并实例化表单的空白版本
        else:
            return super().get(*args, request, *args, **kwargs)

image-20240606025024288

视图类MySearch指定模板文件search.html作为HTML网页文件; 
并且自定义GET请求的处理函数get(), 通过判断当前请求是否存在请求参数q, 
若不存在, 则将模型Product的全部数据进行分页显示, 否则使用搜索引擎全文搜索模型Product的数据.
模板文件search.html用于显示搜索结果的数据, 网页实现的功能包括搜索文本框, 产品信息列表和产品信息分页.
模板文件search.html的代码如下:
<!-- template的search.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>搜索引擎</title>
    {# 导入CSS样式文件 #}
    {% load static %}
    <link rel="stylesheet" href="{% static "common.css" %}">
    <link rel="stylesheet" href="{% static "search.css" %}">
</head>
<body>
<div class="header">
    <div class="search-box">
        <form action="" method="get">
            <div class="search-keyword">
                {# 搜索文本框必须命名为q #}
                <input name="q" type="text" class="keyword">
            </div>
            <input type="submit" class="search-button" value="搜 索">
        </form>
    </div>
</div>

<div class="wrapper clearfix">
    <div class="listinfo">
        <ul class="listheader">
            <li class="name">产品名称</li>
            <li class="weight">重量</li>
            <li class="describe">描述</li>
        </ul>
        <ul class="ullsit">
            {# 列出当前分页所对应的数据内容 #}
            {% if query %}
                {# 导入自带高亮功能 #}
                {% load highlight %}
                {% for item in page_obj.object_list %}
                    <li>
                        <div class="item">
                            <div class="nameinfo">{% highlight item.object.name with query %}</div>
                            <div class="weightinfo">{{ item.object.weight }}</div>
                            <div class="describeinfo">{% highlight item.object.describe with query %}</div>
                        </div>
                    </li>
                {% endfor %}
            {% else %}
                {% for item in page_obj.object_list %}
                    <li>
                        <div class="item">
                            <div class="nameinfo">{{ item.name }}</div>
                            <div class="weightinfo">{{ item.weight }}</div>
                            <div class="describeinfo">{{ item.describe }}</div>
                        </div>
                    </li>
                {% endfor %}
            {% endif %}
        </ul>
        {# 分页导航 #}
        <div class="page-box">
            <div class="pagebar" id="pageBar">
                {# 上一页的路由地址 #}
                {% if page_obj.has_previous %}
                    {% if query %}
                        <a href="{% url 'index:haystack' %}?q={{ query }}&page={{ page_obj.previous_page_number }}"
                           class="prev">上一页</a>
                    {% else %}
                        <a href="{% url 'index:haystack' %}?page={{ page_obj.previous_page_number }}"
                           class="prev">上一页</a>
                    {% endif %}
                {% endif %}
                {# 列出所有的路由地址 #}
                {% for num in page_obj.paginator.page_range %}
                    {% if num == page_obj.number %}
                        <span class="sel">{{ page_obj.number }}</span>
                    {% else %}
                        {% if query %}
                            <a href="{% url 'index:haystack' %}?q={{ query }}&page={{ num }}">{{ num }}</a>
                        {% else %}
                            <a href="{% url 'index:haystack' %}?page={{ num }}">{{ num }}</a>
                        {% endif %}
                    {% endif %}
                {% endfor %}
                {# 下一页的路由地址 #}
                {% if page_obj.has_next %}
                    {% if query %}
                        <a href="{% url 'index:haystack' %}?q={{ query }}&page={{ page_obj.next_page_number }}"
                           class="next">下一页</a>
                    {% else %}
                        <a href="{% url 'index:haystack' %}?page={{ page_obj.next_page_number }}"
                           class="next">下一页</a>
                    {% endif %}
                {% endif %}
            </div>
        </div>
    </div>
</div>
</body>
</html>

image-20240606025341148

模板文件search.html分别使用了模板上下文page_obj, query和模板标签highlight, 具体说明如下:
 page_obj来自视图类MySearch, 这是模型Product分页处理后的数据对象.
 query来自Django Haystack定义的视图类SearchView, 它的值来自请求参数q, 即搜索文本框的内容.
 Highlight是由Django Haystack定义的模板标签, 它将用户输入的关键词进行高亮处理.	
(搜索和高亮: 代码包含一个判断逻辑{% if query %}, 用于检查是否存在一个搜索查询(query).
如果存在查询, 代码会导入一个名为highlight的模板标签库,
并使用{% highlight ... with query %}来高亮显示与查询匹配的文本.
这允许用户搜索列表中的对象, 并快速识别出与搜索查询相关的部分.)
至此, 我们已完成站内搜索引擎的功能开发.
运行MyDjango并访问: 127.0.0.1:8000 , 网页首先显示模型Product所有数据, 当在搜索页面的文本框中输入'维生素', (至少两个字)
并单击'搜索'按钮即可实现模型Product的字段name和describe的全文搜索, 如图12-15所示.

image-20240606024655595

12-15 运行结果

12.4 第三方网站实现用户注册

(跳过吧, 没有说怎么登录, 还不会弄, 以后补上!!!)
用户注册与登录是网站必备的功能之一, Django内置的Auth认证系统可以帮助开发人员快速实现用户管理功能.
但很多网站为了加强社交功能, 在用户管理功能上增设了第三方网站的用户注册与登录功能, 这是通过OAuth 2.0认证与授权来实现的.

12.4.1 Social-Auth-App-Django的安装与配置

OAuth 2.0的实现过程相对烦琐, 我们通过流程图来大致了解OAuth 2.0的实现过程, 如图12-16所示.

image-20240606181603105

12-16 OAuth 2.0的实现过程
分析图12-16的实现过程, 我们可以简单理解OAuth 2.0认证与授权是两个网站的服务器后台进行通信交流.
根据实现原理, 可以使用requests或urllib模块实现OAuth 2.0认证与授权, 从而实现第三方网站的用户注册与登录功能.
但是一个网站可能同时使用多个第三方网站, 这种实现方式就不太可取, 而且代码会出现重复使用的情况.
因此, 我们可以使用Django第三方功能应用Social-Auth-App-Django, 它为我们提供了各大网站平台的认证与授权功能.
Social-Auth-App-Django是在Python Social Auth的基础上进行封装而成的, 而Python Social Auth支持多个Python的Web框架使用,
如Django, Flask, Pyramid, CherryPy和Webpy.
除了安装Social-Auth-App-Django之外, 还需要安装Python Social Auth, 我们通过pip方式进行安装, 安装指令如下:
pip install python-social-auth  # 用于为Python web框架(如Django, Flask等)提供第三方身份验证支持.
# 若Django使用关系式数据库, 如MySQL, Oracle等
# 则安装social-auth-app-django
pip install social-auth-app-django  # 专门为Django设计. 它提供了Django特定的配置, 模板和集成.
# 若Django使用非关系式数据库, 如MongoDB
# 则安装social-auth-app-django-mongoengine
pip install social-auth-app-django-mongoengine
# 使用清华源安装:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple python-social-auth
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple social-auth-app-django 
功能模块安装成功后, 以MyDjango为例, 讲述如何使用Social-Auth-App-Django实现第三方网站的用户注册.
在MyDjango中创建项目应用user, 模板文件夹templates创建模板文件user.html, MyDjango的目录结构如图12-17所示.

image-20240606183848887

12-17 MyDjango的目录结构
MyDjango的目录结构较为简单, 因为Social-Auth-App-Django的本质是一个Django的项目应用.
我们可以在Python的安装目录下找到Social-Auth-App-Django的源代码文件,
发现它具有urls.py, views.py和models.py等文件, 这与Django的项目应用的文件架构是一样的, 如图12-18所示.

image-20240606184143776

12-18 Social-Auth-App-Django的源码文件
了解Social-Auth-App-Django的本质, 在后续的使用中可以更加清晰地知道它的实现过程.
Social-Auth-App-Django支持国内外多个网站认证, 
可以在官方文档中查阅网站认证信息(https://python-social-auth.readthedocs.io/en/latest/).
(https://python-social-auth.readthedocs.io/en/latest/index.html)
本节以微博为例, 通过微博认证实现MyDjango的用户注册功能.
首先在浏览器中打开微博开放平台(http://open.weibo.com/), 登录微博并新建网站接入应用, 如图12-19所示.
* 1. 先登录.
* 2. 点击网站接入WEB.

2024-06-06_185258

12-19 新建网站接入应用
* 3. 填写基本信息和身份认证.

202406061903902

2024-06-06_191611

* 4. 打开邮箱, 点击完成确认注册的连接.

image-20240606190543940

* 5. 完成身份验证.

image-20240606191010799

* 6. 创建新应用.
     我们将网站接入应用命名为MyDjango1, 在网站接入应用的页面中找到"应用信息",
     从中获取网站接入应用的App Key和App Secret, 如图12-20所示.

image-20240606190650400

* 7. 保存App Key和App Secret
     App Key: 154944094721_21
     App Secret: e96a606e700b33714c657d95937d937a21_21

2024-06-09_021900

12-20 应用信息
* 8. 设置OAuto2.0 授权设置.
     下一步设置网站接入应用的OAuth 2.0授权设置, '应用信息'的标签里单击'高级信息',
     编辑OAuth 2.0授权设置的授权回调页, 如图12-21所示.
     授权回调页: http://127.0.0.1:8000/complete/weibo/
     取消授权回调页: http://127.0.0.1:8000/disconnect/weibo/

2024-06-07_145427

12-21 OAuth 2.0授权设置
授权回调页和取消授权回调页的路由地址是有格式要求的, 因为两者的执行过程是由Social-Auth-App-Django实现的,
而Social-Auth-App-Django的源码文件urls.py已为授权回调页和取消授权回调页设置具体的路由地址.
打开源码文件urls.py, 查看路由定义过程, 如图12-22所示.

image-20240609023019198

12-22 源码文件urls.py
(每个路由的说明:
* 1. 登录/关联路由: path(f"login/<str:backend>{extra}", views.auth, name="begin")
  URL模式: login/后面跟随一个字符串(:backend), 这个字符串可能是一个认证后端(例如: google, facebook等).
  {extra}: 这里的{extra}是一个占位符, 但它在您给出的代码段中并未定义.
  通常, 它可能是为了后续添加额外的URL参数或模式(例如, 查询参数, 可选的路径部分等).
  但在没有具体定义的情况下, 它可能不会正常工作.
  
  视图函数: views.auth是处理登录/关联逻辑的视图函数. 
  名称: 这个路由的URL名称是begin, 可以在Django模板或其他URL重定向中使用这个名称.

* 2. 完成路由: path(f"complete/<str:backend>{extra}", views.complete, name="complete")
     URL模式: 与登录路由类似, complete/后面跟随一个字符串(:backend), 表示完成某个认证后端的流程.
     视图函数: views.complete是处理认证后端完成逻辑的视图函数.
     名称: 这个路由的URL名称是complete.

* 3. 断开连接路由: 
     path(f"disconnect/<str:backend>{extra}", views.disconnect, name="disconnect")
     URL模式: disconnect/后面跟随一个字符串(:backend), 表示断开与某个认证后端的连接。
     视图函数: views.disconnect是处理断开连接逻辑的视图函数.
     名称: 这个路由的URL名称是disconnect.
     
     path(f"disconnect/<str:backend>/<int:association_id>{extra}", views.disconnect, name="disconnect_individual")/
     URL模式: 与上面的断开连接路由类似, 但它还包含了一个整数(:association_id),
     可能表示特定的关联ID, 用于断开与特定服务或用户的连接.
     视图函数: 同样使用 views.disconnect 视图函数来处理逻辑, 但这次它可能会根据提供的association_id来断开特定的连接.
     名称: 这个路由的URL名称是disconnect_individual.)
源码文件urls.py的路由变量backend代表网站名称, /complete/weibo/代表微博网站的OAuth 2.0授权设置.
完成上述配置后, 接着在settings.py中设置Social-Auth-App-Django的配置信息,
主要在INSTALLED_APPS和TEMPLATES中引入功能模块以及设置功能配置, 配置信息如下:
# MyDjango的settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'user.apps.UserConfig',
    # 添加第三方应用
    'social_django'
]

# 设置第三方的OAuth2.0
AUTHENTICATION_BACKENDS = {  # 身份验证后端
    # 微博的功能
    'social.backends.weibo.WeiboOAuth2',
    # QQ的功能
    # 'social.backends.qq.QQOAuth2',
    # 微信的功能
    # 'social.backends.weixin.WeixinOAuth2',
    # 模型后端
    'django.contrib.auth.backends.ModelBackend'
}

# 注册成功后跳转页面
SOCIAL_AUTH_LOGIN_REDIRECT_URL = 'user:success'  # 社交身份验证登录重定向 URL
SOCIAL_AUTH_WEIBO_KEY = '1549440947'  # 社交授权微博密钥
SOCIAL_AUTH_WEIBO_SECRET = 'e96a606e700b33714c657d95937d937a'  # 社交授权微博秘密
# SOCIAL_AUTH_QQ_KEY = 'APPID'
# SOCIAL_AUTH_QQ_SECRET = 'SECRET'
# SOCIAL_AUTH_WEIXIN_KEY = 'APPID'
# SOCIAL_AUTH_WEIXIN_SECRET = 'SECRET'
SOCIAL_AUTH_URL_NAMESPACE = 'social'  # 社交身份验证 URL 命名空间

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates']
        ,
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'social_django.context_processors.backends',  # 认证后端列表
                'social_django.context_processors.login_redirect'  # 重定向逻辑
            ],
        },
    },
]

image-20240609031730979

(context_processors是用于向模板上下文添加额外变量的函数.
这些变量可以在所有的模板中直接使用, 无需在每个视图函数中都显式地传递它们.
'social_django.context_processors.backends''social_django.context_processors.login_redirect'
是两个由social-django应用提供的上下文处理器.

'social_django.context_processors.backends': 这个上下文处理器通常用于向模板中添加当前可用的认证后端列表.
这对于在模板中动态显示第三方登录按钮(如Google, Facebook等)非常有用.
例如, 如果在模板中需要显示多个第三方登录按钮, 并且这些按钮应该基于当前配置和可用的认证后端来动态显示,
那么这个上下文处理器会非常有用.

'social_django.context_processors.login_redirect': 这个上下文处理器通常用于处理登录后的重定向逻辑.	
当用户使用第三方认证登录后, 可能想将他们重定向到一个特定的页面,
这个页面可能是他们之前试图访问的受保护页面, 或者是你的应用的主页. 
这个上下文处理器可以帮助你管理这个重定向逻辑.)
由于Social-Auth-App-Django的源码文件models.py中定义了多个模型, 完成MyDjango的配置后, 
还需要使用migrate指令执行数据迁移, 在MyDjango的数据库文件db.sqlite3中生成数据表.
# 数据迁移
PS D:\MyDjango> python manage.py makemigrations
No changes detected
# 报错:
PS D:\MyDjango> python manage.py migrate
SystemCheckError: System check identified some issues:

ERRORS:
social_django.Partial: (fields.E180) SQLite does not support JSONFields.
social_django.UserSocialAuth: (fields.E180) SQLite does not support JSONFields.

# 这个SystemCheckError 错误表明 Django 项目中使用了 JSONField, 但是数据库是SQLite, 而SQLite不原生支持JSONField.
# Django的JSONField是在Django 3.1版本中引入的, 用于在数据库中存储JSON数据.
对于使用JSONField与Django 3.1+和sqlite, 必须安装sqlite扩展.

要在sqlite上启用JSON1, 请按照Django的wiki: https://code.djangoproject.com/wiki/JSON1Extension上的说明操作.

从Python 3.9开始, Windows上的官方Python安装程序已经包含JSON1默认情况下扩展名.
如果您使用的是早期版本的Python或非官方安装程序,  您可以执行以下操作:

下载与您的 Python 安装 (32 位或 64 )匹配的预编译 DLL.
找到您的Python安装.
默认情况下, 它应该位于%localappdata%\Programs\Python\PythonXX中, 其中XX是Python版本.
例如, 它位于 C:\Users\<username>\AppData\Local\Programs\Python\Python37 .
如果将Python安装目录添加到PATH环境变量中, 则可以运行命令, 其中python在命令提示符下找到它.
在Python安装中输入DLL目录.
重命名(或删除)DLLs目录中的sqlite3.dll.
从下载的DLL存档中提取sqlite3.dll并将其放在DLL目录中.
现在, JSON1扩展应该可以在Python和Django中使用了.

意思就是用3.9版本以后的sqlite3.dll文件替换旧版本的sqlite3.dll文件.
(不推荐跨太多版本, 我从3.11版本提取出来的文件复制过去后, 无法启动Django项目...)

2024-06-09_040029

# 数据迁移, 都没反应, 这个dll文件有问题...
PS D:\MyDjango> python manage.py makemigrations
PS D:\MyDjango> python manage.py migrate    

image-20240609040316100

推荐: sqlite3下载地址: https://www.sqlite.org/download.html , (成功).

2024-06-09_041209

# 数据迁移, 成功!
PS D:\MyDjango> python manage.py migrate                                                       
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, social_django
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK
  Applying social_django.0001_initial... OK
  Applying social_django.0002_add_related_name... OK
  Applying social_django.0003_alter_email_max_length... OK
  Applying social_django.0004_auto_20160423_0400... OK
  Applying social_django.0005_auto_20160727_2333... OK
  Applying social_django.0006_partial... OK
  Applying social_django.0007_code_timestamp... OK
  Applying social_django.0008_partial_timestamp... OK
  Applying social_django.0009_auto_20191118_0520... OK
  Applying social_django.0010_uid_db_index... OK
  Applying social_django.0011_alter_id_fields... OK
  Applying social_django.0012_usersocialauth_extra_data_new... OK
  Applying social_django.0013_migrate_extra_data... OK
  Applying social_django.0014_remove_usersocialauth_extra_data... OK
  Applying social_django.0015_rename_extra_data_new_usersocialauth_extra_data... OK

image-20240609042433296

12.4.2 微博账号实现用户注册

在MyDjango中完成Social-Auth-App-Django的环境搭建后, 
下一步在MyDjango中实现用户注册页面和Social-Auth-App-Django的回调页面.首先定义路由register和success, 
并且引入Social-Auth-App-Django的路由文件, 实现代码如下:
# MyDjango的urls.py
from django.urls import path, include

urlpatterns = [
    path('', include(('user.urls', 'user'), namespace='user')),
    # 导入social_django的路由信息
    path('', include('social_django.urls', namespace='social'))
]

image-20240609035614553

(当用户访问根URL(/), Django会首先检查第一个include(即user.urls中的模式).
如果在user.urls中没有找到与根URL匹配的模式, Django会继续检查第二个include(即social_django.urls中的模式).

这意味着, 如果第一个include中没有匹配的项, Django确实会'去第二个路由中找'.
然而, 这种配置可能导致预期之外的匹配行为, 因为两个include都试图处理相同的URL路径.
通常, 为了避免混淆和潜在的冲突, 最好为每个include指定一个独特的路径前缀,
除非你有明确的理由让它们都匹配根URL, 并且你已经仔细设计了每个include中的URL模式以避免冲突.)
# user的urls.py
from django.urls import path
from .views import *

urlpatterns = [
    # 用户注册界面的路由地址,显示微博登录链接
    path('', login, name='login'),
    # 注册后回调的页面,成功注册后跳转回站内地址
    path('success', success, name='success')
]

image-20240609045807742

路由login和success分别指向视图函数register和success, 视图函数无须实现任何功能,
因为整个授权认证过程都是由Social-Auth-App-Django实现的.
视图函数register和success的代码如下:
# user的views.py
from django.shortcuts import render
from django.http import HttpResponse


# 用户注册页面
def register(request):
    title = '用户注册'
    return render(request, 'register.html', locals())


# 注册后回调的页面
def success(request):
    return HttpResponse('注册成功')

image-20240609045842767

视图函数register使用模板文件register.html作为网页内容,
模板文件register.html将使用Social-Auth-App-Django定义的路由地址作为第三方网站的认证链接, 具体代码如下:
<!-- templates的register.html  -->
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
    <link rel="stylesheet"
          href="https://unpkg.com/mobi.css/dist/mobi.min.css">
</head>
<body>
<div class="flex-center">
    <div class="container">
        <div class="flex-center">
            <div>
                <h1>Social-Auth-App-Django</h1>
                <div>
                    {# 路由地址来自Social-Auth-App-Django的源码文件urls.py #}
                    <a class="btn btn-primary btn-block"
                       href="{% url "social:begin" "weibo" %}">微博注册</a>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

image-20240609045929096

( href="{% url "social:begin" "weibo" %}">微博注册</a> 对应路由
path(f"login/<str:backend>{extra}", views.auth, name="begin"))
至此, 我们通过微博认证实现了MyDjango的用户注册功能.
运行MyDjango, 在浏览器上访问: 127.0.0.1:8000, 在用户登录页面单击'微博注册'按钮,
Django将触发Social-Auth-App-Django的路由begin, 由路由begin向微博平台发送OAuth 2.0授权认证,
如果认证成功, 就访问Social-Auth-App-Django的路由complete完成认证过程, 
再由路由complete重定向路由success, 从而完成整个用户注册过程.

微博授权成功后, 打开数据表social_auth_usersocialauth和auth_user可以看到用户注册信息, 如图12-23所示.

image-20240609042559657

image-20240609042626256

image-20240609042645020

image-20240609042924897

12.5 异步任务和定时任务

网站的并发编程主要处理网站烦琐的业务流程, 网站从收到HTTP请求到发送响应内容的过程中, 
Django处理用户请求主要在视图中执行, 视图是一个函数或类, 而且是单线程执行的.
在视图函数或视图类处理用户请求时, 如果遇到烦琐的数据读写或高密度计算, 往往会造成响应时间过长, 
在网页上容易出现卡死的情况, 不利于用户体验.
为了解决这种情况, 我们可以在视图中加入异步任务, 让它处理一些耗时的业务流程, 从而缩短用户的响应时间.

12.5.1 Celery的安装与配置

Django的分布式主要由Celery框架实现, 这是Python开发的异步任务队列.
它支持使用任务队列的方式在分布的机器, 进程和线程上执行任务调度.
Celery侧重于实时操作, 每天处理数以百万计的任务.
Celery本身不提供消息存储服务, 它使用第三方数据库来传递任务, 目前第三方数据库支持RabbitMQ, Redis和MongoDB等.
本节使用第三方应用Django Celery Results, Django Celery Beat, Celery和Redis数据库实现Django的异步任务和定时任务开发.
值得注意的是, 定时任务是异步任务的一种特殊类型的任务.
首先需要安装Redis数据库, 在Windows中安装Redis数据库有两种方式: 在官网下载压缩包安装或者在GitHub下载MSI安装程序.
前者的数据库版本是最新的, 但需要通过指令安装并设置相关的环境配置; 
后者是旧版本, 安装方法是傻瓜式安装, 启动安装程序后按照安装提示即可完成安装.
两者的下载地址如下:
# 官网下载地址
https://redis.io/download
# GitHub下载地址
https://github.com/MicrosoftArchive/redis/releases
# 下载地址
https://github.com/microsoftarchive/redis/releases/download/win-3.2.100/Redis-x64-3.2.100.msi

2024-06-06_213625

Redis数据库的安装过程本书就不详细讲述了, 读者可以自行查阅相关的资料.
安装完成后会自动启动.

image-20240608041445257

除了安装Redis数据库之外, 还可以安装Redis数据库的可视化工具, 可视化工具可以帮助初次接触Redis的读者了解数据库结构.
本书使用Redis Desktop Manager作为Redis的可视化工具, 如图12-24所示.

下载地址: https://wwx.lanzoui.com/i5C6Ntpf9qj
密码: 21wh

image-20240606215922497

12-24 Redis Desktop Manager
接下来介绍安装异步任务所需要的功能模块, 这些功能模块有celery, redis, django-celery-results, django-celery-beat和eventlet.它们都能使用pip指令实现安装, 安装指令如下:
pip install celery
pip install redis
pip install django-celery-results
pip install django-celery-beat
pip install eventlet
# 使用清华源安装:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple celery
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple redis
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple django-celery-results
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple django-celery-beat
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple eventlet
每个功能模块负责实现不同的功能, 在此简单讲解各个功能模块的具体作用, 分别说明如下:
(1) celery: 安装Celery框架, 实现异步任务和定时任务的调度控制.
(2) redis: 使Python与Redis数据库实现连接.
(3) django-celery-results: 基于Celery封装的异步任务功能.
(4) django-celery-beat: 基于Celery封装的定时任务功能.
(5) eventlet: Python的协程并发库, 这是Celery实现的异步并发运行模式之一.

详细说明:
* 1. celery
     作用: Celery是一个简单, 灵活且可靠的分布式任务队列,
     它可以让你的应用进行异步和周期性(定时)任务(jobs)的处理. 它专注于实时处理, 同时也支持任务调度.
     应用场景: 在Web应用中, 可以使用Celery来执行后台任务, 如发送电子邮件, 图片处理, 数据分析等, 从而避免阻塞Web请求.
* 2. redis
     作用: Redis是一个开源的, 内存中的数据结构存储系统, 它可以用作数据库, 缓存和消息代理.
     在这里, 它被用作Celery的消息代理(broker), 用于在Celery的工作节点和任务队列之间传递任务.
* 3. django-celery-results
     作用: 这是一个Django应用, 用于将Celery的结果后端(result backend)存储在Django的数据库中.
     结果后端用于存储任务的状态和结果, 使得你可以查询和检索它们.
     应用场景: 在Django项目中, 可能希望将Celery的任务结果存储在Django的数据库中, 以便进行进一步的查询和处理.
* 4. django-celery-beat
     作用: 这是一个Django应用, 用于添加周期性任务(定时任务)到Celery.
     它提供了一个数据库模型来定义周期性任务, 并使用Django的admin界面进行管理.
     应用场景: 在Django项目中, 可能希望定期执行某些任务, 如每天发送报告, 每周清理旧数据等.
     使用django-celery-beat, 可以轻松定义和管理这些周期性任务.
* 5. eventlet
     作用: Eventlet是一个网络库, 用于构建并发网络应用. 它基于Greenlet库, 支持异步I/O, 并提供了一个高性能的事件循环.
     应用场景: 虽然Eventlet本身与Celery或Django没有直接的联系, 但它可以用于提高网络应用的性能,
     特别是当应用需要处理大量并发连接时.
完成功能模块的安装后, 在MyDjango里创建项目应用index, 并且在index里创建文件tasks.py, 该文件用于定义异步任务;
在MyDjango文件夹创建celery.py, 该文件用于将Celery框架引入Django框架.
然后在配置文件settings.py中配置异步任务和定时任务的功能模块和功能设置, 配置代码如下:
# MyDjango的settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'index.apps.IndexConfig',
    # 添加异步任务功能
    'django_celery_results',
    # 添加定时任务功能
    'django_celery_beat'
]

# 设置存储Celery任务队列的Redis数据库
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0'  # 代理地址
CELERY_ACCEPT_CONTENT = ['json']  # 接收内容格式, 必须小写json
CELERY_TASK_SERIALIZER = 'json'  #
# 设置存储Celery任务结果的Redis数据库
CELERY_RESULT_BACKEND = 'django-db'
# 设置定时任务相关配置
CELERY_ENABLE_UTC = False
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
CELERY_ENABLE_UTC = False 是个坑, 后面有解释!

image-20240608071703622

配置属性解释如下:
* 1. CELERY_BROKER_URL: 这行指定了Celery使用的消息代理(Broker)的URL.
     'redis://127.0.0.1:6379/0': 表示Celery将使用运行在本地(127.0.0.1)的Redis服务器, 端口为6379,
     并使用默认的Redis数据库0作为消息代理.
* 2. CELERY_ACCEPT_CONTENT 定义了 Celery 接受的序列化内容类型. 在这个例子中, 它只接受'JSON'格式的内容.
* 3. CELERY_TASK_SERIALIZER 定义了用于序列化任务的序列化器. 这里它被设置为'JSON', 意味着任务将以JSON格式进行序列化.
* 4. CELERY_RESULT_BACKEND: 结果后端存储数据使用的数据库.
* 5. CELERY_ENABLE_UTC: 这个设置决定是否使用UTC时间来调度定时任务. 当设置为False时, Celery将使用服务器的本地时间.
     然而, 为了避免时区相关的问题, 通常建议将其设置为True.
* 6. CELERY_BEAT_SCHEDULER: 这行指定了Celery Beat使用的调度器.
     django_celery_beat.schedulers:DatabaseScheduler表示将使用django-celery-beat这个Django应用来管理定时任务,
     并使用其提供的数据库调度器.
     django-celery-beat允许通过Django管理界面来创建, 编辑和删除定时任务, 并将这些任务存储在Django数据库中.
最后在index的models.py中定义模型PersonInfo, 利用异步任务实现模型PersonInfo的数据修改功能.
模型PersonInfo的定义过程如下:
# index的models.py
from django.db import models


# 人员信息
class PersonInfo(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20)
    age = models.IntegerField()
    hire_date = models.DateField()

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = '人员信息'
        

image-20240606231154984

将整个项目执行数据迁移, 因为异步任务和定时任务在运行过程中需要依赖数据表才能完成任务执行.
# 数据迁移
PS D:\MyDjango> python manage.py makemigrations
Migrations for 'index':
  index\migrations\0001_initial.py
    - Create model PersonInfo
PS D:\MyDjango> python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, django_celery_beat, django_celery_results, index, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying django_celery_beat.0005_add_solarschedule_events_choices... OK
  Applying django_celery_beat.0006_auto_20180322_0932... OK
  Applying django_celery_beat.0007_auto_20180521_0826... OK
  Applying django_celery_beat.0008_auto_20180914_1922... OK
  Applying django_celery_beat.0006_auto_20180210_1226... OK
  Applying django_celery_beat.0006_periodictask_priority... OK
  Applying django_celery_beat.0009_periodictask_headers... OK
  Applying django_celery_beat.0010_auto_20190429_0326... OK
  Applying django_celery_beat.0011_auto_20190508_0153... OK
  Applying django_celery_beat.0012_periodictask_expire_seconds... OK
  Applying django_celery_beat.0013_auto_20200609_0727... OK
  Applying django_celery_beat.0014_remove_clockedschedule_enabled... OK
  Applying django_celery_beat.0015_edit_solarschedule_events_choices... OK
  Applying django_celery_beat.0016_alter_crontabschedule_timezone... OK
  Applying django_celery_beat.0017_alter_crontabschedule_month_of_year... OK
  Applying django_celery_beat.0018_improve_crontab_helptext... OK
  Applying django_celery_results.0001_initial... OK
  Applying django_celery_results.0002_add_task_name_args_kwargs... OK
  Applying django_celery_results.0003_auto_20181106_1101... OK
  Applying django_celery_results.0004_auto_20190516_0412... OK
  Applying django_celery_results.0005_taskresult_worker... OK
  Applying django_celery_results.0006_taskresult_date_created... OK
  Applying django_celery_results.0007_remove_taskresult_hidden... OK
  Applying django_celery_results.0008_chordcounter... OK
  Applying django_celery_results.0009_groupresult... OK
  Applying django_celery_results.0010_remove_duplicate_indices... OK
  Applying django_celery_results.0011_taskresult_periodic_task_name... OK
  Applying index.0001_initial... OK
  Applying sessions.0001_initial... OK

image-20240606230440661

数据迁移后, 打开项目的db.sqlite3数据库文件, 可以看到项目一共生成了20个数据表.

image-20240606230814162

然后在数据表index_personinfo中添加人员信息, 如图12-25所示.

image-20240606231731799

12-25 数据表index_personinfo

12.5.2 异步任务

12.5.1小节, 我们已经完成了MyDjango的开发环境搭建, 接下来在MyDjango中实现异步任务开发.
首先在MyDjango的celery.py文件中实现Celery框架和Django框架组合使用, Celery框架根据Django的运行环境进行实例化并生成app对象.
celery.py的代码如下:
# MyDjango的celery.py
import os
from celery import Celery

# 获取settings.py的配置信息
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'MyDjango.settings')
# 定义Celery对象, 并将项目配置信息加载到对象中
# Celery的参数一般以项目名称命名
app = Celery('MyDjango', BROKER_CONNECTION_RETRY_ON_STARTUP=True)
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

image-20240608045810735

(代码的含义:
* 1. import os: 这行代码导入了Python的os模块, 它提供了很多与操作系统交互的功能.
* 2. from celery import CeleryL 这行代码从Celery库中导入了Celery类. Celery类用于创建和管理Celery应用实例.
* 3. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'MyDjango.settings')
     这行代码设置了一个环境变量DJANGO_SETTINGS_MODULE, 其值为'MyDjango.settings'.
     这个环境变量告诉Django哪个模块包含项目的设置(通常是一个名为settings.py的文件).
     虽然Django通常会自动设置这个环境变量, 但在某些情况下(比如在独立的Celery进程中), 可能需要手动设置.
* 4. app = Celery('MyDjango'): 这行代码创建了一个名为app的Celery应用实例, 并使用项目名称'MyDjango'作为其名称.
     Celery应用的名称通常用于在日志和消息中标识应用.
     BROKER_CONNECTION_RETRY_ON_STARTUP=True: 如果不进行此设置, 消息代理在Celery Worker启动时不可用,
     那么Worker可能无法成功连接到消息代理, 从而导致任务无法被正确处理.
     进行了上述设置后, 即使在启动时消息代理暂时不可用, Celery也会尝试重新连接, 增加了系统的健壮性.
* 5. app.config_from_object('django.conf:settings', namespace='CELERY')
     这行代码告诉Celery从Django的设置文件中加载配置.
     它指定了从'django.conf:settings'(这是Django设置模块的路径)加载配置, 
     并使用namespace='CELERY'来指定只加载以CELERY_开头的设置项.
     这意味着你可以在Django的 settings.py文件中使用如CELERY_BROKER_URL这样的设置来配置Celery.
* 6. app.autodiscover_tasks(): 这行代码告诉Celery自动发现任务.
     在Django项目中, Celery 任务通常定义在应用(app)的tasks.py文件中.
     autodiscover_tasks()会遍历所有已安装的Django应用, 并尝试导入这些应用中的tasks.py模块, 从而发现这些应用中定义的任务. )
上述代码只是在MyDjango中创建Celery框架的实例化对象app, 我们还需要将实例化对象app与MyDjango进行绑定,
使Django运行的时候能自动加载Celery框架的实例化对象app.
因此, 在MyDjango的初始化文件__init__.py中编写加载过程, 代码如下:
# MyDjango的__init__.py
from .celery import app as celery_app

__all__ = ['celery_app']

image-20240608034749777

现在已将Celery框架加载到Django框架, 下一步讲述如何在Django中使用Celery框架实现异步任务开发,
将MyDjango定义的函数与Celery框架的实例化对象app进行绑定, 使该函数转变为异步任务.
我们在index的task.py中定义函数update_data, 函数代码如下:
# index的task.py

from celery import shared_task  # 共享任务
from .models import PersonInfo


# 带参数的异步任务
@shared_task
def update_data(pk, info):
    try:
        PersonInfo.objects.filter(id=pk).update(**info)
        return 'Done!'
    except Exception as e:
        return f'Fail! Error: {e}'

image-20240608035813052

函数update_data由装饰器shared_task转化成异步任务updateData, 它设有参数pk和info,
分别代表模型PersonInfo的主键id和模型字段的修改内容.

异步任务没有要求函数设置返回值, 如果函数设有返回值, 它就作为异步任务的执行结果, 否则将执行结果设为None,
所有的执行结果都会记录在数据表django_celery_results_taskresult的result字段中.

最后在MyDjango中编写异步任务update_data的触发条件, 
换句话说, 当用户访问特定的网址时, 视图函数或视图类将调用异步任务update_data, 
由Celery框架执行异步任务, 视图函数或视图类只需返回响应内容即可, 无须等待异步任务的执行过程.
我们在MyDjango的urls.py和index的urls.py中定义路由index和Admin后台系统的路由, 路由信息如下:
# MyDjango的urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include(('index.urls', 'index'), namespace='index'))
]

image-20240608040553723

# index的urls.py
from django.urls import path
from .views import *

urlpatterns = [
    path('', index, name='index'),
]

image-20240608040722509

上述代码中, Admin后台系统的路由是为了设置定时任务, 定时任务将在下一节详细讲述.
路由index为异步任务update_data的触发条件提供路由地址, 
路由的请求处理由视图函数index执行, 因此在index的views.py中定义视图函数index, 代码如下:
# index的views.py
from django.http import HttpResponse
from .tasks import update_data


def index(request):
    # 传递参数并执行异步任务
    pk = request.GET.get('id', 1)
    info = dict(name='刘一', age=19, hire_date='2019-10-10')
    update_data.delay(pk, info)  # 异步执行数据更新
    return HttpResponse("Hello Celery")

image-20240608045852938

视图函数index设置变量pk和infom 异步任务update_data调用delay方法创建任务队列,
由Celery框架完成执行过程, 如果异步任务设有参数, 那么可以在delay方法里添加异步任务的函数参数.

至此, 我们已完成Django的异步任务开发. 最后讲述如何启动异步任务的运行环境.
因为异步任务由Celery框架执行, 除了启动MyDjango之外, 还需要启动Celery框架.
首先运行MyDjango, 然后在PyCharm的Terminal中输入以下指令启动Celery:
# 指令中的MyDjango是项目名称
# 启动Celery
celery -A MyDjango worker -l info -P eventlet
上述指令是通过eventlet模块运行Celery框架的, Celery框架有多种运行方式, 读者可以自行在网上查阅相关资料.
当Celery框架成功启动后, 它会自动加载MyDjango定义的异步任务update_data,
并在PyCharm的Terminal中显示Redis数据库连接信息, 如图12-26所示.
PS D:\MyDjango> celery -A MyDjango worker -l info -P eventlet
 
 -------------- celery@blue v5.4.0 (opalescent)
--- ***** ----- 
-- ******* ---- Windows-10-10.0.22621-SP0 2024-06-08 04:52:36
- *** --- * --- 
- ** ---------- [config]
- ** ---------- .> app:         MyDjango:0x27e5cf17a30
- ** ---------- .> transport:   redis://127.0.0.1:6379/0
- ** ---------- .> results:     
- *** --- * --- .> concurrency: 20 (eventlet)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** ----- 
 -------------- [queues]
                .> celery           exchange=celery(direct) key=celery


[tasks] PowerShell
  . index.tasks.update_dataporation。保留所有权利。

[2024-06-08 04:52:36,157: INFO/MainProcess] Connected to redis://127.0.0.1:6379/0
[2024-06-08 04:52:36,200: INFO/MainProcess] mingle: searching for neighbors
[2024-06-08 04:52:37,269: INFO/MainProcess] mingle: all alone
[2024-06-08 04:52:37,277: INFO/MainProcess] pidbox: Connected to redis://127.0.0.1:6379/0.
[2024-06-08 04:52:37,280: INFO/MainProcess] celery@blue ready.

2024-06-08_045239

12-26 Celery启动信息
在浏览器上输入: 127.0.0.1:8000, 视图函数index将触发异步任务update_data,
如果当前请求没有请求参数id, 就默认修改模型PersonInfo的主键id等于1的数据.
当异步任务执行成功后, 执行结果将会显示在PyCharm的Terminal界面, 
并且保存到数据表django_celery_results_taskresult中, 如图12-27所示.

image-20240608050312853

image-20240608050256565

image-20240608050341058

image-20240608050456086

12-27 数据表django_celery_results_taskresult

12.5.3 定时任务

定时任务是一种较为特殊的异步任务, 它在特定的时间内触发并执行某个异步任务, 换句话说, 所有异步任务都可以设为定时任务.
我们为了区分12.5.2小节的异步任务update_Data, 在index的tasks.py中定义异步任务timing, 具体代码如下:
# index的task.py
import time


@shared_task
def timing():
    now = time.strftime('%H:%M:%S')  # 时间格式
    with open('d://output.txt', 'a', encoding='utf8') as f:
        f.write('This time is ' + now + '\n')

(a为追加模式, 文件不存在会自动创建.)

image-20240608051211724

下一步将异步任务timing设为定时任务, 首先在PyCharm的Terminal中使用Django指令创建超级管理员账号(账号和密码皆为admin),
然后运行MyDjango, 在浏览器上访问Admin后台系统并使用超级管理员登录.
# 创建超级用户
PS D:\MyDjango> python manage.py createsuperuser
Username (leave blank to use 'blue'): admin
Email address: (回车)
Password: (admin)
Password (again): (admin)
The password is too similar to the username.
This password is too short. It must contain at least 8 characters.
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
PS D:\MyDjango> 

2024-06-08_051439

最后在Admin后台系统的主页面找到Periodic Tasks, 单击该链接进入Periodic Tasks页面,
在Periodic Tasks页面单击Add periodic task链接, 如图12-28所示.

image-20240608051640207

image-20240608051715188

在Add periodic task页面, 我们将name设置为: Mytiming, Task设置为异步任务: timing, Interval设置为5,
这是创建定时任务Mytiming, 每间隔5秒就会执行异步任务timing. 
设置完成后, 下拉页面点击save保存.

2024-06-08_052851

image-20240608052356333

12-28 创建定时任务
定时任务创建成功后, 可以在数据表django_celery_beat_periodictask中查看任务信息, 如图12-29所示.

image-20240608053143692

12-29 数据表django_celery_beat_periodictask
如果异步任务设有参数, 那么可以在Add periodic task页面设置Arguments或Keyword arguments, 
参数以JSON格式表示, 它们分别对应数据表字段args和kwargs.

image-20240608053416524

定时任务设置后, 我们通过输入指令来启动定时任务.
在输入指令之前, 必须保证MyDjango和Celery处于运行状态.
换句话说, 如果使用PyCharm启动定时任务, 就需要运行MyDjango, 启动Celery和Celery的定时任务.
在PyCharm中创建两个Terminal界面, 每个Terminal界面依次输入以下指令:
# 指令中的MyDjango是项目名称
# 启动Celery
celery -A MyDjango worker -l info -P eventlet
# 启动定时任务
celery -A MyDjango beat -l info -S django
定时任务启动后, 数据表django_celery_beat_periodictask所有的定时任务都会进入等待状态,
只有Django的系统时间达到定时任务的触发时间, Celery才会运行符合触发条件的定时任务.
比如定时任务Mytiming, 它是每间隔5秒就会执行异步任务timing, 
我们在Terminal或数据表django_celery_results_taskresult中都可以查看定时任务的执行情况, 如图12-30所示.

image-20240608065353940

12-30 定时任务的执行情况
django celery.backend_cleanup是一个用于清理Celery任务结果的函数.
它的主要作用是删除已经过期的任务结果, 以避免数据库中的任务结果数据过多导致性能下降.

启动定时任务后会, 自动创建celery.backend_cleanup任务, 这个任务是UTC时间凌晨4点执行一次.
由于配置文件中我们设置关闭UTC时间: CELERY_ENABLE_UTC = False , 这会出现一个BUG,

2024-06-08_065649

启动定时后, celery.backend_cleanup任务每秒执行上百次...修改了间隔时间后, 启动beat后自动还原... 

image-20240608070953570

解决方式1: CELERY_ENABLE_UTC = True
解决方式1: 禁用任务

2024-06-08_070015

12.6 即时通信–在线聊天

Web在线聊天室的实现方法有多种, 每一种实现方法的基本原理各不相同, 详细说明如下:
(1) 使用AJAX技术: 通过AJAX实现网页与服务器的无刷新交互,
    在网页上每隔一段时间就通过AJAX从服务器中获取数据, 然后将数据更新并显示在网页上, 这种方法简单明了, 缺点是实时性不高.
(2) 使用Comet(Pushlet)技术: Comet是一种Web应用架构, 服务器以异步方式向浏览器推送数据, 无须浏览器发送请求.
    Comet架构非常适合事件驱动的Web应用, 以及对交互性和实时性要求较高的应用, 如股票交易行情分析, 聊天室和Web版在线游戏等.
(3) 使用XMPP协议: XMPP(可扩展消息处理现场协议)是基于XML的协议, 
    这是专为即时通信系统设计的通信协议, 用于即时消息以及在线现场探测, 这个协议允许用户向其他用户发送即时消息.
(4) 使用Flash的XmlSocket: Flash Media Server是一个强大的流媒体服务器, 它基于RTMP协议, 提供了稳定的流媒体交互功能,
    内置远程共享对象(Shared Object)的机制, 是浏览器创建并连接服务器的远程共享对象.
(5) 使用WebSocket协议: WebSocket是通过单个TCP连接提供全双工(双向通信)通信信道的计算机通信协议,
    可在浏览器和服务器之间进行双向通信, 允许多个用户连接到同一个实时服务器, 并通过API进行通信并立即获得响应.
    WebSocket不仅限于聊天/消息传递应用程序, 还适用于实时更新和即时信息交换的应用程序,
    比如现场体育更新, 股票行情, 多人游戏, 聊天应用, 社交媒体等.

12.6.1 Channels的安装与配置

如果想在Django里开发Web在线聊天功能, 建议使用WebSocket协议实现.
虽然Django 3版本新增了ASGI服务, 但它对WebSocket的功能支持尚未完善, 所以还需要借助第三方模块Channels, 
通过Channels使用WebSocket协议实现浏览器和服务器的双向通信, 它的实现原理是在Django里定义Channels的API接口,
由网页的JavaScript与API接口构建通信连接, 使浏览器和服务器之间相互传递数据.
Channels的功能必须依赖于Redis数据库, 除了安装channels模块之外, 还需要安装channels_redis模块.
我们使用pip指令分别安装channels和channels_redis模块, 安装指令如下:
pip install channels  # 提供了一个框架, 可以编写异步的Django视图来处理WebSocket连接和其他非HTTP协议.
pip install channels_redis  #  是Channels框架的一个通道层实现, 它使用Redis作为消息代理.
# Channels的功能依赖模块
pip install pypiwin32  #  提供了对Windows API的Python访问, 允许调用许多Windows的底层函数和特性.
# pywin32 的注意功能:
# 1. 访问Windows API: pywin32 允许Python脚本和应用程序调用Windows API中的函数,
#    从而能够执行各种系统级任务, 如访问注册表, 管理文件和目录, 控制窗口和进程等.
# 2. COM支持: pywin32提供了对COM(组件对象模型)的支持, 这使得Python可以与许多Windows应用程序和组件进行交互.
# 3. Windows服务: 可以使用pywin32创建和管理Windows服务.
# 4. Windows消息: 可以使用pywin32发送和接收Windows消息, 这在某些UI自动化或低级系统编程场景中可能很有用.
# 使用清华源安装:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple channels
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple channels_redis
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pypiwin32
完成第三方功能应用Channels的安装后, 下一步在Django中配置Channels.
以MyDjango为例, 首先创建项目应用chat, 并在项目应用chat里新建urls.py文件;
然后在MyDjango文件夹里创建文件consumers.py和routing.py, 文件名称没有硬性要求, 读者可以自行命名,
在templates文件夹中创建模板文件chat.html和room.html;

image-20240609162628374

最后执行整个项目的数据迁移, 因为Channels需要使用Django内置的会话Session机制, 用于区分和识别每个用户的身份信息.
# 数据迁移
PS D:\MyDjango> python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

image-20240609162954070

打开MyDjango的settings.py, 将第三方功能应用Channels添加到MyDjango.
首先在配置属性INSTALLED_APPS中分别添加channels和chat, 前者是第三方功能应用Channels, 后者是项目应用chat.
第三方功能应用Channels以项目应用的形式添加到MyDjango, 由于Channels的功能依赖于Redis数据库,
因此还需要在settings.py中设置Channels的功能配置, 配置信息如下:
# MyDjango的settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'chat.apps.ChatConfig',
    # 添加Channels功能
    'channels',
]

# ASGI配置
ASGI_APPLICATION = 'MyDjngo.routing.application'

# Channels配置
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],  # Redis配置
        },
    },
}

image-20240609164933434

功能配置CHANNEL_LAYERS用于设置Redis数据库的连接方式, 
ASGI_APPLICATION指向MyDjango的routing.py定义的application对象, 该对象把Django与Channels建立连接, 具体的定义过程如下:
# MyDjango的routing.py
from channels.auth import AuthMiddlewareStack  # 身份验证中间件堆栈
from channels.routing import ProtocolTypeRouter  # 协议类型路由器
from channels.routing import URLRouter  # URL 路由器
from .urls import websocket_urlpatterns  # websocket URL 模式

# 创建一个协议类型路由器实例, 并为其配置路由规则
application = ProtocolTypeRouter({
    # WebSocket协议使用身份验证中间件堆栈和URL路由器来处理
    # 默认情况下, 对于HTTP请求, Django 会自动将其路由到相应的视图函数或类
    'websocket': AuthMiddlewareStack(
        URLRouter(
            # 使用websocket_urlpatterns中的URL模式来路由WebSocket请求
            websocket_urlpatterns
        )
    ),
})

image-20240609170119860

上述代码的application对象由ProtocolTypeRouter实例化生成, 
在实例化过程中, 需要传入参数application_mapping, 
参数以字典格式表示, 字典的key为websocket, 代表使用WebSocket协议;
字典的value是Channels的路由对象websocket_urlpatterns, 它定义在MyDjango的urls.py中.
因此, 在MyDjango的urls.py中定义路由对象urlpatterns和websocket_urlpatterns, 代码如下:
# MyDjango的urls.py
from django.urls import path, include
from .consumers import ChatConsumer

urlpatterns = [
    path('', include(('chat.urls', 'chat'), namespace='chat'))
]
websocket_urlpatterns = [
    # 使用同步方式实现
    # path('ws/chat/<room_name>/', ChatConsumer),
    # 如果使用异步方式实现, 路由的视图必须调用as_asgi()
    path('ws/chat/<room_name>/', ChatConsumer.as_asgi()),
]

路由对象urlpatterns用于定义项目应用chat的路由信息, websocket_urlpatterns用于定义Channels的路由信息.
上述代码只定义路由ws/chat/<room_name>/, 它是由视图类ChatConsumer处理和响应HTTP请求的, 
该路由作为Channels的API接口, 由网页的JavaScript与该路由构建通信连接, 使浏览器和服务器之间相互传递数据.
在MyDjango的consumers.py中定义视图类ChatConsumer, 代码如下:
# MyDjango的consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer
import json


class ChatConsumer(AsyncWebsocketConsumer):

    # 连接
    async def connect(self):
        # 当 WebSocket 连接建立时, 将用户的组添加到缓存中
        self.room_name = self.scope['url_route']['kwargs']['room_name']  # 房间名称
        self.room_group_name = f'chat_{self.room_name}'  # 会议室组名称

        # 将连接添加到组
        await self.channel_layer.group_add(
            self.room_group_name,  # 会议室组名称
            self.channel_name  # 频道名称
        )

        await self.accept()  # 接受传入套接字

    # 断开
    async def disconnect(self, close_code):
        # 当 WebSocket 连接关闭时, 从缓存中移除用户的组
        await self.channel_layer.group_discard(
            self.room_group_name,  # 会议室组名称
            self.channel_name  # 频道名称
        )

    # 收到, 接收从 WebSocket 发送的消息
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)  # 反序列化文本数据
        message = text_data_json['message']  # 取出聊天信息

        # 发送消息到组
        await self.channel_layer.group_send(
            self.room_group_name,  # 会议室组名称
            {  # 字典格式
                'type': 'chat_message',  # 类型: 聊天消息
                'message': message  # 信息: 聊天信息
            }
        )

    # 从组接收消息并发送给 WebSocket
    async def chat_message(self, event):  # event用于传递与事件相关的数据.
        message = event['message']  # 从字典中获取信息

        # 发送消息到 WebSocket
        await self.send(text_data=json.dumps({  # 序列化聊天信息
            'message': message
        }))

image-20240609175753211

视图类ChatConsumer继承父类AsyncWebsocketConsumer, 父类定义WebSocket协议的异步通信方式,
从视图类ChatConsumer的类方法看到, 每个类方法都使用async语法标识, async语法从Python 3.5开始引入,
该语法能将函数方法以异步方式运行, 从而提高代码的运行速度和性能.

除此之外, Channels还定义WebsocketConsumer类, 它与AsyncWebsocketConsumer实现的功能一致, 
但是以同步方式运行, 对比AsyncWebsocketConsumer而言, 它的运行速度和性能较低.
WebsocketConsumer和AsyncWebsocketConsumer是通过字符串格式进行数据通信的, 
如果使用JSON格式进行数据通信, 那么可以采用Channels定义的JsonWebsocketConsumer或AsyncJsonWebsocketConsumer.
综上所述, 在Django里配置第三方功能应用Channels的步骤如下:
(1) 在settings.py的INSTALLED_APPS中添加channels, 第三方功能应用Channels以项目应用的形式添加到Django中;
    同时设置Channels的功能配置属性ASGI_APPLICATION和CHANNEL_LAYERS.
(2) 功能配置ASGI_APPLICATION设置了Django与Channels的连接方式, 连接过程由application对象实现.
    application对象是由ProtocolTypeRouter实例化生成的, 在实例化过程中, 需要传入Channels的路由对象websocket_urlpatterns.
(3) 路由对象websocket_urlpatterns定义在MyDjango的urls.py中, 这是定义Channels的API接口,
    由网页的JavaScript与API接口构建通信连接, 使浏览器和服务器之间相互传递数据.
(4) 路由ws/chat/<room_name>/由视图类ChatConsumer处理和响应HTTP请求,
    视图类ChatConsumer继承父类AsyncWebsocketConsumer, 这是使用WebSocket协议的异步通信方式实现浏览器和服务器之间的数据传递.

12.6.2 Web在线聊天功能

12.6.1小节实现了Django和Channels的异步通信连接, 本节将讲述如何在Django中使用Channels实现Web在线聊天功能.
我们在MyDjango的urls.py中定义了项目应用chat的路由空间, 它指向项目应用chat的urls.py,
在项目应用chat的urls.py中分别定义路由new_chat和room, 代码如下:
# chat的urls.py
from django.urls import path
from .views import *

urlpatterns = [
    # 用于开启新的聊天室
    path('', new_chat, name='new_chat'),
    # 创建聊天室
    path('<room_name>/', room, name='room'),
]

image-20240609180208894

路由new_chat供用户创建或进入某个聊天室, 由视图函数newChat处理和响应HTTP请求.
路由room是聊天室的聊天页面, 路由地址设有路由变量room_name, 它代表聊天室名称, 由视图函数room处理和响应HTTP请求.
在chat的views.py中定义视图函数new_chat和room, 视图函数的定义过程如下:
# chat的views.py
from django.shortcuts import render


# 用于创建或进入聊天室
def new_chat(request):
    return render(request, 'chat.html', locals())


# 创建聊天室
def room(request, room_name):
    return render(request, 'room.html', locals())

image-20240609180231874

视图函数new_chat和room分别使用模板文件chat.html和room.html生成网页内容.
由于路由new_chat供用户创建或进入某个聊天室, 因此模板文件chat.html需要生成路由room的路由地址, 实现代码如下:
<!-- tempalte的room.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Chat Rooms</title>
</head>
<body>
<div>请输入聊天室名称</div>
<br>
<input type="text" id="input" size="30">
<br>
<input type="button" id="submit" value="进入">

<script>
    // querySelector方法选择页面上ID为input的元素, 然后调用其focus方法, 使该输入框在页面加载后立即获得焦点.
    document.querySelector('#input').focus();
    // keyup事件监听器, 当用户在输入框中键入内容并释放键时, 这个事件会被触发.
    // 如果用户按下了回车键, 代码会模拟点击ID为 submit 的元素.
    document.querySelector('#input').onkeyup = function (e) {
        // e.keyCode === 13 检查按下的键的键码是否为13, 即回车键(Enter)
        if (e.keyCode === 13) {
            document.querySelector('#submit').click();
        }
    }
    // submit元素添加了一个click事件监听器
    document.querySelector('#submit').onclick = function (e) {
        // 获取房间名称
        var roomName = document.querySelector('#input').value;
        // 更改了当前页面的URL路径
        window.location.pathname = '/' + roomName + '/';
    };
</script>
</body>
</html>
模板文件chat.html设有文本输入框和'进入'按钮, 当用户在文本输入框输入聊天室名称后, 
单击'进入'按钮或按回车键将触发网页的JavaScript脚本,
JavaScript将用户输入的聊天室名称作为路由room的路由变量room_name, 控制浏览器访问路由room.
我们在路由new_chat的页面使用JavaScript脚本访问路由room,
此外还可以使用网页表单方式实现, 由视图函数new_chat重定向访问路由room.
路由room使用模板文件room.html生成聊天室的聊天页面, 因此模板文件room.html的代码如下:
<!-- tempalte的chat.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>聊天室{{ room_name }}</title>
</head>
<body>
<textarea name="" id="chat-log" cols="50" rows="6"></textarea>
<br>
<input type="text" id="input" size="50">
<br>
<input type="button" id="submit" value="发 送">

<script>
    var  roomName = '{{ room_name }}'
    // 标注1
    var chatSocket = new WebSocket(
        'ws://' + window.location.host + '/ws/chat/' + roomName + '/'
    ) ;

    // 标注2
    // 将MyDjango发送的数据显示在多行文本输入框中
    chatSocket.onmessage = function (e) {
        var data = JSON.parse(e.data);
        var message = data['message']
        document.querySelector('#chat-log').value += (message + '\n')
    };

    // 标注3
    // 关闭网页与MyDjango的Channels连接
    chatSocket.onclose = function (e) {
        console.error('Chat socket closed unexpectedly');  // 聊天套接字意外关闭
    };

    // 标注4
    document.querySelector('#input').focus();
    document.querySelector('#input').onkeyup = function (e) {
        // e.keyCode === 13 检查按下的键的键码是否为13, 即回车键(Enter)
        if (e.keyCode === 13) {
            document.querySelector('#submit').click();
        }
    };

    // 标注5
    // 网页向MyDjango的Channels发送数据
    document.querySelector('#submit').onclick = function (e) {
        var messageInputDom = document.querySelector('#input');
        var message = messageInputDom.value;
        chatSocket.send(JSON.stringify({
            'message': message
        }));
        messageInputDom.value = '';

    }
</script>
</body>
</html>
WebSocket连接(标注1): 创建一个新的WebSocket连接, 连接到后端服务器上的特定端点.
这个端点由当前页面的主机名, WebSocket路径和从后端传递的room_name变量组成.

接收消息(标注2): 当WebSocket接收到来自后端的消息时, 会触发onmessage事件.
在这个事件处理程序中, 接收到的数据被解析为JSON格式, 然后提取其中的message字段.
将接收到的消息追加到chat-log文本框的末尾, 并添加一个换行符以分隔不同的消息.

连接关闭处理(标注3): 如果WebSocket连接意外关闭(例如, 由于网络问题或后端服务器关闭), 会触发onclose事件.
在这个事件处理程序中, 将一条错误消息打印到控制台.

输入框处理(标注4): 当用户将焦点放在输入框(#input_上并按下回车键(键码为13), 触发一个键盘事件.
在这个事件处理程序中, 模拟点击'发送'按钮(#submit).

发送消息)标注5): 当用户点击'发送'按钮时, 触发一个点击事件.
在这个事件处理程序中, 从输入框获取用户输入的消息, 并将其作为JSON对象发送到后端.
发送的消息包含一个message字段, 其值为用户输入的消息.
发送消息后, 清空输入框以便用户可以输入新的消息.
聊天室的聊天页面设有多行文本输入框(<textarea>标签), 单行文本输入框(<input type="text">标签)'发送'按钮.
多行文本输入框用于显示用户之间的聊天记录; 单行文本输入框供当前用户输入聊天内容;
'发送'按钮可以将聊天内容发送给所有在线的用户, 并且显示在多行文本输入框中.

聊天功能的实现过程由JavaScript脚本执行, 上述的JavaScript代码设有5个标注, 每个标注实现的功能说明如下:
(1) 标注①创建WebSocket对象, 对象命名为chatSocket,
    它连接了MyDjango定义的路由ws/chat/<room_name>/, 使网页与MyDjango的Channels实现通信连接.
(2) 标注②设置chatSocket对象的onmessage事件触发, 该事件接收MyDjango发送的数据, 并将数据显示在多行文本输入框中.
(3) 标注③设置chatSocket对象的onclose事件触发, 如果JavaScript与MyDjango的通信连接发生异常, 就会触发此事件,
    该事件在浏览器的开发者工具中输出提示信息.
(4) 标注④设置'发送'按钮的onkeyup事件触发, 如果用户按回车键即可触发此事件, 那么该事件将会触发'发送'按钮的onclick事件.
(5) 标注⑤设置'发送'按钮的onclick事件触发, 该事件将单行文本输入框的内容发送给MyDjango的Channels.
    Channels接收某用户的数据后, 将数据发送给同一个聊天室的所有用户, 
    此时就会触发chatSocket对象的onmessage事件, 将数据显示在每个用户的多行文本输入框中,
    这样同一个聊天室的所有用户都能看到某个用户发送的信息.
最后运行MyDjango, 打开谷歌浏览器并访问127.0.0.1:8000, 在文本输入框中输入聊天室名称并单击'进入'按钮, 如图12-31所示.

image-20240609200754649

12-31 创建或进入聊天室
(启动的时候会遇到问题两个问题:
问题1: https://blog.csdn.net/qq_46137324/article/details/139568935?spm=1001.2014.3001.5501
问题2: https://blog.csdn.net/qq_46137324/article/details/139568922?spm=1001.2014.3001.5501 )
当成功创建聊天室MyDjango后, 打开IE浏览器访问127.0.0.1:8000/MyDjango.
聊天室MyDjango已有两位在线用户, 谷歌浏览器代表用户A,EDGE浏览器代表用户B.
我们使用用户A发送信息, 在用户B的页面可以即时看到信息内容, 如图12-32所示.

image-20240610015102721

12-32 Web在线聊天

12.7 本章小结

因为Django具有很强的可扩展性, 从而延伸了第三方功能应用.
通过本章的学习, 读者能够在网站开发过程中快速实现API接口开发, 验证码生成与使用, 站内搜索引擎, 
第三方网站实现用户注册, 异步任务和定时任务, 即时通信等功能.

开发网站的API可以在视图函数中使用响应类JsonResponse实现, 它将字典格式的数据作为响应内容.
使用响应类JsonResponse开发API需要根据用户请求信息构建字典格式的数据内容,
数据构建的过程可能涉及模型的数据查询, 数据分页处理等业务逻辑, 这种方式开发API很容易造成代码冗余, 不利于功能的变更和维护.
为了简化API的开发过程, 我们可以使用Django Rest Framework框架实现API开发.
使用框架开发不仅能减少代码冗余, 还可以规范代码的编写格式, 这对企业级开发来说很有必要,
毕竟每个开发人员的编程风格存在一定的差异, 开发规范化可以方便其他开发人员查看和修改.
如果想在Django中实现验证码功能, 那么可以使用PIL模块生成图片验证码, 但不建议使用这种实现方式.
除此之外, 还可以通过第三方应用Django Simple Captcha实现,
验证码的生成过程由该应用自动执行, 开发者只需考虑如何应用到Django项目中即可.
站内搜索是网站常用的功能之一, 其作用是方便用户快速查找站内数据以便查阅.
对于一些初学者来说, 站内搜索可以使用SQL模糊查询实现,
从某个角度来说, 这种实现方式只适用于个人小型网站, 对于企业级的开发, 站内搜索是由搜索引擎实现的.
Django Haystack是一个专门提供搜索功能的Django第三方应用, 它支持Solr, Elasticsearch, Whoosh和Xapian等多种搜索引擎,
配合著名的中文自然语言处理库jieba分词可以实现全文搜索系统.
用户注册与登录是网站必备的功能之一, Django内置的Auth认证系统可以帮助开发人员快速实现用户管理功能.
但很多网站为了加强社交功能, 在用户管理功能上增设了第三方网站的用户注册与登录功能, 这是通过OAuth 2.0认证与授权来实现的.
Django的异步主要由Celery框架实现, 这是Python开发的异步任务队列, 
它支持使用任务队列的方式在分布的机器, 进程和线程上执行任务调度.

Celery侧重于实时操作, 每天处理数以百万计的任务.
Celery本身不提供消息存储服务, 它使用第三方数据库来传递任务, 目前第三方数据库支持RabbitMQ, Redis和MongoDB等.
Channels是使用WebSocket协议实现浏览器和服务器的双向通信的, 它的实现原理是在Django中定义Channels的API接口,
由网页的JavaScript与API接口构建通信连接, 使浏览器和服务器之间相互传递数据.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/695827.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【YOLOv5进阶】——修改网络结构(以C2f模块为例)

一、站在巨人的肩膀上 这里我们借鉴YOLOv8源码&#xff1a; 上期说到&#xff0c;对于网络模块定义详情在common.py这个文件&#xff0c;如Conv、CrossConv、C3f等。本期要修改的需要参考YOLOv8里的C2f模块&#xff0c;它定义在YOLOv8的module文件夹的block.py文件里&#xf…

js调试过程中修改变量值

1.在想要变更的地方添加断点 2.添加监视表达式 3.执行网页代码&#xff0c;当执行到断点处则会停止 4.点击执行下一步&#xff0c;则会执行监视表达式

新材料正不断推动模具3D打印行业发展

随着工业4.0的浪潮席卷全球&#xff0c;模具制造行业也迎来了技术革新的新纪元。3D打印技术以其独特的制造优势&#xff0c;正逐渐在模具制造领域崭露头角。然而&#xff0c;要实现模具3D打印技术的广泛应用&#xff0c;高性能的打印材料是不可或缺的关键因素。 材料是模具3D打…

一、Socket创建和连接

C网络编程&#xff08;asio&#xff09; 文章目录 C网络编程&#xff08;asio&#xff09;1、Asio概述2、网络编程基本流程2、创建socket3、创建监听socket4、绑定accpet监听套接字5、连接指定的端点6、服务器接收连接 点击查看代码 1、Asio概述 ​ Asio起源于Boost库&#xf…

超详解——python条件和循环——小白篇

目录 1. 缩进和悬挂else 2. 条件表达式 3. 和循环搭配的else 4. 可调用对象 总结&#xff1a; 1. 缩进和悬挂else 在Python中&#xff0c;代码块是通过缩进来表示的。条件判断和循环结构的代码块需要正确缩进。悬挂else指的是else子句和相应的if或循环在同一级别的缩进。 …

AVL树 ---(C++)

本篇讲全面的讲解 AVL 树的插入&#xff0c;旋转以及验证 AVL 树的性能&#xff08;本篇未实现删除代码&#xff09;。至于为什么会有 AVL 树&#xff0c;这是因为简单的二叉搜索树并不能直接的保证搜索的效率&#xff0c;因为当我们在二叉搜索树中插入一段有序的序列的时候&am…

STC90C51驱动LCD1602、LCD12864、OLED

主控芯片&#xff08;STC90C516RDPG5151028&#xff09;介绍 ROM64K,RAM1280字节&#xff0c;40Pin&#xff0c;3个定时器&#xff0c;1个串口&#xff0c;8个中断源&#xff08;分别是&#xff1a;外部中断0(INTO)、外部中断 1(INT1)、外部中断 2(INT2)、外部中断 3(INT3)、定…

【微信小程序开发(从零到一)】——个人中心页面的实战项目(二)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

「动态规划」如何计算能获得多少点数?

740. 删除并获得点数https://leetcode.cn/problems/delete-and-earn/description/ 给你一个整数数组nums&#xff0c;你可以对它进行一些操作。每次操作中&#xff0c;选择任意一个nums[i]&#xff0c;删除它并获得nums[i]的点数。之后&#xff0c;你必须删除所有等于nums[i] …

【网络安全的神秘世界】web应用程序安全与风险

&#x1f31d;博客主页&#xff1a;泥菩萨 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 第一章&#xff1a;web应用程序安全与风险 web攻击基础知识 1、什么是web应用攻击 web攻击的本质&#xff0c;就是通过http协议篡改应用程序&#xff0…

虚拟机ping不通主机,但是主机可以ping通虚拟机

我在Windows10系统安装了虚拟机&#xff0c;设置的主机与虚拟机的连接方式是桥接&#xff0c;安装好后&#xff0c;发现虚拟机ping不通主机&#xff0c;但是主机可以ping通虚拟机。 我的操作是&#xff1a;关闭防火墙&#xff0c;发现虚拟机可以ping通主机了。说明是Windows10…

python后端结合uniapp与uview组件tabs,实现自定义导航按钮与小标签颜色控制

实现效果&#xff08;红框内&#xff09;&#xff1a; 后端api如下&#xff1a; task_api.route(/user/task/states_list, methods[POST, GET]) visitor_token_required def task_states(user):name_list [待接单, 设计中, 交付中, 已完成, 全部]data []color [#F04864, …

CPP初级:模板的运用!

目录 一.泛型编程 二.函数模板 1.函数模板概念 2.函数模板格式 3.函数模板的原理 三.函数模板的实例化 1.隐式实例化 2.显式实例化 3.模板参数的匹配原则 四.类模板 1.类模板的定义格式 2.类模板的实例化 一.泛型编程 泛型编程&#xff1a;编写与类型无关的通用代码…

express入门01服务器搭建以及get和post请求的监听

微搭提供了后端API的能力&#xff0c;但是不同的版本收费差别巨大&#xff0c;因为使用的门槛限制了中小企业使用低代码平台。那可不可以既要又要呢&#xff1f;答案是肯定的&#xff0c;那其实掌握一定的后端框架&#xff0c;借助我们在低代码中已经熟练掌握的技能其实是比较容…

2024.6.9 七

Python的time库 先导入库 import time相关函数 time.time() 返回当前时间的时间戳(一个记录时间的浮点数),从1970年开始算的 time.localtime(sec) 返回一个指定时间戳(sec)的struct_time对象,是一个元组封装起来的,默认是当地时间 struct_time对象 tm_year 年 tm_mon 月 tm_…

CDR2024软件破解Keygen激活工具2024最新版

CorelDRAW Graphics Suite2024最新版&#xff0c;这是一款让我爱不释手的图形设计神器&#xff01;作为一个软件评测专家&#xff0c;我一直在寻找一款能够提升我的设计效率和创造力的工具。而这款软件&#xff0c;简直就是为我量身定制的&#xff01;&#x1f389; 「CorelDR…

算法金 | AI 基石,无处不在的朴素贝叶斯算法

大侠幸会&#xff0c;在下全网同名「算法金」 0 基础转 AI 上岸&#xff0c;多个算法赛 Top 「日更万日&#xff0c;让更多人享受智能乐趣」 历史上&#xff0c;许多杰出人才在他们有生之年默默无闻&#xff0c; 却在逝世后被人们广泛追忆和崇拜。 18世纪的数学家托马斯贝叶斯…

温度传感器十大品牌

温度传感器品牌排行榜-十大热电偶品牌-热敏电阻品牌排行-Maigoo品牌榜

TikTok Shop账号需要防关联吗?

在TikTokShop作为新兴的电商销售渠道中&#xff0c;保护账号的安全和隐私&#xff0c;防止账号关联成为了重要的任务。为了更好地理解为何需要防关联以及如何进行防范&#xff0c;让我们深入探讨一下这个问题。 为什么要防关联&#xff1f; 1. 账号异常风险&#xff1a;防关联…

电容十大品牌供应商

十大电容器品牌&#xff0c;电解电容-陶瓷电容-超级电容器品牌排行榜-Maigoo品牌榜