定制speedtest-cli:实现指定服务器测速与自动化监控 1. 项目概述为什么需要定制你的测速工具如果你经常用speedtest-cli这个命令行工具来测试网络带宽大概率会遇到一个不大不小的烦恼它自动选择的测速服务器可能离你十万八千里或者负载高得离谱导致测出来的数据跟你的实际体验完全对不上。官方提供的那个--list参数虽然能列出服务器但每次都要手动去翻找那一长串的 ID再复制粘贴到命令里效率低不说还容易出错。更别提在一些自动化脚本或者监控任务里你肯定希望每次都能稳定地测试同一个或同一组服务器这样才能得到有对比价值的趋势数据。所以这个项目的核心诉求就非常明确了改造原生的speedtest-cli让它能方便、稳定地支持指定服务器进行测速。这不仅仅是加个参数那么简单它涉及到对工具底层工作机制的理解、对网络请求流程的干预以及最终打造一个更贴合个人或生产环境需求的专属工具。无论是为了获得更准确的本地网络质量评估还是为了在运维监控中建立稳定的基准线这个修改都极具实用价值。接下来我会带你一步步拆解实现过程并分享我在这个过程中踩过的坑和总结的经验。2. 核心思路与方案选型2.1 理解 speedtest-cli 的工作机制在动手修改之前我们必须先搞清楚speedtest-cli是怎么工作的。它的流程大致可以拆解为以下几个步骤获取服务器列表工具启动后首先会向 Speedtest 的官方 API 端点例如www.speedtest.net/api/js/servers发起请求获取一个包含全球数千个测速服务器的 JSON 列表。这个列表里包含了每个服务器的 ID、名称、赞助商、地理位置经纬度、主机名等信息。计算距离并排序speedtest-cli会尝试获取你本机的公网 IP 和粗略的地理位置信息通常通过 IP 地理定位服务。然后它计算列表中每一个服务器与你本地位置之间的地理距离并按照从近到远的顺序进行排序。延迟测试与服务器选择默认情况下工具会向前 10 个数量可配置距离最近的服务器发送 ICMP Ping 或 HTTP 请求测量延迟。最终它会选择延迟最低的那个服务器作为本次测速的目标。执行带宽测试与选定的服务器建立连接分别进行下载和上传测试。下载测试是客户端从服务器拉取数据块上传测试则是客户端向服务器推送数据块。通过计算单位时间内传输的数据量得出带宽值。我们的修改目标主要就是干预第 2 步和第 3 步。我们需要绕过自动的距离计算和延迟筛选直接让工具使用我们预先指定好的服务器。2.2 修改方案对比要实现“指定服务器”有几种不同粒度的方案各有优劣方案一命令行参数指定服务器ID思路增加一个如--server-id或--select的参数允许用户直接输入从--list中看到的服务器数字 ID。优点实现相对简单对原工具流程改动最小。只需要在获取服务器列表后跳过自动选择逻辑直接根据提供的 ID 查找对应的服务器对象即可。缺点用户体验不够友好。用户需要先执行一次speedtest-cli --list来查找和记录 ID这个列表可能非常长查找困难。且 ID 是数字不易记忆。方案二配置文件指定服务器信息思路允许用户在一个配置文件如~/.config/speedtest-cli/servers.conf中以更易读的方式如城市名、赞助商、主机名片段定义一组优先或默认的服务器。优点一劳永逸设置一次即可。特别适合自动化场景脚本无需再处理服务器 ID。可以配置多个服务器工具按顺序尝试或随机选择。缺点实现复杂度较高。需要解析配置文件并实现一套从用户定义的“易读标识”到官方服务器列表中具体服务器对象的匹配逻辑。匹配规则可能需要支持模糊匹配、正则表达式等。方案三交互式选择增强思路在--list的基础上进行增强例如支持过滤--list | grep “China Telecom”或者实现一个交互式的 TUI文本用户界面让用户可以通过方向键和搜索来更方便地选择。优点用户体验极佳无需记忆 ID 或编辑配置。缺点改动量最大可能涉及引入额外的依赖库如urwid用于 TUI。更偏向于前端交互的改进而非核心测速逻辑的定制。我的选择与理由 对于大多数追求实用和稳定的场景我推荐方案一和方案二的结合。即优先实现通过命令行参数直接指定 ID 的核心功能方案一因为它最直接、最可靠。在此基础上可以额外实现一个简单的“服务器别名”功能方案二的简化版例如允许用户在一个简单的文本文件里将ID1234映射为别名Shanghai_CT然后在命令行中使用--server Shanghai_CT。这样既满足了快速指定的需求又通过别名机制解决了 ID 难记的问题实现复杂度可控。注意直接修改speedtest-cli的源码意味着你需要维护一个自己的分支。当官方版本更新时你可能需要手动合并更改。这是一个需要考虑的维护成本。3. 动手修改代码层面的实现细节这里我以 Python 版本的speedtest-cli这是最常见的一个版本为例进行修改。请先确保你本地有它的源码。3.1 定位关键代码段首先我们需要找到负责服务器选择的核心函数。在speedtest.py这个主文件中搜索get_best_server或choose_server这类函数名。通常逻辑是这样的def get_best_server(self, servers): 从服务器列表中根据延迟选择最佳服务器。 results {} # ... 这里会有一堆代码对 servers 列表中的前N个进行延迟测试 ... # 测试完成后对结果按延迟排序 sorted_results sorted(results.items(), keylambda x: x[1]) # 返回延迟最低的服务器信息 best sorted_results[0] return best我们的目标就是绕过这个函数或者修改它的行为。3.2 实现--server-id参数第一步在命令行参数解析部分通常是main()函数开头或者一个专门的parse_args()函数里添加新的参数。找到类似下面的代码块可能使用argparse库parser argparse.ArgumentParser(description...) parser.add_argument(--list, actionstore_true, helpDisplay a list of speedtest.net servers.) # ... 其他已有参数 ...在其中添加parser.add_argument(--server-id, typeint, helpSpecify a server ID to test against, bypassing automatic selection.)第二步修改主逻辑。在main()函数中找到调用get_best_server的地方。通常逻辑是如果用户用了--list就列出服务器并退出否则获取服务器列表然后选择最佳服务器。我们需要在这里插入判断def main(): args parse_args() # 初始化 speedtest 对象 s Speedtest() # 获取服务器列表 s.get_servers() # 如果指定了 --server-id if args.server_id: # 在服务器列表中查找该ID selected_server None for server in s.servers: # 注意s.servers 的数据结构可能需要查看源码确认 # 通常 servers 是一个字典键是服务器ID if str(server[id]) str(args.server_id): selected_server server break if not selected_server: print(f错误未找到 ID 为 {args.server_id} 的服务器。) sys.exit(1) # 手动设置当前测试的服务器 s.servers {selected_server[id]: [selected_server]} # 调整数据结构以匹配原程序期望的格式 s.best selected_server # 将选中的服务器设为“最佳” print(f已选定服务器{selected_server[sponsor]} ({selected_server[name]}) [ID: {selected_server[id]}]) else: # 原有的自动选择逻辑 s.get_best_server() # 后续的下载/上传测试逻辑通常直接使用 s.best s.download() s.upload() s.results.share() s.results.print()关键点解析s.servers的数据结构需要仔细查看源码。它可能是一个列表也可能是一个以 ID 为键的字典值可能是包含单个服务器字典的列表。你需要根据实际情况调整selected_server的查找和赋值逻辑。直接设置s.best很重要因为后续的download()和upload()方法很可能直接引用这个属性。原有的get_best_server()方法里可能包含延迟测试和结果打印我们绕过它之后最好手动打印一条信息告知用户当前使用的是哪个服务器。3.3 实现简易的服务器别名配置进阶如果你想更进一步实现一个别名系统可以这样做定义配置文件格式创建一个简单的文本文件比如~/.speedtest-servers。# 格式别名 服务器ID home_cn 12345 office_us 54321 hk_cmcc 67890添加--server参数parser.add_argument(--server, typestr, helpTest against a server defined by alias in config file.)在代码中加载和解析配置def load_server_config(config_path~/.speedtest-servers): servers_map {} path os.path.expanduser(config_path) if os.path.exists(path): with open(path, r) as f: for line in f: line line.strip() if line and not line.startswith(#): if in line: alias, server_id line.split(, 1) servers_map[alias.strip()] server_id.strip() return servers_map def main(): args parse_args() server_config load_server_config() s Speedtest() s.get_servers() target_server_id None # 优先级--server-id 直接 --server 别名 自动选择 if args.server_id: target_server_id args.server_id elif args.server: if args.server in server_config: target_server_id int(server_config[args.server]) else: print(f错误别名 {args.server} 未在配置文件中定义。) sys.exit(1) if target_server_id: # ... 使用上面实现的根据 ID 查找服务器的逻辑 ... pass else: # 原有的自动选择逻辑 s.get_best_server() # ... 后续测试逻辑 ...这样你就可以使用speedtest-cli --server home_cn这样的命令了远比记数字 ID 方便。4. 测试与验证你的修改修改完成后务必进行全面的测试确保功能正常且没有引入新的问题。4.1 功能测试清单基础功能不添加任何新参数运行python speedtest.py确保原有的自动选择、测速功能完全正常。这是底线。列表功能运行python speedtest.py --list确认服务器列表能正常显示并记下几个你想测试的服务器 ID。指定ID功能python speedtest.py --server-id 有效ID应该直接使用你指定的服务器开始测速并打印出该服务器信息。对比--list中的信息确认一致。python speedtest.py --server-id 无效ID应该给出清晰的错误提示并退出而不是崩溃或去测试其他服务器。别名功能如果实现创建配置文件并添加别名。python speedtest.py --server 有效别名应该能正确映射并测试。python speedtest.py --server 无效别名应提示未定义。参数优先级如果同时实现了--server-id和--server测试当两者同时提供时是否按照你设计的优先级例如 ID 优先执行。测速结果合理性使用修改后的工具分别测试一个距离很近的服务器和一个距离很远的服务器。结果应该体现出明显的差异通常远距离服务器的延迟更高带宽可能更低。这能验证你的指定是否真的生效了。4.2 一个常见的坑服务器列表数据结构我在修改时遇到的最大坑就是s.servers的数据结构。不同版本、甚至不同分支的speedtest-cli这个数据结构可能不同。早期版本可能是一个简单的服务器字典列表而较新的版本可能是一个嵌套字典例如# 可能的结构1 s.servers [ {id: 1234, sponsor: Provider A, ...}, {id: 5678, sponsor: Provider B, ...}, ] # 可能的结构2 s.servers { 1234: [{id: 1234, sponsor: Provider A, ...}], 5678: [{id: 5678, sponsor: Provider B, ...}], }排查技巧在修改代码前先写几行调试代码或者直接使用 Python 交互环境打印出s.servers的类型和前面几个元素看看。确保你的查找逻辑 (for server in s.servers) 能正确地遍历到每一个服务器字典。如果s.servers是结构2你可能需要这样遍历for server_id, server_list in s.servers.items(): server server_list[0] # 通常列表里只有一个元素 if server[id] args.server_id: # 找到目标5. 扩展应用与自动化场景修改后的speedtest-cli威力在于其可预测性和可脚本化。下面分享几个实用的应用场景。5.1 集成到监控系统如 Zabbix, Prometheus你可以编写一个简单的 Shell 或 Python 脚本定期使用固定服务器进行测速并将结果延迟、下载速度、上传速度输出为监控系统可以抓取的格式如 JSON或者直接打印数值。示例脚本speedtest_monitor.sh#!/bin/bash # 定义你的目标服务器ID TARGET_SERVER_ID12345 # 运行修改后的 speedtest-cli使用 --json 参数如果原版支持输出机器可读格式 # 如果不支持 --json可能需要用 grep/sed/awk 解析文本输出 OUTPUT$(python /path/to/modified_speedtest.py --server-id $TARGET_SERVER_ID --json 2/dev/null) # 假设 --json 输出是一个 JSON 对象 # 使用 jq 解析 PING$(echo $OUTPUT | jq .ping) DOWNLOAD$(echo $OUTPUT | jq .download) UPLOAD$(echo $OUTPUT | jq .upload) # 输出给监控代理抓取 echo speedtest.ping $PING echo speedtest.download $DOWNLOAD echo speedtest.upload $UPLOAD然后让 Zabbix Agent 或 Node Exporter 的textfile收集器来抓取这个脚本的输出。这样你就能在 Grafana 上绘制出从你的网络到某个特定机房比如你的云服务器所在机房的长期带宽质量图表。5.2 多服务器轮询测试如果你关心到多个关键站点的网络质量可以写一个脚本轮询测试。示例 Python 脚本#!/usr/bin/env python3 import subprocess import json import time servers_to_test [ {id: 12345, name: 北京联通}, {id: 23456, name: 上海电信}, {id: 34567, name: 广州移动}, ] for server in servers_to_test: print(f\n 开始测试 {server[name]} (ID: {server[id]}) ) try: # 调用修改后的 speedtest-cli假设它支持 --json cmd [python, speedtest.py, --server-id, str(server[id]), --json] result subprocess.run(cmd, capture_outputTrue, textTrue, timeout120) if result.returncode 0: data json.loads(result.stdout) print(f延迟: {data.get(ping, N/A)} ms) print(f下载: {data.get(download, N/A) / 1_000_000:.2f} Mbps) print(f上传: {data.get(upload, N/A) / 1_000_000:.2f} Mbps) else: print(f测试失败: {result.stderr}) except subprocess.TimeoutExpired: print(测试超时) except json.JSONDecodeError: print(输出解析失败) time.sleep(10) # 每次测试间隔10秒避免对服务器造成压力这个脚本可以放到 crontab 里每小时跑一次把结果记录到日志文件或数据库中用于长期分析网络到各个方向的稳定性。5.3 与运维流程结合在部署新服务或进行网络变更如切换 ISP、调整路由前后运行一组到核心机房的定点测速可以作为变更效果评估的量化依据。将测试命令集成到 Ansible Playbook 或 Shell 部署脚本中实现自动化验收。6. 常见问题与故障排除即使代码修改正确在实际使用中也可能遇到各种问题。这里记录一些典型情况。6.1 测速失败或速度异常低可能原因1服务器已下线或不可达。Speedtest 的服务器列表并非永远有效。有些服务器可能被赞助商移除或暂时关闭。用--list命令查看该服务器的详细信息或者尝试在浏览器中访问 Speedtest 网站手动选择同一提供商和地点看是否能正常测试。可能原因2网络路径问题。你到指定服务器之间的网络路由可能拥塞或存在问题。尝试更换另一个同区域或不同运营商的服务器 ID 进行对比测试。可能原因3服务器负载过高。免费的 Speedtest 服务器资源有限在高峰时段可能满载。可以尝试在非高峰时段测试或者选择列表中更冷门的服务器。排查命令在测试前可以先手动ping一下服务器的主机名在--list的输出里有看看基础连通性和延迟是否正常。6.2 修改后的脚本报错AttributeError典型错误AttributeError: Speedtest object has no attribute best或servers。原因你修改代码时引用了一个错误的属性名或者官方库在新版本中更改了内部变量名。解决仔细阅读你正在修改的版本的源码。使用打印语句print(dir(s))在运行时查看Speedtest对象实际拥有的属性和方法。确保你的赋值和引用与源码中其他部分的用法保持一致。6.3 如何更新官方版本并保留修改这是一个典型的源码合并问题。使用 Git如果你是在 Git 仓库中修改的这是最好的情况。将官方仓库添加为远程上游 (git remote add upstream 官方仓库URL)。当官方更新时先拉取你的修改到本地分支然后拉取上游更新 (git fetch upstream)最后在本地分支上执行变基 (git rebase upstream/main) 或合并 (git merge upstream/main)。Git 会尝试自动合并冲突需要手动解决。手动合并如果没有用 Git你需要下载新版本的源码然后使用 diff/merge 工具如diff -u old_file.py new_file.py或者 Meld、Beyond Compare 等图形工具对比新旧版本将你的修改逻辑重新应用到新文件上。这比较繁琐但能让你理解代码的变化。6.4 性能与超时问题默认的speedtest-cli可能有全局超时设置。当你指定一个地理上非常遥远或响应慢的服务器时下载/上传测试阶段可能会超时。查看源码在speedtest.py中搜索timeout。你可能需要增加download_timeout和upload_timeout的值。修改方式可以在初始化Speedtest对象后直接设置其属性例如s.download_timeout 60单位秒。更好的方式是将超时作为新的命令行参数暴露出来。经过以上步骤你应该已经拥有了一个可以自由指定测速服务器的强大工具。这个改造过程本身也是一次对开源工具进行定制化以满足特定需求的经典实践。记住核心是理解工具的工作原理然后精准地介入其工作流程。