1. 程式人生 > >所見即所得,使用Java將HTML解析為Excel,支援多級表頭巢狀、單元格合併

所見即所得,使用Java將HTML解析為Excel,支援多級表頭巢狀、單元格合併

最近專案需要實現如題“所見即所得”的功能,之前每次生成Excel都需要重新從資料庫查詢一遍,降低效率不說,那些巢狀的表頭實在是很難用Sql巢狀拼接實現。而且這樣做還沒有通用性,不同的表格需要寫不同的Sql實現,非常繁瑣。


        在網上找了很多關於HTML解析為Excel的文章,有以下兩種情況:

1、大部分用“偷懶”的辦法,使用js直接將HTML程式碼輸出為文字檔案,然後將檔案字尾改為.xls。這種方式的確簡單易行,但是有它的缺點,之後講到。

2、解析HTML然後生成Excel,網上的部落格的確可以搜到一兩篇這樣的文章,但是內容太有侷限性,適用於沒有單元格合併的情況,並不適用於複雜單元格合併(eg:多級表頭巢狀)的情況

。而實際的報表,結構都很複雜。

終於找到一篇這樣的文章《所見即所得的EXCEL報表生成(二)——從HTMLTABLE到EXCEL CELL》,文章介紹了使用.NET+HtmlAgilityPack解析工具實現HTML解析為Excel,無奈對.NET沒接觸過,看的過程中只借鑑了其中的推導思路,然後自己構思Java程式碼,很感謝文章的作者。我的原創點在於使用Java+Jsoup+POI的程式碼實現。

 

----------------------------------------------(づ ̄3 ̄)づ╭❤~華麗的分割線----------------------------------------------

 

為了實現“所見即所得”的效果,即將頁面的報表直接匯出為Excel,做了兩種方法的嘗試。

方法1:JS直接輸HTML到文字檔案,更改字尾為.xls

優點:

簡單有效,程式碼量最少

缺點:

1、使用者體驗有欠缺。這種方式生成的文字檔案是HTML程式碼,不是真正的Excel檔案。但是可以被Excel解析,並且還附帶有樣式。但是每次開啟時會彈出提示“檔案安全性有問題”對話方塊,太煩人…

2、匯入Excel解析是問題。匯入Excel的功能一般只解析真正的Excel檔案,不能解析HTML。這樣還要區分不同的檔案解析,有點麻煩

 

方法2:將HTML程式碼解析為Excel檔案

優點:

1、生成的是真正的Excel檔案,無需擔心匯入問題

2、良好的使用者體驗,沒有任何彈窗提示

缺點:

對一些多餘的HTML程式碼,或者不規範的HTLM,解析會不能正確識別。所以使用時需要注意傳入HTML的規範。

 

第1種方法大家可以自己搜,本文重點介紹第2種方法,廢話說完了_(:з」∠)_,終於可以進入正題了…

過程中用到的jar包:commons-lang-2.6.jar、jsoup-1.8.1.jar、poi-3.10.1-20140818.jar

下載地址:http://pan.baidu.com/s/1c02amQk

 

----------------------------------------------(づ ̄3 ̄)づ╭❤~華麗的分割線----------------------------------------------

 

我們目的是將HTML解析為Excel檔案,需要分兩步走:

1、使用Jsoup解析HTML

Jsoup是一個第三方HTML解析工具包,你可能沒用過,但是你一看官方的介紹就明白了http://www.open-open.com/jsoup/,和DOM的操作方法特別相似。列舉幾個常用方法:

據標籤名稱獲取元素:getElementsByTag(String tagName)

移除指定元素:removeAll(Elemente)

獲取元素標籤屬性值:attr(StringattrName)

 2、使用POI構造Excel檔案

POI不用多說了,Excel匯入匯出經常用到的工具包。

 

第一步:使用Jsoup解析HTML

 

以下分析部分基於原文修改,並加入自己的詳細解釋:

直觀的看,一個完整Excel的內容是由位於各個單元格(Cell)中的內容組合而成的。而每個單元格(Cell)都有相應X、Y座標來標示其位置(最左上角的單元格,座標(1,1))。也就是說,一個Excel檔案實質上就是許多Cell構成的集合,每個Cell用座標屬性確定位置,用內容屬性儲存內容。

