235 lines
12 KiB
Python
235 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
systeminfo.py - 将你给的 Bash 信息展示脚本等效地实现为 Python 版本。
|
||
依赖:仅使用系统命令(ip、df、free、uptime、last、df 等),在没有命令时会尽量降级显示。
|
||
|
||
此脚本会收集并以彩色终端输出显示系统信息,包括:
|
||
- 系统概览(主机名、操作系统、内核、运行时间、负载)
|
||
- 硬件资源(CPU 型号与核数、CPU 温度、内存、磁盘分区)
|
||
- 网络连接(活动接口、IPv4/IPv6、MAC)
|
||
- 活动与统计(最近登录、已安装软件包数量、当前时间)
|
||
|
||
实现细节:尽量使用标准系统工具获取信息;当某些工具不可用时,会尝试替代命令或回退为默认值。
|
||
"""
|
||
|
||
import subprocess
|
||
import shutil
|
||
import os
|
||
import re
|
||
from datetime import datetime
|
||
from typing import List
|
||
|
||
# -------------------- 颜色 --------------------
|
||
NONE = '\033[00m'
|
||
BOLD = '\033[1m'
|
||
TITLE_COLOR = '\033[01;36m' # 亮青色
|
||
INFO_COLOR = '\033[01;33m' # 亮黄色
|
||
VALUE_COLOR = '\033[00;37m' # 白色
|
||
LABEL_COLOR = '\033[00;32m' # 绿色
|
||
SUBTLE_COLOR = '\033[01;30m' # 深灰色
|
||
BLUE_COLOR = '\033[01;34m' # 亮蓝色
|
||
ORANGE_COLOR = '\033[01;33m' # 同 INFO_COLOR,用于 16 色终端
|
||
|
||
# -------------------- 运行本地系统命令 --------------------
|
||
def run_cmd(cmd: str) -> str:
|
||
"""运行一个 shell 命令并返回其标准输出(字符串)。错误时返回空字符串。"""
|
||
try:
|
||
out = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, text=True)
|
||
return out.strip()
|
||
except Exception:
|
||
return ""
|
||
|
||
def has_cmd(name: str) -> bool:
|
||
"""检查命令是否存在于 PATH 中。"""
|
||
return shutil.which(name) is not None
|
||
|
||
# -------------------- ASCII 艺术头部 --------------------
|
||
def print_header() -> None:
|
||
"""打印带颜色的 ASCII 艺术化标题和分隔线。"""
|
||
print(f"{SUBTLE_COLOR}─────────────────────────────────────────────────────────────{NONE}")
|
||
print(f"{ORANGE_COLOR} _____ _ _ {NONE}")
|
||
print(f"{ORANGE_COLOR} | __ \\ | | (_) {NONE}")
|
||
print(f"{ORANGE_COLOR} | | | | ___ | |__ _ __ _ _ __ {NONE}")
|
||
print(f"{ORANGE_COLOR} | | | | / _ \\| '_ \\ | | / _` || '_ \\ {NONE}")
|
||
print(f"{ORANGE_COLOR} | |__| || __/| |_) || || (_| || | | |{NONE}")
|
||
print(f"{ORANGE_COLOR} |_____/ \\___||_.__/ |_| \\__,_||_| |_|{NONE}")
|
||
print()
|
||
print(f"{SUBTLE_COLOR}─────────────────────────────────────────────────────────────{NONE}")
|
||
|
||
# -------------------- 系统概览 --------------------
|
||
def system_overview() -> None:
|
||
"""收集并打印系统概览信息。"""
|
||
print(f"{TITLE_COLOR}{BOLD}系统概览{NONE}")
|
||
hostname = run_cmd("hostname") or "N/A"
|
||
os_info = run_cmd("lsb_release -ds") or run_cmd("cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '\"'") or "N/A"
|
||
kernel = run_cmd("uname -sr") or "N/A"
|
||
uptime = run_cmd("uptime -p").replace("up ", "") if has_cmd("uptime") else "N/A"
|
||
loadavg = run_cmd("uptime | awk -F'load average: ' '{print $2}'") or "N/A"
|
||
|
||
print(f" {LABEL_COLOR}系统名称:{NONE} {VALUE_COLOR}{hostname}{NONE}")
|
||
print(f" {LABEL_COLOR}操作系统:{NONE} {VALUE_COLOR}{os_info}{NONE}")
|
||
print(f" {LABEL_COLOR}内核版本:{NONE} {VALUE_COLOR}{kernel}{NONE}")
|
||
print(f" {LABEL_COLOR}运行时间:{NONE} {VALUE_COLOR}{uptime}{NONE}")
|
||
print(f" {LABEL_COLOR}平均负载:{NONE} {VALUE_COLOR}{loadavg}{NONE}")
|
||
print(f"{SUBTLE_COLOR}─────────────────────────────────────────────────────────────{NONE}")
|
||
|
||
# -------------------- 硬件资源 --------------------
|
||
def hardware_resources() -> None:
|
||
"""收集并打印硬件资源信息(CPU、内存、存储等)。"""
|
||
print(f"{TITLE_COLOR}{BOLD}硬件资源{NONE}")
|
||
# CPU 核心数与型号
|
||
cpu_model = "N/A"
|
||
cores: int | str = "N/A"
|
||
if os.path.exists("/proc/cpuinfo"):
|
||
try:
|
||
with open("/proc/cpuinfo", "r", encoding="utf-8", errors="ignore") as f:
|
||
data = f.read()
|
||
m = re.search(r"model name\s+:\s+(.+)", data)
|
||
if m:
|
||
cpu_model = m.group(1).strip()
|
||
cores = len(re.findall(r"^processor\s+:", data, flags=re.M))
|
||
except Exception:
|
||
pass
|
||
else:
|
||
cpu_model = run_cmd("uname -p") or cpu_model
|
||
cores = run_cmd("nproc") or cores
|
||
|
||
# CPU 温度(可选,通过 sensors 命令)
|
||
cpu_temp = ""
|
||
if has_cmd("sensors"):
|
||
sensors_out = run_cmd("sensors")
|
||
m = re.search(r"(\+\d+\.\d+°C)", sensors_out)
|
||
if m:
|
||
cpu_temp = f"({INFO_COLOR}{m.group(1)}{VALUE_COLOR})"
|
||
|
||
# 内存
|
||
mem_total = run_cmd("free -g | awk 'NR==2{print $2\"GB\"}'") or "N/A"
|
||
mem_used_pct = run_cmd("free -m | awk 'NR==2{printf \"%.1f%%\", $3*100/$2 }'") or "N/A"
|
||
mem_avail = run_cmd("free -m | awk 'NR==2{print $7\"MB\"}'") or "N/A"
|
||
|
||
print(f" {LABEL_COLOR}中央处理器 (CPU):{NONE}")
|
||
cores_str = str(cores)
|
||
print(f" {VALUE_COLOR}{cpu_model} - {cores_str} 核心 {cpu_temp}{NONE}")
|
||
print(f" {LABEL_COLOR}内存 (RAM):{NONE}")
|
||
print(f" {VALUE_COLOR}总计: {INFO_COLOR}{mem_total}{VALUE_COLOR} | 已用: {INFO_COLOR}{mem_used_pct}{VALUE_COLOR} | 可用: {INFO_COLOR}{mem_avail}{NONE}")
|
||
|
||
# 存储
|
||
print(f" {LABEL_COLOR}存储空间:{NONE}")
|
||
if has_cmd("df"):
|
||
df_cmd = "df -hT -x tmpfs -x devtmpfs -x devpts -x proc -x sysfs -x cgroup -x fusectl -x securityfs -x pstore -x efivarfs -x autofs 2>/dev/null | awk 'NR>1 {print $1, $2, $3, $4, $6, $7}'"
|
||
df_out = run_cmd(df_cmd)
|
||
if df_out:
|
||
for line in df_out.splitlines():
|
||
parts = line.split(None, 5)
|
||
if len(parts) == 6:
|
||
device, ftype, size, used, percent, mount = parts
|
||
print(f" {INFO_COLOR}{mount}{NONE} ({VALUE_COLOR}{device} - {ftype}{NONE})")
|
||
print(f" {LABEL_COLOR}总:{NONE} {BLUE_COLOR}{size}{NONE}, {LABEL_COLOR}已用:{NONE} {BLUE_COLOR}{used}{NONE} ({VALUE_COLOR}{percent}{NONE})")
|
||
else:
|
||
print(f" {VALUE_COLOR}无法读取磁盘信息{NONE}")
|
||
else:
|
||
print(f" {VALUE_COLOR}df 命令不可用{NONE}")
|
||
|
||
print(f"{SUBTLE_COLOR}─────────────────────────────────────────────────────────────{NONE}")
|
||
|
||
# -------------------- 网络相关信息 --------------------
|
||
def network_info() -> None:
|
||
"""收集并打印网络接口与 IP 信息。"""
|
||
print(f"{TITLE_COLOR}{BOLD}网络连接{NONE}")
|
||
interfaces: List[str] = []
|
||
ip_out = run_cmd("ip -o link show up")
|
||
if ip_out:
|
||
for line in ip_out.splitlines():
|
||
m = re.match(r"\d+:\s+([^:@]+)", line)
|
||
if m:
|
||
interfaces.append(m.group(1))
|
||
else:
|
||
try:
|
||
interfaces = [i for i in os.listdir("/sys/class/net")]
|
||
except Exception:
|
||
interfaces = []
|
||
|
||
has_ip = False
|
||
for iface in interfaces:
|
||
if iface == "lo":
|
||
continue
|
||
ip_addrs = run_cmd(f"ip -4 addr show dev {iface} 2>/dev/null | grep -oP 'inet \\K[\\d.]+' | tr '\\n' ' '").strip()
|
||
ip6 = run_cmd(f"ip -6 addr show dev {iface} 2>/dev/null | grep -oP 'inet6 \\K[0-9a-fA-F:]+' | grep -ivE '^fe80::' | head -n1 | tr '\\n' ' '").strip()
|
||
mac = run_cmd(f"ip link show dev {iface} 2>/dev/null | awk '/ether/ {{print $2}}'").strip()
|
||
if ip_addrs or ip6:
|
||
has_ip = True
|
||
iface_label = ""
|
||
if re.match(r'^(wlan|wlp|ath|ra)', iface):
|
||
iface_label = f"({LABEL_COLOR}WiFi{NONE}) "
|
||
elif re.match(r'^(eth|enp|eno)', iface):
|
||
iface_label = f"({LABEL_COLOR}有线{NONE}) "
|
||
elif re.match(r'^(docker|br-|veth|tun|tap|virbr)', iface):
|
||
iface_label = f"({LABEL_COLOR}虚拟{NONE}) "
|
||
else:
|
||
br = run_cmd(f"ip link show {iface} | grep -i bridge || true")
|
||
if br:
|
||
iface_label = f"({LABEL_COLOR}桥接{NONE}) "
|
||
|
||
print(f" {INFO_COLOR}{BOLD}{iface}{NONE} {iface_label}")
|
||
if ip_addrs:
|
||
print(f" {LABEL_COLOR}IPv4:{NONE} {VALUE_COLOR}{ip_addrs.strip()}{NONE}")
|
||
if ip6:
|
||
print(f" {LABEL_COLOR}IPv6:{NONE} {VALUE_COLOR}{ip6.strip()}{NONE}")
|
||
if mac:
|
||
print(f" {LABEL_COLOR}MAC:{NONE} {VALUE_COLOR}{mac}{NONE}")
|
||
|
||
if not has_ip:
|
||
print(f" {VALUE_COLOR}未检测到活动的网络连接或IP地址{NONE}")
|
||
print(f"{SUBTLE_COLOR}─────────────────────────────────────────────────────────────{NONE}")
|
||
|
||
# -------------------- 活动与统计 --------------------
|
||
def activity_stats() -> None:
|
||
"""收集并打印用户活动与系统统计信息(最近登录、包数量等)。"""
|
||
print(f"{TITLE_COLOR}{BOLD}活动与统计{NONE}")
|
||
last_login_info = run_cmd("last -n 1 -wFai 2>/dev/null | head -n 1")
|
||
last_login_display = ""
|
||
if not last_login_info or "wtmp begins" in last_login_info or len(last_login_info.split()) < 5:
|
||
last_login_display = f"{VALUE_COLOR}无先前登录记录或记录格式异常{NONE}"
|
||
else:
|
||
parts = last_login_info.split()
|
||
user = parts[0] if len(parts) > 0 else "N/A"
|
||
from_host = parts[2] if len(parts) > 2 else "N/A"
|
||
time_fields = " ".join(parts[3:7]) if len(parts) >= 7 else " ".join(parts[3:])
|
||
try:
|
||
formatted = datetime.strptime(time_fields + " " + str(datetime.now().year), "%b %d %H:%M %Y").strftime("%Y-%m-%d %H:%M:%S")
|
||
except Exception:
|
||
formatted = f"{time_fields} (raw)"
|
||
last_login_display = f"{LABEL_COLOR}用户:{NONE} {INFO_COLOR}{user}{NONE}, {LABEL_COLOR}来自:{NONE} {INFO_COLOR}{from_host}{NONE}, {LABEL_COLOR}时间:{NONE} {VALUE_COLOR}{formatted}{NONE}"
|
||
|
||
package_count = "N/A"
|
||
package_label = ""
|
||
if has_cmd("dpkg-query"):
|
||
package_count = run_cmd("dpkg-query -f '${Package}\\n' -W 2>/dev/null | wc -l") or "N/A"
|
||
package_label = "(Debian/APT)"
|
||
elif has_cmd("rpm"):
|
||
package_count = run_cmd("rpm -qa 2>/dev/null | wc -l") or "N/A"
|
||
package_label = "(RPM/Yum/Dnf)"
|
||
elif has_cmd("pacman"):
|
||
package_count = run_cmd("pacman -Qq 2>/dev/null | wc -l") or "N/A"
|
||
package_label = "(Pacman)"
|
||
else:
|
||
package_label = "(未知包管理器)"
|
||
|
||
current_dt = datetime.now().strftime("%Y年%m月%d日 %A %H:%M:%S")
|
||
print(f" {LABEL_COLOR}上次登录:{NONE} {last_login_display}")
|
||
print(f" {LABEL_COLOR}软件包数:{NONE} {VALUE_COLOR}{package_count} {package_label}{NONE}")
|
||
print(f" {LABEL_COLOR}当前登录时间:{NONE} {VALUE_COLOR}{current_dt}{NONE}")
|
||
print(f"{SUBTLE_COLOR}─────────────────────────────────────────────────────────────{NONE}")
|
||
|
||
# -------------------- 主程序 --------------------
|
||
def main() -> None:
|
||
print_header()
|
||
system_overview()
|
||
hardware_resources()
|
||
network_info()
|
||
activity_stats()
|
||
|
||
if __name__ == "__main__":
|
||
main()
|