SharePoint REST API:children按修改时间过滤的字段名与正确写法
1. 先给结论/children 支持按修改时间过滤但字段名和姿势都跟想象中不太一样做 SharePoint 开发的人迟早都会面对这个需求给你一个文件夹你想把最近 7 天修改过的文件全部捞出来。第一次写 REST 请求大家都会习惯性地拼一个/_api/web/GetFolderByServerRelativeUrl(/sites/xxx/共享文档)/children?$filterModified ge datetime2024-01-01T00:00:00Z然后满怀期待地跑结果往往是三种情况直接报错、返回空数组、或者最诡异的——过滤条件被忽略把所有子项全返回了。这个标题问的问题我直接给答案children 端点本身是支持 OData 过滤参数的按修改时间查询也能做但有几个前置条件必须满足。第一字段名不是Modified而是TimeLastModified第二时间格式必须严格按照 OData 的datetime...字面量写法第三要注意 children 返回的是文件和文件夹的混合集合你要想清楚自己到底是想过滤文件还是连文件夹一起过滤。这篇文章就是把这三件事一次说透。我会从头讲清楚 children 端点的内部结构为什么字段名会踩坑正确的 URL 怎么写然后再延伸到 5000 条列表阈值、分页、以及 children 满足不了需求时你还有哪些替代方案。适合正在做 SharePoint Online 或 SharePoint 2013/2016/2019 REST API 开发的人参考不管你是用 JavaScript、C# 还是 Python 调接口思路是一样的。先说一个我自己的经历。去年给客户做一个文档归档工具需求就是从某个大文件夹里把超过 180 天没修改的 Office 文件挪到冷存储里。我第一次用 children 写过滤就是按Modified字段拼的结果接口返回了一个让人摸不着头脑的错误大概意思是“无法将 Modified 应用于此类资源”。当时我在网上查了一圈发现遇到这个问题的人不少但回答质量参差不齐有的说 children 不支持过滤让我改用其他接口有的说支持但没说清楚字段名。后来我抓包看了返回的 JSON 结构才发现文件对象上写的明明是TimeLastModified——就一字之差浪费了我半天时间。所以这篇博文不是要卖关子就是把坑一个个给你标出来。2. /children 返回的到底是什么对象——字段名陷阱的根源要理解为什么不能直接写Modified你得先知道children端点返回的数据结构跟/items完全是两回事。2.1 四个端点各回各的在 SharePoint REST API 里围绕一个文件夹你能调用的端点主要有四个我列个表对比一下端点返回对象类型典型字段适用场景/itemsListItem列表项Title, Modified, Created, EditorId基于列表字段做业务过滤/filesFile文件Name, TimeCreated, TimeLastModified, Length只处理文件不要文件夹/foldersFolder文件夹Name, TimeCreated, TimeLastModified, ItemCount只要子文件夹不要文件/childrenFile 和 Folder 混合Name, TimeCreated, TimeLastModified, FileSystemObjectType不关心类型全要很多人会把/items和/children混为一谈因为它们看起来都是“拿数据”的。但底层逻辑完全不同/items出的是列表项列表项上有Modified这个元数据列是 SharePoint 内容数据库里的字段而/children出的是文件系统对象FileSystemObject它对应的是 SPFile 和 SPFolder 实体的简化外壳暴露的是TimeCreated和TimeLastModified这两个底层属性。这就是为什么你在 children 上拼$filterModified ge ...会报错——因为这套对象的属性集合里压根没有Modified这个成员。2.2 响应 JSON 长什么样我抓一个典型的 children 返回片段给你看为了便于阅读我精简了字段{ d: { results: [ { FileSystemObjectType: 0, Id: 42, ServerRelativeUrl: /sites/team/共享文档/项目A/需求文档.docx, TimeCreated: 2023-11-02T08:30:11Z, TimeLastModified: 2024-03-18T14:22:05Z, Name: 需求文档.docx, UniqueId: 9a1e..., File: { Length: 458752 } }, { FileSystemObjectType: 1, Id: 7, ServerRelativeUrl: /sites/team/共享文档/项目A/设计稿, TimeCreated: 2023-09-15T02:11:43Z, TimeLastModified: 2024-02-28T09:00:12Z, Name: 设计稿, Folder: { ItemCount: 12 } } ] } }注意看FileSystemObjectType这个字段0 代表文件1 代表文件夹。两个对象都带着TimeLastModified所以你没写错类型的话用它过滤是能同时作用于文件和文件夹的。如果你只想过滤文件还得额外加一个条件把文件夹排除掉。2.3 为什么叫 TimeLastModified 而不是 Modified这背后的原因没那么玄。ListItem 体系里的Modified是属于列表架构内的字段可以被内容类型继承、可以出现在视图里、也受制于列表项的版本管理。而 File 对象上的TimeLastModified来自底层文件系统的元数据它更接近物理文件的修改时间戳不受列表视图和字段定制的影响。两者在大多数情况下值是一样的——修改一个文档会同时更新这两种时间。但特殊情况下会出现偏差比如通过服务器端 API 直接改文件字节流而不走列表更新逻辑或者批量迁移历史文档时手工指定了 ListItem 的 Modified 却没有同步文件系统时间戳。所以你在做时间过滤时想清楚自己依赖的是哪一层的时间否则可能挖出历史数据不一致的坑。3. 按修改时间过滤的正确写法——从 URL 到代码的完整示例理论讲完直接上实操。这一节给出能直接跑通的写法。3.1 OData 过滤语法基础REST API 支持标准的 OData 查询参数过滤时间用的是比较运算符$filterTimeLastModified ge datetime2024-03-01T00:00:00Z这里几个关键点拆开说ge是“大于等于”对应的还有gt大于、le小于等于、lt小于、eq等于、ne不等于。时间值必须写在datetime...字面量里单引号不能少这是 OData V3 的日期字面量写法。如果把日期裸写一个字符串接口会直接报“无法将 ip 等价的字符串转换为 Edm.DateTime”这类错误。时间要用 ISO 8601 格式YYYY-MM-DDTHH:mm:ssZ结尾的Z表示 UTC 零时区。SharePoint REST 接口认为传入的时间是 UTC如果你本地在中国时区UTC8想过滤“今天早上 8 点之后修改的”要换算成datetime2024-03-20T00:00:00Z别把本地时间直接丢进去不然查出来的区间会整体偏移 8 小时。3.2 一个完整的、能跑的 URL假设站点地址是https://tenant.sharepoint.com/sites/team文档库叫“共享文档”库里有个子文件夹“项目A”你要找出“项目A”下 2024 年 3 月 1 日之后修改过的所有文件和文件夹https://tenant.sharepoint.com/sites/team/_api/web/GetFolderByServerRelativeUrl(/sites/team/共享文档/项目A)/children?$filterTimeLastModified ge datetime2024-03-01T00:00:00Z$selectName,TimeLastModified,ServerRelativeUrl,FileSystemObjectType$orderbyTimeLastModified desc$top100这段 URL 有几个坑我逐个标注文件夹路径里的中文要 URL 编码。示例里“共享文档”在真实请求中要变成%E5%85%B1%E4%BA%AB%E6%96%87%E6%A1%A3否则某些环境会返回 404 或 400。你可以用浏览器的encodeURI或后端的 URLEncoder 统一编码整个路径段。$filter里的空格和单引号要不要编码严格来说URL 中的空格应编码为%20单引号在某些网关层会被拦截常见的做法是把$filter整体做一次 URL 编码再拼接。我自己实践下来至少要把空格编码单引号如果不编码IIS 和 SharePoint Online 基本都能接受。但如果你用了一些 CDN 或 API 网关单引号也建议编码成%27一劳永逸。$orderbyTimeLastModified desc可以帮你拿到最新修改的在最前面省去前端排序。建议跟$top配合用一次不要拉太多数据。3.3 JavaScript fetch 的完整调用用前端代码调 REST API 是需要带请求头Accept: application/json;odataverbose的否则返回的是轻量 JSON 格式字段结构略有差异。我一般固定用 verbose 模式兼容性最好。async function getRecentChildren(siteUrl, folderPath, sinceDate) { const folderEncoded encodeURIComponent(folderPath); const filter TimeLastModified ge datetime${sinceDate}; // 注意这里对 $filter 做了 encodeURIComponent空格会变成 %20单引号变成 %27 const encodedFilter encodeURIComponent(filter); const apiUrl ${siteUrl}/_api/web/GetFolderByServerRelativeUrl(${folderEncoded})/children ?$filter${encodedFilter} $selectName,TimeLastModified,ServerRelativeUrl,FileSystemObjectType $orderbyTimeLastModified desc; const response await fetch(apiUrl, { headers: { Accept: application/json;odataverbose } }); if (!response.ok) { const errorText await response.text(); throw new Error(REST API 请求失败: ${response.status} ${errorText}); } const data await response.json(); return data.d.results; } // 调用示例返回“项目A”目录下 2024-03-01 之后修改的所有对象 const results await getRecentChildren( https://tenant.sharepoint.com/sites/team, /sites/team/共享文档/项目A, 2024-03-01T00:00:00Z );这段代码里有几个我在生产环境里验证过的细节encodeURIComponent会对整个过滤表达式编码包括datetime...里的单引号服务端能正确解析不用担心过度编码问题。如果不加$select返回的数据会很臃肿每个对象还带着Metadata、File、Folder一大坨子对象网络传输和 JSON 解析都会变慢。务必用$select只挑你要的字段。需要在 SharePoint 页面内部执行这段脚本时可以使用contextInfo.webFullUrl动态拼站点地址别写死域名。3.4 用 PowerShell 做验证有时候在浏览器里验证一个 URL 比写完整代码快得多。PowerShell 配合Invoke-RestMethod是最快的验证手段$siteUrl https://tenant.sharepoint.com/sites/team $folderPath /sites/team/共享文档/项目A $folderEncoded [System.Uri]::EscapeDataString($folderPath) $filter [System.Uri]::EscapeDataString(TimeLastModified ge datetime2024-03-01T00:00:00Z) $apiUrl $siteUrl/_api/web/GetFolderByServerRelativeUrl($folderEncoded)/children?$filter$filter$selectName,TimeLastModified,FileSystemObjectType$orderbyTimeLastModified desc # 这里需要带认证头如果你的环境已经登录 SharePoint 模块可以用下面的方式 # Connect-PnPOnline -Url $siteUrl -Interactive # 然后用 PnP 的 WebRequest 也行但纯 REST 可以用 Add-Type 加认证 $headers { Accept application/json;odataverbose } $response Invoke-RestMethod -Uri $apiUrl -Headers $headers -Method Get $response.d.results | Select-Object Name, TimeLastModified, FileSystemObjectType | Format-Table如果你不想写完整认证流程直接用浏览器开发者工具复制请求头也行。我的习惯是先拿 Postman 验证 URL 的正确性再翻译成正式代码能省不少调试时间。3.5 组合条件的使用场景实际需求里过滤条件往往不止一个。比如只要文件、不要文件夹同时修改时间在某个区间内$filterFileSystemObjectType eq 0 and TimeLastModified ge datetime2024-03-01T00:00:00Z and TimeLastModified lt datetime2024-04-01T00:00:00ZOData 支持and、or、not和括号来组织优先级。这里有个新手容易犯的错误把FileSystemObjectType eq 0写在最后同时前面两个时间条件用or连接结果因为优先级问题把文件夹也查出来了。正确的做法是用括号把 or 条件包起来$filterFileSystemObjectType eq 0 and (TimeLastModified ge datetime2024-03-01T00:00:00Z or TimeLastModified le datetime2024-01-01T00:00:00Z)另外一个不太常见的技巧OData 还支持子串函数和时间函数比如year(TimeLastModified) eq 2024可以用来按年过滤。但这种写法性能一般而且会让索引失效大数据量下不推荐。能用范围比较就用ge/lt别用函数。4. 列表阈值和分页限制为什么过滤条件正确却还是拿不到数据写对了字段名、写对了日期格式你会发现还有一个杀手级别的限制拦在路上——SharePoint 的列表视图阈值List View Threshold简称 LVT默认 5000。这个数字针对的是列表项数量不是文件夹内文件数量但文档库本质上也是一个列表库里所有文件都会计入这个 5000 的计数。4.1 5000 条阈值是怎么坑你的假设你的文档库一共 8000 个文件分布在 20 个文件夹里。你调children?$filterTimeLastModified ge datetime2024-01-01...虽然目标文件夹里只有 300 个文件但因为整个库超过了 5000 条某些版本或配置下REST 查询依然会报“无法执行操作因为列表视图阈值已超出”或者“尝试的操作被禁止因为它超过了列表视图阈值强制执行的限制”。这个坑的诡异之处在于children接口按文件夹维度读取理论上是绕过列表阈值的但实际上它底层还是会走列表的查询管线所以一旦库整体超标过滤操作就变得不稳定。我在 SharePoint 2013 环境里就碰到过文件夹里明明只有几十个文件过滤一加就报阈值错误去掉过滤反而能返回数据。4.2 分页的问题$skip 不可靠nextLink 才靠谱当你真的用 children 拉大数据量时第二个坑是分页。REST API 的集合返回默认上限是 100 条有些环境是 200取决于版本和配置。你要拿全量数据就得处理分页。理论上 OData 分页用$skip$top。但 children 这个端点上$skip的行为在不同 SharePoint 版本之间表现不一致有时它会返回“此资源不支持 skipToken”之类的错误有时直接忽略 skip 参数导致数据重复。我自己总结的结论是在 children 上不要依赖$skip用服务端返回的__nextURL。实现方式其实很简单第一次请求时响应体里会带一个d.__next字段指向下一页数据的完整 URL你只需要循环消费这个 URL 直到它为 null。async function getAllChildren(siteUrl, folderPath, filter) { let items []; let nextUrl ${siteUrl}/_api/web/GetFolderByServerRelativeUrl(${encodeURIComponent(folderPath)})/children ?$filter${encodeURIComponent(filter)}$selectName,TimeLastModified,ServerRelativeUrl,FileSystemObjectType; while (nextUrl) { const response await fetch(nextUrl, { headers: { Accept: application/json;odataverbose } }); const data await response.json(); items items.concat(data.d.results); // 关键点下一页的 URL 在 __next 字段里 nextUrl data.d.__next || null; } return items; }这套循环分页的逻辑适用于绝大多数 SharePoint REST 集合查询不管你在 children 还是 items 上用都一样。记住一句话能用__next就别手动算 skip。4.3 大数据量下的正确应对策略如果文档库真的超过了 5000 条而且你要做全量扫描我有几条实际的建议尽量减少返回字段。$select只留主键、时间、相对路径这类必要字段别把解析过的文件内容字段也拖进来。按时间段分段拉取。不要一次查“2020 年至今”的所有文件先查 2020 年 1 月到 3 月的再查 4 月到 6 月的把每个查询控制在几千条以内。时间粒度拆得足够细单次请求的数据量就小就算列表很大只要查询走的是时间索引也能跑得动。优先用 items 接口加 FolderServerRelativeUrl 参数。这个方案我在下一节展开它走的是列表项的索引查询路径对阈值的处理比 children 更成熟。如果是 SharePoint Online考虑用 Microsoft Graph 的 delta 接口做增量同步。Graph 支持$filterfileSystemInfo/lastModifiedDateTime ge ...而且对大数据量的游标分页支持很完善不限 5000 条。这个方案适合你的租户开了 Graph 权限、并且代码可以脱离 SharePoint 上下文运行的场景。5. 当 /children 不够用用 items 接口和搜索引擎替代在任何技术选型里children都不是唯一答案。实际项目中我遇到 children 不好使的情况主要有三类一是列表超过阈值导致过滤不稳二是需要递归查所有子文件夹三是需要按 ListItem 级别的字段比如自定义列“审批状态”组合过滤。应对这三个场景我分别用三种方案下面逐一展开。5.1 方案 Aitems 接口 文件夹路径过滤/items是列表项查询的入口它天然支持Modified字段而且可以按Folder/ServerRelativeUrl限定到某个文件夹。这是我最推荐在文档库场景下替代 children 的做法。/_api/web/lists/getbytitle(共享文档)/items?$filterFolder/ServerRelativeUrl eq /sites/team/共享文档/项目A and Modified ge datetime2024-03-01T00:00:00Z这个请求的正交性很好Folder/ServerRelativeUrl走的是文件夹索引Modified走的是列表字段索引两个条件叠加起来即使全库有上万条目只要目标文件夹内在阈值以内查询就能正常返回。用这个接口拿到的就是 ListItem 对象可以直接操作Modified、Title、Editor这些业务字段做二次处理也很顺手。需要注意几点lists/getbytitle(共享文档)中的库名必须是列表的真实名称不是显示名。你可以在站点设置里查看中文显示名和内部名不同的情况很常见。返回的 ListItem 里文件的下载地址在File/ServerRelativeUrl字段。这个查询默认只查根目录的文件加上Folder/ServerRelativeUrl eq ...之后才能限定子文件夹。如果你要查根目录且包含子目录递归用这个方案就不太合适了得改递归轮询各层目录。5.2 方案 Bfiles 和 folders 分开取再内存合并这个方案从源码级别绕开了 children 的设计限制。先用$filterTimeLastModified ge ...在 files 端点上取文件列表再用同样的过滤条件在 folders 端点上取文件夹列表两边各自带上FileSystemObjectType标记最后在代码里合并成一个统一数组。/_api/web/GetFolderByServerRelativeUrl(/sites/team/共享文档/项目A)/files?$filterTimeLastModified ge datetime2024-03-01T00:00:00Z$selectName,TimeLastModified,ServerRelativeUrl /_api/web/GetFolderByServerRelativeUrl(/sites/team/共享文档/项目A)/folders?$filterTimeLastModified ge datetime2024-03-01T00:00:00Z$selectName,TimeLastModified,ServerRelativeUrl好处是你不用解析FileSystemObjectType来判断类型了files 端点出来的全是文件folders 端点出来的全是文件夹代码里直接按对象类型归组就行。坏处是发两次请求网络往返翻倍。如果你的接口调用频率很低或者前端本身就要分别处理文件列表和文件夹列表比如做自定义资源管理器这个方案反而更顺手。5.3 方案 C搜索 REST API适合全库递归和复杂条件如果你要的不只是“某个文件夹下”而是“整个站点里所有路径含‘项目A’的文件且最近改过”继续用 REST 列表查询会很痛苦递归所有文件夹极其耗时。这时应该切到 SharePoint 搜索 API/_api/search/query?querytextPath:https://tenant.sharepoint.com/sites/team/共享文档/项目A AND LastModifiedTime2024-03-01selectpropertiesPath,Title,LastModifiedTime,UniqueIDtrimduplicatesfalserowlimit500第一次用搜索接口的人容易被 query 语法劝退其实核心就三个概念Path:...限定搜索范围双引号内的路径匹配支持前缀匹配能把“项目A”下所有递归文件全部命中。LastModifiedTime2024-03-01是搜索管道的托管属性过滤时间格式写YYYY-MM-DD就行不需要datetime()字面量。trimduplicatesfalse确保不合并重复条目否则你可能拿不到全部文件。搜索方案的优势是支持 5000 以上的结果集通过后续页游标适合全库扫描和复杂条件组合。缺点是结果有索引延迟新上传的文件可能要几分钟后才能搜索到不适合对实时性要求极高的同步场景。而且搜索返回的是索引快照字段也就是 Managed Property不如 ListItem 字段那么齐全。5.4 方案选型对比场景推荐方案原因单文件夹下按时间过滤列表未超阈值children TimeLastModified最直接请求最少单文件夹下按时间过滤列表超阈值items Folder/ServerRelativeUrl走列表索引对阈值有更好的兼容性需要区分文件和目录并分别处理files / folders 分开查结构清晰免去类型判断全库递归搜索条件复杂搜索 API一次请求覆盖全库支持复杂条件增量同步、需要持续轮询变更Microsoft Graph delta游标管理完善不依赖列表阈值5.5 兜底思路递归遍历加本地过滤最后分享一个土办法但非常实用。有些场景下你根本不用纠结接口支不支持服务端过滤——数据量小几千条以内服务器又在高峰期容易超时直接在客户端拉全量 children然后内存里过滤TimeLastModified。不要觉得这样“不优雅”项目上线求的是稳定。我遇到过不少接口服务端过滤逻辑在某种配置下会莫名其妙失效反而是把全量拉回来用前端筛选逻辑透明、排错也容易。当然这个方案的前提是数据量可控你要是几万条文件还这么干那还不如直接用搜索 API。6. 排错思路当过滤结果不对时按这条链路排查写代码出 bug 不可怕可怕的是没有排查思路。针对 children 时间过滤这种场景我给你一套可复用的排错链路按顺序过一遍90% 的问题都能定位。第一用浏览器或 Postman 直接拼 URL观察原始响应。别急着看代码先把接口返回的 JSON 通读一遍。如果是 400 错误响应体里通常带着具体字段名和建议如果是 200 但数据不对检查过滤条件是否被 URL 编码后拆分错了。第二确认你查的字段真的在响应里。先不带$filter只带$selectName,TimeLastModified,Modified看看返回的 JSON 里到底哪个字段有值、哪个字段不存在。用了不存在的字段做过滤有些环境直接报错有些环境把过滤当空条件处理——后者更坑你会以为过滤成功了其实只是返回了全量。第三验证时间时区转换。把筛选时间和目标文件的显示时间对比一下看看是不是存在 8 小时偏移。很多人查“今天修改的文件”查不到就是因为本地的“今天”和 UTC 的“今天”不是一回事。写成datetime2024-03-20T00:00:00Z意味着 UTC 零点也就是北京时间的早上 8 点。第四单独测试两个条件。如果你同时用了FileSystemObjectType eq 0和TimeLastModified ge ...先把FileSystemObjectType条件去掉只测时间过滤再把时间条件去掉只测类型过滤。确认每个条件单独都生效再组合起来测。这样能快速定位是哪个条件出了问题。第五看__next是否被忽略。当你有分页逻辑时如果忽略了__next而用$skip手动翻页某些环境会出现漏数据和重复数据。日志里记录每次请求的时间和条数如果发现总条数对不上十有八九是分页策略的问题。这套排查链路不仅适用于 children换成 items 也是一样的。核心思想就一条分而治之逐个击破。别让两个可疑因素叠加在一起那只会让排查变成猜谜。7. 实测中容易忽略的小细节三次踩坑记录最后分享三个我在实际项目中真正踩过、并且花了不少时间才定位的细节问题它们都不在官方文档的显眼位置却有很高的复现率。7.1 文件夹路径中的特殊字符我用 GetFolderByServerRelativeUrl 时路径里带过一个带空格的文件夹名“我的 项目”。直接拼 URL 后SharePoint 返回了 404怎么查都查不到文件夹。后来才发现空格在路径段里必须编码为%20而不是。encodeURIComponent会把空格编码成%20但在某些后端库中也会被当成空格结果因为和%20在路径里的语义不完全一致就出现了诡异的 404。我的教训是无论用哪个语言路径编码统一用标准库的 URL 编码函数别手写替换也别把塞进去。7.2 用 getbytitle 时库名写错使用lists/getbytitle(...)时这个 title 是列表的内部 Title不是显示名。有个客户把文档库显示名改成了“项目文档”但内部 Title 还是“Shared Documents”。我直接拿显示名去拼 URL一查一个 404。这个坑的排查方法很简单先在浏览器里打开/_api/web/lists?$selectTitle,Id看一眼所有列表的真实 Title再写进你的请求里。7.3 日期字段的值里有毫秒某些情况下从 SharePoint 返回的TimeLastModified是带毫秒的比如2024-03-18T14:22:05.377Z。你在代码里做字符串比较时如果一个是2024-03-18T14:22:05Z另一个是2024-03-18T14:22:05.377Z直接按字符串排序就会出错。正确的做法是解析成 Date 类型再比较或者统一格式化到秒级再去重。这个问题在从 REST 转到数据库、再转回内存做比较的链路中尤其常见。按照上面这套写法children 的修改时间过滤这个需求基本可以落地了。最后说一句我在多个项目里反复验证过的话SharePoint REST API 不是一个“标准 OData”服务它更像一个戴着 OData 帽子的半封闭系统永远以实际返回响应为准而不是以文档为准。写之前先抓一次包、看一眼 JSON比什么都管用。