将invRegex.py移植到Javascript(Node.js)

时间:2013-12-28 13:47:44

标签: javascript python node.js iterator generator

我一直试图将invRegex.py移植到node.js实现一段时间,但我仍然在努力解决它。由于ret.js标记符,我已经有了正则表达式解析树,并且它运行得很好,但是以一种内存效率的方式实际生成和连接所有不同的元素对我来说是非常具有挑战性的。为了简单起见,我可以说我有以下正则表达式:

[01]{1,2}@[a-f]

将其提供给invRegex.py会产生以下输出( tabbified 以占用更少的空间):

 0@a     0@b     0@c     0@d     0@e     0@f
00@a    00@b    00@c    00@d    00@e    00@f
01@a    01@b    01@c    01@d    01@e    01@f
 1@a     1@b     1@c     1@d     1@e     1@f
10@a    10@b    10@c    10@d    10@e    10@f
11@a    11@b    11@c    11@d    11@e    11@f

考虑到我能够获得每个单独的令牌并生成所有有效单个输出的数组:

[01]{1,2} = function () {
    return ['0', '00', '01', '1', '10', '11'];
};

@ = function () {
    return ['@'];
};

[a-f] = function () {
    return ['a', 'b', 'c', 'd', 'e', 'f'];
};

我可以计算所有数组的cartesian product并获得相同的预期输出:

var _ = require('underscore');

function cartesianProductOf() {
    return _.reduce(arguments, function(a, b) {
        return _.flatten(_.map(a, function(x) {
            return _.map(b, function(y) {
                return x.concat([y]);
            });
        }), true);
    }, [ [] ]);
};

var tokens = [
    ['0', '00', '01', '1', '10', '11'],
    ['@'],
    ['a', 'b', 'c', 'd', 'e', 'f'],
];

var result = cartesianProductOf(tokens[0], tokens[1], tokens[2]);

_.each(result, function (value, key) {
    console.log(value.join(''));
});

这个问题在于它将所有36个值保存在内存中,如果我有一个稍微复杂的正则表达式,例如[a-z]{0,10}它会在内存中保存146813779479511值,这完全是不可行的。我想以异步方式处理这个巨大的列表,将每个生成的组合传递给回调,并允许我在我认为合适的任何合理点上中断该过程,就像invRegex.py或this Haskell package - 不幸的是我可以不理解Haskell,我不知道如何模仿Python中的生成器行为到Javascript。

我尝试在节点0.11.9(带--harmony)中运行几个简单的生成器实验,如下所示:

function* alpha() {
    yield 'a'; yield 'b'; yield 'c';
}

function* numeric() {
    yield '0'; yield '1';
}

function* alphanumeric() {
    yield* alpha() + numeric(); // what's the diff between yield and yield*?
}

for (var i of alphanumeric()) {
    console.log(i);
}

毋庸置疑,上述方法无效。 = /

我的头靠在墙上,所以任何帮助解决这个问题都会受到高度赞赏。


更新:以下是b[a-z]{3}的示例ret.js解析树:

{
    "type": ret.types.ROOT,
    "stack": [
            {
                "type": ret.types.CHAR,
                "value": 98 // b
            },
            {
                "type": ret.types.REPETITION,
                "max": 3,
                "min": 3,
                "value": {
                    "type": ret.types.SET,
                    "not": false,
                    "set": [
                        {
                            "type": ret.types.RANGE,
                            "from": 97, // a
                            "to": 122   // z
                        }
                    ]
                }
            }
        ]
    ]
}

SET / RANGE类型应该产生26个不同的值,而父REPETITION类型应该将之前的值取为3的幂,产生17576个不同的组合。如果我像tokens之前那样生成扁平的cartesianProductOf数组,则中间扁平值将占用与实际笛卡尔积本身一样多的空间。

我希望这个例子能更好地解释我面临的问题。

6 个答案:

答案 0 :(得分:5)

我建议你编写迭代器类。它们易于实现(基本上它们是状态机),它们具有较低的内存占用,它们可以组合以构建越来越复杂的表达式(请向下滚动以查看最终结果),并且生成的迭代器可以包装在枚举器。

