三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Python爬虫入门实战:从Requests+BeautifulSoup到数据采集全流程

Python爬虫入门实战:从Requests+BeautifulSoup到数据采集全流程

1. 从“数据荒漠”到“信息绿洲”:为什么我们需要爬虫

几年前,我接手一个市场分析项目,需要了解某个细分领域所有竞品的公开定价和功能列表。当时我的第一反应是打开浏览器,一家家官网去翻,手动复制粘贴到Excel里。干了不到十家,人就麻了——效率低下不说,还容易出错,数据格式也乱七八糟。那一刻我深刻体会到,在数据驱动的时代,没有自动化获取数据的能力,就像在沙漠里用手捧水喝,既慢又累,还解不了渴。

这就是Python爬虫的价值所在。它不是什么高深莫测的黑科技,本质上就是一个自动化的数据采集程序,模拟人类在浏览器上的操作(访问网页、点击链接、翻页),但速度是人类的成千上万倍,并且不知疲倦、精准无误。无论是你想分析电商平台的商品价格趋势,追踪社交媒体上的舆情热点,还是批量下载公开的学术论文、图片素材,爬虫都能将你从重复、繁琐的体力劳动中解放出来,把宝贵的时间投入到更有价值的分析和决策上。

很多人一听到“爬虫”就觉得是灰色地带,其实不然。爬取公开的、非敏感的数据,用于个人学习、研究或合法的商业分析,在遵守网站robots.txt协议(网站告知爬虫哪些页面可以抓取的君子协定)和不过度占用服务器资源的前提下,是普遍被接受的实践。它的核心是提升效率,而不是突破边界。今天,我们就来彻底拆解一下,用Python爬取数据的完整流程、核心工具以及那些新手必踩的坑。我会假设你已经有最基础的Python语法知识(知道变量、列表、循环),我们从环境搭建开始,一步步构建一个健壮可用的爬虫。

2. 工欲善其事:搭建你的爬虫作战环境

写爬虫和写普通的Python脚本有点不一样,它严重依赖几个第三方库。如果你还没配置好环境,跟着下面的步骤来,能避开90%的初期环境问题。

2.1 Python安装与包管理工具pip

首先,确保你安装了Python。去Python官网下载最新稳定版(比如3.8以上版本)即可。安装时,务必勾选“Add Python to PATH”这个选项,这能让你在命令行(CMD或终端)里直接使用pythonpip命令,省去后续手动配置环境变量的麻烦。

安装完成后,打开命令行,输入以下命令检查是否成功:

python --version pip --version

如果都能正确显示版本号,说明基础环境OK。pip是Python的包管理工具,可以理解为Python世界的“应用商店”,我们接下来需要的所有第三方库都通过它来安装。

2.2 核心武器库:Requests 与 BeautifulSoup

对于绝大多数静态网页(即打开网页后,数据就直接在HTML源代码里的页面),我们主要依赖两个库:

  1. Requests: 负责“敲门和取东西”。它模拟浏览器发送HTTP请求(GET、POST等)到目标网站,并把服务器返回的网页内容(HTML代码)拿回来。它简单易用,是网络请求的事实标准
  2. BeautifulSoup (bs4): 负责“拆包裹和找宝贝”。服务器返回的HTML代码像一团乱麻,BeautifulSoup能把它解析成一棵结构清晰的树,然后让你能像使用CSS选择器一样,轻松地找到并提取里面的特定数据(如标题、价格、链接)。

安装它们只需一行命令:

pip install requests beautifulsoup4

这里有个小细节:包名是beautifulsoup4,但在代码里导入时我们写from bs4 import BeautifulSoup

2.3 进阶与伪装:应对复杂场景

随着你爬取的网站越来越复杂,可能会遇到以下情况:

  • 动态加载: 数据不是一开始就在HTML里,而是网页加载后,由JavaScript代码通过Ajax请求后台API获取并渲染的。这时候用Requests拿到的HTML是空的,看不到数据。
  • 反爬机制: 网站为了防止被过度抓取,会检测请求头、验证IP频率、甚至要求登录。

