Pandas:解析IP地址为国家的最快方法

8

我有一个函数find_country_from_connection_ip,它接受一个IP地址,在一些处理后返回一个国家。如下:

def find_country_from_connection_ip(ip):
    # Do some processing
    return county

我正在使用 apply 方法中的函数,如下所示:
df['Country'] = df.apply(lambda x: find_country_from_ip(x['IP']), axis=1)

作为一个比较简单的问题,我想要从一个包含>400000行的DataFrame中的现有列计算一个新列。
它可以运行,但速度非常慢,并且会抛出以下异常:
...........:SettingWithCopyWarning: 尝试在来自DataFrame的切片副本上设置值。 尝试使用.loc[row_indexer,col_indexer] = value代替
请参阅文档中的警告:http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy 如果name == 'main': In [38]:
我理解这个问题,但无法确定如何使用loc与apply和lambda一起使用。
请注意,如果您有更有效的替代解决方案,请提出建议,以实现最终结果。
****编辑********
该函数主要是对mmdb数据库进行查找,如下所示:
def find_country_from_ip(ip):
    result = subprocess.Popen("mmdblookup --file GeoIP2-Country.mmdb --ip {} country names en".format(ip).split(" "), stdout=subprocess.PIPE).stdout.read()
    if result:
        return re.search(r'\"(.+?)\"', result).group(1) 
    else:
        final_output = subprocess.Popen("mmdblookup --file GeoIP2-Country.mmdb --ip {} registered_country names en".format(ip).split(" "), stdout=subprocess.PIPE).stdout.read()
        return re.search(r'\"(.+?)\"', final_output).group(1)

尽管如此,这是一项昂贵的操作,如果您有一个包含>400000行的DataFrame,它应该需要时间。但是需要多长时间呢?这就是问题所在。大约需要2个小时,我认为这相当长了。


如果可能的话,我认为更有效率的解决方案是省略apply函数并将自定义函数重写为一些pandas向量化函数。 - jezrael
你能否在问题中添加函数 find_country_from_ip 的所有功能? - jezrael
@jezrael,已编辑。现在您可以查看。 - Ahsanul Haque
哦,这有点难,也许我只能给你一些建议。需要对每一行调用subprocess.Popen("mmdblookup --file GeoIP2-Country.mmdb --ip {} country names en".format(ip).split(" "), stdout=subprocess.PIPE).stdout.read()吗?还是只需要对每个唯一的IP调用一次?是否存在重复的IP - jezrael
1
print(len(df.IP.drop_duplicates())) 是什么意思? - jezrael
5个回答

8
我会使用 maxminddb-geolite2(GeoLite)模块来实现。首先安装 maxminddb-geolite2 模块。
pip install maxminddb-geolite2

Python 代码:

import pandas as pd
from geolite2 import geolite2

def get_country(ip):
    try:
        x = geo.get(ip)
    except ValueError:
        return pd.np.nan
    try:
        return x['country']['names']['en'] if x else pd.np.nan
    except KeyError:
        return pd.np.nan

geo = geolite2.reader()

# it took me quite some time to find a free and large enough list of IPs ;)
# IP's for testing: http://upd.emule-security.org/ipfilter.zip
x = pd.read_csv(r'D:\download\ipfilter.zip',
                usecols=[0], sep='\s*\-\s*',
                header=None, names=['ip'])

# get unique IPs
unique_ips = x['ip'].unique()
# make series out of it
unique_ips = pd.Series(unique_ips, index = unique_ips)
# map IP --> country
x['country'] = x['ip'].map(unique_ips.apply(get_country))

geolite2.close()

输出:

In [90]: x
Out[90]:
                     ip     country
0       000.000.000.000         NaN
1       001.002.004.000         NaN
2       001.002.008.000         NaN
3       001.009.096.105         NaN
4       001.009.102.251         NaN
5       001.009.106.186         NaN
6       001.016.000.000         NaN
7       001.055.241.140         NaN
8       001.093.021.147         NaN
9       001.179.136.040         NaN
10      001.179.138.224    Thailand
11      001.179.140.200    Thailand
12      001.179.146.052         NaN
13      001.179.147.002    Thailand
14      001.179.153.216    Thailand
15      001.179.164.124    Thailand
16      001.179.167.188    Thailand
17      001.186.188.000         NaN
18      001.202.096.052         NaN
19      001.204.179.141       China
20      002.051.000.165         NaN
21      002.056.000.000         NaN
22      002.095.041.202         NaN
23      002.135.237.106  Kazakhstan
24      002.135.237.250  Kazakhstan
...                 ...         ...

时间:针对171,884个唯一IP地址:

In [85]: %timeit unique_ips.apply(get_country)
1 loop, best of 3: 14.8 s per loop

In [86]: unique_ips.shape
Out[86]: (171884,)

结论:在我的硬件上,使用400K个独特IP的DF需要大约35秒。
In [93]: 400000/171884*15
Out[93]: 34.90726303786274

太棒了...正是我正在寻找的解决方案。 - Ahsanul Haque
@AhsanulHaque,很高兴我能帮到你 :) - MaxU - stand with Ukraine

1

如果我理解正确,你可以通过以下方式使用自定义函数和 Series.apply

df['Country'] = df['IP'].apply(find_country_from_ip)

样例:
df = pd.DataFrame({'IP':[1,2,3],
                   'B':[4,5,6]})




def find_country_from_ip(ip):
            # Do some processing 
            # some testing formula
            country = ip + 5
            return country



   df['Country'] = df['IP'].apply(find_country_from_ip)

print (df)
   B  IP  Country
0  4   1        6
1  5   2        7
2  6   3        8

1
你的问题不在于如何使用applyloc。问题在于你的df被标记为另一个数据帧的副本。
让我们更深入地探讨一下。
df = pd.DataFrame(dict(IP=[1, 2, 3], A=list('xyz')))
df

enter image description here

def find_country_from_connection_ip(ip):
    return {1: 'A', 2: 'B', 3: 'C'}[ip]

df['Country'] = df.IP.apply(find_country_from_connection_ip)
df

enter image description here

没有问题 让我们制造一些问题
# This should make a copy
print(bool(df.is_copy))
df = df[['A', 'IP']]
print(df)
print(bool(df.is_copy))

False
   A  IP
0  x   1
1  y   2
2  z   3
True

完美,现在我们有了一份复制品。让我们使用 apply 执行相同的赋值任务。
df['Country'] = df.IP.apply(find_country_from_connection_ip)
df
//anaconda/envs/3.5/lib/python3.5/site-packages/ipykernel/__main__.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  if __name__ == '__main__':

enter image description here


如何修复它?
无论您在哪里创建了df,都可以使用df.loc。 我上面的示例,我使用了df = df [:]触发了复制。 如果我改用loc,我就可以避免这个麻烦。

print(bool(df.is_copy))
df = df.loc[:]
print(df)
print(bool(df.is_copy))

False
   A  IP
0  x   1
1  y   2
2  z   3
False

你需要找到创建 df 的位置,在对源数据框进行切片时使用 lociloc。或者,你可以简单地这样做...
df.is_copy = None

完整演示
df = pd.DataFrame(dict(IP=[1, 2, 3], A=list('xyz')))

def find_country_from_connection_ip(ip):
    return {1: 'A', 2: 'B', 3: 'C'}[ip]

df = df[:]

df.is_copy = None

df['Country'] = df.IP.apply(find_country_from_connection_ip)
df

enter image description here


谢谢,解释得很清楚。但是,在使用 d2 = d1.loc[d1['Country'] == '$$'] 后,bool(d2.is_copy) 仍然评估为 True。而且,仅100行数据就需要大约5秒钟的时间,这意味着处理 400000 行数据将需要大约6个小时的时间。时间相当长,不是吗? - Ahsanul Haque
唯一让它更快的方法是让我们看到IP转换代码,以便查看是否可以以任何方式进行向量化。 - piRSquared

0
我通过以下代码传递了一个包含IP地址列的数据框,其中大约有300k行。这个过程大约需要20秒钟。
import pandas as pd
from geolite2 import geolite2

def get_country(row,ip):
    try:
        x = geo.get(row[ip])
    except ValueError:
        return pd.np.nan
    try:
        return x['country']['names']['en'] if x else pd.np.nan
    except KeyError:
        return pd.np.nan

geo = geolite2.reader()

# map IP --> country
df_test['login_ip_country'] = df_test.apply(lambda row: get_country(row,'login_ip_address'), axis = 1)
df_test['registered_ip_country'] = df_test.apply(lambda row: get_country(row,'registered_ip_address'), axis = 1)

geolite2.close()

df_test.head()

不需要把它变成一个系列。只需将“行”传递到您的函数中,该函数充当“df”


0

