ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

基于ParserCombinator的正则表达式引擎

2021-10-20 20:03:45  阅读:337  来源: 互联网

标签:Regex parse return ParserCombinator 正则表达式 Cursor 引擎 input new


这篇文章介绍如何用Parser Combinator实现一个简单的正则表达式引擎。

Cursor

Cursor封装了字符串的状态,表示一个光标位置。光标位置只能向后移动,可以方便地获取光标指向的字符,以及判断是否到达字符串末尾。

public class Cursor {
    private final String input;
    private final int index;

    public Cursor(String input, int index) {
        this.input = input;
        this.index = index;
    }

    /**
     * 是否到达字符串结尾
     */
    public boolean end() {
        return index == input.length();
    }

    /**
     * 当前指向的字符
     */
    public char current() {
        return input.charAt(index);
    }

    /**
     * 光标向后移动一个字符
     */
    public Cursor next() {
        return new Cursor(input, index + 1);
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Cursor cursor = (Cursor) o;
        return index == cursor.index && Objects.equals(input, cursor.input);
    }

    @Override
    public int hashCode() {
        return Objects.hash(input, index);
    }
    
    /**
     * 方便输出显示
     */
    @Override
    public String toString() {
        return String.format("Cursor{parsed: '%s', remain: '%s'}",
                input.substring(0, index), input.substring(index));
    }
}

测试代码:

Cursor cursor = new Cursor("hello", 0);
while (!cursor.end()) {
    System.out.println(cursor);
    System.out.println("current: " + cursor.current());
    cursor = cursor.next();
}

输出结果:

Cursor{parsed: '', remain: 'hello'}
current: h
Cursor{parsed: 'h', remain: 'ello'}
current: e
Cursor{parsed: 'he', remain: 'llo'}
current: l
Cursor{parsed: 'hel', remain: 'lo'}
current: l
Cursor{parsed: 'hell', remain: 'o'}
current: o

Cursor被设计成不可变的,主要是为了简化Parser的实现。如果Cursor是可变的,则在解析的过程中需要时刻注意保存当前光标的位置,这样十分麻烦。

Cursorequals方法和hashCode方法用于判断重复的Cursor状态,因为接下来我们要把Cursor放进集合里。

Regex

Regex封装了对字符串的解析操作,它从一个光标位置开始解析字符串,返回解析后所有可能的光标位置。

public interface Regex {
    Set<Cursor> parse(Cursor input);

    default boolean match(String input) {
        return parse(new Cursor(input, 0)).stream().anyMatch(Cursor::end);
    }
}

parse方法就是解析操作的具体实现,它从一个光标开始尝试向后解析,返回解析之后所有可能的光标位置。使用Set作为返回值类型,可以对匹配结果进行去重,避免重复解析相同的状态。

match方法是对parse方法的简单封装,它用来判断当前Regex是否能消耗掉整个字符串。只要在解析结果中存在一个到达字符串末尾的光标位置,就说明当前解析器能把字符串消耗完。

接下来,我们会实现一些基本的Regex实现类,使用它们可以将任意正则表达式转换成一个Regex的实例。调用Regexmatch方法,并传入待匹配的字符串,就能判断这个字符串是否与正则表达式匹配。

原子Regex

下面实现两个基本的Regex,虽然它们只能实现简单的功能,但是后面将非常有用。

首先是匹配单个指定字符的解析器:

public class Ch implements Regex {
    private final char c;

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

    @Override
    public Set<Cursor> parse(Cursor input) {
        if (input.end() || input.current() != c) {
            return Collections.emptySet();
        }
        return Set.of(input.next());
    }
}

如果当前光标没有到达字符串结尾,且指向的字符等于给定字符,它会消耗掉这个字符,然后返回剩余的部分,否则返回一个空列表作为解析失败的结果。

测试代码:

Regex r = new Ch('a');
System.out.println(r.parse(new Cursor("abc", 0)));
System.out.println(r.parse(new Cursor("xyz", 0)));
System.out.println(r.parse(new Cursor("", 0)));

输出结果:

[Cursor{parsed: 'a', remain: 'bc'}]
[]
[]

与之类似的,还有匹配任意单个字符的解析器,对应于正则表达式中的元字符.

public class Any implements Regex {
    @Override
    public Set<Cursor> parse(Cursor input) {
        if (input.end()) {
            return Collections.emptySet();
        }
        return Set.of(input.next());
    }
}

测试代码:

Regex r = new Any();
System.out.println(r.parse(new Cursor("abc", 0)));
System.out.println(r.parse(new Cursor("xyz", 0)));
System.out.println(r.parse(new Cursor("", 0)));

输出结果:

[Cursor{parsed: 'a', remain: 'bc'}]
[Cursor{parsed: 'x', remain: 'yz'}]
[]

我们现在已经有了能够匹配单个字符的Regex,但是它们并不能做很多事情,最多只能让光标向前移动一个字符。我们急需一种能够将多个小的Regex组合成一个复杂的Regex的机制。

