如何用Python解析HTML?
用一些簡單的腳本,可以很容易地清理文檔和其它大量的 HTML 文件。但是首先你需要解析它們。
作為 Scribus 文檔團(tuán)隊(duì)的長期成員,我要隨時(shí)了解***的源代碼更新,以便對(duì)文檔進(jìn)行更新和補(bǔ)充。 我最近在剛升級(jí)到 Fedora 27 系統(tǒng)的計(jì)算機(jī)上使用 Subversion 進(jìn)行檢出操作時(shí),對(duì)于下載該文檔所需要的時(shí)間我感到很驚訝,文檔由 HTML 頁面和相關(guān)圖像組成。 我恐怕該項(xiàng)目的文檔看起來比項(xiàng)目本身大得多,并且懷疑其中的一些內(nèi)容是“僵尸”文檔——不再使用的 HTML 文件以及 HTML 中無法訪問到的圖像。
我決定為自己創(chuàng)建一個(gè)項(xiàng)目來解決這個(gè)問題。 一種方法是搜索未使用的現(xiàn)有圖像文件。 如果我可以掃描所有 HTML 文件中的圖像引用,然后將該列表與實(shí)際圖像文件進(jìn)行比較,那么我可能會(huì)看到不匹配的文件。
這是一個(gè)典型的圖像標(biāo)簽:
<img src="images/edit_shapes.png" ALT="Edit examples" ALIGN=left>
我對(duì) src=
之后的***組引號(hào)之間的部分很感興趣。 在尋找了一些解決方案后,我找到一個(gè)名為 BeautifulSoup 的 Python 模塊。 腳本的核心部分如下所示:
soup = BeautifulSoup(all_text, 'html.parser')
match = soup.findAll("img")
if len(match) > 0:
for m in match:
imagelist.append(str(m))
我們可以使用這個(gè) findAll
方法來挖出圖片標(biāo)簽。 這是一小部分輸出:
<img src="images/pdf-form-ht3.png"/><img src="images/pdf-form-ht4.png"/><img src="images/pdf-form-ht5.png"/><img src="images/pdf-form-ht6.png"/><img align="middle" alt="GSview - Advanced Options Panel" src="images/gsadv1.png" title="GSview - Advanced Options Panel"/><img align="middle" alt="Scribus External Tools Preferences" src="images/gsadv2.png" title="Scribus External Tools Preferences"/>
到現(xiàn)在為止還挺好。我原以為下一步就可以搞定了,但是當(dāng)我在腳本中嘗試了一些字符串方法時(shí),它返回了有關(guān)標(biāo)記的錯(cuò)誤而不是字符串的錯(cuò)誤。 我將輸出保存到一個(gè)文件中,并在 KWrite 中進(jìn)行編輯。 KWrite 的一個(gè)好處是你可以使用正則表達(dá)式(regex)來做“查找和替換”操作,所以我可以用 \n<img
替換 <img
,這樣可以看得更清楚。 KWrite 的另一個(gè)好處是,如果你用正則表達(dá)式做了一個(gè)不明智的選擇,你還可以撤消。
但我認(rèn)為,肯定有比這更好的東西,所以我轉(zhuǎn)而使用正則表達(dá)式,或者更具體地說 Python 的 re
模塊。 這個(gè)新腳本的相關(guān)部分如下所示:
match = re.findall(r'src="(.*)/>', all_text)
if len(match)>0:
for m in match:
imagelist.append(m)
它的一小部分輸出如下所示:
images/cmcanvas.png" title="Context Menu for the document canvas" alt="Context Menu for the document canvas" /></td></tr></table><br images/eps-imp1.png" title="EPS preview in a file dialog" alt="EPS preview in a file dialog" images/eps-imp5.png" title="Colors imported from an EPS file" alt="Colors imported from an EPS file" images/eps-imp4.png" title="EPS font substitution" alt="EPS font substitution" images/eps-imp2.png" title="EPS import progress" alt="EPS import progress" images/eps-imp3.png" title="Bitmap conversion failure" alt="Bitmap conversion failure"
乍一看,它看起來與上面的輸出類似,并且附帶有去除圖像的標(biāo)簽部分的好處,但是有令人費(fèi)解的是還夾雜著表格標(biāo)簽和其他內(nèi)容。 我認(rèn)為這涉及到這個(gè)正則表達(dá)式 src="(.*)/>
,這被稱為貪婪,意味著它不一定停止在遇到 />
的***個(gè)實(shí)例。我應(yīng)該補(bǔ)充一點(diǎn),我也嘗試過 src="(.*)"
,這真的沒有什么更好的效果,我不是一個(gè)正則表達(dá)式專家(只是做了這個(gè)),找了各種方法來改進(jìn)這一點(diǎn)但是并沒什么用。
做了一系列的事情之后,甚至嘗試了 Perl 的 HTML::Parser
模塊,最終我試圖將這與我為 Scribus 編寫的一些腳本進(jìn)行比較,這些腳本逐個(gè)字符的分析文本內(nèi)容,然后采取一些行動(dòng)。 為了最終目的,我終于想出了所有這些方法,并且完全不需要正則表達(dá)式或 HTML 解析器。 讓我們回到展示的那個(gè) img
標(biāo)簽的例子。
<img src="images/edit_shapes.png" ALT="Edit examples" ALIGN=left>
我決定回到 src=
這一塊。 一種方法是等待 s
出現(xiàn),然后看下一個(gè)字符是否是 r
,下一個(gè)是 c
,下一個(gè)是否 =
。 如果是這樣,那就匹配上了! 那么兩個(gè)雙引號(hào)之間的內(nèi)容就是我所需要的。 這種方法的問題在于需要連續(xù)識(shí)別上面這樣的結(jié)構(gòu)。 一種查看代表一行 HTML 文本的字符串的方法是:
for c in all_text:
但是這個(gè)邏輯太亂了,以至于不能持續(xù)匹配到前面的 c
,還有之前的字符,更之前的字符,更更之前的字符。
***,我決定專注于 =
并使用索引方法,以便我可以輕松地引用字符串中的任何先前或?qū)淼淖址?這里是搜索部分:
index = 3
while index < linelength:
if (all_text[index] == '='):
if (all_text[index-3] == 's') and (all_text[index-2] == 'r') and (all_text[index-1] == 'c'):
imagefound(all_text, imagelist, index)
index += 1
else:
index += 1
else:
index += 1
我用第四個(gè)字符開始搜索(索引從 0 開始),所以我在下面沒有出現(xiàn)索引錯(cuò)誤,并且實(shí)際上,在每一行的第四個(gè)字符之前不會(huì)有等號(hào)。 ***個(gè)測(cè)試是看字符串中是否出現(xiàn)了 =
,如果沒有,我們就會(huì)前進(jìn)。 如果我們確實(shí)看到一個(gè)等號(hào),那么我們會(huì)看前三個(gè)字符是否是 s
、r
和 c
。 如果全都匹配了,就調(diào)用函數(shù) imagefound
:
def imagefound(all_text, imagelist, index):
end = 0
index += 2
newimage = ''
while end == 0:
if (all_text[index] != '"'):
newimage = newimage + all_text[index]
index += 1
else:
newimage = newimage + '\n'
imagelist.append(newimage)
end = 1
return
我們給函數(shù)發(fā)送當(dāng)前索引,它代表著 =
。 我們知道下一個(gè)字符將會(huì)是 "
,所以我們跳過兩個(gè)字符,并開始向名為 newimage
的控制字符串添加字符,直到我們發(fā)現(xiàn)下一個(gè) "
,此時(shí)我們完成了一次匹配。 我們將字符串加一個(gè)換行符(\n
)添加到列表 imagelist
中并返回(return
),請(qǐng)記住,在剩余的這個(gè) HTML 字符串中可能會(huì)有更多圖片標(biāo)簽,所以我們馬上回到搜索循環(huán)中。
以下是我們的輸出現(xiàn)在的樣子:
images/text-frame-link.png
images/text-frame-unlink.png
images/gimpoptions1.png
images/gimpoptions3.png
images/gimpoptions2.png
images/fontpref3.png
images/font-subst.png
images/fontpref2.png
images/fontpref1.png
images/dtp-studio.png
啊,干凈多了,而這只花費(fèi)幾秒鐘的時(shí)間。 我本可以將索引前移 7 步來剪切 images/
部分,但我更愿意把這個(gè)部分保存下來,以確保我沒有剪切掉圖像文件名的***個(gè)字母,這很容易用 KWrite 編輯成功 —— 你甚至不需要正則表達(dá)式。 做完這些并保存文件后,下一步就是運(yùn)行我編寫的另一個(gè)腳本 sortlist.py
:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# sortlist.py
import os
imagelist = []
for line in open('/tmp/imagelist_parse4.txt').xreadlines():
imagelist.append(line)
imagelist.sort()
outfile = open('/tmp/imagelist_parse4_sorted.txt', 'w')
outfile.writelines(imagelist)
outfile.close()
這會(huì)讀取文件內(nèi)容,并存儲(chǔ)為列表,對(duì)其排序,然后另存為另一個(gè)文件。 之后,我可以做到以下幾點(diǎn):
ls /home/gregp/development/Scribus15x/doc/en/images/*.png > '/tmp/actual_images.txt'
然后我需要在該文件上運(yùn)行 sortlist.py
,因?yàn)?ls
方法的排序與 Python 不同。 我原本可以在這些文件上運(yùn)行比較腳本,但我更愿意以可視方式進(jìn)行操作。 ***,我成功找到了 42 個(gè)圖像,這些圖像沒有來自文檔的 HTML 引用。
這是我的完整解析腳本:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# parseimg4.py
import os
def imagefound(all_text, imagelist, index):
end = 0
index += 2
newimage = ''
while end == 0:
if (all_text[index] != '"'):
newimage = newimage + all_text[index]
index += 1
else:
newimage = newimage + '\n'
imagelist.append(newimage)
end = 1
return
htmlnames = []
imagelist = []
tempstring = ''
filenames = os.listdir('/home/gregp/development/Scribus15x/doc/en/')
for name in filenames:
if name.endswith('.html'):
htmlnames.append(name)
#print htmlnames
for htmlfile in htmlnames:
all_text = open('/home/gregp/development/Scribus15x/doc/en/' + htmlfile).read()
linelength = len(all_text)
index = 3
while index < linelength:
if (all_text[index] == '='):
if (all_text[index-3] == 's') and (all_text[index-2] == 'r') and
(all_text[index-1] == 'c'):
imagefound(all_text, imagelist, index)
index += 1
else:
index += 1
else:
index += 1
outfile = open('/tmp/imagelist_parse4.txt', 'w')
outfile.writelines(imagelist)
outfile.close()
imageno = len(imagelist)
print str(imageno) + " images were found and saved"
腳本名稱為 parseimg4.py
,這并不能真實(shí)反映我陸續(xù)編寫的腳本數(shù)量(包括微調(diào)的和大改的以及丟棄并重新開始寫的)。 請(qǐng)注意,我已經(jīng)對(duì)這些目錄和文件名進(jìn)行了硬編碼,但是很容易變得通用化,讓用戶輸入這些信息。 同樣,因?yàn)樗鼈兪枪ぷ髂_本,所以我將輸出發(fā)送到 /tmp
目錄,所以一旦重新啟動(dòng)系統(tǒng),它們就會(huì)消失。
這不是故事的結(jié)尾,因?yàn)橄乱粋€(gè)問題是:僵尸 HTML 文件怎么辦? 任何未使用的文件都可能會(huì)引用圖像,不能被前面的方法所找出。 我們有一個(gè) menu.xml
文件作為聯(lián)機(jī)手冊(cè)的目錄,但我還需要考慮 TOC(LCTT 譯注:TOC 是 table of contents 的縮寫)中列出的某些文件可能引用了不在 TOC 中的文件,是的,我確實(shí)找到了一些這樣的文件。
***我可以說,這是一個(gè)比圖像搜索更簡單的任務(wù),而且開發(fā)的過程對(duì)我有很大的幫助。