基於此,我們得到了最基本的Cell結構:

  • X座標
  • Y座標
  • 合併列情況
  • 合併行情況
  • 內容

構成Excel的最基本的結構已經確定,下一步擺在我們面前的就是將html table轉化為Excel Cell集合。

Html table中的每個td節點對應一個Excel單元格。單元格的內容可以通過解析table的td獲取,行、列的合併情況也可由td的rowspan、colspan屬性得出,轉化的關鍵點就在於如何由table的tr、td結構確定Excel單元格位置,即如何確定X、Y座標。

1、確定Y座標

Y座標容易確定,即td所在tr的行數。

例如:當前的td在table的第2行中,則當前td的Y座標為2

2、確定X座標

X座標的確定沒有那麼簡單,它要受到兩方面因素的影響:

(1)與當前td處於同一tr,但位於其之前(反映在表格視覺上即其左側td)td的佔位情況。

也就是說,如果td在第2行,那麼需要考慮同樣在第2行的td的左邊,可能存在有單元格有列的合併(佔據當前行的若干個單元格),因為合併後的單元格只有一個座標值,因此td左邊的單元格合併的越多,td的X會越小。

(2)當前td所在tr的之前tr中某些td的跨行情況。

也就是說,如果td在第3行,那麼需要考慮第1行和第2行可能存在單元格有行的合併(佔據不同行的若干單元格)。比如在第1行中,一個td跨3行1列,那麼當前第3行的td的X座標會變小,因為第3行被合併的那個單元格並不屬於第3行,屬於第1行。

基於此種考慮,定位td的X座標需經過兩個過程的推導:用於處理左側td佔位影響的橫向推導(HorizontalDeduction)和處理之前行跨行td影響的縱向推導(VerticalDeduction)。


 以下圖的table為例,展示兩次推導過程,這兩次的推導過程,就是這個問題的核心:

1

2

3

4

5

6

7

8

9

10

 

1、橫向推導(HorizontalDeduction)

橫向推導的目的就是發現與當前td處在同一行,且在td單元格左邊,有單元格合併的情況。整個過程基於遞迴的原理,遞迴模型如下:

解釋:n為當前行的單元格序列,從1開始。以table圖為例,第1個單元格的X座標X1=1,第二個單元格的X座標X2=X1+colspan1=1+2=3。

也就是說,對於橫向推導,td的X座標=上個兄弟td的座標+兄弟td的合併列數


橫向推導的java程式碼(文章裡的原點為(1,1),程式碼裡是(0,0)):

<span style="font-size:12px;">	/**
	 * @Title : HorizontalDeduction
	 * @Description : 使用遞迴,進行橫座標的第一步推導,橫向推導,同時刪除多餘的非td元素
	 * @author : Qinchz
	 * @date : 2014年12月12日 下午8:51:39
	 * @param e
	 * @return
	 */
	private  int HorizontalDeduction(Element e) {
		Element preElement=e.previousElementSibling();
		if(preElement!=null){
			//表示td的上一個兄弟節點不是td,則刪除這個多餘的元素
			if(!preElement.tagName().equals("td")){ 
				preElement.remove();
			}else{
				int nColSpan=1;//預設為1
				if(StringUtils.isNotBlank(preElement.attr("colspan"))){
					//前一個元素的列合併情況
					nColSpan=Integer.valueOf(preElement.attr("colspan").trim());
				}
				return HorizontalDeduction(preElement) + nColSpan;
			}
		}
		return 0;
	}</span><span style="font-size: 14px;">
</span>


經過橫向推導,table的座標情況:

1(1,1)

2(3,1)

3(4,1)

4(1,2)

5(2,2)

6(1,3)

7(2,3)

8(3,3)

9(1,4)

10(2,4)

 

2、縱向推導(VerticalDeduction)

縱向推導的目的是發現當前td所在行之前的行,存在跨行合併的情況。一次縱向推導的過程可以描述為(當前推導td用A表示):

找到A所在行之前的行tr中與A具有相同X座標的td節點B

   if(B.rowspan>(A.Y-B.Y))

    {

X+=B.colspan,即A的X座標向後推B.colspan的位置;

同時,與A同處一tr但在其後邊的td節點均應向後推B.colspan個位移;

      }

 