为此,你需要更多工具:

  • Selenium / Playwright: 它们可以控制一个真实的浏览器(如Chrome)去访问网页,能完整执行页面里的JavaScript,等数据加载完成后再获取HTML。相当于派了一个“机器人”去帮你操作浏览器,适合解决动态加载问题,但速度较慢,资源消耗大。
  • 请求头(Headers)伪装: 用Requests时,默认的请求头很简陋,网站一眼就能认出是爬虫。我们需要把请求头伪装成普通浏览器的样子,最关键的是User-Agent字段。
    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' } response = requests.get(url, headers=headers)
  • 代理IP池: 如果单个IP在短时间内发送过多请求,容易被封。使用代理IP可以轮换不同的IP地址,降低风险。但免费代理大多不稳定,商用代理需要成本,初学者可以先从控制请求频率(加延时)做起。

对于本文,我们将聚焦在最常用、最经典的Requests + BeautifulSoup组合上,掌握它们,你就已经能解决70%以上的爬虫需求了。

3. 庖丁解牛:拆解一个完整的爬虫工作流

理论说再多不如动手做一遍。我们以一个简单的实战为例:爬取一个豆瓣电影Top250列表页面(https://movie.douban.com/top250)上的电影名称和评分。请注意,实际操作前请查看该网站的robots.txt,并确保你的爬取行为温和、有间隔,仅用于学习。

3.1 第一步:观察与分析——开发者工具是你的眼睛

在写代码之前,一定要先手动打开目标网页,按F12打开浏览器的“开发者工具”。这是爬虫工程师最重要的“眼睛”。

  1. 切换到“Elements”(元素)标签页,这里可以看到网页完整的HTML结构。
  2. 使用左上角的箭头工具(或按Ctrl+Shift+C),去点击页面上你想抓取的数据,比如一个电影名。开发者工具会自动定位到该数据对应的HTML代码位置。
  3. 仔细观察它的标签结构属性。比如,你可能会发现每个电影条目都包裹在一个<div class="item">里,电影名在一个<span class="title">标签内,评分在一个<span class="rating_num">标签内。
  4. 切换到“Network”(网络)标签页,然后刷新页面。这里记录了浏览器发出的所有请求。如果数据是动态加载的,你可能会看到一些XHRFetch类型的请求,这些就是Ajax请求,直接抓取这些请求的地址往往更高效。但我们这个例子是静态页面。

通过观察,我们确定了数据的位置和特征,接下来就可以用代码来模拟了。

3.2 第二步:获取数据——Requests发起请求

现在,我们用Requests去获取这个页面的HTML内容。

import requests from bs4 import BeautifulSoup url = 'https://movie.douban.com/top250' # 伪装成浏览器的请求头 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' } try: response = requests.get(url, headers=headers) # 检查请求是否成功(状态码200表示成功) response.raise_for_status() # 设置正确的编码,防止中文乱码 response.encoding = response.apparent_encoding html_content = response.text print("页面获取成功!") except requests.RequestException as e: print(f"请求出错:{e}") html_content = None if html_content: # 接下来就可以用BeautifulSoup解析了

注意:这里使用了try...except来捕获请求异常(如网络错误、404等),这是编写健壮爬虫的好习惯。response.raise_for_status()会在状态码不是200时抛出异常。

3.3 第三步:解析与提取——BeautifulSoup大显身手

拿到HTML字符串后,交给BeautifulSoup解析。

if html_content: soup = BeautifulSoup(html_content, 'html.parser') # 使用我们之前观察到的规律:每个电影条目都在 class="item" 的div里 movie_items = soup.find_all('div', class_='item') for item in movie_items: # 在每个条目里,查找电影名。注意:豆瓣Top250页面的中文名在第一个class="title"的span里 title_tag = item.find('span', class_='title') # 查找评分 rating_tag = item.find('span', class_='rating_num') # 防止有些条目可能缺少信息,做一下判断 title = title_tag.text if title_tag else '未找到片名' rating = rating_tag.text if rating_tag else '无评分' print(f"电影:{title}, 评分:{rating}")

这段代码已经可以运行并打印出第一页25部电影的名称和评分了。soup.find_all()是核心方法,它找到所有符合条件的标签。find()则只找第一个。class_参数后面有个下划线,是因为class是Python的关键字,所以BeautifulSoup用class_来代表HTML的class属性。

3.4 第四步:处理翻页与数据存储

豆瓣Top250有10页,我们需要爬取所有页面。观察网址规律:第一页是top250,第二页是top250?start=25,第三页是top250?start=50... 规律是start=(页码-1)*25

import time base_url = 'https://movie.douban.com/top250' headers = { ... } # 同上 all_movies = [] # 用一个列表来存储所有电影信息 for page in range(0, 10): # 0到9,共10页 start = page * 25 url = f'{base_url}?start={start}' print(f"正在抓取第{page+1}页: {url}") try: response = requests.get(url, headers=headers) response.raise_for_status() response.encoding = response.apparent_encoding soup = BeautifulSoup(response.text, 'html.parser') items = soup.find_all('div', class_='item') for item in items: title_tag = item.find('span', class_='title') rating_tag = item.find('span', class_='rating_num') # 还可以提取其他信息,比如简介、评价人数等 # comment_tag = item.find('div', class_='star').find_all('span')[-1].text movie_info = { 'title': title_tag.text if title_tag else 'N/A', 'rating': rating_tag.text if rating_tag else 'N/A', # 'comment': comment_tag } all_movies.append(movie_info) # 礼貌性延时,避免请求过快给服务器造成压力 time.sleep(2) except requests.RequestException as e: print(f"抓取第{page+1}页时出错:{e}") continue print(f"共抓取到{len(all_movies)}部电影信息。")

数据抓取到列表里后,我们需要把它存下来。最常用的方式是存为CSV或JSON文件,方便用Excel或后续的Python程序(如pandas)进行分析。

import csv import json # 保存为CSV with open('douban_top250.csv', 'w', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=['title', 'rating']) writer.writeheader() writer.writerows(all_movies) # 保存为JSON with open('douban_top250.json', 'w', encoding='utf-8') as f: json.dump(all_movies, f, ensure_ascii=False, indent=2) print("数据已保存至 douban_top250.csv 和 douban_top250.json")

4. 从能跑到跑得稳:爬虫工程化与反爬应对策略

上面的例子是一个理想化的教学场景。真实世界的网站往往设有各种障碍。让你的爬虫从“能跑”到“跑得稳、跑得久”,需要关注以下几点。

4.1 构建健壮的请求会话

使用requests.Session()可以建立一个会话,它会在多次请求间自动保持某些参数(如cookies),并且能复用底层的TCP连接,提升效率。

session = requests.Session() session.headers.update(headers) # 为整个会话设置统一的请求头 response = session.get(url) # 后续请求可以直接用 session.get/post

4.2 处理常见的反爬机制

  1. User-Agent检测: 我们已经做了伪装。更进一步,可以准备一个User-Agent列表,每次请求随机选取一个。
  2. 请求频率限制: 这是最基本的道德和技术要求。在循环请求中一定要加time.sleep(),比如time.sleep(random.uniform(1, 3))随机休眠1-3秒。过于频繁的请求等同于攻击。
  3. IP封锁: 如果加了延时还被封IP,就需要考虑使用代理IP。使用方式很简单:
    proxies = { 'http': 'http://your_proxy_ip:port', 'https': 'https://your_proxy_ip:port', } response = requests.get(url, headers=headers, proxies=proxies)
    免费代理的稳定性极差,自己搭建或购买可靠的代理服务是商业化爬虫的必经之路。
  4. 验证码: 遇到验证码,小规模爬虫可以手动处理,或者接入打码平台。大规模情况下需要研究验证码的破解(如图像识别),但这通常涉及更复杂的技术和道德考量。
  5. 登录与Cookie: 对于需要登录的网站,先用浏览器手动登录,然后从开发者工具的Network标签里复制出Cookie,放到请求头里。或者用Selenium模拟登录过程,获取登录后的Cookie再交给Requests使用。

4.3 异常处理与日志记录

一个工业级的爬虫必须有完善的异常处理和日志。

import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[logging.FileHandler('spider.log'), logging.StreamHandler()]) try: response = session.get(url, timeout=10) # 设置超时 response.raise_for_status() except requests.exceptions.Timeout: logging.error(f"请求超时: {url}") except requests.exceptions.HTTPError as e: logging.error(f"HTTP错误 {e.response.status_code}: {url}") except Exception as e: logging.error(f"未知错误: {e}")

