Android使用ApachePOI組件讀寫(xiě)Worddoc和docx文件

本文章已同步至微信公眾號(hào):是天哥哥啊

最近在項(xiàng)目中要生成Word的doc和docx文件,一番百度google之后,發(fā)現(xiàn)通過(guò)java語(yǔ)言實(shí)現(xiàn)的主流是Apache的POI組件。除了POI,這里還有另一種實(shí)現(xiàn),不過(guò)我沒(méi)有去研究,有興趣的同學(xué)可以研究研究。

關(guān)于POI可以訪(fǎng)問(wèn)Apache POI的官網(wǎng)獲取詳細(xì)的信息。

進(jìn)入主題!

由于項(xiàng)目中只是用到了doc和docx的組件,下面也只是介紹這兩個(gè)組件的使用

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

從POI官網(wǎng)上看,貌似暫并不支持IntelliJ IDE,如下圖,所以這里我們采用直接下載jar包并導(dǎo)入項(xiàng)目的方式。

官網(wǎng)how to build

通過(guò)官網(wǎng) ->Overview->Components,可以看到 d和docx文件分別對(duì)應(yīng)著組件HWPFXWPF,而HWPF和XWPF則對(duì)應(yīng)著poi-scratchpad和poi-ooxml

文件類(lèi)型 組件名 MavenId
doc HWPF poi-scratchpad
docx XWPF poi-ooxml
Components Map

下載

進(jìn)入Apache下載頁(yè)面,選擇最新版下載,如下。選擇The latest beta release is Apache POI 3.16-beta2會(huì)跳轉(zhuǎn)到poi-bin-3.16-beta2-20170202.tar.gz,然后點(diǎn)擊poi-bin-3.16-beta2-20170202.tar.gz,選擇鏡像后即可成功下載。


linux系統(tǒng)選擇.tar.gz
windows系統(tǒng)選擇.zip

POI下載頁(yè)面

解壓

將下載后的壓縮包解壓,會(huì)得到以下文件。

文件(夾)名 作用
docs 文檔(包括API文檔和如何使用及版本信息)
lib doc功能實(shí)現(xiàn)依賴(lài)的包
ooxml-lib docx功能實(shí)現(xiàn)依賴(lài)的包
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功能實(shí)現(xiàn)
poi-ooxml-3.16-beta2.jar docx功能實(shí)現(xiàn)
poi-ooxml-schemas-3.16-beta2.jar The prerequisite of poi-ooxml-3.16-beta2.jar
poi-scratchpad-3.16-beta2.jar doc功能實(shí)現(xiàn)
解壓后的poi包

導(dǎo)入

不熟悉怎么導(dǎo)入的同學(xué)可以看看Android Studio導(dǎo)入jar包教程
1、doc
對(duì)于doc文件,需要將lib文件夾下jar包,poi-3.16-beta2.jar,poi-scratchpad-3.16-beta2.jar放入android項(xiàng)目libs目錄下(lib文件夾下的junit-4.12.jar和log4j-1.2.17.jar不放我的項(xiàng)目也沒(méi)出現(xiàn)異常,能少點(diǎn)是點(diǎn))。

項(xiàng)目中的libs

2、docx
對(duì)于docx,需要導(dǎo)入lib文件夾下jar包,poi-3.16-beta2.jar,poi-ooxml-3.16-beta2.jar,poi-ooxml-schemas-3.16-beta2.jar和ooxml-lib下的包,由于一直我這一直出現(xiàn)Warning:Ingoring InnerClasses attribute for an anonymous inner class的錯(cuò)誤,同時(shí)由于doc基本滿(mǎn)足我的需求以及導(dǎo)入這么多jar導(dǎo)致apk體積增大,就沒(méi)有去實(shí)現(xiàn)。
有興趣的同學(xué)可以研究研究。


二、實(shí)現(xiàn)doc文件的讀寫(xiě)

