所见即所得,使用Java将HTML解析为Excel,支持多级表头嵌套、单元格合并

2023-11-05

最近项目需要实现如题“所见即所得”的功能,之前每次生成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

下载地址:Jar文件_免费高速下载|百度网盘-分享无限制

----------------------------------------------(づ ̄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)):

/**
	 * @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;
	}

经过横向推导,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;
         }
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

所见即所得,使用Java将HTML解析为Excel,支持多级表头嵌套、单元格合并 的相关文章

  • VBA FreeLibrary 不卸载 DLL

    当我使用完一个 DLL 文件后 我需要删除它 代码完成后清理 我尝试在 Excel VBA 中使用 LoadLibrary 和 FreeLibrary 但无论我做什么 Excel exe 都会坚持 DLL 文件 Public Declare
  • 为什么Iterator接口没有add方法

    In IteratorSun 添加了remove 方法来删 除集合中最后访问的元素 为什么没有add方法来向集合中添加新元素 它可能对集合或迭代器产生什么样的副作用 好的 我们开始吧 设计常见问题解答中明确给出了答案 为什么不提供 Iter
  • Android蓝牙java.io.IOException:bt套接字已关闭,读取返回:-1

    我正在尝试编写一个代码 仅连接到运行 Android 5 0 KitKat 的设备上的 目前 唯一配对的设备 无论我尝试了多少方法 我仍然会收到此错误 这是我尝试过的最后一个代码 它似乎完成了我看到人们报告为成功的所有事情 有人能指出我做错
  • 如何将本地文本文件上传到文本区域(网页内)

    我是一名新手程序员 需要一些帮助来弄清楚如何将本地文本文件上传到我正在构建的网站内的文本区域 我非常精通 HTML CSS 对 Javascript JQuery 有相当的了解 而且我刚刚学习 PHP 您能提供的任何帮助我将不胜感激 我有一
  • 如何通过注解用try-catch包装方法?

    如果应该在方法调用中忽略异常 则可以编写以下内容 public void addEntryIfPresent String key Dto dto try Map
  • 什么可能导致 Firefox 中出现此 HTML/CSS 渲染问题?

    款式 h2 color 71D0FF font size 11px font weight bold margin 0px 0px 5px 0px a box color FFFFFF important cursor pointer di
  • 如何防止输入文本中出现“后重音”

    我相信这是一个简单的问题 但在谷歌上搜索几个小时后我找不到任何答案 也许我无法在搜索中使用正确的单词 P 我有一个 javascript 方法 可以防止用户用数字以外的其他字符填充文本框 如下面的代码所示 它在 KeyDown 事件中使用
  • 添加到列表时有没有办法避免循环?

    我想知道这样的代码 List
  • 在 Java 中通过 XSLT 分解 XML

    我需要转换具有嵌套 分层 表单结构的大型 XML 文件
  • 如何从日期中删除毫秒、秒、分钟和小时[重复]

    这个问题在这里已经有答案了 我遇到了一个问题 我想比较两个日期 然而 我只想比较年 月 日 这就是我能想到的 private Date trim Date date Calendar calendar Calendar getInstanc
  • 将不连续范围从一张纸复制到另一张纸

    VBA 新手 也是第一次发帖 可能会问一个非常基本的问题 然而 我在互联网上 或在我拥有的参考书中 没有找到答案 所以我很困惑 如何将一张纸中的一堆间隔开的列放入另一张纸中 但没有间隙 例如 我想从这样的工作表中复制标记为 x 的单元格 x
  • 在 Clojure 中解压缩 zlib 流

    我有一个二进制文件 其内容由zlib compress在Python上 有没有一种简单的方法可以在Clojure中打开和解压缩它 import zlib import json with open data json zlib wb as
  • 如何停止执行的 Jar 文件

    这感觉像是一个愚蠢的问题 但我似乎无法弄清楚 当我在 Windows 上运行 jar 文件时 它不会出现在任务管理器进程中 我怎样才能终止它 我已经尝试过 TASKKILL 但它对我也不起作用 On Linux ps ef grep jav
  • 如何让 Emma 或 Cobertura 与 Maven 一起报告其他模块中源代码的覆盖率?

    我有一个带有 Java 代码的多模块 Maven 设置 我的单元测试在其中一个模块中测试多个模块中的代码 当然 这些模块具有相互依赖性 并且在测试执行之前根据需要编译所有相关模块中的代码 那么 如何获得整个代码库覆盖率的报告 注意 我不是问
  • 禁用 Android 菜单组

    我尝试使用以下代码禁用菜单组 但它不起作用 菜单项仍然启用 你能告诉我出了什么问题吗 资源 菜单 menu xml menu menu
  • Hadoop NoSuchMethodError apache.commons.cli

    我在用着hadoop 2 7 2我用 IntelliJ 做了一个 MapReduce 工作 在我的工作中 我正在使用apache commons cli 1 3 1我把库放在罐子里 当我在 Hadoop 集群上使用 MapReduceJob
  • 标题的下边框小于宽度

    我需要创建一个下划线效果底部边框小于h2标题的宽度 通常我不上传图片 但我认为这可能有助于进一步解释问题 您可以为此使用伪元素 例子 http jsfiddle net SZ39x pseudo border position relati
  • HttpClient请求设置属性问题

    我使用这个 HttpClient 库玩了一段时间 几周 我想以某种方式将属性设置为请求 不是参数而是属性 在我的 servlet 中 我想使用 Integer inte Integer request getAttribute obj 我不
  • 记录类名、方法名和行号的性能影响

    我正在我的 java 应用程序中实现日志记录 以便我可以调试应用程序投入生产后可能出现的潜在问题 考虑到在这种情况下 人们不会奢侈地使用 IDE 开发工具 以调试模式运行事物或单步执行完整代码 因此在每条消息中记录类名 方法名和行号将非常有
  • try-with-resources 中出现死代码警告,但翻译后的 try-catch-finally 中没有出现死代码警告

    以下代码使用try 有资源 https docs oracle com javase specs jls se7 html jls 14 html jls 14 20 3Java 8 中引入的构造 偶尔抛出 方法被声明为抛出一个偶尔的异常

随机推荐

  • 搭建商城的微服务架构-1

    创建父项目 mall 先创建一个 父项目 mall 再在这个父项目中创建多个子项目 修改pom文件 最终mall的pom文件如下
  • 目标检测算法回顾之Anchor free篇章

    基于anchor free的目标检测方法 一 背景与定义 1 1 anchor based的特征 1 2 anchor的好处 1 3 anchor的局限 1 4 anchor free 与anchor based的区别 二 概述 三 早期探
  • 以太坊2.0 节点搭建:共识端+执行端

    1 配置 本人使用配置 共识端 prysm 版本 3 1 1 执行端 geth 版本 1 10 23 当前使用1 10 25 OS centos7 6 CPU 8核 Memory 16GB RAM Storage 3T SSD Networ
  • 算法梳理boosting\bagging\RF(1)

    LeetCode题目记录 1 集成学习概念 1 1 集成学习分类 1 2 集成学习步骤 2 个体学习器概念 3 boosting bagging 3 1 boosting 3 2 bagging 3 3 二者的区别 4 随机森林的思想 5
  • mlxtend实现简单的Apriori算法(关联算法)

    关联算法有几个重要的概念 下面以官方教程为例 Apple Beer Rice Chicken Apple Beer Rice Apple Beer Apple Bananas Milk Beer Rice Chicken Milk Beer
  • springmvc使用JSR-303进行校验

    下面提供一种springmvc的校验方案 一般没有校验或者手动写validator的话都要写好多代码好多if判断 使用JSR 303规范校验只需要在Pojo字段上加上相应的注解就可以实现校验了 1 依赖的jar包 我直接贴pom了
  • cannot find -l 问题处理

    graalvm打包最后一步 第七步 报错 主要错误简述 It appears as though libstdc a is missing Please install it cannot find lstdc 几个要点 1 l是link的
  • c++模板的概念全新解释

    文章目录 前言 一 模板的概念 强类型的严格性和灵活性 解决冲突的路径 模板的概念 一 1 函数模板 函数模板的定义 函数模板的实例化 函数模板的重载 1 函数模板的重载 2 用普通函数重载函数模板 3 用特定函数重载函数模板 类模板 类模
  • Jsch网络工具包的使用及源码简析

    一 背景 最近 导师安排了些看论文文献并整理论文至文件服务器的工作 在实验的过程中 我们知道常见的上传文件至服务器有以下方式 ftp sftp协议进行上传 ssh连接 并通过scp命令进行上传 通过xftp xshell ftplina等图
  • 【QT】Qt Creator 右击添加库无反应解决方案【转发】

    一 软件版本 Qt 5 9 0 二 问题现象 想向工程添加外部库 但点击添加库无反应 三 解决方案 1 打开 pro 文件 2 在 pro 文件界面内 右击鼠标 选择添加库 3 添加库的 UI 弹出 四 总结 Qt 5 9 0 Creato
  • c#.net常用的小函数参考

    选择自 crabapple2 的 Blog c net常用的小函数和方法集 1 DateTime 数字型 System DateTime currentTime new System DateTime 1 1 取当前年月日时分秒 curre
  • 记录JsonNode文本处理asText()和toString()的差异

    原文地址 https blog csdn net xudc0521 article details 89926158 最近使用JsonNode解析json字符串时 遇到一个与预期不一致的小问题 记录一下 先来看一个Test author x
  • log4j问题解决:log4j:WARN No appenders could be found for logger

    在resources目录下新建log4j properties文件 添加以下代码 log4j rootLogger ERROR log4j appender CONSOLE org apache log4j ConsoleAppender
  • 浙江大学 陈越_浙江大学陈越教授开展“程序设计课程建设”讲座

    12月10日下午 媒体工程学院耿卫东院长邀请了浙江大学陈越教授开展 程序设计课程建设 讲座 学院各课程群负责人 专业主任及其他专业教师共30余人聆听了讲座 并围绕 程序设计课程建设 的主题展开了深入探讨和交流 学院副院长章化冰主持讲座 代表
  • java基础学习 day25(二维数组)

    什么是二维数组 在数组中存放数组 二维数组的应用场景 当我们需要把数据分组管理的时候 就需要用二维数组 静态初始化格式 数据类型 数组名 new 数据类型 元素1 元素2 元素1 元素2 简化格式 数据类型 数组名 元素1 元素2 元素1
  • Java使用JVM工具检测问题

    1 jps 显示运行程序的进程 编码 主类目录信息 public class Demo01 jps 显示进程ID 主类名称 jps v 显示进程ID 主类名称以及详细编码信息 jps l 显示进程ID 主类目录 param args thr
  • 简单理解B树和B+树

    前言 前面我们说了红黑树 他是一种特殊的搜索树 但是由于他只是二叉树 所以这就导致他在大量的数据面前深度过高 同时会造成大量的磁盘空间浪费 所以我们又研究出来了B树和B 树 B树 他是人们早期的一种设计 他打破了二叉树的方式 它可以有多个分
  • android小项目之新闻客户端二

    基于Android的小巫新闻客户端开发 UI设计 主界面 2013年2月15日 由于太多事情要乱 不可能只专注一样东西 因为怕完成不了任务 原本这系列博客就是要在寒假搞定的 没想到拖了那么久 没办法 现在只能有空的时候就回顾一下小巫新闻客户
  • 多输入多输出

    多输入多输出 MATLAB实现DNN全连接神经网络多输入多输出 目录 多输入多输出 MATLAB实现DNN全连接神经网络多输入多输出 预测效果 基本介绍 模型结构 程序设计 参考资料 预测效果 基本介绍 DNN的结构不固定 一般神经网络包括
  • 所见即所得,使用Java将HTML解析为Excel,支持多级表头嵌套、单元格合并

    最近项目需要实现如题 所见即所得 的功能 之前每次生成Excel都需要重新从数据库查询一遍 降低效率不说 那些嵌套的表头实在是很难用Sql嵌套拼接实现 而且这样做还没有通用性 不同的表格需要写不同的Sql实现 非常繁琐 在网上找了很多关于H