您当前的位置:首页 > 计算机 > 编程开发 > Python

【Python + HTML】局域网扫描飞牛设备

时间:01-19来源:作者:点击数:
CDSY,CDSY.XYZ

最近有扫描本地飞牛设备的需要,本来想着写个前端在线网页扫描,以实现像群晖Synology Web Assistant一样功能,无奈前端限制太多加之个人能力有限无法实现,于是有了后面结合DeepSeek回答后写的代码(前端HTML后端Python),打包成一个exe可执行文件。

一、文件结构

lan-scanner-project/
├── main.py              # 后端Python程序
├── templates/
│   └── index.html      # 前端HTML代码
├── static/              # 静态文件目录
│   └── icon.ico        # exe文件图标
└── requirements.txt     # 依赖文件

将相应文件(夹)创建好备用

二、前端文件

将index.html文件内写入以下内容并保存

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>飞牛设备扫描器</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
        }
         
        body {
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
            min-height: 100vh;
            padding: 20px;
            color: #333;
        }
         
        .container {
            max-width: 1400px;
            margin: 0 auto;
            background-color: white;
            border-radius: 15px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
            overflow: hidden;
        }
         
        header {
            background: linear-gradient(90deg, #4b6cb7 0%, #182848 100%);
            color: white;
            padding: 25px 30px;
            text-align: center;
        }
         
        h1 {
            font-size: 2.4rem;
            margin-bottom: 10px;
            letter-spacing: 0.5px;
        }
         
        .subtitle {
            font-size: 1.1rem;
            opacity: 0.85;
            font-weight: 300;
        }
         
        .content {
            padding: 30px;
        }
         
        .dashboard {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 30px;
            margin-bottom: 30px;
        }
         
        [url=home.php?mod=space&uid=945662]@media[/url] (max-width: 1024px) {
            .dashboard {
                grid-template-columns: 1fr;
            }
        }
         
        .panel {
            background-color: #f8f9fa;
            border-radius: 10px;
            padding: 25px;
            border-left: 5px solid #4b6cb7;
        }
         
        .panel.full-width {
            grid-column: 1 / -1;
        }
         
        .panel-title {
            font-size: 1.4rem;
            color: #182848;
            margin-bottom: 20px;
            display: flex;
            align-items: center;
            gap: 10px;
        }
         
        .panel-title i {
            color: #4b6cb7;
        }
         
        .form-group {
            margin-bottom: 20px;
        }
         
        label {
            display: block;
            margin-bottom: 8px;
            font-weight: 600;
            color: #182848;
        }
         
        .input-row {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            align-items: flex-end;
        }
         
        .input-group {
            flex: 1;
            min-width: 180px;
        }
         
        input, select {
            width: 100%;
            padding: 12px 15px;
            border: 2px solid #ddd;
            border-radius: 8px;
            font-size: 1rem;
            transition: border-color 0.3s;
        }
         
        input:focus, select:focus {
            border-color: #4b6cb7;
            outline: none;
        }
         
        .btn {
            background-color: #4b6cb7;
            color: white;
            border: none;
            border-radius: 8px;
            padding: 12px 25px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
        }
         
        .btn:hover {
            background-color: #3a5ca9;
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(75, 108, 183, 0.3);
        }
         
        .btn:active {
            transform: translateY(0);
        }
         
        .btn-primary {
            background-color: #4b6cb7;
        }
         
        .btn-primary:hover {
            background-color: #3a5ca9;
        }
         
        .btn-success {
            background-color: #28a745;
        }
         
        .btn-success:hover {
            background-color: #218838;
        }
         
        .btn-danger {
            background-color: #dc3545;
        }
         
        .btn-danger:hover {
            background-color: #c82333;
        }
         
        .btn-warning {
            background-color: #ffc107;
            color: #212529;
        }
         
        .btn-warning:hover {
            background-color: #e0a800;
        }
         
        .btn-group {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
         
        .network-cards {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: 15px;
            margin-top: 15px;
        }
         
        .network-card {
            background-color: white;
            border-radius: 8px;
            padding: 15px;
            border: 2px solid #e9ecef;
            cursor: pointer;
            transition: all 0.3s;
        }
         
        .network-card:hover {
            border-color: #4b6cb7;
            transform: translateY(-3px);
        }
         
        .network-card.active {
            border-color: #4b6cb7;
            background-color: #e7f3ff;
        }
         
        .network-base {
            font-weight: 700;
            font-size: 1.2rem;
            color: #182848;
        }
         
        .network-brand {
            font-size: 0.9rem;
            color: #6c757d;
            margin-top: 5px;
        }
         
        .local-network {
            background-color: #fff3cd;
            border-color: #ffc107;
        }
         
        .progress-container {
            margin-top: 20px;
        }
         
        .progress-label {
            display: flex;
            justify-content: space-between;
            margin-bottom: 8px;
        }
         
        .progress-bar {
            height: 12px;
            background-color: #e9ecef;
            border-radius: 6px;
            overflow: hidden;
        }
         
        .progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #4b6cb7 0%, #28a745 100%);
            width: 0%;
            transition: width 0.5s;
        }
         
        .task-info {
            background-color: white;
            border-radius: 8px;
            padding: 15px;
            margin-top: 15px;
            border-left: 4px solid #4b6cb7;
        }
         
        .task-info .info-row {
            display: flex;
            justify-content: space-between;
            margin-bottom: 8px;
        }
         
        .task-info .info-label {
            font-weight: 600;
            color: #6c757d;
        }
         
        .task-info .info-value {
            color: #182848;
        }
         
        .results-container {
            margin-top: 30px;
        }
         
        .results-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            padding-bottom: 15px;
            border-bottom: 2px solid #e9ecef;
        }
         
        .results-title {
            font-size: 1.4rem;
            color: #182848;
        }
         
        .device-count {
            background-color: #4b6cb7;
            color: white;
            padding: 5px 12px;
            border-radius: 20px;
            font-size: 0.9rem;
            font-weight: 600;
        }
         
        .device-list {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
            gap: 20px;
        }
         
        .device-card {
            background-color: white;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
            border-top: 4px solid #4b6cb7;
            transition: transform 0.3s;
        }
         
        .device-card:hover {
            transform: translateY(-5px);
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
        }
         
        .device-card.active {
            border-top-color: #28a745;
        }
         
        .device-ip {
            font-size: 1.3rem;
            font-weight: 700;
            color: #182848;
            margin-bottom: 5px;
        }
         
        .device-hostname {
            color: #6c757d;
            font-size: 0.9rem;
            margin-bottom: 10px;
        }
         
        .device-status {
            display: inline-block;
            padding: 4px 10px;
            border-radius: 20px;
            font-size: 0.85rem;
            font-weight: 600;
            margin-bottom: 15px;
        }
         
        .status-open {
            background-color: rgba(40, 167, 69, 0.15);
            color: #218838;
        }
         
        .status-closed {
            background-color: rgba(220, 53, 69, 0.15);
            color: #c82333;
        }
         
        .device-info {
            margin-bottom: 15px;
        }
         
        .device-info p {
            margin-bottom: 5px;
            display: flex;
            align-items: center;
            gap: 8px;
        }
         
        .device-actions {
            display: flex;
            gap: 10px;
        }
         
        .btn-access {
            background-color: #6c757d;
            padding: 8px 15px;
            font-size: 0.9rem;
            flex: 1;
        }
         
        .btn-access:hover {
            background-color: #5a6268;
        }
         
        .loading {
            display: none;
            text-align: center;
            padding: 40px;
        }
         
        .loading.active {
            display: block;
        }
         
        .spinner {
            border: 5px solid #f3f3f3;
            border-top: 5px solid #4b6cb7;
            border-radius: 50%;
            width: 50px;
            height: 50px;
            animation: spin 1s linear infinite;
            margin: 0 auto 20px;
        }
         
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
         
        .no-results {
            text-align: center;
            padding: 40px;
            color: #6c757d;
            font-size: 1.1rem;
        }
         
        .alert {
            padding: 15px;
            border-radius: 8px;
            margin-bottom: 20px;
            display: none;
        }
         
        .alert.active {
            display: block;
        }
         
        .alert-success {
            background-color: #d4edda;
            color: #155724;
            border-left: 4px solid #28a745;
        }
         
        .alert-error {
            background-color: #f8d7da;
            color: #721c24;
            border-left: 4px solid #dc3545;
        }
         
        .alert-info {
            background-color: #d1ecf1;
            color: #0c5460;
            border-left: 4px solid #17a2b8;
        }
         
        .status-badge {
            display: inline-block;
            padding: 4px 10px;
            border-radius: 20px;
            font-size: 0.85rem;
            font-weight: 600;
        }
         
        .status-pending {
            background-color: #fff3cd;
            color: #856404;
        }
         
        .status-scanning {
            background-color: #d1ecf1;
            color: #0c5460;
        }
         
        .status-completed {
            background-color: #d4edda;
            color: #155724;
        }
         
        .status-stopped {
            background-color: #f8d7da;
            color: #721c24;
        }
         
        .tab-container {
            margin-top: 20px;
        }
         
        .tabs {
            display: flex;
            border-bottom: 2px solid #dee2e6;
        }
         
        .tab {
            padding: 10px 20px;
            cursor: pointer;
            font-weight: 600;
            color: #6c757d;
            border-bottom: 3px solid transparent;
            transition: all 0.3s;
        }
         
        .tab:hover {
            color: #4b6cb7;
        }
         
        .tab.active {
            color: #4b6cb7;
            border-bottom-color: #4b6cb7;
        }
         
        .tab-content {
            display: none;
            padding: 20px 0;
        }
         
        .tab-content.active {
            display: block;
        }
         
        @media (max-width: 768px) {
            .content {
                padding: 20px;
            }
             
            .input-row {
                flex-direction: column;
            }
             
            .device-list {
                grid-template-columns: 1fr;
            }
             
            h1 {
                font-size: 1.8rem;
            }
             
            .device-actions {
                flex-direction: column;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1><i class="fas fa-network-wired"></i>飞牛设备扫描器</h1>
            <p class="subtitle">扫描本地局域网飞牛设备</p>
        </header>
         
        <div class="content">
            <div class="alert" id="alert-message"></div>
             
            <div class="dashboard">
                <div class="panel">
                    <h2 class="panel-title"><i class="fas fa-cogs"></i> 扫描配置</h2>
                     
                    <div class="form-group">
                        <label for="network-base">网络地址段</label>
                        <input type="text" id="network-base" value="192.168.1" placeholder="例如: 192.168.1">
                    </div>
                     
                    <div class="form-group">
                        <label>IP范围</label>
                        <div class="input-row">
                            <div class="input-group">
                                <input type="number" id="start-ip" min="1" max="254" value="1" placeholder="起始IP">
                            </div>
                            <div class="input-group">
                                <span style="text-align: center; padding: 12px;">到</span>
                            </div>
                            <div class="input-group">
                                <input type="number" id="end-ip" min="1" max="254" value="254" placeholder="结束IP">
                            </div>
                        </div>
                    </div>
                     
                    <div class="form-group">
                        <label for="port">端口号</label>
                        <input type="number" id="port" min="1" max="65535" value="5666" placeholder="端口号">
                    </div>
                     
                    <div class="btn-group">
                        <button class="btn btn-success" id="start-scan">
                            <i class="fas fa-search"></i> 开始扫描
                        </button>
                        <button class="btn btn-danger" id="stop-scan" disabled>
                            <i class="fas fa-stop"></i> 停止扫描
                        </button>
                        <button class="btn btn-warning" id="quick-scan">
                            <i class="fas fa-bolt"></i> 快速扫描
                        </button>
                    </div>
                </div>
                 
                <div class="panel">
                    <h2 class="panel-title"><i class="fas fa-info-circle"></i> 当前任务</h2>
                     
                    <div class="task-info" id="task-info" style="display: none;">
                        <div class="info-row">
                            <span class="info-label">任务ID:</span>
                            <span class="info-value" id="task-id">-</span>
                        </div>
                        <div class="info-row">
                            <span class="info-label">状态:</span>
                            <span class="info-value">
                                <span class="status-badge" id="task-status">-</span>
                            </span>
                        </div>
                        <div class="info-row">
                            <span class="info-label">进度:</span>
                            <span class="info-value" id="task-progress">0%</span>
                        </div>
                        <div class="info-row">
                            <span class="info-label">已扫描:</span>
                            <span class="info-value" id="task-scanned">0 / 0</span>
                        </div>
                        <div class="info-row">
                            <span class="info-label">发现设备:</span>
                            <span class="info-value" id="task-found">0</span>
                        </div>
                    </div>
                     
                    <div class="progress-container" id="progress-container" style="display: none;">
                        <div class="progress-label">
                            <span>扫描进度</span>
                            <span id="progress-text">0%</span>
                        </div>
                        <div class="progress-bar">
                            <div class="progress-fill" id="progress-fill"></div>
                        </div>
                    </div>
                     
                    <div style="text-align: center; padding: 20px; color: #6c757d;" id="no-task">
                        <i class="fas fa-info-circle fa-2x" style="margin-bottom: 10px;"></i>
                        <p>暂无扫描任务</p>
                    </div>
                </div>
                 
                <div class="panel full-width">
                    <h2 class="panel-title"><i class="fas fa-network-wired"></i> 网络选择</h2>
                     
                    <div class="tab-container">
                        <div class="tabs">
                            <div class="tab active" data-tab="local">本地网络</div>
                            <div class="tab" data-tab="common">常见网段</div>
                            <div class="tab" data-tab="custom">自定义</div>
                        </div>
                         
                        <div class="tab-content active" id="tab-local">
                            <div style="text-align: center; padding: 20px;" id="loading-networks">
                                <div class="spinner" style="width: 30px; height: 30px; margin-bottom: 10px;"></div>
                                <p>正在检测本地网络...</p>
                            </div>
                            <div class="network-cards" id="local-networks"></div>
                        </div>
                         
                        <div class="tab-content" id="tab-common">
                            <p>选择常见路由器网段:</p>
                            <div class="network-cards" id="common-networks"></div>
                        </div>
                         
                        <div class="tab-content" id="tab-custom">
                            <p>手动输入网络地址段:</p>
                            <div class="input-row">
                                <div class="input-group">
                                    <input type="text" id="custom-network" placeholder="例如: 192.168.3">
                                </div>
                                <button class="btn btn-primary" id="apply-custom">
                                    <i class="fas fa-check"></i> 应用
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
             
            <div class="loading" id="loading">
                <div class="spinner"></div>
                <p>正在扫描网络,请稍候...</p>
                <p id="scan-status">准备开始扫描</p>
            </div>
             
            <div class="results-container">
                <div class="results-header">
                    <h2 class="results-title">扫描结果</h2>
                    <div class="device-count">找到设备: <span id="device-count">0</span></div>
                </div>
                 
                <div id="device-list" class="device-list">
                    <div class="no-results" id="no-results">
                        <i class="fas fa-search fa-3x" style="margin-bottom: 15px; opacity: 0.5;"></i>
                        <p>等待扫描结果...</p>
                        <p style="font-size: 0.9rem; margin-top: 10px; color: #888;">扫描到的设备将显示在这里</p>
                    </div>
                </div>
            </div>
        </div>
    </div>
  
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // DOM元素
            const elements = {
                startScanBtn: document.getElementById('start-scan'),
                stopScanBtn: document.getElementById('stop-scan'),
                quickScanBtn: document.getElementById('quick-scan'),
                networkBaseInput: document.getElementById('network-base'),
                startIpInput: document.getElementById('start-ip'),
                endIpInput: document.getElementById('end-ip'),
                portInput: document.getElementById('port'),
                deviceList: document.getElementById('device-list'),
                noResults: document.getElementById('no-results'),
                deviceCount: document.getElementById('device-count'),
                loading: document.getElementById('loading'),
                progressContainer: document.getElementById('progress-container'),
                progressFill: document.getElementById('progress-fill'),
                progressText: document.getElementById('progress-text'),
                scanStatus: document.getElementById('scan-status'),
                taskInfo: document.getElementById('task-info'),
                noTask: document.getElementById('no-task'),
                taskId: document.getElementById('task-id'),
                taskStatus: document.getElementById('task-status'),
                taskProgress: document.getElementById('task-progress'),
                taskScanned: document.getElementById('task-scanned'),
                taskFound: document.getElementById('task-found'),
                alertMessage: document.getElementById('alert-message'),
                localNetworks: document.getElementById('local-networks'),
                commonNetworks: document.getElementById('common-networks'),
                loadingNetworks: document.getElementById('loading-networks'),
                customNetworkInput: document.getElementById('custom-network'),
                applyCustomBtn: document.getElementById('apply-custom')
            };
             
            // 全局变量
            let currentTaskId = null;
            let statusPollInterval = null;
            let currentTab = 'local';
             
            // API基础URL
            const API_BASE = window.location.origin;
             
            // 显示提示消息
            function showAlert(message, type = 'info') {
                elements.alertMessage.textContent = message;
                elements.alertMessage.className = `alert alert-${type} active`;
                 
                setTimeout(() => {
                    elements.alertMessage.classList.remove('active');
                }, 5000);
            }
             
            // 获取网络信息
            async function loadNetworkInfo() {
                try {
                    const response = await fetch(`${API_BASE}/api/networks`);
                    const data = await response.json();
                     
                    // 隐藏加载指示器
                    elements.loadingNetworks.style.display = 'none';
                     
                    // 显示本地网络
                    if (data.local_networks && data.local_networks.length > 0) {
                        let html = '';
                        data.local_networks.forEach(network => {
                            const networkBase = network.network.split('.').slice(0, 3).join('.');
                            html += `
                                <div class="network-card local-network" data-base="${networkBase}">
                                    <div class="network-base">${networkBase}.0/24</div>
                                    <div class="network-brand">本地网络 (${network.interface})</div>
                                    <div style="font-size: 0.8rem; margin-top: 5px; color: #6c757d;">
                                        ${network.ip} / ${network.netmask}
                                    </div>
                                </div>
                            `;
                        });
                        elements.localNetworks.innerHTML = html;
                         
                        // 默认选择第一个本地网络
                        const firstNetwork = data.local_networks[0];
                        const firstNetworkBase = firstNetwork.network.split('.').slice(0, 3).join('.');
                        elements.networkBaseInput.value = firstNetworkBase;
                         
                        // 添加点击事件
                        document.querySelectorAll('#local-networks .network-card').forEach(card => {
                            card.addEventListener('click', function() {
                                const base = this.dataset.base;
                                elements.networkBaseInput.value = base;
                                 
                                // 更新卡片选中状态
                                document.querySelectorAll('#local-networks .network-card').forEach(c => {
                                    c.classList.remove('active');
                                });
                                this.classList.add('active');
                            });
                        });
                         
                        // 选中第一个
                        if (document.querySelector('#local-networks .network-card')) {
                            document.querySelector('#local-networks .network-card').classList.add('active');
                        }
                    } else {
                        elements.localNetworks.innerHTML = `
                            <div style="text-align: center; padding: 20px; color: #6c757d; grid-column: 1 / -1;">
                                <i class="fas fa-exclamation-triangle fa-2x" style="margin-bottom: 10px;"></i>
                                <p>未检测到本地网络</p>
                                <p style="font-size: 0.9rem; margin-top: 5px;">请检查网络连接</p>
                            </div>
                        `;
                    }
                     
                    // 显示常见网络
                    if (data.common_networks && data.common_networks.length > 0) {
                        let html = '';
                        data.common_networks.forEach(network => {
                            html += `
                                <div class="network-card" data-base="${network.base}">
                                    <div class="network-base">${network.base}.0/24</div>
                                    <div class="network-brand">${network.brand}</div>
                                </div>
                            `;
                        });
                        elements.commonNetworks.innerHTML = html;
                         
                        // 添加点击事件
                        document.querySelectorAll('#common-networks .network-card').forEach(card => {
                            card.addEventListener('click', function() {
                                const base = this.dataset.base;
                                elements.networkBaseInput.value = base;
                                 
                                // 切换到自定义标签页
                                switchTab('custom');
                                elements.customNetworkInput.value = base;
                                 
                                // 更新卡片选中状态
                                document.querySelectorAll('#common-networks .network-card').forEach(c => {
                                    c.classList.remove('active');
                                });
                                this.classList.add('active');
                            });
                        });
                    }
                     
                } catch (error) {
                    console.error('加载网络信息失败:', error);
                    elements.loadingNetworks.innerHTML = `
                        <div style="text-align: center; padding: 20px; color: #dc3545;">
                            <i class="fas fa-exclamation-circle fa-2x" style="margin-bottom: 10px;"></i>
                            <p>加载网络信息失败</p>
                            <p style="font-size: 0.9rem; margin-top: 5px;">${error.message}</p>
                        </div>
                    `;
                }
            }
             
            // 切换标签页
            function switchTab(tabName) {
                currentTab = tabName;
                 
                // 更新标签
                document.querySelectorAll('.tab').forEach(tab => {
                    tab.classList.remove('active');
                    if (tab.dataset.tab === tabName) {
                        tab.classList.add('active');
                    }
                });
                 
                // 更新内容
                document.querySelectorAll('.tab-content').forEach(content => {
                    content.classList.remove('active');
                    if (content.id === `tab-${tabName}`) {
                        content.classList.add('active');
                    }
                });
            }
             
            // 开始扫描
            async function startScan() {
                const networkBase = elements.networkBaseInput.value.trim();
                const startIp = parseInt(elements.startIpInput.value);
                const endIp = parseInt(elements.endIpInput.value);
                const port = parseInt(elements.portInput.value);
                 
                // 验证输入
                if (!networkBase.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
                    showAlert('请输入有效的网络地址段(如 192.168.1)', 'error');
                    return;
                }
                 
                if (isNaN(startIp) || isNaN(endIp) || startIp < 1 || endIp > 254 || startIp > endIp) {
                    showAlert('请输入有效的IP范围(1-254)', 'error');
                    return;
                }
                 
                if (isNaN(port) || port < 1 || port > 65535) {
                    showAlert('请输入有效的端口号(1-65535)', 'error');
                    return;
                }
                 
                // 显示加载状态
                elements.loading.classList.add('active');
                elements.startScanBtn.disabled = true;
                elements.stopScanBtn.disabled = false;
                 
                try {
                    const response = await fetch(`${API_BASE}/api/scan`, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({
                            network_base: networkBase,
                            start_ip: startIp,
                            end_ip: endIp,
                            port: port
                        })
                    });
                     
                    const data = await response.json();
                     
                    if (response.ok) {
                        currentTaskId = data.task_id;
                        showAlert('扫描任务已启动', 'success');
                         
                        // 显示任务信息
                        elements.taskInfo.style.display = 'block';
                        elements.noTask.style.display = 'none';
                        elements.progressContainer.style.display = 'block';
                         
                        // 开始轮询任务状态
                        startStatusPolling();
                    } else {
                        showAlert(data.error || '启动扫描失败', 'error');
                        elements.loading.classList.remove('active');
                        elements.startScanBtn.disabled = false;
                    }
                } catch (error) {
                    showAlert(`网络错误: ${error.message}`, 'error');
                    elements.loading.classList.remove('active');
                    elements.startScanBtn.disabled = false;
                }
            }
             
            // 快速扫描
            async function quickScan() {
                const port = parseInt(elements.portInput.value);
                 
                if (isNaN(port) || port < 1 || port > 65535) {
                    showAlert('请输入有效的端口号(1-65535)', 'error');
                    return;
                }
                 
                elements.loading.classList.add('active');
                elements.quickScanBtn.disabled = true;
                 
                try {
                    const response = await fetch(`${API_BASE}/api/quick-scan`, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({
                            port: port
                        })
                    });
                     
                    const data = await response.json();
                     
                    if (response.ok) {
                        // 显示结果
                        displayResults(data.results);
                        showAlert(`快速扫描完成,发现 ${data.found} 个设备`, 'success');
                    } else {
                        showAlert(data.error || '快速扫描失败', 'error');
                    }
                } catch (error) {
                    showAlert(`网络错误: ${error.message}`, 'error');
                } finally {
                    elements.loading.classList.remove('active');
                    elements.quickScanBtn.disabled = false;
                }
            }
             
            // 停止扫描
            async function stopScan() {
                if (!currentTaskId) return;
                 
                try {
                    const response = await fetch(`${API_BASE}/api/scan/${currentTaskId}`, {
                        method: 'DELETE'
                    });
                     
                    if (response.ok) {
                        showAlert('扫描任务已停止', 'info');
                        elements.stopScanBtn.disabled = true;
                    }
                } catch (error) {
                    showAlert(`停止扫描失败: ${error.message}`, 'error');
                }
            }
             
            // 开始轮询任务状态
            function startStatusPolling() {
                if (statusPollInterval) {
                    clearInterval(statusPollInterval);
                }
                 
                statusPollInterval = setInterval(async () => {
                    await updateTaskStatus();
                }, 1000); // 每秒更新一次
            }
             
            // 更新任务状态
            async function updateTaskStatus() {
                if (!currentTaskId) return;
                 
                try {
                    const response = await fetch(`${API_BASE}/api/scan/${currentTaskId}`);
                    const task = await response.json();
                     
                    if (response.ok) {
                        // 更新任务信息
                        elements.taskId.textContent = task.id;
                        elements.taskStatus.textContent = task.status;
                        elements.taskStatus.className = `status-badge status-${task.status}`;
                        elements.taskProgress.textContent = `${task.progress.toFixed(1)}%`;
                        elements.taskScanned.textContent = `${task.scanned} / ${task.total}`;
                        elements.taskFound.textContent = task.found;
                         
                        // 更新进度条
                        elements.progressFill.style.width = `${task.progress}%`;
                        elements.progressText.textContent = `${task.progress.toFixed(1)}%`;
                        elements.scanStatus.textContent = `已扫描 ${task.scanned}/${task.total} 个IP,发现 ${task.found} 个设备`;
                         
                        // 显示结果
                        if (task.results && task.results.length > 0) {
                            displayResults(task.results);
                        }
                         
                        // 如果任务完成或停止,清理轮询
                        if (task.status === 'completed' || task.status === 'stopped') {
                            clearInterval(statusPollInterval);
                            elements.loading.classList.remove('active');
                            elements.startScanBtn.disabled = false;
                            elements.stopScanBtn.disabled = true;
                             
                            if (task.status === 'completed') {
                                showAlert(`扫描完成,共发现 ${task.found} 个设备`, 'success');
                            }
                        }
                    } else {
                        // 任务不存在或出错
                        clearInterval(statusPollInterval);
                        showAlert('扫描任务出错或不存在', 'error');
                        elements.loading.classList.remove('active');
                        elements.startScanBtn.disabled = false;
                        elements.stopScanBtn.disabled = true;
                    }
                } catch (error) {
                    console.error('获取任务状态失败:', error);
                }
            }
             
            // 显示结果
            function displayResults(devices) {
                if (devices.length === 0) {
                    elements.noResults.style.display = 'block';
                    elements.deviceCount.textContent = '0';
                    return;
                }
                 
                elements.noResults.style.display = 'none';
                elements.deviceCount.textContent = devices.length;
                 
                let html = '';
                 
                devices.forEach(device => {
                    const statusClass = device.open ? 'status-open' : 'status-closed';
                    const statusText = device.open ? '端口开放' : '端口关闭';
                    const hostname = device.hostname || '未知';
                     
                    html += `
                        <div class="device-card ${device.open ? 'active' : ''}">
                            <div class="device-ip">${device.ip}:${device.port}</div>
                            <div class="device-hostname">${hostname}</div>
                            <div class="device-status ${statusClass}">
                                <i class="fas fa-${device.open ? 'check-circle' : 'times-circle'}"></i> ${statusText}
                            </div>
                            <div class="device-info">
                                <p><i class="fas fa-clock"></i> 检测时间: ${new Date(device.timestamp).toLocaleTimeString()}</p>
                                ${device.service ? `<p><i class="fas fa-server"></i> 可能服务: ${device.service}</p>` : ''}
                            </div>
                            <div class="device-actions">
                                <a href="http://${device.ip}:${device.port}" target="_blank" class="btn btn-access">
                                    <i class="fas fa-external-link-alt"></i> 访问设备
                                </a>
                                <button class="btn btn-access" style="background-color: #17a2b8;">
                                    <i class="fas fa-info-circle"></i> 详细信息
                                </button>
                            </div>
                        </div>
                    `;
                });
                 
                elements.deviceList.innerHTML = html;
            }
             
            // 获取设备详细信息
            window.getDeviceInfo = async function(ip, port) {
                try {
                    const response = await fetch(`${API_BASE}/api/device/${ip}/${port}`);
                    const device = await response.json();
                     
                    let message = `设备信息:\n`;
                    message += `IP地址: ${device.ip}\n`;
                    message += `端口: ${device.port} (${device.port_open ? '开放' : '关闭'})\n`;
                     
                    if (device.hostname) {
                        message += `主机名: ${device.hostname}\n`;
                    }
                     
                    if (device.service) {
                        message += `可能服务: ${device.service}\n`;
                    }
                     
                    message += `检测时间: ${new Date(device.checked_at).toLocaleString()}`;
                     
                    alert(message);
                } catch (error) {
                    alert(`获取设备信息失败: ${error.message}`);
                }
            };
             
            // 事件监听器
            elements.startScanBtn.addEventListener('click', startScan);
            elements.stopScanBtn.addEventListener('click', stopScan);
            elements.quickScanBtn.addEventListener('click', quickScan);
             
            elements.applyCustomBtn.addEventListener('click', function() {
                const customNetwork = elements.customNetworkInput.value.trim();
                if (customNetwork.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
                    elements.networkBaseInput.value = customNetwork;
                    showAlert(`已应用网络地址段: ${customNetwork}`, 'success');
                } else {
                    showAlert('请输入有效的网络地址段(如 192.168.1)', 'error');
                }
            });
             
            // 标签页切换
            document.querySelectorAll('.tab').forEach(tab => {
                tab.addEventListener('click', function() {
                    switchTab(this.dataset.tab);
                });
            });
             
            // 初始化
            loadNetworkInfo();
             
            // 页面卸载时清理轮询
            window.addEventListener('beforeunload', function() {
                if (statusPollInterval) {
                    clearInterval(statusPollInterval);
                }
            });
        });
    </script>
