Scrapy 是一个用于Python的开源网络爬虫框架,它为编写网络爬虫来抓取网站数据并提取结构化信息提供了一种高效的方法。Scrapy可以用于各种目的的数据抓取,如数据挖掘、监控和自动化测试等。
【1】安装
pip install scrapy
安装成功如下所示:
如果安装过程出错,可以参考下面步骤解决:
# (1) pip install scrapy
# (2) 报错1: building 'twisted.test.raiser' extension
# error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++
# Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
# 解决1
# http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
# Twisted‑20.3.0‑cp37‑cp37m‑win_amd64.whl
# cp是你的python版本
# amd是你的操作系统的版本
# 下载完成之后 使用pip install twisted的路径 安装
# 切记安装完twisted 再次安装scrapy
# (3) 报错2 提示python -m pip install --upgrade pip
# 解决2 运行python -m pip install --upgrade pip
# (4) 报错3 win32的错误
# 解决3 pip install pypiwin32
# (5) anaconda
【2】基础入门
scrapy项目的结构
项目名字
项目名字
spiders文件夹 (存储的是爬虫文件)
init
自定义的爬虫文件 核心功能文件 ****************
init
items 定义数据结构的地方 爬取的数据都包含哪些
middleware 中间件 代理
pipelines 管道 用来处理下载的数据
settings 配置文件 robots协议 ua定义等
创建Scrapy项目
创建一个新的Scrapy项目,你可以在命令行中输入以下命令:
scrapy startproject myproject
这将在当前目录下创建一个名为myproject
的新目录,其中包含了Scrapy项目的结构。
scrapy startproject jane
定义Item
在Scrapy中,Item是被用来保存抓取到的数据的容器。你可以定义自己的Item类,类似于Python字典,但是提供了额外保护机制和便利方法。Item通常定义在items.py
文件中。
class ScrapyDangdangItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
# 通俗的说就是你要下载的数据都有什么
# 图片
src = scrapy.Field()
# 名字
name = scrapy.Field()
# 价格
price = scrapy.Field()
# 详情URL
detail_url = scrapy.Field()
编写Spider
创建爬虫文件, 要在spiders文件夹中去创建爬虫文件: cd 项目的名字\项目的名字\spiders
scrapy genspider 爬虫文件的名字 要爬取网页
# 示例如下
scrapy genspider baidu http://www.baidu.com
一般情况下不需要添加http协议 因为start_urls的值是根据allowed_domains修改的 所以添加了http的话 那么start_urls就需要我们手动去修改了
Spiders是定义如何抓取某个(或某些)网站的类。每个Spider负责处理一个特定的网站,或者一组相关的网页。Spiders通常位于spiders
目录下,并且以.py
文件的形式存在。
class BaiduSpider(scrapy.Spider):
name = "baidu"
allowed_domains = ["www.baidu.com"]
start_urls = ["http://www.baidu.com"]
# 是执行了start_urls之后 执行的方法 方法中的response 就是返回的那个对象
# 相当于 response = urllib.request.urlopen()
# response = requests.get()
def parse(self, response):
# 可以直接使用xpath或BS4
span = response.xpath('//div[@id="filter"]/div[@class="tabs"]/a/span')[0]
print('=======================')
print(span.extract())
response的属性和方法
response.text 获取的是响应的字符串
response.body 获取的是二进制数据
response.xpath 可以直接是xpath方法来解析response中的内容
response.extract() 提取seletor对象的data属性值
response.extract_first() 提取的seletor列表的第一个数据
管道 (Pipeline)
管道是用来处理由Spider抓取并返回的Items的地方。你可以在pipelines.py
中定义如何处理这些Items,比如清洗、验证数据或者将它们存储到数据库中。
设置 (Settings)
Scrapy的行为可以通过修改settings.py
文件来定制,例如设置下载延迟、启用/禁用中间件、更改用户代理等。
运行 Spider
最后,你可以通过命令行运行你的Spider:
scrapy crawl baidu
【3】管道的使用
pipelines管道就是用来处理数据的,如数据清洗、处理、校验、修正以及存储。
如果想使用管道的话 那么就必须在settings中开启管道,可以定义多个管道通过优先级数值来决定执行次序。
from itemadapter import ItemAdapter
class ScrapyDangdangPipeline:
# 在爬虫文件开始的之前就执行的一个方法
def open_spider(self,spider):
self.fp = open('book.json','w',encoding='utf-8')
# item就是yield后面的book对象
def process_item(self, item, spider):
# 以下这种模式不推荐 因为每传递过来一个对象 那么就打开一次文件 对文件的操作过于频繁
# # (1) write方法必须要写一个字符串 而不能是其他的对象
# # (2) w模式 会每一个对象都打开一次文件 覆盖之前的内容
# with open('book.json','a',encoding='utf-8')as fp:
# fp.write(str(item))
self.fp.write(str(item))
return item
# 在爬虫文件执行完之后 执行的方法
def close_spider(self,spider):
self.fp.close()
import urllib.request
# 多条管道开启
# (1) 定义管道类
# (2) 在settings中开启管道
# 'scrapy_dangdang_.pipelines.DangDangDownloadPipeline':301
class DangDangDownloadPipeline:
def process_item(self, item, spider):
url = 'http:' + item.get('src')
filename = './books/' + item.get('name') + '.jpg'
urllib.request.urlretrieve(url = url, filename= filename)
return item
settings.py中开启管道:
ITEM_PIPELINES = {
# 管道可以有很多个 那么管道是有优先级的 优先级的范围是1到1000 值越小优先级越高
'scrapy_dangdang_.pipelines.ScrapyDangdangPipeline': 300,
# DangDangDownloadPipeline
'scrapy_dangdang_.pipelines.DangDangDownloadPipeline':301
}
【4】Scrapy中yield的使用
在 Scrapy 中,yield
有着特别重要的作用,尤其是在 Spider 类中。Scrapy 使用 yield
来返回请求(Request
)和项目(Item
),而不需要将它们全部加载到内存中。这使得 Scrapy 可以高效地处理大量的页面和数据。
在 Scrapy 中,yield
主要用于以下两个场景:
1. 返回 Request
对象
当您需要从一个页面抓取多个链接并发送请求时,可以使用 yield
来逐个返回 Request
对象。Scrapy 会自动处理这些请求,并将响应传递给指定的回调函数。
class DangSpider(scrapy.Spider):
name = 'dang'
start_urls = ['http://category.dangdang.com/cp01.01.02.00.00.00.html']
def parse(self, response):
li_list = response.xpath('//ul[@id="component_59"]/li')
for li in li_list:
detail_url = 'http:' + li.xpath('./a/@href').get()
if detail_url:
yield scrapy.Request(detail_url, callback=self.parse_detail)
def parse_detail(self, response):
review_count = response.xpath('//a[@id="comm_num_down"]/@dd_name').get()
if review_count is None:
review_count = "评论数未找到"
else:
review_count = review_count.strip()
self.logger.info(f"当前图书评论数为: {review_count}")
在这个例子中,parse
方法会遍历列表页中的每个书籍链接,并使用 yield
逐个返回 Request
对象。Scrapy 会依次处理这些请求,并将详情页的响应传递给 parse_detail
方法。
2. 返回 Item
对象
当您从页面中提取数据并构建 Item
时,可以使用 yield
将 Item
返回给 Scrapy 的管道进行进一步处理(如保存到数据库、导出为文件等)。
class DangSpider(scrapy.Spider):
name = 'dang'
start_urls = ['http://category.dangdang.com/cp01.01.02.00.00.00.html']
def parse(self, response):
li_list = response.xpath('//ul[@id="component_59"]/li')
for li in li_list:
src = li.xpath('.//img/@data-original').get() or li.xpath('.//img/@src').get()
name = li.xpath('.//img/@alt').get()
price = li.xpath('.//p[@class="price"]/span[1]/text()').get()
detail_url = 'http:' + li.xpath('./a/@href').get()
if not src or not name or not price or not detail_url:
self.logger.warning("缺少关键信息,跳过此条目")
continue
book_info = {
'src': src,
'name': name,
'price': price,
'detail_url': detail_url
}
yield scrapy.Request(detail_url, callback=self.parse_detail, cb_kwargs={'book_info': book_info})
def parse_detail(self, response, book_info):
review_count = response.xpath('//a[@id="comm_num_down"]/@dd_name').get()
if review_count is None:
review_count = "评论数未找到"
else:
review_count = review_count.strip()
book_info['review_count'] = review_count
book_item = ScrapyDangdangItem(**book_info)
self.logger.info(f"抓取到完整图书信息: {book_item}")
yield book_item
在这个例子中,parse_detail
方法会从详情页提取评论数,并将其添加到 book_info
字典中。然后,它会创建一个 ScrapyDangdangItem
实例,并使用 yield
将其返回给 Scrapy 的管道。
【5】parse方法之间如何传参?
比如,需要先获取分类列表,然后获取每一个详情,最后整合获取得到book信息。
在 Scrapy 中,通常情况下,您会希望将从详情页获取的数据(如评论数)与列表页获取的数据(如书名、价格等)结合起来,形成一个完整的 Item。为了实现这一点,您可以使用 Request
的 meta
参数来传递数据,或者使用 cb_kwargs
参数(Scrapy 1.7+)来传递关键字参数。
方法 1: 使用 meta
参数
meta
参数允许您在请求之间传递数据。您可以在 parse
方法中将书籍的基本信息(如书名、价格、图片链接等)通过 meta
传递给 parse_detail
方法,然后在 parse_detail
中提取评论数并返回一个完整的 Item。
修改后的代码:
import scrapy
from ..items import ScrapyDangdangItem # 确保这里正确导入了Item
class DangSpider(scrapy.Spider):
name = 'dang'
allowed_domains = ['dangdang.com'] # 放宽域名限制
start_urls = ['http://category.dangdang.com/cp01.01.02.00.00.00.html']
base_url = 'http://category.dangdang.com/pg'
page = 1
def parse(self, response):
li_list = response.xpath('//ul[@id="component_59"]/li')
for li in li_list:
src = li.xpath('.//img/@data-original').get() or li.xpath('.//img/@src').get()
name = li.xpath('.//img/@alt').get()
price = li.xpath('.//p[@class="price"]/span[1]/text()').get()
detail_url = 'http:' + li.xpath('./a/@href').get()
if not src or not name or not price or not detail_url:
self.logger.warning("缺少关键信息,跳过此条目")
continue
# 构建基础的图书信息
book_info = {
'src': src,
'name': name,
'price': price,
'detail_url': detail_url
}
# 发送详情页请求,并通过 meta 传递图书信息
yield scrapy.Request(detail_url, callback=self.parse_detail, meta={'book_info': book_info})
def parse_detail(self, response):
# 从 meta 中获取图书信息
book_info = response.meta['book_info']
# 提取评论数
review_count = response.xpath('//a[@id="comm_num_down"]/@dd_name').get()
if review_count is None:
review_count = "评论数未找到"
else:
review_count = review_count.strip()
# 将评论数添加到图书信息中
book_info['review_count'] = review_count
# 创建并返回完整的 Item
book_item = ScrapyDangdangItem(**book_info)
self.logger.info(f"抓取到完整图书信息: {book_item}")
yield book_item
方法 2: 使用 cb_kwargs
参数 (Scrapy 1.7+)
cb_kwargs
是 Scrapy 1.7 版本引入的一个新特性,它允许您直接在 Request
中传递关键字参数,而不需要通过 meta
。这种方式更加直观和简洁。
修改后的代码:
import scrapy
from ..items import ScrapyDangdangItem # 确保这里正确导入了Item
class DangSpider(scrapy.Spider):
name = 'dang'
allowed_domains = ['dangdang.com'] # 放宽域名限制
start_urls = ['http://category.dangdang.com/cp01.01.02.00.00.00.html']
base_url = 'http://category.dangdang.com/pg'
page = 1
def parse(self, response):
li_list = response.xpath('//ul[@id="component_59"]/li')
for li in li_list:
src = li.xpath('.//img/@data-original').get() or li.xpath('.//img/@src').get()
name = li.xpath('.//img/@alt').get()
price = li.xpath('.//p[@class="price"]/span[1]/text()').get()
detail_url = 'http:' + li.xpath('./a/@href').get()
if not src or not name or not price or not detail_url:
self.logger.warning("缺少关键信息,跳过此条目")
continue
# 构建基础的图书信息
book_info = {
'src': src,
'name': name,
'price': price,
'detail_url': detail_url
}
# 发送详情页请求,并通过 cb_kwargs 传递图书信息
yield scrapy.Request(detail_url, callback=self.parse_detail, cb_kwargs={'book_info': book_info})
def parse_detail(self, response, book_info):
# 提取评论数
review_count = response.xpath('//a[@id="comm_num_down"]/@dd_name').get()
if review_count is None:
review_count = "评论数未找到"
else:
review_count = review_count.strip()
# 将评论数添加到图书信息中
book_info['review_count'] = review_count
# 创建并返回完整的 Item
book_item = ScrapyDangdangItem(**book_info)
self.logger.info(f"抓取到完整图书信息: {book_item}")
yield book_item
关键点解释
-
meta
参数:- 在
parse
方法中,我们构建了一个包含图书基本信息的字典book_info
。 - 使用
meta
参数将book_info
传递给parse_detail
方法。 - 在
parse_detail
中,通过response.meta['book_info']
获取传递过来的图书信息。
- 在
-
cb_kwargs
参数:- 在
parse
方法中,我们同样构建了一个包含图书基本信息的字典book_info
。 - 使用
cb_kwargs
参数将book_info
作为关键字参数传递给parse_detail
方法。 - 在
parse_detail
中,直接通过函数参数book_info
获取传递过来的图书信息。
- 在
-
合并数据:
- 在
parse_detail
中,我们提取了评论数,并将其添加到book_info
字典中。 - 最后,我们创建了一个
ScrapyDangdangItem
实例,并将其返回给 Scrapy 的管道进行处理。
- 在
【6】管道中使用pymysql存储数据
from itemadapter import ItemAdapter
class ScrapyReadbookPipeline:
def open_spider(self,spider):
self.fp = open('book.json','w',encoding='utf-8')
def process_item(self, item, spider):
self.fp.write(str(item))
return item
def close_spider(self,spider):
self.fp.close()
# 加载settings文件
from scrapy.utils.project import get_project_settings
import pymysql
class MysqlPipeline:
def open_spider(self,spider):
settings = get_project_settings()
self.host = settings['DB_HOST']
self.port =settings['DB_PORT']
self.user =settings['DB_USER']
self.password =settings['DB_PASSWROD']
self.name =settings['DB_NAME']
self.charset =settings['DB_CHARSET']
self.connect()
def connect(self):
self.conn = pymysql.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
db=self.name,
charset=self.charset
)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
sql = 'insert into book(name,src) values("{}","{}")'.format(item['name'],item['src'])
# 执行sql语句
self.cursor.execute(sql)
# 提交
self.conn.commit()
return item
def close_spider(self,spider):
self.cursor.close()
self.conn.close()
数据库配置信息在settings.py中:
【7】日志级别与存储路径
settings.py中配置日志级别与路径即可:
# 指定日志的级别
# LOG_LEVEL='WARNING'
LOG_FILE = 'logdemo.log'
【8】POST请求
如下所示是GET请求:
yield scrapy.Request(url=detail_url, callback=self.parse_detail)
重写start_requests方法使用FormRequest发送POST请求
class TestpostSpider(scrapy.Spider):
name = 'testpost'
allowed_domains = ['https://fanyi.baidu.com/sug']
# post请求 如果没有参数 那么这个请求将没有任何意义
# 所以start_urls 也没有用了
# parse方法也没有用了
# start_urls = ['https://fanyi.baidu.com/sug/']
#
# def parse(self, response):
# pass
def start_requests(self):
url = 'https://fanyi.baidu.com/sug'
data = {
'kw': 'final'
}
yield scrapy.FormRequest(url=url,formdata=data,callback=self.parse_second)
def parse_second(self,response):
content = response.text
obj = json.loads(content,encoding='utf-8')
print(obj)
【9】与xpath结合使用
如下图所示,想要提取week内容
dl_weather = response.xpath('//dl[@class="weather_info"]')
dl_weather_xpath = dl_weather.xpath('//dd[@class="week"]')
# 这里获取的是Selector 数组,data为目标html 元素
print("dl_weather.xpath('//dd[@class='week\"]')", dl_weather_xpath)
# 这里获取的是Selector 数组,data为目标html 元素的文本内容
weather_xpath = dl_weather.xpath('//dd[@class="week"]/text()')
print("dl_weather.xpath('//dd[@class='week\"]/text()')", weather_xpath)
# 这里获取的是文本数组
week__extract = weather_xpath.extract()
# extract_first表示获取第一个文本节点
print("weather_xpath.extract_first() :",weather_xpath.extract_first())
print("weather_xpath.extract() :",week__extract)
结果如下所示:
dl_weather.xpath('//dd[@class='week"]') [<Selector query='//dd[@class="week"]' data='<dd class="week">2024年12月16日\u3000星期一\u3000甲辰年冬...'>]
dl_weather.xpath('//dd[@class='week"]/text()') [<Selector query='//dd[@class="week"]/text()' data='2024年12月16日\u3000星期一\u3000甲辰年冬月十六 '>]
weather_xpath.extract_first() : 2024年12月16日 星期一 甲辰年冬月十六
weather_xpath.extract() : ['2024年12月16日\u3000星期一\u3000甲辰年冬月十六 ']
{'date': ['2024年12月16日\u3000星期一\u3000甲辰年冬月十六 ']}