java - 使用多态性对正则表达式解析器进行建模

标签 java regex parsing polymorphism abstract-class

所以,我正在为学校做一个正则表达式解析器,它创建负责匹配的对象层次结构。我决定采用面向对象的方式,因为我更容易想象这种语法的实现。所以,这些是我的正则表达式类。全部都是用 Java 编写的,但我认为如果您精通任何面向对象的语言,您就可以跟着学习。

我们需要实现的唯一运算符是 Union (+), Kleene-Star (*),表达式的串联(ab 或可能是 (a+b)c),当然还有括号,如串联示例中所示。这就是我现在所实现的,我让它像一个魅力一样工作,但主要是有一点开销。

父类Regexp.java

public abstract class Regexp {

    //Print out the regular expression it's holding
    //Used for debugging purposes
    abstract public void print();

    //Checks if the string matches the expression it's holding
    abstract public Boolean match(String text);

    //Adds a regular expression to be operated upon by the operators
    abstract public void add(Regexp regexp);

    /*
    *To help the main with the overhead to help it decide which regexp will
    *hold the other
    */
    abstract public Boolean isEmpty();

}

有最简单的正则表达式 Base.java,它保存一个字符,如果字符串与该字符匹配则返回 true。

public class Base extends Regexp{
    char c;

    public Base(char c){
        this.c = c;
    }

    public Base(){
        c = null;
    }

    @Override
    public void print() {
        System.out.println(c);
    }

    //If the string is the char, return true
    @Override
    public Boolean match(String text) {
        if(text.length() > 1) return false;
        return text.startsWith(""+c);
    }

    //Not utilized, since base is only contained and cannot contain
    @Override
    public void add(Regexp regexp) {

    }

    @Override
    public Boolean isEmpty() {
        return c == null;
    }

}

括号,Paren.java,用于在其中保存正则表达式。这里没什么特别的,但说明了匹配是如何工作的。

public class Paren extends Regexp{
    //Member variables: What it's holding and if it's holding something
    private Regexp regexp;
    Boolean empty;

    //Parenthesis starts out empty
    public Paren(){
        empty = true;
    }

    //Unless you create it with something to hold
    public Paren(Regexp regexp){
        this.regexp = regexp;
        empty = false;
    }

    //Print out what it's holding
    @Override
    public void print() {
        regexp.print();
    }

    //Real simple; either what you're holding matches the string or it doesn't
    @Override
    public Boolean match(String text) {
        return regexp.match(text);
    }

    //Pass something for it to hold, then it's not empty
    @Override
    public void add(Regexp regexp) {
        this.regexp = regexp;
        empty = false;
    }

    //Return if it's holding something
    @Override
    public Boolean isEmpty() {
        return empty;
    }

}

一个Union.java,它是两个可以匹配的正则表达式。如果其中一个匹配,则整个联盟匹配。

public class Union extends Regexp{
    //Members
    Regexp lhs;
    Regexp rhs;

    //Indicating if there's room to push more stuff in
    private Boolean lhsEmpty;
    private Boolean rhsEmpty;

    public Union(){
        lhsEmpty = true;
        rhsEmpty = true;
    }

    //Can start out with something on the left side
    public Union(Regexp lhs){
        this.lhs = lhs;

        lhsEmpty = false;
        rhsEmpty = true;
    }

    //Or with both members set
    public Union(Regexp lhs, Regexp rhs) {
        this.lhs = lhs;
        this.rhs = rhs;

        lhsEmpty = false;
        rhsEmpty = false;
    }

    //Some stuff to help me see the unions format when I'm debugging
    @Override
    public void print() {
        System.out.println("(");
        lhs.print();
        System.out.println("union");
        rhs.print();
        System.out.println(")");

    }

    //If the string matches the left side or right side, it's a match
    @Override
    public Boolean match(String text) {
        if(lhs.match(text) || rhs.match(text)) return true;
        return false;
    }

    /*
    *If the left side is not set, add the member there first
    *If not, and right side is empty, add the member there
    *If they're both full, merge it with the right side
    *(This is a consequence of left-to-right parsing)
    */
    @Override
    public void add(Regexp regexp) {
        if(lhsEmpty){
        lhs = regexp;

        lhsEmpty = false;
        }else if(rhsEmpty){
            rhs = regexp;

            rhsEmpty = false;
        }else{
            rhs.add(regexp);
        }
    }

    //If it's not full, it's empty
    @Override
    public Boolean isEmpty() {
        return (lhsEmpty || rhsEmpty);
    }
}

串联,Concat.java,它基本上是链接在一起的正则表达式列表。这个很复杂。

public class Concat extends Regexp{
    /*
    *The list of regexps is called product and the 
    *regexps inside called factors
    */
    List<Regexp> product;

    public Concat(){
        product = new ArrayList<Regexp>();
    }

    public Concat(Regexp regexp){
        product = new ArrayList<Regexp>();
        pushRegexp(regexp);
    }

    public Concat(List<Regexp> product) {
        this.product = product;
    }

    //Adding a new regexp pushes it into the list
    public void pushRegexp(Regexp regexp){
        product.add(regexp);
    }
    //Loops over and prints them
    @Override
    public void print() {
        for(Regexp factor: product){
            factor.print();
        }
    }