解釋:

在橫向推導之後,實際單元格的排列是有重合的(上圖中不重合是因為忽略了單元格的高度,把高度視為相等),所以縱向推導就是要單元格合理的“避開”它之前行已經佔用的位置。    

例如:上圖中,單元格4的座標是(1,2),這並不是4最終的正確座標,那麼4為什麼會跑到這裡?因為橫向推導只是讓4在同一行中它的位置是對的,不能保證和4處在不同行的1對它產生的影響。

4為A節點,縱向推導先找到1為B節點。如果B跨越的行數>當前AB的Y座標之差(即,判斷A和B有重合),那麼A的X座標 =A.X+B跨越的列數(即,讓A“避開”B已經佔用的位置)。同時,A的移動影響了與A同行且在右邊的節點,A右邊的節點需要移動相同的長度。

 就以節點4為例,按照上述的方法移動後的位置如圖:

1(1,1)

2(3,1)

3(4,1)

4(1,2)

 

5(2,2)

 

 

6(1,3)

7(2,3)

8(3,3)

9(1,4)

10(2,4)

 

縱向推導的java程式碼(文章裡的原點為(1,1),程式碼裡是(0,0)):

	/**
	 * @Title : verticalDeduction
	 * @Description : 縱向推導
	 * @author : Qin
	 * @date : 2014年12月12日 下午9:12:25
	 * @param headerList
	 * @return
	 */
	private List<TD> verticalDeduction(List<TD> headerList) {
		int headerSize = headerList.size();
		for (int i = 0; i < headerSize; i++) {
			TD tdA = headerList.get(i);
			boolean flag = true;
			while (flag) {// 不斷排列當前節點的位置,直到它的位置絕對正確
				flag = false;// 不需要移位
				for (int j = i - 1; j >= 0; j--) {// 找到之前與td的橫座標相等的值
					TD tdB = headerList.get(j);
					if (tdA.getX() == tdB.getX()) {// A找到與其X座標相等的B
				         // 如果B單元格“擋住”了A單元格,才進行移位操作。即:只有B佔的行數
				         // 大於或等於A、B之間的距離,那麼B才會擋住A
						if (tdB.getRowspan() > tdA.getY() - tdB.getY()) {
				              // 如果存在移位單元格,則仍然需要重新判斷當前的位置是否正確。需要移位
				              flag = true;
				              // A的X座標向後推B.colspan的位置
							tdA.setX(tdA.getX() + tdB.getColspan());
							int YA = tdA.getY();
				              // 同時,與A同處一tr但在其後邊的td節點均應向後推B.colspan位移
							for (int m = i + 1; m < headerSize; m++) {
								TD td = headerList.get(m);
								if (td.getY() == YA) {
									td.setX(td.getX() + tdB.getColspan());
								}
							}
						}
					}
				}
			}
		}
		return headerList;
	}



單元格類:

/**
 * @ClassName: TD
 * @Description: 單元格類
 * @author Qin
 * @date 2014年12月12日 下午5:45:10
 */
class TD {
       private int rowspan=1;
       private int colspan=1;
       private int x;
       private int y;
       private String content;
 
       public int getRowspan() {
              return rowspan;
       }
       public void setRowspan(int rowspan) {
              this.rowspan = rowspan;
       }
       public int getColspan() {
              return colspan;
       }
       public void setColspan(int colspan) {
              this.colspan = colspan;
       }
       public String getContent() {
              return content;
       }
       public void setContent(String content) {
              this.content = content;
       }
       public int getX() {
              return x;
       }
       public void setX(int x) {
              this.x = x;
       }
       public int getY() {
              return y;
       }
       public void setY(int y) {
              this.y = y;
       }
}


以上是從HTML table構造Excel Cell的整個過程的核心程式碼(完成原始碼在文章附在文章末尾),經過整個構造過程,我們就得到了每個td的絕對座標X、Y,再結合已知的td行列合併情況、td的內容,我們完全可以在Excel中構造出需要的表格。



