做者:Matt Laymanweb
翻譯:老齊正則表達式
當用戶訪問網站的時候,實際上是經過瀏覽器向網站發起訪問請求,那麼,網站是如何處理請求的?本文以Django框架爲例進行說明,關於Django更詳細的使用方法,請閱讀《跟老齊學Python:Django實戰(第二版)》。django
來自瀏覽器的HTTP請求包含一個URL,URL描述Django應該生成哪些資源。因爲URL能夠有多種形式,咱們必須把web應用能夠處理的URL類型告訴Django,這就是URL配置的目的。在Django文檔中,URL配置簡稱爲URLconf。編程
URLconf在哪裏?URLconf位於項目設置文件中的ROOT_URLCONF所設置的模塊路徑上。若是運行startproject命令,則該以project.url形式命名路由,其中「project」是項目名稱。換句話說,URLconf就放在project/urls.py
中,它與settings.py
文件在同一個目錄。瀏覽器
知道了文件所在的位置,但並無講它是如何工做的,下面來深刻探索。bash
能夠將URL配置看做URL路徑列表,Django會把這些路徑從上到下進行匹配。當Django找到匹配的路徑時,HTTP請求將轉到與該路徑相關聯的Python代碼塊。這個「Python代碼塊」被稱爲視圖,咱們將在稍後對其進行更多的探討。目前,請相信視圖知道如何處理HTTP請求。微信
咱們可使用一個URLconf示例來實踐上述觀點。app
# project/urls.py
from django.urls import path
from application import views
urlpatterns = [
path("", views.home),
path("about/", views.about),
path("contact/", views.contact),
path("terms/", views.terms),
]
複製代碼
這裏的內容與我上面的描述相吻合,Django對列表中的URL路徑從上到下進行匹配。此列表的關鍵是urlpatterns
。Django將把urlpatterns
變量中的列表視爲URLconf。框架
列表中的順序也很重要,上述示例沒有顯示路徑之間的任何衝突,但能夠建立兩個不一樣的path
,它們能夠匹配同一個URL,在後續內容中,會探究這個問題。函數
咱們能夠經過這個例子來看看它在www.itdiffer.com上是如何運行的。對於URLconf中的URL,Django不使用https://協議、或者主域名(www.itdiffer.com)及斜槓匹配路由,其餘的都是URLconf所要匹配的。
views.about
視圖。views.home
視圖。補充說明:你可能注意到Django URL以斜槓結尾。事實上,若是你嘗試訪問像https://www.itdiffer.com/about這樣的URL,Django會自動附加斜槓,再將請求重定向到相同的URL,這是由於使用了默認設置APPEND_SLASH
,這種是Django在設計理念上的選擇。
path
的做用若是我給出的只是上面的例子,你可能會說:「哇,Django太笨了,爲何urlpatterns不是下面這樣的字典?」
urlpatterns = {
"": views.home,
"about/": views.about,
"contact/": views.contact,
"terms/": views.terms,
}
複製代碼
我不會由於你以上言論而苛責。
緣由是path
的能力更強大,大部分功能都在傳給函數的第一個字符串參數中。path
的字符串部分(例如,「about/」)稱爲路由。
正如你所看到的,路由能夠是一個簡單的字符串,它也能夠包含一些轉換特徵,如此,就能夠從URL中提取信息,並傳給視圖,例如這樣的一個路徑:
path("blog/<int:year>/<slug:slug>/", views.blog_post),
複製代碼
此路徑中的兩個轉換器是:
使用尖括號和一些保留關鍵詞,讓Django對URL進行解析,每一個轉換器都有一些預期的規則要遵循。
考慮到這些轉換器定義,讓咱們與一些URL進行比較!
如今咱們能夠從新審視一下以前的排序問題,以不一樣的順序考慮這兩條路徑:
path("blog/<int:year>/", views.blog_by_year),
path("blog/2020/", views.blog_for_twenty_twenty),
# vs.
path("blog/2020/", views.blog_for_twenty_twenty),
path("blog/<int:year>/", views.blog_by_year),
複製代碼
在第一次排序中,轉換器將匹配blog/以後的任何整數,包括https://www.itdiffer.com/blog/2020/。這意味着第一次排序將永遠不會調用views.blog_for_twenty_twenty
,由於Django按順序匹配路徑。
相反,在第二次排序中,blog/2020/將正確地路由到views.blog_for_twenty_twenty
,由於它首先匹配。這意味着這裏有一個教訓要謹記:
當包含與轉換器範圍匹配的路徑項時,請確保將它們放在更具體的項以後。
轉換器如何處理這些從路徑中捕獲的參數?這很難在不觸及視圖的狀況下解釋,因此要對視圖給予入門介紹,關於視圖更詳細的內容,仍是要閱讀《跟老齊學Python:Django實戰(第二版)》的深刻淺出講解。
視圖的做用是接受請求並返回響應,使用Python的可選類型檢查,下面是一個發送Hello World響應的示例。
from django.http import HttpRequest, HttpResponse
def some_view(request: HttpRequest) -> HttpResponse:
return HttpResponse('Hello World')
複製代碼
Django將用戶的HTTP請求封裝在一個容器類中,這就是HttpRequest。一樣,咱們可使用HttpResponse,以便讓Django將咱們的響應數據轉換爲格式正確的HTTP響應,並將其發送回用戶的瀏覽器。
如今咱們能夠再看一個轉換器。
path("blog/<int:year>/", views.blog_by_year),
複製代碼
路由中有了這個轉換器,blog_by_year
會是什麼樣子?
# application/views.py
from django.http import HttpResponse
def blog_by_year(request, year):
# ... some code to handle the year
data = 'Some data that would be set by code above'
return HttpResponse(data)
複製代碼
Django開始在這裏展現出一些不錯的品質!轉換器爲咱們作了一堆乏味的工做。Django設置的year參數已是一個整數,由於Django進行了字符串解析和轉換。
若是有人提交/blog/not_a_number/
,Django將返回Not Found
響應,由於not_a_number
不是整數。這樣作的好處是,咱們沒必要在blog_by_year
中添加額外的檢查邏輯來處理年份看起來不像數字的奇怪狀況。這種函數能夠節省時間!它使代碼更乾淨,處理更精確。
怎樣理解咱們以前看到的另外一個奇怪的例子/blog/0/life-in-rome/
呢?這將與前面部分中的模式相匹配,但假設咱們但願匹配一個四位數的年份,咱們該怎麼作到呢?可使用正則表達式。
在編程中,正則表達式應用很廣泛,經常被比做電鋸:功能強大得使人難以置信,但若是不當心的話,也能夠砍掉「你的腳」。
正則表達式能夠以很是簡潔的方式表示複雜的關係和匹配模式,這種簡潔性經常給正則表達式帶來難以理解的壞名聲。但若是當心使用,它們能夠是很好的工具。
正則表達式(一般縮寫爲「regex」)適合於匹配字符串中的複雜模式。這聽起來像是要解決前年提到的年份問題!在咱們的問題中,只想匹配一個四位數的整數。讓咱們看看Django的解決方案,而後分解它的含義。
提醒一下,此解決方案將匹配某些URL路徑,如blog/2020/urls-lead-way/。
re_path("^blog/(?P<year>[0-9]{4})/(?P<slug>[\w-]+)/$", views.blog_post),
複製代碼
這個瘋狂的字符串的行爲與前面的例子徹底同樣,只是它把年份精確到四位數,這個瘋狂的字符串被稱爲regex模式。當Django代碼運行時,它將根據此模式中定義的規則測試URL路徑。
要了解它的工做原理,咱們必須知道模式的各個部分意味着什麼,依次解釋:
(?P<year>[0-9]{4})
中,圓括號內的部分是一個捕獲組。?P<year>
是與捕獲組關聯的名稱,相似於<int:year>
這樣的轉換器中的冒號右側部分。該名稱容許Django將名爲year
的參數中的內容傳給視圖。捕獲組的另外一部分[0-9]{4}
是模式實際匹配的內容。[0-9]
是一個字符類,意思是「匹配從0到9的任意數字,{4}表示它必須精確匹配四次。re_path
提供的這種特殊性是int
轉換器所不能作到的!(?P<slug>[\w-]+)
將匹配的內容放入名爲slug
的參數中。[\w-]
包含兩種類型,\w
是指天然語言中的任何單詞字符,另外一種是破折號(注意:英文的,即-
)字符。最後,加號(+
)字符表示字符類必須匹配一次或屢次。/
。$
的做用與^
相反,表示「模式必須在這裏結束」。所以blog/2020/some-slug/another-slug/是不匹配的。祝賀你!這絕對是本文最難的部分。若是你明白咱們對re_path
作了什麼,接下來的內容應該會讓你很舒服。若是沒有,請不要擔憂!若是你想了解有關正則表達式的更多信息,請知曉我在模式中描述的全部內容都不是Django所特有的。相反,這是Python的內置行爲,你能夠在網上搜到不少信息。
知道了re_path
的強大做用,即便你今天不須要它,它也能夠對你之後的Django開發有幫助。
到目前爲止,咱們已經研究了能夠在URLconf中映射的各個路由。若是多個視圖函數使用同一個URL時,咱們能夠怎麼作?爲何?
想象一下你正在建立一個教育項目。在你的項目中,有學校、學生和其餘與教育相關的概念。你能夠這樣作:
# project/urls.py
from django.urls import path
from schools import views as schools_views
from students import views as students_views
urlpatterns = [
path("schools/", schools_views.index),
path("schools/<int:school_id>/", schools_views.school_detail),
path("students/", students_views.index),
path("students/<int:student_id>/", students_views.student_detail),
]
複製代碼
這種方法能夠奏效,可是,若是這樣,根的路由就要遍歷schools_views
和students_views
每一個應用中的視圖。實際上,咱們可使用include來更好地處理這個問題。
# project/urls.py
from django.urls import include
urlpatterns = [
path("schools/", include("schools.urls"),
path("students/", include("students.urls"),
]
複製代碼
而後,在每一個應用中建立urls.py
,並按照以下方式配置該應用的路由:
# schools/urls.py
from django.urls import path
from schools import views
urlpatterns = [
path("", views.index),
path("<int:school_id>/", views.school_detail),
]
複製代碼
include
使每一個Django應用在須要定義的視圖中擁有自主權,項目能夠幸運地「忽視」應用要作的事情。
此外,還刪除了重複的schools/
或students/
。當Django處理一個路由時,它將匹配路由的第一部分,並將其他部分傳遞到在單個應用中定義的URLconf。經過這種方式,URL配置能夠造成一個樹,其中的root URLconf 是全部請求開始的地方,可是當一個請求被轉到相應的應用時,該應用能夠處理細節。
咱們已經研究了用path
、re-path
和include
定義URL的主要方法。還有另一個方面須要考慮,如何在代碼中使用URL?想一想這個(至關愚蠢的)作法:
# application/views.py
from django.http import HttpResponseRedirect
def old_blog_categories(request):
return HttpRequestRedirect("/blog/categories/")
複製代碼
重定向是指用戶試圖訪問一個頁面時,被瀏覽器發送到其餘地方。有比這個例子更好的方法來處理重定向,可是這個例子說明了一個不一樣的觀點。若是你但願從新構造該項目,以便將博客類別從/blog/categories/
改成/marketing/blog/categories/
,會發生什麼狀況?在當前代碼中,咱們必須重寫這個視圖,而且經過路由轉到其餘視圖。
真是浪費時間!Django爲咱們提供解決此問題的方法。能夠在路由中設置每一個URL的名稱,使用path
的name
參數來實現。
# project/urls.py
from django.urls import path
from blog import views as blog_views
urlpatterns = [
...
path("/marketing/blog/categories/",
blog_views.categories, name="blog_categories"),
...
]
複製代碼
這使得blog_categories
成爲/marketing/blog/categories/
的名稱。要使用這個名稱,咱們須要返回到它的對應項。修改後的視圖以下:
# application/views.py
from django.http import HttpResponseRedirect
from django.urls import reverse
def old_blog_categories(request):
return HttpRequestRedirect(reverse("blog_categories"))
複製代碼
reverse
的任務是查找任何url的名稱並返回其對應路徑。這意味着:
reverse("blog_categories") == "/marketing/blog/categories/"
複製代碼
至少持續到你想再次改變它以前。😁
若是你對多個URL取相同的名稱,會發生什麼狀況?例如,index或detail是你可能要應用的常見名稱。咱們能夠向《Python之禪》尋求建議。
若是編程時間不長,命名空間對你來講多是新知識,它們是共享的名稱空間。也許這一點很清楚,但我記得當我第一次開始寫軟件的時候,我還在爲這個概念而掙扎。
咱們用現實中的一類場景來類別,假設你有兩個紅色球、兩個藍色球和分別標有「A」和「B」的兩個桶,把球分別放在兩個桶裏面。若是我想要藍色球,我不能說「請把藍色球給我」,由於那樣會模棱兩可。相反,爲了獲得一個特定的球,我須要說「請給我B桶中的藍色球。」在這個場景中,bucket(桶)是命名空間。
咱們所舉的關於學校和學生的示例,也能夠做爲例子說明這個想法。兩個應用都有一個index視圖函數來表示該應用部分的根(即schools/
和 students/
)。若是想指向某個視圖中的函數,不得不使用views.index
,而後,這樣Django就沒法判斷應該轉向哪一個視圖的index函數。
一種解決方案是經過在名稱前面加上schools_
之類的常見名稱來建立本身的命名空間。這種方法的問題在於URLconf會自我重複。
# schools/urls.py
from django.urls import path
from schools import views
urlpatterns = [
path("", views.index, name="schools_index"),
path("<int:school_id>/", views.school_detail, name="schools_detail"),
]
複製代碼
Django提供了一個替代方法,可讓你保留一個較短的名稱。
# schools/urls.py
from django.urls import path
from schools import views
app_name = "schools"
urlpatterns = [
path("", views.index, name="index"),
path("<int:school_id>/", views.school_detail, name="detail"),
]
複製代碼
經過添加app_name
,咱們向Django發出信號,這些視圖位於一個命名空間中。如今,當咱們想要得到一個URL時,咱們使用命名空間的名稱和URL名稱,並用冒號將它們鏈接起來。
reverse("schools:index") == "/schools/"
複製代碼
這是Django提供的另外一個便利,它使咱們更容易專一於應用的開發。
咱們的URL話題進入尾聲。最後給你們推薦的是一本學習Django的好書《跟老齊學Python:Django實戰(第二版)》。
原文連接:www.mattlayman.com/understand-…
關注微信公衆號:老齊教室。讀深度文章,得精湛技藝,享絢麗人生。