Python實現(xiàn)自動備份U盤內(nèi)容
一、前言:為什么需要U盤自動備份工具
在日常工作和學(xué)習(xí)中,U盤作為便攜存儲設(shè)備被廣泛使用,但同時也面臨著數(shù)據(jù)丟失的風(fēng)險。傳統(tǒng)的手動備份方式存在以下痛點:
- 容易遺忘:重要數(shù)據(jù)經(jīng)常因忘記備份而丟失
- 效率低下:每次都需要手動復(fù)制粘貼
- 版本混亂:難以管理不同時間點的備份版本
本文將帶你從零開始實現(xiàn)一個智能U盤自動備份工具,具備以下亮點功能:
- 自動檢測:實時監(jiān)控U盤插入事件
- 增量備份:僅復(fù)制新增或修改的文件
- 多線程加速:大幅提升大文件復(fù)制效率
- 可視化界面:實時顯示備份進度和日志
- 異常處理:完善的錯誤恢復(fù)機制
二、技術(shù)架構(gòu)設(shè)計
2.1 系統(tǒng)架構(gòu)圖

2.2 關(guān)鍵技術(shù)選型
| 技術(shù) | 用途 | 優(yōu)勢 |
|---|---|---|
win32file | 驅(qū)動器類型檢測 | 精準識別可移動設(shè)備 |
ThreadPoolExecutor | 并發(fā)文件復(fù)制 | 充分利用多核CPU |
logging | 日志記錄 | 完善的日志分級 |
tkinter | GUI界面 | 原生跨平臺支持 |
shutil | 文件操作 | 高性能文件復(fù)制 |
三、核心代碼深度解析
3.1 驅(qū)動器監(jiān)控機制
def get_available_drives():
"""獲取當(dāng)前所有可用的驅(qū)動器盤符"""
drives = []
bitmask = win32file.GetLogicalDrives()
for letter in string.ascii_uppercase:
if bitmask & 1:
drives.append(letter)
bitmask >>= 1
return set(drives)
關(guān)鍵技術(shù)點:
- 使用Windows API GetLogicalDrives()獲取驅(qū)動器位掩碼
- 通過位運算解析每個盤符狀態(tài)
- 返回結(jié)果為集合類型,便于后續(xù)差集運算
3.2 智能增量備份實現(xiàn)
def should_skip_file(src, dst):
"""判斷是否需要跳過備份(增量備份邏輯)"""
if not os.path.exists(dst):
return False
try:
src_stat = os.stat(src)
dst_stat = os.stat(dst)
return src_stat.st_size == dst_stat.st_size and int(src_stat.st_mtime) == int(dst_stat.st_mtime)
except Exception:
return False
優(yōu)化策略:
- 文件大小比對(快速篩選)
- 修改時間比對(精確判斷)
- 異常捕獲機制(增強魯棒性)
3.3 多線程文件復(fù)制引擎
def threaded_copytree(src, dst, max_workers=8, app_instance=None, total_files=0):
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 大文件單獨提交任務(wù)
tasks.append(executor.submit(copy_file_with_log, s, d))
# 小文件批量處理
batch_size = 16
for i in range(0, len(small_files), batch_size):
tasks.append(executor.submit(batch_copy_files, batch))
性能優(yōu)化點:
- 動態(tài)線程池管理
- 大文件獨立線程處理
- 小文件批量提交(減少線程切換開銷)
- 進度回調(diào)機制
四、GUI界面設(shè)計與實現(xiàn)
4.1 馬卡龍配色方案
COLORS = {
"background": "#f8f3ff", # 淡紫色背景
"button": "#a8e6cf", # 薄荷綠按鈕
"status": "#ffd3b6", # 桃色狀態(tài)欄
"highlight": "#ffaaa5" # 珊瑚紅高亮
}
設(shè)計理念:
- 低飽和度配色減輕視覺疲勞
- 色彩心理學(xué)應(yīng)用(綠色-安全,紅色-警告)
- 符合現(xiàn)代UI設(shè)計趨勢
4.2 實時日志系統(tǒng)
class TextHandler(logging.Handler):
def emit(self, record):
msg = self.format(record)
self.queue.put(msg)
self.text_widget.after(0, self.update_widget)
關(guān)鍵技術(shù):
- 異步消息隊列處理
- 線程安全更新UI
- 自動滾動到底部
五、高級功能擴展
5.1 備份策略優(yōu)化
def backup_usb_drive(self, drive_letter):
# 智能路徑生成規(guī)則
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
destination_folder = os.path.join(
self.backup_destination.get(),
f"Backup_{drive_letter}_{timestamp}"
)
備份策略:
- 按時間戳創(chuàng)建獨立目錄
- 保留原始目錄結(jié)構(gòu)
- 自動跳過系統(tǒng)文件(如$RECYCLE.BIN)
5.2 異常處理機制
try:
threaded_copytree(...)
except PermissionError:
logging.error("權(quán)限錯誤處理")
except FileNotFoundError:
logging.error("文件不存在處理")
except Exception as e:
logging.error(f"未知錯誤: {e}")
健壯性設(shè)計:
- 分級異常捕獲
- 錯誤上下文記錄
- 用戶友好提示
六、性能測試與優(yōu)化
6.1 不同線程數(shù)下的備份速度對比
| 線程數(shù) | 1GB文件耗時(s) | CPU占用率 |
|---|---|---|
| 1 | 58.7 | 15% |
| 4 | 32.1 | 45% |
| 8 | 28.5 | 70% |
| 16 | 27.9 | 90% |
結(jié)論:8線程為最佳平衡點
6.2 內(nèi)存優(yōu)化策略
分塊讀取大文件(16MB/塊)
及時釋放文件句柄
避免不必要的緩存
七、完整代碼部署指南
7.1 環(huán)境準備
pip install pywin32 pip install pillow # 如需圖標支持
7.2 打包為EXE
使用PyInstaller打包:
pyinstaller -w -F --icon=usb.ico usb_backup_tool.py
7.3 開機自啟動配置
將快捷方式放入啟動文件夾:
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup
八、效果展示