良好的日志能让你在爬虫运行数小时后,依然能清晰定位哪里出了问题。

4.4 解析策略的容错性

不要假设网页结构永远不变。你的解析代码要足够健壮。

# 不推荐的脆弱写法 title = soup.find('span', class_='title').text # 如果找不到,这里会抛出AttributeError # 推荐的健壮写法 title_tag = soup.find('span', class_='title') if title_tag: title = title_tag.text.strip() # 用strip()去掉空白字符 else: title = 'N/A' logging.warning(f"在页面中未找到标题标签")

或者使用try...except包裹,或者使用get_text()方法(如果标签存在则返回文本,否则返回空字符串)。

5. 进阶之路:当Requests+BS4力不从心时

当你遇到以下情况,就需要请出更强大的工具了:

  • 页面内容由JavaScript动态生成: 用Requests获取的HTML里没有数据。此时可以:

    1. 寻找隐藏的API: 在开发者工具的Network标签里,过滤XHRFetch请求,找到真正返回数据的那个接口地址。然后用Requests直接请求这个接口(通常返回JSON格式),效率更高。这是首选方案。
    2. 使用Selenium/Playwright: 让程序控制浏览器渲染页面,等数据加载完成后再获取HTML。虽然慢,但能解决几乎所有前端渲染的问题。
      from selenium import webdriver from selenium.webdriver.common.by import By driver = webdriver.Chrome() # 需要先下载对应浏览器的驱动 driver.get(url) # 等待某个元素出现 element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, "item")) ) html = driver.page_source # 之后就可以用BeautifulSoup解析html了 driver.quit()
  • 大规模分布式爬取: 当需要爬取的数据量极其庞大(数千万页面)时,单机爬虫在速度和稳定性上都不够。这时需要考虑使用Scrapy框架。Scrapy是一个为爬虫而生的异步框架,它内置了请求调度、去重、管道处理(数据清洗、存储)等功能,可以轻松地扩展到多台机器上运行,是构建大型爬虫项目的首选。

  • 需要解析JavaScript执行结果: 除了Selenium,还可以使用PyppeteerPlaywright,它们提供了比Selenium更现代、性能更好的无头浏览器控制接口。