</body>
</html>

三、后端文件

在main.py文件内写入以下内容并保存

from flask import Flask, request, jsonify, render_template
from flask_cors import CORS
import socket
import ipaddress
import threading
import time
import json
from datetime import datetime
import subprocess
import platform
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
import sys
  
# 获取资源路径(支持打包后运行)
def get_resource_path(relative_path):
    """获取资源文件的绝对路径,支持开发和打包后的环境"""
    if hasattr(sys, '_MEIPASS'):
        # 如果是PyInstaller打包后的exe
        base_path = sys._MEIPASS
    else:
        base_path = os.path.abspath(".")
     
    return os.path.join(base_path, relative_path)
  
# 初始化Flask应用,使用正确的资源路径
app = Flask(__name__, 
           template_folder=get_resource_path('templates'),
           static_folder=get_resource_path('static'))
CORS(app)
  
# 存储扫描任务的状态和结果
scan_tasks = {}
task_counter = 0
  
# 常见路由器品牌对应的网段
COMMON_NETWORKS = [
    {"base": "192.168.1", "brand": "TP-Link/水星/迅捷"},
    {"base": "192.168.0", "brand": "TP-Link/D-Link"},
    {"base": "192.168.3", "brand": "华为"},
    {"base": "192.168.31", "brand": "小米"},
    {"base": "192.168.10", "brand": "腾达"},
    {"base": "192.168.11", "brand": "Buffalo"},
    {"base": "192.168.18", "brand": "华硕"},
    {"base": "192.168.50", "brand": "网件/领势"},
    {"base": "192.168.123", "brand": "极路由"},
    {"base": "10.0.0", "brand": "部分企业级"},
    {"base": "10.1.1", "brand": "Apple/部分企业级"},
    {"base": "172.16.0", "brand": "企业级网络"},
    {"base": "172.16.1", "brand": "企业级网络"},
]
  
