使用正则表达式查找哈希表/字典/地图

时间:2008-11-03 21:39:39

标签: python regex dictionary hash

我正在试图找出是否有一种合理有效的方法在字典中执行查找(或者哈希,或地图,或者您喜欢的语言称之为),其中键是正则表达式并且查找字符串反对这套钥匙。例如(在Python语法中):

>>> regex_dict = { re.compile(r'foo.') : 12, re.compile(r'^FileN.*$') : 35 }
>>> regex_dict['food']
12
>>> regex_dict['foot in my mouth']
12
>>> regex_dict['FileNotFoundException: file.x does not exist']
35

(显然上面的例子不能用Python编写,但这是我希望能够做的事情。)

我可以想到一种天真的方式来实现它,我在其中迭代字典中的所有键并尝试将传入的字符串与它们匹配,但后来我失去了O(1)的查找时间。哈希映射,而是有O(n),其中n是我的字典中的键数。这可能是一个大问题,因为我希望这个词典变得非常大,我需要一遍又一遍地搜索它(实际上我需要为我在文本文件中读取的每一行迭代它,并且文件大小可以达到几百兆。)

有没有办法实现这一目标,而不是诉诸O(n)效率?

或者,如果您知道在数据库中完成此类查找的方法,那也很棒。

(任何编程语言都很好 - 我使用的是Python,但我对这里的数据结构和算法更感兴趣。)

有人指出不止一场比赛是可能的,这是绝对正确的。理想情况下,在这种情况下,我想返回一个包含所有匹配项的列表或元组。不过,我会为第一场比赛做好准备。

在这种情况下,我看不到O(1)是可能的;不过,我会满足于低于O(n)的任何东西。此外,底层数据结构可以是任何东西,但我想要的基本行为是我上面写的:查找字符串,并返回与正则表达式键匹配的值。

19 个答案:

答案 0 :(得分:4)

使用任何语言的常规哈希表都无法做到这一点。您要么必须遍历整个密钥集,尝试将密钥与正则表达式匹配,要么使用不同的数据结构。

您应该选择适合您尝试解决的问题的数据结构。如果你必须匹配任意正则表达式,我不知道一个好的解决方案。如果您要使用的正则表达式类更具限制性,则可以使用triesuffix tree等数据结构。

答案 1 :(得分:4)

在一般情况下,您需要的是词法分析器。它需要一堆正则表达式并将它们编译成识别器。如果您使用C语言,“lex”将会起作用。我从未在Python中使用词法分析器生成器,但似乎有一些可供选择。 Google会显示PLYPyGgyPyLexer

如果正则表达式在某种程度上彼此相似,那么您可以采取一些快捷方式。我们需要更多地了解您要解决的最终问题,以便提出任何建议。你能分享一些样本正则表达式和一些样本数据吗?

另外,你在这里处理了多少正则表达式?你确定天真的方法不会工作吗?正如Rob Pike once said,“当n很小时,花式算法很慢,n通常很小。”除非你有成千上万的正则表达式,以及成千上万的东西要匹配它们,并且这是一个用户在等你的交互式应用程序,你可能最好只是简单地执行它并循环遍历正则表达式。 / p>

答案 2 :(得分:4)

您想要做的与xrdb支持的非常相似。然而,他们只支持一个相当小的globbing概念。

通过将正则表达式存储为角色特里,您可以在内部实现比他们更大的常规语言系列。

  • 单个字符只是成为节点。
  • 。成为覆盖当前trie节点的所有子节点的通配符插入。
  • *成为上一项开头的trie到节点的反向链接。
  • [a-z]范围在范围中的每个字符下重复插入相同的后续子节点。小心,虽然插入/更新可能有点昂贵,但搜索可以是字符串大小的线性。通过一些占位符填充物,可以控制常见的组合爆炸情况。
  • (foo)|(bar)节点成为多个插入

