docx格式文件詳解:xml解析並用html還原

語言: CN / TW / HK

theme: healer-readable

本文正在參加「金石計劃 . 瓜分6萬現金大獎」

一、應用場景:有什麼用處?

我在從事教育資訊化的工作中,經常會用到word的解析。

有人會納悶,我們都是用word編輯文字,你卻去解析它。你今天必須解釋一下,解析……是什麼意思?

解析就是解剖並分析。解析word,就是提取word裡面的結構和內容。

舉個例子,不管是考駕照,還是考資格證,我們都有過在裝置上做題的經歷。

這些題目,你之所以能在前端看到,其實是有人在後端一條條錄入的。就像你提交個人資訊報備那樣。

上面的這種方式的錄入,在我們行業,是會被罵的!

儘管你覺得,這並沒有什麼不妥。

因為對於試卷,錄入員拿到的是這樣的原稿。

一套試卷有幾十道題,一道題又有十來個屬性,這麼算來,一套題得填上百個框框(請用重慶話讀:框框兒)。

其實,程式設計師完全可以通過技術手段,自己去讀出word的內容。

理論上,word裡展示的所有內容,程式設計師都能拿到。拿到資料之後,家境差點的公司,通過程式碼邏輯去找試卷名、選項這類屬性。家境好點兒的企業,通過人工智慧的NLP文字分類(我就是這麼幹的),可以更好地實現智慧分類。

反正,兩種途徑都能實現。

而此時,錄入員只負責編輯word就可以了。這個過程就叫做“word匯入”。

能匯入的前提,是可以解析出來word的內容。能解析word的前提,就是docx格式。

二、docx格式:為什麼出現?

不知道您各位有沒有印象,從某個時期開始,微軟辦公檔名的小尾巴多了一個“x”。原來word、PowerPoint還有excel,字尾名分別是doc、ppt、xls。後來,就突然湧現出了docx、pptx、xlsx。

到目前為止,這些x們佔據了主流市場。

為什麼?為什麼要這樣?我彷彿聽到Office家族在抱著掃帚悲傷地唱:你傷害了Word,還Excel而過,你Access的貪婪,我PowerPoint……

原來的doc格式是加密的,只有微軟自己家的軟體才能開啟。

後來微軟覺得,這樣並沒有讓自己很神聖,反而限制了自己的發展。

比如,金山WPS也是搞辦公軟體的,它的發展很快,積累了很多使用者。不少人開始用wps了,word的使用者面臨被瓜分。於是,微軟就基於Office Open XML標準,把文件家族做了相容。

因為新標準是採用的xml格式記錄資訊的,所以就在.doc後面加了個x。新標準之後,即便是微軟建立的文件,WPS也能開啟。這樣,使用者只關心文件就行,不用在意用哪個軟體,這就實現了使用者共享。

反正,是這個意思,我猜的,別全信……

三、檔案結構:怎麼來解析?

那麼,docx格式究竟是什麼樣子的呢?

它看起來是個檔案,其實,它是個壓縮包。

下面就是一個docx文件,是我親手編輯的。

我再親自做一下解壓縮:將.docx改為.zip,右鍵選擇解壓檔案,它就露出原形了。

上圖演示瞭解壓縮,並打開了word/media資料夾,這是文件裡出現的圖片。分析發現,在docx中同一張圖複製多次,media裡面只保留一張原圖。這說明,它是個勤儉節約的好孩子。承載同樣多的內容,docx格式比doc的體積要小。

正如我們看到,解壓後它是有目錄結構的。

放心,我不會挨個把檔案都給你介紹一遍。因為,那並沒有什麼用。即便有用,你也記不住。即便你記得住,我也說不明白……為什麼要學習那麼全面的知識呢?漢字總計10萬多個,常用字只有2500個,夠用了。

我說幾個比較關鍵的點。

3.1 主檔案 document.xml

位於word下的document.xml檔案,是docx的主戰場。可以說,文件中你能看到的所有內容,在這裡都有直接或者間接的記錄。

document.xml是一個XML格式的文件。

我們來開啟它,咱們先只打開一級。

為了你能看下去,我對資料做了處理,保證你只能看到關鍵資訊。

xml <w:document> <w:body> <w:p>...</w:p> <w:p>...</w:p> <w:tbl>...</w:tbl> <w:p>...</w:p> ... <w:sectPr>...</w:sectPr> </w:body> </w:document> 我們看到,body體裡,只有3種標籤,分別是<w:p><w:tbl><w:sectPr>

一個docx文件,基本由這三種成分組成。w指的是wordp指的是paragraph段落,tbl表示table表格。sectPr全稱是section primp,這個千萬別記,容易擾亂思路。

清空你的大腦,docx文件裡就兩種大類,一種叫<w:p>段落,另一種叫<w:tbl>表格。

