• 【django2.0之Rest_Framework框架三】rest_framework认证,权限,限流,过滤,排序,分页,异常处理,生成文档介绍


    1. 认证Authentication

    1. 全局配置
      可以在配置文件中配置全局默认的认证方案。
    from rest_framework import settings
    
    # 在settings配置文件中,我们可以进行下面的配置来覆盖默认配置
    REST_FRAMEWORK = {
        'DEFAULT_AUTHENTICATION_CLASSES': (
          #哪个写在前面,优先使用哪个认证
            'rest_framework.authentication.SessionAuthentication',  # session认证,admin后台其实就使用的session认证,其实接口开发很少用到session认证,所以我们通过配置可以改为其他认证,比如后面项目里面我们用到jwt,JSON WEB TOKEN认证,或者一些配合redis的认证
            'rest_framework.authentication.BasicAuthentication',   # 基本认证,工作当中可能一些测试人员会参与的话,他们会将一些认证数据保存在内存当中,然后验证的,我们基本上用不上
        )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 局部使用
      也可以在每个视图中通过设置authentication_classess属性来设置,比如说我们很多接口的数据都是可以让别人获取数据的,但是有可能有些接口是调用给别人网站的,有可能到时候我们就需要一些单独的认证了
    from rest_framework.authentication import SessionAuthentication, BasicAuthentication
    from rest_framework.views import APIView
    
    class ExampleView(APIView):
        # 类属性
        authentication_classes = [SessionAuthentication, BasicAuthentication] #也可以写成元祖形式的,到时候我们使用我们自己开发的认证组件的时候,就需要自己写一个认证组件类,然后写在列表中来使用
        ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    认证失败会有两种可能的返回值:

    • 401 Unauthorized 未认证
    • 403 Permission Denied 权限被禁止

    自定义认证组件

    ## 写一个认证类 students/utils/app_auth.py
    from rest_framework.authentication import BaseAuthentication
    from rest_framework.exceptions import AuthenticationFailed
    from students.models import UserToken
    class MyAuthentication(BaseAuthentication):
        def authenticate(self, request):
            # 认证逻辑,如果认证通过,返回两个值
            #如果认证失败,抛出AuthenticationFailed异常
            token=request.GET.get('token')
            if  token:
                user_token=UserToken.objects.filter(token=token).first()
                # 认证通过
                if user_token:
                    return user_token.user,token
                else:
                    raise AuthenticationFailed('认证失败')
            else:
                raise AuthenticationFailed('请求地址中需要携带token')
    
    # 可以有多个认证,从左到右依次执行
    # 全局使用,在setting.py中配置
    REST_FRAMEWORK={
        "DEFAULT_AUTHENTICATION_CLASSES":["students.utils.app_auth.MyAuthentication",]
    }
    # 局部使用,在视图类上写
    ## views.py
    from students.utils.app_auth import MyAuthentication
    class StudentView(APIView):
        authentication_classes = [MyAuthentication, ]
        def get(self,request):
            print(request.user)  # 用户名
            print(request.auth)  # token
    
            return Response({'msg':'ok'})
    # 局部禁用
    authentication_classes=[]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    2. 权限Permissions

    权限控制可以限制用户对于视图的访问和对于具体数据对象的访问。

    • 在执行视图的dispatch()方法前,会先进行视图访问权限的判断
    • 在通过get_object()获取具体对象时,会进行模型对象访问权限的判断
    1. 全局使用
      可以在配置文件中全局设置默认的权限管理类,如
    REST_FRAMEWORK = {
        ....
        
        'DEFAULT_PERMISSION_CLASSES': (
            'rest_framework.permissions.IsAuthenticated', #登录状态下才能访问我们的接口,可以通过退出admin后台之后,你看一下还能不能访问我们正常的接口就看到效果了
        )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果未指明,则采用如下默认配置

    from rest_framework import permissions
    'DEFAULT_PERMISSION_CLASSES': (
       'rest_framework.permissions.AllowAny', #表示任何人都可以进行任何的操作,没做限制
    )
    
    • 1
    • 2
    • 3
    • 4
    1. 局部使用
      也可以在具体的视图中通过permission_classes属性来设置,如
    from rest_framework.permissions import IsAuthenticated
    from rest_framework.views import APIView
    
    class ExampleView(APIView):
        permission_classes = (IsAuthenticated,)
        ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    内置提供的权限

    • AllowAny 允许所有用户
    • IsAuthenticated 仅通过认证的用户

    自定义权限的使用

    如需自定义权限,需继承rest_framework.permissions.BasePermission父类,并重写has_permission(self, request, view) 方法表示是否可以访问视图, view表示当前视图对象。如果权限通过,就返回True,不通过就返回False

    ## utils/permissions.py
    from rest_framework.permissions import BasePermission
    
    class UserPermission(BasePermission):
        def  has_permission(self, request, view):
            # 不是超级用户,不能访问
            # 由于认证已经过了,request内就有user对象了,当前登录用户
            user=request.user  # 当前登录用户
            # 如果该字段用了choice,通过get_字段名_display()就能取出choice后面的中文
            print(user.get_user_type_display())
            if user.user_type==1:
                return True
            else:
                return False
            
    # 局部使用
    ## views.py
    class TestView(APIView):
        permission_classes = [permissions.UserPermission]
        def get(self,request):
            return Response({'msg':'ok'})
    # 全局使用
    REST_FRAMEWORK={
        "DEFAULT_AUTHENTICATION_CLASSES":["app01.app_auth.MyAuthentication",],
        'DEFAULT_PERMISSION_CLASSES': [
            'app01.app_auth.UserPermission',
        ],
    }
    # 局部禁用
    class TestView(APIView):
        permission_classes = []
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    3. 限流Throttling

    可以对接口访问的频次进行限制,以减轻服务器压力。
    一般用于付费购买次数,投票等场景使用.

    1. 全局使用
      可以在配置文件中,使用DEFAULT_THROTTLE_CLASSESDEFAULT_THROTTLE_RATES进行全局配置
    REST_FRAMEWORK = {
        'DEFAULT_THROTTLE_CLASSES': (
            'rest_framework.throttling.AnonRateThrottle', # 匿名用户,未登录的
            'rest_framework.throttling.UserRateThrottle'  # 经过登录之后的用户
        ),
        'DEFAULT_THROTTLE_RATES': {
            'anon': '100/day',
            'user': '1000/day'
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    DEFAULT_THROTTLE_RATES 可以使用 second, minute, hourday来指明周期。

    源码:
    {'s': 1, 'm': 60, 'h': 3600, 'd': 86400} m表示分钟,可以写m,也可以写minute
    
    • 1
    • 2
    1. 局部使用
      也可以在具体视图中通过throttle_classess属性来配置
    from rest_framework.throttling import UserRateThrottle
    from rest_framework.views import APIView
    
    class ExampleView(APIView):
        throttle_classes = (UserRateThrottle,)
        ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    内置频率组件源码

    from rest_framework.throttling import BaseThrottle,SimpleRateThrottle
    import time
    from rest_framework import exceptions
    visit_record = {}
    class VisitThrottle(BaseThrottle):
        # 限制访问时间
        VISIT_TIME = 10
        VISIT_COUNT = 3
    
        # 定义方法 方法名和参数不能变
        def allow_request(self, request, view):
            # 获取登录主机的id
            id = request.META.get('REMOTE_ADDR')
            self.now = time.time()
    
            if id not in visit_record:
                visit_record[id] = []
    
            self.history = visit_record[id]
            # 限制访问时间
            while self.history and self.now - self.history[-1] > self.VISIT_TIME:
                self.history.pop()
            # 此时 history中只保存了最近10秒钟的访问记录
            if len(self.history) >= self.VISIT_COUNT:
                return False
            else:
                self.history.insert(0, self.now)
                return True
    
        def wait(self):
            return self.history[-1] + self.VISIT_TIME - self.now
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    可选限流类

    1) AnonRateThrottle
    限制所有匿名未认证用户,使用IP区分用户。
    使用DEFAULT_THROTTLE_RATES['anon'] 来设置频次

    2)UserRateThrottle
    限制认证用户,使用User id 来区分。
    使用DEFAULT_THROTTLE_RATES['user'] 来设置频次

    示例

    全局配置中设置访问频率

    	'DEFAULT_THROTTLE_CLASSES': (
            'rest_framework.throttling.AnonRateThrottle',
            'rest_framework.throttling.UserRateThrottle'
        ),
        'DEFAULT_THROTTLE_RATES': {
            'anon': '3/m',
            'user': '10/m'
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4. 过滤Filtering

    对于列表数据可能需要根据字段进行过滤,我们可以通过添加django-fitlter扩展来增强支持。

    pip install django-filter
    
    • 1

    在配置文件中增加过滤后端的设置:

    INSTALLED_APPS = [
        ...
        'django_filters',  # 需要注册应用,
    ]
    # 全局配置
    REST_FRAMEWORK = {
        ...
        'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在视图中添加filter_fields属性,指定可以过滤的字段

    class StudentListView(ListAPIView):
        queryset = Student.objects.all()
        serializer_class = StudentSerializer
        filter_fields = ('age', 'sex') # 配置可以按照哪个字段来过滤
    
    # 127.0.0.1:8000/stu/students/?sex=1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    5. 排序Ordering

    对于列表数据,REST framework提供了OrderingFilter过滤器来帮助我们快速指明数据按照指定字段进行排序。

    使用方法:

    在类视图中设置filter_backends,使用rest_framework.filters.OrderingFilter过滤器,REST framework会在请求的查询字符串参数中检查是否包含了ordering参数,如果包含了ordering参数,则按照ordering参数指明的排序字段对数据集进行排序。

    前端可以传递的ordering参数的可选字段值需要在ordering_fields中指明。

    示例:

    from rest_framework.filters import OrderingFilter
    class Students7View(ModelViewSet):
        queryset = models.Student.objects.all()  # 必须写这个参数 ,方法中使用的self.get_queryset()方法自动获取到queryset属性数据
        serializer_class = StudentModelSerializer  # 非必填属性,self.get_serializer获取到serializer_class制定的序列化器类
        filter_backends = (OrderingFilter,)
        ordering_fields = ('id', 'age')
        # students/?ordering=-id
    
    # 127.0.0.1:8000/books/?ordering=-age 
    # 必须是ordering=某个值
    # -id 表示针对id字段进行倒序排序
    # id  表示针对id字段进行升序排序
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    如果需要在过滤以后再次进行排序,则需要两者结合!

    from rest_framework.generics import ListAPIView
    from students.models import Student
    from .serializers import StudentModelSerializer
    from django_filters.rest_framework import DjangoFilterBackend #需要使用一下它才能结合使用
    class Student3ListView(ListAPIView):
        queryset = Student.objects.all()
        serializer_class = StudentModelSerializer
        filter_fields = ('age', 'sex')
        # 因为filter_backends是局部过滤配置,局部配置会覆盖全局配置,所以需要重新把过滤组件核心类再次声明,
        # 否则过滤功能会失效
        filter_backends = [OrderingFilter,DjangoFilterBackend]
        ordering_fields = ('id', 'age')
        
    # 127.0.0.1:8000/books/?sex=1&ordering=-age 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    6. 分页Pagination

    REST framework提供了分页的支持。
    我们可以在配置文件中设置全局的分页方式,如:

    REST_FRAMEWORK = {
      # 全局分页,一旦设置了全局分页,那么我们drf中的视图扩展类里面的list方法提供的列表页都会产生分页的效果。所以一般不用全局分页
        'DEFAULT_PAGINATION_CLASS':  'rest_framework.pagination.PageNumberPagination',
        'PAGE_SIZE': 100  # 每页最大数据量
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    也可通过自定义Pagination类,来为视图添加不同分页行为。在视图中通过pagination_class属性来指明。

    class LargeResultsSetPagination(PageNumberPagination):
        page_size = 1000
        #127.0.0.1:8001/students/?page=5&page_size=10
        page_size_query_param = 'page_size'
        max_page_size = 10000
    class BookDetailView(RetrieveAPIView):
        queryset = BookInfo.objects.all()
        serializer_class = BookInfoSerializer
        pagination_class = LargeResultsSetPagination
        # pagination_class = None
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意:如果在视图内关闭分页功能,只需在视图内设置

    pagination_class = None
    
    • 1

    可选分页器

    1) PageNumberPagination
    前端访问地址形式:

    GET  http://127.0.0.1:8000/students/?page=4
    
    • 1

    可以在子类中定义的属性:

    • page_size 每页数目
    • page_query_param 前端发送的页数关键字名,默认为"page"
    • page_size_query_param 前端发送的每页数目关键字名,默认为None
    • max_page_size 前端最多能设置的每页数量
    # 声明分页的配置类
    from rest_framework.pagination import PageNumberPagination
    class StandardPageNumberPagination(PageNumberPagination):
        # 默认每一页显示的数据量
        page_size = 2
        # 允许客户端通过get参数来控制每一页的数据量
        page_size_query_param = "size"
        max_page_size = 10
        # 自定义页码的参数名
        page_query_param = "p"
    
    class StudentAPIView(ListAPIView):
        queryset = Student.objects.all()
        serializer_class = StudentModelSerializer
        pagination_class = StandardPageNumberPagination
    
    # 127.0.0.1/four/students/?p=1&size=5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2)LimitOffsetPagination(了解)

    前端访问网址形式:其实就是通过偏移量来取数据

    GET http://127.0.0.1/four/students/?limit=100&offset=400  #从下标为400的记录开始,取100条记录
    
    • 1

    可以在子类中定义的属性:

    • default_limit 默认限制,每页数据量大小,默认值与PAGE_SIZE设置一致
    • limit_query_param limit参数名,默认'limit' , 可以通过这个参数来改名字
    • offset_query_param offset参数名,默认'offset' ,可以通过这个参数来改名字
    • max_limit 最大limit限制,默认None, 无限制
    from rest_framework.pagination import LimitOffsetPagination
    class StandardLimitOffsetPagination(LimitOffsetPagination):
        # 默认每一页查询的数据量,类似上面的page_size
        default_limit = 2
        limit_query_param = "size"
        offset_query_param = "start"
    
    class StudentAPIView(ListAPIView):
        queryset = Student.objects.all()
        serializer_class = StudentModelSerializer
        # 调用页码分页类
        # pagination_class = StandardPageNumberPagination
        # 调用查询偏移分页类
        pagination_class = StandardLimitOffsetPagination
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    7. 异常处理 Exceptions

    看一个简单的示例

    class APIError(Exception):
        pass
    
    class Student2APIView(APIView):
        def get(self,request,pk):
            try:
                instance = Student.objects.get(pk=pk)
            except Student.DoesNotExist:
                raise APIError('自定义API错误')
                return Response({"message":"访问的商品已经下架~"})
    
            serializer = StudentModelSerializer(instance=instance)
            return Response(serializer.data)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    REST framework提供了异常处理,我们可以自定义异常处理函数。

    可以创建一个utils文件夹,里面放一个exceptions.py文件,名字随便写,然后写下面的内容

    from rest_framework.views import exception_handler
    
    def custom_exception_handler(exc, context): #自定义的错误处理函数
      	"""
        	exc错误对象
          	context 异常发生时的一些上下文信息
        """
        # 先调用REST framework默认的异常处理方法获得标准错误响应对象
        response = exception_handler(exc, context) #这个函数是drf提供的,它处理了一些错误,但是如果它处理不了的,它会返回None,所以,如果是None的话,我们需要自己来处理错误
    
        # 在此处补充自定义的异常处理
        if response is None:
          	if isinstance(exc,APIError)
            #这里就可以记录错误信息了,一般记录到文件中,可以使用日志系统来进行记录
            # return Respose({'msg':'自定义API错误了'})
            response.data['status_code'] = response.status_code
    
        return response
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在配置文件中还要声明自定义的异常处理

    REST_FRAMEWORK = {
        'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
    }
    
    • 1
    • 2
    • 3

    如果未声明,会采用默认的方式,如下

    ## rest_frame/settings.py
    REST_FRAMEWORK = {
        'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
    }
    
    • 1
    • 2
    • 3
    • 4

    例如:补充上处理关于数据库的异常

    from rest_framework.views import exception_handler as drf_exception_handler
    from rest_framework import status
    from django.db import DatabaseError
    
    def exception_handler(exc, context):
        response = drf_exception_handler(exc, context)
    
        if response is None:
            view = context['view'] #出错的方法或者函数名称
            if isinstance(exc, DatabaseError):
                print('[%s]: %s' % (view, exc))
                response = Response({'detail': '服务器内部错误'}, status=status.HTTP_507_INSUFFICIENT_STORAGE)
    
        return response
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    REST framework定义的异常

    • APIException 所有异常的父类
    • ParseError 解析错误
    • AuthenticationFailed 认证失败
    • NotAuthenticated 尚未认证
    • PermissionDenied 权限决绝
    • NotFound 未找到
    • MethodNotAllowed 请求方式不支持
    • NotAcceptable 要获取的数据格式不支持
    • Throttled 超过限流次数
    • ValidationError 校验失败

    也就是说,上面列出来的异常不需要我们自行处理了,很多的没有在上面列出来的异常,就需要我们在自定义异常中自己处理了。

    8. 自动生成接口文档

    REST framework可以自动帮助我们生成接口文档。
    接口文档以网页的方式呈现。
    自动接口文档能生成的是继承自APIView及其子类的视图。
    文档:https://www.kernel.org/doc/html/v4.12/core-api/index.html

    8.1. 安装依赖

    REST framewrok生成接口文档需要coreapi库的支持。

    pip install coreapi
    
    • 1

    8.2. 设置接口文档访问路径

    在总路由中添加接口文档路径。
    文档路由对应的视图配置为rest_framework.documentation.include_docs_urls
    参数title为接口文档网站的标题。

    from rest_framework.documentation import include_docs_urls
    
    urlpatterns = [
        ...
        path('docs/', include_docs_urls(title='站点页面标题'))
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果报错了下面的错误,说明我们缺少一个依赖,配置一下就行了
    'AutoSchema' object has no attribute 'get_link'
    配置:

    REST_FRAMEWORK = {
        ...
        'DEFAULT_SCHEMA_CLASS': "rest_framework.schemas.AutoSchema",
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    8.3. 文档描述说明的定义位置

    1) 单一方法的视图,可直接使用类视图的文档字符串,如

    class BookListView(generics.ListAPIView):
        """
        get: 返回所有图书信息.
        post: 添加记录
        """
        #注意,这是在类中声明的注释,如果在方法中你声明了其他注释,会覆盖这个注释的
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2)包含多个方法的视图,在类视图的文档字符串中,分开方法定义,如

    class BookListCreateView(generics.ListCreateAPIView):
        """
        get:
        返回所有图书信息.
    
        post:
        新建图书.
        """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3)对于视图集ViewSet,仍在类视图的文档字符串中封开定义,但是应使用action名称区分,如

    class BookInfoViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericViewSet):
        """
        list:
        返回图书列表数据
    
        retrieve:
        返回图书详情数据
    
        latest:
        返回最新的图书数据
    
        read:
        修改图书的阅读量
        """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    8.4. 访问接口文档网页

    浏览器访问 127.0.0.1:8000/docs/,即可看到自动生成的接口文档。

    两点说明:

    1) 视图集ViewSet中的retrieve名称,在接口文档网站中叫做read
    2)参数的Description需要在模型类或序列化器类的字段中以help_text选项定义,如:

    class Student(models.Model):
        ...
        age = models.IntegerField(default=0, verbose_name='年龄', help_text='年龄')
        ...
    
    • 1
    • 2
    • 3
    • 4

    或 注意,如果你多个应用使用同一个序列化器,可能会导致help_text的内容显示有些问题,小事情

    class StudentSerializer(serializers.ModelSerializer):
        class Meta:
            model = Student
            fields = "__all__"
            extra_kwargs = {
                'age': {
                    'required': True,
                    'help_text': '年龄'
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    数据挖掘知识点整理(期末复习版)
    Python爬虫(五)
    Spark基础【RDD累加器、广播变量、一个小案例】
    SpringCloud&Nacos注册中心服务分级存储模型
    Linux、docker、kubernetes、MySql、Shell运维快餐
    供应商管理软件有哪些特点和优势?
    很详细的系列Shell基础— Shell简介
    一文看懂推荐系统:召回05:矩阵补充、最近邻查找,工业界基本不用了,但是有助于理解双塔模型
    Docker in Docker原理与实战
    怎样下载视频号视频?分享6种有效方法
  • 原文地址:https://blog.csdn.net/monoplasty/article/details/133898899