def is_port_open(ip, port, timeout=1):
    """检查指定IP的端口是否开放"""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((ip, port))
        sock.close()
        return result == 0
    except Exception as e:
        return False
  
def scan_single_ip(task_id, ip, port, timeout=1):
    """扫描单个IP地址"""
    try:
        if task_id in scan_tasks and scan_tasks[task_id].get('stopped', False):
            return None
             
        # 检查端口
        is_open = is_port_open(ip, port, timeout)
         
        # 尝试获取主机名
        hostname = ""
        try:
            hostname = socket.gethostbyaddr(ip)[0]
        except:
            pass
         
        return {
            "ip": ip,
            "port": port,
            "open": is_open,
            "hostname": hostname,
            "timestamp": datetime.now().isoformat()
        }
    except Exception as e:
        return {
            "ip": ip,
            "port": port,
            "open": False,
            "error": str(e),
            "timestamp": datetime.now().isoformat()
        }
  
def scan_network_range(task_id, network_base, start_ip, end_ip, port, max_workers=50):
    """扫描网络范围"""
    if task_id not in scan_tasks:
        return []
     
    task_info = scan_tasks[task_id]
    task_info['status'] = 'scanning'
    task_info['start_time'] = datetime.now().isoformat()
     
    total_ips = end_ip - start_ip + 1
    scanned_ips = 0
    found_devices = []
     
    # 使用线程池并发扫描
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {}
        for i in range(start_ip, end_ip + 1):
            if task_info.get('stopped', False):
                break
                 
            ip = f"{network_base}.{i}"
            future = executor.submit(scan_single_ip, task_id, ip, port)
            futures[future] = ip
         
        for future in as_completed(futures):
            if task_info.get('stopped', False):
                break
                 
            scanned_ips += 1
            result = future.result()
             
            if result:
                if result.get('open', False):
                    found_devices.append(result)
                 
                # 更新进度
                task_info['progress'] = (scanned_ips / total_ips) * 100
                task_info['scanned'] = scanned_ips
                task_info['total'] = total_ips
                task_info['found'] = len(found_devices)
     
    task_info['status'] = 'completed'
    task_info['end_time'] = datetime.now().isoformat()
    task_info['results'] = found_devices
     
    return found_devices
  
