newechoes/src/pages/api/douban.ts

301 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { APIRoute } from 'astro';
import { load } from 'cheerio';
// 添加服务器渲染标记
export const prerender = false;
// 请求配置常量
const MAX_RETRIES = 0; // 最大重试次数
const RETRY_DELAY = 1500; // 重试延迟(毫秒)
const REQUEST_TIMEOUT = 10000; // 请求超时时间(毫秒)
// 生成随机的bid Cookie值
function generateBid() {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < 11; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
// 添加延迟函数
function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 带超时的 fetch 函数
async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs: number) {
const controller = new AbortController();
const { signal } = controller;
const timeout = setTimeout(() => {
controller.abort();
}, timeoutMs);
try {
const response = await fetch(url, {
...options,
signal
});
return response;
} finally {
clearTimeout(timeout);
}
}
export const GET: APIRoute = async ({ request }) => {
const url = new URL(request.url);
const type = url.searchParams.get('type') || 'movie';
const start = parseInt(url.searchParams.get('start') || '0');
const doubanId = url.searchParams.get('doubanId'); // 从查询参数获取 doubanId
if (!doubanId) {
return new Response(JSON.stringify({ error: '缺少豆瓣ID' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, max-age=0'
}
});
}
// 添加缓存键的构建,用于区分不同的请求
const cacheKey = `douban_${type}_${doubanId}_${start}`;
// 尝试从缓存获取数据
try {
// 如果有缓存系统,可以在这里检查和返回缓存数据
// 重试逻辑
let retries = 0;
let lastError: Error | null = null;
while (retries <= MAX_RETRIES) {
try {
let doubanUrl = '';
if (type === 'book') {
doubanUrl = `https://book.douban.com/people/${doubanId}/collect?start=${start}&sort=time&rating=all&filter=all&mode=grid`;
} else {
doubanUrl = `https://movie.douban.com/people/${doubanId}/collect?start=${start}&sort=time&rating=all&filter=all&mode=grid`;
}
// 生成随机bid
const bid = generateBid();
// 随机化一些请求参数,减少被检测的风险
const userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0'
];
const randomUserAgent = userAgents[Math.floor(Math.random() * userAgents.length)];
// 使用带超时的fetch发送请求
const response = await fetchWithTimeout(doubanUrl, {
headers: {
'User-Agent': randomUserAgent,
'Sec-Fetch-Site': 'none',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?1',
'Sec-Fetch-Dest': 'document',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Cache-Control': 'max-age=0',
'Cookie': `bid=${bid}`
}
}, REQUEST_TIMEOUT);
if (!response.ok) {
throw new Error(`豆瓣请求失败,状态码: ${response.status}`);
}
const html = await response.text();
// 检查是否包含验证码页面的特征
if (html.includes('验证码') || html.includes('captcha') || html.includes('too many requests')) {
throw new Error('请求被豆瓣限制,需要验证码');
}
const $ = load(html);
// 添加类型定义
interface DoubanItem {
imageUrl: string;
title: string;
subtitle: string;
link: string;
intro: string;
rating: number;
date: string;
}
const items: DoubanItem[] = [];
// 尝试不同的选择器
let itemSelector = '.item.comment-item';
let itemCount = $(itemSelector).length;
if (itemCount === 0) {
// 尝试其他可能的选择器
itemSelector = '.subject-item';
itemCount = $(itemSelector).length;
}
if (itemCount === 0) {
// 如果两个选择器都没有找到内容,可能是页面结构变化或被封锁
console.error('未找到内容,页面结构可能已变化');
// 记录HTML以便调试
console.debug('HTML片段:', html.substring(0, 500) + '...');
if (retries < MAX_RETRIES) {
retries++;
// 增加重试延迟,避免频繁请求
await delay(RETRY_DELAY * retries);
continue;
} else {
throw new Error('未找到电影/图书内容,可能是页面结构已变化');
}
}
$(itemSelector).each((_, element) => {
const $element = $(element);
try {
// 根据选择器调整查找逻辑
let imageUrl = '';
let title = '';
let subtitle = '';
let link = '';
let intro = '';
let rating = 0;
let date = '';
if (itemSelector === '.item.comment-item') {
// 原始逻辑
imageUrl = $element.find('.pic img').attr('src') || '';
title = $element.find('.title a em').text().trim();
subtitle = $element.find('.title a').text().replace(title, '').trim();
link = $element.find('.title a').attr('href') || '';
intro = $element.find('.intro').text().trim();
// 获取评分从rating1-t到rating5-t
for (let i = 1; i <= 5; i++) {
if ($element.find(`.rating${i}-t`).length > 0) {
rating = i;
break;
}
}
date = $element.find('.date').text().trim();
} else if (itemSelector === '.subject-item') {
// 新的图书页面结构
imageUrl = $element.find('.pic img').attr('src') || '';
title = $element.find('.info h2 a').text().trim();
link = $element.find('.info h2 a').attr('href') || '';
intro = $element.find('.info .pub').text().trim();
// 获取评分
const ratingClass = $element.find('.rating-star').attr('class') || '';
const ratingMatch = ratingClass.match(/rating(\d)-t/);
if (ratingMatch) {
rating = parseInt(ratingMatch[1]);
}
date = $element.find('.info .date').text().trim();
}
// 确保所有字段至少有空字符串
items.push({
imageUrl: imageUrl || '',
title: title || '',
subtitle: subtitle || '',
link: link || '',
intro: intro || '',
rating: rating || 0,
date: date || ''
});
} catch (error) {
console.error('解析项目时出错:', error);
// 继续处理下一个项目,而不是终止整个循环
}
});
// 改进分页信息获取逻辑
let currentPage = 1;
let totalPages = 1;
// 尝试从当前页码元素获取信息
if ($('.paginator .thispage').length > 0) {
currentPage = parseInt($('.paginator .thispage').text() || '1');
// 豆瓣可能不直接提供总页数,需要计算
const paginatorLinks = $('.paginator a');
let maxPage = currentPage;
paginatorLinks.each((_, el) => {
const pageNum = parseInt($(el).text());
if (!isNaN(pageNum) && pageNum > maxPage) {
maxPage = pageNum;
}
});
totalPages = maxPage;
}
const pagination = {
current: currentPage,
total: totalPages,
hasNext: $('.paginator .next a').length > 0,
hasPrev: $('.paginator .prev a').length > 0
};
// 如果有缓存系统,可以在这里保存数据到缓存
return new Response(JSON.stringify({ items, pagination }), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, s-maxage=300', // 5分钟服务器缓存
'CDN-Cache-Control': 'public, max-age=300' // CDN缓存
}
});
} catch (error) {
console.error(`尝试第 ${retries + 1}/${MAX_RETRIES + 1} 次失败:`, error);
lastError = error instanceof Error ? error : new Error(String(error));
if (retries < MAX_RETRIES) {
retries++;
// 增加重试延迟,避免频繁请求
await delay(RETRY_DELAY * retries);
} else {
break;
}
}
}
// 所有尝试都失败了
console.error('所有尝试都失败了:', lastError);
return new Response(JSON.stringify({
error: '获取豆瓣数据失败',
message: lastError?.message || '未知错误'
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, max-age=0'
}
});
} catch (error) {
console.error('处理请求时出错:', error);
return new Response(JSON.stringify({
error: '获取豆瓣数据失败',
message: error instanceof Error ? error.message : '未知错误'
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, max-age=0'
}
});
}
}