    /*
    *Builds up a substring approaching the input string.
    *When it matches, it builds another substring from where it 
    *stopped. If the entire string has been pushed, it checks if
    *there's an equal amount of matches and factors.
    */
    @Override
    public Boolean match(String text) {
        ArrayList<Boolean> bools = new ArrayList<Boolean>();

        int start = 0;
        ListIterator<Regexp> itr = product.listIterator();

        Regexp factor = itr.next();

        for(int i = 0; i <= text.length(); i++){
            String test = text.substring(start, i);

            if(factor.match(test)){
                    start = i;
                    bools.add(true);
                    if(itr.hasNext())
                        factor = itr.next();
            }
        }

        return (allTrue(bools) && (start == text.length()));
    }

    private Boolean allTrue(List<Boolean> bools){
        return product.size() == bools.size();
    }

    @Override
    public void add(Regexp regexp) {
        pushRegexp(regexp);
    }

    @Override
    public Boolean isEmpty() {
        return product.isEmpty();
    }
}

再次,我已经让这些工作对我的开销、标记化和所有这些好东西感到满意。现在我想介绍一下Kleene-star手术。它匹配文本中出现的任意次数,甚至 0 次。因此,ba* 将匹配 b、ba、baa、baaa 等,而 (ba)* 将匹配 ba、baba、bababa 等。是否有可能将我的正则表达式扩展到此,或者您是否看到解决此问题的另一种方法?

PS:还有 getter、setter 和各种其他支持函数,我没有写出来,但这主要是为了让您快速了解这些类的工作原理。

最佳答案

您似乎正在尝试使用后备算法来进行解析。这可以工作——尽管使用高阶函数更容易——但它远不是解析正则表达式的最佳方法(我的意思是数学上的正则表达式,而不是解析的全部内容)各种语言的“正则表达式”库实现的语言)。

这不是最好的方法,因为解析时间与要匹配的字符串的大小不是线性的;事实上,它可以是指数级的。但要理解这一点,重要的是要理解为什么当前的实现会出现问题。

考虑相当简单的正则表达式 (ab+a)(bb+a) 。它可以匹配四个字符串: abbbabaabbaa 。所有这些字符串都以 a 开头,因此您的连接算法将匹配位置 1 处的第一个连接数 ( (ab+a) ),并继续尝试第二个连接数 ( bb+a )。这将成功匹配 abbaa ,但在 abaabbb 上将失败。

现在,假设您修改了串联函数以选择最长的匹配子字符串而不是最短的子字符串。在这种情况下,第一个子表达式将在三个可能的字符串(除了 ab 之外的所有字符串)中匹配 aa ,并且在 abb 的情况下匹配将失败。

简而言之,当您匹配串联 R·S 时,您需要执行以下操作:

  • 查找与 R 匹配的初始字符串
  • 查看 S 是否与文本的其余部分匹配
  • 如果没有,请使用与 R 匹配的另一个初始字符串重复

在正则表达式完全匹配的情况下,我们列出 R 的匹配顺序并不重要,但通常我们会尝试找到与正则表达式匹配的最长子字符串,因此可以方便地枚举可能的子字符串从最长到最短匹配。

这样做意味着我们需要能够在下游故障后重新启动比赛,以找到“下一场比赛”。这并不是非常复杂,但它确实使界面变得复杂,因为所有复合正则表达式运算符都需要将失败“传递”给它们的子级,以便找到下一个替代方案。也就是说,运算符 R+S 可能首先找到与 R 匹配的内容。如果询问下一个可能性,它首先必须询问 R 是否有另一个可以匹配的字符串,然后再转到 S 。 (这就忽略了如何让 + 按长度顺序列出匹配项的问题。)

通过这样的实现,很容易看出如何实现 Kleene 星 (R<sup>*</sup>),也很容易看出为什么它会花费指数时间。一种可能的实现:

  • 首先,匹配尽可能多的 R
  • 如果要求另一场比赛:要求最后一个 R 进行另一场比赛
  • 如果没有更多的可能性,请从列表中删除最后一个 R,并询问现在最后一个 R 是什么来进行另一场比赛
  • 如果这些都不起作用,建议使用空字符串作为匹配
  • 失败

(这可以通过递归来简化:匹配一个 R ,然后匹配一个 R<sup>*</sup> 。对于下一个匹配,首先尝试下一个 R<sup>*</sup> ;如果失败,请尝试下一个 R 和后面的第一个 R<sup>*</sup> ;当所有其他都失败时,尝试空字符串。)

实现这是一个有趣的编程练习,所以我鼓励您继续。但请注意,还有更好的算法。您可能想阅读 Russ Cox's interesting essays on regular expression matching

关于java - 使用多态性对正则表达式解析器进行建模,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28394607/

相关文章:

java - 如何找出一个对象有多少引用?

java - 内部路径长度

ios - 我如何在 iOS Swift 中通过聚类解析自定义标记图标

python - 在Python中,如何检查一个字符串以查看其中是否包含另一个字符串的任何组合?

php - 将数组分配给静态函数变量php

python - 如何从 Google App Engine 应用程序解析 XML?

java - MIDI 初学者 - 需要演奏一个音符

java - 将添加到表格单元格的组合框中的文本居中对齐

html - 用于查找 http 和 .html 的正则表达式

javascript - 排除包含正则表达式字符串的 URL