如何更好地結構化表示一個 URL?

  • 2019 年 10 月 11 日
  • 筆記

相信各位 Python 開發者都用過 Requests 庫,有些朋友還用過 WebSockets 庫。這裡回顧一下它們的基本用法,例如使用 Requests 庫向目標網站發出 GET 請求:

import requests      url = "https://www.baidu.com"  resp = requests.get(url)  print(resp.status_code)  # output -> 200

使用起來非常簡單,我們很輕鬆地向目標網站發出了請求並列印輸出響應狀態碼。當然,你還可以把它縮短:

import requests      print(requests.get("https://www.baidu.com").status_code)  # output -> 200

怎麼寫出更短的程式碼並不是這次要討論的話題。今天我們來研究一下:運行程式碼的電腦是如何找到目標伺服器的?

顯然,你的第一映象是 IP 地址和埠號。

沒錯,就是 IP 地址和埠號。

但你明明輸入的是 URL 地址,怎麼就 IP + 埠號呢?

URL 解析的原因

一下子你也回答不上來吧?

我們可以將上方程式碼的邏輯,即電腦向目標伺服器發出請求並拿到響應資訊的過程抽象成下圖:

程式輸入的是 https://www.baidu.com,但最終要解析出具體的 IP 地址和埠號才能訪問,例如 183.232.231.172:443

網路交互實際上屬於 Socket 編程的範疇,無論是 Requests 還是 WebSockets 庫,最終都會通過 Socket 與目標網站的伺服器進行交互。

而 Socket 編程中並不能直接使用域名,而是採用 IP + 埠號這種形式進行定址的。

假設你現在需要編寫一個網路請求庫,有可能是 HTTP 協議的,也有可能是 WebSocket 協議的。

你要解決的第一個問題就是解析 URL,將網址轉換成 IP + 埠號,甚至還需要分割出協議類型、資源路徑以及是否採用更安全的傳輸方式等。

URL 解析格式

以 WebSocket 協議方面的客戶端庫為例,在雙端確認連接之前有一個「握手」的過程,這個過程之前已經需要雙端的 IP 和埠號等資訊了。下面的程式碼描述了 WebSocket 發出「握手」請求之前,雙端建立連接時需要用到的基本資訊:

 # aiowebsocket   reader, writer = await asyncio.open_connection(host=host, port=port, ssl=ssl)

也就是 hostportssl

大部分的 WebSocket 服務給出的都是域名,例如 wss://echo.websocket.org。「握手」時還會用到資源路徑。

接下來,我們來嘗試一下,如何將域名轉換為 IP + 埠號和 is ssl 這樣的格式。

程式碼實現 URL 解析

開始之前,我們先規劃一下基本步驟:

然後確定要使用的標準庫:解析 URL 當然要用到 urllib 庫中的 url parse;解析 address 則需要用到 socket 庫;為了方面取數據,可以嘗試使用 collections 庫中的 namedtuple。

首先引入這幾個庫:

import socket  from collections import namedtuple  from urllib.parse import urlparse

然後定義輸出結構,對應程式碼如下:

REMOTE = namedtuple('REMOTE', ['scheme', 'hostname', 'address', 'port', 'resource', 'ssl'])

然後定義一個方法,我們傳入 URL,獲得解析好的 REMOTE 對象。方法定義如下:

def parses(url: str) -> REMOTE:      pass

待會我們在 pass 處編寫屬於該方法的程式碼。

最開始要解析 URL,獲得 schemehostname,對應程式碼如下:

url = urlparse(url)

urlparse 方法會返回一個 ParseResult 對象,對象大體格式如下:

ParseResult(scheme='wss', netloc='echo.websocket.org', path='', params='', query='', fragment='')

有了 schemehostname 後,就可以得到 portis ssladdress。對應程式碼如下:

scheme = url.scheme  address = url.hostname  port = url.port or (443 if scheme == 'wss' else 80)  ssl = True if scheme == 'wss' else False

WebSocket 協議中只有兩種協議頭:wswss。它們對應的埠分別是 80443,這裡藉助 scheme 的值進行判斷即可得到答案。同理,也直接得到了 is ssl 答案。

拿到 hostname 後,調用 socket 庫的 getbyhostname 方法就能夠得到目標伺服器的 IP 地址了。對應程式碼如下:

address = socket.gethostbyname(hostname)

至於資源路徑,它早已存在於 ParseResult 對象中,直接取出即可:

resource = url.path

要注意的是,有些 URL 中還會攜帶請求正文(即參數和值)。所以這裡需要取 query,並將其拼接到 resource 中:

if url.query:      resource += '?' + url.query

至此,我們已經拿到了所需的所有數據。

現在將它們裝在到 REMOTE 結構中,返回給調用方:

return REMOTE(scheme, hostname, address, port, resource, ssl)

此時,調用 parses 方法後就會拿到 REMOTE 結構,它的取值方式很舒服,用 . 符號取值即可。例如:

res = parses("ws://echo.websocket.org?sign=i9878")  print(res.address, res.port, res.resource)

程式碼運行結果如下:

174.129.224.73  80  ?sign=i9878

這樣,我們就完成了 URL 解析的程式碼編寫。

小結

程式碼雖然不多,邏輯也並不複雜。但我們完整實現了網路請求庫中的 URL 解析模組,這代表著完成了編寫庫的基石之一。

在這個過程當中,我們了解到雙端通訊的基本過程和要用到的資訊。在編碼中學會了如何將 urlparsesocketnamedtuple 結合到一起。

而且,你今天學到了 namedtuple 這個新姿勢!

「你好騷啊.gif」

完整程式碼可在我的 Github 倉庫查看:https://github.com/asyncins/CFA/tree/master/FightingCoder