一次关于故事书能否起号做网站的探讨
问1
这是一本故事书(里面有很多图片,左侧图片,右侧内容),我准备用前端技术来实现故事书翻页等效果,
现在我增加难度,我需要朗读这个故事书,而且当朗读到这一页末尾时自动翻页,当然用户也可以暂停,暂停后朗读和翻页都暂停。
看看怎么实现,感觉最难的应该是 朗读跟踪。
答1
这是一个很有趣的项目!实现故事书朗读和自动翻页确实有一定技术挑战,特别是朗读跟踪部分。让我为你分析一下技术实现方案:
核心技术方案
1. 语音合成 (Text-to-Speech)
// 使用 Web Speech API
const synth = window.speechSynthesis;
const utterance = new SpeechSynthesisUtterance();
// 配置语音参数
utterance.rate = 0.8; // 语速
utterance.pitch = 1; // 音调
utterance.volume = 1; // 音量
utterance.lang = 'zh-CN'; // 语言
2. 朗读跟踪的关键实现
方案一:基于时间估算
class StoryReader {
constructor() {
this.currentPage = 0;
this.isReading = false;
this.isPaused = false;
this.readingTimer = null;
}
// 估算文本朗读时间
estimateReadingTime(text) {
const wordsPerMinute = 150; // 中文约150字/分钟
const characters = text.length;
return (characters / wordsPerMinute) * 60 * 1000; // 转换为毫秒
}
// 开始朗读当前页
startReading(pageText) {
if (this.isPaused) return;
const utterance = new SpeechSynthesisUtterance(pageText);
const estimatedTime = this.estimateReadingTime(pageText);
// 朗读结束事件
utterance.onend = () => {
if (!this.isPaused) {
this.nextPage();
}
};
speechSynthesis.speak(utterance);
this.isReading = true;
}
}
方案二:基于语音事件(推荐)
class AdvancedStoryReader {
constructor() {
this.currentPage = 0;
this.isReading = false;
this.isPaused = false;
this.currentUtterance = null;
}
// 分句朗读,更精确的控制
readPageWithTracking(pageText) {
const sentences = this.splitIntoSentences(pageText);
let currentSentenceIndex = 0;
const readNextSentence = () => {
if (this.isPaused || currentSentenceIndex >= sentences.length) {
return;
}
const utterance = new SpeechSynthesisUtterance(sentences[currentSentenceIndex]);
utterance.onend = () => {
currentSentenceIndex++;
if (currentSentenceIndex >= sentences.length) {
// 当前页朗读完成,自动翻页
this.nextPage();
} else {
// 继续朗读下一句
setTimeout(readNextSentence, 200); // 短暂停顿
}
};
utterance.onerror = (event) => {
console.error('朗读错误:', event);
};
this.currentUtterance = utterance;
speechSynthesis.speak(utterance);
};
readNextSentence();
}
// 分割文本为句子
splitIntoSentences(text) {
return text.split(/[。!?.!?]/).filter(sentence => sentence.trim().length > 0);
}
// 暂停朗读
pause() {
this.isPaused = true;
speechSynthesis.pause();
}
// 恢复朗读
resume() {
this.isPaused = false;
speechSynthesis.resume();
}
// 停止朗读
stop() {
this.isPaused = true;
this.isReading = false;
speechSynthesis.cancel();
}
}
3. 翻页动画效果
/* 翻页动画 */
.storybook-page {
transition: transform 0.6s ease-in-out;
transform-style: preserve-3d;
}
.page-flip-left {
transform: rotateY(-180deg);
}
.page-flip-right {
transform: rotateY(180deg);
}
/* 3D翻页效果 */
.page-container {
perspective: 1000px;
position: relative;
}
4. 完整的故事书组件
class StoryBook {
constructor(pages) {
this.pages = pages;
this.currentPageIndex = 0;
this.reader = new AdvancedStoryReader();
this.isAutoReading = false;
}
// 开始自动朗读
startAutoReading() {
this.isAutoReading = true;
this.readCurrentPage();
}
// 朗读当前页
readCurrentPage() {
if (!this.isAutoReading) return;
const currentPage = this.pages[this.currentPageIndex];
this.reader.readPageWithTracking(currentPage.text);
}
// 翻到下一页
nextPage() {
if (this.currentPageIndex < this.pages.length - 1) {
this.currentPageIndex++;
this.animatePageFlip();
// 翻页完成后继续朗读
setTimeout(() => {
if (this.isAutoReading) {
this.readCurrentPage();
}
}, 600); // 等待翻页动画完成
} else {
// 故事结束
this.stopAutoReading();
}
}
// 翻页动画
animatePageFlip() {
const pageElement = document.querySelector('.current-page');
pageElement.classList.add('page-flip-left');
setTimeout(() => {
this.updatePageContent();
pageElement.classList.remove('page-flip-left');
}, 300);
}
// 暂停/恢复
togglePause() {
if (this.reader.isPaused) {
this.reader.resume();
} else {
this.reader.pause();
}
}
// 停止自动朗读
stopAutoReading() {
this.isAutoReading = false;
this.reader.stop();
}
}
关键技术难点解决方案
1. 朗读跟踪精度
- 问题:如何准确知道朗读进度
- 解决:使用
SpeechSynthesisUtterance
的事件回调,分句朗读提高精度
2. 暂停恢复控制
- 问题:暂停后如何精确恢复
- 解决:使用
speechSynthesis.pause()
和speechSynthesis.resume()
3. 跨浏览器兼容性
// 检查浏览器支持
if ('speechSynthesis' in window) {
// 支持语音合成
} else {
// 降级方案:使用第三方TTS服务
console.warn('浏览器不支持语音合成,使用备用方案');
}
4. 性能优化
- 预加载下一页内容
- 语音缓存机制
- 分页懒加载
使用示例
// 初始化故事书
const storyPages = [
{ text: "从前有一个小男孩...", image: "page1.jpg" },
{ text: "他住在一个美丽的村庄里...", image: "page2.jpg" },
// 更多页面...
];
const storyBook = new StoryBook(storyPages);
// 开始自动朗读
document.getElementById('start-reading').addEventListener('click', () => {
storyBook.startAutoReading();
});
// 暂停/恢复
document.getElementById('pause-resume').addEventListener('click', () => {
storyBook.togglePause();
});
这个方案的优势:
- 精确控制:分句朗读,精确跟踪进度
- 用户体验:流畅的翻页动画和语音控制
- 可扩展性:易于添加更多功能(如语速调节、语音选择等)
- 兼容性:基于标准Web API,兼容性良好
问2
使用 Web Speech API 是谁提供的? 我做的是个网站,可能是各种浏览器, 同时如果要多音色 怎么办?第三方?费用如何?
答2
用户询问Web Speech API的提供方、浏览器兼容性、多音色支持和第三方方案费用,我需要搜索最新的相关信息来提供准确的答案。
toolName: web_search
status: success
query: Web Speech API browser support compatibility 2024 multiple voices third party TTS services pricing
现在搜索一些第三方TTS服务的具体价格信息,以便给用户提供更全面的对比。
toolName: web_search
status: success
query: Azure Speech Services Amazon Polly ElevenLabs TTS pricing comparison 2024
关于Web Speech API和第三方TTS服务的详细解答:
Web Speech API 提供方
Web Speech API是由W3C(万维网联盟)标准化的浏览器原生API
- Chrome/Edge: 使用Google的语音服务
- Safari: 使用Apple的语音服务
- Firefox: 有限支持
- 移动浏览器: 通常使用设备的系统语音服务
浏览器兼容性现状
支持情况
- ✅ Chrome: 全面支持,包括语音识别和合成
- ✅ Safari: 支持语音合成,语音识别有限
- ✅ Edge: 全面支持
- ⚠️ Firefox: 部分支持语音合成
- ✅ 移动浏览器: iOS Safari、Android Chrome支持良好
注意事项
- Chrome的语音识别需要网络连接(基于服务器)
2 - 不同浏览器可用的语音数量和质量差异很大
4
多音色解决方案
Web Speech API的局限性
// 获取可用语音
const voices = speechSynthesis.getVoices();
console.log(voices.length); // 数量因浏览器而异
// 不同浏览器的语音差异很大
voices.forEach(voice => {
console.log(`${voice.name} - ${voice.lang} - ${voice.localService}`);
});
问题:
- 语音数量有限(通常10-50个)
- 质量参差不齐
- 跨浏览器不一致
- 无法自定义语音
第三方TTS服务对比
1. Google Cloud Text-to-Speech
价格
- WaveNet语音:前100万字符免费/月,之后$16/100万字符
- 标准语音:前400万字符免费/月,之后$4/100万字符
- 380+种语音,50+种语言
2. Microsoft Azure Speech Services
价格
- Neural语音:$15/100万字符
- 免费层:每月5小时音频(约22.5万字符)
- 支持语音克隆和自定义语音
3. Amazon Polly
价格
- Neural语音:$16/100万字符
- 标准语音:$4/100万字符
- 免费层:前12个月100万字符/月
4. ElevenLabs
价格
- 订阅制:$5-99/月
- 免费层:1万字符/月
- 最高质量的语音合成,支持语音克隆
- 平均每本书转换成本$21-35
2
5. OpenAI TTS
价格
- $6.35/100万字符
- 高质量语音合成
推荐方案
混合方案(推荐)
class HybridTTSService {
constructor() {
this.fallbackOrder = [
'elevenlabs', // 最高质量
'azure', // 平衡性价比
'webspeech' // 免费备选
];
}
async speak(text, options = {}) {
// 根据用户偏好和可用性选择服务
for (const service of this.fallbackOrder) {
try {
if (service === 'webspeech' && 'speechSynthesis' in window) {
return this.useWebSpeech(text, options);
} else if (service === 'elevenlabs' && this.hasApiKey('elevenlabs')) {
return await this.useElevenLabs(text, options);
} else if (service === 'azure' && this.hasApiKey('azure')) {
return await this.useAzure(text, options);
}
} catch (error) {
console.warn(`${service} failed, trying next option`);
}
}
}
}
成本优化建议
- 开发阶段:使用Web Speech API免费测试
- 小规模应用:Azure或Google(有免费额度)
- 高质量需求:ElevenLabs
- 大规模应用:Azure/AWS/Google(按量付费更经济)
实际成本估算
以一本9万字的故事书为例
- Web Speech API: 免费
- Azure/Google/AWS: $6.35-6.77
- ElevenLabs: $21-35
- OpenAI: $6.35
最终建议
对于故事书项目,建议采用渐进式方案:
- MVP阶段:Web Speech API + 简单的语音选择
- 优化阶段:集成Azure或Google作为高质量选项
- 高端版本:提供ElevenLabs作为付费高级功能
问3
使用第三方的tts服务 能不能 用它 (某个音色)读一个故事 就将其存储起来,这样可以节省费用,存储是mp3?
答3
绝对可以!这是一个非常聪明的成本优化策略。预生成音频文件确实能大幅降低TTS服务费用,特别适合故事书这种内容相对固定的场景。
预生成音频的优势
1. 成本效益
- 一次性费用:只需为每个故事支付一次TTS费用
- 无重复计费:用户重复听同一故事不产生额外费用
- 批量优惠:可以批量处理多个故事获得更好价格
2. 性能优势
- 即时播放:无需等待TTS生成
- 离线支持:音频文件可以缓存到本地
- 稳定性:不依赖TTS服务的实时可用性
技术实现方案
音频格式选择
// 推荐的音频格式优先级
const audioFormats = {
production: 'mp3', // 最佳兼容性和压缩比
highQuality: 'wav', // 无损质量
streaming: 'ogg', // 更好的流式播放
mobile: 'aac' // 移动设备优化
};
MP3是最佳选择:
- 优秀的压缩比(通常比WAV小10倍)
- 全平台兼容性
- 支持流式播放
- 质量损失可接受
预生成服务架构
/**
* 故事音频预生成服务
*/
class StoryAudioGenerator {
constructor(ttsService) {
this.ttsService = ttsService;
this.audioStorage = new AudioStorage();
}
/**
* 批量生成故事音频
*/
async generateStoryAudio(story, voiceConfig) {
const { pages, metadata } = story;
const audioFiles = [];
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
const audioBuffer = await this.ttsService.synthesize({
text: page.text,
voice: voiceConfig.voice,
speed: voiceConfig.speed,
pitch: voiceConfig.pitch,
format: 'mp3',
quality: 'high'
});
const filename = `story_${story.id}_page_${i + 1}_${voiceConfig.voice}.mp3`;
const audioUrl = await this.audioStorage.save(filename, audioBuffer);
audioFiles.push({
pageIndex: i,
audioUrl,
duration: await this.getAudioDuration(audioBuffer),
fileSize: audioBuffer.byteLength
});
}
// 保存音频元数据
await this.saveAudioMetadata(story.id, voiceConfig, audioFiles);
return audioFiles;
}
/**
* 获取音频时长
*/
async getAudioDuration(audioBuffer) {
const audioContext = new AudioContext();
const decodedAudio = await audioContext.decodeAudioData(audioBuffer.slice());
return decodedAudio.duration;
}
}
多音色管理
/**
* 多音色音频管理
*/
class MultiVoiceAudioManager {
constructor() {
this.voiceConfigs = [
{ id: 'narrator', voice: 'en-US-AriaNeural', role: 'narrator' },
{ id: 'character1', voice: 'en-US-DavisNeural', role: 'character' },
{ id: 'character2', voice: 'en-US-JaneNeural', role: 'character' }
];
}
/**
* 为故事生成多个音色版本
*/
async generateMultiVoiceVersions(story) {
const versions = {};
for (const voiceConfig of this.voiceConfigs) {
console.log(`Generating audio with voice: ${voiceConfig.voice}`);
const audioFiles = await this.audioGenerator.generateStoryAudio(
story,
voiceConfig
);
versions[voiceConfig.id] = {
voiceConfig,
audioFiles,
totalDuration: audioFiles.reduce((sum, file) => sum + file.duration, 0),
totalSize: audioFiles.reduce((sum, file) => sum + file.fileSize, 0)
};
}
return versions;
}
}
存储优化策略
/**
* 音频存储优化
*/
class AudioStorage {
constructor() {
this.compressionSettings = {
mp3: {
bitrate: '128k', // 平衡质量和大小
sampleRate: '44100', // 标准采样率
channels: 'mono' // 语音用单声道即可
}
};
}
/**
* 保存音频文件
*/
async save(filename, audioBuffer) {
// 压缩音频
const compressedBuffer = await this.compressAudio(audioBuffer);
// 上传到CDN
const cdnUrl = await this.uploadToCDN(filename, compressedBuffer);
// 本地缓存
await this.cacheLocally(filename, compressedBuffer);
return cdnUrl;
}
/**
* 音频压缩
*/
async compressAudio(audioBuffer) {
// 使用Web Audio API或服务端工具压缩
// 这里是示例,实际需要使用专业的音频处理库
return audioBuffer; // 简化示例
}
}
成本对比分析
传统实时TTS vs 预生成
// 成本计算示例
const costAnalysis = {
story: {
pages: 20,
charactersPerPage: 500,
totalCharacters: 10000
},
realTimeTTS: {
costPerCharacter: 0.000016, // Azure Neural Voice
expectedPlays: 1000, // 预期播放次数
totalCost: 10000 * 0.000016 * 1000 // $160
},
preGenerated: {
oneTimeCost: 10000 * 0.000016, // $0.16
storageCostPerMonth: 0.02, // 假设20MB文件
totalCostFirstYear: 0.16 + (0.02 * 12) // $0.40
}
};
console.log('Cost savings:',
costAnalysis.realTimeTTS.totalCost - costAnalysis.preGenerated.totalCostFirstYear
); // 节省 $159.6
实际部署建议
1. 分层存储策略
const storageStrategy = {
hot: {
description: '热门故事,CDN缓存',
location: 'CDN + Browser Cache',
cost: 'Higher',
performance: 'Excellent'
},
warm: {
description: '普通故事,云存储',
location: 'Cloud Storage',
cost: 'Medium',
performance: 'Good'
},
cold: {
description: '冷门故事,按需生成',
location: 'Generate on demand',
cost: 'Lower',
performance: 'Slower first load'
}
};
2. 渐进式加载
/**
* 渐进式音频加载
*/
class ProgressiveAudioLoader {
constructor(storyId, voiceId) {
this.storyId = storyId;
this.voiceId = voiceId;
this.audioCache = new Map();
}
/**
* 预加载当前页和下一页音频
*/
async preloadAudio(currentPageIndex) {
const pagesToLoad = [currentPageIndex, currentPageIndex + 1];
for (const pageIndex of pagesToLoad) {
if (!this.audioCache.has(pageIndex)) {
const audioUrl = this.getAudioUrl(pageIndex);
const audioElement = new Audio(audioUrl);
// 预加载但不播放
audioElement.preload = 'auto';
this.audioCache.set(pageIndex, audioElement);
}
}
}
/**
* 获取音频URL
*/
getAudioUrl(pageIndex) {
return `https://cdn.example.com/stories/${this.storyId}/page_${pageIndex}_${this.voiceId}.mp3`;
}
}
3. 版本管理
/**
* 音频版本管理
*/
class AudioVersionManager {
constructor() {
this.versions = new Map();
}
/**
* 注册音频版本
*/
registerVersion(storyId, voiceId, version, metadata) {
const key = `${storyId}_${voiceId}`;
this.versions.set(key, {
version,
metadata,
createdAt: new Date(),
urls: metadata.audioFiles.map(file => file.audioUrl)
});
}
/**
* 检查是否需要更新
*/
needsUpdate(storyId, voiceId, currentVersion) {
const key = `${storyId}_${voiceId}`;
const stored = this.versions.get(key);
return !stored || stored.version !== currentVersion;
}
}
最佳实践建议
- 批量处理:一次性生成多个故事的音频,获得批量折扣
- 质量平衡:使用128kbps MP3,在质量和大小间取得平衡
- CDN分发:使用CDN确保全球用户的快速访问
- 缓存策略:在用户设备上缓存常听的故事
- 版本控制:当故事内容更新时,重新生成对应的音频
这种预生成策略特别适合故事书应用,因为内容相对固定,用户会重复收听,能够最大化成本效益。
问4
故事书 我应该怎么设计数据库?
答4
对于故事书应用的数据库设计,我建议采用以下结构来平衡灵活性和性能:
数据库表设计
1. 故事书主表 (storybooks)
CREATE TABLE storybooks (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL COMMENT '故事书标题',
description TEXT COMMENT '故事书描述',
cover_image VARCHAR(500) COMMENT '封面图片URL',
author VARCHAR(100) COMMENT '作者',
category VARCHAR(50) COMMENT '分类',
age_range VARCHAR(20) COMMENT '适合年龄段',
total_pages INT NOT NULL COMMENT '总页数',
duration_minutes INT COMMENT '预估朗读时长(分钟)',
status ENUM('draft', 'published', 'archived') DEFAULT 'draft',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
2. 故事书页面表 (storybook_pages)
CREATE TABLE storybook_pages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
storybook_id BIGINT NOT NULL,
page_number INT NOT NULL COMMENT '页码',
image_url VARCHAR(500) NOT NULL COMMENT '页面图片URL',
text_content TEXT NOT NULL COMMENT '页面文本内容',
word_count INT COMMENT '字数统计',
estimated_duration_seconds INT COMMENT '预估朗读时长(秒)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (storybook_id) REFERENCES storybooks(id) ON DELETE CASCADE,
UNIQUE KEY unique_page (storybook_id, page_number)
);
3. 音频文件表 (audio_files)
CREATE TABLE audio_files (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
storybook_id BIGINT NOT NULL,
page_number INT NOT NULL,
voice_type VARCHAR(50) NOT NULL COMMENT '音色类型',
audio_url VARCHAR(500) NOT NULL COMMENT '音频文件URL',
file_size BIGINT COMMENT '文件大小(字节)',
duration_seconds DECIMAL(8,2) COMMENT '音频时长(秒)',
format VARCHAR(10) DEFAULT 'mp3' COMMENT '音频格式',
quality VARCHAR(20) DEFAULT 'standard' COMMENT '音质等级',
generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (storybook_id) REFERENCES storybooks(id) ON DELETE CASCADE,
UNIQUE KEY unique_audio (storybook_id, page_number, voice_type)
);
JSON 存储方案对比
方案A:单个JSON文件存储
{
"id": 1,
"title": "小红帽的故事",
"pages": [
{
"pageNumber": 1,
"image": "https://cdn.example.com/story1/page1.jpg",
"text": "从前有一个可爱的小女孩...",
"audio": {
"male": "https://cdn.example.com/audio/story1/page1_male.mp3",
"female": "https://cdn.example.com/audio/story1/page1_female.mp3",
"child": "https://cdn.example.com/audio/story1/page1_child.mp3"
}
}
]
}
优点:
- 结构简单,易于理解
- 单次查询获取完整故事
- 适合小型故事书
缺点:
- 大型故事书JSON文件过大
- 更新单页需要重写整个文件
- 不利于分页加载和缓存
- 音频文件管理困难
方案B:关系型数据库(推荐)
优点:
- 数据结构清晰,便于维护
- 支持复杂查询和统计
- 便于分页加载和性能优化
- 音频文件独立管理
- 支持多版本和增量更新
- 便于实现用户阅读进度跟踪
推荐的混合方案
结合两种方案的优点:
- 主数据存储:使用关系型数据库存储结构化数据
- 缓存层:将热门故事书生成JSON缓存文件
- CDN分发:音频和图片文件通过CDN分发
API接口设计示例
// 获取故事书基本信息
GET /api/storybooks/{id}
// 获取故事书页面(支持分页)
GET /api/storybooks/{id}/pages?page=1&limit=5
// 获取特定页面的音频文件
GET /api/storybooks/{id}/pages/{pageNumber}/audio/{voiceType}
// 批量获取故事书完整数据(用于离线缓存)
GET /api/storybooks/{id}/complete
前端数据管理
interface StoryBookPage {
pageNumber: number;
imageUrl: string;
textContent: string;
wordCount: number;
estimatedDuration: number;
audioFiles: {
[voiceType: string]: {
url: string;
duration: number;
fileSize: number;
};
};
}
interface StoryBook {
id: number;
title: string;
description: string;
coverImage: string;
totalPages: number;
pages: StoryBookPage[];
}
性能优化建议
- 分页加载:首次只加载前几页,用户翻页时动态加载
- 预加载策略:提前加载下一页的图片和音频
- 缓存机制:本地缓存已加载的页面数据
- CDN加速:图片和音频文件使用CDN分发
- 压缩优化:音频文件使用适当的压缩率平衡质量和大小
这种设计既保证了数据的结构化管理,又能提供良好的用户体验和性能表现。
问5
这是一个google的故事书 https://gemini.google.com/share/48915d6285d8 ,你看看能否通过什么方式爬取故事书中的图片 和 文字
答5
用户想要爬取Google Gemini故事书中的图片和文字。我需要先搜索相关的网页爬取技术和方法,特别是针对Google Gemini这类动态网站的爬取方案。
toolName: web_search
status: success
query: 如何爬取Google Gemini故事书内容 图片文字提取 网页爬虫技术
基于搜索结果,我发现了几种可能的方法来爬取Google Gemini故事书的内容。现在我需要搜索更具体的技术方案,特别是针对动态网页内容提取的方法。
toolName: web_search
status: success
query: 网页爬虫 动态内容提取 JavaScript渲染页面 Selenium Puppeteer 图片文字提取
基于你提供的Google Gemini故事书链接,我可以为你提供几种爬取图片和文字内容的技术方案:
方案一:使用Gemini 2.0实时API(推荐)
最新的Google Gemini 2.0提供了强大的多模态网页内容提取功能
操作步骤:
- 访问Google AI Studio:用谷歌账号登录Google AI Studio
2 - 开启屏幕共享:选择"共享整个屏幕"功能,这样Gemini 2.0可以实时处理屏幕上的所有内容
2 - 设置输出格式:将输出格式设为"文本"或"JSON"格式
2 - 语音指令:通过语音输入指令如"提取当前页面的所有文字和图片内容,并转换为结构化格式"
优势:
- 零代码操作:无需编程知识,自然语言指令即可完成
2 - 动态内容处理:完美应对无限滚动页面、懒加载等现代网页技术
2 - 智能识别:能区分正文、广告、导航栏等非目标内容
2
方案二:使用Puppeteer(Node.js)
Puppeteer是Google Chrome团队开发的浏览器自动化工具
const puppeteer = require('puppeteer');
(async () => {
// 启动浏览器
const browser = await puppeteer.launch({
headless: false // 可视化模式,便于调试
});
const page = await browser.newPage();
// 访问故事书页面
await page.goto('https://gemini.google.com/share/48915d6285d8');
// 等待页面加载完成
await page.waitForTimeout(3000);
// 自动滚动加载所有内容
await autoScroll(page);
// 提取文字内容
const textContent = await page.evaluate(() => {
// 根据页面结构提取文字
const textElements = document.querySelectorAll('p, h1, h2, h3, div[class*="text"]');
return Array.from(textElements).map(el => el.textContent.trim()).filter(text => text.length > 0);
});
// 提取图片链接
const images = await page.evaluate(() => {
const imgElements = document.querySelectorAll('img');
return Array.from(imgElements).map(img => ({
src: img.src,
alt: img.alt || '',
width: img.width,
height: img.height
}));
});
// 下载图片
for (let i = 0; i < images.length; i++) {
const image = images[i];
if (image.src && !image.src.startsWith('data:')) {
await page.goto(image.src);
await page.screenshot({
path: `image_${i}.png`,
fullPage: true
});
}
}
console.log('提取的文字内容:', textContent);
console.log('提取的图片信息:', images);
await browser.close();
})();
// 自动滚动函数
async function autoScroll(page) {
await page.evaluate(async () => {
await new Promise((resolve) => {
let totalHeight = 0;
const distance = 100;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 100);
});
});
}
方案三:使用Selenium(Python)
Selenium是另一个强大的浏览器自动化工具
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import requests
import time
import os
# 设置Chrome选项
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
# 启动浏览器
driver = webdriver.Chrome(options=chrome_options)
try:
# 访问故事书页面
driver.get('https://gemini.google.com/share/48915d6285d8')
# 等待页面加载
time.sleep(5)
# 滚动页面加载所有内容
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
new_height = driver.execute_script("return document.body.scrollHeight")
if new_height == last_height:
break
last_height = new_height
# 提取文字内容
text_elements = driver.find_elements(By.CSS_SELECTOR, "p, h1, h2, h3, div[class*='text']")
texts = [element.text.strip() for element in text_elements if element.text.strip()]
# 提取图片
img_elements = driver.find_elements(By.TAG_NAME, "img")
# 创建保存目录
os.makedirs('story_images', exist_ok=True)
# 下载图片
for i, img in enumerate(img_elements):
src = img.get_attribute('src')
if src and not src.startswith('data:'):
try:
response = requests.get(src)
if response.status_code == 200:
with open(f'story_images/image_{i}.jpg', 'wb') as f:
f.write(response.content)
print(f'已下载图片 {i}: {src}')
except Exception as e:
print(f'下载图片失败: {e}')
# 保存文字内容
with open('story_text.txt', 'w', encoding='utf-8') as f:
for text in texts:
f.write(text + '\n\n')
print(f'提取完成!共提取 {len(texts)} 段文字,{len(img_elements)} 张图片')
finally:
driver.quit()
注意事项
-
访问限制:Google Gemini可能有访问限制或需要登录,建议先手动访问确认页面可正常加载
-
动态加载:故事书内容可能是动态加载的,需要适当的等待时间和滚动操作
2 -
反爬虫机制:大型网站通常有反爬虫措施,建议:
- 设置合理的请求间隔
- 使用随机User-Agent
- 避免频繁请求
-
法律合规:确保爬取行为符合网站的使用条款和相关法律法规
推荐方案
对于你的需求,我推荐首先尝试方案一(Gemini 2.0实时API),因为它最简单且专门针对这类内容设计。如果需要更多自定义控制,再考虑使用Puppeteer或Selenium方案。