首页 > 技术文章 > drf05--频率、过滤排序与分页

Edmondhui 2021-12-09 02:06 原文

内容回顾

# drf:方便我们在django框架上写出符合restful规范的接口
# 请求和响应
    -请求类的对象
    -请求解析编码格式:局部,全局
    
    -响应对象
    	-data,status,header
    -响应格式:浏览器,json
         
# 序列化类
    -Serializer
    -ModelSerializer(用的多,表模型有对应关系)

# 视图类
    -两个视图基类
        -APIView:执行流程
        -GenericAPIView:两个类属性,三个方法
    -5个视图扩展类(不是视图类)
    	-需要配合GenericAPIView使用
    -9个视图子类
    -视图集
    	-ModelViewSet
        -ReadOnlyModelViewSet
        -ViewSet
        -GenericViewSet
        -ViewSetMixin
        
 # 路由
    -自动生成路由(SimpleRouter,DefaultRouter)
    -action装饰器
    
 # 认证类
    -写一个类继承BaseAuthentication重写authenticate在里面判断用户是否登录,如果登录了返回两个值,第一个值必须是当前登录用户,如果认证失败,抛出异常
    -局部使用
    -全局使用
    
# 权限类
    -写一个类继承BasePermission重写has_permission在里面判断用户是否有权限,如果有,return True,否则return False
    -局部使用
    -全局使用

今日内容

1. drf之频率限制

# 限制用户的访问次数:根据用户ip地址限制

例:一个IP地址,一分钟之内只允许访问三次

# 获取访问者IP地址
    -request.META.get('REMOTE_ADDR')  # REMOTE:远程
    
# request.META:请求头中所有的数据
# 自定义请求头的数据,也在request.META中,且给你转成 key为 'HTTP_大写'的形式

1.1 频率类定义

# 1.写一个类,继承SimpleRateThrottle

# 2.重写get_cache_key(),返回以什么做限制 (返回ip就以ip限制)

# 3.配置 频率类 限制值的 key 
在类中写一个类属性:scope = 'ip_m_3'  key自定义,要在 配置文件中或频率类中 直接配置一个与之对应的值

# 4.配置 频率类 限制的值
# 写在配置文件中
在settings.py中写
REST_FRAMEWORK = {
    # key值是频率类中scop字段对应的值,value是访问次数限制值
    'DEFAULT_THROTTLE_RATES': {'ip_m_3': '3/m',}  
}

# 写在频率限制类中
    THROTTLE_RATES = {'ip_m_3': '3/m'}

    
# 写一个频率限制类,根据ip地址 一分钟只能访问三次
from rest_framework.throttling import SimpleRateThrottle
class MyThrotting(SimpleRateThrottle):
    scope = 'ip_m_3'  # 配置 限制值的key 
    THROTTLE_RATES = {'ip_m_3': '3/m'}  # 配置 限制的值
    
    def get_cache_key(self, request, view):
        # return request.META.get('REMOTE_ADDR')
        return self.get_ident(request)  # 内置的get_ident()就是返回访问者的ip,是父类BaseThrottle的方法

1.2 频率类使用

# 局部使用
-配置在视图类中
    class IndexView(APIView):
        throttle_classes = [MyThrotting, ]
            
# 全局使用            
-配置在settings.py中
    REST_FRAMEWORK = {
    	'DEFAULT_THROTTLE_CLASSES': ['app01.auth.MyThrotting',],
    }

1.3 自定义频率类(继承BaseThrottle) (了解逻辑 以后不会用这个)

# 自定制频率类(继承BaseThrottle),限制ip一分钟只能访问三次
实际可以不继承BaseThrottle类,只要你有该类里面的allow_request方法,你就是该类,原理是鸭子类型

    # 需要写两个方法
    判断是否限次:没有限次可以请求True,限次了不可以请求False
    def allow_request(self, request, view):
    限次后调用,显示还需等待多长时间才能再访问,返回等待的时间seconds
    def wait(self):
      
