驚呆了,我的 Python 代碼裏面出現了薛定諤的 Bug



攝影:產品經理

文章來源:未聞Code

作者:kingname

GNE: 新聞網頁正文通用抽取器[1]更新了0.2.1版本,大幅度提高了正文的提取速度。在開發這個版本的時候,我遇到了一個非常奇怪的 Bug,最終發現是由於垃圾回收機制和內存重用機制導致的。今天我們來看看這個問題。

問題背景

先來看一段代碼:

圖1

這段代碼讀取tests/163/9.html這個文件裏面的 HTML 代碼,分別獲取 <body> 下面的所有標籤內部的所有<a>標籤中的文本。說起來可能有點繞口,我舉個例子。

<body>
    <div>
        <a href="/xx">你好</a>
    </div>
    <h2>
        <a>世界</a>
    </h2>
</body>

分別獲取<div>標籤和<h2>標籤下面的<a>標籤中的文本,也就是你好世界

但這段代碼有個問題,就是對於嵌套結構的標籤,會重複提取。例如:

<body>
    <div>
        <h2>
            <a href="/xx">你好</a>
        </h2>
    </div>
</body>

首先,獲取<div>標籤下面的<a>標籤,獲取到的是你好所在的<a>標籤。但是,獲取<h2>標籤下面的<a>標籤時,獲取的仍然是同一個<a>標籤。

這樣一來,在上圖代碼裏面第15-20行就會重複執行兩次。

爲了提高代碼的運行效率,我們引入緩存,記錄每一個<a>標籤的分析結果,如果發現一個<a>標籤已經被分析了,就直接使用緩存的結果,避免重複分析。

於是,代碼修改成下面這樣:

圖2

代碼第18行的str(element)對應了這個節點的內存地址,如下圖所示:

圖3

這段代碼看起來似乎沒有什麼問題,但在實際提取數據的時候,發現提取的結果不太正常。

薛定諤的 Element

爲了調試這個問題,我對代碼做了一下修改:

圖4

可以看到,同一個 HTML 標籤,之前緩存的結果竟然跟新提取的不一樣。

於是,我想看看每次提取的時候,對應的 element 是哪個,但卻發生了更詭異的事情,我們做一個看起來對代碼不會有任何影響的改動:

圖5

圖4裏面,我們直接把element_text_list緩存起來。圖5裏面,我們把[element_text_list, element]緩存起來,讀取的時候,讀取這個列表的下標爲0的元素。也就是說,這個緩存的element我們根本不使用。

但奇怪的事情就這樣發生了,問題消失了!在圖4大量打印的同一個標籤,緩存的數據跟提取的數據不一致!,在圖5裏面卻一條都沒有打印。這樣修改以後,GNE 的提取的結果就正確了。

但爲什麼會發生這種事情呢?難道說跟緩存的結果有關係?那麼我們把列表裏面的 element改成其他數據看看:

圖6

僅僅是把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:

圖7

問題得以解決。

參考資料

[1]

GNE: 新聞網頁正文通用抽取器: https://github.com/kingname/GeneralNewsExtractor

近期推薦閱讀:
【1】整理了我開始分享學習筆記到現在超過250篇優質文章,涵蓋數據分析、爬蟲、機器學習等方面,別再說不知道該從哪開始,實戰哪裏找了【2】【終篇】Pandas中文官方文檔:基礎用法6(含1-5)
如果你覺得文章不錯的話,分享、收藏、在看、留言666是對老表的最大支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章