同步异步
异步:调用在发出之后,这个调用就直接返回,不管有无结果
非阻塞:关注的是程序在等待调用结果 (消息,返回值)时的状态,指在不能立刻得到结果之前,该调用不会阻塞当前线程
scrapy的安装与使用
windows安装方式
pip3 install ipgrade pip
通过pip安装Scrapy框架
pip3 install Scrapy
Ubuntu 安装方式
sudo pip3 install scrapy
如果安装不成功再试着添加这些依赖库:
安装非Python的依赖
sudo apt-get install python3-dev python-pip libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev
新建项目
scrapy startproject 爬虫项目名称
新建爬虫文件
scrapy genspider jobbole jobbole.com
启动
scrapy crawl +项目名称
根据目标网站分析需要提取的数据,在item.py文件中添加字段
打开jobboleproject文件下的item.py文件
item 定义结构化数据字段,用来保存爬取到的数据,有点像Python中的dict,但是提供了一些额外的保护减少错误。
可以通过创建一个 scrapy.Item 类, 并且定义类型为 scrapy.Field的类属性来定义一个Item(可以理解成类似于ORM的映射关系)
制作爬虫
要建立一个Spider, 你必须用scrapy.Spider类创建一个子类,并确定了三个强制的属性 和 一个方法。
name = “” :这个爬虫的识别名称,必须是唯一的,在不同的爬虫必须定义不同的名字。
allow_domains = [] 是搜索的域名范围,也就是爬虫的约束区域,规定爬虫只爬取这个域名下的网页,不存在的URL会被忽略。
start_urls = () :爬取的URL元祖/列表。爬虫从这里开始抓取数据,所以,第一次下载的数据将会从这些urls开始。其他子URL将会从这些起始URL中继承性生成。
parse(self, response) :解析的方法,每个初始URL完成下载后将被调用,调用的时候传入从每一个URL传回的Response对象来作为唯一参数,
主要作用如下:
负责解析返回的网页数据(response.body),提取结构化数据(生成item)
生成需要下一页的URL请求。
将start_urls(设置起始url)的值修改为需要爬取的第一个url
yelid函数
yield 的作用就是把一个函数变成一个 generator(生成器),带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,带有yeild的函数遇到yeild的时候就返回一个迭代值,下次迭代时,代码从 yield 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,
直到再次遇到 yield。
通俗的讲就是:在一个函数中,程序执行到yield语句的时候,程序暂停,返回yield后面表达式的值,在下一次调用的时候,从yield语句暂停的地方继续执行,如此循环,直到函数执行完。
Scrapy Item pipeline(管道文件)使用
Item Pipeline
当Item在Spider中被收集之后,它将会被传递到Item Pipeline,这些Item Pipeline组件按定义的顺序处理Item。
每个Item Pipeline都是实现了简单方法的Python类,比如决定此Item是丢弃而存储。以下是item pipeline的一些典型应用:
验证爬取的数据(检查item包含某些字段,比如说name字段)
查重(并丢弃)
将爬取结果保存到文件或者数据库中
数据持久化
方式一:数据存入mongodb数据库
settings.py文件:
设置文件,在这里设置User-Agent,激活管道文件等
ITEM_PIPELINES = {
'douban.pipelines.DoubanPipeline': 300,
}
MONGODB_HOST = '127.0.0.1' #MONGODB 主机名
MONGODB_PORT= 27017 #MONGODB 端口号
MONGODB_DBNAME = "Douban" #数据库名称
MONGODB_SHEETNAME= "doubanmovies" #存储数据的表名称
在管道pipelines.py中进行数据持久化
import pymongo from scrapy.conf import settings
class DoubanPipeline(object):
# 将数据存储在mongodb中
def __init__(self,host,port,dbname,sheetname):
# 创建MONGODB数据库链接
client = pymongo.MongoClient(host=host, port=port)
# 指定数据库
mydb = client[dbname]
# 存放数据的集合名称
self.mysheet = mydb[sheetname]
@classmethod
def from_crawler(cls, crawler):
host = crawler.settings["MONGODB_HOST"]
port = crawler.settings["MONGODB_PORT"]
dbname = crawler.settings["MONGODB_DBNAME"]
sheetname = crawler.settings["MONGODB_SHEETNAME"]
return cls(host,port,dbname,sheetname)
def process_item(self,item,spider):
data = dict(item) # mongodb
数据插入语句,使用save保存数据的效率会很慢,因为它需要循环便利,操作费时
self.mysheet.insert(data) r
eturn item
数据存入mysql数据库
settings.py文件:
设置文件,在这里设置User-Agent,激活管道文件等...
ITEM_PIPELINES = {
'douban.pipelines.DoubanPipeline': 300,
} #关于数据库的相关配置
MYSQL_HOST = '127.0.0.1'
MYSQL_PORT = 3306
MYSQL_USER = '用户名'
MYSQL_PWD = '密码'
MYSQL_DB = '数据库'
pipelines.py管道文件
import pymysql
class DoubanPipeline(object):
# 将数据存储值mysql数据库
# _mysql_exceptions.OperationalError: (1366, 是因为数据库中的字符集与charset="utf8"不符合
def __init__(self,host,port,user,pwd,db,charset):
self.client = pymysql.Connect(host,user,pwd,db,port,charset='utf8')
self.cursor = self.client.cursor()
@classmethod
def from_crawler(cls,crawler):
host = crawler.settings['MYSQL_HOST']
port = crawler.settings['MYSQL_PORT']
user = crawler.settings['MYSQL_USER']
pwd = crawler.settings['MYSQL_PWD']
db = crawler.settings['MYSQL_DB']
return cls(host,port,user,pwd,db,charset)
def process_item(self,item,spider):
insert_sql = """ insert into 表名(title, playable, content, star, commentnum, inq) VALUES (%s, %s, %s, %s, %s, %s) """
try: self.cursor.execute(insert_sql, (item['title'], item['content'], item['score'], item['info']))
self.client.commit()
except Exception as err: print(err)
self.client.rollback()
return item
将sql语句和要插入的数据在item中定义一个方法返回,通过item调用,然后返回
class XxxxItem(scrapy.Item):
#名称
title = scrapy.Field()
def insert_data_to_db(self,dataDict):
sql = """
INSERT INTO caipu (%s)
VALUES (%s)
""" % (','.join(dataDict.keys()),','.join(['%s']*len(dataDict)))
data = list(dataDict.values())
return sql,data
mysql数据异步插入
settings.py配置文件将mysql相关信息写在settings中
MYSQL_HOST = '127.0.0.1'
MYSQL_ROOT = '数据库用户名'
MYSQL_PASSWORD = '数据库密码'
MYSQL_DBNAME = 'DouBan'
异步插入数据库pipeline管道中的相关代码
import pymysql
#twisted是一个异步的网络框架,这里可以帮助我们 实现异步将数据插入数据库
#adbapi里面的子线程会去执行数据库的阻塞操作, 当一个线程执行完毕之后,同时,原始线程能继续 进行正常的工作,服务其他请求。 from twisted.enterprise import adbapi
#mysql数据异步插入
class DoubanPipeline(object):
def __init__(self,dbpool):
self.dbpool = dbpool
#使用这个函数来应用settings配置文件。
@classmethod
def from_crawler(cls, crawler):
parmas = { 'host':crawler.settings['MYSQL_HOST'], 'user':crawler.settings['MYSQL_USER'], 'passwd':crawler.settings['MYSQL_PASSWD'], 'db':crawler.settings['MYSQL_DB'],
'port':3306, 'charset':'utf8', }
# **表示字典,*tuple元组,
# 使用ConnectionPool,起始最后返回的是一个ThreadPool
dbpool = adbapi.ConnectionPool(
'pymysql',
**parmas
)
return cls(dbpool)
def process_item(self, item, spider):
#这里去调用任务分配的方法
query = self.dbpool.runInteraction(
self.insert_data_todb,
item,
spider )
#数据插入失败的回调
query.addErrback(
self.handle_error,
item )
#执行数据插入的函数 def insert_data_todb(self,cursor,item,spider):
insert_str,parmas = item.insertdata() cursor.execute(insert_str,parmas)
print('插入成功')
def handle_error(self,failure,item):
print(failure)
print('插入错误')
#在这里执行你想要的操作
def close_spider(self, spider):
self.pool.close()
下载项目图片
Scrapy提供了一个 item pipeline ,来下载属于某个特定项目的图片,比如,当你抓取产品时,也想把它们的图片下载到本地。
这条管道,被称作图片管道,在 ImagesPipeline 类中实现,提供了一个方便并具有额外特性的方法,来下载并本地存储图片:
Pillow 是用来生成缩略图,并将图片归一化为JPEG/RGB格式,因此为了使用图片管道,你需要安装这个库。 Python Imaging Library (PIL) 在大多数情况下是有效的,但众所周知,在一些设置里会出现问题,因此我们推荐使用 Pillow 而不是 PIL.
使用图片管道
当使用 ImagesPipeline ,典型的工作流程如下所示:
在一个爬虫里,你抓取一个项目,把其中图片的URL放入 image_urls 组内。
项目从爬虫内返回,进入项目管道。
当项目进入 ImagesPipeline,image_urls 组内的URLs将被Scrapy的调度器和下载器(这意味着调度器和下载器的中间件可以复用)安排下载,当优先级更高,会在其他页面被抓取前处理。项目会在这个特定的管道阶段保持“locker”的状态,直到完成图片的下载(或者由于某些原因未完成下载)。
* 当图片下载完,另一个组(images)将被更新到结构中。这个组将包含一个字典列表,其中包括下载图片的信息,比如下载路径、源抓取地址(从 image_urls 组获得)和图片的校验码。 images 列表中的图片顺序将和源 image_urls 组保持一致。如果某个图片下载失败,将会记录下错误信息,图片也不会出现在 images 组中。
实现定制图片管道
from scrapy.contrib.pipeline.images import ImagesPipeline
import scrapy
from scrapy.exceptions import DropItem
import os
from scrapy.utils.project import get_project_settings
#下载图片
image_store = get_project_settings().get('IMAGES_STORE')
class jobboleImagePipline(ImagesPipeline):
def get_media_requests(self, item, info):
return scrapy.Request(item['coverImage'])
# return [Request(x) for x in item.get(self.images_urls_field, [])]
def item_completed(self, results, item, info):
print(results)
#获取图片下载成功的请求路径
image_paths = [x['path'] for ok, x in results if ok]
#判断图片是否下载成功
if not image_paths:
raise DropItem("Item contains no images")
#替换图片的名称,自定义图片名称 os.rename(image_store+'/'+image_paths[0],
image_store+'/'+item['title']+'.jpg')
#将图片地址赋值给item
item['localImagePath'] = image_store+'/'+item['title']+'.jpg'
return item
解释
item_completed() 接收的元组列表需要保证与 get_media_requests() 方法返回请求的顺序相一致。下面是 results 参数的一个典型值:
默认 get_media_requests() 方法返回 None ,这意味着项目中没有图片可下载。
[(True,
{'checksum': '2b00042f7481c7b056c4b410d28f33cf',
'path': 'full/7d97e98f8af710c7e7fe703abc8f639e0ee507c4.jpg',
'url': 'http://www.example.com/images/product1.jpg'}),
(True,
{'checksum': 'b9628c4ab9b595f72f280b90c4fd093d',
'path': 'full/1ca5879492b8fd606df1964ea3c1e2f4520f076f.jpg',
'url': 'http://www.example.com/images/product2.jpg'}),
(False,
Failure(...))]
Scrapy Shell
Scrapy终端是一个交互终端,我们可以在未启动spider的情况下尝试及调试代码,也可以用来测试XPath或CSS表达式,查看他们的工作方式,方便我们爬取的网页中提取的数据。
如果安装了 IPython ,Scrapy终端将使用 IPython (替代标准Python终端)。 IPython 终端与其他相比更为强大,提供智能的自动补全,高亮输出,及其他特性。(推荐安装IPython)
Scrapy Shell根据下载的页面会自动创建一些方便使用的对象,例如 Response 对象,以及 Selector 对象 (对HTML及XML内容)。
当shell载入后,将得到一个包含response数据的本地 response 变量,输入 response.body将输出response的包体,输出 response.headers 可以看到response的包头。
输入 response.selector 时, 将获取到一个response 初始化的类 Selector 的对象,此时可以通过使用 response.selector.xpath()或response.selector.css() 来对 response 进行查询。
Scrapy也提供了一些快捷方式, 例如 response.xpath()或response.css()同样可以生效(如之前的案例)。
Selectors选择器Scrapy Selectors 内置 XPath 和 CSS Selector 表达式机制
Selector有四个基本的方法,最常用的还是xpath:
* xpath(): 传入xpath表达式,返回该表达式所对应的所有节点的selector list列表
* extract(): 序列化该节点为字符串并返回list
* css(): 传入CSS表达式,返回该表达式所对应的所有节点的selector list列表,语法同 BeautifulSoup4
* re(): 根据传入的正则表达式对数据进行提取,返回字符串list列表
以后做数据提取的时候,可以把现在Scrapy Shell中测试,测试通过后再应用到代码中。
当然Scrapy 的commend不仅仅如此,
例如:
* scrapy -h 查看所有可用的命令:
* scrapy view -h 查看view命令的详细内容:
* scrapy list列出当前项目中所有可用的spider
* scrapy runspider xxxx.py在未创建项目的情况下,运行一个编写在Python文件中的spider。
* scrapy version输出Scrapy版本
Scrapy Spider文件源码介绍
Spider类定义了如何爬取某个(或某些)网站。包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item)。 换句话说,Spider就是您定义爬取的动作及分析某个网页(或者是有些网页)的地方。
scrapy.Spider是最基本的类,所有编写的爬虫必须继承这个类。
主要用到的函数及调用顺序为:
init() : 初始化爬虫名字和start_urls列表
start_requests() 调用make_requests_from url():生成Requests对象交给Scrapy下载并返回response
parse():
* 解析response,并返回Item或Requests(需指定回调函数)。
* Item传给Item pipline持久化 , 而Requests交由Scrapy下载,并由指定的回调函数处理(默认parse()),一直进行循环,直到处理完所有的数据为止。
所有爬虫的基类,用户定义的爬虫必须从这个类继承
class Spider(object_ref):
主要属性和方法
name
定义spider名字的字符串。
例如,如果spider爬取 mywebsite.com ,该spider通常会被命名为 mywebsite
allowed_domains
包含了spider允许爬取的域名(domain)的列表,可选。
start_urls
初始URL元组/列表。当没有制定特定的URL时,spider将从该列 表中开始进行爬取。
start_requests(self)
该方法必须返回一个可迭代对象(iterable)。该对象包含了 spider用于爬取(
默认实现是使用 start_urls 的url)的第 一个Request。
当spider启动爬取并且未指定start_urls时,该方法被调用。
parse(self, response)
当请求url返回网页没有指定回调函数时,默认的Request对象回 调函数。
用来处理网页返回的response,以及生成Item或者 Request对象。
log(self, message[, level, component])
使用 scrapy.log.msg() 方法记录(log)message。 更多数 据请参见 logging
补充启动方式二:
在开发过程中我们避免不了要调试程序,那么我们需要在工程中添加main.py 文件
import os
import sys
from scrapy.cmdline import execute
#设置工程目录 sys.path.append(os.path.dirname(os.path.abspath(__file__)))
#这里我们可以打印出来可以查看输出的是什么,便于理解为什么这么写 print(os.path.abspath(__file__)) print(os.path.dirname(os.path.abspath(__file__)))
#启动爬虫
execute(["scrapy","crawl","爬虫名称"])
parse()方法的工作机制:
1. 因为使用的yield,而不是return。parse函数将会被当做一个生成器使用。scrapy会逐一获取parse方法中生成的结果,并判断该结果是一个什么样的类型;
2. 如果是request则加入爬取队列,如果是item类型则使用pipeline处理,其他类型则返回错误信息。
3. scrapy取到第一部分的request不会立马就去发送这个request,只是把这个request放到队列里,然后接着从生成器里获取;
4. 取尽第一部分的request,然后再获取第二部分的item,取到item了,就会放到对应的pipeline里处理;
5. parse()方法作为回调函数(callback)赋值给了Request,指定parse()方法来处理这些请求 scrapy.Request(url, callback=self.parse)
6. Request对象经过调度,执行生成 scrapy.http.response()的响应对象,并送回给parse()方法,直到调度器中没有Request(递归的思路)
7. 取尽之后,parse()工作结束,引擎再根据队列和pipelines中的内容去执行相应的操作;
8. 程序在取得各个页面的items前,会先处理完之前所有的request队列里的请求,然后再提取items。
9. 这一切的一切,Scrapy引擎和调度器将负责到底。
Scrapy Request和Response相关参数介绍
url: 就是需要请求,并进行下一步处理的url
callback: 指定该请求返回的Response,由那个函数来处理。
method: 请求一般不需要指定,默认GET方法,可设置为"GET", “POST”, "PUT"等,且保证字符串大写
headers: 请求头
cookies: cookies,模拟用户登录需要指定用户的cookies,字典dict型
meta: 比较常用,在不同的请求之间传递数据使用的。字典dict型
encoding: 编码类型,使用默认的 ‘utf-8’ 就行。
dont_filter: 表明该请求不由调度器过滤。这是当你想使用多次执行相同的请求,忽略重复的过滤器。默认为False。
errback: 指定错误处理函数
发送POST请求(补充)
yield scrapy.FormRequest(url, formdata, callback)
如果希望程序执行一开始就发送POST请求,可以重写Spider类的start_requests(self) 方法,并且不再调用start_urls里的url。
class mySpider(scrapy.Spider):
name = 'myspider'
allow_domeas = ['renren.com']
start_urls = ["http://www.renren.com/PLogin.do"]
def start_requests(self):
# FormRequest 是Scrapy发送POST请求的方法
for url in self.start_urls:
yield scrapy.FormRequest(
url = url,
formdata = {"email" : "mr_mao_hacker@163.com", "password" : "axxxxxxxe"},
callback = self.parse_page
)
def parse_page(self, response):
""" 请求成的回调函数 """
pass
Response相关参数
部分代码
class Response(object_ref):
def __init__(self, url, status=200, headers=None, body='', flags=None, request=None):
self.headers = Headers(headers or {})
self.status = int(status)
self._set_body(body)
self._set_url(url)
self.request = request
self.flags = [] if flags is None else list(flags)
@property
def meta(self):
try:
return self.request.meta
except AttributeError:
raise AttributeError("Response.meta not available, this response " \ "is not tied to any request")
``
大部分参数和上面的差不多:
status: 响应码
body: 响应体
url:响应url
self.request (request对象)
self.headers (响应头)