# 自定义实现频率类功能的逻辑:
    1)取出访问者ip
    2)判断当前ip不在访问字典里,添加进去,并且直接返回True,表示第一次访问,在字典里,继续往下走
    3)循环判断当前ip的列表,有值,并且当前时间减去列表的最后一个时间大于60s,把这种数据pop掉,这样列表中只有60s以内的访问时间,
    4)判断,当列表小于3,说明一分钟以内访问不足三次,把当前时间插入到列表第一个位置,返回True,顺利通过
    5)当大于等于3,说明一分钟内访问超过三次,返回False验证失败
        
# 代码
import time
class IPThrottle():
    # 定义成类属性,所有对象用的都是这一个
    VISIT_RECORD = {}
    def __init__(self):
        self.history=[]
    def allow_request(self, request, view):
        ip=request.META.get('REMOTE_ADDR')
        ctime=time.time()
        if ip not in self.VISIT_RECORD:
            self.VISIT_DIC[ip]=[ctime]
            return True
        self.historyt=self.VISIT_VISIT_RECORD.get(ip)   # 当前访问者时间列表拿出来
        while self.history and ctime-self.history_list[-1]>60:
            self.history.pop() # 把最后一个移除
        if len(self.history)<3:
            self.history.insert(0,ctime)
            return True
        else:
            return False

    def wait(self):
        # 当前时间,减去列表中最后一个时间
        ctime=time.time()
        return 60-(ctime-self.history_list[-1])

1.4 内置的频率类

from rest_framework.throttling import AnonRateThrottle, UserRateThrottle, ScopeRateThrottle

# AnonRateThrottle
限制所有匿名未认证用户,使用IP区分用户
setting中 'DEFAULT_THROTTLE_RATES': {'anon': '5/m',}  设置频次

# UserRateThrottle
限制认证用户,使用User id 来区分  # 但是认证用户,也必须使用内置的认证类:SessionAuthentication
setting中 'DEFAULT_THROTTLE_RATES': {'user': '10/m',}  设置频次

# ScopeRateThrottle
限制用户对于每个视图的访问频次,使用IP或User id


# 例1:限制未登录用户1分钟访问5次
from rest_framework.throttling import AnonRateThrottle
class TestView1(APIView):
    # 配置内置频率类 (局部配置)
    throttle_classes = [AnonRateThrottle]
    def get(self,request,*args,**kwargs):
        return Response('我是未登录用户')
    
-配置在settings.py中
    REST_FRAMEWORK = {
        'DEFAULT_THROTTLE_RATES': {'anon': '5/m',}
    }
    
    
# 例2:限制登录用户1分钟访问10次    
from rest_framework.authentication import SessionAuthentication
from rest_framework.throttling import UserRateThrottle
class TestView2(APIView):
    # 配置内置认证类 (局部配置)
    authentication_classes=[SessionAuthentication]
    # 配置内置频率类 (局部配置)
    throttle_classes = [UserRateThrottle]
    def get(self,request,*args,**kwargs):
        return Response('我是登录用户')
    
-配置在settings.py中
    REST_FRAMEWORK = {
        'DEFAULT_THROTTLE_RATES': {'user': '10/m',}
    } 
    
    
# 或直接例1和例2 采用全局配置
全局:在setting中
  'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ),
    'DEFAULT_THROTTLE_RATES': {
        'user': '10/m',
        'anon': '5/m',
    }   

2. drf之过滤与排序

# 请求地址中带过滤条件 (条件过滤、排序过滤)

# 配置使用:
# 1.局部配置
在视图函数中配置: filter_backends= [内置,第三方,自己写]
# 2.全局配置
在setting中: 'DEFAULT_FILTER_BACKENDS': (内置,第三方,自己写)

# 注意:
1.获取所有的数据 才需要过滤条件
2.必须继承GenericAPIView++ListModelMixin及其子类,才能使用过滤、排序和分页
(只有获取所有 list()方法 才有过滤、排序和分页功能, 因为过滤、排序、分页都需要queryset对象)

3.因内置的过滤和排序类,使用时很固定,必须是search=值 、ordering=值 
故:通常过滤类 不使用内置的,使用第三方(django-filter)或者自定义过滤类,排序类使用内置的就可以了

