Django后端开发笔记

0x00 Django核心技术栈

Django的能力

Url路由配置和管理

https://docs.djangoproject.com/en/4.2/topics/http/urls/

数据库迁移(Migrations)

https://docs.djangoproject.com/en/4.2/topics/migrations/

Django提供了一种数据库状态迁移的功能,即通过migrate命令将models.py文件中定义数据库模型状态(包括变更)同步到数据库中。

  • makemigrations

  • sqlmigrate

    输出数据库模型迁移相关sql语句(DDL)

  • showmigrations

    显示数据库模型变更情况,如下图,由于django项目刚创建,默认的管理员后台相关数据模型并未同步到数据库中,所以下图显示存在若干变更待迁移。

  • migrate

python manage.py migrate

执行如上所述数据库迁徙命令,再showmigrations,可见数据库迁徙记录(已完成)

关于django对postgres中多模式(schema)的适用问题

Accessing multiple postgres schemas from Django

https://github.com/bernardopires/django-tenant-schemas

postgresql - Django postgres multiple schema - Database Administrators Stack Exchange

多数据库协作

实际项目中,不同模块的数据可能存放于不同的数据库中,那么Django在对Model进行增删改查时,如何定位到Model属于哪个数据库并进行正确操作呢?

我们可以通过定义以app_label为key,数据库配置为value的数据库配置映射。

通过实现DATABASE_ROUTERS配置所需要的辅助类,可以自定义应用相关数据所关联的数据库相关操作行为。官方文档给出了一些示例代码,本文暂时不展开(我还没详细看其中的源码)。

Multiple databases | Django documentation | Django

Form和ModelForm表单验证

Model数据库设计和操作

cookie和session的登录原理

template模版

表结构设计

外键和一对多关系设计

https://stackoverflow.com/questions/49470367/install-virtualenv-and-virtualenvwrapper-on-macos

settings

models设计数据表

url设计

视图(views)业务逻辑代码

当用户发起的请求匹配上某条url路由规则,用户请求就会被转发到对应的视图函数中处理。

我们可以在视图函数中对请求参数进行处理,按照业务逻辑完成相关操作(如数据查询、变更等),最后返回响应。

数据查询

http://127.0.0.1:8000/map_basic/provinces/

  • 以列表形式返回结果集
  province_objs = Province.objects.values_list('name', 'code').order_by('code')

  • 以字典方式返回结果集
  province_objs = Province.objects.values('name', 'code').order_by('code')

聚合查询
 from django.db.models import Count
 # ...
 # 按照省份名称过滤记录
 museum_province_queryset = Museum.objects.filter(province_name=province_name).values('city_name')
# 分组统计每个地级市有多少条记录(博物馆数量)
 province_count_set = museum_province_queryset.annotate(count=Count("city_name"))

Django应用目录结构

默认来说,使用django-admin startapp命令新建的应用目录会在第一层,在应用数量较多的时候可能并不美观,因此需要重组应用的目录结构。

比如,新建apps目录,用于放置django应用,然后需要在settings文件中将apps目录添加到搜索路径确保django服务能找到应用代码.

BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, os.path.join(BASE_DIR, "apps"))

对应的,settings.py中的INSTALLED_APP需要进行修改:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    "django.contrib.gis",
    'django_admin_listfilter_dropdown',
    'rest_framework',
    "apps.map_basic"
]

template html生成

pass

0x01 Django项目实战

创建Django项目

django-admin startproject Message
cd Message
django-admin startapp message_form 

配置后台管理站点

后台管理站点的地址

默认来说,后台管理系统的地址为:http://127.0.0.1:8000/admin,这是在主应用的urls.py配置的路由,当然我们也可以进行改动:

中间件

Django的中间件是对Django请求/响应处理的一种框架,是一种轻量级的“插件”系统,用于全局处理Django的输入或输出。比如默认Django项目注册了如下中间件:

SecurityMiddleware
    def process_request(self, request):
        path = request.path.lstrip("/")
        if (
            self.redirect
            and not request.is_secure()
            and not any(pattern.search(path) for pattern in self.redirect_exempt)
        ):
            host = self.redirect_host or request.get_host()
            return HttpResponsePermanentRedirect(
                "https://%s%s" % (host, request.get_full_path())
            )
