Python 爬虫入门教程:requests + BeautifulSoup 实战抓取网页数据(2026)

📝 1096 字 · ☕ 4 分钟阅读

前言

在互联网时代,数据无处不在。无论是做市场调研、竞品分析,还是建立个人数据集,网页爬虫都是一项必备技能。2026 年的今天,虽然各大网站的反爬机制越来越完善,但 Python 爬虫的基础依然没有变——requestsBeautifulSoup 这对黄金组合仍然是入门的最佳选择。

本文将带你从零开始,系统地学习如何使用 Python 的 requests 库发送 HTTP 请求,配合 BeautifulSoup 解析 HTML 文档,最终实现一个完整的网页数据抓取项目。全文包含大量可直接运行的代码示例,适合有一定 Python 基础但从未接触过爬虫的读者。

一、环境准备与安装

在开始之前,请确保你的 Python 版本在 3.8 以上。推荐使用虚拟环境来管理项目依赖:

python -m venv scraper-env
source scraper-env/bin/activate  # Linux/Mac
# 或 scraper-env\Scripts\activate  # Windows

pip install requests beautifulsoup4 lxml

这里我们额外安装了 lxml 解析器,它比 Python 自带的 html.parser 速度更快、容错性更好。后续所有示例均基于这三个库。

二、requests 库基础

2.1 GET 请求——获取网页内容

requests 库最基础的功能就是发送 HTTP 请求。GET 是最常用的方法,用于从服务器获取资源。下面是一个最简单的例子:

import requests

url = 'https://httpbin.org/get'
response = requests.get(url)

print(response.status_code)   # 200 表示成功
print(response.text)           # 返回的 HTML 或 JSON 文本

response.status_code 返回 HTTP 状态码,200 代表请求成功,404 代表页面不存在,503 表示服务器暂时不可用。建议总是先检查状态码再处理内容。

2.2 POST 请求——提交数据

当需要向服务器提交数据时(如表单登录、搜索查询),使用 POST 方法:

data = {'username': 'test', 'password': '123456'}
response = requests.post('https://httpbin.org/post', data=data)
print(response.json())  # 直接解析 JSON 响应

如果提交的是 JSON 格式,则使用 json 参数:

import json

data = {'query': 'Python 爬虫', 'page': 1}
response = requests.post('https://httpbin.org/post', json=data)

2.3 请求头(Headers)——伪装浏览器

很多网站会检查请求头中的 User-Agent 字段,如果发现是 Python 的默认标识,可能会直接拒绝响应。因此,我们需要设置合理的请求头:

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    'Referer': 'https://www.google.com/',
}

response = requests.get('https://example.com', headers=headers)

设置合适的 User-Agent 是最基本的反反爬措施。Referer 告诉服务器请求从哪里来,有些网站会检查这个字段防止盗链。

2.4 超时设置(Timeout)

网络请求不可靠,如果不设置超时,程序可能在网络故障时永远挂起:

try:
    response = requests.get('https://example.com', timeout=5)  # 5 秒超时
    response.raise_for_status()  # 状态码不是 2xx 时抛出异常
except requests.exceptions.Timeout:
    print('请求超时,请检查网络连接')
except requests.exceptions.ConnectionError:
    print('网络连接失败')
except requests.exceptions.HTTPError as e:
    print(f'HTTP 错误:{e}')

timeout 参数可以传一个数字(秒),也可以传一个元组 (connect_timeout, read_timeout) 分别控制连接超时和读取超时。

2.5 Session 会话——保持登录状态

当需要模拟登录或保持 Cookie 时,使用 Session 对象。它会自动保存服务器返回的 Cookie,并在后续请求中携带:

session = requests.Session()

# 第一次请求:登录
login_data = {'username': 'demo', 'password': 'demo123'}
session.post('https://httpbin.org/post', data=login_data)

# 后续请求自动携带 Cookie
response = session.get('https://httpbin.org/cookies')
print(response.text)

Session 不仅可以保持 Cookie,还可以统一设置 headers,避免每次重复编写:

session = requests.Session()
session.headers.update({
    'User-Agent': 'Mozilla/5.0 ...',
    'Accept-Language': 'zh-CN,zh;q=0.9',
})

# 所有通过 session 发出的请求都会带上上述 headers
resp1 = session.get('https://example.com/page1')
resp2 = session.get('https://example.com/page2')

三、BeautifulSoup 解析基础

3.1 解析器的选择

获取到网页内容后,我们需要从 HTML 中提取有用的信息。BeautifulSoup 是 Python 最流行的 HTML/XML 解析库:

from bs4 import BeautifulSoup

html = '

标题

正文内容

' soup = BeautifulSoup(html, 'lxml') # 使用 lxml 解析器

常用的解析器对比:

  • html.parser:Python 内置,无需额外安装,但速度一般
  • lxml:速度快,容错性强,推荐使用(需 pip install lxml)
  • html5lib:解析最严谨,但速度最慢

3.2 find() 和 find_all()

find() 返回第一个匹配的元素,find_all() 返回所有匹配的元素列表。这两个方法是最常用的查找方式:

# 按标签名查找
h1 = soup.find('h1')  # 返回第一个 h1 标签
all_p = soup.find_all('p')  # 返回所有 p 标签

# 按 class 查找
content = soup.find('p', class_='content')

# 按 id 查找
title = soup.find(id='main-title')

# 组合条件
item = soup.find('div', class_='item', id='special')

# 限制返回数量
first_three = soup.find_all('li', limit=3)

获取元素的属性非常直观:

link = soup.find('a')
print(link.get('href'))       # 获取 href 属性
print(link.text.strip())      # 获取纯文本内容
print(link.get('class'))      # 获取 class 列表

# 获取所有链接
all_links = [a.get('href') for a in soup.find_all('a') if a.get('href')]

3.3 select() 与 CSS 选择器

如果你熟悉 CSS,select() 方法会让你感觉更加亲切。它支持几乎所有 CSS 选择器语法:

# 标签选择器
soup.select('h1')      # 所有 h1 标签
soup.select('div')     # 所有 div 标签

# class 选择器
soup.select('.content')  # class="content" 的元素

# ID 选择器
soup.select('#main-title')

# 层级选择器
soup.select('div p')          # div 内部的 p 标签
soup.select('div > p')       # div 的直接子元素 p

# 属性选择器
soup.select('a[href]')                        # 所有带 href 的 a 标签
soup.select('a[href^="https"]')               # href 以 https 开头的 a 标签
soup.select('a[href$=".pdf"]')                # href 以 .pdf 结尾的 a 标签

# 伪类选择器
soup.select('li:nth-child(2)')   # 第二个 li 元素
soup.select('li:first-child')    # 第一个 li 元素

select_one() 返回第一个匹配元素,相当于 find()

first_link = soup.select_one('a.external')

3.4 遍历文档树

除了查找方法,BeautifulSoup 还提供了丰富的文档树遍历API:

# 父子关系
div = soup.find('div')
print(div.parent)       # 父节点
print(div.children)     # 子节点迭代器
print(div.contents)     # 子节点列表

# 兄弟关系
p = soup.find('p')
print(p.next_sibling)   # 下一个兄弟节点
print(p.previous_sibling)  # 上一个兄弟节点

# 搜索所有子孙节点
all_texts = soup.find_all(string=True)  # 所有文本节点

四、实战项目:抓取豆瓣电影 Top250

理论知识学得差不多了,现在我们来做一个完整的实战项目——抓取豆瓣电影 Top250 排行榜的电影信息。豆瓣 Top250 是一个非常适合初学者的目标:页面结构规整,没有复杂的 JavaScript 渲染,反爬机制也相对温和。

4.1 分析目标页面

