Python結(jié)合PyQt5實現(xiàn)GitHub文件/文件夾批量上傳工具
基于 Python + PyQt5 開發(fā)的可視化桌面應用,核心功能是自動創(chuàng)建 GitHub 倉庫并支持單個文件或整個文件夾的批量上傳,無需手動操作 Git 命令或 GitHub 網(wǎng)頁端,適合非技術(shù)人員或需要快速上傳文件到 GitHub 的場景。

核心特性
可視化操作界面,無需命令行;
自動檢測/創(chuàng)建 GitHub 倉庫(不存在則創(chuàng)建,存在則直接使用);
支持單文件/整文件夾上傳,保留文件夾原有目錄結(jié)構(gòu);
實時上傳進度顯示 + 詳細日志輸出,便于排查問題;
支持文件覆蓋(自動獲取文件 SHA 值實現(xiàn)更新);
自定義倉庫內(nèi)上傳根路徑(如 uploads/)。
二、環(huán)境準備
1. 依賴安裝
工具基于 Python 開發(fā),需先安裝以下依賴包:
pip install requests PyQt5
2. GitHub 個人訪問令牌(PAT)準備
上傳文件需要 GitHub 權(quán)限,需創(chuàng)建帶 repo 權(quán)限的經(jīng)典令牌:
- 打開 GitHub 設置
- 點擊「Generate new token (classic)」;
- 勾選
repo權(quán)限(倉庫相關(guān)操作); - 設置令牌有效期(建議選「No expiration」,或按需設置);
- 生成令牌后立即復制保存(僅顯示一次,丟失需重新創(chuàng)建)。
三、工具使用教程
1. 運行程序
將代碼保存為 github_uploader.py,執(zhí)行以下命令啟動:
python github_uploader.py
啟動后會看到可視化主界面,分為 5 個核心區(qū)域:
2. 操作步驟
步驟 1:選擇上傳模式
- 「單個文件」:上傳指定的單個文件(如
test.txt); - 「整個文件夾」:上傳文件夾內(nèi)所有文件(保留目錄結(jié)構(gòu))。
步驟 2:選擇本地路徑
- 點擊「選擇文件/選擇文件夾」按鈕,選擇要上傳的本地文件/文件夾;
- 路徑會自動填充到輸入框中。
步驟 3:填寫 GitHub 配置
| 配置項 | 說明 |
|---|---|
| 倉庫所有者 | 你的 GitHub 用戶名(區(qū)分大小寫,如 octocat) |
| 倉庫名稱 | 要創(chuàng)建/上傳的倉庫名(如 file-upload-test,不存在則自動創(chuàng)建) |
| PAT 令牌 | 步驟 2 中創(chuàng)建的帶 repo 權(quán)限的令牌(輸入時隱藏,保障安全) |
| 倉庫內(nèi)根路徑 | 可選,文件上傳到倉庫的指定目錄(默認 uploads/,如填 docs/ 則文件上傳到 docs/ 下) |
步驟 4:開始上傳
點擊「開始上傳」按鈕,程序會:
- 校驗輸入是否完整(路徑、用戶名、倉庫名、令牌不能為空);
- 檢查倉庫是否存在,不存在則自動創(chuàng)建;
- 掃描本地文件,批量上傳(實時輸出日志 + 進度);
「清空日志」按鈕可清空當前日志區(qū)域內(nèi)容,便于重新上傳。
3. 上傳結(jié)果說明
- 上傳成功:彈窗提示「上傳完成!所有文件均成功上傳到 GitHub」,日志顯示每個文件的上傳狀態(tài);
- 部分失?。簭棿疤崾尽干蟼魍瓿?,但有步驟失敗」,需查看日志中的錯誤信息(如令牌權(quán)限不足、文件路徑錯誤等)。
四、核心代碼解析
1. 核心類說明
| 類名 | 作用 |
|---|---|
UploadThread | 上傳線程類(繼承 QThread),避免 UI 卡頓,包含倉庫創(chuàng)建、文件掃描、文件上傳邏輯 |
GitHubUploader | 主界面類(繼承 QWidget),負責界面布局、用戶交互、線程調(diào)用 |
2. 關(guān)鍵功能實現(xiàn)
(1)自動創(chuàng)建倉庫
def create_github_repo(self):
# 檢查倉庫是否存在
check_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}"
check_response = requests.get(check_url, headers=headers)
# 不存在則創(chuàng)建
if check_response.status_code != 200:
create_url = "https://api.github.com/user/repos"
create_data = {"name": self.repo_name, "private": False, "auto_init": True}
requests.post(create_url, json=create_data, headers=headers)- 通過 GitHub API 先檢查倉庫是否存在,狀態(tài)碼 200 表示存在;
- 不存在則調(diào)用創(chuàng)建倉庫 API,
auto_init=True自動初始化倉庫(避免空倉庫無法上傳)。
(2)文件上傳核心邏輯
def upload_single_file(self, local_file_path, github_file_path):
# 讀取文件并 Base64 編碼(GitHub API 要求)
with open(local_file_path, "rb") as f:
file_content = base64.b64encode(f.read()).decode("utf-8")
# 構(gòu)建上傳請求(PUT 方法,支持覆蓋)
api_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}/contents/{github_file_path}"
data = {"message": f"Upload file: {os.path.basename(local_file_path)}", "content": file_content}
# 若文件已存在,添加 SHA 值實現(xiàn)覆蓋
if response.status_code == 200:
data["sha"] = response.json()["sha"]
requests.put(api_url, json=data, headers=headers)- GitHub API 要求文件內(nèi)容為 Base64 編碼;
- 使用 PUT 方法上傳,已存在的文件需傳入 SHA 值實現(xiàn)覆蓋。
(3)多線程處理
# 線程信號定義(用于線程與 UI 通信) log_signal = pyqtSignal(str) # 日志更新 progress_signal = pyqtSignal(int) # 進度更新 finish_signal = pyqtSignal(bool) # 上傳完成
- 上傳邏輯放在獨立線程中,避免 UI 凍結(jié);
- 通過
pyqtSignal實現(xiàn)線程向主界面?zhèn)鬟f日志、進度等信息。
五、常見問題排查
1. 倉庫創(chuàng)建失敗
- 原因 1:PAT 令牌無
repo權(quán)限 → 重新創(chuàng)建令牌并勾選repo; - 原因 2:倉庫名已被占用 → 更換倉庫名;
- 原因 3:網(wǎng)絡問題 → 檢查網(wǎng)絡連接,或更換 GitHub 鏡像源。
2. 文件上傳失敗
- 原因 1:本地文件路徑錯誤 → 確認文件/文件夾存在;
- 原因 2:令牌過期/錯誤 → 檢查令牌是否正確,或重新生成;
- 原因 3:文件過大 → GitHub API 單文件上傳限制 100MB,超大文件需用 Git LFS(工具暫不支持)。
3. UI 無響應
原因:上傳超大文件夾時,線程未及時釋放資源 → 等待日志輸出,或拆分文件夾分批上傳。
七、完整代碼(可直接復制)
import sys
import requests
import base64
import os
from pathlib import Path
from PyQt5.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QFileDialog,
QTextEdit, QGroupBox, QMessageBox, QRadioButton
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
# 上傳線程(支持自動創(chuàng)建倉庫 + 文件/文件夾上傳)
class UploadThread(QThread):
log_signal = pyqtSignal(str) # 日志信號
progress_signal = pyqtSignal(int) # 進度信號
finish_signal = pyqtSignal(bool) # 完成信號
def __init__(self, target_path, repo_owner, repo_name, github_token, github_root_path, is_folder):
super().__init__()
self.target_path = target_path # 本地文件/文件夾路徑
self.repo_owner = repo_owner # GitHub倉庫所有者
self.repo_name = repo_name # GitHub倉庫名
self.github_token = github_token # PAT令牌
self.github_root_path = github_root_path # 倉庫內(nèi)根路徑
self.is_folder = is_folder # 是否是文件夾上傳
self.total_files = 0 # 總文件數(shù)
self.uploaded_files = 0 # 已上傳文件數(shù)
# 自動創(chuàng)建GitHub倉庫(不存在則創(chuàng)建)
def create_github_repo(self):
try:
# 1. 先檢查倉庫是否已存在
check_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}"
headers = {
"Authorization": f"token {self.github_token}",
"Accept": "application/vnd.github.v3+json"
}
check_response = requests.get(check_url, headers=headers)
# 倉庫已存在
if check_response.status_code == 200:
self.log_signal.emit(f"倉庫 {self.repo_owner}/{self.repo_name} 已存在,無需創(chuàng)建")
return True
# 倉庫不存在,嘗試創(chuàng)建
self.log_signal.emit(f"倉庫 {self.repo_owner}/{self.repo_name} 不存在,正在自動創(chuàng)建...")
create_url = "https://api.github.com/user/repos"
create_data = {
"name": self.repo_name,
"private": False, # 創(chuàng)建公共倉庫(如需私有,改為True)
"auto_init": True, # 自動初始化(創(chuàng)建README,避免空倉庫)
"description": "Auto-created by GitHub Upload Tool"
}
create_response = requests.post(create_url, json=create_data, headers=headers)
if create_response.status_code == 201:
self.log_signal.emit(f"倉庫 {self.repo_name} 創(chuàng)建成功!")
return True
else:
error_detail = create_response.json() if create_response.headers.get(
'Content-Type') == 'application/json' else create_response.text
self.log_signal.emit(f"倉庫創(chuàng)建失敗: {error_detail}")
return False
except Exception as e:
self.log_signal.emit(f"倉庫檢查/創(chuàng)建出錯: {str(e)}")
return False
# 遞歸獲取文件夾內(nèi)所有文件
def get_all_files(self, folder_path):
file_list = []
for root, dirs, files in os.walk(folder_path):
for file in files:
file_path = os.path.join(root, file)
# 計算相對路徑(保持文件夾結(jié)構(gòu))
rel_path = os.path.relpath(file_path, folder_path)
file_list.append((file_path, rel_path))
self.total_files = len(file_list)
return file_list
# 單個文件上傳邏輯
def upload_single_file(self, local_file_path, github_file_path):
try:
# 讀取并編碼文件內(nèi)容
with open(local_file_path, "rb") as f:
file_content = f.read()
encoded_content = base64.b64encode(file_content).decode("utf-8")
# 構(gòu)建API請求URL
api_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}/contents/{github_file_path}"
headers = {
"Authorization": f"token {self.github_token}",
"Accept": "application/vnd.github.v3+json"
}
# 檢查文件是否已存在(獲取SHA)
response = requests.get(api_url, headers=headers)
data = {
"message": f"Upload file: {os.path.basename(local_file_path)}",
"content": encoded_content
}
if response.status_code == 200:
data["sha"] = response.json()["sha"]
# 發(fā)送上傳請求
response = requests.put(api_url, json=data, headers=headers)
if response.status_code in [200, 201]:
self.uploaded_files += 1
progress = int(self.uploaded_files / self.total_files * 100) if self.total_files > 0 else 100
self.progress_signal.emit(progress)
return True, f"上傳成功: {github_file_path}"
else:
error_detail = response.json() if response.headers.get(
'Content-Type') == 'application/json' else response.text
return False, f"上傳失敗: {github_file_path} (狀態(tài)碼: {response.status_code})\n 錯誤詳情: {error_detail}"
except Exception as e:
return False, f"文件出錯: {local_file_path} - {str(e)}"
def run(self):
try:
# 第一步:檢查/創(chuàng)建倉庫(核心新增邏輯)
self.log_signal.emit("開始檢查倉庫狀態(tài)...")
if not self.create_github_repo():
self.finish_signal.emit(False)
return
# 第二步:掃描文件
self.log_signal.emit("開始掃描本地文件...")
# 區(qū)分文件/文件夾處理
if self.is_folder:
# 文件夾上傳:遞歸獲取所有文件
if not os.path.isdir(self.target_path):
raise NotADirectoryError(f"指定路徑不是文件夾: {self.target_path}")
file_list = self.get_all_files(self.target_path)
if self.total_files == 0:
self.log_signal.emit("文件夾內(nèi)未找到任何文件")
self.finish_signal.emit(True)
return
self.log_signal.emit(f"共檢測到 {self.total_files} 個文件,開始批量上傳...")
# 逐個上傳文件
for local_file, rel_path in file_list:
# 拼接倉庫內(nèi)完整路徑(根路徑 + 相對路徑)
github_file_path = os.path.join(self.github_root_path, rel_path).replace("\\", "/")
success, msg = self.upload_single_file(local_file, github_file_path)
self.log_signal.emit(msg)
# 如果上傳失敗,記錄但繼續(xù)上傳其他文件
if not success:
self.log_signal.emit(f"繼續(xù)上傳其他文件...")
self.log_signal.emit(f"\n上傳完成!總計 {self.total_files} 個文件,成功上傳 {self.uploaded_files} 個")
self.finish_signal.emit(self.uploaded_files == self.total_files)
else:
# 單個文件上傳
self.total_files = 1
self.progress_signal.emit(0)
github_file_path = os.path.join(self.github_root_path, os.path.basename(self.target_path)).replace("\\",
"/")
success, msg = self.upload_single_file(self.target_path, github_file_path)
self.log_signal.emit(msg)
self.progress_signal.emit(100)
self.finish_signal.emit(success)
except Exception as e:
self.log_signal.emit(f"整體流程出錯: {str(e)}")
self.finish_signal.emit(False)
# 主界面窗口
class GitHubUploader(QWidget):
def __init__(self):
super().__init__()
self.upload_thread = None
self.is_folder_mode = False # 默認是文件模式
self.init_ui()
def init_ui(self):
# 窗口基本設置
self.setWindowTitle("GitHub 文件/文件夾上傳工具")
self.setGeometry(100, 100, 800, 600)
# 整體布局
main_layout = QVBoxLayout()
main_layout.setSpacing(15)
main_layout.setContentsMargins(20, 20, 20, 20)
# 1. 選擇模式(文件/文件夾)
mode_group = QGroupBox("上傳模式")
mode_layout = QHBoxLayout()
self.file_radio = QRadioButton("單個文件")
self.folder_radio = QRadioButton("整個文件夾")
self.file_radio.setChecked(True) # 默認選中文件模式
# 綁定模式切換事件
self.file_radio.clicked.connect(self.switch_mode)
self.folder_radio.clicked.connect(self.switch_mode)
mode_layout.addWidget(self.file_radio)
mode_layout.addWidget(self.folder_radio)
mode_group.setLayout(mode_layout)
main_layout.addWidget(mode_group)
# 2. 路徑選擇區(qū)域
path_group = QGroupBox("路徑選擇")
path_layout = QHBoxLayout()
self.path_edit = QLineEdit()
self.path_edit.setPlaceholderText("請選擇要上傳的文件/文件夾路徑")
self.path_btn = QPushButton("選擇文件")
self.path_btn.clicked.connect(self.select_path)
path_layout.addWidget(self.path_edit)
path_layout.addWidget(self.path_btn)
path_group.setLayout(path_layout)
main_layout.addWidget(path_group)
# 3. GitHub配置區(qū)域
github_group = QGroupBox("GitHub 配置")
github_layout = QVBoxLayout()
# 倉庫所有者
owner_layout = QHBoxLayout()
owner_layout.addWidget(QLabel("倉庫所有者:"))
self.repo_owner_edit = QLineEdit()
self.repo_owner_edit.setPlaceholderText("必填:你的GitHub用戶名(區(qū)分大小寫)")
owner_layout.addWidget(self.repo_owner_edit)
github_layout.addLayout(owner_layout)
# 倉庫名稱
repo_layout = QHBoxLayout()
repo_layout.addWidget(QLabel("倉庫名稱:"))
self.repo_name_edit = QLineEdit()
self.repo_name_edit.setPlaceholderText("必填:要創(chuàng)建/上傳的倉庫名(如test-upload)")
repo_layout.addWidget(self.repo_name_edit)
github_layout.addLayout(repo_layout)
# 訪問令牌
token_layout = QHBoxLayout()
token_layout.addWidget(QLabel("PAT令牌:"))
self.token_edit = QLineEdit()
self.token_edit.setPlaceholderText("必填:帶repo權(quán)限的GitHub經(jīng)典令牌")
self.token_edit.setEchoMode(QLineEdit.Password)
token_layout.addWidget(self.token_edit)
github_layout.addLayout(token_layout)
# 倉庫內(nèi)根路徑
root_layout = QHBoxLayout()
root_layout.addWidget(QLabel("倉庫內(nèi)根路徑:"))
self.github_root_edit = QLineEdit()
self.github_root_edit.setPlaceholderText("可選:如uploads/(默認自動填充)")
self.github_root_edit.setText("uploads/") # 默認根路徑
root_layout.addWidget(self.github_root_edit)
github_layout.addLayout(root_layout)
github_group.setLayout(github_layout)
main_layout.addWidget(github_group)
# 4. 操作按鈕 + 進度提示
op_layout = QHBoxLayout()
self.upload_btn = QPushButton("開始上傳")
self.upload_btn.clicked.connect(self.start_upload)
self.clear_btn = QPushButton("清空日志")
self.clear_btn.clicked.connect(self.clear_log)
self.progress_label = QLabel("進度: 0%")
op_layout.addWidget(self.upload_btn)
op_layout.addWidget(self.clear_btn)
op_layout.addStretch()
op_layout.addWidget(self.progress_label)
main_layout.addLayout(op_layout)
# 5. 日志輸出區(qū)域
log_group = QGroupBox("上傳日志")
log_layout = QVBoxLayout()
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
log_layout.addWidget(self.log_text)
log_group.setLayout(log_layout)
main_layout.addWidget(log_group)
self.setLayout(main_layout)
# 切換上傳模式(文件/文件夾)
def switch_mode(self):
self.is_folder_mode = self.folder_radio.isChecked()
if self.is_folder_mode:
self.path_btn.setText("選擇文件夾")
self.path_edit.setPlaceholderText("請選擇要上傳的文件夾路徑")
else:
self.path_btn.setText("選擇文件")
self.path_edit.setPlaceholderText("請選擇要上傳的文件路徑")
# 選擇文件/文件夾路徑
def select_path(self):
if self.is_folder_mode:
# 選擇文件夾
folder_path = QFileDialog.getExistingDirectory(self, "選擇文件夾")
if folder_path:
self.path_edit.setText(folder_path)
else:
# 選擇文件
file_path, _ = QFileDialog.getOpenFileName(self, "選擇文件", "", "所有文件 (*.*)")
if file_path:
self.path_edit.setText(file_path)
# 清空日志
def clear_log(self):
self.log_text.clear()
self.progress_label.setText("進度: 0%")
# 開始上傳
def start_upload(self):
# 1. 校驗輸入
target_path = self.path_edit.text().strip()
repo_owner = self.repo_owner_edit.text().strip()
repo_name = self.repo_name_edit.text().strip()
token = self.token_edit.text().strip()
github_root_path = self.github_root_edit.text().strip()
if not target_path:
tip = "請先選擇要上傳的文件夾!" if self.is_folder_mode else "請先選擇要上傳的文件!"
QMessageBox.warning(self, "提示", tip)
return
if not repo_owner or not repo_name or not token:
QMessageBox.warning(self, "提示", "倉庫所有者、倉庫名稱、PAT令牌均為必填項!")
return
# 2. 初始化狀態(tài)
self.upload_btn.setEnabled(False)
self.clear_log()
self.log_text.append("?? 開始執(zhí)行上傳流程...")
# 3. 創(chuàng)建并啟動上傳線程
self.upload_thread = UploadThread(
target_path=target_path,
repo_owner=repo_owner,
repo_name=repo_name,
github_token=token,
github_root_path=github_root_path,
is_folder=self.is_folder_mode
)
self.upload_thread.log_signal.connect(self.append_log)
self.upload_thread.progress_signal.connect(self.update_progress)
self.upload_thread.finish_signal.connect(self.upload_finish)
self.upload_thread.start()
# 追加日志
def append_log(self, msg):
self.log_text.append(msg)
# 自動滾動到最新日志
self.log_text.verticalScrollBar().setValue(self.log_text.verticalScrollBar().maximum())
# 更新進度
def update_progress(self, progress):
self.progress_label.setText(f"進度: {progress}%")
# 上傳完成
def upload_finish(self, is_success):
self.upload_btn.setEnabled(True)
if is_success:
QMessageBox.information(self, "成功", "上傳完成!所有文件均成功上傳到GitHub")
else:
QMessageBox.warning(self, "部分失敗", "上傳完成,但有步驟失敗,請查看日志詳情!")
# 程序入口
if __name__ == "__main__":
app = QApplication(sys.argv)
window = GitHubUploader()
window.show()
sys.exit(app.exec_())總結(jié)
- 該工具核心是通過 GitHub API 實現(xiàn)倉庫自動創(chuàng)建和文件上傳,結(jié)合 PyQt5 實現(xiàn)可視化操作;
- 使用前需準備帶
repo權(quán)限的 GitHub PAT 令牌,確保依賴包安裝完整; - 支持單文件/文件夾上傳,保留目錄結(jié)構(gòu),實時輸出日志和進度,便于排查問題;
- 可通過
pyinstaller打包為 exe 文件,提升易用性,也可擴展私有倉庫、斷點續(xù)傳等功能。
到此這篇關(guān)于Python結(jié)合PyQt5實現(xiàn)GitHub文件/文件夾批量上傳工具的文章就介紹到這了,更多相關(guān)Python GitHub文件上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python安裝win32com.client的實現(xiàn)示例
win32com.client是Python操作Windows COM對象的模塊,用于與Office交互,本文主要介紹了python安裝win32com.client的實現(xiàn)示例,具有一定的參考價值,感興趣的可以了解一下2025-06-06
Python Pandas中根據(jù)列的值選取多行數(shù)據(jù)
這篇文章主要介紹了Python Pandas中根據(jù)列的值選取多行數(shù)據(jù)的實例代碼,本文通過實例代碼給大家介紹的非常詳細 ,需要的朋友可以參考下2019-07-07
python 辦公自動化——基于pyqt5和openpyxl統(tǒng)計符合要求的名單
前幾天接到的一個需求,因為學校給的名單是青年大學習已學習的名單,然而要知道未學習的名單只能從所有團員中再排查一次,過程相當麻煩。剛好我也學過一些操作辦公軟件的基礎,再加上最近在學pyqt5,所以我決定用python寫個自動操作文件的腳本給她用用。2021-05-05
Pytorch訓練模型得到輸出后計算F1-Score 和AUC的操作
這篇文章主要介紹了Pytorch訓練模型得到輸出后計算F1-Score 和AUC的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-05-05
對Python中創(chuàng)建進程的兩種方式以及進程池詳解
今天小編就為大家分享一篇對Python中創(chuàng)建進程的兩種方式以及進程池詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-01-01

