使用多态建模正则表达式解析器

时间:2015-02-08 13:46:08

标签: java regex parsing polymorphism abstract-class

所以,我正在为学校做一个正则表达式解析器,它创建一个负责匹配的对象层次结构。我决定以面向对象的方式进行操作,因为我更容易想象一下这种语法的实现方式。所以,这些是构成正则表达式的我的类。一切都在Java中,但我认为如果你精通任何面向对象的语言,你都可以继续学习。

我们需要实现的唯一运算符是Union(+),Kleene-Star(*),表达式的连接(ab或者(a + b)c),当然还有括号如Concatination的例子所示。这就是我现在所实现的,并且我已经让它像一个主要有点开销的魅力一样工作。

父类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,它包含一个char,如果字符串与char匹配则返回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和各种其他支持函数,但这主要是为了让你快速了解这些类的工作原理。

1 个答案:

答案 0 :(得分:2)

您似乎尝试使用回退算法进行解析。这可以工作 - 尽管使用高阶函数更容易 - 但它远不是解析正则表达式的最佳方法(我指的是数学上正则表达式的东西,而不是解析的全部内容)由&#34;正则表达式&#34;各种语言的库实现的语言。)

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

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

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

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

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

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

这样做意味着我们需要能够在下游失败后重新开始匹配,以找到&#34;下一场比赛&#34;。这并不是非常复杂,但它肯定使界面复杂化,因为所有复合正则表达式运算符都需要通过&#34;为了找到下一个选择,他们的孩子失败了。也就是说,运算符R+S可能首先找到与R匹配的内容。如果被要求提供下一种可能性,首先必须询问R是否还有其他可匹配的字符串,然后再转到S。 (那就是如何让+按长度顺序列出匹配的问题。)

通过这样的实现,很容易看出如何实现Kleene星(R*),并且很容易理解它为什么需要指数时间。一种可能的实现方式:

  • 首先,尽可能多地匹配R
  • 如果要求另一场比赛:请求最后一次R进行另一场比赛
  • 如果没有其他可能性,请从列表中删除最后一个R,然后询问现在最后R的另一场比赛
  • 如果这些都不起作用,建议将空字符串作为匹配
  • 故障

(这可以通过递归来简化:匹配R,然后匹配R*。对于下一场比赛,首先尝试下一个R*;否则尝试下一个R 1}}和第一个R*;当其他所有方法都失败时,请尝试空字符串。)

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