Django-中间件-csrf扩展请求伪造拦截中间件-Django Auth模块使用-效仿 django 中间件配置实现功能插拔式效果-09

目录

昨日补充:将自己写的 login_auth 装饰装在 CBV 上

类里面的方法一般都是类绑定方法 或者 对象绑定方法,第一个参数是类 或者 对象本身,那么前面写的装饰器就要改参数才能用了,不过这里我们可以用 django 给我们写好的装饰器,从不需要更改写好的装饰器

三种方式 (别忘了导模块)

from django.utils.decorators import method_decorator

# @method_decorator(login_auth, name='get')  # 第一种, name 参数必须指定
class MyHome(View):
    # @method_decorator(login_auth)  # 第二种, get 和 post 都会被装饰(登录验证)(直接把 dispatch 拿过来,加个装饰器)
    def dispatch(self, request, *args, **kwargs):
        super().dispatch(request, *args, **kwargs)
     
    @method_decorator(login_auth)  # 第三种,直接装饰在单个方法上
    def get(self, request):
        return HttpResponse('get')
    
    def post(self, request):
        return HttpResponse('post')

django 中间件

django 中间件 就类似于是 django 的门户,请求来的时候需要先经过 中间件 才能到达 django 后端(urls),响应走的时候也需要经过 中间件 才能到达 web服务网关接口(wsgif 模块)

django 中间件可以用来做什么

  • 做网站全局的身份校验,限制访问频率,权限校验(反爬)... 只要是涉及到全局的校验几乎都可以在中间件中完成,第一时间该想到的也是中间件

django 的中间件是设计比较完善的,逻辑最清晰,最简单(flask的中间件不如它)

讲完这个中间件就知道为什么我们前面每次提交 post 请求都会写上先去 settings.py 里把 csrf 这个中间件暂时注释掉了

django 请求生命周期 *****

经过 中间件 之后才能进入 urls.py(再 views.py ... 一层一层递进)

科普:

  1. wsgiref 不能够承受高并发,所以上线之后会换成 uwsgi 模块(前面再加一个 nginx 做反向代理)
  • WSGI 与 wsgi 以及 uwsgi 分别什么意思

​ WSGI是一个协议标准,wsgiref 和 uwsgi 都是实现了 WSGI 协议的功能模块

  1. 请求在进入第一层中间件时会去缓存数据库中判断有没有数据
  • 如果有的话会直接拿到数据并返回请求(这样可以节约资源,降低服务器以及数据库的压力)
  • 如果没有的话会接着一层一层地走中间件,然后路由配置、views.py ...,等请求再次来到最后一层中间件时,在返回数据的同时,会保存一份在缓存数据库中。(下次就可以直接在缓存数据库中拿到数据了)

具体原理等后期涉及到展开来讲, 先知道这个概念就行

默认中间件及其大概方法组成

django 默认有七个中间件 django 支持用户自定义自己的中间件,并且暴露给用户,还暴露给用户五个可以自定义中间件的方法

# settings.py 里的七个默认中间件
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

点进去观察 中间件源码

'''
django.middleware.security.SecurityMiddleware
--> 本质是动态导入(可以看最后面的那个模仿案例) 
# from django.middleware.security import SecurityMiddleware

django.middleware.csrf.CsrfViewMiddleware
--> from django.middleware.csrf import CsrfViewMiddleware
'''

发现 django 中间件 中有五个用户可以自定义的方法

# django.middleware.csrf.CsrfViewMiddleware  --> from django.middleware.csrf import CsrfViewMiddleware
class CsrfViewMiddleware(MiddlewareMixin):
    def _accept(self, request):
    def _reject(self, request, reason):
    def _get_token(self, request):
    def _set_token(self, request, response):
    def process_request(self, request):
    def process_view(self, request, callback, callback_args, callback_kwargs):
    def process_response(self, request, response):

