Django学习笔记
初始Django
Django 最初被设计用于具有快速开发需求的新闻类站点,目的是要实现简单快捷的网站开发。Django是一个开放源代码的 Web 应用框架,由Python写成。采用了 MVT 的软件设计模式,即模型 Model,视图 View 和模板 Template。
Django 原理图示
命令 manage
命令 manage
# 查看详细命令列表python manage.py# 进入脚本模式python manage.py shell# 创建一个项目django-admin.py startproject project_name# 创建一个 apppython manage.py startapp app_name# 同步数据python manage.py makemigrations # 生成同步信息python manage.py makemigrations web # 指定 apppython manage.py showmigrations --list # 预同步查看python manage.py migrate # 同步python manage.py migrate web # 同步指定 apppython manage.py migrate --database=default-db # 同步指定数据库# 运行网站python manage.py runserverpython manage.py runserver 8001python manage.py runserver 0.0.0.0:8000 # 本机任意 IP,可指定# 完整脚本<path>/python <path>/manage.py runserver 8123# 停止运行pkill -f runserver# 或者ps auxw | grep runserverkill <pid=3424> # 找到显示的 pid# 清空数据,只留下表结构python manage.py flush# 创建管理员python manage.py createsuperuser# 修改用户密码:python manage.py changepassword username
配置 settings
开发环境与测试环境自适应:
# 在项目原 settings.py (删除),创建以下文档结构:settings-- __init__.py-- common.py # 能用配置-- dev.py # 开发环境配置-- pro.py # 生产环境(线上)配置
__init__.py
的内容为:
# 在 mac 和 windows 电脑上为开发环境,生产环境一般为 linuximport socketfrom .common import *host_name_prefix = socket.gethostname().lower()[:3]if host_name_prefix == 'mac' or host_name_prefix == 'win': from .dev import *else: from .pro import *
配置内容:
# 站点所在目录BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))# 允许的域名及IP, 线上需要如实填写ALLOWED_HOSTS = [ '*', # Allow domain and subdomains # '', # Also allow FQDN and subdomains]# 数据库DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'data_db.sqlite3'), }}# 时间格式、语言LANGUAGE_CODE = 'en-us'TIME_ZONE = 'Asia/Shanghai'DATETIME_FORMAT = 'Y-m-d H:i:s'USE_I18N = FalseUSE_L10N = FalseUSE_TZ = True# 静态目录STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), os.path.join(BASE_DIR, "file"),]# 媒体文件MEDIA_ROOT = os.path.join(BASE_DIR, "file")MEDIA_URL = "/file/" # "//xx.xx.com/" dev# 静态文件STATIC_URL = "/static/"
在项目中如果需要引用配置变量:
from django.conf import settings# 使用媒体文件的目录path_prefix = settings.MEDIA_ROOT
模型 models
模型结构:
from django.db import modelsfrom django.urls import reverseimport django.utils.timezone as timezoneclass Item(models.Model): id = models.AutoField(primary_key=True) title = models.CharField(max_length=200, blank=False, ) slug = models.SlugField(max_length=50, unique=True, db_index=True) class Meta: db_table = "my_item" # 建议给每个 app 增加统计的前缀 verbose_name = 'Item' ordering = ('id',) # 自定义属性方法 def tag_list(self): return [i for i in str(self.tags).split('#')] def get_absolute_url(self): return reverse("item_detail", args=[str(self.slug)]) def __str__(self): return str(self.title)
字段:
# 主键,自增 IDid = models.AutoField(primary_key=True)# slugslug = models.SlugField(max_length=50, default=default_slug, unique=True, db_index=True)# 短正数字dy = models.SmallIntegerField(blank=True, null=True)# 整数qty = models.PositiveSmallIntegerField(blank=True, null=True, )# 带小数num = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=6)# 其他数字qty = models.IntegerField(blank=True, null=True)qty = models.BigIntegerField(blank=True, null=True)# 文本des = models.CharField(max_length=250, blank=True, null=True)# 长文本text = models.TextField(blank=True, null=True)# 单选gender = models.NullBooleanField(choices=((True, "男"), (False, "女"), (None, "")), default=True)# 布尔型published = models.BooleanField(default=True)# 时间, 默认为创建时间created = models.DateTimeField('Created', auto_now_add=True, auto_now=False, editable=False, null=True)# 时间, 自动取更新时间update_time = models.DateTimeField(auto_now=True, null=True)# 可编辑时间, auto_now/auto_now_add 为 True 不起作用begin_time = models.DateTimeField(default=timezone.now, editable=True)# 图片上传pic = models.ImageField(upload_to='pic/%Y/%m', blank=True, null=True)# 文件上传file = models.FileField(upload_to='up/%Y/%m', blank=True, null=True)# 外键item = models.ForeignKey("Item", on_delete=models.PROTECT, blank=True, null=True)# 外键及选择约束type = models.ForeignKey("Choice", default=4, limit_choices_to={'type': 'text_type'}, related_name=' ', on_delete=models.PROTECT)
查询结果集:
# filter 方法, 返回所有条件为均为真 and 的结果集Entry.objects.filter()# exclude 方法, 返回排除满足所有条件 or 的结果集Entry.objects.exclude()# get() 方法, 返回满足条件的唯一结果, 如结果为多个报错e = Entry.objects.get(id=5)e.title # 直接取字段值, 下条效果相同Entry.objects.select_related('title').get(id=5) # 直接返回指定字段# 不存在的错误对象from django.core.exceptions import ObjectDoesNotExist# extra() 灵活调用实现 SQLEntry.objects.extra(select={'is_recent': "pub_date > '2006-01-01'"})Entry.objects.extra(where=["foo='a' OR bar = 'a'", "baz = 'a'"])Entry.objects.extra(where=['headline=%s'], params=['Lennon'])q.extra(order_by = ['-is_recent'])# dates() 获得结果集中的时间列表(字段,精确度), 如 datetime.date(2005, 1, 1)Entry.objects.dates('pub_date', 'day', order='DESC')
结果集处理:
# 返回空查询结果集Entry.objects.none()# 返回当前结果集的副本Entry.objects.all()# 将 qs1 和 qs2、qs3 联接组合起来qs1.union(qs2, qs3)# 取结果集的交集, 共同的部分qs1.intersection(qs2, qs3)# 取结果集的差集, 仅在 qs1 中有的qs1.difference(qs2, qs3)# 指定取出信息, 提高性能, 可同时使用Entry.objects.defer("des", "body") # 除了这两个字段Person.objects.only("name") # 只取此字段
其他:
# 使用指定的数据库Entry.objects.using('db2')# 使用原生 SQL 语句Person.objects.raw('SELECT * FROM myapp_person')# 联合使用Entry.objects.filter().exclude().get()# 聚合 aggregate()Blog.objects.aggregate(Count('entry'))
结果集查询方法:
# 迭代for e in Item.objects.all(): print(e.title)# 支持切片my_queryset.all()[:5]# 转为列表list(my_queryset.all())# 是否存在查询结果my_queryset.filter(pk=entry.pk).exists()# 获取指定一条结果my_queryset.first() # 第一个结果my_queryset.last() # 最后一个结果, 另有 latest() earliest()# 排序my_queryset.filter.order_by('-pub_date', 'age') # 负号表示降序my_queryset.order_by('?') # 随机排序my_queryset.order_by('blog__name', 'title') # blog__name 中 blog 为外键my_queryset.order_by('title').order_by('pub_date') # 两次排序my_queryset.reverse() # 按相反的顺序返回# 去重my_queryset.distinct()# 结果数my_queryset.count()# 给定列表对应字段的结果, 默认为主键my_queryset.in_bulk([1, 2])my_queryset.in_bulk(['lily', 'tom'], field_name='name')# 获取时间列表, datetimes() 类似my_queryset.dates('pub_date', 'day', order='DESC') # 逻辑使用同上文# exclude 排除# 与 filter 相同, 取非P.objects.exclude(name__contains="tlp")
返回数据格式:
# 返回指定字段值, 类 list [(),()]my_queryset.values_list() # 返回所有字段my_queryset.values_list('id', flat=True) # 单个字段返回此字段所有值组成的列表my_queryset.values_list('id', 'title', named=True) # 返回一个 namedtuple# 返回指定字段值, 类 json [{},{}]my_queryset.values() # 返回所有字段my_queryset.values('blog_id') # blog 为外键, 返回 key 为 blog_idmy_queryset.values('id', 'name')from django.db.models.functions import Lowermy_queryset.values(lower_name=Lower('name')) # 字段名指定, 值转小写# 返回指定类型数据# 返回字典 {'user_name': 'lily', ...}Tweet.objects.values("user_name")# json 和 xmlfrom django.core import serializersdata = serializers.serialize("xml", SomeModel.objects.all())serializers.serialize('json', [book1, book2], indent=2, use_natural_foreign_keys=True, use_natural_primary_keys=True)from django.core.serializers import serializeserialize('json', SomeModel.objects.all(), cls=LazyEncoder)
Q():
# Q(), 对象的复杂查询# 注: 与字段同时查询时放在前, 不可使用__双下划线from django.db.models import QModel.objects.filter(x=1, y=2) # 字段查询方法Model.objects.filter(Q(x=1) & Q(y=2)) # ANDModel.objects.filter(Q(x=1, z=4) | Q(y=2)) # ORModel.objects.filter(~Q(name="cox")) # NOT
F() 表达式:
# F() 表达式, 对象中某列值的操作# 注: 与字段同时查询时放在前, __双下划线需在一个model里from django.db.models import F# 原价加10, 更新数据, 支持单个和多个E.objects.update(price=F("price") 10)# 查询语文成绩大于数学成绩减5分的学生E.objects.filter(chinese__gt=F('math')-5)E.objects.filter(authors__name=F('blog__name'))E.objects.filter(mod_date__gt=F('pub_date') timedelta(days=3))
aggregate:
# aggregate 聚合函数# 定义 avg, 值为字段 math 的平均数, 返回: {'avg': 27}from django.db.models import Count, Avg, Sumfrom django.db.models import FloatFieldP.objects.aggregate(avg=Avg('math'))# {'price__avg': 43.54}Book.objects.all().aggregate(Avg('price'))# {'page_price': 0.4470664529184653}Book.objects.all().aggregate(page_price=Avg(F('price') / F('pages'), output_field=FloatField()))
annotate:
# annotate 在 聚合 aggregate 基础上 GROUP BY# 统计每个班最小的年龄l = S.objects.all().annotate(min_age=Min("age"))[(i.class, i.min_age) for i in l]# Fun 自定义类,继承 Func 类, 处理数据from django.db.models import Funclass Lower(Func): # 继承Func类 function = 'LOWER' # 使用类属性指定要使用的方法 pass # 细节得看官方文档qs.annotate(field_lower=Func(F('field'), function='LOWER'))# https://docs.djangoproject.com/en/3.0/ref/models/expressions
字段(穿透)查询 lookups:
# 简单匹配Entry.objects.get(id__exact=14) # 精确匹配 sql =Blog.objects.get(name__iexact='beatles blog') # 模糊匹配 sql likeE.objects.get(headline__contains='Lennon') # 包含 sql LIKE '%Lennon%'E.objects.get(headline__icontains='Lennon') # sql ILIKE '%Lennon%'E.objects.filter(headline__startswith='Lennon') # 开头包含 LIKE 'Lennon%'E.objects.filter(headline__istartswith='Lennon') # ILIKE 'Lennon%'E.objects.filter(headline__endswith='Lennon') # 结尾包含 LIKE '%Lennon'E.objects.filter(headline__iendswith='Lennon') # ILIKE '%Lennon'E.objects.filter(id__in=[1, 3, 4]) # sql IN (1, 3, 4)E.objects.filter(headline__in='abc') # IN ('a', 'b', 'c')# 图片 ImageField 为空的查询E.objects.filter(picture__exact='')# 条件, 数据和时间# gt 大于; gte 大于等于; lt 小于; lte 小于等于;E.objects.filter(id__gt=4) # 大于 sql id > 4# 是否为空筛选Entry.objects.filter(pub_date__isnull=True)# 正则匹配Entry.objects.get(title__regex=r'^(An?|The) ')Entry.objects.get(title__iregex=r'^(an?|the) ')# 关联外键查询Blog.objects.filter(entry__authors__name='Lennon')
结果集操作方法:
# 构造一个空的对象实例Person.objects.none()# 增加信息Person.objects.create(first_name="Bruce", last_name="Springsteen")# 增加信息 2p = Person(first_name="Bruce", last_name="Springsteen")p.save(force_insert=True)# 先查询, 无结果创建一条信息, 返回 [obj, created]Person.objects.get_or_create()# 批量创建, 批量更新 bulk_update() 略Entry.objects.bulk_create([ Entry(headline='This is a test'), Entry(headline='This is only a test'), ])# 更新, 返回的更新条数Entry.objects.filter(pub_date__year=2010).update(comments_on=False)# 更新方法 2e = Entry.objects.get(id=10)e.comments_on = Falsee.save()# 有则更新, 无刚创建Person.update_or_create()# 删除b = Blog.objects.get(pk=1)Entry.objects.filter(blog=b).delete()# 输出解释信息 explainprint(Blog.objects.filter(title='My Blog').explain(verbose=True))p.delete() # 删除本数据P.objects.all().delete() # [危险]全删除
实践技巧:
# 查看实际 SQLprint(queryset.query)# 随机返回一条信息Play.objects.filter().order_by('?').first()# 链式查询: Q 查询 OR, 赋默认值, 多重排序, 取第一条obj = (Artist .objects .filter(Q(bm=now.month, bd=now.day, type__id=5) | Q(dm=now.month, dd=now.day, type__id=5)) .extra(select={"null_rating": "rating is null"}, order_by=['null_rating', '-rating', 'id']) .first() )
时间相关
from django.utils import timezone# 当前本地时间now = timezone.localtime(timezone.now())now.month # 取年月日等# 查询几天内内容ago_3_days = now - timezone.timedelta(days=3)E.objects.filter(published=1, pub_date__gte=ago_3_days)# 在两个时间内的内容E.objects.filter(begin_time__lte=now, end_time__gt=now)# 将文本解析为时间from django.utils.dateparse import parse_datetimetime = parse_datetime("2012-02-21 10:28:45")# 时间范围start_date = datetime.date(2005, 1, 1)end_date = datetime.date(2005, 3, 31)E.objects.filter(pub_date__range=(start_date, end_date))# 时间条件my_queryset.filter(pub_date__date=datetime.date(2005, 1, 1)) # 在指定时间my_queryset.filter(pub_date__date__gt=datetime.date(2005, 1, 1)) # 时间之后my_queryset.filter(pub_date__year=2005) # 在某年内# 支持 year/month/day/week_day/time/hour 等my_queryset.filter(pub_date__hour__gte=20) # 在时间后
视图 views
基本视图:
from django.shortcuts import render, get_object_or_404from web.models import Itemdef item_detail(request, slug): item = Item.objects.get(slug=slug, published=1) return render(request, 'item.html', {'item': item}) # return render(request, 'i.html', # {'item': get_object_or_404(Item, slug=slug, published=1) # })
返回基础内容及跳转:
from django.shortcuts import HttpResponse, HttpResponseRedirectfrom django.contrib.auth.decorators import login_required@login_required(login_url='/login/') # 页面需要登录def item_detail(request, slug): try: pass return HttpResponse('获取成功!') # 返回内容 except: HttpResponseRedirect('/login/') # 跳转
其他:
# 301 跳转from django.shortcuts import HttpResponsePermanentRedirectdef photo_item(request, slug): return HttpResponsePermanentRedirect('/node/{0}'.format(str(slug)))
路由 urls
from django.contrib import adminfrom django.urls import include, path, re_pathfrom web import viewsurlpatterns = [ path('', views.index, name='index'), path('item/<slug:slug>', views.item_detail, name='item_detail'), path('login/', views_user.login_site, name='login'), # api path('api/', include('api._urls')), # admin path('admin.noname/', admin.site.urls),]
缓存 cache
配置:
# CachesCACHES = { 'default-no': { # 不执行, 开发模式下可使用 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 'LOCATION': os.path.join(BASE_DIR, 'cache'), 'TIMEOUT': 60 * 60 * 24 * 7, # a week 'OPTIONS': { 'MAX_ENTRIES': 10000 } }, 'default': { # Memcached 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'LOCATION': '127.0.0.1:11211', 'TIMEOUT': 60 * 60 * 24 * 7, # a week }, 'file': { # 文件 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 'LOCATION': os.path.join(BASE_DIR, 'cache'), 'TIMEOUT': 60 * 60 * 24 * 7, # a week 'OPTIONS': { 'MAX_ENTRIES': 10000 } },}
在视图 views.py 中使用
from django.views.decorators.cache import cache_page@cache_page(60*10, cache='file') # unit of second, 10mindef index(request): pass
在路由 urls.py 中使用
from django.views.decorators.cache import cache_pageurlpatterns = [path('node', cache_page(60*2, cache="default")(node.web), name='node'),]
写一个缓存装饰器
# todo
模板
基础功能:
# 取模型对象的属性{{ item.title }}# 媒体地址{{ MEDIA_URL }}# 引用模板{% include "footer.html" %}# 给引用模板传入变量{% include "pub/m_review.html" with type='wiki' cell=wik.slug %}# 当前时间{% now "Y" %} # 格式如:2020# 取其对应外键中的条目{% for i in teacher.class_set.all %}
流程控制:
# if 语句{% if item.switch_on %} 开启{% else %} 关闭{% endif %}# for 中的计算{% if i.type.item == 'var' %} # > < != or and# for 循环, 处理首个元素{% for i in item.tag_list %} {% if forloop.first %}标签:{% endif %}<code>{{ i }}</code>{% empty %} no msg.{% endfor %}
信息过滤处理:
# 按 html 代码输出{{ i.html|safe }}# 取 type 的对外显示内容i.get_type_display }}# 取外键的属性{{ i.teacher.name }}# 切片计算{% for i in names|slice:":1" %} # 第一个{% for i in names|slice:":4" %} # 前4个# 默认值{{ i.year|default:"2019" }}# 排序{% for i in timeline|dictsort:"time" %} # 正序{% for i in timeline|dictsortreversed:"time" %} # 降序# 显示前12个字符,剩余用「...」代替{{ item.nameCN|truncatechars:12 }}# 时间格式化{{ i.pub_time|date:'Y-m-d' }} # 如:2020-01-05{{ i.pub_time|date:"H:i:s" }} # 如:12:11:04# 取长度{{ name|length }} # 返回如:3# 复合处理{% for i in teacher.class_set.all|dictsortreversed:"id"|slice:":6" %}# 格式化为标题{{ i.name|title }}# 值减去1, 加为正{{ evt.ty|add:-1 }}# 去掉渲染后 html 中的空格{% spaceless %} code {% endspaceless %}# 注释{% comment %} code {% endcomment %} 和 {# code #}{{ foo|truncatechars:7 }} # 显示部分长度, 用省略号代码{{ foo|truncatechars_html:7 }} # 针对 html 显示部分长度
分组:
# 将学生名单按年龄分组,同年龄的显示在一起{% regroup students|dictsortreversed:"time_int" by age as aged %}<ul>{% for age in aged %} <li>{{ age.grouper }} <ul> {% for i in age.list %} <li>{{ i.name }}: {{ i.height }}</li> {% endfor %} </ul> </li>{% endfor %}</ul>
后台 admin
# 在 url 中增加路由from django.urls import pathfrom django.contrib import adminurlpatterns = [path('web-admin/', admin.site.urls), ]# 在 app 目录中创建 admin.pyfrom django.contrib import adminfrom .models import Newsclass NewsAdmin(admin.ModelAdmin): list_display = ('id', 'title','tags' ,'url', 'update_time') raw_id_fields = ('writer', ) list_display_links = ('id', 'title',) search_fields = ['title', 'writer__name', 'slug'] form = NewsForm # 可定义表单样式 radio_fields = {"type": admin.HORIZONTAL, } # VERTICAL list_per_page = 30admin.site.register(News, NewsAdmin)
数据库支持 DB
TODO
富文本管理器
推荐使用 django-ckeditor
。
升级 django 3.0 后 ckeditor 无法返回上传文件路径问题的解决:
CKEditor Refused to display 'XXX' in a frame because it set 'X-Frame-Options' to 'deny'.
Django 3.0 需要将 X_FRAME_OPTIONS 的默认值由 SAMEORIGIN 调整为 DENY.
# https://docs.djangoproject.com/en/3.0/ref/clickjacking/# settings:MIDDLEWARE = [ ... 'django.middleware.clickjacking.XFrameOptionsMiddleware', ...]# 给定配置X_FRAME_OPTIONS = 'SAMEORIGIN'
文件图片
TODO
日志
TODO
信号
TODO
登录退出
TODO
特殊字段
ArrayField
支持级数、列表,包括嵌套形式数据,常用场景为文章标签、经纬度等,为 PostgreSQL 所支持, 数据类型如 character varying[]
。
from django.contrib.postgres.fields import ArrayField, JSONField# 可用 IntegerField 等作为数组内容,格式如 [1, 3, 5, 7],tags = ArrayField(models.CharField(max_length=10, null=True), blank=True, null=True)# 格式如 [[1,3], [2,4]], 限定每个元素里有两个元素(必须)tags = ArrayField(ArrayField(models.CharField(max_length=10), size=2), null=True)# 表单, 上例对应。每个元素编辑和显示时可以用 # 号分隔from django.contrib.postgres.forms import SimpleArrayFieldtags = SimpleArrayField(forms.CharField(), delimiter='#', required=False)tags = SimpleArrayField(SimpleArrayField(forms.CharField()), delimiter='#', required=False)# 查询Tags.objects.filter(tags__contains=['a', 'b']) # 包含所有元素Tags.objects.filter(tags__contained_by=['a', 'b']) # 所有内容为其的子集Tags.objects.filter(tags__overlap=['a']) # 包含其中一个Tags.objects.filter(tags__len=1) # 长度为1的Tags.objects.filter(tags__0='a') # 第一个元素为 a 的Tags.objects.filter(tags__1__iexact='b')Tags.objects.filter(tags__0_2__contains=['c']) # 第1和2和元素中包含 c 的
JSONField
可以存储 json 格式数据,为 PostgreSQL 所支持。
data = JSONField() # 在 models 中定义Dog.objects.filter(data__owner__name='Bob') # 指定 owner.name 值Dog.objects.filter(data__owner__other_pets__0__name='Fishy') # 第一条数据值Dog.objects.filter(data__contains={'owner': 'Bob'})Dog.objects.filter(data__contained_by={'breed': 'collie', 'owner': 'Bob'})Dog.objects.filter(data__has_key='owner')Dog.objects.filter(data__has_any_keys=['owner', 'breed'])Dog.objects.filter(data__values__contains=['collie'])Dog.objects.filter(data__keys__overlap=['breed', 'toy'])
分页
from django.core.paginator import Paginator, EmptyPage, PageNotAnIntegerpic_list = Picture.objects.filter(photo_type=1).order_by('id')paginator = Paginator(pic_list, 1) # Show Qty contacts per pagepage = request.GET.get('page')try: pics = paginator.get_page(page)except PageNotAnInteger: # If page is not an integer, deliver first page. pics = paginator.get_page(1)except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. pics = paginator.get_page(1) # (paginator.num_pages)# 以模板中可以调用, 如 {{ pics.number }}# pics.has_previous 是否有上一页# pics.has_next 是否有下一页# pics.number 当前页序# pics.paginator.num_pages 总页数# pics.previous_page_number 上一页页序# pics.next_page_number 下一页页序
Session 和 Cookie
# 设置 cookie, 有效期一年, 单位秒response = self.get_response(request)response.set_cookie("uuid", 'cookie值', max_age=365 * 24 * 60 * 60)# 读取 Cookierequest.COOKIES['uuid'] # 取指定 cookie 值request.COOKIES.keys() # 取所有 cookie 名
TODO
接口 api
简单的接口实现:
from django.http import JsonResponsefrom music.models import Newsdef single_object(obj): return { 'title': obj.title, 'slug': obj.slug, 'type': obj.type, 'files': ['https://pic.gairuo.com/file/' i for i in obj.file], 'text': obj.text, }def news_detail(request): slug = request.GET['slug'] new = News.objects.get(slug=slug).id news_data = [single_object_score(obj) for obj in new] data = {'data': news_data} return JsonResponse(data, safe=False)
其他
TODO
赞 (0)