资讯详情

用Python从零实现IP扫描工具:主机探测与端口扫描实战

📅 2026/10/11 2:05:37 | 华诺云谱 👁 阅读
用Python从零实现IP扫描工具:主机探测与端口扫描实战
朋友们平时做网络调试、内网排查或者学习 TCP/IP 协议的时候经常需要快速了解某个网段内有哪些主机在线、哪些端口是开放的。如果用现成的商业软件要么收费要么功能太重如果手动一条条 ping 和 telnet又太低效。其实用 Python 写一个简易的 IP 扫描工具并不复杂既能锻炼网络编程能力又能满足日常的轻量扫描需求。本文将基于 Python 实现一个命令行下的简易 IP 扫描工具覆盖主机存活探测、TCP 端口扫描、多线程并发、结果导出等核心功能。全文会先讲清楚扫描工具背后的网络原理再给出完整代码、运行演示和常见问题排查思路。无论你是 Python 初学者还是有一定经验的开发者都可以跟着本文一步步搭建并扩展自己的扫描工具。1. 背景与核心概念1.1 什么是 IP 扫描工具IP 扫描工具是一类用于探测网络中主机状态和端口开放情况的软件。它通常执行两种任务主机存活探测判断某个 IP 地址是否有主机在线常用的手段是 ICMP Ping。端口开放扫描尝试连接目标主机的指定端口判断该端口是否提供了服务。在网络管理、系统运维、渗透测试前期的信息收集阶段IP 扫描都是非常基础且常见的操作。一个简单的 Python 扫描脚本在实际工作中可以帮助你完成这些事情批量获取办公室内网在线设备、确认某台服务器的 Web 端口是否正常开放、检查防火墙策略是否放行了指定端口等。不过需要特别强调的是这个工具仅建议在你自己拥有或已获得授权的网络环境中使用。未经许可扫描他人网络是不被允许的本文所有示例都基于本地环境和实验网段展开。1.2 两类核心扫描原理理解扫描原理是写好工具的关键下面用通俗的语言拆解一下。第一类是主机存活探测。最常见的实现方式是 ICMP Echo 请求也就是常说的 ping 命令。它向目标地址发送一个 ICMP 报文如果主机在线且没有禁用 ICMP 响应则会回送一个 Echo Reply 报文说明主机存活。Python 中不一定需要亲自构造 ICMP 数据包直接调用系统 ping 命令再解析返回值是一种轻量且稳定的实现方式。第二类是端口扫描。TCP 协议建立连接时有三次握手过程客户端发送 SYN 包服务端回复 SYNACK客户端再回复 ACK连接就建立了。如果我们要判断目标端口是否开放最简单的办法是尝试和它完成一次完整的 TCP 连接连接成功则说明端口开放。这种扫描方式在 TCP 扫描中称为 TCP Connect 扫描它在 Python 中对应的就是socket.connect_ex()。1.3 为什么选择 Python 来实现Python 非常适合这种规模适中的网络脚本工具原因有几个标准库已经提供了socket、ipaddress、subprocess、concurrent.futures等模块不需要安装第三方依赖。代码简洁适合快速开发和二次修改。跨平台能力较强基本可以在 Windows、Linux、macOS 上运行个别命令细节需要适配。多线程和并发模型可以直接使用标准库实现扫描效率足够日常使用。掌握这个项目之后你也能加深对 TCP、ICMP、套接字编程和多线程调度的理解后续想扩展端口服务识别、ARP 扫描等功能时基础都在这里。2. 环境准备与项目结构2.1 运行环境本文示例以常见的 Python 3 环境为主。因为代码只用标准库所以不涉及额外的依赖安装。如果你本地还没有 Python建议安装 3.8 或更高版本。项目代码不需要虚拟环境但如果你习惯使用 venv也可以先创建虚拟环境再操作。操作系统方面Windows 和 Linux 的差异主要在于 ping 命令的返回值处理。本文的代码会做兼容处理保证在两种系统下都能工作。开发工具可以选择 PyCharm、VS Code 或直接使用命令行编辑器。代码文件本身比较小即使用系统自带的记事本编写也能完成。2.2 版本说明扫描工具的代码会用到以下 Python 标准库模块socket用于 TCP 端口连接。ipaddress用于解析 IP 地址段。subprocess用于调用系统 ping 命令。concurrent.futures用于多线程并发扫描。argparse用于解析命令行参数。csv用于导出扫描结果。datetime用于计算扫描耗时。这些模块在 Python 3.8 及以上版本中都是内置的。不同操作系统对 ping 命令的返回码处理略有不同代码中已经做了兼容判断不需要额外安装任何第三方包。2.3 项目目录结构为了保持工程整洁建议按下面的结构组织文件ip_scanner/ ├── scanner.py # 主程序包含参数解析和调度逻辑 ├── core/ │ ├── __init__.py │ ├── ping.py # 主机存活探测模块 │ ├── port_scan.py # 端口扫描模块 │ └── utils.py # 工具函数如 IP 范围解析、结果导出 └── README.md # 项目说明文档如果你的项目规模不需要拆分模块也可以把全部代码放进一个scanner.py。不过拆分成独立模块后后续扩展会方便很多比如给 ping 模块增加 ARP 探测、给端口扫描模块增加服务指纹识别。3. 核心原理与关键技术拆解在写代码之前需要把几个关键知识点理清楚。3.1 socket 套接字与 connect_ex 方法Python 的socket模块封装了 BSD 套接字接口。创建 TCP 连接的基本流程是创建 socket 对象指定地址族IPv4 使用AF_INET和套接字类型TCP 使用SOCK_STREAM。调用connect()或connect_ex()连接目标地址和端口。关闭连接。connect()在连接失败时会抛出异常而connect_ex()会返回一个错误码。返回值为 0 时表示连接成功其他值表示不同原因的错误。扫描场景下我们更推荐使用connect_ex()因为它可以用返回值来判断结果避免异常处理干扰代码逻辑。import socket port 80 result socket.socket(socket.AF_INET, socket.SOCK_STREAM) code result.connect_ex((192.168.1.1, port)) if code 0: print(f端口 {port} 开放) else: print(f端口 {port} 关闭或不可达) result.close()3.2 超时机制扫描时不能无限等待某个端口响应。socket.settimeout()可以设置连接超时时间例如设置 1 秒表示如果 1 秒内没有完成连接或收到拒绝信息就认为该端口处于过滤或关闭状态。超时值的选择直接影响扫描速度与准确性。超时过短容易漏报特别是对延迟较高的目标主机。超时过长扫描速度变慢端口多时总体耗时会明显增加。实际项目中推荐将超时做成命令行参数由使用者根据网络环境调整默认值可以设为 1 秒。3.3 多线程并发扫描端口扫描本质上是一个典型的 IO 密集型任务连接等待期间 CPU 大部分时间处于空闲状态。使用多线程可以让多个端口在同一时间段内并发连接大幅提升扫描速度。Python 的concurrent.futures.ThreadPoolExecutor是一个非常适合这种任务的高层封装。它的用法非常简单把任务函数和参数列表提交给线程池再统一收集结果即可。需要注意控制最大线程数如果同时开启几千个线程操作系统和本机网络栈都可能有压力反而会降低扫描效率。默认最大线程数可以设为 100也可以作为参数调整。3.4 IP 地址范围解析用户输入可能是单个 IP、IP 段也可能是带掩码的 CIDR 地址。Python 标准库中的ipaddress模块正好支持这些格式的解析。import ipaddress network ipaddress.ip_network(192.168.1.0/24, strictFalse) for ip in network.hosts(): print(ip)对于形如192.168.1.1-192.168.1.10的起止范围可以自己编写解析逻辑先将地址转换为整数再在范围内循环生成。3.5 调用系统 ping 命令的跨平台处理subprocess模块可以执行外部命令。在 Windows 上ping 命令的选项是ping -n 1 -w 1000命令成功时返回码为 0在 Linux 和 macOS 上常用的是ping -c 1 -W 1同样以返回码判断结果。为了让程序跨平台运行可以通过sys.platform判断当前操作系统类型再组装对应的命令参数。这种方式的缺点是依赖外部命令优点是无需构造原始 ICMP 包代码量少且稳定。如果你希望完全基于 Python 实现 ICMP 探测可以使用socket构造 ICMP 报文但 ICMP 包的校验和计算相对复杂而且普通用户权限下在 Windows 上发送 ICMP 报文也有限制。因此本文优先介绍命令调用方式。4. 完整实战Python 简易 IP 扫描工具下面开始逐步实现这个工具。我会先把代码拆成多个模块然后给出完整的运行方式。你可以直接复制完整代码运行也可以按章节顺序理解每个函数的作用。4.1 创建项目结构先创建项目目录和空文件。在命令行执行mkdir ip_scanner cd ip_scanner mkdir core touch core/__init__.py之后我们会在core目录下添加ping.py、port_scan.py、utils.py并在根目录编写scanner.py。4.2 编写主机存活探测模块文件路径core/ping.pyimport subprocess import sys def check_host(ip, timeout1): 通过调用系统 ping 命令检测主机是否存活。 :param ip: 目标 IP 地址 :param timeout: 超时时间秒 :return: True 表示主机存活False 表示不可达 if sys.platform.startswith(win): cmd [ping, -n, 1, -w, str(int(timeout * 1000)), ip] else: cmd [ping, -c, 1, -W, str(int(timeout)), ip] try: result subprocess.run( cmd, stdoutsubprocess.DEVNULL, stderrsubprocess.DEVNULL, timeouttimeout 2, ) return result.returncode 0 except subprocess.TimeoutExpired: return False这段代码的关键点有两个通过sys.platform.startswith(win)区分 Windows 和其他系统。使用subprocess.run()并设置stdout和stderr为DEVNULL避免每次 ping 的输出刷屏。4.3 编写端口扫描模块文件路径core/port_scan.pyimport socket # 常见端口列表可以按需增删 COMMON_PORTS [ 21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1433, 1521, 3306, 3389, 5432, 5900, 6379, 8080, 8443, 9200, 27017 ] def scan_port(ip, port, timeout1): 扫描单个端口。 :param ip: 目标 IP :param port: 目标端口 :param timeout: 连接超时时间 :return: True 表示端口开放False 表示关闭/过滤 sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) result sock.connect_ex((ip, port)) sock.close() return result 0 def scan_ports(ip, ports, timeout1, max_workers100): 并发扫描多个端口。 :param ip: 目标 IP :param ports: 端口列表 :param timeout: 超时时间 :param max_workers: 线程池最大并发数 :return: 开放端口列表 from concurrent.futures import ThreadPoolExecutor open_ports [] with ThreadPoolExecutor(max_workersmax_workers) as executor: futures {executor.submit(scan_port, ip, port, timeout): port for port in ports} for future in futures: if future.result(): open_ports.append(futures[future]) return sorted(open_ports)这里使用了ThreadPoolExecutor将每个端口扫描任务提交到线程池futures字典保存了任务与端口号之间的映射关系。这样最终可以拿到具体哪些端口开放。4.4 编写工具函数模块文件路径core/utils.pyimport ipaddress import csv def parse_targets(target_str): 解析用户输入的目标地址范围。 支持格式 - 192.168.1.1 单个 IP - 192.168.1.0/24 CIDR 网段 - 192.168.1.1-100 IP 段仅最后一段变化 - 192.168.1.1-192.168.1.50 起止 IP :param target_str: 用户输入字符串 :return: IP 地址字符串列表 target_str target_str.strip() if not target_str: return [] # 单个 IP try: ip ipaddress.ip_address(target_str) return [str(ip)] except ValueError: pass # CIDR 格式 if / in target_str: network ipaddress.ip_network(target_str, strictFalse) return [str(ip) for ip in network.hosts()] # 处理带横线的范围 if - in target_str: parts target_str.split(-) left, right parts[0].strip(), parts[1].strip() # 如果右侧只有数字表示最后一段范围例如 192.168.1.1-100 try: right_ip ipaddress.ip_address(right) except ValueError: if right.isdigit(): left_ip ipaddress.ip_address(left) prefix left.rsplit(., 1)[0] right_ip ipaddress.ip_address(f{prefix}.{int(right)}) else: raise ValueError(f无法解析目标范围: {target_str}) left_int int(ipaddress.ip_address(left)) right_int int(ipaddress.ip_address(right_ip)) if left_int right_int: raise ValueError(f起始 IP 不能大于结束 IP: {target_str}) return [str(ipaddress.ip_address(i)) for i in range(left_int, right_int 1)] raise ValueError(f无法解析目标地址: {target_str}) def export_csv(filename, header, rows): 将扫描结果导出为 CSV 文件。 :param filename: 文件路径 :param header: 表头列表 :param rows: 数据行列表 with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow(header) writer.writerows(rows)parse_targets函数覆盖了最常见的几种输入方式export_csv使用utf-8-sig编码是为了让 Excel 打开 CSV 文件时中文表头不乱码。4.5 编写主程序文件路径scanner.pyimport argparse import time from concurrent.futures import ThreadPoolExecutor from core.ping import check_host from core.port_scan import scan_ports, COMMON_PORTS from core.utils import parse_targets, export_csv def scan_host(ip, scan_ports_flag, timeout, port_list): 扫描单个主机的完整流程。 :param ip: 目标 IP :param scan_ports_flag: 是否进行端口扫描 :param timeout: 超时时间 :param port_list: 需要扫描的端口列表 :return: (ip, is_alive, open_ports) alive check_host(ip, timeout) open_ports [] if alive and scan_ports_flag: open_ports scan_ports(ip, port_list, timeout) return ip, alive, open_ports def main(): parser argparse.ArgumentParser(description基于 Python 的简易 IP 扫描工具) parser.add_argument(targets, help目标地址支持单 IP、CIDR、IP 段例如 192.168.1.0/24) parser.add_argument(--ports, help要扫描的端口逗号分隔默认常见端口) parser.add_argument(--all-ports, actionstore_true, help扫描 1-1024 全部端口慎用) parser.add_argument(--timeout, typefloat, default1.0, help连接超时时间默认 1 秒) parser.add_argument(--workers, typeint, default100, help并发线程数默认 100) parser.add_argument(--port-scan, actionstore_true, help启用端口扫描) parser.add_argument(--output, help结果导出到 CSV 文件路径) args parser.parse_args() print([*] 开始解析目标地址...) ip_list parse_targets(args.targets) print(f[*] 共解析到 {len(ip_list)} 个 IP 地址) if args.all_ports: port_list list(range(1, 1025)) elif args.ports: port_list [int(p.strip()) for p in args.ports.split(,) if p.strip()] else: port_list COMMON_PORTS print([*] 开始主机存活探测...) start_time time.time() results [] with ThreadPoolExecutor(max_workersargs.workers) as executor: futures [] for ip in ip_list: futures.append(executor.submit(scan_host, ip, args.port_scan, args.timeout, port_list)) for future in futures: ip, alive, open_ports future.result() results.append((ip, alive, open_ports)) status 存活 if alive else 离线 print(f {ip} - {status} (f开放端口: {open_ports} if open_ports else )) elapsed time.time() - start_time print(f\n[*] 扫描完成耗时 {elapsed:.2f} 秒在线主机 {sum(1 for r in results if r[1])} 台) if args.output: rows [] for ip, alive, open_ports in results: rows.append([ip, 存活 if alive else 离线, ,.join(map(str, open_ports))]) export_csv(args.output, [IP, 状态, 开放端口], rows) print(f[*] 结果已导出到 {args.output}) if __name__ __main__: main()主程序的逻辑并不复杂解析命令行参数。调用parse_targets()展开 IP 列表。使用线程池并发执行scan_host()函数每个 IP 先做主机存活探测存活且启用端口扫描时再扫端口。收集结果并输出按需保存 CSV。4.6 完整命令行示例假设你想扫描本机所在的192.168.1.0/24网段并检查常见端口的开放情况同时把结果保存到result.csv可以执行python scanner.py 192.168.1.0/24 --port-scan --output result.csv如果只想扫描单个 IP 的指定端口python scanner.py 192.168.1.100 --port-scan --ports 22,80,443,3306 --timeout 0.5需要提前说明的是不要在未授权环境下对公网或他人内网执行上述命令本文示例仅用于实验环境。4.7 预期运行结果在本地网段运行后终端输出大致如下[*] 开始解析目标地址... [*] 共解析到 254 个 IP 地址 [*] 开始主机存活探测... 192.168.1.1 - 存活 192.168.1.2 - 离线 192.168.1.10 - 存活开放端口: [22, 80, 443] ... [*] 扫描完成耗时 25.36 秒在线主机 6 台 [*] 结果已导出到 result.csv这里的时间取决于网段大小、超时时间和线程数。在有线内网环境中扫描一个 /24 网段常见端口通常需要几十秒到几分钟。如果你希望更快可以把超时调低例如--timeout 0.5但要注意误报率可能上升。5. 常见问题与排查思路5.1 为什么有些在线主机显示离线很多网络设备为了减少广播风暴和安全风险会默认禁用 ICMP 响应。也就是说主机本身在线并且服务正常但不会回复 ping 请求。此时工具会将其判断为离线。解决办法是把扫描目标从“ICMP 存活探测”切换到“TCP 端口探测”只要主机有一个端口开放就能确认在线。你可以在代码中增加一个参数当未检测到 ICMP 存活时也继续尝试端口扫描。5.2 端口扫描结果不准端口扫描结果受网络环境影响较大。如果目标在防火墙后面防火墙可能直接丢弃 SYN 包导致连接超时也可能对未开放端口返回 RST区别表现为扫描缓慢或快速失败。遇到类似情况时先确认目标主机是否开放了该端口例如在目标机上执行netstat -an | grep 8080。调整超时时间例如从 1.0 秒改成 2.0 秒排除网络延迟导致的漏报。确认你选择的端口没有被杀毒软件或主机防火墙拦截。5.3 线程数调到很大反而更慢线程不是越多越好。当线程数超过系统能支持的有效并发连接数时大量线程会争抢 CPU 和网络资源连接建立速度反而下降。建议--workers设置在 50 到 200 之间。另外你还可以考虑把每个 IP 的端口扫描并发和多个 IP 的扫描并发解耦避免同时开启太多连接。5.4 CSV 文件在 Excel 中打开中文乱码导出模块使用encodingutf-8-sig已经考虑了这个问题。如果你仍然看到乱码可能是因为旧版本 Excel 不支持 UTF-8 带 BOM 的 CSV可以改为GBK编码但会牺牲跨平台兼容性。建议保留当前实现。5.5 Windows 下 ping 命令一闪而过代码中已经将 stdout 和 stderr 重定向到 DEVNULL所以不会弹出命令行窗口。如果只是简单测试check_host()函数注意subprocess.run()的timeout参数Windows 系统下 ping 进程偶尔不会因为网络环境立刻退出加上超时保护是必要的。5.6 扫描到一半程序报错“线程池无法创建新线程”通常是因为线程数设置过大或是操作系统达到进程线程数上限。解决办法是调低--workers或者关闭其他占用线程资源的程序。如果在 Linux 下遇到此问题也可以检查ulimit -u限制。为了方便排查下面整理一个速查表问题现象常见原因解决思路在线主机被判定离线主机禁用 ICMP 响应改用 TCP 端口探测确认存活端口扫描漏报超时时间过短调大--timeout重试扫描速度异常慢线程数过多/过少调整并发数默认 100CSV 中文乱码编码不一致使用utf-8-sig编码导出程序报线程池异常系统线程资源耗尽降低 worker 数量6. 工程化与安全边界建议写一个能跑的脚本比较简单但要让它适合真实场景使用下面这些工程化建议值得关注。6.1 授权边界这是最重要的一条。IP 扫描工具本质上是网络探测工具使用它的前提是获得目标网络所有者的明确授权。你可以用它管理自己公司的办公网、家庭路由器下的设备也可以用于 CTF 比赛或授权渗透测试中的信息收集。不要在未授权情况下对任何公网网段或第三方内网发起扫描这可能违反所在地区的法律法规。工程上应该在你的工具 README 中明确写出“仅限授权环境使用”。6.2 扫描速率控制大规模扫描会给目标网络带来多余的流量也可能触发 IDS/IPS 告警。如果用于企业内部网络建议控制线程数和时间间隔避免被误判为攻击行为。可以增加一个--delay参数在两次探测之间加入少量延迟。虽然扫描速度有所下降但会更温和。6.3 错误处理与日志当前代码使用subprocess.run()和socket.connect_ex()已经避免了大量异常抛出。但真实网络中会出现非常多的偶然情况比如 DNS 解析失败、socket 超时、权限受限等。工程化实践中应该为关键函数添加 try-except记录异常但不中断整体任务。使用logging模块输出级别不同的日志例如 DEBUG 记录每个端口扫描详情INFO 记录扫描进度ERROR 记录异常。不要把扫描结果直接放在终端输出建议同时输出到文件后续分析更方便。6.4 结果去重与排序当扫描多个重叠网段时同一 IP 可能出现多次。工程实践中可以用set对 IP 列表去重后再扫描减少重复任务。端口列表也可以去重排序保证结果稳定可复盘。6.5 性能优化方向当前工具的性能瓶颈主要在以下三处ICMP 存活探测每次都启动独立进程进程开销较高。TCP 扫描对每个端口创建一个 socket端口多时对象创建开销不可忽略。多线程调度本身有上下文切换成本。如果想继续深入优化可以考虑用异步 IO 方式实现端口探测使用asyncio和await代替线程池。它的资源占用会更低能够同时维护更多的连接状态。另外可以按照端口分散度对任务进行分组让同一主机上的多个端口连续探测减少 socket 重复创建。6.6 代码可维护性为生产使用维护这个项目时建议使用类型注解提升代码可读性为每个公共函数补充 docstring。同时把扫描参数抽象成配置类或配置文件避免命令行参数过多导致命令执行困难。如果团队内分享工具可以考虑打包成 CLI 命令而不是每次执行python scanner.py。7. 总结与下一步学习方向本文完成了一个基于 Python 的简易 IP 扫描工具核心能力包括解析用户输入的 IP 地址、CIDR 网段和 IP 起止范围。使用系统 ping 命令探测主机存活。使用 socket 的connect_ex()方法完成 TCP 端口扫描。借助ThreadPoolExecutor并发扫描提高效率。支持常见端口列表、自定义端口、全端口扫描模式。结果可输出到终端或导出为 CSV 文件。顺着这个项目继续往下走你可以尝试几个明显的扩展方向增加端口服务识别能力用 socket 连接后发送简单探测数据比如 HTTP 请求根据响应判断端口上跑的是什么服务。增加 UDP 端口探测思路虽然是另一种机制但能丰富工具能力。使用asyncio重构扫描器解决线程数量受限问题。增加图形界面使用 PyQt 或 Web 页面展示扫描结果。把扫描结果入库方便多轮现场对比和增量分析。如果你是在学习 Python 网络编程这个项目也能帮助你理解 socket 编程、多线程调度、外部进程调用、地址解析等核心概念。建议不要只复制代码而是自己重新写一遍并在实验网段中修改不同参数观察效果。遇到端口扫描超时、主机误判这些情况时停下来想一想背后的网络行为这比单纯多跑几次脚本更有收获。动手写一个属于你自己的扫描工具跑通第一轮扫描之后再逐步加入新的功能相信你对 TCP/IP 的理解会比只看文档深刻不少。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