6. 法律与道德的边界:做一个负责任的爬虫工程师

这是最重要的一节。技术本身无罪,但使用技术的方式有对错。

  1. 尊重robots.txt: 这是网站管理员与爬虫之间的基本协议。访问https://example.com/robots.txt可以查看该网站对爬虫的限制。明确禁止爬取的目录,就不要去碰。
  2. 控制访问频率: 这是最基本的网络礼仪。你的爬虫不应该影响网站的正常服务。添加显著的延时(如每秒请求数低于1),避免并发大量请求。
  3. 识别并遵守使用条款: 很多网站的用户协议里会明确禁止爬虫或自动化访问。虽然法律效力因地区而异,但违反协议是不道德且有风险的。
  4. 只爬取公开数据: 切勿尝试爬取需要登录才能访问的非公开数据、个人隐私数据(如用户手机号、邮箱)、或受版权严格保护的内容。
  5. 数据用途: 将爬取的数据用于个人学习、研究或公益目的,风险较低。如果用于商业盈利,务必进行法律风险评估。
  6. 知识产权: 爬取的数据可能涉及数据库权或版权。对数据进行实质性加工、分析后形成新的成果,比单纯复制原始数据风险更低。

我个人的经验是,在开始一个爬虫项目前,先问自己三个问题:我的爬取行为会拖慢甚至搞垮对方的服务器吗?我爬取的数据会侵犯个人隐私或商业秘密吗?我使用这些数据的方式会伤害到数据提供方吗?如果任何一个答案是“可能”,那就停下来,重新设计你的方案,或者放弃。

← 返回列表