C語言中文網 目錄

Python urllib.request模塊讀取資源用法詳解

在 urllib.request 子模塊下包含了一個非常實用的urllib.request.urlopen(url, data=None) 方法,該方法用于打開 url 指定的資源,并從中讀取數據。根據請求 url 的不同,該方法的返回值會發生動態改變。如果 url 是一個 HTTP 地址,那么該方法返回一個 http.client.HTTPResponse 對象。

例如如下程序:
from urllib.request import *

# 打開URL對應的資源
result = urlopen('http://www.crazyit.org/index.php')
# 按字節讀取數據
data = result.read(326)
# 將字節數據恢復成字符串
print(data.decode('utf-8'))

# 用context manager來管理打開的URL資源
with urlopen('http://www.crazyit.org/index.php') as f:
    # 按字節讀取數據
    data = f.read(326)
    # 將字節數據恢復成字符串
    print(data.decode('utf-8'))
上面程序都是向 http://www.crazyit.org/index.php 發送請求,并請求下載該頁面的內容。只不過第一個示例程序是直接獲取 http://www.crazyit.org/index.php 資源的數據;第二個示例程序則使用 context manager 來管理通過 urlopen 打開的資源。

運行上面程序,可以看到程序分兩次獲取并輸出了 http://www.crazyit.org/index.php 頁面內容。

在使用 urlopen() 函數時,可以通過 data 屬性向被請求的 URL 發送數據。例如如下程序:
from urllib.request import *

# 向https://localhost/cgi-bin/test.cgi發送請求數據
#with urlopen(url='https://localhost/cgi-bin/test.cgi',
with urlopen(url='http://localhost:8888/test/test',    #①
    data='測試數據'.encode('utf-8')) as f:
    # 讀取服務器全部響應
    print(f.read().decode('utf-8'))
上面程序為 data 屬性指定了一個 bytes 字節數據,該字節數據會以原始二進制流的方式提交給服務器。

上面程序需要在本地(localhost)部署一個 Web 應用,該程序對應的服務器端所使用的 CGI 代碼為:

#!/usr/bin/env python
import sys
data = sys.stdin.read()
print('Content-type: text/plain\n\nGot Data:"%s"' % data)

運行上面程序,可以看到如下輸出結果:

Got Data:"測試數據"


如果使用 urlopen() 函數向服務器頁面發送 GET 請求參數,則無須使用 data 屬性,直接把請求參數附加在 URL 之后即可。例如如下代碼:
import urllib.parse
params = urllib.parse.urlencode({'name': 'fkit', 'password': '123888'})
# 將請求參數添加到URL的后面
url = 'http://localhost:8888/test/get.jsp?%s' % params
with urlopen(url=url) as f:
    # 讀取服務器全部響應
    print(f.read().decode('utf-8'))
正如程序中第 4 行代碼所看到的,程序在發送 GET 請求參數時,只要將請求參數附加到 URL 的后面即可(上面程序同樣需要服務器的支持)。

運行上面程序,可以看到如下輸出結果:

name 參數:fkit
password 參數:123888


如果想通過 urlopen() 函數發送 POST 請求參數,則同樣可通過 data 屬性來實現。例如如下代碼:
import urllib.parse
params = urllib.parse.urlencode({'name': '瘋狂軟件', 'password': '123888'})
params = params.encode('utf-8')
# 使用data指定請求參數
with urlopen("http://localhost:8888/test/post.jsp", data=params) as f:
    print(f.read().decode('utf-8'))
從上面第 5 行代碼可以看到,如果要向指定地址發送 POST 請求,那么通過 data 指定請求參數即可。

實際上,使用 data 屬性不僅可以發送 POST 請求,還可以發送 PUT、PATCH、DELETE 等請求,此時需要使用 urllib.request.Request 來構建請求參數。程序使用 urlopen() 函數打開遠程資源時,第一個 url 參數既可以是 URL 字符串,也可以使用 urllib.request.Request 對象。

urllib.request.Request 對象的構造器如下:

urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)

從該構造器可以看出,使用 Request 可以通過 method 指定請求方法,也可以通過 data 指定請求參數,還可以通過 headers 指定請求頭。

下面代碼示范了如何通過 Request 對象來發送 PUT 請求:
from urllib.request import *

params = 'put請求數據'.encode('utf-8')
# 創建Request對象,設置使用PUT請求
req = Request(url='http://localhost:8888/test/put',
    data=params, method='PUT')
with urlopen(req) as f:
    print(f.status)
    print(f.read().decode('utf-8'))
正如從代碼中所看到的,程序在創建 Request 對象時通過 method 指定使用 PUT 請求方式,這意味著程序會發送 PUT 請求。

正如剛剛所提到的,程序也可以使用 Request 對象來添加請求頭。例如如下代碼:
# 創建Request對象
req = Request('http://localhost:8888/test/header.jsp')
# 添加請求頭
req.add_header('Referer', 'http://www.crazyit.org/')
with urlopen(req) as f:
    print(f.status)
    print(f.read().decode('utf-8'))
