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,35 @@
|
|
|
1
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 西元YYYYMMDD日期字串轉民國日期字串
|
|
6
|
+
*
|
|
7
|
+
* 民國年為西元年減1911, 月與日保留原始2碼不補零亦不去零, 以符合證交所與櫃買中心API之查詢格式
|
|
8
|
+
*
|
|
9
|
+
* @param {String} dateStr 輸入西元日期YYYYMMDD字串
|
|
10
|
+
* @returns {String} 回傳民國日期字串,格式為'YYY/MM/DD',輸入非有效字串時回傳''
|
|
11
|
+
* @example
|
|
12
|
+
*
|
|
13
|
+
* import toRocDate from './src/toRocDate.mjs'
|
|
14
|
+
*
|
|
15
|
+
* console.log(toRocDate('20260807'), toRocDate('19110101'))
|
|
16
|
+
* // => '115/08/07' '0/01/01'
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
19
|
+
function toRocDate(dateStr) {
|
|
20
|
+
|
|
21
|
+
//check
|
|
22
|
+
if (!isestr(dateStr)) {
|
|
23
|
+
return ''
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//y, m, d
|
|
27
|
+
let y = parseInt(dateStr.substring(0, 4), 10) - 1911
|
|
28
|
+
let m = dateStr.substring(4, 6)
|
|
29
|
+
let d = dateStr.substring(6, 8)
|
|
30
|
+
|
|
31
|
+
return `${y}/${m}/${d}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
export default toRocDate
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import WDwdataHub from '../src/WDwdataHub.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('WDwdataHub', function() {
|
|
6
|
+
|
|
7
|
+
it('匯出全部抓取函數', function() {
|
|
8
|
+
let r = Object.keys(WDwdataHub).sort()
|
|
9
|
+
let rr = [
|
|
10
|
+
'fetchAiNewsAggregator',
|
|
11
|
+
'fetchAisixiang',
|
|
12
|
+
'fetchCnyes',
|
|
13
|
+
'fetchGuancha',
|
|
14
|
+
'fetchHackerNews',
|
|
15
|
+
'fetchMoneydj',
|
|
16
|
+
'fetchMops',
|
|
17
|
+
'fetchRSS',
|
|
18
|
+
'fetchStatementdog',
|
|
19
|
+
'fetchTaifex',
|
|
20
|
+
'fetchTpex3insti',
|
|
21
|
+
'fetchTpexMargin',
|
|
22
|
+
'fetchTpexStock',
|
|
23
|
+
'fetchTwDataHoliday',
|
|
24
|
+
'fetchTwseMargin',
|
|
25
|
+
'fetchTwseStock',
|
|
26
|
+
'fetchTwseT86',
|
|
27
|
+
]
|
|
28
|
+
assert.strict.deepEqual(r, rr)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('單一來源者為函數', function() {
|
|
32
|
+
let ks = [
|
|
33
|
+
'fetchAiNewsAggregator', 'fetchCnyes', 'fetchHackerNews', 'fetchMoneydj', 'fetchMops',
|
|
34
|
+
'fetchRSS', 'fetchStatementdog', 'fetchTaifex', 'fetchTpex3insti', 'fetchTpexMargin',
|
|
35
|
+
'fetchTpexStock', 'fetchTwDataHoliday', 'fetchTwseMargin', 'fetchTwseStock', 'fetchTwseT86',
|
|
36
|
+
]
|
|
37
|
+
let r = ks.map((k) => typeof WDwdataHub[k])
|
|
38
|
+
let rr = ks.map(() => 'function')
|
|
39
|
+
assert.strict.deepEqual(r, rr)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('多函數來源者為物件, 其內各項為函數或常數', function() {
|
|
43
|
+
let r = [
|
|
44
|
+
typeof WDwdataHub.fetchAisixiang,
|
|
45
|
+
typeof WDwdataHub.fetchAisixiang.fetchAuthorArticles,
|
|
46
|
+
typeof WDwdataHub.fetchAisixiang.fetchArticle,
|
|
47
|
+
typeof WDwdataHub.fetchGuancha,
|
|
48
|
+
typeof WDwdataHub.fetchGuancha.fetchAuthorArticles,
|
|
49
|
+
typeof WDwdataHub.fetchGuancha.fetchArticle,
|
|
50
|
+
Array.isArray(WDwdataHub.fetchGuancha.KNOWN_TOPICS),
|
|
51
|
+
]
|
|
52
|
+
let rr = ['object', 'function', 'function', 'object', 'function', 'function', true]
|
|
53
|
+
assert.strict.deepEqual(r, rr)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('不匯出內部共用函數', function() {
|
|
57
|
+
let ks = ['fetchWithRetry', 'getOptFetch', 'isYmd', 'toRocDate', 'toDatetimeUTC8', 'decodeEntities', 'htmlToMarkdown', 'extractDivContent', 'parseIntComma']
|
|
58
|
+
let r = ks.map((k) => Object.prototype.hasOwnProperty.call(WDwdataHub, k))
|
|
59
|
+
let rr = ks.map(() => false)
|
|
60
|
+
assert.strict.deepEqual(r, rr)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import decodeEntities from '../src/decodeEntities.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('decodeEntities', function() {
|
|
6
|
+
|
|
7
|
+
it('解碼具名實體', function() {
|
|
8
|
+
let r = decodeEntities('a&b<c>d"e'f g')
|
|
9
|
+
let rr = 'a&b<c>d"e\'f g'
|
|
10
|
+
assert.strict.deepEqual(r, rr)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('解碼中文網站正文常見之標點實體', function() {
|
|
14
|
+
let r = decodeEntities('…—–·«»©®')
|
|
15
|
+
let rr = '…—–·«»©®'
|
|
16
|
+
assert.strict.deepEqual(r, rr)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('解碼十進位與十六進位數值實體', function() {
|
|
20
|
+
let r = decodeEntities('ABCD中文')
|
|
21
|
+
let rr = 'ABCD中文'
|
|
22
|
+
assert.strict.deepEqual(r, rr)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('無對照之具名實體保留原字串', function() {
|
|
26
|
+
let r = decodeEntities('a&unknown;b&c')
|
|
27
|
+
let rr = 'a&unknown;b&c'
|
|
28
|
+
assert.strict.deepEqual(r, rr)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('非字串回傳空字串', function() {
|
|
32
|
+
let r = [
|
|
33
|
+
decodeEntities(null),
|
|
34
|
+
decodeEntities(undefined),
|
|
35
|
+
decodeEntities(123),
|
|
36
|
+
decodeEntities({}),
|
|
37
|
+
]
|
|
38
|
+
let rr = ['', '', '', '']
|
|
39
|
+
assert.strict.deepEqual(r, rr)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import extractDivContent from '../src/extractDivContent.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('extractDivContent', function() {
|
|
6
|
+
|
|
7
|
+
it('以深度追蹤切出含巢狀div之完整內容', function() {
|
|
8
|
+
let r = extractDivContent('<body><div class="ct">a<div>b</div>c</div><p>外部</p></body>', 'ct')
|
|
9
|
+
let rr = 'a<div>b</div>c'
|
|
10
|
+
assert.strict.deepEqual(r, rr)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('切出空容器回傳空字串而非null', function() {
|
|
14
|
+
let r = extractDivContent('<div class="ct"></div><div class="side"><a>不應被抓</a></div>', 'ct')
|
|
15
|
+
let rr = ''
|
|
16
|
+
assert.strict.deepEqual(r, rr)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('起始標籤採前綴比對, div帶其他屬性亦可命中', function() {
|
|
20
|
+
let r = extractDivContent('<div class="content all-txt" id="cont"><p>內文</p></div>', 'content all-txt')
|
|
21
|
+
let rr = '<p>內文</p>'
|
|
22
|
+
assert.strict.deepEqual(r, rr)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('找不到指定class回傳null', function() {
|
|
26
|
+
let r = extractDivContent('<div class="other">a</div>', 'ct')
|
|
27
|
+
let rr = null
|
|
28
|
+
assert.strict.deepEqual(r, rr)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('標籤未閉合回傳null', function() {
|
|
32
|
+
let r = extractDivContent('<div class="ct">a<div>b', 'ct')
|
|
33
|
+
let rr = null
|
|
34
|
+
assert.strict.deepEqual(r, rr)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('非有效輸入回傳null', function() {
|
|
38
|
+
let r = [
|
|
39
|
+
extractDivContent(null, 'ct'),
|
|
40
|
+
extractDivContent('<div class="ct">a</div>', ''),
|
|
41
|
+
extractDivContent('<div class="ct">a</div>', null),
|
|
42
|
+
extractDivContent(123, 'ct'),
|
|
43
|
+
]
|
|
44
|
+
let rr = [null, null, null, null]
|
|
45
|
+
assert.strict.deepEqual(r, rr)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchAiNewsAggregator from '../src/fetchAiNewsAggregator.mjs'
|
|
3
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('fetchAiNewsAggregator', function() {
|
|
7
|
+
|
|
8
|
+
let svr = null
|
|
9
|
+
|
|
10
|
+
before(async function() {
|
|
11
|
+
svr = await serverForTest()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
after(async function() {
|
|
15
|
+
if (svr) {
|
|
16
|
+
await svr.close()
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('轉為統一格式並去除欄位前後空白', async function() {
|
|
21
|
+
let r = await fetchAiNewsAggregator({ url: svr.url('/ai-news/latest-24h.json'), showLog: false })
|
|
22
|
+
let rr = [
|
|
23
|
+
{
|
|
24
|
+
url: 'https://ai.example/a1',
|
|
25
|
+
time: '2026-08-07 09:02:03',
|
|
26
|
+
title: 'AI標題一',
|
|
27
|
+
description: '',
|
|
28
|
+
from: 'Example News',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
url: 'https://ai.example/a2',
|
|
32
|
+
time: '',
|
|
33
|
+
title: 'AI標題二',
|
|
34
|
+
description: '',
|
|
35
|
+
from: 'Other News',
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
assert.strict.deepEqual(r, rr)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('published_at無法解析時time退回空字串', async function() {
|
|
42
|
+
let t = await fetchAiNewsAggregator({ url: svr.url('/ai-news/latest-24h.json'), showLog: false })
|
|
43
|
+
let r = t[1].time
|
|
44
|
+
let rr = ''
|
|
45
|
+
assert.strict.deepEqual(r, rr)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('回傳非JSON時reject', async function() {
|
|
49
|
+
let r = null
|
|
50
|
+
await fetchAiNewsAggregator({ url: svr.url('/json/bad'), maxRetries: 0, showLog: false })
|
|
51
|
+
.then(() => {
|
|
52
|
+
r = 'resolved'
|
|
53
|
+
})
|
|
54
|
+
.catch((err) => {
|
|
55
|
+
r = err.message.startsWith('回傳非JSON格式')
|
|
56
|
+
})
|
|
57
|
+
let rr = true
|
|
58
|
+
assert.strict.deepEqual(r, rr)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('HTTP 404不重試並reject', async function() {
|
|
62
|
+
let c0 = svr.getCount('/status/404')
|
|
63
|
+
let e = null
|
|
64
|
+
await fetchAiNewsAggregator({ url: svr.url('/status/404'), maxRetries: 3, showLog: false })
|
|
65
|
+
.catch((err) => {
|
|
66
|
+
e = err
|
|
67
|
+
})
|
|
68
|
+
let r = [e.statusCode, svr.getCount('/status/404') - c0]
|
|
69
|
+
let rr = [404, 1]
|
|
70
|
+
assert.strict.deepEqual(r, rr)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('回應無items欄位時回傳空陣列', async function() {
|
|
74
|
+
let r = await fetchAiNewsAggregator({ url: svr.url('/json/ok'), showLog: false })
|
|
75
|
+
let rr = []
|
|
76
|
+
assert.strict.deepEqual(r, rr)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
})
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchAisixiang, {
|
|
3
|
+
BASE_URL,
|
|
4
|
+
MAX_PAGES,
|
|
5
|
+
fetchAuthorsList,
|
|
6
|
+
lookupAuthor,
|
|
7
|
+
fetchTopicsList,
|
|
8
|
+
lookupTopic,
|
|
9
|
+
fetchAuthorArticles,
|
|
10
|
+
fetchKeywordArticles,
|
|
11
|
+
fetchTitleArticles,
|
|
12
|
+
fetchTopicArticles,
|
|
13
|
+
fetchArticle
|
|
14
|
+
} from '../src/fetchAisixiang.mjs'
|
|
15
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
describe('fetchAisixiang', function() {
|
|
19
|
+
|
|
20
|
+
let svr = null
|
|
21
|
+
let optFast = null
|
|
22
|
+
|
|
23
|
+
before(async function() {
|
|
24
|
+
svr = await serverForTest()
|
|
25
|
+
optFast = { baseUrl: svr.url(''), baseDelayMs: 10, maxDelayMs: 10, pageDelayMs: 0, showLog: false }
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
after(async function() {
|
|
29
|
+
if (svr) {
|
|
30
|
+
await svr.close()
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('模組常數與預設匯出之函數齊備', function() {
|
|
35
|
+
let r = [
|
|
36
|
+
BASE_URL,
|
|
37
|
+
MAX_PAGES,
|
|
38
|
+
Object.keys(fetchAisixiang).sort(),
|
|
39
|
+
]
|
|
40
|
+
let rr = [
|
|
41
|
+
'https://www.aisixiang.com',
|
|
42
|
+
50,
|
|
43
|
+
[
|
|
44
|
+
'BASE_URL', 'MAX_PAGES', 'USER_AGENT', 'fetchArticle', 'fetchAuthorArticles',
|
|
45
|
+
'fetchAuthorsList', 'fetchKeywordArticles', 'fetchTitleArticles', 'fetchTopicArticles',
|
|
46
|
+
'fetchTopicsList', 'lookupAuthor', 'lookupTopic', 'safeFilename',
|
|
47
|
+
],
|
|
48
|
+
]
|
|
49
|
+
assert.strict.deepEqual(r, rr)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('fetchAuthorsList解析作者清單並去除重複slug', async function() {
|
|
53
|
+
let r = await fetchAuthorsList(optFast)
|
|
54
|
+
let rr = [
|
|
55
|
+
{ slug: 'gezhaoguang', name: '葛兆光' },
|
|
56
|
+
{ slug: 'qinhui', name: '秦晖' },
|
|
57
|
+
{ slug: 'zhangwei', name: '张维' },
|
|
58
|
+
]
|
|
59
|
+
assert.strict.deepEqual(r, rr)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('lookupAuthor查無時回傳null', function() {
|
|
63
|
+
let authors = [{ slug: 'gezhaoguang', name: '葛兆光' }]
|
|
64
|
+
let r = [
|
|
65
|
+
lookupAuthor(authors, '葛兆光'),
|
|
66
|
+
lookupAuthor(authors, '葛兆光'.replace('葛', '楊')),
|
|
67
|
+
lookupAuthor(authors, ''),
|
|
68
|
+
lookupAuthor(null, '葛兆光'),
|
|
69
|
+
]
|
|
70
|
+
let rr = [{ slug: 'gezhaoguang', name: '葛兆光' }, null, null, null]
|
|
71
|
+
assert.strict.deepEqual(r, rr)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('fetchTopicsList解析主題清單並標記分類', async function() {
|
|
75
|
+
let r = await fetchTopicsList(optFast)
|
|
76
|
+
let rr = [
|
|
77
|
+
{ id: '301', name: '大数据', category: '学科' },
|
|
78
|
+
{ id: '302', name: '政治学', category: '学科' },
|
|
79
|
+
{ id: '401', name: '中美贸易战', category: '事件' },
|
|
80
|
+
{ id: '501', name: '康德', category: '人物' },
|
|
81
|
+
]
|
|
82
|
+
assert.strict.deepEqual(r, rr)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('lookupTopic查無時回傳null', function() {
|
|
86
|
+
let topics = [{ id: '301', name: '大数据', category: '学科' }]
|
|
87
|
+
let r = [
|
|
88
|
+
lookupTopic(topics, '大数据').id,
|
|
89
|
+
lookupTopic(topics, '大數據'),
|
|
90
|
+
lookupTopic(null, '大数据'),
|
|
91
|
+
]
|
|
92
|
+
let rr = ['301', null, null]
|
|
93
|
+
assert.strict.deepEqual(r, rr)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('fetchAuthorArticles以name兩跳查得專欄文章並標記分類', async function() {
|
|
97
|
+
let t = await fetchAuthorArticles({ ...optFast, name: '葛兆光' })
|
|
98
|
+
let r = [t.status, t.site, t.mode, t.resolved.slug, t.resolved.name, t.count, t.items[0], t.items[2].category]
|
|
99
|
+
let rr = [
|
|
100
|
+
'success', 'aisixiang', 'author', 'gezhaoguang', '葛兆光', 3,
|
|
101
|
+
{ aid: '146669', url: svr.url('/data/146669.html'), title: '禅宗与中国文化', category: '论文' },
|
|
102
|
+
'时评',
|
|
103
|
+
]
|
|
104
|
+
assert.strict.deepEqual(r, rr)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('fetchAuthorArticles以slug捷徑跳過清單查找', async function() {
|
|
108
|
+
let c0 = svr.getCount('/thinktank/')
|
|
109
|
+
let t = await fetchAuthorArticles({ ...optFast, slug: 'gezhaoguang' })
|
|
110
|
+
let r = [t.status, t.count, svr.getCount('/thinktank/') - c0]
|
|
111
|
+
let rr = ['success', 3, 0]
|
|
112
|
+
assert.strict.deepEqual(r, rr)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('fetchAuthorArticles查無作者時回success且count為0並附說明', async function() {
|
|
116
|
+
let t = await fetchAuthorArticles({ ...optFast, name: '不存在的作者XYZ' })
|
|
117
|
+
let r = [t.status, t.count, t.items, t.authors_count, t.message.includes('尚無此作者文章'), t.message.includes('簡繁')]
|
|
118
|
+
let rr = ['success', 0, [], 3, true, true]
|
|
119
|
+
assert.strict.deepEqual(r, rr)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('fetchAuthorArticles命中欄頁但0篇時回success且附說明', async function() {
|
|
123
|
+
let t = await fetchAuthorArticles({ ...optFast, slug: 'nobody' })
|
|
124
|
+
let r = [t.status, t.count, t.message.includes('0 篇文章')]
|
|
125
|
+
let rr = ['success', 0, true]
|
|
126
|
+
assert.strict.deepEqual(r, rr)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('fetchKeywordArticles自動翻頁並解析搜尋結果', async function() {
|
|
130
|
+
let t = await fetchKeywordArticles('老庄', optFast)
|
|
131
|
+
let r = [t.status, t.mode, t.count, t.items[0], t.resolved.search_url.includes('searchfield=keywords')]
|
|
132
|
+
let rr = [
|
|
133
|
+
'success', 'keyword', 2,
|
|
134
|
+
{ aid: '300001', url: svr.url('/data/300001.html'), title: '老庄之道', author: '作者甲' },
|
|
135
|
+
true,
|
|
136
|
+
]
|
|
137
|
+
assert.strict.deepEqual(r, rr)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('fetchKeywordArticles無結果時回success且count為0, 不誤抓側邊欄連結', async function() {
|
|
141
|
+
let t = await fetchKeywordArticles('老莊', optFast)
|
|
142
|
+
let r = [t.status, t.count, t.items, t.message.includes('無相關文章'), t.message.includes('簡體')]
|
|
143
|
+
let rr = ['success', 0, [], true, true]
|
|
144
|
+
assert.strict.deepEqual(r, rr)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('fetchTitleArticles走title搜尋端點', async function() {
|
|
148
|
+
let t = await fetchTitleArticles('老庄', optFast)
|
|
149
|
+
let r = [t.status, t.mode, t.count, t.resolved.search_url.includes('searchfield=title')]
|
|
150
|
+
let rr = ['success', 'title', 2, true]
|
|
151
|
+
assert.strict.deepEqual(r, rr)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('fetchTopicArticles以keyword兩跳並自動翻頁全抓', async function() {
|
|
155
|
+
let t = await fetchTopicArticles({ ...optFast, keyword: '大数据' })
|
|
156
|
+
let r = [t.status, t.mode, t.resolved.id, t.resolved.category, t.resolved.total_pages, t.resolved.pages_fetched, t.count]
|
|
157
|
+
let rr = ['success', 'topic', '301', '学科', 2, 2, 3]
|
|
158
|
+
assert.strict.deepEqual(r, rr)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('fetchTopicArticles以id捷徑跳過清單查找', async function() {
|
|
162
|
+
let c0 = svr.getCount('/zhuanti/')
|
|
163
|
+
let t = await fetchTopicArticles({ ...optFast, id: '301' })
|
|
164
|
+
let r = [t.status, t.count, svr.getCount('/zhuanti/') - c0]
|
|
165
|
+
let rr = ['success', 3, 0]
|
|
166
|
+
assert.strict.deepEqual(r, rr)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('fetchTopicArticles查無主題時回success且count為0並建議改查keyword', async function() {
|
|
170
|
+
let t = await fetchTopicArticles({ ...optFast, keyword: '不存在的主題ABC' })
|
|
171
|
+
let r = [t.status, t.count, t.topics_count, t.message.includes('fetchKeywordArticles')]
|
|
172
|
+
let rr = ['success', 0, 4, true]
|
|
173
|
+
assert.strict.deepEqual(r, rr)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('fetchArticle轉為含frontmatter之Markdown並去除站名後綴', async function() {
|
|
177
|
+
let t = await fetchArticle({ ...optFast, aid: '146669' })
|
|
178
|
+
let r = [
|
|
179
|
+
t.status,
|
|
180
|
+
t.mode,
|
|
181
|
+
t.url,
|
|
182
|
+
t.title,
|
|
183
|
+
t.markdown.startsWith('---\n'),
|
|
184
|
+
t.markdown.includes(`source: "${svr.url('/data/146669.html')}"`),
|
|
185
|
+
t.markdown.includes('**禅宗**'),
|
|
186
|
+
t.markdown.includes('[士大夫](/data/1.html)'),
|
|
187
|
+
t.markdown.includes('&思想史脉络'),
|
|
188
|
+
t.markdown.includes('嵌套段落亦應被完整切出。'),
|
|
189
|
+
]
|
|
190
|
+
let rr = ['success', 'fetch', svr.url('/data/146669.html'), '葛兆光:禅宗与中国文化', true, true, true, true, true, true]
|
|
191
|
+
assert.strict.deepEqual(r, rr)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('fetchArticle以url為入口', async function() {
|
|
195
|
+
let t = await fetchArticle({ ...optFast, url: svr.url('/data/146669.html') })
|
|
196
|
+
let r = [t.status, t.title, t.chars > 30]
|
|
197
|
+
let rr = ['success', '葛兆光:禅宗与中国文化', true]
|
|
198
|
+
assert.strict.deepEqual(r, rr)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('fetchArticle解析失敗時reject', async function() {
|
|
202
|
+
let r = null
|
|
203
|
+
await fetchArticle({ ...optFast, aid: '999999' })
|
|
204
|
+
.then(() => {
|
|
205
|
+
r = 'resolved'
|
|
206
|
+
})
|
|
207
|
+
.catch((err) => {
|
|
208
|
+
r = err.message.startsWith('解析失敗')
|
|
209
|
+
})
|
|
210
|
+
let rr = true
|
|
211
|
+
assert.strict.deepEqual(r, rr)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('必要參數缺失時拋錯', async function() {
|
|
215
|
+
let r = []
|
|
216
|
+
await fetchAuthorArticles(optFast).catch((err) => r.push(err.message))
|
|
217
|
+
await fetchKeywordArticles(undefined, optFast).catch((err) => r.push(err.message))
|
|
218
|
+
await fetchTitleArticles(undefined, optFast).catch((err) => r.push(err.message))
|
|
219
|
+
await fetchTopicArticles(optFast).catch((err) => r.push(err.message))
|
|
220
|
+
await fetchArticle(optFast).catch((err) => r.push(err.message))
|
|
221
|
+
let rr = ['需要 name 或 slug', '需要 keyword', '需要 keyword', '需要 keyword 或 id', '需要 aid 或 url']
|
|
222
|
+
assert.strict.deepEqual(r, rr)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchCnyes from '../src/fetchCnyes.mjs'
|
|
3
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('fetchCnyes', function() {
|
|
7
|
+
|
|
8
|
+
let svr = null
|
|
9
|
+
|
|
10
|
+
before(async function() {
|
|
11
|
+
svr = await serverForTest()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
after(async function() {
|
|
15
|
+
if (svr) {
|
|
16
|
+
await svr.close()
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('轉為統一格式並組出新聞內容頁網址', async function() {
|
|
21
|
+
let r = await fetchCnyes({ url: svr.url('/cnyes/newslist'), pageDelayMs: 0, showLog: false })
|
|
22
|
+
let rr = [
|
|
23
|
+
{ time: '2026-08-06 15:06:40', title: '鉅亨標題一', link: 'https://news.cnyes.com/news/id/5001' },
|
|
24
|
+
{ time: '', title: '鉅亨標題二', link: 'https://news.cnyes.com/news/id/5002' },
|
|
25
|
+
]
|
|
26
|
+
assert.strict.deepEqual(r, rr)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('publishAt缺漏時該筆time退回空字串, 不影響其餘新聞', async function() {
|
|
30
|
+
let t = await fetchCnyes({ url: svr.url('/cnyes/newslist'), pageDelayMs: 0, showLog: false })
|
|
31
|
+
let r = [t.length, t[1].time, t[1].title]
|
|
32
|
+
let rr = [2, '', '鉅亨標題二']
|
|
33
|
+
assert.strict.deepEqual(r, rr)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('無更多資料時停止翻頁', async function() {
|
|
37
|
+
let c0 = svr.getCount('/cnyes/newslist')
|
|
38
|
+
await fetchCnyes({ url: svr.url('/cnyes/newslist'), pageDelayMs: 0, showLog: false })
|
|
39
|
+
let r = svr.getCount('/cnyes/newslist') - c0
|
|
40
|
+
let rr = 2
|
|
41
|
+
assert.strict.deepEqual(r, rr)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('targetTotal達標即停止翻頁', async function() {
|
|
45
|
+
let c0 = svr.getCount('/cnyes/newslist')
|
|
46
|
+
let t = await fetchCnyes({ url: svr.url('/cnyes/newslist'), targetTotal: 1, pageDelayMs: 0, showLog: false })
|
|
47
|
+
let r = [t.length, svr.getCount('/cnyes/newslist') - c0]
|
|
48
|
+
let rr = [1, 1]
|
|
49
|
+
assert.strict.deepEqual(r, rr)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('HTTP 403納入重試範圍, 含初始共執行maxRetries+1次', async function() {
|
|
53
|
+
let c0 = svr.getCount('/status/403')
|
|
54
|
+
await fetchCnyes({ url: svr.url('/status/403'), maxRetries: 2, baseDelayMs: 10, maxDelayMs: 10, pageDelayMs: 0, showLog: false })
|
|
55
|
+
.catch(() => {})
|
|
56
|
+
let r = svr.getCount('/status/403') - c0
|
|
57
|
+
let rr = 3
|
|
58
|
+
assert.strict.deepEqual(r, rr)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
})
|