Python Web 爬虫:Scrapy 和 BeautifulSoup 的高级用法
大家好,今天我们来深入探讨 Python Web 爬虫中的两个重要工具:Scrapy 和 BeautifulSoup。我们将从高级用法入手,结合实际案例,让大家能够更高效、更灵活地运用它们。
一、Scrapy 高级用法
Scrapy 是一个强大的、开源的 Web 爬虫框架,它提供了完整的爬虫生命周期管理,包括请求调度、数据提取、数据持久化等。下面我们将讨论 Scrapy 的一些高级特性。
1.1 中间件 (Middleware)
Scrapy 的中间件机制允许我们在请求和响应的流程中插入自定义的处理逻辑。 常见的中间件类型包括:
- Spider Middlewares: 处理 Spider 的输入(请求)和输出(Item)。
- Downloader Middlewares: 处理请求发送到服务器和响应返回给 Spider 之间的过程。
1.1.1 自定义 User-Agent 中间件
一个常见的需求是随机更换 User-Agent,以避免被网站识别为爬虫。我们可以创建一个自定义的 Downloader Middleware 来实现这个功能。
# middlewares.py
import random
class RandomUserAgentMiddleware:
def __init__(self, user_agent_list):
self.user_agent_list = user_agent_list
@classmethod
def from_crawler(cls, crawler):
return cls(
user_agent_list=crawler.settings.get('USER_AGENT_LIST', [])
)
def process_request(self, request, spider):
user_agent = random.choice(self.user_agent_list)
request.headers['User-Agent'] = user_agent
# settings.py
USER_AGENT_LIST = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/16.16299'
]
DOWNLOADER_MIDDLEWARES = {
'your_project.middlewares.RandomUserAgentMiddleware': 400, # 调整优先级
}
这段代码首先定义了一个 RandomUserAgentMiddleware
类,它从 settings.py 中读取 USER_AGENT_LIST
,并在 process_request
方法中随机选择一个 User-Agent 并设置到请求头中。 from_crawler
是一个类方法,用于从 Scrapy 的 Crawler 对象中获取设置。 最后,我们需要在 settings.py
中启用这个中间件,并设置一个优先级。
1.1.2 自定义 Retry 中间件
有时,我们需要在请求失败时进行重试。 Scrapy 默认提供了 RetryMiddleware,但我们可以自定义它以满足更复杂的需求。
# middlewares.py
from scrapy.exceptions import IgnoreRequest
class CustomRetryMiddleware:
def __init__(self, settings):
self.max_retry_times = settings.getint('RETRY_TIMES', 2)
self.retry_http_codes = set(int(x) for x in settings.getlist('RETRY_HTTP_CODES'))
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings)
def process_response(self, request, response, spider):
if request.meta.get('dont_retry', False):
return response
if response.status in self.retry_http_codes:
reason = f'Received retryable HTTP code: {response.status}'
return self._retry(request, reason, spider) or response
return response
def process_exception(self, request, exception, spider):
if isinstance(exception, self.EXCEPTIONS_TO_RETRY) and not request.meta.get('dont_retry', False):
return self._retry(request, exception, spider)
def _retry(self, request, reason, spider):
retries = request.meta.get('retry_times', 0) + 1
if retries <= self.max_retry_times:
new_request = request.copy()
new_request.meta['retry_times'] = retries
new_request.dont_filter = True # 避免被去重过滤器过滤
spider.logger.debug(f"Retrying {request.url} (attempt {retries}) due to {reason}")
return new_request
else:
spider.logger.debug(f"Gave up retrying {request.url} after {retries} attempts")
return None # 或者 raise IgnoreRequest
EXCEPTIONS_TO_RETRY = (
# 常见的可重试异常
)
# settings.py
RETRY_TIMES = 3 # 最大重试次数
RETRY_HTTP_CODES = [500, 502, 503, 504, 400, 408]
DOWNLOADER_MIDDLEWARES = {
'your_project.middlewares.CustomRetryMiddleware': 500, # 调整优先级
}
这段代码定义了一个 CustomRetryMiddleware
,它允许我们自定义最大重试次数和需要重试的 HTTP 状态码。process_response
处理响应,如果响应状态码在 RETRY_HTTP_CODES
中,则进行重试。 process_exception
处理异常,如果异常在 EXCEPTIONS_TO_RETRY
中,也进行重试。 _retry
方法负责创建新的请求,并设置重试次数。 注意 new_request.dont_filter = True
,这是为了避免重试的请求被去重过滤器过滤掉。
1.2 管道 (Pipeline)
Scrapy 的管道用于处理 Spider 提取的 Item。 常见的用法包括:
- 数据清洗
- 数据验证
- 数据存储 (数据库、文件等)
1.2.1 数据去重管道
我们可以创建一个管道来去重 Item。
# pipelines.py
class DuplicatesPipeline:
def __init__(self):
self.seen_titles = set()
def process_item(self, item, spider):
if item['title'] in self.seen_titles:
raise DropItem(f"Duplicate item found: {item['title']}")
else:
self.seen_titles.add(item['title'])
return item
# settings.py
ITEM_PIPELINES = {
'your_project.pipelines.DuplicatesPipeline': 300,
}
from scrapy.exceptions import DropItem
这段代码创建了一个 DuplicatesPipeline
,它使用一个 set
来存储已经见过的标题。 如果 Item 的标题已经在 set
中,则抛出一个 DropItem
异常,Scrapy 会丢弃这个 Item。 否则,将标题添加到 set
中,并返回 Item。
1.2.2 数据存储管道
我们可以创建一个管道将 Item 存储到数据库。
# pipelines.py
import pymongo
class MongoDBPipeline:
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def close_spider(self, spider):
self.client.close()
def process_item(self, item, spider):
self.db['items'].insert_one(dict(item))
return item
# settings.py
MONGO_URI = 'mongodb://localhost:27017/'
MONGO_DATABASE = 'scrapy_db'
ITEM_PIPELINES = {
'your_project.pipelines.MongoDBPipeline': 400,
}
这段代码创建了一个 MongoDBPipeline
,它将 Item 存储到 MongoDB 数据库。 from_crawler
方法从 settings.py 中读取 MongoDB 的 URI 和数据库名称。 open_spider
方法在 Spider 启动时连接到 MongoDB。 close_spider
方法在 Spider 关闭时关闭连接。 process_item
方法将 Item 插入到 MongoDB 的 items
集合中。
1.3 扩展 (Extensions)
Scrapy 的扩展机制允许我们自定义 Scrapy 的核心功能。 常见的用法包括:
- 监控爬虫状态
- 发送爬虫统计信息
- 自定义日志记录
1.3.1 监控爬虫状态扩展
我们可以创建一个扩展来监控爬虫的运行状态,例如抓取的页面数量、错误数量等。
# extensions.py
from scrapy import signals
class StatsMonitorExtension:
def __init__(self, stats):
self.stats = stats
@classmethod
def from_crawler(cls, crawler):
ext = cls(crawler.stats)
crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened)
crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)
crawler.signals.connect(ext.item_scraped, signal=signals.item_scraped)
return ext
def spider_opened(self, spider):
spider.logger.info("Spider opened: %s", spider.name)
def spider_closed(self, spider, reason):
spider.logger.info("Spider closed: %s", spider.name)
spider.logger.info("Stats: %s", self.stats.get_stats())
def item_scraped(self, item, spider):
spider.logger.debug("Scraped item: %s", item)
# settings.py
EXTENSIONS = {
'your_project.extensions.StatsMonitorExtension': 500,
}
这段代码创建了一个 StatsMonitorExtension
,它使用 Scrapy 的 stats
对象来获取爬虫的统计信息。 from_crawler
方法连接到 Scrapy 的 signals,以便在 Spider 启动、关闭和 Item 被抓取时执行相应的操作。 spider_opened
方法在 Spider 启动时记录一条信息。 spider_closed
方法在 Spider 关闭时记录一条信息,并输出爬虫的统计信息。 item_scraped
方法在 Item 被抓取时记录一条信息。
1.4 Feed Exports
Scrapy 提供了 Feed Exports 功能,可以将抓取的数据导出到多种格式的文件中,例如 JSON, CSV, XML 等。
1.4.1 导出为 CSV 文件
# settings.py
FEED_FORMAT = 'csv'
FEED_URI = 'output.csv'
FEED_EXPORT_ENCODING = 'utf-8' # 避免中文乱码
这段代码将抓取的数据导出到名为 output.csv
的 CSV 文件中,并使用 UTF-8 编码。
二、BeautifulSoup 高级用法
BeautifulSoup 是一个用于解析 HTML 和 XML 文档的 Python 库。 它提供了一种简单的方法来遍历文档树,查找特定的元素,并提取数据。 下面我们将讨论 BeautifulSoup 的一些高级特性。
2.1 CSS 选择器
BeautifulSoup 支持使用 CSS 选择器来查找元素。 这种方法比使用 find() 和 find_all() 方法更简洁、更灵活。
from bs4 import BeautifulSoup
html = """
<div class="content">
<h2 class="title">Article Title</h2>
<p class="author">By John Doe</p>
<div class="body">
<p>This is the first paragraph.</p>
<p>This is the second paragraph.</p>
</div>
</div>
"""
soup = BeautifulSoup(html, 'html.parser')
# 使用 CSS 选择器查找标题
title = soup.select_one('.content .title').text
print(f"Title: {title}") # Output: Title: Article Title
# 使用 CSS 选择器查找所有段落
paragraphs = soup.select('.content .body p')
for p in paragraphs:
print(f"Paragraph: {p.text}")
# Output:
# Paragraph: This is the first paragraph.
# Paragraph: This is the second paragraph.
这段代码使用 select_one()
方法查找第一个匹配 CSS 选择器的元素,并使用 select()
方法查找所有匹配 CSS 选择器的元素。
2.2 正则表达式
BeautifulSoup 允许我们在查找元素时使用正则表达式。 这对于查找具有特定模式的属性值的元素非常有用。
import re
from bs4 import BeautifulSoup
html = """
<a href="/article/123">Article 1</a>
<a href="/article/456">Article 2</a>
<a href="/product/789">Product 1</a>
"""
soup = BeautifulSoup(html, 'html.parser')
# 查找所有 href 属性以 /article/ 开头的链接
articles = soup.find_all('a', href=re.compile(r'^/article/'))
for a in articles:
print(f"Article Link: {a['href']}")
# Output:
# Article Link: /article/123
# Article Link: /article/456
这段代码使用 re.compile()
方法创建一个正则表达式对象,并将其传递给 find_all()
方法的 href
参数。
2.3 Lambda 函数
BeautifulSoup 允许我们在查找元素时使用 Lambda 函数。 这对于执行更复杂的过滤逻辑非常有用。
from bs4 import BeautifulSoup
html = """
<a href="/article/123" data-category="news">Article 1</a>
<a href="/article/456" data-category="sports">Article 2</a>
"""
soup = BeautifulSoup(html, 'html.parser')
# 查找所有 data-category 属性值为 news 的链接
articles = soup.find_all('a', lambda tag: tag.get('data-category') == 'news')
for a in articles:
print(f"Article Link: {a['href']}") # Output: Article Link: /article/123
这段代码使用一个 Lambda 函数来检查元素的 data-category
属性值是否为 news
。
2.4 处理动态内容 (配合 Selenium)
有些网站使用 JavaScript 动态生成内容。 BeautifulSoup 无法直接处理这些内容。 我们可以使用 Selenium 等工具来渲染 JavaScript,然后再使用 BeautifulSoup 解析渲染后的 HTML。
from selenium import webdriver
from bs4 import BeautifulSoup
# 启动 Chrome 浏览器 (需要安装 ChromeDriver)
driver = webdriver.Chrome() # 或者 Firefox, Edge 等
# 加载网页
driver.get('https://www.example.com') # 替换为实际的 URL
# 等待 JavaScript 渲染完成 (可以使用 WebDriverWait 显式等待)
import time
time.sleep(3) # 简单粗暴的等待
# 获取渲染后的 HTML
html = driver.page_source
# 关闭浏览器
driver.quit()
# 使用 BeautifulSoup 解析 HTML
soup = BeautifulSoup(html, 'html.parser')
# 查找元素
title = soup.find('h1').text
print(f"Title: {title}")
这段代码首先使用 Selenium 启动一个 Chrome 浏览器,然后加载一个网页。 time.sleep(3)
只是一个简单的等待方式,实际应用中应该使用 WebDriverWait
进行显式等待,以确保 JavaScript 渲染完成。 然后,它获取渲染后的 HTML,并使用 BeautifulSoup 解析 HTML。 最后,它关闭浏览器。
2.5 增量解析 (lxml 的 iterparse)
对于非常大的 XML 文件,一次性加载到内存中可能会导致内存溢出。 lxml
库提供了 iterparse
方法,可以实现增量解析。 虽然 BeautifulSoup 本身没有这个功能,但我们可以利用 lxml 解析部分文档,然后将其传递给 BeautifulSoup。
from lxml import etree
from io import StringIO
from bs4 import BeautifulSoup
xml_data = """
<root>
<item id="1"><name>Item 1</name><description>Description 1</description></item>
<item id="2"><name>Item 2</name><description>Description 2</description></item>
<item id="3"><name>Item 3</name><description>Description 3</description></item>
</root>
"""
xml_file = StringIO(xml_data)
for event, element in etree.iterparse(xml_file, events=('end',), tag='item'):
# 将 lxml 的 element 转换为字符串
element_string = etree.tostring(element, encoding='utf-8').decode('utf-8')
# 使用 BeautifulSoup 解析字符串
soup = BeautifulSoup(element_string, 'xml') # 注意使用 'xml' 解析器
name = soup.find('name').text
description = soup.find('description').text
print(f"Name: {name}, Description: {description}")
# 清理 element,避免内存泄漏
element.clear()
while element.getprevious() is not None:
del element.getparent()[0]
这段代码使用 lxml.etree.iterparse
增量解析 XML 文件,每次处理一个 <item>
元素。 将 lxml
的 element
转换为字符串,然后使用 BeautifulSoup 解析字符串。 最后,清理 element
,避免内存泄漏。
三、Scrapy 和 BeautifulSoup 的结合使用
Scrapy 和 BeautifulSoup 可以很好地结合使用。 Scrapy 负责请求和调度,BeautifulSoup 负责解析 HTML。
# scrapy_beautifulsoup_example/spiders/example_spider.py
import scrapy
from bs4 import BeautifulSoup
class ExampleSpider(scrapy.Spider):
name = "example"
start_urls = ['https://www.example.com']
def parse(self, response):
soup = BeautifulSoup(response.text, 'html.parser')
title = soup.find('h1').text
yield {'title': title}
这段代码创建了一个 Scrapy Spider,它使用 BeautifulSoup 解析响应的 HTML,并提取标题。
四、案例分析:爬取电商网站商品信息
我们以爬取一个电商网站的商品信息为例,来演示 Scrapy 和 BeautifulSoup 的高级用法。 假设我们要爬取商品的名称、价格和描述。
4.1 定义 Item
# items.py
import scrapy
class ProductItem(scrapy.Item):
name = scrapy.Field()
price = scrapy.Field()
description = scrapy.Field()
image_url = scrapy.Field()
4.2 创建 Spider
# spiders/ecommerce_spider.py
import scrapy
from bs4 import BeautifulSoup
from scrapy_beautifulsoup_example.items import ProductItem
class EcommerceSpider(scrapy.Spider):
name = "ecommerce"
start_urls = ['https://example.com/products'] # 替换为实际的 URL
def parse(self, response):
soup = BeautifulSoup(response.text, 'html.parser')
product_list = soup.find_all('div', class_='product')
for product in product_list:
name = product.find('h2', class_='product-name').text.strip()
price = product.find('span', class_='product-price').text.strip()
description = product.find('p', class_='product-description').text.strip()
image_url = product.find('img', class_='product-image')['src']
item = ProductItem(name=name, price=price, description=description, image_url=image_url)
yield item
# 翻页 (如果网站有分页)
next_page_url = soup.find('a', class_='next-page')['href']
if next_page_url:
yield scrapy.Request(url=response.urljoin(next_page_url), callback=self.parse)
4.3 配置 settings.py
配置 User-Agent,启用管道等。
# settings.py
BOT_NAME = 'scrapy_beautifulsoup_example'
SPIDER_MODULES = ['scrapy_beautifulsoup_example.spiders']
NEWSPIDER_MODULE = 'scrapy_beautifulsoup_example.spiders'
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
ITEM_PIPELINES = {
'scrapy_beautifulsoup_example.pipelines.MongoDBPipeline': 300, # 启用 MongoDB 管道
}
MONGO_URI = 'mongodb://localhost:27017/'
MONGO_DATABASE = 'ecommerce_db'
4.4 创建 MongoDB 管道
# pipelines.py
import pymongo
class MongoDBPipeline:
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def close_spider(self, spider):
self.client.close()
def process_item(self, item, spider):
self.db['products'].insert_one(dict(item))
return item
这个案例演示了如何使用 Scrapy 和 BeautifulSoup 爬取电商网站的商品信息,并将数据存储到 MongoDB 数据库。 实际应用中,需要根据目标网站的 HTML 结构进行调整。
五、总结:灵活运用 Scrapy 和 BeautifulSoup
我们深入探讨了 Scrapy 和 BeautifulSoup 的高级用法,包括中间件、管道、扩展、CSS 选择器、正则表达式、Lambda 函数、处理动态内容和增量解析。 结合实际案例,我们演示了如何使用 Scrapy 和 BeautifulSoup 爬取电商网站的商品信息,并将数据存储到 MongoDB 数据库。 熟练掌握这些高级特性,可以更高效、更灵活地进行 Web 爬虫开发。