每个迭代器类都有以下方法:

  • 首先:初始化状态机(第一场比赛)
  • next:进入下一个州(下一场比赛)
  • ok:最初为true,但在“下一次”超出最后一场比赛后变为false
  • 获取:返回当前匹配(作为字符串)
  • 克隆:克隆对象;重复的必要条件,因为每个实例都需要自己的状态

从最简单的案例开始:一个或多个应按字面匹配的字符序列(例如 / foo / )。毋庸置疑,这只有一场比赛,所以'好'将在第一次'下一次'召唤时变为假。

function Literal(literal) { this.literal = literal; }

Literal.prototype.first = function() { this.i = 0; };
Literal.prototype.next = function() { this.i++; };
Literal.prototype.ok = function() { return this.i == 0; };
Literal.prototype.get = function() { return this.literal; };
Literal.prototype.clone = function() { return new Literal(this.literal); };

字符类( [abc] )也是微不足道的。构造函数接受一串字符;如果你喜欢数组,那很容易解决。

function CharacterClass(chars) { this.chars = chars; }

CharacterClass.prototype.first = function() { this.i = 0; };
CharacterClass.prototype.next = function() { this.i++; };
CharacterClass.prototype.ok = function() { return this.i < this.chars.length; };
CharacterClass.prototype.get = function() { return this.chars.charAt(this.i); };
CharacterClass.prototype.clone = function() { return new CharacterClass(this.chars); };

现在我们需要结合其他迭代器的迭代器来形成更复杂的正则表达式。一个序列只是一行中的两个或多个模式(如 foo [abc] )。

function Sequence(iterators) {
   if (arguments.length > 0) {
      this.iterators = iterators.length ? iterators : [new Literal('')];
   }
}
Sequence.prototype.first = function() {
   for (var i in this.iterators) this.iterators[i].first();
};
Sequence.prototype.next = function() {
   if (this.ok()) {
      var i = this.iterators.length;
      while (this.iterators[--i].next(), i > 0 && !this.iterators[i].ok()) {
         this.iterators[i].first();
      }
   }
};
Sequence.prototype.ok = function() {
   return this.iterators[0].ok();
};
Sequence.prototype.get = function() {
   var retval = '';
   for (var i in this.iterators) {
      retval += this.iterators[i].get();
   }
   return retval;
};
Sequence.prototype.clone = function() {
   return new Sequence(this.iterators.map(function(it) { return it.clone(); }));
};

组合迭代器的另一种方法是选择(例如,替代方案),例如: FOO |。酒吧

function Choice(iterators) { this.iterators = iterators; }

Choice.prototype.first = function() {
   this.count = 0;
   for (var i in this.iterators) this.iterators[i].first();
};
Choice.prototype.next = function() {
   if (this.ok()) {
      this.iterators[this.count].next();
      while (this.ok() && !this.iterators[this.count].ok()) this.count++;
   }
};
Choice.prototype.ok = function() {
   return this.count < this.iterators.length;
};
Choice.prototype.get = function() {
   return this.iterators[this.count].get();
};
Choice.prototype.clone = function() {
   return new Choice(this.iterators.map(function(it) { return it.clone(); }));
};

可以通过组合现有类来实现其他正则表达式功能。类继承是一种很好的方法。例如,可选模式( x?)只是空字符串和 x 之间的选择。

function Optional(iterator) {
   if (arguments.length > 0) {
      Choice.call(this, [new Literal(''), iterator]);
   }
}
Optional.prototype = new Choice();

重复( x {n,m} )是Sequence和Optional的组合。因为我必须继承一个或另一个,所以我的实现包含两个相互依赖的类。

function RepeatFromZero(maxTimes, iterator) {
   if (arguments.length > 0) {
      Optional.call(this, new Repeat(1, maxTimes, iterator));
   }
}
RepeatFromZero.prototype = new Optional();