CommonMiddleware

启用该中间件后,每一个请求经过中间件时,会经如下函数进行处理:比如检查用户的USER_AGENT是否在黑名单,是否应该进行重定向等。

    def process_request(self, request):
        """
        Check for denied User-Agents and rewrite the URL based on
        settings.APPEND_SLASH and settings.PREPEND_WWW
        """

        # Check for denied User-Agents
        user_agent = request.META.get("HTTP_USER_AGENT")
        if user_agent is not None:
            for user_agent_regex in settings.DISALLOWED_USER_AGENTS:
                if user_agent_regex.search(user_agent):
                    raise PermissionDenied("Forbidden user agent")

        # Check for a redirect based on settings.PREPEND_WWW
        host = request.get_host()

        if settings.PREPEND_WWW and host and not host.startswith("www."):
            # Check if we also need to append a slash so we can do it all
            # with a single redirect. (This check may be somewhat expensive,
            # so we only do it if we already know we're sending a redirect,
            # or in process_response if we get a 404.)
            if self.should_redirect_with_slash(request):
                path = self.get_full_path_with_slash(request)
            else:
                path = request.get_full_path()

            return self.response_redirect_class(f"{request.scheme}://www.{host}{path}")

以USER_AGENT检查为例,默认情况下Django的配置文件没有设置user-agent黑名单,此时使用curl或者python发起请求接口均可正常返回数据。

  • curl请求接口

  • python requests

如果想要禁止具有特定USERAGENT的请求访问接口,可以在SETTINGS文件中配置黑名单settings.DISALLOWED_USER_AGENTS

DISALLOWED_USER_AGENTS = [
    re.compile("curl"),
    re.compile("python")
]

配置成功后,当请求的user-agent匹配上黑名单的其中一项,请求就会被拒绝,如下图所示:

当然,默认情况下,编译的正则表达式不区分大小写,如果需要区分,可以在编译正则表达式时加上re.IGNORECASE参数。

# User-Agent黑名单,编译过的正则表达式, 同时配置不区分大小写
DISALLOWED_USER_AGENTS = [
    re.compile("curl", re.IGNORECASE),
    re.compile("python", re.IGNORECASE),
    re.compile("PhantomJS", re.IGNORECASE),
    re.compile("selenium", re.IGNORECASE),
    re.compile("java", re.IGNORECASE),

]

当然,如果仅按上述配置设置user agent黑名单,那么随便构造一个user-agent即可绕过该限制,如:

如果具有反爬虫的需求,实现时可能还需要以黑名单结合白名单的方式进行才能达到好的效果。

CsrfViewMiddleware
AuthenticationMiddleware
    def process_request(self, request):
        path = request.path.lstrip("/")
        if (
            self.redirect
            and not request.is_secure()
            and not any(pattern.search(path) for pattern in self.redirect_exempt)
        ):
            host = self.redirect_host or request.get_host()
            return HttpResponsePermanentRedirect(
                "https://%s%s" % (host, request.get_full_path())
            )

自定义中间件-反爬虫

反爬虫通常需要考虑user-agent、ip访问频率等参数。

通过新建并注册一个专门用于判断爬虫特征的中间件并适当给予拦截是接口服务的一项重要需求。

先以简单的ip访问频率限制为例,如果同一个ip前后两次的访问时间差小于某个阈值,我们直接返回响应,提示其应该降低频率,如果确定为恶意爬虫,可以将ip加入黑名单。

import time
from django.utils.deprecation import MiddlewareMixin
from django.http import HttpResponse


class AntiCrawlMiddleware(MiddlewareMixin):
    last_id = None
    last_time = 0

    def process_request(self, request):
        print("反爬虫中间件")

        # request META 'HTTP_USER_AGENT'
        # 针对USER_AGENT的拦截已经在common.py的CommonMiddleware有实现

        # IP访问频率限制
        ip = request.META.get("REMOTE_ADDR")
        now = time.time()
        print(now, ip)
        if ip == AntiCrawlMiddleware.last_id and now - AntiCrawlMiddleware.last_time < 1:
            return HttpResponse("your visit frequency is too high, please try again later.")
        AntiCrawlMiddleware.last_id = ip
        AntiCrawlMiddleware.last_time = now
