基于语法树对文章中的章节数据进行匹配
发表于|更新于
|阅读量:
背景
之前有尝试过使用正则提取一篇文章中的标题信息,并还原其中的层级,碰到以下几个问题:
- 正则规则复杂,调试不方便
- 正则对于匹配到标题中的编号信息需要在代码中进行二次处理,处理的步骤也很麻烦,需要考虑多种边界条件
- 当正则变动时,对应的代码也需要进行变动
突发奇想是否可以使用ANTLR4通过构建语法树的方式来解决这几个问题
构建语法树
标题识别的思路
常见标题样式可分为以下两种:
(左侧分隔符)
编号
右侧分隔符
正文
,比如:
左侧分隔符不一定存在,但是右侧分隔符会存在
编号
.编号
.编号
右侧分隔符
正文
,比如:
对于列举式,像带有如下列所述
等字眼,通过a 、b、c等序号一条一条列举出来的暂时不在文本考虑范围内。
开始构建语法树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| grammar TitleDetect;
//代表整个段落,有3个部分 expr: title_begin ? num_seg splite ; //代表段落第一部分,标题的开始 title_begin: TITLE_BEGIN ; //代表段落第二部分,编号部分 num_seg: (num_cn|num) (multive_level_num_splite (num_cn|num))* ; //代表段落第三部分,分隔符 splite: (SPLITE_CHAR|SPLITE_SYMBLE|WS|MULTIVE_LEVEL_NUM_SPLITE|NOT_NATURE_CHAR)+ ;
num_cn: NUM_CN ; num: NUM ;
multive_level_num_splite: MULTIVE_LEVEL_NUM_SPLITE ;
WS : [ \t\r] ; // spaces, tabs TITLE_BEGIN : '第'; // 匹配开头 NUM : [0-9]+; // 匹配阿拉伯数字 NUM_CN : [一二三四五六七八九十]+; // 匹配中文阿拉伯数字 MULTIVE_LEVEL_NUM_SPLITE : [.]+; SPLITE_SYMBLE : [。.,、,::]+; SPLITE_CHAR : [条章节话目]+; NOT_NATURE_CHAR: ~[a-zA-Z0-9\u4e00-\u9fa5]; CHAE: .;
|
使用antlr4-maven-plugin插件进行编译
插件文档地址: https://www.antlr.org/api/maven-plugin/latest/antlr4-mojo.html
在pom.xml中加入antlr4-maven-plugin插件
注意g4文件所编译代码中的包名,此处演示使用antlr4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <plugin> <groupId>org.antlr</groupId> <artifactId>antlr4-maven-plugin</artifactId> <version>${antlr4.version}</version> <executions> <execution> <goals> <goal>antlr4</goal> </goals> </execution> </executions> <configuration> <visitor>true</visitor> <arguments> <argument>-package</argument> <argument>antlr4</argument> //此处填写包名 </arguments> </configuration> </plugin>
|
用于测试的文件
1 2 3 4 5 6 7
| 第一章:这是第一章 第二章:这是第二章 第三章:这是第三章
1、 这是一级标题 1.2、 这是二级标题 1.2.3、 这是三级标题
|
演示代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| package org.example;
import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Scanner;
import antlr4.TitleDetectBaseVisitor; import antlr4.TitleDetectLexer; import antlr4.TitleDetectParser; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream;
public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(Main.class.getClassLoader().getResourceAsStream("input.txt")); while (scanner.hasNext()) { String input = scanner.nextLine(); TitleDetectLexer lexer = new TitleDetectLexer(CharStreams.fromString(input)); lexer.removeErrorListeners(); CommonTokenStream tokens = new CommonTokenStream(lexer); TitleDetectParser parser = new TitleDetectParser(tokens); parser.removeErrorListeners(); ResultWrapper result = parser.expr().accept(new TitleDetectVisitorImpl()); if (parser.getNumberOfSyntaxErrors() == 0 && !result.level.isEmpty()) { System.out.format("%s : %s\n", input, result); } } scanner.close(); } }
class TitleDetectVisitorImpl extends TitleDetectBaseVisitor<ResultWrapper> { Map<String, Integer> cn2int = Map.of("一", 1, "二", 2, "三", 3, "四", 4, "五", 5, "六", 6, "七", 7, "八", 8, "九", 9, "十", 10);
List<Integer> level = new ArrayList<>(); StringBuilder sb = new StringBuilder();
@Override public ResultWrapper visitTitle_begin(TitleDetectParser.Title_beginContext ctx) { sb.append(ctx.getText()); return super.visitTitle_begin(ctx); }
@Override public ResultWrapper visitSplite(TitleDetectParser.SpliteContext ctx) { sb.append(ctx.getText()); return super.visitSplite(ctx); }
@Override public ResultWrapper visitNum(TitleDetectParser.NumContext ctx) { sb.append("num"); level.add(Integer.valueOf(ctx.getText())); return super.visitNum(ctx); }
@Override public ResultWrapper visitNum_cn(TitleDetectParser.Num_cnContext ctx) { sb.append("num_cn"); level.add(cn2int.get(ctx.getText())); return super.visitNum_cn(ctx); }
@Override public ResultWrapper visitMultive_level_num_splite(TitleDetectParser.Multive_level_num_spliteContext ctx) { sb.append(ctx.getText()); return super.visitMultive_level_num_splite(ctx); }
public ResultWrapper defaultResult() { return new ResultWrapper(sb.toString(), level); } }
class ResultWrapper { public final String patten; public final List<Integer> level;
ResultWrapper(String patten, List<Integer> level) { this.patten = patten; this.level = level; }
@Override public String toString() { return "ResultWrapper{" + "patten='" + patten + '\'' + ", level=" + level + '}'; } }
|
演示结果
此处拿到了标题的patten,并且可以拿到对应的标题中编号数值及编号层级,在此基础上可以进行对标题层级的还原。
基于语法树的方式相较基于正则的方式在后续维护过程中更为直观,并且在灵活性和性能上也更高。