网段存活主机扫描工具
scan_alive_network.py
#!/usr/bin/env python3
"""
网段存活主机扫描工具
用法:
python scan_alive_network.py [网段] [-t 并发数] [-w 超时秒数]
示例:
python scan_alive_network.py # 默认扫描 192.168.0.0/24
python scan_alive_network.py 192.168.1.0/24 # 指定网段扫描
python scan_alive_network.py -t 50 -w 0.5 # 50并发、0.5秒超时
"""
import argparse
import asyncio
import ipaddress
import os
import sys
import time
from typing import List, Optional
from icmplib import async_ping
# 单个网段允许扫描的地址数上限(分配协程前拦截,防超大网段 OOM),可按需调整(500000 ~ 800 MB)
MAX_HOSTS = 100000
# 单次探测超时上限(秒),防止 -w 误填超大值导致扫描长时间挂起
MAX_TIMEOUT = 30.0
# 并发数上限,防止一次性创建过多协程
MAX_WORKERS = 1000
# 非交互终端(输出重定向/管道)时禁用颜色和进度条,避免 ANSI 码与覆盖行污染文件
_INTERACTIVE = sys.stdout.isatty()
_USE_COLOR = _INTERACTIVE and os.environ.get("NO_COLOR") is None
# 统一显示风格:错误=红、中断/警告=黄、信息=蓝、成功=绿
class Colors:
"""ANSI 颜色代码(非交互或设 NO_COLOR 时自动清空)。"""
BLUE = "\033[94m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
RESET = "\033[0m"
BOLD = "\033[1m"
if not _USE_COLOR:
Colors.BLUE = Colors.GREEN = Colors.YELLOW = Colors.RED = Colors.RESET = (
Colors.BOLD
) = ""
# ---------- 终端支持函数 ----------
def enable_windows_ansi() -> None:
"""Windows 下启用终端 ANSI 转义序列(仅交互式终端需要,避免闪出 cmd 窗口)。"""
if sys.platform == "win32" and _INTERACTIVE:
import ctypes
kernel32 = ctypes.windll.kernel32
# STD_OUTPUT_HANDLE;ENABLE_PROCESSED_OUTPUT|ENABLE_WRAP_AT_EOL_OUTPUT|ENABLE_VIRTUAL_TERMINAL_PROCESSING
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
# ---------- 基础扫描函数 ----------
async def ping_host(ip: str, timeout: float) -> bool:
"""对单个 IP 发起一次 ping,返回是否存活。"""
try:
# privileged=None 由 icmplib 自动探测,避免非 root/管理员下全部误判离线
host = await async_ping(ip, count=1, timeout=timeout, privileged=None)
return host.is_alive
except Exception:
# 单主机超时/无权限/拒绝响应都是常态,统一按离线处理
return False
# ---------- 进度条函数 ----------
def print_progress(completed: int, total: int, alive_count: int) -> None:
"""打印进度条(覆盖行);非交互终端下静默。"""
if not _INTERACTIVE:
return
percent = completed / total * 100
bar_len = 30
filled = int(bar_len * completed / total)
bar = "█" * filled + "░" * (bar_len - filled)
sys.stdout.write(
f"\r扫描进度: [{bar}] {percent:>5.1f}% ({completed}/{total}) 已发现存活: {alive_count}"
)
sys.stdout.flush()
# ---------- 输出辅助函数 ----------
def print_alive_list(alive_list: List[str]) -> None:
"""按行优先、每行最多 4 个对齐打印 IP 列表。"""
if not alive_list:
return
max_len = max(len(ip) for ip in alive_list) + 2 # 加两个空格间隔
cols = 4
for i in range(0, len(alive_list), cols):
chunk = alive_list[i : i + cols]
print("".join(f"{Colors.GREEN}{ip:<{max_len}}{Colors.RESET}" for ip in chunk))
# ---------- 核心扫描逻辑 ----------
async def scan_network(
network: str, max_workers: int = 100, timeout: float = 1.0
) -> List[str]:
"""
异步并发扫描网段所有存活主机。
:param network: CIDR 格式网段
:param max_workers: 最大并发数(默认 100)
:param timeout: 单次探测超时
:return: 排序后的存活 IP 列表
:raises ValueError: 网段格式非法或地址数超过上限
"""
# 防御性二次校验:即使绕过了启动前校验直接调用本函数,也能安全拦截
try:
net = ipaddress.ip_network(network, strict=False)
except ValueError as e:
raise ValueError(f"无效的网段格式:{e}")
# 在构建 hosts 列表之前按地址数上限拦截,避免超大网段分配内存即崩溃
if net.num_addresses > MAX_HOSTS:
raise ValueError(
f"网段地址数 {net.num_addresses} 超过上限 {MAX_HOSTS},请缩小网段范围"
)
hosts = [str(ip) for ip in net.hosts()]
total = len(hosts)
print(
f"{Colors.BLUE}[扫描配置] 网段:{network} | 主机总数:{total} | "
f"并发数:{max_workers} | 超时:{timeout}s{Colors.RESET}"
)
alive_hosts = []
semaphore = asyncio.Semaphore(max_workers)
async def worker(ip: str):
async with semaphore:
if await ping_host(ip, timeout):
alive_hosts.append(ip)
tasks = [asyncio.create_task(worker(ip)) for ip in hosts]
completed = 0
for future in asyncio.as_completed(tasks):
await future
completed += 1
if total > 0:
print_progress(completed, total, len(alive_hosts))
if _INTERACTIVE:
sys.stdout.write("\n")
alive_hosts.sort(key=lambda x: ipaddress.ip_address(x))
return alive_hosts
# ---------- 参数校验函数 ----------
def validate_scan_params(network: str, threads: int, timeout: float) -> Optional[str]:
"""校验扫描参数(网络合法性、主机数是否过大、超时设置、并发设置等)。
在命令行扫描启动前调用,全部通过才允许进入扫描流程;
任一检查不通过则返回可直接展示给用户的错误描述,由调用方决定终止方式。
注意:并发数/超时的数字转换由 argparse 的 type=int/type=float 完成,
本函数只负责取值范围与网段相关的校验。
返回 None 表示全部通过;否则返回错误信息字符串。
"""
# 1) 网络合法性:网段必须是合法的 CIDR 格式
try:
net = ipaddress.ip_network(network, strict=False)
except ValueError as e:
return f"无效的网段格式:{e}"
# 2) 主机数是否过大:在构建 hosts 列表之前按地址数上限拦截,避免 /16 以上网段分配内存即崩溃
if net.num_addresses > MAX_HOSTS:
return f"网段地址数 {net.num_addresses} 超过上限 {MAX_HOSTS},请缩小网段范围"
# 3) 并发设置:至少 1,且不超过上限
if threads < 1:
return "并发数必须 >= 1"
if threads > MAX_WORKERS:
return f"并发数过大(上限 {MAX_WORKERS})"
# 4) 超时设置:必须大于 0,且不超过上限(防止误填超大值导致扫描长时间挂起)
if timeout <= 0:
return "超时时间必须 > 0"
if timeout > MAX_TIMEOUT:
return f"超时时间过大(上限 {MAX_TIMEOUT:.0f} 秒)"
return None
# ---------- 主入口 ----------
def main() -> None:
enable_windows_ansi()
parser = argparse.ArgumentParser(description="网段存活主机扫描工具")
parser.add_argument(
"network",
nargs="?",
default="192.168.0.0/24",
help="目标网段(CIDR格式,默认:192.168.0.0/24)",
)
parser.add_argument(
"-t", "--threads", type=int, default=100, help="并发数(默认100)"
)
parser.add_argument(
"-w",
"--timeout",
type=float,
default=1.0,
help="单次探测超时时间(秒,默认1.0)",
)
args = parser.parse_args()
# 扫描启动前统一校验:网络合法性、主机数上限、超时/并发设置等
err = validate_scan_params(args.network, args.threads, args.timeout)
if err is not None:
# 校验不通过:按 argparse 惯例输出 usage 并以退出码 2 终止,不进入扫描
parser.error(err)
print(f"{Colors.BOLD}{Colors.BLUE}{'=' * 50}{Colors.RESET}")
print(f"{Colors.BOLD}{Colors.BLUE} 网段存活主机扫描工具{Colors.RESET}")
print(f"{Colors.BOLD}{Colors.BLUE}{'=' * 50}{Colors.RESET}")
start_time = time.time()
try:
alive_list = asyncio.run(scan_network(args.network, args.threads, args.timeout))
except ValueError as e:
print(f"{Colors.RED}[错误] {e}{Colors.RESET}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
# 先换行再输出,避免中断消息黏在进度条后面
print(f"\n{Colors.YELLOW}[中断] 扫描已取消{Colors.RESET}", file=sys.stderr)
sys.exit(130)
cost_time = time.time() - start_time
print(f"\n{Colors.BLUE}{'=' * 50}{Colors.RESET}")
print(
f"{Colors.BLUE}[扫描完成] 总耗时:{cost_time:.2f} 秒 | "
f"存活主机总数:{len(alive_list)} 台{Colors.RESET}"
)
print(f"{Colors.BLUE}{'=' * 50}{Colors.RESET}")
if alive_list:
print(f"{Colors.GREEN}[扫描完成] 在线IP列表:{Colors.RESET}")
print_alive_list(alive_list)
else:
print(f"{Colors.YELLOW}[扫描完成] 未探测到任何存活主机{Colors.RESET}")
if __name__ == "__main__":
main()
scan_alive_network_gui.py
import asyncio
import ipaddress
import time
import tkinter as tk
from queue import Empty, Queue
from threading import Event, Thread
from tkinter import scrolledtext, ttk
from typing import Callable, List, Optional
from icmplib import async_ping
# 单个网段允许扫描的地址数上限(分配协程前拦截,防超大网段 OOM),可按需调整(500000 ~ 800 MB)
MAX_HOSTS = 100000
# 单次探测超时上限(秒),防止误填超大值导致扫描长时间挂起
MAX_TIMEOUT = 30.0
# 并发数上限,防止一次性创建过多协程
MAX_WORKERS = 1000
# ---------- 基础扫描函数 ----------
async def ping_host(ip: str, timeout: float) -> bool:
"""对单个 IP 发起一次 ping,返回是否存活。"""
try:
# privileged=None 由 icmplib 自动探测,避免非 root/管理员下全部误判离线
host = await async_ping(ip, count=1, timeout=timeout, privileged=None)
return host.is_alive
except Exception:
# 单主机超时/无权限/拒绝响应都是常态,统一按离线处理
return False
# ---------- 核心扫描逻辑 ----------
async def scan_network(
network: str,
max_workers: int,
timeout: float,
on_alive: Callable[[str], None],
on_progress: Callable[[int, int], None],
stop_event: Event,
) -> List[str]:
"""在独立事件循环中并发扫描整个网段,返回按 IP 排序的存活主机列表。"""
try:
net = ipaddress.ip_network(network, strict=False)
except ValueError as e:
raise ValueError(f"无效网段格式: {e}")
# 在构建 hosts 列表之前按地址数上限拦截,避免 /16 以上网段分配内存即崩溃
if net.num_addresses > MAX_HOSTS:
raise ValueError(
f"网段地址数 {net.num_addresses} 超过上限 {MAX_HOSTS},请缩小网段范围"
)
hosts = [str(ip) for ip in net.hosts()]
total = len(hosts)
alive_hosts = []
semaphore = asyncio.Semaphore(max_workers)
async def worker(ip: str):
async with semaphore:
# 信号量内再次检查,确保终止后已排队的任务不再发请求
if stop_event.is_set():
return
if await ping_host(ip, timeout):
alive_hosts.append(ip)
on_alive(ip)
tasks = [asyncio.create_task(worker(ip)) for ip in hosts]
completed = 0
for future in asyncio.as_completed(tasks):
await future
completed += 1
on_progress(completed, total)
if stop_event.is_set():
for task in tasks:
if not task.done():
task.cancel()
break
await asyncio.gather(*tasks, return_exceptions=True)
alive_hosts.sort(key=lambda x: ipaddress.ip_address(x))
return alive_hosts
# ---------- 线程入口函数 ----------
def scan_network_thread(
network: str,
max_workers: int,
timeout: float,
on_alive: Callable[[str], None],
on_progress: Callable[[int, int], None],
on_finished: Callable[[List[str], float, bool, Optional[str]], None],
stop_event: Event,
) -> None:
"""后台线程入口:创建独立事件循环执行扫描,结束后回调 on_finished(alive, elapsed, terminated, error)。"""
async def run():
start = time.time()
try:
alive = await scan_network(
network, max_workers, timeout, on_alive, on_progress, stop_event
)
elapsed = time.time() - start
terminated = stop_event.is_set()
on_finished(alive, elapsed, terminated, None)
except Exception as e:
on_finished([], 0, False, str(e))
asyncio.run(run())
# ---------- 参数校验函数 ----------
def validate_scan_params(
network: str,
max_workers_raw,
timeout_raw,
) -> tuple:
"""校验扫描参数(网络合法性、主机数是否过大、超时设置、并发设置等)。
在点击“开始扫描”后调用,全部通过才允许启动扫描线程;
任一检查不通过则返回错误信息,由调用方直接展示给用户。
返回 (ok, network, max_workers, timeout, error):
- ok=True 时,network / max_workers / timeout 为规范化后的可用值,error 为空字符串;
- ok=False 时,error 为可直接展示给用户的错误描述,其余字段无效。
"""
network = (network or "").strip()
# 1) 网络合法性:网段必须是合法的 CIDR 格式
try:
net = ipaddress.ip_network(network, strict=False)
except ValueError as e:
return False, "", 0, 0.0, f"无效网段格式: {e}"
# 2) 主机数是否过大:在构建 hosts 列表之前按地址数上限拦截,避免 /16 以上网段分配内存即崩溃
if net.num_addresses > MAX_HOSTS:
return (
False,
"",
0,
0.0,
f"网段地址数 {net.num_addresses} 超过上限 {MAX_HOSTS},请缩小网段范围",
)
# 3) 并发数 / 超时必须能解析为数字
try:
max_workers = int(max_workers_raw)
timeout = float(timeout_raw)
except (ValueError, TypeError):
return False, "", 0, 0.0, "并发数和超时必须是数字"
# 4) 取值下限:并发数至少 1,超时必须大于 0
if max_workers < 1 or timeout <= 0:
return False, "", 0, 0.0, "并发数需>=1,超时需>0"
# 5) 超时上限:防止误填超大值导致扫描长时间挂起
if timeout > MAX_TIMEOUT:
return False, "", 0, 0.0, f"超时时间过大(上限 {MAX_TIMEOUT:.0f} 秒)"
# 6) 并发上限:防止一次性创建过多协程
if max_workers > MAX_WORKERS:
return False, "", 0, 0.0, f"并发数过大(上限 {MAX_WORKERS})"
return True, network, max_workers, timeout, ""
# ---------- GUI 部分 ----------
class ScanApp:
def __init__(self, root):
self.root = root
root.title("网段存活主机扫描工具-v1.0")
root.geometry("720x580")
root.resizable(True, True)
# 变量
self.network_var = tk.StringVar(value="192.168.0.0/24")
# 用 StringVar 而非 IntVar/DoubleVar:IntVar/DoubleVar 在 .get() 时会先尝试
# 把输入转成数字,遇到非法字符(如 "c")会直接抛 TclError,导致无法进入
# validate_scan_params 的“必须是数字”检查;StringVar 只返回原始字符串,
# 数字解析统一交由 validate_scan_params 兜底处理
self.threads_var = tk.StringVar(value="100")
self.timeout_var = tk.StringVar(value="1.0")
self.scanning = False
self.stop_event = Event()
# 输入区域
frame_input = ttk.LabelFrame(root, text="扫描参数", padding=5)
frame_input.pack(fill=tk.X, padx=10, pady=5)
# 网段
ttk.Label(frame_input, text="网段 (CIDR):").grid(
row=0, column=0, sticky=tk.W, padx=5
)
entry_network = ttk.Entry(frame_input, textvariable=self.network_var, width=18)
entry_network.grid(row=0, column=1, padx=5, pady=2, sticky=tk.W)
# 并发数
ttk.Label(frame_input, text="并发数:").grid(
row=0, column=2, sticky=tk.W, padx=5
)
entry_threads = ttk.Entry(frame_input, textvariable=self.threads_var, width=8)
entry_threads.grid(row=0, column=3, padx=5, pady=2, sticky=tk.W)
# 超时
ttk.Label(frame_input, text="超时 (秒):").grid(
row=0, column=4, sticky=tk.W, padx=5
)
entry_timeout = ttk.Entry(frame_input, textvariable=self.timeout_var, width=8)
entry_timeout.grid(row=0, column=5, padx=5, pady=2, sticky=tk.W)
# 扫描
self.scan_btn = ttk.Button(
frame_input, text="开始扫描", command=self.start_scan
)
self.scan_btn.grid(row=0, column=6, padx=5)
self.stop_btn = ttk.Button(
frame_input, text="终止扫描", command=self.stop_scan, state=tk.DISABLED
)
self.stop_btn.grid(row=0, column=7, padx=5)
# 进度条
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(
root, variable=self.progress_var, maximum=100
)
self.progress_bar.pack(fill=tk.X, padx=10, pady=5)
self.status_label = ttk.Label(root, text="就绪")
self.status_label.pack(anchor=tk.W, padx=10)
# 结果显示
frame_result = ttk.LabelFrame(root, text="在线主机列表", padding=5)
frame_result.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
self.result_text = scrolledtext.ScrolledText(
frame_result, wrap=tk.WORD, font=("Consolas", 10), state=tk.DISABLED
)
self.result_text.pack(fill=tk.BOTH, expand=True)
# 复制按钮
btn_copy = ttk.Button(root, text="复制结果", command=self.copy_results)
btn_copy.pack(anchor=tk.E, padx=10, pady=5)
self.queue = Queue()
self._poll_queue()
def _poll_queue(self):
try:
while True:
msg = self.queue.get_nowait()
if msg[0] == "alive":
self._add_alive(msg[1])
elif msg[0] == "progress":
self._update_progress(msg[1], msg[2])
elif msg[0] == "finished":
self._scan_finished(
msg[1], msg[2], msg[3] if len(msg) > 3 else False
)
elif msg[0] == "error":
self._show_error(msg[1])
except Empty:
pass
self.root.after(100, self._poll_queue)
def _add_alive(self, ip):
self.result_text.config(state=tk.NORMAL)
self.result_text.insert(tk.END, f"{ip}\n")
self.result_text.see(tk.END)
self.result_text.config(state=tk.DISABLED)
def _update_progress(self, completed, total):
if total > 0:
pct = (completed / total) * 100
self.progress_var.set(pct)
self.status_label.config(text=f"扫描中... {completed}/{total}")
def _scan_finished(self, alive_list, elapsed, terminated=False, error=None):
self.scanning = False
self.scan_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
# 仅正常完成时进度到 100%,终止/出错时保留实际进度,避免误导
if not terminated and not error:
self.progress_var.set(100)
if error:
self.status_label.config(text=f"扫描出错: {error}")
elif terminated:
self.status_label.config(
text=f"扫描已终止,已发现 {len(alive_list)} 台,耗时 {elapsed:.2f} 秒"
)
else:
self.status_label.config(
text=f"扫描完成,存活 {len(alive_list)} 台,耗时 {elapsed:.2f} 秒"
)
def _show_error(self, err):
self.status_label.config(text=f"错误: {err}")
self.scanning = False
self.scan_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
def copy_results(self):
content = self.result_text.get(1.0, tk.END).strip()
if not content:
self.status_label.config(text="没有可复制的内容")
return
self.root.clipboard_clear()
self.root.clipboard_append(content)
self.status_label.config(
text=f"已复制 {len(content.splitlines())} 行 IP 地址到剪贴板"
)
def stop_scan(self):
if self.scanning:
self.stop_event.set()
self.stop_btn.config(state=tk.DISABLED)
self.status_label.config(text="正在终止扫描...")
def start_scan(self):
if self.scanning:
return
# 点击“开始扫描”后统一校验:网络合法性、主机数上限、超时/并发设置等
ok, network, max_workers, timeout, error = validate_scan_params(
self.network_var.get(),
self.threads_var.get(),
self.timeout_var.get(),
)
if not ok:
# 校验不通过:提示错误,不启动后台线程
self._show_error(error)
return
# 清空旧结果
self.result_text.config(state=tk.NORMAL)
self.result_text.delete(1.0, tk.END)
self.result_text.config(state=tk.DISABLED)
self.progress_var.set(0)
self.status_label.config(text="正在扫描...")
self.scan_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
self.scanning = True
self.stop_event.clear()
def on_alive(ip):
self.queue.put(("alive", ip))
def on_progress(completed, total):
self.queue.put(("progress", completed, total))
def on_finished(alive, elapsed, terminated, error=None):
if error:
self.queue.put(("error", error))
else:
self.queue.put(("finished", alive, elapsed, terminated))
thread = Thread(
target=scan_network_thread,
args=(
network,
max_workers,
timeout,
on_alive,
on_progress,
on_finished,
self.stop_event,
),
daemon=True,
)
thread.start()
# ---------- 主入口 ----------
def main():
root = tk.Tk()
app = ScanApp(root)
root.mainloop()
if __name__ == "__main__":
main()
作者:wanghongwei
版权声明:本作品遵循<CC BY-NC-ND 4.0>版权协议,商业转载请联系作者获得授权,非商业转载请附上原文出处链接及本声明。

浙公网安备 33010602011771号