# 自己读源码发现:第三条的原因是错误的,是可以修改查询条件的参数名,且有一定的查询条件(但还是太少,且不方便)
思路:继承内置的SearchFilter类,重写父类的 search_param参数,就可以按照自己给的参数进行查询

from rest_framework.filters import SearchFilter
class FilterByName(SearchFilter):
    search_param = 'name'

2.1 内置过滤类使用

from .models import Book
from .serializer import BookSerializer
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin

# 过滤功能的使用
from rest_framework.filters import SearchFilter,OrderingFilter  # 条件过滤、排序过滤
class BookView(GenericViewSet, ListModelMixin):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    # 配置过滤类
    filter_backends = [SearchFilter,]
    # 配置要过滤的字段
    search_fields=['name',]  # books/?search=红   name中 带红都会查出来
    search_fields=['name','price'] # books/?search=红  name中或者price中 带红都会查出来
    
    # 特殊字符+配置过滤的字段 ===> 起到额外的筛选作用   自己读源码获知(了解即可)
    # 默认是'icontains'  忽略大小写的包含
    lookup_prefixes = {     # 查询特殊符 做前缀
        '^': 'istartswith', # 以什么开始
        '=': 'iexact',      # 完整的查
        '@': 'search',      # 搜索 (貌似使用有点问题)
        '$': 'iregex',      # 以正则查
    }
    # 例:
    search_fields=['^name',]  # 查找name字段中 以搜索值为开头的
    
# 使用:
http://127.0.0.1:8000/books/?search=红   

2.2 内置排序类使用

from rest_framework.filters import SearchFilter, OrderingFilter
class BookView(GenericViewSet, ListModelMixin):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    # 配置排序类
    filter_backends = [OrderingFilter, ]
    # 配置要排序的字段
    # ordering_fields = ['price']   # books/?ordering=-price  按price降序排列
    ordering_fields = ['price','id']   # books/?ordering=-price,-id 按price降序排列,如果price一样,再按id的降序排列
    
# 使用:
http://127.0.0.1:8000/books/?ordering=-price  

2.3 排序和过滤同时使用

# 既有排序,又有过滤
from rest_framework.filters import SearchFilter, OrderingFilter
class BookView(GenericViewSet, ListModelMixin):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    # 配置排序类
    filter_backends = [SearchFilter,OrderingFilter, ]  # 先过滤,再排序 (减少排序的数据量)
    # 配置要过滤的字段
    search_fields=['name',]
    # 配置要排序的字段
    ordering_fields = ['price']  # books/?search=记&ordering=-price  查询名字中带记的并且按价格降序排列

2.4 第三方过滤类使用

# 实现:http://127.0.0.1:8000/books/?name=红楼梦&price=12  过滤查询

# django-filter--基本使用(更高级,后面项目讲)
# pip install django-filter

# 第一步:安装 django-filter 模块
# 第二步:setting中注册: INSTALLED_APPS=['django_filters']
# 第三步:使用第三方的过滤类
from django_filters.rest_framework import DjangoFilterBackend
class BookView(GenericViewSet, ListModelMixin):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    # 配置第三方过滤类
    filter_backends = [DjangoFilterBackend]
    # 配置要过滤的字段
    filter_fields=['name','price']  
    filterset_fields=['name','price']  # 同上
    
# 使用    
http://127.0.0.1:8000/books/?name=红楼梦&price=12  名字是红楼梦并且价格为12的

2.5 自定义过滤类及使用

# 1.写一个类,继承BaseFilterBackend,重写filter_queryset(),返回过滤完的数据 queryset

# 自定义过滤类:
http://127.0.0.1:8000/books/?price_gt=12  (价格大于12)

from rest_framework.filters import BaseFilterBackend
class MyFilterByPrice(BaseFilterBackend):  
    def filter_queryset(self, request, queryset, view):
        # queryset就是要过滤的数据
        price = request.query_params.get('price_gt')  # 获取条件值
        if price:
            queryset = queryset.filter(price__gt=price)
        return queryset  # 返回过滤完的数据

    