Regex的组合

接下来这几个Regex非常重要,它们可以将一个或多个Regex包装成一个功能更强大的Regex

假设我们有一个Ch('a')和一个Ch('b'),如何用它们组合成一个能够匹配字符串abRegex呢?请看下面Concat的实现,它表示对输入串依次应用lhsrhs这两个Regex,并返回所有能够得到的结果,对应于正则表达式中的连接操作。

public class Concat implements Regex {
    private final Regex lhs, rhs;

    public Concat(Regex lhs, Regex rhs) {
        this.lhs = lhs;
        this.rhs = rhs;
    }

    @Override
    public Set<Cursor> parse(Cursor input) {
        Set<Cursor> r = new HashSet<>();
        for (Cursor c : lhs.parse(input)) {
            r.addAll(rhs.parse(c));
        }
        return r;
    }
}

测试代码:

Regex r = new Concat(new Ch('a'), new Ch('b'));
System.out.println(r.parse(new Cursor("abc", 0)));
System.out.println(r.parse(new Cursor("xyz", 0)));
System.out.println(r.parse(new Cursor("ac", 0)));

输出结果:

[Cursor{parsed: 'ab', remain: 'c'}]
[]
[]

只要lhsrhs任意一个解析器解析失败,都会导致返回结果为空。

注意,lhsrhs可能返回多个解析结果,因此在实现的过程中需要遍历所有可能的结果。

返回值类型为Set确保了结果中不会存在重复的状态。

多个Concat可以串联使用,例如,以下Regex匹配以abc开头的字符串:

Regex r = new Concat(new Ch('a'), new Concat(new Ch('b'), new Ch('c')));

与此对应的还有Or,它表示从lhsrhs中选择一个执行,对应于正则表达式中的|运算符:

public class Or implements Regex {
    private final Regex lhs, rhs;

    public Or(Regex lhs, Regex rhs) {
        this.lhs = lhs;
        this.rhs = rhs;
    }

    @Override
    public Set<Cursor> parse(Cursor input) {
        Set<Cursor> result = new HashSet<>(lhs.parse(input));
        result.addAll(rhs.parse(input));
        return result;
    }
}

测试代码:

Regex r = new Or(new Ch('a'), new Ch('b'));
System.out.println(r.parse(new Cursor("apple", 0)));
System.out.println(r.parse(new Cursor("banana", 0)));
System.out.println(r.parse(new Cursor("cat", 0)));

输出结果:

[Cursor{parsed: 'a', remain: 'pple'}]
[Cursor{parsed: 'b', remain: 'anana'}]
[]

接下来的ZeroOrMore是一个很重要的Regex,它实现了正则表达式中*运算符的功能。它尝试对当前光标多次应用parser,每次应用都会产生一个不同的光标位置。在实现的过程中,使用Queue来进行广度优先搜索,同时用一个额外的Set来避免重复搜索。

public class ZeroOrMore implements Regex {
    private final Regex parser;

    public ZeroOrMore(Regex parser) {
        this.parser = parser;
    }

    @Override
    public Set<Cursor> parse(Cursor input) {
        Set<Cursor> result = new HashSet<>();
        Queue<Cursor> queue = new LinkedList<>(List.of(input));
        Set<Cursor> set = new HashSet<>(Set.of(input));

        while (!queue.isEmpty()) {
            int cnt = queue.size();
            while (cnt-- > 0) {
                Cursor cursor = queue.remove();
                result.add(cursor);
                for (Cursor c : parser.parse(cursor)) {
                    if (!set.contains(c)) {
                        queue.add(c);
                        set.add(c);
                    }
                }
            }
        }

        return result;
    }
}

测试代码:

Regex r = new ZeroOrMore(new Ch('a'));
System.out.println(r.parse(new Cursor("aaa", 0)));

输出结果:

[
	Cursor{parsed: '', remain: 'aaa'},
	Cursor{parsed: 'a', remain: 'aa'},
    Cursor{parsed: 'aa', remain: 'a'},
	Cursor{parsed: 'aaa', remain: ''}
]

与之类似的,还有OneOrMore,对应于正则表达式中的+运算符。它的实现与ZeroOrMore及其类似,只是搜索的起始条件不同。

public class OneOrMore implements Regex {
    private final Regex parser;

    public OneOrMore(Regex parser) {
        this.parser = parser;
    }

    @Override
    public Set<Cursor> parse(Cursor input) {
        Set<Cursor> result = new HashSet<>();
        Set<Cursor> start = parser.parse(input);
        Queue<Cursor> queue = new LinkedList<>(start);
        Set<Cursor> set = new HashSet<>(start);

        while (!queue.isEmpty()) {
            int cnt = queue.size();
            while (cnt-- > 0) {
                Cursor cursor = queue.remove();
                result.add(cursor);
                for (Cursor c : parser.parse(cursor)) {
                    if (!set.contains(c)) {
                        queue.add(c);
                        set.add(c);
                    }
                }
            }
        }

        return result;
    }
}