首先,@MaxU的答案是正确的,对于向量化的pd.series/dataframe并行应用非常高效和理想。

将比较两个流行库的性能,以返回给定IP地址信息的位置数据。简而言之:使用geolite2方法。

1. geolite2包来自geolite2

输入

# !pip install maxminddb-geolite2
import time
from geolite2 import geolite2
geo = geolite2.reader()
df_1 = train_data.loc[:50,['IP_Address']]

def IP_info_1(ip):
    try:
        x = geo.get(ip)
    except ValueError:   #Faulty IP value
        return np.nan
    try:
        return x['country']['names']['en'] if x is not None else np.nan
    except KeyError:   #Faulty Key value
        return np.nan


s_time = time.time()
# map IP --> country
#apply(fn) applies fn. on all pd.series elements
df_1['country'] = df_1.loc[:,'IP_Address'].apply(IP_info_1)
print(df_1.head(), '\n')
print('Time:',str(time.time()-s_time)+'s \n')

print(type(geo.get('48.151.136.76')))

输出

       IP_Address         country
0   48.151.136.76   United States
1    94.9.145.169  United Kingdom
2   58.94.157.121           Japan
3  193.187.41.186         Austria
4   125.96.20.172           China 

Time: 0.09906983375549316s 

<class 'dict'>

2. ip2geotools 库中的 DbIpCity

输入

# !pip install ip2geotools
import time
s_time = time.time()
from ip2geotools.databases.noncommercial import DbIpCity
df_2 = train_data.loc[:50,['IP_Address']]
def IP_info_2(ip):
    try:
        return DbIpCity.get(ip, api_key = 'free').country
    except:
        return np.nan
df_2['country'] = df_2.loc[:, 'IP_Address'].apply(IP_info_2)
print(df_2.head())
print('Time:',str(time.time()-s_time)+'s')

print(type(DbIpCity.get('48.151.136.76',api_key = 'free')))

输出

       IP_Address country
0   48.151.136.76      US
1    94.9.145.169      GB
2   58.94.157.121      JP
3  193.187.41.186      AT
4   125.96.20.172      CN

Time: 80.53318452835083s 

<class 'ip2geotools.models.IpLocation'>

一个可能导致巨大时间差异的原因是输出的数据结构,即直接从字典中进行子集提取似乎比从专门的ip2geotools.models.IpLocation对象进行索引更有效率。
此外,第一种方法的输出是包含地理位置数据的字典,可以分别对其进行子集提取以获取所需信息:
x = geolite2.reader().get('48.151.136.76')
print(x)

>>>
    {'city': {'geoname_id': 5101798, 'names': {'de': 'Newark', 'en': 'Newark', 'es': 'Newark', 'fr': 'Newark', 'ja': 'ニューアーク', 'pt-BR': 'Newark', 'ru': 'Ньюарк'}},

 'continent': {'code': 'NA', 'geoname_id': 6255149, 'names': {'de': 'Nordamerika', 'en': 'North America', 'es': 'Norteamérica', 'fr': 'Amérique du Nord', 'ja': '北アメリカ', 'pt-BR': 'América do Norte', 'ru': 'Северная Америка', 'zh-CN': '北美洲'}}, 

'country': {'geoname_id': 6252001, 'iso_code': 'US', 'names': {'de': 'USA', 'en': 'United States', 'es': 'Estados Unidos', 'fr': 'États-Unis', 'ja': 'アメリカ合衆国', 'pt-BR': 'Estados Unidos', 'ru': 'США', 'zh-CN': '美国'}}, 

'location': {'accuracy_radius': 1000, 'latitude': 40.7355, 'longitude': -74.1741, 'metro_code': 501, 'time_zone': 'America/New_York'}, 

'postal': {'code': '07102'}, 

'registered_country': {'geoname_id': 6252001, 'iso_code': 'US', 'names': {'de': 'USA', 'en': 'United States', 'es': 'Estados Unidos', 'fr': 'États-Unis', 'ja': 'アメリカ合衆国', 'pt-BR': 'Estados Unidos', 'ru': 'США', 'zh-CN': '美国'}}, 

'subdivisions': [{'geoname_id': 5101760, 'iso_code': 'NJ', 'names': {'en': 'New Jersey', 'es': 'Nueva Jersey', 'fr': 'New Jersey', 'ja': 'ニュージャージー州', 'pt-BR': 'Nova Jérsia', 'ru': 'Нью-Джерси', 'zh-CN': '新泽西州'}}]}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接