13. 爬虫之Scrapy框架的使用总结
介绍
scrapy 是一个开源和协作的框架,用于爬虫。
以快速、简单、可扩展的方式。
项目地址:https://github.com/scrapy/scrapy
官方网站:https://scrapy.org/
官方文档:https://docs.scrapy.org/en/latest/
官方教程:https://docs.scrapy.org/en/latest/intro/tutorial.html
架构:
scrapy-redis架构:
安装
基于 1.5.0
使用 pip install scrapy
各个模块
(1)settings,配置文件
所有的key 都是全部大写!
BOT_NAME = 'firstscrapy' # 项目名
ROBOTSTXT_OBEY = Ture,是否遵守 robots.txt, 修改为False
DEFAULT_REQUEST_HEADERS : 默认的请求headers
PROXIES = [
{'ip_port': '111.8.60.9:8123', 'user_pass': ''},
{'ip_port': '101.71.27.120:80', 'user_pass': ''},
] # 代理 ,在downloder middleware 的 代理中间件中 proxy = random.choice(PROXIES) request.meta['proxy'] = "http://%s" % proxy['ip_port']
SPIDER_MIDDLEWARES:爬虫中间层
DOWNLOADER_MIDDLEWARES:下载中间层
ITEM_PIPELINES = {'项目名.pipelines.PipeLine类名': 300,}
# 开发模式时,把下面的注释取消掉,启用缓存,可以提高调试效率
# 同样的请求,如果 缓存 当中有保存内容的话,不会去进行网络请求,直接从缓存中返回
# 记住:开发环境下启用!!!!部署时一定要注释掉!!!
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
# 日志管理
LOG_ENABLED 默认: True,启用logging
LOG_ENCODING 默认: 'utf-8',logging使用的编码
LOG_FILE 默认: None,在当前目录里创建logging输出文件的文件名,例如:LOG_FILE = 'log.txt'
配置了这个文件,就不会在控制台输出日志了
LOG_LEVEL 默认: 'DEBUG',log的最低级别,共五级:
CRITICAL - 严重错误
ERROR - 一般错误
WARNING - 警告信息
INFO - 一般信息 print 属于这个 info
DEBUG - 调试信息
LOG_STDOUT 默认: False 如果为 True,进程所有的标准输出(及错误)将会被重定向到log中。
例如,执行 print("hello") ,其将会在Scrapy log中显示
# 并发 ,现在 = 号右边的 value 就是默认值
CONCURRENT_ITEMS = 100 # 并发处理 items 的最大数量
CONCURRENT_REQUESTS = 16 # 并发下载request页面的最大数量
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # 并发下载任何单域的最大数量, baidu.com , sina.cn 各 8 个
CONCURRENT_REQUESTS_PER_IP = 0 # 并发 每个IP 请求的最大数量,
DOWNLOAD_DELAY = 0.25 # 单位秒,支持小数,一般都是随机范围:
0.5*DOWNLOAD_DELAY 到 1.5*DOWNLOAD_DELAY 之间
CONCURRENT_REQUESTS_PER_IP 不为0时,这个延时是针对每个IP,而不是每个域
(2)爬虫类(spider怎么写)
继承自scrapy.Spider,用于构造Request对象给Scheduler
属性
name:爬虫的名字,必须唯一 ,必须写!
start_urls:爬虫初始爬取的链接列表
custom_settings = {} # 自定义的setting配置
方法
start_requests:启动爬虫的时候调用,爬取urls的链接,可以省略
parse:response到达spider的时候默认调用,如果在Request对象配置了callback函数,则不会调用,
parse方法可以迭代返回Item或Request对象,如果返回Request对象,则会进行增量爬取
items
定义:
class Product(scrapy.Item):
name = scrapy.Field()
title = scrapy.Field()
调用:
和dict一样的调用
product = Product( , title='pc title')
# 像字典一样的使用:
print(product['name'])
print(product.get('name'))
product['title'] = 'new title'
可以这样转换为字典:dict(product)
pipelines
必须在settings中,添加
ITEM_PIPELINES = {
'first_scrapy.pipelines.FirstScrapyPipeline': 300, # 优先级,数字越小,
优先级越高,越早调用范围 0-1000
}
对象如下:
class FirstScrapyPipeline(object):
def process_item(self, item, spider):
return item
方法:
process_item(self, item, spider): 处理item的方法, 必须有的!!!
参数:
item (Item object or a dict) : 获取到的item
spider (Spider object) : 获取到item的spider
返回 一个dict或者item
open_spider(self, spider) : 当spider启动时,调用这个方法
参数:
spider (Spider object) – 启动的spider
close_spider(self, spider): 当spider关闭时,调用这个方法
参数:
spider (Spider object) – 关闭的spider
@classmethod
from_crawler(cls, crawler)
参数:
crawler (Crawler object) – 使用这个pipe的爬虫crawler
4.1 返回 item 的例子:
from scrapy.exceptions import DropItem
class PricePipeline(object):
vat_factor = 1.15
def process_item(self, item, spider):
if item['price']:
if item['price_excludes_vat']:
item['price'] = item['price'] * self.vat_factor
return item
else:
raise DropItem("Missing price in %s" % item)
4.2 写入文件的例子:
import json
class JsonWriterPipeline(object):
def open_spider(self, spider):
self.file = open('items.jl', 'w')
def close_spider(self, spider):
self.file.close()
def process_item(self, item, spider):
line = json.dumps(dict(item)) + "\n"
self.file.write(line)
return item
4.3 写入 mongodb 的例子:
import pymongo
class MongoPipeline(object):
collection_name = 'scrapy_items'
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
# 必须在settings中 配置 MONGO_URI 和 MONGO_DATABASE
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
# items 是默认值,如果settings当中没有配置 MONGO_DATABASE ,那么 mongo_db = 'items'
mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
)
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[self.collection_name].insert_one(dict(item))
return item
4.4 网页快照的例子
import scrapy
import hashlib
from urllib.parse import quote
class ScreenshotPipeline(object):
"""Pipeline that uses Splash to render screenshot of
every Scrapy item."""
SPLASH_URL = "http://localhost:8050/render.png?url={}"
def process_item(self, item, spider):
encoded_item_url = quote(item["url"])
screenshot_url = self.SPLASH_URL.format(encoded_item_url)
request = scrapy.Request(screenshot_url)
dfd = spider.crawler.engine.download(request, spider)
dfd.addBoth(self.return_item, item)
return dfd
def return_item(self, response, item):
if response.status != 200:
# Error happened, return item.
return item
# Save screenshot to file, filename will be hash of url.
url = item["url"]
url_hash = hashlib.md5(url.encode("utf8")).hexdigest()
filename = "{}.png".format(url_hash)
with open(filename, "wb") as f:
f.write(response.body)
# Store filename in item.
item["screenshot_filename"] = filename
return item
4.5 去重复值的pipe
from scrapy.exceptions import DropItem
class DuplicatesPipeline(object):
def __init__(self):
self.ids_seen = set()
def process_item(self, item, spider):
if item['id'] in self.ids_seen:
raise DropItem("Duplicate item found: %s" % item)
else:
self.ids_seen.add(item['id'])
return item
第一个项目
1、在命令行中,切换到 项目目录:譬如,f:\py_study>
执行命令:scrapy startproject first_scrapy
将在 f:\py_study 路径下建立一个 first_scrapy 项目文件夹
文件夹结构如下:
scrapy.cfg # 部署的配置文件,不需要修改
first_scrapy/
__init__.py
items.py # 类一定要继承scrapy.Item,定义我们需要的结构化数据,和ORM有点类似,使用相当于dict
first_item = FirstscrapyItem()
name = first_item['name']
name1 = first_item.get('name')
first_item['name'] = 'lucy'
# 可以转换为字典
dict_item = dict(first_item)
# 格式是: dict_item = {name:'terry', age:10, sex:'1'}
middlewares.py # 中间件,相当于钩子,可以对爬取前后做预处理,如修改请求header,url过滤等
分 downloadermiddleware、spidermiddleware
pipelines.py # 数据处理,将items中结构化的数据进行处理
settings.py # 项目配置文件,key=value 的方式,key必须全部大写!
包括所有的配置,一些公用的常量也可以写在里面
spiders/ # 爬虫模块的目录,负责配置需要爬取的数据和爬取规则,以及解析数据,
并且把结构化数据,return 到 pipelines 模块处理
__init__.py
2、在 spiders 中新建:
quotes.py:.
import scrapy
# 必须继承 scrapy.Spider
class QuotesSpider(scrapy.Spider):
# 用于区别Spider。 该名字必须是唯一的,您不可以为不同的Spider设定相同的名字。
name = "quotes"
def start_requests(self):
# 包含了Spider在启动时进行爬取的url列表。 因此,第一个被获取到的页面将是其中之一。
# 后续的URL则从初始的URL获取到的数据中提取。
urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
for url in urls:
# 必须使用 yield
# Request 是 scrapy 自定义的类
# callback, 获取到response 之后的 回调函数
yield scrapy.Request(url=url, callback=self.parse)
# 是spider的一个方法。 被调用时,每个初始URL完成下载后生成的 Response 对象将会作为唯一的参数传递给该函数。
def parse(self, response):
page = response.url.split("/")[-2]
filename = 'quotes-%s.html' % page
with open(filename, 'wb') as f:
f.write(response.body)
self.log('Saved file %s' % filename)
或
class BlogSpider(scrapy.Spider):
name = 'blogspider'
# 允许访问的域名,可以不写
allowed_domains = ['scrapinghub.com']
start_urls = ['https://blog.scrapinghub.com']
def parse(self, response):
for title in response.css('h2.entry-title'):
yield {'title': title.css('a ::text').extract_first()}
for next_page in response.css('div.prev-post > a'):
yield response.follow(next_page, self.parse)
补充:
数据流,所有中间件都启动:
1、启动 spiders
2、spiders 包装一个request, 发送到 scheduler
2.1 request进入 scheduler 之前,会先到 scheduler middleware 进行处理,
处理后,再发送给 scheduler
3、scheduler 接收到 request 后,将之放入到一个 队列
4、engine 向 scheduler 申请 request,得到后,将request
发送给 downloader
4.1 request 从 scheduler 出来后,先到 scheduler middeware 进行处理,
再传给 scheduler middeware
4.2 request 从 scheduler middeware 出来后,需要到 downloader middleware
进行处理,再传给 downloader
5、downloader 接收到 request 之后,访问对应的http资源,接收 response
6、downloader 接收到 response 之后,将 response 发送给 spider
6.1 response 从 downloader 出来后,先经过 downloader middleware 处理
6.2 response 从 ownloader middleware 处理后,经过 spider middleware 处理,
再 传给 spider
7、spider 接收到reponse之后,解析内容,
7.1 url 资源,需要再次请求的,继续包装成 request ,继续第二个步骤
7.2 非 url 资源,就是 数据,结构化成 item ,向后传
8、item 从 spider 出来后,经过 spider middleware ,处理后 ,发送给
item pipeline
9、pipeline 接收到 item 后,读取其中的数据,进行数据持久化
运行
1、命令行中运行:
命令行 中 进入到 first_scrapy 目录中,执行:
scrapy crawl quotes
quotes是spider类中的 name 属性
2、pycharm 运行
在 项目 根目录 添加 run.py 文件:
from first_scrapy.spiders.quotes import QuotesSpider
from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings
# 获取settings.py模块的设置
settings = get_project_settings()
process = CrawlerProcess(settings=settings)
# 可以添加多个spider
# process.crawl(Spider1)
# process.crawl(Spider2)
process.crawl(QuotesSpider)
# 启动爬虫,会阻塞,直到爬取完成
process.start()
或者:
from scrapy.cmdline import execute
#设置工程命令
import sys
import os
#设置工程路径,在cmd 命令更改路径而执行scrapy命令调试
#获取run文件的父目录,os.path.abspath(__file__) 为__file__文件目录
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
execute(["scrapy","crawl","quotes" ])
spider的 parse
1、ItemLoader
def parse(self, response):
# text = response.text
# tree = etree.HTML(text)
# product = Product()
# product['name'] = tree.xpath('//div[@class="product_name"]')[0].text + tree.xpath('//div[@class="product_title"]')[0].text
# product['price'] = tree.xpath('//p[@id="price"]')[0].text
# product['stock'] = tree.xpath('//p#stock')[0].text
# product['last_updated'] = 'today'
# return product
l = ItemLoader(item=Product(), response=response)
l.add_xpath('name', '//div[@class="product_name"]')
l.add_xpath('title', '//div[@class="product_title"]')
l.add_xpath('price', '//p[@id="price"]')
l.add_css('stock', 'p#stock')
l.add_value('last_updated', 'today') # you can also use literal values
return l.load_item()
2、selector
response.selector.xpath('//span/text()').extract()
# 结果是 list,但是一般情况我们都是只取到一个值,如 ['想要的值'], extract_first 取第一个
response.css('title::text')
response.css('img').xpath('@src').extract()
response.xpath('//div[@id="not-exists"]/text()').extract_first()
response.xpath('//div[@id="not-exists"]/text()').extract_first(default='not-found')
response.xpath('//a[contains(@href, "image")]/text()').re(r'Name:\s*(.*)')
3、meta
yield Request(novel_url, self.parse_next, meta={'name':name, 'age':age})
在 parse_next 中 通过 response.meta['name'] 获取参数
传递参数到下一个 parse_next 函数
middleware
1、settings中配置
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.CustomDownloaderMiddleware': 543,
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, # 不启用
}
2、process_request(request, spider)
2.1 如果其返回 None ,Scrapy将继续处理该request,执行其他的中间件的相应方法,
直到合适的下载器处理函数(download handler)被调用, 该request被执行(其response被下载)。
2.2 如果其返回 Response 对象,Scrapy将不会调用 任何 其他的 process_request() 或 process_exception() 方法,
或相应地下载函数; 其将返回该response。 已安装的中间件的 process_response() 方法则会在每个response返回时被调用。
2.3 如果其返回 Request 对象,Scrapy则停止调用 process_request方法并重新调度返回的request。
当新返回的request被执行后, 相应地中间件链将会根据下载的response被调用。
2.4 如果其raise一个 IgnoreRequest 异常,则安装的下载中间件的 process_exception() 方法会被调用。
如果没有任何一个方法处理该异常, 则request的errback(Request.errback)方法会被调用。
如果没有代码处理抛出的异常, 则该异常被忽略且不记录(不同于其他异常那样)。
3、process_response(request, response, spider)
3.1 如果其返回一个 Response (可以与传入的response相同,也可以是全新的对象),
该response会被在链中的其他中间件的 process_response() 方法处理。
3.2 如果其返回一个 Request 对象,则中间件链停止, 返回的request会被重新调度下载。
处理类似于 process_request() 返回request所做的那样。
3.3 如果其抛出一个 IgnoreRequest 异常,则调用request的errback(Request.errback)。
如果没有代码处理抛出的异常,则该异常被忽略且不记录(不同于其他异常那样)。
4、process_exception(request, exception, spider)
5、from_crawler(cls, crawler)
笔记1:
1. 幂等函数&纯函数
幂等函数: 只要输入参数不变,返回值永远一样,不管执行多少次
纯函数 : 首先必须是幂等函数,其次不会和环境有任何交互, 不能执行的操作包括:外部变量、print、数据库操作
2、request请求中的 callback 参数
绝大部分情况下,是可以不用提交的!!!
3、在 python 下运行
1、import scrapy 出错!
2、提示错误是 from .. import etree 出错, 查看错误可以找到是 lxml 出问题了
3、查看 lxml 是否安装: pip install lxml , 提示我们已经安装
4、继续运行 python 命令, 执行 : from lxml import etree ,继续提示错误
5、基本可以确定是 lxml 库安装有问题!!!
6、使用卸载命令: pip uninstall lxml
7、重新安装lxml: pip install lxml
8、问题解决
4、常见的配置文件
ini、xml、config、conf、cfg等
python项目中,一般使用 .py 作为配置文件,譬如: settings.py
项目:顶点小说
需求:
1、访问 https://www.23us.so/
2、访问 从玄幻魔法 到 其他小说 的 9个类别的 小说
3、获取第二步中 9 个类别 的所有小说信息:
小说名
类别
作者
状态
全文长度
最后更新时间
4、保存到 mongodb
5、使用 scrapy 框架实现
实现:
1、建立一个 scrapy 项目
2、修改 settings
3、分析目标网站
类似这样的小说网站,一般都全部是 get 请求, 不需要抓包即可实现功能
如果是其他类型的网站,需要抓包进行分析===》对比拉勾网
4、建立 spider
5、建立 item
6、修改 settings中的 pipeline 的配置
7、建立 pipeline
笔记2
1、scrapy框架的 response
去属性和文本,都只能通过xpath表达式,不像 lxml中 有 .text 和 get('属性名')
获取一个值(多个值时取第一个值):
response.xpath('').extract_first()
获取列表:
response.xpath('').extract()
2、scrapy框架 Request 对象的参数
常用:
url : 网页地址
callback : 回调处理response的函数
meta: 字典,用于 上下多个函数之间传递参数
dont_filter: 默认是 False, 是过滤重复的!!如果不顾虑设置为 True
偶尔用:
headers: 一般是哟用 settings 中的默认headers, 有些请求需要使用 Referer
errback: 错误处理函数,类似 callback, 一般不用
不用:
method: http协议的 请求 method,默认是GET, post请求一般直接使用 FormRequest
body: 请求体, 等同 上面的 method, 就是charles中的raw 视图下看到的 body 内容, 传的就是 字符串
cookies: cookies,一般使用不上
encoding: 默认 UTF-8
priority: 默认 0, 调度器调度时随机的, 其实根据这个优先级选择的
flags: 几乎用不上
3、downloader middleware
process_request
4种返回值:
None:
如果当前中间层,后续还有其他中间层,那么将 request对象发送给下一个中间层的 process_request 进行处理
如果当前中间层是最后一个中间层了,那么 将 request对象 发送给 downloader 进行处理
Request:
将 返回的 request 冲入放入到 请求队列 中,重新接受 scheduler 的调度
相当于在 spider 中, yield request 一样的作用
Response:
一般自己构建一个 response
中间层就不会继续往后执行,而是反过来,往前执行
相当于已经从 downloader 中 获得了 response
假设 6个 中间层, 顺序是 1到 6 ,如果你在 中间层3 就 return response
那么,接下操作的就是 中间层2 的 process_response 接受 这个 response
raise 异常:
会调用 本中间层 的 process_exception
4、scrapy框架提交 请求
GET: scrapy.Request
POST: scrapy.FormRequest , 比 Request 多了一个 formdata, 传的参数是字典
data = {}
scrapy.FormRequest(url, callback=self.parse, formdata=data)
如果使用 response.follow 来构造请求对象的话,提交post请求
yield response.follow(url, callback=self.parse_list, method='POST', body='请求体的字符串')
5、密码学相关的知识
拖库:不管使用什么手段,得到某一个应用的数据库,把这个数据库下载了
洗库:将拖库得到的数据库进行清洗,整理,分类
撞库:将拖库得到的 账号、密码, 进行多个其他应用的尝试
彩虹表: 将 某一个组合(6到16位,数字+大小写字母 组成的字符串)的 可能性, 穷举, 使用某一个 加密手段进行加密
保存到一个数据库的表中, 2个字段, 原始密码、加密后的密码
盐:特殊的字符串, 在进行 密码的加密之前, 将 原始的密码和 这个盐,进行组合 盐+原始密码 ,
将 组合后 的 字符串 进行加密, 保存 加密后的密码串
爬虫中,对js加密手段的处理
1、找到 加密方式
2、找到 加密的盐
3、找到上述2个信息后,在python代码实现一个函数, 达到 js 加密函数一样的加密效果,那么在我们的爬虫程序中,
就可以提交,同样的加密后的 密码串
6、如何查找js代码中的加密函数
譬如:百度的
password r9Xu1+WwLLgZZ0MG5L2PKPnXyuoJqxoZh3RNgwb0yVzwoMqEid6IqxrzDQE+roUtgXmzINwWFII/0kqc4yk1wbtD355ydG2ADLxJ087ChcKzt7jocUD3FaylxtQ+a6WKfrSwOI7SAyfXqlCuA8zWL6nN5OwzYN1Y40NJUfFpCII=
搜索 value ,肯定是找不到的!因为是加密算出来的
不能搜索 value,那么就搜索 key
因为这种加密算法, 终究在 js 代码中,会有 类似这样的代码存在:
password=加密函数()
搜索范围必然是: response body
1、搜索 password ,会得到很多值
2、 搜索 password= , 因为所有的js基本都会进行压缩, 所以一般都直接搜索 password= 可以得到
但是少部分, 得搜索 'password ='
还有部分网站, 搜索 password 搜索不到, password 这个key 是拼接得来的 'pass' + 'word'
项目:拉勾网
1、拉勾网的岗位搜索
2、登录拉钩:
url: https://passport.lagou.com/login/login.html
3、登录成功后,搜索
python爬虫
python工程师
4、获取如下岗位信息
岗位名
薪资
公司
发布时间
工作经验
5、用 scrapy 项目实现