1. 程式人生 > >Android使用ApachePOI元件讀寫Worddoc和docx檔案【總結不錯】

Android使用ApachePOI元件讀寫Worddoc和docx檔案【總結不錯】

開發十年,就只剩下這套架構體系了! >>>   

最近在專案中要生成Word的doc和docx檔案,一番百度google之後,發現通過java語言實現的主流是Apache的POI元件。除了POI,這裡還有另一種實現,不過我沒有去研究,有興趣的同學可以研究研究。

關於POI可以訪問Apache POI的官網獲取詳細的資訊。

進入主題!

由於專案中只是用到了doc和docx的元件,下面也只是介紹這兩個元件的使用

一、在Android Studio中如何用POI元件

從POI官網上看,貌似暫並不支援IntelliJ IDE,如下圖,所以這裡我們採用直接下載jar包並匯入專案的方式。

官網how to build

通過官網 ->Overview->Components,可以看到 d和docx檔案分別對應著元件HWPFXWPF,而HWPF和XWPF則對應著poi-scratchpad和poi-ooxml

檔案型別 元件名 MavenId
doc HWPF poi-scratchpad
docx XWPF poi-ooxml

 

Components Map


下載

進入Apache下載頁面,選擇最新版下載,如下。選擇The latest beta release is Apache POI 3.16-beta2會跳轉到poi-bin-3.16-beta2-20170202.tar.gz,然後點選poi-bin-3.16-beta2-20170202.tar.gz,選擇映象後即可成功下載。


linux系統選擇.tar.gz
windows系統選擇.zip

 

POI下載頁面


解壓

將下載後的壓縮包解壓,會得到以下檔案。

檔案(夾)名 作用
docs 文件(包括API文件和如何使用及版本資訊)
lib doc功能實現依賴的包
ooxml-lib docx功能實現依賴的包
LICENSE  
NOTICE  
poi-3.16-beta2.jar The prerequisite poi-scratchpad-3.16-beta2.jar
poi-examples-3.16-beta2.jar 不明確
poi-excelant-3.16-beta2.jar excel功能實現
poi-ooxml-3.16-beta2.jar docx功能實現
poi-ooxml-schemas-3.16-beta2.jar The prerequisite of poi-ooxml-3.16-beta2.jar
poi-scratchpad-3.16-beta2.jar doc功能實現

 

解壓後的poi包


匯入

不熟悉怎麼匯入的同學可以看看Android Studio匯入jar包教程
1、doc
對於doc檔案,需要將lib資料夾下jar包,poi-3.16-beta2.jar,poi-scratchpad-3.16-beta2.jar放入android專案libs目錄下(lib資料夾下的junit-4.12.jar和log4j-1.2.17.jar不放我的專案也沒出現異常,能少點是點)。

 

專案中的libs

2、docx
對於docx,需要匯入lib資料夾下jar包,poi-3.16-beta2.jar,poi-ooxml-3.16-beta2.jar,poi-ooxml-schemas-3.16-beta2.jar和ooxml-lib下的包,由於一直我這一直出現Warning:Ingoring InnerClasses attribute for an anonymous inner class的錯誤,同時由於doc基本滿足我的需求以及匯入這麼多jar導致apk體積增大,就沒有去實現。
有興趣的同學可以研究研究。


二、實現doc檔案的讀寫

Apache POI中的HWPF模組是專門用來讀取和生成doc格式的檔案。在HWPF中,我們使用HWPFDocument來表示一個word doc文件。在看程式碼之前,有必要了解HWPFDocument中的幾個概念:

名稱 含義
Range 表示一個範圍,這個範圍可以是整個文件,也可以是裡面的某個小節(Section),也可以是段落(Paragraph),還可以是擁有功能屬性的一段文字(CharacterRun)
Section word文件的一個小節,一個word文件可以由多個小節構成。
Paragraph word文件的一個段落,一個小節可以由多個段落構成。
CharacterRun 具有相同屬性的一段文字,一個段落可以由多個CharacterRun組成。
Table 一個表格。
TableRow 表格對應的行
TableCell 表格對應的單元格

