突破反爬壁垒:Python爬取微信公众号与CSDN文章并存储为JSON的实战技巧
1. 从零开始:为什么我们需要“反反爬虫”?
大家好,我是老张,一个在AI和数据领域摸爬滚打了十多年的老码农。最近在捣鼓一个智能阅读助手,想让它自动帮我抓取、总结一些技术文章。想法很美好,但现实很骨感——我刚把目标对准微信公众号和CSDN,就结结实实地撞上了一堵墙:反爬虫机制。
你可能也遇到过类似的情况:用requests和BeautifulSoup写了个简单的爬虫,信心满满地去抓取文章,结果要么返回一堆乱码,要么直接给你一个“403 Forbidden”,告诉你访问被拒绝。这感觉就像你兴冲冲地去图书馆借书,却发现大门紧锁,门口还站着个保安。
其实,这不能全怪网站“小气”。想象一下,如果一个网站毫无防护,任由程序疯狂抓取,服务器可能分分钟被拖垮,影响正常用户的访问。所以,像微信公众号、CSDN这类内容平台,为了保护自身数据、服务器资源以及原创作者的权益,都会部署各种反爬虫策略。它们就像一道道关卡,我们的任务就是找到合法、合理的“通行证”,在不干扰网站正常运行的前提下,拿到我们需要的数据。
那么,这些关卡具体有哪些呢?最常见的就是检查你的“身份证”。当你用浏览器访问网站时,浏览器会告诉服务器:“我是Chrome,版本是XXX”。这个信息就是User-Agent。而直接用requests库发请求,默认的User-Agent会暴露你是个Python程序,一下就被识破了。其次,有些内容(比如微信公众号文章列表)是动态加载的,初次访问的HTML里根本没有文章数据,需要浏览器执行JavaScript后才会通过Ajax请求加载,传统的requests对此无能为力。此外,还有访问频率限制、登录验证、复杂的数据加密等等。
别担心,这篇文章就是来帮你拆解这些关卡的。我会手把手带你,用Python中最实用的工具——requests、BeautifulSoup和Selenium,来攻克微信公众号和CSDN这两个典型目标,并把爬取到的文章标题、正文、链接等信息,干净利落地存储为结构清晰的JSON文件。整个过程我会尽量讲得通俗,就像朋友间分享经验一样,把我踩过的坑和总结的技巧都告诉你。
2. 第一道防线:User-Agent伪装与基础请求
当我们用Python的requests库去访问一个网页时,服务器第一眼就会看我们的“来头”,也就是HTTP请求头。一个“裸奔”的requests请求,其User-Agent通常是类似python-requests/2.28.1这样的字符串,这在服务器看来简直就是在脑门上写着“我是爬虫”四个大字。
所以,我们的第一步就是给自己打造一个合法的“马甲”,伪装成一个真实的浏览器。这个操作非常简单,但效果立竿见影。
2.1 获取并设置你的浏览器User-Agent
首先,你得知道一个真实的浏览器User-Agent长什么样。打开你电脑上的Chrome或Edge浏览器,在地址栏输入 chrome://version 或 edge://version,回车。在打开的页面里,找到“用户代理”这一行,后面那一长串复杂的字符串就是你的浏览器的身份证。它包含了操作系统、浏览器内核、版本等详细信息。
有了这个字符串,我们就可以在Python请求中把它加进去。这里有个小技巧,我习惯准备几个不同的User-Agent轮换使用,避免长期使用同一个导致被识别。下面是一个基础的爬虫函数,它已经穿上了“马甲”:
import requests
from bs4 import BeautifulSoup
import json
def simple_crawler_with_ua(url):
"""
一个带有User-Agent伪装的基础爬虫函数。
"""
# 准备几个常见的浏览器User-Agent,可以轮流使用
user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15'
]
# 随机选择一个,或者固定使用第一个
headers = {
'User-Agent': user_agents[0],
# 可以添加其他必要的头部,例如Referer,但初期可以不加
# 'Referer': 'https://www.google.com/'
}
try:
response = requests.get(url, headers=headers, timeout=10)
# 检查请求是否成功
response.raise_for_status()
# 处理编码问题,防止中文乱码
if response.encoding == 'ISO-8859-1':
response.encoding = response.apparent_encoding or 'utf-8'
return response.text
except requests.exceptions.RequestException as e:
print(f"请求发生错误: {e}")
return None
# 测试一下
test_url = "https://blog.csdn.net"
html_content = simple_crawler_with_ua(test_url)
if html_content:
print("页面标题获取成功!")
# 这里可以用BeautifulSoup解析标题,我们先不展开
这个函数已经能绕过很多基础的反爬了。但仅仅这样,对于微信公众号和CSDN来说,可能还不够。因为CSDN对于未登录用户或高频访问,可能会弹出验证码或者直接限制。所以,我们还需要更精细的策略。
2.2 解析HTML结构:BeautifulSoup的基本功
拿到HTML文档后,我们就要从中“淘金”了。BeautifulSoup 就像一把瑞士军刀,能帮我们轻松地解析和提取HTML中的标签和数据。它的核心是理解HTML的树状结构。
对于CSDN博客文章,其结构相对标准。标题通常在<title>标签里,或者页面内某个特定的<h1>标签中。正文则包裹在<article>标签或者某个具有特定class的<div>里。我们需要做的就是找到这些标签。
def parse_csdn_article(html_content):
"""
解析CSDN文章页面的标题和正文。
注意:CSDN页面结构可能变化,需要根据实际情况调整选择器。
"""
if not html_content:
return None
soup = BeautifulSoup(html_content, 'html.parser')
article_data = {}
# 1. 提取标题 - 尝试多种选择器以提高容错率
title_elem = soup.find('h1', class_='title-article') # CSDN常见标题类名
if not title_elem:
title_elem = soup.find('title') # 备用方案
article_data['title'] = title_elem.get_text().strip() if title_elem else '未找到标题'
# 2. 提取正文 - 寻找文章主体内容区域
# CSDN的正文通常在一个id或class很明显的div里,例如 id='article_content', class='blog-content-box'
content_div = soup.find('div', id='article_content')
if not content_div:
content_div = soup.find('div', class_='blog-content-box')
if content_div:
# 获取所有段落文本,并过滤掉空的或无关的段落
paragraphs = []
for p in content_div.find_all('p'):
text = p.get_text().strip()
if text and len(text) > 5: # 简单过滤过短的文本(可能是代码行号等)
paragraphs.append(text)
article_data['content'] = '\n\n'.join(paragraphs)
else:
article_data['content'] = '未找到正文内容'
# 3. 提取文章发布时间、作者等信息(如果存在)
time_elem = soup.find('span', class_='time')
article_data['publish_time'] = time_elem.get_text().strip() if time_elem else None
author_elem = soup.find('a', class_='follow-nickName')
article_data['author'] = author_elem.get_text().strip() if author_elem else None
return article_data
这段代码展示了如何针对一个特定网站(CSDN)进行解析。关键在于使用soup.find()或soup.find_all()方法,并传入正确的标签名和属性(如id、class_)。如何找到这些属性?这就需要用到浏览器的开发者工具(F12),去“检查”元素,观察其HTML结构。这是一个熟能生巧的过程。
3. 攻克动态加载:Selenium模拟真实浏览器
解决了基础伪装和静态解析,我们遇到了爬虫路上的第二个大BOSS:动态加载内容。微信公众号的文章列表页(mp.weixin.qq.com)是典型的例子。你打开页面后,文章列表并不是直接写在HTML里的,而是浏览器执行了一段JavaScript代码,向后台发送了一个Ajax请求,才把数据加载并渲染出来。用requests只能拿到最初那个“空壳”HTML,看不到文章数据。
这时候,Selenium就该登场了。它的核心思想是:我不去模拟HTTP请求了,我直接启动一个真实的浏览器(如Chrome),让这个浏览器去加载页面、执行JS、渲染最终结果,然后我再从浏览器里把完整的页面源代码“抄”下来。
3.1 Selenium环境搭建与基础使用
首先,你需要安装Selenium库和对应的浏览器驱动(如ChromeDriver)。
# 安装selenium库
pip install selenium
接下来是驱动。最省事的方法是使用webdriver-manager,它能自动下载和管理匹配你浏览器版本的驱动。
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import time
def init_selenium_driver(headless=False):
"""
初始化并返回一个Selenium WebDriver实例。
headless参数:True表示无头模式(不显示浏览器界面),False表示显示界面。
"""
options = webdriver.ChromeOptions()
# 添加一些常用选项,避免被检测为自动化工具
options.add_argument('--disable-blink-features=AutomationControlled')
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
# 添加User-Agent,同样可以伪装
options.add_argument('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')
if headless:
options.add_argument('--headless') # 无头模式,后台运行
options.add_argument('--disable-gpu')
options.add_argument('--window-size=1920,1080')
# 使用webdriver-manager自动管理驱动
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=options)
# 执行一个JS脚本,进一步隐藏自动化特征
driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
return driver
初始化好driver后,使用它就和使用真实浏览器几乎一样:
def fetch_dynamic_page(url):
"""使用Selenium获取动态加载后的完整页面源代码。"""
driver = init_selenium_driver(headless=True) # 建议测试时用False看过程,正式运行时用True
try:
driver.get(url)
# 等待页面加载完成,对于动态内容,可能需要显式等待特定元素出现
time.sleep(3) # 简单粗暴的等待,生产环境建议用WebDriverWait
# 获取渲染后的页面源代码
page_source = driver.page_source
return page_source
finally:
driver.quit() # 务必退出,释放资源
# 尝试抓取一个动态页面
dynamic_url = "https://mp.weixin.qq.com/" # 公众号主页
html = fetch_dynamic_page(dynamic_url)
print(f"获取到的页面长度:{len(html)}")
3.2 针对微信公众号文章的专项爬取
拿到了包含动态内容的HTML后,我们就可以用BeautifulSoup像之前一样解析了。但微信公众号文章页本身也有其特殊的结构。
微信公众号文章的正文内容,通常包裹在一个class为rich_media_content的<div>标签内。标题则在class为rich_media_title的<h1>或<h2>标签里。这个结构相对稳定,是我们抓取的突破口。
def parse_weixin_article(html_content):
"""解析微信公众号单篇文章页面。"""
soup = BeautifulSoup(html_content, 'html.parser')
article_data = {}
# 提取标题
title_elem = soup.find('h1', class_='rich_media_title') or soup.find('h2', class_='rich_media_title')
if title_elem:
# 微信公众号标题可能包含大量空白字符
article_data['title'] = title_elem.get_text().strip().replace('\n', '').replace(' ', '')
else:
# 备用方案:查找title标签
article_data['title'] = soup.title.string.strip() if soup.title else '未知标题'
# 提取正文 - 核心区域
content_div = soup.find('div', class_='rich_media_content')
if content_div:
# 清理正文中的广告、空白div等无关内容(可选)
for elem in content_div.find_all(['script', 'style', 'iframe', 'ins']):
elem.decompose()
# 获取所有文本段落
paragraphs = []
for p in content_div.find_all('p'):
text = p.get_text().strip()
if text:
paragraphs.append(text)
article_data['content'] = '\n\n'.join(paragraphs)
# 也可以直接获取整个div的文本(可能包含更多格式信息)
# article_data['content'] = content_div.get_text(separator='\n', strip=True)
else:
article_data['content'] = '未找到正文内容'
# 可能是分享卡片形式,可以尝试其他选择器,如 js_content
alt_div = soup.find('div', id='js_content')
if alt_div:
article_data['content'] = alt_div.get_text(separator='\n', strip=True)
# 提取公众号名称和发布日期
account_elem = soup.find('strong', class_='profile_nickname')
article_data['account'] = account_elem.get_text().strip() if account_elem else None
date_elem = soup.find('em', id='publish_time')
article_data['publish_date'] = date_elem.get_text().strip() if date_elem else None
return article_data
重要提示:直接爬取公众号文章链接是可行的,但批量爬取公众号的历史文章列表,则需要更复杂的技术,例如分析其后台API接口(通常需要登录态cookie和特定的请求参数如__biz、uin等),这涉及到抓包分析,超出了本文基础篇的范围。我们这里聚焦于解决已获得单篇文章链接后的抓取和反爬问题。
4. 数据落地:结构化存储与JSON处理
爬取数据不是终点,有效地存储下来才是。JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,也易于机器解析和生成。Python内置的json模块让这一切变得非常简单。
我们的目标是将爬取到的每篇文章信息(标题、正文、链接、发布时间等)存储为一个结构化的JSON对象,并保存到文件中。
4.1 设计数据结构与JSON序列化
首先,我们设计一个字典来存放一篇文章的所有信息。然后,使用json.dump()方法将其写入文件。ensure_ascii=False参数确保中文字符能正常显示,indent=4参数让生成的JSON文件有漂亮的缩进,便于阅读。
import json
from datetime import datetime
def save_article_to_json(article_data, url, filename=None):
"""
将文章数据保存到JSON文件。
article_data: 包含标题、内容等信息的字典。
url: 文章原始链接。
filename: 指定的文件名,如果为None则自动生成。
"""
# 完善文章数据对象
output_data = {
"source_url": url,
"crawl_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
**article_data # 将article_data的所有键值对合并进来
}
# 生成文件名
if filename is None:
# 用标题前20个字符和当前时间戳生成文件名,避免非法字符
safe_title = "".join([c for c in article_data.get('title', 'article')[:20] if c.isalnum()]).strip()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{safe_title}_{timestamp}.json"
elif not filename.endswith('.json'):
filename += '.json'
# 写入文件
try:
with open(filename, 'w', encoding='utf-8') as f:
json.dump(output_data, f, ensure_ascii=False, indent=4)
print(f"文章已成功保存至: {filename}")
return filename
except IOError as e:
print(f"写入文件时出错: {e}")
return None
4.2 整合爬取与存储流程
现在,我们把前面所有的步骤串联起来,形成一个完整的、健壮的爬取流程。这个流程会先尝试用requests(快),如果失败或需要处理动态内容,则降级到使用Selenium(慢但强)。
def robust_article_crawler(url, use_selenium=False):
"""
健壮的文章爬取器。
url: 文章链接。
use_selenium: 是否强制使用Selenium。如果为False,会先尝试requests。
"""
html_content = None
# 阶段1:尝试使用requests(快速)
if not use_selenium:
print("尝试使用requests获取...")
html_content = simple_crawler_with_ua(url)
# 简单判断:如果获取到的内容很短,或者包含反爬提示(如“验证”字样),可能失败了
if html_content and (len(html_content) < 5000 or "验证" in html_content):
print("requests获取可能失败,尝试切换Selenium...")
html_content = None
# 阶段2:如果requests失败或指定使用Selenium,则启用Selenium
if html_content is None:
print("使用Selenium获取...")
html_content = fetch_dynamic_page(url) # 这里复用之前的Selenium函数
if not html_content:
print("无法获取页面内容。")
return None
# 阶段3:根据URL域名,选择不同的解析器
if 'weixin.qq.com' in url:
print("解析为微信公众号文章...")
article_data = parse_weixin_article(html_content)
elif 'csdn.net' in url:
print("解析为CSDN文章...")
article_data = parse_csdn_article(html_content)
else:
print("通用解析...")
# 可以尝试一个通用的解析函数,这里简化为只提取标题
soup = BeautifulSoup(html_content, 'html.parser')
article_data = {
'title': soup.title.string.strip() if soup.title else '通用页面',
'content': '请自定义通用解析逻辑'
}
# 阶段4:保存数据
if article_data:
saved_file = save_article_to_json(article_data, url)
return saved_file
else:
return None
# 实战演示
if __name__ == "__main__":
# 你可以替换成任何你想爬取的微信公众号或CSDN文章链接
test_weixin_url = "https://mp.weixin.qq.com/s/某个示例文章ID"
test_csdn_url = "https://blog.csdn.net/某作者/article/details/某文章ID"
print("开始爬取微信公众号文章示例...")
result = robust_article_crawler(test_weixin_url, use_selenium=True) # 公众号建议直接用Selenium
if result:
print(f"成功!文件保存在: {result}")
print("\n开始爬取CSDN文章示例...")
result2 = robust_article_crawler(test_csdn_url, use_selenium=False) # CSDN可先试requests
if result2:
print(f"成功!文件保存在: {result2}")
这个robust_article_crawler函数是一个简单的框架,它体现了在实际爬虫开发中非常重要的“降级”和“适配”思想。根据目标网站的特点,灵活选择最合适的工具和方法。
5. 高级策略与伦理边界
掌握了基本技巧后,我们还需要聊点更深入的东西:如何让爬虫更稳定、更持久,以及最重要的——如何合法合规地操作。
5.1 应对更复杂的反爬机制
-
IP限制与代理池:如果你短时间内发出大量请求,服务器很可能会封禁你的IP地址。解决方案是使用代理IP池。你可以购买付费代理服务,或者自己搭建一个代理池,从免费代理网站抓取IP并验证其可用性,然后在
requests或Selenium中轮换使用。# 在requests中使用代理示例 proxies = { 'http': 'http://your-proxy-ip:port', 'https': 'http://your-proxy-ip:port', } response = requests.get(url, headers=headers, proxies=proxies, timeout=10) -
请求频率控制:即使用了代理,过快的请求频率(例如每秒几十次)依然会引起怀疑。务必在请求间添加随机延时,模拟人类浏览的间隔。
import time import random def polite_delay(min_seconds=2, max_seconds=5): """礼貌的随机延迟""" time.sleep(random.uniform(min_seconds, max_seconds)) # 在爬取循环中调用 for url in url_list: polite_delay() # ... 发起爬取请求 -
处理Cookie和Session:有些内容需要登录后才能查看(如CSDN的某些文章)。这时你需要维护一个会话(Session),并在首次登录后保存关键的Cookie,在后续请求中携带。
session = requests.Session() # 先模拟登录(需要分析登录接口) login_data = {'username': '...', 'password': '...'} session.post(login_url, data=login_data) # 然后用这个session去访问需要登录的页面 response = session.get(protected_url) -
绕过自动化工具检测:像
Selenium这样的自动化工具,其启动的浏览器环境与真人使用的环境有一些细微差别(如navigator.webdriver属性)。高级的反爬系统会检测这些差别。我们前面在init_selenium_driver函数中已经添加了一些选项(如excludeSwitches和执行隐藏webdriver的JS)来应对,但这是一场持续的攻防战。
5.2 遵守Robots协议与法律法规
这是所有爬虫开发者必须坚守的底线。
- Robots协议:访问网站的
/robots.txt文件(例如https://www.csdn.net/robots.txt),这个文件指明了网站允许和禁止爬虫访问的路径。尊重robots.txt是网络爬虫的基本礼仪。虽然技术上可以无视,但无视它可能带来法律风险。 - 数据使用目的:爬取的数据应仅限于个人学习、研究或公益用途。严禁用于商业牟利、恶意竞争、侵犯他人隐私或对目标网站进行攻击(如DDoS)。
- 避免对网站造成负担:控制爬取速度和并发数,不要为了追求效率而把别人的服务器搞垮。你的爬虫行为应该尽可能“低调”,不影响网站的正常服务。
- 关注网站条款:许多网站的用户协议中明确禁止未经授权的数据抓取。在开始大规模爬取前,最好能仔细阅读相关条款。
爬虫技术是一把双刃剑。我这些年用爬虫做了很多有趣的事情,比如构建个人知识库、做市场趋势分析、为AI模型收集训练语料。但每一次,我都会反复问自己:我这样做合适吗?会对数据来源方造成困扰吗?只有时刻保持这份敬畏之心,我们才能既享受到技术带来的便利,又能行走在阳光之下。
技术细节可以不断迭代,但这条伦理和法律的红线,永远不能跨越。希望你在实践这些技巧时,也能牢记这一点。好了,关于Python爬取微信公众号和CSDN文章的基础实战技巧,就先分享到这里。剩下的,就靠你在具体的项目中不断摸索和优化了。如果在实践中遇到具体问题,多看看浏览器的开发者工具,多分析网络请求,你总能找到解决办法。
更多推荐



所有评论(0)