有没有更好的方法来迭代两个列表来查找python中的项之间的关系?

时间:2016-03-17 08:01:38

标签: python performance optimization

我将ip列表和子网dict模拟为输入:

# ip address list
ip_list = [
'192.168.1.151', '192.168.10.191', '192.168.6.127', 
'192.168.2.227', '192.168.2.5', '192.168.3.237', 
'192.168.6.188', '192.168.7.209', '192.168.9.10',
# Edited: add some /28, /16 case
'192.168.12.39', '192.168.12.58', '10.63.11.1', '10.63.102.69',
]

# subnet dict
netsets = {
'192.168.1.0/24': 'subnet-A',     # {subnet: subnet's name} 
'192.168.10.0/24': 'subnet-B', 
'192.168.2.0/24': 'subnet-C', 
'192.168.3.0/24': 'subnet-C',
'192.168.6.0/24': 'subnet-D', 
'192.168.7.0/24': 'subnet-D', 
'192.168.9.0/24': 'subnet-E',
# Edited: add some /28, /16 case
'192.168.12.32/28': 'subnet-F',
'192.168.12.48/28': 'subnet-G',
'10.63.0.0/16': 'subnet-I',
}

然后ip_list中的每个IP地址都需要找到子网的名称。

我们假设每个IP地址都可以在netsets中找到相应的子网。

像这样的输出:

192.168.1.151   subnet-A
192.168.10.191  subnet-B
192.168.6.127   subnet-D
192.168.2.227   subnet-C
192.168.2.5     subnet-C
192.168.3.237   subnet-C
192.168.6.188   subnet-D
192.168.7.209   subnet-D
192.168.9.10    subnet-E
# add some /28, /16 case
192.168.12.39   subnet-F
192.168.12.58   subnet-G
10.63.11.1      subnet-I
10.63.102.69    subnet-I

我使用netaddr来计算CIDR,这是我的代码:

from netaddr import IPAddress, IPNetwork

def netaddr_test(ips, netsets):
    for ip in ips:
        for subnet, name in netsets.iteritems():
            if IPAddress(ip) in IPNetwork(subnet):
                print ip, '\t',  name
                break

netaddr_test(ip_list, netsets)

但是这段代码太慢了,迭代太多了。时间的复杂性是O(n ** 2)。

一旦我们有数万个ip进行迭代,这段代码就会花费太多时间。

有没有更好的方法来解决这个问题?

6 个答案:

答案 0 :(得分:3)

我建议使用经过特别优化的intervaltree模块来快速搜索。因此,可以针对 O(m * log n)时间来解决该任务。例如:

   from intervaltree import Interval, IntervalTree
   from ipaddress import ip_network, ip_address

   # build nets tree
   netstree = IntervalTree(
                           Interval(
                                    ip_network(net).network_address, 
                                    ip_network(net).broadcast_address, 
                                    name
                                   ) 
                          for 
                          net, name 
                          in 
                          netsets.items()
                         )

   # Now you may check ip intervals     
   for i in ip_list:
       ip = ip_address(i)
       nets = netstree[ip]
       if nets:   # set is not empty
            netdata = list(nets)[0]
            print(netdata.data)
            # prints 'subnet-E'

答案 1 :(得分:2)

# ip address list
ip_list = [
'192.168.1.151', '192.168.10.191', '192.168.6.127',
'192.168.2.227', '192.168.2.5', '192.168.3.237',
'192.168.6.188', '192.168.7.209', '192.168.9.10'
]

# subnet dict
netsets = {
'192.168.1.0/24': 'subnet-A',     # {subnet: subnet's name} 
'192.168.10.0/24': 'subnet-B',
'192.168.2.0/24': 'subnet-C',
'192.168.3.0/24': 'subnet-C',
'192.168.6.0/24': 'subnet-D',
'192.168.7.0/24': 'subnet-D',
'192.168.9.0/24': 'subnet-E',
}
new_netsets = {}
for k,v in netsets.items():
   new_netsets['.'.join(k.split('.')[:3])] = v

for IP in ip_list:
   newIP = '.'.join(IP.split('.')[:3])
   print IP, new_netsets[newIP]

希望这有帮助。

答案 2 :(得分:1)

我建议避免在for循环中创建新实例。这不会降低复杂性(它会增加它),但它会加速netaddr_test,特别是如果它被调用超过一次。例如:

def _init(ips, netsets):
    """Initialize all objects"""
    new_ips = []
    new_subs = {}
    for ip in ips:
         new_ips.append(IPAddress(ip))

    for subnet, info in netsets.iteritems():

        new_subs[subnet] = {'name': info, 'subnet': IPNetwork(subnet)}

    return new_ips, new_subs

def netaddr_test(ips, netsets):
    for ip in ips:
        for stringnet, info in netsets.iteritems():
            if ip in info['subnet']:
                print ip, '\t',  info['name']
                break

ni, ns = _init(ip_list, netsets)
netaddr_test(ni, ns)

更新:用

测试上面的代码
ip_list = [
    '192.168.1.151', '192.168.10.191', '192.168.6.127', 
    '192.168.2.227', '192.168.2.5', '192.168.3.237', 
    '192.168.6.188', '192.168.7.209', '192.168.9.10'
] * 1000

结果:

# Original
$ time python /tmp/test.py > /dev/null

real    0m0.357s
user    0m0.345s
sys     0m0.012s

# Modified
$ time python /tmp/test2.py > /dev/null

