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
This commit is contained in:
Jiamin
2026-07-12 00:34:07 +08:00
committed by GitHub
parent 48f7d4cf4c
commit af7f36d44c
3 changed files with 88 additions and 84 deletions
+4 -4
View File
@@ -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(),
+2 -2
View File
@@ -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(),
+82 -78
View File
@@ -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<string, string> = {
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<string>();
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<Data> => {
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(
<div>
{
<>
<img src={imgUrl} />
<br />
</>
}
<img src={imgUrl} />
<br />
<p>
<b>地点:</b>
{location}
{location || '参考详情'}
</p>
<p>
<b>开展:</b>
{startDate ?? '未定/常设'}
{startDate || '未定/常设'}
</p>
<p>
<b>闭展:</b>
{endDate ?? '未定/常设'}
{endDate || '未定/常设'}
</p>
{fullDuration && (
<p>
<small>
原始展期:
{fullDuration}
</small>
<small>原始展期:{fullDuration}</small>
</p>
)}
</div>
@@ -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<DataItem>;