简介:本文以音乐榜单为练手场景,系统讲解如何用Python完成页面结构分析、分页规律识别、请求与解析、数据清洗与持久化,并延伸出歌手热度、时长分布等分析思路。读者可掌握一套可复用的榜单采集方法论,理解反爬应对与工程化取舍,并将其迁移到其他结构化列表页场景。
音乐排行榜是典型的“结构规整、字段清晰、分页有规律”的列表型页面,非常适合作为爬虫能力的综合训练场。本文不以某个具体站点为推广对象,而是以通用音乐榜单为技术背景,完整拆解一套可复用的采集与分析方案:先做页面结构与分页规律分析,再实现请求、解析、清洗与持久化,最后延伸到数据洞察与工程化改进。文中的 URL、字段与选择器均以示意方式给出,读者可按目标页面的实际结构调整。
榜单类页面通常没有复杂的加密参数与高频风控,适合把爬虫的核心环节一次性打通:
这五个环节构成的链路,是后续所有复杂采集任务的骨架。把这条链路跑通,比记忆某个库的 API 更有价值。
采集到的“歌曲名 + 歌手 + 时长”三要素,看似简单,却能支撑多维度分析:
打开榜单页面,确认需要采集的字段:歌曲名、歌手名、歌曲时长。同时观察页面是否为服务端渲染——若关键信息直接出现在 HTML 中,解析成本最低;若由前端脚本异步填充,则需考虑接口抓取或渲染方案。
榜单页面常见两种分页形式:显式翻页按钮,或通过 URL 路径中的页码参数控制。后者更常见也更易处理:形如 /rank/home/{page}-8888.html 的路径,把 {page} 从 1 递增即可获得不同页面的数据。
识别分页规律的关键是验证边界:先确认每页条目数(例如每页 22 首),再用“总条目数 ÷ 每页条目数”推算总页数。若榜单共 500 首、每页 22 首,则前 22 页各 22 首、第 23 页为剩余 16 首,共 23 个页面。这个推算过程要在代码中用常量显式表达,避免魔法数字散落。
在浏览器开发者工具中定位歌曲条目的容器元素,确认歌曲名、歌手、时长分别位于哪个子节点。优先选择语义稳定的选择器(如列表项容器 + 类名),避免依赖易变的层级路径。
import timeimport requestsimport pandas as pdfrom bs4 import BeautifulSoupfrom requests.exceptions import RequestExceptionBASE_URL = "https://example.com/rank/home/{page}-8888.html" # 示意地址HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ""AppleWebKit/537.36 (KHTML, like Gecko) ""Chrome/120.0.0.0 Safari/537.36","Accept-Language": "zh-CN,zh;q=0.9",}PAGE_NUM = 23 # 总页数,由条目总数与每页条数推算DATA_LIST = []
def fetch_page(page: int, retries: int = 3) -> str | None:url = BASE_URL.format(page=page)for attempt in range(1, retries + 1):try:resp = requests.get(url, headers=HEADERS, timeout=10)if resp.status_code == 200:resp.encoding = resp.apparent_encodingreturn resp.textprint(f"[WARN] page={page} status={resp.status_code}")except RequestException as e:print(f"[ERROR] page={page} attempt={attempt} {e}")time.sleep(1.5 * attempt) # 退避,避免密集重试return None
这里有两个工程细节值得强调:超时必须显式设置,否则个别慢请求会拖垮整体流程;重试要有退避,线性或指数退避均可,但不要在失败后立即高频重试。编码方面,优先使用响应头声明的编码,必要时用 apparent_encoding 兜底,避免中文乱码。
def parse_page(html: str) -> list[dict]:soup = BeautifulSoup(html, "html.parser")rows = []for item in soup.select("ul.rank_list > li"):name_el = item.select_one(".song_name")singer_el = item.select_one(".singer_name")time_el = item.select_one(".song_time")if not name_el:continue # 关键字段缺失则跳过,保证数据质量rows.append({"song": name_el.get_text(strip=True),"singer": singer_el.get_text(strip=True) if singer_el else "未知","duration": time_el.get_text(strip=True) if time_el else "",})return rows
清洗环节建议在解析后集中处理,而不是散落在各处:
mm:ss 形式的时长统一转换为秒数,便于后续统计;
def main():for page in range(1, PAGE_NUM + 1):html = fetch_page(page)if not html:print(f"[SKIP] page={page}")continueDATA_LIST.extend(parse_page(html))time.sleep(0.5) # 控制请求节奏df = pd.DataFrame(DATA_LIST).drop_duplicates(subset=["song", "singer"])df["duration_sec"] = df["duration"].apply(to_seconds)df.to_excel("rank_songs.xlsx", index=False)print(f"共采集 {len(df)} 条记录")
to_seconds 可用简单的字符串切分实现:
def to_seconds(text: str) -> int | None:parts = text.strip().split(":")try:if len(parts) == 2:return int(parts[0]) * 60 + int(parts[1])if len(parts) == 3:return int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2])except ValueError:return Nonereturn None
歌手热度:按歌手分组计数并排序,取前若干名绘制柱状图。注意“同一歌手多首上榜”是热度信号,但也要留意合作曲目带来的归属歧义,可在清洗阶段约定归属规则。
时长分布:以秒为单位的时长做直方图或分箱统计,观察主流区间是否集中在 3~4 分钟;同时可计算中位数与分位数,比单一均值更能反映分布形态。
风格关联:若数据中包含流派标签,按流派统计占比并做交叉分析;若标签缺失,可借助公开的歌手信息做映射,但要明确标注数据来源与推断边界,避免把推断当事实。
榜单采集的价值不在“抓到数据”本身,而在于把页面分析、分页规律、请求控制、解析清洗、持久化与分析串成一条可复用的工程链路。掌握这套方法论后,面对结构化列表页的采集需求,你只需要替换选择器与字段定义,就能快速搭建出一条稳定、可维护的数据管道。