精通Scrapy网络爬虫 读书笔记

一 初始Scrapy

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7aKLFdYc-1662358929434)(assets/scrapy.jpg)]

  • 安装scrapy

    建议使用python3.6以上版本

    pip install scrapy
    
  • 创建scrapy项目

    scrapy startproject projectname

    scrapy startproject projectname
    

scrapy运行流程

  • 当SPIDER要爬取某URL地址的页面时,需使用该url构造一个Request对象,提交给ENGINE
  • Request对象随后进入SCHEDULER按某种算法进行排队,之后的某个时刻SCHEDULER将其出队,送往DOWNLOADER
  • DOWNLOADER 根据Request对象中的url地址发送一次HTTP请求到网站服务器,之后用服务器返回的HTTP响应构造一个Response对象,其中包含页面的HTML文本
  • Response对象最终会被递送给SPIDER的页面解析函数(构造Request对象时指定)进行处理,页面解析函数从页面中提取数据,封装成Item后提交给ENGINE,Item之后被送往ITEM PIPELINE进行处理,最终可能由EXPORTER以某种数据格式写入文件(csv,json);另一方面,页面解析函数还从页面中提取链接(URL),构造出新的Request对象提交给ENGINE

Request对象

参数描述
url(必选)请求页面的URL
callback回调函数,指定回调函数,若为空,默认parse函数
methodHTTP请求的方法,默认GET
headers请求头,若{‘Cookie’:‘None’},表示禁止发送cookie
bodyHTTP请求的正文,bytes或str类型
cookies传cookie,dict类型
metaRequest元数据字典,dict类型,用于给框架中其他组件传递信息,比如Item Pipeline(request.meta)
encoding默认’utf-8’
priority优先级,默认为0,优先级高的请求优先下载
dont_filter默认False,默认去重,如果需要重复访问,该为True
errback请求出现异常或404时的回调函数

Response对象

属性描述
urlHTTP相应的url,str类型
statusHTTP响应的状态码,int类型,例如200,404
headersHTTP相应的头部,类字典类型,调用方式:response.headers.get(getlist)(‘Content-Type’ | ‘Set-Cookie’)
bodyHTTP响应正文,bytes类型
text文本形式的HTTP响应正文,str类型,由body解码得来,text=body.decode(response.encoding)
encodingHTTP正文编码
request产生该HTTP响应的Request对象
meta传递的元数据
selector
方法
xpath(query)dddd
css(query)
urljoin(url)构造绝对url, 如baidu.com, a/index.html调用后结果为baidu.com/a/index.html

二 使用Item封装数据

当使用scrapy爬取数据时我们一般在items.py文件中数据类,使用Field描述字段,如:

from scrapy import Item,Field
class BookItem(Item):
    bookNames = Field()
    bookAuthors = Field()
  • Item支持字典接口,因此使用数据类和python字典类似,如:BookItem[‘bookNames’]

  • 对字段进行赋值时,BookItem内部会对字段名进行检测,如果赋值一个没有定义的字段,就会抛出KeyError异常

三 使用Item Pipeline 处理数据

Spider获得数据Item会传给数据处理中间件pipelines.py,我们可以在这里进行对数据的清洗、校验、去重和保存

一般的管道:

'''
一般的管道不需要继承别的类
只需要实现几个某些特定的方法,例:
process_item(必须)
open_spider
close_spidern
'''
class CommonPipeline():
    def prcess_item(self,item,spider):
        ...
        ...
        return item
    def open_spider(self,spider):
        ...

    def close_spider(self,spider):
		...        

专门下载处理图片的管道:

# pipelines.py

from scrapy.pipelines.images import ImagesPipeline
from scrapy.exceptions import DropItem
import scrapy,re

class MyImagePipeline(ImagesPipeline):
    # 可以在这里重写文件目录
    # 默认的是url的md5值作为目录名
    def file_path(self,request,response=None,info=None):
        item = request.meta['item']
        folder = strip(item['name'])
        image_guid = request.url.split('/')[-1]
        filename = u"full/{0}/{1}".format(folder,image_guid)
        return filename
	# 图片下载中转站
    def get_media_requests(self, item, info):
        for image_url in item['image_urls']:
            yield scrapy.Request(image_url,meta={'item':item})
	
 	# 图片下载成功或者失败后会进入这个函数
    def item_completed(self, results, item, info):
		image_paths = [x['path'] for ok, x in results if ok]
        if not image_paths:
            raise DropItem("Item contains no images")
        return item

def strip(path):
    """
    :param path: 需要清洗的文件夹名字
    :return: 清洗掉Windows系统非法文件夹名字的字符串
    """
    path = re.sub(r'[?\\*|“<>:/]', '', str(path))
    return path
# items.py
import scrapy

class MyItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    name = scrapy.Field()
    # 默认字段image_urls,需要赋为一个可迭代对象
    image_urls = scrapy.Field()

专门下载文件其他文件的管道:

# pipelines.py

from scrapy.pipelines.images import ImagesPipeline
from scrapy.exceptions import DropItem
import scrapy,re

class MyFilePipeline(ImagesPipeline):
    # 可以在这里重写文件目录
    # 默认的是url的md5值作为目录名
    def file_path(self,request,response=None,info=None):
        item = request.meta['item']
        folder = strip(item['name'])
        file_guid = request.url.split('/')[-1]
        filename = u"full/{0}/{1}".format(folder,file_guid)
        return filename
	# 文件下载中转站
    def get_media_requests(self, item, info):
        for file_urls in item['file_urls']:
            yield scrapy.Request(file_url,meta={'item':item})
	
 	# 文件下载成功或者失败后会进入这个函数
    def item_completed(self, results, item, info):
		file_paths = [x['path'] for ok, x in results if ok]
        if not file_paths:
            raise DropItem("Item contains no images")
        return item

def strip(path):
    """
    :param path: 需要清洗的文件夹名字
    :return: 清洗掉Windows系统非法文件夹名字的字符串
    """
    path = re.sub(r'[?\\*|“<>:/]', '', str(path))
    return path
import scrapy

class MyItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    name = scrapy.Field()
    file_urls = scrapy.Field()

最后,需要在settings.py文件里面配置上写好的管道

# settings.py
# ...
# 文件下载链接失效的天数
# 一般scrapy运行后会把下载url缓存在本地提高效率
FILES_EXPIRES = 30
# 文件下载后保存的地址
FILES_STORE = "D:/download/"
# 图片同理
IMAGES_EXPIRES = 30
IMAGES_STORE = "D:/images/"

# 在这里把写好的管道配置上,value项是执行的先后顺序,越小越先执行
ITEM_PIPELINES = {
    # 如果懒得写pipeline可以用默认的
    'scrapy.pipelines.images.ImagesPipeline':300,
    # 如果写了
    'projectname.pipelines.images.MyImagesPipeline':300,
    # 文件同理
    'scrapy.pipelines.files.FilesPipeline':300,
    'projectname.pipelines.images.MyFilesPipeline':300,
}

# ...
FilesPipelineImagesPipeline
导入路径scrapy.pipelines.files.FilesPipelinescrapy.pipelines.images.ImagesPipeline
Item字段file_urls,filesimage_urls,images
下载目录FILES_STOREIMAGES_STORE

最后还有两个图片下载的小功能:

  • 为图片生成缩略图,在settings.py中添加以下代码

    # 可以自定义尺寸和字段
    # 引擎会自动创建字段文件夹
    IMAGES_THUMBS = {
        'small':(50,50),
        'big':(270,270),
    }
    
  • 过滤尺寸过小的图片(只存大图),在settings.py中添加以下代码

    IMAGES_MIN_WIDTH = 110
    IMAGES_MIN_HEIGHT = 110
    

四 使用LinkExtractor提取数据

主要用于提取大量且复杂的链接

先给出实例代码:

from scrapy.linkextractors import LinkExtractor
class BooksSpider(scrapy.Spider):
    ...
    def parse(self,response):
        ...
        # 提取链接
        # 下一页的 url 在 ul.parse > li.next > a 里面
        le = LinkExtractor(restrict_css='ul.pager li.next')
        # 这里会提取所有链接,返回链接对象列表
        links = le.extract_links(response)
        if links:
            next_url = links[0].url
            yield scrapy.Request(next_url,callback=self.parse)

LinkExtractor构造器的各个参数

参数描述
allow接收一个正则表达式或列表,提取绝对配对url,若为空,则提取全部链接
deny接收一个正则表达式或列表,与allow相反,排除绝对匹配的url
allow_domains接受一个域名或域名列表,提取到指定域的链接
deny_domains接受一个域名或域名列表,排除指定域的链接
restrict_xpaths接受一个xpath表达式或xpath表达式列表,提取符合规则的所有链接
restrict_css接受一个css选择器或css选择器列表,提取符合规则的所有链接
tags接收一个标签(字符串)或列表,提取指定标签内的链接,默认为[‘a’,‘area’]
attrs接收一个属性(字符串)或列表,提取指定属性内的链接,默认为[‘href’]
uniqueTrue/False 链接是否去重
process_value接收func(value)的回调函数,如果传递了该函数,LinkExtractor将调用该回调函数对提取的每一个链接进行处理,回调函数正常情况应返回一个字符串(处理结果),想要抛弃返回None

