TWSE MOPS 月營收端點的「出表日期」vs「資料年月」— 兩個不同欄位的用途差異與 silent failure 機制
串接台股月營收資料時,TWSE MOPS 月營收端點(t187ap05_L)的每筆回傳包含兩個看起來都跟「哪個月」有關的欄位:出表日期(申報揭露日)與資料年月(實際營收所屬月份)。兩者不是同一件事,用錯欄位會造成整月位移的 silent failure:流程照樣跑完、數字看起來合理、不拋出任何錯誤,只有人工比對月份時才會察覺。
本文整理這兩個欄位的定義差異、民國字串解析規則、千元單位陷阱,以及穩定取值的建議做法。
① t187ap05_L 端點與欄位結構
GET https://openapi.twse.com.tw/v1/opendata/t187ap05_L
此端點為 TWSE MOPS 上市公司每月營業收入彙總,每月彙整更新一次。批次內所有公司共用同一個 出表日期(即該批次資料的釋出日);該釋出日與「公司須於次月 10 日前完成申報」的法定截止日是兩回事 — 釋出日可能落在截止日之後(本批次 出表日期 = 1150517,即 2026-05-17,對應 4 月營收批次)。回傳為 JSON 陣列,每筆代表一間上市公司當月申報結果。
以下欄位清單以 2026-05-28 快照(民國 115 年 4 月營收批次)為準,台泥(1101)為 schema 示意:
| 欄位名 | 範例值 | 型別 | 說明 |
|---|---|---|---|
出表日期 | "1150517" | 字串,7 碼民國年 | 申報揭露日 |
資料年月 | "11504" | 字串,5 碼民國年月 | 實際營收所屬月份 |
公司代號 | "1101" | 字串 | 股票代號 |
公司名稱 | "台泥" | 字串 | 公司名稱(僅供 schema 示意,非市場判斷依據) |
產業別 | "水泥工業" | 字串 | TWSE 產業分類 |
營業收入-當月營收 | "12213195" | 字串,單位千元 | 當月實際營收 |
營業收入-上月營收 | "12412837" | 字串,單位千元 | 前一個月的營收 |
營業收入-去年當月營收 | "12681318" | 字串,單位千元 | 去年同月營收 |
營業收入-上月比較增減(%) | "-1.6083510965301486" | 字串,超長小數 | 與上月比較增減幅度 |
營業收入-去年同月增減(%) | "-3.6914380666110573" | 字串,超長小數 | 與去年同月比較增減幅度 |
累計營業收入-當月累計營收 | "45472613" | 字串,單位千元 | 當年度累積至當月 |
累計營業收入-去年累計營收 | "47653544" | 字串,單位千元 | 去年同期累積 |
累計營業收入-前期比較增減(%) | "-4.576639672381974" | 字串,超長小數 | 累計與去年同期比較增減幅度 |
備註 | "-" | 字串 | 公司自填說明 |
所有數值欄位均以字串型別回傳,計算前需明確轉型。百分比欄位為超長小數字串(例 -1.6083510965301486),顯示前建議 round 至 1 位;空字串或非數字輸入需另行處理。
② 出表日期 vs 資料年月:兩欄各是什麼
這是此端點最容易誤用的核心差異。
出表日期(7 碼民國年 YYYMMDD)= 這筆數字申報上 MOPS 的日期
以台泥(1101)2026 年 4 月快照為例:出表日期 為 "1150517",解析為民國 115 年 5 月 17 日,即 2026 年 5 月 17 日。這是台泥向主管機關申報、揭露在 MOPS 的時間點。
資料年月(5 碼民國年月 YYYMM)= 這筆數字所屬的營收月份
同一筆資料的 資料年月 為 "11504",解析為民國 115 年 04 月,即 2026 年 4 月。這是台泥 4 月份的實際營收。
兩個欄位的時間差在一個月左右:4 月的營收,在 5 月 17 日申報揭露。這個時差是制度設計的結果,不是資料異常。
格式差異一覽:
| 欄位 | 格式 | 碼數 | 含義 |
|---|---|---|---|
出表日期 | ROC YYYMMDD | 7 碼,含日 | 申報日(5 月) |
資料年月 | ROC YYYMM | 5 碼,無日 | 所屬月份(4 月) |
資料年月 為 5 碼,不含日。解析時不可套用 YYYMMDD 的 7 碼格式,也不可用 new Date() 直接傳入,需手動拆年份(前 3 碼)和月份(後 2 碼)再轉換:
// 資料年月: "11504" → 2026-04
const raw = "11504";
const rocYear = parseInt(raw.slice(0, 3), 10); // 115
const month = raw.slice(3, 5); // "04"
const adYear = rocYear + 1911; // 2026
const period = `${adYear}-${month}`; // "2026-04"
③ 為什麼會錯 — silent failure 機制
用 出表日期 當營收月份是這個端點最典型的 silent failure。
典型的錯誤邏輯是:「拿到這筆資料 → 取 出表日期 當作這筆營收屬於 5 月 → 存入 pipeline 標記為 5 月數據。」但這筆資料的實際內容是 4 月的營收,只是在 5 月申報揭露。結果是每一筆資料都整月位移:4 月營收被標成 5 月,3 月營收被標成 4 月,依此類推。
這個錯誤的 silent failure 特徵:
- 流程不報錯:
出表日期是合法的字串,parse 不會 throw - 數字看起來合理:位移後的月份數字本身沒有異常,在合理的量級內
- 推播正常完成:自動化通知會如常送出
- 發現困難:只有在人工把端點回傳的
資料年月和自己記錄的「月份標記」交叉比對時,才能察覺整批數據月份都差了一個月
其餘常見的 silent failure 入口:
民國 5 碼無日,不可直接套 7 碼解析邏輯。"11504" 若誤套 YYYMMDD 格式,會把後 2 碼 04 讀成日期,月份部分變成空缺或亂讀。不報錯,但解出的日期是錯的。
千元單位造成數字看起來天文數字。"12213195" 的單位是千元,意即 12,213,195,000 元(約 122 億)。若未轉換直接顯示,數字量級誤導判讀。換算為億元的公式:
// 營業收入欄位單位: 千元 → 億元
// 12213195 千元 = 122.13 億
const rawKUnit = parseInt("12213195", 10); // 千元
const hundredMil = rawKUnit / 100000; // ÷ 100000 = 億元(保留 2 位)
const display = hundredMil.toFixed(2) + " 億"; // "122.13 億"
百分比欄位為空字串或非數字。部分公司某些比較期間的百分比欄位回傳空字串 ""(例如新掛牌公司無去年同期),或前期基期為 0 時出現極端值(例如 "161502.90456431537")。直接呼叫 parseFloat() 不做空值判斷,會帶入 NaN 進入下游計算。
④ 穩定取值的建議做法
做法 1:永遠以 資料年月 為營收月份識別欄位
出表日期 的正確用途是「這批資料是什麼時候上線的」,適合用作 pipeline 的新鮮度判斷或 log 記錄,不應作為「這筆數據屬於哪個月」的依據。
// 取 資料年月 作為月份識別
function parseRevenuePeriod(raw) {
// raw: "11504" (ROC YYYMM, 5 碼, 無日)
if (!raw || raw.length !== 5) {
throw new Error(`Unexpected 資料年月 format: "${raw}"`);
}
const rocYear = parseInt(raw.slice(0, 3), 10);
const month = raw.slice(3, 5);
const adYear = rocYear + 1911;
return `${adYear}-${month}`; // "2026-04"
}
indexOf / 欄位 key 不存在時直接拋出錯誤,不要靜默退回預設值。
做法 2:千元 → 億元換算在資料入口統一處理
// 千元→億元,回傳 2 位小數字串
function kUnitToHundredMil(rawStr) {
if (!rawStr || rawStr === "") return null;
const val = parseInt(rawStr, 10);
if (isNaN(val)) return null;
return (val / 100000).toFixed(2);
}
做法 3:百分比欄位統一做空值與極端值過濾
// 百分比欄位: 超長小數字串 → round 1 位,空字串/NaN → null
function parseRevenuePct(rawStr) {
if (!rawStr || rawStr === "") return null;
const val = parseFloat(rawStr);
if (isNaN(val)) return null;
// 前期基期為 0 時可能出現極端值,視使用情境決定是否 cap
return val.toFixed(1); // e.g. "-1.6"
}
做法 4:資料年月 欄位不存在時直接拋出,不靜默降級
const record = apiResponse[0];
const period = record["資料年月"];
if (period === undefined || period === null) {
throw new Error("t187ap05_L: 資料年月 field missing — possible schema change");
}
⑤ 為什麼值得知道
對用 n8n 或自建流程串接月營收端點的開發者,出表日期 vs 資料年月 的混淆是月份標記位移的最常見來源。這個端點的批次更新節奏是:公司次月 10 日前完成申報(法定截止),TWSE 彙整後統一釋出批次;釋出後每次呼叫拿到的 出表日期 都是批次釋出當月,而 資料年月 都是上個月 — 這個設計符合申報制度邏輯,但對自動化流程的欄位選取形成干擾。
對存股族追蹤自選標的月營收趨勢的使用情境,正確解析 資料年月(而非 出表日期)是月份比較準確的前提。月對月、年對年的比較欄位(上月比較增減(%)、去年同月增減(%))已由 TWSE 計算在端點回傳內,但分子分母的正確定位建立在先確認 資料年月 是目標月份。
想把 TWSE 月營收端點的資料自動整理、每月送到自己的 Telegram 或 LINE?floviq 月營收追蹤模板(portaly.cc/floviq)處理了這篇整理的四個取值眉角,包含欄位對齊、民國年解析、千元換算與空值過濾,設定一次後每月自動跑。
⑥ Edge case 已知
端點一次回傳全量上市公司:t187ap05_L 每次呼叫回傳當批次所有上市公司的月營收,不支援單一公司查詢參數。若只需要特定代號,需要在 client 端過濾 公司代號 欄位。
新上市公司與去年同期空缺:部分新掛牌公司無去年同期資料,去年同月增減(%) 欄位回傳空字串 "",去年當月營收 欄位可能為 "0"。不做空值檢查直接計算增減會得到 NaN 或除以 0 的極端值。
備註欄位有實質內容:多數公司的 備註 欄位為 "-",但部分公司會填入營收變化說明(例如「主因子公司營收上升」),自動化流程若需要呈現這欄,需過濾 "-" 這個哨兵值。
月份更新時間點與批次釋出:每月 10 日為公司申報截止日。TWSE 在截止日之後彙整為批次統一釋出;出表日期 是該批次的統一資料釋出日(同一批次每筆紀錄的值完全相同),不是各公司各自的申報時間戳。呼叫時機若在批次尚未釋出之前,端點可能仍回傳上一批次資料。
申報截止日落在假日時的順延:若次月 10 日為假日,申報截止日順延至下一個營業日,批次釋出時間預期也會落在順延後的截止日之後。但無論釋出日如何浮動,資料年月 都不受影響,仍代表該批次所屬月份的營收期間,是月份定位的唯一可靠欄位。
⑦ See also
- TWSE OpenAPI 日期格式陷阱 — 五端點曆法差異完整分析,含 ROC 7 碼、5 碼、3 碼、斜線格式的 parse pattern 對照
- TWSE T86 欄位命名實況 — 同類 silent failure:欄位名稱與口語不一致的取值陷阱
- TWSE BWIBBU_d 端點本益比與殖利率欄位機制 — 同批次 TWSE OpenAPI 體系下另一端點的曆法問題
- TWSE OpenAPI Swagger:https://openapi.twse.com.tw/
- floviq 月營收追蹤模板:https://portaly.cc/floviq
※ 本文描述 TWSE MOPS t187ap05_L 月營收端點的欄位機制、申報日期與資料所屬月份的差異,以及相關的 silent failure 風險 · 不構成投資建議,投資請自行評估風險。文中台泥(1101)資料僅作為端點欄位結構的示意,不構成對該公司任何評估。
資料來源:TWSE OpenAPI t187ap05_L(資料快照 2026-05-28,回傳批次 資料年月 = "11504" 即民國 115 年 4 月)
如果這篇對你有用: