缘起
起因任职公司为了所谓需要内网部署,不依赖外网的文本敏感词检测。这波啊,内网是假,不花钱是真,百度人家现成的文本检测API,成熟的,完善的API多香,非要手写一个。
考虑过蓝冰的word处理也不错,但是他的免费版会出现红字提醒,但是目的是不花钱。
查阅网上资料包括掘金,CSDN,百度等等。发现网上的比较少,大部分都是替换的功能,但是不能对敏感词高亮。或者对word处理的比较少。
所以没有办法,只能开发一个简单的文本探测的功能。可能代码不太优雅,或者存在一定缺陷,只提供各位参考,也希望技术大牛,可以提供一些更好的思路或者解法。
DOCX文档,敏感词探测高亮功能实现
不多废话,先看效果(演示文档内容百度搜的新闻,没有其他用意)
实现功能: 1.可替换标题,文本中的敏感词。
2.对表格中的单元格进行敏感词处理,如果单个格子里出现套娃字段也是可以处理。
不足: 对图片,图表直接跳过不做处理。
功能演示
原始文档
标记后文档
依赖
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>1.1</version>
</dependency>
业务代码
此处是上传下载的业务代码 视情况个人自己修改,我这里是上传至存储桶。
需要将脱敏的词 转换为正则表达式
/**
* regularExpression
* 对文章处理,保留原格式 替换新文档,异常啥的就不做处理里
* @param inputStream
* @param fileName
* @return
*/
public Map replaceAndRemark( String fileName) throws IOException{
InputStream inputStream = new FileInputStream("C:\\Users\\Administrator\\Desktop\\数据分析报告.docx");
//正则表达式,我是直接丢缓存中。
String regularExpression = "[代][孕][妈][妈]|[借][腹][生][子]";
//标记字段的字体 以及背景色
String targetColor = "ff0000"; // 红字
String targetBgColor = "ffff00"; // 黄底
//用于存储返回结果:保存生成文件地址 和 敏感词出现数量
Map result = new HashMap<String, Object>();
//初始化数量
int num = 0;
//使用try with resource 管理
try (XWPFDocument docx = new XWPFDocument(inputStream);
ByteArrayOutputStream out = new ByteArrayOutputStream()
) {
// 获取正则表式,获取pattern 供以下match使用
Pattern pattern = Pattern.compile(regularExpression);
//获取段落
List<XWPFParagraph> paragraphs = docx.getParagraphs();
//对单个段落遍历
for (XWPFParagraph paragraph : paragraphs) {
// 注: paragraph为文章中的段落 run 可以理解为给段落自动拆分的句子 但是他随机长度不一定以句号结尾 格式 指原文章中排版 比如段落字体 标题 大小等....
List<XWPFRun> runs = paragraph.getRuns();
doFilter(runs,pattern,paragraph);
}
//对文档表格处理
Iterator itTable = docx.getTablesIterator();
while (itTable.hasNext()) { //遍历表格
XWPFTable table = (XWPFTable) itTable.next();
int count = table.getNumberOfRows();//获得表格总行数
for (int i = 0; i < count; i++) { //遍历表格的每一行
XWPFTableRow row = table.getRow(i);//获得表格的行
List<XWPFTableCell> cells = row.getTableCells();//在行元素中,获得表格的单元格
for (XWPFTableCell cell : cells) { //遍历单元格
String text = cell.getText();
Matcher matcher = pattern.matcher(text);
if(matcher.find()) {
List<XWPFParagraph> cellParagraphs = cell.getParagraphs();
for(XWPFParagraph paragraph:cellParagraphs){
List<XWPFRun> runs = paragraph.getRuns();
doFilter(runs,pattern,paragraph);
}
}
}
}
}
// 保存文档
OutputStream os = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\test2.docx");
docx.write(os);
inputStream.close();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
doFliter业务代码
public void doFilter(List<XWPFRun> runs,Pattern pattern,XWPFParagraph paragraph){
Boolean flag = Boolean.FALSE;
for (int i = 0; i < runs.size(); ) {
// 以下逻辑:首先获取该run的String文本,如若存在关键词会拆分3段落 关键词前面为before 关键词为substring 关键词后为after
// 由于需要保留原文档格式,只能删除原来的run 注意必须要用paragraph.insertNewRun新建的run去写文本 如果直接调用createRun去写文本会导致原格式消失。
// 以下的flag 是标志位,用于判断是跳1个run 还是2个run 在for循环内 我们在判断出现关键词后 会进行run新增;如不出现 则run不新增
// 所以在给for循环的 i 需要根据flag标识位去判断+1 还是+2
flag = Boolean.FALSE;
//获取原来的run的内容,字体,以及字体大小
String text = runs.get(i).getText(0);
String fontFamily = runs.get(i).getFontFamily();
int fontSize = runs.get(i).getFontSize();
if(Objects.nonNull(text)) {
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
String substring = text.substring(matcher.start(), matcher.end());
String before = text.substring(0, matcher.start()); // 取出段落中关键字之前的内容
String after = text.substring(matcher.end());
//去除原来的内容,进行替换
paragraph.removeRun(i);
XWPFRun newRun = paragraph.insertNewRun(i);
newRun.setText(before);
XWPFRun newRun2 = paragraph.insertNewRun(i + 1);
newRun2.setBold(true);
newRun2.setColor("ff0000");
newRun2.setFontFamily(fontFamily);
// 部分字段比如标题 他会出现fontSize为-1导致出现问题 这样只能默认暂未找到解决方法
if(fontSize != -1) {
newRun2.setFontSize(fontSize);
}
newRun2.setText(substring);
XWPFRun newRun3 = paragraph.insertNewRun(i + 2);
newRun3.setText(after);
flag = true;
}
i = i + (flag ? 2 : 1);
}else{
i++;
}
}
WORD绘制表格 进行替换
自定义模板文档
此处是word
处理后
此处是转PDF后的
/**
** document
* map 为 KEY VALUE形式 如上图 map.put("X01","2023年04月03日");类推,自定义。
**/
public static <K, V> void searchAndReplace(XWPFDocument document, Map<String,String> map) {
try {
// 替换段落中的指定文字
Iterator itPara = document.getParagraphsIterator();//获得word中段落
while (itPara.hasNext()) { //遍历段落
XWPFParagraph paragraph = (XWPFParagraph) itPara.next();
Set set = map.keySet();
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
String key = iterator.next().toString();
List<XWPFRun> run = paragraph.getRuns();
for (int i = 0; i < run.size(); i++) {
if (run.get(i).getText(run.get(i).getTextPosition()) != null &&
run.get(i).getText(run.get(i).getTextPosition()).equals(key)) {
/**
* 参数0表示生成的文字是要从哪一个地方开始放置,设置文字从位置0开始
* 就可以把原来的文字全部替换掉了
*/
run.get(i).setText(map.get(key), 0);
}
}
Iterator itTable = document.getTablesIterator();
while (itTable.hasNext()) { //遍历表格
XWPFTable table = (XWPFTable) itTable.next();
int count = table.getNumberOfRows();//获得表格总行数
for (int i = 0; i < count; i++) { //遍历表格的每一行
XWPFTableRow row = table.getRow(i);//获得表格的行
List<XWPFTableCell> cells = row.getTableCells();//在行元素中,获得表格的单元格
for (XWPFTableCell cell : cells) { //遍历单元格
for (Map.Entry<String, String> e : map.entrySet()) {
String xx = cell.getText();
if (xx.equals(e.getKey())) {//如果单元格中的变量和‘键’相等,就用‘键’所对应的‘值’代替。
cell.removeParagraph(0);//所以这里就要求每一个单元格只能有唯一的变量。
//这边不用cell.setText 设置内容是为了设置样式
//主要是出现了 setText后他会默认宋体(正文)样式 然后导致我WORD转PDF失败
//所以后来指定黑体才能显示
XWPFParagraph paragraph1 = cell.addParagraph();
XWPFRun run1 = paragraph1.createRun();
run1.setFontFamily("黑体");
run1.setText(e.getValue());
setAlignCenter(cell);
}else if (xx.contains(e.getKey())){
cell.removeParagraph(0);
XWPFParagraph paragraph1 = cell.addParagraph();
XWPFRun run1 = paragraph1.createRun();
run1.setFontFamily("黑体");
run1.setText(xx.replace(e.getKey(),e.getValue()));
if (xx.contains("编号")){
setAlignRight(cell);
}
}
}
}
}
}
}
}
return;
}catch (Exception e){
e.printStackTrace();
}
}
Word转PDF就不细说了 ,注意点就是字体库问题
PdfConverter.getInstance().convert(xwpfDocument,fileOutputStream,pdfOptions);