九、相關(guān)源碼
import os
import shutil
import time
import string
import win32file
import logging
from datetime import datetime
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
import tkinter as tk
from tkinter import scrolledtext, ttk, filedialog, messagebox
import queue
# --- 配置 ---
DEFAULT_BACKUP_PATH = r"D:\USB_Backups"
CHECK_INTERVAL = 5
LOG_FILE_NAME = "usb_backup_log.txt"
# --- 馬卡龍配色方案 ---
COLORS = {
"background": "#f8f3ff", # 淡紫色背景
"text": "#5a5a5a", # 深灰色文字
"button": "#a8e6cf", # 薄荷綠按鈕
"button_hover": "#dcedc1", # 淺綠色按鈕懸停
"button_text": "#333333", # 深灰色按鈕文字
"log_background": "#ffffff", # 白色日志背景
"status": "#ffd3b6", # 桃色狀態(tài)欄
"highlight": "#ffaaa5", # 珊瑚紅高亮
"success": "#dcedc1", # 淺綠色成功提示
"error": "#ffaaa5", # 珊瑚紅錯誤提示
"menu_bg": "#dcedc1", # 菜單背景色
"menu_fg": "#333333" # 菜單文字色
}
# --- 字體設(shè)置 ---
FONT_FAMILY = "Segoe UI"
FONT_SIZE_SMALL = 9
FONT_SIZE_NORMAL = 10
FONT_SIZE_LARGE = 12
FONT_SIZE_TITLE = 16
class TextHandler(logging.Handler):
"""自定義日志處理器,將日志記錄發(fā)送到 Text 控件"""
def __init__(self, text_widget):
logging.Handler.__init__(self)
self.text_widget = text_widget
self.queue = queue.Queue()
self.thread = threading.Thread(target=self.process_queue, daemon=True)
self.thread.start()
def emit(self, record):
msg = self.format(record)
self.queue.put(msg)
def process_queue(self):
while True:
try:
msg = self.queue.get()
if msg is None:
break
def update_widget():
try:
self.text_widget.configure(state='normal')
self.text_widget.insert(tk.END, msg + '\n')
self.text_widget.configure(state='disabled')
self.text_widget.yview(tk.END)
except tk.TclError:
pass
self.text_widget.after(0, update_widget)
self.queue.task_done()
except Exception:
import traceback
traceback.print_exc()
break
def close(self):
self.stop_processing()
logging.Handler.close(self)
def stop_processing(self):
self.queue.put(None)
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("USB 自動備份工具")
self.geometry("800x600")
self.minsize(700, 500)
self.configure(bg=COLORS["background"])
# 配置變量
self.backup_destination = tk.StringVar(value=DEFAULT_BACKUP_PATH)
self.log_file_path = os.path.join(self.backup_destination.get(), LOG_FILE_NAME)
self.running = True
self.currently_backing_up = False
# 設(shè)置窗口圖標
try:
self.iconbitmap('usb_icon.ico')
except:
pass
# 創(chuàng)建菜單欄
self.create_menu()
# 初始化樣式
self.init_styles()
# 主界面布局
self.create_widgets()
# 初始化日志系統(tǒng)
self.configure_logging()
# 啟動監(jiān)控線程
self.start_backup_monitor()
def init_styles(self):
"""初始化界面樣式"""
style = ttk.Style()
# 按鈕樣式
style.configure('TButton',
font=(FONT_FAMILY, FONT_SIZE_NORMAL),
background=COLORS["button"],
foreground=COLORS["button_text"],
borderwidth=1,
padding=6)
style.map('TButton',
background=[('active', COLORS["button_hover"])])
# 進度條樣式
style.configure('Horizontal.TProgressbar',
thickness=20,
troughcolor=COLORS["background"],
background=COLORS["button"],
troughrelief='flat',
relief='flat')
def create_menu(self):
"""創(chuàng)建菜單欄"""
menubar = tk.Menu(self, bg=COLORS["menu_bg"], fg=COLORS["menu_fg"])
# 文件菜單
file_menu = tk.Menu(menubar, tearoff=0, bg=COLORS["menu_bg"], fg=COLORS["menu_fg"])
file_menu.add_command(
label="更改備份路徑",
command=self.change_backup_path,
accelerator="Ctrl+P"
)
file_menu.add_separator()
file_menu.add_command(
label="打開日志文件",
command=self.open_log_file,
accelerator="Ctrl+L"
)
file_menu.add_separator()
file_menu.add_command(
label="退出",
command=self.quit_app,
accelerator="Ctrl+Q"
)
menubar.add_cascade(label="文件", menu=file_menu)
# 幫助菜單
help_menu = tk.Menu(menubar, tearoff=0, bg=COLORS["menu_bg"], fg=COLORS["menu_fg"])
help_menu.add_command(
label="使用說明",
command=self.show_instructions
)
help_menu.add_command(
label="關(guān)于",
command=self.show_about
)
menubar.add_cascade(label="幫助", menu=help_menu)
self.config(menu=menubar)
# 綁定快捷鍵
self.bind("<Control-p>", lambda e: self.change_backup_path())
self.bind("<Control-l>", lambda e: self.open_log_file())
self.bind("<Control-q>", lambda e: self.quit_app())
def create_widgets(self):
"""創(chuàng)建主界面控件"""
# 主框架
main_frame = tk.Frame(self, bg=COLORS["background"], padx=15, pady=15)
main_frame.pack(expand=True, fill='both')
# 標題區(qū)域
title_frame = tk.Frame(main_frame, bg=COLORS["background"])
title_frame.pack(fill='x', pady=(0, 15))
# 標題標簽
title_label = tk.Label(
title_frame,
text="USB 自動備份工具",
font=(FONT_FAMILY, FONT_SIZE_TITLE, 'bold'),
fg=COLORS["text"],
bg=COLORS["background"]
)
title_label.pack(side=tk.LEFT)
# 當(dāng)前路徑顯示
path_frame = tk.Frame(title_frame, bg=COLORS["background"])
path_frame.pack(side=tk.RIGHT, fill='x', expand=True)
path_label = tk.Label(
path_frame,
text="備份路徑:",
font=(FONT_FAMILY, FONT_SIZE_SMALL),
fg=COLORS["text"],
bg=COLORS["background"],
anchor='e'
)
path_label.pack(side=tk.LEFT)
self.path_entry = ttk.Entry(
path_frame,
textvariable=self.backup_destination,
font=(FONT_FAMILY, FONT_SIZE_SMALL),
state='readonly',
width=40
)
self.path_entry.pack(side=tk.LEFT, padx=(5, 0))
# 日志區(qū)域
log_frame = tk.LabelFrame(
main_frame,
text=" 日志記錄 ",
font=(FONT_FAMILY, FONT_SIZE_LARGE),
bg=COLORS["background"],
fg=COLORS["text"],
padx=5,
pady=5
)
log_frame.pack(expand=True, fill='both')
self.log_text = scrolledtext.ScrolledText(
log_frame,
state='disabled',
wrap=tk.WORD,
bg=COLORS["log_background"],
fg=COLORS["text"],
font=(FONT_FAMILY, FONT_SIZE_NORMAL),
padx=10,
pady=10
)
self.log_text.pack(expand=True, fill='both')
# 控制面板
control_frame = tk.Frame(main_frame, bg=COLORS["background"])
control_frame.pack(fill='x', pady=(15, 0))
# 進度條
self.progress = ttk.Progressbar(
control_frame,
orient='horizontal',
mode='determinate',
style='Horizontal.TProgressbar'
)
self.progress.pack(side=tk.LEFT, expand=True, fill='x', padx=(0, 10))
# 狀態(tài)標簽
self.status_label = tk.Label(
control_frame,
text="就緒",
font=(FONT_FAMILY, FONT_SIZE_SMALL),
fg=COLORS["text"],
bg=COLORS["background"],
width=15,
anchor='w'
)
self.status_label.pack(side=tk.LEFT, padx=(0, 10))
# 退出按鈕
self.exit_button = ttk.Button(
control_frame,
text="退出",
command=self.quit_app,
style='TButton'
)
self.exit_button.pack(side=tk.RIGHT)
# 狀態(tài)欄
self.status_bar = tk.Label(
main_frame,
text="狀態(tài): 初始化中...",
anchor='w',
bg=COLORS["status"],
fg=COLORS["text"],
font=(FONT_FAMILY, FONT_SIZE_SMALL),
padx=10,
pady=5,
relief=tk.SUNKEN
)
self.status_bar.pack(fill='x', pady=(10, 0))
def configure_logging(self):
"""配置日志系統(tǒng)"""
# 確保備份目錄存在
if not os.path.exists(self.backup_destination.get()):
try:
os.makedirs(self.backup_destination.get())
logging.info(f"創(chuàng)建備份目錄: {self.backup_destination.get()}")
except Exception as e:
self.update_status(f"錯誤: 無法創(chuàng)建備份目錄 {self.backup_destination.get()}: {e}", "error")
self.log_text.configure(state='normal')
self.log_text.insert(tk.END, f"錯誤: 無法創(chuàng)建備份目錄 {self.backup_destination.get()}: {e}\n")
self.log_text.configure(state='disabled')
return
# 更新日志文件路徑
self.log_file_path = os.path.join(self.backup_destination.get(), LOG_FILE_NAME)
log_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
# 文件處理器
file_handler = logging.FileHandler(self.log_file_path, encoding='utf-8')
file_handler.setFormatter(log_formatter)
# GUI 文本處理器
self.text_handler = TextHandler(self.log_text)
self.text_handler.setFormatter(log_formatter)
# 配置根日志記錄器
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
# 清除現(xiàn)有處理器
if root_logger.hasHandlers():
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
root_logger.addHandler(file_handler)
root_logger.addHandler(self.text_handler)
logging.info("="*50)
logging.info("USB 自動備份工具啟動")
logging.info(f"備份目錄: {self.backup_destination.get()}")
logging.info(f"日志文件: {self.log_file_path}")
logging.info("="*50)
def change_backup_path(self):
"""更改備份路徑"""
if self.currently_backing_up:
messagebox.showwarning("警告", "當(dāng)前正在備份中,請等待備份完成后再更改路徑。")
return
new_path = filedialog.askdirectory(
title="選擇備份目錄",
initialdir=self.backup_destination.get()
)
if new_path:
try:
# 測試新路徑是否可寫
test_file = os.path.join(new_path, "test_write.tmp")
with open(test_file, 'w') as f:
f.write("test")
os.remove(test_file)
self.backup_destination.set(new_path)
logging.info(f"備份路徑已更改為: {new_path}")
self.update_status(f"備份路徑已更改為: {new_path}", "highlight")
self.configure_logging()
except Exception as e:
messagebox.showerror("錯誤", f"無法使用該路徑: {str(e)}")
logging.error(f"更改備份路徑失敗: {str(e)}")
def open_log_file(self):
"""打開日志文件"""
if os.path.exists(self.log_file_path):
try:
os.startfile(self.log_file_path)
except Exception as e:
messagebox.showerror("錯誤", f"無法打開日志文件: {str(e)}")
logging.error(f"打開日志文件失敗: {str(e)}")
else:
messagebox.showinfo("信息", "日志文件尚未創(chuàng)建。")
def show_instructions(self):
"""顯示使用說明"""
instructions = (
"USB 自動備份工具使用說明\n\n"
"1. 插入U盤后,程序會自動檢測并開始備份\n"
"2. 備份文件將存儲在指定的備份目錄中\(zhòng)n"
"3. 每次備份會創(chuàng)建一個帶有時間戳的新文件夾\n"
"4. 程序會自動跳過已備份且未更改的文件\n"
"5. 可以通過菜單更改備份路徑\n\n"
"快捷鍵:\n"
"Ctrl+P - 更改備份路徑\n"
"Ctrl+L - 打開日志文件\n"
"Ctrl+Q - 退出程序"
)
messagebox.showinfo("使用說明", instructions)
def show_about(self):
"""顯示關(guān)于對話框"""
about_text = (
"USB 自動備份工具\n\n"
"版本: 2.0\n"
"功能: 自動檢測并備份插入的U盤\n"
"特點:\n"
" - 增量備份\n"
" - 多線程復(fù)制\n"
" - 實時進度顯示\n\n"
"作者: 創(chuàng)客白澤\n"
"版權(quán)所有 ? 2025"
)
messagebox.showinfo("關(guān)于", about_text)
def update_status(self, message, status_type="normal"):
"""更新狀態(tài)欄"""
colors = {
"normal": COLORS["status"],
"success": COLORS["success"],
"error": COLORS["error"],
"highlight": COLORS["highlight"],
"warning": "#ffcc5c" # 警告色
}
bg_color = colors.get(status_type, COLORS["status"])
def update():
self.status_bar.config(
text=f"狀態(tài): {message}",
bg=bg_color,
fg=COLORS["text"]
)
self.after(0, update)
def update_progress(self, value):
"""更新進度條"""
def update():
self.progress['value'] = value
self.status_label.config(text=f"{int(value)}%")
self.after(0, update)
def start_backup_monitor(self):
"""啟動備份監(jiān)控線程"""
self.backup_thread = threading.Thread(
target=self.run_backup_monitor,
daemon=True
)
self.backup_thread.start()
def run_backup_monitor(self):
"""后臺監(jiān)控線程的主函數(shù)"""
logging.info("U盤自動備份程序啟動...")
logging.info(f"備份將存儲在: {self.backup_destination.get()}")
self.update_status("啟動成功,等待U盤插入...")
if not os.path.exists(self.backup_destination.get()):
logging.error(f"無法啟動監(jiān)控:備份目錄 {self.backup_destination.get()} 不存在且無法創(chuàng)建。")
self.update_status(f"錯誤: 備份目錄不存在且無法創(chuàng)建", "error")
return
try:
known_drives = get_available_drives()
logging.info(f"當(dāng)前已知驅(qū)動器: {sorted(list(known_drives))}")
except Exception as e_init_drives:
logging.error(f"初始化獲取驅(qū)動器列表失敗: {e_init_drives}")
self.update_status(f"錯誤: 獲取驅(qū)動器列表失敗", "error")
known_drives = set()
while self.running:
try:
self.update_status("正在檢測驅(qū)動器...")
current_drives = get_available_drives()
new_drives = current_drives - known_drives
removed_drives = known_drives - current_drives
if new_drives:
logging.info(f"檢測到新驅(qū)動器: {sorted(list(new_drives))}")
for drive in new_drives:
if not self.running:
break
logging.info(f"等待驅(qū)動器 {drive}: 準備就緒...")
self.update_status(f"檢測到新驅(qū)動器 {drive}:,等待準備就緒...", "highlight")
# 等待驅(qū)動器準備就緒
ready = False
for _ in range(5): # 最多等待5秒
if not self.running:
break
try:
if os.path.exists(f"{drive}:\\"):
ready = True
break
except:
pass
time.sleep(1)
if not self.running:
break
if not ready:
logging.warning(f"驅(qū)動器 {drive}: 未能在5秒內(nèi)準備就緒,跳過")
self.update_status(f"驅(qū)動器 {drive}: 準備超時", "warning")
continue
try:
if is_removable_drive(drive):
self.currently_backing_up = True
self.backup_usb_drive(drive)
self.currently_backing_up = False
else:
logging.info(f"驅(qū)動器 {drive}: 不是可移動驅(qū)動器,跳過備份。")
self.update_status(f"驅(qū)動器 {drive}: 非U盤,跳過")
except Exception as e_check:
logging.error(f"檢查或備份驅(qū)動器 {drive}: 時出錯: {e_check}")
self.update_status(f"錯誤: 處理驅(qū)動器 {drive}: 時出錯", "error")
finally:
if self.running:
self.after(3000, lambda: self.update_status("空閑,等待U盤插入..."))
if removed_drives:
logging.info(f"檢測到驅(qū)動器移除: {sorted(list(removed_drives))}")
known_drives = current_drives
if not new_drives and self.status_bar.cget("text").startswith("狀態(tài): 正在檢測驅(qū)動器"):
self.update_status("空閑,等待U盤插入...")
# 等待指定間隔,并允許提前退出
interval_counter = 0
while self.running and interval_counter < CHECK_INTERVAL:
time.sleep(1)
interval_counter += 1
except Exception as e:
logging.error(f"主循環(huán)發(fā)生錯誤: {e}")
self.update_status(f"錯誤: {e}", "error")
error_wait_counter = 0
while self.running and error_wait_counter < CHECK_INTERVAL * 2:
time.sleep(1)
error_wait_counter += 1
logging.info("后臺監(jiān)控線程已停止。")
self.update_status("程序已停止")
def backup_usb_drive(self, drive_letter):
"""執(zhí)行U盤備份"""
source_drive = f"{drive_letter}:\\"
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
destination_folder = os.path.join(self.backup_destination.get(), f"Backup_{drive_letter}_{timestamp}")
logging.info(f"檢測到U盤: {source_drive}")
self.update_status(f"檢測到U盤: {drive_letter}:\\,準備備份...", "highlight")
logging.info(f"開始備份到: {destination_folder}")
self.update_status(f"開始備份 {drive_letter}:\\ 到 {destination_folder}", "highlight")
# 重置進度條
self.update_progress(0)
start_time = time.time()
try:
# 獲取U盤總大小和可用空間
try:
total_bytes, free_bytes, _ = shutil.disk_usage(source_drive)
total_gb = total_bytes / (1024**3)
free_gb = free_bytes / (1024**3)
logging.info(f"U盤總空間: {total_gb:.2f}GB, 可用空間: {free_gb:.2f}GB")
except Exception as e_size:
logging.warning(f"無法獲取U盤空間信息: {e_size}")
# 計算需要備份的文件總數(shù)
total_files = 0
for root, dirs, files in os.walk(source_drive):
dirs[:] = [d for d in dirs if d not in ['$RECYCLE.BIN', 'System Volume Information']]
files[:] = [f for f in files if not f.lower().endswith(('.tmp', '.log', '.sys'))]
total_files += len(files)
logging.info(f"需要備份的文件總數(shù): {total_files}")
if total_files == 0:
logging.warning("U盤上沒有可備份的文件")
self.update_status(f"{drive_letter}:\\ 沒有可備份的文件", "warning")
return
# 執(zhí)行備份
threaded_copytree(
source_drive,
destination_folder,
max_workers=8,
app_instance=self,
total_files=total_files
)
end_time = time.time()
duration = end_time - start_time
logging.info(f"成功完成備份: {source_drive} -> {destination_folder} (耗時: {duration:.2f} 秒)")
self.update_status(f"備份完成: {drive_letter}:\\ (耗時: {duration:.2f} 秒)", "success")
self.update_progress(100)
# 計算備份大小
try:
backup_size = sum(os.path.getsize(os.path.join(dirpath, filename))
for dirpath, dirnames, filenames in os.walk(destination_folder)
for filename in filenames)
backup_size_gb = backup_size / (1024**3)
logging.info(f"備份總大小: {backup_size_gb:.2f}GB")
except Exception as e_size:
logging.warning(f"無法計算備份大小: {e_size}")
except FileNotFoundError:
logging.error(f"錯誤:源驅(qū)動器 {source_drive} 不存在或無法訪問。")
self.update_status(f"錯誤: 無法訪問 {drive_letter}:\\", "error")
except PermissionError:
logging.error(f"錯誤:沒有權(quán)限讀取 {source_drive} 或?qū)懭?{destination_folder}。")
self.update_status(f"錯誤: 權(quán)限不足 {drive_letter}:\\ 或目標文件夾", "error")
except Exception as e:
logging.error(f"備份U盤 {source_drive} 時發(fā)生未知錯誤: {e}")
self.update_status(f"錯誤: 備份 {drive_letter}:\\ 時發(fā)生未知錯誤", "error")
finally:
if self.running:
self.after(5000, lambda: self.update_status("空閑,等待U盤插入..."))
def quit_app(self):
"""退出應(yīng)用程序"""
if self.currently_backing_up:
if not messagebox.askyesno("確認", "當(dāng)前正在備份中,確定要退出嗎?"):
return
logging.info("收到退出信號,程序即將關(guān)閉。")
self.running = False
if hasattr(self, 'text_handler'):
self.text_handler.stop_processing()
if hasattr(self, 'backup_thread') and self.backup_thread and self.backup_thread.is_alive():
try:
self.backup_thread.join(timeout=2.0)
if self.backup_thread.is_alive():
logging.warning("備份線程未能在2秒內(nèi)停止,將強制關(guān)閉窗口。")
except Exception as e:
logging.error(f"等待備份線程時出錯: {e}")
self.destroy()
# --- 核心備份函數(shù) ---
def get_available_drives():
"""獲取當(dāng)前所有可用的驅(qū)動器盤符"""
drives = []
bitmask = win32file.GetLogicalDrives()
for letter in string.ascii_uppercase:
if bitmask & 1:
drives.append(letter)
bitmask >>= 1
return set(drives)
def is_removable_drive(drive_letter):
"""判斷指定盤符是否是可移動驅(qū)動器"""
drive_path = f"{drive_letter}:\\"
try:
return win32file.GetDriveTypeW(drive_path) == win32file.DRIVE_REMOVABLE
except Exception:
return False
def should_skip_file(src, dst):
"""判斷是否需要跳過備份(增量備份邏輯)"""
if not os.path.exists(dst):
return False
try:
src_stat = os.stat(src)
dst_stat = os.stat(dst)
return src_stat.st_size == dst_stat.st_size and int(src_stat.st_mtime) == int(dst_stat.st_mtime)
except Exception:
return False
def copy_file_with_log(src, dst):
"""復(fù)制單個文件并記錄日志"""
try:
file_size = os.path.getsize(src)
if file_size > 128 * 1024 * 1024: # 大于128MB的文件使用分塊復(fù)制
chunk_size = 16 * 1024 * 1024 # 16MB塊大小
with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst:
while True:
chunk = fsrc.read(chunk_size)
if not chunk:
break
fdst.write(chunk)
try:
shutil.copystat(src, dst) # 復(fù)制文件元數(shù)據(jù)
except Exception as e_stat:
logging.warning(f"無法復(fù)制元數(shù)據(jù) {src} -> {dst}: {e_stat}")
logging.info(f"分塊復(fù)制大文件: {src} -> {dst} ({file_size/1024/1024:.2f}MB)")
else:
shutil.copy2(src, dst) # 小文件直接復(fù)制
logging.info(f"已復(fù)制: {src} -> {dst} ({file_size/1024/1024:.2f}MB)")
except PermissionError as e_perm:
logging.error(f"無權(quán)限復(fù)制文件 {src}: {e_perm}")
raise
except FileNotFoundError as e_notfound:
logging.error(f"文件不存在 {src}: {e_notfound}")
raise
except Exception as e:
logging.error(f"復(fù)制文件 {src} 時出錯: {e}")
raise
def threaded_copytree(src, dst, skip_exts=None, skip_dirs=None, max_workers=8, app_instance=None, total_files=0):
"""線程池遞歸復(fù)制目錄"""
if skip_exts is None:
skip_exts = ['.tmp', '.log', '.sys']
if skip_dirs is None:
skip_dirs = ['$RECYCLE.BIN', 'System Volume Information']
if not os.path.exists(dst):
try:
os.makedirs(dst)
except Exception as e_mkdir:
logging.error(f"創(chuàng)建目錄 {dst} 失敗: {e_mkdir}")
return
copied_files = 0
tasks = []
small_files = []
try:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
try:
if os.path.isdir(s):
if item in skip_dirs:
logging.info(f"跳過系統(tǒng)目錄: {s}")
continue
tasks.append(executor.submit(
threaded_copytree, s, d, skip_exts, skip_dirs, max_workers, app_instance, total_files
))
else:
ext = os.path.splitext(item)[1].lower()
if ext in skip_exts:
logging.info(f"跳過系統(tǒng)文件: {s}")
continue
if should_skip_file(s, d):
copied_files += 1
if app_instance and total_files > 0:
progress = (copied_files / total_files) * 100
app_instance.update_progress(progress)
continue
if os.path.getsize(s) < 16 * 1024 * 1024: # 小于16MB的文件批量處理
small_files.append((s, d))
else:
tasks.append(executor.submit(copy_file_with_log, s, d))
except PermissionError:
logging.warning(f"無權(quán)限訪問: {s},跳過")
except FileNotFoundError:
logging.warning(f"文件或目錄不存在: {s},跳過")
except Exception as e_item:
logging.error(f"處理 {s} 時出錯: {e_item}")
# 批量提交小文件任務(wù)
batch_size = 16
for i in range(0, len(small_files), batch_size):
batch = small_files[i:i+batch_size]
tasks.append(executor.submit(batch_copy_files, batch, app_instance, total_files, copied_files))
copied_files += len(batch)
# 等待所有任務(wù)完成并更新進度
for future in as_completed(tasks):
try:
future.result()
if app_instance and total_files > 0:
copied_files += 1
progress = (copied_files / total_files) * 100
app_instance.update_progress(min(100, progress))
except Exception as e_future:
logging.error(f"線程池任務(wù)出錯: {e_future}")
except PermissionError:
logging.error(f"無權(quán)限訪問源目錄: {src}")
raise
except FileNotFoundError:
logging.error(f"源目錄不存在: {src}")
raise
except Exception as e_pool:
logging.error(f"處理目錄 {src} 時線程池出錯: {e_pool}")
raise
def batch_copy_files(file_pairs, app_instance=None, total_files=0, base_count=0):
"""批量復(fù)制小文件"""
copied = 0
for src, dst in file_pairs:
try:
copy_file_with_log(src, dst)
copied += 1
if app_instance and total_files > 0:
progress = ((base_count + copied) / total_files) * 100
app_instance.update_progress(progress)
except Exception:
continue
if __name__ == "__main__":
# 創(chuàng)建并運行主應(yīng)用
app = App()
app.mainloop()
十、總結(jié)與展望
本文實現(xiàn)的U盤自動備份工具具有以下優(yōu)勢:
- 自動化程度高:完全無需人工干預(yù)
- 備份效率高:多線程+增量備份
- 用戶體驗好:直觀的可視化界面
未來擴展方向:
- 增加云存儲備份支持
- 實現(xiàn)備份數(shù)據(jù)加密
- 添加定期自動清理功能
- 開發(fā)手機端監(jiān)控APP
到此這篇關(guān)于Python實現(xiàn)自動備份U盤內(nèi)容的文章就介紹到這了,更多相關(guān)Python自動備份U盤內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python 時間操作例子和時間格式化參數(shù)小結(jié)
這篇文章主要介紹了Python 時間操作例子,例如取前幾天、后幾天、前一月、后一月等,需要的朋友可以參考下2014-04-04
介紹Python的Django框架中的靜態(tài)資源管理器django-pipeline
這篇文章主要介紹了介紹Python的Django框架中的靜態(tài)資源管理器django-pipeline,django-pipeline是一個開源項目,被用來處理css等靜態(tài)文件,需要的朋友可以參考下2015-04-04
Python結(jié)合PyWebView庫打造跨平臺桌面應(yīng)用
隨著Web技術(shù)的發(fā)展,將HTML/CSS/JavaScript與Python結(jié)合構(gòu)建桌面應(yīng)用成為可能,本文將系統(tǒng)講解如何使用PyWebView庫實現(xiàn)這一創(chuàng)新方案,希望對大家有一定的幫助2025-04-04
解決keras.datasets 在loaddata時,無法下載的問題
這篇文章主要介紹了解決keras.datasets 在loaddata時,無法下載的問題,具有很好的參考價值,希望對大家有所幫助。2021-05-05

