如何在Python中获取NIC(网络接口控制器)的IP地址?

103

当在Unix上的Python脚本中发生错误时,会发送一封电子邮件。

如果IP地址是192.168.100.37(即测试服务器),则要求我将{Testing Environment}添加到邮件主题行中。这样我们就可以有一个脚本版本和一种方式来判断邮件是否来自测试服务器上出错的数据。

然而,当我搜索时,我总是找到以下代码:

import socket
socket.gethostbyname(socket.gethostname())

但是,这给我提供的是127.0.1.1的IP地址。当我使用ifconfig时,我得到了这个:

eth0      Link encap:Ethernet  HWaddr 00:1c:c4:2c:c8:3e
          inet addr:192.168.100.37  Bcast:192.168.100.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:75760697 errors:0 dropped:411180 overruns:0 frame:0
          TX packets:23166399 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:59525958247 (59.5 GB)  TX bytes:10142130096 (10.1 GB)
          Interrupt:19 Memory:f0500000-f0520000

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:16436  Metric:1
          RX packets:25573544 errors:0 dropped:0 overruns:0 frame:0
          TX packets:25573544 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:44531490070 (44.5 GB)  TX bytes:44531490070 (44.5 GB)
首先,我不知道它从哪里获取了127.0.1.1,但无论如何那都不是我想要的。当我搜索时,我一直看到同样的语法,Bash脚本或netifaces,并且我正在尝试使用标准库。那么,我怎样才能在Python中获取eth0的IP地址?

我不知道它从哪里得到了127.0.1.1,来自于/etc/hosts文件。 - Ivan De Paz Centeno
你提到了 "127.0.1.1"。你是不是想说 "127.0.0.1"?因为这是你的本地回环接口 lo 的地址(在你的 ifconfig 输出中是第二个条目),也是如果你运行 cat /etc/hosts 命令时的第一个条目。 - voices
15个回答

225

两种方法:

方法一(使用外部包)

您需要请求绑定在您的eth0接口上的IP地址。这可以从netifaces包中获取。

import netifaces as ni
ip = ni.ifaddresses('eth0')[ni.AF_INET][0]['addr']
print(ip)  # should print "192.168.100.37"

您还可以通过以下方式获取所有可用接口的列表:

ni.interfaces()

方法二(无需外部包)

以下是一种获取 IP 地址的方式,无需使用 Python 包:

import socket
import fcntl
import struct

def get_ip_address(ifname):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    return socket.inet_ntoa(fcntl.ioctl(
        s.fileno(),
        0x8915,  # SIOCGIFADDR
        struct.pack('256s', ifname[:15])
    )[20:24])

get_ip_address('eth0')  # '192.168.0.110'
注意:通过检测IP地址来确定您正在使用的环境是一种相当粗糙的方法。几乎所有框架都提供了一种非常简单的方法来设置/修改环境变量以指示当前的环境。请查看您的文档以了解此内容。这应该很简单,只需执行以下操作即可。
if app.config['ENV'] == 'production':
  # send production email
else:
  # send development email

5
我知道我来晚了,但有人能解释一下Edit 2的巫术吗?我对套接字库还很陌生,从未使用过另外两个。 - Austin A
3
为什么使用ifname[:15]而不是直接使用ifname? - bdrx
44
针对Python 3,如果要使用ifname[:15],应该使用bytes(ifname[:15], 'utf-8')进行转换。 - Taranjeet
15
考虑到这是Python语言,使用"ni"作为"netifaces"的导入别名对某些骑士来说尤其合适...至少是一些爱开玩笑的人。;-) - Ubuntourist
3
方法2每次被调用时都会泄漏一个套接字。你需要在finally块中调用s.close()。 - hallidave
显示剩余17条评论

157

或者,如果您想获取用于连接到网络的任何接口的IP地址而无需知道其名称,可以使用以下内容:

import socket
def get_ip_address():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.connect(("8.8.8.8", 80))
    return s.getsockname()[0]

我知道这与你的问题有点不同,但其他人可能会来到这里并发现这个更有用。你不必拥有到8.8.8.8的路由就可以使用它。它所做的只是打开一个套接字,但不发送任何数据。


4
是的,可能不会走得太远,但有些局域网可能无法接收到它。 - jeremyjjbrown
1
这里关闭套接字是不必要的吗? - krizajb
1
@krizajb 我从来没有费心去做这件事,因为我每个pid只做一次,如果文件描述符在gc期间没有被释放,它将随着pid一起清理。如果出于某种原因您需要经常这样做,那么使用上下文管理器将是Pythonic的方式。https://dev59.com/i2Qn5IYBdhLWcg3wg3aR - jeremyjjbrown
1
@k4ppa 连接到8.8.8.8会强制系统进行外部连接,从而解析完整路由。这反过来为该系统提供了物理网络接口(或其中一个网络接口)的IP地址(基于内核为此连接选择的任何适配器)。如果没有连接,您将得到套接字名称的0.0.0.0。8.8.8.8中没有任何魔法,除了它(可能)总是存在并且肯定能够处理流量。在非互联网连接的私有网络上,您将使用该网络上的某个服务器号码(或名称)。 - Steve Cohen
1
@llpinokio 这只是获取IP的一种方式。这与未经授权监听所有IP相关的安全问题无关。它们是正交的。 - jeremyjjbrown
显示剩余7条评论

