将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
相关产品推荐
相关产品推荐

