一 初始Scrapy
-
安装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函数 |
method | HTTP请求的方法,默认GET |
headers | 请求头,若{‘Cookie’:‘None’},表示禁止发送cookie |
body | HTTP请求的正文,bytes或str类型 |
cookies | 传cookie,dict类型 |
meta | Request元数据字典,dict类型,用于给框架中其他组件传递信息,比如Item Pipeline(request.meta) |
encoding | 默认’utf-8’ |
priority | 优先级,默认为0,优先级高的请求优先下载 |
dont_filter | 默认False,默认去重,如果需要重复访问,该为True |
errback | 请求出现异常或404时的回调函数 |
Response对象
属性 | 描述 |
---|---|
url | HTTP相应的url,str类型 |
status | HTTP响应的状态码,int类型,例如200,404 |
headers | HTTP相应的头部,类字典类型,调用方式:response.headers.get(getlist)(‘Content-Type’ | ‘Set-Cookie’) |
body | HTTP响应正文,bytes类型 |
text | 文本形式的HTTP响应正文,str类型,由body解码得来,text=body.decode(response.encoding) |
encoding | HTTP正文编码 |
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,
}
# ...
FilesPipeline | ImagesPipeline | |
---|---|---|
导入路径 | scrapy.pipelines.files.FilesPipeline | scrapy.pipelines.images.ImagesPipeline |
Item字段 | file_urls,files | image_urls,images |
下载目录 | FILES_STORE | IMAGES_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’] |
unique | True/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实现一种数据格式的导出,支持的数据格式如下
- JSON
- JSON lines
- CSV
- XML
- Pickle
- 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不会加上