尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python爬虫实战:微信公众号历史文章数据自动化抓取与解析

Python爬虫实战:微信公众号历史文章数据自动化抓取与解析 1. 项目概述与核心需求解析那天晚上我盯着电脑屏幕看着自己运营了半年的公众号后台心里一阵烦躁。我需要整理一份季度运营报告分析一下历史文章的阅读量、点赞数、用户互动趋势。后台的数据导出功能一次只能导出一百条而且格式零散光是手动复制粘贴、合并Excel表格就花掉了我整整一个下午还差点搞错数据。这已经不是第一次了。无论是做数据分析、内容复盘还是想备份自己的心血手动操作都效率低下且容易出错。我相信很多公众号运营者、数据分析师甚至是对某个领域公众号内容感兴趣的研究者都遇到过类似的痛点我们如何高效、准确、自动化地获取一个公众号的全部历史文章数据这就是我动手写这个“公众号历史数据一键抓取脚本”的初衷。它不是一个复杂的商业爬虫框架而是一个聚焦于解决实际问题的个人工具。它的核心目标非常明确给定一个公众号的名称或唯一标识脚本能够自动模拟用户操作遍历其所有历史文章列表并将关键信息结构化地保存下来。这些信息通常包括文章标题、发布时间、原文链接、阅读数、点赞数、在看数、摘要以及封面图链接等。最终数据会被规整地保存到CSV或Excel文件中方便后续进行数据分析、可视化或归档。这个需求背后其实涉及几个关键的技术挑战和设计考量。首先微信公众号平台为了安全和性能其历史文章列表是通过滚动加载下拉刷新的方式动态获取的并没有一个简单的“所有文章”页面。其次数据的获取需要模拟真实的微信客户端请求这其中涉及Cookie管理、请求参数构造和反爬策略应对。最后数据的解析和存储需要稳定可靠能够处理各种可能出现的异常情况比如网络波动、页面结构微调等。我的脚本就是围绕解决这些问题而构建的。2. 技术选型与核心思路拆解在技术选型上我毫不犹豫地选择了Python。原因很简单生态丰富社区活跃对于爬虫和数据处理有大量成熟、高效的库支持。具体到库的选择我主要依赖以下几个核心工具Requests BeautifulSoup4这是经典的“静态”页面抓取组合。Requests库负责发送HTTP请求获取网页的HTML源代码BeautifulSoup4则负责解析HTML像一把精准的手术刀从中提取出我们需要的标题、链接等信息。对于结构相对固定的页面这个组合简单直接效率很高。Selenium这是应对微信公众号动态加载问题的关键。微信公众号的历史列表在浏览器中是通过用户滚动不断加载的这背后是JavaScript发起的Ajax请求。Selenium可以驱动一个真实的浏览器如Chrome模拟人的滚动操作让页面加载出所有内容然后再获取完整的页面源码。它相当于一个“自动化机器人”。Pandas数据处理和保存的利器。将爬取到的杂乱数据列表、字典快速整理成结构化的DataFrame并轻松导出为CSV或Excel文件Pandas是完成这一步的不二之选。整个脚本的核心工作流程我将其设计为以下几个步骤这也是大多数爬虫项目的通用思路身份模拟与初始化脚本需要携带有效的Cookie尤其是登录态Cookie去请求数据。对于公众号历史页有时不需要登录也能查看部分文章但获取完整列表或某些交互数据如点赞数可能需要登录状态。我会先手动登录一次然后将浏览器中的Cookie导出供脚本初始化时使用。同时配置好Selenium的WebDriver无头模式可隐藏浏览器界面。列表页遍历与链接提取这是最核心的一步。使用Selenium打开公众号的历史消息页面通过循环执行“滚动到页面底部 - 等待新内容加载 - 获取当前页面所有文章链接”的操作直到无法再加载出新内容或达到预设的文章数量上限。在这个过程中需要精心设置等待时间WebDriverWait确保内容加载完成再解析避免抓取到空数据。详情页数据抓取获取到所有文章的URL链接后再逐个访问这些详情页。这里我通常切换回Requests库因为详情页是静态的用Requests效率远高于Selenium。针对每一个详情页的HTML用BeautifulSoup定位到文章标题、作者、发布时间、正文内容如果需要、阅读数等元素并将这些信息提取出来存储到一个字典里。数据存储与持久化在内存中积累一定数量的文章数据比如每50篇后就使用Pandas将其追加写入到本地的CSV文件中。这样做的好处是即使脚本中途因网络问题意外中断已经抓取的数据也不会丢失。全部完成后一份完整的数据表格就生成了。异常处理与日志记录在整个过程中网络超时、页面元素找不到、反爬虫限制等都是家常便饭。因此必须用try...except语句包裹每一个可能出错的环节如网络请求、元素解析并记录下出错的文章链接和原因方便后续排查和补抓。同时打印关键步骤的日志让我们能实时了解脚本的运行进度。注意这里存在一个重要的技术细节和潜在风险。微信公众号的阅读数、点赞数等数据在网页源码中可能不是直接以文本形式呈现的而是通过JavaScript动态渲染的或者其HTML元素的class名、id会经常变动。直接解析静态HTML可能抓不到这些数据。这时有几种应对策略一是继续使用Selenium来抓取详情页确保JS执行完毕二是尝试分析页面加载过程中的网络请求看是否能找到直接返回这些数据的API接口但这需要一定的逆向工程能力且接口可能不稳定。在我的实现中我首先尝试从静态HTML获取如果失败会记录为“N/A”并在日志中注明。3. 核心代码模块与实操要点下面我将分模块拆解脚本中的关键代码部分并解释其中的设计意图和实操要点。请注意以下代码为示例实际使用时需要根据目标公众号页面的实际HTML结构进行调整。3.1 环境准备与驱动初始化首先我们需要安装必要的库并准备好浏览器驱动。# 安装核心依赖库 pip install requests beautifulsoup4 selenium pandas # 安装与Chrome浏览器版本对应的ChromeDriver或使用WebDriver Manager自动管理 pip install webdriver-manager在脚本开头进行导入和初始化import time import csv import json from datetime import datetime import pandas as pd import requests from bs4 import BeautifulSoup from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.common.exceptions import TimeoutException, NoSuchElementException # 初始化Chrome浏览器驱动使用无头模式不显示图形界面 options webdriver.ChromeOptions() options.add_argument(--headless) # 无头模式后台运行 options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) # 可选项设置用户代理模拟真实浏览器 options.add_argument(user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36) # 使用webdriver-manager自动下载和管理chromedriver service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionsoptions) # 设置全局请求会话和请求头 session requests.Session() 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 } session.headers.update(headers)3.2 加载Cookie与访问历史页为了保持登录状态如果需要我们可以从文件加载之前保存的Cookie并让Selenium的driver载入。def load_cookies_to_driver(driver, cookie_filewechat_cookies.json): 从JSON文件加载Cookie到Selenium driver try: with open(cookie_file, r, encodingutf-8) as f: cookies json.load(f) # 先访问一下域名才能设置该域下的Cookie driver.get(https://mp.weixin.qq.com) for cookie in cookies: # 为Cookie添加必要的字段并删除Selenium不支持的字段如‘sameSite’ if sameSite in cookie: del cookie[sameSite] driver.add_cookie(cookie) print(Cookie加载成功。) except FileNotFoundError: print(fCookie文件 {cookie_file} 未找到将以未登录状态运行。) except Exception as e: print(f加载Cookie时发生错误: {e}) # 使用函数加载Cookie load_cookies_to_driver(driver) # 目标公众号的历史消息页面URL模板需要替换biz和uin等参数 # 注意这个URL需要从公众号主页的“查看历史消息”入口手动获取一次分析其结构。 # 示例https://mp.weixin.qq.com/mp/profile_ext?actionhome__bizYOUR_BIZscene124#wechat_redirect history_url YOUR_SPECIFIC_HISTORY_PAGE_URL driver.get(history_url) time.sleep(3) # 初始加载等待实操心得1如何获取历史页URL和Cookie这是项目启动最关键也最“手工”的一步。你需要用Chrome浏览器手动打开目标公众号的任意一篇文章点击公众号名称进入主页再点击“查看历史消息”。然后复制地址栏的URL它就是你的history_url。这个URL里包含了标识该公众号的__biz参数。关于Cookie你需要在登录状态下在公众号后台或网页版微信使用浏览器开发者工具F12的“网络(Network)”选项卡找到一个对mp.weixin.qq.com域的请求将其请求头中的Cookie值复制出来并整理成JSON格式保存。更稳妥的方法是使用driver.get_cookies()在登录后一次性导出所有Cookie。3.3 滚动爬取文章链接列表这是使用Selenium模拟滚动的核心循环。def scroll_to_bottom_and_collect_links(driver, max_articles500): 滚动公众号历史页面到底部收集所有文章链接。 :param driver: Selenium WebDriver :param max_articles: 最大收集文章数防止无限滚动 :return: 文章链接列表 article_links [] last_height driver.execute_script(return document.body.scrollHeight) scroll_attempts 0 max_attempts 50 # 最大滚动尝试次数防止死循环 print(开始滚动加载文章列表...) while len(article_links) max_articles and scroll_attempts max_attempts: # 1. 模拟滚动到页面底部 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) # 2. 等待新内容加载关键 time.sleep(2) # 基础等待 try: # 更智能的等待等待页面高度发生变化或者出现特定的加载元素 WebDriverWait(driver, 5).until( lambda d: d.execute_script(return document.body.scrollHeight) last_height ) except TimeoutException: # 如果高度不再变化可能已加载完毕 print(页面高度未变化可能已加载到底部或加载超时。) # 可以尝试检查是否有“没有更多了”的提示元素 break # 3. 获取当前页面的所有文章链接 soup BeautifulSoup(driver.page_source, html.parser) # 注意这里的CSS选择器需要根据实际页面结构调整这是最常见的模式。 link_elements soup.select(div[class*msg] a[href*http]) # 示例选择器 for elem in link_elements: link elem.get(href) # 过滤掉非文章链接如广告、菜单链接 if link and mp.weixin.qq.com/s in link and link not in article_links: article_links.append(link) print(f已发现链接 {len(article_links)}: {link[:80]}...) # 4. 更新上一次的滚动高度和尝试计数 new_height driver.execute_script(return document.body.scrollHeight) if new_height last_height: scroll_attempts 1 print(f滚动后高度未变尝试次数 {scroll_attempts}/{max_attempts}) else: scroll_attempts 0 # 重置计数器 last_height new_height # 5. 简单进度提示 if len(article_links) % 20 0: print(f当前已收集 {len(article_links)} 篇文章链接。) print(f滚动结束共收集到 {len(article_links)} 个文章链接。) # 去重并返回 return list(set(article_links)) # 执行滚动收集 all_links scroll_to_bottom_and_collect_links(driver, max_articles1000)3.4 解析单篇文章详情获取链接后我们使用Requests库来高效抓取详情页。def parse_article_detail(article_url, session): 解析单篇文章的详细信息。 :param article_url: 文章链接 :param session: requests Session对象 :return: 包含文章信息的字典解析失败返回None article_info { title: N/A, publish_time: N/A, author: N/A, read_num: N/A, like_num: N/A, # 在看数 content_summary: N/A, # 摘要或前N字 cover_img: N/A, article_url: article_url, crawl_time: datetime.now().strftime(%Y-%m-%d %H:%M:%S) } try: resp session.get(article_url, timeout10) resp.raise_for_status() # 检查HTTP错误 resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) # 1. 提取标题 - 查找id为“activity-name”或class包含“rich_media_title”的div title_elem soup.find(idactivity-name) or soup.find(class_rich_media_title) if title_elem: article_info[title] title_elem.get_text(stripTrue) # 2. 提取发布时间 - 查找id为“publish_time”的span time_elem soup.find(idpublish_time) if time_elem: article_info[publish_time] time_elem.get_text(stripTrue) # 3. 提取作者/公众号名 - 查找id为“js_name”的strong或a author_elem soup.find(idjs_name) or soup.select_one(#meta_content span.rich_media_meta_text) if author_elem: article_info[author] author_elem.get_text(stripTrue) # 4. 提取阅读数和点赞数在看数 - 这部分HTML结构可能变化需要仔细查找 # 通常在一个class包含“read_num”或“like_num”的span里 read_elem soup.find(class_read_num) or soup.select_one(span#readNum3) if read_elem: article_info[read_num] read_elem.get_text(stripTrue) like_elem soup.find(class_like_num) or soup.select_one(span#likeNum3) if like_elem: article_info[like_num] like_elem.get_text(stripTrue) # 5. 提取封面图 - 查找og:image meta标签 cover_elem soup.find(meta, propertyog:image) if cover_elem and cover_elem.get(content): article_info[cover_img] cover_elem[content] # 6. 提取内容摘要可选 - 获取正文前200个字符 content_elem soup.find(idjs_content) or soup.find(class_rich_media_content) if content_elem: full_text content_elem.get_text(stripTrue, separator ) article_info[content_summary] full_text[:200] ... if len(full_text) 200 else full_text print(f成功解析: {article_info[title][:50]}...) return article_info except requests.exceptions.RequestException as e: print(f请求文章 {article_url} 失败: {e}) return None except Exception as e: print(f解析文章 {article_url} 时发生未知错误: {e}) return None3.5 主循环与数据存储最后我们将所有模块串联起来并加入批处理和错误重试机制。def main(): # 初始化驱动和会话 driver init_driver() session init_requests_session() try: # 1. 加载Cookie并访问历史页 load_cookies_to_driver(driver) driver.get(HISTORY_URL) time.sleep(5) # 2. 滚动爬取所有文章链接 print(开始收集文章链接...) article_links scroll_to_bottom_and_collect_links(driver, max_articles2000) print(f链接收集完成共 {len(article_links)} 条。) # 3. 准备存储文件 output_file wechat_articles.csv fieldnames [title, publish_time, author, read_num, like_num, content_summary, cover_img, article_url, crawl_time] # 如果文件不存在先写入表头 try: pd.read_csv(output_file) print(f文件 {output_file} 已存在将追加数据。) except FileNotFoundError: pd.DataFrame(columnsfieldnames).to_csv(output_file, indexFalse, encodingutf-8-sig) print(f创建新文件 {output_file}。) # 4. 遍历链接解析并保存数据 total_links len(article_links) for idx, link in enumerate(article_links, 1): print(f\n正在处理第 {idx}/{total_links} 篇文章...) article_data parse_article_detail(link, session) if article_data: # 使用Pandas追加单行数据到CSV效率尚可对于几千条数据没问题 df_row pd.DataFrame([article_data]) df_row.to_csv(output_file, modea, headerFalse, indexFalse, encodingutf-8-sig) print(f 已保存。) else: print(f 解析失败已跳过。) # 礼貌性延迟避免请求过快 time.sleep(1) print(f\n所有文章处理完成数据已保存至 {output_file}) finally: # 确保浏览器驱动被关闭 driver.quit() print(浏览器驱动已关闭。) if __name__ __main__: main()4. 常见问题、避坑指南与优化建议在实际运行这个脚本的过程中我踩过不少坑也总结出一些让脚本更健壮、更高效的技巧。4.1 常见问题与排查技巧抓不到阅读数/点赞数原因这些数据很可能是通过JavaScript动态加载的在静态HTML源码中不存在或者其HTML元素的class/id名已经更新。排查在浏览器中打开文章页右键“检查”在“元素(Elements)”面板中搜索“阅读”或“read”看对应的数字是否在HTML中。如果不在查看“网络(Network)”面板过滤XHR或Fetch请求看是否有包含这些数据的API请求被调用。解决如果找到API可以尝试直接模拟该请求需要分析参数。否则退而求其次使用Selenium来抓取详情页确保JS执行完毕。代码修改在parse_article_detail函数中将requests.get替换为用driver.get加载页面然后用driver.page_source创建BeautifulSoup对象。Selenium滚动无法触发加载或加载不全原因等待时间不足或滚动到底部的判断逻辑有问题。有些页面可能需要在某个特定元素上滚动。排查在脚本运行时暂时关闭无头模式options.add_argument(--headless)观察浏览器实际滚动和加载过程。解决增加time.sleep的等待时间或使用更精确的WebDriverWait等待某个表示新内容已加载的元素出现例如等待新的.msg元素出现。也可以尝试用JavaScript直接滚动到某个特定元素driver.execute_script(arguments[0].scrollIntoView();, element)。请求被限制或封禁原因请求频率过高触发了公众号平台的反爬机制。现象返回403错误或页面提示“操作过于频繁请稍后再试”。解决降低频率在每次请求无论是列表滚动还是详情页抓取后增加随机延迟例如time.sleep(random.uniform(2, 5))。使用代理IP如果请求量非常大可以考虑使用代理IP池来分散请求。更换User-Agent定期更换headers中的User-Agent字符串。尊重robots.txt检查目标网站的robots.txt文件避免爬取被明确禁止的目录。Cookie失效原因微信登录态Cookie有有效期。解决定期手动更新Cookie文件。可以考虑实现一个半自动化的登录模块但复杂度会大大增加且可能违反平台使用条款。对于只需公开文章数据的需求可以尝试寻找无需登录即可获取的接口或方法。4.2 避坑指南与实操心得选择器要稳健BeautifulSoup和Selenium的CSS选择器不要写得太“死”。避免使用绝对路径或过于具体的class名如div.class1.class2因为它们很容易随前端改版而变化。多用包含选择器*和属性选择器来匹配关键特征例如soup.select(div[class*msg])。异常处理要周全网络爬虫运行在复杂的环境中任何一步都可能出错。务必用try...except包裹每一个网络请求、元素查找、数据解析的步骤并记录详细的错误日志包括出错的URL和异常信息这样在脚本中断后你可以知道从哪里恢复或者哪些文章需要重新抓取。数据存储要增量不要等所有数据都在内存中处理好了一次性写入文件。像我上面那样每解析完一篇文章或每N篇文章就追加写入一次文件。这样即使程序崩溃或断电你的工作成果也不会全部丢失。设置合理的上限在scroll_to_bottom_and_collect_links函数中设置max_articles和max_attempts参数非常重要。这能防止脚本因某些原因如页面加载逻辑改变导致无限滚动陷入死循环。验证与清洗数据抓取下来的数据可能包含多余的空格、换行符或者某些字段为None。在存入CSV前进行简单的清洗和格式化比如用.strip()去除首尾空格将None替换为N/A或空字符串。4.3 进阶优化建议如果你的需求更复杂可以考虑以下优化方向异步抓取使用aiohttp和asyncio库来异步并发抓取文章详情页这可以极大提高抓取速度尤其是在有数百上千篇文章时。使用Scrapy框架对于大型、复杂的爬虫项目Scrapy框架提供了更强大的调度、去重、管道处理等功能能让代码结构更清晰也更易于维护和扩展。数据入库将数据存入SQLite、MySQL或MongoDB等数据库便于进行更复杂的查询和分析。定时任务结合schedule或APScheduler库将脚本设置为定时任务定期抓取公众号的新文章实现数据监控。封装成工具/API使用Flask或FastAPI将脚本封装成一个简单的Web服务提供“输入公众号信息返回数据文件”的API接口。最后我必须强调合规与道德。在运行任何爬虫脚本前请务必仔细阅读目标网站的robots.txt文件和服务条款。控制请求速率避免对目标服务器造成过大压力。尊重版权和个人隐私抓取的数据请用于合法、正当的用途。对于明确禁止爬虫的网站或敏感数据请勿尝试抓取。这个脚本是我在实际需求中打磨出来的工具它不一定完美但足够解决从公众号手动收集数据的繁琐问题。希望这份详细的拆解和代码能帮助你理解爬虫的核心思路并成功搭建起属于自己的数据抓取工具。如果在实践中遇到新的问题那正是学习和优化的好机会。
返回列表