# django.middleware.security.SecurityMiddleware  --> django.middleware.security.SecurityMiddleware
class SecurityMiddleware(MiddlewareMixin):
    def __init__(self, get_response=None):
    def process_request(self, request):
    def process_response(self, request, response):      

# django.contrib.sessions.middleware.SessionMiddleware
class SessionMiddleware(MiddlewareMixin):
    def __init__(self, get_response=None):
    def process_request(self, request):
    def process_response(self, request, response):
  • 需要我们掌握的方法有
  1. process_ request() 方法
  2. process_ response ()方法
  • 需要了解的方法
  1. process_ view()
  2. process exception ()
  3. process_ template_ response ()

中间件的执行顺序

大体同 django 请求生命周期 那张图,可能会受以下情况的影响

自定义中间件探究不同操作对中间件执行顺序的影响

测试思路:

  • 在 settings.py 里注册不同中间件,探究默认的执行顺序
  • 在不同中间件的 process_request 和 process_response 等方法中 return HttpResponse 对象会对执行顺序造成什么影响
  • 了解五种方法的触发时机

自定义中间件

  1. 新建一个文件夹(放在全局或 app 内)
  2. 写一个类继承 MiddlewareMiXin 类
  3. 里面书写需要的(五个方法中的某些)方法
  4. 一定要在 settings.py 里配置中间件

代码

mymiddleware/mdd.py 自定义中间件

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse


class MyMdd(MiddlewareMixin):
    def process_request(self, request):
        print('我是第一个中间件里面的process_request方法')

    def process_response(self, request, response):
        print('我是第一个中间件里面的process_response方法')
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        print(view_func)
        print(view_args)
        print(view_kwargs)
        print('我是第一个中间件里面的process_view方法')

    def process_exception(self, request, exception):
        print('我是第一个中间件里面的process_exception')

    def process_template_response(self, request, response):
        print('我是第一个中间件里面的process_template_response')
        return response


class MyMdd1(MiddlewareMixin):
    def process_request(self, request):
        print('我是第二个中间件里面的process_request方法')

    def process_response(self, request, response):
        print('我是第二个中间件里面的process_response方法')
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        print(view_func)
        print(view_args)
        print(view_kwargs)
        print('我是第二个中间件里面的process_view方法')

    def process_exception(self, request, exception):
        print('我是第二个中间件里面的process_exception')

    def process_template_response(self, request, response):
        print('我是第二个中间件里面的process_template_response')
        return response


class MyMdd2(MiddlewareMixin):
    def process_request(self, request):
        print('我是第三个中间件里面的process_request方法')

    def process_response(self, request, response):
        print('我是第三个中间件里面的process_response方法')
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        print(view_func)
        print(view_args)
        print(view_kwargs)
        print('我是第三个中间件里面的process_view方法')

    def process_exception(self, request, exception):
        print('我是第三个中间件里面的process_exception')

    def process_template_response(self, request, response):
        print('我是第三个中间件里面的process_template_response')
        return response

在 settings.py 中的配置

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'mymiddleware.mdd.MyMdd',  # 配置上
    'mymiddleware.mdd.MyMdd1',  # 配置上
    'mymiddleware.mdd.MyMdd2',  # 配置上
]

需要掌握的方法

process_request

请求来的时候会依次执行 settings.py 配置文件中注册了的中间件里的该方法

  • 如果没有该方法则直接跳过,走下一个中间件
  • 如果该方法里返回了 HttpResponse 对象,那么会直接从当前中间件的 process_response 方法 从下往上依次执行返回,不会再接着往下执行
  • 执行顺序:从上往下
  • 该方法可以实现对用户身份的校验,访问频率的限制,用户权限的校验...

基于该特点就可以做访问频率限制

process_response

响应走的时候会依次执行 settings.py 配置文件中注册了的中间件里的该方法(必须将 response 形参返回,因为这个 response 指代的就是返回给前端的数据)

  • 如果没有该方法则直接跳过,走下一个中间件
  • 执行顺序:从下往上
  • 该方法可以帮你实现缓存机制(减缓服务器、数据库的压力)

