前言
大家好,今天想和大家分享一个非常实用的话题——网络爬虫入门。很多人听到"爬虫"两个字,第一反应可能是"写代码抓数据",觉得门槛很高。其实不然,爬虫的核心原理非常简单:模拟浏览器向服务器发送请求,然后解析服务器返回的网页内容,从中提取出我们想要的数据。
这篇文章是入门级的内容,适合对爬虫感兴趣的零基础读者。我会尽量用通俗的语言,配合完整的代码示例,带大家走完一个爬虫从发送请求到存储数据的完整流程。
一、网络爬虫概述
1.1 什么是网络爬虫?
网络爬虫(Web Crawler / Web Scraper)是一种自动化程序,它按照一定的规则,自动地浏览万维网并获取信息。我们可以把它理解成一个"机器人",这个机器人不知疲倦地访问网页、下载页面内容,然后从中提取出我们需要的数据。
爬虫的规模可以很小,小到只抓取一个网页获取某条新闻标题;也可以非常庞大,大到像 Google、百度这样的搜索引擎,它们每天爬取数十亿个网页,建立索引供用户搜索。
1.2 爬虫的应用场景
爬虫技术的应用非常广泛,这里列举几个常见的场景:
- **搜索引擎**:Google、Bing、百度等搜索引擎的核心就是爬虫
- **价格监控**:电商平台的价格对比、降价提醒
- **数据采集**:学术研究、市场调研、舆情分析
- **内容聚合**:新闻聚合、RSS 订阅更新
- **SEO 监测**:检查网站排名、外链情况
- **自动化测试**:模拟用户操作测试网站功能
1.3 法律与道德边界
学习爬虫,首先要建立正确的法律意识。这里几条红线一定要记住:
- **遵守 robots.txt**:网站根目录下的 `robots.txt` 文件声明了哪些路径允许爬取
- **尊重版权**:爬取的数据如果涉及版权内容,不可随意商用
- **控制频率**:不要对服务器造成压力,这是基本的网络礼仪
- **遵守《数据安全法》**:在中国,爬取个人信息、商业机密等受法律保护的数据可能构成违法
- **绕过反爬措施可能违法**:破解验证码、绕过登录认证等措施,可能触犯《刑法》中"非法获取计算机信息系统数据罪"
**一句话总结:学爬虫是技术,用爬虫是法律。技术本身中立,但使用方式决定了是否合规。**
二、HTTP协议基础
爬虫的本质就是发送 HTTP 请求并处理响应,所以理解 HTTP 协议非常关键。
2.1 什么是 HTTP?
HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网上应用最广泛的协议。浏览器和服务器之间通过 HTTP 进行通信:浏览器发送"请求"(Request),服务器返回"响应"(Response)。
一个典型的 HTTP 请求包含:
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html
2.2 请求方法(HTTP Methods)
最常用的请求方法有:
| 方法 | 含义 | 爬虫中的用途 |
|------|------|------------|
| GET | 获取资源 | 最常用,获取网页内容 |
| POST | 提交数据 | 登录、搜索、表单提交 |
| HEAD | 获取响应头 | 探测链接是否有效 |
| PUT | 更新资源 | 较少使用 |
| DELETE | 删除资源 | 极少使用 |
编写爬虫时,绝大多数场景下我们只需要使用 GET 和 `POST`。
2.3 状态码(Status Codes)
服务器返回的响应中会包含一个状态码,表示请求的处理结果:
- **200 OK**:请求成功,正常返回页面内容
- **301/302 Moved**:资源被重定向到其他 URL
- **403 Forbidden**:服务器拒绝访问(常见于反爬)
- **404 Not Found**:资源不存在
- **500 Internal Server Error**:服务器内部错误
- **503 Service Unavailable**:服务器暂时无法处理请求
# Python中检查状态码
import requests
response = requests.get('https://httpbin.org/status/200')
if response.status_code == 200:
print('请求成功!')
elif response.status_code == 403:
print('被拒绝了,需要加请求头或使用代理')
elif response.status_code == 404:
print('页面不存在')
2.4 请求头(Headers)
请求头是爬虫中非常关键的一环。服务器通过请求头来判断请求来源,其中最重要的几个字段:
- **`User-Agent`**:标识客户端类型,如浏览器版本、操作系统等
- **`Referer`**:来源页面,告诉服务器是从哪个页面跳转过来的
- **`Cookie`**:携带用户的身份信息和状态
- **`Accept`**:告诉服务器客户端能处理的内容类型
- **`Authorization`**:携带认证信息(如 Token)
很多网站会检查 User-Agent,如果检测到是爬虫工具(如 Python-requests)就会拒绝访问。所以我们通常需要伪装成真实浏览器:
headers = {
'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',
'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)
2.5 Cookie 与 Session
HTTP 是"无状态"协议,服务器默认不知道两次请求是否来自同一个用户。Cookie 和 Session 就是用来解决这个问题的:
- **Cookie**:存储在客户端(浏览器)的小段文本,每次请求会自动附带
- **Session**:存储在服务器端的用户状态数据,通过 Session ID 关联
爬虫处理登录状态的常见方式:
import requests
# 创建一个 Session 对象,可以自动保存 Cookie
session = requests.Session()
# 模拟登录,提交表单
login_data = {'username': 'user', 'password': 'pass'}
session.post('https://example.com/login', data=login_data)
# 后续请求会自动带上登录后的 Cookie
response = session.get('https://example.com/dashboard')
print(response.text)
三、Python爬虫工具链
Python 是爬虫领域最受欢迎的语言,生态非常丰富。这里对比三款最主流的 HTTP 请求库:
3.1 requests —— 简单易用的首选
requests 是 Python 爬虫的入门标配,API 设计人性化,使用非常简洁。
import requests
# GET 请求
resp = requests.get('https://api.github.com')
print(resp.json()) # 自动解析 JSON
# POST 请求
resp = requests.post('https://httpbin.org/post', data={'key': 'value'})
# 带参数的 GET 请求
params = {'q': 'python', 'page': 1}
resp = requests.get('https://httpbin.org/get', params=params)
优点:API 简洁、文档丰富、社区庞大
缺点:同步阻塞、不支持 HTTP/2
3.2 httpx —— requests 的现代替代者
httpx 兼容 `requests` 的 API,同时支持异步和 HTTP/2,是近年来的热门选择。
import httpx
# 同步方式(和 requests 类似)
resp = httpx.get('https://httpbin.org/get')
print(resp.status_code)
# 异步方式(需要 async/await)
async def fetch():
async with httpx.AsyncClient() as client:
resp = await client.get('https://httpbin.org/get')
return resp.text
# 支持 HTTP/2
resp = httpx.get('https://httpbin.org/get', http2=True)
优点:支持异步和 HTTP/2、API 兼容 requests
缺点:部分场景下性能不如 aiohttp
3.3 aiohttp —— 高性能异步爬虫
aiohttp 是专为异步而生 HTTP 库,适合大规模并发爬取。
import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
html = await fetch(session, 'https://httpbin.org/get')
print(html)
asyncio.run(main())
优点:异步性能好、支持 WebSocket
缺点:API 相对底层、学习曲线稍陡
3.4 如何选择?
| 场景 | 推荐库 |
|------|--------|
| 快速原型、简单爬虫 | requests |
| 需要 HTTP/2 或异步 | httpx |
| 大规模并发爬取 | aiohttp |
对于初学者,我建议 先从 requests 入手,等需要大规模爬取时再学习异步方案。
四、页面解析技术
拿到 HTML 源码后,下一步就是从中提取我们需要的数据。以下是几种常用的页面解析方案:
4.1 正则表达式
正则表达式是提取文本的"瑞士军刀",性能好,但可读性较差,不适合解析复杂的 HTML 结构。
import re
import requests
html = requests.get('https://httpbin.org/html').text
# 提取标题
title_pattern = r'(.*?)
'
match = re.search(title_pattern, html)
if match:
print('标题:', match.group(1))
# 提取所有链接
link_pattern = r'href=["\'](https?://[^"\']+)["\']'
links = re.findall(link_pattern, html)
print('找到链接:', links[:5])
适用场景:简单的文本提取、日志分析
不适用场景:复杂的嵌套 HTML、属性提取
4.2 BeautifulSoup —— 入门首选
BeautifulSoup 是 Python 最流行的 HTML/XML 解析库,API 非常友好,适合初学者。
from bs4 import BeautifulSoup
import requests
html = requests.get('https://httpbin.org/html').text
soup = BeautifulSoup(html, 'html.parser')
# 通过标签名获取
h1 = soup.find('h1')
print('标题:', h1.text)
# 通过 class 查找
items = soup.find_all(class_='item')
for item in items:
print(item.text)
# 通过属性查找
link = soup.find('a', href=True)
print('链接:', link['href'])
# CSS 选择器(通过 select 方法)
titles = soup.select('div.content h2')
for title in titles:
print(title.text.strip())
解析器选择:
- `html.parser`:Python 内置,不需要额外安装
- `lxml`:需要安装(`pip install lxml`),速度更快
- `html5lib`:最慢但最容错
建议:直接上 lxml,速度最快。
4.3 lxml + XPath —— 精准高效
lxml 是底层用 C 语言实现的 XML/HTML 解析库,速度极快,配合 XPath 语法可以非常精准地定位元素。
from lxml import html
import requests
resp = requests.get('https://httpbin.org/html')
tree = html.fromstring(resp.content)
# XPath 定位
title = tree.xpath('//h1/text()')
print('标题:', title[0] if title else '')
# 通过 class 定位
items = tree.xpath('//div[@class="item"]')
for item in items:
print(item.text)
# 提取属性值
links = tree.xpath('//a/@href')
print('所有链接:', links[:5])
# 多条件组合
result = tree.xpath('//div[@id="main"]//a[contains(@href, "article")]/text()')
print(result)
XPath 速查表:
| 表达式 | 含义 |
|--------|------|
| //div | 选取所有 div 元素 |
| //div[@class="foo"] | 选取 class 为 foo 的 div |
| //a/@href | 提取所有 a 标签的 href 属性 |
| //div[contains(@class, "bar")] | class 包含 bar 的 div |
| //div/p[1] | div 下的第一个 p 标签 |
4.4 四种解析方案对比
| 方案 | 学习难度 | 性能 | HTML容错 | 推荐指数 |
|------|---------|------|---------|---------|
| 正则表达式 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | ⭐⭐ |
| BeautifulSoup | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| lxml + XPath | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| CSS 选择器 | ⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
**个人建议**:新手先学 BeautifulSoup 上手,然后深入学习 lxml + XPath 应对复杂场景。正则表达式可以作为辅助工具偶尔使用。
五、动态页面处理
现实中的很多网站都是"动态页面"——页面内容不是直接写在 HTML 里的,而是通过 JavaScript 加载渲染出来的。对于这类页面,单纯请求 HTML 源码是拿不到数据的。
5.1 判断页面是否动态
最简单的判断方法:在浏览器中右键查看"网页源码",如果内容很少,但实际页面看起来很丰富,大概率是动态渲染。
或者用 Python 请求一下看看:
import requests
from bs4 import BeautifulSoup
resp = requests.get('https://example.com')
soup = BeautifulSoup(resp.text, 'html.parser')
# 如果这里找不到内容,但浏览器打开能看到,说明是动态渲染
print(soup.prettify()[:1000])
5.2 寻找 API 接口(推荐方案)
很多 SPA(单页应用)网站的数据是通过 XHR/Fetch 请求从 API 获取的。如果能直接调用 API,就不需要渲染页面了。
如何发现 API:
1. 打开浏览器开发者工具(F12)
2. 切换到 Network → Fetch/XHR 标签
3. 刷新页面,观察请求
4. 找到返回 JSON 数据的请求,直接调用
import requests
# 直接调用 API 获取数据,比渲染页面高效得多
api_url = 'https://example.com/api/articles?page=1'
headers = {
'User-Agent': 'Mozilla/5.0 ...',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
}
resp = requests.get(api_url, headers=headers)
data = resp.json() # 直接拿到结构化的 JSON 数据
print(data)
5.3 Selenium —— 模拟真实浏览器
当无法找到 API 时,就需要使用浏览器自动化工具了。Selenium 是最经典的选择。
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
# 配置无头浏览器(不显示界面)
options = Options()
options.add_argument('--headless') # 无头模式
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('User-Agent=Mozilla/5.0 ...')
driver = webdriver.Chrome(options=options)
# 访问页面
driver.get('https://example.com')
# 等待 JavaScript 渲染完成
time.sleep(3) # 简单粗暴
# 或者使用显式等待
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 10)
element = wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'content')))
# 获取渲染后的页面源码
html = driver.page_source
# 传给 BeautifulSoup 解析
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
print(soup.find('h1').text)
driver.quit()
5.4 Playwright —— 新一代浏览器自动化
Playwright 是微软开发的新一代浏览器自动化工具,比 Selenium 更现代、更强大。
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# 拦截网络请求(可选)
page.route('**/*.{png,jpg,jpeg,gif}', lambda route: route.abort())
# 访问页面
page.goto('https://example.com')
# 等待某个元素出现
page.wait_for_selector('.content', timeout=10000)
# 获取渲染后的内容
content = page.content()
# 点击和交互
page.click('button#load-more')
page.wait_for_timeout(2000)
# 截图(调试用)
page.screenshot(path='debug.png')
browser.close()
5.5 Puppeteer(Node.js)
如果你使用 Node.js,Puppeteer 是 Google 官方维护的 Chrome 控制库:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://example.com');
await page.waitForSelector('.content');
const html = await page.content();
console.log(html);
await browser.close();
})();
5.6 动态页面处理方案对比
| 工具 | 语言 | 速度 | 资源占用 | 推荐场景 |
|------|------|------|---------|---------|
| 直接调 API | 任意 | ⭐⭐⭐⭐⭐ | 极低 | 能调 API 就优先选 |
| Selenium | 多语言 | ⭐⭐ | 高 | 传统项目、兼容性要求高 |
| Playwright | 多语言 | ⭐⭐⭐ | 中 | 强烈推荐,新项目首选 |
| Puppeteer | Node.js | ⭐⭐⭐ | 中 | Node.js 生态项目 |
六、数据存储
数据爬取下来后,如何持久化存储?下面是几种常用方案:
6.1 CSV 存储
CSV 是最简单的表格格式,适合结构化数据:
import csv
data = [
{'title': '文章一', 'url': 'https://example.com/1', 'date': '2025-01-01'},
{'title': '文章二', 'url': 'https://example.com/2', 'date': '2025-01-02'},
]
with open('articles.csv', 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.DictWriter(f, fieldnames=['title', 'url', 'date'])
writer.writeheader()
writer.writerows(data)
print('已保存到 articles.csv')
6.2 JSON 存储
JSON 格式灵活,支持嵌套结构:
import json
data = {
'source': 'example.com',
'count': 2,
'articles': [
{'title': '文章一', 'url': 'https://example.com/1'},
{'title': '文章二', 'url': 'https://example.com/2'},
]
}
with open('articles.json', 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print('已保存到 articles.json')
6.3 SQLite 存储
SQLite 是轻量级嵌入式数据库,无需安装服务端,适合单机爬虫:
import sqlite3
conn = sqlite3.connect('crawler.db')
cursor = conn.cursor()
# 创建表
cursor.execute('''
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT UNIQUE NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 插入数据
cursor.execute(
'INSERT OR IGNORE INTO articles (title, url, content) VALUES (?, ?, ?)',
('文章标题', 'https://example.com/1', '文章内容...')
)
conn.commit()
# 查询数据
cursor.execute('SELECT * FROM articles LIMIT 10')
for row in cursor.fetchall():
print(row)
conn.close()
6.4 MySQL / PostgreSQL
大规模生产环境需要更强大的数据库,这里以 MySQL 为例:
import pymysql
conn = pymysql.connect(
host='localhost',
user='root',
password='password',
database='crawler',
charset='utf8mb4',
)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS articles (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(500) NOT NULL,
url VARCHAR(2000) UNIQUE NOT NULL,
content LONGTEXT,
crawled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''')
# 批量插入
sql = 'INSERT IGNORE INTO articles (title, url) VALUES (%s, %s)'
data = [('文章一', 'https://example.com/1'), ('文章二', 'https://example.com/2')]
cursor.executemany(sql, data)
conn.commit()
conn.close()
6.5 存储方案选择
| 方案 | 适用场景 | 优点 | 缺点 |
|------|---------|------|------|
| CSV | 简单报表、Excel 兼容 | 通用性强、可读性好 | 不支持复杂结构 |
| JSON | API 数据、嵌套结构 | 灵活、易于调试 | 不适合大数据量 |
| SQLite | 单机爬虫、原型 | 零配置、轻量 | 不支持并发写入 |
| MySQL/PG | 生产环境、大量数据 | 性能好、支持并发 | 需要维护数据库服务 |
七、完整的爬虫示例
下面我们编写一个完整的爬虫示例:从 Hacker News 首页抓取热门文章的标题、链接和分数,并存储到 SQLite 数据库中。
"""
Hacker News 热门文章爬虫
功能:抓取首页文章列表,提取标题、链接、分数,存入 SQLite
"""
import requests
from bs4 import BeautifulSoup
import sqlite3
import time
import re
def setup_database():
"""创建数据库和表"""
conn = sqlite3.connect('hackernews.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS stories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT,
score INTEGER DEFAULT 0,
author TEXT,
comments INTEGER DEFAULT 0,
crawled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
return conn
def fetch_page(url):
"""发送 HTTP 请求,获取页面内容"""
headers = {
'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',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
}
print(f'正在请求: {url}')
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status() # 如果状态码不是 200,抛出异常
return response.text
except requests.RequestException as e:
print(f'请求失败: {e}')
return None
def parse_stories(html):
"""解析 HTML,提取文章信息"""
soup = BeautifulSoup(html, 'html.parser')
stories = []
# Hacker News 每一行是一个
for row in soup.select('tr.athing'):
title_cell = row.find('td', class_='title')
if not title_cell:
continue
# 标题和链接
link_tag = title_cell.find('a')
if not link_tag:
continue
title = link_tag.get_text(strip=True)
url = link_tag.get('href', '')
# 如果是相对路径,拼接完整 URL
if url.startswith('item?'):
url = f'https://news.ycombinator.com/{url}'
# 获取分数和评论数(在下一行 中)
subtext_row = row.find_next_sibling('tr')
score = 0
author = ''
comments = 0
if subtext_row:
subtext = subtext_row.find('td', class_='subtext')
if subtext:
score_span = subtext.find('span', class_='score')
if score_span:
score_text = score_span.get_text()
score = int(re.search(r'\d+', score_text).group())
author_tag = subtext.find('a', class_='hnuser')
if author_tag:
author = author_tag.get_text(strip=True)
# 评论数
links = subtext.find_all('a')
for link in links:
text = link.get_text()
if 'comment' in text:
num = re.search(r'\d+', text)
if num:
comments = int(num.group())
stories.append({
'title': title,
'url': url,
'score': score,
'author': author,
'comments': comments,
})
return stories
def save_to_database(conn, stories):
"""将文章数据存入数据库"""
cursor = conn.cursor()
inserted = 0
for story in stories:
try:
cursor.execute('''
INSERT OR IGNORE INTO stories (title, url, score, author, comments)
VALUES (?, ?, ?, ?, ?)
''', (
story['title'],
story['url'],
story['score'],
story['author'],
story['comments'],
))
if cursor.rowcount > 0:
inserted += 1
except sqlite3.Error as e:
print(f'数据库写入错误: {e}')
conn.commit()
return inserted
def main():
"""主函数:执行爬虫流程"""
print('=' * 50)
print('Hacker News 热门文章爬虫')
print('=' * 50)
# 1. 初始化数据库
conn = setup_database()
# 2. 获取页面
html = fetch_page('https://news.ycombinator.com/')
if not html:
conn.close()
return
# 3. 解析数据
stories = parse_stories(html)
print(f'解析到 {len(stories)} 篇文章')
# 4. 存储数据
count = save_to_database(conn, stories)
print(f'新增了 {count} 条记录')
# 5. 展示结果
print('\n抓取结果预览:')
print('-' * 50)
for i, story in enumerate(stories[:5], 1):
print(f'{i}. [{story["score"]}分] {story["title"]}')
print(f' 作者: {story["author"]} | 评论: {story["comments"]}')
print(f' 链接: {story["url"][:70]}...')
print()
# 6. 关闭数据库连接
conn.close()
print(f'数据已保存到 hackernews.db')
print('爬取完成!')
if __name__ == '__main__':
main()
运行这个脚本,你会在终端看到抓取过程,同时数据会自动保存到 hackernews.db 中。
运行结果示例:
==================================================
Hacker News 热门文章爬虫
==================================================
正在请求: https://news.ycombinator.com/
解析到 30 篇文章
新增了 30 条记录
抓取结果预览:
--------------------------------------------------
1. [423分] Show HN: I built a CLI tool to generate landing pages
作者: user123 | 评论: 158
链接: https://example.com/cli-landing-page...
2. [387分] Ask HN: What are you working on?
作者: techfan | 评论: 342
...
数据已保存到 hackernews.db
爬取完成!
八、爬虫礼仪与最佳实践
最后,我们来聊聊写爬虫时应该遵守的一些基本原则和技巧。
8.1 尊重 robots.txt
robots.txt 是网站根目录下的一个文本文件,声明了哪些路径允许或禁止爬虫访问。
# 查看网站的 robots.txt
import requests
resp = requests.get('https://example.com/robots.txt')
print(resp.text)
示例输出:
User-agent: *
Disallow: /admin/
Disallow: /private/
Allow: /public/
Sitemap: https://example.com/sitemap.xml
爬虫应该做的事情:
- 爬取前检查 `robots.txt`
- 遵守 `Disallow` 规则
- 记录并遵循 `Crawl-delay` 指令
一个简单的 robots.txt 解析示例:
import re
import requests
def check_robots_txt(base_url):
"""检查 robots.txt,返回是否允许爬取"""
robots_url = base_url.rstrip('/') + '/robots.txt'
try:
resp = requests.get(robots_url, timeout=5)
if resp.status_code == 200:
print(f'robots.txt 内容:\n{resp.text}')
# 这里可以用 urllib.robotparser 做更完整的解析
from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.parse(resp.text.splitlines())
can_fetch = rp.can_fetch('MyCrawler/1.0', base_url)
print(f'允许爬取首页: {can_fetch}')
return rp
except Exception as e:
print(f'无法读取 robots.txt: {e}')
return None
8.2 控制请求频率
这是最基本的网络礼仪,也是避免被 IP 封禁的关键。
import time
import random
class PoliteCrawler:
"""有礼貌的爬虫——控制请求频率"""
def __init__(self, min_delay=1.0, max_delay=3.0):
self.min_delay = min_delay
self.max_delay = max_delay
self.last_request_time = 0
def wait_before_request(self):
"""在每次请求之前等待"""
elapsed = time.time() - self.last_request_time
delay = random.uniform(self.min_delay, self.max_delay)
if elapsed < delay:
sleep_time = delay - elapsed
print(f'等待 {sleep_time:.1f} 秒...')
time.sleep(sleep_time)
self.last_request_time = time.time()
def get(self, url, **kwargs):
self.wait_before_request()
print(f'请求: {url}')
return requests.get(url, **kwargs)
# 使用示例
crawler = PoliteCrawler(min_delay=1.5, max_delay=3.0)
for page in range(1, 6):
resp = crawler.get(f'https://example.com/articles?page={page}')
# 处理响应...
8.3 设置合理的 User-Agent
总是使用真实的浏览器 User-Agent,不要暴露自己是用 Python 请求库:
# ❌ 不好的做法
headers = {'User-Agent': 'python-requests/2.31.0'}
# ✅ 好的做法
headers = {
'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'
}
也可以准备一个 User-Agent 池,随机切换:
import random
USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ...',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ...',
]
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',
}
8.4 错误重试与异常处理
网络不稳定,爬虫要有容错机制:
import time
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_resilient_session(retries=3, backoff_factor=0.5):
"""创建一个带有重试机制的 Session"""
session = requests.Session()
retry_strategy = Retry(
total=retries,
read=retries,
connect=retries,
backoff_factor=backoff_factor,
status_forcelist=[500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount('http://', adapter)
session.mount('https://', adapter)
return session
# 使用
session = create_resilient_session()
try:
resp = session.get('https://example.com', timeout=10)
resp.raise_for_status()
except requests.RequestException as e:
print(f'多次重试后仍然失败: {e}')
8.5 总结:爬虫礼仪清单
| 实践 | 说明 | 优先级 |
|------|------|--------|
| ✅ 检查 robots.txt | 遵守网站的爬取规则 | 必须 |
| ✅ 控制请求频率 | 设置合理的延迟 | 必须 |
| ✅ 设置 User-Agent | 伪装成真实浏览器 | 必须 |
| ✅ 错误重试 | 网络波动时自动重试 | 建议 |
| ✅ 使用 Session | 保持 Cookie 和连接复用 | 建议 |
| ✅ 数据去重 | 避免重复爬取相同内容 | 建议 |
| ✅ 日志记录 | 记录请求和错误信息 | 建议 |
| ❌ 暴力爬取 | 大量并发请求压垮服务器 | 禁止 |
| ❌ 爬取敏感数据 | 个人信息、版权内容 | 禁止 |
结语
至此,我们从 HTTP 协议基础开始,一路走到了 Python 爬虫的工具链、页面解析技术、动态页面处理和数据存储,最后还写了一个完整的爬虫示例,并讨论了爬虫礼仪。
回顾一下核心要点:
1. 爬虫的本质就是发送 HTTP 请求 + 解析页面内容
2. requests + BeautifulSoup/lxml 是入门阶段的最佳组合
3. 先找 API,不行再用浏览器自动化
4. 控制频率、遵守规则是爬虫能长期运行的基础
5. 数据存储根据场景选择 CSV/JSON/SQLite/MySQL
爬虫是一个非常实用的技能,但力量越大责任越大。希望大家在学习和使用爬虫技术时,始终记得尊重数据、尊重网站、遵守法律。
如果这篇文章对你有帮助,欢迎收藏和分享。有任何问题也欢迎在评论区交流讨论!
*本文发布于 [博客名称] · 转载请注明出处*
评论