注意 : Section、Paragraph、CharacterRun和Table都繼承自Range。


讀寫前注意Apache POI 提供的HWPFDocument類只能讀寫規範的.doc檔案,也就是說假如你使用修改 字尾名 的方式生成doc檔案或者直接以命名的方式建立,將會出現錯誤“Your file appears not to be a valid OLE2 document”

Invalid header signature; read 0x7267617266202E31, expected 0xE11AB1A1E011CFD0 - Your file appears not to be a valid OLE2 document 

DOC讀

讀doc檔案有兩種方式
(a)通過WordExtractor讀檔案
(b)通過HWPFDocument讀檔案

在日常應用中,我們從word檔案裡面讀取資訊的情況非常少見,更多的還是把內容寫入到word檔案中。使用POI從word doc檔案讀取資料時主要有兩種方式:通過WordExtractor讀和通過HWPFDocument讀。在WordExtractor內部進行資訊讀取時還是通過HWPFDocument來獲取的。

使用WordExtractor讀

在使用WordExtractor讀檔案時我們只能讀到檔案的文字內容和基於文件的一些屬性,至於文件內容的屬性等是無法讀到的。如果要讀到文件內容的屬性則需要使用HWPFDocument來讀取了。下面是使用WordExtractor讀取檔案的一個示例:

//通過WordExtractor讀檔案
public class WordExtractorTest {

