diff --git a/lib/config.ts b/lib/config.ts index 9227906df..87a526083 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -351,6 +351,9 @@ export type Config = { device_id?: string; refresh_token?: string; }; + xiaohongshu: { + cookie?: string; + }; ximalaya: { token?: string; }; @@ -772,6 +775,9 @@ const calculateValue = () => { device_id: envs.XIAOYUZHOU_ID, refresh_token: envs.XIAOYUZHOU_TOKEN, }, + xiaohongshu: { + cookie: envs.XIAOHONGSHU_COOKIE, + }, ximalaya: { token: envs.XIMALAYA_TOKEN, }, diff --git a/lib/routes/xiaohongshu/notes.ts b/lib/routes/xiaohongshu/notes.ts index a4d85a5d5..18d028819 100644 --- a/lib/routes/xiaohongshu/notes.ts +++ b/lib/routes/xiaohongshu/notes.ts @@ -1,9 +1,12 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import { getNotes, formatText, formatNote } from './util'; +import { formatNote, formatText, getNotes } from './util'; +import { config } from '@/config'; +import got from '@/utils/got'; +import { load } from 'cheerio'; export const route: Route = { - path: '/user/:user_id/notes/fulltext', + path: '/user/:user_id/notes/:fulltext', radar: [ { source: ['xiaohongshu.com/user/profile/:user_id'], @@ -17,23 +20,150 @@ export const route: Route = { features: { antiCrawler: true, requirePuppeteer: true, + requireConfig: [ + { + name: 'XIAOHONGSHU_COOKIE', + optional: true, + description: '小红书 cookie 值,可在网络里面看到。', + }, + ], }, parameters: { user_id: 'user id, length 24 characters', + fulltext: { + description: '是否获取全文', + default: '', + }, }, }; async function handler(ctx) { const userId = ctx.req.param('user_id'); const url = `https://www.xiaohongshu.com/user/profile/${userId}`; - - const { user, notes } = await getNotes(url, cache); - - return { - title: `${user.nickname} - 笔记 • 小红书 / RED`, - description: formatText(user.desc), - image: user.imageb || user.images, - link: url, - item: notes.map((item) => formatNote(url, item)), - }; + const cookie = config.xiaohongshu.cookie; + // 获取详情需要cookie登录 + if (cookie && ctx.req.param('fulltext')) { + const urlNotePrefix = 'https://www.xiaohongshu.com/explore'; + // 获取用户的笔记信息 + const user = await getUser(url, cookie); + const notes = await renderNotesFulltext(user.notes, urlNotePrefix); + return { + title: `${user.userPageData.basicInfo.nickname} - 笔记 • 小红书 / RED`, + description: user.userPageData.basicInfo.desc, + image: user.userPageData.basicInfo.imageb || user.userPageData.basicInfo.images, + link: url, + item: notes, + }; + } else { + const { user, notes } = await getNotes(url, cache); + return { + title: `${user.nickname} - 笔记 • 小红书 / RED`, + description: formatText(user.desc), + image: user.imageb || user.images, + link: url, + item: notes.map((item) => formatNote(url, item)), + }; + } +} + +/** + * 获取所有笔记 + * @param notes 用户笔记信息 + * @param urlPrex 笔记详情前缀 + */ +async function renderNotesFulltext(notes, urlPrex) { + const data = []; + const promises = notes.flatMap((note) => + note.map(async ({ noteCard, id }) => { + const link = `${urlPrex}/${id}`; + const { title, description, pubDate } = await getFullNote(link); + return { + title, + link, + description, + author: noteCard.user.nickName, + guid: noteCard.noteId, + pubDate, + }; + }) + ); + data.push(...(await Promise.all(promises))); + return data; +} + +/** + * 获取笔记全文 + * @param link + */ +async function getFullNote(link) { + const cookie = config.xiaohongshu.cookie; + const data = (await cache.tryGet(link, async () => { + const res = await got(link, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + Cookie: cookie, + }, + }); + const $ = load(res.data); + let script = $('script') + .filter((i, script) => { + const text = script.children[0]?.data; + return text?.startsWith('window.__INITIAL_STATE__='); + }) + .text(); + script = script.slice('window.__INITIAL_STATE__='.length); + script = script.replaceAll('undefined', 'null'); + const state = JSON.parse(script); + const note = state.note.noteDetailMap[state.note.firstNoteId].note; + const images = note.imageList.map((image) => image.urlDefault); + const title = note.title; + let desc = note.desc; + desc = desc.replaceAll(/\[.*?\]/g, ''); + desc = desc.replaceAll(/#(.*?)#/g, '#$1'); + desc = desc.replaceAll('\n', '
'); + const pubDate = new Date(note.time); + const description = `${images.map((image) => ``).join('')}
${title}
${desc}`; + return { + title, + description, + pubDate, + }; + })) as Promise<{ title: string; description: string; pubDate: Date }>; + return data; +} + +/** + * 获取用户信息和笔记列表信息 + * @param url + * @param cookie + */ +async function getUser(url: string, cookie: string) { + const res = await got(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + Cookie: cookie, + }, + }); + const $ = load(res.data); + // 取出dom里面的信息,防止反爬 + const paths = $('#userPostedFeeds > section > div > a.cover.ld.mask').map((i, item) => item.attributes[3].value); + let script = $('script') + .filter((i, script) => { + const text = script.children[0]?.data; + return text?.startsWith('window.__INITIAL_STATE__='); + }) + .text(); + script = script.slice('window.__INITIAL_STATE__='.length); + script = script.replaceAll('undefined', 'null'); + const state = JSON.parse(script); + let index = 0; + // 将 dom里面的随机后缀补到每个文章id后面,防止反爬 + for (const item of state.user.notes.flat()) { + const path = paths[index]; + if (path && path.includes('?')) { + item.id = item.id + path?.substring(path.indexOf('?')); + } + index = index + 1; + } + return state.user; }