程序通過 Request 的 add_header() 方法添加了一個 Referer 請求頭,服務器端的處理程序將可以讀到此處添加的請求頭。

通過 urlopen() 函數打開遠程資源之后,也可以非常方便地讀取遠程資源(甚至實現多線程下載)。如下程序實現了一個多線程下載的工具類:
from urllib.request import *
import threading

class DownUtil:
    def __init__(self, path, target_file, thread_num):
        # 定義下載資源的路徑
        self.path = path
        # 定義需要使用多少個線程下載資源
        self.thread_num = thread_num
        # 指定所下載的文件的保存位置
        self.target_file = target_file
        # 初始化threads數組
        self.threads = []
    def download(self):
        # 創建Request對象
        req = Request(url=self.path, method='GET')
        # 添加請求頭
        req.add_header('Accept', '*/*')
        req.add_header('Charset', 'UTF-8')
        req.add_header('Connection', 'Keep-Alive')
        # 打開要下載的資源
        f = urlopen(req)
        # 獲取要下載的文件大小
        self.file_size = int(dict(f.headers).get('Content-Length', 0))
        f.close()
        # 計算每個線程要下載的資源大小
        current_part_size = self.file_size // self.thread_num + 1
        for i in range(self.thread_num):
            # 計算每個線程下載的開始位置
            start_pos = i * current_part_size
            # 每個線程使用一個wb模式打開的文件進行下載
            t = open(self.target_file, 'wb')
            # 定位該線程的下載位置
            t.seek(start_pos, 0);
            # 創建下載線程
            td = DownThread(self.path, start_pos, current_part_size, t)
            self.threads.append(td)
            # 啟動下載線程
            td.start()
    # 獲取下載的完成百分比
    def get_complete_rate(self):
        # 統計多個線程已經下載的總大小
        sum_size = 0
        for i in range(self.thread_num):
            sum_size += self.threads[i].length
        # 返回已經完成的百分比
        return sum_size / self.file_size
class DownThread(threading.Thread):
    def __init__(self, path, start_pos, current_part_size, current_part):
        super().__init__()
        self.path = path
        # 當前線程的下載位置
        self.start_pos = start_pos
        # 定義當前線程負責下載的文件大小
        self.current_part_size = current_part_size
        # 當前線程需要下載的文件塊
        self.current_part = current_part
        # 定義該線程已下載的字節數
        self.length = 0
    def run(self):
        # 創建Request對象
        req = Request(url = self.path, method='GET')
        # 添加請求頭
        req.add_header('Accept', '*/*')
        req.add_header('Charset', 'UTF-8')
        req.add_header('Connection', 'Keep-Alive')
        # 打開要下載的資源
        f = urlopen(req)
        # 跳過self.start_pos個字節,表明該線程只下載自己負責的那部分內容
        for i in range(self.start_pos):
            f.read(1)
        # 讀取網絡數據,并寫入本地文件
        while self.length < self.current_part_size:
            data = f.read(1024)
            if data is None or len(data) <= 0:
                break
            self.current_part.write(data)
            # 累計該線程下載的總大小
            self.length += len(data)
        self.current_part.close()
        f.close()
程序中定義了 DownThread 線程類,該線程類負責讀取從 start_pos 開始、長度為 current_part_size 的所有字節數據,并寫入本地文件對象中。DownThread 線程類的 run() 方法就是一個簡單的輸入/輸出實現。

程序中 DownUtils 類的 download() 方法負責按如下步驟來實現多線程下載:
  1. 使用 urlopen() 方法打開遠程資源。
  2. 獲取指定的 URL 對象所指向資源的大小(通過 Content-Length 響應頭獲取)。
  3. 計算每個線程應該下載網絡資源的哪個部分(從哪個字節開始,到哪個字節結束)。
  4. 依次創建并啟動多個線程來下載網絡資源的指定部分。

上面程序已經實現了多線程下載的核心代碼,如果要實現斷點下載,則需要額外增加一個配直文件(讀者可以發現,所有的斷點下載工具都會在下載開始時生成兩個文件:一個是與網絡資源具有相同大小的空文件;一個是配置文件),該配置文件分別記錄每個線程已經下載到哪個字節,當網絡斷開后再次開始下載時,每個線程根據配置文件中記錄的位置向后下載即可。


有了上面的 DownUtil 工具類之后,接下來就可以在主程序中調用該工具類的 download() 方法執行下載,如下面的程序所示:
from DownUtil import *

du = DownUtil("http://www.crazyit.org/data/attachment/" \
    + "forum/201801/19/121212ituj1s9gj8g880jr.png", 'a.png', 3)
du.download()
def show_process():
    print("已完成:%.2f" % du.get_complete_rate())
    global t
    if du.get_complete_rate() < 1:
        # 通過定時器啟動0.1之后執行show_process函數
        t = threading.Timer(0.1, show_process)
        t.start()
# 通過定時器啟動0.1之后執行show_process函數
t = threading.Timer(0.1, show_process)
t.start()
運行上面程序,即可看到程序從 www.crazyit.org 下載得到一個名為 a.png 的圖片文件。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo