ネットワークスキャナー
LAN内のデバイスをスキャンしてIPアドレス・ホスト名・開放ポートを一覧表示するツール。socketとsubprocessの活用を学びます。
1. アプリ概要
LAN内のデバイスをスキャンしてIPアドレス・ホスト名・開放ポートを一覧表示するツール。socketとsubprocessの活用を学びます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★☆ です。
このアプリは「ネットワーク」カテゴリです。ネットワーク連携は Python の代表的な活用領域で、I/O 待ちと UI 更新の関係や非同期処理の考え方が他の Web API 連携にも直接活きます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。
コードを読む前に実行することをおすすめします。動いている挙動を先に把握しておくと、解説で出てくる関数や処理がどこに対応するかが頭に入りやすくなります。
応用のヒントは、機能を 1 つ増やす・見た目を整える・例外を一つでも丁寧に扱う、のいずれかから始めるのがおすすめです。
2. 機能一覧
- ループで生成するボタン群(それぞれ
_ping_single()・_resolve_host()・_traceroute()を実行) - 「▶ スキャン開始」ボタン(
_toggle_scan())で操作 ttk.Treeviewによる表形式の一覧表示messagebox.showerror()・messagebox.showinfo()によるダイアログ通知- ウィンドウはタイトル「ネットワークスキャナー」・サイズ 820x580 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は標準ライブラリの範囲で動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox
import socket
import subprocess
import threading
import ipaddress
import platform
import re
from datetime import datetime
class App13:
"""ネットワークスキャナー"""
def __init__(self, root):
self.root = root
self.root.title("ネットワークスキャナー")
self.root.geometry("820x580")
self.root.configure(bg="#f8f9fc")
self.scanning = False
self._build_ui()
self._detect_local_ip()
def _build_ui(self):
title_frame = tk.Frame(self.root, bg="#e65100", pady=10)
title_frame.pack(fill=tk.X)
tk.Label(title_frame, text="🔍 ネットワークスキャナー",
font=("Noto Sans JP", 15, "bold"),
bg="#e65100", fg="white").pack(side=tk.LEFT, padx=12)
# スキャン設定
cfg_frame = ttk.LabelFrame(self.root, text="スキャン設定", padding=10)
cfg_frame.pack(fill=tk.X, padx=8, pady=6)
tk.Label(cfg_frame, text="IPレンジ:").grid(row=0, column=0, sticky="w")
self.range_var = tk.StringVar(value="192.168.1.0/24")
ttk.Entry(cfg_frame, textvariable=self.range_var,
width=22, font=("Courier New", 11)).grid(
row=0, column=1, padx=8, sticky="w")
tk.Label(cfg_frame, text="タイムアウト(ms):").grid(
row=0, column=2, sticky="w", padx=(16, 4))
self.timeout_var = tk.IntVar(value=300)
ttk.Spinbox(cfg_frame, from_=50, to=2000,
textvariable=self.timeout_var, width=6).grid(row=0, column=3)
tk.Label(cfg_frame, text="スレッド数:").grid(
row=0, column=4, sticky="w", padx=(16, 4))
self.threads_var = tk.IntVar(value=50)
ttk.Spinbox(cfg_frame, from_=1, to=200,
textvariable=self.threads_var, width=6).grid(row=0, column=5)
self.port_var = tk.BooleanVar(value=False)
ttk.Checkbutton(cfg_frame, text="ポートスキャン",
variable=self.port_var).grid(row=0, column=6, padx=16)
self.scan_btn = ttk.Button(cfg_frame, text="▶ スキャン開始",
command=self._toggle_scan)
self.scan_btn.grid(row=0, column=7, padx=8)
# プログレス
self.progress_var = tk.IntVar(value=0)
self.progress = ttk.Progressbar(self.root, variable=self.progress_var,
maximum=100)
self.progress.pack(fill=tk.X, padx=8)
# 結果テーブル
result_frame = ttk.LabelFrame(self.root, text="スキャン結果", padding=4)
result_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=6)
cols = ("ip", "hostname", "ping", "ports", "status")
self.tree = ttk.Treeview(result_frame, columns=cols,
show="headings", selectmode="browse")
for c, h, w in [("ip", "IPアドレス", 130), ("hostname", "ホスト名", 200),
("ping", "応答(ms)", 80), ("ports", "開放ポート", 200),
("status", "状態", 70)]:
self.tree.heading(c, text=h)
self.tree.column(c, width=w, minwidth=40)
v_sb = ttk.Scrollbar(result_frame, command=self.tree.yview)
self.tree.configure(yscrollcommand=v_sb.set)
v_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.pack(fill=tk.BOTH, expand=True)
self.tree.tag_configure("online", foreground="#27ae60")
self.tree.tag_configure("offline", foreground="#bbb")
# 下部: ローカル情報 + ツール
bottom = tk.Frame(self.root, bg="#f8f9fc")
bottom.pack(fill=tk.X, padx=8, pady=4)
self.local_info_var = tk.StringVar(value="")
tk.Label(bottom, textvariable=self.local_info_var,
bg="#f8f9fc", font=("Courier New", 10),
fg="#555").pack(side=tk.LEFT)
tool_frame = tk.Frame(bottom, bg="#f8f9fc")
tool_frame.pack(side=tk.RIGHT)
tk.Label(tool_frame, text="ツール: ", bg="#f8f9fc").pack(side=tk.LEFT)
self.tool_ip_var = tk.StringVar()
ttk.Entry(tool_frame, textvariable=self.tool_ip_var, width=16).pack(side=tk.LEFT, padx=4)
for text, cmd in [("Ping", self._ping_single),
("ホスト名解決", self._resolve_host),
("Traceroute", self._traceroute)]:
ttk.Button(tool_frame, text=text, command=cmd).pack(side=tk.LEFT, padx=2)
self.status_var = tk.StringVar(value="スキャン待機中")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
def _detect_local_ip(self):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
s.close()
# デフォルトレンジをセット
parts = local_ip.rsplit(".", 1)
self.range_var.set(f"{parts[0]}.0/24")
hostname = socket.gethostname()
self.local_info_var.set(
f"ホスト: {hostname} ローカルIP: {local_ip}")
self.tool_ip_var.set(local_ip)
except Exception:
pass
def _toggle_scan(self):
if self.scanning:
self.scanning = False
self.scan_btn.config(text="▶ スキャン開始")
else:
self._start_scan()
def _start_scan(self):
try:
network = ipaddress.ip_network(self.range_var.get().strip(),
strict=False)
except ValueError as e:
messagebox.showerror("エラー", f"無効なIPレンジ: {e}")
return
self.tree.delete(*self.tree.get_children())
self.scanning = True
self.scan_btn.config(text="⏹ 停止")
hosts = list(network.hosts())
self.progress_var.set(0)
threading.Thread(target=self._scan_range,
args=(hosts,), daemon=True).start()
def _scan_range(self, hosts):
total = len(hosts)
found = 0
semaphore = threading.Semaphore(self.threads_var.get())
results = []
lock = threading.Lock()
def scan_host(ip):
nonlocal found
with semaphore:
if not self.scanning:
return
ip_str = str(ip)
online, ms = self._ping(ip_str)
if online:
hostname = self._get_hostname(ip_str)
ports = self._scan_ports(ip_str) if self.port_var.get() else ""
with lock:
found += 1
results.append((ip_str, hostname, ms, ports))
self.root.after(0, self._add_result,
ip_str, hostname, ms, ports)
threads = []
for i, ip in enumerate(hosts):
if not self.scanning:
break
t = threading.Thread(target=scan_host, args=(ip,), daemon=True)
t.start()
threads.append(t)
pct = int((i + 1) / total * 100)
self.root.after(0, self.progress_var.set, pct)
self.root.after(0, self.status_var.set,
f"スキャン中: {i+1}/{total} 発見: {found}")
for t in threads:
t.join(timeout=2)
self.root.after(0, self._scan_done, found)
def _ping(self, ip):
"""pingを実行して(alive, ms)を返す"""
timeout_s = self.timeout_var.get() / 1000
try:
param = "-n" if platform.system().lower() == "windows" else "-c"
cmd = ["ping", param, "1", "-w" if platform.system().lower() == "windows"
else "-W", str(self.timeout_var.get()), ip]
import time
t0 = time.time()
result = subprocess.run(cmd, capture_output=True,
timeout=timeout_s + 1)
ms = int((time.time() - t0) * 1000)
alive = result.returncode == 0
return alive, ms
except Exception:
# socketで接続チェック
try:
import time
t0 = time.time()
s = socket.create_connection((ip, 80),
timeout=timeout_s)
s.close()
ms = int((time.time() - t0) * 1000)
return True, ms
except Exception:
return False, 0
def _get_hostname(self, ip):
try:
return socket.gethostbyaddr(ip)[0]
except Exception:
return ""
def _scan_ports(self, ip):
common_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443,
445, 3306, 3389, 5432, 8080, 8443]
open_ports = []
for port in common_ports:
try:
s = socket.create_connection((ip, port), timeout=0.3)
s.close()
open_ports.append(port)
except Exception:
pass
return ", ".join(str(p) for p in open_ports)
def _add_result(self, ip, hostname, ms, ports):
self.tree.insert("", "end",
values=(ip, hostname, f"{ms} ms", ports, "オンライン"),
tags=("online",))
def _scan_done(self, found):
self.scanning = False
self.scan_btn.config(text="▶ スキャン開始")
self.progress_var.set(100)
self.status_var.set(
f"スキャン完了 {found} 台発見 "
f"({datetime.now().strftime('%H:%M:%S')})")
def _ping_single(self):
ip = self.tool_ip_var.get().strip()
if not ip:
return
threading.Thread(target=self._do_ping_single,
args=(ip,), daemon=True).start()
def _do_ping_single(self, ip):
alive, ms = self._ping(ip)
msg = f"{ip}: {'応答あり' if alive else '応答なし'} ({ms} ms)"
self.root.after(0, lambda: messagebox.showinfo("Ping結果", msg))
def _resolve_host(self):
target = self.tool_ip_var.get().strip()
if not target:
return
try:
result = socket.gethostbyaddr(target)
messagebox.showinfo("ホスト名解決",
f"ホスト名: {result[0]}\nエイリアス: {result[1]}\nIP: {result[2]}")
except Exception as e:
messagebox.showinfo("ホスト名解決", f"解決できませんでした: {e}")
def _traceroute(self):
ip = self.tool_ip_var.get().strip()
if not ip:
return
win = tk.Toplevel(self.root)
win.title(f"Traceroute — {ip}")
win.geometry("500x300")
txt = tk.Text(win, font=("Courier New", 10), bg="#1e1e1e",
fg="#ccc")
txt.pack(fill=tk.BOTH, expand=True)
def run():
cmd = (["tracert", ip] if platform.system().lower() == "windows"
else ["traceroute", ip])
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True)
for line in proc.stdout:
win.after(0, txt.insert, tk.END, line)
except Exception as e:
win.after(0, txt.insert, tk.END, f"エラー: {e}")
threading.Thread(target=run, daemon=True).start()
if __name__ == "__main__":
root = tk.Tk()
app = App13(root)
root.mainloop()
5. コード解説
ネットワークスキャナーのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App13 クラスにアプリの全機能をまとめています(メソッド15個・全297行)。__init__ ではタイトル「ネットワークスキャナー」とウィンドウサイズ 820x580 を設定します。あわせて状態を保持する変数(self.scanning)を初期化し、そのあと _build_ui() → _detect_local_ip() の順に呼び出して初期状態を作ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×3・tk.Label×7・ttk.LabelFrame×2・ttk.Entry×2・ttk.Spinbox×2・ttk.Checkbutton・ttk.Button(ループで生成)・ttk.Progressbar・ttk.Treeview・ttk.Scrollbar・tk.Toplevel・tk.Text で構成しています。見出し付きの枠(LabelFrame)は「スキャン設定」「スキャン結果」のラベルでエリアを区切っています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「▶ スキャン開始」ボタン(_toggle_scan())を command= で接続しています。ボタンはラベルと処理をペアで並べたデータをループで回して生成し、それぞれ _ping_single()・_resolve_host()・_traceroute() を command= に渡しています。ボタンを増やしたいときはこのペアを1行足すだけで済みます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception・ValueError を捕捉しています。あわせて messagebox.showerror()・messagebox.showinfo() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app13.py と保存します。使うのは
datetime・ipaddress・platform・re・socket・subprocess・threading・time・tkinterだけなので、追加インストールは不要です。 -
2クラスの骨格を作る
App13クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("ネットワークスキャナー")・geometry("820x580")・configure(bg="#f8f9fc")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×3・tk.Label×7・ttk.LabelFrame×2・ttk.Entry×2・ttk.Spinbox×2・ttk.Checkbutton・ttk.Button(ループで生成)・ttk.Progressbar・ttk.Treeview・ttk.Scrollbar・tk.Toplevel・tk.Textを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_toggle_scan())につなぎます。ループ生成のボタンは、ラベルと処理(_ping_single()・_resolve_host()・_traceroute())をペアで並べたデータをループで回し、command=に渡して接続します。 -
6中心になるメソッドを実装する
アプリの本体である
_add_result()(4行)・_scan_done()(7行)・_traceroute()(24行)など を実装します。 -
7動作確認する
python app13.pyで起動し、各ボタンが反応することを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 結果をファイルに保存する
このコードに保存処理はないため、表示中の数値や結果はアプリを閉じると消えます。テキストファイルへ書き出して、次回起動時に読み込む機能を追加してみましょう。
💡 入力履歴機能
以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("820x580") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。スキャンは自分が管理している機器・ネットワークにだけ実行してください。会社や公共 Wi-Fi、他人の回線に向けると規約違反や不正アクセスとみなされることがあります。試すときは 192.168.1.0/29 のような狭いレンジにすると、すぐ終わって確かめやすくなります。
-
課題1:進み具合のバーを「調べ終わった数」で進める
いまの
_scan_range()は、スレッドを起こした数だけprogress_varを進めます。_ping()の結果を待たずに数えるので、バーはすぐ 100% になり、そのあとも一覧に結果が増え続けます。調べ終わった台数で進むようにしてください。期待結果:狭いレンジでスキャンすると、バーが 100% に届くタイミングと「スキャン完了」の表示がほぼそろう。
合格条件
- 一覧に結果が増えている間は、バーが 100% に達していない
- 帯に出る「スキャン中: n/total」の n も、調べ終わった数で増える
- 「⏹ 停止」を押すとその場でスキャンが止まり、ボタンの表示が「▶ スキャン開始」に戻る
ヒント①(どこを触るか): 触るのは
_scan_range()の2か所です。内側のscan_host()に調べ終わった数を数える変数を足し、for i, ip in enumerate(hosts):の中でやっているself.progress_var.setの呼び出しをそちらへ移します。
ヒント②(使うもの): 数え方の手本は同じ関数のfoundです。nonlocalで宣言し、with lock:の中で1ずつ足せば取りこぼしません。画面の更新はself.root.after(0, self.progress_var.set, pct)の形で、必ずメインスレッドに渡します。
つまずきやすい点: 応答が無かったホストはif online:のブロックに入りません。数える処理をこの中へ置くと、バーがほとんど進まないままになります。self.scanningがFalseになるとscan_host()は途中で戻るため、停止したあとの合計は台数に届きません。 -
課題2:応答(ms) に ping が返した実測値を出す
いまの
_ping()はsubprocess.run()の前後で経過時間を測っています。この値には ping コマンドの起動時間も含まれるため、コマンドの出力に並ぶ数字より大きめに出ます。出力から実測値を取り出して表示してください。期待結果:手元のルーターを「Ping」ツールで叩くと、表示される ms がコマンドの出力に近い値になる。
合格条件
- 数字を取り出せなかったときは、これまでどおり測った経過時間を出す
- 応答が無いホストは、これまでどおり一覧に出ない
- 一覧の「応答(ms)」列にも、同じ取り出し方の値が並ぶ
ヒント①(どこを触るか): 触るのは
_ping()の1か所です。subprocess.run(cmd, capture_output=True, ...)の戻り値に出力が入っているので、ms = int((time.time() - t0) * 1000)の前に取り出す処理を足します。ファイル先頭のimport re(8行目)はまだどこでも使われていないので、そのまま使える状態です。
ヒント②(使うもの): 出力はresult.stdoutにバイト列で入ります。文字列へ直したうえで、re.search()にmsの直前の数字を拾うパターンを渡します。表記は環境ごとに違い、Windows の日本語版は「時間 =1ms」・英語版はtime=1ms・Linux や macOS はtime=1.23 msの形です。
つまずきやすい点: Windows のpingは UTF-8 で出力しないことがあります(日本語版は CP932)。decode()にerrors="ignore"を付けるか、subprocess.run(..., text=True, encoding="cp932", errors="ignore")のように読み方を指定してください。1ms 未満は<1msと表示されるため、=の後ろだけを探すと取りこぼします。 -
課題3:保存機能の追加
入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。