HTML转换为 Apache POI的RichTextString

位置:首页>文章>详情   分类: Java教程 > 编程技术   阅读(90)   2024-06-15 06:21:34

一、概述

在本教程中,我们将构建一个将 HTML 作为输入的应用程序,并使用提供的 HTML 的 RichText 表示 创建 Microsoft Excel 工作簿。要生成 Microsoft Excel 工作簿,我们将使用 Apache POI。为了分析 HTML,我们将使用 Jericho。

Github 上提供了本教程的完整源代码。

2. 什么是耶利哥?

Jericho 是一个 java 库,它允许分析和操作 HTML 文档的各个部分,包括服务器端标记,同时逐字复制任何无法识别或无效的 HTML。它还提供高级 HTML 表单操作功能。它是根据以下许可证发布的开源库:Eclipse 公共许可证 (EPL)GNU 宽松通用公共许可证 (LGPL)Apache 许可证

我发现 Jericho 非常易于使用,可以实现将 HTML 转换为 RichText 的目标。

3.pom.xml

这是我们正在构建的应用程序所需的依赖项。请注意,对于此应用程序,我们必须使用 Java 9。这是因为我们使用的 java.util.regex appendReplacement 方法 从 Java 9 开始才可用。

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>1.5.9.RELEASE</version>
	<relativePath /> <!-- lookup parent from repository -->
</parent>

<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	<java.version>9</java.version>
</properties>

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-batch</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-thymeleaf</artifactId>
	</dependency>

	<dependency>
		<groupId>com.h2database</groupId>
		<artifactId>h2</artifactId>
		<scope>runtime</scope>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
	<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
	<dependency>
		<groupId>org.apache.commons</groupId>
		<artifactId>commons-lang3</artifactId>
		<version>3.7</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.batch</groupId>
		<artifactId>spring-batch-test</artifactId>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.apache.poi</groupId>
		<artifactId>poi</artifactId>
		<version>3.15</version>
	</dependency>

	<dependency>
		<groupId>org.apache.poi</groupId>
		<artifactId>poi-ooxml</artifactId>
		<version>3.15</version>
	</dependency>
	<!-- https://mvnrepository.com/artifact/net.htmlparser.jericho/jericho-html -->
	<dependency>
		<groupId>net.htmlparser.jericho</groupId>
		<artifactId>jericho-html</artifactId>
		<version>3.4</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-configuration-processor</artifactId>
		<optional>true</optional>
	</dependency>
	<!-- legacy html allow -->
	<dependency>
		<groupId>net.sourceforge.nekohtml</groupId>
		<artifactId>nekohtml</artifactId>
	</dependency>
</dependencies>

4. 网页 – Thymeleaf

我们使用 Thymeleaf 创建一个基本网页,该网页具有带有文本区域的表单。 Thymeleaf 页面的源代码在 Github 上可用。如果我们愿意,这个文本区域可以用 RichText 编辑器替换,例如 CKEditor。我们必须注意使用适当的 setData 方法使 AJAX 的 data 正确。之前有一个关于 CKeditor 的教程,标题为 AJAX with CKEditor in Spring Boot

5.控制器

在我们的控制器中,我们自动装配 JobLauncher 和我们将要创建的名为 GenerateExcel 的 Spring Batch 作业。自动装配这两个类允许我们在将 POST 请求发送到 “/export” 时按需运行 Spring Batch Job GenerateExcel

另一件需要注意的事情是,为了确保 Spring Batch 作业将多次运行,我们在这段代码中包含唯一参数:addLong(“uniqueness”, System.nanoTime()).toJobParameters()。如果我们不包含唯一参数,可能会发生错误,因为只有唯一的 JobInstances 可能会被创建和执行,否则 Spring Batch 无法区分第一个和第二个 JobInstance .

@Controller
public class WebController {

    private String currentContent;

    @Autowired
    JobLauncher jobLauncher;
    
    @Autowired
    GenerateExcel exceljob; 

