w-dwdata-hub 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +55 -0
  4. package/.jsdoc +25 -0
  5. package/LICENSE +21 -0
  6. package/README.md +168 -0
  7. package/SECURITY.md +5 -0
  8. package/babel.config.js +16 -0
  9. package/dist/w-dwdata-hub.umd.js +7 -0
  10. package/dist/w-dwdata-hub.umd.js.map +1 -0
  11. package/docs/WDwdataHub.mjs.html +136 -0
  12. package/docs/decodeEntities.mjs.html +129 -0
  13. package/docs/extractDivContent.mjs.html +147 -0
  14. package/docs/fetchAiNewsAggregator.mjs.html +155 -0
  15. package/docs/fetchAisixiang.mjs.html +1038 -0
  16. package/docs/fetchCnyes.mjs.html +260 -0
  17. package/docs/fetchGuancha.mjs.html +1007 -0
  18. package/docs/fetchHackerNews.mjs.html +189 -0
  19. package/docs/fetchMoneydj.mjs.html +251 -0
  20. package/docs/fetchMops.mjs.html +385 -0
  21. package/docs/fetchRSS.mjs.html +160 -0
  22. package/docs/fetchStatementdog.mjs.html +213 -0
  23. package/docs/fetchTaifex.mjs.html +513 -0
  24. package/docs/fetchTpex3insti.mjs.html +278 -0
  25. package/docs/fetchTpexMargin.mjs.html +248 -0
  26. package/docs/fetchTpexStock.mjs.html +199 -0
  27. package/docs/fetchTwDataHoliday.mjs.html +236 -0
  28. package/docs/fetchTwseMargin.mjs.html +253 -0
  29. package/docs/fetchTwseStock.mjs.html +188 -0
  30. package/docs/fetchTwseT86.mjs.html +208 -0
  31. package/docs/fetchWithRetry.mjs.html +376 -0
  32. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  33. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  34. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  35. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  36. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  37. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  38. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  39. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  40. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  41. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  42. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  43. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  44. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  45. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  46. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  47. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  48. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  49. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  50. package/docs/getOptFetch.mjs.html +185 -0
  51. package/docs/global.html +22649 -0
  52. package/docs/htmlToMarkdown.mjs.html +168 -0
  53. package/docs/index.html +84 -0
  54. package/docs/isYmd.mjs.html +114 -0
  55. package/docs/parseIntComma.mjs.html +100 -0
  56. package/docs/safeFilename.mjs.html +102 -0
  57. package/docs/scripts/collapse.js +39 -0
  58. package/docs/scripts/commonNav.js +28 -0
  59. package/docs/scripts/linenumber.js +25 -0
  60. package/docs/scripts/nav.js +12 -0
  61. package/docs/scripts/polyfill.js +4 -0
  62. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  63. package/docs/scripts/prettify/lang-css.js +2 -0
  64. package/docs/scripts/prettify/prettify.js +28 -0
  65. package/docs/scripts/search.js +99 -0
  66. package/docs/styles/jsdoc.css +776 -0
  67. package/docs/styles/prettify.css +80 -0
  68. package/docs/toDatetimeUTC8.mjs.html +143 -0
  69. package/docs/toRocDate.mjs.html +107 -0
  70. package/g.mjs +73 -0
  71. package/package.json +31 -0
  72. package/script.txt +17 -0
  73. package/src/WDwdataHub.mjs +64 -0
  74. package/src/decodeEntities.mjs +57 -0
  75. package/src/extractDivContent.mjs +75 -0
  76. package/src/fetchAiNewsAggregator.mjs +83 -0
  77. package/src/fetchAisixiang.mjs +966 -0
  78. package/src/fetchCnyes.mjs +188 -0
  79. package/src/fetchGuancha.mjs +935 -0
  80. package/src/fetchHackerNews.mjs +117 -0
  81. package/src/fetchMoneydj.mjs +179 -0
  82. package/src/fetchMops.mjs +313 -0
  83. package/src/fetchRSS.mjs +88 -0
  84. package/src/fetchStatementdog.mjs +141 -0
  85. package/src/fetchTaifex.mjs +441 -0
  86. package/src/fetchTpex3insti.mjs +206 -0
  87. package/src/fetchTpexMargin.mjs +176 -0
  88. package/src/fetchTpexStock.mjs +127 -0
  89. package/src/fetchTwDataHoliday.mjs +164 -0
  90. package/src/fetchTwseMargin.mjs +181 -0
  91. package/src/fetchTwseStock.mjs +116 -0
  92. package/src/fetchTwseT86.mjs +136 -0
  93. package/src/fetchWithRetry.mjs +304 -0
  94. package/src/getOptFetch.mjs +113 -0
  95. package/src/htmlToMarkdown.mjs +96 -0
  96. package/src/isYmd.mjs +42 -0
  97. package/src/parseIntComma.mjs +28 -0
  98. package/src/safeFilename.mjs +30 -0
  99. package/src/toDatetimeUTC8.mjs +71 -0
  100. package/src/toRocDate.mjs +35 -0
  101. package/test/WDwdataHub.test.mjs +63 -0
  102. package/test/decodeEntities.test.mjs +42 -0
  103. package/test/extractDivContent.test.mjs +48 -0
  104. package/test/fetchAiNewsAggregator.test.mjs +79 -0
  105. package/test/fetchAisixiang.test.mjs +225 -0
  106. package/test/fetchCnyes.test.mjs +61 -0
  107. package/test/fetchGuancha.test.mjs +190 -0
  108. package/test/fetchHackerNews.test.mjs +78 -0
  109. package/test/fetchMoneydj.test.mjs +82 -0
  110. package/test/fetchMops.test.mjs +106 -0
  111. package/test/fetchRSS.test.mjs +85 -0
  112. package/test/fetchStatementdog.test.mjs +69 -0
  113. package/test/fetchTaifex.test.mjs +99 -0
  114. package/test/fetchTpex3insti.test.mjs +90 -0
  115. package/test/fetchTpexMargin.test.mjs +94 -0
  116. package/test/fetchTpexStock.test.mjs +68 -0
  117. package/test/fetchTwDataHoliday.test.mjs +100 -0
  118. package/test/fetchTwseMargin.test.mjs +81 -0
  119. package/test/fetchTwseStock.test.mjs +80 -0
  120. package/test/fetchTwseT86.test.mjs +76 -0
  121. package/test/fetchWithRetry.test.mjs +174 -0
  122. package/test/htmlToMarkdown.test.mjs +71 -0
  123. package/test/isYmd.test.mjs +56 -0
  124. package/test/parseIntComma.test.mjs +38 -0
  125. package/test/safeFilename.test.mjs +35 -0
  126. package/test/toDatetimeUTC8.test.mjs +65 -0
  127. package/test/toRocDate.test.mjs +38 -0
  128. package/test/tools/encodeBig5.mjs +80 -0
  129. package/test/tools/fixtures.mjs +539 -0
  130. package/test/tools/hasChrome.mjs +39 -0
  131. package/test/tools/serverForTest.mjs +397 -0
  132. package/toolg/addVersion.mjs +4 -0
  133. package/toolg/cleanFolder.mjs +5 -0
  134. package/toolg/gDistRollup.mjs +49 -0
  135. package/toolg/modifyReadme.mjs +4 -0
