Scrapy 是一个基于 Twisted 实现的异步处理爬虫框架,该框架使用纯 Python 语言编写。Scrapy 框架应用广泛,常用于数据采集、网络监测,以及自动化测试等
pip install pywin32
pip install wheel
pip install twisted
pip install scrapy
命令 | 格式 | 说明 |
---|---|---|
startproject | scrapy startproject <项目名> | 创建一个新项目 |
genspider | scrapy genspider <爬虫文件名> <域名> | 新建爬虫文件 |
runspider | scrapy runspider <爬虫文件> | 运行一个爬虫文件,不需要创建项目 |
crawl | scrapy crawl |
运行一个爬虫项目,必须要创建项目 |
list | scrapy list | 列出项目中所有爬虫文件 |
view | scrapy view |
从浏览器中打开 url 地址 |
shell | scrapy shell |
命令行交互模式 |
settings | scrapy settings | 查看当前项目的配置信息 |
引擎(Engine)
引擎
负责控制系统所有组件之间的数据流,并在某些动作发生时触发事件。
调度器(Scheduler)
用来接受引擎
发过来的请求, 压入队列中, 并在引擎
再次请求的时候返回. 可以想像成一个URL的优先级队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
下载器(Downloader)
用于下载网页内容, 并将网页内容返回给EGINE
,下载器是建立在twisted这个高效的异步模型上的
爬虫(Spiders)
是开发人员自定义的类,它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎
,再次进入Scheduler(调度器)
项目管道(Item Pipeline)
在items被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作
下载中间件(Downloader Middlerwares)
你可以当作是一个可以自定义扩展下载功能的组件。
爬虫中间件(Spider Middlerwares)
位于EGINE和SPIDERS之间,主要工作是处理SPIDERS的输入(即responses)和输出(即requests)
引擎
:Hi!Spider
, 你要处理哪一个网站?Spider
:老大要我处理xxxx.com。引擎
:你把第一个需要处理的URL给我吧。Spider
:给你,第一个URL是xxxxxxx.com。引擎
:Hi!调度器
,我这有request请求你帮我排序入队一下。调度器
:好的,正在处理你等一下。引擎
:Hi!调度器
,把你处理好的request请求给我。调度器
:给你,这是我处理好的request引擎
:Hi!下载器,你按照老大的下载中间件
的设置帮我下载一下这个request请求下载器
:好的!给你,这是下载好的东西。(如果失败:sorry,这个request下载失败了。然后引擎
告诉调度器
,这个request下载失败了,你记录一下,我们待会儿再下载)引擎
:Hi!Spider
,这是下载好的东西,并且已经按照老大的下载中间件
处理过了,你自己处理一下(注意!这儿responses默认是交给def parse()
这个函数处理的)Spider
:(处理完毕数据之后对于需要跟进的URL),Hi!引擎
,我这里有两个结果,这个是我需要跟进的URL,还有这个是我获取到的Item数据。引擎
:Hi !管道
我这儿有个item你帮我处理一下!调度器
!这是需要跟进URL你帮我处理下。然后从第四步开始循环,直到获取完老大需要全部信息。管道调度器
:好的,现在就做!注意:只有当
调度器
没有request需要处理时,整个程序才会停止。(对于下载失败的URL,Scrapy也会重新下载。)
本次示例是爬取豆瓣
LOG_LEVEL = "WARNING" # 设置日志等级
from fake_useragent import UserAgent
USER_AGENT = UserAgent().random # 设置请求头
ROBOTSTXT_OBEY = False # 是否遵守 robots 协议,默认为 True
ITEM_PIPELINES = { # 开启管道
'myFirstSpider.pipelines.MyfirstspiderPipeline': 300, # 300 为权重,
'myFirstSpider.pipelines.DoubanPipeline': 301, # 数字越大权重越小
}
在命令行输入:
(scrapy_) D:\programme\Python\scrapy_>scrapy startproject myFirstSpider
(scrapy_) D:\programme\Python\scrapy_>cd myFirstSpider
(scrapy_) D:\programme\Python\scrapy_\myFirstSpider>scrapy genspider douban "douban.com"
定义一个提取的结构化数据(Item)
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html
import scrapy
class MyfirstspiderItem(scrapy.Item): # 可以自己创建一个类,但是要继承 scrapy.Item 类
# define the fields for your item here like:
# name = scrapy.Field()
pass
class DoubanItem(scrapy.Item):
title = scrapy.Field() # 标题
introduce = scrapy.Field() # 介绍
编写爬取网站的 Spider 并提取出结构化数据(Item)
在定义的爬虫文件中写入:
import scrapy
from ..items import DoubanItem # 导入定义的格式化数据
class DoubanSpider(scrapy.Spider):
name = 'douban' # 爬虫的识别名称,唯一的
# allowed_domains = ['douban.com'] # 允许爬取的范围
# start_urls = ['http://douban.com/'] # 最初爬取的 url
start_urls = ['https://movie.douban.com/top250'] # 可以自己定义要爬取的 url
def parse(self, response):
info = response.xpath('//div[@class="info"]')
for i in info:
# 存放电影信息合集
item = DoubanItem()
title = i.xpath("./div[1]/a/span[1]/text()").extract_first() # 获取第一个内容,通过extract方法提取selector对象
introduce = i.xpath("./div[2]/p[1]//text()").extract() # 获取全部内容
introduce = "".join(j.strip() for j in [i.replace("\\xa0", '') for i in introduce]) # 整理信息
item["title"] = title
item["introduce"] = introduce
# 将获取的数据交给 pipeline
yield item
编写 Item Pipelines 来存储提取到的 Item (即结构化数据)
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
# useful for handling different item types with a single interface
from itemadapter import ItemAdapter
class MyfirstspiderPipeline:
def process_item(self, item, spider):
return item
class DoubanPipeline:
# 在爬虫文件开始时,运行此函数
def open_spider(self, spider):
if spider.name == "douban": # 如果数据是从豆瓣爬虫传进来的
print("爬虫开始运行!")
self.fp = open("./douban.txt", "w", encoding="utf-8")
def process_item(self, item, spider):
if spider.name == "douban":
self.fp.write(f"标题:{item['title']}, 信息:{item['introduce']}") # 保存文件
# 爬虫结束的时候运行
def close_spider(self, spider):
if spider.name == "douban":
print("爬虫结束运行!")
self.fp.close()
(scrapy_) D:\programme\Python\scrapy_\myFirstSpider>scrapy crawl douban
日志信息等级:
设置日志信息的制定输出
LOG_LEVEL = "ERROR" # 指定日志信息种类
LOG_FILE = "log.txt" # 表示将日志信息写到指定的文件中进行存储
imoprt logging
logger = logging.getLogger(__name__) # __name__ 获得项目的文件名
logger.warning(" info ") # 打印要输出的日志信息
yield scrapy.Request(url=new_url, callback=self.parse_taoche, meta={"page": page})
参数:
- url:传递的地址
- callback:请求后响应数据的处理函数
- meta:传递数据
- 每次请求都会携带meta参数
- 传递给响应
- 可以通过
response.meta \ response.meta["page"]
获取
import scrapy, logging
from ..items import DetailItem
logger = logging.Logger(__name__)
class DoubanSpider(scrapy.Spider):
name = 'douban'
# allowed_domains = ['douban.com']
start_urls = ['https://movie.douban.com/top250']
def parse(self, response):
print(response)
info = response.xpath('//div[@class="info"]')
for i in info:
item_detail = DetailItem() # 详情页的内容
# 存放电影信息合集
title = i.xpath("./div[1]/a/span[1]/text()").extract_first() # 获取第一个内容,通过extract方法提取selector对象
item_detail["title"] = title
logger.warning(title)
detail_url = i.xpath("./div[1]/a/@href").extract_first() # 获取详情页的url
# print(detail_url)
yield scrapy.Request(url=detail_url, callback=self.parse_detail, meta=item_detail) # 将请求传递给调度器,重新请求
next_url = response.xpath("//div[@class='paginator']/span[3]/a/@href").extract_first() # 获取下一页的url
if next_url:
next_url = "https://movie.douban.com/top250" + next_url
# print(next_url)
yield scrapy.Request(url=next_url, callback=self.parse, ) # 将请求传递给调度器,重新请求
def parse_detail(self, resp):
item = resp.meta # 接收结构化数据
introduce = resp.xpath("//div[@id='link-report']/span[1]/span//text()").extract() # 获取介绍
item["introduce"] = introduce
logger.warning(introduce)
content = resp.xpath("//div[@id='hot-comments']/div[1]//text()").extract() # 获取评论
item["content"] = content
logger.warning(content)
yield item
Scrapy框架中分为两类爬虫:
Spider
CrawlSpider
:
CrawlSpider
是Spider的派生类,Spider类的设计原理是指爬取start_url
列表中的网页,而CrwalSpider
类定义了一些规则来提供跟进链接的方便的机制,从爬取的网页中获取链接并继续爬取的工作更合理创建方法:
scrapy genspider -t crawl 项目名称 网站
创建后,其显示为
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class FuhaoSpider(CrawlSpider):
name = 'fuhao'
# allowed_domains = ['fuhao.com']
start_urls = ['https://www.phb123.com/renwu/fuhao/shishi_1.html']
rules = (
Rule(
LinkExtractor(allow=r'shishi_\d+.html'), # 链接提取器,根据正则规则提取url
callback='parse_item', # 指定回调函数
follow=True # 获取的响应页面是否再次经过rules来进行提取url地址
),
)
def parse_item(self, response):
print(response.request.url)
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True)
:
LinkExtractor
:链接提取器,根据正则规则提取url地址callback
:提取出来的url地址发送请求获取响应,会把响应对象给callback指定的函数进行处理follow
:获取的响应页面是否再次经过rules来进行提取url地址# 匹配豆瓣 start_urls = ['https://movie.douban.com/top250?start=0&filter='] rules = ( Rule(LinkExtractor(allow=r'?start=\d+&filter='), callback='parse_item', follow=True), )
ImagesPipeLine
:图片下载的模块
在pipeline
中,编写代码(已知,item里面传输的是图片的下载地址)
import logging
import scrapy
from itemadapter import ItemAdapter
from scrapy.pipelines.images import ImagesPipeline
# 继承ImagesPipeLine
class PicPipeLine(ImagesPipeline):
# 根据图片地址,发起请求
def get_media_requests(self, item, info):
src = item["src"] # item["src] 里面存储的是图片的地址
logging.warning("正在访问图片:", src)
yield scrapy.Request(url = src,meta={'item':item}) # 对图片发起请求
# 指定图片的名字
def file_path(self, request, response=None, info=None, *, item=None):
item = request.meta['item'] # 接收meta参数
return request.url.split("/")[-1] # 设置文件名字
# 在settings中设置 IMAGES_STORE = "./imags" # 设置图片保存的文件夹
# 返回数据给下一个即将被执行的管道类
def item_completed(self, results, item, info):
return item
更换代理IP,更换Cookies,更换User-Agent,自动重试
在settings.py 中添加
# 建立ip池
PROXY_LIST = []
在middlewares.py中添加
from fake_useragent import UserAgent
import random
class Spider4DownloaderMiddleware:
# 拦截所有请求
def process_request(self, request, spider):
# UA 伪装
request.headers["User-Agent"] = UserAgent().random
return None
# 处理请求,可以篡改响应信息
def process_response(self, request, response, spider):
bro = spider.bro
if request.url in spider.model_urls:
# print(request.url)
# 要篡改request请求的响应对象, response
bro.get(request.url)
# 执行js代码
bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
# 一拉到底,发现我们滚动条还是在中间位置
bottom = [] # 空列表,表示没有到底部
while not bottom: # bool([]) ==> false not false
bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
page_text = bro.page_source # 获取页面内容
# 如果到底,循环结束
bottom = re.findall(r':-\)已经到最后啦~', page_text)
time.sleep(1)
if not bottom:
try:
bro.find_element(By.CSS_SELECTOR, '.load_more_btn').click() # 找到加载更多进行点击
except:
bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
return HtmlResponse(url=request.url, body=page_text, encoding='utf-8', request=request)
return response
# 处理异常,当网络请求失败时,执行此函数
def process_exception(self, request, exception, spider):
# 添加代理ip
type_ = request.url.split(":")[0]
request.meta['proxy'] = f"{type_}://{random.choice(spider.settings.get('PROXY_LIST'))}"
return request # 如果ip被封了,就使用代理ip,重新发送请求
# 开始爬虫时执行
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
设置完下载中间件后,要在settings配置文件中开启
爬虫中间件的用法与下载器中间件非常相似,只是它们的作用对象不同。下载器中间件的作用对象是请求request和返回response;爬虫中间件的作用对象是爬虫,更具体地来说,就是写在spiders文件夹下面的各个文件
yield scrapy.Request()
或者yield item
的时候,爬虫中间件的process_spider_output()
方法被调用Exception
的时候,爬虫中间件的process_spider_exception()
方法被调用parse_xxx()
被调用之前,爬虫中间件的process_spider_input()
方法被调用start_requests()
的时候,爬虫中间件的process_start_requests()
方法被调用import scrapy
class Spider5SpiderMiddleware:
# 在下载器中间件处理完成后,马上要进入某个回调函数parse_xxx()前调用
def process_spider_input(self, response, spider):
return None
# 在爬虫运行yield item或者yield scrapy.Request()的时候调用
def process_spider_output(self, response, result, spider):
for item in result:
print(result)
if isinstance(item, scrapy.Item):
# 这里可以对即将被提交给pipeline的item进行各种操作
print(f'item将会被提交给pipeline')
yield item # 也可以 yield request,当为yield request时,可以修改请求信息,如meta等
# 当在爬虫程序运行过程中报错时调用
def process_spider_exception(self, response, exception, spider):
"""
爬虫里面如果发现了参数错误,就使用raise这个关键字人工抛出一个自定义的异常。在实际爬虫开发中,可以在某些地方故意不使用try ...
except捕获异常,而是让异常直接抛出。例如XPath匹配处理的结果,直接读里面的值,不用先判断列表是否为空。这样如果列表为空,就会被抛出一个IndexError,
于是就能让爬虫的流程进入到爬虫中间件的process_spider_exception()中
"""
print("第%s页出现错误,错误信息:%s" % response.meta["page"], exception) # 这里可以捕获异常信息,也可以有返回值
# 当爬虫运行到start_request时被调用
def process_start_requests(self, start_requests, spider):
for r in start_requests:
print(r.text)
yield r
# 当爬虫开始时调用
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
注意:要在settings配置文件中开启爬虫中间件
在整个框架运作前,需要一个启动条件,这个启动条件就是start_urls,首先从start_urls的网页发起requests请求,才会有后面的调度器、下载器、爬虫、管道的运转。所以,这里我们可以针对start_urls进行网络请求的start_requests方法进行重写,把我们的cookie给携带进去
注意:必须要使用yield返回,不然没办法运行
import scrapy
class ExampleSpider(scrapy.Spider):
name = 'example'
# allowed_domains = ['example.com']
start_urls = ['https://www.baidu.com']
# 重写start_request方法,scrapy从这里开始
def start_requests(self):
# 添加cookie的第一种方法,直接添加
cookie = " "
cookie_dic = {}
for i in cookie.split(";"):
cookie_dic[i.split("=")[0]] = i.split("=")[1]
# 添加cookie的第二种方法:添加头部
headers = {
"cookie": "cookie_info",
# 使用headers传入cookie时,要在settings中加入COOKIES_ENABLE = True
}
for url in self.start_urls:
yield scrapy.Request(url=url, callback=self.parse, headers=headers) # 添加cookies
def parse(self, response):
print(response.text)
通过传递参数,访问接口,来实现模拟登录:
第一种方法的使用方法:
import scrapy
class ExampleSpider(scrapy.Spider):
name = 'example'
# allowed_domains = ['example.com']
start_urls = ['https://github.com']
def parse(self, response):
# 这里面填写大量的登录参数
post_data = {
"username": "lzk",
"password": "123456",
"time": "123",
"sad": "asdsad12",
}
# 把登录参数传入服务器,验证登录
# 方法一
yield scrapy.FormRequest(
url='https://github.com/session',
formdata=post_data,
callback=self.parse_login,
)
def parse_login(self, response):
print(response.text)
第二种方法的使用方法
# -*- coding: utf-8 -*-
import scrapy
from scrapy import FormRequest, Request
class ExampleLoginSpider(scrapy.Spider):
name = "login_"
# allowed_domains = ["example.webscraping.com"]
start_urls = ['http://example.webscraping.com/user/profile']
login_url = 'http://example.webscraping.com/places/default/user/login'
def start_requests(self):
# 重写start_requests方法,用来登录
yield scrapy.Request(
self.login_url,
callback=self.login
)
def login(self,response):
formdata = {
'email': '[email protected]',
'password': '12345678'
}
yield FormRequest.from_response(
response,
formdata=formdata,
callback=self.parse_login
)
def parse_login(self, response):
if 'Welcome Liu' in response.text:
yield from super().start_requests() # 继承start_requests 的作用,访问要访问的页面
def parse(self, response):
print(response.text)
使用
from_response
方法发送请求,等同于selenium
里面的查找表单直接将数据填入表单中,不用考虑加密
概念:
作用:
实现:
pip install scrapy-redis -i https://pypi.com/simple
在settings配置文件中添加
# 使用scrapy_redis的管道,其为定义好的管道,直接调用就可以
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300,
}
# 指定redis地址
REDIS_HOST = '192.168.45.132' # redis服务器地址,我们使用的虚拟机
REDIS_PORT = 6379 # redis端口
# 使用scrapy_redis 的调度器
SCHEDULER = 'scrapy_redis.scheduler.Scheduler'
# 去重容器类配置,作用:redis的set集合,来存储请求的指纹数据,从而实现去重的持久化
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
# 配置调度器是否需要持久化,爬虫结束的时候要不要清空Redis中请求队列和指纹的set集合,要持久化设置为True
SCHEDULER_PERSIST = True
在爬虫文件中添加
import scrapy
from ..items import TaoCheItem
from scrapy_redis.spiders import RedisCrawlSpider
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
# 注意 如果使用的是scrapy.Spider 那么使用redis分布式的时候,就继承 RedisSpider
# 如果是CrawlSpider 就继承 RedisCrawlSpider
class TaocheSpider(RedisCrawlSpider):
name = 'taoche'
# allowed_domains = ['taoche.com']
# start_urls = ['https://changsha.taoche.com/bmw/?page=1'] # 起始的url应该去redis(公共调度器)里面获取
redis_key = 'taoche' # 回去redis里面获取key值为taoche的数据
rules = (
Rule(LinkExtractor(allow=r'/\?page=\d+'), callback='parse_item', follow=True),
)
def parse_item(self, response):
car_list = response.xpath('//div[@id="container_base"]/ul/li')
for car in car_list:
lazyimg = car.xpath('./div[1]/div/a/img/@src').extract_first()
lazyimg = 'https:' + lazyimg
title = car.xpath('./div[2]/a/span/text()').extract_first()
resisted_date = car.xpath('./div[2]/p/i[1]/text()').extract_first()
mileage = car.xpath('./div[2]/p/i[2]/text()').extract_first()
city = car.xpath('./div[2]/p/i[3]/text()').extract_first().replace('\n', '').strip()
price = car.xpath('./div[2]/div[1]/i[1]//text()').extract()
price = ''.join(price)
sail_price = car.xpath('./div[2]/div[1]/i[2]/text()').extract_first()
print(lazyimg, title, resisted_date, mileage, city, price, sail_price)