def get_local_networks():
    """获取本地网络接口信息(使用纯Python方法)"""
    networks = []
     
    try:
        # 获取本机主机名
        hostname = socket.gethostname()
         
        # 获取本机IP地址
        local_ip = socket.gethostbyname(hostname)
         
        # 如果是私有IP地址,添加到网络列表
        if local_ip.startswith('192.168.') or local_ip.startswith('10.') or local_ip.startswith('172.'):
            # 提取网络地址段
            ip_parts = local_ip.split('.')
            if len(ip_parts) >= 3:
                network_base = f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}"
                networks.append({
                    "interface": "本地连接",
                    "ip": local_ip,
                    "netmask": "255.255.255.0",  # 假设标准子网掩码
                    "network": f"{network_base}.0",
                    "cidr": f"{network_base}.0/24"
                })
         
        # 尝试获取更多网络接口信息(Windows系统)
        if platform.system() == 'Windows':
            try:
                # 执行ipconfig命令获取网络信息
                result = subprocess.run(['ipconfig'], capture_output=True, text=True, encoding='gbk')
                output = result.stdout
                 
                # 解析ipconfig输出
                lines = output.split('\n')
                current_adapter = ""
                for i, line in enumerate(lines):
                    line = line.strip()
                     
                    # 查找适配器名称
                    if line and not line.startswith(' ') and ':' in line:
                        current_adapter = line.replace(':', '')
                     
                    # 查找IPv4地址
                    if 'IPv4' in line and '地址' in line and ':' in line:
                        ip_part = line.split(':')[1].strip()
                        if ip_part and not ip_part.startswith('169.254'):  # 排除APIPA地址
                            # 提取网络地址段
                            ip_parts = ip_part.split('.')
                            if len(ip_parts) >= 3:
                                network_base = f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}"
                                networks.append({
                                    "interface": current_adapter,
                                    "ip": ip_part,
                                    "netmask": "255.255.255.0",  # 假设标准子网掩码
                                    "network": f"{network_base}.0",
                                    "cidr": f"{network_base}.0/24"
                                })
            except:
                pass
         
        # 如果没有找到任何网络,添加常见网络作为备用
        if not networks:
            networks.append({
                "interface": "默认网络",
                "ip": "192.168.1.100",
                "netmask": "255.255.255.0",
                "network": "192.168.1.0",
                "cidr": "192.168.1.0/24"
            })
             
    except Exception as e:
        print(f"获取网络接口失败: {e}")
        # 添加默认网络
        networks.append({
            "interface": "默认网络",
            "ip": "192.168.1.100",
            "netmask": "255.255.255.0",
            "network": "192.168.1.0",
            "cidr": "192.168.1.0/24"
        })
     
    return networks
  
@app.route('/')
def index():
    """主页面"""
    return render_template('index.html')
  
@app.route('/api/scan', methods=['POST'])
def start_scan():
    """开始扫描任务"""
    global task_counter
     
    data = request.json
    network_base = data.get('network_base', '192.168.1')
    start_ip = int(data.get('start_ip', 1))
    end_ip = int(data.get('end_ip', 254))
    port = int(data.get('port', 5666))
     
    # 验证参数
    if start_ip < 1 or end_ip > 254 or start_ip > end_ip:
        return jsonify({"error": "无效的IP范围"}), 400
     
    if port < 1 or port > 65535:
        return jsonify({"error": "无效的端口号"}), 400
     
    # 创建任务
    task_counter += 1
    task_id = f"task_{task_counter}_{int(time.time())}"
     
    scan_tasks[task_id] = {
        'id': task_id,
        'network_base': network_base,
        'start_ip': start_ip,
        'end_ip': end_ip,
        'port': port,
        'status': 'pending',
        'progress': 0,
        'scanned': 0,
        'total': end_ip - start_ip + 1,
        'found': 0,
        'results': [],
        'stopped': False,
        'created_at': datetime.now().isoformat()
    }
     
    # 启动后台扫描线程
    def run_scan():
        scan_network_range(task_id, network_base, start_ip, end_ip, port)
     
    thread = threading.Thread(target=run_scan)
    thread.daemon = True
    thread.start()
     
    return jsonify({
        "task_id": task_id,
        "message": "扫描任务已启动",
        "status_url": f"/api/scan/{task_id}"
    })
  
@app.route('/api/scan/<task_id>', methods=['GET'])
def get_scan_status(task_id):
    """获取扫描任务状态"""
    if task_id not in scan_tasks:
        return jsonify({"error": "任务不存在"}), 404
     
    task_info = scan_tasks[task_id].copy()
     
    # 清理敏感信息或不必要的信息
    if 'stopped' in task_info:
        del task_info['stopped']
     
    return jsonify(task_info)
  
@app.route('/api/scan/<task_id>', methods=['DELETE'])
def stop_scan(task_id):
    """停止扫描任务"""
    if task_id not in scan_tasks:
        return jsonify({"error": "任务不存在"}), 404
     
    scan_tasks[task_id]['stopped'] = True
    scan_tasks[task_id]['status'] = 'stopped'
     
    return jsonify({
        "message": "扫描任务已停止",
        "task_id": task_id
    })
  
@app.route('/api/networks', methods=['GET'])
def get_networks():
    """获取本地网络信息"""
    local_networks = get_local_networks()
     
    return jsonify({
        "local_networks": local_networks,
        "common_networks": COMMON_NETWORKS
    })
  
@app.route('/api/device/<ip>/<port>', methods=['GET'])
def get_device_info(ip, port):
    """获取设备详细信息"""
    try:
        # 检查端口状态
        is_open = is_port_open(ip, int(port), timeout=2)
         
        # 尝试获取更多信息
        info = {
            "ip": ip,
            "port": port,
            "port_open": is_open,
            "checked_at": datetime.now().isoformat()
        }
         
        # 尝试获取主机名
        try:
            hostname, aliases, addresses = socket.gethostbyaddr(ip)
            info["hostname"] = hostname
        except:
            info["hostname"] = ""
         
        # 尝试获取服务信息(如果有已知服务)
        known_services = {
            80: "HTTP Web服务",
            443: "HTTPS Web服务",
            22: "SSH服务",
            21: "FTP服务",
            23: "Telnet服务",
            25: "SMTP服务",
            3389: "远程桌面",
            5900: "VNC服务",
            5666: "可能为NAS/监控设备",
            8080: "备用HTTP服务",
            8888: "常见管理端口"
        }
         
        port_int = int(port)
        if port_int in known_services:
            info["service"] = known_services[port_int]
         
        return jsonify(info)
    except Exception as e:
        return jsonify({"error": str(e)}), 500
  