@@ -0,0 +1,199 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>fetchTpexStock.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#KNOWN_TOPICS">KNOWN_TOPICS</a></li><li><a href="global.html#WDwdataHub">WDwdataHub</a></li><li><a href="global.html#assertTpexFieldShape">assertTpexFieldShape</a></li><li><a href="global.html#decodeEntities">decodeEntities</a></li><li><a href="global.html#extractAuthorFromContent">extractAuthorFromContent</a></li><li><a href="global.html#extractDivContent">extractDivContent</a></li><li><a href="global.html#extractHtmlTitle">extractHtmlTitle</a></li><li><a href="global.html#extractPubTime">extractPubTime</a></li><li><a href="global.html#extractTitle">extractTitle</a></li><li><a href="global.html#fetchAiNewsAggregator">fetchAiNewsAggregator</a></li><li><a href="global.html#fetchAisixiang">fetchAisixiang</a></li><li><a href="global.html#fetchAllListPages">fetchAllListPages</a></li><li><a href="global.html#fetchAllSearchPages">fetchAllSearchPages</a></li><li><a href="global.html#fetchArticle">fetchArticle</a></li><li><a href="global.html#fetchAuthorArticles">fetchAuthorArticles</a></li><li><a href="global.html#fetchAuthorsList">fetchAuthorsList</a></li><li><a href="global.html#fetchCnyes">fetchCnyes</a></li><li><a href="global.html#fetchFuturesData">fetchFuturesData</a></li><li><a href="global.html#fetchGuancha">fetchGuancha</a></li><li><a href="global.html#fetchHackerNews">fetchHackerNews</a></li><li><a href="global.html#fetchHtml">fetchHtml</a></li><li><a href="global.html#fetchInstitutionalData">fetchInstitutionalData</a></li><li><a href="global.html#fetchKeywordArticles">fetchKeywordArticles</a></li><li><a href="global.html#fetchMoneydj">fetchMoneydj</a></li><li><a href="global.html#fetchMops">fetchMops</a></li><li><a href="global.html#fetchOnce">fetchOnce</a></li><li><a href="global.html#fetchPCRatio">fetchPCRatio</a></li><li><a href="global.html#fetchRSS">fetchRSS</a></li><li><a href="global.html#fetchStatementdog">fetchStatementdog</a></li><li><a href="global.html#fetchTaifex">fetchTaifex</a></li><li><a href="global.html#fetchTargetWithRetry">fetchTargetWithRetry</a></li><li><a href="global.html#fetchTitleArticles">fetchTitleArticles</a></li><li><a href="global.html#fetchTopicArticles">fetchTopicArticles</a></li><li><a href="global.html#fetchTopicsList">fetchTopicsList</a></li><li><a href="global.html#fetchTpex3insti">fetchTpex3insti</a></li><li><a href="global.html#fetchTpexMargin">fetchTpexMargin</a></li><li><a href="global.html#fetchTpexStock">fetchTpexStock</a></li><li><a href="global.html#fetchTwDataHoliday">fetchTwDataHoliday</a></li><li><a href="global.html#fetchTwseMargin">fetchTwseMargin</a></li><li><a href="global.html#fetchTwseStock">fetchTwseStock</a></li><li><a href="global.html#fetchTwseT86">fetchTwseT86</a></li><li><a href="global.html#fetchWithRetry">fetchWithRetry</a></li><li><a href="global.html#getCfg">getCfg</a></li><li><a href="global.html#getOptFetch">getOptFetch</a></li><li><a href="global.html#getPayload">getPayload</a></li><li><a href="global.html#htmlToMarkdown">htmlToMarkdown</a></li><li><a href="global.html#isHomepageHtml">isHomepageHtml</a></li><li><a href="global.html#isNonHolidayEntry">isNonHolidayEntry</a></li><li><a href="global.html#isRetryableError">isRetryableError</a></li><li><a href="global.html#isYmd">isYmd</a></li><li><a href="global.html#lookupAuthor">lookupAuthor</a></li><li><a href="global.html#lookupTopic">lookupTopic</a></li><li><a href="global.html#parseAuthorColumnPage">parseAuthorColumnPage</a></li><li><a href="global.html#parseAuthorIndex">parseAuthorIndex</a></li><li><a href="global.html#parseCSV">parseCSV</a></li><li><a href="global.html#parseCSVLine">parseCSVLine</a></li><li><a href="global.html#parseIntComma">parseIntComma</a></li><li><a href="global.html#parseListPage">parseListPage</a></li><li><a href="global.html#parseNum">parseNum</a></li><li><a href="global.html#parseSearchResults">parseSearchResults</a></li><li><a href="global.html#parseSearchTotalPages">parseSearchTotalPages</a></li><li><a href="global.html#parseZhuantiArticles">parseZhuantiArticles</a></li><li><a href="global.html#pickPint">pickPint</a></li><li><a href="global.html#rocToWestern">rocToWestern</a></li><li><a href="global.html#safeFilename">safeFilename</a></li><li><a href="global.html#toDatetimeUTC8">toDatetimeUTC8</a></li><li><a href="global.html#toRocDate">toRocDate</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">fetchTpexStock.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import isestr from 'wsemi/src/isestr.mjs'
50
+ import isearr from 'wsemi/src/isearr.mjs'
51
+ import fetchWithRetry from './fetchWithRetry.mjs'
52
+ import getOptFetch from './getOptFetch.mjs'
53
+ import isYmd from './isYmd.mjs'
54
+ import toRocDate from './toRocDate.mjs'
55
+
56
+
57
+ //櫃買中心網站根網址
58
+ let BASE_URL = 'https://www.tpex.org.tw'
59
+
60
+
61
+ //本函數之預設抓取設定
62
+ let DFLT = {
63
+ timeout: 15000,
64
+ maxRetries: 10,
65
+ baseDelayMs: 5000,
66
+ maxDelayMs: 30000,
67
+ label: 'fetch-tpex-stock',
68
+ }
69
+
70
+
71
+ //下游依固定欄序讀取(index 0代號、2收盤、4-6開高低、7成交股數), 故驗證欄數下限
72
+ //門檻8遠低於實際約17欄, 不會誤擋正常資料
73
+ let MIN_COLUMNS = 8
74
+
75
+
76
+ /**
77
+ * 抓取櫃買中心(TPEX)上櫃股價
78
+ *
79
+ * 抓取指定日之上櫃股票行情, 可指定股票代號陣列過濾;
80
+ * 選表優先以標題含「行情」比對, 避免未來新增其他資料表時誤抓, 並驗證欄數以避免API改版後靜默讀錯欄
81
+ *
82
+ * @param {String} dateStr 輸入日期YYYYMMDD字串
83
+ * @param {Array} [stockCodes] 輸入股票代號字串陣列,例如['6499','6610'],省略或空陣列表示全市場
84
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
85
+ * @param {String} [opt.baseUrl='https://www.tpex.org.tw'] 輸入櫃買中心根網址字串,供測試或改指向鏡像時覆寫
86
+ * @param {Integer} [opt.timeout=15000] 輸入單次請求逾時毫秒整數,預設15000
87
+ * @param {Integer} [opt.maxRetries=10] 輸入最大重試次數整數,含初始共執行maxRetries+1次,預設10
88
+ * @param {Integer} [opt.baseDelayMs=5000] 輸入重試之線性退避基礎毫秒整數,預設5000
89
+ * @param {Integer} [opt.maxDelayMs=30000] 輸入重試之線性退避上限毫秒整數,預設30000
90
+ * @param {Boolean} [opt.showLog=true] 輸入是否顯示過程訊息布林值,預設true
91
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件{source,date,count,data},日期無效、無資料或指定個股查無資料時reject回傳錯誤物件
92
+ * @example
93
+ *
94
+ * import fetchTpexStock from './src/fetchTpexStock.mjs'
95
+ *
96
+ * let test = async () => {
97
+ *
98
+ * let r = await fetchTpexStock('20260807', ['6488'])
99
+ * console.log(r.source, r.count)
100
+ * // => 'tpex' 1
101
+ *
102
+ * }
103
+ * await test()
104
+ * .catch((err) => {
105
+ * console.log(err)
106
+ * })
107
+ *
108
+ */
109
+ async function fetchTpexStock(dateStr, stockCodes, opt = {}) {
110
+
111
+ //check, 函數入口驗日期, 令程式化呼叫不繞過CLI之檢核
112
+ if (!isYmd(dateStr)) {
113
+ throw new Error(`dateStr 須為合法之 YYYYMMDD 字串,得到: ${dateStr}`)
114
+ }
115
+
116
+ //baseUrl
117
+ let baseUrl = get(opt, 'baseUrl')
118
+ if (!isestr(baseUrl)) {
119
+ baseUrl = BASE_URL
120
+ }
121
+ baseUrl = baseUrl.replace(/\/+$/, '')
122
+
123
+ //optFetch
124
+ let optFetch = getOptFetch(opt, DFLT)
125
+ let showLog = optFetch.showLog
126
+
127
+ //targetCodes
128
+ let targetCodes = isearr(stockCodes) ? stockCodes : []
129
+
130
+ //url
131
+ let rocDate = toRocDate(dateStr)
132
+ let url = `${baseUrl}/web/stock/aftertrading/daily_close_quotes/stk_quote_result.php?l=zh-tw&amp;d=${rocDate}&amp;s=0,asc,0&amp;o=json`
133
+
134
+ if (showLog) {
135
+ console.log(`Fetching TPEX data: ${dateStr} (${rocDate})`)
136
+ console.log(`Target: ${targetCodes.length > 0 ? targetCodes.join(', ') : 'All Market'}`)
137
+ console.log(`URL: ${url}`)
138
+ }
139
+
140
+ //data
141
+ let data = await fetchWithRetry(url, optFetch)
142
+
143
+ //TPEX API格式(2026年後): { stat, tables: [{ title: '上櫃股票行情', data: [...] }] }
144
+ //優先以title「行情」字樣比對, 避免未來新增其他資料表時誤抓
145
+ let tables = Array.isArray(data.tables) ? data.tables : []
146
+ let targetTable = tables.find((t) => get(t, 'title', '').includes('行情') &amp;&amp; get(t, 'data.length', 0) > 0) ||
147
+ tables.find((t) => get(t, 'data.length', 0) > 0)
148
+ let rows = get(targetTable, 'data')
149
+
150
+ //check
151
+ if (!isearr(rows)) {
152
+ throw new Error('TPEX API returned no data. Possibly a holiday or data not yet available.')
153
+ }
154
+
155
+ //shape sanity, 驗證選中表欄數符合預期, 避免API改版後靜默讀錯欄
156
+ if (!Array.isArray(rows[0]) || rows[0].length &lt; MIN_COLUMNS) {
157
+ throw new Error(`TPEX 行情資料欄數不符預期(首列 ${Array.isArray(rows[0]) ? rows[0].length : 'N/A'} 欄,應 >= ${MIN_COLUMNS}),可能 API 格式變更`)
158
+ }
159
+
160
+ //resultData
161
+ let resultData = rows
162
+ if (targetCodes.length > 0) {
163
+ resultData = resultData.filter((row) => targetCodes.includes(row[0]))
164
+ if (resultData.length === 0) {
165
+ //整體有資料但過濾後為空 → 個股不在上櫃市場, 與整體無資料之錯誤訊息明確區分
166
+ throw new Error(`指定個股 ${targetCodes.join(',')} 在 ${dateStr} 之上櫃資料中查無資料(可能為上市股、代碼有誤、或當日無交易)`)
167
+ }
168
+ }
169
+
170
+ return { source: 'tpex', date: dateStr, count: resultData.length, data: resultData }
171
+ }
172
+
173
+
174
+ export default fetchTpexStock
175
+ </code></pre>
176
+ </article>
177
+ </section>
178
+
179
+
180
+
181
+
182
+
183
+
184
+ </div>
185
+
186
+ <br class="clear">
187
+
188
+ <footer>
189
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 07 2026 12:04:51 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
190
+ </footer>
191
+
192
+ <script>prettyPrint();</script>
193
+ <script src="scripts/polyfill.js"></script>
194
+ <script src="scripts/linenumber.js"></script>
195
+
196
+
197
+
198
+ </body>
199
+ </html>
@@ -0,0 +1,236 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>fetchTwDataHoliday.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#KNOWN_TOPICS">KNOWN_TOPICS</a></li><li><a href="global.html#WDwdataHub">WDwdataHub</a></li><li><a href="global.html#assertTpexFieldShape">assertTpexFieldShape</a></li><li><a href="global.html#decodeEntities">decodeEntities</a></li><li><a href="global.html#extractAuthorFromContent">extractAuthorFromContent</a></li><li><a href="global.html#extractDivContent">extractDivContent</a></li><li><a href="global.html#extractHtmlTitle">extractHtmlTitle</a></li><li><a href="global.html#extractPubTime">extractPubTime</a></li><li><a href="global.html#extractTitle">extractTitle</a></li><li><a href="global.html#fetchAiNewsAggregator">fetchAiNewsAggregator</a></li><li><a href="global.html#fetchAisixiang">fetchAisixiang</a></li><li><a href="global.html#fetchAllListPages">fetchAllListPages</a></li><li><a href="global.html#fetchAllSearchPages">fetchAllSearchPages</a></li><li><a href="global.html#fetchArticle">fetchArticle</a></li><li><a href="global.html#fetchAuthorArticles">fetchAuthorArticles</a></li><li><a href="global.html#fetchAuthorsList">fetchAuthorsList</a></li><li><a href="global.html#fetchCnyes">fetchCnyes</a></li><li><a href="global.html#fetchFuturesData">fetchFuturesData</a></li><li><a href="global.html#fetchGuancha">fetchGuancha</a></li><li><a href="global.html#fetchHackerNews">fetchHackerNews</a></li><li><a href="global.html#fetchHtml">fetchHtml</a></li><li><a href="global.html#fetchInstitutionalData">fetchInstitutionalData</a></li><li><a href="global.html#fetchKeywordArticles">fetchKeywordArticles</a></li><li><a href="global.html#fetchMoneydj">fetchMoneydj</a></li><li><a href="global.html#fetchMops">fetchMops</a></li><li><a href="global.html#fetchOnce">fetchOnce</a></li><li><a href="global.html#fetchPCRatio">fetchPCRatio</a></li><li><a href="global.html#fetchRSS">fetchRSS</a></li><li><a href="global.html#fetchStatementdog">fetchStatementdog</a></li><li><a href="global.html#fetchTaifex">fetchTaifex</a></li><li><a href="global.html#fetchTargetWithRetry">fetchTargetWithRetry</a></li><li><a href="global.html#fetchTitleArticles">fetchTitleArticles</a></li><li><a href="global.html#fetchTopicArticles">fetchTopicArticles</a></li><li><a href="global.html#fetchTopicsList">fetchTopicsList</a></li><li><a href="global.html#fetchTpex3insti">fetchTpex3insti</a></li><li><a href="global.html#fetchTpexMargin">fetchTpexMargin</a></li><li><a href="global.html#fetchTpexStock">fetchTpexStock</a></li><li><a href="global.html#fetchTwDataHoliday">fetchTwDataHoliday</a></li><li><a href="global.html#fetchTwseMargin">fetchTwseMargin</a></li><li><a href="global.html#fetchTwseStock">fetchTwseStock</a></li><li><a href="global.html#fetchTwseT86">fetchTwseT86</a></li><li><a href="global.html#fetchWithRetry">fetchWithRetry</a></li><li><a href="global.html#getCfg">getCfg</a></li><li><a href="global.html#getOptFetch">getOptFetch</a></li><li><a href="global.html#getPayload">getPayload</a></li><li><a href="global.html#htmlToMarkdown">htmlToMarkdown</a></li><li><a href="global.html#isHomepageHtml">isHomepageHtml</a></li><li><a href="global.html#isNonHolidayEntry">isNonHolidayEntry</a></li><li><a href="global.html#isRetryableError">isRetryableError</a></li><li><a href="global.html#isYmd">isYmd</a></li><li><a href="global.html#lookupAuthor">lookupAuthor</a></li><li><a href="global.html#lookupTopic">lookupTopic</a></li><li><a href="global.html#parseAuthorColumnPage">parseAuthorColumnPage</a></li><li><a href="global.html#parseAuthorIndex">parseAuthorIndex</a></li><li><a href="global.html#parseCSV">parseCSV</a></li><li><a href="global.html#parseCSVLine">parseCSVLine</a></li><li><a href="global.html#parseIntComma">parseIntComma</a></li><li><a href="global.html#parseListPage">parseListPage</a></li><li><a href="global.html#parseNum">parseNum</a></li><li><a href="global.html#parseSearchResults">parseSearchResults</a></li><li><a href="global.html#parseSearchTotalPages">parseSearchTotalPages</a></li><li><a href="global.html#parseZhuantiArticles">parseZhuantiArticles</a></li><li><a href="global.html#pickPint">pickPint</a></li><li><a href="global.html#rocToWestern">rocToWestern</a></li><li><a href="global.html#safeFilename">safeFilename</a></li><li><a href="global.html#toDatetimeUTC8">toDatetimeUTC8</a></li><li><a href="global.html#toRocDate">toRocDate</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">fetchTwDataHoliday.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import isestr from 'wsemi/src/isestr.mjs'
50
+ import isearr from 'wsemi/src/isearr.mjs'
51
+ import fetchWithRetry from './fetchWithRetry.mjs'
52
+ import getOptFetch from './getOptFetch.mjs'
53
+
54
+
55
+ //證交所開放資料平台之休市日期API
56
+ let API_URL = 'https://openapi.twse.com.tw/v1/holidaySchedule/holidaySchedule'
57
+
58
+
59
+ //本函數之預設抓取設定
60
+ let DFLT = {
61
+ responseType: 'text',
62
+ timeout: 30000,
63
+ maxRetries: 10,
64
+ baseDelayMs: 5000,
65
+ maxDelayMs: 30000,
66
+ label: 'fetch-tw-data-holiday',
67
+ }
68
+
69
+
70
+ /**
71
+ * 民國日期YYYMMDD轉西元日期YYYYMMDD
72
+ *
73
+ * @param {String} rocDate 輸入民國日期字串,年份位數不限
74
+ * @returns {String} 回傳西元日期YYYYMMDD字串
75
+ */
76
+ function rocToWestern(rocDate) {
77
+
78
+ let rocYear = parseInt(rocDate.substring(0, rocDate.length - 4), 10)
79
+ let mmdd = rocDate.substring(rocDate.length - 4)
80
+
81
+ return `${rocYear + 1911}${mmdd}`
82
+ }
83
+
84
+
85
+ /**
86
+ * 判斷條目是否為非假日條目
87
+ *
88
+ * 應排除者有二類:
89
+ * 交易日標記(「國曆新年開始交易日」「農曆春節前最後交易日」「農曆春節後開始交易日」);
90
+ * 結算作業日(「市場無交易,僅辦理結算交割作業」)
91
+ *
92
+ * @param {Object} entry 輸入API回傳之單筆條目物件
93
+ * @returns {Boolean} 回傳是否為非假日條目之布林值
94
+ */
95
+ function isNonHolidayEntry(entry) {
96
+
97
+ let name = String(get(entry, 'Name', ''))
98
+ let description = String(get(entry, 'Description', ''))
99
+
100
+ return /交易日/.test(name) ||
101
+ /市場無交易/.test(name) ||
102
+ /開始交易/.test(description) ||
103
+ /最後交易/.test(description)
104
+ }
105
+
106
+
107
+ /**
108
+ * 抓取台灣證券市場休市日期
109
+ *
110
+ * 由證交所開放資料平台取得當年度休市日期清單, 排除交易日標記與結算作業日, 並依日期去重與排序;
111
+ * 給定checkDate時另回傳該日是否為休市日
112
+ *
113
+ * @param {String} [checkDate] 輸入待查詢日期YYYYMMDD字串,非合法YYYYMMDD時略過單日比對
114
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
115
+ * @param {String} [opt.url='https://openapi.twse.com.tw/v1/holidaySchedule/holidaySchedule'] 輸入API網址字串,供測試或改指向鏡像時覆寫
116
+ * @param {Integer} [opt.timeout=30000] 輸入單次請求逾時毫秒整數,預設30000
117
+ * @param {Integer} [opt.maxRetries=10] 輸入最大重試次數整數,含初始共執行maxRetries+1次,預設10
118
+ * @param {Integer} [opt.baseDelayMs=5000] 輸入重試之線性退避基礎毫秒整數,預設5000
119
+ * @param {Integer} [opt.maxDelayMs=30000] 輸入重試之線性退避上限毫秒整數,預設30000
120
+ * @param {Boolean} [opt.showLog=true] 輸入是否顯示重試訊息布林值,預設true
121
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件{dataYear,totalHolidays,holidays},有給checkDate時另含{checkDate,isHoliday,holidayName},API回傳非JSON或空陣列時reject回傳錯誤物件
122
+ * @example
123
+ *
124
+ * import fetchTwDataHoliday from './src/fetchTwDataHoliday.mjs'
125
+ *
126
+ * let test = async () => {
127
+ *
128
+ * let r = await fetchTwDataHoliday('20260101')
129
+ * console.log(r.dataYear, r.totalHolidays, r.isHoliday, r.holidayName)
130
+ * // => '2026' 15 true '中華民國開國紀念日'
131
+ *
132
+ * }
133
+ * await test()
134
+ * .catch((err) => {
135
+ * console.log(err)
136
+ * })
137
+ *
138
+ */
139
+ async function fetchTwDataHoliday(checkDate, opt = {}) {
140
+
141
+ //url
142
+ let url = get(opt, 'url')
143
+ if (!isestr(url)) {
144
+ url = API_URL
145
+ }
146
+
147
+ //optFetch
148
+ let optFetch = getOptFetch(opt, DFLT)
149
+
150
+ //body
151
+ let body = await fetchWithRetry(url, optFetch)
152
+
153
+ //raw, API維護時可能回HTTP 200加HTML(非JSON), 產生文件記載之友善訊息而非裸SyntaxError
154
+ let raw = null
155
+ try {
156
+ raw = JSON.parse(body)
157
+ }
158
+ catch (err) {
159
+ throw new Error('API 回傳非 JSON 格式(可能為維護頁面或網路中介),請稍後再試')
160
+ }
161
+ if (!isearr(raw)) {
162
+ throw new Error('API 回傳空陣列或格式異常')
163
+ }
164
+
165
+ //holidays, 轉為結構化假日清單(排除交易日標記與結算作業日)
166
+ let holidays = raw
167
+ .filter((entry) => !isNonHolidayEntry(entry))
168
+ .map((entry) => {
169
+ return {
170
+ date: rocToWestern(get(entry, 'Date', '')),
171
+ rocDate: get(entry, 'Date', ''),
172
+ name: get(entry, 'Name', ''),
173
+ weekday: get(entry, 'Weekday', ''),
174
+ description: String(get(entry, 'Description', '') || '').replace(/&lt;br\s*\/?>/gi, '').trim(),
175
+ }
176
+ })
177
+
178
+ //uniqueHolidays, 去重(同一天可能有多筆同名條目)
179
+ let seen = new Set()
180
+ let uniqueHolidays = holidays.filter((h) => {
181
+ if (seen.has(h.date)) {
182
+ return false
183
+ }
184
+ seen.add(h.date)
185
+ return true
186
+ })
187
+ uniqueHolidays.sort((a, b) => a.date.localeCompare(b.date))
188
+
189
+ //dataYear
190
+ let dataYear = uniqueHolidays.length > 0 ? uniqueHolidays[0].date.substring(0, 4) : null
191
+
192
+ //result
193
+ let result = {
194
+ dataYear,
195
+ totalHolidays: uniqueHolidays.length,
196
+ holidays: uniqueHolidays,
197
+ }
198
+
199
+ //checkDate為opt, 僅當為合法YYYYMMDD字串才做單日假日比對, 無效則略過(不誤標isHoliday)
200
+ if (isestr(checkDate) &amp;&amp; /^\d{8}$/.test(checkDate)) {
201
+ result.checkDate = checkDate
202
+ let match = uniqueHolidays.find((h) => h.date === checkDate)
203
+ result.isHoliday = !!match
204
+ result.holidayName = match ? match.name : null
205
+ }
206
+
207
+ return result
208
+ }
209
+
210
+
211
+ export default fetchTwDataHoliday
212
+ </code></pre>
213
+ </article>
214
+ </section>
215
+
216
+
217
+
218
+
219
+
220
+
221
+ </div>
222
+
223
+ <br class="clear">
224
+
225
+ <footer>
226
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 07 2026 12:04:51 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
227
+ </footer>
228
+
229
+ <script>prettyPrint();</script>
230
+ <script src="scripts/polyfill.js"></script>
231
+ <script src="scripts/linenumber.js"></script>
232
+
233
+
234
+
235
+ </body>
236
+ </html>