function Repeat(minTimes, maxTimes, iterator) {
   if (arguments.length > 0) {
      var sequence = [];
      for (var i = 0; i < minTimes; i++) {
         sequence.push(iterator.clone());   // need to clone the iterator
      }
      if (minTimes < maxTimes) {
         sequence.push(new RepeatFromZero(maxTimes - minTimes, iterator));
      }
      Sequence.call(this, sequence);
   }
}
Repeat.prototype = new Sequence();

正如我之前所说,迭代器可以包装到枚举器中。这只是一个你可以随时打破的循环。

function Enumerator(iterator) {
   this.iterator = iterator;

   this.each = function(callback) {
      for (this.iterator.first(); this.iterator.ok(); this.iterator.next()) {
         callback(this.iterator.get());
      }
   };
}

把时间放在一起的时间。让我们采取一些愚蠢的正则表达式:

([ab]{2}){1,2}|[cd](f|ef{0,2}e)

编写迭代器对象非常简单:

function GetIterationsAsHtml() {

   var iterator = new Choice([
      new Repeat(1, 2,
         new Repeat(2, 2, new CharacterClass('ab'))),
      new Sequence([
         new CharacterClass('cd'),
         new Choice([
            new Literal('f'),
            new Sequence([
               new Literal('e'),
               new RepeatFromZero(2, new Literal('f')),
               new Literal('e')
            ])
         ])
      ])
   ]);

   var iterations = '<ol>\n';
   var enumerator = new Enumerator(iterator);
   enumerator.each(function(iteration) { iterations += '<li>' + iteration + '</li>\n'; });
   return iterations + '</ol>';
}

这会产生28场比赛,但我会省去你的输出。

如果我的代码不符合软件模式,不兼容浏览器(在Chrome和Firefox上运行正常)或者OOP不佳,我很抱歉。我希望它能使这个概念变得清晰。

编辑:为了完整性,并且遵循OP的倡议,我实现了一个迭代器类:引用

引用(\ 1 \ 2等)获取先前捕获组的当前匹配(即括号中的任何内容)。它的实现与 Literal 非常相似,因为它只有一个匹配。

function Reference(iterator) { this.iterator = iterator; }

Reference.prototype.first = function() { this.i = 0; };
Reference.prototype.next  = function() { this.i++; };
Reference.prototype.ok    = function() { return this.i == 0; };
Reference.prototype.get   = function() { return this.iterator.get(); };
Reference.prototype.clone = function() { return new Reference(this.iterator); };

构造函数被赋予一个表示引用的子模式的迭代器。以(foo|bar)([xy])\2\1为例(产生 fooxxfoo,fooyyfoo,barxxbar,baryybar ):

var groups = new Array();

var iterator = new Sequence([
   groups[1] = new Choice([new Literal('foo'), new Literal('bar')]),
   groups[2] = new CharacterClass('xy'),
   new Reference(groups[2]),
   new Reference(groups[1])
]);

在构建迭代器类树时指定捕获组。我仍在这里手动执行此操作,但最终您希望自动执行此操作。这只是将您的解析树映射到类似的迭代器类树的问题。

编辑2:这是一个相对简单的递归函数,它将ret.js生成的解析树转换为迭代器。