real    0m0.126s
user    0m0.122s
sys     0m0.005s

现在,我从未使用netaddr所以我不确定它如何在内部处理子网。在您的情况下,您可以将子网视为一系列IP,每个IP都是uint_32,因此您可以将所有内容转换为整数:

 # IPs now are 
 ip_list_int = [3232235927, 3232238271, ...]

 netsets_expanded = {
     '192.168.1.0/24': {'name': 'subnet-A', 'start': 3232235776, 'end': 3232236031}

netaddr可用于转换上述格式的数据。在那里,您的netaddr_test变为(并且仅适用于整数比较):

def netaddr_test(ips, netsets):
    for ip in ips:
        for subnet, subinfo in netsets.iteritems():
            if ip >= subinfo['start'] and ip < subinfo['end']:
                print ip, '\t',  subinfo.name
                break

答案 3 :(得分:1)

一般情况下,你有N个模板和M值来测试匹配,你可以做任何比O(N * M)更好的事情。但是,如果你可以重新制定任务,那么你可以加速它。

我的建议是对模板进行分组,以便您拥有一些高级模板,如果IP匹配它,那么您可以转到最终模板。在你的例子中,这将是

grouped_netsets = {
    "192.168.0.0/16":  {
        '192.168.1.0/24': 'subnet-A',     # {subnet: subnet's name} 
        '192.168.10.0/24': 'subnet-B', 
        '192.168.2.0/24': 'subnet-C', 
        '192.168.3.0/24': 'subnet-C',
        '192.168.6.0/24': 'subnet-D', 
        '192.168.7.0/24': 'subnet-D', 
        '192.168.9.0/24': 'subnet-E',
        }
    }   

def netaddr_test(ips, grouped_netsets):
    for ip in ips:
        for group, netsets in grouped_netsets.iteritems():
            if IPAddress(ip) in IPNetwork(group):
                for subnet, name in netsets.iteritems():
                    if IPAddress(ip) in IPNetwork(subnet):
                        print(ip, '\t',  name)
                        break

因此,如果ip_list包含任何不以192.168开头的内容,您将通过一次检查将其丢弃。

剩下的唯一问题是编写用于对具有最佳配置的网络进行分组的功能。

答案 4 :(得分:0)

  

我将ip列表和子网dict模拟为输入:

# ip address list ip_list = 
[ '192.168.1.151', '192.168.10.191', '192.168.6.127',  '192.168.2.227', '192.168.2.5', '192.168.3.237', 
'192.168.6.188', '192.168.7.209', '192.168.9.10' ]

# subnet dict 
netsets = { '192.168.1.0/24': 'subnet-A', # {subnet: subnet's name}
'192.168.10.0/24': 'subnet-B',  
'192.168.2.0/24':'subnet-C',  
'192.168.3.0/24': 'subnet-C', 
'192.168.6.0/24': 'subnet-D',  
'192.168.7.0/24': 'subnet-D',  
'192.168.9.0/24':'subnet-E', } 
     

然后ip_list中的每个ip地址都需要找到   子网名称。

     

我们假设每个IP地址都可以找到相应的子网   netsets。

     

像这样的输出:

192.168.1.151   subnet-A
192.168.10.191  subnet-B
192.168.6.127   subnet-D
192.168.2.227   subnet-C
192.168.2.5     subnet-C
192.168.3.237   subnet-C
192.168.6.188   subnet-D
192.168.7.209   subnet-D
192.168.9.10    subnet-E
     

[...]   有没有更好的解决这个问题?

这是一个实现它的两个班轮:

for ip_addr in ip_list:
    print "{0}\t{1}".format(ip_addr,netsets[".".join(ip_addr.split('.')[0:-1])+".0/24"])

答案 5 :(得分:0)

假设子网彼此不重叠,您可以将子网转换为两个整数,即范围的开头和结尾。这些数字将被添加到将被排序的列表中。在执行此操作时,我们需要构建一个字典,以后可以使用该字典来检索范围开头的子网名称。

def to_int(ip):
    parts = map(int, ip.split('.'))

    return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]

def build(netsets):
    ranges = []
    subnets = {}

    for net, name in netsets.iteritems():
        ip, size = net.split('/')
        start = to_int(ip)
        end = start | 0xffffffff >> int(size)
        ranges.extend([start, end])
        subnets[start] = name

    ranges.sort()
return ranges, subnets

搜索IP时,您需要再次将其转为数字,并在列表或范围内执行bisect_left。如果结果是数字不均匀或IP匹配列表中的任何数字,则IP在子网内。然后,您可以使用范围中的星号从之前构建的字典中获取子网的名称:

def find(ranges, subnets, ip):
    num = to_int(ip)
    pos = bisect.bisect_left(ranges, to_int(ip))

    # Check if first IP in the range
    if pos % 2 == 0 and ranges[pos] == num:
        pos += 1

    if pos % 2:
        return subnets[ranges[pos - 1]]
    else:
        return None

使用上一个构建块,可以使用以下代码轻松获取每个IP的子网:

ranges, subnets = build(netsets)
for ip in ip_list:
    print 'ip: {0}, subnet: {1}'.format(ip, find(ranges, subnets, ip))

构建字典和范围列表需要O(m log m)时间,并且通过IP列表将需要O(n log m),其中m是子网的数量,n是IP的数量。解决方案适用于不同大小的不同子网,如果IP不属于任何子网,将打印None