[Python] 減少 BeautifulSoup 解析 HTML 時的記憶體用量
之前在專案裡是使用 lxml 來解析 HTML 檔案,
速度很快,大部分狀況下也運作的不錯,
不過最近遇到有些比較怪異的 HTML 檔案,在 HTML 裡加入一堆二進位碼 ,
讓 lxml 沒辦法正確解析,導致後面的東西都沒能解出來…
後來是改用 BeautifulSoup,解析器也改用 Python 內建的 html.parser,
就可以正確處理這類的 HTML 檔案,速度變慢是當然,
但連記憶體的使用量也不小,只好來找找看有沒有改善的空間…
在 BeautifulSoup 的文件裡找到了一線曙光~
在 Parsing only part of a document 這一段裡面,
提到了如果只需要某些 tag 的話,就沒必要將所有的解析結果都儲存下來。
這樣對減少解析時間沒太大影響,但對於減少記憶體使用量就有很大的幫助~
先看一下原本的程式吧,這程式用 BeautifulSoup 解析 large_html.htm 這個檔案後,
印出它的 <base> 和所有的 <a> 連結:
from bs4 import BeautifulSoup soup = BeautifulSoup(open("large_html.htm"), "html.parser") print "Base", soup.base.attrs["href"] for elem in soup.find_all("a"): print elem
在改善它之前,先來看一下它到底用了多少記憶體~
我的程式是在 Linux 上跑的,用 ps 去查的話時間有點難抓,
程式跑太快的話就看不到了,而且記憶體用量可能上上下下的…
查了一下,用 valgrind –tool=massif 的話,
可以比較準確的記錄 process 的記憶體用量,用法如下:
valgrind --tool=massif /usr/bin/python test_bs.py
執行完會產生一個 massif.out.<pid> 的檔案,
我們可以用 ms_print massif.out.xxx 來看一下結果,
最前面會用 ASCII 圖的方式秀出記憶體用量的時間軸,
大概可以看出來後來衝到了 19.72 MB:
MB 19.72^ # | # | # | @# | @# | @# | @@@# | @ @# | @ @# | @:::::::::::::@::::::@:::::::@@::::::::@@:::: :::::: @ @# | @: :::::: : : @::: : @: :: : @ : : : : @ : : :::::::::::: : : :::::@ @# | @: :::::: : : @::: : @: :: : @ : : : : @ : : : : : : : : :@ @# | @: :::::: : : @::: : @: :: : @ : : : : @ : : : : : : : : :@ @# | @: :::::: : : @::: : @: :: : @ : : : : @ : : : : : : : : :@ @# | @: :::::: : : @::: : @: :: : @ : : : : @ : : : : : : : : :@ @# | @: :::::: : : @::: : @: :: : @ : : : : @ : : : : : : : : :@ @# | @: :::::: : : @::: : @: :: : @ : : : : @ : : : : : : : : :@ @# | @: :::::: : : @::: : @: :: : @ : : : : @ : : : : : : : : :@ @# | @: :::::: : : @::: : @: :: : @ : : : : @ : : : : : : : : :@ @# | @: :::::: : : @::: : @: :: : @ : : : : @ : : : : : : : : :@ @# 0 +----------------------------------------------------------------------->Gi 0 11.96
在 ms_print 輸出的最下面,也可以看到統計數值:
-------------------------------------------------------------------------------- n time(i) total(B) useful-heap(B) extra-heap(B) stacks(B) -------------------------------------------------------------------------------- 54 12,846,640,583 20,094,264 18,411,862 1,682,402 0
接著我們來用文件裡提到的 SoupStrainer,限制 BeautifulSoup 要儲存下來的資料,
因為我們會用到 <base> 和 <a> 的東西,因此 SoupStrainer 就設定為 [“base”, “a”]:
from bs4 import BeautifulSoup, SoupStrainer soup = BeautifulSoup(open("large_html.htm"), "html.parser", parse_only=SoupStrainer(["base", "a"])) print "Base", soup.base.attrs["href"] for elem in soup.find_all("a"): print elem
如果稍微比較一下執行時間的話,舊的寫法是 3.86 秒,
加上 SoupStrainer 後是 3.53 秒,有些微的改善。
重點的記憶體用量,可以看到加上 SoupStrainer 之後,
大約是 11 MB 左右,比原本的 19.72 MB 減少了 8 MB,節省了超過 1/3:
-------------------------------------------------------------------------------- n time(i) total(B) useful-heap(B) extra-heap(B) stacks(B) -------------------------------------------------------------------------------- 81 12,529,311,161 11,828,064 11,264,844 563,220 0 82 12,570,833,691 11,828,416 11,264,832 563,584 0 83 12,612,354,220 11,828,880 11,265,724 563,156 0 84 12,653,870,654 11,828,176 11,264,592 563,584 0 85 12,695,390,535 11,828,496 11,264,956 563,540 0 86 12,736,907,333 11,828,248 11,265,059 563,189 0 87 12,778,423,947 11,829,592 11,266,056 563,536 0
當然記憶體用量的節省要看 HTML 檔案的內容,
不過如果只需要 HTML 裡特定元素的話,用 SoupStrainer 應該都可以有不錯的效果喔~
參考資料:stackoverflow: How to measure actual memory usage of an application or process?