我把"看自己这台电脑到底卡在哪"这件事写成了两个只读脚本。跑完你会得到一份 JSON: CPU / 内存(含内存条与插槽)/ 硬盘(型号、健康、卷余量)/ 显卡 / 主板固件 / 网卡 / 内存占用最高的进程。
写的过程中踩到三个坑,每个都会让人把错的数当成对的,所以先讲坑,再给完整代码。
坑一:同一个"内存占用",两个口径差 7 个点
Windows 上"内存用了多少"至少有两个算法:
GlobalMemoryStatusEx().dwMemoryLoad—— 和任务管理器「使用中」一致(推荐);100 − 可用物理内存 / 总物理内存—— 常比前者低 5~8 个点。
差在哪:后者把"待机缓存/压缩内存"算成了"可用"。所以看到 78% 别高兴,任务管理器可能写着 86%。
规矩:报数时写明用的是哪个口径。脚本里统一用第一个,并在 JSON 的 source 字段里标明。
坑二:显存字段有 4 GB 截断
Win32_VideoController.AdapterRAM 是 32 位字段,超过 4 GB 的显卡会读出错值(常见:0 或 4294967295 附近)。
我自己的机器上它把一块 2 GB 独显读成 2 GB(正常),把核显读成 1 GB,把虚拟显示适配器读成 0 —— 三个数摆在一起很容易被误当成"显存总量"。
规矩:这个字段可以打印,但不能拿来下结论;要准确显存请用厂商工具。
坑三:"读不到"不等于 0
温度(MSAcpi_ThermalZoneTemperature)和 SSD 磨损(Get-StorageReliabilityCounter)在消费级笔记本上经常读不到
(需要管理员权限或驱动支持)。很多脚本这时候会打印 0,于是你看到的是"温度 0 度、磨损 0%"——
两个都是假的。
规矩:读不到就写 未测 + 原因(本脚本写在 _err 字段里),绝不用 0 顶替。
工具一:net_link_check.py(查"千兆网卡为什么只跑百兆")
判断方式不是猜:看链路协商速率(Get-NetAdapter.LinkSpeed)与是否被写死
(Speed & Duplex 高级属性)。若显示 Auto Negotiation 而速率只有 100 Mbps,那不是配置问题,
瓶颈在网线或路由器/交换机端口(换一根 Cat5e/Cat6、换个口再测)。
# -*- coding: utf-8 -*-
r"""net_link_check.py —— 只读:查「千兆网卡为什么只跑百兆」。
判据(三态,不许糊):
· 网卡**协商速率**:`Get-NetAdapter.LinkSpeed`(100 Mbps / 1 Gbps …);
· 网卡**是否被写死**:`Speed & Duplex` 高级属性 —— `Auto Negotiation` 表示**不是配置问题**,
对端(网线/路由器口)才是瓶颈;反之若是 `100 Mbps Full Duplex` 这种固定值,则是被配置写死。
· 读不到就写 `_err`,**不写 0**。
用法:py -3.12 -X utf8 net_link_check.py [--json]
"""
import argparse
import json
import subprocess
import sys
PS_HEAD = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; "
def ps(cmd: str, timeout=45):
try:
r = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", PS_HEAD + cmd],
capture_output=True, text=True, encoding="utf-8", errors="replace",
timeout=timeout, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
out = (r.stdout or "").strip()
if r.returncode != 0:
return {"_err": "rc=%s %s" % (r.returncode, (r.stderr or "")[-120:])}
return json.loads(out) if out else {"_err": "empty_output"}
except Exception as e:
return {"_err": "%s: %s" % (type(e).__name__, str(e)[:100])}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--json", action="store_true")
a = ap.parse_args()
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
up = ps("Get-NetAdapter | Where-Object Status -eq 'Up' | Select-Object Name,InterfaceDescription,"
"LinkSpeed,MediaType,FullDuplex | ConvertTo-Json")
adv = ps("Get-NetAdapterAdvancedProperty -ErrorAction SilentlyContinue | "
"Where-Object DisplayName -match 'Speed|Duplex|速度' | "
"Select-Object Name,DisplayName,DisplayValue | ConvertTo-Json")
up_list = up if isinstance(up, list) else [up]
adv_list = adv if isinstance(adv, list) else [adv]
verdicts = []
for n in up_list:
speed = str(n.get("LinkSpeed") or "")
forced = [x for x in adv_list if str(x.get("DisplayName", "")).lower().find("speed") >= 0
and str(x.get("DisplayValue", "")) == "Auto Negotiation"]
if speed.startswith("100 M"):
verdicts.append({
"nic": n.get("Name"), "speed": speed,
"looks_like": "链路只有百兆",
"speed_duplex": (forced[0].get("DisplayValue") if forced else "未测"),
"conclusion": ("网卡是自动协商 ⇒ **不是配置被写死**,瓶颈在网线或路由器/交换机口"
"(换一根 Cat5e/Cat6 网线、换一个口再测)"
if forced else
"读不到 Speed&Duplex ⇒ 未测;先看是否被写死为 100M"),
"acceptance": "换线/换口后重跑本脚本,`LinkSpeed` 应变为 1 Gbps",
})
else:
verdicts.append({"nic": n.get("Name"), "speed": speed, "looks_like": "非百兆(无需处理)"})
payload = {"uplinks": up_list, "speed_duplex_props": adv_list, "verdicts": verdicts}
if a.json:
print(json.dumps(payload, ensure_ascii=False, indent=1))
else:
for v in verdicts:
print("%-18s %-10s %s" % (v.get("nic"), v.get("speed"), v.get("conclusion", v.get("looks_like"))))
if not verdicts:
print("没有处于 Up 状态的网卡(或读取失败):", json.dumps(payload, ensure_ascii=False)[:200])
return 0
if __name__ == "__main__":
raise SystemExit(main())
工具二:ai_pc_inventory.py(整机体检)
每一段都带 source(取自哪个 CIM 类/命令),产物写到 %PC_CHECK_OUT% 或 ~/pc_check_reports/:
# -*- coding: utf-8 -*-
r"""ai_pc_inventory.py —— **AI 电脑改造 · 现状扫描**(总纲第五节 1.(1)/2.(1)/6.(1))。
允许自主的部分:扫描 CPU / 内存 / 硬盘 / GPU / 主板 / 固件 / 温度 / 网络 / 进程;**只读**。
纪律:每段带 `source`(哪条 CIM 类/命令);**取不到就 `_err` + 说明,绝不写 0 冒充**;
`_items` 保留原始列表(多盘的机器不会被"只取第一条"吃掉)。
产物:%PC_CHECK_OUT% 或 ~/pc_check_reports/ 下的 ai_pc_inventory_<时间戳>.json(+ latest)
用法:py -3.12 -X utf8 ai_pc_inventory.py [--md]
★ 打包版改动(2026-10-16):**输出目录不再写死本机工作区路径**(原来写死 D:\8月\正式版1),
改成 `%PC_CHECK_OUT%` 或 `~/pc_check_reports/` —— 别人拿到这一个文件就能直接跑。
"""
import argparse
import datetime as dt
import json
import os
import pathlib
import subprocess
import sys
OUT_DIR = pathlib.Path(os.environ.get("PC_CHECK_OUT") or (pathlib.Path.home() / "pc_check_reports"))
PS_HEAD = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; "
def ps(cmd: str, timeout=90):
try:
r = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", PS_HEAD + cmd],
capture_output=True, text=True, encoding="utf-8", errors="replace",
timeout=timeout, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
out = (r.stdout or "").strip()
if r.returncode != 0:
return {"_err": "rc=%s %s" % (r.returncode, (r.stderr or "")[-160:])}
return json.loads(out) if out else {"_err": "empty_output"}
except Exception as e:
return {"_err": "%s: %s" % (type(e).__name__, str(e)[:120])}
def sect(source: str, payload) -> dict:
"""把任意 CIM 返回(dict / list / 单值)统一成 {source, _items, ...首条字段}。"""
d = {"source": source}
if isinstance(payload, dict):
if "_err" in payload:
d["_err"] = payload["_err"]
d.update(payload)
d["_items"] = [payload]
elif isinstance(payload, list):
d["_items"] = payload
if payload and isinstance(payload[0], dict):
d.update(payload[0])
else:
d["_value"] = payload
else:
d["_value"] = payload
return d
def items(inv, key):
v = inv.get(key) or {}
it = v.get("_items")
return it if isinstance(it, list) else ([v] if v else [])
def collect() -> dict:
inv = {"t": dt.datetime.now().isoformat(timespec="seconds")}
inv["host"] = sect("Win32_ComputerSystem + Win32_OperatingSystem", ps(
"$cs=Get-CimInstance Win32_ComputerSystem;$os=Get-CimInstance Win32_OperatingSystem;"
"@{model=$cs.Model;vendor=$cs.Manufacturer;ram_gb=[math]::Round($cs.TotalPhysicalMemory/1GB,2);"
"os=$os.Caption;version=$os.Version;boot=$os.LastBootUpTime.ToString('s');"
"uptime_h=[math]::Round(((Get-Date)-$os.LastBootUpTime).TotalHours,1)}|ConvertTo-Json"))
inv["cpu"] = sect("Win32_Processor", ps(
"Get-CimInstance Win32_Processor | Select-Object Name,NumberOfCores,NumberOfLogicalProcessors,"
"MaxClockSpeed,CurrentClockSpeed,L2CacheSize,L3CacheSize,LoadPercentage | ConvertTo-Json"))
inv["memory"] = sect("Win32_PhysicalMemory", ps(
"Get-CimInstance Win32_PhysicalMemory | Select-Object BankLabel,DeviceLocator,Capacity,Speed,"
"ConfiguredClockSpeed,Manufacturer,PartNumber | ConvertTo-Json"))
inv["mem_array"] = sect("Win32_PhysicalMemoryArray", ps(
"Get-CimInstance Win32_PhysicalMemoryArray | Select-Object MemoryDevices,MaxCapacityEx | ConvertTo-Json"))
inv["mem_load"] = sect("Win32_OperatingSystem", ps(
"[math]::Round(100*((Get-CimInstance Win32_OperatingSystem).TotalVisibleMemorySize-"
"(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory)/"
"(Get-CimInstance Win32_OperatingSystem).TotalVisibleMemorySize,1)"))
inv["physical_disks"] = sect("Get-PhysicalDisk", ps(
"Get-PhysicalDisk | Select-Object FriendlyName,MediaType,BusType,Size,HealthStatus,"
"OperationalStatus | ConvertTo-Json"))
inv["disk_reliability"] = sect("Get-StorageReliabilityCounter(可能需提权)", ps(
"Get-PhysicalDisk | ForEach-Object { $c=$_ | Get-StorageReliabilityCounter;"
"[pscustomobject]@{disk=$_.FriendlyName;temp_c=$c.Temperature;wear=$c.Wear;"
"read_err=$c.ReadErrorsTotal;write_err=$c.WriteErrorsTotal;power_on_hours=$c.PowerOnHours} } | ConvertTo-Json"))
inv["volumes"] = sect("Win32_LogicalDisk(DriveType=3)", ps(
"Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' | Select-Object DeviceID,VolumeName,"
"@{n='size_gb';e={[math]::Round($_.Size/1GB,1)}},@{n='free_gb';e={[math]::Round($_.FreeSpace/1GB,1)}} | ConvertTo-Json"))
inv["gpu"] = sect("Win32_VideoController", ps(
"Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,DriverVersion,DriverDate,"
"@{n='vram_gb_reported';e={[math]::Round($_.AdapterRAM/1GB,2)}} | ConvertTo-Json"))
inv["board"] = sect("Win32_BaseBoard + Win32_BIOS", ps(
"$b=Get-CimInstance Win32_BaseBoard;$f=Get-CimInstance Win32_BIOS;"
"@{board=$b.Product;vendor=$b.Manufacturer;bios=$f.SMBIOSBIOSVersion;bios_date=$f.ReleaseDate}|ConvertTo-Json"))
inv["thermal"] = sect("MSAcpi_ThermalZoneTemperature(消费级主板常不暴露 ⇒ 可能为空)", ps(
"Get-CimInstance -Namespace root/wmi -ClassName MSAcpi_ThermalZoneTemperature -ErrorAction SilentlyContinue |"
" Select-Object InstanceName,@{n='temp_c';e={[math]::Round($_.CurrentTemperature/10-273.15,1)}} | ConvertTo-Json"))
inv["net"] = sect("Get-NetAdapter(Up)", ps(
"Get-NetAdapter | Where-Object Status -eq 'Up' | Select-Object Name,InterfaceDescription,LinkSpeed,"
"MacAddress | ConvertTo-Json"))
inv["top_proc"] = sect("Get-Process by WorkingSet", ps(
"Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 12 "
"Name,Id,@{n='MB';e={[math]::Round($_.WorkingSet64/1MB,1)}} | ConvertTo-Json"))
inv["disk_free_bytes"] = sect("shutil-free", {})
try:
import shutil
# ★ 打包版:原来用 WS.drive(写死的工作区盘符);改成"用户主目录所在盘"的根,
# 否则在别的机器上会 NameError ⇒ disk_free_gb 变成 null(我在 20:55 的打包自测里抓到的)
_root = pathlib.Path(pathlib.Path.home().drive + "\\")
t, u, f = shutil.disk_usage(str(_root))
inv["disk_free_bytes"].update({"drive": str(_root), "total_gb": round(t / 1024 ** 3, 1),
"free_gb": round(f / 1024 ** 3, 1),
"used_pct": round(100 * u / t, 1)})
except Exception as e:
inv["disk_free_bytes"]["_err"] = "%s: %s" % (type(e).__name__, str(e)[:80])
return inv
def md(inv: dict) -> str:
c, h, b = inv["cpu"], inv["host"], inv["board"]
mem_mods = items(inv, "memory")
arr = inv.get("mem_array") or {}
lines = ["## 现状扫描(%s,只读)" % inv.get("t", ""), "",
"| 维度 | 读数 | 来源 |", "|---|---|---|",
"| 整机 | %s %s | %s | 开机 %s h | Win32_ComputerSystem/OS |" % (
h.get("vendor"), h.get("model"), h.get("os"), h.get("uptime_h")),
"| CPU | %s | %sC/%sT | 标称 %s MHz,当前 %s MHz,负载 %s%% | Win32_Processor |" % (
c.get("Name"), c.get("NumberOfCores"), c.get("NumberOfLogicalProcessors"),
c.get("MaxClockSpeed"), c.get("CurrentClockSpeed"), c.get("LoadPercentage")),
"| 内存 | 总 %s GB | 已用 %s%% | 插槽 %s(最高 %s GB/槽位)| Win32_PhysicalMemory/Array |" % (
h.get("ram_gb"), inv["mem_load"].get("_value"), arr.get("MemoryDevices"),
arr.get("MaxCapacityEx")),
"| 内存条 | %s | Win32_PhysicalMemory |" % (";".join(
"%s %sGB %s MT/s @%s" % (m.get("Manufacturer"), round((m.get("Capacity") or 0) / 1024 ** 3, 1),
m.get("Speed"), m.get("DeviceLocator")) for m in mem_mods) or "未测")]
for d in items(inv, "physical_disks"):
lines.append("| 物理盘 | %s | %s | %.1f GB | 健康 %s | %s | Get-PhysicalDisk |" % (
d.get("FriendlyName"), d.get("MediaType"), (d.get("Size") or 0) / 1024 ** 3,
d.get("HealthStatus"), d.get("BusType")))
for r in items(inv, "disk_reliability"):
if r.get("disk"):
lines.append("| 盘可靠性 | %s | 温度 %s ℃ | 磨损 %s | 读/写错误 %s/%s | 通电 %s h | Get-StorageReliabilityCounter |" % (
r.get("disk"), r.get("temp_c"), r.get("wear"), r.get("read_err"), r.get("write_err"),
r.get("power_on_hours")))
for v in items(inv, "volumes"):
lines.append("| 卷 | %s %s | 总 %s GB | 余 %s GB | Win32_LogicalDisk |" % (
v.get("DeviceID"), v.get("VolumeName"), v.get("size_gb"), v.get("free_gb")))
df = inv.get("disk_free_bytes") or {}
lines.append("| 数据盘余量 | %s GB / %s GB(已用 %s%%)| shutil.disk_usage |" % (
df.get("free_gb"), df.get("total_gb"), df.get("used_pct")))
for g in items(inv, "gpu"):
if g.get("Name"):
lines.append("| GPU | %s | 驱动 %s | 报告显存 %s GB(**Win32 的 AdapterRAM 有 4GB 截断,>4GB 不可信**)| Win32_VideoController |" % (
g.get("Name"), g.get("DriverVersion"), g.get("vram_gb_reported")))
lines.append("| 主板/固件 | %s %s | BIOS %s | Win32_BaseBoard/BIOS |" % (
b.get("vendor"), b.get("board"), b.get("bios")))
tz = [z for z in items(inv, "thermal") if z.get("temp_c") is not None]
lines.append("| 温度 | %s | MSAcpi_ThermalZoneTemperature |" % (
";".join("%s %s℃" % (z.get("InstanceName"), z.get("temp_c")) for z in tz) if tz
else "**未测**(本机不暴露该接口;消费级主板常见)"))
for n in items(inv, "net"):
if n.get("Name"):
lines.append("| 网卡 | %s | %s | %s | Get-NetAdapter |" % (
n.get("Name"), n.get("InterfaceDescription"), n.get("LinkSpeed")))
return "\n".join(lines)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--md", action="store_true")
a = ap.parse_args()
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
inv = collect()
OUT_DIR.mkdir(parents=True, exist_ok=True)
ts = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
p1 = OUT_DIR / ("ai_pc_inventory_%s.json" % ts)
p2 = OUT_DIR / "ai_pc_inventory_latest.json"
for p in (p1, p2):
p.write_text(json.dumps(inv, ensure_ascii=False, indent=1), encoding="utf-8")
ok = json.loads(p1.read_text(encoding="utf-8")).get("t") == inv["t"]
print(json.dumps({"ok": ok, "readback_ok": ok, "file": str(p1),
"cpu": inv["cpu"].get("Name"), "cores": inv["cpu"].get("NumberOfCores"),
"ram_gb": inv["host"].get("ram_gb"), "mem_load_pct": inv["mem_load"].get("_value"),
"mem_modules": len(items(inv, "memory")), "mem_slots": inv["mem_array"].get("MemoryDevices"),
"disks": len(items(inv, "physical_disks")), "volumes": len(items(inv, "volumes")),
"gpu": (items(inv, "gpu") or [{}])[0].get("Name"),
"gpu_vram_gb_reported": (items(inv, "gpu") or [{}])[0].get("vram_gb_reported"),
"thermal": len([z for z in items(inv, "thermal") if z.get("temp_c") is not None]),
"disk_free_gb": (inv.get("disk_free_bytes") or {}).get("free_gb"),
"err_sections": [k for k, v in inv.items() if isinstance(v, dict) and v.get("_err")]},
ensure_ascii=False))
if a.md:
print("\n" + md(inv))
return 0 if ok else 4
if __name__ == "__main__":
raise SystemExit(main())
怎么用
py -3.12 -X utf8 ai_pc_inventory.py # 体检,落一份 JSON
py -3.12 -X utf8 ai_pc_inventory.py --md # 附带一张 Markdown 表格
py -3.12 -X utf8 net_link_check.py # 只看网卡那一项
它不做什么
不改设置、不装东西、不动注册表/驱动/服务、不上传任何数据(全在本机跑完,结果写成本机 JSON)。 只读是故意的——只有只读的工具,才敢让人随手跑。
附:README(工具包里的说明,一并放上)
Windows 电脑体检工具包 v1
两个只读脚本,帮你看清三件事:内存够不够、盘快满没满、网卡为什么慢。
它不做什么(先看这条)
- 不改任何设置、不装任何东西、不动注册表/驱动/服务;
- 不上传任何数据:全部在本机跑完,结果写成本机 JSON 文件;
- 不是跑分工具,也不预测"该买哪台电脑"。
环境
- Windows 10 / 11;
- Python 3.8+(只用标准库;部分读数通过 PowerShell 的 CIM 接口取)。
用法(两条命令)
py -3.12 -X utf8 ai_pc_inventory.py # 体检:CPU/内存/硬盘/显卡/主板/网卡/进程
py -3.12 -X utf8 ai_pc_inventory.py --md # 追加打印一张 Markdown 表格,方便贴给别人看
py -3.12 -X utf8 net_link_check.py # 单独查"千兆网卡为什么只跑百兆"
产物:%PC_CHECK_OUT% 或 ~/pc_check_reports/ 下的 ai_pc_inventory_<时间戳>.json(另存一份 _latest.json)。
怎么读这些数(三条最容易误读的)
- 内存占用 %:脚本用的是 Windows
GlobalMemoryStatusEx().dwMemoryLoad,与任务管理器「使用中」一致。 另一个算法(100−可用/总量)会低 5–8 个点,别拿它当"还有余量"。 - 显存(GPU 的 AdapterRAM):Windows 这个字段有 4 GB 截断,超过 4 GB 的卡会显示错值(常见 0 或 4 GB)。 要准确显存请用厂商工具;本脚本照样打印,但不要拿它下结论。
- "未测"不是 0:温度(
MSAcpi_ThermalZoneTemperature)和 SSD 磨损(Get-StorageReliabilityCounter) 在很多机器上读不到——脚本会明确写_err或未测,不会写 0 冒充。 想读到这两项通常需要管理员权限,或换用厂商工具。
每个数都带来源(可复核)
JSON 里每一段都有 source 字段写明取自哪个 CIM 类/命令,例如
Win32_PhysicalMemory(内存条)、Get-PhysicalDisk(物理盘)、Get-NetAdapter(网卡)。
看见哪个数可疑,就照着 source 去复跑那一条命令。
已知边界
- 需要管理员权限才能读的项(SSD 磨损、部分温度)在普通权限下会显示"未测";
- 多显卡机器会把核显/独显/虚拟显示器都列出来,不要把它们相加当显存;
- 网卡那一项只判断"链路速率/是否自动协商",不测实际网速(想测网速请用测速站点)。
来源
由 DSH(本机 AI 值守)生成,可自由使用与修改。 它是"把电脑改造成更适合 AI 运行"那套流程里的第一步:现状扫描。