Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
```
# Compiled Python files
*.pyc
__pycache__/

# Virtual environments
venv/
.venv/
.env
.env.local
*.env.*

# Logs
*.log

# Coverage reports
.coverage
coverage/
htmlcov/

# IDE files
.vscode/
.idea/
*.swp
*.swo
*.tmp

# Build/test artifacts
.pytest_cache/
.mypy_cache/
```
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
## ✨ 核心功能
- **🚀 异步深度采集**:利用多线程(Threading)技术,程序启动即秒开,后台自动完成 CPU、内存、硬盘、主板等核心硬件信息的静默扫描。
- **🏷️ 智能品牌引擎**:内置模糊匹配算法,能够从杂乱的 OEM 原始字符串中精准识别品牌(如将 `TUF GAMING B550M` 自动归类为 `华硕`)。
- **🌐 网络信息采集**:自动获取本机 IP 地址和 MAC 地址,确保来自同一网卡,便于网络资产定位。
- **📥 引导式信息补充**:采集完成后自动弹出交互框,引导运维人员补充部门、使用人等关键归档信息。
- **💾 数据双保险机制**:
- **实时持久化**:采用 JSON 格式进行本地数据库存储,支持断电/异常退出数据保护。
Expand Down
113 changes: 96 additions & 17 deletions collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,43 +48,121 @@ def is_temp_directory(path):
return False


def get_ip_address():
"""获取本机 IP 地址"""
# 方法1:尝试 socket 连接(最快)
def get_ip_and_mac_address():
"""获取本机 IP 地址和对应的 MAC 地址(确保来自同一网卡)"""
# 方法 1:尝试 socket 连接获取 IP,然后找到对应网卡的 MAC
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip_address = s.getsockname()[0]
print(ip_address)
s.close()

if ip_address and not ip_address.startswith('169.254.'):
return ip_address
# 找到了 IP,现在获取对应网卡的 MAC 地址
mac_address = get_mac_for_ip(ip_address)
if mac_address:
print(f"IP: {ip_address}, MAC: {mac_address}")
return ip_address, mac_address
except:
pass
# 方法2:使用系统命令

