此博客为一个详细的Python爬虫教程,从基础知识到完整实现,包括爬取网页内容、解析数据、存储数据、使用代理、反反爬策略等。稍后会提供完整的教程供你参考。
1. 爬虫基础
什么是爬虫: 网络爬虫(Web Crawler),又称网络蜘蛛(Spider),是一种自动化脚本或程序,用于按照一定规则批量获取网页数据。爬虫通过模拟浏览器行为向目标网站发送HTTP请求,获取网页的HTML源码,然后解析并提取所需的信息。简单来说,爬虫就是让计算机替代人工,自动从互联网上“爬”取数据。
基本概念: 爬虫运行时通常遵循以下流程:
- 发送请求: 爬虫指定一个URL,向网站服务器发送HTTP请求(如GET请求)。就像我们在浏览器中访问一个网址一样,服务器会返回该页面的内容。
- 获取响应: 服务器返回网页的HTML内容(以及可能的JSON、图片等资源)。爬虫程序接收响应数据,如果请求成功会得到状态码200以及页面内容。
- 解析内容: 爬虫从获取的HTML中提取目标数据。这需要对网页的结构有所了解,通过解析HTML来定位我们需要的信息(比如文章标题、价格、图片链接等)。
- 保存结果: 将提取的数据进行处理和存储,例如输出到屏幕、保存到文件或数据库,供后续分析和使用。
值得注意的是,网络爬虫可以小到针对单个网站的数据采集,也可以大到像搜索引擎那样抓取整个互联网的页面并建立索引。根据需求不同,爬虫可以是一次性的小脚本,也可以是持续运行的大规模系统。
合法性与道德问题: 在编写和运行爬虫时,需要注意法律法规和道德规范:
- 遵守网站规则: 大多数网站都会在根目录提供一个
robots.txt
协议文件,里面声明了爬虫可以或不可以抓取的范围。爬虫应遵循该协议,不要爬取被禁止的页面,以体现对网站意愿的尊重(虽然robots.txt
不是强制性的,但遵守它是业界的君子协议)。 - 尊重使用条款: 仔细阅读目标网站的服务条款/使用政策。有些网站明确禁止未经授权的抓取行为,违反这些规定可能带来法律风险。在动手爬取之前,确保爬虫行为不违背目标站点的政策要求。
- 避免扰乱服务: 控制爬虫的抓取频率和并发量,避免对目标服务器造成过大压力。过于频繁的请求可能被视作拒绝服务攻击(DDoS)行为,这既不道德也可能触发目标站的防御机制,导致爬虫被封禁。
- 尊重隐私与版权: 不要抓取涉及个人隐私的数据(如个人邮箱、电话号码、住所等)或受版权保护的内容。在不少国家和地区,未授权获取他人受保护的数据可能违法。确保爬取的数据是公开信息,且用于合法用途。必要时,可以征得网站或数据所有者的授权。
- 安全与责任: 当遇到需要登录、验证码(CAPTCHA)等明显的反爬措施时,要格外谨慎。这些通常表明网站希望保护其内容不被自动抓取。强行绕过这些机制可能违法,甚至触及刑事风险。此外,爬取到的数据要妥善保存和使用,防止泄露或滥用。
总之,负责任地爬取是每个开发者应遵循的准则。在获取数据的同时,确保自己的爬虫行为合法合规、不过度消耗公共资源,并尊重目标网站和用户的权益。
2. 环境准备
在开始编写爬虫之前,需要准备好Python编程环境并安装相关的依赖库。以下是在不同操作系统上设置Python爬虫环境的一般步骤:
- 安装 Python: 如果尚未安装Python,请从Python官网下载适用于你操作系统的版本进行安装。建议使用Python 3.x版本。安装过程中记得将Python添加到环境变量(Windows系统)或确保在终端可以调用
python3
命令(macOS/Linux系统)。 - 创建虚拟环境: 为了避免依赖混杂,建议为爬虫项目创建一个Python虚拟环境。可以使用内置的venv模块:
激活虚拟环境后,命令行提示符通常会出现环境名称前缀,表示接下来安装的库都会只针对该环境。python3 -m venv venv # 创建名为venv的虚拟环境文件夹 source venv/bin/activate # 在Linux/macOS上激活虚拟环境 .\venv\Scripts\activate # 在Windows上激活虚拟环境
- 升级pip工具: 确保使用最新的pip包管理器,以便顺利安装依赖:
python -m pip install --upgrade pip
- 安装必备库: 爬虫常用的Python库包括:
requests
:用于发送HTTP请求,获取网页内容。beautifulsoup4
(即bs4
):用于方便地解析HTML/XML数据。lxml
:高性能的XML和HTML解析库,支持XPath查询。Scrapy
:功能强大的爬虫框架,适合构建大型爬虫项目。selenium
:浏览器自动化工具,能驱动浏览器执行JS,用于处理需要动态渲染的网站。
(如果不需要用到某些高级功能,比如本教程后续提到的Scrapy框架或Selenium模拟浏览器,可以暂时不安装它们。)pip install requests beautifulsoup4 lxml scrapy selenium
- 验证安装: 可以通过Python交互环境或脚本导入以上库来验证是否安装成功:
如果没有报错说明环境准备就绪。如果遇到某个库导入错误,可能是安装失败或者未加入虚拟环境,需检查并重新安装。import requests, bs4, lxml, scrapy, selenium print("All libraries installed successfully!")
- 开发工具选择: 你可以使用任意文本编辑器或IDE来编写爬虫代码,例如:VS Code、PyCharm、Jupyter Notebook等。IDE通常提供更好的调试和自动补全,有助于提高开发效率。
- 其他依赖: 根据具体需求,可能还需安装数据库驱动(如
pymysql
用于连接MySQL)或其他解析库(如regex
正则库,Python自带re
模块不需安装)。在动手写代码前,先根据规划列出所有需要的依赖库并安装好。
完成以上步骤后,你就拥有了一个干净的Python爬虫开发环境。接下来可以开始编写和运行爬虫代码了。
3. 基础爬取
在了解了爬虫原理并准备好环境后,我们可以编写第一个简单的爬虫。基础爬取通常涉及两个部分:获取网页内容和解析网页内容。
3.1 使用Requests获取网页
Python的requests
库使HTTP请求变得非常简单。以下是使用requests获取网页HTML内容的基本示例:
import requests
url = "http://example.com" # 目标网页的URL
headers = {"User-Agent": "Mozilla/5.0"} # 设置User-Agent头,伪装成浏览器
response = requests.get(url, headers=headers)
print(response.status_code) # 输出HTTP状态码,200表示成功
print(response.text[:500]) # 输出返回内容的前500个字符
解释: 上述代码向example.com
发送一个GET请求,并打印了响应的状态码和部分正文。我们构造了一个简单的headers
字典来设置User-Agent
,这是为了防止某些网站拒绝响应默认的爬虫请求。response.text
属性包含了Unicode解码后的文本内容(HTML源码),而response.content
则包含原始的字节流。如果你需要处理图片、PDF等二进制内容,可以使用response.content
。
在实际编写爬虫时,发送请求需要考虑:
- 错误处理: 如果
status_code
不是200,可能需要处理重定向(3xx)或访问错误(4xx/5xx)。可以使用response.status_code
判断,或使用requests
的异常处理,例如捕获requests.exceptions.RequestException
。 - 超时和重试: 使用
requests.get(url, timeout=5)
设置超时,避免请求卡住。此外可以使用requests
的重试机制或第三方库,提高健壮性。 - Session维持: 使用
requests.Session()
保持会话,cookies会自动保存和发送,这对需要登录的网站非常有用。
3.2 解析HTML内容
获取到网页HTML后,我们需要从中提取有用的数据。通常有两种常用方法:基于DOM解析(如BeautifulSoup、lxml)和正则表达式。先介绍DOM解析法:
使用BeautifulSoup解析:
from bs4 import BeautifulSoup
html = response.text # 假设这是我们获取的HTML字符串
soup = BeautifulSoup(html, 'lxml') # 用lxml解析器,也可以使用默认的html.parser
title = soup.find('title').get_text() # 找到<title>标签并获取文本
all_links = [a['href'] for a in soup.find_all('a', href=True)] # 获取页面中所有链接URL
print("页面标题:", title)
print("链接数:", len(all_links))
在这段代码中,我们:
- 使用
BeautifulSoup(html, 'lxml')
创建了一个BeautifulSoup对象,这会将HTML字符串解析成一个DOM树结构,方便我们搜索元素。这里指定使用lxml
解析器会更快;如果未安装lxml,也可以用内置的html.parser
。 soup.find('title')
返回第一个匹配<title>
标签的Tag对象,我们接着用.get_text()
获取其中的文本内容。soup.find_all('a', href=True)
则找到所有带href
属性的<a>
链接标签,并用列表推导式提取每个链接的URL。
使用CSS选择器: BeautifulSoup还支持类似jQuery的CSS选择器语法,通过select()
方法:
# 使用 CSS 选择器提取数据
headings = soup.select("h2.article-title") # 例如:提取class为article-title的<h2>元素
for h in headings:
print(h.get_text())
如果熟悉网页前端开发,这种方式会非常直观,比如选择器div.content > ul li
可以获取特定层级的元素。
使用lxml解析和XPath: 有时我们也会直接使用lxml
的etree模块:
from lxml import etree
parser = etree.HTMLParser()
tree = etree.fromstring(html, parser)
titles = tree.xpath("//title/text()") # 用XPath获取<title>文本
links = tree.xpath("//a/@href") # 获取所有<a>的href属性值
print("页面标题:", titles[0] if titles else "")
print("链接数:", len(links))
这里etree.fromstring
结合HTMLParser
将HTML解析成可查询的树,.xpath()
方法返回符合XPath表达式的所有结果列表。//title/text()
表示获取所有<title>
标签下的文本节点,//a/@href
则获取所有<a>
标签的href属性。XPath在复杂嵌套的HTML提取中非常强大,我们会在下一节详细介绍。
解析结果验证: 无论使用哪种解析方法,拿到数据后最好打印或检查一下是否符合预期。如果提取不到数据,可能是选择器写得不对,或者目标网页是通过JavaScript动态加载数据(这需要特殊处理,后面会提到模拟浏览器)。
通过Requests获取HTML并用BeautifulSoup解析,是大部分Python爬虫的基础流程。在掌握这点后,我们可以进一步学习不同的数据提取技巧和处理更复杂的页面。
4. 数据提取
在上一节中,我们通过DOM方法提取了HTML中的信息。本节将更系统地介绍数据提取的几种技术,包括正则表达式、XPath以及处理JSON格式的数据。选择合适的提取方式取决于网页的结构和数据呈现形式:
4.1 正则表达式解析
正则表达式(Regular Expression)是一种强大的字符串匹配工具,对于提取特定格式的文本非常有效。Python内置re
模块支持正则操作。
使用正则提取示例: 假设我们想从网页文本中提取所有Email地址,可以使用正则:
import re
text = "联系我:email@example.com 或 admin@test.org"
pattern = r'[A-Za-z0-9\._+-]+@[A-Za-z0-9\.-]+\.[A-Za-z]{2,}' # 匹配Email模式
emails = re.findall(pattern, text)
print(emails) # 输出: ['email@example.com', 'admin@test.org']
在这个示例中:
pattern
是Email的正则规则:匹配用户名部分(允许字母数字和._+-),@符号,域名部分,和后面的顶级域(2位以上字母)。re.findall()
会返回所有匹配的字符串列表。如果只想找到第一个匹配,可以用re.search()
,如果要替换则用re.sub()
。- 正则在处理高度自由的纯文本时很有用,比如从网页中提取所有符合某模式的字符串(电话、邮编等)。
注意: 尽管正则表达式功能强大,但用于解析HTML时需要谨慎。因为HTML是层级结构化的数据,用正则匹配嵌套的标签容易写出复杂难维护的模式,而且稍微变化的结构可能导致匹配失败。一般来说:
- 对于结构明确、简单的任务,可以使用正则快速提取(例如从一段HTML中抓取所有图片URL:
<img src="(.*?)">
)。 - 更多情况下,**优先使用解析器(BeautifulSoup、lxml)**来处理HTML,把正则留给纯文本提取或辅助清理数据。
4.2 XPath 解析
XPath是一种在XML/HTML文档中定位节点的语言,适合结构清晰、嵌套层次较深的页面解析。使用XPath可以用简洁的表达式获取节点集,前面已经简单示例过。下面更详细的例子:
from lxml import etree
html = """<div class="article"><h1>标题</h1><p>内容段落1</p><p>内容段落2</p></div>"""
tree = etree.HTML(html) # 直接解析成HTML树
# 提取h1文本
title = tree.xpath("//div[@class='article']/h1/text()")
# 提取p标签文本
paragraphs = tree.xpath("//div[@class='article']/p/text()")
print("标题:", title[0] if title else "")
print("段落:", paragraphs) # 列出所有段落文本
在这个例子中:
"//div[@class='article']/h1/text()"
表示找到class="article"
的,在其中直接子节点里选,取其文本。"//div[@class='article']/p/text()"
则获取内所有的文本,返回列表。- 我们用
etree.HTML()
快捷地把字符串转换为可查询对象,也可以用etree.fromstring
类似效果。
更多XPath语法:
//tag
:选取所有tag,不考虑位置。/tag1/tag2
:选取tag1下的直接子节点tag2。@attr
:选取属性值,例如//img/@src
拿到所有<img>
的src。contains(@attr, 'value')
:用于匹配属性中包含特定值的元素,如//div[contains(@class, 'article')]
匹配class属性包含'article'的。text()
:获取元素文本;node()
可以获取文本或子元素。[index]
:选取特定序号的元素(1表示第一个),如(//p)[1]
表示文档中第一个。
XPath相比BeautifulSoup的主要优势在于定位精准和支持更复杂的条件查询。Scrapy框架的Selector就支持XPath和CSS选择器,让我们灵活选用。但初学者可能需要一些时间熟悉XPath的语法。
4.3 JSON 数据解析
现代网站往往会通过后端接口返回JSON格式的数据,或在网页中嵌入JSON。例如,有些网页通过Ajax请求获取数据,直接返回JSON而非HTML。这种情况下,我们可以直接请求该API接口,然后解析JSON。
请求返回JSON示例:
import requests, json
api_url = "https://api.github.com/repos/python/cpython"
res = requests.get(api_url)
data = res.json() # 直接将返回内容解析为JSON(字典/列表)
print(f"Repository: {data['name']}, Stars: {data['stargazers_count']}")
在这个例子里,我们调用GitHub的API获取Python语言的仓库信息,res.json()
快捷地把返回内容转成Python字典,然后我们就能按键访问各字段。如果不使用res.json()
,也可以用json.loads(res.text)
达到同样效果。
解析嵌入的JSON: 有时网页的HTML中包含一段JSON,例如:
<script id="data">{"name": "Alice", "age": 30}</script>
我们可以在爬虫中先用BeautifulSoup找到这个标签的内容,再用json.loads()
解析:
script_tag = soup.find("script", {"id": "data"})
if script_tag:
data_text = script_tag.get_text()
data = json.loads(data_text)
print(data["name"], data["age"])
这样便能提取页面中嵌入的数据结构。
处理JSON列表/字典: 一旦拿到Python中的列表或字典(通过json.loads
或res.json()
),后续处理就跟普通数据结构一样了。例如可以遍历列表、按键取值、甚至组合使用pandas来分析。关键是确保成功抓取到正确的JSON文本并转换。
总结: 数据提取手段很多:
- 简单场景下,正则可以快速拉取需要的字符串。
- 结构化HTML,用BeautifulSoup或lxml进行DOM解析更稳健。
- 动态数据接口,直接请求JSON再解析最高效。
- 根据任务需要,有时这些方法可以混合使用,比如先用DOM解析定位大块内容,再用正则清理细节,或者获取JSON后仍需一些字符串处理。
熟练掌握多种解析方法,可以应对不同的网站结构和数据格式,这是一个合格爬虫工程师的必备技能。
5. 存储数据
将爬取到的数据进行妥善保存是爬虫工作的最后一步。根据用途和数据量的不同,我们可以选择多种存储方式,包括保存为文本文件(CSV/JSON)、关系型数据库、NoSQL数据库等。本节重点介绍几种常用的存储方案及代码示例。
5.1 保存到 CSV 文件
CSV(Comma-Separated Values)是一种简单的文本格式,常用于保存表格数据,每一行是一条记录,各字段以逗号分隔。Python内置csv
模块可方便地写入CSV。
示例:将爬取的数据列表保存为CSV:
import csv
# 假设我们爬取到的数据结构如下 (列表里面每个元素是一个包含数据的字典)
items = [
{"title": "示例文章1", "author": "张三", "date": "2025-03-01"},
{"title": "示例文章2", "author": "李四", "date": "2025-03-02"},
]
with open("articles.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
# 写入表头
writer.writerow(["标题", "作者", "日期"])
# 写入每一行数据
for item in items:
writer.writerow([item["title"], item["author"], item["date"]])
print("CSV文件保存完成")
在上面的代码中:
- 使用
open()
打开(或创建)一个名为articles.csv
的文件,模式为写入"w"
。newline=""
参数避免不同操作系统换行符差异导致的空行问题,encoding="utf-8"
确保中文写入不乱码。 - 用
csv.writer(f)
创建写入器,然后先写入表头行,再逐条写入数据行。 - 每条记录以列表形式提供给
writer.writerow()
,顺序要和表头对应。
这样生成的CSV文件可以用Excel、Google表格等软件打开查看。对于结构规整、以行为单位的数据,CSV是一种直观并通用的存储形式。
若我们的数据本身就是字典列表,也可以使用csv.DictWriter
按字典键写入,更方便。但无论哪种方式,都需要提前规划好字段顺序和名称。
5.2 保存为 JSON 文件
JSON(JavaScript Object Notation)适合存储结构化且层次分明的数据,例如列表、字典的嵌套。Python的json
模块可以将对象轻松序列化为JSON字符串。
示例:将数据保存为本地JSON文件:
import json
data = {
"timestamp": "2025-03-04 22:00:00",
"articles": items # 复用上面定义的 items 列表
}
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
print("JSON文件保存完成")
这里我们创建了一个包含爬取时间戳和文章列表的字典data
:
json.dump(obj, f)
直接将对象序列化并写入文件。ensure_ascii=False
参数确保非ASCII字符(如中文)以原始形式输出,而不是\u4e00\u4e8c
这样的Unicode转义。indent=4
让输出有缩进,便于阅读。若追求文件小,可以省略这个参数以紧凑格式写出。
JSON文件在程序之间传递数据非常方便,而且保留了丰富的结构信息。但对非技术人员来说,可读性不如CSV直观,需要用专门的软件查看。
5.3 存储到数据库
当数据量较大,或需要支持复杂查询、长期保存时,将数据存入数据库是更稳健的方案。常用的关系型数据库有SQLite、MySQL/PostgreSQL等,NoSQL有MongoDB等。这里以SQLite和MySQL为例,介绍基本使用。
使用 SQLite 数据库:
SQLite是一种轻量级的嵌入式关系数据库,Python内置sqlite3
模块,无需额外安装,非常适合小型爬虫项目。SQLite将数据存储在一个.db
文件中。
import sqlite3
# 连接SQLite数据库(如果文件不存在会自动创建)
conn = sqlite3.connect("data.db")
cursor = conn.cursor()
# 创建表(如果已存在则跳过)
cursor.execute("""
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
author TEXT,
date TEXT
)
""")
# 插入数据
for item in items:
cursor.execute(
"INSERT INTO articles (title, author, date) VALUES (?, ?, ?)",
(item["title"], item["author"], item["date"])
)
conn.commit() # 提交事务,确保写入
conn.close() # 关闭连接
print("数据已插入SQLite数据库")
在这个示例中,我们:
- 用
sqlite3.connect()
建立数据库连接,如果指定的数据库文件不存在,SQLite会创建一个同名文件。 - 创建一个
articles
表,包含id(主键)、标题、作者、日期字段。使用IF NOT EXISTS
确保脚本重复运行时不会重复创建表。 - 遍历数据列表,用
cursor.execute
执行插入SQL语句,?
是占位符,后面的tuple提供实际的值。这种参数化可以防止SQL注入问题。 - 最后
commit
提交事务,并关闭连接。生成的data.db
文件可以用SQLite管理工具查看,或者通过代码查询。
使用 MySQL 数据库:
MySQL等客户端/服务器式数据库适合更大规模的数据和并发访问。Python可以使用pymysql
或MySQLdb
驱动连接MySQL。以下是简要的流程(不执行实际连接):
import pymysql
# 建立MySQL连接 (请替换实际的主机、用户名、密码、数据库名)
conn = pymysql.connect(host="localhost", user="root", password="123456", database="mydb", charset="utf8mb4")
cursor = conn.cursor()
# 创建表
cursor.execute("""
CREATE TABLE IF NOT EXISTS articles (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255),
author VARCHAR(100),
date DATE
)
""")
# 插入数据
for item in items:
cursor.execute(
"INSERT INTO articles (title, author, date) VALUES (%s, %s, %s)",
(item["title"], item["author"], item["date"])
)
conn.commit()
conn.close()
这里占位符使用%s
,其他逻辑与SQLite类似。使用MySQL时需要提前安装数据库服务器、创建数据库,并提供正确的连接参数。字段类型也相对严格,需要根据数据设计(上例把日期存为DATE类型)。
其他存储:
- 对于文档型数据,MongoDB是常用的NoSQL数据库,可以用
pymongo
库进行插入和查询。 - 如果数据需要进一步分析,Pandas库可以将数据存为DataFrame,再直接输出为Excel、或利用其IO接口保存为SQL表等。
- 在Scrapy框架中,提供了Item Pipeline,可以方便地对接数据库或保存文件,只需定义好Pipeline类即可自动处理爬取项的保存。
选择存储方式取决于应用场景:短期的小数据量可以用CSV/JSON方便共享,长期的大数据量应该考虑数据库便于检索分析。如果只是练习,小规模数据直接打印或存CSV已经足够;但构建真实项目时,设计好数据库 Schema 将使数据更有价值。
6. 高级爬虫:使用Scrapy框架
当爬取需求变得复杂或规模较大时,手工编写requests+BS4脚本可能会变得难以维护。这时可以考虑使用 Scrapy —— 一个为爬虫开发打造的高性能框架。Scrapy提供了请求调度、解析、管道、并发等一系列机制,使我们能更快速地构建一个健壮的爬虫。下面我们概览Scrapy的核心概念和流程,并给出简单示例。
(Architecture overview — Scrapy 2.12.0 documentation) (Architecture overview — Scrapy 2.12.0 documentation)Scrapy 架构概览: 上图展示了Scrapy内部架构和数据流。Scrapy包含几个主要组件:
- Engine(引擎): 核心调度器,负责各组件间的数据流转。引擎按照一定流程推动爬虫进行,比如从Scheduler取出下一个待抓取请求、将响应交给Spider解析等。
- Scheduler(调度器): 请求队列管理器。它接受引擎发来的请求(Request)并加入队列,再在引擎需要时吐出下一个请求,实现对待爬取URL的调度管理,可视为一个优先队列(Queues)。
- Downloader(下载器): 负责执行HTTP/HTTPS请求,将请求发送到互联网,并获取网页响应(Response)。Downloader在Scrapy中由高效的异步网络框架(Twisted)实现,能够高速并发下载。
- Spider(爬虫/蜘蛛): 由用户编写的爬虫解析代码。Spider定义了要爬取的起始URL、如何解析页面从而提取数据(Item)以及新的后续请求。每个Spider通常对应一类网站的抓取逻辑。
- Item Pipeline(项目管道): 处理Spider提取出的Item数据的组件。典型管道任务包括清洗数据、去重验证、将Item保存到文件或数据库等。可以有多个Pipeline按顺序执行。
- Middlewares(中间件): 包括Downloader Middleware和Spider Middleware,是Scrapy提供的钩子,可以定制请求和响应的处理。例如Downloader Middleware可以拦截请求加上代理或修改Headers,Spider Middleware可以在Item传递前后做额外处理等。
整个Scrapy的工作流程大致如下:Engine从Spider获取初始请求 -> Scheduler安排请求 -> Downloader获取响应 -> Spider解析响应返回Item或新的请求 -> Item交给Pipeline处理,新的请求再给Scheduler,周而复始,直到没有新的请求。Scrapy利用这种架构可以非常高效地进行异步抓取,默认情况下同一时间可以并发处理多页内容,大大提高爬取速度。
6.1 Scrapy 项目结构
Scrapy鼓励分模块开发。使用命令scrapy startproject mycrawler
可以创建一个新的爬虫项目,包含:
scrapy.cfg
:项目配置文件。mycrawler/
:项目的Python模块,包含代码。spiders/
目录:放置Spider代码,每个Spider通常是一个Python类文件。items.py
:定义Item数据结构(类似ORM模型,可以定义字段)。pipelines.py
:定义Item Pipeline处理逻辑。middlewares.py
:定义中间件逻辑。settings.py
:全局配置,例如并发数、下载延迟、Pipeline启用等。
Scrapy项目可以包含多个Spider。比如你可能写一个Spider爬取新闻网站A,另一个Spider爬取电商网站B,它们共享同一套Pipeline和配置,但Spider实现不同。
6.2 编写一个Spider
Scrapy中的Spider类继承自scrapy.Spider
,需要定义几个关键属性和方法:
name
:Spider的名称,用于运行时标识。start_urls
:初始请求URL列表,或者定义start_requests()
方法来自行生成初始请求。parse
方法:默认的解析回调函数,每当有新的响应返回时,Scrapy会调用这个方法,传入response
对象。我们在此方法中编写解析逻辑,提取数据和生成新的请求。
以下是一个简单的Spider示例(爬取 Quotes 网站的示例):
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes" # 爬虫名称
start_urls = ["http://quotes.toscrape.com/"] # 初始爬取页面
def parse(self, response):
# 提取名言和作者
for quote in response.css("div.quote"):
text = quote.css("span.text::text").get() # 提取名言文本
author = quote.css("small.author::text").get() # 提取作者名字
yield { "text": text, "author": author } # 产生一个数据项(Item)
# 找到下一页链接,构造新的请求
next_page = response.css("li.next a::attr(href)").get()
if next_page is not None:
# 使用response.follow保持相对URL正确拼接,并指定解析函数
yield response.follow(next_page, callback=self.parse)
解释:
- 这个Spider名为quotes,起始页面是quotes.toscrape.com的首页。
response.css("div.quote")
使用了CSS选择器获取页面中所有类名为quote的<div>
元素(每个包含一条名言)。我们在循环中对每个quote块,用子选择器提取文本和作者,并用yield
返回一个字典。Scrapy会自动把这个字典当作Item处理。- 然后检查是否存在下一页链接(页面底部有“Next”按钮),如果存在就构造一个新的请求。
response.follow
是Scrapy提供的简便方法,它会自动处理相对路径拼接为绝对URL,并让我们指定由哪个回调方法处理响应(这里继续使用同一个parse
方法解析下一页)。 - 就这样,Scrapy会不断深度爬取这个网站的分页直到没有下一页。所有yield出的Item会进入Item Pipeline处理,或者根据设置存储为JSON/CSV等格式。
6.3 Scrapy中间件与Pipeline
Downloader Middleware(下载中间件): Scrapy允许在请求发出前、响应返回后插入自定义处理逻辑。例如我们可以编写Downloader Middleware来随机更换User-Agent、设置代理IP、或处理重定向等。Middleware本质上是一些钩子函数,有点像洋葱圈模型,Request出去和Response回来都会经过中间件链。启用中间件需要在settings.py
中配置。
Spider Middleware: 主要作用于Spider处理环节,可以在Spider解析前后做处理,使用相对较少,一般默认配置足矣。
Item Pipeline: 前面提到,Pipeline负责处理Item的数据结果。比如我们想把上面QuotesSpider抓取到的名言保存到数据库,就可以在pipelines.py
中创建一个类:
class SaveQuotesPipeline:
def open_spider(self, spider):
self.conn = sqlite3.connect("quotes.db")
self.cursor = self.conn.cursor()
self.cursor.execute("""CREATE TABLE IF NOT EXISTS quotes (text TEXT, author TEXT)""")
def process_item(self, item, spider):
self.cursor.execute("INSERT INTO quotes VALUES (?, ?)", (item['text'], item['author']))
return item # 返回item以便下一个Pipeline(如果有)处理
def close_spider(self, spider):
self.conn.commit()
self.conn.close()
并在settings.py
中启用:
ITEM_PIPELINES = {
'mycrawler.pipelines.SaveQuotesPipeline': 300,
}
其中数字300
表示Pipeline的执行顺序优先级(越小越先执行)。
Scrapy还有很多强大之处,如自动的请求去重、异常重试、日志和调试工具(scrapy shell
交互调试特定页面)等等。尽管学习曲线相对爬虫脚本要高一些,但Scrapy对于复杂爬虫项目的开发维护无疑更加高效。
提示:如果你是初学者,在掌握Requests+BS4写简单爬虫后,再学习Scrapy会比较顺畅,因为Scrapy的很多概念(Request、Response、解析)与基础方法类似,只是框架帮你做了调度和优化。
7. 反反爬策略
当我们爬取网站时,常常会遇到网站的反爬虫机制:如要求登录、使用验证码、限制请求频率、检查请求头等。为提高爬虫的成功率,我们需要采取一些“反反爬”策略,模拟更接近真实用户的行为,避开被网站检测到是爬虫。以下是常见的反爬对抗技巧:
-
伪装 User-Agent: 大部分爬虫库(如requests)默认的User-Agent字符串明显带有
Python-requests
字样,容易被网站屏蔽。我们可以将请求头中的User-Agent更改为常见浏览器标识。例如:headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)... Safari/537.36" } requests.get(url, headers=headers)
也可以使用第三方库如
fake-useragent
随机生成不同浏览器的UA字符串,每次请求更换,增加多样性。 -
使用 Cookies 和 会话: 有些网站需要维护登录状态或者根据上一次访问情况返回内容。利用
requests.Session
对象可以自动保存cookie,下次请求时带上。或者手动从浏览器复制登录后的cookies,在请求时附加:cookies = {"sessionid": "abcd1234..."} # 伪造登录后的cookie requests.get(url, headers=headers, cookies=cookies)
这样服务器会认为是已登录用户,提高访问权限。注意滥用他人Cookies是违法的,这里仅讨论技术手段。
-
设置请求频率和随机延迟: 快速连续的请求容易触发反爬。可以在爬虫中加入
time.sleep()
随机睡眠几秒再请求下一个页面,或在Scrapy中设置DOWNLOAD_DELAY等参数。通过降低速度、模拟人类浏览节奏来降低被封的概率。另外,可以对爬取顺序进行随机打乱,不要每次都严格按照固定间隔抓取同一模式。 -
使用代理IP: 如果爬取频率较高或目标网站对单IP请求数有限制,可以使用代理服务器来切换IP地址。网上有免费代理IP,但质量参差不齐,更可靠的是购买付费代理服务。使用requests设置HTTP代理:
proxies = {"http": "http://123.123.123.123:8080", "https": "https://123.123.123.123:8080"} requests.get(url, headers=headers, proxies=proxies)
Scrapy中也可以在settings中配置
HTTP_PROXY
或使用scrapy-proxies中间件。代理池要定期更换检测,确保IP未被目标网站封禁。 -
动态模拟 Headers: 除了User-Agent,还可以模仿浏览器发送的其他Headers,例如
Accept-Language
(接受语言),Referer
(引荐页面),Accept-Encoding
(接受的压缩格式)等。合理设置这些头信息可以让请求更“正常”。最简单办法是从浏览器开发者工具复制真实请求的所有Headers,在代码里使用同样的。 -
模拟浏览器行为: 某些网站通过执行复杂的JavaScript生成内容或检查行为(如鼠标移动、点击)。对此最有效的办法是采用Selenium等浏览器自动化工具,直接驱动真实浏览器完成操作,然后获取渲染后的页面源代码。示例:
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.headless = True # 使用无头模式,不打开浏览器窗口 driver = webdriver.Chrome(options=options) driver.get("https://example.com/") # 让Chrome打开页面 content = driver.page_source # 获取动态渲染后的HTML driver.quit()
Selenium支持与页面元素交互,如点击、表单填写等,能突破很多反爬限制。不过它的缺点是速度慢、资源占用高,不适合大规模爬取。可以配合无头浏览器(如Chrome Headless、PhantomJS)或者无界面模拟(如Splash服务器)来加快一定速度,但相较于Requests还是慢很多。
-
识别并绕过验证码: 当遇到验证码(常见于登录或频繁访问后弹出),基本宣告自动化受阻。常见策略包括:人工打码(将验证码图片截图保存,让人工或打码平台识别填写)、使用OCR技术自动识别简单验证码,或者分析网站有没有提供验证码绕过的机制。但这些都超出了一般爬虫的范畴,而且处理不当可能违法。通常个人爬虫遇到复杂验证码往往只能放弃或减小频率避免触发。
-
混淆爬虫特征: 有的网站可能通过检测请求的某些特征来识别爬虫,比如请求顺序、参数特征、IP地址地理位置等。对此可以做的是:随机化——包括访问顺序随机、使用不同账号或不同IP地域去请求等,尽量避免呈现固定模式。另外,使用分布式爬虫框架和多个节点从不同地方爬,也是一种办法(如Scrapy结合scrapy-redis组件实现分布式调度)。
小结: 反反爬是一场与网站策略的博弈。以上方法可能需要组合使用。例如爬取某网站可能同时需要模拟登录(cookies)、降低频率、使用代理、更换UA等才能顺利获取数据。要根据具体的反爬手段进行针对性应对。同时也要把握分寸,尊重网站的反爬措施。如果对方明确不希望被爬取或已强力防护,那么贸然绕过不但花费巨大代价,也可能惹上法律麻烦。
8. 实战案例:爬取示例网站
理论讲解之后,我们通过一个完整的示例来演练如何从零开始实现一个爬虫脚本。目标: 爬取一个示例网站的多页内容,并将数据保存到文件。本案例选用Quotes to Scrape网站,这是一个专门供爬虫练习的公开站点,页面上有名人名言及作者,并支持分页,非常适合作为示范。
任务描述: 抓取该网站上的所有名言文本和作者名,并将结果保存到CSV文件中。
8.1 分析目标网站
用浏览器打开目标网站首页,可以看到每条名言的HTML结构(可以按F12打开开发者工具查看DOM):
<div class="quote">
<span class="text">“The world as we have created it is a process of our thinking. ...”</span>
<small class="author">Albert Einstein</small>
<div class="tags">
Tags: <a class="tag" href="...">change</a> <a class="tag" href="...">deep-thoughts</a> ...
</div>
</div>
每个.quote包含一句话.text
、作者.author
和若干标签,我们需要提取前两项。底部分页部分(仅展示结构):
<ul class="pager">
<li class="next"><a href="/page/2/">Next <span aria-hidden="true">→</span></a></li>
</ul>
可以看到“Next”按钮的链接在<li class="next">
中,我们可以据此找到下一页的URL。
8.2 编写爬虫代码
我们将使用requests和BeautifulSoup来实现,逻辑为:从第一页开始抓 -> 解析数据 -> 找到下一页链接 -> 循环抓取直至没有下一页。以下是完整代码:
import requests
from bs4 import BeautifulSoup
import csv
base_url = "http://quotes.toscrape.com" # 基础URL
start_url = base_url + "/" # 起始页
all_quotes = [] # 用于存储抓取到的所有名言和作者
url = start_url
while url:
# 获取页面内容
res = requests.get(url)
soup = BeautifulSoup(res.text, 'lxml')
# 解析名言块
quote_divs = soup.find_all("div", class_="quote")
for div in quote_divs:
text = div.find("span", class_="text").get_text(strip=True) # 名言文本
author = div.find("small", class_="author").get_text(strip=True) # 作者
all_quotes.append((text, author))
# 查找下一页链接
next_btn = soup.find("li", class_="next")
if next_btn:
next_url = next_btn.find("a")["href"] # 如 "/page/2/"
url = base_url + next_url # 拼接绝对URL
else:
url = None # 没有下一页,结束循环
# 将结果保存到CSV文件
with open("quotes.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["Quote", "Author"]) # 写入表头
writer.writerows(all_quotes)
print(f"抓取完成,共保存 {len(all_quotes)} 条名言到 quotes.csv")
代码说明:
- 使用
while url:
循环,当存在下一页时继续抓取。初始的url
为首页地址,循环内每次更新url
为下一页。 - 对于每个页面,用
requests.get
获取HTML文本,然后用BeautifulSoup
解析。 soup.find_all("div", class_="quote")
找出当页的所有名言块。然后对每个块提取文字和作者。其中get_text(strip=True)
用于获取纯文本并去除首尾空白。- 解析完当前页后,通过查找
<li class="next">
判断是否有下一页。如果有,提取里面<a>
标签的href,并拼接成完整URL赋给url
。如果没有(最后一页没有.next),则置url=None
跳出循环。 - 最后将收集的
all_quotes
列表写入CSV,每行两列,分别是名言和作者。完成后打印一条总结信息。
8.3 运行结果
运行上述脚本,程序将逐页爬取该网站的所有10页内容。最终quotes.csv
文件将包含所有名言和对应的作者。例如前几行内容如下(这里展示CSV文本,每条记录换行显示):
"Quote","Author"
"“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”","Albert Einstein"
"“... A day without sunshine is like, you know, night.”","Steve Martin"
...
我们成功地将目标网站的数据采集下来。这一案例规模不大,但涵盖了发送请求、解析网页、处理分页、数据存储等关键步骤。在实际应用中,可以根据类似思路扩展:比如增加异常处理(网络错误重试)、更加复杂的信息提取逻辑、或者将结果存入数据库而非CSV等。
提示: 当你调试爬虫时,建议先限制页数或数据量,确认抓取和解析逻辑正确,再放开爬取所有内容。比如在上述例子中,可先尝试抓取前2页看看输出是否预期,以避免逻辑错误导致爬虫长时间运行却抓取不到想要的数据。
9. 优化与部署
编写完爬虫脚本并不意味着大功告成,实际应用中我们经常需要考虑如何更快更稳健地爬取以及如何在服务器上长期运行等问题。本节讨论一些爬虫优化技巧和部署方法。
9.1 爬虫性能优化
(1) 并发与异步:
串行的爬虫在遇到大量页面时会非常缓慢,因为需要等待每个请求完成再进行下一个。通过并发手段可以极大提高效率:
- 多线程:使用Python的
threading
或concurrent.futures.ThreadPoolExecutor
,让多个线程同时发请求解析页面。在I/O密集型任务(如网络请求)中,多线程能够提升速度。需要注意线程安全(如对全局数据写入时要加锁)。 - 多进程:使用
multiprocessing
或ProcessPoolExecutor
,可以绕过GIL限制,让多个进程各自爬取不同数据。多进程适合CPU密集任务或者需要隔离GI锁的场景,但启动开销较大,数据需跨进程传递,通常用于更重型的并发任务。 - 异步IO:利用
asyncio
框架和aiohttp
库,编写异步爬虫。在单线程下实现高并发请求,性能非常出色。示例:
这段代码展示了aiohttp的基本用法。在Scrapy内部,其实也是基于异步IO实现高并发的,所以使用Scrapy往往不需要手动处理线程/async,它会帮你并发抓取。import asyncio, aiohttp async def fetch(session, url): async with session.get(url) as resp: return await resp.text() async def main(): urls = ["http://example.com/page1", "..."] # 一堆URL async with aiohttp.ClientSession() as session: tasks = [asyncio.create_task(fetch(session, u)) for u in urls] pages = await asyncio.gather(*tasks) print("Fetched", len(pages), "pages") asyncio.run(main())
(2) 控制抓取速度:
一味追求速度可能导致被封禁或自身资源耗尽。优化也包含适当的延迟和限速。Scrapy提供了AutoThrottle
扩展,可根据延迟动态调整抓取速度。对于requests自建的爬虫,可以手工控制每秒请求数或在代码中加入sleep。确保既快又稳才是有效的优化。
(3) 减少重复请求:
利用缓存或记录机制避免抓取相同页面多次。例如对列表页经常会反复抓取相同的详情页链接,可在请求前检查是否已抓过。Scrapy默认有去重(Scheduler会过滤相同请求),在自写爬虫中可以用集合或数据库记录已访问URL。
(4) 提高解析效率:
如果页面很大或者解析逻辑复杂,解析也会耗时。选择高效的解析方式很重要。一般lxml的速度比BeautifulSoup快不少。或者在能用简单字符串操作时就不必构建DOM。此外,尽量精准地提取所需部分,而不是无谓地处理整页数据。
(5) 合理利用硬件:
爬虫运行时要关注CPU、内存、网络等资源。多线程可能受制于GIL无法利用多核,适当可以开多进程或分布式部署。内存方面,如果抓了几百万条数据,别一次性全部存在Python列表里,可以边抓边写硬盘(流式处理),避免内存爆炸。对于超大规模爬取,可以考虑使用分布式爬虫框架(如Scrapy+Redis,Heritrix等)和更强劲的硬件。
9.2 部署爬虫
当爬虫脚本需要长时间运行或定期执行时,通常会部署到服务器环境。例如你的爬虫每天抓取一次新闻网站,需要一个稳定的地方每天运行。
(1) 本地部署 vs 云服务器:
小型任务可以在自己电脑上用定时任务跑,但要求长时间开机。更可靠的是将爬虫部署到云服务器(如AWS EC2、阿里云ECS等),在Linux环境下后台运行。选择服务器时,根据爬虫的资源需求选配适当的CPU、内存、带宽。
(2) 使用任务调度:
在Linux服务器上,可以使用cron
定时执行爬虫脚本。例如,编辑crontab -e
加入一行:
0 2 * * * /usr/bin/python3 /path/to/spider.py >> /path/to/spider.log 2>&1
表示每天凌晨2点运行spider.py并将日志追加输出。Windows下可以用任务计划程序完成类似功能。如果爬虫需要24/7连续运行,可以用shell脚本监控,异常退出时自动重启,或者借助supervisor
等进程管理工具来保持运行。
(3) 日志与监控:
部署后最好加上日志记录。可以使用Python自带的logging
模块,将重要的事件(开始/结束、错误、数据统计)写入日志文件,以便出问题时诊断。Scrapy框架天生带日志配置,自己写的脚本也应至少捕获异常写日志。监控方面,可以简单通过日志检查进度,或更高级地用监控工具对运行的进程、网络IO等进行观察。
(4) 容器化部署:
将爬虫打包进Docker容器也是流行的做法。编写一个Dockerfile,把Python环境和依赖装好,复制爬虫代码进去,设置CMD运行。然后在任意主机上只要有Docker,运行该镜像就可启动爬虫。这对部署多个爬虫、迁移环境来说非常方便,也利于将环境配置写定(Infrastructure as Code)。
(5) Scrapy部署工具:
如果使用Scrapy,可以利用其提供的Scrapyd服务部署。Scrapyd可以在服务器上运行,接收打包好的爬虫项目(.egg或.zip)并提供API启动/停止爬虫。更简单的,Scrapy官方还提供了Scrapy Cloud(即Zyte,收费服务),无需自己管服务器即可托管爬虫,并有友好的监控界面。
(6) 多机爬取和负载均衡:
对于超大规模的爬取任务,单台机器可能不够,可以考虑多机分担。常见方式是分布式爬虫:将URL任务队列放在公共的地方(例如Redis队列),多台爬虫机器同时从队列取URL抓取,抓完的新URL再放回队列,实现分工协作。Scrapy有开源的scrapy-redis组件能比较容易地实现这种调度。部署上则需要一个调度机 + 多个爬虫工作机,并行工作,大幅扩展爬取量。
无论哪种部署方案,都要确保稳定性和可持续运行。有时网络波动、目标网站临时关闭、服务器自身重启等都会中断爬虫,需要有应对措施(如定时检查进程存活、失败后告警等)。
部署和运维往往是爬虫项目中容易被忽视但非常重要的一环。一个写得再好的爬虫,如果不能长时间可靠地运行,也难以产生实际价值。
10. 常见问题与技巧
在实际开发和运行爬虫的过程中,你可能会遇到各种各样的问题。最后,我们总结一些常见问题及处理方法、调试技巧和效率提升的要点:
- IP被封/请求被拒绝: 这通常表现为连续收到HTTP 403 Forbidden或请求没有响应。处理办法包括使用代理IP、降低抓取频率、模拟Headers等(参考第7节的反反爬策略)。如果是IP被临时封禁,可暂停爬虫等待一段时间再试。
- 遇到验证码或登陆墙: 如果目标网站要求登录才能访问数据,尝试使用爬虫提交登录表单或者使用已有账号的Cookies。验证码是更难的障碍,可考虑绕过(有时验证码在首次请求或频繁后才出现,可以分散请求避免触发),或者利用打码服务平台**(付费,人机结合识别)**解决。但需权衡成本和收益。
- 数据提取不全或解析失败: 可能是因为网页采用了动态加载,爬虫获取的HTML不包含目标信息。这时需要分析网络请求,在浏览器开发者工具的“Network”面板寻找是否有XHR请求获取数据,如果有直接调用这些API接口;若数据通过JS计算得出,则可能需要用Selenium加载后再抓取。调试方法:对照浏览器页面源代码和爬虫获取的源码,找差异。
- 乱码与编码问题: 有时抓下来的内容是乱码,通常是编码处理不当导致。requests会根据HTTP头猜测编码并赋给
response.encoding
,但并不总是正确。可以手动设置response.encoding = 'utf-8'
或其他编码,然后再response.text
。对于中文网站,常见编码包括UTF-8和GBK/GB2312。如果乱码,可以尝试用res.content.decode('gbk', errors='ignore')
之类的方法解码。BeautifulSoup解析时也要确保给定正确的编码。 - 高并发爬虫的稳定性: 当使用多线程/异步时,常会遇到一些线程安全或连接超时等问题。例如使用aiohttp需要谨慎处理
session
的重用和关闭;多线程下如需写入统一文件,要用锁串行化写操作。调试这类问题可以先在小规模并发下运行,逐渐增加线程数观察是否正常,并捕获异常做日志。 - 调试技巧: 对于抓取和解析阶段的问题,可以使用交互式调试。如Scrapy有
scrapy shell 'URL'
可以打开一个终端调试特定URL的解析,非常好用。自己写脚本时,也可以在报错处打印相关HTML片段或保存到文件手动检查。另外,善用日志打印关键节点信息,能帮助追踪爬虫进度和发现异常。 - 规避触发监控: 一些大型网站可能对访问行为进行机器学习监控,检测异常模式。应避免过于规则化的行为,比如每次都整点启动,每页精确延迟5秒等。这些都可能成为特征。可以加入一些抖动和随机元素,使爬虫行为更类似真实用户(当然也不能太随机无序而降低效率)。
- 提升爬取效率的小窍门:
- 利用分层爬取:先抓取索引页列表,再抓取详情页内容,这样可以先拿到整体结构,再并发抓详情。
- 对于特别大的页面,考虑流式处理:一边下载一边解析(Python
requests
可用stream=True
获取流),或者先下载到本地文件再逐行解析,减少一次性内存占用。 - 如果频繁爬取同一网站,可以缓存一些不变的内容,比如列表页每天可能变化不大,就不要每次都全抓新的,或者用ETag/Last-Modified头和增量抓取的方法,节省流量和时间。
- 法律和版权:再次提醒,合规运行爬虫很重要。近期一些国家加强了对数据抓取的监管,例如欧盟GDPR对个人数据抓取有严格限制,中国也有《数据安全法》《个人信息保护法》等。不遵守法律可能导致严重后果。在发布或分享抓取的数据前,也要考虑版权和隐私影响,通常公开的统计数据或匿名化的数据风险较小,而涉及个人的信息要慎之又慎。
开发一个爬虫不仅仅是把数据抓下来那么简单,还包括让它稳定、高效、安全地跑下去。希望这个教程提供的知识能帮助你构建出成功的Python爬虫项目。