40

一个简单的方法,返回接口IP地址的字符串是:

from subprocess import check_output

ips = check_output(['hostname', '--all-ip-addresses'])

更多信息请参见 hostname

我特别喜欢这个,因为它在容器内部提供了正确的Docker容器IP。否则,人们会选择报告的第一个IP。太棒了! - Guido U. Draheim
4
这仅适用于Linux net-tools版本的“hostname”。Mac/BSD版本的 hostname命令不提供此功能。如果您正在使用Linux版本,则这可能是最简单的方法。 - theferrit32

23

由于大多数答案使用ifconfigeth0接口提取IPv4地址,但在大多数Linux发行版中已被废弃,改用ip addr,因此可以改用以下代码:

import os

ipv4 = os.popen('ip addr show eth0 | grep "\<inet\>" | awk \'{ print $2 }\' | awk -F "/" \'{ print $1 }\'').read().strip()
ipv6 = os.popen('ip addr show eth0 | grep "\<inet6\>" | awk \'{ print $2 }\' | awk -F "/" \'{ print $1 }\'').read().strip()

或者,您可以通过使用 split() 替代 grepAWK 将解析任务的一部分移交给 Python 解释器,正如 Sergiy Kolodyazhnyy 指出的那样

import os

ipv4 = os.popen('ip addr show eth0').read().split("inet ")[1].split("/")[0]
ipv6 = os.popen('ip addr show eth0').read().split("inet6 ")[1].split("/")[0]

但在这种情况下,您需要检查每个split()调用返回的数组的边界。


使用正则表达式的另一种版本:

import os
import re

ipv4 = re.search(re.compile(r'(?<=inet )(.*)(?=\/)', re.M), os.popen('ip addr show eth0').read()).groups()[0]
ipv6 = re.search(re.compile(r'(?<=inet6 )(.*)(?=\/)', re.M), os.popen('ip addr show eth0').read()).groups()[0]

3
使用 ip addr 命令做得不错,但其实没必要将输出通过管道传递给 grep 和 awk。Python 完全可以用 split() 方法来解析输出。 - Sergiy Kolodyazhnyy
为了使其更易于机器解析,请尝试使用-oneline选项。 "将每个记录输出到单独的一行,并用'\'字符替换换行符。当您想要使用wc(1)计算记录数或使用grep(1)搜索输出时,这非常方便。" - CivFan
1
此外,更新版本的 iproute2(其中包括 ip)支持 -j, --json 参数用于大多数命令,其功能与您所期望的相同。 - iamkroot

17

如果你只需要在Unix上工作,你可以使用系统调用(参考 Stack Overflow 问题 使用Bash解析ifconfig,仅获取我的IP地址):

import os
f = os.popen('ifconfig eth0 | grep "inet\ addr" | cut -d: -f2 | cut -d" " -f1')
your_ip=f.read()

2
这个问题是特定于发行版的。例如,在RHEL上,cut命令会切掉错误的术语。因此,这个命令在*nix发行版之间不具备可移植性。 - Eric Leschinski
2
在这里,grepcut真的不必要,只需要获取输出并解析它。此外,ip addr命令可能更受青睐。 - Sergiy Kolodyazhnyy

5

这将收集主机上的所有IP地址,并过滤掉环回/链路本地和IPv6。这也可以编辑为仅允许IPv6,或同时允许IPv4和IPv6,以及允许在IP地址列表中包含环回/链路本地。

from socket import getaddrinfo, gethostname
import ipaddress

def get_ip(ip_addr_proto="ipv4", ignore_local_ips=True):
    # By default, this method only returns non-local IPv4 addresses
    # To return IPv6 only, call get_ip('ipv6')
    # To return both IPv4 and IPv6, call get_ip('both')
    # To return local IPs, call get_ip(None, False)
    # Can combine options like so get_ip('both', False)

    af_inet = 2
    if ip_addr_proto == "ipv6":
        af_inet = 30
    elif ip_addr_proto == "both":
        af_inet = 0

    system_ip_list = getaddrinfo(gethostname(), None, af_inet, 1, 0)
    ip_list = []

    for ip in system_ip_list:
        ip = ip[4][0]

        try:
            ipaddress.ip_address(str(ip))
            ip_address_valid = True
        except ValueError:
            ip_address_valid = False
        else:
            if ipaddress.ip_address(ip).is_loopback and ignore_local_ips or ipaddress.ip_address(ip).is_link_local and ignore_local_ips:
                pass
            elif ip_address_valid:
                ip_list.append(ip)

    return ip_list

print(f"Your IP address is: {get_ip()}")

返回

您的IP地址是:['192.168.1.118']

如果我运行get_ip('both', False),它将返回