3.2 段落標籤 w:p

在docx中,段落是最常見的,是文件中最主要的組成單元。

和你理解的段落一樣。不換行就屬於一個段落。即便是“咔咔咔”敲上6個回車,雖然沒有內容,那它也屬於6個段落,在xml中是6個<w:p></w:p>

另外,包含圖片、流程圖、公式等元素的內容,也是包含在段落中的。換句話說,它們都是小弟。

我們能用程式碼取到段落的資訊嗎?當然能!(這自問自答,被懷疑是湊字數)

我們用哪類程式語言都可以做到,因為僅僅就是讀取XML檔案。

但是對於教學而言,用python無疑是最佳的選擇。

```python

匯入解析xml的庫

import xml.dom.minidom as xdom

載入文件

xp = xdom.parse('word/document.xml')

獲取文件根節點

root=xp.documentElement

獲取body節點們

bodys = root.getElementsByTagName("w:body")

因為getElements返回多個物件,我們只有一個

body = bodys[0]

迴圈遍歷body下的節點

for i,ele in enumerate(body.childNodes): e_name = ele.nodeName # 列印{序號} -> {節點名稱} is {物件} print(i,"->",e_name,"is",ele) ``` 我們看一下列印結果,它和源文件完全對應,一個都沒有少。

下面該詳細說一下w:p的小弟們了。

3.3 文字標籤 w:t

在紛雜的xml檔案中,可以扒拉出來一個叫<w:t>標籤。

xml <w:p> <w:r> <w:t>word的doc格式原來是不開源的,後來改成了docx格式後,是開源的。</w:t> …… </w:r> …… </w:p>

這裡面儲存的內容,就是word裡面的文字,t就是text的簡稱。

你在docx裡面看到的每一個字,基本上都是被<w:t></w:r>所包裹的。

也就是說,如果我們拿出所有<w:t>標籤內的文字,我們就做到了純文字docx的解析。

程式碼,其實很簡單,就是在<w:p>元素中,掃描<w:t>的標籤並取出其內容。

上面我們已經拿到了body標籤,所以從那裡繼續。

```python

迴圈body下的大單元 w:p段落,w:tbl表格

for i,ele in enumerate(body.childNodes): # 找到包含w:t的標籤,可能是多個 wts = ele.getElementsByTagName("w:t") ele_text = "" # 記錄大單元內所有文字 for wt in wts: # 迴圈 ele_text = ele_text + wt.text print(ele_text) # 列印輸出 ```

看看程式碼對應的效果。

恭喜你,你已經學會了解析docx的文字了。

是的,提取一個docx的文字,就是這麼簡單。你現在就可以寫一個程式,可以做到把docx轉為txt。

但是……有一點我要告訴你,執行程式碼裡的wt.text可能會報錯,為了便於你理解,我特意寫了虛擬碼。實際上,要從<w:t>中取出文字內容,可以像下面這樣:

```python import re # 匯入正則庫

構建一個正則,去除<>標籤

pattern_del_tag = re.compile(r'<[^>]+>',re.S)

元素轉為xml格式xxx,然後去標籤

t_text = pattern_del_tag.sub('', wt.toxml()) ```

我認為這麼講,你反而能理解。因為,不用在理解什麼叫<w:t>的時候,還要分散注意力到正則表示式。

3.4 連續塊 w:r

上面我們講了如何去解析文字。但是,那太簡單了。

文字是有樣式的。

都在同一段w:p內的文字,它們的樣式,可能不一樣。

比如前兩個字是紅色,那麼這兩個字樣式一樣。但是,後兩個字是綠色,和前面又不一樣。

為了解決這個問題,docx把具有相同樣式的文字,用<w:r>標籤包裹。

r代表run。關於這個run的解釋,很多國內文件都直接翻譯為“執行”。

其實,run在英文中有很多解釋。我覺得在這裡更適合它的釋義應該是:“一段”、“一系列”、“連續上演”。因此,我個人給這個標籤起名叫:連續塊。表示在這個標籤之內的文字,是一個系列的,他們的特點是連續不間斷的。

程式碼依然是處理xml檔案的那一套,不是找標籤就是拿屬性。

還有其他的樣式標籤,你可以自己研究。我這裡先拋磚引玉,舉兩個例子。

比如上面例子中的字型顏色,一般在<w:rPr>標籤內。w依然表示wordr表示runPr表示Primp,是修飾、裝飾的意思。<w:rPr>這個標籤的釋義就是:連續塊的樣式說明。類似的還有<w:pPr>,表示對段落paragraph的樣式說明。<w:tcPr>,表示對錶格單元格table cell的樣式說明。

我們來看一下,連續塊修飾<w:rPr>是如何定義的。

比如對於粗體、斜體的說明。

