Python 基于 Paramiko 实现交互式 SFTP 客户端(完整可运行源码+使用教程)

文章简介

日常运维、文件传输经常会用到 SFTP,虽然有 WinSCP、FileZilla、系统自带 sftp 命令等工具,但在定制化、脚本集成、轻量化场景下,自研一个 Python 交互式 SFTP 客户端会更加灵活。

本文使用 paramiko 库,从零实现一款功能完整、支持密码/密钥登录、交互式命令行的 SFTP 客户端,包含文件上传、下载、目录切换、文件增删等常用功能,代码开箱即用,同时讲解核心逻辑与使用方法,适合运维、Python 开发者学习参考。

环境:Python3 + paramiko
功能清单

  1. 支持账号密码、SSH 私钥两种登录方式
  2. 交互式命令行,模拟原生 SFTP 操作习惯
  3. 远程目录操作:ls/cd/pwd/mkdir/rmdir/rm
  4. 本地目录操作:lcd/lpwd
  5. 文件传输:get(下载)、put(上传)
  6. 帮助文档、连接断开、异常捕获

一、环境准备

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

执行流程:

  1. 输入 SFTP 服务器 IP/域名
  2. 输入登录用户名
  3. 输入登录密码
  4. 进入 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. 远程路径:以 / 开头为绝对路径,推荐优先使用;不带 / 为相对路径(基于当前远程目录)
  2. 本地路径:遵循当前操作系统路径规则

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() 将本地文件推送至远程,传输前自动校验文件是否存在

六、常见问题与排错

  1. 连接超时
    • 检查服务器 IP、端口、防火墙是否放行 22 端口
  2. 权限拒绝 Permission denied
    • 账号密码/私钥错误;远程文件/目录无读写权限
  3. 文件找不到
    • 核对路径:远程路径建议使用绝对路径(以 / 开头),避免相对路径歧义
  4. 私钥登录失败
    • 私钥文件权限过高(Linux 需设置 chmod 600 私钥文件

七、扩展方向(二次开发建议)

  1. 增加文件夹递归上传/下载get -r / put -r
  2. 增加文件断点续传功能
  3. 增加批量文件传输、通配符匹配(*.txt
  4. 增加日志输出、传输进度条
  5. 封装为接口,供其他 Python 项目调用

总结

本文基于 paramiko 实现了一款功能完备的交互式 SFTP 客户端,代码轻量化、无多余依赖,兼容 Windows/Linux/macOS 全平台。

对比传统 SFTP 工具,该脚本优势:

  • 纯 Python 实现,可嵌入自动化运维脚本、爬虫、部署流程中
  • 交互式操作贴近原生命令行,上手成本极低
  • 支持密码/私钥两种主流登录方式,适配测试、生产环境

代码可直接部署使用,也可根据业务需求二次扩展,是学习 Paramiko、SFTP 协议、Python 交互式终端开发的优质实战案例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

跟着Jacky学AI

喜欢作者

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值