Apache POI中的HWPF模塊是專(zhuān)門(mén)用來(lái)讀取和生成doc格式的文件。在HWPF中,我們使用HWPFDocument來(lái)表示一個(gè)word doc文檔。在看代碼之前,有必要了解HWPFDocument中的幾個(gè)概念:

名稱(chēng) 含義
Range 表示一個(gè)范圍,這個(gè)范圍可以是整個(gè)文檔,也可以是里面的某個(gè)小節(jié)(Section),也可以是段落(Paragraph),還可以是擁有功能屬性的一段文本(CharacterRun)
Section word文檔的一個(gè)小節(jié),一個(gè)word文檔可以由多個(gè)小節(jié)構(gòu)成。
Paragraph word文檔的一個(gè)段落,一個(gè)小節(jié)可以由多個(gè)段落構(gòu)成。
CharacterRun 具有相同屬性的一段文本,一個(gè)段落可以由多個(gè)CharacterRun組成。
Table 一個(gè)表格。
TableRow 表格對(duì)應(yīng)的行
TableCell 表格對(duì)應(yīng)的單元格

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


讀寫(xiě)前注意Apache POI 提供的HWPFDocument類(lèi)只能讀寫(xiě)規(guī)范的.doc文件,也就是說(shuō)假如你使用修改 后綴名 的方式生成doc文件或者直接以命名的方式創(chuàng)建,將會(huì)出現(xiàn)錯(cuò)誤“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)通過(guò)WordExtractor讀文件
(b)通過(guò)HWPFDocument讀文件

在日常應(yīng)用中,我們從word文件里面讀取信息的情況非常少見(jiàn),更多的還是把內(nèi)容寫(xiě)入到word文件中。使用POI從word doc文件讀取數(shù)據(jù)時(shí)主要有兩種方式:通過(guò)WordExtractor讀和通過(guò)HWPFDocument讀。在WordExtractor內(nèi)部進(jìn)行信息讀取時(shí)還是通過(guò)HWPFDocument來(lái)獲取的。

使用WordExtractor讀

在使用WordExtractor讀文件時(shí)我們只能讀到文件的文本內(nèi)容和基于文檔的一些屬性,至于文檔內(nèi)容的屬性等是無(wú)法讀到的。如果要讀到文檔內(nèi)容的屬性則需要使用HWPFDocument來(lái)讀取了。下面是使用WordExtractor讀取文件的一個(gè)示例:

//通過(guò)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());
      //輸出頁(yè)眉的內(nèi)容
      log("頁(yè)眉:" + extractor.getHeaderText());
      //輸出頁(yè)腳的內(nèi)容
      log("頁(yè)腳:" + extractor.getFooterText());
      //輸出當(dāng)前word文檔的元數(shù)據(jù)信息,包括作者、文檔的修改時(shí)間等。
      log(extractor.getMetadataTextExtractor().getText());
      //獲取各個(gè)段落的文本
      String paraTexts[] = extractor.getParagraphText();
      for (int i=0; i<paraTexts.length; i++) {
         log("Paragraph " + (i+1) + " : " + paraTexts[i]);
      }
      //輸出當(dāng)前word的一些信息
      printInfo(extractor.getSummaryInformation());
      //輸出當(dāng)前word的一些信息
      this.printInfo(extractor.getDocSummaryInformation());
      this.closeStream(is);
   }
  
   /**
    * 輸出SummaryInfomation
    * @param info
    */
   private void printInfo(SummaryInformation info) {
      //作者
      log(info.getAuthor());
      //字符統(tǒng)計(jì)
      log(info.getCharCount());
      //頁(yè)數(shù)
      log(info.getPageCount());
      //標(biāo)題
      log(info.getTitle());
      //主題
      log(info.getSubject());
   }
  
   /**
    * 輸出DocumentSummaryInfomation
    * @param info
    */
   private void printInfo(DocumentSummaryInformation info) {
      //分類(lèi)
      log(info.getCategory());
      //公司
      log(info.getCompany());
   }
  
   /**
    * 關(guān)閉輸入流
    * @param is
    */
   private void closeStream(InputStream is) {
      if (is != null) {
         try {
            is.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }
}
使用HWPFDocument讀

HWPFDocument是當(dāng)前Word文檔的代表,它的功能比WordExtractor要強(qiáng)。通過(guò)它我們可以讀取文檔中的表格、列表等,還可以對(duì)文檔的內(nèi)容進(jìn)行新增、修改和刪除操作。只是在進(jìn)行完這些新增、修改和刪除后相關(guān)信息是保存在HWPFDocument中的,也就是說(shuō)我們改變的是HWPFDocument,而不是磁盤(pán)上的文件。如果要使這些修改生效的話(huà),我們可以調(diào)用HWPFDocument的write方法把修改后的HWPFDocument輸出到指定的輸出流中。這可以是原文件的輸出流,也可以是新文件的輸出流(相當(dāng)于另存為)或其它輸出流。下面是一個(gè)通過(guò)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);
      //輸出書(shū)簽信息
      this.printInfo(doc.getBookmarks());
      //輸出文本
      log(doc.getDocumentText());
      Range range = doc.getRange();
      //讀整體
      this.printInfo(range);
      //讀表格
      this.readTable(range);
      //讀列表
      this.readList(range);
      this.closeStream(is);
   }
  
   /**
    * 關(guān)閉輸入流
    * @param is
    */
   private void closeStream(InputStream is) {
      if (is != null) {
         try {
            is.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }
  
   /**
    * 輸出書(shū)簽信息
    * @param bookmarks
    */
   private void printInfo(Bookmarks bookmarks) {
      int count = bookmarks.getBookmarksCount();
      log("書(shū)簽數(shù)量:" + count);
      Bookmark bookmark;
      for (int i=0; i<count; i++) {
         bookmark = bookmarks.getBookmark(i);
         log("書(shū)簽" + (i+1) + "的名稱(chēng)是:" + bookmark.getName());
         log("開(kāi)始位置:" + bookmark.getStart());
         log("結(jié)束位置:" + bookmark.getEnd());
      }
   }
  
   /**
    * 讀表格
    * 每一個(gè)回車(chē)符代表一個(gè)段落,所以對(duì)于表格而言,每一個(gè)單元格至少包含一個(gè)段落,每行結(jié)束都是一個(gè)段落。
    * @param range
    */
   private void readTable(Range range) {
      //遍歷range范圍內(nèi)的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) {
      //獲取段落數(shù)
      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());
      }
   }
  
   /**
    * 插入內(nèi)容到Range,這里只會(huì)寫(xiě)到內(nèi)存中
    * @param range
    */
   private void insertInfo(Range range) {
      range.insertAfter("Hello");
   }
}

DOC寫(xiě)

使用HWPFDocument寫(xiě)文件

在使用POI寫(xiě)word doc文件的時(shí)候我們必須要先有一個(gè)doc文件才行,因?yàn)槲覀冊(cè)趯?xiě)doc文件的時(shí)候是通過(guò)HWPFDocument來(lái)寫(xiě)的,而HWPFDocument是要依附于一個(gè)doc文件的。所以通常的做法是我們先在硬盤(pán)上準(zhǔn)備好一個(gè)內(nèi)容空白的doc文件,然后建立一個(gè)基于該空白文件的HWPFDocument。之后我們就可以往HWPFDocument里面新增內(nèi)容了,然后再把它寫(xiě)入到另外一個(gè)doc文件中,這樣就相當(dāng)于我們使用POI生成了word doc文件。

//寫(xiě)字符串進(jìn)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
        }
    }
    //寫(xiě)到原文件中
    OutputStream os = new FileOutputStream(PATH);
    //寫(xiě)到另一個(gè)文件中
    //OutputStream os = new FileOutputStream(其他路徑);
    doc.write(os);
    this.closeStream(is);
    this.closeStream(os);

