2017年6月15日 星期四

MicroPython on ESP8266 (十一) : urllib.urequest 模組測試

做完 socket 模組測試後, 我在 Oreilly 出版的 "精通 Python" 這本書的第九章看到 Python 內建模組 urllib 的介紹, 可以使用 urllib.request 類別來處理 HTTP 請求, 這比用 socket 要簡單, 因為不需要去操作 socket 與處理 HTTP 協定格式.

原來 Python 3 整合原本 Python 2 零散的用戶端與伺服器模組, 在標準函式庫中提供 http 與 urllib 模組. 其中 http 模組負責用戶端與伺服端的 HTTP 協定操作 (包括 cookie 管理); 而 urllib 模組負責請求 (urllib.request) 與回應 (urllib.response) 之處理, 以及 URL 的拆解.

可惜 MicroPython 沒有實作 http 模組, 但有實作 urllib 模組處理請求的部分, 不過模組名稱是 urllib.ureqest 而非 urllib.request (不會 fall-back).

>>> import urllib
>>> import http
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: no module named 'http'

雖然 urllib.ureqest 實作的功能不多, 但也足夠使用了, 相關文件可參考 CPython 的 urllib.request 模組說明 :

https://docs.python.org/3.5/library/urllib.request.html#module-urllib.request
# HOWTO Fetch Internet Resources Using The urllib Package

本系列測試紀錄前十篇參考 :

MicroPython on ESP8266 (二) : 數值型別測試
MicroPython on ESP8266 (三) : 序列型別測試
MicroPython on ESP8266 (四) : 字典與集合型別測試
MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試
MicroPython on ESP8266 (六) : 檔案系統測試
MicroPython on ESP8266 (七) : 時間日期測試
MicroPython on ESP8266 (八) : GPIO 測試
MicroPython on ESP8266 (九) : PIR 紅外線移動偵測
MicroPython on ESP8266 (十) : socket 模組測試

MicroPython 文件參考 :

MicroPython tutorial for ESP8266  (官方教學)
http://docs.micropython.org/en/latest/micropython-esp8266.pdf
http://docs.micropython.org/en/latest/pyboard/library/usocket.html#class-socket
http://docs.micropython.org/en/v1.8.7/esp8266/library/usocket.html#module-usocket
https://gist.github.com/xyb/9a4c8d7fba92e6e3761a (驅動程式)
使用 urllib.urequest 模組要先匯入 :

import urllib.urequest

或者取個別名以減少輸入時間 :

import urllib.urequest as u

這樣就可以呼叫模組的方法 urlopen() 來開啟遠端 URL 資源, 其 API 如下 :

urllib.urequest.urlopen(url[, data][, timeout])

其中可有可無的第二參數 data 是要傳送給伺服器的資料字串, 如果有傳送 data 的話就用 POST 方法提出請求; 否則就是用 GET 方法. urlopen() 方法會傳回一個 socket 物件, 這樣就可以用 socket() 物件的 read() 方法來讀取 HTTP 回應內容, 例如讀取 MicroPython 測試網頁 :

>>> import urllib.urequest as u
>>> s=u.urlopen('http://micropython.org/ks/test.html')  #以 GET 方法提出請求
>>> type(s)
<class 'socket'>      #傳回值是 socket 物件
>>> s
<socket state=3 timeout=-1 incoming=3fffa058 off=0>
>>> s.status      #沒有實作 HTTP 回應狀態碼
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'socket' object has no attribute 'status'
>>> s.getheader('Content-Type')    #沒有實作 getheader()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'socket' object has no attribute 'getheader'
>>> s.getheaders()      #沒有實作 getheaders()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'socket' object has no attribute 'getheaders'
>>> r=s.read()    #讀取 TCP 串流緩衝區 (傳回 bytes 型態)
>>> s
<socket state=3 timeout=-1 incoming=0 off=0>      #讀取過後 incoming 歸零
>>> type(r)
<class 'bytes'>            #傳回 bytes 型態
>>> r          #二進位 (byte) 資料
b'<!DOCTYPE html>\n<html lang="en">\n    <head>\n        <title>Test</title>\n    </head>\n    <body>\n        <h1>Test</h1>\n        It\'s working if you can read this!\n    </body>\n</html>\n'
>>> r=s.read()      #讀過之後緩衝區已清空, 再次 read() 無內容
>>> r
b''

上面 socket.read() 傳回的 bytes 串流可以用 decode('utf-8') 方法轉成 utf-8 編碼的字串, 然後再用字串的 split() 方法以 '\n' 為界拆分字串為串列型態, 這樣就可以用 for 迴圈印出 HTML 格式了 :