   private final String PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "test.doc");
   private static final String TAG = "WordExtractorTest";
   
   private void log(Object o) {
       Log.d(TAG, String.valueOf(o));
   }

   public void testReadByExtractor() throws Exception {
      InputStream is = new FileInputStream(PATH);
      WordExtractor extractor = new WordExtractor(is);
      //輸出word文件所有的文字
      log(extractor.getText());
      log(extractor.getTextFromPieces());
      //輸出頁首的內容
      log("頁首:" + extractor.getHeaderText());
      //輸出頁尾的內容
      log("頁尾:" + extractor.getFooterText());
      //輸出當前word文件的元資料資訊,包括作者、文件的修改時間等。
      log(extractor.getMetadataTextExtractor().getText());
      //獲取各個段落的文字
      String paraTexts[] = extractor.getParagraphText();
      for (int i=0; i<paraTexts.length; i++) {
         log("Paragraph " + (i+1) + " : " + paraTexts[i]);
      }
      //輸出當前word的一些資訊
      printInfo(extractor.getSummaryInformation());
      //輸出當前word的一些資訊
      this.printInfo(extractor.getDocSummaryInformation());
      this.closeStream(is);
   }
  
   /**
    * 輸出SummaryInfomation
    * @param info
    */
   private void printInfo(SummaryInformation info) {
      //作者
      log(info.getAuthor());
      //字元統計
      log(info.getCharCount());
      //頁數
      log(info.getPageCount());
      //標題
      log(info.getTitle());
      //主題
      log(info.getSubject());
   }
  
   /**
    * 輸出DocumentSummaryInfomation
    * @param info
    */
   private void printInfo(DocumentSummaryInformation info) {
      //分類
      log(info.getCategory());
      //公司
      log(info.getCompany());
   }
  
   /**
    * 關閉輸入流
    * @param is
    */
   private void closeStream(InputStream is) {
      if (is != null) {
         try {
            is.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }
}

使用HWPFDocument讀

HWPFDocument是當前Word文件的代表,它的功能比WordExtractor要強。通過它我們可以讀取文件中的表格、列表等,還可以對文件的內容進行新增、修改和刪除操作。只是在進行完這些新增、修改和刪除後相關資訊是儲存在HWPFDocument中的,也就是說我們改變的是HWPFDocument,而不是磁碟上的檔案。如果要使這些修改生效的話,我們可以呼叫HWPFDocument的write方法把修改後的HWPFDocument輸出到指定的輸出流中。這可以是原檔案的輸出流,也可以是新檔案的輸出流(相當於另存為)或其它輸出流。下面是一個通過HWPFDocument讀檔案的示例:

//使用HWPFDocument讀檔案
public class HWPFDocumentTest {
  
   private final String PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "test.doc");
   private static final String TAG = "HWPFDocumentTest";
   
   private void log(Object o) {
       Log.d(TAG, String.valueOf(o));
   }

   public void testReadByDoc() throws Exception {
      InputStream is = new FileInputStream(PATH);
      HWPFDocument doc = new HWPFDocument(is);
      //輸出書籤資訊
      this.printInfo(doc.getBookmarks());
      //輸出文字
      log(doc.getDocumentText());
      Range range = doc.getRange();
      //讀整體
      this.printInfo(range);
      //讀表格
      this.readTable(range);
      //讀列表
      this.readList(range);
      this.closeStream(is);
   }
  
   /**
    * 關閉輸入流
    * @param is
    */
   private void closeStream(InputStream is) {
      if (is != null) {
         try {
            is.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }
  
   /**
    * 輸出書籤資訊
    * @param bookmarks
    */
   private void printInfo(Bookmarks bookmarks) {
      int count = bookmarks.getBookmarksCount();
      log("書籤數量:" + count);
      Bookmark bookmark;
      for (int i=0; i<count; i++) {
         bookmark = bookmarks.getBookmark(i);
         log("書籤" + (i+1) + "的名稱是:" + bookmark.getName());
         log("開始位置:" + bookmark.getStart());
         log("結束位置:" + bookmark.getEnd());
      }
   }
  
   /**
    * 讀表格
    * 每一個回車符代表一個段落,所以對於表格而言,每一個單元格至少包含一個段落,每行結束都是一個段落。
    * @param range
    */
   private void readTable(Range range) {
      //遍歷range範圍內的table。
      TableIterator tableIter = new TableIterator(range);
      Table table;
      TableRow row;
      TableCell cell;
      while (tableIter.hasNext()) {
         table = tableIter.next();
         int rowNum = table.numRows();
         for (int j=0; j<rowNum; j++) {
            row = table.getRow(j);
            int cellNum = row.numCells();
            for (int k=0; k<cellNum; k++) {
                cell = row.getCell(k);
                //輸出單元格的文字
                log(cell.text().trim());
            }
         }
      }
   }
  
   /**
    * 讀列表
    * @param range
    */
   private void readList(Range range) {
      int num = range.numParagraphs();
      Paragraph para;
      for (int i=0; i<num; i++) {
         para = range.getParagraph(i);
         if (para.isInList()) {
            log("list: " + para.text());
         }
      }
   }
  
   /**
    * 輸出Range
    * @param range
    */
   private void printInfo(Range range) {
      //獲取段落數
      int paraNum = range.numParagraphs();
      log(paraNum);
      for (int i=0; i<paraNum; i++) {
         log("段落" + (i+1) + ":" + range.getParagraph(i).text());
         if (i == (paraNum-1)) {
            this.insertInfo(range.getParagraph(i));
         }
      }
      int secNum = range.numSections();
      log(secNum);
      Section section;
      for (int i=0; i<secNum; i++) {
         section = range.getSection(i);
         log(section.getMarginLeft());
         log(section.getMarginRight());
         log(section.getMarginTop());
         log(section.getMarginBottom());
         log(section.getPageHeight());
         log(section.text());
      }
   }
  
   /**
    * 插入內容到Range,這裡只會寫到記憶體中
    * @param range
    */
   private void insertInfo(Range range) {
      range.insertAfter("Hello");
   }
}

DOC寫

使用HWPFDocument寫檔案

在使用POI寫word doc檔案的時候我們必須要先有一個doc檔案才行,因為我們在寫doc檔案的時候是通過HWPFDocument來寫的,而HWPFDocument是要依附於一個doc檔案的。所以通常的做法是我們先在硬碟上準備好一個內容空白的doc檔案,然後建立一個基於該空白檔案的HWPFDocument。之後我們就可以往HWPFDocument裡面新增內容了,然後再把它寫入到另外一個doc檔案中,這樣就相當於我們使用POI生成了word doc檔案。

//寫字串進word
    InputStream is = new FileInputStream(PATH);
    HWPFDocument doc = new HWPFDocument(is);

    //獲取Range
    Range range = doc.getRange();
    for(int i = 0; i < 100; i++) {
        if( i % 2 == 0 ) {
            range.insertAfter("Hello " + i + "\n");//在檔案末尾插入String
        } else {
            range.insertBefore("      Bye " + i + "\n");//在檔案頭插入String
        }
    }
    //寫到原檔案中
    OutputStream os = new FileOutputStream(PATH);
    //寫到另一個檔案中
    //OutputStream os = new FileOutputStream(其他路徑);
    doc.write(os);
    this.closeStream(is);
    this.closeStream(os);

但是,在實際應用中,我們在生成word檔案的時候都是生成某一類檔案,該類檔案的格式是固定的,只是某些欄位不一樣罷了。所以在實際應用中,我們大可不必將整個word檔案的內容都通過HWPFDocument生成。而是先在磁碟上新建一個word文件,其內容就是我們需要生成的word檔案的內容,然後把裡面一些屬於變數的內容使用類似於“${paramName}”這樣的方式代替。這樣我們在基於某些資訊生成word檔案的時候,只需要獲取基於該word檔案的HWPFDocument,然後呼叫Range的replaceText()方法把對應的變數替換為對應的值即可,之後再把當前的HWPFDocument寫入到新的輸出流中。這種方式在實際應用中用的比較多,因為它不但可以減少我們的工作量,還可以讓文字的格式更加的清晰。下面我們就來基於這種方式做一個示例。

假設我們有個模板是這樣的:

 

 

doc模板

 

之後我們以該檔案作為模板,利用相關資料把裡面的變數進行替換,然後把替換後的文件輸出到另一個doc檔案中。具體做法如下:

public class HWPFTemplateTest {
    /**
    * 用一個doc文件作為模板,然後替換其中的內容,再寫入目標文件中。
    * @throws Exception
    */
    
     @Test
   public void testTemplateWrite() throws Exception {
      String templatePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "template.doc");

      String targetPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "target.doc";
      InputStream is = new FileInputStream(templatePath);
      HWPFDocument doc = new HWPFDocument(is);
      Range range = doc.getRange();
      //把range範圍內的${reportDate}替換為當前的日期
      range.replaceText("${reportDate}", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
      range.replaceText("${appleAmt}", "100.00");
      range.replaceText("${bananaAmt}", "200.00");
      range.replaceText("${totalAmt}", "300.00");
      OutputStream os = new FileOutputStream(targetPath);
      //把doc輸出到輸出流中
      doc.write(os);
      this.closeStream(os);
      this.closeStream(is);
   }
  
   /**
    * 關閉輸入流
    * @param is
    */
   private void closeStream(InputStream is) {
      if (is != null) {
         try {
            is.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }
 
   /**
    * 關閉輸出流
    * @param os
    */
   private void closeStream(OutputStream os) {
      if (os != null) {
         try {
            os.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }
}

三、實現docx檔案的讀寫

POI在讀寫word docx檔案時是通過xwpf模組來進行的,其核心是XWPFDocument。一個XWPFDocument代表一個docx文件,其可以用來讀docx文件,也可以用來寫docx文件。XWPFDocument中主要包含下面這幾種物件:

物件 含義
XWPFParagraph 代表一個段落
XWPFRun 代表具有相同屬性的一段文字
XWPFTable 代表一個表格
XWPFTableRow 表格的一行
XWPFTableCell 表格對應的一個單元格

同時XWPFDocument可以直接new一個docx檔案出來而不需要像HWPFDocument一樣需要一個模板存在。

具體可以參考這位同學寫的POI讀寫docx檔案


四、總結

歡迎大家提出建議和糾正本文可能存在的錯誤之處,感謝支援。



作者:猿溼Xoong
連結:https://www.jianshu.com/p/8d23b7f54b8e
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授