一日一技:使用Python翻譯HTML中的文字字串

語言: CN / TW / HK

攝影:產品經理

麻婆豆腐和紅莧菜

相信大家都用過瀏覽器的翻譯網頁功能,例如對於下圖這個英文網頁:

一鍵翻譯成中文以後是這樣的:

你可能會覺得這個功能很簡單,不就是字串替換嗎?那你可以試一試把下面這個HTML片段中的 <p> 標籤下面的英文翻譯成中文。其它標籤中的不要改動:

<div>
 <p>if you want to parse date and time, your could use <em>datetime</em>, by use this library, you can generate now time by one line code <span>datetime.datetime.now()</span> this is so easy.</p>
</div>

<em> 標籤中的 datetime<span> 標籤中的 datetime.datetime.now() 不需要翻譯。

你一拍腦袋,馬上寫出了下面這幾行程式碼(假設你已經有了一個現成的 translate() 函式,傳入英文,輸出中文):

from lxml.html import fromstring
source = '''<div>
 <p>if you want to parse date and time, your could use <em>datetime</em>, by use this library, you can generate now time by one line code <span>datetime.datetime.now()</span> this is so easy.</p>
</div>
'''

selector = fromstring(source)
text_list = selector.xpath('//p/text()')
for text in text_list:
    chinese = translate(text)
    ...

當你寫到這裡,你應該會愣一下。因為你突然發現一個問題,怎麼把中文替換回去?

不用嘗試去百度了。在今天(2022-06-20)之前,整個中文網路裡面,你找不到解決方法。

一個比較笨的辦法是直接對原始的HTML字串進行文字替換:

for text in text_list:
    chinese = translate(text)
    source = source.replace(text, chinese)

但這樣做,效率非常低。因為你要不停掃描整個HTML字串。一般一箇中型網站的HTML就有幾千上萬行,十幾二十萬個字元。你每翻譯一小段就全文替換一次,這個時間會非常漫長。

那有沒有辦法只對當前這一個 <p> 標籤裡面的文字進行替換呢?關鍵的問題來了,你替換可以,但是怎麼才能不影響這個 <p> 標籤下面的兩個子標籤?要保證文字和子標籤的相對位置不改變。

如果 <p> 標籤下面只有一段文字,沒有子標籤,那麼非常簡單,如下圖所示:

但現在的問題是, <p> 標籤下面有三段文字。每段文字之間還插入了其它的子標籤。我們怎麼樣對每一段文字進行替換,但是又保持文字的相對順序,並且還不能影響子標籤?

p.text 這種寫法首先就可以排除了,因為它沒有辦法指定替換第幾段文字。

你之所以會覺得這個問題很難解決,是因為你有一個錯覺,請看上面這張截圖,我列印了 text_list 。打印出來是一個包含字串的列表。所以你可能會覺得。使用lxml寫Xpath的時候, /text() 返回的總是包含字串的列表。

但實際上,返回的列表裡面的元素並不是字串,而是 _ElementUnicodeResult 物件。如下圖所示:

不是字串就簡單了,那麼我們可以獲取每一個文字物件的父標籤。然後修改父標籤下面的文字就可以了。

看到這裡,你肯定會問,這三個文字節點的父標籤,不都是同一個 <p> 嗎?如果你覺得是,那你就犯了想當然的錯誤。我們用程式碼來看看:

其實只有第一段文字的父標籤是 <p> 。第二段文字的父標籤,竟然是 <p> 的子標籤 <em> 。第三段文字的父標籤,是 <span>

等等,如果第二段文字的父標籤是 <em> ,那麼 <em>datetime</em> 裡面的 datetime 的父標籤是什麼?它的父標籤也是 <em> !那麼問題來了, <em>text() 文字節點,怎麼可能又是 datetime ,又是 <p> 下面的第二段文字呢?

實際上, <em>text() 始終都是 datetime 。如下圖所示:

那麼, <p> 的第二段文字跟這個 <em> 標籤是什麼關係?實際上,這個關係叫做 tail 。如下圖所示:

在一個標籤裡面,只有第一段 text 是它真正的 text() ,如果這個標籤有子標籤,那麼位於子標籤後面的文字,是這個子標籤的 tail 。只不過當我們在正則表示式裡面寫 /text() 的時候,lxml會幫我們把所有子標籤的 tail 都算作當前標籤的text。

我們可以使用文字節點的 .is_text.is_tail 來判斷它屬於哪種文字。最終執行效果如下圖所示:

END

我的爬蟲架構課開課啦!

爬蟲架構進階就在這裡

送未聞Code知識星球一年訂閱!

一二線大廠在職員工

十多年碼齡的程式設計老鳥

國內外高校在讀學生

中小學剛剛入門的新人

“未聞 Code技術交流群” 等你來!

入群方式:新增微信“mekingname”,備註“粉絲群”(謝絕廣告黨,非誠勿擾!)

好文和朋友一起看~