date: 2021-11-24title: Django之URL路由系统 #标题
tags: #标签
categories: python # 分类

这篇文章用于详细学习下Django项目中的路由系统,也就是urls.py文件。

参考:官方文档

URL配置

URL配置(URLconf)就像Django 所支撑网站的目录。它的本质是URL与要为该URL调用的视图函数之间的映射表。你就是以这种方式告诉Django,对于这个URL调用这段代码,对于那个URL调用那段代码…

它的基本格式如下:

  1. from django.conf.urls import url
  2. '''
  3. 循环urlpatterns,找到对应的函数执行,匹配上一个路径就找到对应的函数执行,就不再往下循环了,
  4. 并给函数传一个参数request,和wsgiref的environ类似,就是请求信息的所有内容
  5. '''
  6. urlpatterns = [
  7. url(正则表达式, views视图函数,参数,别名),
  8. ]

注意:Django 2.0版本中的路由系统已经替换成下面的写法,但是django2.0是向下兼容1.x版本的语法的(官方文档):

from django.urls import path


urlpatterns = [
    path('articles/2003/', views.special_case_2003),
    path('articles/<int:year>/', views.year_archive),
    path('articles/<int:year>/<int:month>/', views.month_archive),
    path('articles/<int:year>/<int:month>/<slug:slug>/', views.article_detail),
]

参数说明

  • 正则表达式:一个正则表达式字符串
  • views视图函数:一个可调用对象,通常为一个视图函数或一个指定视图函数路径的字符串
  • 参数:可选的要传递给视图函数的默认参数(字典形式)
  • 别名:一个可选的name参数

正则表达式详解

无名分组

# 如下是基本配置
from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^articles/2003/$', views.special_case_2003), #思考:如果用户想看2004、2005、2006....等,你要写一堆的url吗,是不是在articles后面写一个正则表达式/d{4}/就行啦,网址里面输入127.0.0.1:8000/articles/1999/试一下看看
    url(r'^articles/([0-9]{4})/$', views.year_archive), 
    url(r'^articles/([0-9]{4})/([0-9]{2})/$', views.month_archive), #思考,如果你想拿到用户输入的什么年份,并通过这个年份去数据库里面匹配对应年份的文章,你怎么办?怎么获取用户输入的年份啊,分组/(\d{4})/,一个小括号搞定
    url(r'^articles/([0-9]{4})/([0-9]{2})/([0-9]+)/$', views.article_detail),
]

views.py中视图函数的写法:

# 第一个参数必须是request,后面跟的三个参数是对应着上面分组正则匹配的每个参数的
def article_detail(request,year,month,day):
    return HttpResponse(year+month+day)

注意事项

  • urlpatterns中的元素按照书写顺序从上往下逐一匹配正则表达式,一旦匹配成功则不再继续。
  • 若要从URL中捕获一个值,只需要在它周围放置一对圆括号(分组匹配)。
  • 不需要添加一个前导的反斜杠(也就是写在正则最前面的那个/),因为每个URL 都有。例如,应该是^articles 而不是 ^/articles。
  • 每个正则表达式前面的’r’ 是可选的但是建议加上。
  • ^articles& 以什么结尾,以什么开头,严格限制路径。

无名分组正则使用示例

1、urls.py文件内容如下:

from django.conf.urls import url
from django.contrib import admin
from apps import views  # 将views导入进来

urlpatterns = [
    url(r'^articles/2003/$', views.special_case_2003),
    url(r'^articles/([0-9]{4})/$', views.year_archive),
    url(r'^articles/([0-9]{4})/([0-9]{2})/$', views.month_archive),
    url(r'^articles/([0-9]{4})/([0-9]{2})/([0-9]+)/$', views.article_detail),
]

2、views.py文件内容如下:

from django.shortcuts import render, HttpResponse
import datetime


# Create your views here.

def special_case_2003(request):
    data = '<h1>执行的是 special_case_2003 视图函数<h1/>'
    return HttpResponse(data)


# 位置参数,第一个参数接收的就是无名分组路径中匹配 到的第一个分组的数据,第二个参数接收的就是无名分组路径中匹配到的第二个分组的数据

def year_archive(request, year):
    data = '<h1>执行的是 year_archive 视图函数,访问的年份为:' + year + '<h1/>'
    return HttpResponse(data)


def month_archive(request, year, month):
    data = '<h1>执行的是 month_archive 视图函数,访问的年份为:' + year + ',月份为:' + month + '<h1/>'
    return HttpResponse(data)


def article_detail(request, year, month, num):
    data = '<h1>执行的是 month_archive 视图函数,访问的年份为:' + year + ',月份为:' + month + ', 访问的是第' + num + '篇文章<h1/>'
    return HttpResponse(data)

访问http://127.0.0.1:8000/articles/2003/,返回页面如下:

Django之URL路由系统 - 图1

访问http://127.0.0.1:8000/articles/1999/,返回页面如下:

Django之URL路由系统 - 图2