五 CrawlSpider

这里不多说了,发现一个CSDN博客写的很全,大体上是前一章的补充

很全的网站

补充的网站


六 存入数据库

原理就是写个数据保存的管道

  • SQLite

    SQLite 是一个文本型轻量级数据库,它的处理速度很快,在数据量不是很大的情况下,使用SQLite足够了

    如果使用就不举例了,这里直接给出实例Pipeline代码

    Python标准库自带sqlite3模块,无需安装

    实例代码:pipelines.py

    import sqlite3
    
    class SQLitePipeline(object):
        def open_spider(self,spider):
        	# 读取配置文件中指定的数据库
            db_name = spider.settings.get('SQLITE_DB_NAME','scrapy_defaut.db')
            self.db_conn = sqlite3.connect(db_name)
            self.db_cur = self.db_conn.cursor()
        
        # 关闭数据库连接
        def close_spider(self,spider):
        	self.db_conn.commit()
        	self.db_conn.close()
    
    	def process_spider(self,item,spider):
    		self.insert_db(item)
    
    		return item
    
    	def insert_db(self,item):
    		values = (
    			item['..'],
    			item['..'],
    			...
    
    			)
    		sql = 'INSERT INTO tableName VALUES (?,?,....,)'
    		self.db_cur.execute(sql,values)
    

    settings.py

    SQLITE_DB_NAME = 'scrapy.db'
    ITEM_PIPELINE = {
        'projectname.pipelines.SQLitePipeline':400,
    }
    
  • MySQL

    MySQL 是一个应用极其广泛的关系型数据库,它是开源免费的,可以支持大型数据库,在也个人用户和中小企业首选

    用法同样几乎和sqlite3相同

    安装:

    pip install mysqlclient
    

    实例代码:pipelines.py

    import MySQLdb
    
    class MySQLPipeline(object):
        def open_spider(self,spider):
        	# 读取配置文件中指定的数据库
            db = spider.settings.get('SQLITE_DB_NAME','scrapy_defaut')
            host = spider.settings.get('MYSQL_HOST','localhost')
            port = spider.settings.get('MYSQL_PORT',3306)
            user = spider.settings.get('MYSQL_USER','root')
            passwd = spider.settings.get('MYSQL_PASSWORD','root')
            self.db_conn = MySQLdb.connect(host=host,port=port,db=db,
            					user=user,passwd=passwd,charset='utf8')
            self.db_cur = self.db_conn.cursor()
        
        # 关闭数据库连接
        def close_spider(self,spider):
        	self.db_conn.commit()
        	self.db_conn.close()
    
    	def process_spider(self,item,spider):
    		self.insert_db(item)
    
    		return item
    
    	def insert_db(self,item):
    		values = (
    			item['..'],
    			item['..'],
    			...
    
    			)
    		sql = 'INSERT INTO tableName VALUES (?,?,....,)'
    		self.db_cur.execute(sql,values)
    

    settings.py

    SQLITE_DB_NAME = 'scrapy.db'
    MYSQL_HOST = 'localhost'
    MYSQL_USER = 'root'
    MYSQL_PASSWORD = 'root'
    ITEM_PIPELINES = {
        'projectname.pipelines.MySQLPipeline':401,
    }
    
  • MongoDB

    MongoDB 是一个面向文档的非关系型数据库,功能强大、灵活、易于扩展

    安装:

    pip install pymongo
    

    仿照SQLitePipeline 实现 MongoDBPipeline ,代码如下:

    from pymongo import MongoClient
    from scrapy import Item
    
    class MySQLPipeline(object):
        def open_spider(self,spider):
        	# 读取配置文件中指定的数据库
            db_url = spider.settings.get('MONGODB_URL','mongodb://localhost:27017')
            db_name = spider.settings.get('MONGODB_DB_NAME','scapy_default')
            self.db_clinet = MongoClient('mongodb://localhost:27017')
            self.db = self.db_clinet[db_name]
        
        # 关闭数据库连接
        def close_spider(self,spider):
        	self.db_clinet.close()
    
    	def process_spider(self,item,spider):
    		self.insert_db(item)
    
    		return item
    
    	def insert_db(self,item):
    		if isinstance(item,Item):
    			item = dict(item)
    		self.db.dbname.insert_one(item)
    

    settings.py

    MONGODB_URL = 'mongodb://localhost:27017'
    MONGODB_DB_NAME = 'scrapy.db'
    ITEM_PIPELINES = {
        'projectname.pipelines.MySQLPipeline':403,
    }
    
  • Redis

    Redis 是一个使用 ANSI C 编写的高性能 Key-Value 数据库,使用内存作为主存储,内存中的数据也可以被持久化到硬盘

    安装:

    pip install redis
    

    仿照SQLitePipeline 实现 RedisPipeline ,代码如下:

    import redis
    from scrapy import Item
    
    class MySQLPipeline(object):
        def open_spider(self,spider):
        	# 读取配置文件中指定的数据库
            db_host = spider.settings.get('REDIS_HOST','localhost')
            db_port = spider.settings.get('REDIS_PORT',6379)
            db_index = spider.settings.get('REDIS_DB_INDEX',0)
        	
        	self.db_conn = redis.StrictRedis(host=db_host,port=db_port,db=db_index)
        	self.item_i = 0
        # 关闭数据库连接
        def close_spider(self,spider):
        	self.db_conn.connection_pool.disconnect()
    
    
    	def process_item(self,item,spider):
    		self.insert_db(item)
    
    		return item
    
    	def insert_db(self,item):
    		if isinstance(item,Item):
    			item = dict(item)
    
    		self.item_i += 1
    		self.db_conn.hmset('%s.tableName:%s'%self.item_i,item)
    

    settings.py

    REDIS_HOST = 'localhost'
    REDIS_PORT = 6379
    REDIS_DB_INDEX = 0
    ITEM_PIPELINES = {
        'projectname.pipelines.MySQLPipeline':404,
    }
    

