在兑现列表顺序时如何在正则表达式列表中使用pandas .replace()?

时间:2019-05-03 00:17:46

标签: python pandas replace

我有2个数据帧:一个(A)带有一些正则表达式形式的白名单主机名(即(.*)microsoft.com(*.)go.microsoft.com ...)和另一个(B)带有网站的实际完整主机名。我想使用白名单(第一个)数据框的正则表达式文本向此第二个数据框添加新列。但是,看来Pandas的.replace()方法并不关心其to_replacevalue参数所处的订单项。

我的数据如下:

In [1] A
Out[1]: 
                                  wildcards  \
42   (.*)activation.playready.microsoft.com   
35    (.*)v10.vortex-win.data.microsoft.com   
40      (.*)settings-win.data.microsoft.com   
43            (.*)smartscreen.microsoft.com   
39             (.*).playready.microsoft.com   
38                     (.*)go.microsoft.com   
240                     (.*)i.microsoft.com   
238                       (.*)microsoft.com   
                                                 regex  
42   re.compile('^(.*)activation.playready.microsof...  
35   re.compile('^(.*)v10.vortex-win.data.microsoft...  
40   re.compile('^(.*)settings-win.data.microsoft.c...  
43       re.compile('^(.*)smartscreen.microsoft.com$')  
39        re.compile('^(.*).playready.microsoft.com$')  
38                re.compile('^(.*)go.microsoft.com$')  
240                re.compile('^(.*)i.microsoft.com$')  
238                  re.compile('^(.*)microsoft.com$')  


In [2] B.head()
Out[2]: 
                       server_hostname
146     mobile.pipe.aria.microsoft.com
205    settings-win.data.microsoft.com
341      nav.smartscreen.microsoft.com
406  v10.vortex-win.data.microsoft.com
667                  www.microsoft.com

请注意,A有一列与wildcards相似的形式的正则表达式。我想像这样向wildcard添加一个B列:

B.loc[:,'wildcards'] = B['server_hostname'].replace(A['regex'].tolist(), A['wildcards'].tolist())

但是问题是,B的所有通配符值都变成(.*)microsoft.com。无论A的通配符值的顺序如何,都会发生这种情况。看来.replace()的目的是首先以最短的值而不是所提供的顺序使用to_replace正则表达式。

如何提供to_replace值的列表,以便最终获得与wildcards的{​​{1}}值关联的最详细的主机名B值?

7 个答案:

答案 0 :(得分:1)

大多数答案使用apply(),它比内置矢量函数解决方案要慢。我希望使用.replace()是因为它是内置向量函数,因此速度很快。 @vlemaistre的答案是唯一不使用.apply()的答案,就像我在这里的解决方案一样,它不将每个通配符编译成一个正则表达式,而是将其视为使用逻辑的右侧子字符串:“如果{{1} }以server_hostname结尾,那就是匹配项”。只要我按长度对通配符进行排序,就可以正常工作。

我执行此操作的功能是:

wildcard

在这里,def match_to_whitelist(accepts_df, whitelist_df): """ Adds `whitelists` column to accepts_df showing which (if any) whitelist entry it matches with """ accepts_df.loc[:, 'wildcards'] = None for wildcard in whitelist_df['wildcards']: accepts_df.loc[(accepts_df['wildcards'].isnull()) & ( accepts_df['server_hostname'].str.endswith(wildcard)), 'wildcards'] = wildcard rows_matched = len(accepts_df['wildcards'].notnull()) matched {rows_matched}") return accepts_df 就像以前的accepts_dfB就像以前的whitelist_df,但有2个区别:

  1. 没有A
  2. regex值不再采用glob / regex格式(即,“(。*)microsoft.com”变为“ microsoft.com”

要在我的机器上建立基准测试基准,我将使用我的基准作为基准,花费27秒的时间来处理100k wildcards行和400 accepts_df行。使用相同的数据集,下面是其他解决方案的时间(我很懒:如果它们没有用尽,我就不会花很多时间来找出答案):

  • @vlemaistre-具有矢量功能的列表理解:193秒
  • @ user214-SequenceMatcher:234秒
  • @aws_apprentice-比较RE搜索结果的长度:24秒
  • @fpersyn-第一场比赛(如果对whitelist_df进行排序将是最佳比赛):超过6分钟,所以退出...
  • @Andy Hayden-A:未测试,因为我无法(快速)以编程方式构建较长的RE。
  • @capelastegui-lastgroup:错误:“ pandas.core.indexes.base.InvalidIndexError:仅对具有唯一值的Index对象有效的索引”

最终,我们没有一个答案说明如何根据需要使用Series.str.match(),所以暂时,如果有人可以提供一个更好地使用{{1 },或至少其他一些基于向量的快速解决方案。在此之前,我将保持现有状态,或者在验证结果后使用aws_apprentice的状态。

编辑 我通过向两个DF添加一个“域”列来改进匹配器,该域由每个通配符/服务器主机名的后2个部分组成(即www.microsoft.com变为“ microsoft.com”)。然后,我在两个DF上都使用.replace(),遍历域的白名单组,从server_hostname DF(B)提取了相同的域组,并且仅使用每个组的通配符/ server_hostnames的子集进行匹配。这将我的处理时间减少了一半。

答案 1 :(得分:0)

这是一种使用双列表理解和re.sub()函数的方法:

import re

A = pd.DataFrame({'wildcards' : ['(.*)activation.playready.microsoft.com',
                                 '(.*)v10.vortex-win.data.microsoft.com',
                                 '(.*)i.microsoft.com', '(.*)microsoft.com'],
                  'regex' : [re.compile('^(.*)activation.playready.microsoft.com$'),
                             re.compile('^(.*)v10.vortex-win.data.microsoft.com$'), 
                             re.compile('^(.*)i.microsoft.com$'), 
                             re.compile('^(.*)microsoft.com$')]})

B = pd.DataFrame({'server_hostname' : ['v10.vortex-win.data.microsoft.com',
                                       'www.microsoft.com']})
# For each server_hostname we try each regex and keep the longest matching one
B['wildcards'] = [max([re.sub(to_replace, value, x) for to_replace, value
                       in A[['regex', 'wildcards']].values
                       if re.sub(to_replace, value, x)!=x], key=len) 
                  for x in B['server_hostname']]

Output : 
                     server_hostname                              wildcards
0  v10.vortex-win.data.microsoft.com  (.*)v10.vortex-win.data.microsoft.com
1                  www.microsoft.com                      (.*)microsoft.com

答案 2 :(得分:0)

一种替代方法是使用SequenceMatcherre.match

从@vlemaistre给出的答案中获取数据

from difflib import SequenceMatcher
import pandas as pd
import re

A = pd.DataFrame({'wildcards' : ['(.*)activation.playready.microsoft.com',
                                 '(.*)v10.vortex-win.data.microsoft.com',
                                 '(.*)i.microsoft.com', '(.*)microsoft.com'],
                  'regex' : [re.compile('^(.*)activation.playready.microsoft.com$'),
                             re.compile('^(.*)v10.vortex-win.data.microsoft.com$'), 
                             re.compile('^(.*)i.microsoft.com$'), 
                             re.compile('^(.*)microsoft.com$')]})

B = pd.DataFrame({'server_hostname' : ['v10.vortex-win.data.microsoft.com',
                                       'www.microsoft.com', 'www.i.microsoft.com']})

def regex_match(x):
    match = None
    ratio = 0
    for w, r in A[['wildcards', 'regex']].to_numpy():
        if re.match(r, x) is not None:
            pct = SequenceMatcher(None, w, x).ratio()
            if ratio < pct: ratio = pct; match = w
    return match

B['wildcards'] = B.server_hostname.apply(regex_match)

# print(B.wildcards)
0    (.*)v10.vortex-win.data.microsoft.com
1                        (.*)microsoft.com
2                      (.*)i.microsoft.com
Name: server_hostname, dtype: object

答案 3 :(得分:0)

这是使用apply的另一种方法。据我所知,没有 pure pandas方法可以做到这一点。我还借用了@vlemaistre提供的数据。

A = pd.DataFrame({'wildcards' : ['(.*)activation.playready.microsoft.com',
                                 '(.*)v10.vortex-win.data.microsoft.com',
                                 '(.*)i.microsoft.com', '(.*)microsoft.com'],
                  'regex' : [re.compile('^(.*)activation.playready.microsoft.com$'),
                             re.compile('^(.*)v10.vortex-win.data.microsoft.com$'), 
                             re.compile('^(.*)i.microsoft.com$'), 
                             re.compile('^(.*)microsoft.com$')]})

B = pd.DataFrame({'server_hostname' : ['v10.vortex-win.data.microsoft.com',
                                       'www.microsoft.com']})

pats = set(A.regex)

def max_match(hostname):
    d = {}
    for pat in pats:
        maybe_result = pat.search(hostname)
        if maybe_result:
            p = pat.pattern
            d[len(p)] = p
    return d.get(max([*d]))

B['wildcards'] = B['server_hostname'].apply(max_match)

                     server_hostname                                wildcards
0  v10.vortex-win.data.microsoft.com  ^(.*)v10.vortex-win.data.microsoft.com$
1                  www.microsoft.com                      ^(.*)microsoft.com$

答案 4 :(得分:0)

pandas documentation.replace()方法描述为:

  

DataFrame的值动态地替换为其他值。这与使用.loc或.iloc进行更新不同,后者需要您指定要使用某些值进行更新的位置。

这意味着该方法将遍历数据帧中的所有单元格,并为to_replace参数中提供的每个查询替换它所能执行的操作。一个简单的例子来证明这一点:

df = pd.DataFrame({'A':['a','c'],'B':['b','d']})
df.replace(['a','b'],['b','c'])

Output:
    A   B
0   c   c
1   c   d

在您的示例中,当有新的匹配项时,每个正则表达式规则都会覆盖以前的替换项,这就是您最终得到(.*)microsoft.com结果向量的原因。

您可以改用.apply()方法。例如,通过按长度降序对白名单(A)进行排序,可以遍历值DataFrame(B)的每一行并返回每个第一个匹配项:

import pandas as pd
import re

# Using the definitions for A and B from your question, 
# where A is sorted descending by length.

def first_match(x):
    for index, row in A.iterrows():
        if bool(re.search(row['wildcards'], x['server_hostname'])) is True:
            return row['wildcards']
B['wildcards'] = B.apply(first_match, axis=1)
B

Output:
    server_hostname                     wildcards
0   mobile.pipe.aria.microsoft.com      (.*)microsoft.com
1   settings-win.data.microsoft.com     (.*)settings-win.data.microsoft.com
2   nav.smartscreen.microsoft.com       (.*)smartscreen.microsoft.com
3   v10.vortex-win.data.microsoft.com   (.*)v10.vortex-win.data.microsoft.com
4   www.microsoft.com                   (.*)microsoft.com

也许还应该阅读split-apply-combine模式以获得更高级的策略。希望对您有所帮助。

答案 5 :(得分:0)

不幸的是,仍然需要应用的另一种方法是使用lastgroup。这需要编译单个正则表达式,然后查找匹配的组(行)的名称:

In [11]: regex = re.compile("|".join([f"(?P<i{i}>{regex})" for i, regex in s["wildcards"].items()]))

In [12]: regex
Out[12]:
re.compile(r'(?P<i42>(.*)activation.playready.microsoft.com)|(?P<i35>(.*)v10.vortex-win.data.microsoft.com)|(?P<i40>(.*)settings-win.data.microsoft.com)|(?P<i43>(.*)smartscreen.microsoft.com)|(?P<i39>(.*).playready.microsoft.com)|(?P<i38>(.*)go.microsoft.com)|(?P<i240>(.*)i.microsoft.com)|(?P<i238>(.*)microsoft.com)',
re.UNICODE)

In [13]: B.server_hostname.apply(lambda s: int(re.match(regex, s).lastgroup[1:]))
Out[13]:
146    238
205     40
341     43
406     35
667    238
Name: server_hostname, dtype: int64

In [14]: B.server_hostname.apply(lambda s: int(re.match(regex, s).lastgroup[1:])).map(s.wildcards)
Out[14]:
146                        (.*)microsoft.com
205      (.*)settings-win.data.microsoft.com
341            (.*)smartscreen.microsoft.com
406    (.*)v10.vortex-win.data.microsoft.com
667                        (.*)microsoft.com
Name: server_hostname, dtype: object

熊猫没有暴露此属性(但是可能可以对内部进行一些巧妙的处理)...

答案 6 :(得分:0)

我能找到的最纯粹的熊猫方法包括在B.server_hostname上为每个正则表达式运行Series.str.match(),然后使用idxmax()从每列中进行第一个匹配。

# Create input data
A = pd.DataFrame({'wildcards' : ['(.*)activation.playready.microsoft.com',
                                 '(.*)v10.vortex-win.data.microsoft.com',
                                 '(.*)i.microsoft.com', '(.*)microsoft.com'],
                  'regex' : [re.compile('^(.*)activation.playready.microsoft.com$'),
                             re.compile('^(.*)v10.vortex-win.data.microsoft.com$'), 
                             re.compile('^(.*)i.microsoft.com$'), 
                             re.compile('^(.*)microsoft.com$')]})

B = pd.DataFrame({'server_hostname' : ['v10.vortex-win.data.microsoft.com',
                                       'www.microsoft.com']})

# Ensure B has a unique index
B = B.reset_index(drop=True)

# Check which regexes match each hostname
df_match = A.regex.apply(lambda x: B.server_hostname.str.match(x))
df_match.index= A.wildcards
df_match.columns=B.server_hostname

# Get first match for each hostname
df_first_match = df_match.idxmax().rename('wildcards').reset_index()

输出:

print(df_match)
print(df_first_match)

server_hostname                         v10.vortex-win.data.microsoft.com  www.microsoft.com
wildcards                                                                                   
(.*)activation.playready.microsoft.com                              False              False
(.*)v10.vortex-win.data.microsoft.com                                True              False
(.*)i.microsoft.com                                                 False              False
(.*)microsoft.com                                                    True               True

                     server_hostname                              wildcards
0  v10.vortex-win.data.microsoft.com  (.*)v10.vortex-win.data.microsoft.com
1                  www.microsoft.com                      (.*)microsoft.com

也就是说,这似乎比之前发布的其他解决方案要慢一些。