ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Confluence API按最后更新时间查询:CQL实战与增量同步脚本

Confluence API按最后更新时间查询:CQL实战与增量同步脚本 维护一套长期在用的Confluence知识库早晚会遇到这种需求把最近几天更新过的页面全部拉出来过一遍或者每天定时把改动的页面同步到另一个系统。这时候就必须用Confluence API按最后更新时间去查询而不是写个脚本把全站页面灌进内存再慢慢过滤。这个需求听着很简单我第一次做的时候却折腾了大半天——认证方式选错了CQL时间格式被URL编码弄乱翻页翻到一半又触发了限流甚至连返回字段里时间藏在history下面都没找到。这篇把我踩过的坑和最终沉淀下来的方案完整写出来包含可以直接复制改用的脚本希望能让你少走这些弯路。1. 先搞清楚你为什么要按最后更新时间查页面1.1 三个最常见的真实场景第一种场景是做增量同步。很多团队会把Confluence当成内容源页面更新后要自动同步到内部文档站、知识中台或者某个数据仓库。全量同步第一次做没问题但每次都全量跑不仅慢还白白消耗API配额。更合理的做法是记录上次成功同步的时间点每次只拉取lastModified大于这个时间的页面这个需求本质就是一次带时间条件的查询。第二种场景是内容治理和僵尸页面清理。知识库用了两年以后里面会有大量没人维护的老页面。运营想统计“近90天没有更新过的页面有哪些”或者反过来看“最近30天内容活跃度最高的空间是哪个”。这些统计口径全部落在最后更新时间上用UI导出根本没法自动化必须靠API按时间范围批量取数。第三种场景是审计和变更留痕。某些合规要求比较严格的团队需要定期导出某个空间内所有页面的修改记录包括谁改的、什么时候改的、改了几次。这里要查的不光是“最后更新时间”还要结合version和history信息但第一步仍然是先把时间范围内的页面筛选出来。1.2 为什么不建议先拉全量再过滤我见过不少同事一上来就写一个遍历所有页面的脚本循环调用分页接口把每一页的id和title都拿到然后自己在代码里判断更新时间是否大于某个阈值。在小站点比如几百个页面这确实能跑通但一旦页面数量上万问题就暴露了。首先是慢。每页limit最多拉到一两百条上万页面意味着几十次HTTP请求每次请求都要走网络、序列化、解析整体耗时非常可观。其次是费配额。Confluence Cloud对API有速率限制短时间高频请求很容易触发429反过来还得写重试逻辑。最后是容易把服务打挂尤其是自建Server/Data Center的站点数据库扛不住全表扫描。正确的做法是把过滤条件下推到服务端让Confluence用CQL把符合条件的页面筛选好再返回。这才是“按最后更新时间查询”的正确打开方式也是这篇文章想讲清楚的核心。2. Confluence API 的认证与请求基础2.1 认证方式选型API Token、用户名密码还是授权码调用Confluence REST API之前第一关是认证。很多新手在这里就卡住了因为Confluence的认证体系在不同版本和部署方式下差别挺大。先说Confluence Cloud。它是Atlassian云产品使用Atlassian账号体系。你要先到Atlassian账号的安全设置里创建一个API Token然后调用接口时用“账号邮箱 API Token”做HTTP Basic认证。注意这里的密码不是你的登录密码而是API Token如果你开了两步验证用登录密码直接调API基本都会失败。再说Confluence Server和Data Center。它们属于自建部署认证方式会灵活一些。如果用户的密码用的是本地目录认证可以直接用“用户名 密码”做Basic认证。但如果站点接了SSO或者统一登录用普通账号密码就调不通了这时候建议单独建一个服务账号在这个账号下生成API Token来调用。这里必须提醒一个经典误区网上能看到有人问“confluence 不是一个有效的授权码”是怎么回事。他大概率是把安装激活时用的授权码license key当成API Token贴在请求里了。授权码是激活软件许可证用的跟REST API认证完全是两码事你拿着许可证密钥去调接口服务器认不出来自然就报错。正确做法是先访问Atlassian账号或Server的“应用链接”设置生成真正的API Token。部署方式认证主体密码/密钥来源适用场景Confluence CloudAtlassian账号邮箱官网创建的API Token云站点API调用推荐统一用TokenServer / Data Center本地用户或服务账号用户密码或应用Token自建站点优先给脚本单独建服务账号已接入SSO的Server服务账号服务账号API TokenSSO环境下不能走普通密码认证2.2 确认接口连通性的第一个请求认证方式确定了先不要急着写复杂查询用最简单的请求确认能连通。这里要特别注意URL路径差异Confluence Cloud的访问地址一般是https://你的域名.atlassian.net/wiki所以REST API的根路径是/wiki/rest/api但自建Server/Data Center的URL路径取决于部署时的context path可能是https://confluence.example.com/rest/api也可能是https://confluence.example.com/confluence/rest/api。下面这个请求会返回最近更新的页面列表limit5先限制数量避免响应体太大。curl -u your-emailexample.com:your-api-token \ -G https://your-domain.atlassian.net/wiki/rest/api/content \ --data-urlencode limit5 \ --data-urlencode expandhistory.lastUpdated \ -H Accept: application/json如果返回了JSON数组并且每个结果里有history.lastUpdated.when字段说明认证和接口路径都没问题。如果返回401优先检查邮箱拼写、Token是否复制完整如果返回404大概率是路径前缀不对把/wiki去掉或加context path再试。这一步相当于打通了管道后面要做的就是在CQL查询里加时间条件、排序、翻页这些逻辑了。3. CQL 时间条件怎么写才靠谱3.1 lastModified 的语法与操作符Confluence的CQL全称是Confluence Query Language跟Jira的JQL类似。按最后更新时间查询核心字段就是lastModified。它的基础用法是给字段配一个比较操作符加上一个日期字符串作为条件。lastModified 2024-01-01 00:00操作符支持、!、、、、也可以组合使用。比如查“上个月到现在更新过的页面”可以写成lastModified 2024-06-01 00:00 AND lastModified 2024-07-01 00:00有个容易混淆的点lastModified是CQL里用来筛选的字段名而API返回的JSON里最后更新时间藏在history.lastUpdated.when里面。筛选用前者取数据用后者两边的名字不一样我最早就是在这里迷路的。时间格式方面大多数版本支持yyyy-MM-dd HH:mm和ISO 8601格式。建议统一用带时区的ISO 8601比如2024-01-01T00:00:0008:00避免对方按UTC解析导致结果偏了8个小时。如果不想处理时区偏移就直接用UTC时间。另外CQL也支持now-1d、startOfDay()这类相对时间写法但不同版本的判定规则有细微差异生产环境我还是推荐绝对时间可读性与可复现性都更好。3.2 排序与字段展开lastUpdated 藏在 history 里光有筛选还不够做增量同步时你肯定希望最新的排在最前面这样处理前几条往往就覆盖了最近的改动。CQL支持在末尾加排序子句type page AND lastModified 2024-01-01 00:00 ORDER BY lastModified DESC需要注意的是CQL的排序要写在查询字符串的末尾并且空格在URL请求里要编码成%20不然服务器解析会出错。我们用curl的时候可以用--data-urlencode让它自动处理但如果用拼接URL的方式就必须手动编码。接下来是expand参数。很多人查出来的结果里看不到时间字段其实是因为没有展开。Confluence为了控制响应体积默认不会把所有嵌套信息都塞进返回结果里历史信息要用expand显式请求--data-urlencode expandhistory.lastUpdated加了之后返回结果里会多出history.lastUpdated.when这个字段就是最后更新时间。如果你还想知道是谁最后更新的同样在history.lastUpdated.by里能拿到用户名需要的话可以继续展开。3.3 组合条件与 URL 编码这些坑实际查询很少只有一个条件通常要同时限定空间、页面类型和时间范围。例如查某个空间下最近一周更新的普通页面space TECH AND type page AND lastModified 2024-01-01 00:00这里要注意几点。第一type page不是pages这是Confluence CQL的枚举值写错会直接报CQL解析错误。第二字符串值要用双引号包住而双引号在URL里要编码成%22。第三很多请求库会自动帮你做参数编码但也有些老旧脚本是手拼URL的一个空格忘了编码就够查半天。一个我在实战中踩过的细节如果时间字符串里带08:00这种时区偏移直接把整个字符串塞进URL号会被服务器解析成空格。所以要么用requests这类库的params参数自动编码要么手动把编码成%2B。最简单粗暴的规避方式是一律用UTC并带上Z后缀比如2024-01-01T00:00:00Z省掉加号的麻烦。4. 实操把按时间查询写成可复用的增量脚本4.1 用 curl 快速验证查询结果完整了解语法后先写一条curl验证一下多条件查询。下面这个请求查的是TECH空间下所有类型为page、最后更新时间在2024年1月1日之后的页面按时间倒序排列一次取20条并且展开了history信息。curl -u your-emailexample.com:your-api-token \ -G https://your-domain.atlassian.net/wiki/rest/api/content \ --data-urlencode cqlspace TECH AND type page AND lastModified 2024-01-01 00:00 ORDER BY lastModified DESC \ --data-urlencode limit20 \ --data-urlencode expandhistory.lastUpdated \ -H Accept: application/json | python -m json.tool用python -m json.tool格式化输出能很直观地看到每个页面id、title和history.lastUpdated.when。这里验证通过后再落到正式脚本里。有一点需要提醒/rest/api/content接口支持cql参数在Confluence Cloud和较新的Data Center版本里都没问题但如果你的自建版本比较老这个接口可能不认cql会返回400。遇到这种情况不要死磕改用/rest/api/search它从设计上就是CQL的主要入口只是返回结果里多包了一层content字段解析时多取一层就行。4.2 Python 增量同步脚本实例下面的脚本是我项目里一直用的简化版核心逻辑是从配置时间点往后查一直翻页拉到没有下一页为止把所有页面的id、标题和最后更新时间记录下来最后更新本地时间标记作为下一次同步的起点。import base64 import time from datetime import datetime, timezone, timedelta import requests BASE_URL https://your-domain.atlassian.net/wiki EMAIL your-emailexample.com API_TOKEN your-api-token LAST_RUN_FILE last_run.txt # 上次成功同步时间首次运行默认取30天前 try: with open(LAST_RUN_FILE, r) as f: last_run f.read().strip() if not last_run: last_run 2024-01-01T00:00:00Z except FileNotFoundError: last_run 2024-01-01T00:00:00Z # 构建认证头 auth base64.b64encode(f{EMAIL}:{API_TOKEN}.encode()).decode() headers { Authorization: fBasic {auth}, Accept: application/json, } # 查询条件只取 page且时间大于上次同步点 cql ftype page AND lastModified {last_run} ORDER BY lastModified DESC url f{BASE_URL}/rest/api/content params { cql: cql, limit: 100, expand: history.lastUpdated, } pages [] while url: resp requests.get(url, headersheaders, paramsparams) if resp.status_code 429: # 被限流则退避重试 retry_after int(resp.headers.get(Retry-After, 5)) time.sleep(retry_after 1) continue resp.raise_for_status() data resp.json() for item in data.get(results, []): last_updated item.get(history, {}).get(lastUpdated, {}) pages.append({ id: item.get(id), title: item.get(title), last_updated: last_updated.get(when), }) # 从返回的 _links.next 里取下一条地址 url data.get(_links, {}).get(next) if url: if not url.startswith(http): url BASE_URL url params None # 后续跳转地址已包含完整参数 else: break for p in pages: print(p[id], p[title], p[last_updated]) # 把当前时间写回本地文件下次从这个点开始 now datetime.now(timezone.utc).isoformat().replace(00:00, Z) with open(LAST_RUN_FILE, w) as f: f.write(now)这段脚本有两个设计细节值得说。第一limit我设成了100这个值在响应大小和请求次数之间比较均衡具体上限以你站点版本为准Cloud建议不要超过200。第二翻页逻辑读取_links.next它返回的往往是相对路径所以要拼上站点根地址。如果你在旧版本上发现next字段里带的是start100这种参数形式循环逻辑依然成立它本来就是分页offset的含义。4.3 分页策略、断点续跑与幂等性当结果集特别大的时候翻页策略直接决定脚本能不能稳定跑完。_links.next在数据量小时很好用但start这种offset方式有个性能陷阱数据库要跳过前面N条记录才能取后续数据N越大越慢。结果去到几万条时最后几页的响应时间会明显变长。我的经验是把时间范围切成小窗口而不是一次性查询超大范围。比如你要同步一整年的页面不要写lastModified 2024-01-01而是按月拆成12次查询每次查一个月的范围。这样单次查询结果量有限分页深度浅响应快出错了也只需要重跑失败的那个月。断点续跑也很重要。上一节的脚本把当前时间写回last_run文件但这会引入一个竞态问题你在处理页面的时候可能有同事正在编辑某个页面它的最后更新时间处理完之后才变化而下一次同步是从你写文件的时间点开始的这个页面就漏掉了。所以更稳妥的做法是每次查询的截止时间比当前时间回拨几分钟例如记录now - 5分钟作为下次起点留出重叠窗口然后在业务侧按页面id去重。增量同步宁可重复处理几条也不能漏掉一条这是基本准则。5. 常见报错与排查经验5.1 授权码无效与 401 类报错API调用最常见的一大类问题就是认证报错。HTTP 401表示认证失败但具体原因千奇百怪。刚才提到的“confluence 不是一个有效的授权码”本质就是客户端拿着错误的字符串去认证服务器根本不认。这里要做的不是去查授权码是否过期而是确认你用的到底是不是API Token。API Token在Cloud上是一串以ATATT开头的长字符串而且只在创建时完整显示一次丢了只能重新生成。Server/Data Center的API Token创建位置在个人资料里的“应用链接”或“API Token”菜单不同版本略有差异但一定不是License密钥。如果你在请求里用的是激活码或者产品密钥立刻去改。还有一种情况是邮箱和Token填反了或者Basic Auth的格式不对。Basic认证是Authorization: Basic base64(用户名:密码)注意中间是半角冒号。使用curl时用-u user:token它会自动编码但如果你是自己拼Authorization头漏了冒号或者没做base64都会导致401。5.2 登录失败、版本兼容与代理环境有时候你会看到类似“login failed. check api token or gitlab version”这样的报错尤其从第三方工具或IDE插件里调Confluence时容易出现。这个文案本身可能不是Confluence返回的而是工具自己的提示它只是想告诉你“登录没成功去查一下Token是不是有效的版本是不是匹配”。面对这种提示不要被文字带偏最可靠的手段是直接用curl发一个不带任何业务参数的最小请求看HTTP状态码和响应体。状态码是200说明接口链路通问题出在上层工具配置是401则继续查认证信息。自建站点的版本兼容问题也要留意。老版本的Confluence REST API对CQL的支持没那么完整我见过在7.x版本上/rest/api/content不认cql参数的情况。排查思路很简单先用最简单的CQL比如只带type page请求如果还是报CQL解析错误就改用/rest/api/search。再有就是自建站点如果走了反向代理代理层可能会吞掉Authorization头或者因为证书问题导致连接失败。这种情况先绕过代理直连内网地址测一次能很快定位是不是中间链路的问题。5.3 查询结果异常时区、索引延迟与备份恢复这类问题最隐蔽因为请求本身是成功的但结果不对。最常见的是时区偏差。你本地时间早上9点认为“今天”是9点之后但Confluence如果按UTC存储你在CQL里不写时区它可能把你输入的时间当成UTC结果查回来的页面比预期少8小时。解决办法是CQL里写带时区的绝对时间比如2024-07-01T09:00:0008:00或者统一转成UTC。另一个坑是索引延迟。Confluence的搜索和CQL查询依赖索引刚更新的页面可能不会立刻出现在查询结果里会有几秒甚至几十秒的延迟。如果你的脚本逻辑是“更新完页面立刻按lastModified查回来校验”不要马上断言失败稍微等待一下再查。这里顺带说一个热词里提到的“confluence恢复备份数据报错: isshowsignup application cannot be null”。这个报错多发生在站点备份恢复或者版本升级之后属于实例自身状态异常跟API查询没有直接关系。如果你发现恢复备份后之前一直正常的按时间查询脚本开始报奇怪错误不要只盯着API调参先确认站点健康检查是否通过。而且备份恢复后的页面时间戳和版本号通常会恢复到备份时的状态增量同步的时间基线要及时重设否则会漏掉恢复点之后的数据或重复处理大量历史页面。5.4 限流 429 的处理请求一多最常撞见的就是HTTP 429。Confluence Cloud对每个用户或应用有速率限制超了就丢给你429。很多人的第一反应是加长的sleep其实更合理的做法是判断响应头里的Retry-After字段按它给出的秒数退避。如果没拿到这个字段再采用指数退避策略第一次等1秒第二次等2秒第三次等4秒逐步增加并且加一点随机抖动避免多个脚本同时重试发生“惊群效应”。批量场景下我还会主动控制并发。不要同时开几十个线程去拉接口Confluence的REST API不太喜欢短时间高并发尤其自建站点数据库线程池和内存都会被拖垮。建议并发数控制在5以下或者干脆串行处理稳定压倒一切。6. 几个让我省了很多事的经验用Confluence API做按最后更新时间查询整体并不复杂但真正的复杂度往往藏在边界情况里。我自己的项目里最后沉淀下来几条固定经验第一所有脚本统一走服务账号加API Token的认证方式绝不使用个人账号的登录密码这样即使有人离职只要停用服务账号就能立刻切断所有定时任务第二查询条件一定写绝对时间并带上时区脚本里默认UTC展示层再转本地时间彻底杜绝时区问题第三增量逻辑保留重叠窗口宁可重复处理也不漏数据这是一条适用于所有同步任务的通用原则。另外如果你要把这套逻辑做成定时任务推荐把查询区间、空间名、时间戳这些变量全部抽出来放到配置文件里不要让运维同事每次改脚本代码。这样后续接新的使用方、调整查询范围都只需要改配置而不用动逻辑。实际维护下来这套方案最大的价值不是省了多少手工时间而是让“哪些页面什么时候更新过”这件事变成了一个随时可以查、可以自动化、可以追溯的可靠数据源。
返回列表