☰
从零构建微博热搜爬虫:数据采集到可视化分析实战
2026/10/22 3:35:37 网站建设 项目流程

1. 项目概述

1.1 为什么选择微博热搜作为分析对象

微博热搜榜是国内互联网上信息更新速度最快、话题覆盖面最广的实时榜单之一,每天数十次的榜单刷新背后,反映的是数亿用户的关注焦点和情绪走向。对开发者来说,这个榜单天然就是一个极具研究价值的数据集——它既包含实时的热点事件,又包含公众情绪的微妙变化,同时数据获取路径相对清晰可控。我选择微博热搜作为爬虫项目的分析对象,并不是因为它是最容易爬的网站,而是因为它的数据结构、更新频率和话题维度非常适合用来练习爬虫编写、数据清洗和趋势分析这一整套流程。

简单说,这个项目解决的不仅仅是“怎么把数据抓下来”的问题,而是“抓下来之后怎么用”的问题。对于一个Python学习者或者数据分析爱好者来说,从零开始写一个能够自动抓取、存储、分析和可视化呈现热点趋势的小工具,远比单纯跑通一个教程Demo有价值。这也是我在这篇文章里想完整呈现的核心:从URL分析、请求构造、数据解析,到Excel存储、词频统计、情感倾向判断、趋势折线图绘制,全部用一套代码串起来跑通。

1.2 项目目标与最终产出

这个项目的最终产出是一个运行在本地环境的热点分析工具,核心功能可以概括为四点:

  • 定时抓取微博热搜榜前50条数据,包含排名、关键词、热度值(或阅读量);
  • 将抓取到的数据保存为Excel文件和CSV文件,便于离线分析和长期积累;
  • 对热搜关键词做分词、词频统计、热度值汇总,自动识别当前热门话题的集中领域(如社会、娱乐、科技、体育等);
  • 生成可视化图表,包括热搜词词频Top20柱状图、热门话题类别占比饼图,以及一天内热搜词热度变化趋势折线图。

在硬件和软件方面,只需要一台普通的电脑,Python环境3.8以上即可,基本不需要额外的高性能配置。依赖库方面,主要用到了requests、pandas、matplotlib、jieba和openpyxl,这些库安装起来非常简单,没有复杂的编译依赖。后面我会给出完整的pip安装命令。

这套工具适合的人群也比较明确:想通过实战案例巩固Python基础语法和requests库用法的初学者;对数据分析感兴趣、想尝试从非结构化网页数据中提取价值的数据爱好者;以及需要每天关注热点动向,希望用自动化方式替代手动刷榜单的运营或内容从业者。

2. 数据源分析与技术选型

2.1 微博热搜数据结构解析

在写任何爬虫之前,第一步永远是分析目标网页的数据结构,这一步做得越扎实,后面写代码就越省事。微博热搜榜的PC端页面地址是公开的,打开之后能看到榜单按热度降序排列,每条热搜包含排名序号、话题关键词、热度值(以“万”为单位)或者“新”“热”“沸”这样的标识。

我推荐直接用浏览器的开发者工具来观察数据交互方式。按F12打开开发者工具,切换到网络(Network)面板,刷新页面后就能看到页面加载过程中发出的所有请求。微博热搜页面有一部分数据是直接渲染在HTML里的,但更完整、更新更及时的榜单数据,其实是通过一个接口返回的JSON格式数据。用开发者工具过滤XHR类型的请求,就能找到返回热搜列表的那个接口。

这个接口不需要复杂的签名认证,也不需要登录Cookie就能访问,这对初学者来说非常友好。接口返回的JSON结构大致是这样的:最外层是一个状态码字段,内部包含一组热搜数据,每条热搜数据里有关键词、热度值、排名序号这些信息。具体的字段名和层级关系,你在开发者工具里一看就明白,不同时期可能会有细微调整,所以写代码时一定要保持解析逻辑的灵活性,不要硬编码字段路径。

提示:这里不直接给出接口地址,因为这种公开接口的路径可能会调整,而且直接复制粘贴会让你失去一次宝贵的“自己分析网络请求”的练习机会。按照上面说的步骤走一遍,你就能自己找到它。这也是爬虫开发者最核心的基本功之一。

2.2 Python爬虫技术栈选择

选型方面,我用了四个核心库,各有各的不可替代性:

requests库是Python生态里最主流的HTTP请求库,相比Python自带的urllib,requests的API设计要人性化得多,只需要几行代码就能完成带请求头、带超时控制的GET请求。对于爬虫项目来说,requests基本是标准配置。

pandas库负责数据处理。热搜数据抓下来之后是JSON格式,直接转成DataFrame之后,做排序、过滤、统计、去重都非常方便。后面保存Excel文件,也直接靠pandas的to_excel方法,一步到位。

matplotlib库是Python里最经典的可视化库,用它绘制柱状图、饼图和折线图非常成熟。虽然它在视觉效果上不如一些新锐库(比如pyecharts)华丽,但胜在稳定可靠,而且在中文标签处理上只要设置好字体就能正常工作。

jieba库是中文分词工具。热搜关键词通常是短语,比如“某明星新电影定档”、“某地发生地震”,要统计话题焦点,需要先把这些短语切成有意义的词,再做词频统计。jieba在这方面表现稳定,安装简单,对中文支持好。

我自己在写代码时还有一个习惯:把依赖库统一写在一个requirements.txt文件里,这样换机器或者在团队里分享时,一条命令就能复现环境。具体文件内容如下:

requests==2.31.0 pandas==2.0.3 matplotlib==3.7.2 jieba==0.42.1 openpyxl==3.1.2

关于版本锁定,我建议初学者不要直接对版本做太严苛的锁定,但写成>=某个版本号的形式更好,比如requests>=2.25.0。因为某些库的新版本可能修复了旧版的安全漏洞,又或者新版API有变化,留有余地能让环境搭建更顺利。

2.3 为什么不用Selenium等重型工具

有的读者可能会问:为什么不用Selenium或者Playwright这类浏览器自动化工具?我用一句话概括:能用简单请求解决的问题,绝不上浏览器模拟。Selenium这类工具会启动一个真实的浏览器实例,占用大量系统资源,执行速度也慢,而且很容易被目标网站的防护策略识别。数据分析项目需要的是稳定、高效、可重复的数据获取方式,直接构造HTTP请求是最轻量也最可靠的手段。

这并不是说Selenium没有用。如果目标网站的数据是通过复杂的JavaScript动态渲染出来的,而且请求接口又做了很强的签名校验,那浏览器自动化确实是必要的。但微博热搜这个场景明显不属于这种情况,JSON接口直接暴露在开发者工具里,用requests就能稳定拿到数据。选型的原则永远是:先尝试最轻量的方案,遇到问题再考虑升级。

3. 爬虫核心实现

3.1 请求头构造与请求发送

写爬虫的第一步就是构造一个看起来像真实用户发出的HTTP请求。服务器在收到请求时,会首先检查请求头(Headers)里的User-Agent字段,如果这个字段显示是Python-requests,很多服务器会直接拒绝访问。我一般会伪装成一个常见浏览器的UA信息。

这一步的原理并不复杂——用户代理字符串告诉服务器“我是谁”,浏览器和爬虫的UA格式差异很大,服务器可以通过UA特征做基础的访问控制。当然,现在很多网站的防护策略远比这复杂,会综合IP、Cookie、访问频率等多个维度做判断,但UA伪装依然是爬虫请求的基础操作。

我建议把请求头单独封装成一个函数,方便复用和后期维护。下面是我在项目中实际使用的代码示例,注释部分标注了每个字段的作用:

import requests def get_headers(): """构造一个看起来像真实浏览器的请求头""" 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": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Referer": "https://weibo.com/", "Connection": "keep-alive" } return headers def fetch_hot_search(): """发送GET请求获取热搜数据JSON""" url = "https://weibo.com/ajax/side/hotSearch" # 请以开发者工具实际看到的接口为准 try: response = requests.get(url, headers=get_headers(), timeout=10) response.raise_for_status() # 如果状态码不是200,抛出异常 return response.json() except requests.RequestException as e: print(f"请求失败:{e}") return None

这里的timeout参数很多人会忽略,但我建议一定要写上。如果不设超时,万一目标网站响应异常慢,程序就会一直挂着等待,整个定时任务就被卡死了。设一个10秒的超时,请求失败就快速重试或者跳过,比死等要稳妥得多。

需要特别强调的是,response.raise_for_status()这一行非常关键。它会在HTTP状态码是4xx或5xx时抛出异常,让你能立刻感知到请求异常,而不是拿着一个404的空页面继续解析,浪费大量时间排查后续逻辑。

3.2 JSON数据解析与字段提取

拿到接口返回的JSON数据之后,接下来的任务就是把它解析成结构化的表格数据。为了弄清楚JSON里有哪些字段,我在开发时一般会先写一段临时代码把原始数据打印出来看看结构,比如用print(json.dumps(data, ensure_ascii=False, indent=2)),这样字段层级一目了然。

真实的微博热搜接口返回的数据大致包含这些信息:每个热搜条目有一个排名序号、一个话题关键词、一个热度值。有的热搜会带标签,比如“沸”表示讨论热度很高,“新”表示新上榜,“商”表示商业推广位。这些标签后面做分析时其实很有用,可以通过标签来筛选“自然热搜”和“商业热搜”。

解析代码需要注意两点:一是用.get()方法而不是直接下标访问,因为字段偶尔会缺失,直接下标访问会导致KeyError崩溃;二是热度值字段的格式处理——有些热度值直接是数字(如1234567),有些则是带单位的中文格式(如“123.4万”),如果后续要做数值计算,最好统一转换成整数。

