本篇将从零开始,介绍爬虫最核心的基础知识。学习如何像一个真正的爬虫工程师一样思考,并熟练运用数据请求、解析和存储的“三板斧”。
如何分析目标网站
在编写任何代码之前,最重要的一步是分析目标网站。爬虫工程师的“听诊器”就是浏览器的开发者工具(通常按 F12
或 Ctrl+Shift+I
打开)。
区分静态页面与动态页面
-
静态页面:你看到的页面内容,在 HTML 源码(右键 -> 查看网页源码)里都能找到。这种页面最简单,数据一次性返回,适合直接爬取。
-
动态页面 (AJAX):页面初次加载时可能只是一个框架,具体数据是通过 JavaScript 在后台异步请求并填充到页面上的。
应对策略:在开发者工具的 Network (网络) 标签页中,筛选
Fetch/XHR
请求。这些通常就是承载着核心数据的 API 接口。找到这些接口后,我们可以直接模拟请求这些接口,获取干净的 JSON 数据,这远比处理复杂的 HTML 更高效!
开发者工具的核心用途
- Elements (元素):查看和分析页面最终渲染出的 HTML 结构。你可以在这里测试你的 CSS 选择器或 XPath 路径是否正确。
- Network (网络):抓取所有网络请求。这是分析动态网页、查找数据接口、查看请求头和 Cookies 的关键所在。
核心思维:爬虫的目标是获取数据,而不是模仿浏览器。能直接请求数据接口,就绝不费力去解析 HTML。
数据请求与requests库
requests
是一个优雅、简洁的 HTTP 客户端库。它被誉为 “HTTP for Humans”,极大地简化了发送 HTTP 请求的过程。
requests vs urllib
urllib
是 Python 的内置库,功能强大但 API 相对繁琐。requests
作为一个第三方库,在易用性上完胜。
特性 | requests |
urllib |
---|---|---|
GET 请求 | requests.get(url, params=payload) |
urllib.request.urlopen(url + '?' + urlencode(payload)) |
POST 请求 | requests.post(url, data=payload) |
data = urlencode(payload).encode('utf-8'); req = Request(url, data=data) |
JSON 处理 | 响应体自带 .json() 方法 |
json.loads(response.read().decode('utf-8')) |
Cookies | 自动处理,通过 session 对象轻松管理 |
需要手动管理 http.cookiejar.CookieJar |
API 设计 | 直观、人性化、链式调用 | 功能分散在多个子模块,API 复杂 |
在新项目中,无条件推荐使用 requests
。了解 urllib
主要是为了能看懂一些旧代码或标准库的源码。
requests核心用法
安装: pip install requests
GET 请求
import requests
# 基本 GET 请求
response = requests.get('https://api.github.com/events')
print(f"状态码: {response.status_code}")
# 带参数的 GET 请求
payload = {'key1': 'value1', 'key2': 'value2'}
response = requests.get('https://httpbin.org/get', params=payload)
print(response.url) # 查看最终请求的 URL
POST 请求
# 发送表单数据
payload = {'key1': 'value1', 'key2': 'value2'}
response = requests.post('https://httpbin.org/post', data=payload)
# 发送 JSON 数据
payload_json = {'some': 'data'}
response = requests.post('https://httpbin.org/post', json=payload_json)
自定义请求头 (Headers)
模拟浏览器是爬虫的基本功。最常见的是设置 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('https://httpbin.org/headers', headers=headers)
print(response.json())
处理响应
response = requests.get('https://api.github.com/events')
# 获取文本内容(requests 会自动解码)
print(response.text)
# 获取二进制内容(如图片、文件)
# print(response.content)
# 如果响应是 JSON,直接解析为 Python 字典
print(response.json())
使用 Session
维持会话
如果你需要向同一个网站发送多个请求(如模拟登录后的操作),使用 Session
对象会自动为你管理 Cookies。
session = requests.Session()
# 首次请求,网站可能会设置 cookie
session.get('https://httpbin.org/cookies/set/sessioncookie/123456789')
# 再次请求,会自动带上之前的 cookie
response = session.get('https://httpbin.org/cookies')
print(response.json()) # {'cookies': {'sessioncookie': '123456789'}}
其他常用功能
- 代理:
proxies = {'http': 'http://10.10.1.10:3128', 'https': 'http://10.10.1.10:1080'}
requests.get(url, proxies=proxies)
- 超时:
requests.get(url, timeout=5)
# 设置超时时间为5秒 - 异常处理: 使用
try...except requests.exceptions.RequestException
来捕获所有可能的请求异常。
数据解析及相关库
获取到 HTML 源码后,下一步就是从中提取出我们需要的数据。两大神器是 BeautifulSoup
和 lxml
。
BeautifulSoup
vs. lxml+XPath
如何选择?
- 新手入门/快速开发:
BeautifulSoup
+ CSS 选择器,API 友好,代码可读性高。 - 追求极致性能/复杂解析:
lxml
+ XPath,速度更快,XPath 在处理复杂的父子/兄弟节点关系时更灵活。 - 最佳实践:掌握两者。简单场景用 BS4,复杂场景或性能瓶颈时换用
lxml
。
BeautifulSoup4
BeautifulSoup
(BS4) 的强项在于其简洁、符合 Python 哲学的 API,非常适合新手。它能容忍不规范的 HTML 代码。
安装: pip install beautifulsoup4 lxml
核心用法:
from bs4 import BeautifulSoup
html_doc = """
<html><head><title>一个简单的故事</title></head>
<body>
<p class="story">从前有三个小姐妹,她们的名字是
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 和
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
她们住在一个小池塘的底部。</p>
<p class="story">...</p>
</body>
</html>
"""
# 创建 BeautifulSoup 对象
soup = BeautifulSoup(html_doc, 'lxml')
# 1. 按标签名查找
print(soup.title) # <title>一个简单的故事</title>
print(soup.title.string) # 一个简单的故事
# 2. find(): 查找第一个符合条件的标签
link1 = soup.find('a')
print(link1) # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
# 3. find_all(): 查找所有符合条件的标签,返回一个列表
all_links = soup.find_all('a')
for link in all_links:
print(link.get('href')) # 获取属性值
# 4. 按 CSS 类名查找
story_p = soup.find('p', class_='story')
print(story_p.get_text()) # 获取标签内的所有文本
# 5. 使用 CSS 选择器 (最推荐的方式!)
# CSS 选择器语法简洁强大,前端开发者非常熟悉。
# 查找 id 为 link2 的标签
link2_by_css = soup.select_one('#link2')
print(link2_by_css)
# 查找所有 class 为 sister 的 a 标签
sisters_by_css = soup.select('a.sister')
for sister in sisters_by_css:
print(f"姓名: {sister.string}, 链接: {sister['href']}")
lxml与XPath
lxml
是一个基于 C 语言的高性能库。它原生支持 XPath,这是一种用于在 XML/HTML 文档中定位节点的语言,功能比 CSS 选择器更强大。
XPath 常用语法
/
:从根节点开始选择。//
:从当前节点开始,选择任意位置的后代节点。.
:选取当前节点。..
:选取当前节点的父节点。@
:选取属性。*
:通配符,匹配任何元素节点。text()
:获取节点的文本内容。[ ]
:谓语,用于筛选,如[1]
,[@class="story"]
。
核心用法:
from lxml import etree
html_doc = """
<html><head><title>一个简单的故事</title></head>
<body>
<p class="story">从前有三个小姐妹,她们的名字是
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> 和
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
她们住在一个小池塘的底部。</p>
<p class="story">...</p>
</body>
</html>
"""
# 注意:lxml 需要的是字节串,或者让它自己处理编码
# 使用 requests 获取的 response.content 是字节串,可以直接传入
# 这里我们用字符串来模拟
html_tree = etree.HTML(html_doc)
# 1. 查找所有 a 标签的 href 属性
hrefs = html_tree.xpath('//a/@href')
print(hrefs) # ['http://example.com/elsie', 'http://example.com/lacie', ...]
# 2. 查找所有 class 为 sister 的 a 标签的文本
names = html_tree.xpath('//a[@class="sister"]/text()')
print(names) # ['Elsie', 'Lacie', 'Tillie']
# 3. 查找 id 为 link3 的 a 标签
link3 = html_tree.xpath('//a[@id="link3"]')[0] # xpath 返回列表,需要取第一个
print(etree.tostring(link3, encoding='unicode')) # 打印标签
数据存储(文件或数据库)
最简单的数据持久化方式是存入文件。当数据量增大或需要复杂查询时,将数据存入数据库是更好的选择。
存储为文件
存储为 CSV 文件
CSV (Comma-Separated Values) 文件非常适合存储表格型数据,可以用 Excel 打开。
import csv
data_to_save = [
{'name': 'Elsie', 'link': 'http://example.com/elsie'},
{'name': 'Lacie', 'link': 'http://example.com/lacie'},
{'name': 'Tillie', 'link': 'http://example.com/tillie'}
]
# newline='' 是为了防止写入时出现空行
with open('sisters.csv', 'w', newline='', encoding='utf-8-sig') as f:
# utf-8-sig 可以让 Excel 正确识别中文
fieldnames = ['name', 'link']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader() # 写入表头
writer.writerows(data_to_save) # 写入多行数据
print("数据已保存到 sisters.csv")
存储为 JSON 文件
JSON (JavaScript Object Notation) 格式非常适合存储嵌套的、非结构化的数据,且易于程序读取。
import json
data_to_save = [
{'name': 'Elsie', 'link': 'http://example.com/elsie'},
{'name': 'Lacie', 'link': 'http://example.com/lacie'},
{'name': 'Tillie', 'link': 'http://example.com/tillie'}
]
# ensure_ascii=False 保证中文字符能正常显示
# indent=4 让 JSON 文件格式化,易于阅读
with open('sisters.json', 'w', encoding='utf-8') as f:
json.dump(data_to_save, f, ensure_ascii=False, indent=4)
print("数据已保存到 sisters.json")
存储到数据库
存储到 MySQL
安装驱动: pip install mysql-connector-python
import mysql.connector
# 数据库连接配置
db_config = {
'host': 'localhost',
'user': 'your_username',
'password': 'your_password',
'database': 'crawler_db'
}
data_to_save = [{'name': 'Elsie', 'link': 'http://example.com/elsie'}, {'name': 'Lacie', 'link': 'http://example.com/lacie'}]
try:
conn = mysql.connector.connect(**db_config)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS sisters (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
link VARCHAR(255)
)
''')
sql = "INSERT INTO sisters (name, link) VALUES (%s, %s)"
for item in data_to_save:
cursor.execute(sql, (item['name'], item['link']))
conn.commit()
print("数据已保存到 MySQL")
except mysql.connector.Error as err:
print(f"MySQL 错误: {err}")
finally:
if 'conn' in locals() and conn.is_connected():
cursor.close()
conn.close()
存储到 PostgreSQL
安装驱动: pip install psycopg2-binary
import psycopg2
db_config = {
'host': 'localhost',
'dbname': 'crawler_db',
'user': 'your_username',
'password': 'your_password'
}
data_to_save = [{'name': 'Elsie', 'link': 'http://example.com/elsie'}, {'name': 'Lacie', 'link': 'http://example.com/lacie'}]
try:
conn = psycopg2.connect(**db_config)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS sisters (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
link VARCHAR(255)
)
''')
sql = "INSERT INTO sisters (name, link) VALUES (%s, %s)"
for item in data_to_save:
cursor.execute(sql, (item['name'], item['link']))
conn.commit()
print("数据已保存到 PostgreSQL")
except psycopg2.Error as err:
print(f"PostgreSQL 错误: {err}")
finally:
if 'conn' in locals():
cursor.close()
conn.close()
存储到 MongoDB
安装驱动: pip install pymongo
from pymongo import MongoClient
data_to_save = [{'name': 'Elsie', 'link': 'http://example.com/elsie'}, {'name': 'Lacie', 'link': 'http://example.com/lacie'}]
client = MongoClient('mongodb://localhost:27017/')
db = client['crawler_db']
collection = db['sisters']
if data_to_save:
try:
collection.insert_many(data_to_save)
print("数据已保存到 MongoDB")
except Exception as e:
print(f"MongoDB 存储失败: {e}")
client.close()
实战:豆瓣电影 Top 250
目标:获取电影的排名、名称、链接和评分。
import requests
from bs4 import BeautifulSoup
import csv
import time
def fetch_top250_movies():
"""爬取豆瓣电影Top 250的所有页面,并返回电影信息列表"""
base_url = "https://movie.douban.com/top250"
all_movies = []
# 豆瓣Top250共10页
for i in range(0, 250, 25):
params = {'start': i, 'filter': ''}
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:
print(f"正在爬取第 {i//25 + 1} 页...")
response = requests.get(base_url, params=params, headers=headers)
response.raise_for_status() # 如果请求失败,抛出异常
time.sleep(1) # 增加延时,做一个有礼貌的爬虫
soup = BeautifulSoup(response.text, 'lxml')
# 使用 CSS 选择器定位到每个电影的 li 标签
movie_items = soup.select('div.article ol.grid_view li')
for item in movie_items:
rank = item.select_one('div.pic em').get_text()
title = item.select_one('div.info div.hd a span.title').get_text()
link = item.select_one('div.info div.hd a')['href']
rating = item.select_one('div.info div.bd div.star span.rating_num').get_text()
movie_data = {
'rank': rank,
'title': title,
'link': link,
'rating': rating
}
all_movies.append(movie_data)
except requests.RequestException as e:
print(f"请求失败: {e}")
break # 如果一页失败,则停止
return all_movies
def save_to_csv(movies, filename='douban_top250.csv'):
"""将电影数据保存到 CSV 文件"""
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
fieldnames = ['rank', 'title', 'link', 'rating']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
if movies:
writer.writerows(movies)
print(f"数据已成功保存到 {filename}")
if __name__ == "__main__":
movies_list = fetch_top250_movies()
if movies_list:
save_to_csv(movies_list)
运行这个脚本,将在同目录下得到一个 douban_top250.csv
文件,里面包含了所有电影信息!