###### tags: `MicroPython` `OpenAI` `json` # 在 MicroPython 中使用 HTTP Post 傳送中文--以 OpenAI API 為例 在 MicroPython 中利用 HTTP POST 傳送中文如果不注意會出錯, 本文以 OpenAI API 為例。OpenAI 雖然提供有 [Python 的官方套件](https://github.com/openai/openai-python), 不過如果你是要在 MicroPython 中使用 OpeAnAI 的 API, 並不能直接套用, 這時就要回歸到 OpenAI API 最根本的 HTTP Post API 了。 ## 最簡單的 OpenAI API OpenAI 是透過 HTTP Post 提供服務, 以聊天為例, 的是 [ChatCompletion](https://platform.openai.com/docs/api-reference/chat/create) 服務: - API 進入點為 https://api.openai.com/v1/chat/completions - 資料的傳遞都是 JSON 格式 - HTTP 表頭中要以 Bearer 方式透過金鑰認證身分 因此, 最簡單的 OpenAI HTTP Post API 的程式就像是這樣: ```python import requests API_KEY = '你的 OpenAI API 金鑰' response = requests.post( 'https://api.openai.com/v1/chat/completions', headers = { 'Authorization': 'Bearer ' + API_KEY }, json = { 'model': 'gpt-3.5-turbo', "messages": [{"role": "user", "content": "你好"}]} ) print(response.status_code) print(response.reason) reply = response.json() print(reply["choices"][0]["message"]["content"]) ``` 執行結果如下: ``` >>> %Run openai_pc.py 200 OK 您好!我是语言模型AI的GPT-3,有什么可以帮助您的吗? ``` ## 改用 MicroPython 既然是使用 HTTP Post, 哪麼只要從 requests 模組改成 urequests 模組, 應該就可以原封不動照搬程式了, 我們來試看看: ```python import network import time import urequests # 連線至無線網路 sta=network.WLAN(network.STA_IF) sta.active(True) sta.connect('你的無線網路名稱', '無線網路密碼') while not sta.isconnected() : pass print('Wi-Fi連線成功') API_KEY = '你的 OpenAI API 金鑰' response = urequests.post( 'https://api.openai.com/v1/chat/completions', headers = { 'Authorization': 'Bearer ' + API_KEY }, json = { 'model': 'gpt-3.5-turbo', "messages": [{"role": "user", "content": "你好"}]} ) print(response.status_code) print(response.reason) reply = response.json() print(reply["choices"][0]["message"]["content"]) ``` 不過執行後就會看到 OpenAI 伺服器端回覆 400 錯誤: ``` >>> %Run -c $EDITOR_CONTENT Wi-Fi連線成功 400 b'Bad Request' Traceback (most recent call last): File "<stdin>", line 32, in <module> KeyError: choices ``` 同樣的程式, 搬到 MicroPython 上會出錯, 第一個懷疑的就是中文編碼的問題, 如果把程式中傳遞的 "你好" 改成純英文試看看: ```python ... json = { 'model': 'gpt-3.5-turbo', "messages": [{"role": "user", "content": "hello"}]} ... ``` 再執行一次就會發現可以正確執行: ``` >>> %Run -c $EDITOR_CONTENT Wi-Fi連線成功 200 b'OK' Hello there! How may I assist you today? ``` 顯然問題就是出在 [urequests.post](https://github.com/micropython/micropython-lib/blob/master/python-ecosys/urequests/urequests.py) 對於 [json](https://github.com/micropython/micropython-lib/blob/01db3da37e916bb76b09255ce3a852f4864e9fe6/python-ecosys/urequests/urequests.py#L103) 參數的處理。 ## json 模組的中文處理 在 [`urequests.post`](https://github.com/micropython/micropython-lib/blob/01db3da37e916bb76b09255ce3a852f4864e9fe6/python-ecosys/urequests/urequests.py#L107) 中會使用 `json` 模組 (MicroPython 中 json 與 ujson 是同一個模組) 的 `dumps` 函式將 Python 字典轉成字串格式的 JSON 資料, 可是它的輸出結果會保留以 UTF-16 編碼的中文字, 例如: ```python >>> import json >>> json.dumps({"content": "你好"}) '{"content": "\u4f60\u597d"}' ``` 其中 \u4f60 是 ["你"](https://www.compart.com/en/unicode/U+4F60) 的 UTF-16 編碼, 但是 [json 規格需要的是 UTF8 編碼](https://www.rfc-editor.org/rfc/rfc8259.html#page-9), 或是[使用 "\u" 跳脫序列標註的 UTF-16 編碼](https://www.rfc-editor.org/rfc/rfc8259.html#section-7), 好在 bytes 的 `encode` 方法可以幫我們將字串轉換成 UTF8 編碼的位元組串: ```python >>> json.dumps({"content": "你好"}).encode('utf8') b'{"content": "\xe4\xbd\xa0\xe5\xa5\xbd"}' ``` 這樣結果就對了。不過因為要自行處理字典轉 json 格式位元組的工作, 所以就不能直接在 `urequests.post` 中使用 json 參數傳入字典了。 ## 改用 data 參數傳入 json 資料 `urequests.post` 有 `data` 參數可以直接傳入要送給伺服端的資料, 因此我們就可以將程式改成如下: ```python import network import time import urequests import ujson # 連線至無線網路 sta=network.WLAN(network.STA_IF) sta.active(True) sta.connect('你的無線網路名稱', '你的無線網路密碼') while not sta.isconnected() : pass print('Wi-Fi連線成功') API_KEY = '你的 OpenAI API 金鑰' response = urequests.post( 'https://api.openai.com/v1/chat/completions', headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + API_KEY }, data = ujson.dumps({ 'model': 'gpt-3.5-turbo', "messages": [{"role": "user", "content": "你好"}] }).encode('utf8') ) print(response.status_code) print(response.reason) reply = response.json() print(reply["choices"][0]["message"]["content"]) ``` 要特別留意的是使用 `json` 參數傳入 Python 字典時, `urequests.post` 會幫你[在表頭加上 'Content-Type: application/json'](https://github.com/micropython/micropython-lib/blob/01db3da37e916bb76b09255ce3a852f4864e9fe6/python-ecosys/urequests/urequests.py#L108), 自行使用 `data` 參數傳入 json 資料時就要記得在表頭補上標示遞交內容的格式, 否則無法正確執行。 這樣一來, 就可以正確叫用 API 了: ``` >>> %Run -c $EDITOR_CONTENT Wi-Fi連線成功 200 b'OK' 你好!有什么我可以帮助您的吗? ```