Java將數據寫入word文檔(.doc)


Java可用org.apache.poi包來操作word文檔。org.apache.poi包可於官網上下載,解壓后各jar作用如下圖所示:
poi包中的各jar用途
可根據需求導入對應的jar。

一、HWPFDocument類的使用
用HWPFDocument類將數據寫到指定的word文檔中,基本思路是這樣的:
- 首先,建立一個HWPFDocument類的實例,關聯到一個臨時的word文檔;
- 然后,通過Range類實例,將數據寫入這個word文檔中;
- 接着,將這個臨時的word文檔通過write函數寫入指定的word文檔中。
- 最后,關閉所有資源。
下面詳細說明各個步驟。
1.構造函數
這里要說明一下,經過試驗,暫時還沒找到直接在程序中新建一個word文檔並讀取的方法,只能先創建好temp.doc,然后在程序中讀取。(用File-createNewFile和POIFSFileSystem.create創建出來的.doc文件,都不能被正確讀取)
另外,其實選擇哪種參數傳入都是一樣的,畢竟HWPFDocument關聯的word文檔是無法寫入的,只是當作一個臨時文件。所以,選擇最為熟悉的InputStream較為合適。
參數1:InputStream。可將word文檔用FileInputStream流讀取,然后傳入HWPFDocument類。主要用於讀取word文檔中的數據。
參數2:POIFSFileSystem。POIFSFileSystem的構造函數參數可以是(File,boolean)【這樣的話file必須是已經存在的】,后者為false時可讀寫。這里可以寫為

HWPFDocument doc = new HWPFDocument(new POIFSFileSystem(new File("temp.doc"),false));

2.Range類
(1)獲取Range類實例。
HWPFDocument類中有一系列獲取Range類實例以操作word文檔的方法。比較常用的是getRange(),這個方法可以獲取涵蓋整個文檔的范圍,但不包括任何頁眉和頁腳。

Range range = doc.getRange();

此外,還有獲取所有文本范圍的getOverallRange()、獲取所有文本框的getMainTextboxRange()等等,具體可以根據需求查閱文檔。

(2)Range類操作word文檔
Range類中有大量獲取文檔數據的方法,若有需要可以查閱文檔。這里只說明與寫入數據有關的方法。
1. insertBefore(String),將字符串插入到此range的開頭。返回值類型:CharacterRun
2. insertAfter(String),將字符串插入到此range的結尾。返回值類型:CharacterRun
3. insertTableBefore(short列數, int行數),在此range的開頭插入一個指定行列數的表。返回值類型:Table
4. text(),獲取當前range的所有文本。返回值類型:String。雖然不是寫入數據的方法,但是在調試過程中比較好用。

3.write方法
HWPFDocument類中的write方法有三種重載形式:(實際上可以理解為writeTo)
參數1:空參數。將本對象關聯的word文檔寫入另一個打開的可寫的POIFSFileSystem文件中。
參數2:File。將本對象關聯的word文檔寫入指定的文件(newFile)中。如果該文件不存在,則創建;若存在,則覆蓋。
參數3:OutputStream。將本對象關聯的word文檔寫入指定的字節輸出流中。
可以根據需求選擇,但是最好還是選擇OutputStream,因為輸出流的操作空間更大。參數2的newFile不能續寫,只能覆蓋。
可以將其直接寫入目標文件的輸出流,也可以先寫入一個字節數組輸出流,在通過字節數組輸出流寫入到目標文件輸出流中。

4.關閉資源
- 關閉doc.close();,也即是關閉doc所使用的資源”temp.doc”
- 關閉將數據寫入指定word文檔的輸出流

