杰瑞科技汇

如何用Python pexpect实现SCP自动传输?

Of course! Using pexpect for scp (Secure Copy Protocol) is a classic use case for this library. The primary reason you'd use pexpect instead of Python's native paramiko library is to automate an scp command to a server where you cannot configure SSH keys and must rely on a password.

pexpect works by "expecting" a specific string of text (a pattern) from the command's output and then "sending" a response (like a password or a command).

Here’s a complete guide, from a basic example to a more robust and secure one.


The Core Concept

  1. Spawn a Process: You start the scp command as a new child process.
  2. Expect a Prompt: The scp command will run, and eventually, the remote server will ask for a password. The output will look something like user@host's password:.
  3. Send the Password: When you see that prompt, pexpect automatically sends the password.
  4. Wait for Completion: After sending the password, you wait for the command to finish, which is signaled by the process returning to its shell prompt or simply closing.

Basic pexpect SCP Example (Password-based)

This is the most straightforward example. It copies a local file to a remote server.

Prerequisites

  1. Install pexpect:
    pip install pexpect
  2. Set up a test environment: You need a remote server you can SSH into. For this example, we'll assume:
    • Host: your.remote.server.com
    • Username: your_username
    • Password: your_password
    • You want to copy local_file.txt to the remote home directory.

The Python Script

import pexpect
import sys
import os
# --- Configuration ---
# It's better to use environment variables for sensitive data
HOST = "your.remote.server.com"
USER = "your_username"
PASSWORD = "your_password" # Or use os.environ.get('SCP_PASSWORD')
LOCAL_FILE = "local_file.txt"
REMOTE_PATH = f"{USER}@{HOST}:/home/{USER}/remote_file.txt"
def main():
    """
    Copies a file to a remote server using pexpect and scp.
    """
    if not os.path.exists(LOCAL_FILE):
        print(f"Error: Local file '{LOCAL_FILE}' not found.")
        sys.exit(1)
    # The scp command we want to run
    # Note: The -o 'StrictHostKeyChecking=no' flag is to avoid the
    # "Are you sure you want to continue connecting (yes/no)?" prompt.
    # Use with caution in production; it's less secure.
    command = f"scp -o 'StrictHostKeyChecking=no' {LOCAL_FILE} {REMOTE_PATH}"
    print(f"Executing command: {command}")
    try:
        # Start the scp process
        # The `timeout` is important to prevent the script from hanging indefinitely
        child = pexpect.spawn(command, timeout=20, encoding='utf-8')
        # Expect the password prompt. The pattern is the string we're looking for.
        # The `timeout` here is for the prompt itself.
        # The `before` attribute contains the output seen so far.
        index = child.expect([f"{USER}@{HOST}'s password: ", pexpect.EOF, pexpect.TIMEOUT])
        if index == 0:
            # We got the password prompt, so send the password
            print("Password prompt found. Sending password...")
            child.sendline(PASSWORD)
            # Wait for the command to finish. The EOF (End-Of-File) signal
            # is sent when the scp process closes.
            child.expect(pexpect.EOF)
            print("SCP command finished successfully.")
            print(child.before) # Print the output of the scp command
        elif index == 1:
            # This means the process ended before we saw a password prompt.
            # Could be an error like "Permission denied (publickey,password)."
            print("Error: Process ended unexpectedly.")
            print("Output:", child.before)
            sys.exit(1)
        elif index == 2:
            # This means we timed out waiting for the password prompt.
            print("Error: Timed out waiting for password prompt.")
            sys.exit(1)
    except pexpect.exceptions.ExceptionPexpect as e:
        print(f"An pexpect error occurred: {e}")
        sys.exit(1)
if __name__ == "__main__":
    main()

How to Run It

  1. Save the code as scp_script.py.
  2. Make sure local_file.txt exists in the same directory.
  3. Replace the placeholder values for HOST, USER, and PASSWORD.
  4. Run from your terminal: python scp_script.py

A More Robust Example (Handling Prompts and Errors)

The basic example is good, but it can be improved. What if the SSH key prompt appears anyway? What if the password is wrong? A robust script handles these cases.

This version uses a list of expected patterns.