    @GetMapping("/")
    public ModelAndView getHome() {
        ModelAndView modelAndView = new ModelAndView("index");
        return modelAndView;

    }
    

    @PostMapping("/export")
    public String postTheFile(@RequestBody String body, RedirectAttributes redirectAttributes, Model model)
        throws IOException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException {


        setCurrentContent(body);

        Job job = exceljob.ExcelGenerator();
        jobLauncher.run(job, new JobParametersBuilder().addLong("uniqueness", System.nanoTime()).toJobParameters()
            );

        return "redirect:/";
    }

    //standard getters and setters

}

6.批处理作业

在批处理作业的第 1 步中,我们调用 getCurrentContent() 方法来获取传递到 Thymeleaf 表单中的内容,创建一个新的 XSSFWorkbook,指定任意 Microsoft Excel 工作表选项卡名称,然后将所有三个变量传递到 createWorksheet 方法中我们将在教程的下一步中制作:

@Configuration
@EnableBatchProcessing
@Lazy
public class GenerateExcel {
    
    List<String> docIds = new ArrayList<String>();

    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Autowired
    WebController webcontroller;
    
    @Autowired
    CreateWorksheet createexcel;

    @Bean
    public Step step1() {
        return stepBuilderFactory.get("step1")
            .tasklet(new Tasklet() {
                @Override
                public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception, JSONException {

                    String content = webcontroller.getCurrentContent();
                    
                    System.out.println("content is ::" + content);
                    Workbook wb = new XSSFWorkbook();
                    String tabName = "some";
                    createexcel.createWorkSheet(wb, content, tabName);

                    return RepeatStatus.FINISHED;
                }
            })
            .build();
    }

    @Bean
    public Job ExcelGenerator() {
        return jobBuilderFactory.get("ExcelGenerator")
            .start(step1())
            .build();

    }

}

我们在其他教程中介绍了 Spring Batch,例如 Converting XML to JSON + Spring BatchSpring Batch CSV Processing

7. Excel 创建服务

我们使用各种类来创建我们的 Microsoft Excel 文件。在处理将 HTML 转换为 RichText 时,顺序很重要,因此这将是一个重点。

7.1 富文本详细信息

一个有两个参数的类:一个字符串,其中包含我们将成为 RichText 的内容和一个字体映射。

