From af7f36d44c40fb21682a6d1a24ad51ccb5e50287 Mon Sep 17 00:00:00 2001 From: Jiamin <16831220+magazian@users.noreply.github.com> Date: Sun, 12 Jul 2026 00:34:07 +0800 Subject: [PATCH] fix(route): refactor to adapt to new web design (#22383) * fix(route): refactor to adapt to new web design * fix:reduce complexity * fix: update CSS selector for xingnew and xwzt * fix: update selectors * fix:update hideBoxes * fix: replace for with find --- lib/routes/chnmuseum/xingnew.ts | 8 +- lib/routes/chnmuseum/xwzt.ts | 4 +- lib/routes/chnmuseum/zl.tsx | 160 ++++++++++++++++---------------- 3 files changed, 88 insertions(+), 84 deletions(-) diff --git a/lib/routes/chnmuseum/xingnew.ts b/lib/routes/chnmuseum/xingnew.ts index 85716e0045..e50bd2ec87 100644 --- a/lib/routes/chnmuseum/xingnew.ts +++ b/lib/routes/chnmuseum/xingnew.ts @@ -31,12 +31,12 @@ export const route: Route = { const response = await ofetch('https://www.chnmuseum.cn/zx/xingnew/'); const $ = load(response); - const list = $('ul.cj_xushuliebao_list li') + const list = $('ul.xly_list_ts li') .toArray() .map((item) => { - item = $(item); - const a = item.find('a'); - const dateText = item.find('span.date').text(); + const $item = $(item); + const a = $item.find('a.titles'); + const dateText = $item.find('.times span.sp').text(); return { title: a.attr('title') || a.text(), diff --git a/lib/routes/chnmuseum/xwzt.ts b/lib/routes/chnmuseum/xwzt.ts index 1953957b3c..c8a6804dd6 100644 --- a/lib/routes/chnmuseum/xwzt.ts +++ b/lib/routes/chnmuseum/xwzt.ts @@ -28,11 +28,11 @@ export const route: Route = { const response = await ofetch('https://www.chnmuseum.cn/zx/xwzt/'); const $ = load(response); - const items = $('ul.cj_hd_zhanh li') + const items = $('ul.xly_list_ts li') .toArray() .map((item) => { item = $(item); - const a = item.find('div.cj_hd_biaoti a').first(); + const a = item.find('a.titles').first(); return { title: a.attr('title') || a.text(), diff --git a/lib/routes/chnmuseum/zl.tsx b/lib/routes/chnmuseum/zl.tsx index 65e8322850..b8a533b024 100644 --- a/lib/routes/chnmuseum/zl.tsx +++ b/lib/routes/chnmuseum/zl.tsx @@ -1,4 +1,5 @@ import { load } from 'cheerio'; +import dayjs from 'dayjs'; import type { Context } from 'hono'; import { renderToString } from 'hono/jsx/dom/server'; @@ -10,36 +11,26 @@ import { parseDate } from '@/utils/parse-date'; import { namespace } from './namespace'; const titleTagMap: Record = { - zhanlanyugao: '正在展出', // This is what the website used for current exhibition - ztzl: '主题展览', + zhanlanyugao: '正在展出', jbcl: '基本陈列', ztcl: '专题展览', lszl: '临时展览', - 'lszl/zdztzl': '临时展览 - 主题展览', - 'lszl/dfjpwwxl': '临时展览 - 精品文物展', - 'lszl/lswhxl': '临时展览 - 历史文化展', - 'lszl/kgfjxl': '临时展览 - 考古发现展', - 'lszl/kjcxz': '临时展览 - 科技创新展', - 'lszl/dywhxl': '临时展览 - 地域文化展', - 'lszl/jdmszpxl': '临时展览 - 经典美术展', - 'lszl/gjjlxl': '临时展览 - 国际交流展', + 'lszl/lswh': '临时展览 - 历史文化', + 'lszl/gjjl': '临时展览 - 国际交流', + 'lszl/zdzt': '临时展览 - 重大主题', + 'lszl/yscx': '临时展览 - 艺术创新', + gjzl: '国家展览', gbxz: '国博巡展', }; -// Formatting Function: Returns YYYY-MM-DD only if there are 3 valid numeric segments; otherwise, returns undefined. +// Formatting Function: Returns YYYY-MM-DD when there are 3 valid numeric segments that are formatted by parseExhibitionDate; otherwise, returns undefined. const formatExhibitionDate = (dateStr: string | undefined): string | undefined => { if (!dateStr) { return undefined; } - const normalized = dateStr - .replaceAll(/年|月/g, '-') - .replaceAll('日', '') - .replaceAll(/[/.]/g, '-'); - const parts = normalized.split('-').filter(Boolean); - if (parts.length === 3) { - return `${parts[0]}-${parts[1].padStart(2, '0')}-${parts[2].padStart(2, '0')}`; - } - return undefined; + const normalized = dateStr.replaceAll(/[年月/.]/g, '-').replaceAll('日', ''); + const d = dayjs(normalized); + return d.format('YYYY-MM-DD'); }; const parseExhibitionDuration = (duration: string) => { @@ -57,22 +48,20 @@ const parseExhibitionDuration = (duration: string) => { let startDateRaw: string | undefined; let endDateRaw: string | undefined; - if (cleanStr.startsWith('展至') && allDates.length > 0) { - endDateRaw = allDates[0]; - } else if (allDates.length >= 2) { + if (allDates.length >= 2) { startDateRaw = allDates[0]; let rawEnd = allDates[1]; // Logic to complete the year if (!/\d{4}/.test(rawEnd) && startDateRaw) { const startYear = startDateRaw.match(/^\d{4}/); - if (startYear) { - const sep = startDateRaw.includes('年') ? '年' : startDateRaw.includes('/') ? '/' : '-'; - rawEnd = `${startYear[0]}${sep}${rawEnd}`; + if (startYear?.[0]) { + rawEnd = `${startYear[0]}${startDateRaw[4]}${rawEnd}`; } } endDateRaw = rawEnd; } else if (allDates.length === 1) { - if (cleanStr.includes('展至')) { + if (cleanStr.includes('闭展')) { + // e.g. "2025年2月16日闭展" endDateRaw = allDates[0]; } else { startDateRaw = allDates[0]; @@ -101,23 +90,38 @@ const resolveRouteConfig = (type: string | undefined, subtype: string | undefine }; }; +// create itemLink +const buildItemLink = (rawLink: string, contextUrl: string, baseUrl: string) => (rawLink ? new URL(rawLink, contextUrl).href : baseUrl); + +// create exhibitionLink +const buildExhibitionLink = (rawZtzl: string, itemLink: string, baseUrl: string) => (rawZtzl ? new URL(rawZtzl, baseUrl).href : itemLink); + // to concurrent or single-page retrieval according to titletag const fetchTargetElements = async (cleanType: string, subtype: string | undefined, url: string, baseUrl: string) => { - const items: Array<{ $item: any; contextUrl: string }> = []; + const items: Array<{ $item: any; contextUrl: string; itemLink: string; exhibitionLink: string; rawZtzl: string }> = []; // Use a Set to track visited links and filter out duplicate HTML elements directly at the source // (e.g., when the same exhibition appears in both the main list and a specific sub-category list). const seenLinks = new Set(); const extractItems = (html: string, contextUrl: string) => { const $ = load(html); - const pageElements = $('ul[id="div"] a.recurl, ul.cj_hb_list a.recurl').toArray(); + const selectors = ['ul[id="div"] > li > a', 'li.scale_imgs > a'].join(', '); + + const pageElements = $(selectors).not('.zl_lszl_list *').toArray(); + for (const el of pageElements) { const $item = $(el).closest('li'); if ($item.length > 0) { - const href = $(el).attr('href') || ''; - if (href && !seenLinks.has(href)) { - seenLinks.add(href); - items.push({ $item, contextUrl }); + const rawLink = $(el).attr('href') || ''; + const rawZtzl = $(el).attr('ztzlurl')?.trim() || ''; // some exhibition links have a separate detailed page, use ztzlurl to get the detailed exhibition link if available + + // Use exhibitionLink to remove the repeat ones + const itemLink = buildItemLink(rawLink, contextUrl, baseUrl); + const exhibitionLink = buildExhibitionLink(rawZtzl, itemLink, baseUrl); + + if (exhibitionLink && !seenLinks.has(exhibitionLink)) { + seenLinks.add(exhibitionLink); + items.push({ $item, contextUrl, itemLink, exhibitionLink, rawZtzl }); } } } @@ -142,23 +146,15 @@ const fetchTargetElements = async (cleanType: string, subtype: string | undefine return items; }; -// create itemLink -const buildItemLink = (rawLink: string, contextUrl: string, baseUrl: string) => (rawLink ? new URL(rawLink, contextUrl).href : baseUrl); - -// create exhibitionLink -const buildExhibitionLink = (rawZtzl: string, itemLink: string, baseUrl: string) => (rawZtzl ? new URL(rawZtzl, baseUrl).href : itemLink); - export const route: Route = { path: '/zl/:type?/:subType?', categories: ['travel'], - example: '/chnmuseum/zl/lszl/zdztzl', + example: '/chnmuseum/zl/lszl/zdzt', parameters: { - type: 'Exhibition type, supported values: zhanlanyugao(正在展出)、ztzl(主题展览)、jbcl(基本陈列)、ztcl(专题展览)、lszl(临时展览)、gbxz(国博巡展). Default: All exhibitions.', - subType: - 'subtype only works under type lszl(临时展览), supported values: zdztzl(主题展览)、dfjpwwxl(精品文物展)、lswhxl(历史文化展)、kgfjxl(考古发现展)、kjcxz(科技创新展)、dywhxl(地域文化展)、jdmszpxl(经典美术展)、gjjlxl(国际交流展)', + type: 'Exhibition type, supported values: zhanlanyugao(正在展出)、jbcl(基本陈列)、ztcl(专题展览)、lszl(临时展览)、gjzl(国家展览)、gbxz(国博巡展). Default: All exhibitions.', + subType: 'subtype only works under type lszl(临时展览), supported values: zdzt(重大主题)、lswh(历史文化)、yscx(艺术创新)、gjjl(国际交流)', }, - // Use Chnmuseum English version channel name - name: 'Exhibitions', + name: 'Exhibitions', // Use Chnmuseum English version channel name maintainers: ['magazian'], radar: [ { @@ -167,8 +163,8 @@ export const route: Route = { }, ], handler: async (ctx: Context): Promise => { - const type = ctx.req.param('type')?.trim(); - const subtype = ctx.req.param('subType')?.trim(); + const type = ctx.req.param('type'); + const subtype = ctx.req.param('subType'); const museumName = namespace.zh?.name || namespace.name; const baseUrl = 'https://www.chnmuseum.cn'; @@ -178,11 +174,7 @@ export const route: Route = { const list = ( await Promise.all( itemsToParse.map(({ $item, contextUrl }) => { - const aTag = $item.find('a.recurl'); - - if (!aTag.length) { - return null; - } + const aTag = $item.find('a').first(); const rawLink = aTag.attr('href') || ''; const itemLink = buildItemLink(rawLink, contextUrl, baseUrl); @@ -194,56 +186,70 @@ export const route: Route = { // title may not have full display on the page, use the img alt information instead const imgTag = $item.find('img'); - const title = imgTag.attr('alt') || ''; + let title = $item.find('.hide_title').text() || ''; const rawSrc = imgTag.attr('src')!; const imgUrl = new URL(rawSrc, contextUrl).href; - const location = $item.find('div.cj_zxx1').text().trim(); + const hideBox = $item.find('.hide_box'); + const hideBoxes = hideBox.toArray().map((_, i) => hideBox.eq(i)); + const findValue = (keyword: string) => + hideBoxes + .find((box) => box.find('p').first().text().includes(keyword)) + ?.find('p') + .last() + .text() + .trim() ?? ''; - let fullDuration = $item.find('div.cj_zxx2 p').text().trim(); + const location = findValue('地点'); + let fullDuration = findValue('展期'); - if (fullDuration.endsWith('...')) { + if (!title || title.endsWith('...')) { const detailResponse = await got(itemLink); - const match = detailResponse.data.match(/var\s+qtxszq\s*=\s*"(.*?)";/); - if (match && match[1]) { - fullDuration = match[1]; + const $detail = load(detailResponse.data); + const detailTitle = $detail('.crumb_mod_box .title').text(); + title = + detailTitle || + $detail('title') + .text() + .replace(/-?\s*中国国家博物馆/, ''); + + if (!fullDuration) { + const textDuration = $detail('li, strong, p') + .toArray() + .map((el) => $detail(el).text().trim()) + .find((text) => text.startsWith('展期:') || text.includes('闭展')) + ?.replace('展期:', '') + .trim(); + const regexDuration = detailResponse.data.match(/var\s+qtxszq\s*=\s*"(.*?)";/)?.[1]; + fullDuration = textDuration || regexDuration || ''; } } const { startDate, endDate } = parseExhibitionDuration(fullDuration); // CHN museum didnot have pubDate on the page, use exhibition startDate instead. - // Variable Handling: If the value is `undefined`, it remains `undefined` to facilitate subsequent processing by the Calendar component. const pubDate = startDate ? parseDate(startDate) : undefined; const description = renderToString(
- { - <> - -
- - } + +

地点: - {location} + {location || '参考详情'}

开展: - {startDate ?? '未定/常设'} + {startDate || '未定/常设'}

闭展: - {endDate ?? '未定/常设'} + {endDate || '未定/常设'}

- {fullDuration && (

- - 原始展期: - {fullDuration} - + 原始展期:{fullDuration}

)}
@@ -260,11 +266,9 @@ export const route: Route = { // For further .ics file processing _extra: { museumName, - title, location, - startDate, // format: YYYY-MM-DD or '未定/常设' - endDate, // format: YYYY-MM-DD or '未定/常设' - itemLink, + startDate, + endDate, }, } as DataItem; }) as Promise;