☰
Python Telnet自动化:远程连接设备、执行命令与避坑实战
2026/10/17 9:52:48 网站建设 项目流程

很多人一听到telnet,第一反应是“这都什么年代了还在用这个”。但我在实际维护环境里发现,Python通过telnet远程连接设备依然是很多自动化脚本绕不开的一环:老网络设备、嵌入式板卡、机房带外管理口、一些只开放telnet的仪器设备……在这些场景里,用Python写一个自动化登录、执行命令、抓取结果的脚本,比人工复制粘贴有价值得多。这篇文章就把我常用的连接方法、交互细节、踩过的坑一次讲清楚。适合要写设备巡检、批量配置、数据采集脚本的读者,也适合刚接触自动化运维的Python新手。

1. 为什么还在用telnet,以及Python侧的整体思路

1.1 哪些场景真的需要telnet

先说一个很多人忽略的事实:telnet并没有从网络设备上消失。很多旧型号交换机、路由器、工业防火墙、嵌入式Linux板子,默认只开放23端口。SSH虽然更安全,但这类设备要么没有SSH服务端,要么固件版本太老、强制启用SSH会导致兼容性问题。尤其在一些机房带外管理网络里,telnet依然是唯一能进设备的路。

在我接触过的项目里,Python telnet脚本最常见的用途就三类:批量读取设备状态(巡检)、批量下发配置(开局)、从仪器仪表采集数据(测试自动化)。这类任务的特点是“交互固定、重复性高、量还特别大”,人工连上去敲命令既慢又容易出错,用脚本接管是自然而然的诉求。

telnet本身是明文协议,生产环境里我一般不建议把它暴露在不可信网络上。但如果网络已经做了隔离、设备又只支持telnet,那临时用一下完全合理。安全方面的取舍,我放到最后一节专门展开。

1.2 一条telnet命令在Python里怎么走

telnet协议说白了就是在TCP连接上跑一套网络虚拟终端(NVT)文本协议。服务端默认监听23端口,客户端往TCP流里写入字节,服务端往TCP流里返回文本。没有像HTTP那种“请求-响应”的头和状态码,全靠收发双方约定好的特征字符串来判断进度。

Python标准库里的telnetlib模块,就是对这套流程的封装。它在底层建立socket,帮我们处理telnet协议里的IAC协商(就是那些以0xFF开头的选项协商字节),再提供几个非常直接的操作方法:

  • write(buffer):发送字节数据
  • read_until(expected, timeout):阻塞读取,直到读到期望的字节串
  • expect(list, timeout):用正则表达式匹配多个可能的结果
  • read_very_eager():非阻塞读取缓冲区里已有的数据
  • interact():把终端控制权交给用户

打一个不太严谨但容易理解的比方:telnet是一条不打加密电话的线路,telnetlib就是帮你拨号、接通、按约定等对方说话的接线员。你在read_until里告诉它“等对方说出Password这几个字再喊我”,它就一直守着电话线,直到等到为止。这也解释了为什么写telnet脚本最关键的是搞清楚“要等什么特征字符串”,而不是“等多少秒”。

2. 基础环境与第一个可运行脚本

2.1 确认Python和telnetlib

telnetlib是Python标准库的一部分,不需要pip安装任何额外依赖。你用Python 3.6到3.12之间的版本,直接import telnetlib就能用。唯一要留心的是Python 3.13起telnetlib被移除了,届时需要用第三方库或者自己封装socket,这部分我在第5节末尾给出替代方案。

如果是临时在服务器上跑脚本,建议先确认系统里有没有装Python,再确认版本:

python3 --version python3 -c "import telnetlib; print('ok')"

能打印出ok,环境就齐了。

2.2 快速跑通:登录设备并执行命令的第一个脚本

我写了一个最小可运行的示例。连接一台远程设备、登录、执行一条命令,整个过程也就是十几行代码的事:

import telnetlib HOST = "192.0.2.1" USER = "admin" PASSWORD = "password123" PROMPT = b">" tn = telnetlib.Telnet(HOST, 23, timeout=10) tn.read_until(b"Username:", timeout=10) tn.write(USER.encode("utf-8") + b"\n") tn.read_until(b"Password:", timeout=10) tn.write(PASSWORD.encode("utf-8") + b"\n") tn.read_until(PROMPT, timeout=10) tn.write(b"display version\n") output = tn.read_until(PROMPT, timeout=15) print(output.decode("utf-8", errors="ignore")) tn.close()

逐行拆开看几个关键点:

  • telnetlib.Telnet(HOST, 23, timeout=10)会先建立TCP连接。timeout是socket层超时,如果设备IP不通或者端口没开,这里会在10秒后抛异常。
  • read_until不是在读“一行”,而是阻塞读取直到目标字节出现。它会把目标字符之前的所有内容都作为返回值的一部分。
  • write方法不会自动加换行符。很多新手第一次写脚本,命令发过去设备没反应,就是因为忘了在末尾拼\n。设备上的命令行靠回车触发执行,不带换行等于在终端里打了一串字却没按回车。
  • 用户名和密码都必须转成bytes再发。encode("utf-8")这一步别省。
  • 最后用decode("utf-8", errors="ignore")把返回的bytes转成字符串。之所以加errors忽略,是因为设备输出里很可能会混入一些telnet协议控制字节,直接解码偶尔会炸。

这个脚本跑通之后,你已经掌握了telnet自动化的核心骨架。剩下的工作,无非就是把“等待字符串”和“发命令”这两个动作组合出更复杂的逻辑。

3. 登录交互与提示符匹配的细节

3.1 read_until、expect、read_very_eager怎么配合

实际设备远比上面那个例子复杂。常见的问题包括:不同设备登录提示符不一样、有的先出login再出Password、有的直接出Username、有的输错密码不会退出而是重新弹提示符。这时候只靠一个read_until就有点不够用了。

telnetlib提供了一个更灵活的expect方法,它接收一个列表,列表里是几段正则表达式,谁先匹配上就返回谁。返回值是一个三元组:(匹配到的列表下标、match对象、匹配之前的全部数据):

import telnetlib import socket tn = telnetlib.Telnet("192.0.2.1", 23, timeout=10) tn.write(b"\n") idx, match, before = tn.expect([b"Username:", b"login:", b"Password:"], timeout=10) if idx == 0: tn.write(b"admin\n") idx, match, before = tn.expect([b"Password:", b"Login incorrect"], timeout=10) elif idx == 1: tn.write(b"admin\n") idx, match, before = tn.expect([b"Password:", b"Login incorrect"], timeout=10) else: # 设备直接要求输密码 tn.write(b"admin\n") if idx == 1: raise RuntimeError("账号密码不对,返回内容: %r" % before) tn.write(b"password123\n") idx, match, before = tn.expect([b">", b"#", b"Login incorrect"], timeout=10) if idx == 2: raise RuntimeError("密码错误")