访问http://127.0.0.1:8000/articles/1889/12/,返回页面如下:

Django之URL路由系统 - 图3

如上所示,返回的哪个内容,匹配的就是哪个函数。

补充说明

# 是否开启URL访问地址后面不为/跳转至带有/的路径的配置项
APPEND_SLASH=True

Django settings.py配置文件中默认没有 APPEND_SLASH 这个参数,但 Django 默认这个参数为 APPEND_SLASH = True。 其作用就是自动在网址结尾加’/‘。其效果就是:我们定义了urls.py:

from django.conf.urls import url
from app01 import views

urlpatterns = [
        url(r'^blog/$', views.blog),
]

访问 http://www.example.com/blog 时,默认将网址自动转换为 http://www.example/com/blog/
如果在settings.py中设置了 APPEND_SLASH=False,此时我们再请求 http://www.example.com/blog 时就会提示找不到页面。

这个没什么大用, APPEND_SLASH的值为True或False都行。

有名分组

其实有名分组和无名分组差不太多,如果了解过python中的re模块的话,理解起来就简单多了。

urls.py文件中的内容改为了如下:

from django.conf.urls import url
from apps import views  # 将views导入进来


urlpatterns = [
    url(r'^articles/2003/$', views.special_case_2003),
    url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
    url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),
    url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<num>[0-9]+)/$', views.article_detail),
]

可以看出来,和re模块的分组语法一致。

views.py文件中的视图函数,是完全可以和无名分组时的代码一样的,如下:

from django.shortcuts import render, HttpResponse
import datetime


# Create your views here.

def special_case_2003(request):
    data = '<h1>执行的是 special_case_2003 视图函数<h1/>'
    return HttpResponse(data)


def year_archive(request, year):
    data = '<h1>执行的是 year_archive 视图函数,访问的年份为:' + year + '<h1/>'
    return HttpResponse(data)


def month_archive(request, year, month):
    data = '<h1>执行的是 month_archive 视图函数,访问的年份为:' + year + ',月份为:' + month + '<h1/>'
    return HttpResponse(data)


def article_detail(request, year, month, num):
    data = '<h1>执行的是 month_archive 视图函数,访问的年份为:' + year + ',月份为:' + month + ', 访问的是第' + num + '篇文章<h1/>'
    return HttpResponse(data)

有名分组注意的点:

  • views.py文件中的视图函数中,形参名称要和urls.py文件中分组命名的名字一样,否则无法正常接收参数;
  • 视图函数必须接收所有分组命名匹配到的值,否则会报错500错误代码;
  • 视图函数中的形参名称位置可以混乱,不用按照urls.py文件中匹配顺序来接收,只要名字可以对应上即可。

默认值

# urls.py中 
from django.conf.urls import url
from django.contrib import admin
from apps import views  # 将views导入进来

urlpatterns = [
    url(r'^blog/$', views.page),
    url(r'^blog/page(?P<num>[0-9]+)/$', views.page),
]

# views.py中,可以为num指定默认值 
from django.shortcuts import render, HttpResponse

# 设置num的默认值为1
def page(request, num=1):
    return HttpResponse(num)

访问http://127.0.0.1:8000/blog/,返回的是num设置的默认值:

Django之URL路由系统 - 图4

访问http://127.0.0.1:8000/blog/page123/,返回的是匹配到的数值:

Django之URL路由系统 - 图5

url路由分发:include

在工作中,有些项目并不是只有一个应用,比如京东,可以通过在首页点击,跳转到不同的域名(品牌闪购、拍卖)等,这种域名都不同的,表示的就是不同的应用,那么一个项目是怎么做到引入多个应用呢?这就是下面要说的url路由分发:include。

首先自行创建一个项目(sec_pro)以及两个应用(app01和app02),目录结构如下:

Django之URL路由系统 - 图6

然后分别在两个应用目录下新建urls.py文件,然后下面是所有目录下的urls.py文件的内容:

############### $PROJECT_HOME/$PROJECT_NAME/urls.py 文件内容
from django.conf.urls import url, include
from app01 import views  # 之所以要导入app01的views文件,是因为项目首页写在app01.views中

'''
注意:Django匹配到正则中的内容后,再通过include去找其他urls.py文件时,会自动将本文件中匹配到的内容删除掉.
例如,用户访问的是:http://127.0.0.1:8000/app01/index/ 这个路径,
那么在这里匹配   url(r'^app01/', include('app01.urls')) 这个规则后,
Django会去加载app01/urls.py文件 ,然后在app01/urls.py文件中进行匹配时,
用户访问的路径就变成了:http://127.0.0.1:8000/index/
'''
urlpatterns = [
    url(r'^app01/', include('app01.urls')),  # 如果域名后面匹配到 app01开头的路径,就去加载app01/urls.py文件
    url(r'^app02/', include('app02.urls')),  # 如果域名后面匹配到 app02开头的路径,就去加载app02/urls.py文件
    url(r'^$', views.base),  # 如果域名后面为空,则表示访问的是项目首页,直接去找app01.views.py文件(因为在上面from app01 import views进行导入了)
]