需要了解的方法

process_view

路由匹配成功 执行视图函数之前 自动触发(从上往下依次执行)

process_exception

视图函数报错了,自动触发(从下往上依次执行)

process_template_response

视图函数返回的 HttpResponse 对象中包含了 render 属性时会触发,或者是表明一个对象时 TemplateResponse 对象或等价方法 的时候也会触发(从下往上依次执行)

def index(request):
    print("我是 index 视图函数")
    def render():
        return HttpRespone('用户最终能够看到的结果')  # ******
    obj = HttpResponse('index')
    obj.render = render  # 返回的 HttpResponse 对象中必须包含 render 属性,才能触发中间件里定义的 process_template_response 方法
    return obj

强调:

在写中间件的时候,只要形参中有 response ,就要记得将其返回,这个Response 是要给前端的信息

csrf 中间件 跨站请求伪造

钓鱼网站

原理:写了一个一模一样的网站,一个隐藏框,发送往隐藏当做收钱方

问题:如何区分当前用户朝我们网站发送的请求页面是不是我们本网站给的

防止思路

网站会给返回给用户的 form 表单页面 偷偷塞一个随机字符串

请求到来的时候,会先比对随机字符串是否一致,如果不一致,直接拒绝(403 FORBIDDEN)

解决方案

在页面上放一个 隐藏的 input 框,value 里面放的是一个字符串,每次刷新都会更新里面的 value,这样别人的网站就不知道;,这个 value 就无法伪造了

django 的实现 {% csrf_token %}

该随机字符串有以下特点:

  • 同一个浏览器每一次访问都不一样
  • 不同浏览器绝对不一样

post请求提交数据通过 csrf 校验

form 表单

form 表单发送 post 请求的时候,需要你做的是写一段代码 {% csrf_token %} 即可,不需要注释 csrf 中间件了

ajax 发送

三种方式(第三种可以用在前后端分离时)

  1. 先在页面上写 {% csrf_token %},利用标签查找,获取到该 input 键值信息,放到 data 里
  2. ajax data 值 那里直接写 {{ csrf_token }}data:{'username':'jason','csrfmiddlewaretoken':'{{ csrf_token }}'},
  3. 参考官方文档推荐,自定义 js 文件,在要用到的页面载入这个 js 脚本,自动获取并传递 csrf 校验 *****
    • 你可以将下面的 js 代码 放到一个 js 文件中

    // js 代码(一般放在 static 文件夹下) function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } var csrftoken = getCookie('csrftoken'); function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } $.ajaxSetup({ beforeSend: function (xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } } });

    • 之后要用到的的地方 <script src="{% static 'setjs.js' %}"></script> 导入就行
    • 这样就不需要 在 html 中写 {% csrf_token %} 或在 ajax 中写 {{ csrf_token }}

csrf 装饰器相关

其他中间件也可以效仿下面的方法来校验或者取消校验

两个问题

当你网站全局都需要校验 csrf 的时候(未注释掉 csrf 中间件时),有几个不需要校验该如何处理? @csrf_exempt 当你的网站全局不校验 csrf 的时候(注释掉 csrf 中间件时),有几个需要校验该如何处理 ?@csrf_protect

未注释掉 csrf 中间件时 单功能取消 csrf 校验:csrf_exempt

FBV

from django.views.decorators.csrf import csrf_exempt

# 全局开启时,局部禁用
@csrf_exempt
def index(request):
  pass

CBV

有两种方式,不能针对单个方法,是针对全局的

# CBV比较特殊,不能单独加在某个方法上
# 只能加在类上或dispatch方法上
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt


# @method_decorator(csrf_exempt,name='dispatch')  # 第一种
class Csrf_Token(View):
  @method_decorator(csrf_exempt)  # 第二种
  def dispatch(self,request,*args,**kwargs):
    res = super().dispatch(request,*args,**kwargs)
    return res
  # @method_decorator(csrf_exempt)  # 这里这么写不行!!!
  def get(self,request):
    pass
  def post(self,request):
    pass

注释掉 csrf 中间件时 单功能开启 csrf 校验:csrf_protect

FBV

from django.views.decorators.csrf import csrf_protect


@csrf_protect
def lll(request):
    return HttpResponse('lll')

CBV 还要改改

有三种方式,既可以针对全局的,也可以针对单个的

from django.views.decorators.csrf import csrf_protect

from django.views import View
from django.utils.decorators import method_decorator


# 第一种方式
# @method_decorator(csrf_protect,name='post')  # 有效的
class MyView(View):
    @method_decorator(csrf_protect)  # 第三种方式
    def dispatch(self, request, *args, **kwargs):
        res = super().dispatch(request, *args, **kwargs)
        return res

    def get(self, request):
        return HttpResponse('get')

    # 第二种方式
    # @method_decorator(csrf_protect)  # 有效的
    def post(self, request):
        return HttpResponse('post')

总结:csrf 装饰器中只有 csrf_exempt 是特例,其他的装饰器在给CBV 装饰的时候 都可以有三种方式

Auth 模块

科普提示小点:

  • 一个方法的放回结果打印出来是 “字符串” 最好 type 确认一下,可能是对象重写了 __str__() 方法
  • django 后台管理只有超级用户才能进入
  • 用了 Auth 模块的方法 ,就最好都用 Auth 模块的方法
  • 修改密码必须调用 .save() 保存,否则无效

使用 django 自带的 auth 表做登录功能

涉及到的 auth 相关方法

python3 manage.py createsuperuser  # 命令行下创建超级用户(可以拥有登录 django admin 后台管理的权限)

# 查询用户是否存在
user_obj = auth.authenticate(username=username, password=password)  # 数据库中的密码是密文的(该方法不能只传用户名一个参数),返回值要么是对象,要么是 None

# 记录用户状态
auth.login(request, user_obj)  # 登录,会自动存 session
# 优点:只要执行了这一句话,你就可以在后端任意位置通过 request.user 拿到当前登录的用户对象(未登录会报错,AnonymousUser 匿名用户)

# 获取用户对象
request.user  # 用户登录了直接获取用户对象,用户没登录获取到 AnonymousUser 匿名用户

# 判断当前用户是否登录,未登录(AnonymousUser)会返回 False,其他情况下返回 True
request.user.is_authenticated

# 验证用户密码是否正确
is_right = request.user.check_password(old_password)  # 将获取的用户密码,自动加密,然后去数据库中对比(返回布尔值)

# 修改密码
request.user.set_password(new_password)  # 修改密码
request.user.save()  # 需要保存才能生效

# 注销用户
auth.logout(request)  # 等价于 request.session.flush() (删除了 session
表中记录,浏览器 cookie)


# 登录验证装饰器
from django.contrib.auth.decorators import login_required

# @login_required  # 自动校验当前用户是否登录,如果没有登录,(未传参数的情况下)默认跳转到 django 自带的登录页面(还是 404 ?)
# ------ 局部配置
@login_required(login_url='/login/')
def set_password(request):
    pass

# ------ 全局配置(不用在里面写配置了)
# 在 settings.py 中写
LOGIN_URL = '/login/'


# 注册用户
from django.contrib.auth.models import User  # 这就是那张 auth 表
# 创建普通用户
User.objects.create_user(username=username, password=password)
# 创建超级用户
User.objects.create_superuser(username=username, password=password, email='12323132@qq.com')  # 创建超级用户必须传邮箱
# 不能用 User.objects.create(username=username, password=password)  (这样密码没有加密)

核心代码

app01/views.py

from django.shortcuts import render, HttpResponse
from django.contrib import auth