function ParseTreeMapper() {
    this.capturingGroups = [];
}
ParseTreeMapper.prototype.mapToIterator = function(parseTree) {
    switch (parseTree.type) {
        case ret.types.ROOT:
        case ret.types.GROUP:
            var me = this;
            var mapToSequence = function(parseTrees) {
                return new Sequence(parseTrees.map(function(t) {
                    return me.mapToIterator(t);
                }));
            };
            var group = parseTree.options ?
                new Choice(parseTree.options.map(mapToSequence)) : 
                mapToSequence(parseTree.stack);
            if (parseTree.remember) {
                this.capturingGroups.push(group);
            }
            return group;
        case ret.types.SET:
            return new CharacterClass(this.mapToCharacterClass(parseTree.set));
        case ret.types.REPETITION:
            return new Repeat(parseInt(parseTree.min), parseInt(parseTree.max), this.mapToIterator(parseTree.value));
        case ret.types.REFERENCE:
            var ref = parseInt(parseTree.value) - 1;
            return ref in this.capturingGroups ?
                new Reference(this.capturingGroups[ref]) :
                new Literal('<ReferenceOutOfRange>');
        case ret.types.CHAR:
            return new Literal(String.fromCharCode(parseTree.value));
        default:
            return new Literal('<UnsupportedType>');
    }
};
ParseTreeMapper.prototype.mapToCharacterClass = function(parseTrees) {
    var chars = '';
    for (var i in parseTrees) {
        var tree = parseTrees[i];
        switch (tree.type) {
            case ret.types.CHAR:
                chars += String.fromCharCode(tree.value);
                break;
            case ret.types.RANGE:
                for (var code = tree.from; code <= tree.to; code++) {
                    chars += String.fromCharCode(code);
                }
                break;
        }
    }
    return chars;
};

用法:

var regex = 'b[a-n]{3}';
var parseTree = ret(regex);    // requires ret.js
var iterator = new ParseTreeMapper().mapToIterator(parseTree);

我在此演示中将所有组件放在一起:http://jsfiddle.net/Pmnwk/3/

注意:不支持许多正则表达式语法结构(锚点,前瞻,后瞻,递归),但我想它已经与invRegex.py相提并论了。

答案 1 :(得分:2)

这是一个版本,它为输入的每个部分创建一个函数,并组合它们以生成一个函数,该函数将生成每个正则表达式结果并将其提供给该参数:

//Takes in a list of things, returns a function that takes a function and applies it to
// each Cartesian product. then composes all of the functions to create an
// inverse regex generator.

function CartesianProductOf() {
    var args = arguments;
    return function(callback) {
        Array.prototype.map.call(args, function(vals) {
            return function(c, val) {
                vals.forEach(function(v) {
                    c(val + v);
                });
            };
        }).reduce(function(prev, cur) {
            return function(c, val) {
                prev(function(v){cur(c, v)}, val);
            };
        })(callback, "");
    };
}      

修改以解析解析树(复制来自here的litte代码):

//Takes in a list of things, returns a function that takes a function and applies it to
// each Cartesian product.

function CartesianProductOf(tree) {
    var args = (tree.type == ret.types.ROOT)? tree.stack :
                ((tree.type == ret.types.SET)? tree.set : []);

    return function(callback) {
        var funs = args.map(function(vals) {
            switch(vals.type) {
                case ret.types.CHAR:
                    return function(c, val) {
                        c(val + vals.value);
                    };
                case ret.types.RANGE:
                    return function(c, val) {
                        for(var i=vals.from; i<=vals.to; i++) {
                            c(val+String.fromCharCode(i));
                        }
                    };
                case ret.types.SET:
                     return function(c, val) {
                         CartesianProductOf(vals)(function(i) {c(val+i)});
                     };
/*                   return function(c, val) {
                        vals.set.forEach(function(v) {
                            c(val + v);
                        });
                    };        */
                case ret.types.REPETITION:
                    var tmp = CartesianProductOf(vals.value);

                    if(vals.max == vals.min) {
                        return fillArray(function(c, val) {
                            tmp(function(i){c(val+i);}); //Probably works?
                        }, vals.max);
                    } else {
                        return fillArray(function(c, val) {
                            tmp(function(i){c(val+i);});
                        }, vals.min).concat(fillArray(function(c, val) {
                            c(val);
                            tmp(function(i){c(val+i);});
                        }, vals.max-vals.min));
                    }
                default:
                    return function(c, val) {
                        c(val);
                    };
            }
        }).reduce(function(prev, cur) { //Flatten array.
            return prev.concat(cur);
        }, []);

        if(tree.type == rets.type.ROOT) //If it's a full tree combine all the functions.
            funs.reduce(function(prev, cur) { //Compose!
                return function(c, val) {
                    prev(function(v){cur(c, v)}, val);
                };
            })(callback, "");
        else                          //If it's a set call each function.
            funs.forEach(function(f) {f(callback, "")}); 
    };
}

function fillArray(value, len) {
    var arr = [];
    for (var i = 0; i < len; i++) {
        arr.push(value);
    }
    return arr;
}

如果您的功能较少,更具C-esque解决方案,那就更好了:

function helper(callme, cur, stuff, pos) {
    if(pos == stuff.length) {
        callme(cur);
    } else 
        for(var i=0; i<stuff[pos].length; i++) {
            helper(callme, cur+stuff[pos][i], stuff, pos+1);
        }
}

function CartesianProductOf(callback) {
    helper(callback, "", Array.prototype.slice.call(arguments, 1), 0);
}

答案 2 :(得分:1)

这个怎么样:

var tokens = [
    ['0', '00', '01', '1', '10', '11'],
    ['@'],
    ['a', 'b', 'c', 'd', 'e', 'f'],
];

function cartesianProductOf(arr, callback) {
  var cur = [];
  var f = function(i) {
    if (i >= arr.length) {
      callback(cur.join(''));
      return
    }
    for (var j=0; j<arr[i].length; j++) {
      cur[i] = arr[i][j];
      f(i+1);
    }
  };
  f(0);
}

cartesianProductOf(tokens, function(str) { console.log(str); });

答案 3 :(得分:1)

听起来你要求使用Lazy Cartesian产品:你想要笛卡尔产品,但想要事先计算它们(并消耗)所有的记忆)。换句话说,你想要遍历笛卡尔积。

如果那是对的,你有没看过this Javascript implementation of the X(n) formula?有了它,您可以按自然顺序迭代它们&lt;&lt; 0,0,0&gt;,&lt; 0,0,1&gt ;,&lt; 0,1,0&gt;,...&gt;或选择任意位置进行计算。

看起来你可以做到:

// move through them in natural order, without precomputation
lazyProduct(tokens, function (token) { console.log(token); });

// or...
// pick ones out at random
var LP = new LazyProduct(tokens);
console.log(LP.item(7));   // the 7th from the list, without precompute
console.log(LP.item(242)); // the 242nd from the list, again without precompute

当然,我一定错过了什么......?鉴于X(n)公式,发电机只是过度杀伤。

<强>更新
Into a JSFiddle我已经放置了lazyProduct代码的检测版本,一个示例令牌数组数组,以及lazyProducttokens的调用。

当您在未经修改的情况下运行代码时,您会看到它会生成样本0@a数组所需的tokens等输出。我认为该链接很好地解释了逻辑,但总的来说......如果您取消注释lazyProduct中的工具,您会注意到有两个关键变量lensplens是传入的每个数组(在数组数组中)的长度的预计算。p是一个堆栈,它将当前路径保存到您的位置是(例如,如果你是“第一阵列第三索引,第二阵列第二索引,第三阵列第一索引”p表示),这就是传递给你的回调函数。

我的回调函数只是对参数进行连接(根据你的OP示例),但这些只是映射到原始数组数组的p中的相应值。

如果您对此进行进一步分析,您会发现构建笛卡尔积所需的足迹仅限于调用回调函数所需的内容。试试看你最糟糕的一个令牌吧。

更新2
我编码了大约75%的基于笛卡尔积的方法。我的API采用了一个ret.js解析树,将其转换为RPN,然后生成一组集合以传递给X(n)计算器。使用([ab]{2}){1,2}|[cd](f|ef{0,2}e)的@ruud示例,将生成:

new Option([
    new Set([
        new Set(new Set(['a','b']), new Set['a','b'])),
        new Set(new Set(['a','b']), new Set['a','b']))
    ]),
    new Set(
        ['c', 'd'],
        new Option([
            new Set(['f']),
            new Set(['e']]),
            new Set(['e','f']),
            new Set(new Set(['e','f']), new Set(['e', 'f']))
        ])
])

棘手的部分是嵌套选项(buggy)和反向字符类&amp;反向引用(不支持)。

