Python常见面试题的详解21
1. 说明Scrapy 框架运行的机制
- 要点
Scrapy 是强大的 Python 爬虫框架,其运行依赖多个核心组件协同工作,这些组件之间通过引擎有序调度,实现数据的高效爬取。主要组件有引擎、调度器、下载器、爬虫、下载器中间件、爬虫中间件和管道。
以下是对各组件及运行流程的详细解释和简单示例代码辅助理解:
python
# 假设这是一个简单的 Scrapy 项目结构
import scrapy
from scrapy import signals
from scrapy.crawler import CrawlerProcess
# 定义一个简单的爬虫
class SimpleSpider(scrapy.Spider):
name = "simple_spider"
start_urls = ['http://example.com']
def parse(self, response):
# 解析响应内容
yield {'title': response.css('title::text').get()}
# 以下模拟各组件的交互流程
# 引擎负责协调,这里简化为一个函数
def engine():
# 创建爬虫实例
spider = SimpleSpider()
# 从爬虫获取初始请求
start_requests = iter(spider.start_requests())
request = next(start_requests)
# 模拟调度器调度请求
scheduler = []
scheduler.append(request)
# 模拟下载器下载页面
def downloader(request):
# 这里只是简单模拟返回一个响应对象
class MockResponse:
def __init__(self):
self.text = '<html><title>Example Page</title></html>'
return MockResponse()
# 从调度器获取请求并交给下载器
req = scheduler.pop()
response = downloader(req)
# 爬虫解析响应
for item in spider.parse(response):
print(item)
if __name__ == "__main__":
engine()
- 运行流程详细步骤:
-
引擎获取初始请求:引擎从爬虫的
start_requests
方法获取初始请求。 -
调度器调度请求:引擎将请求发送给调度器,调度器对请求进行排序和管理,等待引擎请求时返回。
-
下载器下载页面:引擎从调度器获取请求后,将其发送给下载器,下载器根据请求下载网页内容,生成响应并返回给引擎。
-
爬虫解析响应:引擎将响应发送给爬虫,爬虫通过
parse
方法解析响应内容,生成新的请求或数据项。 -
新请求调度与数据处理:新的请求返回给引擎,再由引擎发送给调度器;数据项则发送给管道进行处理。
2. 如何理解 Scrapy 框架
- 要点
Scrapy 是基于 Python 的高效,可扩展爬虫框架,它采用模块化设计,将爬虫开发中的网络请求,调度,并发等底层操作封装,开发者只需关注网页解析和数据提取逻辑。同时,它运用异步 I/O 和事件驱动机制,提升了爬取效率,并且提供丰富的中间件和管道机制,方便定制化处理。
python
import scrapy
class MySpider(scrapy.Spider):
name = "myspider"
start_urls = ['http://example.com']
def parse(self, response):
# 提取网页中的所有链接
for link in response.css('a::attr(href)').getall():
yield response.follow(link, self.parse)
# 提取网页标题
title = response.css('title::text').get()
yield {'title': title}
上述代码展示了一个简单的 Scrapy 爬虫,开发者只需定义爬虫类,指定起始 URL 和解析方法,Scrapy 框架会自动处理请求的发送、响应的接收和调度等操作。
3. 如何让 Scrapy 框架发送一个 POST 请求
- 要点
在 Scrapy 中发送 POST 请求,可通过 scrapy.Request
方法,设置 method
参数为 'POST'
,并通过 body
参数传递 POST 数据。
以下通过start_requests
方法生成一个 POST 请求,将 JSON 格式的数据通过 body
参数传递,同时设置请求头的 Content-Type
为 application/json
。请求发送后,响应会交给 parse
方法处理。
python
import scrapy
import json
class PostSpider(scrapy.Spider):
name = "post_spider"
def start_requests(self):
url = 'https://example.com/api'
data = {
'username': 'testuser',
'password': 'testpass'
}
headers = {
'Content-Type': 'application/json'
}
yield scrapy.Request(
url=url,
method='POST',
body=json.dumps(data),
headers=headers,
callback=self.parse
)
def parse(self, response):
print(response.text)
4. 怎么判断网站是否更新
- 要点
判断网站是否更新可以从多个角度入手,如比较内容哈希值、检查更新时间、对比页面元素和使用网站 API。
以下通过计算网页内容的 SHA-256 哈希值,比较当前哈希值和之前记录的哈希值来判断网站是否更新。
python
import hashlib
import requests
def get_content_hash(url):
response = requests.get(url)
content = response.text
hash_object = hashlib.sha256(content.encode())
return hash_object.hexdigest()
# 假设之前记录的哈希值
previous_hash = 'abc123'
current_url = 'http://example.com'
current_hash = get_content_hash(current_url)
if current_hash != previous_hash:
print("网站已更新")
else:
print("网站未更新")
5. 爬取的数据量大概有多大?大概多长时间爬一次?
- 要点
爬取的数据量和爬取频率受多种因素影响。数据量取决于网站规模、内容复杂度和爬取范围;爬取频率需根据网站更新频率、数据时效性要求和网站反爬机制确定。
- 示例说明
-
数据量:对于小型博客网站,每天可能只产生几 KB 到几十 KB 的文本数据;而大型电商网站,每天可能会产生几百 MB 甚至 GB 级别的商品信息数据。
-
爬取频率:新闻网站更新频繁,可设置每小时或每天爬取一次;企业官网更新较慢,可每周或每月爬取一次。以下是一个简单的定时爬取示例:
python
import time
import scrapy
from scrapy.crawler import CrawlerProcess
class MySpider(scrapy.Spider):
name = "myspider"
start_urls = ['http://example.com']
def parse(self, response):
print(response.text)
process = CrawlerProcess()
while True:
process.crawl(MySpider)
process.start()
time.sleep(86400) # 每天爬取一次
6. 用什么数据库存储爬下来的数据?怎么部署?
- 要点
可根据数据特点选择关系型数据库(如 MySQL、PostgreSQL)或非关系型数据库(如 MongoDB、Redis)存储爬取的数据。部署时,需先安装数据库,创建相应的数据库和表结构,再在 Scrapy 项目中配置数据库连接。
以下展示了如何在 Scrapy 项目中使用 MySQL 存储爬取的数据,包括创建表、插入数据和关闭连接等操作。
python
import scrapy
import pymysql
class MySQLPipeline:
def __init__(self):
self.connection = pymysql.connect(
host='localhost',
user='root',
password='password',
database='scrapy_data',
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
self.cursor = self.connection.cursor()
# 创建表
create_table_query = """
CREATE TABLE IF NOT EXISTS items (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255)
)
"""
self.cursor.execute(create_table_query)
self.connection.commit()
def process_item(self, item, spider):
insert_query = "INSERT INTO items (title) VALUES (%s)"
self.cursor.execute(insert_query, (item['title'],))
self.connection.commit()
return item
def close_spider(self, spider):
self.cursor.close()
self.connection.close()
class MySpider(scrapy.Spider):
name = "myspider"
start_urls = ['http://example.com']
def parse(self, response):
title = response.css('title::text').get()
yield {'title': title}
process = CrawlerProcess({
'ITEM_PIPELINES': {
'__main__.MySQLPipeline': 300,
}
})
process.crawl(MySpider)
process.start()
7. 如何实现增量爬取
- 要点
增量爬取可通过使用哈希值、记录更新时间和使用版本号等方法实现。通过比较新旧数据的特征,只处理有更新的数据。
以下使用 SQLite 数据库存储网页的哈希值,每次爬取时比较当前哈希值和数据库中存储的哈希值,只处理有更新的数据。
python
import scrapy
import hashlib
import sqlite3
class IncrementalSpider(scrapy.Spider):
name = "incremental_spider"
start_urls = ['http://example.com']
def __init__(self):
self.conn = sqlite3.connect('hashes.db')
self.cursor = self.conn.cursor()
self.cursor.execute('CREATE TABLE IF NOT EXISTS hashes (url TEXT, hash TEXT)')
self.conn.commit()
def parse(self, response):
content = response.text
hash_object = hashlib.sha256(content.encode())
current_hash = hash_object.hexdigest()
self.cursor.execute('SELECT hash FROM hashes WHERE url =?', (response.url,))
result = self.cursor.fetchone()
if result is None or result[0] != current_hash:
# 数据有更新
yield {'url': response.url, 'content': content}
self.cursor.execute('INSERT OR REPLACE INTO hashes (url, hash) VALUES (?,?)', (response.url, current_hash))
self.conn.commit()
def close(self, reason):
self.conn.close()
8. 爬取下来的数据如何去重,说一下 Scrapy 的具体的算法依据
- 要点
Scrapy 默认使用 RFPDupeFilter
类实现请求去重,通过计算请求的指纹(对请求的 URL、方法、请求体等信息进行哈希计算),并使用集合存储已处理的指纹,比较新请求的指纹是否存在于集合中来判断是否为重复请求。
以下简单模拟了 Scrapy 中请求指纹的计算过程,通过比较指纹来判断请求是否重复。
python
import hashlib
from scrapy.http import Request
def calculate_fingerprint(request):
data = f"{request.url}{request.method}{request.body}"
hash_object = hashlib.sha1(data.encode())
return hash_object.hexdigest()
request1 = Request(url='http://example.com', method='GET')
request2 = Request(url='http://example.com', method='GET')
fingerprint1 = calculate_fingerprint(request1)
fingerprint2 = calculate_fingerprint(request2)
if fingerprint1 == fingerprint2:
print("请求重复")
else:
print("请求不重复")
9. 怎么设置爬取深度
- 要点
在 Scrapy 中,可通过 settings.py
文件中的 DEPTH_LIMIT
和 DEPTH_STATS
配置项设置爬取深度和启用深度统计。
以下DEPTH_LIMIT
设置为 2,表示从起始 URL 开始,最多递归访问两层页面;DEPTH_STATS
启用深度统计,方便开发者了解不同深度的请求数量。
python
import scrapy
from scrapy.crawler import CrawlerProcess
class DepthSpider(scrapy.Spider):
name = "depth_spider"
start_urls = ['http://example.com']
def parse(self, response):
# 提取网页中的所有链接
for link in response.css('a::attr(href)').getall():
yield response.follow(link, self.parse)
process = CrawlerProcess({
'DEPTH_LIMIT': 2,
'DEPTH_STATS': True
})
process.crawl(DepthSpider)
process.start()
10. Scrapy 和 Scrapy-Redis 有什么区别?为什么选择 Redis 数据库?
- 要点
-
区别:Scrapy 是单机爬虫框架,适用于小规模项目,请求调度和数据存储在本地;Scrapy-Redis 是基于 Scrapy 的分布式爬虫框架,借助 Redis 实现请求的分布式调度和数据共享,可支持大规模分布式爬取。
-
选择 Redis 的原因:Redis 具有高性能、分布式支持、数据结构丰富和持久化等优点,能满足大规模请求的快速调度和处理需求。
python
import scrapy
from scrapy_redis.spiders import RedisSpider
class MyRedisSpider(RedisSpider):
name = "myredisspider"
redis_key = 'myspider:start_urls'
def parse(self, response):
yield {'title': response.css('title::text').get()}
在 Scrapy-Redis 中,可通过 RedisSpider
类创建分布式爬虫,redis_key
指定从 Redis 中获取起始 URL 的键名。多个爬虫节点可以同时从 Redis 中获取请求并进行处理。
友情提示:本文已经整理成文档,可以到如下链接免积分下载阅读
https://download.csdn.net/download/ylfhpy/90422345