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
只需要安装并且在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
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
Django核心代码
-
db
-
dispatch
-
forms
-
http
-
middleware
-
template
-
tempatetags
-
test
-
urls
-
utils
-
views
0x03 Django开源项目
django-tenants
djangorestframework
Restful API
GeoDjango
-
获取多边形的中心点:python - Django - Get centroid of polygon in geoJSON format - Stack Overflow
-
空间数据模型相关API:https://docs.djangoproject.com/en/4.2/ref/contrib/gis/db-api/
-
空间数据相关函数:Geographic Database Functions | Django documentation | Django
-
……
django-rest-framework-jwt
接口鉴权。