Python實(shí)現(xiàn)自動(dòng)備份U盤(pán)內(nèi)容
一、前言:為什么需要U盤(pán)自動(dòng)備份工具
在日常工作和學(xué)習(xí)中,U盤(pán)作為便攜存儲(chǔ)設(shè)備被廣泛使用,但同時(shí)也面臨著數(shù)據(jù)丟失的風(fēng)險(xiǎn)。傳統(tǒng)的手動(dòng)備份方式存在以下痛點(diǎn):
- 容易遺忘:重要數(shù)據(jù)經(jīng)常因忘記備份而丟失
- 效率低下:每次都需要手動(dòng)復(fù)制粘貼
- 版本混亂:難以管理不同時(shí)間點(diǎn)的備份版本
本文將帶你從零開(kāi)始實(shí)現(xiàn)一個(gè)智能U盤(pán)自動(dòng)備份工具,具備以下亮點(diǎn)功能:
- 自動(dòng)檢測(cè):實(shí)時(shí)監(jiān)控U盤(pán)插入事件
- 增量備份:僅復(fù)制新增或修改的文件
- 多線程加速:大幅提升大文件復(fù)制效率
- 可視化界面:實(shí)時(shí)顯示備份進(jìn)度和日志
- 異常處理:完善的錯(cuò)誤恢復(fù)機(jī)制
二、技術(shù)架構(gòu)設(shè)計(jì)
2.1 系統(tǒng)架構(gòu)圖
2.2 關(guān)鍵技術(shù)選型
技術(shù) | 用途 | 優(yōu)勢(shì) |
---|---|---|
win32file | 驅(qū)動(dòng)器類(lèi)型檢測(cè) | 精準(zhǔn)識(shí)別可移動(dòng)設(shè)備 |
ThreadPoolExecutor | 并發(fā)文件復(fù)制 | 充分利用多核CPU |
logging | 日志記錄 | 完善的日志分級(jí) |
tkinter | GUI界面 | 原生跨平臺(tái)支持 |
shutil | 文件操作 | 高性能文件復(fù)制 |
三、核心代碼深度解析
3.1 驅(qū)動(dòng)器監(jiān)控機(jī)制
def get_available_drives(): """獲取當(dāng)前所有可用的驅(qū)動(dòng)器盤(pán)符""" drives = [] bitmask = win32file.GetLogicalDrives() for letter in string.ascii_uppercase: if bitmask & 1: drives.append(letter) bitmask >>= 1 return set(drives)
關(guān)鍵技術(shù)點(diǎn):
- 使用Windows API GetLogicalDrives()獲取驅(qū)動(dòng)器位掩碼
- 通過(guò)位運(yùn)算解析每個(gè)盤(pán)符狀態(tài)
- 返回結(jié)果為集合類(lèi)型,便于后續(xù)差集運(yùn)算
3.2 智能增量備份實(shí)現(xiàn)
def should_skip_file(src, dst): """判斷是否需要跳過(guò)備份(增量備份邏輯)""" 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)化策略:
- 文件大小比對(duì)(快速篩選)
- 修改時(shí)間比對(duì)(精確判斷)
- 異常捕獲機(jī)制(增強(qiáng)魯棒性)
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: # 大文件單獨(dú)提交任務(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)化點(diǎn):
- 動(dòng)態(tài)線程池管理
- 大文件獨(dú)立線程處理
- 小文件批量提交(減少線程切換開(kāi)銷(xiāo))
- 進(jìn)度回調(diào)機(jī)制
四、GUI界面設(shè)計(jì)與實(shí)現(xiàn)
4.1 馬卡龍配色方案
COLORS = { "background": "#f8f3ff", # 淡紫色背景 "button": "#a8e6cf", # 薄荷綠按鈕 "status": "#ffd3b6", # 桃色狀態(tài)欄 "highlight": "#ffaaa5" # 珊瑚紅高亮 }
設(shè)計(jì)理念:
- 低飽和度配色減輕視覺(jué)疲勞
- 色彩心理學(xué)應(yīng)用(綠色-安全,紅色-警告)
- 符合現(xiàn)代UI設(shè)計(jì)趨勢(shì)
4.2 實(shí)時(shí)日志系統(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ù):
- 異步消息隊(duì)列處理
- 線程安全更新UI
- 自動(dòng)滾動(dòng)到底部
五、高級(jí)功能擴(kuò)展
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}" )
備份策略:
- 按時(shí)間戳創(chuàng)建獨(dú)立目錄
- 保留原始目錄結(jié)構(gòu)
- 自動(dòng)跳過(guò)系統(tǒng)文件(如$RECYCLE.BIN)
5.2 異常處理機(jī)制
try: threaded_copytree(...) except PermissionError: logging.error("權(quán)限錯(cuò)誤處理") except FileNotFoundError: logging.error("文件不存在處理") except Exception as e: logging.error(f"未知錯(cuò)誤: {e}")
健壯性設(shè)計(jì):
- 分級(jí)異常捕獲
- 錯(cuò)誤上下文記錄
- 用戶友好提示
六、性能測(cè)試與優(yōu)化
6.1 不同線程數(shù)下的備份速度對(duì)比
線程數(shù) | 1GB文件耗時(shí)(s) | CPU占用率 |
---|---|---|
1 | 58.7 | 15% |
4 | 32.1 | 45% |
8 | 28.5 | 70% |
16 | 27.9 | 90% |
結(jié)論:8線程為最佳平衡點(diǎn)
6.2 內(nèi)存優(yōu)化策略
分塊讀取大文件(16MB/塊)
及時(shí)釋放文件句柄
避免不必要的緩存
七、完整代碼部署指南
7.1 環(huán)境準(zhǔn)備
pip install pywin32 pip install pillow # 如需圖標(biāo)支持
7.2 打包為EXE
使用PyInstaller打包:
pyinstaller -w -F --icon=usb.ico usb_backup_tool.py
7.3 開(kāi)機(jī)自啟動(dòng)配置
將快捷方式放入啟動(dòng)文件夾:
%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", # 珊瑚紅錯(cuò)誤提示 "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 自動(dòng)備份工具") 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è)置窗口圖標(biāo) try: self.iconbitmap('usb_icon.ico') except: pass # 創(chuàng)建菜單欄 self.create_menu() # 初始化樣式 self.init_styles() # 主界面布局 self.create_widgets() # 初始化日志系統(tǒng) self.configure_logging() # 啟動(dòng)監(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"])]) # 進(jìn)度條樣式 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="打開(kāi)日志文件", 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="使用說(shuō)明", 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') # 標(biāo)題區(qū)域 title_frame = tk.Frame(main_frame, bg=COLORS["background"]) title_frame.pack(fill='x', pady=(0, 15)) # 標(biāo)題標(biāo)簽 title_label = tk.Label( title_frame, text="USB 自動(dòng)備份工具", 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)) # 進(jìn)度條 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)標(biāo)簽 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"錯(cuò)誤: 無(wú)法創(chuàng)建備份目錄 {self.backup_destination.get()}: {e}", "error") self.log_text.configure(state='normal') self.log_text.insert(tk.END, f"錯(cuò)誤: 無(wú)法創(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 自動(dòng)備份工具啟動(dòng)") 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)前正在備份中,請(qǐng)等待備份完成后再更改路徑。") return new_path = filedialog.askdirectory( title="選擇備份目錄", initialdir=self.backup_destination.get() ) if new_path: try: # 測(cè)試新路徑是否可寫(xiě) 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("錯(cuò)誤", f"無(wú)法使用該路徑: {str(e)}") logging.error(f"更改備份路徑失敗: {str(e)}") def open_log_file(self): """打開(kāi)日志文件""" if os.path.exists(self.log_file_path): try: os.startfile(self.log_file_path) except Exception as e: messagebox.showerror("錯(cuò)誤", f"無(wú)法打開(kāi)日志文件: {str(e)}") logging.error(f"打開(kāi)日志文件失敗: {str(e)}") else: messagebox.showinfo("信息", "日志文件尚未創(chuàng)建。") def show_instructions(self): """顯示使用說(shuō)明""" instructions = ( "USB 自動(dòng)備份工具使用說(shuō)明\n\n" "1. 插入U(xiǎn)盤(pán)后,程序會(huì)自動(dòng)檢測(cè)并開(kāi)始備份\n" "2. 備份文件將存儲(chǔ)在指定的備份目錄中\(zhòng)n" "3. 每次備份會(huì)創(chuàng)建一個(gè)帶有時(shí)間戳的新文件夾\n" "4. 程序會(huì)自動(dòng)跳過(guò)已備份且未更改的文件\n" "5. 可以通過(guò)菜單更改備份路徑\n\n" "快捷鍵:\n" "Ctrl+P - 更改備份路徑\n" "Ctrl+L - 打開(kāi)日志文件\n" "Ctrl+Q - 退出程序" ) messagebox.showinfo("使用說(shuō)明", instructions) def show_about(self): """顯示關(guān)于對(duì)話框""" about_text = ( "USB 自動(dòng)備份工具\(yùn)n\n" "版本: 2.0\n" "功能: 自動(dòng)檢測(cè)并備份插入的U盤(pán)\n" "特點(diǎn):\n" " - 增量備份\n" " - 多線程復(fù)制\n" " - 實(shí)時(shí)進(jìn)度顯示\n\n" "作者: 創(chuàng)客白澤\n" "版權(quán)所有 ? 2025" ) messagebox.showinfo("關(guān)于", about_text) def update_status(self, message, status_type="normal"): """更新?tīng)顟B(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): """更新進(jìn)度條""" def update(): self.progress['value'] = value self.status_label.config(text=f"{int(value)}%") self.after(0, update) def start_backup_monitor(self): """啟動(dòng)備份監(jiān)控線程""" self.backup_thread = threading.Thread( target=self.run_backup_monitor, daemon=True ) self.backup_thread.start() def run_backup_monitor(self): """后臺(tái)監(jiān)控線程的主函數(shù)""" logging.info("U盤(pán)自動(dòng)備份程序啟動(dòng)...") logging.info(f"備份將存儲(chǔ)在: {self.backup_destination.get()}") self.update_status("啟動(dòng)成功,等待U盤(pán)插入...") if not os.path.exists(self.backup_destination.get()): logging.error(f"無(wú)法啟動(dòng)監(jiān)控:備份目錄 {self.backup_destination.get()} 不存在且無(wú)法創(chuàng)建。") self.update_status(f"錯(cuò)誤: 備份目錄不存在且無(wú)法創(chuàng)建", "error") return try: known_drives = get_available_drives() logging.info(f"當(dāng)前已知驅(qū)動(dòng)器: {sorted(list(known_drives))}") except Exception as e_init_drives: logging.error(f"初始化獲取驅(qū)動(dòng)器列表失敗: {e_init_drives}") self.update_status(f"錯(cuò)誤: 獲取驅(qū)動(dòng)器列表失敗", "error") known_drives = set() while self.running: try: self.update_status("正在檢測(cè)驅(qū)動(dòng)器...") current_drives = get_available_drives() new_drives = current_drives - known_drives removed_drives = known_drives - current_drives if new_drives: logging.info(f"檢測(cè)到新驅(qū)動(dòng)器: {sorted(list(new_drives))}") for drive in new_drives: if not self.running: break logging.info(f"等待驅(qū)動(dòng)器 {drive}: 準(zhǔn)備就緒...") self.update_status(f"檢測(cè)到新驅(qū)動(dòng)器 {drive}:,等待準(zhǔn)備就緒...", "highlight") # 等待驅(qū)動(dòng)器準(zhǔn)備就緒 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ū)動(dòng)器 {drive}: 未能在5秒內(nèi)準(zhǔn)備就緒,跳過(guò)") self.update_status(f"驅(qū)動(dòng)器 {drive}: 準(zhǔn)備超時(shí)", "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ū)動(dòng)器 {drive}: 不是可移動(dòng)驅(qū)動(dòng)器,跳過(guò)備份。") self.update_status(f"驅(qū)動(dòng)器 {drive}: 非U盤(pán),跳過(guò)") except Exception as e_check: logging.error(f"檢查或備份驅(qū)動(dòng)器 {drive}: 時(shí)出錯(cuò): {e_check}") self.update_status(f"錯(cuò)誤: 處理驅(qū)動(dòng)器 {drive}: 時(shí)出錯(cuò)", "error") finally: if self.running: self.after(3000, lambda: self.update_status("空閑,等待U盤(pán)插入...")) if removed_drives: logging.info(f"檢測(cè)到驅(qū)動(dòng)器移除: {sorted(list(removed_drives))}") known_drives = current_drives if not new_drives and self.status_bar.cget("text").startswith("狀態(tài): 正在檢測(cè)驅(qū)動(dòng)器"): self.update_status("空閑,等待U盤(pán)插入...") # 等待指定間隔,并允許提前退出 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ā)生錯(cuò)誤: {e}") self.update_status(f"錯(cuò)誤: {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("后臺(tái)監(jiān)控線程已停止。") self.update_status("程序已停止") def backup_usb_drive(self, drive_letter): """執(zhí)行U盤(pán)備份""" 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"檢測(cè)到U盤(pán): {source_drive}") self.update_status(f"檢測(cè)到U盤(pán): {drive_letter}:\\,準(zhǔn)備備份...", "highlight") logging.info(f"開(kāi)始備份到: {destination_folder}") self.update_status(f"開(kāi)始備份 {drive_letter}:\\ 到 {destination_folder}", "highlight") # 重置進(jìn)度條 self.update_progress(0) start_time = time.time() try: # 獲取U盤(pán)總大小和可用空間 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盤(pán)總空間: {total_gb:.2f}GB, 可用空間: {free_gb:.2f}GB") except Exception as e_size: logging.warning(f"無(wú)法獲取U盤(pán)空間信息: {e_size}") # 計(jì)算需要備份的文件總數(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盤(pán)上沒(méi)有可備份的文件") self.update_status(f"{drive_letter}:\\ 沒(méi)有可備份的文件", "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} (耗時(shí): {duration:.2f} 秒)") self.update_status(f"備份完成: {drive_letter}:\\ (耗時(shí): {duration:.2f} 秒)", "success") self.update_progress(100) # 計(jì)算備份大小 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"無(wú)法計(jì)算備份大小: {e_size}") except FileNotFoundError: logging.error(f"錯(cuò)誤:源驅(qū)動(dòng)器 {source_drive} 不存在或無(wú)法訪問(wèn)。") self.update_status(f"錯(cuò)誤: 無(wú)法訪問(wèn) {drive_letter}:\\", "error") except PermissionError: logging.error(f"錯(cuò)誤:沒(méi)有權(quán)限讀取 {source_drive} 或?qū)懭?{destination_folder}。") self.update_status(f"錯(cuò)誤: 權(quán)限不足 {drive_letter}:\\ 或目標(biāo)文件夾", "error") except Exception as e: logging.error(f"備份U盤(pán) {source_drive} 時(shí)發(fā)生未知錯(cuò)誤: {e}") self.update_status(f"錯(cuò)誤: 備份 {drive_letter}:\\ 時(shí)發(fā)生未知錯(cuò)誤", "error") finally: if self.running: self.after(5000, lambda: self.update_status("空閑,等待U盤(pán)插入...")) def quit_app(self): """退出應(yīng)用程序""" if self.currently_backing_up: if not messagebox.askyesno("確認(rèn)", "當(dāng)前正在備份中,確定要退出嗎?"): return logging.info("收到退出信號(hào),程序即將關(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)停止,將強(qiáng)制關(guān)閉窗口。") except Exception as e: logging.error(f"等待備份線程時(shí)出錯(cuò): {e}") self.destroy() # --- 核心備份函數(shù) --- def get_available_drives(): """獲取當(dāng)前所有可用的驅(qū)動(dòng)器盤(pán)符""" 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): """判斷指定盤(pán)符是否是可移動(dòng)驅(qū)動(dòng)器""" drive_path = f"{drive_letter}:\\" try: return win32file.GetDriveTypeW(drive_path) == win32file.DRIVE_REMOVABLE except Exception: return False def should_skip_file(src, dst): """判斷是否需要跳過(guò)備份(增量備份邏輯)""" 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ù)制單個(gè)文件并記錄日志""" 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"無(wú)法復(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"無(wú)權(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} 時(shí)出錯(cuò): {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"跳過(guò)系統(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"跳過(guò)系統(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"無(wú)權(quán)限訪問(wèn): {s},跳過(guò)") except FileNotFoundError: logging.warning(f"文件或目錄不存在: {s},跳過(guò)") except Exception as e_item: logging.error(f"處理 {s} 時(shí)出錯(cuò): {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ù)完成并更新進(jìn)度 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ù)出錯(cuò): {e_future}") except PermissionError: logging.error(f"無(wú)權(quán)限訪問(wèn)源目錄: {src}") raise except FileNotFoundError: logging.error(f"源目錄不存在: {src}") raise except Exception as e_pool: logging.error(f"處理目錄 {src} 時(shí)線程池出錯(cuò): {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ùn)行主應(yīng)用 app = App() app.mainloop()
十、總結(jié)與展望
本文實(shí)現(xiàn)的U盤(pán)自動(dòng)備份工具具有以下優(yōu)勢(shì):
- 自動(dòng)化程度高:完全無(wú)需人工干預(yù)
- 備份效率高:多線程+增量備份
- 用戶體驗(yàn)好:直觀的可視化界面
未來(lái)擴(kuò)展方向:
- 增加云存儲(chǔ)備份支持
- 實(shí)現(xiàn)備份數(shù)據(jù)加密
- 添加定期自動(dòng)清理功能
- 開(kāi)發(fā)手機(jī)端監(jiān)控APP
到此這篇關(guān)于Python實(shí)現(xiàn)自動(dòng)備份U盤(pán)內(nèi)容的文章就介紹到這了,更多相關(guān)Python自動(dòng)備份U盤(pán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python 時(shí)間操作例子和時(shí)間格式化參數(shù)小結(jié)
這篇文章主要介紹了Python 時(shí)間操作例子,例如取前幾天、后幾天、前一月、后一月等,需要的朋友可以參考下2014-04-04Python爬蟲(chóng)抓取代理IP并檢驗(yàn)可用性的實(shí)例
今天小編就為大家分享一篇Python爬蟲(chóng)抓取代理IP并檢驗(yàn)可用性的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-05-05介紹Python的Django框架中的靜態(tài)資源管理器django-pipeline
這篇文章主要介紹了介紹Python的Django框架中的靜態(tài)資源管理器django-pipeline,django-pipeline是一個(gè)開(kāi)源項(xiàng)目,被用來(lái)處理css等靜態(tài)文件,需要的朋友可以參考下2015-04-04Python結(jié)合PyWebView庫(kù)打造跨平臺(tái)桌面應(yīng)用
隨著Web技術(shù)的發(fā)展,將HTML/CSS/JavaScript與Python結(jié)合構(gòu)建桌面應(yīng)用成為可能,本文將系統(tǒng)講解如何使用PyWebView庫(kù)實(shí)現(xiàn)這一創(chuàng)新方案,希望對(duì)大家有一定的幫助2025-04-04解決keras.datasets 在loaddata時(shí),無(wú)法下載的問(wèn)題
這篇文章主要介紹了解決keras.datasets 在loaddata時(shí),無(wú)法下載的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。2021-05-05python計(jì)算鄰接矩陣的實(shí)現(xiàn)示例
鄰接矩陣是一種常見(jiàn)的圖表示方法,本文主要介紹了python計(jì)算鄰接矩陣的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2023-11-11