# 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',
    # 反爬虫中间件
    'middlewares.anticrawl.AntiCrawlMiddleware'
]

服务启用以上中间件后,尝试用脚本对接口进行高频请求,可见确实有限制效果。

注,以上仅为示例代码,实际工程化可以结合缓存数据库存储访问ip相关信息,而非局限于当前ip==上次访问ip的情形。

也有一些专门用于反爬虫的库django-anti-crawler · PyPI

校验COOKIE的值决定是否进行响应

在中间件的process_request方法中, 获取cookie值并进行校验,以下是一个简单的示例例子,只要cookie中不存在token,即认为请求非法,不予返回接口数据。

def process_request(request):
        # Cookie反爬,以token为例
        print("cookies", request.COOKIES)
        token = request.COOKIES.get("token",None)
        print(token)
        if token is None:
            # 如果token为None 可以考虑重定向到某一个页面
            # return redirect("https://www.baidu.com")
            # return HttpResponseRedirect("your visit is illegal.")
            # 示例;直接返回错误响应
            return HttpResponse("your visit is illegal.")

使用postman发送请求(无cookie)

使用postman发送请求(附带cookie)

后台管理用户

创建管理用户,并配置密码(不可为空)

python manage.py createsuperuser

默认django后台使用英文,只需要在settings.py文件修改语言配置即可实现中文的后台管理系统。

对于开发者自定义的app,只要加入settings.py文件中的INSTALLED_APP列表,django就会自动搜索这些app的admin模块,将app纳入后台管理系统

在admin.py中注册Model

后台管理配置项

表名、字段名可读性配置

默认来说,后台会以英文(复数形式)展示Model,如果需要自定义配置,可以在Model类的Meta类中修改单数形式和复数形式的昵称。

支持按字段搜索

需要自定义admin.ModelAdmin的子类,配置search_fields(搜索字段)

更多配置

配置展示的字段、是否以列表形式展示、列表过滤器、排序字段等。

如果对过滤器样式有更多要求,可以参考: How to change the Django admin filter to use a dropdown instead of list? - Stack Overflow

比如,需要下拉式过滤器,可安装扩展django_admin_listfilter_dropdown,在配置文件中配置APP,即可导入相关过滤器使用

from django_admin_listfilter_dropdown.filters import (DropdownFilter, ChoiceDropdownFilter, RelatedDropdownFilter)
class CountyAdmin(admin.ModelAdmin):
    # 配置展示什么字段
    fields = ('provincena', 'city_name', 'name', 'code', 'shape_leng', 'shape_area', 'geo')
    # 以列表的形式展示表数据
    list_display = ['city_name', 'provincena', 'name', 'code', 'shape_area']
    # 可搜索的字段
    search_fields = ('name', 'provincena')
    # 过滤字段: https://docs.djangoproject.com/en/4.2/ref/contrib/admin/filters/
    # list_filter = ('provincena', 'city_name')
    list_filter = (
        ('provincena', DropdownFilter),
        ('city_name', DropdownFilter)
    )
    # 排序字段
    ordering = ("-shape_area",)

models.py中的Model字段属性定义会影响数据在后台管理系统的显示,如verbose_name,浮点类型的显示小数位数等。

禁用删除权限

默认django admin表管理界面可以删除数据.

可以通过重写对应的Admin子类的has_delete_permission方法禁用删除权限。

地理空间要素模型配置地图底图图层

