linke-sdufe 0.3.0 → 0.4.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/package.json +1 -1
- package/src/adapter.js +53 -0
- package/src/index.js +1 -1
- package/src/parsers.js +64 -0
package/package.json
CHANGED
package/src/adapter.js
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
parseJwcNotices,
|
|
35
35
|
parsePortalNotices,
|
|
36
36
|
parsePortalNoticeDetail,
|
|
37
|
+
parsePortalPage,
|
|
37
38
|
parseMakeupsHtml,
|
|
38
39
|
} from './parsers.js'
|
|
39
40
|
|
|
@@ -91,6 +92,23 @@ export const PORTAL_NOTICE_SOURCES = {
|
|
|
91
92
|
ys: { name: '燕山学院', origin: 'https://ys.sdufe.edu.cn', listPath: '/xwtz/zhxw.htm', pagePath: '/xwtz/zhxw', infoPaths: ['1018', '1053'] },
|
|
92
93
|
zfjxpj: { name: '政府绩效评价', origin: 'https://zfjxpj.sdufe.edu.cn', listPath: '/zxdt/tzgg.htm', pagePath: '/zxdt/tzgg', infoPaths: ['1010', '1015', '1018', '1037'] },
|
|
93
94
|
zzb: { name: '组织部', origin: 'https://zzb.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1046', '1076'] },
|
|
95
|
+
sjc: { name: '审计处', origin: 'https://sjc.sdufe.edu.cn', listPath: '/gzdt.htm', pagePath: '/gzdt', infoPaths: ['1006'] },
|
|
96
|
+
cwc: { name: '财务处', origin: 'https://cwc.sdufe.edu.cn', listPath: '/tzgg.htm', pagePath: '/tzgg', infoPaths: ['1024'] },
|
|
97
|
+
sclx: { name: '出国留学培训基地', origin: 'https://sclx.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1078'] },
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 校园官网明面具名页面注册表(T46 续四;学校概况类栏目页/单页——
|
|
102
|
+
* 非标准博达文章容器,解析走 parsePortalPage 容器链)。
|
|
103
|
+
* 逐页实勘 2026-09-04:全部含可提取正文。
|
|
104
|
+
*/
|
|
105
|
+
export const PORTAL_PAGES = {
|
|
106
|
+
xxjj: { name: '学校简介', url: 'https://www.sdufe.edu.cn/xxgk_/xxjj.htm' },
|
|
107
|
+
xrld: { name: '现任领导', url: 'https://www.sdufe.edu.cn/xxgk_/xrld.htm' },
|
|
108
|
+
zuzjg: { name: '组织机构', url: 'https://www.sdufe.edu.cn/zuzjg.htm' },
|
|
109
|
+
xqbc: { name: '校车班线', url: 'https://www.sdufe.edu.cn/xyfw/xqbc.htm' },
|
|
110
|
+
xydh: { name: '校园电话', url: 'https://www.sdufe.edu.cn/xyfw/xydh.htm' },
|
|
111
|
+
zxxl: { name: '校历', url: 'https://www.sdufe.edu.cn/xyfw/zxxl.htm' },
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
/** 课程属性中文名 → zzdKcSX 表单码(强智 kbxx_kc_ifr 口径) */
|
|
@@ -489,6 +507,41 @@ export function createSdufeAdapter(env) {
|
|
|
489
507
|
.filter((x) => !src.infoPaths || !src.infoPaths.length || src.infoPaths.some((c) => x.url.includes(`/info/${c}/`)))
|
|
490
508
|
},
|
|
491
509
|
|
|
510
|
+
/**
|
|
511
|
+
* 校园官网明面具名页面抓取(T46 续四)。
|
|
512
|
+
* 返回 { title, content, links };links=页面内 info 文章链接清单
|
|
513
|
+
* (如现任领导页→每位领导详情页),供下一步详情深入。
|
|
514
|
+
*/
|
|
515
|
+
async fetchPortalPage(pageKey) {
|
|
516
|
+
const page = PORTAL_PAGES[pageKey]
|
|
517
|
+
if (!page) {
|
|
518
|
+
throw new LinkeError('BAD_PAGE', `未知具名页面:${pageKey}(可选:${Object.keys(PORTAL_PAGES).join('/')})`, {
|
|
519
|
+
exitCode: EXIT.GENERAL,
|
|
520
|
+
hint: 'xxjj=学校简介 / xrld=现任领导 / zuzjg=组织机构 / xqbc=校车班线 / xydh=校园电话 / zxxl=校历',
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
let response
|
|
524
|
+
try {
|
|
525
|
+
response = await env.fetch(page.url, {
|
|
526
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) linke-cli' },
|
|
527
|
+
redirect: 'follow',
|
|
528
|
+
timeoutMs: 15000,
|
|
529
|
+
})
|
|
530
|
+
} catch (err) {
|
|
531
|
+
throw networkError(`抓取${page.name}页面`, err)
|
|
532
|
+
}
|
|
533
|
+
if (!response.ok) {
|
|
534
|
+
throw networkError(`抓取${page.name}页面(HTTP ${response.status})`, null)
|
|
535
|
+
}
|
|
536
|
+
const parsed = parsePortalPage(await response.text(), page.url)
|
|
537
|
+
if (!parsed) {
|
|
538
|
+
throw new LinkeError('PARSE', `解析${page.name}页面失败(页面结构可能已变化)`, {
|
|
539
|
+
exitCode: EXIT.PARSE,
|
|
540
|
+
})
|
|
541
|
+
}
|
|
542
|
+
return { page: pageKey, name: page.name, url: page.url, ...parsed }
|
|
543
|
+
},
|
|
544
|
+
|
|
492
545
|
/**
|
|
493
546
|
* 校园官网公开通知详情全文(T46)。url 限源注册表内域名(白名单防抓任意站);
|
|
494
547
|
* 礼貌纪律:只抓用户点名的这一篇,不做批量预取。
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* linke-sdufe:山财强智教务适配器共享包。
|
|
3
3
|
* linke-cli(Node)与 Linke App(uni-app,经 uni.request 垫片)同源引用。
|
|
4
4
|
*/
|
|
5
|
-
export { createSdufeAdapter, COURSE_TYPE_MAP, PORTAL_NOTICE_SOURCES } from './adapter.js'
|
|
5
|
+
export { createSdufeAdapter, COURSE_TYPE_MAP, PORTAL_NOTICE_SOURCES, PORTAL_PAGES } from './adapter.js'
|
|
6
6
|
export { nodeEnv, validateEnv } from './env.js'
|
|
7
7
|
export { computeEncoded } from './encoding.js'
|
|
8
8
|
export * as parsers from './parsers.js'
|
package/src/parsers.js
CHANGED
|
@@ -993,3 +993,67 @@ export function parsePortalNoticeDetail(html, pageUrl = '') {
|
|
|
993
993
|
}
|
|
994
994
|
return { title, date, content, attachments }
|
|
995
995
|
}
|
|
996
|
+
|
|
997
|
+
/** 校园官网明面具名页面解析(T46 续四):容器链兜底——v_news_content →
|
|
998
|
+
* #vsb_content → .c_content → body 主文本(学校概况类页面非标准博达
|
|
999
|
+
* 文章容器,逐页实勘判例:xxjj 学校简介/xrld 现任领导/xydh 校园电话等)。
|
|
1000
|
+
* 同时提取页面内 info 文章链接清单(如现任领导页→每位领导详情页),
|
|
1001
|
+
* 供「列表→详情」动线延续。找不到任何容器时回退 body 去头尾。 */
|
|
1002
|
+
export function parsePortalPage(html, pageUrl = '') {
|
|
1003
|
+
if (!html || typeof html !== 'string') return null
|
|
1004
|
+
let inner = null
|
|
1005
|
+
// 1. 标准博达容器优先(复用详情提取:v_news_content 含空壳跳过逻辑)
|
|
1006
|
+
inner = extractVNewsContent(html)
|
|
1007
|
+
// 2. vsb_content / c_content 容器(单次 div 平衡)
|
|
1008
|
+
if (!inner) {
|
|
1009
|
+
for (const marker of [/id="vsb_content"/i, /class="[^"]*c_content[^"]*"/i]) {
|
|
1010
|
+
const om = html.match(marker)
|
|
1011
|
+
if (!om) continue
|
|
1012
|
+
const openTag = html.lastIndexOf('<div', om.index)
|
|
1013
|
+
if (openTag === -1) continue
|
|
1014
|
+
const openEnd = html.indexOf('>', openTag)
|
|
1015
|
+
if (openEnd === -1) continue
|
|
1016
|
+
let depth = 1
|
|
1017
|
+
const tagRe = /<\/div\s*>|<div\b[^>]*>/gi
|
|
1018
|
+
tagRe.lastIndex = openEnd + 1
|
|
1019
|
+
let t
|
|
1020
|
+
while ((t = tagRe.exec(html))) {
|
|
1021
|
+
if (t[0][1] === '/') depth--
|
|
1022
|
+
else depth++
|
|
1023
|
+
if (depth === 0) {
|
|
1024
|
+
inner = html.slice(openEnd + 1, t.index)
|
|
1025
|
+
break
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (inner) break
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
// 3. 兜底:body 去头尾(script/style 已在 htmlToText 剔除)
|
|
1032
|
+
const bodyMatch = inner ? null : html.match(/<body[^>]*>([\s\S]*?)<\/body>/i)
|
|
1033
|
+
const source = inner || (bodyMatch ? bodyMatch[1] : html)
|
|
1034
|
+
const content = htmlToText(source)
|
|
1035
|
+
if (!content) return null
|
|
1036
|
+
// 页面内 info 文章链接清单(去重、绝对化)
|
|
1037
|
+
const origin = (() => {
|
|
1038
|
+
try { return new URL(pageUrl).origin } catch { return '' }
|
|
1039
|
+
})()
|
|
1040
|
+
const links = []
|
|
1041
|
+
const seen = new Set()
|
|
1042
|
+
const linkRe = /<a\s[^>]*?href="((?:[^"]*?\/)?info\/\d+\/\d+\.htm)"[^>]*>([\s\S]*?)<\/a>/g
|
|
1043
|
+
let a
|
|
1044
|
+
while ((a = linkRe.exec(source))) {
|
|
1045
|
+
let url = a[1]
|
|
1046
|
+
if (!/^https?:/i.test(url) && origin) {
|
|
1047
|
+
url = origin + '/' + url.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
1048
|
+
url = url.replace(/([^:])\/+/g, '$1/')
|
|
1049
|
+
}
|
|
1050
|
+
if (seen.has(url)) continue
|
|
1051
|
+
seen.add(url)
|
|
1052
|
+
const title = decodeEntities(a[2].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim())
|
|
1053
|
+
if (title) links.push({ title: title.slice(0, 80), url })
|
|
1054
|
+
}
|
|
1055
|
+
// 标题:页面 title 剥站名后缀
|
|
1056
|
+
const t = html.match(/<title>([^<]*)<\/title>/)
|
|
1057
|
+
const pageTitle = t ? decodeEntities(t[1].replace(/-[^-]*$/, '').trim()) : ''
|
|
1058
|
+
return { title: pageTitle, content, links }
|
|
1059
|
+
}
|