ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Confluence API按最后更新时间查询页面:CQL实战指南

Confluence API按最后更新时间查询页面:CQL实战指南 上周五临下班运营同事甩给我一个需求把 Confluence 上最近一周更新过的页面整理成清单她要给领导汇报知识库运营情况。我第一反应是去空间里点最近更新结果翻了两页就放弃了——几十个页面散落在五六个空间里手抄根本不可能还容易漏。老老实实写个小脚本用 Confluence API 按最后更新时间查询十分钟不到就把清单导出成了 CSV。类似的需求其实特别多知识库周报、内容增量同步、过期文档清理、迁移前后核对数据……凡是和 Confluence 内容管理沾边的活儿最后都会落到按最后更新时间查出符合条件的页面这一步。这篇文章把我实际跑通的查询思路、接口选择、CQL 写法和各种坑完整梳理一遍给同样被这个需求折磨过的人一个可以直接抄作业的参考。1. 这个需求从哪来知识库周报、增量同步与内容审计先说清楚按最后更新时间查询到底解决了什么问题因为很多人一上来就搜 API 怎么调却没说清楚自己要干什么最后把方案做歪了。我自己碰过的真实场景至少有四类。第一类是知识库运营周报/月报。这个最常见也是最容易让人想当然的场景。运营同学想知道最近 7 天团队更新了哪些页面手动做法是进每个空间看最近更新列表再复制标题整理到 Excel。空间少还行空间一多就成体力活。用 API 按最后更新时间过滤一条 CQL 下去标题、更新时间、更新人、页面链接全出来了直接生成报表。第二类是内容增量同步。很多团队会把 Confluence 页面同步到公司内部的文档站、搜索系统或者做离线归档。全量同步当然能跑但页面数量上来之后全量拉取既慢又浪费资源。合理的做法是每次只抓上次同步之后有更新的页面这就需要以最后更新时间作为增量窗口。我见过不少人图省事每天夜里全量导一次几百个页面还好上万个页面的时候接口响应时间能把人逼疯。第三类是内容审计和清理。知识库用久了大量页面变成僵尸文档——没人更新、没人访问、内容早已过时。要找出超过 12 个月没更新过的页面靠人肉翻列表根本不可能。拿 API 按最后更新时间倒序排列或者过滤出时间阈值之前的页面再结合页面访问数据做归档清理效率高得多。第四类是备份恢复和迁移场景。团队里经常有人问Confluence 恢复备份数据报错这类问题。正常情况下备份恢复有官方流程但恢复之后要核对哪些页面在什么时间点被更新过、哪些页面丢了就得靠 API 拉页面清单和时间戳来做比对。尤其是跨版本迁移之后用最后更新时间字段核对数据完整性比翻阅操作日志靠谱得多。这四类需求背后有一个共同的底层逻辑你要的不是找出所有页面而是找出满足特定时间条件的页面子集。想清楚这一点就不会走错路——比如有人一看标题就跑去查数据库直接连 Confluence 后端数据库执行查询数据库操作。这种做法我非常不建议Confluence 的数据表结构复杂、存在大量缓存和关联关系直接改库查库既不受官方支持还可能把数据搞坏。官方提供的 REST API 才是正规、安全、可持续维护的路径。2. 两个 content 接口的职责边界排序不等于过滤确定了走 REST API紧接着要面对一个容易混淆的点Confluence 提供了一堆 API endpoint其中/rest/api/content和/rest/api/content/search都能返回页面列表表面上差不多实际用起来天差地别。我第一次写这个需求时理所当然地用了/rest/api/content想着通过参数控制排序字段就行了。结果发现它确实可以按修改时间排序比如加orderbymodified desc就能把最近更新的页面排前面。当时心里还挺美然后问题就来了——这个接口的本质是列出当前条件下的内容并排序它不做时间范围过滤更不能只返回最近 7 天更新的结果。我只能在脚本里先把所有页面拉下来再在内存里做日期判断。页面少无所谓几千个页面的时候分页拉全量数据又慢又占内存而且很容易因为中间某页超时导致整个任务失败。后面换成/rest/api/content/search才算是找对了路子。这个接口走的是 CQLConfluence Query Language搜索引擎时间过滤是它原生的能力。通过在 CQL 里写lastmodified startOfDay(-7d)服务端直接就把符合条件的页面筛选好了脚本拿到的就是最终结果不需要二次处理。这两个接口在最后更新时间这个需求上的差异可以用这个表说清楚维度/rest/api/content/rest/api/content/search核心定位内容列表接口侧重分页浏览搜索接口基于 CQL 执行过滤时间排序支持 orderbymodified desc支持 ORDER BY lastmodified时间范围过滤不支持支持cql 里的 lastmodified 条件大数据量下的适用性全量拉取后再在内存里过滤效率低服务端过滤传输量小效率高推荐场景简单列出某个空间/类型的页面按最后更新时间批量筛选、增量同步一句话总结如果你的需求是按最后更新时间过滤出符合条件的页面子集直接用/rest/api/content/search别在/rest/api/content上浪费时间做二次加工。排序只是把结果排个序过滤才是真正缩小数据范围的手段这两者从概念上就得分开。3. lastmodified 的完整语法精确日期、相对时间与复合 CQL确定了用搜索接口下一个核心问题就是 CQL 怎么写。CQL 语法对新手来说有一定门槛但掌握几个关键表达式之后按最后更新时间查询基本就是固定套路。CQL 里表示最后更新时间的字段名就是lastmodified它和页面对象里的history.lastUpdated.when、version.when是同一个时间概念——页面最后一次被修改并保存新版本的时间。注意它和created创建时间不是一回事别把这两个字段搞混。精确日期写法。最基本的用法是给lastmodified指定一个日期可以做等值、范围过滤lastmodified 2024-12-01 lastmodified 2024-12-20 15:30 lastmodified 2024-12-01 AND lastmodified 2024-12-31日期字符串需要用双引号包起来格式一般是YYYY-MM-DD或YYYY-MM-DD HH:mm。这种写法适合你已经有一个明确的时间边界比如导出 2024 年 12 月之后更新的所有页面。相对时间写法。更常用的是相对时间因为它能保证脚本任何时候跑拿到的都是最近 N 天/周/月的正确数据不用每次手动改日期。lastmodified常见的相对时间表达式有lastmodified now(-1d) // 最近 24 小时 lastmodified startOfDay(-7d) // 从 7 天前 0 点至今 lastmodified startOfWeek(-1w) // 从上周一 0 点至今 lastmodified startOfMonth(-1M) // 从本月 1 日 0 点至今我最常用的是startOfDay(-7d)因为它把时间基准对齐到 7 天前的零点而不是精确的168 小时前语义上更贴近最近一周这种表达。如果连时间粒度都不需要直接now(-1d)也可以看业务怎么定义。复合过滤条件。只按时间过滤往往不够实际查询基本都会组合其他维度。最常见的是按内容类型和空间做限制cqltypepage and lastmodified startOfDay(-7d) order by lastmodified desc cqltypeblogpost and spaceTEAM and lastmodified 2024-12-01 order by lastmodified desc cqltypepage and lastmodified startOfYear(-1y) order by lastmodified asc第一句查所有最近 7 天更新的页面第二句限定在 TEAM 空间只要博客文章第三句查一年没更新过的页面适合做清理审计。type字段可选page、blogpost、attachment、comment等按需填。space后面接空间 key同样用双引号。一个容易忽略的细节JOIN 的含义。多条件之间用and连接表示条件同时成立。如果你不写typelastmodified查询可能会命中附件和评论导致返回一些你以为不相关的内容。所以导页面清单时务必把typepage写进去。实际发请求时CQL 字符串里包含空格、双引号、大于等于符号直接拼 URL 会出问题。用curl时我习惯用--data-urlencode让它自动做 URL 编码而不是自己手拼查询串省去一堆转义烦恼。比如curl -u emailexample.com:api_token \ -G https://your-domain.atlassian.net/wiki/rest/api/content/search \ --data-urlencode cqltypepage and lastmodified startOfDay(-7d) order by lastmodified desc \ --data-urlencode expandhistory.lastUpdated \ --data-urlencode limit50注意这里expand也很关键它决定返回的数据里带不带更新时间字段这个放到下一章细讲。4. 响应结构与分页逻辑翻页终止条件为什么不能用 totalSizeCQL 写对了请求发出去接下来就要处理响应和分页。这块看起来简单实际踩坑的人不少尤其是分页终止条件的判断。/rest/api/content/search返回的 JSON 结构大致长这样关键字段{ results: [ { id: 123456, type: page, status: current, title: 本月知识库更新汇总, space: { id: 7890, key: TEAM, name: 团队空间 }, history: { lastUpdated: { when: 2024-12-20T14:32:11.09708:00, by: { displayName: 张三, username: zhangsan } } }, version: { when: 2024-12-20T14:32:11.09708:00, by: { displayName: 张三 } }, _links: { webui: /pages/viewpage.action?pageId123456 } } ], start: 0, limit: 50, size: 50, _links: { base: https://your-domain.atlassian.net/wiki, next: /rest/api/content/search?cql...start50limit50 } }分页逻辑非常简单每次请求传start和limitstart是偏移量从 0 开始limit是本次返回条数。下一波请求把start加上limit继续传直到结束。最大的坑在结束判断上。有朋友习惯依赖响应里的totalSize字段来判断总数但这个接口压根不保证返回这个字段。我实际测下来的结果里搜索接口主要给size、start、limit和results没有稳定的totalSize。如果代码写成while start totalSize会因为totalSize为None直接报错或者死循环。可靠的终止判断是看results的长度如果本次返回的条数小于limit说明这是最后一页。比如你设limit50某一页只返回了 13 条那就没必要继续翻了。这里有一个潜在风险如果总条数正好是limit的整数倍最后一页返回的长度恰好等于limit会多翻一页空数据。所以严谨的写法是返回空数组也终止两个条件同时判断。说完分页再强调一下expand参数。默认情况下/rest/api/content/search返回的每个页面对象里不一定包含完整的时间字段所以我才在上一章的 curl 里加了expandhistory.lastUpdated。如果你还需要版本时间可以写成expandhistory.lastUpdated,version多个字段用逗号分隔。但expand不能乱加。body.storage这种字段会输出完整的页面正文数据量极大一次查询几十条页面能把响应撑到几十兆接口响应时间直接飙升。如果是只导清单坚决不要 expand 正文。等到确确实实需要页面内容做下一步处理时再单独按 pageId 去取详情不要在一开始就把所有重型字段全带上。5. 认证、权限与版本差异三个最磨人的坑讲完了正常跑通的流程下面专门说说我实际使用中被卡住最久的三个问题。这几个问题在官方文档里都有记载但都没提醒到位遇到时相当磨人。第一个坑认证方式配错。Confluence Cloud 现在推荐用 API Token Basic Auth 的方式调用 REST API。具体做法是去 Atlassian 账号的 API Token 管理页面生成一个 token请求时把用户名一般是邮箱和 token 拼成 Basic Auth。Server/Data Center 版本则可以用账号密码做 Basic Auth或者用 Personal Access TokenPAT。很多人在这一步配错。最常见的是把 API Token、站点地址、密码这几个值填串位或者干脆把 token 当成授权码往别的参数里塞结果接口返回认证失败。之前看到网上有人问confluence 不是一个有效的授权码这类问题我按经验判断大概率是认证参数没按 Basic Auth 格式拼对或者 token 本身已过期。排查思路很简单先确认你访问的是 Cloud 还是 Server/Data Center再确认使用了对应支持的认证方式最后用带-v的 curl 看实际请求头排除参数拼写问题。第二个坑权限不足时接口静默过滤。这一点非常容易误判。CQL 查询的结果是基于发起请求的账号在 Confluence 里的权限来决定的。如果账号没有某个空间的访问权限那些页面压根不会出现在搜索结果里——不是报错也不是返回空对象而是直接消失。想象一下这个排查场景你写了脚本查某个空间最近更新的页面返回结果只有十几条怎么检查语法和参数都对最后发现是测试账号根本不在这个空间的用户组里。我当时排查了很久一度怀疑是 CQL 写错了。所以记住一条经验查不到结果时先检查发起请求的账号有没有对应空间的浏览权限再怀疑语法。管理员账号在测试阶段是最省心的等脚本逻辑跑通了再换成最小权限账号做验证。第三个坑接口版本差异和索引延迟。Confluence Cloud 和 Server/Data Center 的 REST API 大体一致但细节上会有差异。比如有些偏旧版本的 Server 对 CQL 的lastmodified相对时间表达式支持得不够完整跑startOfDay(-7d)可能报语法错误那就要降级用精确日期。这种情况下先花两分钟在浏览器地址栏访问一下接口文档/rest/api/content/search的 OpenAPI 描述确认当前版本支持哪些参数能省下大量调试时间。另外要注意一个伪 bug搜索接口基于索引页面刚修改完就去查有可能查不到最新结果。Confluence 的索引更新不是实时的通常有几秒到几分钟的延迟。如果你改了页面后立刻查 lastmodified发现时间没变化先别慌等一下再查大概率就好了。这个特性在做实时性要求高的同步任务时尤其需要重视。6. 可复用的 Python 脚本批量导出最近 N 天更新的页面前面的原理讲了一大堆最后落到代码上。我把自己在用的脚本精简成一个通用版本支持传空间 key、天数、输出文件路径等参数直接跑就能出结果。#!/usr/bin/env python3 # -*- coding: utf-8 -*- 按最后更新时间导出 Confluence 页面清单。 import argparse import csv import requests from requests.auth import HTTPBasicAuth BASE_URL https://your-domain.atlassian.net/wiki # Server 版改成 http://confluence.example.com def build_cql(space_key, days, content_type): conditions [ftype{content_type}] if space_key: conditions.append(fspace{space_key}) conditions.append(flastmodified startOfDay(-{days}d)) return and .join(conditions) order by lastmodified desc def fetch_pages(base_url, email, api_token, space_key, days, content_type, limit50): cql build_cql(space_key, days, content_type) auth HTTPBasicAuth(email, api_token) start 0 while True: params { cql: cql, expand: history.lastUpdated,version, start: start, limit: limit, } resp requests.get( f{base_url}/rest/api/content/search, paramsparams, authauth, timeout30, ) resp.raise_for_status() data resp.json() yield from data[results] current_size len(data[results]) if current_size limit: break start limit def main(): parser argparse.ArgumentParser(description导出最近 N 天更新的 Confluence 页面) parser.add_argument(--email, requiredTrue, help登录邮箱) parser.add_argument(--api-token, requiredTrue, helpAPI Token 或密码) parser.add_argument(--base-url, defaultBASE_URL, helpConfluence 地址) parser.add_argument(--space-key, defaultNone, help空间 key留空则查所有空间) parser.add_argument(--days, typeint, default7, help最近 N 天) parser.add_argument(--type, defaultpage, help内容类型默认 page) parser.add_argument(--output, defaultpages.csv, help输出 CSV 路径) args parser.parse_args() with open(args.output, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([PageID, Title, SpaceKey, LastUpdated, LastUpdatedBy, URL]) for page in fetch_pages( args.base_url, args.email, args.api_token, args.space_key, args.days, args.type, ): history page.get(history, {}) last_updated history.get(lastUpdated, {}) when last_updated.get(when, ) by last_updated.get(by, {}).get(displayName, ) space_key page.get(space, {}).get(key, ) page_id page.get(id, ) webui_link page.get(_links, {}).get(webui, ) url f{args.base_url}{webui_link} if webui_link else writer.writerow([page_id, page[title], space_key, when, by, url]) print(fDone, exported to {args.output}) if __name__ __main__: main()使用方式python3 export_recent_pages.py \ --email zhangsanexample.com \ --api-token 你的token \ --space-key TEAM \ --days 7 \ --output team_recent_pages.csv几点说明CSV 用utf-8-sig编码输出这样用 Excel 打开不会乱码是个小细节但很实用。requests库的params参数会自动处理 URL 编码CQL 里的双引号、空格都不用自己转义这是比手拼 URL 省心很多的地方。limit默认设 50具体上限看服务端版本一般不要一次拉太大避免响应超时。Basic Auth 这里直接用HTTPBasicAuth(email, api_token)官方 Cloud 的做法是用户名 API TokenServer 版则填用户名 密码代码不用动。如果只想快速试一下接口通不通用 curl 更快上一章的示例命令直接改邮箱、token 和地址就能跑。7. 从能查到查得快字段瘦身、限流与增量状态记录脚本能跑通只是第一步。页面数量上到几千甚至几万之后能查和查得快完全是两码事。这一章分享几个从实战中沉淀下来的优化思路。字段瘦身是第一优先级。前面反复强调不要随便 expand 重型字段这里再补充一个具体做法如果只是做增量同步往往只需要页面 ID、标题、更新时间三个字段那就别 expandversion以外的东西。响应体积直接决定网络传输时间和内存占用字段少了速度提升是立竿见影的。我第一次全量导上万条页面时expand 了body.storage结果响应慢到像死机去掉之后速度快了好几倍。限流和重试机制必须有。虽然 Confluence 没有像某些云服务那样把限流写在文档里但请求频率过高时依然可能触发服务端保护。我的经验是分页循环里每页之间加一个短暂间隔比如time.sleep(0.5)既不会明显拖慢任务又能大幅降低被限流的概率。网络超时和 5xx 错误也要做好重试用requests的Session配合urllib3.util.retry.Retry实现自动重试最省心重试次数 3 次、退避间隔递增即可。真正的高效增量方案是记录状态而不是每次硬查全部。按最后更新时间查询虽然在服务端做了过滤但如果你每次同步都去查最近 30 天更新的页面时间窗口重叠的部分会被反复处理。更稳妥的思路是维护一个本地状态表记录每个pageId上次处理到的version.when时间每次跑批时只查最近几天的增量再和本地时间戳做对比时间没变的跳过时间有变化的才拉详情解析。这样即使lastmodified查询因为索引延迟漏掉了几分钟内的修改下一轮同步也能通过比对窗口覆盖回来不会造成数据缺失。关于_links.next和start/limit两种翻页方式的取舍。响应里可能会给出_links.next看起来顺着走就行非常方便。但我在实际使用中发现当查询条件里的相对时间窗口在翻页过程中跨越了时间边界比如正好翻了几个小时后startOfDay(-7d)的语义已经变了游标可能会和预期产生偏移。相比之下自己维护start参数虽然朴素但每次翻页都重新生成 CQL 再查逻辑上更容易控制也方便断点续跑。所以生产脚本里我一直用的是后者。最后分享一个我在深夜跑批任务时踩过的小坑脚本一开始没有强制传space_key结果全量查了一次所有空间最近 7 天更新页面导出的 CSV 里除了目标空间的页面还混进了别的团队的知识库内容清单交给运营时还得手动筛掉。后来我就在脚本里把space_key改成必填参数仅当确实需要全局扫描时才允许为空。这个小改动看着不起眼但避免了我后来无数次导出前才发现数据范围不对的返工。按最后更新时间查询这事儿核心思路其实不复杂真正拉开效率差距的都是这些边界条件和日常习惯。希望这篇梳理能让你少走点弯路。
返回列表