# 方法 2:使用 PowerShell 获取有默认网关的网卡信息(最可靠)
try:
# Windows 系统
if os.name == 'nt':
cmd = 'powershell -Command "(Get-NetIPConfiguration | Where-Object { $_.IPv4DefaultGateway -ne $null }).IPv4Address.IPAddress"'
result = subprocess.run(cmd, capture_output=True,
creationflags=subprocess.CREATE_NO_WINDOW, text=True, shell=True)
cmd = '''
Get-NetIPConfiguration |
Where-Object { $_.IPv4DefaultGateway -ne $null } |
Select-Object -First 1 |
ForEach-Object {
[PSCustomObject]@{
IPAddress = ($_.IPv4Address | Where-Object { $_.AddressFamily -eq 2 }).IPAddress
MACAddress = $_.NetAdapter.MacAddress
}
}
'''
result = subprocess.run(['powershell', '-Command', cmd], capture_output=True,
creationflags=subprocess.CREATE_NO_WINDOW, text=True, encoding='gbk')
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()
lines = result.stdout.strip().split('
')
for line in lines:
if ':' in line:
parts = line.split(':')
if len(parts) >= 2:
ip = parts[0].strip()
mac = parts[1].strip() if len(parts) > 1 else ""
if ip and not ip.startswith('127.') and not ip.startswith('169.254.'):
return ip, mac
except:
return "windows识别未知"
# 方法3:通过主机名获取
pass

# 方法 3:通过主机名获取 IP,MAC 地址尝试从所有网卡中获取
try:
hostname = socket.gethostname()
for ip in socket.gethostbyname_ex(hostname)[2]:
if ip and not ip.startswith('127.') and not ip.startswith('169.254.') and not ip.startswith('172.17.'):
print(socket.gethostbyname_ex(hostname)[2])
return ip
mac_address = get_mac_for_ip(ip)
print(f"IP: {ip}, MAC: {mac_address if mac_address else '未知'}")
return ip, mac_address if mac_address else "未知"
except:
pass

return "未知", "未知"


def get_mac_for_ip(ip_address):
"""根据 IP 地址获取对应网卡的 MAC 地址"""
try:
# 使用 PowerShell 精确匹配 IP 地址找到对应网卡
if os.name == 'nt':
cmd = f'''
Get-NetIPConfiguration |
ForEach-Object {{
$_.IPv4Address | Where-Object {{ $_.IPAddress -eq "{ip_address}" }} |
ForEach-Object {{
(Get-NetAdapter -Name $_.InterfaceAlias).MacAddress
}}
}}
'''
result = subprocess.run(['powershell', '-Command', cmd], capture_output=True,
creationflags=subprocess.CREATE_NO_WINDOW, text=True, encoding='gbk')
if result.returncode == 0 and result.stdout.strip():
mac = result.stdout.strip().upper()
# 格式化 MAC 地址为标准格式(如:00-1A-2B-3C-4D-5E)
if mac and len(mac) == 12:
return '-'.join([mac[i:i+2] for i in range(0, 12, 2)])
elif mac and '-' in mac:
return mac
elif mac and ':' in mac:
return mac.replace(':', '-')
except:
pass

# 备用方法:使用 getmac 命令
try:
result = subprocess.run(['getmac', '/NH', '/FO', 'CSV'], capture_output=True,
creationflags=subprocess.CREATE_NO_WINDOW, text=True, encoding='gbk')
if result.returncode == 0:
lines = result.stdout.strip().split('
')
for line in lines:
if ip_address in line or True: # 返回第一个有效的 MAC
parts = line.split(',')
for part in parts:
if '-' in part and len(part) == 17:
return part.upper()
except:
pass

return "未知"


def get_ip_address():
"""获取本机 IP 地址(保留向后兼容)"""
ip, _ = get_ip_and_mac_address()
return ip


def _format_disk_size( gb_raw):
"""将不规则的原始容量归类为标准档位"""
val = float(gb_raw)
Expand Down Expand Up @@ -147,7 +225,7 @@ def get_hardware_info():
except:
current_user = os.environ.get('USERNAME', '未知用户')

ip= get_ip_address()
ip, mac = get_ip_and_mac_address()


return {
Expand All @@ -159,7 +237,8 @@ def get_hardware_info():
"内存": memory,
"硬盘": disk,
"操作系统": os_info,
"ip地址": ip
"ip地址": ip,
"MAC 地址": mac
}

def get_brand_by_model(self, model_name):
Expand Down
128 changes: 128 additions & 0 deletions test_brand_database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# test_brand_database.py
"""
BrandDatabase 品牌数据库单元测试
测试品牌识别功能,不依赖 Windows 特定模块
"""

import unittest
import json
import os
import tempfile
import shutil


class MockBrandDatabase:
"""简化版 BrandDatabase 用于测试"""

def __init__(self, brands_data=None):
if brands_data is None:
self.brands_data = {
"联想": ["thinkpadx1carbon", "thinkpadt14", "xiaoxin14"],
"戴尔": ["xps13", "xps15", "latitude5440"],
"惠普": ["spectrex36014", "envy13", "elitebook840"],
"华硕": ["rogstrixscar16", "vivobooks14", "tianxuan4"]
}
else:
self.brands_data = brands_data

def detect_brand_from_model(self, model_name):
"""从型号检测品牌"""
if not model_name:
return "未知"

model_lower = str(model_name).lower().replace(" ", "").replace("-", "")

for brand, models in self.brands_data.items():
for model_keyword in models:
if model_keyword in model_lower:
return brand

return "未知"


class TestBrandDatabase(unittest.TestCase):
"""BrandDatabase 单元测试类"""

def setUp(self):
"""测试前准备"""
self.db = MockBrandDatabase()

def test_detect_lenovo_thinkpad(self):
"""测试联想 ThinkPad 识别"""
self.assertEqual(self.db.detect_brand_from_model("ThinkPad X1 Carbon"), "联想")
self.assertEqual(self.db.detect_brand_from_model("ThinkPadT14"), "联想")
self.assertEqual(self.db.detect_brand_from_model("20UDS0KV00"), "未知")

def test_detect_dell_xps(self):
"""测试戴尔 XPS 识别"""
self.assertEqual(self.db.detect_brand_from_model("XPS 13"), "戴尔")
self.assertEqual(self.db.detect_brand_from_model("XPS-15"), "戴尔")
self.assertEqual(self.db.detect_brand_from_model("Latitude 5440"), "戴尔")

def test_detect_hp(self):
"""测试惠普识别"""
self.assertEqual(self.db.detect_brand_from_model("Spectre x360 14"), "惠普")
self.assertEqual(self.db.detect_brand_from_model("ENVY 13"), "惠普")
self.assertEqual(self.db.detect_brand_from_model("EliteBook 840"), "惠普")

def test_detect_asus(self):
"""测试华硕识别"""
self.assertEqual(self.db.detect_brand_from_model("ROG Strix Scar 16"), "华硕")
self.assertEqual(self.db.detect_brand_from_model("VivoBook S14"), "华硕")
self.assertEqual(self.db.detect_brand_from_model("tianxuan4"), "华硕")

def test_unknown_brand(self):
"""测试未知品牌"""
self.assertEqual(self.db.detect_brand_from_model(""), "未知")
self.assertEqual(self.db.detect_brand_from_model(None), "未知")
self.assertEqual(self.db.detect_brand_from_model("Unknown Model XYZ"), "未知")

def test_case_insensitive(self):
"""测试大小写不敏感"""
self.assertEqual(self.db.detect_brand_from_model("XPS 13"), "戴尔")
self.assertEqual(self.db.detect_brand_from_model("xps 13"), "戴尔")
self.assertEqual(self.db.detect_brand_from_model("XpS-13"), "戴尔")


class TestDataHandlerLogic(unittest.TestCase):
"""DataHandler 逻辑测试(不依赖实际文件操作)"""

def test_json_serialization(self):
"""测试 JSON 序列化"""
data = [
{"计算机名称": "PC001", "品牌": "联想", "CPU": "i7"},
{"计算机名称": "PC002", "品牌": "戴尔", "CPU": "i5"}
]

# 序列化
json_str = json.dumps(data, ensure_ascii=False)
self.assertIsInstance(json_str, str)

# 反序列化
loaded = json.loads(json_str)
self.assertEqual(len(loaded), 2)
self.assertEqual(loaded[0]["计算机名称"], "PC001")

def test_empty_data_handling(self):
"""测试空数据处理"""
self.assertFalse([]) # 空列表为 False
self.assertFalse(None) # None 为 False
self.assertTrue([{"key": "value"}]) # 非空列表为 True

def test_excel_headers_extraction(self):
"""测试 Excel 表头提取逻辑"""
data = [
{"计算机名称": "PC001", "ip 地址": "192.168.1.1", "品牌": "联想"},
{"计算机名称": "PC002", "ip 地址": "192.168.1.2", "品牌": "戴尔"}
]

headers = list(data[0].keys())
self.assertEqual(len(headers), 3)
self.assertIn("计算机名称", headers)
self.assertIn("ip 地址", headers)
self.assertIn("品牌", headers)


if __name__ == '__main__':
# 运行测试
unittest.main(verbosity=2)
Loading