def parse_hot_search(data): """解析热搜JSON数据,返回结构化列表""" hot_list = [] if not data: return hot_list # 假设数据结构为 data["data"]["realtime"],以实际接口返回为准 realtime_list = data.get("data", {}).get("realtime", []) for item in realtime_list: rank = item.get("rank", 0) keyword = item.get("word", "") raw_hot = item.get("raw_hot", 0) # 有些接口用raw_hot表示热度 label = item.get("label", "") # 热搜标签,可能是空字符串 if not keyword: continue hot_list.append({ "排名": rank, "关键词": keyword, "热度值": raw_hot, "标签": label }) return hot_list

parse_hot_search函数返回的是一个字典列表,每个字典代表一条热搜记录。接下来直接用pandas把它转成DataFrame,就可以开始各种分析操作了。

3.3 数据清洗与规范化处理

现实世界的数据永远是脏的,热搜数据也不例外。主要的脏数据情况有这些:

有些关键词里会带空格、特殊符号或者HTML实体字符(比如&),需要清理;热度值字段的格式不统一,有的是纯数字,有的是“123.4万”这样的带单位文本,需要转换成统一的数值;排名序号可能不是严格的从1开始连续排列,中间有跳号;偶尔还有完全重复的热搜条目,需要按关键词去重。

我建议写一个专门的数据清洗函数,在进入数据分析之前就把这些问题处理掉。处理热度值时,我习惯写一个专门的小函数来做单位换算,逻辑很直接:如果是纯数字就直接返回,如果包含“万”字就乘以10000,如果包含“亿”字就乘以100000000。

def normalize_hot_value(value): """将热度值统一转换为整数""" if isinstance(value, (int, float)): return int(value) if not value: return 0 text = str(value).strip() text = text.replace(",", "") if "亿" in text: return int(float(text.replace("亿", "")) * 100000000) if "万" in text: return int(float(text.replace("万", "")) * 10000) try: return int(float(text)) except ValueError: return 0

这个函数的思路是把所有可能性都考虑到位:数字直接返回,中文单位换算成数字,字符串里的千分位逗号先去掉,解析失败就返回0而不是直接让程序崩溃。实际运行下来,这个函数处理几十种不同的热度文本格式都没出过问题。

3.4 存储到Excel与CSV

数据规整好之后,存储环节我用pandas来操作。Excel格式适合人类阅读和二次编辑,CSV格式适合程序化处理和长期积累。两个格式都存,代价很小但灵活性很高。

保存Excel时需要指定一个合法的文件名,中文文件名在Windows系统上没问题,但建议还是用带日期的英文文件名更稳妥,比如hot_search_20250115.xlsx。CSV文件保存时,一个经典的坑是中文乱码问题——pandas默认的编码是UTF-8,但Excel打开UTF-8编码的CSV时会默认按GBK解码,导致乱码。解决方法是保存CSV时指定编码为utf-8-sig,这个带BOM的格式Excel能正确识别。

import pandas as pd from datetime import datetime def save_data(df, base_path="output"): """将DataFrame保存为Excel和CSV文件""" import os os.makedirs(base_path, exist_ok=True) today = datetime.now().strftime("%Y%m%d_%H%M%S") excel_path = os.path.join(base_path, f"hot_search_{today}.xlsx") csv_path = os.path.join(base_path, f"hot_search_{today}.csv") df.to_excel(excel_path, index=False, engine="openpyxl") df.to_csv(csv_path, index=False, encoding="utf-8-sig") print(f"数据已保存,Excel:{excel_path}") print(f"数据已保存,CSV:{csv_path}")

这里用os.makedirs(base_path, exist_ok=True)的好处是:目录不存在时会自动创建,存在时也不会报错。exist_ok=True这个参数是Python 3.2以后才有的,老代码里常见的写法是先判断os.path.exists()再创建,两行代码做的事现在一行就搞定了。

4. 热点趋势分析模块

4.1 分词统计:从热搜词到热点标签

数据存下来只是第一步,真正有意思的是从这些热搜词里提取“今天的互联网在关心什么”。我把这一块分成三个层次来做:词频统计、类别聚合、趋势分析。

词频统计的原理是:把每条热搜的关键词用jieba切成一个个词或短语,然后统计所有词的出现频次。出现频次高的词,往往意味着它是多个热点话题里的共同关键词,也就是当下的核心焦点。比如同一天里“科技”、“发布会”、“销量”这些词频繁出现在多条热搜里,就能判断出当天科技领域有一个大事件在发酵。

分词这块有个细节要注意:jieba默认的精确模式会把“某明星道歉”切成“某明星”和“道歉”,这没问题,但有些专有名词会被切碎,比如“iPhone”可能被切成“iPhone”和“Pro”。为了让统计结果更准确,我建议维护一个自定义词典,把常见的品牌名、人名、影视作品名加进去。jieba提供了add_word()方法,在程序启动时预先加载这些词,分词效果会好很多。

词频统计代码实现:

import jieba from collections import Counter def load_custom_words(): """加载自定义词典,避免专有名词被切碎""" custom_words = ["iPhone", "华为", "小米", "特斯拉", "世界杯", "周杰伦", "陈奕迅"] for word in custom_words: jieba.add_word(word) def count_keyword_frequency(df, top_n=20): """统计热搜关键词的词频,返回Top N""" load_custom_words() all_words = [] for keyword in df["关键词"].tolist(): words = jieba.lcut(keyword) # 过滤掉单字词和标点、停用词等无意义词 filtered = [w for w in words if len(w) > 1 and w not in stop_words] all_words.extend(filtered) word_counter = Counter(all_words) return word_counter.most_common(top_n)

stop_words是一个停用词集合,把“今天”“最新”“曝光”这类没有分析价值的词过滤掉。停用词表可以直接在网上找到开源版本,也可以根据自己的分析目的一步步积累维护。第一次跑出来的高频词表里如果混进了“那个”“什么”这类词,手动把它们加进停用词表,再跑一遍就会干净很多。

4.2 热度聚合:多维度的数据处理视角

词频统计看的是“数量”,热度聚合看的是“热度值”。同样是出现高频的词,不同话题的热度可能差几十倍。聚合的逻辑是:把包含同一个词的所有热搜的热度值加起来,得到这个词的“总热度贡献值”。

为了计算方便,我把热度值列转成数值类型后,用pandas的groupby操作来实现聚合。这个操作逻辑不复杂,但得到的结果往往比单纯的词频更有洞察力——某个词只出现了一次,但它出现在排名第一的热搜上,热度值可能比出现三次的小话题加起来还高。

实际处理时,我并不是直接对分词后的所有词做聚合,而是先做一次“核心词提取”,也就是对每条热搜关键词,基于自定义词典和jieba分词结果,只取第一个有实际意义的词或短语作为主标签(比如“某明星道歉”的主标签是“某明星”),然后按主标签聚合热度。

def aggregate_heat_by_keyword(df): """按核心标签聚合热度值""" # 先提取核心标签(简化版:取关键词的第一个分词结果) def extract_main_tag(keyword): words = jieba.lcut(keyword) for w in words: if len(w) >= 2 and w not in stop_words: return w return keyword df["主标签"] = df["关键词"].apply(extract_main_tag) agg_result = df.groupby("主标签")["热度值"].sum().reset_index() agg_result = agg_result.sort_values("热度值", ascending=False) return agg_result

4.3 话题分类:给热搜分个类

词频和热度聚合能告诉我们“什么词很热”,但还不足以回答“哪个领域今天很热”。要做到这一点,需要给每个热搜关键词打上一个领域标签,比如:社会、娱乐、科技、体育、财经、国际、健康、教育等。

分类的方法有好几种,我选了最实用的一种:维护一个“领域关键词映射表”,每个领域对应一组种子词,遍历热搜关键词去匹配。比如:

  • 科技:华为、小米、iPhone、AI、芯片、发布会、技术
  • 娱乐:明星、电影、综艺、演唱会、直播
  • 体育:比赛、夺冠、球员、球队、世界杯
  • 财经:股市、股价、涨价、企业、融资
  • 社会:警方、通报、地震、事故、失踪

这个映射表不需要一开始就特别齐全,随着数据积累不断完善。匹配逻辑也很简单:用if any(word in keyword for word in field_words)来判断一个热搜属于哪个领域,如果匹配不到任何领域就归类到“其他”。

实现代码:

def classify_fields(df): """将热搜关键词归类到领域""" field_map = { "科技": ["iPhone", "华为", "小米", "AI", "芯片", "发布会", "机器人", "卫星", "软件", "系统"], "娱乐": ["明星", "电影", "综艺", "演唱会", "歌手", "演员", "导演", "票房"], "体育": ["比赛", "夺冠", "球员", "球队", "世界杯", "奥运会", "冠军", "教练"], "财经": ["股市", "股价", "涨价", "融资", "企业", "市场", "经济", "银行"], "社会": ["警方", "通报", "地震", "事故", "失踪", "救援", "法院", "调查"], } def get_field(keyword): for field, words in field_map.items(): if any(word in keyword for word in words): return field return "其他" df["领域"] = df["关键词"].apply(get_field) return df

这种基于种子词的分类方法虽然简单粗暴,但实际效果还不错。它的好处是逻辑直观、容易调整——发现分类不准时,你只需要往对应的词表里添加或删除词,不需要重新训练模型。如果你的数据量足够大、分类需求更复杂,也可以考虑用机器学习文本分类方法,但在这个项目阶段,规则匹配的性价比是最高的。

4.4 趋势分析:从单次快照到时间序列

前三个分析都是在做“单次快照”的分析——抓一次数据,分析一次。但热点分析真正有价值的用法,是持续采集数据,观察一段时间内的变化趋势。这也是我推荐大家给爬虫加定时任务的原因。

运行一次采集程序后,把数据追加保存到一个累计的Excel文件中,反复运行几次之后,就有了时间序列数据。基于这个数据,可以画“某热点词的热度值随时间变化”的折线图——比如某条热搜从早上的第10名冲到晚上第1名,热度曲线呈现出爬坡、登顶、回落这样的完整生命周期。这种趋势信息对运营决策很有参考价值。

实现这个功能的思路不复杂:每次采集数据时,额外添加两列——“采集时间”和“采集批次”,然后追加到同一个CSV文件。后续分析时,按关键词过滤数据,再按时间排序,画折线图就可以了。

def append_to_history(df, history_path="output/hot_search_history.csv"): """将本次采集数据追加到历史记录文件中""" import os from datetime import datetime df["采集时间"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if os.path.exists(history_path): old_df = pd.read_csv(history_path) combined_df = pd.concat([old_df, df], ignore_index=True) else: combined_df = df combined_df.to_csv(history_path, index=False, encoding="utf-8-sig") print(f"历史记录已更新,累计 {len(combined_df)} 条")

结合定时调度(后面会讲),你就能积累出一份属于自己的热点变化历史库,这是做趋势分析最重要的基础数据资产。

4.5 情感倾向:快速判断话题正负面

我再分享一个进阶但不太复杂的分析维度——简单的情感倾向判断。热搜话题有的偏正面(“夺冠”“获奖”“破纪录”),有的偏负面(“事故”“地震”“通报”),有的中性(“发布”“官宣”)。通过一个正面词表和负面词表做匹配,就能给热搜贴上一个粗略的情感标签。

这个分析不需要复杂的NLP模型,核心就是一个字典匹配。我整理了一份大约几百个正负面词的词表,具体使用时会根据效果持续补充。

情感分析的价值在于:当天的热搜情感倾向整体偏负面时,可能说明有重大负面事件发生;整体偏正面时,可能是有大型庆典、赛事胜利或产品发布等事件。配合趋势分析,能捕捉到公众情绪在一天内的波动。

5. 可视化图表展示

5.1 中文显示与字体配置

用matplotlib画中文图表,最容易翻车的就是中文乱码问题。matplotlib默认字体是英文系的,直接画中文标签会出现一个个方框。解决方法是显式指定一个支持中文的字体,我一般用系统自带的微软雅黑。

import matplotlib.pyplot as plt # 设置中文字体 plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "PingFang SC"] plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题

这段配置务必放在任何绘图代码之前。axes.unicode_minus这个参数很多人会忘记设置,导致坐标轴上的负号显示成方块。这两行配置是matplotlib中文显示的“黄金两行”,建议直接背下来。

如果是在Linux服务器上运行(比如部署了定时任务),系统可能没有微软雅黑和黑体,这时需要先检查系统支持的中文字体,或者安装字体文件。我踩过一次坑:在Windows上开发好的脚本部署到Linux服务器,chart里全是方块,排查了半天发现是Linux服务器没装中文字体。解决办法是安装fonts-wqy-zenhei(文泉驿正黑),然后在代码里把字体名改成WenQuanYi Zen Hei。

5.2 词频Top20柱状图

先画词频最高的前20个词,以条形图形式展示。横轴是词频数,纵轴是关键词,为了让图更美观,我习惯用barh()画水平条形图,让关键词从高到低排列,视觉上更直观。

def plot_top_words(word_freq, top_n=20, output_path="output/top_words.png"): """绘制热搜词频Top N水平柱状图""" top_items = word_freq[:top_n] words = [item[0] for item in top_items][::-1] counts = [item[1] for item in top_items][::-1] plt.figure(figsize=(10, 8)) bars = plt.barh(words, counts, color="#4472C4") for bar, count in zip(bars, counts): plt.text(bar.get_width() + 0.3, bar.get_y() + bar.get_height() / 2, str(count), va="center", fontsize=10) plt.xlabel("出现次数") plt.title(f"微博热搜关键词词频 Top {top_n}") plt.tight_layout() plt.savefig(output_path, dpi=150) plt.close()

这里有个小技巧:数据先反转([::-1])再画,因为在水平条形图中,数据顺序是从下往上排列的,反转后词频最高的词会出现在最上方,符合阅读习惯。plt.text()那段是在每个条形右侧标注具体数值,一眼就能看出精确数据。

保存图片时用plt.close()而不是plt.show(),这在脚本化运行时非常重要。如果只调show()不调close(),多次绘图会累计打开多个窗口,可能把机器内存吃光。画完一张图就关掉,保持干净。

5.3 领域占比饼图

饼图最适合展示分类型别的占比情况。在完成话题分类之后,统计每个类别下有多少条热搜,就能画出一个直观的饼图。

def plot_field_distribution(df, output_path="output/field_pie.png"): """绘制热搜领域分布饼图""" field_counts = df["领域"].value_counts() plt.figure(figsize=(8, 8)) colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD"] plt.pie(field_counts, labels=field_counts.index, autopct="%1.1f%%", colors=colors[:len(field_counts)], startangle=90, counterclock=False) plt.title("微博热搜领域分布") plt.tight_layout() plt.savefig(output_path, dpi=150) plt.close()

画饼图有个细节:如果某个类别的数据太少,占比会非常小,标签会重叠看不清楚。实际项目里我会设定一个最小占比阈值,低于3%的类别全部合并到“其他”字段里再画,这样图面更干净。另外,startangle=90表示从12点方向开始绘制,counterclock=False表示按顺时针方向排列,这两参数能让饼图的布局更符合直觉。

5.4 热度趋势折线图

趋势折线图需要时间序列数据,也就是前面说的重复采集积累的历史数据。选定一个你要追踪的热搜关键词,从历史数据里筛出所有包含它的记录,按采集时间排序,把热度值画成折线。

def plot_trend_for_keyword(history_df, keyword, output_path="output/trend.png"): """绘制指定关键词的热度变化趋势折线图""" target_data = history_df[history_df["关键词"].str.contains(keyword, na=False)] if target_data.empty: print(f"没有找到包含「{keyword}」的数据") return target_data = target_data.sort_values("采集时间") plt.figure(figsize=(12, 6)) plt.plot(target_data["采集时间"], target_data["热度值"], marker="o", linestyle="-", color="#E63946", linewidth=2) plt.xlabel("采集时间") plt.ylabel("热度值") plt.title(f"「{keyword}」热度变化趋势") plt.xticks(rotation=45) plt.grid(True, linestyle="--", alpha=0.6) plt.tight_layout() plt.savefig(output_path, dpi=150) plt.close()

折线图的x轴是时间,如果采集频率低(比如每小时一次),数据点只有二十几个,没必要做平滑处理。如果采集频率高(比如5分钟一次),一天就有288个点,图像会显得很密,这时可以用滚动平均(rolling mean)做平滑,或者直接按采样间隔取一部分点显示。实际项目中我是设置了每30分钟采集一次,一天下来48个点,折线图效果比较适中。

6. 定时任务与自动化部署

6.1 方案选型:本地定时任务的三种方式

想让这个爬虫项目不依赖手动运行,需要给它配置定时调度。结合不同操作系统和使用场景,我整理了三种方式:

Windows系统最简单的方式是使用任务计划程序,通过图形界面创建一个每天或每几小时运行一次的触发器,执行命令指向Python脚本。这种方式门槛最低,适合Windows用户。

macOS和Linux系统可以直接用crontab写定时任务,一条crontab -e命令加一行配置就能搞定。比如30 * * * *代表每小时的30分运行一次。

Python层面的方案是使用schedule库或者APScheduler库,在代码内部实现定时循环。这种方式跨平台性最好,写一个while True加sleep(1800)的循环配合schedule库,30行代码就能实现定时采集。但这种方式有局限——如果电脑睡眠或者程序进程被杀死,定时就不会触发。

我个人在长期运行项目时更倾向于用操作系统的原生调度,比Python进程内调度更稳定——Python进程挂在后台一旦崩溃或者被系统清理,定时循环就断了,而操作系统级别的调度可以确保每次都拉起一个独立的进程来执行脚本。

6.2 Windows任务计划程序配置

在Windows系统上配置的步骤大致是这样的:先打开任务计划程序,选择“创建基本任务”,然后设置任务名称(比如“热搜采集”),选择触发器为“每天”或者“重复执行”,设置具体时间(我一般设置从早上6点到晚上12点,每30分钟执行一次),在操作里选择“启动程序”,程序填Python解释器的完整路径,参数填脚本的完整路径。

这里有一个非常容易踩的坑:直接填“python”而不是Python的完整路径,任务执行时会找不到环境变量。更麻烦的是,如果你电脑上装了多个Python版本,可能执行的是不同环境,缺少项目的依赖库。我建议在任务计划里程序这一栏,直接填你项目对应虚拟环境里的python.exe完整路径,而不是系统PATH里的python。

比如我用虚拟环境管理依赖,虚拟环境的Python路径是D:\venv\hotsearch\Scripts\python.exe,就填这个,参数填D:\projects\hotsearch\main.py,起始于目录填D:\projects\hotsearch。这样能彻底避免环境错乱的问题。

6.3 Linux服务器Crontab配置

如果是Linux服务器,crontab的配置更简洁。先编辑定时任务,然后添加一行配置:

crontab -e

在里面添加:

30 * * * * cd /home/user/hotsearch && /usr/bin/python3 main.py >> logs/run.log 2>&1

这行配置的意思是:每小时的第30分钟执行一次,先切换到项目目录,再执行main.py,标准输出和错误输出都追加到run.log里。2>&1这个重定向很重要,如果不加,脚本的报错信息会直接丢失(通过邮件发给系统账户,几乎没人会去查),加上之后所有日志都集中在一个文件里,排查问题非常方便。

需要注意的是,如果你的采集频率是每小时一次,一天就是24次,每次采集都会新增数据文件和追加历史记录。日积月累,output目录下会积累很多文件。建议配合一个简单的日志轮转脚本——每天定时把前一天生成的数据文件移动到按日期分层的目录里,比如output/2025/01/15/,或者定期清理过旧的文件。

6.4 采集频率的合理选择

采集频率的选择需要平衡数据密度和访问压力。我在实际使用中选择了30分钟一次的频率,原因有两点:一是微博热搜本身的更新节奏大约是十几分钟到半小时一次,30分钟的粒度不会错过重要变化,数据量也适中;二是过于频繁的请求(比如每1分钟一次)对目标服务器的压力明显上升,容易触发反爬策略。

如果你是给学生做课堂作业,或者只是体验一下流程,建议不要设置低于5分钟一次的频率。爬虫的基本礼仪是:不要让你的采集行为对目标网站造成可感知的影响,采集频率宁疏勿密。

7. 常见问题与排查技巧

7.1 请求状态码异常

最典型的请求异常就是返回403或418状态码。403表示服务器理解请求但拒绝执行,通常意味着你的请求被识别为爬虫;418是另一个常见的反爬状态码,源自“我是一个茶壶”的彩蛋HTTP状态码,实际含义也是服务器拒绝处理。

遇到这类问题,我会按顺序排查:先检查User-Agent是否正确伪装成浏览器;再看请求头里的Accept和Accept-Language字段是否完整;然后检查请求频率是否过高——如果短时间发了大量请求,IP可能暂时被限制,需要停一段时间再试。

如果以上都检查过了还是403,终极方案是把浏览器里完整的Cookie字符串(从开发者工具的请求头里复制)放到请求头里带上。这样身份验证信息就和真实用户一致了,一般能解决问题。但要注意,Cookie是有时效性的,过期后需要重新复制。

7.2 JSON解析报错

有时候接口正常返回了,但解析JSON时报错,最常见的提示是Expecting value: line 1 column 1。这通常意味着服务器返回的响应体不是JSON格式,而是一个HTML错误页面。比如服务器返回了一个“访问过于频繁”的提示页面,或者返回了JavaScript验证挑战页面。

排查方法是先把响应内容打出来看看:

print(response.text[:500])

看到具体内容就能判断是哪种情况。如果是验证页面,说明触发了反爬机制,需要降低请求频率或者检查请求头。如果是在自己的测试环境里误把HTML页面地址当成接口地址了,那就需要回到开发者工具里重新找到真正的JSON接口。

7.3 Excel文件被占用无法写入

用pandas保存Excel时,如果之前用Excel软件手动打开了这个文件,程序就会报PermissionError。这是Windows文件锁定机制导致的——Excel打开文件时会独占访问权限,其他进程无法写入。

解决方法是:要么在程序里捕获PermissionError并换个文件名重新保存,要么养成好习惯——程序输出目录下的Excel文件一直保持关闭状态。我自己写项目时倾向于把输出文件名加上时间戳(比如当前分钟),即使程序意外重跑或者文件被占用,也只是新生成一个文件,不会让整个任务失败。

7.4 数据为空或字段缺失

接口有时候会返回不完整的数据,可能是个别条目的字段缺失,比如没有热度值,或者关键词为空。解析代码中我用.get()方法,就是为了应对这种字段缺失的情况——直接返回默认值,程序继续运行,同时清洗函数把空关键词的条目过滤掉。

有时候整个列表都是空的,一般有三个原因:接口地址变了、请求被重定向到登录页、数据格式改版。不管是哪种情况,第一步都是回到浏览器开发者工具,重新查看当前的真实接口和数据格式。这也是为什么我一直强调“分析数据源的能力”比“跑通代码”更重要——网站改版是常态,你的爬虫代码维护本质上就是在和数据源的每一次变动做博弈。

7.5 反爬策略应对原则

关于反爬,我的原则是:遵守规则,控制频率,不突破网站明确的反制措施。做数据分析项目,应该把“可持续采集”放在“采集数量”前面。这个项目本身就是学术和技术学习用途,不需要与网站的反爬策略正面对抗。

几个不招人烦的采集习惯:

  • 请求间隔不低于5分钟,建议30分钟以上;
  • 采集时间避开网站的流量高峰时段;
  • 单次只抓取必要的字段,不做全站爬取;
  • 抓到的数据仅用于个人学习和非商业分析,不进行公开传播。

这其实也对应了爬虫开发的职业道德:你把别人的公开数据拿来做分析,要尊重对方的内容权益和服务资源。这个问题想明白了,你的爬虫之路才能走得更长远。

8. 项目完整代码与运行指南

8.1 项目目录结构

一个清晰的项目目录结构,能让任何人(包括几个月后的自己)快速理解项目内容。我建议的目录结构如下:

hotsearch/ ├── main.py # 主入口程序 ├── requirements.txt # 依赖库清单 ├── config.py # 配置文件(请求头、停用词表、领域词表等) ├── crawler.py # 爬虫模块(请求、解析、清洗) ├── analyzer.py # 分析模块(分词、分类、情感、聚合) ├── visualizer.py # 可视化模块(图表绘制) ├── output/ # 输出目录(数据文件和图片) │ ├── hot_search_20250115_143000.xlsx │ ├── top_words.png │ └── field_pie.png └── logs/ # 日志目录

模块拆分的价值在于:每个文件职责单一,替换和调试都很方便。修改反爬策略时只需要改crawler.py,调整分类词表时只动config.py,不会牵一发动全身。很多初学者的项目把所有逻辑堆在一个大文件里,几百行代码找问题非常痛苦,拆开之后清爽得多。

8.2 完整代码整合与打包

下面给出一个简化但能直接运行的主入口main.py,把采集、解析、清洗、存储、分析和可视化串起来。每个模块函数前面已经写过,这里主要展示模块之间的调用关系:

import sys import os from datetime import datetime # 确保项目根目录在模块搜索路径中 sys.path.append(os.path.dirname(os.path.abspath(__file__))) from crawler import fetch_hot_search, parse_hot_search, clean_data from analyzer import count_keyword_frequency, classify_fields, aggregate_heat_by_keyword from visualizer import plot_top_words, plot_field_distribution from storage import save_data, append_to_history def main(): print(f"开始采集微博热搜数据:{datetime.now()}") # 1. 获取数据 data = fetch_hot_search() if not data: print("采集失败,程序退出") return # 2. 解析并清洗 hot_list = parse_hot_search(data) df = clean_data(pd.DataFrame(hot_list)) # 3. 保存数据 save_data(df) append_to_history(df) # 4. 分析 word_freq = count_keyword_frequency(df, top_n=20) df = classify_fields(df) agg_result = aggregate_heat_by_keyword(df) # 5. 可视化 plot_top_words(word_freq) plot_field_distribution(df) print("本次采集与全部分析完成") if __name__ == "__main__": import pandas as pd main()

在使用前,需要用pip安装依赖:

pip install -r requirements.txt

然后直接运行:

python main.py

第一次运行时建议把采集频率调低一些(比如只跑一次),确认整个链路没有问题之后再配置定时任务。我见过太多人一上来就配置了每5分钟跑一次的任务,结果接口返回异常或者Excel文件被占用,日志文件里堆满了报错信息,反而是浪费了更多时间去排查。

8.3 运行效果与数据分析实例

实际跑一次的结果大致是怎样的?我举一个虚构的例子说明。

某次采集共获得50条热搜数据。分类结果:社会类15条,娱乐类12条,科技类8条,体育类7条,财经类5条,其他3条。词频统计发现,“发布”出现6次,“比赛”出现5次,“股价”出现4次——说明当天有大型产品发布会、重要赛事和资本市场波动。领域占比饼图上,社会类占比30%,娱乐类24%,科技类16%。

对热度值排序后发现,排名第一的热搜热度值达到800万,远超第二名(500万)。如果对这条热搜做趋势追踪,还能看出它是从上午开始爬升、午后登顶、晚间回落的典型“单日热点”形态。

这组数据能给人不少启发:一天中不同类型的热搜占比反映了公众注意力的投向;高频关键词和超高热度值的组合,往往意味着一个重要事件正在发酵;而借助历史累积数据,还能进一步判断一个热点是“昙花一现”还是“持续升温”。

9. 项目总结与个人经验

9.1 实际体验分享

这个项目写完之后,我持续运行了一段时间。最直观的收获是:每天打开电脑,output目录里已经有当天凌晨到现在的所有热搜采集记录,配合几张图表,这个时间段里大家关心什么、热度如何演变,一目了然。这种用数据“看见”一天脉络的体验,比手动刷榜单有信息量得多。

从技术成长角度看,这个项目把Python爬虫、数据处理、中文分词、可视化和定时调度这五块技能全部串了起来,每个环节都涉及真实世界里的坑——请求头被识别、JSON字段变动、中文字体缺失、文件被占用。我把这些坑一个个踩平的过程,收获远大于单纯看十篇教程。

9.2 后续可以扩展的方向

这个项目的结构留出了很多扩展空间,我自己已经在规划几个方向:

把单日分析扩展到长期趋势库,观察一周、一个月内热搜主题的演变规律,甚至按周几做对比分析(比如娱乐类热搜是不是周五明显增多)。把情感分析做得更细,结合正负面词匹配结果,计算每天的热搜情绪指标,尝试发现情绪与事件类型的相关性。接入更多数据源,比如把微博热搜和某视频平台的热榜数据做对比,分析不同平台上用户关注焦点的差异。

有人可能觉得爬虫项目“不登大雅之堂”,但我不这么认为。数据获取是数据分析的最前端,能稳定、高效、体面地从公开数据源获取数据,本身就是一门很实用的手艺。微博热搜爬虫是这个方向很好的一块磨刀石——数据量级适中、频率适合练习、结构不算复杂、分析空间很大。把这块石头磨锋利了,后面面对更复杂的数据源时,你就有了从容应对的底气。

最后再分享一个小经验:做这类项目时,一定要把代码当成长期维护的工具来写,而不是“跑一次就完事”的一次性脚本。网络环境在变,网站结构在变,依赖库在变。写代码时多写一个异常处理、多做一次数据校验、多设一个超时时间,都是在为未来的自己省时间。保持对数据源的敬畏,保持代码的整洁,这个项目就能持续为你产出价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询