您的IP地址是:['::1','fe80 :: 1','127.0.0.1','192.168.1.118','fe80 :: cb9:d2dd:a505:423a']


4

jeremyjjbrown的答案的基础上,这里有另一个版本,根据他的答案中的评论自动清理。

这个版本还允许提供不同的服务器地址,以供内部私有网络等使用。

import socket

def get_my_ip_address(remote_server="google.com"):
    """
    Return the/a network-facing IP number for this system.
    """
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
        s.connect((remote_server, 80))
        return s.getsockname()[0]

1
我建议使用IP地址而不是主机名作为“remote_server”的默认值,这样您就不会触发DNS查找。 - plugwash

3
使用psutil来回答:
import psutil
import socket

def get_ipv4_from_nic(interface):
    interface_addrs = psutil.net_if_addrs().get(interface) or []
    for snicaddr in interface_addrs:
        if snicaddr.family == socket.AF_INET:
            return snicaddr.address

例子:

>>> get_ipv4_from_nic("eth0")
'192.168.100.37'

1
如果你想采用困难的方式(但可能更快?),这里有一些粗略的 NetlinkRFC 3549)代码(可能仅适用于Linux),可以通过标准库中的一个导入语句同时获取IPv4和IPv6。
    import socket
    # https://www.man7.org/linux/man-pages/man7/rtnetlink.7.html
    # https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h
    RTM_NEWADDR = 20
    RTM_GETADDR = 22
    # https://www.man7.org/linux/man-pages/man7/netlink.7.html
    # https://github.com/torvalds/linux/blob/master/include/uapi/linux/netlink.h
    NLM_F_REQUEST = 0x01
    NLM_F_ROOT = 0x100
    s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW)
    req = (
        # nlmsghdr
        int.to_bytes(0, 4, 'little', signed=False) +  # nlmsg_len
        int.to_bytes(RTM_GETADDR, 2, 'little', signed=False) +  # nlmsg_type
        int.to_bytes(NLM_F_REQUEST | NLM_F_ROOT, 2, 'little', signed=False) +  # nlmsg_flags
        int.to_bytes(0, 2, 'little', signed=False) +  # nlmsg_seq
        int.to_bytes(0, 2, 'little', signed=False) +  # nlmsg_pid
        # ifinfomsg
        b'\0' * 8
    )
    req = int.to_bytes(len(req), 4, 'little') + req[4:]
    s.sendall(req)
    full_resp = s.recv(4096)
    while full_resp:
        resp = full_resp
        # nlmsghdr
        nlmsg_len = int.from_bytes(resp[0:4], 'little', signed=False)
        nlmsg_type = int.from_bytes(resp[4:6], 'little', signed=False)
        assert not nlmsg_len % 4, nlmsg_len
        resp = resp[16:nlmsg_len]
        full_resp = full_resp[nlmsg_len:]
        if nlmsg_type == 3:  # NLMSG_DONE
            assert not full_resp, full_resp
            break
        if not full_resp:
            full_resp = s.recv(4096)
        assert nlmsg_type == RTM_NEWADDR, (nlmsg_type, resp[:32])
        # ifaddrmsg
        ifa_family = int.from_bytes(resp[0:1], 'little', signed=False)
        ifa_index = int.from_bytes(resp[4:8], 'little', signed=False)
        resp = resp[8:]
        while resp:
            # rtattr
            rta_len = int.from_bytes(resp[0:2], 'little', signed=False)
            rta_type = int.from_bytes(resp[2:4], 'little', signed=False)
            data = resp[4:rta_len]
            if rta_type == 1:  # IFLA_ADDRESS
                if ifa_family == socket.AF_INET:
                    ip = '.'.join('%d' % c for c in data)
                if ifa_family == socket.AF_INET6:
                    ip = ':'.join(('%02x%02x' % (chunk[0], chunk[1]) if chunk != b'\0\0' else '') for chunk in [data[0:2], data[2:4], data[4:6], data[6:8], data[8:10], data[10:12], data[12:14], data[14:16]])
                print('interface #%s has %s' % (ifa_index, ip))
            if rta_type == 3:  # IFLA_IFNAME
                ifname = data.rstrip(b'\0').decode()
                print('interface #%s is named %s' % (ifa_index, ifname))
            # need to round up to multiple of 4
            if rta_len % 4:
                rta_len += 4 - rta_len % 4
            resp = resp[rta_len:]
    s.close()

如果您只需要IPv4,则另一个答案中的老派 方法可能更直接。对于IPv6,可以使用/proc/net/if_inet6。

0

这对我有用

 import subprocess
 my_ip = subprocess.Popen(['ifconfig eth0 | awk "/inet /" | cut -d":" -f 2 | cut -d" " -f1'], stdout=subprocess.PIPE, shell=True)
 (IP,errors) = my_ip.communicate()
 my_ip.stdout.close()
 print IP

1
我的猜测是因为Linux在ifconfig输出中不使用“:”分隔符,所以有人给它打了-1。 这个命令只能在包含这样一些东西的某些Unix上工作。 - Mike S

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