1. 从零开始:为什么我们需要“反反爬虫”?

大家好,我是老张,一个在AI和数据领域摸爬滚打了十多年的老码农。最近在捣鼓一个智能阅读助手,想让它自动帮我抓取、总结一些技术文章。想法很美好,但现实很骨感——我刚把目标对准微信公众号和CSDN,就结结实实地撞上了一堵墙:反爬虫机制。

你可能也遇到过类似的情况:用requestsBeautifulSoup写了个简单的爬虫,信心满满地去抓取文章,结果要么返回一堆乱码,要么直接给你一个“403 Forbidden”,告诉你访问被拒绝。这感觉就像你兴冲冲地去图书馆借书,却发现大门紧锁,门口还站着个保安。

其实,这不能全怪网站“小气”。想象一下,如果一个网站毫无防护,任由程序疯狂抓取,服务器可能分分钟被拖垮,影响正常用户的访问。所以,像微信公众号、CSDN这类内容平台,为了保护自身数据、服务器资源以及原创作者的权益,都会部署各种反爬虫策略。它们就像一道道关卡,我们的任务就是找到合法、合理的“通行证”,在不干扰网站正常运行的前提下,拿到我们需要的数据。

那么,这些关卡具体有哪些呢?最常见的就是检查你的“身份证”。当你用浏览器访问网站时,浏览器会告诉服务器:“我是Chrome,版本是XXX”。这个信息就是User-Agent。而直接用requests库发请求,默认的User-Agent会暴露你是个Python程序,一下就被识破了。其次,有些内容(比如微信公众号文章列表)是动态加载的,初次访问的HTML里根本没有文章数据,需要浏览器执行JavaScript后才会通过Ajax请求加载,传统的requests对此无能为力。此外,还有访问频率限制登录验证复杂的数据加密等等。

别担心,这篇文章就是来帮你拆解这些关卡的。我会手把手带你,用Python中最实用的工具——requestsBeautifulSoupSelenium,来攻克微信公众号和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://versionedge://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()方法,并传入正确的标签名和属性(如idclass_)。如何找到这些属性?这就需要用到浏览器的开发者工具(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像之前一样解析了。但微信公众号文章页本身也有其特殊的结构。

微信公众号文章的正文内容,通常包裹在一个classrich_media_content<div>标签内。标题则在classrich_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和特定的请求参数如__bizuin等),这涉及到抓包分析,超出了本文基础篇的范围。我们这里聚焦于解决已获得单篇文章链接后的抓取和反爬问题。

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并验证其可用性,然后在requestsSelenium中轮换使用。

    # 在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文章的基础实战技巧,就先分享到这里。剩下的,就靠你在具体的项目中不断摸索和优化了。如果在实践中遇到具体问题,多看看浏览器的开发者工具,多分析网络请求,你总能找到解决办法。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