# 自定义过滤类的使用
from .auth import MyFilterByPrice
class BookView(GenericViewSet, ListModelMixin):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    
    # 配置自定义过滤类
    filter_backends = [MyFilterByPrice]
    # 不用配置要过滤的字段,因为自己写在自定义过滤类中了

3. drf之分页

分页功能 
# 内置了三种分页类:不能直接使用,需要自定义继承,再修改一下几个类属性

3.1 自定义分页类

# 自定义分页类
from rest_framework.pagination import  PageNumberPagination,LimitOffsetPagination,CursorPagination

# 基本分页 (页数分页) --常用
class MyPageNumberPagination(PageNumberPagination):
    # 重写4个类属性即可
    page_size = 2  # 每页默认显示两条
    
    page_query_param = 'page'  # 设置查询条件--页数的参数名,其值对应的是第几页
    # http://127.0.0.1:8000/books/?page=2
    
    page_size_query_param = 'size'  # 设置查询条件--条数的参数名,其值对应的是显示多少条(最大不超过max_page_size)
    # http://127.0.0.1:8000/books/?page=2&size=4 获取第二页数据,返回4条数据
    
    max_page_size = 5  # 设置每页最大显示多少条 
    # http://127.0.0.1:8000/books/?page=2&size=400 获取第二页数据,最多返回5条


# 偏移分页
class MyLimitOffsetPagination(LimitOffsetPagination):
    # 4个类属性
    default_limit = 2   # 每页默认显示多少条
    
    limit_query_param = 'limit'  # 设置偏移条数的参数名,其值对应的是向后偏移多少条 (最大不超过max_limit)
    # http://127.0.0.1:8000/books/?limit=3
    
    offset_query_param = 'offset'  # 设置偏移开始位置的参数名,其值对应的是从第几条开始偏移
    # http://127.0.0.1:8000/books/?limit=3&offset=2  # 从第2条数据位置向后取3条数据
    
    max_limit = 5   # 限制limit最大条数

    
# 游标分页:针对大数据量,只能上一页或下一页,不能跳到具体某一页
# 优势是速度快,但是不能直接跳到某一页
class MyCursorPagination(CursorPagination):
    page_size = 2  # 每页显示多少条
    cursor_query_param = 'cursor'  # 查询条件,无用 (因为没办法确认其具体的值) 
    ordering = 'id' # 按谁排序

3.2 分页类使用

from app01.auth import MyPageNumberPagination as PageNumberPagination
class BookView(GenericViewSet, ListModelMixin):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    
    # 局部配置分页类
    pagination_class = PageNumberPagination

3.3 自定义实现:继承APIView的视图,实现分页功能

# 原理:根据rest_framework的分页源码思路+自定义分页类
    
# page.py    
from rest_framework.pagination import  PageNumberPagination
class MyPageNumberPagination(PageNumberPagination):
    # 重写4个类属性即可
    page_size = 2 
    page_query_param = 'page'  
    page_size_query_param = 'size'  
    max_page_size = 5


# view.py:
class BookView(APIView):
    def get(self, request):
        # 1.先获取所有需要序列化的对象
        qs = Book.objects.all()
        
        # 2.分页,通过分页类分页
        # 2.1 实例化得到分页类对象,不需要传参数
        page = MyPageNumberPagination()  
        # 2.2 对qs进行分页,分页类对象page的某个方法来实现对qs分页
        res = page.paginate_queryset(qs, request, view=self)  # 返回的res是列表,是当前页码的所有数据
        
        # 3.对当前页码的数据,进行序列化
        ser = BookSerializer(instance=res, many=True)

        # 4.返回分页后的数据
        # return Response(ser.data)
        return page.get_paginated_response(ser.data)  # 使用过滤类的返回方法,带有上/下一页、总页的格式

补充

1.BaseThrottle的get_ident()方法中

xff = requeset.META.get('HTTP_X_FORWARDED_FOR') 
# 'X_FORWARDED_FOR' 是http的请求头,它的作用是拿出所有ip,包括代理

