学习的事情不算爬
- 爬虫的使用必须遵守相关法律法规,不得侵犯他人的隐私权、著作权等合法权益。
- 尊重网站的
robots.txt文件中的规定,遵守网站所有者的规则。 - 合理设置访问频率,避免对网站造成不必要的困扰。
下载角色干声utils
用于GPT-SoVITS训练前置
npm i axios@0.27.2 cheerio@^1.0.0-rc.12
爬取角色语音
js
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
// 下载目录路径
const DOWNLOAD_DIR = path.join(__dirname, 'output');
/**
* 初始化下载目录(不存在则创建)
*/
function initDownloadDir() {
if (!fs.existsSync(DOWNLOAD_DIR)) {
fs.mkdirSync(DOWNLOAD_DIR, { recursive: true }); // recursive确保多级目录创建
console.log(`已创建下载目录: ${DOWNLOAD_DIR}`);
}
}
/**
* 清洗文件名(去除非法字符)
* @param {string} filename - 原始文件名
* @returns {string} 清洗后的安全文件名
*/
function cleanFilename(filename) {
// 替换Windows/Linux/macOS中的非法文件名字符
const illegalChars = /[\/:*?"<>|\\]/g;
return filename.replace(illegalChars, '_').trim();
}
/**
* 下载单个MP3文件
* @param {Object} item - 包含title和url的对象
* @returns {Promise<void>}
*/
async function downloadSingleMp3(item) {
const { title, url } = item;
if (!title || !url) {
console.warn('跳过无效数据:缺少标题或链接');
return;
}
// 处理文件名:清洗 + 添加.mp3后缀
const cleanTitle = cleanFilename(title);
const filename = `${cleanTitle}.mp3`;
const filePath = path.join(DOWNLOAD_DIR, filename);
// 检查文件是否已存在
if (fs.existsSync(filePath)) {
console.log(`✅ 已存在,跳过下载:${filename}`);
return;
}
try {
console.log(`📥 开始下载:${filename}`);
const response = await axios.get(url, {
responseType: 'stream', // 以流形式获取数据
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
// 创建可写流并写入文件
const writer = fs.createWriteStream(filePath);
response.data.pipe(writer);
// 等待写入完成
await new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
console.log(`✅ 下载完成:${filename}`);
} catch (error) {
console.error(`❌ 下载失败 [${title}]:`, error.message);
// 下载失败时删除可能残留的空文件
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
}
/**
* 批量下载MP3文件(串行下载,避免请求过多)
* @param {Array<Object>} mp3List - 包含title和url的对象数组
*/
async function batchDownloadMp3s(mp3List) {
if (mp3List.length === 0) {
console.log('没有可下载的MP3资源');
return;
}
console.log(`\n开始批量下载,共 ${mp3List.length} 个文件`);
// 串行下载(一个完成后再下载下一个)
for (const item of mp3List) {
await downloadSingleMp3(item);
// 可选:添加下载间隔,避免请求过于频繁
// await new Promise(resolve => setTimeout(resolve, 1000));
}
console.log('\n📦 所有下载任务已完成');
}
/**
* 1. 爬取目标页面 HTML 内容
* @param {string} url - 目标网页 URL
* @returns {Promise<string>} 页面 HTML 内容
*/
async function fetchPageHtml(url) {
try {
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
return response.data;
} catch (error) {
console.error('获取页面内容失败:', error.message);
throw new Error('页面请求失败');
}
}
/**
* 2. 从 HTML 中解析 MP3 链接、大标题和具体台词文本
* @param {string} html - 页面 HTML 内容
* @returns {Array<{title: string, info: string, url: string}>} 包含标题、文本和 MP3 链接的数组
*/
function parseMp3Links(html) {
const $ = cheerio.load(html);
const mp3List = [];
// 遍历所有行,寻找包含标题和台词的结构
$('tr').each((trIndex, trElement) => {
const thElement = $(trElement).find('th').first(); // 大标题
const tdElement = $(trElement).find('td').first(); // 包含多个台词的内容块
// 只处理有标题和内容的行
if (thElement.length && tdElement.length) {
const title = thElement.text().trim(); // 大标题
if (!title) return; // 跳过无标题的行
// 拆分<td>中的多个台词条目(按<br>分隔,处理换行分隔的多个条目)
const tdHtml = tdElement.html() || '';
// 使用<br>或<br/>作为分隔符,拆分多个台词块
const lineBlocks = tdHtml.split(/<br\s*\/?>/i);
// 处理每个台词块
lineBlocks.forEach((block, blockIndex) => {
if (!block.trim()) return; // 跳过空内容
// 提取当前块的纯文本(去除HTML标签)
const info = $(`<div>${block}</div>`).text().trim();
// 提取当前块中的MP3链接(只匹配当前块内的链接)
const mp3Regex = /https?:\/\/[^\s"'<>]+\.mp3/g;
const mp3Urls = block.match(mp3Regex);
// 如果有链接,添加到结果数组
if (mp3Urls && mp3Urls.length > 0) {
info.split(/\n\n/g).forEach(i => {
mp3List.push({
title: title, // 大标题
info: i.split(/https:[^\s"'>]+?\.mp3(?=[\s"'>]|$)/gi)[0].trim() || `未命名台词-${blockIndex}`, // 具体台词文本
url: i.match(/https:[^\s"'>]+?\.mp3(?=[\s"'>]|$)/gi)[0].trim()
});
})
}
});
}
});
return mp3List;
}
/**
* 3. 主函数:爬取 + 下载流程
* @param {string} targetUrl - 目标 URL
*/
async function crawlAndDownloadMp3s(targetUrl) {
try {
// 初始化下载目录
initDownloadDir();
// 爬取数据
console.log('开始爬取页面内容...');
const html = await fetchPageHtml(targetUrl);
console.log('解析 MP3 资源信息...');
const mp3List = parseMp3Links(html);
console.log(`解析完成,共找到 ${mp3List.length} 个 MP3 资源`);
const downList = []
mp3List.forEach((item, index) => {
// console.log(`\n${index + 1}. 标题:${item.title} 台词:${item.info}`);
// console.log(`链接:${item.url}`); // 主url
const url = item.url; //下载链接
const title = `${index + 1}_${item.title}_${item.info}`
console.log({
title,
url,
});
downList.push({
title,
url,
})
});
// 批量下载
await batchDownloadMp3s(downList);
} catch (error) {
console.error('流程出错:', error.message);
}
}
// 4. 执行爬虫并输出结果
const targetUrl = 'https://wiki.biligame.com/blhx/%E5%9F%83%E5%B0%94%E5%BE%B7%E9%87%8C%E5%A5%87';
crawlAndDownloadMp3s(targetUrl);