UNPKG

rsshub

Version:
258 lines (224 loc) 9.16 kB
import { Route } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; import { load } from 'cheerio'; import { parseRelativeDate } from '@/utils/parse-date'; const hostUrl = 'http://www.moa.gov.cn/'; const hostUrlObj = new URL(hostUrl); // 用于在下面判断 host export const route: Route = { path: '/moa/suburl/:suburl{.+}', categories: ['government'], example: '/gov/moa/suburl/gk/zcjd/', radar: [ { source: ['moa.gov.cn/'], target: '/moa/suburl/:suburl', }, ], parameters: { suburl: '下级目录,请使用最下级的目录' }, name: '中华人民共和国农业农村部 - 新闻', maintainers: ['Origami404', 'lyqluis'], handler, url: 'moa.gov.cn/', description: `更多例子: - \`农业农村部动态\`的网页链接是\`http://www.moa.gov.cn/xw/zwdt/\`, 对应的\`suburl\`是\`xw/zwdt\` - \`财务公开\`的网页链接是\`http://www.moa.gov.cn/gk/cwgk_1/\`, 对应的\`suburl\`是\`gk/cwgk_1\` - 像[政策法规](http://www.moa.gov.cn/gk/zcfg/)这种页面(\`http://www.moa.gov.cn/gk/zcfg/\`), 它**不是**一个合法的分类目录,它是\`法律\`, \`行政法规\`, \`部门规章\`等一堆栏目的集合,这时候请点开对应栏目的\`更多 >>\`进入栏目的最下级目录,再根据上面的规则提取\`suburl\` - 特别地,\`图片新闻\`对应的\`suburl\`为\`xw/tpxw/\`, \`最新公开\`对应的\`suburl\`为\`govpublic\`, \`数据>最新发布\`对应的\`suburl\`为\`sj/zxfb\``, }; async function handler(ctx) { const rawSuburl = ctx.req.param('suburl'); const suburl = rawSuburl.slice(-1) === '/' ? rawSuburl : rawSuburl + '/'; // 特殊处理两个,其他的栏目都可以找到那种一个列表下去的目录 if (suburl === 'xw/tpxw/') { // 图片新闻 return await dealChannel(suburl, { channelTitleSelector: '.pub-media2-head', listSelector: '.tupian_list li', titleSelector: 'a[class="block w_fill ellipsis adc ahc"]', dateSelector: 'span', }); } else if (suburl.startsWith('sj/zxfb')) { // 数据 - 最新发布 return await dealLatestDataChannel(); } else if (suburl.startsWith('gk')) { // 公开 return await dealChannel(suburl, { channelTitleSelector: 'title', listSelector: '.commonlist li', titleSelector: 'a', dateSelector: 'span', }); } else if (suburl.startsWith('govpublic')) { // 最新公开 return await dealChannel('govpublic/1/index.htm', { channelTitleText: '最新公开', listSelector: '.commonlist li', titleSelector: 'a', dateSelector: 'span', }); } else { return await dealChannel(suburl, { channelTitleSelector: '.pub-media1-head-title', listSelector: '.ztlb', titleSelector: 'a', dateSelector: 'span', }); } } // 处理文章列表,从那里获得一堆要爬取的页面,然后爬取 async function dealChannel(suburl, selectors) { const { channelTitleSelector, listSelector, titleSelector, dateSelector, channelTitleText } = selectors; // 为了与下面解析相对链接的 dealLink 配合,这里末尾必须保证有一条斜杠 const url = suburl.startsWith('http') ? suburl : hostUrl + suburl; const response = await got.get(url); const $ = load(response.data); const channelTitle = channelTitleText ?? $(channelTitleSelector).text(); const pageInfos = $(listSelector) .toArray() .map((e) => { const element = $(e); const titleElement = element.find(titleSelector); const title = titleElement.text(); const [link, pageType] = dealLink(titleElement, url); const dateraw = element.find(dateSelector).text().trim(); return { pageType, title, link, // 先在这里获取一个精确到日的时间 // 如果是正常文章的话可以在那里提取更精确的时间 // 如果是公示文章或者站外文章的话只能用这个保底了 pubDate: parseRelativeDate(dateraw), }; }); const items = await Promise.all( pageInfos.map(async (item) => { const link = item.link; const cacheIn = await cache.get(link); if (cacheIn) { return JSON.parse(cacheIn); } if (item.pageType === 'normal') { // 正常文章 item = await dealNormalPage(link, item); } else if (item.pageType === 'govpublic') { // 公示文章 item = await dealGovpublicPage(link, item); } else { // 外部文章 item.description = `外部链接:${item.link}`; } cache.set(link, JSON.stringify(item)); return item; }) ); return { title: `中华人民共和国农业农村部 - ${channelTitle}`, link: url, item: items, }; } // 处理正常文章,例子:http://www.moa.gov.cn/xw/zwdt/202309/t20230915_6436615.htm async function dealNormalPage(link, item) { const response = await got.get(link); const $ = load(response.data); // 互动-直播访谈 if (link.includes('zbft')) { const pageHeader = $('.nybzb').html() ?? ''; const pics = $('.tpsl').html() ?? ''; const content = $('.wzsl').html() ?? ''; item.description = pageHeader + pics + content; return item; } // normal news const metaElements = $('.bjjMAuthorBox span.dc_2').toArray(); // 政府网站变动不频繁,写死第几个应该没有多大关系 const author = $(metaElements[1]).text(); const source = $(metaElements[2]).text(); item.author = `${author} ${source}`; // 对于这个网站内的链接,能提供更精确的时间 // 这个的日期跟时间之间的空格数量好像会乱变的 const exactTime = $(metaElements[0]).text(); const dateMatch = /\d{4}-\d{2}-\d{2}/.exec(exactTime); const timeMatch = /\d{2}:\d{2}/.exec(exactTime); item.pubDate = parseRelativeDate(`${dateMatch[0]} ${timeMatch[0]}`); item.description = $('.arc_body').html(); return item; } // 处理那种带索引号的公示文章,例子:http://www.moa.gov.cn/gk/zcjd/202402/t20240219_6448654.htm async function dealGovpublicPage(link, item) { if (item.link.endsWith('.pdf')) { return item; } const response = await got.get(link); const $ = load(response.data); const body = $('.gsj_htmlcon_bot'); const [, year, month, date] = $('.pubtime') .text() .match(/:(\d{4})[|年-](\d{1,2})[|月-](\d{1,2})日?/); const [, author] = $('.pubtime.source') ?.text() ?.match(/:(.+)/) ?? [null, '']; if (year && month && date) { item.pubDate = `${year}-${month}-${date}`; } item.author = author; item.description = body.html(); return item; } async function dealLatestDataChannel() { const res = await got({ url: 'http://zdscxx.moa.gov.cn:8080/nyb/getMessages', method: 'post', json: { page: 1, rows: 20, type: '最新发布', isLatestMessage: true, }, }); const items = await Promise.all( res.data.result.table.map((item) => { const { date, id } = item; item.pubDate = date; const link = (item.link = `http://zdscxx.moa.gov.cn:8080/nyb/pc/messageView.jsp?id=${id}`); return cache.tryGet(link, async () => { const { content, source } = await getLatestDataArticleDetail(id); item.description = content; item.author = source; return item; }); }) ); return { title: `中华人民共和国农业农村部 - 数据 - 最新发布`, link: 'http://zdscxx.moa.gov.cn:8080/nyb/pc/messageList.jsp', item: items, }; } async function getLatestDataArticleDetail(id) { const res = await got({ url: 'http://zdscxx.moa.gov.cn:8080/nyb/getMessagesById', method: 'post', form: { id, }, }); return res.data.result; } // 处理相对 url 和 按链接对文章类型进行分类 function dealLink(element, url) { const rawLink = element.attr('href'); const { host, href } = new URL(rawLink, url); // host 不同的是外部文章,outside // url 里带 govpublic 的都是公示文章,govpublic // 其他的都算普通文章,normal let pageType = null; if (host === hostUrlObj.host) { pageType = href.includes('gk') || href.includes('govpublic') ? 'govpublic' : 'normal'; } else { pageType = 'outside'; } return [href, pageType]; }