SharePoint文件下载脚本多线程实现问题求助
问题根源
你的脚本出现部分空文件的核心原因是Office365的ClientContext对象不支持多线程共享。多个线程共用同一个上下文时,请求的会话状态会被打乱,导致文件下载不完整或失败。此外,直接在download_file或download_folder_files上用executor.map但未处理线程安全的上下文,进一步加剧了这个问题。
修复后的代码
import os from office365.sharepoint.client_context import ClientContext from office365.sharepoint.files.file import File from office365.sharepoint.folders.folder import Folder from office365.runtime.auth.user_credential import UserCredential import concurrent.futures import threading USERNAME = "username" PASSWORD = "pwd" SHAREPOINT_SITE = "https://tenant.sharepoint.com/sites/site" # 文件写入锁,避免多线程同时操作同一文件 file_write_lock = threading.Lock() def get_thread_safe_context(site_url, username, password): """每个线程创建独立的ClientContext实例,保证线程安全""" ctx = ClientContext(site_url).with_credentials( UserCredential(username, password) ) return ctx def enum_folder(parent_folder, folder_list): """递归枚举所有文件夹路径""" parent_folder.expand(["Folders"]).get().execute_query() folder_list.append(parent_folder.serverRelativeUrl) for folder in parent_folder.folders: enum_folder(folder, folder_list) def download_file(file, transformed_path, ctx): """单文件下载,使用线程安全的上下文和文件锁""" download_file_name = os.path.join(transformed_path, file.name) # 先检查文件是否存在,避免重复下载 if os.path.exists(download_file_name): print(f"{download_file_name} 已存在,跳过") return print(f"开始下载: {file.name}") with file_write_lock: with open(download_file_name, "wb") as local_file: # 使用当前线程的ctx执行下载 file.download(local_file).execute_query(ctx) print(f"下载完成: {download_file_name}") def download_folder_files(folder_path): """单文件夹下载任务,每个任务创建独立的ClientContext""" if "Forms" in folder_path: return # 转换本地存储路径 transformed_path = f"Downloads1{folder_path.replace('/sites', '').replace('/Shared Documents', '')}" print(f"处理文件夹: {transformed_path}") # 创建本地目录 os.makedirs(transformed_path, exist_ok=True) # 为当前线程创建独立的上下文 ctx = get_thread_safe_context(SHAREPOINT_SITE, USERNAME, PASSWORD) sub_folder = ctx.web.get_folder_by_server_relative_url(folder_path) sub_folder.expand(["Files"]).get().execute_query() # 下载当前文件夹下的所有文件 for file in sub_folder.files: download_file(file, transformed_path, ctx) if __name__ == "__main__": # 第一步:枚举所有文件夹(单线程执行,避免枚举时的上下文冲突) ctx = get_thread_safe_context(SHAREPOINT_SITE, USERNAME, PASSWORD) root_folder = ctx.web.default_document_library().root_folder folder_list = [] enum_folder(root_folder, folder_list) # 第二步:多线程处理文件夹下载任务 with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: executor.map(download_folder_files, folder_list) print("所有文件下载完成!")
关键改动说明
- 线程安全的上下文:新增
get_thread_safe_context函数,每个下载任务(文件夹)都会创建独立的ClientContext,彻底避免多线程共享上下文的冲突。 - 文件写入锁:添加
file_write_lock,确保同一时间只有一个线程写入文件,防止多线程同时写入导致的文件损坏。 - 任务拆分:将文件夹下载作为独立任务交给线程池,每个任务自行管理上下文和文件下载,逻辑更清晰。
- 枚举阶段单线程:文件夹枚举过程保持单线程,因为枚举不需要大量IO,且避免枚举时的上下文冲突。
性能优化建议
- 调整
max_workers参数:根据你的网络带宽和SharePoint服务器限制调整线程数,建议从3-5开始测试,避免因并发过高被服务器限流。 - 跳过已存在文件:代码中保留了文件存在检查,避免重复下载浪费资源。
内容的提问来源于stack exchange,提问作者Afourid Ihab
相关产品推荐
相关产品推荐

