怎樣讓API快速且輕鬆地提取所有數據?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我上週在Twitter上發起了一個關於API端點的"},{"type":"link","attrs":{"href":"https:\/\/twitter.com\/simonw\/status\/1405554676993433605","title":"","type":null},"content":[{"type":"text","text":"討論"}]},{"type":"text","text":"。相比一次返回100個結果,並要求客戶端對所有頁面進行分頁以檢索所有數據的API,這些流式傳輸大量數據的端點可以作爲替代方案:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設這種流式傳輸端點有了高效的實現,那麼提供流式HTTP API端點(例如一次性提供100,000個JSON對象,而不是要求用戶在超過1000個請求中每次分頁100個對象)有任何意想不到的缺陷嗎?——Simon Willison(@simonw),2021年6月17日"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我收到了很多很棒的回覆。我試過在推文上把這些想法濃縮進一個,但我也會在這裏將它們綜合成一些見解。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"批量導出數據"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我花在API上的時間越多(尤其是處理"},{"type":"link","attrs":{"href":"https:\/\/datasette.io\/","title":"","type":null},"content":[{"type":"text","text":"Datasette"}]},{"type":"text","text":"和"},{"type":"link","attrs":{"href":"https:\/\/simonwillison.net\/2020\/Nov\/14\/personal-data-warehouses\/","title":"","type":null},"content":[{"type":"text","text":"Dogsheep"}]},{"type":"text","text":"項目時),我就越意識到自己最喜歡的API應該可以讓你儘可能快速、輕鬆地提取所有數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"API 一般可以通過三種方式提供這種功能:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單擊“導出所有內容”按鈕,然後等待一段時間,等它顯示包含可下載zip文件鏈接的電子郵件。這並不是真正的API,主要因爲用戶通常很難甚至不可能自動執行最初的“點擊”動作,但這總比沒有好。谷歌的"},{"type":"link","attrs":{"href":"https:\/\/takeout.google.com\/","title":"","type":null},"content":[{"type":"text","text":"Takeout"}]},{"type":"text","text":"是這種模式的一個著名實現。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提供一個JSON API,允許用戶對他們的數據進行分頁。這是一種非常常見的模式,儘管它可能會遇到許多困難:例如,如果對原始數據分頁時,有人又添加了新數據,會發生什麼情況?另外,出於性能原因,某些系統也只允許訪問前N頁。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提供一個你可以點擊的單一HTTP端點,該端點將一次性返回你的所有數據(可能是數十或數百MB大小)。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我今天想要談論的是最後一個選項。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"高效地流式傳輸數據"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"過去,大多數Web工程師會很快否定用一個API端點流式輸出無限數量行的這種想法。HTTP請求是應該儘快處理的!處理請求所花費的時間但凡超過幾秒鐘都是一個危險信號,這表明我們應該重新考慮某些事情纔是。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Web堆棧中的幾乎所有內容都針對快速處理小請求進行了優化。但在過去十年中,這一趨勢出現了一些變化:Node.js讓異步Web服務器變得司空見慣,WebSockets 教會了我們如何處理長時間運行的連接,並且在Python世界中,asyncio和ASGI爲使用較少量內存和CPU處理長時間運行的請求提供了堅實的基礎。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我在這個領域做了幾年的實驗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Datasette能"},{"type":"link","attrs":{"href":"https:\/\/github.com\/simonw\/datasette\/blob\/0.57.1\/datasette\/views\/base.py#L264-L428","title":"","type":null},"content":[{"type":"text","text":"使用ASGI技巧"}]},{"type":"text","text":"將表(或過濾表)中的所有行"},{"type":"link","attrs":{"href":"https:\/\/docs.datasette.io\/en\/stable\/csv_export.html#streaming-all-records","title":"","type":null},"content":[{"type":"text","text":"流式傳輸"}]},{"type":"text","text":"爲CSV,可能會返回數百MB的數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/django-sql-dashboard.datasette.io\/","title":"","type":null},"content":[{"type":"text","text":"Django SQL Dashboard"}]},{"type":"text","text":" 可以將SQL查詢的完整結果導出爲CSV或TSV,這次使用的是Django的"},{"type":"link","attrs":{"href":"https:\/\/docs.djangoproject.com\/en\/3.2\/ref\/request-response\/#django.http.StreamingHttpResponse","title":"","type":null},"content":[{"type":"text","text":"StreamingHttpResponse"}]},{"type":"text","text":"(它確實會佔用一個完整的worker進程,但如果你將其限制爲只有一定數量的身份驗證用戶可用,那也沒什麼問題)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/simonwillison.net\/tags\/vaccinateca\/","title":"","type":null},"content":[{"type":"text","text":"VIAL"}]},{"type":"text","text":"用來實現流式響應,以提供“從管理員導出”"},{"type":"link","attrs":{"href":"https:\/\/github.com\/CAVaccineInventory\/vial\/blob\/cdaaab053a9cf1cef40104a2cdf480b7932d58f7\/vaccinate\/core\/admin_actions.py","title":"","type":null},"content":[{"type":"text","text":"功能"}]},{"type":"text","text":"。它還有一個受API密鑰保護的搜索API,可以用JSON或GeoJSON"},{"type":"link","attrs":{"href":"https:\/\/github.com\/CAVaccineInventory\/vial\/blob\/cdaaab053a9cf1cef40104a2cdf480b7932d58f7\/vaccinate\/api\/serialize.py#L38","title":"","type":null},"content":[{"type":"text","text":"輸出"}]},{"type":"text","text":"所有匹配行。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"實現說明"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現這種模式時需要注意的關鍵是內存使用:如果你的服務器在需要爲一個導出請求提供服務時都需要緩衝100MB以上的數據,你就會遇到麻煩。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"某些導出格式比其他格式更適合流式傳輸。CSV和TSV非常容易流式傳輸,換行分隔的JSON也是如此。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常規JSON需要更謹慎的對待:你可以輸出一個[字符,然後以逗號後綴在一個流中輸出每一行,再跳過最後一行的逗號並輸出一個]。這樣做需要提前查看(一次循環兩個)來驗證你還沒有到達終點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"或者……Martin De Wulf "},{"type":"link","attrs":{"href":"https:\/\/twitter.com\/madewulf\/status\/1405559088994467844","title":"","type":null},"content":[{"type":"text","text":"指出"}]},{"type":"text","text":"你可以輸出第一行,然後輸出每行的時候帶上一個前面的逗號——這完全避免了“一次迭代兩個”的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下一個挑戰是高效地循環遍歷所有數據庫結果,但不要先將它們全部拉入內存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PostgreSQL(和psycopg2 Python模塊)提供了服務端"},{"type":"link","attrs":{"href":"https:\/\/www.psycopg.org\/docs\/usage.html#server-side-cursors","title":"","type":null},"content":[{"type":"text","text":"遊標"}]},{"type":"text","text":",這意味着你可以通過代碼流式傳輸結果,而無需一次全部加載它們。我把它們用在了Django SQL"},{"type":"link","attrs":{"href":"https:\/\/github.com\/simonw\/django-sql-dashboard\/blob\/dd1bb18e45b40ce8f3d0553a72b7ec3cdc329e69\/django_sql_dashboard\/views.py#L397-L399","title":"","type":null},"content":[{"type":"text","text":"儀表板"}]},{"type":"text","text":"中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不過,服務端遊標讓我感到有些緊張,因爲它們似乎很可能會佔用數據庫本身的資源。所以我在這裏考慮的另一種技術是"},{"type":"link","attrs":{"href":"https:\/\/use-the-index-luke.com\/no-offset","title":"","type":null},"content":[{"type":"text","text":"鍵集分頁"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鍵集分頁(keyset pagination)適用於所有按唯一列排序的數據,尤其適合主鍵(或其他索引列)。使用如下查詢檢索每一頁數據:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"select * from items order by id limit 21"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意"},{"type":"codeinline","content":[{"type":"text","text":"limit 21"}]},{"type":"text","text":"——如果我們要檢索20個項目的頁面,我們這裏要求的就是21,因爲這樣我們就可以使用最後一個返回的項目來判斷是否有下一頁。然後對於後續頁面,取第20個id值並要求大於該值的內容:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"select * from items where id > 20 limit 21"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些查詢都可以快速響應(因爲它針對有序索引)並使用了可預測的固定內存量。使用鍵集分頁,我們可以遍歷一個任意大的數據表,一次流式傳輸一頁,而不會耗盡任何資源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而且由於每個查詢都是小而快的,我們也不必擔心龐大的查詢會佔用數據庫資源。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"會出什麼問題?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我真的很喜歡這些模式。它們還沒有在我面前暴露出來什麼問題,儘管我還沒有將它們部署到什麼真正大規模的環境裏。所以我在Twitter"},{"type":"link","attrs":{"href":"https:\/\/twitter.com\/simonw\/status\/1405554676993433605","title":"","type":null},"content":[{"type":"text","text":"問了問"}]},{"type":"text","text":"大家,想知道應該留心什麼樣的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據Twitter討論,以下是這種方法面臨的一些挑戰。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"挑戰:重啓服務器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果流需要很長時間才能完成,那麼推出更新就會成爲一個問題。你不想中斷下載,但也不想一直等待它完成才能關閉服務器。——Adam Lowry(@robotadam),2021年6月17日"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種意見出現了幾次,這是我沒有考慮過的。如果你的部署過程涉及重新啓動服務器的操作(很難想象完全不需要重啓的情況),那麼在執行這一操作時需要考慮長時間運行的連接。如果有用戶正在一個500MB的流中走過了一半路程,你可以截斷他們的連接或等待他們完成。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"挑戰:如何返回錯誤"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你正在流式傳輸一個響應,你會從一個HTTP 200代碼開始……但是如果中途發生錯誤,可能是在通過數據庫分頁時發生錯誤會怎樣?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你已經開始發送這個請求,因此你不能將狀態代碼更改爲500。相反,你需要向正在生成的流寫入某種錯誤。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你正在提供一個巨大的JSON文檔,你至少可以讓該JSON變得無效,這應該能向你的客戶端表明出現了某種問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"像CSV這樣的格式處理起來更難。你如何讓用戶知道他們的CSV數據是不完整的呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果某人的連接斷開怎麼辦——他們肯定會注意到他們丟失了某些東西呢,還是會認爲被截斷的文件就是所有數據呢?"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"挑戰:可恢復的下載"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果用戶通過你的API進行分頁,他們可以免費獲得可恢復性:如果出現問題,他們可以從他們獲取的最後一頁重新開始。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但恢復單個流就要困難得多。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HTTP範圍"},{"type":"link","attrs":{"href":"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Range_requests","title":"","type":null},"content":[{"type":"text","text":"機制"}]},{"type":"text","text":"可用於提供針對大文件的可恢復下載,但它僅在你提前生成整個文件時纔有效。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有一種API的設計方法可以用來支持這一點,前提是流中的數據處於可預測的順序(如果你使用鍵集分頁則必須如此,如上所述)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讓觸發下載的端點採用一個可選的"},{"type":"codeinline","content":[{"type":"text","text":"?since="}]},{"type":"text","text":"參數,如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"GET \/stream-everything?since=b24ou34\n[\n {\"id\": \"m442ecc\", \"name\": \"...\"},\n {\"id\": \"c663qo2\", \"name\": \"...\"},\n {\"id\": \"z434hh3\", \"name\": \"...\"},\n]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏"},{"type":"codeinline","content":[{"type":"text","text":"b24ou34"}]},{"type":"text","text":"是一個標識符——它可以是一個故意不透明的令牌,但它需要作爲響應的一部分提供。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果用戶由於任何原因斷開連接,他們可以傳遞他們成功檢索到的最後一個ID來從上次中斷的地方開始:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"GET \/stream-everything?since=z434hh3"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這還需要客戶端應用程序具備某種程度的智能反饋,但它是一個相當簡單的模式,既可以在服務器上實現,也能作爲客戶端實現。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"最簡單的解決方案:從雲存儲生成和返回"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現這種API的最健壯的方法似乎是技術上最讓人覺得無聊的:分離一個後臺任務,讓它生成大型響應並將其推送到雲存儲(S3或GCS),然後將用戶重定向到一個簽名URL來下載生成的文件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種方法很容易擴展,爲用戶提供了帶有內容長度標頭的完整文件(甚至可以恢復下載,因爲S3和GCS支持範圍標頭),用戶很清楚這些文件是可下載的。它還避免了由長連接引起的服務器重啓問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這就是 Mixpanel 處理其導出功能的方式,這也是Sean Coates在嘗試爲 AWS Lambda\/APIGate 響應大小限制尋找解決方法時想到的"},{"type":"link","attrs":{"href":"https:\/\/seancoates.com\/blogs\/lambda-payload-size-workaround","title":"","type":null},"content":[{"type":"text","text":"方案"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你的目標是爲用戶提供強大、可靠的數據批量導出機制,那麼導出到雲存儲可能是最佳選項。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,流式動態響應是一個非常巧妙的技巧,我計劃繼續探索它們!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/simonwillison.net\/2021\/Jun\/25\/streaming-large-api-responses\/"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章