这种方法越来越脆弱,Iterator解决方案确实优越。从您的解析树转换为该应该非常简单。感谢有趣的问题!

答案 4 :(得分:0)

只想分享我的想法,使用生成器并基于invRegex.py

var ret = require('ret');

var tokens = ret('([ab]) ([cd]) \\1 \\2 z');
var references = [];

capture(tokens);
// console.log(references);

for (string of generate(tokens)) {
    console.log(string);
}

function capture(token) {
    if (Array.isArray(token)) {
        for (var i = 0; i < token.length; ++i) {
            capture(token[i]);
        }
    }

    else {
        if ((token.type === ret.types.ROOT) || (token.type === ret.types.GROUP)) {
            if ((token.type === ret.types.GROUP) && (token.remember === true)) {
                var group = [];

                if (token.hasOwnProperty('stack') === true) {
                    references.push(function* () {
                        yield* generate(token.stack);
                    });
                }

                else if (token.hasOwnProperty('options') === true) {
                    for (var generated of generate(token)) {
                        group.push(generated);
                    }

                    references.push(group);
                }
            }

            if (token.hasOwnProperty('stack') === true) {
                capture(token.stack);
            }

            else if (token.hasOwnProperty('options') === true) {
                for (var i = 0; i < token.options.length; ++i) {
                    capture(token.options[i]);
                }
            }

            return true;
        }

        else if (token.type === ret.types.REPETITION) {
            capture(token.value);
        }
    }
}

function* generate(token) {
    if (Array.isArray(token)) {
        if (token.length > 1) {
            for (var prefix of generate(token[0])) {
                for (var suffix of generate(token.slice(1))) {
                    yield prefix + suffix;
                }
            }
        }

        else {
            yield* generate(token[0]);
        }
    }

    else {
        if ((token.type === ret.types.ROOT) || (token.type === ret.types.GROUP)) {
            if (token.hasOwnProperty('stack') === true) {
                token.options = [token.stack];
            }

            for (var i = 0; i < token.options.length; ++i) {
                yield* generate(token.options[i]);
            }
        }

        else if (token.type === ret.types.POSITION) {
            yield '';
        }

        else if (token.type === ret.types.SET) {
            for (var i = 0; i < token.set.length; ++i) {
                var node = token.set[i];

                if (token.not === true) {
                    if ((node.type === ret.types.CHAR) && (node.value === 10)) {
                    }
                }

                yield* generate(node);
            }
        }

        else if (token.type === ret.types.RANGE) {
            for (var i = token.from; i <= token.to; ++i) {
                yield String.fromCharCode(i);
            }
        }

        else if (token.type === ret.types.REPETITION) {
            if (token.min === 0) {
                yield '';
            }

            for (var i = token.min; i <= token.max; ++i) {
                var stack = [];

                for (var j = 0; j < i; ++j) {
                    stack.push(token.value);
                }

                if (stack.length > 0) {
                    yield* generate(stack);
                }
            }
        }

        else if (token.type === ret.types.REFERENCE) {
            console.log(references);
            if (references.hasOwnProperty(token.value - 1)) {
                yield* references[token.value - 1]();
                // yield references[token.value - 1]().next().value;
            }

            else {
                yield '';
            }
        }

        else if (token.type === ret.types.CHAR) {
            yield String.fromCharCode(token.value);
        }
    }
}

我仍然没有想出如何实现捕获组/引用,并且REPETITION令牌类型中产生的值尚未按字典顺序生成,但除此之外还有效。

答案 5 :(得分:0)

这里已经有很多好的答案了,但我特别想让发电机部件工作,这对你来说并不适用。看来你正试图这样做:

//the alphanumeric part
for (x of alpha()) for (y of numeric()) console.log(x + y);

//or as generator itself like you wanted
function* alphanumeric() {
    for (x of alpha()) for (y of numeric()) yield(x + y);
}
//iterating over it
for (var i of alphanumeric()) {
    console.log(i);
}

输出:

a0
a1
b0
b1
c0
c1

您可以将此用于正则表达式匹配所需的笛卡尔积。