You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SharePoint文件下载脚本多线程实现问题求助

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("所有文件下载完成!")

关键改动说明

  1. 线程安全的上下文:新增get_thread_safe_context函数,每个下载任务(文件夹)都会创建独立的ClientContext,彻底避免多线程共享上下文的冲突。
  2. 文件写入锁:添加file_write_lock,确保同一时间只有一个线程写入文件,防止多线程同时写入导致的文件损坏。
  3. 任务拆分:将文件夹下载作为独立任务交给线程池,每个任务自行管理上下文和文件下载,逻辑更清晰。
  4. 枚举阶段单线程:文件夹枚举过程保持单线程,因为枚举不需要大量IO,且避免枚举时的上下文冲突。

性能优化建议

  • 调整max_workers参数:根据你的网络带宽和SharePoint服务器限制调整线程数,建议从3-5开始测试,避免因并发过高被服务器限流。
  • 跳过已存在文件:代码中保留了文件存在检查,避免重复下载浪费资源。

内容的提问来源于stack exchange,提问作者Afourid Ihab

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 02:28:28