二、代碼示例

    /**
     * @description 將數據歸檔到.doc的word文檔中。數據續寫到原目標文件末尾。
     * @param source
     *            源文件(必須存在!)
     * @param sourChs
     *            讀取源文件要用的編碼,若傳入null,則默認是GBK編碼
     * @param target
     *            目標word文檔(必須存在!)
     */
    public static void storeDoc(File source, String sourChs, File target) {
        /*
         * 思路: 1.建立字符輸入流,讀取source中的數據。 2.在目標文件路徑下new File:temp.doc
         * 3.將目標文件重命名為temp.doc,並用HWPFDocument類關聯(temp.doc)。
         * 3.由temp.doc建立Range對象,寫入source中的數據。 4.建立字節輸出流,關聯target。
         * 5.將range中的數據寫入關聯target的字節輸出流。
         */
        if (!target.exists()) {
            throw new RuntimeException("目標文件不存在!");
        }
        if (sourChs == null) {
            sourChs = "GBK";
        }
        BufferedReader in = null;
        HWPFDocument temp = null;
        BufferedOutputStream out = null;
        String path = target.getParent();
        File tempDoc = new File(path, "temp.doc");
        target.renameTo(tempDoc);
        try {
            in = new BufferedReader(new InputStreamReader(new FileInputStream(source), sourChs));
            temp = new HWPFDocument(new BufferedInputStream(new FileInputStream(tempDoc)));
            out = new BufferedOutputStream(new FileOutputStream(target));
            Range range = temp.getRange();
            String line = null;
            range.insertAfter(getDate(12));
            range.insertAfter("\r");
            while ((line = in.readLine()) != null) {
                range.insertAfter(line);
                range.insertAfter("\r"); // word中\r是換行符
            }
            range.insertAfter("\r");
            range.insertAfter("\r");
            temp.write(out);
        } catch (UnsupportedEncodingException e) {
            // TODO 自動生成的 catch 塊
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            // TODO 自動生成的 catch 塊
            e.printStackTrace();
        } catch (IOException e) {
            // TODO 自動生成的 catch 塊
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // TODO 自動生成的 catch 塊
                    e.printStackTrace();
                }
            }
            if (temp != null) {
                try {
                    temp.close();
                } catch (IOException e) {
                    // TODO 自動生成的 catch 塊
                    e.printStackTrace();
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    // TODO 自動生成的 catch 塊
                    e.printStackTrace();
                }
            }
            tempDoc.deleteOnExit();
        }
    }

三、調試記錄
1.關於創建臨時temp.doc的嘗試
目的:在程序開始時創建一個temp.doc,程序結束后刪除。

嘗試1:用File-createNewFile創建出文件,然后用POIFSFileSystem構造函數打開這個文件, 然后用HWPFDocument關聯到這個POIFSFileSystem類實例。
結果:org.apache.poi.EmptyFileException: The supplied file was empty (zero bytes long)。創建出的文件是0字節空文件,不能被POIFSFileSystem打開。

嘗試2:用POIFSFileSystem.create(file)靜態函數創建POIFSFileSystem實例,然后用 HWPFDocument關聯。
結果:java.io.FileNotFoundException: no such entry: “WordDocument”, had: []。文件 是創建成功了,也用POIFSFileSystem成功載入,但是HWPFDocument無法接收這個參數。

結論:目前只能用Office軟件創建的word文檔才行。也就是說暫時還沒找到直接在程序中新建 一個word文檔的方法,只能先創建好temp.doc,然后在程序中讀取。

2.無法寫入temp.doc

描述:用doc.getRange()方法獲取文件的整個范圍,然后用range.insertAfter(String)方法 插入數據。編譯運行沒有任何異常,但是打開文件發現還是原樣。

嘗試:插入數據后用range.text()獲取當前range的所有文本並顯示在控制台上,發現數據的 確是成功插入到了range中,但是temp.doc依然沒有任何變化。

猜測:可能是文件讀取到HWPFDocument的方式不對,只讀不可寫入。
也有可能是range中的內容並不會改變.doc的內容,必須doc.write(*)寫入到另一個文件中才 行。
嘗試:通過各種方式(inputstream,poifsfilesystem,(poifs,readonly))載入temp.doc,結 果都是一樣。於是開始嘗試第二種猜測。

3.加入doc.write(*)方法后,運行報錯,找不到需要的類文件(編譯正常)。
詳情:只加了這一句話,這句話報錯:doc.write(out):java.lang.NoClassDefFoundError錯 誤
分析:NoClassDefFoundError發生在編譯時對應的類可用,而運行時在Java的classpath路徑 中,對應的類不可用導致的錯誤。
解決:要注意看報錯的提示:
Exception in thread “main” java.lang.NoClassDefFoundError: org/apache/commons/collections4/bidimap/TreeBidiMap……
可以看出,是org.apache.commons.collections4包找不到導致的。導入這個包即可。
收獲:使用外部jar包時,並不是只把所有代碼里用到的類所在的jar包導入就萬事大吉了,經 常是代碼中用到的類里需要用到其他包中的類。如果在運行時報錯,要注意看報錯提示,根據 提示導入相關的包。
就這樣一個簡答的小bug卡了我半天,以后代碼出錯時不要只看錯誤類型,一定要細看報錯的 描述。