以下为django.contrib.gis.admin提供的默认地理要素管理类,也可以参照其中配置自定义图层服务,如使用高德地图、百度地图。

  • GeoModelAdmin

    默认使用openlayer在线地图服务

    # RemovedInDjango50Warning.
    class GeoModelAdmin(ModelAdmin):
        """
        The administration options class for Geographic models. Map settings
        may be overloaded from their defaults to create custom maps.
        """
    
        # The default map settings that may be overloaded -- still subject
        # to API changes.
        default_lon = 0
        default_lat = 0
        default_zoom = 4
        display_wkt = False
        display_srid = False
        extra_js = []
        num_zoom = 18
        max_zoom = False
        min_zoom = False
        units = False
        max_resolution = False
        max_extent = False
        modifiable = True
        mouse_position = True
        scale_text = True
        layerswitcher = True
        scrollable = True
        map_width = 600
        map_height = 400
        map_srid = 4326
        map_template = "gis/admin/openlayers.html"
        openlayers_url = (
            "https://cdnjs.cloudflare.com/ajax/libs/openlayers/2.13.1/OpenLayers.js"
        )
        point_zoom = num_zoom - 6
        wms_url = "http://vmap0.tiles.osgeo.org/wms/vmap0"
        wms_layer = "basic"
        wms_name = "OpenLayers WMS"
        wms_options = {"format": "image/jpeg"}
        debug = False
        widget = OpenLayersWidget
    
  • OSMModelAdmin

    默认使用OSM在线地图服务

    # RemovedInDjango50Warning.
    class OSMGeoAdmin(GeoModelAdmin):
        map_template = "gis/admin/osm.html"
        num_zoom = 20
        map_srid = spherical_mercator_srid
        max_extent = "-20037508,-20037508,20037508,20037508"
        max_resolution = "156543.0339"
        point_zoom = num_zoom - 6
        units = "m"
    
  • GISModelAdmin

地理空间要素数据多图层叠加管理

尚未查阅,不知是否有相关现成工具。

后台管理界面主题

https://djangopackages.org/grids/g/admin-styling/

simpleui

simpleui: django admin 的一个主题

只需要安装并且在settings文件的INSTALLED_APPS配置即可。

pip install django-simpleui
  # Application definition

  INSTALLED_APPS = [
      'simpleui',
      'django.contrib.admin',
      'django.contrib.auth',
      'django.contrib.contenttypes',
      'django.contrib.sessions',
      'django.contrib.messages',
      'django.contrib.staticfiles',
      ...
  ]

RESTFUL API服务

https://www.django-rest-framework.org

一杯茶的时间,搞懂 RESTful API

Django中可以通过一些扩展库便利地实现RESTful风格的API,比如:

djangorestframework

只需要安装该库并且在配置文件中配置该APP,即可进行RESTful风格的API开发

pip install djangorestframework
INSTALLED_APPS = [
    ...
    'rest_framework',
]

参考文章

Django项目部署

参考文章

基于uwsgi服务器部署

安装uwsgi

conda install -c conda-forge uwsgi
uwsgi --http :8090 --chdir projectpath --module museum_geo_api.wsgi

也可以将wsgi服务的配置写在.ini文件中

[uwsgi]
http=:8001
chdir=/Users/weirdgiser/文稿/Projects/Cultural/musuem_geo_api_dev/museum_geo_api/museum_geo_api
module=museum_geo_api.wsgi

指定.ini文件启动服务

 uwsgi --ini uwsgi.ini  --enable-threads 

基于gunicorn部署

新建配置文件

# 指定服务器监听的IP和端口
bind = "0.0.0.0:8010"
# 指定工作进程的数量
workers = 4
# 指定工作进程使用的协程库
worker_class = "gevent"

# ----应用程序的设置
# 指定为项目根目录
chdir = "/Users/weirdgiser/文稿/Projects/Cultural/musuem_geo_api_dev/museum_geo_api/museum_geo_api"
# 指定WSGI应用程序的模块和应用
module = "museum_geo_api.wsgi:application"

启动服务

gunicorn -c gunicorn_config.py museum_geo_api.wsgi:application

基于waitress部署

轻量级的Web服务器,纯Python编写,多平台适用。

Django用户模型及拓展

django.contrib.auth.models模块中包含django自带的用户模型实现,如:

class AbstractUser(AbstractBaseUser, PermissionsMixin):
    """
    An abstract base class implementing a fully featured User model with
    admin-compliant permissions.

    Username and password are required. Other fields are optional.
    """

    username_validator = UnicodeUsernameValidator()

    username = models.CharField(
        _("username"),
        max_length=150,
        unique=True,
        help_text=_(
            "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
        ),
        validators=[username_validator],
        error_messages={
            "unique": _("A user with that username already exists."),
        },
    )
    first_name = models.CharField(_("first name"), max_length=150, blank=True)
    last_name = models.CharField(_("last name"), max_length=150, blank=True)
    email = models.EmailField(_("email address"), blank=True)
    is_staff = models.BooleanField(
        _("staff status"),
        default=False,
        help_text=_("Designates whether the user can log into this admin site."),
    )
    is_active = models.BooleanField(
        _("active"),
        default=True,
        help_text=_(
            "Designates whether this user should be treated as active. "
            "Unselect this instead of deleting accounts."
        ),
    )
    date_joined = models.DateTimeField(_("date joined"), default=timezone.now)

    objects = UserManager()

    EMAIL_FIELD = "email"
    USERNAME_FIELD = "username"
    REQUIRED_FIELDS = ["email"]

    class Meta:
        verbose_name = _("user")
        verbose_name_plural = _("users")
        abstract = True

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def get_full_name(self):
        """
        Return the first_name plus the last_name, with a space in between.
        """
        full_name = "%s %s" % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        """Return the short name for the user."""
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)


