Astro 博客集成豆瓣书影音展示功能完整教程 -Retypeset
9 min
Retypeset Theme
前言
很多博主都希望在自己的博客上展示豆瓣的读书、观影记录,但豆瓣官方 API 早已关闭,第三方服务如 mouban.mythsman.com 也相继停止运营。本文将介绍一套完整的解决方案,帮助你在 Astro 博客中实现豆瓣书影音展示功能。
实现原理
整体方案分为三个部分:
- 数据获取:通过豆瓣 RSS Feed 或自部署 API 获取用户的书影音数据
- 数据存储:将数据保存为 JSON 文件,构建时静态渲染
- 图片代理:解决豆瓣图片防盗链导致的 403 问题
为什么选择静态数据?
相比实时调用 API,静态数据方案有以下优势:
- 稳定性高:不依赖第三方服务的可用性
- 加载速度快:数据在构建时已经准备好
- 无 API 限制:不受请求频率限制
- 可离线编辑:可以手动补充或修改数据
项目结构
├── scripts/
│ └── fetch-douban.ts # 数据获取脚本
├── src/
│ ├── config.ts # 配置文件(豆瓣用户 ID)
│ ├── data/
│ │ ├── douban-books.json # 书籍数据
│ │ └── douban-movies.json # 电影数据
│ └── pages/
│ └── [...lang]/
│ └── other/
│ └── douban.astro # 展示页面
└── .github/
└── workflows/
└── fetch-douban.yml # 自动更新工作流第一步:配置豆瓣用户 ID
在 src/config.ts 中添加豆瓣配置:
export const themeConfig: ThemeConfig = {
// ... 其他配置
douban: {
// 豆瓣用户 ID
// 可在豆瓣个人主页 URL 中找到
// 如 https://www.douban.com/people/245847465/
userId: '245847465',
},
}第二步:创建数据获取脚本
创建 scripts/fetch-douban.ts:
/**
* 豆瓣数据获取脚本
*
* 支持两种数据源:
* 1. 豆瓣 RSS Feed (默认,无需配置)
* 2. 自定义 API (需要自行部署)
*/
import * as fs from 'node:fs'
import * as path from 'node:path'
// 配置区域
const API_BASE_URL = '' // 自定义 API 地址(可选)
const CONFIG_PATH = path.join(process.cwd(), 'src/config.ts')
const DATA_DIR = path.join(process.cwd(), 'src/data')
// 类型定义
interface DoubanItem {
id: string
title: string
cover: string
rating?: number
myRating?: number
status: 'done' | 'doing' | 'wish'
comment?: string
date?: string
author?: string
director?: string
year?: string
link?: string
type: 'book' | 'movie'
}
// 从 config.ts 提取 userId
function getUserId(): string {
const configContent = fs.readFileSync(CONFIG_PATH, 'utf-8')
const match = configContent.match(/userId:\s*['"]([^'"]+)['"]/)
return match ? match[1] : ''
}
// 解析状态
function parseStatus(title: string): 'done' | 'doing' | 'wish' {
if (title.startsWith('看过') || title.startsWith('读过'))
return 'done'
if (title.startsWith('在看') || title.startsWith('在读'))
return 'doing'
return 'wish'
}
// 从 RSS Feed 获取数据
async function fetchFromRSS(userId: string): Promise<DoubanItem[]> {
const rssUrl = `https://www.douban.com/feed/people/${userId}/interests`
const response = await fetch(rssUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
},
})
const xml = await response.text()
const items: DoubanItem[] = []
// 解析 XML
const itemMatches = xml.matchAll(/<item>([\s\S]*?)<\/item>/g)
for (const match of itemMatches) {
const itemXml = match[1]
const titleMatch = itemXml.match(/<title>(.*?)<\/title>/)
const linkMatch = itemXml.match(/<link>(.*?)<\/link>/)
const imgMatch = itemXml.match(/<img src="https://tpic2024.en.icu/Astro2026/public/content/posts/2025-12-15-astro-douban-integration/([^"]+)"/)
const pubDateMatch = itemXml.match(/<pubDate>(.*?)<\/pubDate>/)
if (!titleMatch || !linkMatch) continue
const fullTitle = titleMatch[1]
const link = linkMatch[1]
const idMatch = link.match(/subject\/(\d+)/)
items.push({
id: idMatch ? idMatch[1] : '',
title: fullTitle.replace(/^(想看|在看|看过|想读|在读|读过)/, '').trim(),
cover: imgMatch ? imgMatch[1].replace('/s/', '/m/') : '',
status: parseStatus(fullTitle),
type: link.includes('book.douban.com') ? 'book' : 'movie',
link,
date: pubDateMatch
? new Date(pubDateMatch[1]).toISOString().split('T')[0]
: undefined,
})
}
return items
}
// 主函数
async function main() {
const userId = getUserId()
if (!userId) {
console.error('未找到豆瓣用户 ID')
process.exit(1)
}
console.log(`获取豆瓣数据,用户 ID: ${userId}`)
// 确保数据目录存在
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true })
}
const items = await fetchFromRSS(userId)
// 分离电影和书籍
const movies = items.filter(item => item.type === 'movie')
const books = items.filter(item => item.type === 'book')
// 保存数据
fs.writeFileSync(
path.join(DATA_DIR, 'douban-movies.json'),
JSON.stringify(movies, null, 2)
)
fs.writeFileSync(
path.join(DATA_DIR, 'douban-books.json'),
JSON.stringify(books, null, 2)
)
console.log(`完成!电影 ${movies.length} 条,书籍 ${books.length} 条`)
}
main()在 package.json 中添加脚本命令:
{
"scripts": {
"fetch-douban": "tsx scripts/fetch-douban.ts"
}
}运行 pnpm fetch-douban 即可获取数据。
第三步:解决图片防盗链问题
豆瓣图片有防盗链机制,直接引用会返回 403 错误。我们的脚本采用本地存储方案,在获取数据时同时下载封面图片到 public/douban/ 目录。
本地存储方案(推荐)
脚本会自动:
- 下载豆瓣封面图片到
public/douban/目录 - 将数据中的
cover字段更新为本地路径(如/douban/1292052.webp) - 页面直接使用本地图片,完全避免 403 问题
优势:
- 完全避免 403:图片存储在本地,不受豆瓣限制
- 加载速度快:本地图片加载更快
- 离线可用:即使豆瓣服务不可用,图片仍然正常显示
- 版本控制:图片随代码一起管理
备用方案
如果不想下载图片到本地,也可以使用以下方案:
方案一:图片代理服务
function proxyImage(url: string): string {
return `https://wsrv.nl/?url=${encodeURIComponent(url)}`
}方案二:referrerpolicy
<img referrerpolicy="no-referrer" src={item.cover} />第四步:创建展示页面
创建 src/pages/[...lang]/other/douban.astro:
---
import Layout from '@/layouts/Layout.astro'
// 加载数据
let books = []
let movies = []
try {
const booksModule = await import('@/data/douban-books.json')
books = booksModule.default || []
} catch {}
try {
const moviesModule = await import('@/data/douban-movies.json')
movies = moviesModule.default || []
} catch {}
// 图片代理
function proxyImage(url) {
if (!url) return ''
return `https://wsrv.nl/?url=${encodeURIComponent(url)}`
}
---
<Layout>
<h1>书影音</h1>
<section>
<h2>电影 ({movies.length})</h2>
<div class="grid">
{movies.map((item) => (
<a href={item.link} target="_blank">
<img src={proxyImage(item.cover)} alt={item.title} />
<h3>{item.title}</h3>
</a>
))}
</div>
</section>
<section>
<h2>书籍 ({books.length})</h2>
<div class="grid">
{books.map((item) => (
<a href={item.link} target="_blank">
<img src={proxyImage(item.cover)} alt={item.title} />
<h3>{item.title}</h3>
</a>
))}
</div>
</section>
</Layout>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 1rem;
}
</style>第五步:设置自动更新(可选)
创建 .github/workflows/fetch-douban.yml:
name: Fetch Douban Data
on:
schedule:
- cron: '0 0 * * 1' # 每周一运行
workflow_dispatch: # 支持手动触发
jobs:
fetch-douban:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm fetch-douban
- name: Commit changes
run: |
git config user.email "action@github.com"
git config user.name "GitHub Action"
git add src/data/douban-*.json
git diff --staged --quiet || git commit -m "chore: update douban data"
git push数据格式说明
douban-movies.json
[
{
"id": "1292052",
"title": "肖申克的救赎",
"cover": "https://img2.doubanio.com/view/photo/s_ratio_poster/public/p480747492.jpg",
"rating": 9.7,
"myRating": 5,
"status": "done",
"director": "弗兰克·德拉邦特",
"actors": "蒂姆·罗宾斯 / 摩根·弗里曼",
"year": "1994",
"date": "2024-02-10",
"comment": "希望是个好东西",
"link": "https://movie.douban.com/subject/1292052/"
}
]字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 豆瓣条目 ID |
| title | string | 标题 |
| cover | string | 封面图片 URL |
| rating | number | 豆瓣评分 |
| myRating | number | 我的评分(1-5) |
| status | string | 状态:done/doing/wish |
| director | string | 导演(电影) |
| author | string | 作者(书籍) |
| year | string | 年份 |
| date | string | 标记日期 |
| comment | string | 短评 |
| link | string | 豆瓣链接 |
进阶:自部署 API
如果 RSS Feed 数据量不够,可以自己部署一个 Cloudflare Worker 来代理豆瓣数据。
创建 scripts/douban-worker.js 并部署到 Cloudflare Workers:
const DOUBAN_HEADERS = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)',
'Referer': 'https://m.douban.com/',
}
export default {
async fetch(request) {
const url = new URL(request.url)
const pathMatch = url.pathname.match(/^\/(movies|books)\/(\d+)$/)
if (!pathMatch) {
return new Response(JSON.stringify({ error: 'Invalid path' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
const [, category, userId] = pathMatch
const type = url.searchParams.get('type') || 'done'
const doubanUrl = category === 'movies'
? `https://m.douban.com/people/${userId}/movie/${type}`
: `https://m.douban.com/people/${userId}/book/${type}`
const response = await fetch(doubanUrl, { headers: DOUBAN_HEADERS })
const html = await response.text()
// 解析 HTML 并返回 JSON
// ... 解析逻辑
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
})
},
}常见问题
Q: RSS Feed 只能获取最近的数据?
A: 是的,豆瓣 RSS Feed 只返回最近约 10 条记录。如需完整数据,可以:
- 手动编辑 JSON 文件补充历史数据
- 自部署 API 获取完整列表
Q: 图片还是显示不出来?
A: 检查以下几点:
- 确认使用了图片代理或设置了 referrerpolicy
- 检查图片 URL 是否正确
- 尝试清除浏览器缓存
Q: 如何添加更多字段?
A: 直接编辑 JSON 文件即可,页面会自动读取新字段。
总结
通过本教程,你已经学会了:
- 使用 RSS Feed 获取豆瓣数据
- 解决图片防盗链问题
- 创建书影音展示页面
- 设置 GitHub Actions 自动更新
这套方案的优点是不依赖任何第三方服务,数据完全由自己掌控,稳定可靠。
如果你有任何问题,欢迎在评论区留言讨论!