这不处理在字符串中的任意点处发生的正则表达式,但可以通过在任一侧用。*包装正则表达式来建模。

Perl有几个Text :: Trie-like模块,你可以搜索想法。 (哎呀,我想我甚至写了其中一个回来的时候)

答案 3 :(得分:4)

只要您使用“真正的”正则表达式,这绝对是可能的。教科书正则表达式可以被deterministic finite state machine识别,这主要意味着你不能在那里有反向引用。

常规语言的属性是“两种常规语言的联合是常规的”,这意味着您可以使用单个状态机一次识别任意数量的正则表达式。状态机相对于表达式的数量在O(1)时间内运行(它相对于输入字符串的长度在O(n)时间内运行,但哈希表也是如此)。

状态机完成后,您将知道哪些表达式匹配,从那里可以很容易地在O(1)时间内查找值。

答案 4 :(得分:3)

通过将密钥组合到单个编译的正则表达式中,这是一种有效的方法,因此不需要对密钥模式进行任何循环。它滥用lastindex来找出匹配的密钥。 (遗憾的是,regexp库不允许您标记正则表达式编译到的DFA的终端状态,或者这不是一个黑客攻击。)

表达式编译一次,并生成一个不必按顺序搜索的快速匹配器。公共前缀在DFA中一起编译,因此与其他一些建议的解决方案不同,密钥中的每个字符匹配一次,而不是很多次。您正在为键空间有效地编译迷你词法分析器。

如果不重新编译正则表达式,此映射不可扩展(无法定义新键),但在某些情况下它可以很方便。

# Regular expression map
# Abuses match.lastindex to figure out which key was matched
# (i.e. to emulate extracting the terminal state of the DFA of the regexp engine)
# Mostly for amusement.
# Richard Brooksby, Ravenbrook Limited, 2013-06-01

import re

class ReMap(object):

    def __init__(self, items):
        if not items:
            items = [(r'epsilon^', None)] # Match nothing
        key_patterns = []
        self.lookup = {}
        index = 1
        for key, value in items:
            # Ensure there are no capturing parens in the key, because
            # that would mess up match.lastindex
            key_patterns.append('(' + re.sub(r'\((?!\?:)', '(?:', key) + ')')
            self.lookup[index] = value
            index += 1
        self.keys_re = re.compile('|'.join(key_patterns))

    def __getitem__(self, key):
        m = self.keys_re.match(key)
        if m:
            return self.lookup[m.lastindex]
        raise KeyError(key)

if __name__ == '__main__':
    remap = ReMap([(r'foo.', 12), (r'FileN.*', 35)])
    print remap['food']
    print remap['foot in my mouth']
    print remap['FileNotFoundException: file.x does not exist']

答案 5 :(得分:3)

如果你有一个字典,如

,会发生什么
regex_dict = { re.compile("foo.*"): 5, re.compile("f.*"): 6 }

在这种情况下,regex_dict["food"]可以合法地返回5或6。

即使忽略了这个问题,也许没有办法用regex模块有效地做到这一点。相反,您需要的是内部有向图或树结构。

答案 6 :(得分:3)

以下内容如何:

class redict(dict):
def __init__(self, d):
    dict.__init__(self, d)

def __getitem__(self, regex):
    r = re.compile(regex)
    mkeys = filter(r.match, self.keys())
    for i in mkeys:
        yield dict.__getitem__(self, i)

它基本上是Python中dict类型的子类。通过这种方式,您可以提供正则表达式作为键,并且使用yield以可迭代的方式返回与此正则表达式匹配的所有键的值。

有了这个,您可以执行以下操作:

>>> keys = ["a", "b", "c", "ab", "ce", "de"]
>>> vals = range(0,len(keys))
>>> red = redict(zip(keys, vals))
>>> for i in red[r"^.e$"]:
...     print i
... 
5
4
>>>

答案 7 :(得分:2)

@ rptb1您不必避免捕获组,因为您可以使用re.groups来计算它们。像这样:

