Python爬虫实战:抓取今日头条热门文章数据与反爬策略详解
1. 项目概述与核心价值
最近在分析内容趋势时,发现很多朋友对抓取今日头条这类资讯平台的热门文章有需求。无论是做市场分析、舆情监控,还是内容创作寻找灵感,能自动化地获取热门内容,效率会高很多。我自己在做一些内容研究项目时,也经常需要这个能力。今天就来详细聊聊,如何用Python构建一个稳定、高效的今日头条热门文章爬虫。
这个项目听起来简单,但实际操作中会遇到不少“坑”,比如今日头条的动态加载、反爬机制、数据清洗等。网上很多教程要么过于简单只讲基础请求,要么代码已经过时无法运行。我会结合最新的网络环境(2023年底至2024年初)和实战经验,从环境搭建、请求分析、数据解析到数据存储,一步步拆解,并重点分享那些教程里不会写的避坑技巧和性能优化思路。无论你是刚学Python爬虫的新手,还是想寻找一个更稳健方案的老手,这篇文章都能给你提供可直接复现的代码和思路。
2. 环境准备与工具选型
工欲善其事,必先利其器。在开始写代码之前,选择合适的工具和配置好环境是成功的第一步。这里的选择不仅关乎代码能否运行,更关系到后续爬虫的稳定性、可维护性和反爬对抗能力。
2.1 Python环境与核心库安装
首先确保你的Python版本在3.8及以上,这是目前多数库稳定支持的主流版本。不建议使用过老的Python 2.7或3.6,可能会遇到依赖库兼容性问题。
核心库我们主要依赖以下几个:
- requests : 用于发送HTTP请求。这是最基础、最常用的库,比Python内置的urllib更友好、功能更强大。
- BeautifulSoup4 (bs4) : 用于解析HTML文档,从复杂的网页标签中提取我们需要的数据。它语法简洁,学习成本低。
- lxml : 这是一个解析器,BeautifulSoup可以使用它作为后端引擎,其解析速度比Python标准库中的
html.parser快很多,特别是在处理大量数据时优势明显。 - pandas : 非必须,但强烈推荐。用于将爬取到的结构化数据(列表、字典)轻松地转换为DataFrame,并导出为CSV或Excel文件,方便后续分析。
- json : Python标准库,用于处理今日头条接口返回的JSON格式数据。
安装命令非常简单,打开你的终端(Windows的CMD或PowerShell,Mac/Linux的Terminal)执行以下命令:
pip install requests beautifulsoup4 lxml pandas
如果你遇到网络问题导致下载慢或失败,可以使用国内的镜像源,例如清华源:
pip install requests beautifulsoup4 lxml pandas -i https://pypi.tuna.tsinghua.edu.cn/simple
注意 :有些教程可能会推荐使用
selenium或playwright等浏览器自动化工具。对于今日头条的热门文章列表页, 绝大多数情况下我们不需要它们 。这些工具模拟浏览器,资源消耗大、速度慢,更适合处理需要执行JavaScript才能渲染出内容的页面。而今日头条的热门文章数据通常通过接口(API)直接返回,用轻量级的requests库足矣。盲目使用重型工具只会增加复杂度和不稳定性。
2.2 开发工具与辅助配置
- 代码编辑器 :VSCode或PyCharm都是极好的选择。VSCode轻量、插件丰富;PyCharm是专业的Python IDE,对代码提示、调试支持更完善。选择你顺手的即可。
- 浏览器开发者工具 :这是爬虫工程师的“眼睛”。Chrome或Edge浏览器的F12开发者工具是关键。我们主要使用 “网络”(Network) 选项卡来观察页面加载过程中浏览器发送了哪些请求,特别是XHR/Fetch请求,这些往往就是获取数据的接口。
一个关键的前置操作 :在浏览器中打开今日头条的官网,并进入“热榜”或“推荐”页面。打开开发者工具(F12),切换到“网络”选项卡,然后刷新页面。你会看到大量请求记录。我们的目标就是从中找到那个真正返回文章列表数据的请求。这个过程称为“抓包分析”。
3. 核心思路与请求分析
爬虫的核心在于找到数据源。对于现代网页,数据往往不是直接写在HTML里,而是通过JavaScript异步加载的。因此,直接爬取网页HTML(用 requests.get )很可能拿不到文章列表。
3.1 寻找数据接口
- 在浏览器中打开今日头条的“热榜”页面(例如
https://www.toutiao.com/hot-event/hot-board/或首页推荐流)。 - 按F12打开开发者工具,确保“网络”选项卡是开启状态,并勾选上“保留日志”(Preserve log)。
- 刷新页面,观察“网络”选项卡中出现的所有请求。
- 在请求列表上方的筛选器里,点击“Fetch/XHR”。这会将请求过滤为更可能包含数据的AJAX请求。
- 仔细查看这些请求的“名称”(Name)和“响应”(Response)预览。你会看到一些请求的响应内容是清晰的JSON格式,里面包含了文章标题、链接、阅读量等信息。
如何识别正确的接口?
- 看URL :接口URL可能包含
feed、list、hot、api等关键词。 - 看响应 :点击一个请求,在“响应”标签页里查看内容。如果看到
title、source、comments_count、read_count等字段,那基本就是它了。 - 看请求头 :重点关注该请求的
Request Headers,特别是User-Agent、Referer和可能的Cookie。我们写代码时需要模拟这些信息。
以我最近一次分析为例,我找到了一个类似 https://www.toutiao.com/api/pc/list/feed?category=hot_board&... 的接口。它的响应结构大致如下:
{
"data": [
{
"title": "文章标题1",
"source": "来源媒体",
"comments_count": 123,
"read_count": 45678,
"detail_url": "https://www.toutiao.com/article/123456789/",
"behot_time": 1678888888
},
// ... 更多文章
],
"has_more": true
}
请注意 :今日头条的接口地址和参数可能会随时间变化,上述URL仅为示例。你必须以自己当时在开发者工具中分析到的实际接口为准。
3.2 模拟请求与参数解析
找到接口后,我们需要在Python代码中模拟这个请求。关键点在于构造请求头(Headers)和查询参数(Params)。
从开发者工具中,复制该接口请求的 cURL 命令是最快的方式。在请求上右键 -> 复制 -> 复制为cURL (bash)。然后,你可以利用在线工具或手动解析,将其转化为Python requests 的代码。
请求头中最关键的几项:
- User-Agent : 标识客户端类型。必须设置一个常见的浏览器UA,否则服务器可能直接拒绝。例如:
'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' - Referer : 表示请求的来源页面。对于今日头条,通常设置为其主域名即可,如
'Referer': 'https://www.toutiao.com/'。这能增加请求的合法性。 - Cookie : 用户会话标识。对于公开的热门数据,有时不需要Cookie也能获取;但如果请求被拒,可能需要从浏览器中复制一份有效的Cookie填入。 注意:不要泄露自己的个人Cookie 。
接口URL通常带有查询参数,例如: ?category=hot_board&aid=24&app_name=toutiao_web&offset=0&count=20&... 这些参数控制了数据的类型、分页等。其中 offset 和 count 很可能控制分页(起始位置和获取数量)。在代码中,我们可以将这些参数放在一个字典里,传递给 requests.get() 的 params 参数。
4. 完整爬虫代码实现与解析
下面,我将结合上述分析,给出一个完整的、可运行的爬虫示例。这个示例包含了请求发送、数据处理、异常处理和简单存储。
4.1 基础爬取函数
首先,我们实现核心的爬取函数 fetch_hot_articles 。
import requests
import json
import time
import pandas as pd
from typing import List, Dict, Optional
def fetch_hot_articles(offset: int = 0, count: int = 20) -> Optional[List[Dict]]:
"""
从今日头条接口获取热门文章列表
Args:
offset: 分页偏移量
count: 每次请求获取的文章数量
Returns:
文章字典列表,失败则返回None
"""
# 目标API接口 (请根据你实际分析得到的接口替换此URL)
url = "https://www.toutiao.com/api/pc/list/feed"
# 请求头,模拟浏览器访问
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',
'Referer': 'https://www.toutiao.com/',
# 如有需要,可在此处添加Cookie,但公开数据通常不需要
# 'Cookie': '你的Cookie字符串'
}
# 查询参数
params = {
'category': 'hot_board', # 热点类别
'aid': '24',
'app_name': 'toutiao_web',
'offset': offset, # 分页偏移
'count': count, # 每页数量
'min_behot_time': '0', # 控制时间范围,0表示最新
# 可能还有其他动态参数,需根据实际情况调整
}
try:
response = requests.get(url, headers=headers, params=params, timeout=10)
# 检查HTTP状态码
response.raise_for_status()
# 解析JSON响应
data = response.json()
# 检查API返回的业务状态码 (今日头条的接口可能在`data`字段或根层级有状态码)
# 这里需要根据实际接口响应结构调整判断逻辑
if data.get('message') == 'success' and 'data' in data:
articles_raw = data['data']
processed_articles = []
for item in articles_raw:
# 提取所需字段,字段名需根据实际接口响应确定
article = {
'title': item.get('title', ''),
'source': item.get('source', ''),
'comments_count': item.get('comments_count', 0),
'read_count': item.get('read_count', 0),
'detail_url': item.get('detail_url', ''),
# 时间戳转换
'behot_time': item.get('behot_time', 0),
'datetime': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(item.get('behot_time', 0))) if item.get('behot_time') else ''
}
# 确保detail_url是完整链接
if article['detail_url'] and not article['detail_url'].startswith('http'):
article['detail_url'] = 'https://www.toutiao.com' + article['detail_url']
processed_articles.append(article)
return processed_articles
else:
print(f"API返回数据格式异常或失败: {data.get('message')}")
return None
except requests.exceptions.RequestException as e:
print(f"网络请求失败: {e}")
return None
except json.JSONDecodeError as e:
print(f"JSON解析失败: {e}")
print(f"响应文本: {response.text[:200]}") # 打印前200字符辅助调试
return None
代码关键点解析:
- 超时设置 :
timeout=10非常重要。它防止了因网络慢或服务器无响应导致的程序长时间卡死。 - 状态检查 :
response.raise_for_status()会在HTTP状态码不是200时抛出异常,让我们能及时处理网络错误。 - JSON解析 :使用
response.json()直接解析。一定要用try-except包裹,因为服务器可能返回非JSON内容(如反爬验证页面)。 - 数据提取 :使用
.get()方法安全地获取字典值,避免因字段缺失导致KeyError。 - URL补全 :检查
detail_url是否为完整URL,如果不是,拼接上域名前缀。
4.2 分页爬取与数据存储
单次请求获取的数据有限,我们需要实现分页爬取,并将所有数据保存下来。
def crawl_multiple_pages(total_pages: int = 5, per_page: int = 20) -> List[Dict]:
"""
分页爬取多页热门文章
Args:
total_pages: 需要爬取的总页数
per_page: 每页文章数量
Returns:
所有爬取到的文章列表
"""
all_articles = []
for page in range(total_pages):
print(f"正在爬取第 {page + 1} 页...")
offset = page * per_page
articles = fetch_hot_articles(offset=offset, count=per_page)
if articles:
all_articles.extend(articles)
print(f" 成功获取 {len(articles)} 篇文章。")
else:
print(f" 第 {page + 1} 页爬取失败,停止。")
break
# 礼貌性延迟,避免请求过快
time.sleep(2)
print(f"爬取结束,共获取 {len(all_articles)} 篇文章。")
return all_articles
def save_to_csv(articles: List[Dict], filename: str = 'toutiao_hot_articles.csv'):
"""
将文章列表保存为CSV文件
Args:
articles: 文章字典列表
filename: 输出文件名
"""
if not articles:
print("没有数据可保存。")
return
df = pd.DataFrame(articles)
# 选择要保存的列并排序
columns_to_save = ['title', 'source', 'read_count', 'comments_count', 'datetime', 'detail_url']
df = df[columns_to_save]
df.to_csv(filename, index=False, encoding='utf-8-sig') # utf-8-sig支持Excel直接打开无乱码
print(f"数据已保存至 {filename}")
# 主程序
if __name__ == '__main__':
# 爬取5页,每页20条数据
hot_articles = crawl_multiple_pages(total_pages=5, per_page=20)
if hot_articles:
save_to_csv(hot_articles)
# 也可以打印前几条看看
for i, article in enumerate(hot_articles[:3]):
print(f"{i+1}. {article['title']} | 阅读:{article['read_count']} | 来源:{article['source']}")
分页逻辑说明 :分页的核心是 offset 参数。第一页 offset=0 ,第二页 offset=20 ,以此类推。 time.sleep(2) 是一个简单的延迟,降低请求频率,是对目标网站的一种礼貌,也能减少触发反爬的风险。
5. 高级技巧与反爬策略应对
直接使用上面的代码,在短时间内可能可以运行。但今日头条作为大型平台,肯定有反爬机制。以下是几种常见情况及应对策略。
5.1 请求被拒与签名验证
现象 :代码运行后,返回的数据为空,或者响应内容是一个提示“请求异常”的JSON,甚至直接返回验证页面(如滑块验证码)。
原因分析 :现代Web API,尤其是移动端或重要数据接口,常采用签名机制。服务器会要求请求中包含一个由特定算法生成的签名( signature 或 _signature ),该签名通常由URL路径、查询参数、时间戳和一个密钥(salt)通过加密算法(如MD5, SHA1, HMAC)计算得出。客户端(浏览器或App)的JavaScript代码会实时计算这个签名并附加到请求中。我们的Python脚本如果没有计算并发送正确的签名,请求就会被拒绝。
应对策略 :
- 逆向JavaScript :这是最根本但也是最复杂的方法。在开发者工具的“源代码”(Sources)选项卡中,搜索包含
sign、encrypt、token等关键词的JS文件,尝试找到生成签名的函数,并用Python重写该算法。这需要一定的JavaScript逆向工程能力。 - 寻找替代接口 :有时,网站的不同入口或不同版本(如WAP版、旧版API)可能签名验证较弱甚至没有。可以尝试寻找其他数据源。
- 使用自动化工具 :如果逆向难度太大,可以考虑使用
selenium或playwright控制真实浏览器去加载页面,然后从页面中提取已经渲染好的数据。但这种方法效率低、资源消耗大,仅作为备选。 - 谨慎使用代理IP和User-Agent池 :如果是因为单个IP请求频率过高被封,可以考虑使用代理IP池和随机切换User-Agent来分散请求。但这无法解决签名问题。
实操心得 :对于今日头条的热门列表,在我撰写本文时,上述示例中的基础接口可能仍有效,或者签名逻辑相对简单。如果失效,第一步应该是重新“抓包”,看看最新的请求参数比我们代码里多了哪些,特别是那些长串的无规律字符,那很可能就是签名。可以尝试将浏览器中成功请求的所有参数原封不动地复制到Python代码中,如果能成功,再逐个参数分析哪些是固定的,哪些是动态生成的。
5.2 数据解析与清洗
获取到数据后,原始数据可能比较杂乱,需要进行清洗。
- 字段缺失处理 :如代码中所用
.get(key, default),为可能缺失的字段提供默认值。 - 去重 :由于分页或热点更新,可能爬取到重复文章。可以根据
detail_url或title进行去重。 - 数据格式化 :时间戳转换为可读日期;数字字符串转换为整型;清理标题中的特殊字符或空格。
- 过滤无效数据 :有些条目可能不是文章(可能是广告、视频等),可以根据
source字段或URL特征进行过滤。
增强的数据清洗示例 :
def clean_article_data(articles: List[Dict]) -> List[Dict]:
cleaned = []
seen_urls = set() # 用于URL去重
for article in articles:
url = article.get('detail_url', '')
title = article.get('title', '').strip()
# 1. 基础有效性检查
if not url or not title:
continue
# 2. URL去重
if url in seen_urls:
continue
seen_urls.add(url)
# 3. 过滤非文章内容(示例:根据来源或URL模式)
if 'ad' in article.get('source', '').lower():
continue
# 4. 转换阅读量为整数
try:
article['read_count'] = int(article.get('read_count', 0))
except (ValueError, TypeError):
article['read_count'] = 0
cleaned.append(article)
return cleaned
5.3 提升爬虫的健壮性
- 异常重试机制 :网络请求可能因各种原因失败。可以引入重试逻辑,例如使用
tenacity库或自己实现一个简单的重试循环。import requests from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10)) def fetch_with_retry(url, headers, params): response = requests.get(url, headers=headers, params=params, timeout=15) response.raise_for_status() return response - 日志记录 :使用Python的
logging模块替代print,可以更好地记录程序运行状态、错误信息,方便后期排查问题。 - 配置文件 :将URL、请求头、参数等配置信息写入单独的配置文件(如
config.py或config.yaml),使代码更清晰,易于修改。
6. 常见问题与排查指南
在实际操作中,你几乎一定会遇到一些问题。下面是一个快速排查清单。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
返回 <html> 页面或验证码 |
1. 请求头不完整或错误。 2. IP被暂时限制。 3. 缺少关键参数(如签名)。 |
1. 核对并补全 User-Agent , Referer 。 2. 暂停程序,等待几分钟或更换网络环境再试。 3. 重新抓包,对比浏览器请求与代码请求的所有参数是否完全一致。 |
返回 {"message":"error"} 等JSON |
接口参数错误或过期。 | 1. 检查 category , aid 等参数值是否正确。 2. 检查是否有时间戳( _ )或签名( signature )参数,它们可能已更新。 |
json.decoder.JSONDecodeError |
服务器返回的不是JSON,可能是HTML反爬页面。 | 1. 打印 response.text 的前500字符查看实际返回内容。 2. 检查请求是否被重定向。 |
数据为空( data 列表为空) |
1. 分页参数 offset 超出范围。 2. 请求的类别( category )不对。 |
1. 尝试将 offset 设为0, count 设小一点。 2. 在浏览器中手动修改URL参数,测试哪个 category 值能返回数据。 |
| 只能爬到少量数据 | 接口有最大数量限制。 | 1. 尝试减小 count 参数(如设为10)。 2. 通过多次请求不同时间范围( min_behot_time )的数据来获取更多。 |
| 程序运行缓慢 | 1. 网络延迟。 2. 没有使用连接池。 3. 同步请求阻塞。 |
1. 适当增加 timeout 。 2. 考虑使用 requests.Session() 复用连接。 3. 对于大规模爬取,可考虑异步库 aiohttp (进阶内容)。 |
一个关键的调试技巧 :在代码中,将你构造的最终请求URL和头部信息打印出来,与浏览器开发者工具里看到的进行 逐字对比 。一个多余的逗号、一个缺失的引号都可能导致失败。可以使用如下代码辅助调试:
prepared_req = requests.Request('GET', url, headers=headers, params=params).prepare()
print(f"请求URL: {prepared_req.url}")
print(f"请求头: {dict(prepared_req.headers)}")
7. 项目扩展与进阶思路
基础爬虫完成后,你可以根据需求进行扩展:
- 定时任务 :使用
schedule库或操作系统的cron/Task Scheduler,让爬虫每隔一段时间(如每小时)自动运行一次,追踪热点变化。 - 数据入库 :将数据保存到数据库(如SQLite, MySQL, MongoDB)而非CSV,便于复杂查询和分析。
- 内容深度爬取 :上述代码只爬取了文章列表。你可以根据
detail_url进一步爬取每篇文章的详细正文、评论等。注意,这会产生大量请求,务必遵守robots.txt,并添加更长的延迟。 - 数据分析与可视化 :用
pandas和matplotlib等库对爬取的数据进行分析,比如统计哪个来源的热文最多、阅读量和评论数的关系、热点话题随时间的变化趋势等。 - 构建简单API或界面 :使用
Flask或FastAPI将爬虫包装成一个服务,提供数据查询接口,或者用streamlit快速构建一个数据看板。
最后一点个人体会 :爬虫技术是与网站反爬措施不断博弈的过程。今日头条的接口今天有效,明天可能就变了。因此,最核心的能力不是记住某段代码,而是掌握“抓包分析 -> 模拟请求 -> 解析数据 -> 处理异常”这一套方法论。保持耐心,细心观察,多动手尝试,你就能应对大多数类似的爬虫需求。上面的代码为你提供了一个坚实的起点和清晰的框架,当你遇到新问题时,知道该从哪里入手去分析和解决。
更多推荐



所有评论(0)