
最近在整理虚拟歌手相关数据时发现很多朋友对如何系统性地追踪和分析虚拟歌手如初音未来、洛天依等的外语作品热度很感兴趣。无论是作为粉丝想了解全球动态还是作为开发者想构建类似“周刊排行榜”的数据分析项目都需要一套从数据获取、处理到可视化的完整方案。本文将围绕构建一个“虚拟歌手外语排行榜”的数据分析系统展开手把手带你从零开始用Python实现数据爬取、清洗、分析到生成报告的全流程。无论你是Python数据分析的初学者还是想将技术应用于兴趣领域的开发者都能从本文中获得可直接复用的代码和清晰的实现思路。1. 背景与核心概念在深入代码之前我们有必要厘清几个核心概念这有助于理解整个项目的目标和设计。虚拟歌手 (Vocaloid/Synthesizer V 等)指通过语音合成引擎如Yamaha的Vocaloid、Dreamtonics的Synthesizer V创作歌声的虚拟形象。其作品歌曲由创作者Producer/P主使用软件制作并发布在视频平台如Bilibili、YouTube、Niconico。“周刊排行榜”这是一个社区自发形成的、用于统计和展示虚拟歌手作品在一定周期内通常为一周人气数据的榜单。它并非官方发布而是爱好者通过爬取公开平台如Bilibili的播放、点赞、投币、收藏数据YouTube的观看、点赞数据的数据经过加权公式计算得出的综合排名反映了作品在特定时间段内的热度。本项目目标模拟构建一个自动化系统用于定期例如每周生成“虚拟歌手外语作品排行榜”。这里“外语”指相对于创作者主要使用语言的作品例如中文P主创作的日语歌或日语P主创作的英语歌。核心挑战在于多平台数据获取、跨语言识别、合理的热度公式设计以及数据的持久化与可视化。技术栈选择我们将使用Python作为主要语言因为它拥有丰富的数据处理和网络请求库。核心库包括requests/selenium: 用于网络请求和动态页面爬取。pandas: 用于数据清洗、分析和处理。sqlalchemy/sqlite3: 用于数据存储。matplotlib/seaborn/pyecharts: 用于数据可视化。schedule/APScheduler: 用于定时任务调度可选。2. 环境准备与版本说明在开始编码前请确保你的开发环境已就绪。以下版本为本文撰写时的稳定版本你可以根据实际情况调整。操作系统: Windows 10/11, macOS, 或 Linux (如 Ubuntu 20.04)。Python: 版本 3.8 或以上。推荐使用 3.9/3.10以获得更好的库兼容性。包管理工具:pip(通常随Python安装)。创建虚拟环境推荐 为了避免包冲突建议为项目创建独立的虚拟环境。# 在项目根目录下 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate安装依赖库 在激活的虚拟环境中使用以下命令安装所需库。我们将主要使用requests进行静态页面爬取若目标网站数据为JavaScript动态加载则可能需要selenium。pip install requests pandas matplotlib seaborn sqlalchemy schedule # 如果需要更强大的定时任务可以安装APScheduler # pip install apscheduler # 如果需要动态爬取安装selenium和对应的WebDriver # pip install selenium项目结构规划 一个清晰的项目结构有助于代码管理。建议创建如下目录和文件weekly_vocaloid_rank/ ├── config.py # 配置文件存放API密钥、数据库路径、平台URL等 ├── database.py # 数据库连接与操作类 ├── crawler/ │ ├── __init__.py │ ├── bilibili_crawler.py # B站爬虫模块 │ └── youtube_crawler.py # YouTube爬虫模块 (需API) ├── processor/ │ ├── __init__.py │ ├── data_cleaner.py # 数据清洗模块 │ └── rank_calculator.py # 排名计算模块 ├── visualizer/ │ ├── __init__.py │ └── chart_generator.py # 图表生成模块 ├── scheduler.py # 定时任务调度器 ├── main.py # 主程序入口 ├── requirements.txt # 项目依赖列表 └── data/ # 存放原始数据、清洗后数据和图表输出 ├── raw/ ├── processed/ └── output/3. 核心模块设计与原理拆解本系统的核心流程可以概括为爬取 - 清洗 - 计算 - 存储 - 可视化。下面我们逐一拆解每个环节的关键设计。3.1 数据爬取模块设计爬取是数据来源的第一步。我们必须遵守目标网站的robots.txt协议并采用礼貌的爬取策略如设置请求头、添加延迟避免对服务器造成压力。Bilibili平台爬取思路 B站视频数据部分可通过其公开API或解析网页获取。一个典型的视频数据API返回JSON包含播放量(view)、弹幕数(danmaku)、点赞(like)、投币(coin)、收藏(favorite)等关键字段。确定目标: 我们需要找到标签为“VOCALOID”、“初音未来”、“洛天依”等且标题或标签中包含外语如“日语”、“English”的作品。构造请求: 使用requests库向B站搜索API或特定视频API发送GET请求。解析数据: 从返回的JSON中提取所需字段。YouTube平台爬取思路 YouTube数据获取更依赖于其官方Data API v3需要申请API密钥。申请API: 在Google Cloud Console创建项目并启用YouTube Data API v3获取API密钥。搜索视频: 使用search.list接口指定关键词如“初音未来 English cover”和发布时间范围上周。获取统计信息: 使用videos.list接口传入视频ID列表获取观看次数(viewCount)、点赞数(likeCount)、评论数(commentCount)等。关键点频率限制: 两大平台都有严格的请求频率限制代码中必须加入延时(time.sleep)。错误处理: 网络请求可能失败必须使用try-except进行异常捕获和重试。数据去重: 同一作品可能被多次爬取需要根据视频ID进行去重。3.2 数据清洗与热度计算模块设计原始爬取的数据是杂乱且异构的需要清洗和标准化。数据清洗(data_cleaner.py)字段统一: 将不同来源的数据字段映射到统一的模型例如video_id,title,platform,views,likes,coins,favorites,publish_time。处理缺失值: 对于某些平台没有的字段如B站无commentCount可填充为0或进行标记。时间处理: 将发布时间字符串转换为datetime对象便于按周筛选。语言过滤核心: 这是一个难点。简易方案是使用关键词列表如[‘日语’ ‘Japanese’ ‘英語’ ‘English’]在标题和标签中进行匹配。更复杂的方案可以接入机器翻译API或语言检测库如langdetect但成本较高。热度计算(rank_calculator.py) 排行榜的核心是热度公式。一个常见的加权公式借鉴了B站本身的“综合得分”思路但需要根据外语榜的特点调整权重。例如热度分数 (播放量 * W1 点赞数 * W2 投币数 * W3 收藏数 * W4) / 时间衰减因子权重(W1, W2, W3, W4): 需要根据平台特性调整。例如B站的“投币”权重可以设高一些代表观众更强的支持意愿YouTube的“点赞”和“评论”权重可以设高一些。时间衰减因子: 为了体现“周刊”特性需要让发布越早的作品分数有一定衰减。例如可以使用1 / (log(当前时间 - 发布时间 2))。平台归一化: B站和YouTube的绝对数据量级不同需要将各平台数据归一化到同一尺度后再计算或者为不同平台设计独立的公式最后再合并排名。3.3 数据存储与可视化模块设计数据存储(database.py) 使用SQLite数据库轻便易用。主要设计两张表raw_video_data: 存储爬取的原始数据包含所有字段和爬取时间。weekly_rank: 存储每周计算出的最终排行榜结果包含排名、视频ID、热度分数、所属周次等。使用SQLAlchemy ORM可以更方便地进行数据库操作。可视化(chart_generator.py) 使用matplotlib或pyecharts生成图表。榜单表格: 生成一个美观的Markdown或HTML格式的表格包含排名、标题、作者、热度分数、主要数据播放/点赞。趋势图: 展示TOP10作品热度分数的对比柱状图。平台分布饼图: 展示本周上榜作品中B站和YouTube作品的占比。发布趋势图: 展示本周内作品发布的时间分布。4. 完整实战案例构建单次排行榜生成程序现在我们将上述设计转化为可运行的代码。由于篇幅限制我们将聚焦于B站单平台、简化版的单次爬取与排名生成流程。YouTube部分因涉及API申请仅提供思路性代码。4.1 创建配置文件config.py存放所有可配置参数。# config.py import os from datetime import datetime, timedelta # 数据库路径 BASE_DIR os.path.dirname(os.path.abspath(__file__)) DB_PATH os.path.join(BASE_DIR, data, vocaloid_rank.db) # 爬虫配置 BILIBILI_SEARCH_URL https://api.bilibili.com/x/web-interface/search/type BILIBILI_VIDEO_INFO_URL https://api.bilibili.com/x/web-interface/view 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, Referer: https://www.bilibili.com/ } # 搜索关键词虚拟歌手 外语相关标签 SEARCH_KEYWORDS [VOCALOID 日语, 初音未来 英语, 洛天依 日语曲, Synthesizer V English] # 时间范围最近7天 TIME_DELTA_DAYS 7 START_DATE (datetime.now() - timedelta(daysTIME_DELTA_DAYS)).strftime(%Y-%m-%d) # 热度公式权重 (针对B站) WEIGHTS { view: 1.0, like: 2.0, coin: 3.0, # 投币权重高代表强烈推荐 favorite: 1.5, danmaku: 0.5, # 弹幕互动 } # 语言过滤关键词 (简易版) FOREIGN_LANG_KEYWORDS [日语, Japanese, 英語, English, 韓国語, Korean, フランス語, French]4.2 实现B站爬虫模块crawler/bilibili_crawler.py负责从B站获取数据。# crawler/bilibili_crawler.py import requests import time import pandas as pd from datetime import datetime from config import HEADERS, BILIBILI_SEARCH_URL, BILIBILI_VIDEO_INFO_URL, START_DATE, SEARCH_KEYWORDS class BilibiliCrawler: def __init__(self): self.session requests.Session() self.session.headers.update(HEADERS) def search_videos_by_keyword(self, keyword, page1, page_size50): 根据关键词搜索视频 params { search_type: video, keyword: keyword, page: page, page_size: page_size, order: pubdate, # 按发布时间排序 tids: 0, # 全部分区 } try: resp self.session.get(BILIBILI_SEARCH_URL, paramsparams, timeout10) resp.raise_for_status() data resp.json() if data[code] 0: return data[data][result] else: print(f搜索失败: {data[message]}) return [] except requests.exceptions.RequestException as e: print(f请求搜索API失败: {e}) return [] time.sleep(1) # 礼貌延迟 def get_video_detail(self, bvid): 根据视频BV号获取详细信息 params {bvid: bvid} try: resp self.session.get(BILIBILI_VIDEO_INFO_URL, paramsparams, timeout10) resp.raise_for_status() data resp.json() if data[code] 0: return data[data] else: print(f获取视频详情失败: {data[message]}) return None except requests.exceptions.RequestException as e: print(f请求详情API失败: {e}) return None time.sleep(0.5) def crawl_recent_videos(self): 爬取近期相关视频 all_videos [] for keyword in SEARCH_KEYWORDS: print(f正在搜索关键词: {keyword}) # 只取第一页可根据需要调整 videos self.search_videos_by_keyword(keyword, page1, page_size30) for v in videos: # 过滤发布时间 pubdate datetime.fromtimestamp(v[pubdate]) if pubdate.strftime(%Y-%m-%d) START_DATE: continue # 获取详细数据 detail self.get_video_detail(v[bvid]) if detail: video_info { video_id: fbilibili_{v[bvid]}, bvid: v[bvid], title: v[title], author: v[author], publish_time: pubdate, views: detail[stat][view], likes: detail[stat][like], coins: detail[stat][coin], favorites: detail[stat][favorite], danmaku: detail[stat][danmaku], tags: ,.join([tag[tag_name] for tag in detail.get(tags, [])]), platform: bilibili } all_videos.append(video_info) return all_videos if __name__ __main__: crawler BilibiliCrawler() videos crawler.crawl_recent_videos() print(f共爬取到 {len(videos)} 条视频数据。) if videos: df pd.DataFrame(videos) print(df[[title, author, views, likes, publish_time]].head())4.3 实现数据清洗与热度计算模块processor/data_cleaner.py和processor/rank_calculator.py。# processor/data_cleaner.py import pandas as pd import re from config import FOREIGN_LANG_KEYWORDS class DataCleaner: staticmethod def filter_foreign_language(videos_df): 简易外语作品过滤基于标题和标签关键词 def contains_foreign_keyword(text): if not isinstance(text, str): return False text_lower text.lower() for kw in FOREIGN_LANG_KEYWORDS: if kw.lower() in text_lower: return True return False # 检查标题或标签是否包含外语关键词 mask videos_df.apply( lambda row: contains_foreign_keyword(row[title]) or contains_foreign_keyword(row.get(tags, )), axis1 ) filtered_df videos_df[mask].copy() print(f外语作品过滤: 从 {len(videos_df)} 条过滤到 {len(filtered_df)} 条。) return filtered_df staticmethod def normalize_data(videos_df): 数据清洗处理缺失值规范格式 # 确保数值字段为整数类型缺失填0 numeric_cols [views, likes, coins, favorites, danmaku] for col in numeric_cols: if col in videos_df.columns: videos_df[col] pd.to_numeric(videos_df[col], errorscoerce).fillna(0).astype(int) # 确保时间字段为datetime if publish_time in videos_df.columns: videos_df[publish_time] pd.to_datetime(videos_df[publish_time]) return videos_df# processor/rank_calculator.py import pandas as pd import numpy as np from datetime import datetime from config import WEIGHTS class RankCalculator: def __init__(self, base_timeNone): self.base_time base_time or datetime.now() def calculate_hot_score(self, row): 计算单条视频的热度分数简化版未做平台归一化 score 0 for field, weight in WEIGHTS.items(): if field in row: score row[field] * weight # 简单的时间衰减发布越近衰减越小。这里使用小时差。 if publish_time in row and pd.notnull(row[publish_time]): hour_diff max(1, (self.base_time - row[publish_time]).total_seconds() / 3600) # 衰减因子例如使用对数衰减防止新视频优势过大 decay_factor 1 / np.log(hour_diff 2) score * decay_factor return score def calculate_rank(self, cleaned_df): 计算排名 if cleaned_df.empty: return pd.DataFrame() # 计算热度分数 cleaned_df[hot_score] cleaned_df.apply(self.calculate_hot_score, axis1) # 按分数降序排序 ranked_df cleaned_df.sort_values(byhot_score, ascendingFalse).reset_index(dropTrue) # 添加排名 ranked_df[rank] ranked_df.index 1 return ranked_df4.4 主程序串联与执行main.py将各个模块串联起来执行一次完整的排行榜生成流程。# main.py import pandas as pd from crawler.bilibili_crawler import BilibiliCrawler from processor.data_cleaner import DataCleaner from processor.rank_calculator import RankCalculator from database import DatabaseManager # 假设已实现见下文 import os from datetime import datetime def main(): print( 虚拟歌手外语排行榜生成程序启动 ) print(f执行时间: {datetime.now().strftime(%Y-%m-%d %H:%M:%S)}) # 1. 爬取数据 print(\n[步骤1] 正在从Bilibili爬取数据...) crawler BilibiliCrawler() raw_videos crawler.crawl_recent_videos() if not raw_videos: print(未爬取到数据程序退出。) return raw_df pd.DataFrame(raw_videos) # 2. 数据清洗与过滤 print(\n[步骤2] 正在清洗数据并过滤外语作品...) cleaner DataCleaner() cleaned_df cleaner.normalize_data(raw_df) foreign_df cleaner.filter_foreign_language(cleaned_df) if foreign_df.empty: print(未过滤出外语作品程序退出。) return # 3. 计算热度与排名 print(\n[步骤3] 正在计算热度分数并生成排名...) calculator RankCalculator() ranked_df calculator.calculate_rank(foreign_df) # 4. 保存结果到数据库和文件 print(\n[步骤4] 正在保存结果...) # 初始化数据库 db_manager DatabaseManager(data/vocaloid_rank.db) db_manager.init_db() # 保存原始数据可选 # db_manager.save_raw_data(raw_df) # 保存本周排行榜 week_id datetime.now().strftime(%Y-W%W) ranked_df[week_id] week_id db_manager.save_weekly_rank(ranked_df) # 同时保存为CSV文件便于查看 output_dir data/output os.makedirs(output_dir, exist_okTrue) output_path os.path.join(output_dir, fweekly_rank_{week_id}.csv) ranked_df.to_csv(output_path, indexFalse, encodingutf-8-sig) print(f排行榜已保存至: {output_path}) # 5. 打印本次排行榜TOP10 print(f\n 第 {week_id} 期虚拟歌手外语排行榜 TOP10 ) top10 ranked_df.head(10)[[rank, title, author, views, likes, hot_score]] print(top10.to_string(indexFalse)) print(\n 程序执行完毕 ) if __name__ __main__: main()4.5 数据库管理模块示例database.py提供数据库操作的封装。# database.py import sqlite3 import pandas as pd from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime Base declarative_base() class RawVideoData(Base): __tablename__ raw_video_data id Column(Integer, primary_keyTrue) video_id Column(String, uniqueTrue) title Column(String) author Column(String) platform Column(String) views Column(Integer) likes Column(Integer) coins Column(Integer) favorites Column(Integer) danmaku Column(Integer) publish_time Column(DateTime) crawl_time Column(DateTime, defaultdatetime.now) class WeeklyRank(Base): __tablename__ weekly_rank id Column(Integer, primary_keyTrue) week_id Column(String) # 格式2025-W25 rank Column(Integer) video_id Column(String) title Column(String) author Column(String) platform Column(String) hot_score Column(Float) views Column(Integer) likes Column(Integer) publish_time Column(DateTime) record_time Column(DateTime, defaultdatetime.now) class DatabaseManager: def __init__(self, db_pathvocaloid_rank.db): self.engine create_engine(fsqlite:///{db_path}) self.Session sessionmaker(bindself.engine) def init_db(self): 初始化数据库创建表 Base.metadata.create_all(self.engine) print(数据库表初始化完成。) def save_weekly_rank(self, rank_df): 将排行榜DataFrame保存到数据库 session self.Session() try: # 转换DataFrame到ORM对象列表 records [] for _, row in rank_df.iterrows(): record WeeklyRank( week_idrow[week_id], rankrow[rank], video_idrow[video_id], titlerow[title], authorrow[author], platformrow[platform], hot_scorerow[hot_score], viewsrow[views], likesrow[likes], publish_timerow[publish_time] ) records.append(record) session.add_all(records) session.commit() print(f成功保存 {len(records)} 条排行榜记录。) except Exception as e: session.rollback() print(f保存排行榜数据失败: {e}) finally: session.close()4.6 运行与验证确保项目结构完整所有.py文件就位。在项目根目录下运行python main.py。程序将依次执行爬取、清洗、计算、保存步骤。查看终端输出的TOP10榜单并在data/output/目录下找到生成的CSV文件。使用数据库工具如DB Browser for SQLite打开data/vocaloid_rank.db查看weekly_rank表中的数据。预期输出示例 虚拟歌手外语排行榜生成程序启动 执行时间: 2023-10-27 14:30:00 [步骤1] 正在从Bilibili爬取数据... 正在搜索关键词: VOCALOID 日语 ... 共爬取到 127 条视频数据。 [步骤2] 正在清洗数据并过滤外语作品... 外语作品过滤: 从 127 条过滤到 43 条。 [步骤3] 正在计算热度分数并生成排名... [步骤4] 正在保存结果... 数据库表初始化完成。 成功保存 43 条排行榜记录。 排行榜已保存至: data/output/weekly_rank_2023-W43.csv 第 2023-W43 期虚拟歌手外语排行榜 TOP10 rank title author views likes hot_score 1 【初音未来】Into the Night (English Cover) ProducerA 250000 45000 1250000.5 2 【洛天依】Lemon (日语 Cover) ProducerB 180000 38000 980000.2 3 【Synthesizer V】Bad Apple!! (English Ver.) ProducerC 220000 30000 950000.8 ...5. 常见问题与排查思路在实现和运行上述系统时你可能会遇到以下问题问题现象可能原因排查思路与解决方案爬虫返回空数据或403错误1. 请求头User-Agent被识别。2. 目标网站反爬策略升级。3. API接口变更。1. 更新HEADERS中的User-Agent为更现代的浏览器标识。2. 增加请求延迟(time.sleep)。3. 考虑使用selenium模拟浏览器或寻找官方API。4. 检查网络连接和代理设置。数据库操作失败提示表不存在1. 数据库文件路径错误。2.init_db()方法未在保存数据前执行。1. 检查DB_PATH配置是否正确。2. 确保在save_weekly_rank前调用init_db()。3. 手动删除旧的.db文件让程序重新创建。热度分数计算异常所有分数为0或NaN1. 原始数据中的数值字段包含非数字字符或为空。2. 时间字段格式错误导致时间衰减计算失败。1. 在data_cleaner.py的normalize_data函数中加强数据清洗使用pd.to_numeric(..., errors‘coerce’)。2. 打印中间数据(cleaned_df.head())检查publish_time和数值列。外语过滤效果差漏掉很多作品或包含非外语作品1. 关键词列表FOREIGN_LANG_KEYWORDS不完善。2. 仅依赖标题和标签过滤不准确。1. 扩充关键词列表包括更多语言和常见写法如“JP”, “EN”, “中字”。2. 考虑结合视频简介(desc)进行过滤。3.进阶方案调用语言检测API如Google Cloud Translation API的检测功能但需注意成本和速率限制。程序运行一段时间后内存占用高或崩溃1. 爬取数据量过大未及时释放。2. 数据库会话未正常关闭。1. 分批次处理数据避免一次性将所有数据加载到内存。2. 确保在所有数据库操作后调用session.close()或使用上下文管理器(with session:)。3. 对于定时任务考虑将每次运行视为独立进程。无法安装某些Python库1. 网络问题。2. 库版本与Python版本不兼容。1. 使用国内镜像源安装pip install -i https://pypi.tuna.tsinghua.edu.cn/simple package_name。2. 检查库的官方文档确认支持的Python版本。6. 最佳实践与工程建议将一个小脚本升级为稳定、可维护的项目需要遵循一些工程实践。配置化管理将所有可配置项如API密钥、数据库连接字符串、URL、权重参数集中放在config.py或使用.env文件管理。切勿将API密钥等敏感信息硬编码在代码中或提交到版本控制系统如Git。日志记录使用Python内置的logging模块替代print语句可以方便地控制日志级别DEBUG, INFO, WARNING, ERROR和输出目的地文件、控制台。为每个模块配置独立的logger便于追踪问题来源。错误处理与重试机制网络请求必须包裹在try-except中并实现指数退避的重试逻辑。数据库操作需要处理连接失败、重复插入等异常并确保事务回滚。代码可测试性将核心逻辑如热度计算、数据清洗与外部依赖网络请求、数据库分离便于编写单元测试。使用pytest等框架为关键函数编写测试用例。定时任务部署对于每周自动运行的需求可以在服务器上使用cronLinux或计划任务Windows来定时执行main.py。更复杂的调度可以使用APScheduler库它支持更灵活的定时规则和任务持久化。数据备份与版本化定期备份SQLite数据库文件。将每周生成的CSV排行榜文件按日期归档便于历史追溯和数据分析。扩展性考虑多平台支持本文示例仅包含B站。可以按类似模式实现youtube_crawler.py并在主程序中合并多平台数据。合并时需注意平台数据归一化。更智能的语言识别可以集成离线语言检测库如langid或调用免费的在线API需注意限流。Web展示使用Flask或FastAPI构建一个简单的Web应用将数据库中的排行榜数据以网页形式展示并配上pyecharts生成的交互图表。法律与道德合规严格遵守目标网站的robots.txt协议。爬取数据仅用于个人学习、研究和非商业性质的榜单展示尊重创作者版权。在展示结果时最好附上视频的原链接为原作引流。通过以上步骤你不仅能够构建一个可运行的虚拟歌手外语排行榜系统更能掌握一个完整的数据分析项目从设计、开发到部署的通用方法论。这套方法同样适用于构建其他类型的社区热度榜单或市场数据分析工具。