测试代码:

Regex r = new OneOrMore(new Ch('a'));
System.out.println(r.parse(new Cursor("aaa", 0)));

输出结果:

[
	Cursor{parsed: 'a', remain: 'aa'}, 
	Cursor{parsed: 'aa', remain: 'a'},
	Cursor{parsed: 'aaa', remain: ''} 
]

构造复杂的解析器

有了以上的原子Regex和各种组合手段,就可以构造出任意复杂的Regex了,以下是正则表达式((a|b)c*)+对应的Regex

Regex r = new OneOrMore(new Concat(new Or(new Ch('a'), new Ch('b')), new ZeroOrMore(new Ch('c'))));

可以看到,这样的写法嵌套很深,用起来不方便,因此我们在Regex接口中添加一些静态方法和默认方法:

public interface Regex {
    ...
    static Regex any() {
        return new Any();
    }

    static Regex ch(char c) {
        return new Ch(c);
    }

    default Regex concat(Regex rhs) {
        return new Concat(this, rhs);
    }

    default Regex or(Regex rhs) {
        return new Or(this, rhs);
    }

    default Regex zeroOrMore() {
        return new ZeroOrMore(this);
    }

    default Regex oneOrMore() {
        return new OneOrMore(this);
    }
}

然后就可以通过链式调用构造复杂的Regex了:

import static xxx.Regex;

// ((a|b)c*)+
Regex r = ch('a').or(ch('b')).concat(ch('c').zeroOrMore()).oneOrMore();

测试代码:

System.out.println(r.match("acccbcccc"));
System.out.println(r.match("abcc"));

输出结果:

true
false

从字符串生成Regex

到这里,其实我们已经实现了一个简易的正则表达式执行引擎,支持正则表达式中常用的操作,包括连接、选择、重复等,并可以很容易地进行扩展,只需添加新的Regex实现类。

我们可以手动调用方法来构造任意复杂的Regex,但是,当表达式比较复杂时,手动构造的方式还是比较麻烦,所以下面实现了一个简易的正则表达式语法解析器RegexParser,它使用递归下降算法把一个正则表达式的字符串转换成一个Regex对象。这个解析器仅仅支持有限的正则表达式元素,包括普通字符、括号优先级,以及.*+等元字符。有兴趣的读者可以很容易地进行扩展。

public class RegexParser {
    private final String expr;
    private int index;

    public RegexParser(String expr) {
        this.expr = expr;
    }

    private void init() {
        index = 0;
    }

    private char peek() {
        return expr.charAt(index);
    }

    private char next() {
        return expr.charAt(index++);
    }

    private void read(char c) throws RegexParseException {
        if (c != next()) {
            throw new RegexParseException("expected: " + c);
        }
    }

    private boolean end() {
        return index == expr.length();
    }

    public Regex parse() throws RegexParseException {
        init();
        try {
            return parseExpr();
        } catch (RegexParseException e) {
            throw e;
        } catch (Exception e) {
            throw new RegexParseException("unknown error: " + e.getMessage());
        }
    }

    // elem = char | (expr)
    private Regex parseElem() throws RegexParseException {
        if (peek() == '(') {
            next();
            Regex r = parseExpr();
            read(')');
            return r;
        } else if (peek() == '.') {
            next();
            return any();
        } else {
            return ch(next());
        }
    }

    // factor = elem* | elem+ | elem
    private Regex parseFactor() throws RegexParseException {
        Regex r = parseElem();
        if (!end() && peek() == '*') {
            r = r.zeroOrMore();
            next();
        } else if (!end() && peek() == '+') {
            r = r.oneOrMore();
            next();
        }
        return r;
    }

    // term = factor factor ... factor
    private Regex parseTerm() throws RegexParseException {
        Regex r = parseFactor();
        while (!end() && peek() != ')' && peek() != '|') {
            r = r.concat(parseFactor());
        }
        return r;
    }

    // expr = term|term|...|term
    private Regex parseExpr() throws RegexParseException {
        Regex r = parseTerm();
        while (!end() && peek() == '|') {
            next();
            r = r.or(parseTerm());
        }
        return r;
    }
}

同时在Regex接口中添加一个静态方法:

public interface Regex {
    ...
    static Regex of(String expr) throws RegexParseException {
        return new RegexParser(expr).parse();
    }
}

然后就可以像下面这样构造一个正则表达式:

Regex r = Regex.of("((a|b)c*)+");

上面这行代码生成的Regex等价于:

Regex r = ch('a').or(ch('b')).concat(ch('c').zeroOrMore()).oneOrMore();

项目地址

https://github.com/byx2000/RegexCombinator

标签:Regex,parse,return,ParserCombinator,正则表达式,Cursor,引擎,input,new
来源: https://blog.csdn.net/baiyuxuan123123/article/details/120873785

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有