用程式碼進行判斷,主要是tag的讀取。能找到tag,就說明有此種樣式的標記。 python w_i = w_rPr.getElementsByTagName("w:i") if w_i: print("是斜體") w_b = w_rPr.getElementsByTagName("w:b") if w_i: print("是粗體")

再比如對於各種線條的說明。

上面的例子,如果用程式碼進行判斷的話,除了對 tag存在的判斷,還需要獲取屬性值w:val,表示用了哪一類具體的樣式。 python w_u = w_rPr.getElementsByTagName("w:u") if w_u: # 獲取屬性值用 getAttribute line_value = w_u[0].getAttribute("w:val") if line_value == "single": print("是下劃線") if line_value == "double": print("是雙劃線") if line_value == "wave": print("是波浪形線") if line_value == "dotted": print("是虛線")

我可以很負責任地說,只要是文件中呈現的資訊,在xml檔案中都可以找到對應的標註。

我可以“吧啦吧啦”全告訴你,但是會影響你看其他的內容。有興趣你可以去查資料,都是手冊型別的資料,很方便。

我倒是覺得,你自己在word中標記一下,然後解壓縮觀察xml檔案的變化,這樣子學的更牢。反正我就是這麼學來的。

以下是我用html把一個word文件做了復原。

這是docx原文件:

這是解析docx文件後呈現的html頁面:

我們可以看到,包括字號、字型、字色、標線都可以復原。

只剩下兩個重要的內容沒有說了。那就是圖片和表格。

3.5 影象標籤 w:drawing

docx中的圖片是如何從xml中提取出來的呢?

你在連續塊<w:r>中會發現有一個<w:drawing>標籤。這裡面主要存放的,就是圖畫相關的資訊。

圖片僅僅是<w:drawing>中的一個小分類。除了圖片,還有圖表、形狀、流程圖等。

今天,咱們說個最簡單的,那就是如何提取圖片。

圖片的標籤是<pic:pic>,他在xml中如下定義:

xml <w:drawing> <pic:pic> <pic:blipFill> <a:blip r:embed="rId9"/> </pic:blipFill> </pic:pic> </w:drawing>

其中,圖片檔案就藏在<a:blip r:embed="rId9"/>中,裡面的rId9就是捕獲圖片的線索。

還記不記得解壓縮時,那個media資料夾,裡面有好多圖片。

是有圖片,但這也不是rId9呀?

別急,有個檔案專門做關聯這件事情。它就是解壓縮之後的word/_rels/document.xml.rels檔案。

開啟這個檔案:

``` xml

…… ```

哈哈,這裡有檔案,記錄了哪個id指向哪個檔案。於是,我們解析這個檔案,就可以拿到對應關係。

然後,遇到picid等於rId9的,就把media/image1.png這個圖片檔案展示出來。

這,就實現了圖片的解析。

可以鼓掌了!

3.6 表格標籤 w:tbl

表格標籤<w:tbl>的地位很重,它和段落標籤<w:p>平級。

分析整個文件的頂層組成元素,我們不難發現,除了<w:tbl>就是<w:p>

一開始,我不理解。為什麼圖表、流程圖那麼複雜的元素,卻不配得到表格那麼高的地位。天理何在?

後來,我解析到一個表格後發現。我,好像低估了<w:tbl>大佬的地位。

如果,你看到上面的圖沒有笑噴。那說明,你可能沒有看懂本文。或者是我的笑點出了問題。

單純表格的結構,其實不復雜。但是,表格裡每一個單元格,卻可以容納另一個word文件。表格裡面,圖片、圖表、文字樣式,甚至表格裡再來一個表格,什麼都可以新增,它甚至是包含了<w:p>大佬。<w:tbl><w:p>並列,它反而是委屈的。

我們來看一下表格<w:tbl>的基本結構。

xml <w:tbl> <w:tblGrid> <w:gridCol/> <w:gridCol/> </w:tblGrid> <w:tr> <w:tc><w:p>...</w:p></w:tc> <w:tc>...</w:tc> </w:tr> <w:tr> ... </w:tr> </w:tbl>

節點元素中,主要有兩部分內容,一個<w:tblGrid>介紹表格列的個數。另一個是<w:tr>包含表格行的資訊。 trtable row的縮寫。

其中,<w:tr>裡面有<w:tc>,這裡面是格子的內容。我們發現其內容,居然是一個它的兄弟節點<w:p>。在此,給表格大佬深深地鞠上一躬。

tctable cell的縮寫,表示單元格的意思。有人可能會說,tctable column的縮寫,表示表格的列。當我後面論述帶有合併單元格的表格時,從結構上看,我們會感覺table cell更適合語境。

先說基本表格的解析,如下圖所示:

解析的程式碼參考如下:

``` python w_table = body.childNodes[0] # 拿到表格節點

獲取所有的行

w_trs = w_table.getElementsByTagName("w:tr") rows_text = [] # 存放行的文字 for r_index, tr in enumerate(w_trs): # 獲取所有的單元格 cells = tr.getElementsByTagName("w:tc") cells_text = [] # 存放單元格的文字 for c_index, cell in enumerate(cells): # 獲取所有的文字 wts = cell.getElementsByTagName("w:t") for wt in wts: # 把文字拼接 cells_text.append(wt.text) rows_text.append(cells_text) print(rows_text) # 列印結果,二維陣列[[r1c1,r1c2],[r2c1,r2c2]] ```

執行一下程式碼,結果如下:

這裡面我又挖坑了,除了前面講w:t時的wt.text陷阱之外。w:tc的內容應該是去解析完整的w:p結構,而這裡我只取了w:t文字。這樣最簡單。因為,我們是要理解表格的結構。因此,其他的可以假裝看不見。

但是,也由此可見,做好表格解析的前提,是做好段落解析。因為表格單元格里是段落。

我相信,有了表格的解析結果(二維陣列),你很容易就可以把它還原成表格頁面用於展示。只需要迴圈就好,第一層迴圈行,第二層迴圈列。

我還是用1分鐘寫一下程式碼吧:

``` python rows_text = [['名稱', '字尾名'], ['Word', 'docx']] table_html = [""] for row in rows_text: table_html.append("") for cell in row: table_html.append("") table_html.append("") table_html.append("

"+cell+"
") table_html = "".join(table_html) print(table_html)

名稱字尾名
Worddocx

```

據我所知,世界上的表格除了上面那種簡單的,還有一些稍微複雜的。那就是帶有合併單元格的表格。

比如下面這種:

這類表格的解析稍微複雜一些,它們屬於複雜裡面最簡單的。

我們來看看他們的xml資料是如何定義的。

首先看帶有跨行的表格的例子。

對於跨行的情況,我們發現表格的xml資料,該有的行和列,數量都沒有變。只是在要合併的單元格上標記了一個<w:vMerge>標籤。

這個標籤表示有跨行的單元格。v表示vertical,是豎直方向的意思。vMerge表示豎直合併。這個標籤裡面還有屬性值w:val,當值為restart時表示此單元格開始出現合併,continue表示此單元格沒有結束,繼續保持,直到遇到非continue情況。

再來看看跨列的情況。

跨列因為發生在行內,是行內矛盾,不影響其他行。所以,我們看到只有在第1行的第1格中,採用<w:gridSpan>標籤,說明本行有跨列的單元格。val值是2,表示跨2個單位。

為什麼我前面說,tctable cell的縮寫。我的依據就在這裡。其實這個表格的結構是2行2列。如果c指的是column的話,它應該有2個<w:tc>,後一個複用前一個。但是,我們看上圖裡的結構,它只有一個<w:tc>。那我們稱呼它叫單元格更貼切,因為它只有一個框。你可以反駁我,我會立馬說,你說的對,但以後依然稱呼它單元格。

帶有合併單元格的頁面還原,邏輯稍微複雜。但是底層還是和普通表格一樣,通過迴圈行和列。你只需要遇到合併的時候,搞一個colspan=2或者rowspan=3就行。

此處,我不再給大家貼程式碼了。原理已經講得很透徹了,算是留一個作業。你可以自己思考一下,會有更多的收穫。

唉呀,還是給大家一個小提示吧,僅供參考。因為我搞合併單元格時,花費了2天時間才完成。我把一個關鍵點提供給大家:對於複雜表格,最終的資料結構用一維陣列更恰當。這個結構可以像這樣[{"x_start":0, "x_end":1, "y_start":0, "y_end":0, "content":"不要問我"}]

如果你看完了,2個小時還搞不定。那請您不要來問我,問我也是回覆一個狗頭表情。

四、其他妙用:藏初戀照片?

首先宣告,這招我沒用過啊。我只是論述它的可行性。

我的很多讀者都結婚了,沒結婚也有女朋友了,他們還都怕老婆。

但是,誰沒有熱血青春呢?

有人就想存著初戀照片,夜深人靜的時候看一看。但是,又怕被現任發現。

怎麼辦?手機相簿不安全!檔案加密碼或者修改後綴名屬於不打自招。

那麼,你千萬不要放到docx中。它只不過是個壓縮包。解壓縮之後,加點檔案進去,改回來還是正常的文件。看不出來有任何問題,經得起組織考驗和檢查。

我有這麼個文件,很清白的一個文件。

解壓縮之後,裡面包含有圖片,甚至pdf都有。

唉,程式設計師的思維境界就是這麼的樸實無華又低階趣味。要是被銷售員看到,他們該引發商業戰了。

我是ITF男孩,在掘金是TF男孩,但你……帶你從IT視角看世界。