GNE: 新聞網頁正文通用抽取器更新了0.2.1版本,大幅度提升了正文的提取速度。在開發這個版本的時候,我遇到了一個很是奇怪的 Bug,最終發現是因爲垃圾回收機制和內存重用機制致使的。今天咱們來看看這個問題。html
先來看一段代碼:python
這段代碼讀取tests/163/9.html
這個文件裏面的 HTML 代碼,分別獲取 <body>
下面的全部標籤內部的全部<a>
標籤中的文本。提及來可能有點繞口,我舉個例子。git
<body>
<div>
<a href="/xx">你好</a>
</div>
<h2>
<a>世界</a>
</h2>
</body>
複製代碼
分別獲取<div>
標籤和<h2>
標籤下面的<a>
標籤中的文本,也就是你好
和世界
。github
但這段代碼有個問題,就是對於嵌套結構的標籤,會重複提取。例如:緩存
<body>
<div>
<h2>
<a href="/xx">你好</a>
</h2>
</div>
</body>
複製代碼
首先,獲取<div>
標籤下面的<a>
標籤,獲取到的是你好
所在的<a>
標籤。可是,獲取<h2>
標籤下面的<a>
標籤時,獲取的仍然是同一個<a>
標籤。app
這樣一來,在上圖代碼裏面第15-20行就會重複執行兩次。spa
爲了提升代碼的運行效率,咱們引入緩存,記錄每個<a>
標籤的分析結果,若是發現一個<a>
標籤已經被分析了,就直接使用緩存的結果,避免重複分析。調試
因而,代碼修改爲下面這樣:code
代碼第18行的str(element)
對應了這個節點的內存地址,以下圖所示:cdn
這段代碼看起來彷佛沒有什麼問題,但在實際提取數據的時候,發現提取的結果不太正常。
爲了調試這個問題,我對代碼作了一下修改:
能夠看到,同一個 HTML 標籤,以前緩存的結果居然跟新提取的不同。
因而,我想看看每次提取的時候,對應的 element 是哪一個,但卻發生了更詭異的事情,咱們作一個看起來對代碼不會有任何影響的改動:
圖4裏面,咱們直接把element_text_list
緩存起來。圖5裏面,咱們把[element_text_list, element]
緩存起來,讀取的時候,讀取這個列表的下標爲0的元素。也就是說,這個緩存的element
咱們根本不使用。
但奇怪的事情就這樣發生了,問題消失了!在圖4大量打印的同一個標籤,緩存的數據跟提取的數據不一致!
,在圖5裏面卻一條都沒有打印。這樣修改之後,GNE 的提取的結果就正確了。
但爲何會發生這種事情呢?難道說跟緩存的結果有關係?那麼咱們把列表裏面的 element
改爲其餘數據看看:
僅僅是把element
改爲了數字1,Bug 又出現了。
它彷佛知道我在試圖去觀察它,當我嘗試用代碼去觀察 element
時,它就一切正常。當我不觀察它時,它就會出問題。薛定諤的 element
。
遇事不決,量子力學。這個問題跟量子力學實際上沒有關係。致使這個詭異狀況發生的緣由,是一個一直運行在 Python 裏面,可是你經常忽略的機制——垃圾回收。
Python 會把再也不使用的對象清理掉,從而釋放內存。當咱們執行一個 for 循環時:
for element in element_list:
a = element.xpath('//xxx')
b = element.xpath('.//text()')
c = 1 + 1
複製代碼
循環第一次執行的時候,生成第一個element
對象,可是這個對象在循環第二次執行的時候就被新的element
對象覆蓋了。由於沒有其餘地方繼續使用第一個 element
對象,它的引用計數歸零,Python 的垃圾回收機制就會把它清理掉。它佔用的內存空間也會被釋放出來。
但若是換一種寫法:
cache = []
for element in element_list:
a = element.xpath('//xxx')
b = element.xpath('.//text()')
c = 1 + 1
cache.append(element)
複製代碼
因爲列表cache
中包含了對每一個 element
對象的引用,致使第一次循環生成的element
對象的引用計數不爲0,垃圾回收機制不會回收它,它始終佔用了一塊內存區域。這塊區域不會被其餘數據使用。那麼每次循環,新的element
對象都會新申請一塊內存區域來存放數據,因而就等價於每個不一樣的 element
節點對應了不一樣的內存地址。
在示例代碼裏面,你們注意element_flag = str(element)
這一行,它的值相似於<Element a at 0x1087ba638>
,這裏的十六進制數字0x1087ba638
對應了這個對象在內存裏面的地址。
一開始,我有一個不正確的假設,我覺得str(element)
的值,對應的 HTML 裏面的每一個節點。同一個節點,屢次執行,結果都同樣,不一樣的節點,屢次執行,結果都不同。
但實際上這是不正確的。由於若是前一個節點的內存區域被垃圾回收了,那麼這個區域會被從新分配,新來的節點可能碰巧會放到這個地方,這就致使兩個不一樣的 <a>
標籤,當你執行str(element)
時,他們打印出來的結果都是相同的。可是實際上他們的正文不同。
而當我使用element_text_cache[element_flag] = [element_text_list, element]
時,因爲每一個element
對象不會被回收,因而就不會出現不一樣的節點互相覆蓋的問題,因此它的工做就符合了預期。
因此,bug 的根本緣由在於,我不該該使用str(element)
做爲緩存的 Key,應該找一個跟 HTML 節點一一對應的東西來做爲 Key。顯然,使用 XPath 更好。
因而,修改代碼,把element_flag
改爲 XPath:
問題得以解決。