先打开浏览器的开发者工具(F12),查看 https://movie.douban.com/top250 的 HTML 结构。可以看到每部电影的信息都包裏在

  • 标签中,内部包含:

    翻页规律也很明显:?start=0 第一页,?start=25 第二页,?start=50 第三页,以此类推,每页 25 部电影。

    4.2 编写爬虫代码

    首先定义 User-Agent 轮换池和基础请求函数:

    import requests
    from bs4 import BeautifulSoup
    import time
    import random
    import csv
    
    # User-Agent 轮换池
    USER_AGENTS = [
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15',
        'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0',
        'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1',
    ]
    
    
    def get_random_headers():
        return {
            'User-Agent': random.choice(USER_AGENTS),
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
        }
    
    
    def fetch_page(url):
        """带错误处理的页面请求函数"""
        try:
            resp = requests.get(url, headers=get_random_headers(), timeout=10)
            resp.raise_for_status()
            # 检查编码
            resp.encoding = resp.apparent_encoding
            return resp.text
        except requests.exceptions.Timeout:
            print(f'超时:{url}')
            return None
        except requests.exceptions.HTTPError as e:
            print(f'HTTP 错误 {e.response.status_code}:{url}')
            return None
        except Exception as e:
            print(f'未知错误:{e}')
            return None

    4.3 解析单页数据

    接下来我们解析每一页的电影数据:

    def parse_movie_list(html):
        soup = BeautifulSoup(html, 'lxml')
        movies = []
        
        # 每部电影在 
  • 标签中,class 包含 "item" for item in soup.select('li'): title_el = item.select_one('.title') rating_el = item.select_one('.rating_num') star_el = item.select_one('.star') quote_el = item.select_one('.quote span') link_el = item.select_one('a') # 有些电影没有 quote(简介),需要处理 if not title_el or not rating_el: continue movie = { 'title': title_el.text.strip(), 'rating': rating_el.text.strip(), 'quote': quote_el.text.strip() if quote_el else '', 'url': link_el.get('href') if link_el else '', } # 提取评价人数 if star_el: stars = star_el.find_all('span') if len(stars) >= 3: movie['reviews'] = stars[-1].text.strip().replace('人评价', '') else: movie['reviews'] = '0' movies.append(movie) return movies
  • 4.4 多页抓取与礼貌爬虫

    爬取多页时,我们一定要遵守礼貌爬虫的规则:在两次请求之间添加适当的延迟,避免对目标服务器造成压力:

    def crawl_douban_top250():
        base_url = 'https://movie.douban.com/top250'
        all_movies = []
        
        print('开始抓取豆瓣电影 Top250...')
        
        for page in range(10):  # 总共 10 页
            start = page * 25
            url = f'{base_url}?start={start}'
            
            print(f'正在抓取第 {page+1}/10 页...')
            html = fetch_page(url)
            
            if html is None:
                print(f'第 {page+1} 页抓取失败,跳过')
                continue
            
            movies = parse_movie_list(html)
            all_movies.extend(movies)
            print(f'第 {page+1} 页获取到 {len(movies)} 部电影')
            
            # 礼貌爬虫:随机等待 1~3 秒
            if page < 9:  # 最后一页不需要等待
                delay = random.uniform(1, 3)
                print(f'等待 {delay:.1f} 秒...')
                time.sleep(delay)
        
        return all_movies

    4.5 数据导出到 CSV

    抓取到的数据可以保存为 CSV 文件,方便用 Excel 或数据分析工具打开:

    def save_to_csv(movies, filename='douban_top250.csv'):
        if not movies:
            print('没有数据可保存')
            return
        
        fieldnames = ['title', 'rating', 'reviews', 'quote', 'url']
        
        with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(movies)
        
        print(f'成功保存 {len(movies)} 条记录到 {filename}')
    
    
    # 主函数:一键执行完整流程
    if __name__ == '__main__':
        movies = crawl_douban_top250()
        save_to_csv(movies)
        print(f'抓取完成!共获取 {len(movies)} 部电影信息')

    五、异常处理与调试技巧

    5.1 常见的 HTTP 错误

    状态码 含义 常见原因 解决方案
    403 Forbidden 服务器拒绝访问 检查 User-Agent,添加 Referer,尝试使用代理
    404 Not Found 页面不存在 检查 URL 拼写,确认是否需要添加参数
    429 Too Many Requests 请求频率过高被限流 增加 time.sleep 时间,降低请求频率
    503 Service Unavailable 服务器过载或维护 等待一段时间后重试

    5.2 重试机制

    对于网络不稳定的情况,实现指数退避重试策略:

    from functools import wraps
    
    
    def retry(max_retries=3, delay=1):
        def decorator(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                for attempt in range(max_retries):
                    try:
                        return func(*args, **kwargs)
                    except Exception as e:
                        if attempt == max_retries - 1:
                            raise
                        wait = delay * (2 ** attempt)  # 指数退避:1, 2, 4 秒
                        print(f'第 {attempt+1} 次失败:{e},{wait:.1f} 秒后重试...')
                        time.sleep(wait)
                return None
            return wrapper
        return decorator
    
    
    @retry(max_retries=3, delay=1)
    def fetch_with_retry(url):
        return requests.get(url, timeout=5)

    六、礼貌爬虫的五大原则

    作为一名合格的爬虫开发者,我们必须遵守以下准则:

    6.1 遵守 robots.txt

    在抓取任何网站之前,先查看其 /robots.txt 文件。例如 https://example.com/robots.txt 中可能定义了哪些路径允许爬取。虽然 robots.txt 没有法律约束力,但尊重它是行业基本礼仪。

    6.2 控制请求频率

    任何时候都不能以超过人类浏览的速度去抓取数据。一般来说,两次请求之间至少间隔 1~3 秒。使用 time.sleep(random.uniform(1, 3)) 可以让请求间隔更自然。

    6.3 设置合理的 User-Agent

    不要使用 Python 的默认 User-Agent。轮换使用多个真实的浏览器 UA 字符串,降低被识别为爬虫的概率。

    6.4 识别并避免触发反爬

    常见反爬手段包括:

    • IP 封禁:短时间内同一 IP 请求过多会被封。解决方案:使用代理 IP 池
    • Cookie 验证:部分网站需要在请求中携带特定的 Cookie。解决方案:使用 Session 维护会话
    • JavaScript 渲染:内容通过 JS 动态加载,直接请求 HTML 获取不到。解决方案:使用 Selenium 或 Playwright
    • 验证码:触发频率限制后出现验证码。解决方案:降低频率,或接入打码平台

    6.5 尊重数据版权

    抓取的数据不要用于商业用途(除非获得授权),也不要大规模转载、售卖。合理使用爬虫获取的数据,遵守目标网站的条款和政策。

    七、进阶方向与总结

    7.1 本文总结

    通过本文的学习,你掌握了:

    • 使用 requests 库发送 GET/POST 请求,设置 headers、timeout 和 Session
    • 使用 BeautifulSoup 的 find/find_all 和 select/select_one 解析 HTML
    • 完整的实战项目:豆瓣电影 Top250 数据抓取
    • 数据导出到 CSV 格式
    • 异常处理、重试机制和礼貌爬虫的最佳实践

    7.2 下一步学习方向

    当你掌握了基础爬虫后,可以朝着以下方向继续进阶:

    • 异步爬虫:使用 aiohttp + asyncio 实现大规模并发爬取,效率提升 10~100 倍
    • 动态页面抓取:学习 Selenium、Playwright 或 Puppeteer 处理 JavaScript 渲染的页面
    • Scrapy 框架:Python 最强大的爬虫框架,内置调度、去重、管道、中间件等机制
    • 数据清洗与存储:将爬取的数据存入 MongoDB、MySQL 或 Elasticsearch
    • 分布式爬虫:使用 Scrapy-Redis 或 Celery 实现多机协同抓取

    爬虫是一门实践性极强的技术,只看不练永远无法真正掌握。建议你现在就打开电脑,跟着本文的代码一步步操作,从最简单的页面开始,逐步挑战更复杂的目标。

    记住:能力越大,责任越大。合理使用爬虫技术,做一个有职业道德的开发者。