但是,在實(shí)際應(yīng)用中,我們?cè)谏蓋ord文件的時(shí)候都是生成某一類(lèi)文件,該類(lèi)文件的格式是固定的,只是某些字段不一樣罷了。所以在實(shí)際應(yīng)用中,我們大可不必將整個(gè)word文件的內(nèi)容都通過(guò)HWPFDocument生成。而是先在磁盤(pán)上新建一個(gè)word文檔,其內(nèi)容就是我們需要生成的word文件的內(nèi)容,然后把里面一些屬于變量的內(nèi)容使用類(lèi)似于“${paramName}”這樣的方式代替。這樣我們?cè)诨谀承┬畔⑸蓋ord文件的時(shí)候,只需要獲取基于該word文件的HWPFDocument,然后調(diào)用Range的replaceText()方法把對(duì)應(yīng)的變量替換為對(duì)應(yīng)的值即可,之后再把當(dāng)前的HWPFDocument寫(xiě)入到新的輸出流中。這種方式在實(shí)際應(yīng)用中用的比較多,因?yàn)樗坏梢詼p少我們的工作量,還可以讓文本的格式更加的清晰。下面我們就來(lái)基于這種方式做一個(gè)示例。

假設(shè)我們有個(gè)模板是這樣的:


doc模板

之后我們以該文件作為模板,利用相關(guān)數(shù)據(jù)把里面的變量進(jìn)行替換,然后把替換后的文檔輸出到另一個(gè)doc文件中。具體做法如下:

public class HWPFTemplateTest {
    /**
    * 用一個(gè)doc文檔作為模板,然后替換其中的內(nèi)容,再寫(xiě)入目標(biāo)文檔中。
    * @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范圍內(nèi)的${reportDate}替換為當(dāng)前的日期
      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);
   }
  
   /**
    * 關(guān)閉輸入流
    * @param is
    */
   private void closeStream(InputStream is) {
      if (is != null) {
         try {
            is.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }
 
   /**
    * 關(guān)閉輸出流
    * @param os
    */
   private void closeStream(OutputStream os) {
      if (os != null) {
         try {
            os.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }
}

三、實(shí)現(xiàn)docx文件的讀寫(xiě)

POI在讀寫(xiě)word docx文件時(shí)是通過(guò)xwpf模塊來(lái)進(jìn)行的,其核心是XWPFDocument。一個(gè)XWPFDocument代表一個(gè)docx文檔,其可以用來(lái)讀docx文檔,也可以用來(lái)寫(xiě)docx文檔。XWPFDocument中主要包含下面這幾種對(duì)象:

對(duì)象 含義
XWPFParagraph 代表一個(gè)段落
XWPFRun 代表具有相同屬性的一段文本
XWPFTable 代表一個(gè)表格
XWPFTableRow 表格的一行
XWPFTableCell 表格對(duì)應(yīng)的一個(gè)單元格

同時(shí)XWPFDocument可以直接new一個(gè)docx文件出來(lái)而不需要像HWPFDocument一樣需要一個(gè)模板存在。

具體可以參考這位同學(xué)寫(xiě)的POI讀寫(xiě)docx文件


四、總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,991評(píng)論 19 139
  • 使用首先需要了解他的工作原理 1.POI結(jié)構(gòu)與常用類(lèi) (1)創(chuàng)建Workbook和Sheet (2)創(chuàng)建單元格 (...
    長(zhǎng)城ol閱讀 8,553評(píng)論 2 25
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,523評(píng)論 25 708
  • https://yq.aliyun.com/articles/47360# http://www.linuxdiy...
    e5497a4bf107閱讀 1,273評(píng)論 0 1
  • 今天是孩子的家長(zhǎng)會(huì),自己覺(jué)得到教室挺早,進(jìn)入教室看到史老師和大部分家長(zhǎng)都到了,很是害羞和慚愧. 今...
    趙飛揚(yáng)媽媽閱讀 301評(píng)論 0 1