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

将PowerShell脚本转为Python后,使用office365-rest-python-client操作SharePoint时出现"can only concatenate str (not NoneType) to str"错误求助

将PowerShell脚本转为Python后,使用office365-rest-python-client操作SharePoint时出现"can only concatenate str (not NoneType) to str"错误求助

我把之前的PowerShell脚本改成Python版本后,用office365-rest-python-client模块连接Microsoft SharePoint做数据同步(拉取和推送数据),现在跑代码的时候,归档函数一直报这个错:

错误信息:can only concatenate str (not "NoneType") to str

以下是我的完整代码,麻烦帮忙排查问题:

from office365.sharepoint.client_context import ClientContext
from office365.runtime.auth.client_credential import ClientCredential
from office365.sharepoint.listitems.caml.query import CamlQuery
from office365.sharepoint.listitems.creation_information import ListItemCreationInformation
import csv

# ---- Step 1: Authenticate to SharePoint ----
def authenticate(site_url: str, client_id: str, client_secret: str) -> ClientContext:
    """ Authenticates using client ID & secret and returns a ClientContext. """
    creds = ClientCredential(client_id, client_secret)
    ctx = ClientContext(site_url).with_credentials(creds)
    web = ctx.web
    ctx.load(web)
    ctx.execute_query()
    print("Step 1 completed: Authentication successful")
    return ctx

# ---- Step 2: Retrieve all items from the target list ----
def get_list_items(ctx: ClientContext, list_name: str):
    """ Fetches all items from the given SharePoint list. """
    sp_list = ctx.web.lists.get_by_title(list_name)
    caml = CamlQuery.create_all_items_query()
    items = sp_list.get_items(caml)
    ctx.load(items)
    ctx.execute_query()
    print(f"Step 2 completed: Retrieved {len(items)} items from '{list_name}'")
    return items

# ---- Step 3: Read and parse the CSV file ----
def read_csv_rows(csv_path: str):
    """ Reads the CSV and returns a list of row-dicts. """
    with open(csv_path, newline="", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        rows = list(reader)
    print(f"Step 3 completed: Loaded {len(rows)} rows from CSV")
    return rows

# ---- Step 4: Compare CSV vs. SharePoint items ----
def compare_csv_to_sp(sp_items, csv_rows):
    """ Builds identifier lists and finds:
    • new_items: in CSV but not in SP
    • deleted_items: in SP but not in CSV
    • project_diff: projects present only in one source
    """
    sp_ids = [
        f"{item.properties['ProjectNumber'].replace('_',' ')}_{item.properties['RevisionNumber']}"
        for item in sp_items
    ]
    sp_projects = [item.properties['ProjectNumber'].replace('_',' ') for item in sp_items]
    csv_ids = [f"{row['Project Name']}_{row['Revision Number']}" for row in csv_rows]
    csv_projects = [row['Project Name'] for row in csv_rows]

    new_items = set(csv_ids) - set(sp_ids)
    deleted_items = set(sp_ids) - set(csv_ids)
    project_diff = set(sp_projects).symmetric_difference(set(csv_projects))

    print("Step 4 completed: Comparison done")
    print(f" • New to upload: {len(new_items)}")
    print(f" • To archive/delete: {len(deleted_items)}")
    print(f" • Projects with mismatched revisions: {len(project_diff)}")
    return {
        "new_items": new_items,
        "deleted_items": deleted_items,
        "project_diff": project_diff
    }

# ---- Step 5: Archive outdated SharePoint items ----
def archive_sp_items(ctx: ClientContext, list_name: str, archive_list_name: str, sp_items, comparison: dict):
    """ Archives items removed or updated in CSV, then deletes them from the original list.
    Uses separate load+execute_query calls to populate root_folder.serverRelativeUrl.
    """
    # 5a) Load archive list
    archive_list = ctx.web.lists.get_by_title(archive_list_name)
    ctx.load(archive_list)
    ctx.execute_query()
    print(f" • Archive list '{archive_list_name}' loaded")

    # 5a-continued) Load its RootFolder separately
    root_folder = archive_list.root_folder
    ctx.load(root_folder)
    ctx.execute_query()
    print(f"Step 5a completed: root_folder.serverRelativeUrl = '{root_folder.serverRelativeUrl}'")

    # 5b) Identify items to archive/delete
    to_archive = []
    for item in sp_items:
        proj = item.properties['ProjectNumber'].replace('_',' ')
        rev = str(item.properties['RevisionNumber'])
        ident = f"{proj}_{rev}"
        if ident in comparison['deleted_items'] or proj in comparison['project_diff']:
            to_archive.append(item)
    print(f"Step 5b completed: Identified {len(to_archive)} items to archive/delete")

    # 5c) Archive + delete loop
    for item in to_archive:
        props = {
            "ProjectNumber": item.properties['ProjectNumber'],
            "RevisionNumber": str(item.properties['RevisionNumber']),
            "PBDSFlag": item.properties.get('PBDSFlag', ""),
            "ProjectDescription": item.properties.get('ProjectDescription', "")
        }

        # Create & populate archive item
        item_info = ListItemCreationInformation()
        arc_item = archive_list.add_item(item_info)
        for fld, val in props.items():
            arc_item.set_property(fld, val)
        arc_item.update()
        ctx.execute_query()
        print(f" • Archived: {props['ProjectNumber']}_{props['RevisionNumber']}")

问题分析与修复方案

这个错误的核心是你在处理SharePoint列表项时,直接访问了可能为空(None)的字段,导致字符串操作时触发类型错误。比如代码里多处直接用item.properties['ProjectNumber'],如果某个列表项的这个字段是空值,就会返回None,后续的replace或字符串拼接操作就会报错。

1. 优先修复字段为空的情况

在所有涉及访问ProjectNumber和RevisionNumber的地方,改用.get()方法并设置默认空字符串,避免None参与操作:

修复compare_csv_to_sp函数

原代码中生成sp_ids和sp_projects的列表推导式直接访问字段,修改为:

sp_ids = []
sp_projects = []
for item in sp_items:
    # 给可能为空的字段设置默认空字符串
    proj_num = item.properties.get('ProjectNumber', '').replace('_', ' ')
    rev_num = str(item.properties.get('RevisionNumber', ''))
    sp_ids.append(f"{proj_num}_{rev_num}")
    sp_projects.append(proj_num)
修复archive_sp_items函数中的条目筛选逻辑
# 5b) Identify items to archive/delete
to_archive = []
for item in sp_items:
    proj_num = item.properties.get('ProjectNumber', '').replace('_', ' ')
    rev_num = str(item.properties.get('RevisionNumber', ''))
    ident = f"{proj_num}_{rev_num}"
    if ident in comparison['deleted_items'] or proj_num in comparison['project_diff']:
        to_archive.append(item)
print(f"Step 5b completed: Identified {len(to_archive)} items to archive/delete")
修复归档循环中的属性构建
# 5c) Archive + delete loop
for item in to_archive:
    props = {
        "ProjectNumber": item.properties.get('ProjectNumber', ''),
        "RevisionNumber": str(item.properties.get('RevisionNumber', '')),
        "PBDSFlag": item.properties.get('PBDSFlag', ""),
        "ProjectDescription": item.properties.get('ProjectDescription', "")
    }

    # Create & populate archive item
    item_info = ListItemCreationInformation()
    arc_item = archive_list.add_item(item_info)
    for fld, val in props.items():
        arc_item.set_property(fld, val)
    arc_item.update()
    ctx.execute_query()
    # 处理可能为空的ProjectNumber,避免拼接报错
    archived_ident = f"{props['ProjectNumber']}_{props['RevisionNumber']}" if props['ProjectNumber'] else f"Item_{item.id}_{props['RevisionNumber']}"
    print(f" • Archived: {archived_ident}")

2. 添加错误捕获,定位具体出错条目

为了方便后续排查,可以给归档循环加上异常捕获,这样即使某条数据出错,脚本也能继续执行并提示具体错误:

# 5c) Archive + delete loop
for idx, item in enumerate(to_archive):
    try:
        props = {
            "ProjectNumber": item.properties.get('ProjectNumber', ''),
            "RevisionNumber": str(item.properties.get('RevisionNumber', '')),
            "PBDSFlag": item.properties.get('PBDSFlag', ""),
            "ProjectDescription": item.properties.get('ProjectDescription', "")
        }

        item_info = ListItemCreationInformation()
        arc_item = archive_list.add_item(item_info)
        for fld, val in props.items():
            arc_item.set_property(fld, val)
        arc_item.update()
        ctx.execute_query()
        
        archived_ident = f"{props['ProjectNumber']}_{props['RevisionNumber']}" if props['ProjectNumber'] else f"Item_{item.id}_{props['RevisionNumber']}"
        print(f" • Archived: {archived_ident}")
    except Exception as e:
        print(f" • Failed to archive item {idx} (SP ID: {item.id}): {str(e)}")
        continue  # 跳过出错条目继续执行

3. 额外建议:验证SharePoint列表字段类型

确认你的SharePoint列表中ProjectNumber和RevisionNumber字段是否允许为空,如果业务上不允许为空,可以在列表设置中开启必填项,从源头避免空值问题。


内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.09 03:05:30