文章简介
日常运维、文件传输经常会用到 SFTP,虽然有 WinSCP、FileZilla、系统自带 sftp 命令等工具,但在定制化、脚本集成、轻量化场景下,自研一个 Python 交互式 SFTP 客户端会更加灵活。
本文使用 paramiko 库,从零实现一款功能完整、支持密码/密钥登录、交互式命令行的 SFTP 客户端,包含文件上传、下载、目录切换、文件增删等常用功能,代码开箱即用,同时讲解核心逻辑与使用方法,适合运维、Python 开发者学习参考。
环境:Python3 + paramiko
功能清单:
- 支持账号密码、SSH 私钥两种登录方式
- 交互式命令行,模拟原生 SFTP 操作习惯
- 远程目录操作:
ls/cd/pwd/mkdir/rmdir/rm - 本地目录操作:
lcd/lpwd - 文件传输:
get(下载)、put(上传) - 帮助文档、连接断开、异常捕获
一、环境准备
1. 安装依赖库
核心依赖为 paramiko,执行以下命令安装:
pip install paramiko
2. 前置说明
- 服务端:一台开启 SSH/SFTP 服务的 Linux 服务器(默认端口 22)
- 客户端:Windows / Linux / macOS 均可运行该 Python 脚本
- 支持绝对路径 / 相对路径,路径规则兼容原生 SFTP
二、完整源码
新建文件 sftp_client.py,粘贴以下全部代码:
#!/usr/bin/env python3
"""SFTP Client - 轻量级交互式SFTP工具"""
#备注:代码由codex+omlx+qwen3.6-35b-a3b-int8生成。
import paramiko
import os
import sys
import getpass
import argparse
from pathlib import Path
class SFTPClient:
def __init__(self):
self.ssh = None
self.sftp = None
self.remote_dir = "/"
self.local_dir = os.getcwd()
def connect(self, host, port=22, username=None, password=None, key_path=None):
"""连接SFTP服务器,支持密码/私钥登录"""
if not username:
username = input(f"Username for {host}: ")
if not password and not key_path:
password = getpass.getpass(f"Password for {username}@{host}: ")
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
if key_path:
key = paramiko.RSAKey.from_private_key_file(key_path)
self.ssh.connect(host, port=port, username=username, pkey=key)
else:
self.ssh.connect(host, port=port, username=username, password=password)
self.sftp = self.ssh.open_sftp()
self.remote_dir = "/"
print(f"✓ Connected to {host}:{port} as {username}")
return True
except Exception as e:
print(f"✗ Connection failed: {e}")
return False
def disconnect(self):
"""断开SFTP/SSH连接"""
if self.sftp:
self.sftp.close()
if self.ssh:
self.ssh.close()
self.sftp = None
self.ssh = None
print("Disconnected.")
def _remote_path(self, rel):
"""拼接远程绝对路径,兼容相对/绝对路径输入"""
rel = rel.lstrip("/")
if self.remote_dir == "/":
return "/" + rel if rel else "/"
return self.remote_dir.rstrip("/") + "/" + rel
def _ensure_remote_dir(self, path):
"""递归创建远程目录(上传前自动建目录)"""
parts = Path(path).parts
current = "/"
for part in parts[1:]:
current = current.rstrip("/") + "/" + part
try:
self.sftp.stat(current)
except IOError:
try:
self.sftp.mkdir(current)
except IOError:
pass
def do_ls(self, args=""):
"""列出远程目录文件"""
if not self.sftp:
print("✗ Not connected")
return
target = args.strip() if args.strip() else self.remote_dir
# 路径标准化处理
if target.startswith("/"):
rem_path = target.rstrip("/") or "/"
elif self.remote_dir == "/":
rem_path = ("/" + target.lstrip("/")).rstrip("/") or "/"
else:
rem_path = (self.remote_dir.rstrip("/") + "/" + target.lstrip("/")).rstrip("/") or "/"
try:
entries = sorted(self.sftp.listdir_attr(rem_path), key=lambda x: x.filename)
if not entries:
print("(empty)")
return
for e in entries:
marker = "/" if e.st_mode & 0o40000 else ""
size = f"{e.st_size:>10}" if not (e.st_mode & 0o40000) else " "
name = e.filename + marker
print(f"{name:<30} {size}B")
except IOError as e:
print(f"✗ Error listing '{rem_path}': {e}")
def do_cd(self, args=""):
"""切换远程目录"""
if not args or args == ".":
return
target = args.strip()
if target == "..":
parts = self.remote_dir.rstrip("/").split("/")
if len(parts) <= 2:
self.remote_dir = "/"
else:
self.remote_dir = "/".join(parts[:-1]) or "/"
print(self.remote_dir)
return
new_path = self._remote_path(target)
try:
self.sftp.stat(new_path)
self.remote_dir = new_path.rstrip("/") or "/"
print(self.remote_dir)
except IOError as e:
print(f"✗ Error: {e}")
def do_pwd(self, args=""):
"""查看远程当前目录"""
print(self.remote_dir)
def do_lpwd(self, args=""):
"""查看本地当前目录"""
print(self.local_dir)
def do_lcd(self, args=""):
"""切换本地目录"""
if not args:
print(self.local_dir)
return
target = os.path.expanduser(args.strip())
if os.path.isdir(target):
self.local_dir = target
print(self.local_dir)
else:
print(f"✗ Error: {target} is not a directory")
def do_get(self, args=""):
"""下载文件:get 远程文件 [本地保存路径]"""
args = args.strip()
if not args:
print("Usage: get <remote_file> [local_file]")
return
parts = args.split(None, 1)
first = parts[0]
if first.startswith("/"):
remote = first
local = parts[1] if len(parts) > 1 else os.path.basename(remote)
else:
remote = self._remote_path(first)
local = parts[1] if len(parts) > 1 else os.path.basename(first)
# 拼接本地完整路径
local_full = os.path.join(self.local_dir, local)
try:
self.sftp.get(remote, local_full)
print(f"✓ Downloaded: {remote} -> {local_full}")
except IOError as e:
print(f"✗ Error: {e}")
def do_put(self, args=""):
"""上传文件:put 本地文件 [远程保存路径]"""
args = args.strip()
if not args:
print("Usage: put <local_file> [remote_file]")
return
parts = args.split(None, 1)
local = parts[0]
local_full = os.path.join(self.local_dir, local)
if not os.path.isfile(local_full):
print(f"✗ Error: {local_full} not found")
return
remote_name = parts[1] if len(parts) > 1 else os.path.basename(local)
rem_path = self._remote_path(remote_name)
try:
self._ensure_remote_dir(rem_path)
self.sftp.put(local_full, rem_path)
print(f"✓ Uploaded: {local_full} -> {rem_path}")
except IOError as e:
print(f"✗ Error: {e}")
def do_mkdir(self, args=""):
"""创建远程目录"""
if not args.strip():
print("Usage: mkdir <dir>")
return
path = self._remote_path(args.strip())
try:
self.sftp.mkdir(path)
print(f"✓ Created: {path}")
except IOError as e:
print(f"✗ Error: {e}")
def do_rmdir(self, args=""):
"""删除远程空目录"""
if not args.strip():
print("Usage: rmdir <dir>")
return
path = self._remote_path(args.strip())
try:
self.sftp.rmdir(path)
print(f"✓ Removed: {path}")
except IOError as e:
print(f"✗ Error: {e}")
def do_rm(self, args=""):
"""删除远程文件"""
if not args.strip():
print("Usage: rm <file>")
return
path = self._remote_path(args.strip())
try:
self.sftp.remove(path)
print(f"✓ Deleted: {path}")
except IOError as e:
print(f"✗ Error: {e}")
def do_help(self, args=""):
"""打印帮助信息"""
cmds = [
("ls [path]", "列出目录内容"),
("cd <dir>", "切换远程目录 (.. 返回上级)"),
("pwd", "显示远程当前目录"),
("lpwd", "显示本地当前目录"),
("lcd <dir>", "切换本地目录"),
("get <r> [l]", "下载文件 (远程→本地)"),
("put <l> [r]", "上传文件 (本地→远程)"),
("mkdir <dir>", "创建远程目录"),
("rmdir <dir>", "删除远程空目录"),
("rm <file>", "删除远程文件"),
("exit/quit/q", "断开连接并退出"),
("help", "显示此帮助"),
]
print("\n=== SFTP Commands ===")
for cmd, desc in cmds:
print(f" {cmd:<20} {desc}")
print("=====================\n")
def interactive(self):
"""交互式命令行主循环"""
self.do_help()
prompt = "sftp>"
while True:
try:
line = input(prompt).strip()
except (EOFError, KeyboardInterrupt):
print()
self.disconnect()
break
if not line:
continue
parts = line.split(None, 1)
cmd = parts[0].lower()
args = parts[1] if len(parts) > 1 else ""
# 退出命令
if cmd in ("exit", "quit", "q"):
self.disconnect()
sys.exit(0)
# 匹配指令方法
do_method = getattr(self, f"do_{cmd}", None)
if do_method:
do_method(args)
else:
print(f"✗ Unknown command: {cmd} (type 'help')")
def main():
parser = argparse.ArgumentParser(description="SFTP Client")
parser.add_argument("host", nargs="?", help="SFTP server host/IP")
parser.add_argument("-P", "--port", type=int, default=22, help="SFTP 端口 (默认22)")
parser.add_argument("-u", "--user", help="登录用户名")
parser.add_argument("-p", "--password", help="登录密码")
parser.add_argument("-k", "--key", help="SSH 私钥文件路径")
args = parser.parse_args()
client = SFTPClient()
# 交互式输入主机地址
if not args.host:
host = input("SFTP Host: ").strip()
else:
host = args.host
# 建立连接
if not client.connect(host, args.port, args.user, args.password, args.key):
sys.exit(1)
# 进入交互终端
client.interactive()
if __name__ == "__main__":
main()
补充优化:原代码本地路径拼接存在小瑕疵,本文已修复,保证
get/put能正确读取/保存到本地当前目录。
三、运行方式(3种登录模式)
方式1:纯交互式运行(推荐新手)
直接执行脚本,按提示输入服务器地址、账号、密码:
python sftp_client.py
执行流程:
- 输入 SFTP 服务器 IP/域名
- 输入登录用户名
- 输入登录密码
- 进入
sftp>交互式命令行
方式2:命令行参数 + 密码登录
# 格式:python 脚本.py 服务器IP -u 用户名 -p 密码 -P 端口
python sftp_client.py 192.168.1.100 -u root -p 123456 -P 22
方式3:SSH 私钥登录(免密)
适合生产环境免密登录,指定私钥路径:
python sftp_client.py 192.168.1.100 -u root -k /home/user/id_rsa
四、交互式命令详解(重点)
进入 sftp> 终端后,所有命令和原生 Linux SFTP 高度一致,路径规则说明:
- 远程路径:以
/开头为绝对路径,推荐优先使用;不带/为相对路径(基于当前远程目录) - 本地路径:遵循当前操作系统路径规则
1. 目录查看与切换
# 查看远程当前目录
sftp> pwd
# 查看本地当前目录
sftp> lpwd
# 列出远程目录文件
sftp> ls
sftp> ls /home
# 切换远程目录
sftp> cd /home
sftp> cd .. # 返回上级目录
# 切换本地目录
sftp> lcd D:/test
sftp> lcd /home/user
2. 文件下载 get(核心)
语法:get 远程文件 [本地保存名]
# 绝对路径下载(推荐),保存到本地当前目录
sftp> get /home/test.txt
# 下载并指定本地保存路径/文件名
sftp> get /home/test.txt D:/download/new.txt
# 相对路径(当前远程目录为 /home)
sftp> get test.txt
3. 文件上传 put(核心)
语法:put 本地文件 [远程保存名]
# 上传本地文件到远程当前目录
sftp> put test.txt
# 上传并指定远程路径
sftp> put D:/test.txt /home/upload/test.txt
4. 目录/文件管理
# 创建远程目录
sftp> mkdir /home/new_dir
# 删除远程空目录
sftp> rmdir /home/new_dir
# 删除远程文件
sftp> rm /home/test.txt
5. 帮助 & 退出
# 查看所有命令
sftp> help
# 退出连接
sftp> exit
# 简写
sftp> quit
sftp> q
五、核心代码逻辑解析
1. 连接模块 connect
基于 paramiko.SSHClient 创建 SSH 会话,再通过 open_sftp() 开启 SFTP 通道。兼容密码登录和 RSA 私钥登录,自动信任未知主机密钥。
2. 路径处理 _remote_path
统一格式化远程路径,自动拼接相对路径与绝对路径,解决多层目录切换后路径错乱问题。
3. 递归建目录 _ensure_remote_dir
上传文件时,如果远程目录不存在,会逐层递归创建,无需手动提前建文件夹。
4. 交互式主循环 interactive
监听终端输入,解析命令并分发到对应的 do_xxx 方法,模拟原生 SFTP 交互体验,同时捕获 Ctrl+C、EOF 异常,优雅断开连接。
5. 文件传输 get / put
get:调用sftp.get()从远程拉取文件到本地put:调用sftp.put()将本地文件推送至远程,传输前自动校验文件是否存在
六、常见问题与排错
- 连接超时
- 检查服务器 IP、端口、防火墙是否放行 22 端口
- 权限拒绝 Permission denied
- 账号密码/私钥错误;远程文件/目录无读写权限
- 文件找不到
- 核对路径:远程路径建议使用绝对路径(以
/开头),避免相对路径歧义
- 核对路径:远程路径建议使用绝对路径(以
- 私钥登录失败
- 私钥文件权限过高(Linux 需设置
chmod 600 私钥文件)
- 私钥文件权限过高(Linux 需设置
七、扩展方向(二次开发建议)
- 增加文件夹递归上传/下载(
get -r/put -r) - 增加文件断点续传功能
- 增加批量文件传输、通配符匹配(
*.txt) - 增加日志输出、传输进度条
- 封装为接口,供其他 Python 项目调用
总结
本文基于 paramiko 实现了一款功能完备的交互式 SFTP 客户端,代码轻量化、无多余依赖,兼容 Windows/Linux/macOS 全平台。
对比传统 SFTP 工具,该脚本优势:
- 纯 Python 实现,可嵌入自动化运维脚本、爬虫、部署流程中
- 交互式操作贴近原生命令行,上手成本极低
- 支持密码/私钥两种主流登录方式,适配测试、生产环境
代码可直接部署使用,也可根据业务需求二次扩展,是学习 Paramiko、SFTP 协议、Python 交互式终端开发的优质实战案例。
1445

被折叠的 条评论
为什么被折叠?