@app.route('/api/quick-scan', methods=['POST'])
def quick_scan():
    """快速扫描常见网段"""
    data = request.json
    port = data.get('port', 5666)
     
    results = []
     
    # 只扫描常见网段的前10个IP以加快速度
    for network in COMMON_NETWORKS[:5]:
        network_base = network["base"]
         
        for i in range(1, 11):  # 只扫描1-10
            ip = f"{network_base}.{i}"
            is_open = is_port_open(ip, port, timeout=0.5)
             
            if is_open:
                results.append({
                    "ip": ip,
                    "port": port,
                    "open": True,
                    "network": network_base,
                    "brand": network["brand"]
                })
     
    return jsonify({
        "results": results,
        "total_scanned": 50,  # 5个网段 * 10个IP
        "found": len(results)
    })
  
# 清理旧任务(每10分钟清理一次超过1小时的任务)
def cleanup_old_tasks():
    while True:
        time.sleep(600)  # 10分钟
        now = datetime.now()
        to_delete = []
         
        for task_id, task_info in scan_tasks.items():
            created_at_str = task_info.get('created_at', '')
            if created_at_str:
                try:
                    created_at = datetime.fromisoformat(created_at_str)
                    if (now - created_at).total_seconds() > 3600:  # 1小时
                        to_delete.append(task_id)
                except:
                    pass
         
        for task_id in to_delete:
            del scan_tasks[task_id]
  
# 启动清理线程
cleanup_thread = threading.Thread(target=cleanup_old_tasks)
cleanup_thread.daemon = True
cleanup_thread.start()
  
if __name__ == '__main__':
  
    # 检查是否以exe形式运行
    is_exe = getattr(sys, 'frozen', False)
     
    print("=" * 60)
    print("局域网扫描服务器")
    print("=" * 60)
     
    if is_exe:
        print("运行模式: EXE应用程序")
        print("注意: 按Ctrl+C停止服务器")
    else:
        print("运行模式: Python脚本")
        print("访问 http://localhost:5000 使用扫描工具")
        print("按 Ctrl+C 停止服务器")
     
    print("=" * 60)
     
    # 自动打开浏览器(仅Windows且为exe运行时)
    if is_exe and platform.system() == 'Windows':
        import webbrowser
         
        def open_browser():
            time.sleep(2)  # 等待服务器启动
            webbrowser.open('http://localhost:5000')
         
        browser_thread = threading.Thread(target=open_browser)
        browser_thread.daemon = True
        browser_thread.start()
     
    try:
        app.run(host='0.0.0.0', port=5000, debug=False)
    except KeyboardInterrupt:
        print("\n服务器已停止")
    except Exception as e:
        print(f"服务器启动失败: {e}")
        if is_exe:
            input("按回车键退出...")

四、环境依赖

在requirements.txt文件内写入以下内容并保存

Flask==2.3.3
flask-cors==4.0.0

五、编译安装

5.1 安装Python

去官网下载安装:https://www.python.org/downloads/windows/

5.2 安装依赖

在lan-scanner-project文件夹右键-在终端中打开,执行以下命令

pip install -r requirements.txt

5.3 安装工具

PyInstaller 是Python 应用程序打包成独立可执行文件的工具

pip install pyinstaller

5.4 编译文件

pyinstaller --onefile --windowed --name LANScanner --icon=static/icon.ico --hidden-import flask --hidden-import flask_cors --hidden-import netifaces main.py

六、成品使用

在 dist 文件夹中找到 LANScanner.exe,双击运行,允许通过防火墙,程序会自动打开浏览器访问 http://localhost:5000飞牛默认端口为5666,点开始扫描等待即可。由于手头没有飞牛设备,这里测试扫描海康威视摄像头/录像机本地默认8000端口,可全部扫描出。 

CDSY,CDSY.XYZ
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