docx格式文件詳解:xml解析並用html還原
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
指的是word
,p
指的是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
依然表示word
,r
表示run
,Pr
表示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
指向哪個檔案。於是,我們解析這個檔案,就可以拿到對應關係。
然後,遇到pic
中id
等於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>
包含表格行的資訊。 tr
是table row
的縮寫。
其中,<w:tr>
裡面有<w:tc>
,這裡面是格子的內容。我們發現其內容,居然是一個它的兄弟節點<w:p>
。在此,給表格大佬深深地鞠上一躬。
tc
是table cell
的縮寫,表示單元格的意思。有人可能會說,tc
是table 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+" |
名稱 | 字尾名 |
Word | docx |
```
據我所知,世界上的表格除了上面那種簡單的,還有一些稍微複雜的。那就是帶有合併單元格的表格。
比如下面這種:
這類表格的解析稍微複雜一些,它們屬於複雜裡面最簡單的。
我們來看看他們的xml
資料是如何定義的。
首先看帶有跨行的表格的例子。
對於跨行的情況,我們發現表格的xml
資料,該有的行和列,數量都沒有變。只是在要合併的單元格上標記了一個<w:vMerge>
標籤。
這個標籤表示有跨行的單元格。v
表示vertical
,是豎直方向的意思。vMerge
表示豎直合併。這個標籤裡面還有屬性值w:val
,當值為restart
時表示此單元格開始出現合併,continue
表示此單元格沒有結束,繼續保持,直到遇到非continue
情況。
再來看看跨列的情況。
跨列因為發生在行內,是行內矛盾,不影響其他行。所以,我們看到只有在第1行的第1格中,採用<w:gridSpan>
標籤,說明本行有跨列的單元格。val
值是2
,表示跨2
個單位。
為什麼我前面說,tc
是table 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視角看世界。
- 60分鐘的文心一言釋出會:我帶你5分鐘看完
- 程式設計師寫小說:我甘心當韭菜
- docx格式文件詳解:xml解析並用html還原
- 評論二則:年齡大了能否寫程式碼、上大學不喜歡IT專業咋整
- ChatGPT火了,我連夜詳解AIGC原理,並實戰生成動漫頭像
- 咱不吃虧,也不能過度自衛
- 兔年了,利用AI風格化實現剪紙兔、年畫兔、煙花兔
- 傻嗎?談男人們飯桌的拼酒現象
- 他扔出一張對話截圖:王總說的
- 老張說:快過年了,搞個AI作曲,用TensorFlow訓練midi檔案
- 為什麼大家都看中學歷?
- 年底了,裁兄弟當職員那哥們兒,如今咋樣了?
- 老張讓我用TensorFlow識別語音命令:前進、停止、左轉、右轉
- 在掘金第一次記錄失眠
- 十五分鐘簡介人工智慧,以聽懂為目的
- 認知史 12:積累階段,沒資格攀比
- 認知史 16:人過留名,雁過留聲
- 認知史 4:普遍共識
- 一文講通OCR文字識別原理與技術全流程(科普版)
- 認知史 7:選擇和努力