def xxx(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        # 取数据库查询当前用户数据
        # models.User.objects.filter(username=username,password=password).first()
        user_obj = auth.authenticate(username=username, password=password)  # 必须要用 因为数据库中的密码字段是密文的 而你获取的用户输入的是明文
        print(user_obj)
        # print(user_obj)
        # print(user_obj.username)
        # print(user_obj.password)
        # 保存用户状态
        # request.session['user'] = user_obj
        auth.login(request, user_obj)  # 将用户状态记录到session中
        """只要执行了这一句话  你就可以在后端任意位置通过request.user获取到当前用户对象"""
    return render(request, 'xxx.html')


def yyy(request):
    print(request.user)  # 如果没有执行auth.login那么拿到的是匿名用户
    print(request.user.is_authenticated)  # 判断用户是否登录  如果是你们用户会返回False
    # print(request.user.username)
    # print(request.user.password)
    return HttpResponse('yyy')


from django.contrib.auth.decorators import login_required


# 修改用户密码
@login_required  # 自动校验当前用户是否登录  如果没有登录 默认跳转到 一个莫名其妙的登陆页面
def set_password(request):
    if request.method == 'POST':
        old_password = request.POST.get('old_password')
        new_password = request.POST.get('new_password')
        # 先判断原密码是否正确
        is_right = request.user.check_password(old_password)  # 将获取的用户密码 自动加密 然后去数据库中对比当前用户的密码是否一致
        if is_right:
            print(is_right)
            # 修改密码
            request.user.set_password(new_password)
            request.user.save()  # 修改密码的时候 一定要save保存 否则无法生效
    return render(request, 'set_password.html')


@login_required
def logout(request):
    # request.session.flush()
    auth.logout(request)
    return HttpResponse("logout")


from django.contrib.auth.models import User


def register(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        user_obj = User.objects.filter(username=username)
        if not user_obj:
            # User.objects.create(username =username,password=password)  # 创建用户名的时候 千万不要再使用create 了
            # User.objects.create_user(username =username,password=password)  # 创建普通用户
            User.objects.create_superuser(username=username, password=password, email='123@qq.com')  # 创建超级用户
    return render(request, 'register.html')

自定义扩展 autor 表字段

前提:

settings.py 添加额外配置

# ... 其他配置
# 告诉 django 不再使用 auth 默认的表  而是使用你自定义的表
AUTH_USER_MODEL = 'app01.Userinfo'  # '应用名.模型表类名'
# ... 其他配置

两种方式

app01/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser


# Create your models here.
# 第一种 使用一对一关系  不考虑


# 第二种方式   使用类的继承
class Userinfo(AbstractUser):
    # 千万不要跟原来表(AbstractUser)中的字段有冲突
    phone = models.BigIntegerField()
    avatar = models.CharField(max_length=32)
    # 别忘了去 settings.py 里配置

后续操作

执行数据库迁移命令(python3 manage.py makemigrations、python3 manage.py migrate

这样以后,所有的 auth 模块功能,全部都基于你创建的表,而不再使用 auth_user(不会再自动创那些表了)

效仿 django中间件配置 实现 功能插拔式效果

django 的中间件 其实就是一个类,一个个功能可以写成类,注释掉就不执行了

我们效仿中间件(后面要学的 restframework 的设计思想也是这样的),做一个通知功能, 可以发微信通知、短信通知、右键通知

代码实现

重点配置部分(设计思想好好学学)

start.py 入口文件

import notify

notify.send_all('国庆放假了 记住放八天哦')

notify/__init__.py 关键代码(结合了 importlib 动态导入、反射 等知识点)

import settings
import importlib


def send_all(content):
    for path_str in settings.NOTIFY_LIST:  # 1.拿出一个个的字符串   'notify.email.Email'
        module_path, class_name = path_str.rsplit('.', maxsplit=1)  # 2.从右边开始 按照点切一个 ['notify.email', 'Email']
        module = importlib.import_module(module_path)  # from notity import msg/email/wechat
        cls = getattr(module, class_name)  # 利用反射 一切皆对象的思想 从文件中获取属性或者方法 cls = 一个个的类名
        obj = cls()  # 类实例化生成对象
        obj.send(content)  # 对象调方法

settings.py 配置(可以在这里开启或关闭功能)

NOTIFY_LIST = [
    'notify.email.Email',
    'notify.msg.Msg',
    # 'notify.wechat.WeChat',  # 注释掉了,这个功能就不执行了
    'notify.qq.QQ',
]

功能扩展部分

然后是各个功能的文件(拆分成了各个文件,搭配 settings.py 起到可插拔式效果),要想新增这种功能直接加个文件实现这几个代码即可

notify/email.py

class Email(object):
    def __init__(self):
        pass  # 发送邮件需要的代码配置

    def send(self, content):
        print('邮件通知:%s' % content)

notify/msg.py

class Msg(object):
    def __init__(self):
        pass  # 发送短信需要的代码配置

    def send(self, content):
        print('短信通知:%s' % content)

notify/qq.py

class QQ(object):
    def __init__(self):
        pass  # 发送qq需要的代码准备

    def send(self, content):
        print('qq通知:%s' % content)

notify/wechat.py

class WeChat(object):
    def __init__(self):
        pass  # 发送微信需要的代码配置

    def send(self, content):
        print('微信通知:%s' % content)
补充:pycharm 使用技巧

想知道当前跳进来的代码的位置,可以点击图中的图标,快速定位,可以让你知道目录结构(看看推测对不对)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏授客的专栏

Python基于Python实现批量上传文件或目录到不同的Linux服务器

批量上传文件、目录(包括该目录下的所有文件,子目录及其文件)到不同的Linux服务器

26030
来自专栏侯哥的Python分享

python基础-函数(9)

如果在开发程序时,需要某块代码多次,但是为了提高编写的效率以及代码的重用,所以把具有独立功能的代码块组织为一个小模块,这就是函数

8630
来自专栏侯哥的Python分享

python基础-分支判断语句(4)

输出结果为:sex=1 会输出”我是一个帅哥”,当我们把sex的值改为其他值的时候就会显示我是一个美女。

10830
来自专栏侯哥的Python分享

python高级-模块(14)

有过C语言编程经验的朋友都知道在C语言中如果要引用sqrt函数,必须用语句#include <math.h>引入math.h这个头文件,否则是无法正常进行调用的...

10120
来自专栏侯哥的Python分享

python高级-面向对象(11)

在使用对象的过程中,为了将具有共同特征和行为的一组对象抽象定义,提出了另外一个新的概念——类

11830
来自专栏授客的专栏

基于Python实现的死链接自动化检测工具

编辑conf/urlNoLogin.txt文件中填写不需要登录就可访问的种子url,格式如下,每行一条url路径

9130
来自专栏侯哥的Python分享

python基础-字符串(6)

当打来浏览器登录某些网站的时候,需要输入密码,浏览器把密码传送到服务器后,服务器会对密码进行验证,其验证过程是把之前保存的密码与本次传递过去的密码进行对比,如果...

10530
来自专栏毛利学Python

Django项目知识点(二)

通过@register.simple_tag()注册,可以设置name属性,不设置就是函数名

9530
来自专栏侯哥的Python分享

python高级-面向对象特性(12)

在现实生活中,继承一般指的是子女继承父辈的财产,在程序中,继承描述的是事物之间的所属关系,例如猫和狗都属于动物,程序中便可以描述为猫和狗继承自动物;同理,波斯猫...

9640
来自专栏侯哥的Python分享

python高级-包(15)

在桌面创建demo.py文件,并把receiveMsg.py和sendMsg.py使用import文件.模块的方式导入demo.py文件。

5310

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励