2. 变量后直接加逗号

a=(3,)
a=3,  
print(type(a))  # a是元组

作业

1 写一个频率类,限制books接口一分钟只能访问5次 (带基本分页功能)
# 自定义频率类:一分钟只能访问5次
class AccessThrottle(SimpleRateThrottle):
    scope = '5_m'

    def get_cache_key(self, request, view):
        return self.get_ident(request)

# 自定义分页类:基本分页(页数分页)
class PagePagination(PageNumberPagination):
    page_size = 2 
    page_query_param = 'page' 
    page_size_query_param = 'size' 
    max_page_size = 3

2 写Books接口,实现按图书名字查询和按价格排序 (带基本分页功能)

# 重写内置SearchFilter过滤类的 search_param参数,就可以按照自己给的参数名进行查询
class FilterByName(SearchFilter):
    search_param = 'name'
    
# 接口中局部配置:
    # 配置过滤和排序类
    filter_backends = [FilterByName, OrderingFilter]
    # 配置过滤类的字段
    search_fields = ['title']
    # 配置排序类的字段
    ordering_fields = ['price']
  
3 写一个功能,限制年龄小于18岁的用户,每天只能8:00--9:00登录
"""
思路1: 写在自定义认证类中
    这种是都可以登录,但登录之后,校验失败,还是当做没有登录处理
1.根据获取的token,去user_token查找对应的用户
2.再判断用户的年龄字段,和登录时间(在UserToken表中,每次登录会刷新token,也会刷新登录时间)
3.成功就返回用户和token
4.失败,则抛出响应的异常
"""
class LoginAuth(BaseAuthentication):
    # 限制年龄小于18岁的用户,每天只能8:00--9:00登录
    def authenticate(self, request):
        token = request.query_params.get('token')
        user_token_obj = UserToken.objects.filter(token=token).first()
        if not user_token_obj:
            raise AuthenticationFailed('token未传入或不合法')
        if user_token_obj.user.age < 18 and user_token_obj.login_time.hour in (8, 9):
            raise AuthenticationFailed('小于18岁的用户,只能在每天8-9点登录')
        return user_token_obj.user, token
"""
思路2:直接写在登录接口中 (这种是登录接口,再次验证判断)
1.密码验证成功之后
2.再判断用户的年龄字段,和当前登录时间(time.time().year)
3.成功就返回token
4.失败,则返回'小于18岁的用户,只能在每天8-9点登录'
"""
    
    
4 三次密码失败之后,账户被禁用 (管理员用户解锁)
# 拓展:若是连续输错三次,那么就加一个登录时间字段
"""
思路:
登录接口中:
1.先验证用户名
2.判断该用户名的 登录失败次数字段是否大于等于三次,是则返回该用户被锁,不是则验证密码
3.验证成功,登录成功 返回token;
4.验证失败,给该用户表 登录失败次数字段加1 login_fail_number += 1
"""
class LoginView(ViewSet):

    @action(methods=['POST'], detail=False)
    def login(self, request):
        name = request.data.get('name')
		# 1.先验证用户名 是否存在
        name_user = models.LoginUser.objects.filter(name=name).first()
        if not name_user:
            return Response({'code': 101, 'msg': '用户名不存在'})
		# 2.判断该用户名的 登录失败次数字段是否大于等于三次,是则返回该用户被锁,不是则验证密码
        if name_user.login_fail_number >= 3:
            return Response({'code': 101, 'msg': '该用户密码输错已超过三次,被锁定'})

        password = request.data.get('password')
        user = models.LoginUser.objects.filter(name=name, password=password).first()
        # 3. 验证密码,成功就返回token
        if user:
            token = str(uuid4())
            models.UserToken.objects.update_or_create(defaults={'token': token}, user=user)
            return Response({'code': 100, 'msg': '登录成功', 'token': token})
        else:
            # 4. 验证密码,失败就给该用户名的登录失败次数字段加1
            name_user.login_fail_number += 1
            name_user.save()
            return Response({'code': 101, 'msg': '密码错误'})

推荐阅读