public class RichTextDetails {
    private String richText;
    private Map<Integer, Font> fontMap;
    //standard getters and setters
    @Override
    public int hashCode() {
     
        // The goal is to have a more efficient hashcode than standard one.
        return richText.hashCode();
    }

7.2 富文本信息

一个 POJO,它将跟踪 RichText 的位置,而不是:

public class RichTextInfo {
    private int startIndex;
    private int endIndex;
    private STYLES fontStyle;
    private String fontValue;
    // standard getters and setters, and the like

7.3 样式

包含我们要处理的 HTML 标记的枚举。我们可以根据需要添加:

public enum STYLES {
    BOLD("b"), 
    EM("em"), 
    STRONG("strong"), 
    COLOR("color"), 
    UNDERLINE("u"), 
    SPAN("span"), 
    ITALLICS("i"), 
    UNKNOWN("unknown"),
    PRE("pre");
    // standard getters and setters

7.4 标签信息

跟踪标签信息的 POJO:

public class TagInfo {
    private String tagName;
    private String style;
    private int tagType;
    // standard getters and setters

7.5 HTML 到 RichText

这不是一个小类,所以让我们通过方法来分解它。

本质上,我们用 div 标签围绕任意 HTML,因此我们知道我们在寻找什么。然后我们查找 div 标签内的所有元素,将每个元素添加到 RichTextDetails 的 ArrayList 中,然后将整个 ArrayList 传递给 mergeTextDetails 方法。 mergeTextDetails 返回 RichtextString,也就是我们需要设置的单元格值:

public RichTextString fromHtmlToCellValue(String html, Workbook workBook){
       Config.IsHTMLEmptyElementTagRecognised = true;
       
       Matcher m = HEAVY_REGEX.matcher(html);
       String replacedhtml =  m.replaceAll("");
       StringBuilder sb = new StringBuilder();
       sb.insert(0, "<div>");
       sb.append(replacedhtml);
       sb.append("</div>");
       String newhtml = sb.toString();
       Source source = new Source(newhtml);
       List<RichTextDetails> cellValues = new ArrayList<RichTextDetails>();
       for(Element el : source.getAllElements("div")){
           cellValues.add(createCellValue(el.toString(), workBook));
       }
       RichTextString cellValue = mergeTextDetails(cellValues);

       
       return cellValue;
   }

正如我们在上面看到的,我们在这个方法中传递了一个 RichTextDetails 的 ArrayList。 Jericho 有一个设置,它采用布尔值来识别空标签元素,例如:Config.IsHTMLEmptyElementTagRecognised。这在处理在线富文本编辑器时可能很重要,因此我们将其设置为 true。因为我们需要跟踪元素的顺序,所以我们使用 LinkedHashMap 而不是 HashMap。

private static RichTextString mergeTextDetails(List<RichTextDetails> cellValues) {
        Config.IsHTMLEmptyElementTagRecognised = true;
        StringBuilder textBuffer = new StringBuilder();
        Map<Integer, Font> mergedMap = new LinkedHashMap<Integer, Font>(550, .95f);
        int currentIndex = 0;
        for (RichTextDetails richTextDetail : cellValues) {
            //textBuffer.append(BULLET_CHARACTER + " ");
            currentIndex = textBuffer.length();
            for (Entry<Integer, Font> entry : richTextDetail.getFontMap()
                .entrySet()) {
                mergedMap.put(entry.getKey() + currentIndex, entry.getValue());
            }
            textBuffer.append(richTextDetail.getRichText())
                .append(NEW_LINE);
        }

        RichTextString richText = new XSSFRichTextString(textBuffer.toString());
        for (int i = 0; i < textBuffer.length(); i++) {
            Font currentFont = mergedMap.get(i);
            if (currentFont != null) {
                richText.applyFont(i, i + 1, currentFont);
            }
        }
        return richText;
    }

如上所述,我们使用 Java 9 以便将 StringBuilder 与 java.util.regex.Matcher.appendReplacement 一起使用。为什么?那是因为 StringBuffer 在操作上比 StringBuilder 慢。 StringBuffer 函数为了线程安全而同步,因此速度较慢。

我们使用 Deque 而不是 Stack,因为 Deque 接口提供了一组更完整和一致的 LIFO 堆栈操作:

static RichTextDetails createCellValue(String html, Workbook workBook) {
        Config.IsHTMLEmptyElementTagRecognised  = true;
        Source source = new Source(html);
        Map<String, TagInfo> tagMap = new LinkedHashMap<String, TagInfo>(550, .95f);
        for (Element e : source.getChildElements()) {
            getInfo(e, tagMap);
        }

        StringBuilder sbPatt = new StringBuilder();
        sbPatt.append("(").append(StringUtils.join(tagMap.keySet(), "|")).append(")");
        String patternString = sbPatt.toString();
        Pattern pattern = Pattern.compile(patternString);
        Matcher matcher = pattern.matcher(html);

        StringBuilder textBuffer = new StringBuilder();
        List<RichTextInfo> textInfos = new ArrayList<RichTextInfo>();
        ArrayDeque<RichTextInfo> richTextBuffer = new ArrayDeque<RichTextInfo>();
        while (matcher.find()) {
            matcher.appendReplacement(textBuffer, "");
            TagInfo currentTag = tagMap.get(matcher.group(1));
            if (START_TAG == currentTag.getTagType()) {
                richTextBuffer.push(getRichTextInfo(currentTag, textBuffer.length(), workBook));
            } else {
                if (!richTextBuffer.isEmpty()) {
                    RichTextInfo info = richTextBuffer.pop();
                    if (info != null) {
                        info.setEndIndex(textBuffer.length());
                        textInfos.add(info);
                    }
                }
            }
        }
        matcher.appendTail(textBuffer);
        Map<Integer, Font> fontMap = buildFontMap(textInfos, workBook);

        return new RichTextDetails(textBuffer.toString(), fontMap);
    }

我们可以在这里看到 RichTextInfo 的用武之地:

private static Map<Integer, Font> buildFontMap(List<RichTextInfo> textInfos, Workbook workBook) {
        Map<Integer, Font> fontMap = new LinkedHashMap<Integer, Font>(550, .95f);

        for (RichTextInfo richTextInfo : textInfos) {
            if (richTextInfo.isValid()) {
                for (int i = richTextInfo.getStartIndex(); i < richTextInfo.getEndIndex(); i++) {
                    fontMap.put(i, mergeFont(fontMap.get(i), richTextInfo.getFontStyle(), richTextInfo.getFontValue(), workBook));
                }
            }
        }

        return fontMap;
    }

我们在哪里使用 STYLES 枚举:

private static Font mergeFont(Font font, STYLES fontStyle, String fontValue, Workbook workBook) {
        if (font == null) {
            font = workBook.createFont();
        }

        switch (fontStyle) {
        case BOLD:
        case EM:
        case STRONG:
            font.setBoldweight(Font.BOLDWEIGHT_BOLD);
            break;
        case UNDERLINE:
            font.setUnderline(Font.U_SINGLE);
            break;
        case ITALLICS:
            font.setItalic(true);
            break;
        case PRE:
            font.setFontName("Courier New");
        case COLOR:
            if (!isEmpty(fontValue)) {

                font.setColor(IndexedColors.BLACK.getIndex());
            }
            break;
        default:
            break;
        }

        return font;
    }

我们正在使用 TagInfo 类来跟踪当前标签:

private static RichTextInfo getRichTextInfo(TagInfo currentTag, int startIndex, Workbook workBook) {
        RichTextInfo info = null;
        switch (STYLES.fromValue(currentTag.getTagName())) {
        case SPAN:
            if (!isEmpty(currentTag.getStyle())) {
                for (String style : currentTag.getStyle()
                    .split(";")) {
                    String[] styleDetails = style.split(":");
                    if (styleDetails != null && styleDetails.length > 1) {
                        if ("COLOR".equalsIgnoreCase(styleDetails[0].trim())) {
                            info = new RichTextInfo(startIndex, -1, STYLES.COLOR, styleDetails[1]);
                        }
                    }
                }
            }
            break;
        default:
            info = new RichTextInfo(startIndex, -1, STYLES.fromValue(currentTag.getTagName()));
            break;
        }
        return info;
    }

我们处理 HTML 标签:

private static void getInfo(Element e, Map<String, TagInfo> tagMap) {
        tagMap.put(e.getStartTag()
            .toString(),
            new TagInfo(e.getStartTag()
                .getName(), e.getAttributeValue("style"), START_TAG));
        if (e.getChildElements()
            .size() > 0) {
            List<Element> children = e.getChildElements();
            for (Element child : children) {
                getInfo(child, tagMap);
            }
        }
        if (e.getEndTag() != null) {
            tagMap.put(e.getEndTag()
                .toString(),
                new TagInfo(e.getEndTag()
                    .getName(), END_TAG));
        } else {
            // Handling self closing tags
            tagMap.put(e.getStartTag()
                .toString(),
                new TagInfo(e.getStartTag()
                    .getName(), END_TAG));
        }
    }

7.6 创建工作表

使用 StringBuilder,我创建了一个将写入 FileOutPutStream 的字符串。在实际应用中,这应该是用户定义的。我在不同的两行中附加了我的文件夹路径和文件名。请将文件路径更改为您自己的路径。

sheet.createRow(0) 在第一行创建一行,dataRow.createCell(0) 在该行的 A 列创建一个单元格。

public void createWorkSheet(Workbook wb, String content, String tabName) {
        StringBuilder sbFileName = new StringBuilder();
        sbFileName.append("/Users/mike/javaSTS/michaelcgood-apache-poi-richtext/");
        sbFileName.append("myfile.xlsx");
        String fileMacTest = sbFileName.toString();
        try {
            this.fileOut = new FileOutputStream(fileMacTest);
        } catch (FileNotFoundException ex) {
            Logger.getLogger(CreateWorksheet.class.getName())
                .log(Level.SEVERE, null, ex);
        }

        Sheet sheet = wb.createSheet(tabName); // Create new sheet w/ Tab name

        sheet.setZoom(85); // Set sheet zoom: 85%
        

        // content rich text
        RichTextString contentRich = null;
        if (content != null) {
            contentRich = htmlToExcel.fromHtmlToCellValue(content, wb);
        }


        // begin insertion of values into cells
        Row dataRow = sheet.createRow(0);
        Cell A = dataRow.createCell(0); // Row Number
        A.setCellValue(contentRich);
        sheet.autoSizeColumn(0);
        
        
        try {
            /////////////////////////////////
            // Write the output to a file
            wb.write(fileOut);
            fileOut.close();
        } catch (IOException ex) {
            Logger.getLogger(CreateWorksheet.class.getName())
                .log(Level.SEVERE, null, ex);
        }


    }

8.演示

我们访问 localhost:8080

我们用一些 HTML 输入一些文本:

我们打开我们的 excel 文件并查看我们创建的 RichText:

9.总结

我们可以看到将 HTML 转换为 Apache POI 的 RichTextString 类并非易事;但是,对于业务应用程序,将 HTML 转换为 RichTextString 可能是必不可少的,因为 Microsoft Excel 文件的可读性很重要。我们构建的应用程序的性能可能还有改进的空间,但我们涵盖了构建此类应用程序的基础。

完整的源代码可在 Github 上找到。

标签2: Java教程 Apache
地址:https://www.cundage.com/article/jcg-converting-html-richtextstring-apache-poi.html

相关阅读

Apache Fluo 是 Apache Accumulo [3] 的 Percolator [2](用于填充 Google 的搜索索引)的开源实现。使用 Fluo,用户可以不断地将新数据加入到...
Apache FileUtils 教程展示了如何使用 Apache FileUtils 在 Java 中处理文件和目录。这些示例读取、写入、复制、创建、删除、列出和获取文件大小。 Apache ...
Apache Pulsar 是一个开源分布式发布-订阅消息传递系统,最初由 Yahoo 创建,是 Apache Software Foundation 的一部分。
一、概述 在本教程中,我们将构建一个将 HTML 作为输入的应用程序,并使用提供的 HTML 的 RichText 表示 创建 Microsoft Excel 工作簿。
课程大纲 Elasticsearch 是一个基于 Lucene 的搜索引擎。它提供了一个分布式的、支持多租户的全文搜索引擎,带有 HTTP Web 界面和无模式的 JSON 文档。 Elasti...
几个月前,我有幸参与了一些使用 Apache Spark 的 PoC(概念验证)。在那里,我有机会使用弹性分布式数据集(简称 RDD)、转换和操作。 几天后,我意识到虽然 Apache Spar...
虽然我通常使用 Groovy 来编写 JVM 托管的脚本以从命令行运行,但有时我需要在 Java 应用程序中解析命令行参数并且有大量可用的库供 Java 开发人员用来解析命令行参数。在本文中,我...
介绍 Apache Flink 是一个用于分布式流和批数据处理的开源平台。 Flink 是一个流式数据流引擎,具有多个 API 以创建面向数据流的应用程序。 Flink 应用程序使用 Apach...
我在构建spring MVC 文件上传示例时遇到了这个异常。错误堆栈跟踪如下所示: java.lang.ClassNotFoundException: org.apache.commons.fi...
什么是Apache卡夫卡? Apache Kafka 是一个分布式流系统,可以发布和订阅记录流。另一方面,它是企业消息传递系统。