linke-sdufe 0.3.1 → 0.5.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 +77 -0
- package/src/index.js +1 -1
- package/src/parsers.js +78 -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
|
|
|
@@ -96,6 +97,47 @@ export const PORTAL_NOTICE_SOURCES = {
|
|
|
96
97
|
sclx: { name: '出国留学培训基地', origin: 'https://sclx.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1078'] },
|
|
97
98
|
}
|
|
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' },
|
|
112
|
+
// T47 学院简介/领导页(16 站 24 key,照单注册清单 docs/plan/crawl/t47-page-key-registry.md
|
|
113
|
+
// ——地图 v9+批次 8 全量遍历双向验证;命名 {学院缩写}-xyjj=学院简介 / -ldr=学院领导)。
|
|
114
|
+
// 领导页为表格版式(姓名/职务/分工三列)无标准博达容器,走 parsePortalPage body 兜底。
|
|
115
|
+
'gkgc-xyjj': { name: '管理科学与工程学院简介', url: 'https://gkgc.sdufe.edu.cn/xygk/xyjj.htm' },
|
|
116
|
+
'gkgc-ldr': { name: '管理科学与工程学院领导', url: 'https://gkgc.sdufe.edu.cn/xygk/xyld.htm' },
|
|
117
|
+
'cs-xyjj': { name: '计算机与人工智能学院简介', url: 'https://cs.sdufe.edu.cn/xygk/xyjj.htm' },
|
|
118
|
+
'cs-ldr': { name: '计算机与人工智能学院领导', url: 'https://cs.sdufe.edu.cn/xygk/xyld.htm' },
|
|
119
|
+
'spft-xyjj': { name: '财政税务学院简介', url: 'https://spft.sdufe.edu.cn/xygk/xyjj.htm' },
|
|
120
|
+
'spft-ldr': { name: '财政税务学院领导', url: 'https://spft.sdufe.edu.cn/xygk/xyld.htm' },
|
|
121
|
+
'sie-xyjj': { name: '国际教育学院简介', url: 'https://sie.sdufe.edu.cn/xygk/xyjj.htm' },
|
|
122
|
+
'sie-ldr': { name: '国际教育学院领导', url: 'https://sie.sdufe.edu.cn/xygk/xyld.htm' },
|
|
123
|
+
'ys-xyjj': { name: '燕山学院简介', url: 'https://ys.sdufe.edu.cn/xygk/xyjj.htm' },
|
|
124
|
+
'ys-ldr': { name: '燕山学院领导', url: 'https://ys.sdufe.edu.cn/xygk/xyld.htm' },
|
|
125
|
+
'insurance-xyjj': { name: '保险学院简介', url: 'https://insurance.sdufe.edu.cn/xygk/xyjj.htm' },
|
|
126
|
+
'insurance-ldr': { name: '保险学院领导', url: 'https://insurance.sdufe.edu.cn/xygk/xrld.htm' },
|
|
127
|
+
'jinrong-xyjj': { name: '金融学院简介', url: 'https://jinrong.sdufe.edu.cn/xygk.htm' },
|
|
128
|
+
'jinrong-ldr': { name: '金融学院领导', url: 'https://jinrong.sdufe.edu.cn/xygk/xrld.htm' },
|
|
129
|
+
'marx-xyjj': { name: '马克思主义学院简介', url: 'https://marx.sdufe.edu.cn/index/xygk/xyjj.htm' },
|
|
130
|
+
'marx-ldr': { name: '马克思主义学院领导', url: 'https://marx.sdufe.edu.cn/index/xygk/xrld.htm' },
|
|
131
|
+
'sam-ldr': { name: '统计与数学学院领导', url: 'https://sam.sdufe.edu.cn/xygk/xrld.htm' },
|
|
132
|
+
'bcf-ldr': { name: '区块链金融实验室领导', url: 'https://bcf.sdufe.edu.cn/sysgk1/xrld.htm' },
|
|
133
|
+
'mpa-ldr': { name: 'MPA中心领导', url: 'https://mpa.sdufe.edu.cn/zxgk/xrld.htm' },
|
|
134
|
+
'law-xyjj': { name: '法学院简介', url: 'https://law.sdufe.edu.cn/xygk1/xyjj.htm' },
|
|
135
|
+
'gsglxy-xyjj': { name: '工商管理学院简介', url: 'https://gsglxy.sdufe.edu.cn/xygk1/xyjj.htm' },
|
|
136
|
+
'sce-xyjj': { name: '继续教育学院简介', url: 'https://sce.sdufe.edu.cn/xygk1/xyjj.htm' },
|
|
137
|
+
'site-xyjj': { name: '国际经贸学院简介', url: 'https://site.sdufe.edu.cn/xygk1/xyjj.htm' },
|
|
138
|
+
'mba-xyjj': { name: 'MBA学院简介', url: 'https://mba.sdufe.edu.cn/xygk/xyjj.htm' },
|
|
139
|
+
}
|
|
140
|
+
|
|
99
141
|
/** 课程属性中文名 → zzdKcSX 表单码(强智 kbxx_kc_ifr 口径) */
|
|
100
142
|
export const COURSE_TYPE_MAP = {
|
|
101
143
|
必修: '1',
|
|
@@ -492,6 +534,41 @@ export function createSdufeAdapter(env) {
|
|
|
492
534
|
.filter((x) => !src.infoPaths || !src.infoPaths.length || src.infoPaths.some((c) => x.url.includes(`/info/${c}/`)))
|
|
493
535
|
},
|
|
494
536
|
|
|
537
|
+
/**
|
|
538
|
+
* 校园官网明面具名页面抓取(T46 续四)。
|
|
539
|
+
* 返回 { title, content, links };links=页面内 info 文章链接清单
|
|
540
|
+
* (如现任领导页→每位领导详情页),供下一步详情深入。
|
|
541
|
+
*/
|
|
542
|
+
async fetchPortalPage(pageKey) {
|
|
543
|
+
const page = PORTAL_PAGES[pageKey]
|
|
544
|
+
if (!page) {
|
|
545
|
+
throw new LinkeError('BAD_PAGE', `未知具名页面:${pageKey}(可选:${Object.keys(PORTAL_PAGES).join('/')})`, {
|
|
546
|
+
exitCode: EXIT.GENERAL,
|
|
547
|
+
hint: '学校级:xxjj=学校简介 / xrld=现任领导 / zuzjg=组织机构 / xqbc=校车班线 / xydh=校园电话 / zxxl=校历;学院级(T47):{学院缩写}-xyjj=学院简介 / {学院缩写}-ldr=学院领导(如 gkgc-ldr=管科学院领导)',
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
let response
|
|
551
|
+
try {
|
|
552
|
+
response = await env.fetch(page.url, {
|
|
553
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) linke-cli' },
|
|
554
|
+
redirect: 'follow',
|
|
555
|
+
timeoutMs: 15000,
|
|
556
|
+
})
|
|
557
|
+
} catch (err) {
|
|
558
|
+
throw networkError(`抓取${page.name}页面`, err)
|
|
559
|
+
}
|
|
560
|
+
if (!response.ok) {
|
|
561
|
+
throw networkError(`抓取${page.name}页面(HTTP ${response.status})`, null)
|
|
562
|
+
}
|
|
563
|
+
const parsed = parsePortalPage(await response.text(), page.url)
|
|
564
|
+
if (!parsed) {
|
|
565
|
+
throw new LinkeError('PARSE', `解析${page.name}页面失败(页面结构可能已变化)`, {
|
|
566
|
+
exitCode: EXIT.PARSE,
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
return { page: pageKey, name: page.name, url: page.url, ...parsed }
|
|
570
|
+
},
|
|
571
|
+
|
|
495
572
|
/**
|
|
496
573
|
* 校园官网公开通知详情全文(T46)。url 限源注册表内域名(白名单防抓任意站);
|
|
497
574
|
* 礼貌纪律:只抓用户点名的这一篇,不做批量预取。
|
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,81 @@ 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(含序号后缀族,取最外层;T47 学院站实勘)→ .wp_articlecontent /
|
|
999
|
+
* .c_content → body 主文本(学校概况类页面非标准博达
|
|
1000
|
+
* 文章容器,逐页实勘判例:xxjj 学校简介/xrld 现任领导/xydh 校园电话等)。
|
|
1001
|
+
* 同时提取页面内 info 文章链接清单(如现任领导页→每位领导详情页),
|
|
1002
|
+
* 供「列表→详情」动线延续。找不到任何容器时回退 body 去头尾。 */
|
|
1003
|
+
export function parsePortalPage(html, pageUrl = '') {
|
|
1004
|
+
if (!html || typeof html !== 'string') return null
|
|
1005
|
+
let inner = null
|
|
1006
|
+
// 1. 标准博达容器优先(复用详情提取:v_news_content 含空壳跳过逻辑)
|
|
1007
|
+
inner = extractVNewsContent(html)
|
|
1008
|
+
// 2. vsb_content / wp_articlecontent / c_content 容器(单次 div 平衡)。
|
|
1009
|
+
// T47:学院站 vsb_content 常带序号后缀(vsb_content_4、vsb_content_2…),
|
|
1010
|
+
// 且内层 u 系 div 的 id 也以 vsb_content 开头——取 id 最短者=最外层容器
|
|
1011
|
+
// (law/sce/site/gsglxy/mba 实勘),不再只认精确 id="vsb_content"。
|
|
1012
|
+
if (!inner) {
|
|
1013
|
+
const candidates = []
|
|
1014
|
+
const vsbRe = /id="(vsb_content[^"]*)"/gi
|
|
1015
|
+
let vm
|
|
1016
|
+
let vsbBest = null
|
|
1017
|
+
while ((vm = vsbRe.exec(html))) {
|
|
1018
|
+
if (!vsbBest || vm[1].length < vsbBest[1].length) vsbBest = vm
|
|
1019
|
+
}
|
|
1020
|
+
if (vsbBest) candidates.push(vsbBest.index)
|
|
1021
|
+
for (const marker of [/class="[^"]*wp_articlecontent[^"]*"/i, /class="[^"]*c_content[^"]*"/i]) {
|
|
1022
|
+
const om = html.match(marker)
|
|
1023
|
+
if (om) candidates.push(om.index)
|
|
1024
|
+
}
|
|
1025
|
+
for (const idx of candidates) {
|
|
1026
|
+
const openTag = html.lastIndexOf('<div', idx)
|
|
1027
|
+
if (openTag === -1) continue
|
|
1028
|
+
const openEnd = html.indexOf('>', openTag)
|
|
1029
|
+
if (openEnd === -1) continue
|
|
1030
|
+
let depth = 1
|
|
1031
|
+
const tagRe = /<\/div\s*>|<div\b[^>]*>/gi
|
|
1032
|
+
tagRe.lastIndex = openEnd + 1
|
|
1033
|
+
let t
|
|
1034
|
+
while ((t = tagRe.exec(html))) {
|
|
1035
|
+
if (t[0][1] === '/') depth--
|
|
1036
|
+
else depth++
|
|
1037
|
+
if (depth === 0) {
|
|
1038
|
+
inner = html.slice(openEnd + 1, t.index)
|
|
1039
|
+
break
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (inner) break
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
// 3. 兜底:body 去头尾(script/style 已在 htmlToText 剔除)
|
|
1046
|
+
const bodyMatch = inner ? null : html.match(/<body[^>]*>([\s\S]*?)<\/body>/i)
|
|
1047
|
+
const source = inner || (bodyMatch ? bodyMatch[1] : html)
|
|
1048
|
+
const content = htmlToText(source)
|
|
1049
|
+
if (!content) return null
|
|
1050
|
+
// 页面内 info 文章链接清单(去重、绝对化)
|
|
1051
|
+
const origin = (() => {
|
|
1052
|
+
try { return new URL(pageUrl).origin } catch { return '' }
|
|
1053
|
+
})()
|
|
1054
|
+
const links = []
|
|
1055
|
+
const seen = new Set()
|
|
1056
|
+
const linkRe = /<a\s[^>]*?href="((?:[^"]*?\/)?info\/\d+\/\d+\.htm)"[^>]*>([\s\S]*?)<\/a>/g
|
|
1057
|
+
let a
|
|
1058
|
+
while ((a = linkRe.exec(source))) {
|
|
1059
|
+
let url = a[1]
|
|
1060
|
+
if (!/^https?:/i.test(url) && origin) {
|
|
1061
|
+
url = origin + '/' + url.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
|
|
1062
|
+
url = url.replace(/([^:])\/+/g, '$1/')
|
|
1063
|
+
}
|
|
1064
|
+
if (seen.has(url)) continue
|
|
1065
|
+
seen.add(url)
|
|
1066
|
+
const title = decodeEntities(a[2].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim())
|
|
1067
|
+
if (title) links.push({ title: title.slice(0, 80), url })
|
|
1068
|
+
}
|
|
1069
|
+
// 标题:页面 title 剥站名后缀
|
|
1070
|
+
const t = html.match(/<title>([^<]*)<\/title>/)
|
|
1071
|
+
const pageTitle = t ? decodeEntities(t[1].replace(/-[^-]*$/, '').trim()) : ''
|
|
1072
|
+
return { title: pageTitle, content, links }
|
|
1073
|
+
}
|