一、概述
公司的项目遇到一个需求是从office word中导入数据回显到页面当中,里面穿插着一些数学公式,又不想使用图片的方式存储数学公式。所以敲定了将公式转换成latex表达式,然后前端使用MathJax渲染出数学公式的方案。
1.1 所需资源
名称 | 地址 | 描述 |
---|---|---|
mmltx.xsl | https://sourceforge.net/projects/xsltml/files/xsltml/ | MathML转LaTex工具 |
MML2OMML.XSL | https://download.csdn.net/download/han949417140/18465121 | OMML转为MathML的xsl工具 |
1.2 office文档中的公式编辑器
office自带公式编辑器
从2007版开始,Office也自带了一个公式编辑器。
在2007版中Word与Excel之间不同的是,前者插入的公式对象是Office MathML节点,后者插入的还是ole。
到了2010版开始,两个产品的公式编辑器插入的都是Office MathML节点了,但是两者对公式对象中的默认文字编码处理不同。
这些不同点可以看出就算同样属于Office的产品,他们之间也是有很多不统一的地方。公式表达式
LaTeX
LaTeX是一种基于ΤΕΧ的排版系统,它非常适用于生成高印刷质量的科技和数学类文档。
例如勾股定理用LaTeX表达:
a 2 + b 2 = c 2 {a}^{2}+{b}^{2}={c}^{2} a2+b2=c2
常用的LaTeX渲染组件是MathJax,
vue渲染教程请参考:http://www.manongjc.com/detail/11-pwanxyhkqeiokqi.html
我们在项目中使用的便是LaTeX,所以本次研究就是如何将Office中的公式对象转换成LaTeX表达式。Mathml
全称为数学标记语言(Mathematical Markup Language),是一种基于XML的标准,用来在互联网上书写数学符号和公式的置标语言。Office MathML (OMML)
在office2007之后版本所编辑的公式对象便是OMML。OMML是office为了配合Office Open Xml制定的数学标记语言。
1.3 转换关系
我们在项目中使用到的三者之间转换关系是:OMML -> MathML -> LaTex
Office在安装目录中提供了将OMML转为MathML的xsl工具:MML2OMML.XSL
MathML转LaTex使用网上找到另一个xsl工具mmltex.xsl 。
二、代码实现office公式解析
首先要说明我们的功能限制:只针对Office2011及以上的Office Open Xml文档,Word和Excel均可。
这里用Word文档为例子来说明解析过程。
2.1 引入pom包
pom…xml引入office接续所需jar
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.2.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml-schemas -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/ooxml-schemas -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>1.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/dom4j/dom4j -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/jaxen/jaxen -->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
2.2 导入解析所需工具
将上面下载的资源文件拷贝到项目resources/conventer下,如图:
2.3 编写代码解析office公式
package com.hanergy.out.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xwpf.usermodel.*;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
import org.springframework.web.multipart.MultipartFile;
import javax.xml.transform.*;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* word内容段落、公式xml标签
* 正常文本:
* <w:r> 标签对应一个 XWPFRun对象
* <w:t xml:space=“preserve”> 标签对应一段在 Word中的字符(也可以是一个字符)
* 公式:
* <w:object> 标签对应一个公式(当然我们这里只讲公式,此标签中也可以是一个 Excel也可以是一个 PPT等等)
* <v:shape> 标签中有个 style属性,这里 style就是图片在 Word中显示的宽高
* <v:imagedata> 标签关联着显示的图片( <v:imagedata>为 <v:shape>子标签)
* <o:OLEObject>标签关联着图片显示公式对应的二进制文件(二进制文件也是最重要的文件,没有这个文件当你在word中双击时,是打不开第三方公式插件的)
*
*/
/**
* @description:
* @author: Han LiDong
* @create: 2021/5/7 10:33
* @update: 2021/5/7 10:33
*/
@Slf4j
public class WordUtil {
public static void main(String[] args) throws Exception {
// MultipartFile file = null;
// InputStream inputStream = file.getInputStream();
// 所有公式latex表达式集合,借助mathJax可以在页面上进行展示
List <String> formulas = getFormulaMap(new FileInputStream("C:\\D\\算法-β系数.docx"));
log.info("解析到{}个公式",formulas.size());
// word解析 公式+文本
wordAnalysis(new FileInputStream("C:\\D\\算法-β系数.docx"));
}
}
/**
* word所有内容解析(公式、文本)
* @param inputStream
* @throws Exception
*/
public static void wordAnalysis(InputStream inputStream) throws Exception {
XWPFDocument word = new XWPFDocument(inputStream);
try {
for (IBodyElement ibodyelement : word.getBodyElements()) {
if (ibodyelement.getElementType().equals(BodyElementType.PARAGRAPH)) { //段落
XWPFParagraph paragraph = (XWPFParagraph) ibodyelement;
//段落解析
String paragraphStr = parseParagraph(paragraph);
} else if (ibodyelement.getElementType().equals(BodyElementType.TABLE)) { //表格
XWPFTable table = (XWPFTable) ibodyelement;
for (XWPFTableRow row : table.getRows()) { //行
for (XWPFTableCell cell : row.getTableCells()) { //cell
List<String> cellMath = new ArrayList<>(16);
for (XWPFParagraph paragraph : cell.getParagraphs()) { //段落
//cell段落解析
String paragraphStr = parseParagraph(paragraph);
if (!"".equals(paragraphStr.trim())){
cellMath.add(paragraphStr);
}
}
log.info("当前cell有{}个公式",cellMath.size());
}
}
}
}
} finally {
word.close();
}
}
/**
* 解析word中公式(转换成latex表达式)
*
* @param inputStream 文件流
* @return
*/
public static List<String> getFormulaMap(InputStream inputStream) throws IOException, DocumentException {
//XWPFDocument xwpfDocument = new XWPFDocument(inputStream);
Map<Integer, String> result = new HashMap<>();
XWPFDocument word = new XWPFDocument(inputStream);
//storing the found MathML in a AllayList of strings
List<String> mathMLList = new ArrayList<String>(16);
try {
for (IBodyElement ibodyelement : word.getBodyElements()) {
if (ibodyelement.getElementType().equals(BodyElementType.PARAGRAPH)) { //段落
XWPFParagraph paragraph = (XWPFParagraph) ibodyelement;
//段落解析
List<String> mathList = parseMathParagraph(paragraph);
mathMLList.addAll(mathList);
} else if (ibodyelement.getElementType().equals(BodyElementType.TABLE)) { //woed表格
XWPFTable table = (XWPFTable) ibodyelement;
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
List<String> cellMath = new ArrayList<>(16);
for (XWPFParagraph paragraph : cell.getParagraphs()) {
//cell段落解析
List<String> mathList = parseMathParagraph(paragraph);
mathMLList.addAll(mathList);
//cellMath.addAll(mathList);
}
}
}
}
}
} finally {
word.close();
}
log.info("当前文档一共有{}个公式",mathMLList.size());
return mathMLList;
}
/**
* 公式段落解析
* @param xwpfParagraph
* @throws DocumentException
*/
public static List<String> parseMathParagraph(XWPFParagraph xwpfParagraph) throws DocumentException {
CTP ctp = xwpfParagraph.getCTP();
String xmlText = ctp.xmlText();
List<String > mathList = new ArrayList<>();
if (xmlText.contains("<m:oMath>")) {
//得到根节点的值
SAXReader saxReader = new SAXReader();
//将String类型的字符串转换成XML文本对象
Document doc = saxReader.read(new ByteArrayInputStream(xmlText.getBytes()));
Element root = doc.getRootElement();
// 一个段落多个表达式解析
List<Element> omMaths = root.selectNodes("//m:oMath"); //用xpath得到OMML节点
for (Element ele : omMaths) {
/**
* OMML -> MathML -> LaTex
* Office在安装目录中提供了将OMML转为MathML的xsl工具:MML2OMML.XSL
* MathML转LaTex使用网上找到另一个xsl工具mmltex.xsl。
*/
String xml = ele.asXML();
//xml转 mathml
String mml = convertOMML2MML(xml);
//mathml转latx
String latex = convertMML2Latex(mml);
mathList.add(latex);
log.info("late表达式:{}" , latex);
}
}
return mathList;
}
/**
* 段落解析
* @param xwpfParagraph
* @throws DocumentException
*/
public static String parseParagraph(XWPFParagraph xwpfParagraph) throws DocumentException {
CTP ctp = xwpfParagraph.getCTP();
String xmlText = ctp.xmlText();
StringBuilder sb = new StringBuilder();
// if (xmlText.contains("<m:oMath>")) {
//段落文本内容
sb.append(xwpfParagraph.getParagraphText());
//段落公式解析
//得到根节点的值
SAXReader saxReader = new SAXReader();
//将String类型的字符串转换成XML文本对象
Document doc = saxReader.read(new ByteArrayInputStream(xmlText.getBytes()));
Element root = doc.getRootElement();
// 一个段落多个表达式解析
List<Element> omMaths = root.selectNodes("//m:oMath"); //用xpath得到OMML节点
if (omMaths != null && !omMaths.isEmpty()) {
for (Element ele : omMaths) {
/**
* OMML -> MathML -> LaTex
* Office在安装目录中提供了将OMML转为MathML的xsl工具:MML2OMML.XSL
* MathML转LaTex使用网上找到另一个xsl工具mmltex.xsl。
*/
String xml = ele.asXML();
//xml转 mathml
String mathml = convertOMML2MML(xml);
//mathml转latx
String latex = convertMML2Latex(mathml);
sb.append(latex);
log.info("latex表达式:{}",latex);
}
}
log.info("公式个数:{},解析内容:{}",omMaths.size(),sb.toString());
return sb.toString();
}
/**
* Description: xsl转换器</p>
* @param s 公式xml字符串
* @param xslpath 转换器路径
* @param uriResolver xls依赖文件
* @return
*/
public static String xslConvert(String s, String xslpath, URIResolver uriResolver){
TransformerFactory tFac = TransformerFactory.newInstance();
if(uriResolver != null) {
tFac.setURIResolver(uriResolver);
}
StreamSource xslSource = new StreamSource(WordUtil.class.getResourceAsStream(xslpath));
StringWriter writer = new StringWriter();
try {
Transformer t = tFac.newTransformer(xslSource);
Source source = new StreamSource(new StringReader(s));
Result result = new StreamResult(writer);
t.transform(source, result);
} catch (TransformerException e) {
log.error(e.getMessage(), e);
}
return writer.getBuffer().toString();
}
/**
* <p>Description: 将mathml转为latx </p>
* @param mml mathml字符串
* @return
*/
public static String convertMML2Latex(String mml){
mml = mml.substring(mml.indexOf("?>")+2, mml.length()); //去掉xml的头节点
URIResolver r = new URIResolver(){ //设置xls依赖文件的路径
@Override
public Source resolve(String href, String base) throws TransformerException {
File f = new File("/conventer/mml2tex/" + href);
InputStream inputStream = WordUtil.class.getResourceAsStream("/conventer/mml2tex/" + href);
return new StreamSource(inputStream);
}
};
String latex = xslConvert(mml, "/conventer/mml2tex/mmltex.xsl", r);
if(latex != null && latex.length() > 1){
latex = latex.substring(1, latex.length() - 1);
}
return latex;
}
/**
* <p>Description: office xml转为mathml </p>
* @param xml 公式xml
* @return
*/
public static String convertOMML2MML(String xml){
// 进行转换的过程中需要借助这个文件,一般来说本机安装office就会有这个文件,找到就可以
String result = xslConvert(xml, "/conventer/OMML2MML.XSL", null);
return result;
}
- 最终运行结果如图:
- word文档示例
- 在线解析latex表达式
从程序运行果果中拷贝一个latex表达式在线解析:x=\frac{-b\pm \sqrt{{b}^{2}-4ac}}{2a}
在线解析网址:https://latex.91maths.com/
参考博客:
https://blog.csdn.net/qq_41536778/article/details/108454415
https://www.cnblogs.com/wadmwz/p/10460033.html