class User(AbstractUser):
    """
    Users within the Django authentication system are represented by this
    model.

    Username and password are required. Other fields are optional.
    """

    class Meta(AbstractUser.Meta):
        swappable = "AUTH_USER_MODEL"

假如我们想快速实现一个用户管理的功能,最快就是在django用户上进行拓展,使用django自带用户的用户、密码等基础属性,拓展新字段如L电话号码、地址、教育程度等属性。

class ExtendedUser(AbstractUser):
    phone = models.CharField(max_length=32, db_comment="电话号码")
    residential_address = models.CharField(max_length=128, db_comment="住宅地址")
    wechat_openid = models.CharField(max_length=128, db_comment="微信公开ID")
    education = models.CharField(max_length=8, db_comment="教育程度")
    graduate_institution = models.CharField(max_length=8, db_comment="毕业院校")
    vip_effective_time = models.DateTimeField(db_comment="VIP有效日期期限")
    user_permissions = models.ManyToManyField(
        Permission,
        verbose_name= '用户权限',
        blank=True,
        help_text= 'Specific permissions for this user.',
    )

    # 对当前表进行相关设置:
    class Meta:
        managed = True
        db_table = 'extended_users'
        verbose_name = '用户基础表'
        verbose_name_plural = verbose_name

定义好模型文件就可以迁移,将表结构物理同步到数据库中

python manage.py makemigrations app_name
python manage.py migrate app_name

【注】默认用户模型的字段不允许为空,需要显式设置null=True表明字段可以为空,另外,如果需要通过django admin管理用户模型,对于允许为空的字段还需要显式设置blank=True,否则后台管理页面的前端会校验字段值要求必填。

Django服务日志记录

  • 通过中间件记录接口请求记录

参考文档

https://docs.djangoproject.com/zh-hans/4.2/topics/logging/

Django默认使用logging模块进行日志记录,可以在settings.py文件中配置LOGGING变量对日志进行配置。如:

# settings.py
# 日志模块配置
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "verbose": {
            "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
            "style": "{",
        },
        "simple": {
            "format": "{levelname} {asctime} {module} {message}",
            "style": "{",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "simple",


        },
        "file": {
            "level": "DEBUG",
            "class": "logging.FileHandler",
            "filename": os.path.join(BASE_DIR, "logs","museum_geo_api.log"),
            "formatter": "verbose",

        },
    },
    "loggers": {
        "django": {
            "handlers": ["console","file"],
            "level": "INFO",
            "propagate": True,
        },
    },
}

通过formatter可以配置日志格式器,handlers配置日志处理类(如流处理StreamHandler、文件处理FileHandler),loggers配置启用的日志记录器。关于这些模块的介绍可以参考博文Python内置日志模块源码分析和进阶用法 | CoolCats

Django接口鉴权

  • API Token

    用户先通过认证接口,提供用户名、密码等认证信息进行认证,若认证通过后服务端向用户返回登录凭证(具有一定有效期);后续的用户请求需要附带该登录凭证方可请求成功。

  • API Key + API Secret

0x02 Django源码分析

源码结构

以Django4.2.7为例,源码目录结构如下图所示:

  • apps

  • conf

  • contrib

  • core

0x03 Django开源项目

django-tenants

djangorestframework

Restful API

GeoDjango

django-rest-framework-jwt

接口鉴权。

Python3 JWT的生成与验证 - 韩志超 - 博客园

0x04 工具

随机密码和密钥生成器 - 一个工具箱 - 好用的在线工具都在这里!

CoolCats
CoolCats
理学学士

我的研究兴趣是时空数据分析、知识图谱、自然语言处理与服务端开发