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.
- package/.editorconfig +9 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +55 -0
- package/.jsdoc +25 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/SECURITY.md +5 -0
- package/babel.config.js +16 -0
- package/dist/w-dwdata-hub.umd.js +7 -0
- package/dist/w-dwdata-hub.umd.js.map +1 -0
- package/docs/WDwdataHub.mjs.html +136 -0
- package/docs/decodeEntities.mjs.html +129 -0
- package/docs/extractDivContent.mjs.html +147 -0
- package/docs/fetchAiNewsAggregator.mjs.html +155 -0
- package/docs/fetchAisixiang.mjs.html +1038 -0
- package/docs/fetchCnyes.mjs.html +260 -0
- package/docs/fetchGuancha.mjs.html +1007 -0
- package/docs/fetchHackerNews.mjs.html +189 -0
- package/docs/fetchMoneydj.mjs.html +251 -0
- package/docs/fetchMops.mjs.html +385 -0
- package/docs/fetchRSS.mjs.html +160 -0
- package/docs/fetchStatementdog.mjs.html +213 -0
- package/docs/fetchTaifex.mjs.html +513 -0
- package/docs/fetchTpex3insti.mjs.html +278 -0
- package/docs/fetchTpexMargin.mjs.html +248 -0
- package/docs/fetchTpexStock.mjs.html +199 -0
- package/docs/fetchTwDataHoliday.mjs.html +236 -0
- package/docs/fetchTwseMargin.mjs.html +253 -0
- package/docs/fetchTwseStock.mjs.html +188 -0
- package/docs/fetchTwseT86.mjs.html +208 -0
- package/docs/fetchWithRetry.mjs.html +376 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/getOptFetch.mjs.html +185 -0
- package/docs/global.html +22649 -0
- package/docs/htmlToMarkdown.mjs.html +168 -0
- package/docs/index.html +84 -0
- package/docs/isYmd.mjs.html +114 -0
- package/docs/parseIntComma.mjs.html +100 -0
- package/docs/safeFilename.mjs.html +102 -0
- package/docs/scripts/collapse.js +39 -0
- package/docs/scripts/commonNav.js +28 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +99 -0
- package/docs/styles/jsdoc.css +776 -0
- package/docs/styles/prettify.css +80 -0
- package/docs/toDatetimeUTC8.mjs.html +143 -0
- package/docs/toRocDate.mjs.html +107 -0
- package/g.mjs +73 -0
- package/package.json +31 -0
- package/script.txt +17 -0
- package/src/WDwdataHub.mjs +64 -0
- package/src/decodeEntities.mjs +57 -0
- package/src/extractDivContent.mjs +75 -0
- package/src/fetchAiNewsAggregator.mjs +83 -0
- package/src/fetchAisixiang.mjs +966 -0
- package/src/fetchCnyes.mjs +188 -0
- package/src/fetchGuancha.mjs +935 -0
- package/src/fetchHackerNews.mjs +117 -0
- package/src/fetchMoneydj.mjs +179 -0
- package/src/fetchMops.mjs +313 -0
- package/src/fetchRSS.mjs +88 -0
- package/src/fetchStatementdog.mjs +141 -0
- package/src/fetchTaifex.mjs +441 -0
- package/src/fetchTpex3insti.mjs +206 -0
- package/src/fetchTpexMargin.mjs +176 -0
- package/src/fetchTpexStock.mjs +127 -0
- package/src/fetchTwDataHoliday.mjs +164 -0
- package/src/fetchTwseMargin.mjs +181 -0
- package/src/fetchTwseStock.mjs +116 -0
- package/src/fetchTwseT86.mjs +136 -0
- package/src/fetchWithRetry.mjs +304 -0
- package/src/getOptFetch.mjs +113 -0
- package/src/htmlToMarkdown.mjs +96 -0
- package/src/isYmd.mjs +42 -0
- package/src/parseIntComma.mjs +28 -0
- package/src/safeFilename.mjs +30 -0
- package/src/toDatetimeUTC8.mjs +71 -0
- package/src/toRocDate.mjs +35 -0
- package/test/WDwdataHub.test.mjs +63 -0
- package/test/decodeEntities.test.mjs +42 -0
- package/test/extractDivContent.test.mjs +48 -0
- package/test/fetchAiNewsAggregator.test.mjs +79 -0
- package/test/fetchAisixiang.test.mjs +225 -0
- package/test/fetchCnyes.test.mjs +61 -0
- package/test/fetchGuancha.test.mjs +190 -0
- package/test/fetchHackerNews.test.mjs +78 -0
- package/test/fetchMoneydj.test.mjs +82 -0
- package/test/fetchMops.test.mjs +106 -0
- package/test/fetchRSS.test.mjs +85 -0
- package/test/fetchStatementdog.test.mjs +69 -0
- package/test/fetchTaifex.test.mjs +99 -0
- package/test/fetchTpex3insti.test.mjs +90 -0
- package/test/fetchTpexMargin.test.mjs +94 -0
- package/test/fetchTpexStock.test.mjs +68 -0
- package/test/fetchTwDataHoliday.test.mjs +100 -0
- package/test/fetchTwseMargin.test.mjs +81 -0
- package/test/fetchTwseStock.test.mjs +80 -0
- package/test/fetchTwseT86.test.mjs +76 -0
- package/test/fetchWithRetry.test.mjs +174 -0
- package/test/htmlToMarkdown.test.mjs +71 -0
- package/test/isYmd.test.mjs +56 -0
- package/test/parseIntComma.test.mjs +38 -0
- package/test/safeFilename.test.mjs +35 -0
- package/test/toDatetimeUTC8.test.mjs +65 -0
- package/test/toRocDate.test.mjs +38 -0
- package/test/tools/encodeBig5.mjs +80 -0
- package/test/tools/fixtures.mjs +539 -0
- package/test/tools/hasChrome.mjs +39 -0
- package/test/tools/serverForTest.mjs +397 -0
- package/toolg/addVersion.mjs +4 -0
- package/toolg/cleanFolder.mjs +5 -0
- package/toolg/gDistRollup.mjs +49 -0
- package/toolg/modifyReadme.mjs +4 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>fetchMops.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">fetchMops.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>import { chromium } from 'playwright'
|
|
49
|
+
import get from 'lodash-es/get.js'
|
|
50
|
+
import isbol from 'wsemi/src/isbol.mjs'
|
|
51
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
52
|
+
import isp0int from 'wsemi/src/isp0int.mjs'
|
|
53
|
+
import cint from 'wsemi/src/cint.mjs'
|
|
54
|
+
import delay from 'wsemi/src/delay.mjs'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
//公開資訊觀測站重大訊息頁與其查詢API
|
|
58
|
+
let PAGE_URL = 'https://mops.twse.com.tw/mops/#/web/t146sb10'
|
|
59
|
+
let API_URL = 'https://mops.twse.com.tw/mops/api/t146sb10'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
//預設值
|
|
63
|
+
let DEFAULT_MAX_RETRIES = 10
|
|
64
|
+
let DEFAULT_BASE_DELAY_MS = 5000
|
|
65
|
+
let DEFAULT_MAX_DELAY_MS = 30000
|
|
66
|
+
let LAUNCH_TIMEOUT_MS = 360000
|
|
67
|
+
let GOTO_TIMEOUT_MS = 60000
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
//目標市場別, marketKind為API查詢參數
|
|
71
|
+
let MARKET_KINDS = [
|
|
72
|
+
{ name: '上市', marketKind: 'sii' },
|
|
73
|
+
{ name: '上櫃', marketKind: 'otc' },
|
|
74
|
+
{ name: '興櫃', marketKind: 'rotc' },
|
|
75
|
+
{ name: '公開發行', marketKind: 'pub' },
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 組出單一市場別之API查詢payload
|
|
81
|
+
*
|
|
82
|
+
* @param {String} marketKind 輸入市場別字串,可為'sii'、'otc'、'rotc'、'pub'
|
|
83
|
+
* @returns {Object} 回傳API查詢payload物件
|
|
84
|
+
*/
|
|
85
|
+
function getPayload(marketKind) {
|
|
86
|
+
return {
|
|
87
|
+
scopeType: '2',
|
|
88
|
+
companyId: '',
|
|
89
|
+
dateType: '2',
|
|
90
|
+
firstDate: '',
|
|
91
|
+
lastDate: '',
|
|
92
|
+
marketKind,
|
|
93
|
+
announcementBasis: '0',
|
|
94
|
+
dateRangeType: '1',
|
|
95
|
+
announcementType: '1',
|
|
96
|
+
sort: '1',
|
|
97
|
+
encodeURIComponent: 1,
|
|
98
|
+
step: 1,
|
|
99
|
+
firstin: 1,
|
|
100
|
+
off: 1,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 於瀏覽器頁面內以fetch呼叫MOPS查詢API並自動重試
|
|
107
|
+
*
|
|
108
|
+
* 需於MOPS頁面context內發送請求(同源且帶站方cookie), 故採page.evaluate而非node端直接請求;
|
|
109
|
+
* 重試條件為HTTP 5xx、403、429或網路錯誤, application-level錯誤(如MOPS code 500)屬非暫時性故不重試
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} page 輸入playwright之page物件
|
|
112
|
+
* @param {Object} target 輸入目標物件,內含name、marketKind、url與payload
|
|
113
|
+
* @param {Object} cfg 輸入已正規化之設定物件
|
|
114
|
+
* @returns {Promise} 回傳Promise,resolve回傳{json,raw}或{error,retryable}
|
|
115
|
+
*/
|
|
116
|
+
async function fetchTargetWithRetry(page, target, cfg) {
|
|
117
|
+
|
|
118
|
+
let data = null
|
|
119
|
+
for (let attempt = 1; attempt <= cfg.maxRetries + 1; attempt++) {
|
|
120
|
+
|
|
121
|
+
data = await page.evaluate(async (t) => {
|
|
122
|
+
try {
|
|
123
|
+
|
|
124
|
+
let response = await fetch(t.url, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify(t.payload),
|
|
128
|
+
})
|
|
129
|
+
if (response.status >= 500 || response.status === 403 || response.status === 429) {
|
|
130
|
+
return { error: `HTTP ${response.status}`, retryable: true }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let text = await response.text()
|
|
134
|
+
try {
|
|
135
|
+
|
|
136
|
+
let json = JSON.parse(text)
|
|
137
|
+
|
|
138
|
+
//驗證MOPS application-level狀態碼(HTTP 200不代表查詢成功)
|
|
139
|
+
//實測契約: code=200「查詢成功」、code=406「查無相符資料」(正常空結果)、其餘(如500)為application-level錯誤
|
|
140
|
+
let code = json && json.code
|
|
141
|
+
if (code !== 200 && code !== 406) {
|
|
142
|
+
//非成功且非「查無資料」→ application-level錯誤, 帶原始回應供除錯, 屬非暫時性故不重試
|
|
143
|
+
return { error: `MOPS code ${code}: ${(json && json.message) || ''}`, json, raw: text.substring(0, 500), retryable: false }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { json, raw: text.substring(0, 500) }
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
return { error: 'Parse Error', raw: text, retryable: false }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return { error: err.toString(), retryable: true }
|
|
155
|
+
}
|
|
156
|
+
}, target)
|
|
157
|
+
|
|
158
|
+
if (!data.error) {
|
|
159
|
+
return data
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let retryable = data.retryable !== false
|
|
163
|
+
let attemptsLeft = cfg.maxRetries + 1 - attempt
|
|
164
|
+
if (!retryable || attemptsLeft <= 0) {
|
|
165
|
+
return data //回傳錯誤結果, 由呼叫方處理
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let ms = Math.min(cfg.baseDelayMs * attempt, cfg.maxDelayMs)
|
|
169
|
+
if (cfg.showLog) {
|
|
170
|
+
console.warn(`[${target.name}][Retry ${attempt}/${cfg.maxRetries}] ${data.error} — 等待 ${ms / 1000}s 後重試...`)
|
|
171
|
+
}
|
|
172
|
+
await delay(ms)
|
|
173
|
+
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return data
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 抓取公開資訊觀測站(MOPS)今日重大訊息
|
|
182
|
+
*
|
|
183
|
+
* 以playwright啟動本機Chrome(channel為'chrome', 可由環境變數CHROME_PATH指定執行檔)開啟MOPS重大訊息頁,
|
|
184
|
+
* 再於頁面context內逐一查詢上市、上櫃、興櫃與公開發行之今日重大訊息;
|
|
185
|
+
* 單一市場別失敗時記錄於該筆之error並續抓其餘市場別, 由hasError告知是否有失敗
|
|
186
|
+
*
|
|
187
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
188
|
+
* @param {String} [opt.pageUrl='https://mops.twse.com.tw/mops/#/web/t146sb10'] 輸入MOPS重大訊息頁網址字串,供測試或改指向鏡像時覆寫
|
|
189
|
+
* @param {String} [opt.apiUrl='https://mops.twse.com.tw/mops/api/t146sb10'] 輸入MOPS查詢API網址字串,供測試或改指向鏡像時覆寫
|
|
190
|
+
* @param {Integer} [opt.maxRetries=10] 輸入最大重試次數整數,含初始共執行maxRetries+1次,預設10
|
|
191
|
+
* @param {Boolean} [opt.showLog=true] 輸入是否顯示過程訊息布林值,預設true
|
|
192
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件{results,hasError},results各筆為{market,marketKind,data,error,timestamp},瀏覽器啟動失敗時reject回傳錯誤物件
|
|
193
|
+
* @example
|
|
194
|
+
*
|
|
195
|
+
* import fetchMops from './src/fetchMops.mjs'
|
|
196
|
+
*
|
|
197
|
+
* let test = async () => {
|
|
198
|
+
*
|
|
199
|
+
* let r = await fetchMops()
|
|
200
|
+
* console.log(r.hasError, r.results.length, r.results[0].market)
|
|
201
|
+
* // => false 4 '上市'
|
|
202
|
+
*
|
|
203
|
+
* }
|
|
204
|
+
* await test()
|
|
205
|
+
* .catch((err) => {
|
|
206
|
+
* console.log(err)
|
|
207
|
+
* })
|
|
208
|
+
*
|
|
209
|
+
*/
|
|
210
|
+
async function fetchMops(opt = {}) {
|
|
211
|
+
|
|
212
|
+
//pageUrl
|
|
213
|
+
let pageUrl = get(opt, 'pageUrl')
|
|
214
|
+
if (!isestr(pageUrl)) {
|
|
215
|
+
pageUrl = PAGE_URL
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
//apiUrl
|
|
219
|
+
let apiUrl = get(opt, 'apiUrl')
|
|
220
|
+
if (!isestr(apiUrl)) {
|
|
221
|
+
apiUrl = API_URL
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
//maxRetries
|
|
225
|
+
let maxRetries = get(opt, 'maxRetries')
|
|
226
|
+
if (!isp0int(maxRetries)) {
|
|
227
|
+
maxRetries = DEFAULT_MAX_RETRIES
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
maxRetries = cint(maxRetries)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
//showLog
|
|
234
|
+
let showLog = get(opt, 'showLog')
|
|
235
|
+
if (!isbol(showLog)) {
|
|
236
|
+
showLog = true
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
//cfg
|
|
240
|
+
let cfg = {
|
|
241
|
+
maxRetries,
|
|
242
|
+
baseDelayMs: DEFAULT_BASE_DELAY_MS,
|
|
243
|
+
maxDelayMs: DEFAULT_MAX_DELAY_MS,
|
|
244
|
+
showLog,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
//targets
|
|
248
|
+
let targets = MARKET_KINDS.map((v) => {
|
|
249
|
+
return {
|
|
250
|
+
name: v.name,
|
|
251
|
+
marketKind: v.marketKind,
|
|
252
|
+
url: apiUrl,
|
|
253
|
+
payload: getPayload(v.marketKind),
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
if (showLog) {
|
|
258
|
+
console.log('啟動瀏覽器...')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
//browser, 帶重試之瀏覽器啟動
|
|
262
|
+
let browser = null
|
|
263
|
+
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
264
|
+
try {
|
|
265
|
+
browser = await chromium.launch({
|
|
266
|
+
headless: true,
|
|
267
|
+
//設定環境變數CHROME_PATH則用該路徑啟動瀏覽器
|
|
268
|
+
//未設定時為undefined, playwright會忽略executablePath改走channel為'chrome', 行為不變
|
|
269
|
+
executablePath: process.env.CHROME_PATH || undefined,
|
|
270
|
+
channel: 'chrome',
|
|
271
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
272
|
+
timeout: LAUNCH_TIMEOUT_MS,
|
|
273
|
+
})
|
|
274
|
+
break
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
let attemptsLeft = maxRetries + 1 - attempt
|
|
278
|
+
if (attemptsLeft <= 0) {
|
|
279
|
+
throw new Error(`瀏覽器啟動失敗(已重試 ${maxRetries} 次): ${err.message}`)
|
|
280
|
+
}
|
|
281
|
+
let ms = Math.min(DEFAULT_BASE_DELAY_MS * attempt, DEFAULT_MAX_DELAY_MS)
|
|
282
|
+
if (showLog) {
|
|
283
|
+
console.warn(`[browser.launch][Retry ${attempt}/${maxRetries}] ${err.message} — 等待 ${ms / 1000}s 後重試...`)
|
|
284
|
+
}
|
|
285
|
+
await delay(ms)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
|
|
291
|
+
let page = await browser.newPage()
|
|
292
|
+
|
|
293
|
+
//帶重試之頁面導航
|
|
294
|
+
if (showLog) {
|
|
295
|
+
console.log('前往 MOPS 重大訊息頁面 (t146sb10)...')
|
|
296
|
+
}
|
|
297
|
+
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
298
|
+
try {
|
|
299
|
+
await page.goto(pageUrl, { waitUntil: 'networkidle', timeout: GOTO_TIMEOUT_MS })
|
|
300
|
+
break
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
let attemptsLeft = maxRetries + 1 - attempt
|
|
304
|
+
if (attemptsLeft <= 0) {
|
|
305
|
+
throw err
|
|
306
|
+
}
|
|
307
|
+
let ms = Math.min(DEFAULT_BASE_DELAY_MS * attempt, DEFAULT_MAX_DELAY_MS)
|
|
308
|
+
if (showLog) {
|
|
309
|
+
console.warn(`[page.goto][Retry ${attempt}/${maxRetries}] ${err.message} — 等待 ${ms / 1000}s 後重試...`)
|
|
310
|
+
}
|
|
311
|
+
await delay(ms)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await page.waitForTimeout(2000)
|
|
316
|
+
|
|
317
|
+
//results
|
|
318
|
+
let results = []
|
|
319
|
+
for (let target of targets) {
|
|
320
|
+
|
|
321
|
+
if (showLog) {
|
|
322
|
+
console.log(`正在抓取 [${target.name}] 資料...`)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let data = await fetchTargetWithRetry(page, target, cfg)
|
|
326
|
+
|
|
327
|
+
results.push({
|
|
328
|
+
market: target.name,
|
|
329
|
+
marketKind: target.marketKind,
|
|
330
|
+
data: data.json || data,
|
|
331
|
+
...(data.error && { error: data.error }),
|
|
332
|
+
timestamp: new Date().toISOString(),
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
await page.waitForTimeout(1000)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (showLog) {
|
|
339
|
+
let summary = results.map((r) => {
|
|
340
|
+
let resultData = get(r, 'data.result', get(r, 'data'))
|
|
341
|
+
let count = Array.isArray(resultData) ? resultData.length : 0
|
|
342
|
+
return `${r.market}: 取得 ${count} 筆資料`
|
|
343
|
+
})
|
|
344
|
+
console.log('抓取完成。摘要:', summary)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
//hasError
|
|
348
|
+
let hasError = results.some((r) => r.error)
|
|
349
|
+
|
|
350
|
+
return { results, hasError }
|
|
351
|
+
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
await browser.close()
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
export { MARKET_KINDS, getPayload }
|
|
360
|
+
export default fetchMops
|
|
361
|
+
</code></pre>
|
|
362
|
+
</article>
|
|
363
|
+
</section>
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<br class="clear">
|
|
373
|
+
|
|
374
|
+
<footer>
|
|
375
|
+
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.
|
|
376
|
+
</footer>
|
|
377
|
+
|
|
378
|
+
<script>prettyPrint();</script>
|
|
379
|
+
<script src="scripts/polyfill.js"></script>
|
|
380
|
+
<script src="scripts/linenumber.js"></script>
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
</body>
|
|
385
|
+
</html>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>fetchRSS.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">fetchRSS.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>import Parser from 'rss-parser'
|
|
49
|
+
import get from 'lodash-es/get.js'
|
|
50
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
51
|
+
import isearr from 'wsemi/src/isearr.mjs'
|
|
52
|
+
import fetchWithRetry from './fetchWithRetry.mjs'
|
|
53
|
+
import getOptFetch from './getOptFetch.mjs'
|
|
54
|
+
import toDatetimeUTC8 from './toDatetimeUTC8.mjs'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
//本函數之預設抓取設定, 因YouTube等站台之RSS會回傳暫時性404故將404納入重試範圍
|
|
58
|
+
let DFLT = {
|
|
59
|
+
responseType: 'text',
|
|
60
|
+
timeout: 30000,
|
|
61
|
+
maxRetries: 5,
|
|
62
|
+
baseDelayMs: 3000,
|
|
63
|
+
maxDelayMs: 15000,
|
|
64
|
+
retryStatus: [404],
|
|
65
|
+
label: 'fetch-rss',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 抓取RSS訂閱源
|
|
71
|
+
*
|
|
72
|
+
* 支援RSS與Atom, 並轉為統一格式, 時間欄位一律轉為UTC+8之'YYYY-MM-DD HH:mm:ss'字串;
|
|
73
|
+
* 因YouTube等站台之RSS會回傳暫時性404, 故404亦納入重試範圍
|
|
74
|
+
*
|
|
75
|
+
* @param {String} rssUrl 輸入RSS網址字串,須為http或https
|
|
76
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
77
|
+
* @param {Integer} [opt.timeout=30000] 輸入單次請求逾時毫秒整數,預設30000
|
|
78
|
+
* @param {Integer} [opt.maxRetries=5] 輸入最大重試次數整數,含初始共執行maxRetries+1次,預設5
|
|
79
|
+
* @param {Integer} [opt.baseDelayMs=3000] 輸入重試之線性退避基礎毫秒整數,預設3000
|
|
80
|
+
* @param {Integer} [opt.maxDelayMs=15000] 輸入重試之線性退避上限毫秒整數,預設15000
|
|
81
|
+
* @param {Boolean} [opt.showLog=true] 輸入是否顯示重試訊息布林值,預設true
|
|
82
|
+
* @returns {Promise} 回傳Promise,resolve回傳項目物件陣列,各物件為{url,time,title,description,from},網址無效或抓取失敗時reject回傳錯誤物件
|
|
83
|
+
* @example
|
|
84
|
+
*
|
|
85
|
+
* import fetchRSS from './src/fetchRSS.mjs'
|
|
86
|
+
*
|
|
87
|
+
* let test = async () => {
|
|
88
|
+
*
|
|
89
|
+
* let rs = await fetchRSS('https://www.ptt.cc/atom/Gossiping.xml')
|
|
90
|
+
* console.log(rs.length, rs[0])
|
|
91
|
+
* // => 20 { url: 'https://...', time: '2026-08-07 09:02:03', title: '...', description: '...', from: '...' }
|
|
92
|
+
*
|
|
93
|
+
* }
|
|
94
|
+
* await test()
|
|
95
|
+
* .catch((err) => {
|
|
96
|
+
* console.log(err)
|
|
97
|
+
* })
|
|
98
|
+
*
|
|
99
|
+
*/
|
|
100
|
+
async function fetchRSS(rssUrl, opt = {}) {
|
|
101
|
+
|
|
102
|
+
//check
|
|
103
|
+
if (!isestr(rssUrl) || !/^https?:\/\//i.test(rssUrl)) {
|
|
104
|
+
throw new Error('rssUrl 須為有效的 http/https RSS 網址')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//xml
|
|
108
|
+
let xml = await fetchWithRetry(rssUrl, getOptFetch(opt, DFLT))
|
|
109
|
+
|
|
110
|
+
//feed
|
|
111
|
+
let parser = new Parser()
|
|
112
|
+
let feed = await parser.parseString(xml)
|
|
113
|
+
|
|
114
|
+
//feedFrom
|
|
115
|
+
let feedFrom = String(get(feed, 'title', '') || '').trim()
|
|
116
|
+
|
|
117
|
+
//items
|
|
118
|
+
let items = get(feed, 'items')
|
|
119
|
+
if (!isearr(items)) {
|
|
120
|
+
items = []
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return items.map((item) => {
|
|
124
|
+
return {
|
|
125
|
+
url: String(get(item, 'link', '') || '').trim(),
|
|
126
|
+
time: toDatetimeUTC8(get(item, 'isoDate', '') || get(item, 'pubDate', '')),
|
|
127
|
+
title: String(get(item, 'title', '') || '').trim(),
|
|
128
|
+
description: String(get(item, 'contentSnippet', '') || get(item, 'summary', '') || '').trim(),
|
|
129
|
+
from: String(get(item, 'creator', '') || feedFrom).trim(),
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
export default fetchRSS
|
|
136
|
+
</code></pre>
|
|
137
|
+
</article>
|
|
138
|
+
</section>
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<br class="clear">
|
|
148
|
+
|
|
149
|
+
<footer>
|
|
150
|
+
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.
|
|
151
|
+
</footer>
|
|
152
|
+
|
|
153
|
+
<script>prettyPrint();</script>
|
|
154
|
+
<script src="scripts/polyfill.js"></script>
|
|
155
|
+
<script src="scripts/linenumber.js"></script>
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
</body>
|
|
160
|
+
</html>
|