mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-09-24 23:12:34 +08:00
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:
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user