# Regular expression map
# Abuses match.lastindex to figure out which key was matched
# (i.e. to emulate extracting the terminal state of the DFA of the regexp engine)
# Mostly for amusement.
# Richard Brooksby, Ravenbrook Limited, 2013-06-01

import re

class ReMap(object):
    def __init__(self, items):
        if not items:
            items = [(r'epsilon^', None)] # Match nothing
        self.re = re.compile('|'.join('('+k+')' for (k,v) in items))
        self.lookup = {}
        index = 1
        for key, value in items:
            self.lookup[index] = value
            index += re.compile(key).groups + 1

    def __getitem__(self, key):
        m = self.re.match(key)
        if m:
            return self.lookup[m.lastindex]
        raise KeyError(key)

def test():
    remap = ReMap([(r'foo.', 12),
                   (r'.*([0-9]+)', 99),
                   (r'FileN.*', 35),
                   ])
    print remap['food']
    print remap['foot in my mouth']
    print remap['FileNotFoundException: file.x does not exist']
    print remap['there were 99 trombones']
    print remap['food costs $18']
    print remap['bar']

if __name__ == '__main__':
    test()

可悲的是,很少有RE引擎真正将regexp编译为机器代码,尽管这并不是特别难。我怀疑有一个数量级的性能改进等着有人制作一个非常好的RE JIT库。

答案 8 :(得分:2)

有一个Perl模块只执行此Tie::Hash::Regex

use Tie::Hash::Regex;
my %h;

tie %h, 'Tie::Hash::Regex';

$h{key}   = 'value';
$h{key2}  = 'another value';
$h{stuff} = 'something else';

print $h{key};  # prints 'value'
print $h{2};    # prints 'another value'
print $h{'^s'}; # prints 'something else'

print tied(%h)->FETCH(k); # prints 'value' and 'another value'

delete $h{k};   # deletes $h{key} and $h{key2};

答案 9 :(得分:1)

针对演绎数据库的70年代AI语言出现了这个问题的一个特例。这些数据库中的键可以是带变量的模式 - 比如没有*或|的正则表达式运营商。他们倾向于使用特里结构的花式扩展来索引。有关一般概念,请参阅Norvig Paradigms of AI Programming中的krep * .lisp。

答案 10 :(得分:1)

如果您有一小组可能的输入,您可以缓存匹配,因为它们出现在第二个dict中,并获得缓存值的O(1)。

如果可能的输入集太大而无法缓存,也可能只是保留缓存中的最后N个匹配项(请查看Google的“LRU地图” - 最近最少使用)。

如果你不能这样做,你可以尝试通过检查前缀或某些东西来减少必须尝试的正则表达式的数量。

答案 11 :(得分:1)

我为项目创建了一次精确的数据结构。正如你的建议,我天真地实现了它。我确实做了两个非常有用的优化,根据您的数据大小,这些优化可能适用于您,也可能不适用:

  • 记住哈希查找
  • 预先存储记忆表(不知道该怎么称呼这个...预热缓存?)

为了避免多个键匹配输入的问题,我给每个正则表达式键一个优先级,并使用了最高优先级。

答案 12 :(得分:1)

正如其他受访者所指出的那样,在常规时间内使用哈希表是不可能的。

可能有用的一个近似值是使用名为"n-grams"的技术。创建从单词的n个字符块到整个单词的倒排索引。给定模式时,将其拆分为n个字符的块,并使用索引计算匹配单词的得分列表。

即使您不能接受近似值,在大多数情况下,这仍然会提供准确的过滤机制,因此您不必将正则表达式应用于每个键。

答案 13 :(得分:0)

这实际上取决于这些正则表达式的样子。如果你没有很多匹配几乎所有类似“.*”或“\d+”的正则表达式,而是你的正则表达式包含主要是单词和短语或任何超过4个字符的固定模式(例如a*b*c中的“^\d+a\*b\*c:\s+\w+”),如示例所示。你可以做这个普通的技巧,可以很好地扩展到数百万的正则表达式:

为正则表达式构建倒排索引(rabin-karp-hash('fixed pattern') - >包含'固定模式'的正则表达式列表)。然后在匹配时,使用Rabin-Karp哈希计算滑动哈希并查找反向索引,一次推进一个字符。现在,您可以查找倒排索引非匹配的O(1)查找和匹配的合理O(k)时间,k是倒排索引中正则表列表的平均长度。对于许多应用来说,k可以非常小(小于10)。反向索引的质量(误报意味着更大的k,假阴性意味着错过的匹配)取决于索引器理解正则表达式语法的程度。如果正则表达式由人类专家生成,他们也可以为包含的固定模式提供提示。

答案 14 :(得分:0)

可能可以让正则表达式编译器通过将搜索表达式连接到一个由“|”分隔的大型正则表达式来完成大部分工作。在这种情况下,聪明的正则表达式编译器可能会在替代方案中搜索共性,并设计出比仅依次检查每个方法更有效的搜索策略。但我不知道是否有编译器会这样做。

答案 15 :(得分:0)

好吧,我有一个非常相似的要求,我有很多不同的语法行,基本上用一些代码表示行和行,以便在智能卡格式的过程中使用,还有,密钥和秘密的描述符行代码,在每种情况下,我认为“模型”模式/动作是用于识别和处理许多行的野兽方法。
我正在使用C++/CLI来开发名为LanguageProcessor.dll的程序集,该库的核心是一个基本上包含的lex_rule类:

  • 正则表达式成员
  • 活动成员

构造函数加载正则表达式字符串并调用必要的代码,以便使用DynamicMethodEmitReflexion动态构建事件...也进入程序集存在其他类就像构造ans通过发布者和接收者类的简单名称实例化对象的元和对象一样,接收者类为每个匹配的规则提供动作处理程序。

最近,我有一个名为fasterlex_engine的类来构建一个词典<Regex, action_delegate> 加载数组中的定义以便运行。

该项目处于高级阶段,但我今天仍在建设中。我将尝试增强运行周围顺序访问每对foreach行输入的性能,通过使用regexp直接查找字典的一些机制,如:

map_rule[gcnew Regex("[a-zA-Z]")];

这里,我的代码的一些部分:

public ref class lex_rule: ILexRule
{
private:
    Exception           ^m_exception;
    Regex               ^m_pattern;

    //BACKSTORAGE delegates, esto me lo aprendi asiendo la huella.net de m*e*da JEJE
    yy_lexical_action   ^m_yy_lexical_action; 
    yy_user_action      ^m_yy_user_action;

public: 
    virtual property    String ^short_id; 
private:
    void init(String ^_short_id, String ^well_formed_regex);
public:

    lex_rule();
    lex_rule(String ^_short_id,String ^well_formed_regex);
    virtual event    yy_lexical_action ^YY_RULE_MATCHED
    {
        virtual void add(yy_lexical_action ^_delegateHandle)
        {
            if(nullptr==m_yy_lexical_action)
                m_yy_lexical_action=_delegateHandle;
        }
        virtual void remove(yy_lexical_action ^)
        {
            m_yy_lexical_action=nullptr;
        }

        virtual long raise(String ^id_rule, String ^input_string, String ^match_string, int index) 
        {
            long lReturn=-1L;
            if(m_yy_lexical_action)
                lReturn=m_yy_lexical_action(id_rule,input_string, match_string, index);
            return lReturn;
        }
    }
};

现在执行大量模式/动作对的fasterlex_engine类:

public ref class fasterlex_engine 
{
private: 
    Dictionary<String^,ILexRule^> ^m_map_rules;
public:
    fasterlex_engine();
    fasterlex_engine(array<String ^,2>^defs);
    Dictionary<String ^,Exception ^> ^load_definitions(array<String ^,2> ^defs);
    void run();
};