4.無法寫入目標文件
詳情:續2,通過doc.write(out)方法將數據寫到字節輸出流,目標文件毫無變化。

嘗試:用doc.write方法將數據寫到字節數組,看看數據是否真的被輸出了(如果是,就說明 是數據寫入目標文件的過程中出了問題,而不是doc.write輸出的問題)
結果:在輸出的內容中找到了想要輸出的數據。由此說明,前面的一切都沒問題了,問題出在 把數據寫入word文檔上。

嘗試:將目標文件刪除,讓程序創建出一個。結果,寫入成功。那么問題來了:

5.目標文件無法續寫
詳情:由程序自己創建出的word文檔可以寫入,但已存在的word文檔無法續寫。即使是程序自 己創建出的word文檔,也只能寫入一次,無法續寫。

分析:Range輸出的數據是帶有word文檔的創建信息和格式數據的,這些內容對於已存在的 word文檔不適用。
現在的情況是:用於創建HWPFDocument對象的temp.doc必須手動創建;目標文件必須由代碼生 成,且生成后只能用代碼寫入一次。
將數據寫入指定word文檔的流程是:用getRange()方法獲得臨時文件數據(其實是為了獲取 word文檔的創建信息、格式數據),然后將源文件數據寫入range,最后將range寫入目標文件 的字節輸出流。
既然如此,為何不直接將臨時文件的來源設為目標文件呢?這樣getRange所獲取的range就能 同時包含目標文件中的原有文本數據,再在其后添加源文件中的內容,然后將整個range寫入 由代碼新創建的目標文件,不就是另一種意義上的續寫嗎?這樣既避免了手動創建temp.doc, 又能實現續寫,還能讓避免產生垃圾文件(無意義的temp.doc)

解決:考慮到輸入輸出的沖突問題,先將目標文件重命名為temp.doc,然后由程序新創建出一 個空的目標文件。如果需要續寫,就直接用getRange方法獲取原來的所有數據;如果不需要續 寫,就用Range(0,0,tempdoc)獲取一個空的range,只帶有格式和創建信息。將所有源數據寫 入range后,用temp.write(out)將range中的數據寫入新創建的目標文件。

6.如何不續寫?
思路:由傳入的參數,如果不續寫,就用range.replaceText(“”,false),將整個range清空, 然后再往后插入需要的內容。

問題1:角標越界異常
分析:將整個range清空會導致無法插入,可以將整個range改為”tobedeleted"range.replaceText("{tobedeleted}”,”“)刪掉標志即可。

問題2:做了上述操作后,仍然是續寫,原range並沒有清空。
分析:經過一系列測試,發現原因:用程序寫入的文本,用range.text()讀取不到,當然用 range.replaceText也無法操作了。而在程序寫入后,隨便手動在文件中寫點東西然后保存, 再用range.text()就可以讀取到了。

結論:這可能是包的固有bug之二,暫時無法解決。

四、其他
其實,poi包對於word文檔來說,主要功能還是讀取,寫入功能很初級、不完善。poi只能操作最簡單的word格式內容,當要求的樣式復雜、文檔長度較長時,用poi就較難完成要求。
這時,Jacob是一個更好的選擇。Jacob能完整保持復雜的格式內容,操作也更為方便。但Jacob也有個缺點:只能在Windows平台下實現,無法在linux平台下實現。
此外,要生成標准格式的word文檔,還有一種思路,是在另一篇博客上看到的:

先用office2003或者2007編輯好word的樣式,然后另存為xml,將xml翻譯為FreeMarker模板,最后用java來解析FreeMarker模板並輸出Doc。經測試這樣方式生成的word文檔完全符合office標准,樣式、內容控制非常便利,打印也不會變形,生成的文檔和office中編輯文檔完全一樣。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM