通用 REST API → MaxCompute ODS 同步工具:把任意 HTTP 接口返回的 JSON / CSV / ZIP 记录
原样写进 MaxCompute ODS(每条记录一行 JSON + pt 业务日分区),DWD 层再解 JSON、
按主键取最新。接一个新数据源 ≈ 写一份配置文件。
REST API → MaxCompute ODS sync tool. 同系列:feishu2ods(飞书多维表格) · sftp2ods(SFTP 文件)
最短路径: 安装 → --init 生成配置 → --check 体检 → --dry-run 试跑 → 正式同步。
导航: 快速开始 · 它是怎么工作的 · 配置参考 · 行为与保护 · 常见问题
适合把有 REST API 的数据源按天/按窗口原样落到 MaxCompute ODS;不适合增量 CDC、实时同步或非 MaxCompute 数仓。
- 配置驱动:地址、参数、鉴权、分页、时间窗口、目标表都在一个 JSON 里
- 写入幂等:每次按窗口拉数,先删再填
pt分区,重复跑/补数不会重复 - 自动建表:目标表不存在时按
json string + pt string建表,存在则校验结构 - 少见即用的健壮性:请求重试+限流退避、业务错误判定、大数据量分单元落盘、 写后行数校验、密钥脱敏、运行锁,失败退出码非 0(方便调度系统告警)
- 写入不改数据:每条记录原样一行 JSON(只追加
add_fields里的固定字段), 列名/空值/编码都保持接口返回的样子,怎么解留给下游 DWD - 跨平台:Windows / macOS / Linux(Python 3.9+),无系统级依赖
支持的真实接口形态(仓库内 jobs/ 有完整样例):
| 形态 | 例子 |
|---|---|
| Header Token + 页码分页 + JSON | 大多数 SaaS API |
| 自定义 sha256 签名 + POST JSON | Onerway |
| 参数走 Header + 返回 CSV 文件 | Onerway 结算明细 |
| 阿里云 RPC 签名(HMAC-SHA1)+ 总条数分页 | 阿里云账单 QueryInstanceBill |
| Bearer Token + ZIP 内含 CSV | DeepSeek 用量导出 |
| 自定义 md5 签名 + POST JSON + 无总数短页分页 | XMP(Mobvista)Open API |
本项目可独立从 GitHub 下载并安装,不需要其它 ODS 项目或团队内部服务。支持 Windows、macOS、Linux;使用前需要源端读取权限和目标 MaxCompute 项目的凭证与读写权限。
- Python 3.9+;依赖由
pip install .自动安装。建议为工具创建虚拟环境,或使用 pipx。 - 请按目标项目的真实地域设置
maxcompute.endpoint(可用--endpoint覆盖)。默认 us-west-1 仅适用于该地域,不能用于任意项目。 - 业务日默认按
America/New_York的昨天计算;可用ODS_BIZDATE_TZ=Asia/Shanghai、UTC等 IANA 时区名覆盖,也可显式传--bizdate。数据窗口/源文件时区按各自配置设置,不必采用同一地域。 - 从
jobs/*.example.json复制配置并填入自己的源地址、项目和凭证;真实配置不要提交到 Git。首次接入先运行--check和--dry-run。
git clone https://github.com/sandy-wangzining/api2ods.git
cd api2ods
python -m venv .venv
# Windows PowerShell:.\.venv\Scripts\python.exe -m pip install .
.venv/bin/python -m pip install .# 方式一:pip(建议放虚拟环境)
python3 -m venv venv
./venv/bin/pip install . # 在源码目录本地安装
./venv/bin/pip install api2ods # 发布到 PyPI 后
api2ods --version
# 方式二:pipx(全局命令行工具,隔离环境)
pipx install .
# 方式三:直接从 GitHub 安装
pipx install "git+https://github.com/sandy-wangzining/api2ods.git"
# 方式四:源码直接跑(不安装)
python -m api2ods --job jobs/xxx.json --checkWindows / macOS / Linux 通用;Windows 会自动带上 tzdata 依赖(时区数据)。
没准备好 MaxCompute / 飞书凭证时,先跑 python -m unittest discover -s tests -v 验证安装(离线,不连任何外部服务)。
# 1) 生成一份作业配置(交互式问答,密钥直接写进文件、输入时不回显;也可复制 jobs/_template_simple.example.json 手工改)
api2ods --init
# 2) 体检:配置 + 真实请求一次 + 目标表结构(新接源第一步,报错会给线索)
api2ods --job jobs/my_api.json --check
# 3) 试跑:真实拉取统计,不写库(--days 1 只拉一天,先看条数对不对)
api2ods --job jobs/my_api.json --bizdate 20260918 --days 1 --dry-run
# 4) 正式同步:把最近 N 天写进 pt=<业务日>(N 取配置里的 window.days)
api2ods --job jobs/my_api.json --bizdate 20260918
# 常用补充:
api2ods --job jobs/my_api.json --bizdate ${bizdate} --workers 3 # 调度(并发拉)
api2ods --job jobs/my_api.json --bizdate ${bizdate} --days 30 # 回拉最近 30 天(覆盖配置里的 window.days)
api2ods --job jobs/my_api.json --bizdate ${bizdate} --no-notify # 不发飞书通知(新增字段提醒等)
# 补数(闭区间):必须跟着 --bizdate(整段写进 pt=<bizdate>),见下方「一次运行只写一个 pt」
api2ods --job jobs/my_api.json --bizdate 20260920 --start-date 2026-07-01 --end-date 2026-09-20
api2ods --job jobs/my_api.json --bizdate 20260920 --dates 2026-09-01,2026-09-05 # 补零散日期
# 补数月份多的时候并发拉(--workers 只并行网络等待,写库仍是单线程,按各单元完成顺序落盘)
api2ods --job jobs/my_api.json --bizdate 20260920 --start-date 2026-07-01 --end-date 2026-09-20 --workers 3
--workers范围 132(默认 1);回补历史建议 24。并发只并行网络等待,写库仍是单线程。
一次运行只写一个
pt(这是设计:一个分区=一次运行的一份快照,DWD 再按源数据里的 日期字段重新分区)。--dates/--start-date+--end-date只决定"拉哪些天", 不决定"写进哪个分区",所以补数时不给业务日就会把整段窗口全写进默认业务日分区 (pt=<默认业务日,默认美东昨天>)、并先删后填覆盖掉调度刚写进去的数据。为此补数必须跟着--bizdate:--bizdate 20260920表示整段补数数据写进pt=20260920—— 分区口径和日常调度完全一致, DWD 不用区分"正式"和"补数"两种分区。缺了--bizdate会直接报错退出,不会静默写错分区。
一个最简单的作业配置长这样(没写的字段都有默认值:翻页方式自动推断、page/size/100、
startTime/endTime、时区、pt=${bizdate}……):
{
"job": "demo_api",
"maxcompute": { "project": "<项目名>", "ak": "<AK>", "sk": "<SK>" },
"request": {
"base_url": "https://api.example.com",
"path": "/v1/items",
"auth": { "type": "bearer", "token": "<token>" },
"records_path": "data.list"
},
"window": { "days": 15 },
"pagination": { "total_pages_path": "data.totalPages" },
"target": { "table": "ods_demo_api_json_di" }
}接口(JSON/CSV/ZIP)
│ ① 按 window 生成请求单元(每天一次 / 整区间一次),并发拉取
│ ② 每个单元翻页拉全(页码/游标),拉完一个单元就逐条落本地临时文件
│ (Spool:不跨单元累积,峰值内存与并发单元的数据量有关)
│ ③ 任一单元三次重试仍失败 → 本次放弃写库(旧分区保持原样,重跑即可)
▼
MaxCompute ODS:<json 列> + pt 分区(普通表)
│ ④ 自动建表/校验结构 → delete_partition → Tunnel 分批写入(先删再填)
│ ⑤ count(*) 与拉取条数核对,不一致按失败退出
▼
DWD 层:解 JSON、按主键取最新一条(跨 pt 去重)
作业文件(jobs/*.json)按下面几张表分块,只有 request.base_url 和 target.table
是必填,其余按需。字段默认值和逐行注释见 jobs/_template_full.example.json,最小示例见
jobs/_template_simple.example.json。示例里的 <项目名>、<AccessKeyId> 等占位符
换成你自己的值即可。
| 字段 | 必填 | 说明 |
|---|---|---|
job |
否 | 作业名(日志用) |
description |
否 | 一句话描述(日志用) |
secrets |
否 | 密钥键值对;request 里用 ${secrets.键名} 引用。也可以直接把密钥写在用到的地方 |
maxcompute |
是* | 目标项目与凭证:project / endpoint(默认 us-west-1,https)/ access_key_id+access_key_secret(可简写 ak/sk) |
profiles |
否 | 多套 MaxCompute 凭证,配合 target.profile 切换 |
占位符:
${secrets.键名}、${bizdate}(YYYYMMDD)、${bizdate_iso}、${today}、${today_iso}。
| 字段 | 默认 | 说明 |
|---|---|---|
base_url |
- | 根地址(必填);不跟随重定向——301/302/303 会把 POST 降级成不带 body 的 GET(窗口参数全丢),自定义鉴权头也可能被转发到别的地址;接口返回 3xx 会直接报错并打印 Location,请把地址改成最终地址 |
path |
空 | 路径;留空表示 base_url 就是完整地址 |
method |
GET | GET / POST / PUT / PATCH / DELETE |
body_type |
json | POST 请求体格式:json / form |
timeout_seconds |
30 | 单次请求超时 |
max_response_bytes |
1073741824 | 单次响应体上限(解压后字节,默认 1GiB);导出大文件时按需调大,超过直接报错 |
headers / params |
{} | 静态请求头 / 参数 |
params_in |
query | query(默认)/ headers(参数全放请求头,部分文件接口要求) |
auth |
none | 见下表(bearer 最常用) |
records_path |
空 | 记录数组路径(如 data.list);留空=整个返回就是数组 |
response_type |
json | json / bytes(文件流,配 parse,不支持分页) |
json_encoding |
空 | JSON 接口不是 UTF-8(如 GBK)时显式指定编码;不写则按 响应头声明的 charset → UTF-8(sig) 严格解码,解不出直接报错(不静默变乱码写库)。声明编码解不出 JSON、UTF-8 能解出时自动改用 UTF-8;两者都能解出但内容不同会拒绝猜测(报配置错,可用 json_encoding 明确指定);显式编码与 UTF-8 键名不同会报配置错,键名一致、只有值不同时按显式编码处理并告警 |
records_missing |
error | empty:路径取不到时按空数据(零数据日接口) |
add_fields |
{} | 每条记录追加固定字段(多账号打来源标记) |
fail_if |
[] | 业务错误判定,如 [{"path":"code","not_equals":"0","retry":true}](response_type=bytes 时自动跳过) |
retry_times / retry_delay |
5 / 15 | 失败后最多再试几次 / 首次冷却秒数(指数退避);即总请求数 = retry_times+1,写 0 就是只发一次 |
verify / proxies |
true / - | HTTPS 证书校验 / 代理 |
| type | 关键字段 | 适用 |
|---|---|---|
none |
- | 公开接口 |
bearer |
token |
最常用:Authorization: Bearer xxx(只写一个字段) |
token |
header / prefix / value |
自定义头,如 X-Api-Key: xxx |
query |
params |
密钥放 URL 参数 |
basic |
username / password |
HTTP Basic |
sha256_concat |
secret_key / sign_field / sign_in |
参数按 key 排序拼接+密钥做 sha256(Onerway) |
aliyun_rpc |
access_key_id / access_key_secret |
阿里云 OpenAPI RPC 签名(HMAC-SHA1) |
custom |
module / func |
自定义函数(放作业同目录 signers.py,见 signers.example.py) |
| 字段 | 默认 | 说明 |
|---|---|---|
mode |
per_day | per_day 每天一次请求(推荐)/ range 整区间一次。会先去掉首尾空白再匹配、只认这两个值——拼错或带空格("range ")直接报错,不会静默按 per_day 跑 |
days |
1 | 回拉天数(含基准日);--days 可覆盖 |
date_tz |
America/New_York | 窗口起止(“最近 N 天”)和 ${today} 占位符按哪个时区切;不传业务日时的默认业务日取 ODS_BIZDATE_TZ 时区的昨天(默认美东),不受此字段影响 |
api_tz |
+08:00 | 传给接口的时间按哪个时区算:固定偏移(+08:00 / +0800 / +08)或时区名(America/New_York,自动处理夏令时) |
pad_hours |
0 | 窗口前后多拉几小时(防边界丢数),取值 0~24。只要 >0,相邻两天的窗口就会重合 2×pad_hours 小时(同一笔数据拉两遍,靠 DWD 按主键去重兜底)。format 只到日期时本项被忽略并告警:日期参数减 pad 不是多拉一段,而是把日期整体顶到前一天。写布尔(YAML 里当开关写 true)与 NaN/超范围都会报错,不会被当成 1 小时 |
start_param / end_param |
startTime / endTime | 起止时间参数名。两个都不写才启用默认;自定义了 start_param 就不补 end_param(只要开始时间只写 start_param,不要结束时间写 "end_param": null) |
format |
%Y-%m-%d %H:%M:%S | 时间格式;unix=秒、unix_ms=毫秒;%s(epoch 秒)与 %P(am/pm)可在格式串任意位置用(自己实现,跨平台一致;%s 单用返回纯数字)。只到日期(无时分秒)时按 date_tz 直接输出那一天,不做时区换算。格式串至少要含一个真实时间指令:"unixtime"(unix 拼错)、纯 %% 转义("%%Y-%%m-%%d")都会在配置阶段报错——它们会被 strftime 原样输出成参数值、静默查不到任何数据 |
extra_params |
- | 额外派生参数,如 {"BillingCycle": "%Y-%m"}(基于窗口日起算) |
时区怎么配(美东源这种最容易错,先看这段):
date_tz决定“20260920 是哪一天的 20260920”,api_tz决定“接口认哪个时区的时间”。 两个都填对就不会错位。例:美东业务日 + 接口要北京时间字符串 →"date_tz": "America/New_York", "api_tz": "+08:00"时,bizdate=20260920那一天 发出去的是startTime=2026-09-20 12:00:00/endTime=2026-09-21 12:00:00, 也就是美东 9/20 00:00 ~ 9/21 00:00(9 月美东在夏令时,是 UTC-4)。
- 接口认美东时间时写
"api_tz": "America/New_York"(时区名), 不要写死-05:00——夏令时期间美东是-04:00,写死会在半年里每天错一小时;date_tz没按源方时区配置(默认美东)时,装进pt=20260920的会是「美东 9/19 半天 + 9/20 半天」, 数据不缺但跟源方的账期对不上;DWD 层按源数据里的日期字段重新分区可以规避这个影响;format只到日期时(%Y-%m-%d/%Y%m%d)不参与时区换算,直接就是那一天的日期。统一口径:不带
--bizdate且没有环境变量时,默认业务日统一取业务日时区的昨天 (默认America/New_York,可用ODS_BIZDATE_TZ覆盖;三个 ODS 工具共用公共运行层实现);date_tz只负责窗口起止怎么切成“一天”, 不改变默认业务日。pt永远是业务日bizdate本身。format只到日期时(阿里云BillingDate),date_tz完全不影响发给接口的参数;format带时分秒 /unix时(DeepSeek),date_tz决定窗口的实际起止,换时区会改变每个pt里装的是哪段时间(同一pt的内容整体平移, 不影响count校验)。本仓库作业按源方时区显式配置date_tz(Onerway 美东、阿里云 UTC、DeepSeek/XMP 上海)。
| 字段 | 默认 | 说明 |
|---|---|---|
type |
自动推断 | none / page / cursor;不写时:有 cursor_path→cursor、有 total_* 或 stop_when_short 或 page_param→page。只配了 size_param/page_size 会告警(没有翻页终点,只会请求一次) |
page_param / size_param / page_size / param_as_string |
page / size / 100 / false | 页码/游标分页用;size_param 写 null 表示不带页大小参数(接口不认 size 时)。page_param 不能与 size_param 同名:同名时页大小会覆盖页码,接口只回同一页、重复行会静默入库 |
total_pages_path / total_items_path |
- | page 分页至少给一个(翻页终点);两个都给时以条数终点为准(页数说翻完、条数没拉够就继续翻,宁可多翻一页也不静默少拉)。只认正数(-1/0 这类"未知"哨兵会被忽略),且终点值单调不减(某页只回本页条数也不会提前收尾)。cursor 分页也可以用 total_items_path 做兜底:游标提前结束但条数没拉够时会报错(防"游标字段写错 → 只拉第一页") |
stop_when_short |
false | 接口不返回总数时用:本页条数 < page_size 即判末页(含首屏空页=窗口无数据、整页后空页=没有下一页)。与 total_* 互斥、只对 type=page 生效,配置阶段报错兜底。假设接口除末页外按请求的 page_size 返回 |
strict |
true | 严格模式:空页但 TotalCount 没拉够 → 判失败(防静默截断)。接口总数不准才设 false,此时按已拉到的收尾并打警告。终点字段只在第一页返回时会被记住并沿用(末页之后的空页不再误判为"无法确认翻完") |
cursor_param / cursor_path / cursor_start |
游标分页;cursor_start 写 "" 表示首页就带上空的游标参数(默认首页不带)。建议同时配 total_items_path:没配时游标字段写错会被当成"翻完了"只拉第一页(会告警提示)。游标未推进检测:接口原样回显游标(或 cursor_path 指到了恒定字段)时第二页直接报错,不会翻满 max_pages 把同一页重复拉爆 |
|
delay_seconds / max_pages / window_retries |
翻页间隔 / 最大页数保护 / 单窗口失败重试次数 |
| 字段 | 说明 |
|---|---|
format |
csv / tsv / jsonl |
encoding |
默认 utf-8-sig(兼容 BOM);乱码可试 gbk |
delimiter / skip_rows |
分隔符 / 跳过开头 N 行 |
skip_until |
从包含该文字的行开始解析(报表有汇总段时用)。文件为空=当天没数据(按空处理);文件有内容却没有该标记=报错(文件结构可能变了)。与 skip_rows 不要同时配:标记行若是被跳掉的那几行,会直接报冲突 |
unzip / entry_contains |
解 ZIP / 只取文件名含指定串的条目。ZIP 里有多个文件又没写 entry_contains 时报错,确实要全解析时加 allow_multi_entry: true |
strict_encoding |
true 时按 encoding 解码失败直接报错;默认 false(解出乱码会打警告,仍按替换字符解析)——源方文件编码可能变过时建议打开 |
allow_multi_entry |
见 unzip:ZIP 多文件时是否允许全部解析 |
entry_field |
给每条记录加一列“来源文件名”(仅 unzip: true 时生效;没开 unzip 会告警) |
allow_single_record |
仅 format: jsonl 用:整个响应恰好只有一条 JSON 记录时,它和接口 200 返回的 {"code":500,...} 错误体无法区分,默认按错误体拦下(多记录 JSONL、最后一行不带换行都正常解析)。低流量源确实“整个响应就是一条 JSON 记录”时才设 true |
| 字段 | 默认 | 说明 |
|---|---|---|
project / table |
- / 必填 | 目标项目(缺省用 maxcompute.project)/ 表名 |
pt |
${bizdate} | 分区值模板,必须是 8 位业务日 yyyyMMdd(调度与 DWD 按它读取);--pt 可显式覆盖成特殊分区(测试/对比/补数用,如 test_20260921——注意特殊分区不会被调度与 DWD 自动读到) |
column |
json | json 列名 |
comment / stored_as / lifecycle_days |
- | 建表注释 / 存储格式 / 生命周期 |
allow_empty |
false | 本次 0 行时是否允许写空分区 |
profile |
default | 使用 profiles.<名> |
project / table / column / stored_as 会直接拼进建表 DDL 与校验 SQL,只允许字母/数字/下划线且不以数字开头(与 MaxCompute 标识符规则一致)。带空格、连字符、分号的名字会拿到一条明确的配置错,而不是建表失败或注入风险。
| 字段 | 默认 | 说明 |
|---|---|---|
webhook |
- | 飞书群机器人地址(发「接口出现新增字段」提醒);支持字符串或字符串数组(多个群同时告警);也可写在共享 --config 文件(默认工具目录 config.json)的 notify 块里,作业级同键覆盖 |
enabled |
true | false 时关闭该作业的通知(--no-notify 全局关闭) |
字段漂移检测:每次成功运行观察到的记录字段会存成快照(作业同目录
.field-state/下), 下次运行发现新增字段时发一条飞书提醒(只提醒、不阻塞:ODS 是裸 json 列,新字段原样包含、 无需改表)。快照只在真实写库成功后更新;--dry-run只对比不落盘。想使用新字段时, 更新 DWD 的get_json_object提取逻辑即可。
-
看接口文档,回答四个问题:怎么鉴权?怎么翻页?记录在返回的哪个字段?有没有时间参数?
-
挑模板:
你的接口 抄这个 Header Token + 页码分页 jobs/_template_simple.example.json什么都不用改太多、想看所有字段 jobs/_template_full.example.json阿里云 OpenAPI(RPC 签名) jobs/aliyun_bill.example.json自定义签名(参数拼接/加盐) jobs/onerway_transactions.example.json参数走 Header + 返回 CSV 文件 jobs/onerway_settlement_details.example.json返回 ZIP/CSV 文件 jobs/deepseek_usage.example.json自定义 md5 签名 + 无总数短页分页 jobs/xmp.example.json不想研究字段 直接 api2ods --init,按问答生成 -
改三处:
maxcompute(AK/SK)、request(地址/鉴权/records_path)、target.table; -
跑
--check:它会真实请求一次并打印返回顶层字段,帮你确认records_path; -
跑
--days 1 --dry-run看条数对不对,然后就可以配调度了。
-
拉取有任何失败 → 不写库(旧分区保持原样),退出码 1;
-
写库先删分区再写、整段失败自动重试(每次重试用全新的 Tunnel 会话,不会把上次残留的块 一起提交),重跑幂等;单条超限记录在删分区之前就检查,失败时分区原样不动;
-
分页翻完才收尾:接口把
PageSize压小也能拉全,翻页中途记录路径取不到、或空页而总数没拉够 都直接报错(不静默截断;接口总数确实不准时用pagination.strict: false放宽,仍会打警告); 终点字段只有第一页返回时会被记住并沿用,末页之后的空页不会误判成"翻不完"; 终点值取历史最大值(中途变小不缩水),-1/0这类"未知"值不当终点; -
写后
count(*)与拉取条数双重核对; -
默认拒绝写 0 行(防接口异常时清空分区),要写空分区显式
--allow-empty; -
单行超过约 7MB 提前报错(MaxCompute 单列上限 8MB);
-
建表 / 校验 / 分区增删 SQL 都有超时保护(默认 600 秒,可用
--sql-timeout调整,0=不限制、 负数在参数解析阶段报错退出码 2;超时主动取消)。分区增删走alter table … drop/add if [not] exists partition (pt='…')这条带超时的 DDL,不再用同步不限时的 pyodps 表 API——后者在云端 元数据操作卡住时会让任务一直挂起、占着运行锁把后续调度顶掉(与 sftp2ods / feishu2ods 同口径); -
日志与异常里的 token/sign/secret 一律脱敏:形态规则 + 按配置里的密钥值精确遮蔽 (凭证的明文与其 URL 编码形态
quote/quote_plus、以及把-也编码的激进编码器形态都替;含中文的密钥同样覆盖),接口把凭证写进自由文本报错时也不会漏; -
429/5xx 按
Retry-After退避重试,4xx 直接失败(参数/密钥问题快速暴露); 每次重试都重新鉴权(一次性签名如阿里云SignatureNonce不会因复用被判 400); 连接抖动(ConnectionError / Timeout / SSL 错误)走请求级退避重试; 而配置/签名错误(ConfigError)立即失败,不做无意义的空等重试; 4xx 与配置错误也不会再被window_retries整窗放大(重试多少次结果都一样); -
每个作业一把运行锁(Linux/macOS 用 flock、Windows 用 msvcrt;锁名=文件名+路径哈希, 同机同一作业互斥、不同作业可并行,同名不同目录的作业文件不会互相阻塞); 锁目录默认在工具目录
.run-locks/(不可写时退回当前用户的私有临时目录);同一作业会以不同身份或 不同TMPDIR跑(root 手动补数与普通用户调度混用)时,用环境变量API2ODS_LOCK_DIR把锁目录钉在固定位置,并按多用户权限说明配置共用组。 跨主机互斥取决于共享文件系统的锁支持;不支持文件锁时默认拒绝运行; Ctrl+C 不写库(退出码 130),并发拉取时进程立即结束、不等在飞的请求跑完 (拉取阶段返回的 130 会在main里转成os._exit——光靠shutdown(wait=False), 解释器退出时仍会 join 工作线程;代价是本次的临时 spool 文件可能留在系统 temp); -
大数据量流式落盘(本地临时 JSONL,默认系统 temp,可用
TMPDIR指定):每个请求单元拉完就落盘, 不跨单元累积;单次请求单元(一天 / range 模式整个区间)的记录会先在内存里攒齐再落盘, 待消费单元数限制为 workers,峰值内存与并发单元的数据量有关。range 会把整个区间攒在内存里, 增大 page_size 也不会减少单元总量;大源优先缩小请求窗口并降低 workers,部署前按实际单元大小评估内存; 拉取进度每 1000 条打一次日志;写入分批同时受行数与字节数限制(默认 1000 行 / 8MB), 单条超限记录会在写库前拒绝;--keep-spool可保留本次落盘的临时文件排查(成功失败都保留, 不加时运行结束自动清理)。 -
接口新增字段只提醒、不阻塞:与上次成功运行观察到的字段快照对比,出现新字段时发一条 飞书提醒(列名 + 处理建议;一次运行最多一条,
--no-notify/notify.enabled:false可关)。 数据本身不受影响——json 列原样落库、新字段自动包含;要分析新字段时更新 DWD 提取逻辑即可。 快照更新只在写库成功后(与"写后核对"同一口径),失败/中断/--dry-run都不会推进快照, 保证提醒不丢。快照文件在作业同目录.field-state/(读写失败只记日志,不影响主流程与退出码)。 -
JSON 标量错误体不会当成"空数据":HTTP 200 下返回
"rate limit exceeded"/500/null这类单行响应(文件流路径)时直接报错——原来会被当"只有表头的 CSV"解析出 0 行、 按零数据静默收尾(宁失败勿写错); -
凭证不全不会静默换身份:
maxcompute的 AK/SK 只填一半(典型:键名拼错成access_key_secect)直接报错、不会警告后回退到环境变量/本机 aliyun CLI;显式指定的--mc-profile/maxcompute.profile找不到时同样报错、不会改用其它 profile 的 AK; 确实想用环境变量/CLI 时把 AK/SK 整对留空即可; -
运行锁按规范化路径计算:同一作业用相对/绝对/带
..的写法落到同一把锁(否则两份 调度各拿一把锁、互斥静默失效、同删同写一个分区)。
| 码 | 含义 |
|---|---|
| 0 | 成功(--dry-run 拉到数据时同样返回 0;0 行但 --allow-empty 时也会写空分区并返回 0) |
| 1 | 运行失败:请求失败、写库失败、写后行数对不上、0 行保护触发(--dry-run 按同一判定,0 行即退出非 0)、--check 未通过;作业文件不存在、业务日格式不对等配置类问题也归这里 |
| 2 | 命令行参数问题(缺 --job、--days 不是整数、--sql-timeout 为负等 argparse 层),没发过请求 |
| 130 | 用户中断(Ctrl+C) |
配置类错误(作业 JSON 写错、ConfigError)走的是 SystemExit,退出码同样是 1:
凡是"重跑结果一样"的问题都归到 1,调度直接告警即可,不必按码分流重试。
| 现象 | 处理 |
|---|---|
解析失败:找不到 records_path |
路径写错;--check 会打印返回的顶层字段;接口“空对象=无数据”时加 records_missing: "empty" |
| JSON 接口中文变乱码 / 报“解不出来” | 源不是 UTF-8(如 GBK):给 request 加 json_encoding: "gbk"(不配时工具不静默替换,直接报错)。若日志提示显式编码与 utf-8-sig 不一致,先核对接口真实编码;键名不同会直接报错,只有值不同则按显式编码继续并留告警 |
HTTP 401/403/400 |
密钥/参数问题(不重试);检查 auth 与 params |
期望文件流,但接口返回了 JSON |
文件接口的权限/参数错误(如未开通下载权限);按提示里的 JSON 内容排查 |
接口返回业务错误:code=... |
命中 fail_if;限流类错误给 "retry": true 先重试几次 |
| 大量“重试”日志 | 撞限流:降 --workers、调大 pagination.delay_seconds |
本次拉取 0 行...未写库 |
确认接口当天确实无数据;要写空分区加 --allow-empty |
--dry-run 显示 0 行并退出 1 |
有意的:dry-run 与正式跑同一套判定(0 行且未 --allow-empty 即非 0),当预检用时能提前发现坏配置 |
游标未推进(接口回显了同一个游标) |
cursor_path 指到了恒定字段(如 total/page_size):改成真正的下一页游标字段。接口确实原样回显游标时,这个接口不适合游标翻页,改用 page + total_* |
指定的 aliyun CLI profile「x」不存在或没有 AK/SK |
拼写核对 ~/.aliyun/config.json 里的 profile 名;有意换来源就清空 --mc-profile/profile——工具不会自动改用别的 profile |
--start-date/--end-date 区间过大 |
上限 3660 天(≈10 年):日期大概率打错了(如 end 写成下个世纪);确实要超长补数请分段执行 |
响应是一个 JSON 标量(疑似错误体) |
接口在 200 状态下返回了 "错误消息"/数字/null(常见于限流/网关异常):先确认接口行为;确属数据再调整 response_type / parse.format |
补数(--dates / --start-date+--end-date)必须跟着 --bizdate 一起用 |
加 --bizdate 20260920,整段数据写进 pt=20260920;见上方「一次运行只写一个 pt」 |
配置文件报 不是合法 JSON 但内容看着没问题 |
多半是文件存成了「UTF-8 with BOM」;新版已自动兼容,升级后仍报错再查逗号/引号 |
pagination.page_size 必须是数字 / window.days 必须是整数 |
配置里写成了带引号的字符串或带了单位,改成纯数字 |
表结构与框架要求不一致 |
目标表是宽表/事务表:换表名(建议 ods_xxx_json_di)或先 drop 重建 |
| 时区不对/跨天差一小时 | 检查 window.date_tz(怎么切天)与 api_tz(怎么传给接口)。两者都可以写 IANA 名称(如 America/New_York),api_tz 写时区名会自动处理夏令时(见上方「时区怎么配」)。请按数据源的业务日口径设置 date_tz,例如 America/New_York、Asia/Shanghai 或 UTC |
pagination.strict=false 的警告日志 |
接口给的 TotalCount 不准(翻页期间数据还在涨);警告里会写明"已拉到多少条就收尾了" |
记录里有无法序列化的值 ... NaN |
接口返回了 NaN/Infinity(非法 JSON),落库后下游取不到值,已在拉取阶段中止 |
环境变量 bizdate/SKYNET_BIZDATE 的值不是合法日期 |
调度传下来的业务日格式不对(应为 YYYYMMDD)。不会回退成"昨天"——那会先删再填写错分区、退出码还是 0;先 unset bizdate 或显式 --bizdate |
| Windows 报找不到时区 | pip install tzdata |
python -m pip install -e ".[dev]" # 开发依赖(ruff)
python -m unittest discover -s tests -v # 离线用例:不访问网络、不连数仓
ruff check . # 代码检查
ruff format --check . # 格式检查(需要时先跑 ruff format .)review_ignore.json 是代码审查工具的误报抑制名单,不参与运行。测试按模块拆分(tests/test_*.py),公共替身在 tests/_helpers.py。
CI 在 ubuntu / windows / macos × Python 3.9 / 3.10 / 3.11 / 3.12 / 3.13 / 3.14
十八种组合上跑同一套用例(见 .github/workflows/tests.yml)。想改代码或加数据源,先看 CONTRIBUTING.md
——里面写了这个工具的几条"设计红线"(先删再填、宁可失败不可静默丢数、密钥不进日志……),
不少看着顺手的改法会踩到它们。
模块结构(都在 api2ods/ 包内,每个文件头部有职责说明):
| 模块 | 职责 |
|---|---|
cli.py |
命令行入口 / 体检 / 同步主流程 |
init_wizard.py |
--init 交互式配置生成 |
config.py |
配置加载/占位符/校验/目标解析 |
dates.py |
日期与时区、窗口参数生成 |
auth.py |
八种鉴权(含阿里云 RPC 签名) |
http.py |
请求、重试、Retry-After、业务错误判定 |
parsers.py |
JSON 路径 / CSV / ZIP / JSONL 解析 |
fetch.py |
请求单元、翻页、并发、整窗重试 |
spool.py |
按单元落盘与写入分批 |
mc.py |
MaxCompute:建表校验 / 先删再填 / 行数核对 |
notify.py |
飞书群卡片通知(新增字段提醒等,发送失败不改退出码) |
fieldwatch.py |
字段漂移检测(快照对比,只提醒不阻塞入库) |
utils.py |
日志、运行锁、脱敏、重试工具 |
MIT(见 LICENSE)。
默认临时锁目录在 POSIX 上校验属主和写权限,拒绝软链或公共可写目录;无安全可写目录时停止运行。
多身份或多环境运行同一作业时,请用对应的 *_LOCK_DIR 固定同一目录,保持互斥。
三个项目保持独立安装;SQL 等待、告警和部分脱敏工具维护一份源码并同步,详见
公共源码说明。贡献前请运行 python tools/sync_common.py --check。
JSON 和 JSONL 中的十进制数字使用 Decimal 解析,并通过 simplejson 保留为 JSON 数字写入;大整数、高精度金额和极小非零数不会先转换成二进制浮点数。输出的空白、指数大小写等文本形式可以变化,数字的值和类型保持不变。simplejson>=4.2 已列入安装依赖,源码运行时也需安装 requirements.txt。
页码分页默认检测整页重复;即使累计条数达到 total,也不能靠重复第一页冒充拉取完整。发现重复立即失败,本次不写库。没有唯一标识、且源数据确实允许完全相同页面的接口,可以显式配置:
"pagination": {
"type": "page",
"total_items_path": "data.totalCount",
"allow_repeated_pages": true
}该开关只用于已确认的源端语义,不会替接口补齐缺失数据。检测比较整页内容并忽略对象键顺序;它不代替业务主键去重,也不保证上游在分页期间被编辑时仍能获得一致快照。
正式运行在作业锁之外,还会取得按 project.table 归一化的目标表锁。同一用户的三个 ODS 工具、不同虚拟环境和不同配置文件写同一张表时互斥;不同表可并行,争用锁的任务以失败退出而非无限等待。它仍不能消除直接覆盖分区失败后需要重跑的限制。
默认目标锁位于系统临时目录的 ods-target-locks[-UID]。多用户、不同 TMPDIR 或容器运行同一目标时,用 ODS_TARGET_LOCK_DIR 指向同一固定目录,并确认文件系统提供有效的文件锁。仅设置共享路径不能保证跨主机互斥;推荐固定调度机器。升级前停止旧进程,并更新所有可能写同一目标的工具。原有 *_LOCK_DIR 继续只控制各工具的作业锁。
POSIX 跨用户部署需将共享目录设为共用组所有的 2770 setgid 目录,运行锁会继承组并以 0660 供组内账号复用;默认私有目录仍使用 0600。Windows 需配置继承的读写 ACL。完整示例和旧锁权限迁移步骤见多用户共享锁目录。