import pexpect
import sys
import os
# --- Configuration ---
HOST = "your.remote.server.com"
USER = "your_username"
PASSWORD = os.environ.get('SCP_PASSWORD') # Safer: get from environment variable
LOCAL_FILE = "local_file.txt"
REMOTE_PATH = f"{USER}@{HOST}:/home/{USER}/remote_file.txt"
def main():
    if not os.path.exists(LOCAL_FILE):
        print(f"Error: Local file '{LOCAL_FILE}' not found.")
        sys.exit(1)
    if not PASSWORD:
        print("Error: Password environment variable 'SCP_PASSWORD' not set.")
        sys.exit(1)
    command = f"scp -o 'StrictHostKeyChecking=no' {LOCAL_FILE} {REMOTE_PATH}"
    print(f"Executing: {command}")
    try:
        child = pexpect.spawn(command, timeout=30, encoding='utf-8')
        # Define a list of patterns we expect to see and their corresponding actions.
        # The order matters. pexpect checks them in order.
        patterns = [
            # 1. The password prompt we want to match
            (f"{USER}@{HOST}'s password: ", 0),
            # 2. The "Are you sure..." prompt, just in case - should be rare with -o flag
            (r"Are you sure you want to continue connecting \(yes/no\)\?", 1),
            # 3. End of File - means the process finished
            (pexpect.EOF, 2),
            # 4. Timeout - means we waited too long for any of the above
            (pexpect.TIMEOUT, 3)
        ]
        while True:
            # The expect_withlist function returns the index of the matched pattern
            # and the match object itself.
            try:
                index, match = child.expect_withlist(patterns, timeout=10)
            except pexpect.TIMEOUT:
                print("Error: Timed out waiting for a response from the server.")
                child.close(force=True)
                sys.exit(1)
            if index == 0:
                # Matched the password prompt
                print("Sending password...")
                child.sendline(PASSWORD)
            elif index == 1:
                # Matched the confirmation prompt (unexpected)
                print("Unexpected SSH key confirmation prompt. Answering 'yes'.")
                child.sendline("yes")
            elif index == 2:
                # Matched EOF - process is finished
                print("SCP command finished.")
                print("Output:\n", child.before)
                break # Exit the loop
            elif index == 3:
                # This should not be reached if the timeout is handled in the loop
                print("Error: A timeout occurred within the loop.")
                child.close(force=True)
                sys.exit(1)
    except pexpect.exceptions.ExceptionPexpect as e:
        print(f"A pexpect error occurred: {e}")
        sys.exit(1)
if __name__ == "__main__":
    main()

Security Best Practices (Crucial!)

Hardcoding passwords in a script is a major security risk. Here are the best practices:

  1. Use Environment Variables: This is the bare minimum. Store the password in an environment variable and read it in your script. This prevents the password from being committed to version control (like Git).

    # In your shell before running the script
    export SCP_PASSWORD="your_super_secret_password"
    # In your Python script
    PASSWORD = os.environ.get('SCP_PASSWORD')
  2. Use SSH Keys (The Recommended Solution): If you have any control over the remote server, the best and most secure method is to set up SSH key-based authentication. This way, no password is ever needed, and you can use Python's paramiko library, which is generally more robust and easier to manage for this purpose.

    # On your local machine
    ssh-keygen -t rsa -b 4096
    ssh-copy-id your_username@your.remote.server.com

    Once keys are set up, your pexpect script becomes much simpler because you don't need to handle the password prompt at all.

  3. Use paramiko for a Better Python Experience: paramiko is a pure Python implementation of SSHv2. It's the standard library for this kind of task in Python and is designed to be used from within Python, not to parse interactive shell output.

    Here’s how you'd do the same file copy with paramiko (after setting up SSH keys):

    import paramiko
    import os
    HOST = "your.remote.server.com"
    USER = "your_username"
    LOCAL_FILE = "local_file.txt"
    REMOTE_PATH = f"/home/{USER}/remote_file_paramiko.txt"
    # Create an SSH client
    ssh = paramiko.SSHClient()
    # Automatically add the server's host key (not recommended for production)
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    try:
        # Connect using the SSH key (no password needed)
        print("Connecting to server...")
        ssh.connect(hostname=HOST, username=USER)
        # Open an SFTP session
        sftp = ssh.open_sftp()
        # Use the put() method to transfer the file
        print(f"Transferring {LOCAL_FILE} to {REMOTE_PATH}...")
        sftp.put(LOCAL_FILE, REMOTE_PATH)
        print("File transfer complete.")
    except paramiko.AuthenticationException:
        print("Authentication failed, please verify your credentials.")
    except paramiko.SSHException as sshException:
        print(f"Unable to establish SSH connection: {sshException}")
    except Exception as e:
        print(f"An error occurred: {e}")
    finally:
        # Close the SFTP and SSH connections
        if 'sftp' in locals():
            sftp.close()
        ssh.close()
        print("Connection closed.")

Summary: pexpect vs. paramiko for SCP

Feature pexpect paramiko
How it works Spawns a command-line process and interacts with it like a human. Pure Python library that implements the SSH/SFTP protocol directly.
Use Case Automating legacy tools or commands where you can't change the command itself (e.g., scp, rsync, mysql client). Building native Python applications that need to interact with remote servers.
Authentication Primarily for interactive password prompts. Can handle keys but is awkward. Excellent support for passwords, keys, and even keyboard-interactive auth.
Error Handling Can be brittle. Relies on matching exact strings in command output. More robust and programmatic. Throws specific Python exceptions.
Recommendation Use when you must automate a specific command-line tool that doesn't have a good Python API. Highly recommended for any new Python project needing remote file transfer or command execution.
分享:
扫描分享到社交APP
上一篇
下一篇