使用Python标准库查找本地IP地址

719

如何在Python中独立于平台且仅使用标准库找到本地IP地址(即192.168.x.x或10.0.x.x)?


11
本地IP?还是公共IP?你要如何处理有多个IP的系统? - Sargun Dhillon
使用 ifconfig -a 命令并使用其输出... - Fredrik Pihl
18
@Fredrik,那是个糟糕的想法。首先,你没有必要分叉一个新进程,这可能会防止你的程序在严格锁定的配置中工作(或者你将不得不授予你的程序不需要的权限)。其次,你会为不同语言环境的用户引入错误。第三,如果您决定启动一个新程序,您不应该启动一个已经弃用的程序 - ip addr 更加适合(而且更容易解析)。 - phihag
14
@phihag,你说得完全正确,感谢你指出我的错误。 - Fredrik Pihl
1
这里更根本的问题是,在一个正确编写的现代网络程序中,正确的(一组)本地IP地址取决于对等方或潜在对等方的集合。如果需要将本地IP地址用于将套接字“绑定”到特定接口,则这是一个策略问题。如果需要将本地IP地址交给对等方,以便对等方可以“回调”,即打开到本地机器的连接,则情况取决于是否存在任何NAT(网络地址转换)盒子。如果没有NAT,则getsockname是一个不错的选择。 - Pekka Nikander
如果存在网络地址转换(NAT),而我仍然想使用回调来连接套接字,那该怎么办? - Parth Lathiya
50个回答

631

我刚发现这个,但它似乎有点hackish。不过他们说他们在*nix上试过了,我在Windows上也试了一下,可以用。

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
print(s.getsockname()[0])
s.close()

假设您有互联网访问,且没有本地代理。


39
如果你的电脑拥有多个接口,并且需要找到连接到例如gmail.com的那个接口。 - elzapp
5
捕获由s.connect()引发的socket.error异常可能是个好主意! - phobie
45
使用IP地址而不是域名更好,这样可以更快地访问,并且不受DNS可用性的影响。例如,我们可以使用8.8.8.8 IP地址——谷歌的公共DNS服务器。 - wobmene
12
非常聪明,完美地发挥了作用。如果适用的话,您还可以使用想要被看到的服务器的IP地址或地址,而不是Gmail或8.8.8.8。 - Prof. Falken
3
这个例子需要外部依赖来解析gmail.com。如果你把它设置为一个不在本地局域网的IP地址(无论它是上线还是下线),它将可以在没有依赖和网络流量的情况下工作。 - grim
显示剩余19条评论

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

这并不总是有效的(在拥有主机名为127.0.0.1/etc/hosts上返回127.0.0.1),一个临时的解决方案是像gimel所展示的那样,改用socket.getfqdn()。当然,您的计算机需要具有可解析的主机名。


54
需要注意的是,这不是一个平台无关的解决方案。很多Linux系统会使用这种方法返回127.0.0.1作为您的IP地址。 - Jason Baker
25
获取本地主机名并将其解析为IP地址的代码:socket.gethostbyname(socket.getfqdn()) - gimel
65
看起来这只返回单个IP地址。如果机器有多个地址怎么办? - Jason R. Coombs
42
在Ubuntu上,由于某种原因,这会返回127.0.1.1。 - slikts
27
使用以下代码获取属于主机的 IPv4 地址列表: socket.gethostbyname_ex(socket.gethostname())[-1] - Barmaley
显示剩余17条评论

464
该方法返回本地计算机上带有默认路由的“主要”IP地址。
  • 不需要可路由的网络访问或任何连接。
  • 即使所有接口都从网络中拔掉,也能正常工作。
  • 不需要,甚至不会尝试连接到其他任何地方。
  • 适用于 NAT、公共、私有、外部和内部 IP 地址。
  • 纯 Python 2(或3),没有外部依赖项。
  • 适用于 Linux、Windows 和 OSX。

Python 3 或 2:

    import socket
    def get_ip():
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.settimeout(0)
        try:
            # doesn't even have to be reachable
            s.connect(('10.254.254.254', 1))
            IP = s.getsockname()[0]
        except Exception:
            IP = '127.0.0.1'
        finally:
            s.close()
        return IP
    print(get_ip())

此函数返回一个单一的IP地址,即主IP地址(带有默认路由)。如果您需要获取所有连接到所有接口(包括本地主机等)的IP地址,请参见这个答案。如果您在家中使用NAT防火墙,如wifi路由器,则它不会显示您的公共NAT IP地址,而是显示连接到您的本地WIFI路由器的本地网络上的私有IP地址,该地址具有指向您的WIFI路由器的默认路由。如果您需要获取外部IP地址:①在外部设备(wifi路由器)上运行此函数,或②连接到外部服务(例如https://www.ipify.org/),该服务可以反映出从外部世界看到的IP地址。但这些想法完全不同于原始问题。 :)

11
能在Raspbian上使用Python 2和3! - pierce.jason
3
太棒了!支持在Win7、8、8.1以及Linux Mint和Arch上运行,包括虚拟机。 - shermy
3
由于某种原因,这在Mac OS X El Capitan 10.11.6上无法运行(它生成一个异常OS错误:[Errno 49]无法分配请求的地址)。将端口从“0”更改为“1”:s.connect(('10.255.255.255', 1))对我来说在Mac OS X和Linux Ubuntu 17.04上都有效。 - Pedro Scarapicchia Junior
40
这应该是被接受的答案。socket.gethostbyname(socket.gethostname()) 得到的结果很糟糕。 - Jason Floyd
2
我在Ubuntu 22.04上使用此代码时遇到了“权限被拒绝”异常。 - Bram
显示剩余16条评论

166
作为一个被称为myip的别名:
alias myip="python -c 'import socket; print([l for l in ([ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith(\"127.\")][:1], [[(s.connect((\"8.8.8.8\", 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]]) if l][0][0])'"
  • 在Python 2.x、Python 3.x、现代和旧版Linux发行版、OSX/macOS和Windows上,可以正确运行,用于查找当前的IPv4地址。
  • 对于具有多个IP地址、IPv6、未配置IP地址或无互联网访问的机器,将不会返回正确的结果。
  • 据报道,这在最新版本的macOS上不起作用。

注意:如果您打算在Python程序中使用类似的功能,正确的方法是使用支持IPv6的Python模块。


与上述相同,但仅包括Python代码:

import socket
print([l for l in ([ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith("127.")][:1], [[(s.connect(('8.8.8.8', 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]]) if l][0][0])

如果没有配置IP地址,这将抛出一个异常。

适用于没有互联网连接的局域网的版本:

import socket
print((([ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith("127.")] or [[(s.connect(("8.8.8.8", 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]]) + ["no IP found"])[0])

谢谢@ccpizza


背景:

在这里使用socket.gethostbyname(socket.gethostname())是行不通的,因为我所在的其中一台计算机有一个/etc/hosts文件,其中有重复的条目和对自身的引用。 socket.gethostbyname()只会返回/etc/hosts中的最后一个条目。

这是我的初始尝试,它会过滤掉所有以"127."开头的地址:

import socket
print([ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith("127.")][:1])

这适用于Python 2和3,在Linux和Windows上运行,但不适用于几个网络设备或IPv6。然而,它在最近的Linux发行版上停止工作,所以我尝试了这种替代技术。它试图连接到Google DNS服务器的IP地址为8.8.8.8,端口为53。
import socket
print([(s.connect(('8.8.8.8', 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1])

这种方法也适用于最新版本的 macOS!
然后,我将上述两种技术结合成一个一行代码,应该在任何地方都能运行,并在答案的顶部创建了“myip”别名和 Python 代码片段。
随着 IPv6 的日益普及,对于具有多个网络接口的服务器来说,使用第三方 Python 模块来查找 IP 地址可能比这里列出的任何方法都更稳定可靠。

3
@Alexander: 只是说这个答案比以前不那么有用了(而过滤重复项并不是什么大问题;)。 根据文档,socket.getaddrinfo() 应该在所有平台上工作一致 - 但我只在 Linux 上进行了检查,没有关心其他操作系统。 - Wladimir Palant
1
@Alexander,“/etc/resolve.conf:没有这样的文件或目录”,但我可以通过ifconfig显示本地IPv4地址。 - anatoly techtonik
2
我可以确认更新版本在Ubuntu 14.04上与Python2和Py3k都能正常工作。 - Uli Köhler
4
“更新”展示了使用UDP套接字上的connect()方法的一个巧妙技巧。它不发送任何流量,但可以让您找到发送给指定接收者的数据包的发送者地址。端口可能无关紧要(即使为0也应该可以)。在多宿主机上,选择正确子网中的地址非常重要。 - Peter Hansen
1
合并的别名代码即使gethostbyname_ex返回有效的IP地址,也会初始化到8.8.8.8的不必要的外部连接。这将在没有互联网的“有围墙花园”类型的局域网中出现问题。可以通过使用or来条件地进行外部调用,例如:ips = [ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith("127.")] or [[(s.connect(("8.8.8.8", 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]] - ccpizza
显示剩余13条评论

104
你可以使用netifaces模块。只需输入:
pip install netifaces

在您的命令行 shell 中运行该命令,它将安装到默认的 Python 安装中。

然后您可以像这样使用它:

from netifaces import interfaces, ifaddresses, AF_INET
for ifaceName in interfaces():
    addresses = [i['addr'] for i in ifaddresses(ifaceName).setdefault(AF_INET, [{'addr':'No IP addr'}] )]
    print '%s: %s' % (ifaceName, ', '.join(addresses))

在我的电脑上打印出如下内容:

{45639BDC-1050-46E0-9BE9-075C30DE1FBC}: 192.168.0.100
{D43A468B-F3AE-4BF9-9391-4863A4500583}: 10.5.9.207

模块的作者声称它可以在 Windows、UNIX 和 Mac OS X 上正常工作。


24
如题所述,我希望从默认安装中获取某些内容,也就是说不需要进行额外的安装。 - UnkwnTech
6
@MattJoiner ,这些事情现在都不再是真的了(最新版本在PyPI上有Windows二进制文件,并支持Py3K)。 - al45tair
6
就目前而言,netifaces最新版本确实在Windows上支持IPv6。 - al45tair
3
鉴于Python秉承“一揽子式”(batteries included)的理念,这个模块必须成为标准库的一部分。 - ccpizza
提出一个 bug,也许它会被包含在 sys 中,这似乎是一个很好的候选项。 - Jasen
1
@MattJoiner - 注意,在Ubuntu上,最新版本的Python或Py3K都不需要C编译器。而且该模块也有相应的软件包可用。 - Craig S. Anderson

55
如果计算机有通往互联网的路由,即使/etc/hosts没有正确设置,也始终可以使用此方法来获取首选本地IP地址。
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 1))  # connect() for UDP doesn't send packets
local_ip_address = s.getsockname()[0]

1
这是如何工作的?8.8.8.8是谷歌DNS服务器,我们能否使用本地DNS服务器来完成它? - Ciasto piekarz
1
@Ciastopiekarz 地址无需有效。参见: https://dev59.com/m3VC5IYBdhLWcg3w1E_w#28950776 - aiwl

52

Socket API 方法

参见 https://dev59.com/m3VC5IYBdhLWcg3w1E_w#28950776

缺点:

  • 不能跨平台。
  • 需要更多的回退代码,与特定互联网地址的存在相关
  • 如果您在NAT后面,则这也无法工作
  • 可能会创建一个UDP连接,不独立于(通常是ISP的)DNS可用性(有关使用8.8.8.8:Google的(巧合地也是DNS)服务器等想法,请参见其他答案)
  • 确保将目标地址设置为 UNREACHABLE,例如保证未使用的数字IP地址。不要使用像fakesubdomain.google.com或somefakewebsite.com这样的域名; 您仍然会在处理过程中向该方发送垃圾邮件(现在或将来),并且还会向自己的网络盒子发送垃圾邮件。

反射器方法

(请注意,这并没有回答 OP 的本地 IP 地址问题,例如 192.168...;它提供了您的公共 IP 地址,根据用例可能更可取。)

您可以查询像 whatismyip.com 这样的网站(但使用 API),例如:

from urllib.request import urlopen
import re
def getPublicIp():
    data = str(urlopen('http://checkip.dyndns.com/').read())
    # data = '<html><head><title>Current IP Check</title></head><body>Current IP Address: 65.96.168.198</body></html>\r\n'

    return re.compile(r'Address: (\d+\.\d+\.\d+\.\d+)').search(data).group(1)

或者如果使用Python2:

from urllib import urlopen
import re
def getPublicIp():
    data = str(urlopen('http://checkip.dyndns.com/').read())
    # data = '<html><head><title>Current IP Check</title></head><body>Current IP Address: 65.96.168.198</body></html>\r\n'

    return re.compile(r'Address: (\d+\.\d+\.\d+\.\d+)').search(data).group(1)

优点:

  • 这种方法的一个优点是它跨平台。
  • 它可以在不利网络地址转换(例如您家中的路由器)的情况下工作。

缺点(及解决方法):

  • 需要此网站处于运行状态,格式不更改(几乎肯定不会),并且您的 DNS 服务器正常工作。如果出现故障,您可以通过查询其他第三方 IP 地址反射器来减轻此问题。
  • 如果您不查询多个反射器(以防止被篡改的反射器告诉您您的地址与实际不符),或者如果您不使用 HTTPS(以防止中间人攻击假冒服务器),则可能存在攻击向量。

编辑:尽管最初我认为这些方法非常糟糕(除非您使用多个备选项,否则代码在未来很多年可能就不再适用了),但它确实引出了“互联网是什么?”这个问题。计算机可能具有指向许多不同网络的许多接口。有关该主题的更详细描述,请搜索“网关和路由”。计算机可能能够通过内部网关访问内部网络,或者通过例如路由器上的网关访问全球互联网(通常情况下)。 OP 询问的本地 IP 地址仅对于单个链路有明确定义,因此您必须指定它(“我们谈论的是网络卡还是以太网电缆?”)。对于这个问题的提出可能会有多个非唯一的答案。但是,在世界范围内的全局 IP 地址可能已经明确定义了(在没有大规模网络分片的情况下):可能是通过可以访问 TLD 的网关的返回路径。


如果您在NAT后面,这将返回您的局域网地址。如果您连接到互联网,您可以连接到一个返回您公共IP地址之一的Web服务。 - phihag
它不会创建TCP连接,因为它创建的是UDP连接。 - Anuj Gupta
2
作为套接字 API 版本的替代方案,将 s.connect(('INSERT SOME TARGET WEBSITE.com', 0)) 替换为 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1);s.connect(('<broadcast>', 0)) 可以避免 DNS 查找。(如果有防火墙,则广播可能会出现问题) - dlm

41
在Linux上:
>>> import socket, struct, fcntl
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> sockfd = sock.fileno()
>>> SIOCGIFADDR = 0x8915
>>>
>>> def get_ip(iface = 'eth0'):
...     ifreq = struct.pack('16sH14s', iface, socket.AF_INET, '\x00'*14)
...     try:
...         res = fcntl.ioctl(sockfd, SIOCGIFADDR, ifreq)
...     except:
...         return None
...     ip = struct.unpack('16sH2x4s8x', res)[2]
...     return socket.inet_ntoa(ip)
... 
>>> get_ip('eth0')
'10.80.40.234'
>>> 

那么这实际上打开了一个套接字,但它没有执行任何操作,您可以检查该套接字的原始数据以获取本地IP? - Dave
1
打开套接字以获取与内核通信的fd(通过ioctl)。套接字未绑定您想要有关addr信息的接口-它只是用户空间和内核之间的通信机制。https://en.wikipedia.org/wiki/Ioctl http://lxr.free-electrons.com/source/net/socket.c - tMC
3
在Python3上工作,只需进行一项修改:将struct.pack('16sH14s', iface, socket.AF_INET, '\x00'*14)替换为struct.pack('16sH14s', iface.encode('utf-8'), socket.AF_INET, b'\x00'*14) - pepoluan
2
@ChristianFischer ioctl 是一个遗留接口,我不认为它支持 IPv6,也很可能永远不会支持。我认为“正确”的方法是通过 Netlink,但在 Python 中并不是非常直观。我认为 libc 应该有函数 getifaddrs,可以通过 Python 的 ctypes 模块访问,这可能有效 - http://man7.org/linux/man-pages/man3/getifaddrs.3.html - tMC
1
@Maddy ioctl是一个传统的接口,我不认为它支持IPv6,也很可能永远不会支持。我认为“正确”的方法是通过Netlink,在Python中并不是非常直观。我认为libc应该有getifaddrs函数,可以通过Python的ctypes模块访问,这可能有效- man7.org/linux/man-pages/man3/getifaddrs.3.html - tMC
显示剩余2条评论

28

我正在使用以下模块:

#!/usr/bin/python
# module for getting the lan ip address of the computer

import os
import socket

if os.name != "nt":
    import fcntl
    import struct
    def get_interface_ip(ifname):
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        return socket.inet_ntoa(fcntl.ioctl(
                s.fileno(),
                0x8915,  # SIOCGIFADDR
                struct.pack('256s', bytes(ifname[:15], 'utf-8'))
                # Python 2.7: remove the second argument for the bytes call
            )[20:24])

def get_lan_ip():
    ip = socket.gethostbyname(socket.gethostname())
    if ip.startswith("127.") and os.name != "nt":
        interfaces = ["eth0","eth1","eth2","wlan0","wlan1","wifi0","ath0","ath1","ppp0"]
        for ifname in interfaces:
            try:
                ip = get_interface_ip(ifname)
                break;
            except IOError:
                pass
    return ip

已在 Windows 和 Linux 上进行了测试(不需要为这些系统安装额外的模块),适用于单个基于 IPv4 的局域网系统。

固定接口名称列表不适用于最近的 Linux 版本,它们采用了 systemd v197 更改以实现可预测的接口命名,正如 Alexander 指出的那样。 在这种情况下,您需要手动替换列表中的接口名称,或使用另一个解决方案,如netifaces


2
这与新的可预测Linux接口名称(如enp0s25)不兼容。有关更多信息,请参见https://wiki.archlinux.org/index.php/Network_Configuration#Device_names。 - Alexander
3
我使用的是Python 3.4,'struct.pack(...)' 部分需要更改为 'struct.pack('256s', bytes(ifname[:15], 'utf-8'))'。请参考此问题:https://dev59.com/94Xca4cB1Zd3GeqPPOq0。 - Bakanekobrain
1
在Raspbian上使用Python 2.7.3 - bytes()不支持第二个参数。但是这个可以用:struct.pack('256s', bytes(ifname[:15])) - colm.anseo

24

[仅适用于Windows] 如果你不想使用外部包并且不想依赖外部互联网服务器,这可能会有所帮助。这是我在Google Code Search上找到并修改以返回所需信息的代码示例:

def getIPAddresses():
    from ctypes import Structure, windll, sizeof
    from ctypes import POINTER, byref
    from ctypes import c_ulong, c_uint, c_ubyte, c_char
    MAX_ADAPTER_DESCRIPTION_LENGTH = 128
    MAX_ADAPTER_NAME_LENGTH = 256
    MAX_ADAPTER_ADDRESS_LENGTH = 8
    class IP_ADDR_STRING(Structure):
        pass
    LP_IP_ADDR_STRING = POINTER(IP_ADDR_STRING)
    IP_ADDR_STRING._fields_ = [
        ("next", LP_IP_ADDR_STRING),
        ("ipAddress", c_char * 16),
        ("ipMask", c_char * 16),
        ("context", c_ulong)]
    class IP_ADAPTER_INFO (Structure):
        pass
    LP_IP_ADAPTER_INFO = POINTER(IP_ADAPTER_INFO)
    IP_ADAPTER_INFO._fields_ = [
        ("next", LP_IP_ADAPTER_INFO),
        ("comboIndex", c_ulong),
        ("adapterName", c_char * (MAX_ADAPTER_NAME_LENGTH + 4)),
        ("description", c_char * (MAX_ADAPTER_DESCRIPTION_LENGTH + 4)),
        ("addressLength", c_uint),
        ("address", c_ubyte * MAX_ADAPTER_ADDRESS_LENGTH),
        ("index", c_ulong),
        ("type", c_uint),
        ("dhcpEnabled", c_uint),
        ("currentIpAddress", LP_IP_ADDR_STRING),
        ("ipAddressList", IP_ADDR_STRING),
        ("gatewayList", IP_ADDR_STRING),
        ("dhcpServer", IP_ADDR_STRING),
        ("haveWins", c_uint),
        ("primaryWinsServer", IP_ADDR_STRING),
        ("secondaryWinsServer", IP_ADDR_STRING),
        ("leaseObtained", c_ulong),
        ("leaseExpires", c_ulong)]
    GetAdaptersInfo = windll.iphlpapi.GetAdaptersInfo
    GetAdaptersInfo.restype = c_ulong
    GetAdaptersInfo.argtypes = [LP_IP_ADAPTER_INFO, POINTER(c_ulong)]
    adapterList = (IP_ADAPTER_INFO * 10)()
    buflen = c_ulong(sizeof(adapterList))
    rc = GetAdaptersInfo(byref(adapterList[0]), byref(buflen))
    if rc == 0:
        for a in adapterList:
            adNode = a.ipAddressList
            while True:
                ipAddr = adNode.ipAddress
                if ipAddr:
                    yield ipAddr
                adNode = adNode.next
                if not adNode:
                    break

用法:

>>> for addr in getIPAddresses():
>>>    print addr
192.168.0.100
10.5.9.207

由于它依赖于windll,因此只能在Windows上工作。


上面的一行解决方案通常在Windows上运行良好。问题出在Linux上。 - ricree
16
这种技术至少尝试返回计算机上的所有地址。 - Jason R. Coombs
1
这个脚本在我的机器上返回第一个地址后失败了。错误是“AttributeError: 'LP_IP_ADDR_STRING' object has no attribute 'ipAddress'”,我怀疑这与IPv6地址有关。 - Jason R. Coombs
1
原来问题是除了第一个IP地址外,adNode没有被解引用。在while循环的示例中再添加一行代码,它对我有效:adNode = adNode.contents。 - Jason R. Coombs

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