并且要解决这个主题..我的cpp文件的一些代码:

此代码通过参数符号

创建构造函数调用程序
inline Exception ^object::builder(ConstructorInfo ^target, array<Type^> ^args)
{
try
{
    DynamicMethod ^dm=gcnew DynamicMethod(
        "dyna_method_by_totem_motorist",
        Object::typeid,
        args,
        target->DeclaringType);
    ILGenerator ^il=dm->GetILGenerator();
    il->Emit(OpCodes::Ldarg_0);
    il->Emit(OpCodes::Call,Object::typeid->GetConstructor(Type::EmptyTypes)); //invoca a constructor base
    il->Emit(OpCodes::Ldarg_0);
    il->Emit(OpCodes::Ldarg_1);
    il->Emit(OpCodes::Newobj, target); //NewObj crea el objeto e invoca al constructor definido en target
    il->Emit(OpCodes::Ret);
    method_handler=(method_invoker ^) dm->CreateDelegate(method_invoker::typeid);
}
catch (Exception ^e)
{
    return  e;
}
return nullptr;

}

此代码附加任何处理函数(静态或非静态),以处理通过匹配输入字符串引发的回调

Delegate ^connection_point::hook(String ^receiver_namespace,String ^receiver_class_name, String ^handler_name)
{
Delegate ^d=nullptr;
if(connection_point::waitfor_hook<=m_state) // si es 0,1,2 o mas => intenta hookear
{ 
    try 
    {
        Type ^tmp=meta::_class(receiver_namespace+"."+receiver_class_name);
        m_handler=tmp->GetMethod(handler_name);
        m_receiver_object=Activator::CreateInstance(tmp,false); 

        d=m_handler->IsStatic?
            Delegate::CreateDelegate(m_tdelegate,m_handler):
            Delegate::CreateDelegate(m_tdelegate,m_receiver_object,m_handler);

        m_add_handler=m_connection_point->GetAddMethod();
        array<Object^> ^add_handler_args={d};
        m_add_handler->Invoke(m_publisher_object, add_handler_args);
        ++m_state;
        m_exception_flag=false;
    }
    catch(Exception ^e)
    {
        m_exception_flag=true;
        throw gcnew Exception(e->ToString()) ;
    }
}
return d;       
}

最后调用lexer引擎的代码:

array<String ^,2> ^defs=gcnew array<String^,2>  {/*   shortID    pattern         namespc    clase           fun*/
                                                    {"LETRAS",  "[A-Za-z]+"     ,"prueba",  "manejador",    "procesa_directriz"},
                                                    {"INTS",    "[0-9]+"        ,"prueba",  "manejador",    "procesa_comentario"},
                                                    {"REM",     "--[^\\n]*"     ,"prueba",  "manejador",    "nullptr"}
                                                }; //[3,5]

//USO EL IDENTIFICADOR ESPECIAL "nullptr" para que el sistema asigne el proceso del evento a un default que realice nada
fasterlex_engine ^lex=gcnew fasterlex_engine();
Dictionary<String ^,Exception ^> ^map_error_list=lex->load_definitions(defs);
lex->run();

答案 16 :(得分:0)

问题与正则表达式无关 - 对于带有键作为lambdas函数的字典,你会遇到同样的问题。所以你面临的问题是如何确定是否有一种方法可以将你的函数分类为哪个将返回true,这不是一个搜索问题,因为f(x)一般都不为人知。

分布式编程或缓存答案集假设有x的共同值可能有帮助。

- DM

答案 17 :(得分:0)

我认为这在理论上是不可能的。如果有人传入匹配多于1个正则表达式的字符串会发生什么。

例如,如果有人这样做会发生什么:

>>> regex_dict['FileNfoo']

这样的事情怎么可能是O(1)?

答案 18 :(得分:0)

我认为,基本假设是有缺陷的。你不能将哈希映射到正则表达式。