############### app01/urls.py 文件内容
from django.conf.urls import url
from app01 import views

'''
由于用户在访问http://127.0.0.1:8000/app01/index/ 时,
项目目录下的urls.py文件已经匹配了然后转发到这里,
也就是说,这里需要匹配的url就是:http://127.0.0.1:8000/index/,
因为项目目录下的urls.py文件匹配后会自动删除匹配的内容
'''
urlpatterns = [
    url(r'^index/', views.index),
    url(r'^$', views.home_page)
]






############### app02/urls.py 文件内容
from django.conf.urls import url
from app02 import views

urlpatterns = [
    url(r'^index', views.index),
    url(r'^$', views.home_page)
]

上面是项目中所有urls.py文件的内容,那么下面来看看views.py文件的内容:

############### app01/views.py文件内容如下:
from django.shortcuts import render

# views这里只需要正常写处理请求的逻辑即可

# 访问 http://127.0.0.1:8000/app01/index/ 执行此函数
def index(request):
    return render(request, 'app01.html')

# 访问 http://127.0.0.1:8000/app01/index/ 执行此函数
def home_page(request):
    return render(request, 'app01_home.html')

# 访问 http://127.0.0.1:8000 执行此函数(在项目目录下的urls.py文件中定义的)
def base(request):
    return render(request, 'home.html')






############### app02/views.py文件内容如下:
from django.shortcuts import render


def index(request):
    return render(request, 'app02.html')


def home_page(request):
    return render(request, 'app02_home.html')

接下来,进行访问测试(自行将相应的html文件放到 templates目录下):

1、访问 http://127.0.0.1:8000/,页面内容如下:

Django之URL路由系统 - 图7

2、访问 http://127.0.0.1:8000/app01/index/,页面内容如下:

Django之URL路由系统 - 图8

3、访问 http://127.0.0.1:8000/app01/,页面内容如下:

Django之URL路由系统 - 图9

4、访问 http://127.0.0.1:8000/app02/,页面内容如下:

Django之URL路由系统 - 图10
5、访问 http://127.0.0.1:8000/app02/index/,页面内容如下:

Django之URL路由系统 - 图11

命名URL(别名)和URL的反向解析

先来一段官方解释:

Django之URL路由系统 - 图12

Django之URL路由系统 - 图13

简单来说就是可以给我们的URL匹配规则起个名字,一个URL匹配模式起一个名字。这样我们以后就不需要写死URL代码了,只需要通过名字来调用当前的URL。当然,也仅限于代码项目内部通过别名调用url,客户端访问的话,还是需要访问实际的url。

就用我书籍管理系统的代码来说,如下:

定义别名

# urls.py文件内容

from django.urls import path
from Books_Manager import views
from django.urls import re_path  # Django 2.0以后的版本需要通过re_path在路由系统中使用正则匹配

# 路由系统
urlpatterns = [
    # name是给当前路由设置个别名,这样如果前面的匹配变了,
    # 但代码内部照样可以通过原来的别名匹配到这个路由
    path('index/', views.Show_and_add_books.as_view(), name='show_books'),
    path('addbook/', views.addbook, name='add_book'),
    re_path('editbook/(\d+)/', views.Edit_book.as_view(), name='edit_book'),
    re_path('deletebook/(\d+)/', views.Delete_book.as_view(), name='delete_book'),
]

可以看到上面的代码中,给每一个路由都定义了一个name值,这就是别名。

现在来看看我在项目代码中是如何使用这些别名的,也就是“反向解析”。

视图中使用别名

# views.py文件内容(下面是部分代码)

    def post(self, request):
        book_name = request.POST.get('book_name')
        book_price = request.POST.get('book_price')
        book_press = request.POST.get('book_press')
        book_date = request.POST.get('book_date')
        book_obj = models.books.objects
        book_obj.create(
            book_name=book_name,
            price=book_price,
            press=book_press,
            date=book_date
        )

        # 上面的代码忽略,这里是和反向解析有关的,reverse用于解析url的别名,
        # 其实可以省略reverse,redirect的源码中已经尝试帮我们reverse了,后面就都不写reverse了哈
        return redirect(reverse('show_books'))   # 相当于重定向到 /index


# 上面是不带参数的方式使用别名,如果想要带参数的使用别名,可以通过如下语法:
print(reverse('delete_book',args=(71,)))   # 相当于重定向到/deletebook/71/

模板中使用别名

    {% url 'abook' %}    # abook 是urls.py中定义的别名,这种是不带参数的写法
    {% url 'delete_book' book.id %}  # 带参数的方式,book.id 作为参数传给路由系统,表示访问的是 /deletebook/{book.id的实际值}/

上面提到的图书管理系统中,模板中就使用到了别名,如下:

Django之URL路由系统 - 图14