>>> import urllib.urequest as u
>>> s=u.urlopen('http://micropython.org/ks/test.html')
>>> r=s.read()
>>> r.decode("utf-8")
'<!DOCTYPE html>\n<html lang="en">\n    <head>\n        <title>Test</title>\n    </head>\n    <body>\n        <h1>Test</h1>\n        It\'s working if you can read this!\n    </body>\n</html>\n'
>>> html=r.decode('utf-8').split('\n')
>>> html
['<!DOCTYPE html>', '<html lang="en">', '    <head>', '        <title>Test</title>', '    </head>', '    <body>', '        <h1>Test</h1>', "        It's working if you can read this!", '    </body>', '</html>', '']
>>> for line in html:
...     print(line)
...
...
...
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Test</title>
    </head>
    <body>
        <h1>Test</h1>
        It's working if you can read this!
    </body>
</html>

>>>

以上是 urllib.urequest 模組處理 GET 請求的方法, 不過由於 MicroPython 測試網頁只是單純的靜態網頁, 無法測試帶參數的 GET 請求. 測試含有參數的 GET 可以利用 httpbin.org 的主機所提供的服務, 其 GET 測試網址為 :

http://httpbin.org/get

此網頁會回應客戶端所傳送的參數 (args) 與請求標頭資訊, 如果在瀏覽器輸入如下網址傳遞 a=1 與 b=2 參數, 伺服器回應如下  :

http://httpbin.org/get?a=1&b=2

{
  "args": { 
    "a": "1", 
    "b": "2"
  }, 
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 
    "Accept-Encoding": "gzip, deflate, sdch", 
    "Accept-Language": "zh-TW,zh;q=0.8,en-US;q=0.6,en;q=0.4", 
    "Cache-Control": "max-stale=0", 
    "Connection": "close", 
    "Cookie": "_gauges_unique_day=1; _gauges_unique_month=1; _gauges_unique_year=1; _gauges_unique=1", 
    "Host": "httpbin.org", 
    "Upgrade-Insecure-Requests": "1", 
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 OPR/45.0.2552.888", 
    "X-Bluecoat-Via": "e850b88f17ae9572"
  }, 
  "origin": "210.88.213.252", 
  "url": "http://httpbin.org/get?a=1&b=2"
}

而在 MicroPython 輸入如下指令 :

import urllib.urequest as u
url='http://httpbin.org/get?a=1&b=2'
s=u.urlopen(url)
r=s.read()
lines=r.decode('utf-8').split('\n')
for line in lines:
    print(line)

結果如下 :

MicroPython v1.9.1-8-g7213e78d on 2017-06-12; ESP module with ESP8266
Type "help()" for more information.
>>> import urllib.urequest as u
>>> url='http://httpbin.org/get?a=1&b=2'
>>> s=u.urlopen(url)
>>> r=s.read()
>>> r
b'{\n  "args": {\n    "a": "1", \n    "b": "2"\n  }, \n  "headers": {\n    "Connection": "close", \n    "Host": "httpbin.org"\n  }, \n  "origin": "223.138.76.144", \n  "url": "http://httpbin.org/get?a=1&b=2"\n}\n'
>>> lines=r.decode('utf-8').split('\n')    #格式化
>>> for line in lines:
...     print(line)
...
...
...
{
  "args": {
    "a": "1",
    "b": "2"
  },
  "headers": {
    "Connection": "close",
    "Host": "httpbin.org"
  },
  "origin": "223.138.76.144",
  "url": "http://httpbin.org/get?a=1&b=2"
}

>>>

可見伺服器確實有收到所傳遞之參數.

測試 POST 請求可以用 httpbin.org 主機的 POST 測試網址 :

http://httpbin.org/post

在 MicroPython 輸入下列指令, 當 urlopen() 含有第二參數時, 將會以 POST 方法提出請求. 注意, 這裡傳遞的參數須以 dict 形式的字串呈現, 不可直接用 dict 物件, 否則會產生 "TypeError: object with buffer protocol required" 的錯誤訊息.

import urllib.urequest as u
url='http://httpbin.org/post'
data="{'a':'1','b':'2'}"
s=u.urlopen(url,data)
r=s.read()
lines=r.decode('utf-8').split('\n')
for line in lines:
    print(line)

結果如下 :

MicroPython v1.9.1-8-g7213e78d on 2017-06-12; ESP module with ESP8266
Type "help()" for more information.
>>> import urllib.urequest as u
>>> url='http://httpbin.org/post'
>>> data="{'a':'1','b':'2'}"             #必須是字串
>>> s=u.urlopen(url,data)
>>> r=s.read()
>>> lines=r.decode('utf-8').split('\n')
>>> for line in lines:
...     print(line)
...
...
...
{
  "args": {},
  "data": "{'a':'1','b':'2'}",
  "files": {},
  "form": {},
  "headers": {
    "Connection": "close",
    "Content-Length": "17",
    "Host": "httpbin.org"
  },
  "json": null,
  "origin": "223.198.79.144",
  "url": "http://httpbin.org/post"
}

可見所傳遞的參數是放在 data 屬性中, 而記錄 GET 參數的 args 為空.

參考 :

# Henry's HTTP Post Dumping Server
http://posttestserver.com/post.php
# Testing multipart/form-data uploads with Post Test Server
# MICROPYTHON ON THE ESP8266: KICKING THE TIRES (socket 部分)

沒有留言 :