注意这里的正则匹配是按字节流的。如果需要匹配的字符串里包含正则特殊字符,比如[、(、*,提前用re.escape处理一下更稳妥。

3.2 登录成功/失败的判断与容错

判断登录是否成功,我建议把“失败特征”和“成功特征”一起放进expect,而不是只等成功特征。这样既能快速失败,也不用等完整的超时时间。

另外一个非常容易踩的坑是:read_until在超时时通常不会抛异常,而是把当前已经收到的数据直接返回。也就是说,你写了tn.read_until(b">", timeout=10),如果10秒内没等到>,它并不会告诉你“出错了”,而是把期间收到的内容原样丢给你。如果这段内容里恰好没有任何信息,你根本不知道发生了什么。

所以严谨的写法应该是拿到返回值后主动检查关键字:

data = tn.read_until(b">", timeout=10) if b">" not in data: print("警告:没等到提示符,当前输出为:") print(data.decode("utf-8", errors="ignore"))

如果连接被远端关闭,read_until会抛EOFError,socket层异常也可能抛socket.timeout。把这些异常包进try/except,程序就不会因为一次网络抖动直接崩掉。

3.3 编码与乱码问题

telnet数据本质上是一串字节,设备回什么编码就得按什么编码解。大多数网络设备默认ASCII或UTF-8,但国产设备、Windows下的一些仪器仪表,输出很可能是GBK。遇到中文乱码,不要急着改终端,先在脚本里试编码:

def smart_decode(data): for encoding in ("utf-8", "gbk", "ascii"): try: return data.decode(encoding) except UnicodeDecodeError: continue return data.decode("utf-8", errors="ignore")

我在实际项目里用这个函数处理过不少设备,基本能覆盖九成以上情况。还有个小经验:如果输出里夹杂着大量telnet控制字符,print出来会看到一堆乱码符号,可以先把这些控制字符过滤掉再展示:

import re def clean_telnet_output(data): # 去掉IAC协商相关的控制字节 data = re.sub(rb"\xff[\x00-\xff][\x00-\xff]", b"", data) return smart_decode(data)

过滤逻辑不用太复杂,目的只是让日志和调试输出可读。设备输出的核心内容基本不受影响。

4. 执行命令与输出解析

4.1 发送命令的换行细节和分页处理

命令后面加不加换行,直接影响命令能否执行。telnet协议规范里约定的行结束符是\r\n,但现实里很多设备只要\n就能识别。我习惯统一写成tn.write(cmd.encode() + b"\r\n"),兼容性最好。如果遇到个别设备对\r\n过敏,再退回到\n。

还有一个很隐蔽的问题:连续发送两条命令时,如果第一条命令的输出还没结束就发了第二条,设备可能把第二条当成上一条命令的补充参数。解决办法就是等待前一条命令的“结束特征”。常见的结束特征是提示符本身,比如>或#。但也有些设备提示符一直不变,得靠命令输出里的特定结尾来判断。

说到分页,这是telnet脚本最容易卡死的地方。很多设备输出长内容时会进入---- More ----翻页状态,脚本如果没有处理,就会一直卡在那边直到超时。两种应对方式:

第一种,登录后先关掉分页。不同的设备命令不太一样,比较常见的是screen-length 0 temporary、terminal length 0、set pagination off。登录成功之后立刻发一条关分页命令,后续输出就不会再翻页了。

第二种,如果关不掉分页,就在读取循环里识别翻页提示符,遇到就发一个空格翻页:

output = b"" while True: chunk = tn.read_until(b"---- More ----", timeout=5) output += chunk if chunk.endswith(b"---- More ----"): tn.write(b" ") # 翻页 else: break

这个循环会把整个输出全部读出来。注意最后一段chunk里可能还带着提示符,解析时要先按提示符截断,再去掉尾部的翻页标记文本。

4.2 清理回显与控制字符

设备默认开启回显,你发送的命令会被设备原样回显到终端里,这样脚本拿到的输出里包含了命令本身。做数据解析之前,建议把回显部分清掉。最简单的办法是只解析提示符之后的内容,或者按命令字符串切分:

cmd = "display version" output = output.decode("utf-8", errors="ignore") if cmd in output: output = output.split(cmd, 1)[1]

如果设备关闭了回显,那么命令不会出现在输出里,就不用做这步了。判断设备是否回显,可以在手工连一次telnet看看输命令时命令字有没有出现在屏幕上。

控制字符的处理主要针对\r、\x00、\x1b等。用splitlines()而不是split("\n")有个好处,它能同时处理\r\n和\n两种换行。

4.3 把设备输出解析成结构化数据

拿到原始输出只是第一步,自动化脚本最终要的是结构化结果。比如要统计设备上所有接口的状态,可以先看原始输出里接口行的格式,再写正则:

import re lines = output.splitlines() for line in lines: m = re.search(r"(\S+)\s+(\S+)\s+(up|down)", line) if m: print(m.group(1), m.group(2), m.group(3))

正则的具体写法一定要以设备的真实输出为准。不同厂商、不同型号的输出格式差别很大,有的人列顺序都不一样。我的习惯是:先手工跑一遍命令,把原始回显存成文件,再对着文件写解析逻辑,不要在没看到真实数据的情况下凭空写正则。

如果解析逻辑越来越复杂,比如要做几十个字段的提取,可以考虑用textfsm、pyparsing这类专门工具,甚至直接把输出喂给大模型做语义提取。但这些都是后话,核心前提永远是先把telnet环节稳下来。

5. 常见问题排查与避坑实录

5.1 一张表看懂高频问题

我把这些年遇到的典型问题整理成了速查表,照着一项项排除能省不少时间:

症状可能原因排查与解决
连接被拒绝端口未开放、设备不支持telnet、防火墙拦截先用nc或socket探测端口;确认服务端已开启telnet服务
用户名密码对但登录失败等待的提示符大小写不匹配用read_until(b"assword")而不是精确写Password;优先看完整回显
中文输出乱码编码不匹配依次尝试utf-8、gbk;用smart_decode和errors=ignore兜底
命令没执行write字符串末尾忘了加换行统一在命令后拼b"\r\n"再发送
输出读到一半就卡住设备进入分页状态登录后先关分页;或识别More标记并发送空格
程序不报错但结果为空read_until超时后直接返回空内容检查返回数据里是否包含期望关键字,不包含就打印原始数据
输出混入大量控制字符telnet协议字节未被清理启动set_debuglevel(1)观察原始字节;解析前过滤IAC控制字符
脚本偶发卡死socket默认超时未设置Telnet(host, port, timeout=10)显式传timeout;外层再包try/except

5.2 超时和数据读取不全的实战解法

读取不全这个问题,比连接失败更让人头疼。它的典型表现是:程序正常退出,但拿到的输出比设备终端上显示的短一截。原因往往是read_until等到了提示符就立刻返回,但设备在提示符后面还有一小段数据没来得及发过来。

解决办法是读取流程上做两层保障。第一层,等待提示符之前先用read_very_eager把缓冲区里的数据全部清空,避免旧数据干扰新命令。第二层,在等提示符时再额外循环读取一次,把残留数据补回来:

def read_until_prompt(tn, prompt=b">", timeout=10): data = b"" deadline = time.time() + timeout while time.time() < deadline: chunk = tn.read_until(prompt, timeout=max(0.1, deadline - time.time())) data += chunk if prompt in data: # 再给一点时间让残留数据到达 time.sleep(0.2) data += tn.read_very_eager() return data return data

这里的关键思路是:不要指望一次read_until就拿到完整数据,读到提示符之后稍微等一下,再把缓冲区里剩余的内容取干净。实测下来,这种写法在慢速链路和高延迟设备上明显比裸用read_until稳定。

5.3 安全提示:明文传输与替代方案

telnet最大的问题就是全程明文。用户名、密码、命令内容、返回数据,任何能在链路上抓包的人都能直接看到。所以只要网络环境允许,我都优先用SSH而不是telnet。Python侧做SSH自动化,paramiko是比较成熟的方案,语法和telnetlib有几分相似,但加密能力完全不同。

如果设备实在只支持telnet,我会做几个安全措施:把脚本限定在隔离的内网网段里跑;不在脚本里硬编码生产环境的密码,从环境变量或密钥管理系统读取;操作日志完整保留原始输入输出,出问题能追溯。这套做法不复杂,但能避免“明文协议”变成安全事故。

关于Python 3.13移除telnetlib之后的落地方案,我目前看到比较靠谱的有三条路:用telnetlib3这个纯异步库,兼容性更好;用pexpect配合系统telnet命令,适合交互复杂的场景;或者自己基于socket写一个极简telnet客户端,因为NVT文本协议本身不复杂,处理一下IAC协商和换行就够了。

我个人在实际维护中的体会是:telnet脚本最花时间的从来不是“连不上”,而是适配不同设备的交互节奏。每换一批设备,我建议先手工telnet上去,把登录提示符、分页符、换行方式、输出格式都抓一遍再开始写脚本。另外强烈建议脚本里加日志,把每次连接的原始回显落盘,排查问题时有据可查。这套流程看上去笨,但比对着文档猜设备行为效率高得多,也是写设备自动化脚本最稳的一条路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询