Python爬虫实战:抓取今日头条热门文章,应对反爬与数据解析

发布时间:2026/7/30 4:28:24
Python爬虫实战:抓取今日头条热门文章,应对反爬与数据解析 1. 项目概述与核心价值最近在分析内容趋势时我经常需要获取不同平台的热门资讯作为数据样本。手动收集不仅效率低下而且难以进行量化分析。于是我决定用Python写一个爬虫专门用来抓取今日头条的热门文章。这个项目听起来简单但实际操作中涉及到反爬策略应对、数据解析和结构化存储等多个环节是一个很好的综合练习。无论你是想学习网络爬虫的实战技巧还是需要构建自己的资讯监控工具这个项目都能提供一条清晰的路径。它不仅能帮你掌握requests、BeautifulSoup等基础库的使用更能让你深入理解现代Web应用尤其是内容聚合平台的数据交互逻辑。接下来我会详细拆解从环境搭建到数据落地的每一个步骤并分享我踩过的坑和总结的实用技巧。2. 整体思路与核心挑战解析2.1 目标分析与技术选型我们的目标是获取“今日头条”的热门文章列表并提取每篇文章的关键信息如标题、链接、发布时间、阅读量、评论数等。首先需要明确数据源。今日头条的网页版和移动版数据呈现方式不同反爬机制也有差异。经过测试其移动端页面m.toutiao.com结构相对清晰且对简单爬虫的容忍度稍高更适合作为入门练习的目标。因此我们选择以移动端页面作为抓取入口。技术栈方面核心是HTTP请求库和HTML解析库。requests库简单易用足以应对基础请求而BeautifulSoup4则是解析HTML的利器。对于需要处理JavaScript渲染的动态内容虽然今日头条的部分数据是通过接口异步加载的但热门文章列表在初始HTML中就有基本框架我们可以先从静态解析入手再进阶到接口抓取。此外我们还需要pandas来整理数据以及time、random模块来模拟人类操作规避反爬。2.2 面临的核心挑战与应对策略直接对今日头条进行爬取你会遇到几个典型的“拦路虎”请求头校验服务器会检查请求的User-Agent、Referer等头部信息缺乏这些信息或使用默认值会直接被拒绝。解决方案是伪装成主流浏览器的请求头。IP限制与频率控制短时间内发起大量请求IP可能会被暂时封禁。必须通过设置请求间隔如time.sleep来降低频率模拟真人浏览节奏。数据动态加载文章列表和详情页的很多数据如阅读量是通过JavaScript异步请求接口获取的不在初始HTML中。这就需要我们分析网络请求找到这些数据接口API。参数签名与加密一些关键接口特别是涉及用户行为数据的可能对请求参数进行加密或签名增加了直接调用的难度。对于热门文章列表这类公开数据通常可以找到未加密或规律简单的接口。我们的策略是“由易到难步步为营”先从能够直接获取的静态数据开始构建基础爬虫框架然后通过浏览器开发者工具分析网络请求找到数据接口并模拟调用最后完善细节如异常处理和数据存储。3. 环境准备与基础爬虫搭建3.1 安装必要的Python库确保你的Python环境在3.6以上。打开命令行使用pip安装以下核心库pip install requests beautifulsoup4 pandasrequests: 用于发送HTTP请求获取网页内容。beautifulsoup4: 用于解析HTML或XML文档提取所需数据。pandas: 用于将提取的数据整理成结构化的DataFrame并方便地导出为CSV或Excel文件。如果你使用Anaconda这些库通常已经预装可以直接使用。3.2 分析页面结构与编写首个请求首先我们打开浏览器以Chrome为例访问今日头条移动版首页https://m.toutiao.com。按F12打开开发者工具切换到“Network”网络选项卡然后刷新页面。观察“Elements”元素选项卡可以找到文章列表的HTML结构。通常每篇文章被包裹在一个类似div class”article-item”的容器内。但更可靠的方法是直接分析网络请求。在“Network”选项卡中筛选XHR或Fetch请求你会发现一个名为list或feed的请求其响应是JSON格式包含了文章列表的所有信息。这就是我们要找的API接口。例如你可能会发现一个类似这样的请求URLhttps://m.toutiao.com/list/?tag__all__acwapcount20formatjson_raw...这个接口通过GET请求返回一个JSON对象其中的data字段就是一个文章列表。这种方式比解析HTML更直接、更稳定因为HTML结构可能经常变动而API接口相对稳定。现在让我们用Python来模拟这个请求import requests import json # 1. 定义目标URL和请求头 url ‘https://m.toutiao.com/list/‘ params { ‘tag’: ‘__all__‘, # ‘__all__‘表示全部频道也可换成‘news_hot‘等 ‘ac’: ‘wap‘, ‘count’: ‘20‘, # 每次请求获取的文章数量 ‘format’: ‘json_raw‘, ‘as’: ‘A1C5BE5R1E5F3A‘, # 这些参数需要从实际请求中捕获可能会变 ‘cp’: ‘5C45E1F5E5F3AE1‘, ‘min_behot_time’: ‘0‘, # 控制获取“最新”还是“更多”0表示最新 } headers { ‘User-Agent’: ‘Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1‘, ‘Referer’: ‘https://m.toutiao.com/‘, } # 2. 发送GET请求 try: response requests.get(url, headersheaders, paramsparams, timeout10) response.raise_for_status() # 检查请求是否成功 # 3. 解析JSON响应 data response.json() print(‘请求成功‘) # 打印一下数据结构 print(json.dumps(data, indent2, ensure_asciiFalse)[:500]) # 只打印前500字符看看结构 except requests.exceptions.RequestException as e: print(f‘请求失败: {e}‘) except json.JSONDecodeError as e: print(f‘JSON解析失败: {e}‘)注意上面的as、cp等参数是示例它们很可能具有时效性需要你从自己浏览器捕获的真实请求中复制。直接使用示例参数很可能无法返回数据。这是爬虫实战中第一个关键点所有参数都必须从实际网络请求中获取不能臆造。3.3 解析数据与提取关键信息假设我们成功获取了JSON响应并且数据结构是{‘data’: [文章1信息, 文章2信息, …]}。接下来我们需要从每篇文章的信息中提取所需字段。# 接上面的代码假设data是成功解析的JSON对象 if data and ‘data‘ in data: article_list data[‘data‘] for article in article_list: # 提取字段注意字段名可能因接口不同而异需要根据实际响应调整 title article.get(‘title‘, ‘N/A‘) article_url ‘https://www.toutiao.com/article/‘ article.get(‘group_id‘, ‘‘) # 构造文章详情页链接 source article.get(‘source‘, ‘N/A‘) # 来源 comment_count article.get(‘comment_count‘, 0) read_count article.get(‘read_count‘, 0) # 阅读量有时接口不直接返回 publish_time article.get(‘publish_time‘, 0) # 时间戳 # 处理时间戳 if publish_time: from datetime import datetime publish_time_str datetime.fromtimestamp(publish_time).strftime(‘%Y-%m-%d %H:%M:%S‘) else: publish_time_str ‘N/A‘ print(f“标题: {title}“) print(f“链接: {article_url}“) print(f“来源: {source}“) print(f“评论数: {comment_count}“) print(f“发布时间: {publish_time_str}“) print(“-” * 50)运行这段代码你应该能在控制台看到抓取到的文章信息列表。至此一个最基础的热门文章爬虫就完成了。但这仅仅是开始它的健壮性和完整性还远远不够。4. 构建健壮且可持续的爬虫系统4.1 处理反爬机制与请求优化基础的请求很容易被识别。我们需要让爬虫的行为更像一个真实的用户。使用Session对象requests.Session()可以保持会话自动处理cookies比单次requests.get更接近浏览器行为。丰富请求头除了User-Agent和Referer还可以添加Accept、Accept-Language、Accept-Encoding等。设置随机延迟在连续请求之间插入随机等待时间避免请求过于规律。处理可能的重定向和异常状态码。优化后的请求代码框架如下import requests import time import random from datetime import datetime class ToutiaoSpider: def __init__(self): self.session requests.Session() # 初始化一个更真实的请求头 self.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;q0.9,en;q0.8‘, ‘Referer‘: ‘https://m.toutiao.com/‘, ‘Sec-Fetch-Dest‘: ‘empty‘, ‘Sec-Fetch-Mode‘: ‘cors‘, ‘Sec-Fetch-Site‘: ‘same-origin‘, } self.session.headers.update(self.headers) def fetch_hot_articles(self, max_pages3): 抓取多页热门文章 all_articles [] min_behot_time 0 # 初始时间戳用于翻页 base_params { ‘tag‘: ‘__all__‘, ‘ac‘: ‘wap‘, ‘count‘: ‘20‘, ‘format‘: ‘json_raw‘, } for page in range(1, max_pages 1): print(f‘正在抓取第 {page} 页...‘) params base_params.copy() params[‘min_behot_time‘] min_behot_time # 注意as, cp等签名参数需要动态生成或从首次请求中获取这里用占位符 # 实际项目中需要分析其生成规律或直接复用首次请求的值 params[‘as‘] ‘A1C5BE5R1E5F3A‘ # 需替换 params[‘cp‘] ‘5C45E1F5E5F3AE1‘ # 需替换 try: resp self.session.get(‘https://m.toutiao.com/list/‘, paramsparams, timeout15) resp.raise_for_status() data resp.json() if data.get(‘message‘) ! ‘success‘: print(f‘第{page}页请求返回异常: {data.get(“message“)}‘) break articles data.get(‘data‘, []) if not articles: print(‘没有更多文章了。‘) break for article in articles: processed_article self._parse_article(article) if processed_article: all_articles.append(processed_article) # 更新下一页的 min_behot_time通常取最后一篇文章的 publish_time min_behot_time article.get(‘publish_time‘, min_behot_time) # 随机延迟模拟人工浏览 sleep_time random.uniform(2, 5) print(f‘第{page}页抓取完成等待{sleep_time:.2f}秒...‘) time.sleep(sleep_time) except Exception as e: print(f‘抓取第{page}页时发生错误: {e}‘) break return all_articles def _parse_article(self, article_data): 解析单篇文章数据 try: title article_data.get(‘title‘) if not title: # 没有标题的条目可能是广告或其他跳过 return None group_id article_data.get(‘group_id‘, ‘‘) article_url f‘https://www.toutiao.com/article/{group_id}/‘ if group_id else ‘N/A‘ source article_data.get(‘source‘, ‘N/A‘) comment_count article_data.get(‘comment_count‘, 0) read_count article_data.get(‘read_count‘, 0) or article_data.get(‘go_detail_count‘, 0) # 阅读量字段可能不同 publish_timestamp article_data.get(‘publish_time‘, 0) publish_time datetime.fromtimestamp(publish_timestamp).strftime(‘%Y-%m-%d %H:%M:%S‘) if publish_timestamp else ‘N/A‘ return { ‘标题‘: title, ‘链接‘: article_url, ‘来源‘: source, ‘评论数‘: comment_count, ‘阅读量‘: read_count, ‘发布时间‘: publish_time, ‘原始数据‘: article_data # 保留原始数据以备后续提取其他字段 } except Exception as e: print(f‘解析文章数据时出错: {e}, 数据: {article_data}‘) return None4.2 数据存储与持久化将数据保存在内存中不是长久之计。我们需要将数据存储到文件或数据库中。这里使用pandas将数据保存为CSV文件简单直观。import pandas as pd def save_to_csv(articles_list, filename‘toutiao_hot_articles.csv‘): 将文章列表保存为CSV文件 if not articles_list: print(‘没有数据可保存。‘) return df pd.DataFrame(articles_list) # 选择要保存的列去掉‘原始数据‘这类大字段 columns_to_save [‘标题‘, ‘链接‘, ‘来源‘, ‘评论数‘, ‘阅读量‘, ‘发布时间‘] df_to_save df[columns_to_save] df_to_save.to_csv(filename, indexFalse, encoding‘utf_8_sig‘) # 使用utf_8_sig编码避免中文乱码 print(f‘数据已保存至 {filename}, 共 {len(df_to_save)} 条记录。‘) # 在主流程中使用 if __name__ ‘__main__‘: spider ToutiaoSpider() articles spider.fetch_hot_articles(max_pages2) # 先抓2页试试 save_to_csv(articles)运行后你会在当前目录下得到一个toutiao_hot_articles.csv文件可以用Excel或文本编辑器打开查看。4.3 获取文章详情与完整内容列表页只提供了文章摘要。有时我们需要文章的完整正文。这需要进一步抓取文章详情页。文章详情页同样可能是静态HTML或动态接口。以静态页为例def fetch_article_detail(self, article_url): 抓取文章详情页内容 if not article_url or article_url ‘N/A‘: return None try: # 详情页可能需要不同的请求头 detail_headers self.headers.copy() detail_headers[‘Referer‘] ‘https://m.toutiao.com/‘ resp self.session.get(article_url, headersdetail_headers, timeout10) resp.raise_for_status() # 假设正文在特定的HTML标签内例如 div class“article-content” # 这里需要根据实际页面结构调整 from bs4 import BeautifulSoup soup BeautifulSoup(resp.text, ‘html.parser‘) # 这是一个示例选择器实际需要根据页面分析 content_div soup.find(‘div‘, class_‘article-content‘) if content_div: # 清理内容去除多余标签和空白 full_text content_div.get_text(stripTrue, separator‘\n‘) return full_text[:500] # 返回前500字符作为示例 else: return ‘[未能提取正文可能页面结构已变或需要处理JS]‘ except Exception as e: print(f‘抓取详情页 {article_url} 失败: {e}‘) return None重要提示今日头条详情页的正文很可能也是通过JavaScript动态加载的。直接请求HTML页面可能拿不到正文内容。此时你需要再次使用开发者工具的“Network”选项卡在文章页面查找加载正文数据的XHR请求可能包含content或article等关键字然后直接模拟请求那个API接口来获取结构化的正文内容。这是爬取现代网站更常用、更可靠的方法。5. 常见问题排查与实战技巧5.1 请求返回空数据或错误码问题代码逻辑没错但返回的data字段为空或者message不是success。排查检查URL和参数最关键的一步。务必使用浏览器开发者工具捕获最新的请求URL和所有参数特别是as、cp、_signature等。这些参数通常有有效期直接使用旧参数会失败。检查请求头确保User-Agent是有效的并且Referer等头部信息与正常浏览时一致。检查Cookies有些接口需要携带特定的cookies。使用Session对象可以自动管理cookies。你也可以在初始化时手动添加必要的cookiesself.session.cookies.update({‘name‘: ‘value‘})。使用代理IP如果你的IP被限制可以尝试使用代理。requests库支持通过proxies参数设置代理。5.2 如何应对参数签名_signature问题你发现接口URL中有一个像_signature这样的长字符串参数它看起来是加密生成的且每次请求都不同。分析与解决寻找生成逻辑在浏览器的开发者工具中全局搜索_signature或signature关键词可能会在某个JavaScript文件里找到生成这个参数的函数。使用自动化工具如果生成逻辑过于复杂涉及大量JavaScript加密代码可以考虑使用Selenium、Playwright或Pyppeteer这类浏览器自动化工具直接控制浏览器获取渲染后的页面数据。虽然效率较低但能绕过复杂的JS加密。寻找替代接口有时同一个数据可能有多个接口。可以尝试寻找其他返回相同数据但未加密或加密更简单的接口。例如一些公开的、用于移动端渲染的接口可能要求更低。5.3 数据字段缺失或名称不符问题代码中按照read_count字段提取阅读量但实际JSON中这个字段是view_count或根本不存在。解决永远不要假设字段名。在编写解析代码前先完整打印出一两条文章的原始JSON数据仔细查看其结构。使用.get()方法并提供默认值如.get(‘view_count‘, 0)可以避免因字段缺失导致程序崩溃。5.4 提升爬虫的稳定性和可维护性异常处理对所有网络请求和解析操作使用try…except进行包裹记录错误日志避免程序因单次请求失败而整体崩溃。配置化将URL、请求头、关键参数等提取到配置文件如config.py或字典中方便统一管理和修改。速率限制严格遵守time.sleep()并加入随机性。可以考虑使用更专业的库如time.sleep(random.uniform(1, 3))。增量爬取利用min_behot_time或文章ID作为游标记录上次爬取的最后一条数据下次请求时传入实现增量更新避免重复爬取。尊重robots.txt在爬取前访问https://m.toutiao.com/robots.txt查看网站允许或禁止爬取的目录。虽然这不是法律文件但遵守它是一个好的实践。6. 项目总结与扩展方向通过这个项目我们完成了一个从分析目标、抓取接口、解析数据到存储结果的完整爬虫流程。核心要点在于不要与页面结构硬碰硬优先寻找数据接口API伪装成浏览器是基础处理反爬要有耐心从易到难。这个爬虫还可以从多个方向进行扩展和深化多频道爬取修改tag参数可以抓取科技(news_tech)、娱乐(news_entertainment)等不同频道的热门文章。定时任务结合schedule或APScheduler库让爬虫每隔一段时间如每小时自动运行一次实现热点监控。数据可视化将爬取到的阅读量、评论数随时间变化的数据用matplotlib或pyecharts绘制成图表直观展示热点趋势。情感分析对文章标题或摘要进行简单的文本情感分析判断舆情倾向。构建数据库将CSV存储升级为MySQL、MongoDB或SQLite数据库便于进行更复杂的数据查询和分析。使用更强大的框架对于大规模、复杂的爬取任务可以考虑使用Scrapy框架它能更好地处理并发、去重、管道和中间件。爬虫技术是一把双刃剑。在实践过程中务必注意控制请求频率避免对目标服务器造成过大压力。将爬取的数据用于个人学习、研究或合法的数据分析是没问题的但切勿用于商业牟利、侵犯版权或干扰网站正常运行。在实际操作中我最大的体会是耐心分析网络请求比盲目写解析代码更重要。很多时候一个看似复杂的页面其数据源头可能只是一个简单的JSON接口。花半小时分析能节省后面几小时的调试时间。