六 使用 Exporter 导出数据

scrapy中负责导出数据的组件叫Exporter,scrapy内部实现了多个Exporter,每个Exporter实现一种数据格式的导出,支持的数据格式如下

  1. JSON
  2. JSON lines
  3. CSV
  4. XML
  5. Pickle
  6. Marshal

大部分情况下只用到前四种,后两种是python特有的,需要其他数据格式自行实现Exporter即可


一般用法:

scrapy crawl projectName -t 导出格式 -o 导出文件名
例:scrapy crawl douban -t csv -o douban.csv

另外,指定带出文件路径时,还可以使用%(name)s和%(time)s两个特殊变量

  • %(name)s 会被替换为Spider的名字
  • %(time)s 会被替换为文件创建时间

几个常用的配置文件:

  • FEED_URL 导出文件路径

    FEED_URL = 'export_data/%(name)s.data'
    
  • FEED_FORMAT 导出文件格式

    FEED_FORMAT = 'csv'
    
  • FEED_EXPORT_ENCODING 导出文件编码 默认json使用数字编码 其他使用utf-8编码

    FEED_EXPORT_ENCODING = 'gbk'
    
  • FEED_EXPORT_FIELDS 导出数据包含的字段 默认情况下导出所有字段 并指定次序

    FEED_EXPORT_FIELDS = ['name','author','price']
    
  • FEED_EXPORTERS 用户自定义的 Exporter 字典,添加新的导出数据格式时使用

    FEED_EXPORTERS = {'excel':'my_project.my_exporters.ExcelItemExporter'}
    

自定义导出数据类型,例:excel

每一个Exporter都继承自BaseItemExporter,BaseItemExporter提供了三个接口供子类实现:

  • exporter_item(self,item)

    负责导出爬取到的每一项数据,参数item 为每一项爬取到的数据,每个子类必须实现该方法

  • start_exporting(self)

    在导出开始时被调用,可在该方法中执行某些初始化工作

  • finish_exporting(self)

    在导出完成时被调用,可在该方法中执行某些清理工作

在项目中创建my_exporters.py,与settings.py同级,代码如下:

from scrapy.exporters import BaseItemExporter
import xlwt
# 这里导入xlwt库对excel数据处理
class ExcelItemExporter(BaseItemExporter):
    def __init__(self,file,**kwargs):
        self._configure(kwargs)
        self.file = file
        self.wbook = xlwt.Workbook()
        self.wsheet = self.wbook.add_sheet('scrapy')
        self.row = 0

    def finish_exporting(self):
        self.wbook.save(self.file)

    def export_item(self,item):
        # 调用基类的方法,获取item所有字段的迭代器
        fields = self._get_serialized_fields(item)
        for col, v in enumerate(x for _,x in fields):
            self.wsheet.write(self.row,col,v)
        self.row += 1

settings.py

# 在设置文件里添加自定义Exporter
FEED_EXPORTERS = {
    'excel':'example.my_exporters.ExcelItemExporter'
}

ps:如果导出csv格式,会在第一行自动加上item字段名,而excel不会加上

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值