第二步:使用POI構造Excel檔案

         燒腦的內容都在第一步,第二步很簡單。

         第一步中,我們得到了List< TD >這個含有表格資訊的集合,其中含有每個表格的座標、合併情況、內容。我們需要利用這些資訊進行Excel中單元格的構造。

其實重點是進行單元格的合併,POI中HSSFSheet類有一個單元格合併的方法:

//引數:起始行,終止行,起始列,終止列。這四個引數,就要用到第一步得到的座標
addMergedRegion(newCellRangeAddress(int firstRow, int lastRow, int firstCol, int lastCol)


下是合併單元格的部分核心程式碼:

		//表頭、單元格資料內容寫入
		for(int i=0;i<finalHeaderList.size();i++){
			TD td=finalHeaderList.get(i);
			sheet.getRow(td.getY()).getCell(td.getX()).setCellValue(td.getContent());
			//單元格合併
			sheet.addMergedRegion(
					new CellRangeAddress(//起始行,終止行,起始列,終止列
							td.getY(),
							td.getY()+(td.getRowspan()-1),
							td.getX(),
							td.getX()+(td.getColspan()-1))
					);
		}


既然是web專案,得實現“下載Excel”功能吧,起初想通過Struts2的下載功能實現,結果不太好用,換了一種更好用的方式。

專案中所涉及的所有原始碼貼到下面:

 

1、Action

頁面傳遞兩個引數:需要解析的HTML字串,下載的檔名稱

import java.net.URLEncoder;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.struts2.ServletActionContext;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import com.opensymphony.xwork2.ActionContext;
import com.wondersgroup.qyws.common.utils.HtmlToExcel;
import com.wondersgroup.qyws.tjfx.common.BaseAction;
 
@Controller("htmlToExcelAction")
@Scope("prototype")
public class HtmlToExcelAction extends BaseAction{
         privatestatic final long serialVersionUID = 5975906847955053344L;
         /**前臺傳遞的html字串*/
         privateString htmlStr;
         /**檔名稱*/
         privateString fileName;
        
         publicvoid htmlToExcel() throws Exception {
                   if(StringUtils.isNotBlank(htmlStr)){
                   try{
                            HtmlToExcelhtmlToExcel=new HtmlToExcel(2,fileName);
                            HSSFWorkbookwb= htmlToExcel.readHtmlStr(htmlStr);
                           
                            ActionContextcontext = ActionContext.getContext();
                            HttpServletResponseresponse = (HttpServletResponse)context.get(ServletActionContext.HTTP_RESPONSE);
                            response.setHeader("Content-Disposition","attachment; filename="
                                               +URLEncoder.encode(fileName+".xls", "utf-8"));
                            ServletOutputStreamfOut = response.getOutputStream();
                           
                            wb.write(fOut);
                            fOut.flush();
                            fOut.close();
 
                            }catch (Exception e) {
                                     e.printStackTrace();
                            }
                   }
         }
        
         publicString getHtmlStr() {
                   returnhtmlStr;
         }
         publicvoid setHtmlStr(String htmlStr) {
                   this.htmlStr= htmlStr;
         }
         publicString getFileName() {
                   returnfileName;
         }
         publicvoid setFileName(String fileName) {
                   this.fileName= fileName;
         }
}

 

2、HTMLTOEXCEL工具類

構造器兩個引數:生成Excel的標題所佔行數,需要解析的HTML字串

import java.util.ArrayList;
import java.util.List;
 
import org.apache.commons.lang.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.hssf.usermodel.HSSFFont;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.util.CellRangeAddress;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
 
import com.wondersgroup.qyws.tjfx.common.BaseAction;
/**
 * @ClassName:HtmlToExcel
 * @Description: 傳入html程式碼字串,返回POI的工作簿物件HSSFWorkbook
 * @author Qin
 * @date 2014年12月13日 下午6:36:31
 */
public class HtmlToExcel extends BaseAction{
         privatestatic final long serialVersionUID = 4175158575304402752L;
         /**表格的列數*/
         private  int columnSize;
         /**表格的行數*/
         private  int rowSize;
         /**資料的行數,不含表頭*/
         private  int rowSize_data;
         /**標題所佔行數*/
         privateint rowSize_title=2;
         /**表頭所佔行數*/
         privateint rowSize_header;
         /**工作表名稱*/
         privateString sheetName;
        
         publicHtmlToExcel(int rowSize_title,String sheetName){
                   this.rowSize_title=rowSize_title;
                   this.sheetName=sheetName;
         }
         /**
          * @Title : readHtmlStr
          * @Description : 使用jsoup解析html,得到表頭資料List,表體資料String[][]
          * @author : Qin
          * @date : 2014年12月13日 下午6:32:55
          * @return
          * @throws Exception
          */
         public  HSSFWorkbook readHtmlStr(String htmlStr)throws Exception{
                   Documentdoc = Jsoup.parseBodyFragment(htmlStr, "utf-8");
                   doc.select("input[type$=hidden]").remove();//刪除所有input隱藏域
                   doc.select("tr[style*=none]").remove();//刪除隱藏的tr
                  
                   Elementscaptions=doc.getElementsByTag("caption");//查詢表頭
                   StringtableTitle="";// 儲存表頭標題
                   if(captions!=null&&captions.size()>0){
                            Elementcaption=captions.get(0);
                            tableTitle=caption.text();
                   }else{
                            rowSize_title=0;//表示,沒有表頭
                   }
                  
                   Elementstrs_data = doc.getElementsByTag("tr");//獲取所有tr
                   rowSize= trs_data.size()+rowSize_title;//獲取表格的行數,外加標題行數
                  
                   Elementstheads=doc.getElementsByTag("thead");//表頭thead標籤
                  
                   List<TD>finalHeaderList=new ArrayList<TD>();//存放推導完畢的正確資料
                  
                   List<TD>dataList = new ArrayList<TD>();//表頭1單元格List
                   if(theads!=null&&theads.size()>0){//表示有表頭
                            Elementsthead_trs=theads.get(0).getElementsByTag("tr");//表頭中的tr
                            rowSize_header=thead_trs.size();
                            trs_data.removeAll(thead_trs);//移除表頭中的的tr元素,trs中剩下資料行
                           
                            List<TD>headerList = new ArrayList<TD>();//表頭1單元格List
                            //構造表頭
                            //將表頭資料存到List中。x、y座標從0開始
                            //確定x座標之1:橫向推導
                            intbasicY_thead=rowSize_title;
                            for(inti=0;i<thead_trs.size();i++){
                                     Elementthead_tr=thead_trs.get(i);
                                     Elementsthead_tr_ths=thead_tr.getElementsByTag("th");
                                     for(intj=0;j<thead_tr_ths.size();j++){
                                               Elemente=thead_tr_ths.get(j);
                                               TDtd=new TD();
                                               td.setContent(e.text());
                                               if(StringUtils.isNotBlank(e.attr("colspan"))){
                                                        td.setColspan(Integer.valueOf(e.attr("colspan").trim()));
                                               }
                                               if(StringUtils.isNotBlank(e.attr("rowspan"))){
                                                        td.setRowspan(Integer.valueOf(e.attr("rowspan").trim()));
                                               }
                                               td.setX(HorizontalDeduction_th(e));//步驟1:橫向推導,但這個座標並不是最終座標,需要進行縱向推導
                                               td.setY(i+basicY_thead);//y座標很簡單,就是tr的值
                                               headerList.add(td);
                                     }
                            }
                            //確定x座標之2:縱向推導
                            finalHeaderList=verticalDeduction(headerList);
                           
                            if(trs_data.size()>0){//表示有表格內容資料
                                     rowSize_data=trs_data.size();
                            }else{//表示只有表頭資料,沒有表格內容資料
                                     rowSize_data=0;
                            }
                           
                   }else{//表示沒有表頭
                            rowSize_header=0;
                   }
                   //迴圈每一個數據單元格
                   intbasicY_data=rowSize_title+rowSize_header;
                   for(int i = 0; i < trs_data.size(); i++) {
                            Elementtr = trs_data.get(i);
                            Elementstds = tr.getElementsByTag("td");
                            //迴圈每一行的所有列
                            for(int j = 0; j < tds.size(); j++) {
                                     Elemente = tds.get(j);
                                     Elementsinp=e.getElementsByTag("input");
                                     TDtd=new TD();
                                     if(StringUtils.isNotBlank(e.attr("colspan"))){
                                               td.setColspan(Integer.valueOf(e.attr("colspan").trim()));
                                     }
                                     if(StringUtils.isNotBlank(e.attr("rowspan"))){
                                               td.setRowspan(Integer.valueOf(e.attr("rowspan").trim()));
                                     }
                                     if(inp!=null&&inp.size()>0){//表示td中巢狀input
                                               td.setContent(inp.get(0).val());
                                     }else{//表示td中沒有巢狀input
                                               td.setContent(e.text().trim());
                                     }
                                     td.setX(HorizontalDeduction_td(e));//步驟1:橫向推導,但這個座標並不是最終座標,需要進行縱向推導
                                     td.setY(i+basicY_data);//y座標很簡單,就是tr的值
                                     dataList.add(td);
                            }
                   }
                   //步驟2:縱向推導
                   dataList=verticalDeduction(dataList);
                  
                   //表頭和表內容合併為一個List
                   finalHeaderList.addAll(dataList);
                  
                   //對列進行賦值,找到第一個單元格計算列寬
                   TDlastTd=finalHeaderList.get(finalHeaderList.size()-1);
                   columnSize=lastTd.getX()+lastTd.getColspan();
                  
                   int[][]contextSizeArr=new int[rowSize][columnSize];//記錄內容單元格內容長度,便於進行列寬自適應調整
                   String[][]dataArr=new String[rowSize_data][columnSize];
                   //將表格的長度按照該單元格的位置填入字串長度
                   //不能使用普通下標方式賦值,因為如果有合併單元格的情況,陣列的位置就會錯位,使用座標保證不會錯位
                   for(int i = 0; i < finalHeaderList.size(); i++) {
                            TDtd = finalHeaderList.get(i);
                            contextSizeArr[td.getY()][td.getX()]= getStringLength(td.getContent())+1;
                   }
                   int[]maxLengthArr = getMaxLength(contextSizeArr);
                  
                   //根據解析到的資料返回POI的Excel物件
                   returnbuildExcel(tableTitle,finalHeaderList,dataArr,maxLengthArr);
         }
         /**
          * @Title : getStringLength
          * @Description : 中文字元與非中文字元長度計算
          * @author : Qin
          * @date : 2014年12月14日 下午12:30:45
          * @param s
          * @return
          */
         privateint getStringLength(String s) {
                   doublevalueLength = 0;   
                String chinese ="[\u4e00-\u9fa5]";   
                // 獲取欄位值的長度,如果含中文字元,則每個中文字元長度為2,否則為1   
                for (int i = 0; i < s.length(); i++){   
                    // 獲取一個字元   
                    String temp = s.substring(i, i +1);   
                    // 判斷是否為中文字元   
                    if (temp.matches(chinese)) {   
                        // 中文字元長度為1   
                        valueLength += 1;   
                    } else {   
                        // 其他字元長度為0.5   
                        valueLength += 0.5;   
                    }   
                }   
                //進位取整   
                return (int) Math.ceil(valueLength);   
         }
         /**
          * @Title : getMaxLength
          * @Description : 豎向遍歷二維陣列,找到每一列的最大值
          * @author : Qin
          * @date : 2014年12月14日 上午1:18:02
          * @param contextSizeArr
          * @return
          */
         privateint[] getMaxLength(int[][] contextSizeArr) {
                   int[]maxArr=new int[columnSize];
                   for(inti=0;i<columnSize;i++){
                            intbasic=0;
                            for(intj=0;j<rowSize;j++){
                                     if(contextSizeArr[j][i]>basic){//注意下標的寫法
                                               basic=contextSizeArr[j][i];
                                     }
                            }
                            maxArr[i]=basic;
                   }
                   returnmaxArr;
         }
         /**
          * @Title : HorizontalDeduction
          * @Description : 使用遞迴,進行橫座標的第一步推導,橫向推導,同時刪除多餘的非td元素
          * @author : Qin
          * @date : 2014年12月12日 下午8:51:39
          * @param e
          * @return
          */
         private  int HorizontalDeduction_td(Element e) {
                   ElementpreElement=e.previousElementSibling();
                   if(preElement!=null){
                            if(!preElement.tagName().equals("td")){//表示td的上一個兄弟節點不是td,則刪除這個多餘的元素
                                     preElement.remove();
                            }else{
                                     intnColSpan=1;//預設為1
                                     if(StringUtils.isNotBlank(preElement.attr("colspan"))){
                                               nColSpan=Integer.valueOf(preElement.attr("colspan").trim());//前一個元素的列合併情況
                                     }
                                     returnHorizontalDeduction_td(preElement) + nColSpan;
                            }
                   }
                   return0;
         }
        
         /**
          * @Title : HorizontalDeduction
          * @Description : 使用遞迴,進行橫座標的第一步推導,橫向推導,同時刪除多餘的非td元素
          * @author : Qin
          * @date : 2014年12月12日 下午8:51:39
          * @param e
          * @return
          */
         private  int HorizontalDeduction_th(Element e) {
                   ElementpreElement=e.previousElementSibling();
                   if(preElement!=null){
                            if(!preElement.tagName().equals("th")){//表示td的上一個兄弟節點不是td,則刪除這個多餘的元素
                                     preElement.remove();
                            }else{
                                      int nColSpan=1;//預設為1
                                     if(StringUtils.isNotBlank(preElement.attr("colspan"))){
                                               nColSpan=Integer.valueOf(preElement.attr("colspan").trim());//前一個元素的列合併情況
                                     }
                                     returnHorizontalDeduction_th(preElement) + nColSpan;
                            }
                   }
                   return0;
         }
        
         /**
          * @Title : verticalDeduction
          * @Description : 縱向推導
          * @author : Qin
          * @date : 2014年12月12日 下午9:12:25
          * @param headerList
          * @return
          */
         privateList<TD> verticalDeduction(List<TD> headerList) {
                   intheaderSize = headerList.size();
                   for(int i = 0; i < headerSize; i++) {
                            TDtdA = headerList.get(i);
                            booleanflag = true;
                            while(flag) {// 不斷排列當前節點的位置,直到它的位置絕對正確
                                     flag= false;// 不需要移位
                                     for(int j = i - 1; j >= 0; j--) {// 找到之前與td的橫座標相等的值
                                               TDtdB = headerList.get(j);
                                              if(tdA.getX() == tdB.getX()) {// A找到與其X座標相等的B
                                                        if(tdB.getRowspan() > tdA.getY() - tdB.getY()) {// 如果B單元格“擋住”了A單元格,才進行移位操作。即:只有B佔的行數大於或等於A、B之間的距離,那麼B才會擋住A
                                                                 flag= true;// 如果存在移位單元格,則仍然需要重新判斷當前的位置是否正確。需要移位
                                                                 tdA.setX(tdA.getX()+ tdB.getColspan());// A的X座標向後推B.colspan的位置
                                                                 intYA = tdA.getY();
                                                                 for(int m = i + 1; m < headerSize; m++) {// 同時,與A同處一tr但在其後邊的td節點均應向後推B.colspan個位移
                                                                           TDtd = headerList.get(m);
                                                                           if(td.getY() == YA) {
                                                                                    td.setX(td.getX()+ tdB.getColspan());
                                                                           }
                                                                 }
                                                        }
                                               }
                                     }
                            }
                   }
                   returnheaderList;
         }
         /**
          * @Title : buildExcel
          * @Description : 依據傳入的資料生成Excel檔案
          * @author : Qin
          * @date : 2014年12月13日 下午6:32:06
          * @param title
          * @param finalHeaderList 表格表頭資料
          * @param dataArr                                 表格內容資料
          * @return
          * @throws Exception
          */
         private  HSSFWorkbook buildExcel(Stringtitle,List<TD> finalHeaderList,String[][] dataArr,int[] maxLengthArr)
                            throwsException {
                   HSSFWorkbookwb=new HSSFWorkbook();
                   HSSFSheetsheet=null;
                   if(StringUtils.isNotBlank(sheetName)){
                             sheet=wb.createSheet(sheetName);
                   }elseif(StringUtils.isNotBlank(title)){
                            sheet=wb.createSheet("title");
                   }else{
                            sheet=wb.createSheet("Sheet1");
                   }
                  
                   //表格樣式
                   //1、基礎樣式
                   HSSFCellStylebasicStyle=wb.createCellStyle();
                   basicStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);//設定水平居中
                   basicStyle.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);    //設定垂直居中 
                  
                   basicStyle.setBorderBottom(HSSFCellStyle.BORDER_THIN);// 下邊框 
                   basicStyle.setBorderLeft(HSSFCellStyle.BORDER_THIN);//左邊框 
                   basicStyle.setBorderTop(HSSFCellStyle.BORDER_THIN);//上邊框 
                   basicStyle.setBorderRight(HSSFCellStyle.BORDER_THIN);//右邊框
                   //2、標題樣式
                   HSSFCellStyletitleStyle=wb.createCellStyle();
                   titleStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);//設定水平居中
                   titleStyle.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);    //設定垂直居中 
                  
                   HSSFFontheaderFont1 = (HSSFFont) wb.createFont();
                   headerFont1.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);//設定字型加粗
                   headerFont1.setFontHeightInPoints((short)14);//設定字型大小
                   titleStyle.setFont(headerFont1);
                   //3、偶數行樣式
                   HSSFCellStyleevenStyle=wb.createCellStyle();
                   evenStyle.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);  
                   evenStyle.setFillForegroundColor(HSSFColor.WHITE.index);
                   evenStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);
                  
                   HSSFCellStyleoldStyle=wb.createCellStyle();
                   oldStyle.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);  
                   oldStyle.setFillForegroundColor(HSSFColor.GREY_25_PERCENT.index);
                   oldStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);
                  
                   //構建基本空白表格
                   for(inti=0;i<rowSize;i++){
                            sheet.createRow(i);
                            for(intj=0;j<columnSize;j++){
                                     sheet.getRow(i).createCell(j).setCellStyle(basicStyle);
                            }
                   }
                   //填充資料
                   if(rowSize_title!=0){
                            //1、標題
                            HSSFCellcell=sheet.getRow(0).getCell(0);
                            cell.setCellStyle(titleStyle);
                            cell.setCellValue(title);
                            //單元格合併
                            sheet.addMergedRegion(newCellRangeAddress(0,rowSize_title-1,0,columnSize-1));//起始行,終止行,起始列,終止列
                   }
                   //2、表頭、單元格資料內容寫入
                   for(inti=0;i<finalHeaderList.size();i++){
                            TDtd=finalHeaderList.get(i);
                            sheet.getRow(td.getY()).getCell(td.getX()).setCellValue(td.getContent());
                            //單元格合併
                            sheet.addMergedRegion(
                                               newCellRangeAddress(//起始行,終止行,起始列,終止列
                                                                 td.getY(),
                                                                 td.getY()+(td.getRowspan()-1),
                                                                 td.getX(),
                                                                 td.getX()+(td.getColspan()-1))
                                               );
                   }
                   //3、設定每一列寬度以該列的最長內容為準
                   for(inti=0;i<maxLengthArr.length;i++){
                            sheet.setColumnWidth(i,maxLengthArr[i]*2*235);
                   }
                  
                   for(inti=0;i<rowSize;i++){
                            HSSFRowrow =sheet.getRow(i);
                            row.setHeightInPoints(row.getHeightInPoints()+3);
                   }
                   returnwb;
         }
}
 
/**
 * @ClassName: TD
 * @Description: 單元格類
 * @author Qin
 * @date 2014年12月12日 下午5:45:10
 */
class TD {
         privateint rowspan=1;
         privateint colspan=1;
         privateint x;
         privateint y;
         privateString content;
 
         publicint getRowspan() {
                   returnrowspan;
         }
         publicvoid setRowspan(int rowspan) {
                   this.rowspan= rowspan;
         }
         publicint getColspan() {
                   returncolspan;
         }
         publicvoid setColspan(int colspan) {
                   this.colspan= colspan;
         }
         publicString getContent() {
                   returncontent;
         }
         publicvoid setContent(String content) {
                   this.content= content;
         }
         publicint getX() {
                   returnx;
         }
         publicvoid setX(int x) {
                   this.x= x;
         }
         publicint getY() {
                   returny;
         }
         publicvoid setY(int y) {
                   this.y= y;
         }
}