如果你需要在 Python 中處理一個大的 JSON 文件,會很容易出現(xiàn)耗盡內(nèi)存的情況。即使原始數(shù)據(jù)大小小于內(nèi)存容量,Python 也會進一步增加內(nèi)存使用量。這意味著程序會在與磁盤交互時處理緩慢,或在內(nèi)存不足時崩潰。
一種常見的解決方案是流解析,也就是惰性解析、迭代解析或分塊處理。讓我們看看如何將此技術(shù)應(yīng)用于 JSON 處理。
問題:Python中加載JSON內(nèi)存效率低
我們使用這個大小為24MB的JSON文件來舉例,它在加載時會對內(nèi)存產(chǎn)生明顯的影響。這個JSON對象是在GitHub中,用戶對存儲庫執(zhí)行操作時的事件列表:
[{"id":"2489651045","type":"CreateEvent","actor":
{"id":665991,"login":"petroav","gravatar_id":"","url":"https://api.github.com/users/petroav","avatar_url":"https://avatars.githubusercontent.com/u/665991?"},"repo":
{"id":28688495,"name":"petroav/6.828","url":"https://api.github.com/repos/petroav/6.828"},"payload":
{"ref":"master","ref_type":"branch","master_branch":"master","description":"SolutiontohomeworkandassignmentsfromMIT's6.828(OperatingSystemsEngineering).Doneinmysparetime.","pusher_type":"user"},"public":true,"created_at":"2015-01-01T1500Z"},
...
]
我們的目標是找出給定用戶在與哪些存儲庫進行交互。下面是一個簡單的 Python 程序:
importjson
withopen("large-file.json","r")asf:
data=json.load(f)
user_to_repos={}
forrecordindata:
user=record["actor"]["login"]
repo=record["repo"]["name"]
ifusernotinuser_to_repos:
user_to_repos[user]=set()
user_to_repos[user].add(repo)
輸出結(jié)果是一個用戶名映射到存儲庫名稱的字典。我們使用 Fil 內(nèi)存分析器運行它時,可以發(fā)現(xiàn)內(nèi)存使用的峰值達到了124MB,還可以發(fā)現(xiàn)兩個主要的內(nèi)存分配來源:
- 讀取文件
-
將生成的字節(jié)解碼為 Unicode 字符串
但我們加載的原始文件是24MB。一旦我們將它加載到內(nèi)存中并將其解碼為文本 (Unicode)Python 字符串,它需要的空間遠遠超過 24MB。這是為什么?
擴展知識:Python字符串的內(nèi)存表示
Python字符串在表示時會被更少使用內(nèi)存的方法優(yōu)化。每個字符串都有固定的開銷,如果字符串可以表示為 ASCII,則每個字符只使用一個字節(jié)的內(nèi)存。如果字符串使用更多擴展字符,則每個字符可能使用4個字節(jié)。我們可以使用 sys.getsizeof() 查看一個對象需要多少內(nèi)存:
>>>importsys
>>>s="a"*1000
>>>len(s)
1000
>>>sys.getsizeof(s)
1049
>>>s2=""+"a"*999
>>>len(s2)
1000
>>>sys.getsizeof(s2)
2074
>>>s3=""+"a"*999
>>>len(s3)
1000
>>>sys.getsizeof(s3)
4076
在上面的例子中3個字符串都是 1000 個字符長,但它們使用的內(nèi)存量取決于它們包含的字符。
在本例中我們的大JSON 文件里包含不適合ASCII編碼的字符,正是因為它是作為一個巨大的字符串加載的,所以整個巨大的字符串會使用效率較低的內(nèi)存表示。
流處理解決方案
很明顯,將整個JSON文件直接加載到內(nèi)存中是一種內(nèi)存浪費。
對一個結(jié)構(gòu)為對象列表的 JSON 文件,理論上我們可以一次解析一個塊,而不是一次全部解析,以此來減少內(nèi)存的使用量。目前有許多 Python 庫支持這種 JSON 解析方式,下面我們使用 ijson 庫來舉例。
importijson
user_to_repos={}
withopen("large-file.json","r")asf:
forrecordinijson.items(f,"item"):
user=record["actor"]["login"]
repo=record["repo"]["name"]
ifusernotinuser_to_repos:
user_to_repos[user]=set()
user_to_repos[user].add(repo)
如果使用json標準庫,數(shù)據(jù)一旦被加載文件就會被關(guān)閉。而使用ijson,文件必須保持打開狀態(tài),因為當我們遍歷記錄時,JSON 解析器正在按需讀取文件。有關(guān)更多詳細信息,請參閱 ijson 文檔。
在內(nèi)存分析器運行它時,可以發(fā)現(xiàn)內(nèi)存使用的峰值降到了3.6MB,問題解決了!而且在此例子中,使用 ijson 的流式處理也會提升運行時的性能,當然這個性能取決于數(shù)據(jù)集或算法。
其他解決方法
- Pandas:Pandas 具有讀取 JSON 的能力,理論上它可以以更節(jié)省內(nèi)存的方式讀取。
- SQLite:SQLite 數(shù)據(jù)庫可以解析 JSON,將 JSON 存儲在列中,以及查詢 JSON數(shù)據(jù)。因此,可以將 JSON 加載到磁盤支持的數(shù)據(jù)庫文件中,并對它運行查詢來提取相關(guān)的數(shù)據(jù)子集。
最后,如果可以控制輸出格式,則可以通過切換到更高效的表示來減少 JSON 處理的內(nèi)存使用量。例如,從單個巨大的 JSON 對象列表切換到每行一條 JSON 記錄,這意味著每條解碼的 JSON 記錄將只使用少量內(nèi)存。
知識延伸
前段時間,Python開發(fā)者公號推薦了一款很?實用的 JSON 工具?,可以更輕松直觀地查看 JSON。
原文標題:Python 處理超大 JSON 文件,這個方法簡單!
文章出處:【微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
-
內(nèi)存
+關(guān)注
關(guān)注
8文章
3034瀏覽量
74130 -
python
+關(guān)注
關(guān)注
56文章
4798瀏覽量
84799 -
JSON
+關(guān)注
關(guān)注
0文章
119瀏覽量
6980
原文標題:Python 處理超大 JSON 文件,這個方法簡單!
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論