Scrapy 是一个为了爬取网站数据,提取结构性数据而编写的应用框架。
其最初是为了 页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。
下图展示了Scrapy的大致架构,其中包含了主要组件和系统的数据处理流程(绿色箭头表示)。下面会对组件和流程进行了一个简单的解释。
一、组件介绍:
1.Scrapy Engine(Scrapy引擎)
Scrapy引擎是用来控制整个系统的数据处理流程,并进行事务处理的触发。更多的详细内容可以看下面的数据处理流程。
2.Scheduler(调度程序)
调度程序从Scrapy引擎接受请求并排序列入队列,并在Scrapy引擎发出请求后返还给它们。
3.Downloader(下载器)
下载器的主要职责是抓取网页并将网页内容返还给蜘蛛(Spiders)。
4.Spiders(蜘蛛)
蜘蛛是有Scrapy用户自己定义用来解析网页并抓取制定URL返回的内容的类,每个蜘蛛都能处理一个域名或一组域名。换句话说就是用来定义特定网站的抓取和解析规则。
5.Item Pipeline(项目管道)
项目管道的主要责任是负责处理有蜘蛛从网页中抽取的项目,它的主要任务是清晰、验证和存储数据。当页面被蜘蛛解析后,将被发送到项目管道,并经过几个特定的次序处理数据。每个项目管道的组件都是有一个简单的方法组成的Python类。它们获取了项目并执行它们的方法,同时还需要确定的是是否需要在项目管道中继续执行下一步或是直接丢弃掉不处理。
项目管道通常执行的过程有:
清洗HTML数据 验证解析到的数据(检查项目是否包含必要的字段) 检查是否是重复数据(如果重复就删除) 将解析到的数据存储到数据库中
6.Middlewares(中间件)
中间件是介于Scrapy引擎和其他组件之间的一个钩子框架,主要是为了提供一个自定义的代码来拓展Scrapy的功能。
二、数据处理流程:
Scrapy的整个数据处理流程有Scrapy引擎进行控制,其主要的运行方式为:
1、引擎打开一个域名,时蜘蛛处理这个域名,并让蜘蛛获取第一个爬取的URL。
2、引擎从蜘蛛那获取第一个需要爬取的URL,然后作为请求在调度中进行调度。
3、引擎从调度那获取接下来进行爬取的页面。
4、调度将下一个爬取的URL返回给引擎,引擎将它们通过下载中间件发送到下载器。
5、当网页被下载器下载完成以后,响应内容通过下载中间件被发送到引擎。
6、引擎收到下载器的响应并将它通过蜘蛛中间件发送到蜘蛛进行处理。
7、蜘蛛处理响应并返回爬取到的项目,然后给引擎发送新的请求。
8、引擎将抓取到的项目项目管道,并向调度发送请求。
9、系统重复第二部后面的操作,直到调度中没有请求,然后断开引擎与域之间的联系。
三、安装scrapy
使用 pip 安装:
pip install Scrapy
其他平台的安装请参考官方文档
四、开始scrapy之旅
1、创建一个 Scrapy 项目
在CMD中进入一个你希望保存代码的目录,然后执行:
scrapy startproject doubanmovie250
此时项目已创建成功;
这个命令会在当前目录下创建一个新的目录doubanmoive,目录结构如下:
doubanmoive250/spiders:放置spider的目录,定义提取的 Item
doubanmoive250/items.py:定义需要获取的内容字段,类似于实体类。
doubanmoive250/middlewares.py:中间件(Middleware) 下载器中间件是介入到 Scrapy 的 spider 处理机制的钩子框架,您可以添加代码来处理发送给 Spiders 的 response 及 spider 产生的 item 和 request。暂时可以先不配置,详情可查看官方文档
doubanmoive250/pipelines.py:项目管道文件,用来处理Spider抓取的数据。
doubanmoive250/settings.py:项目配置文件
2、定义提取的 Item
Item是用来装载抓取数据的容器,打开doubanmoive250/items.py可以看到默认创建了以下代码。
import scrapy
class Doubanmovie250Item(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
pass
我们只需要在Doubanmoive类中增加需要抓取的字段即可,如name=Field(),最后根据我们的需求完成代码如下。
import scrapy
class Doubanmovie250Item(scrapy.Item):
# define the fields for your item here like:
name = scrapy.Field() # 电影名
score = scrapy.Field() # 豆瓣分数
detail_url = scrapy.Field() # 文章链接在首页爬取
quote = scrapy.Field() # 引用
top = scrapy.Field() # 排名
year = scrapy.Field() # 上映年份
director = scrapy.Field() # 导演
scriptwriter = scrapy.Field() # 编剧
actor = scrapy.Field() # 演员
classification = scrapy.Field() # 分类
made_country = scrapy.Field() # 制片国家/地区
language = scrapy.Field() # 语言
showtime = scrapy.Field() # 上映日期
film_time = scrapy.Field() # 片长
alias = scrapy.Field() # 别名
IMDb_url = scrapy.Field() # IMDb链接
votes = scrapy.Field() # 评价人数
describe = scrapy.Field() # 剧情简介
item的操作和dict的非常相似。
1)创建Doubanmovie250Item的对象
item= Doubanmovie250Item(year=1994, made_country='美国')
2)获取字段的值
item['year']
item.get['year']
3)设置字段的值
item['year'] = 1994
4)获取所有的键和值
获取所有的键:item.keys()
获取所有的值:item.items()
5)item复制
item2 = Doubanmovie250Item(item)
item3 = item.copy()
6)item与dict的转换
item转换为dict:dict_item = dict(item)
dict转换为item:item = Doubanmovie250Item({'year':'1994', 'made_country':'美国'})
7)可以通过继承原始的item增加或修改item;
增加:
class newitem(Doubanmovie250Item):
short_comment = scrapy.Field()#短评
修改:
class newitem(Doubanmovie250Item):
#增加了五星评论的百分比
votes = scrapy.Field(Doubanmovie250Item.fields['votes'], five_star = five_star_votes)
3、编写爬取网站的 spider 并提取 Item
from scrapy.selector import Selector
from scrapy.spiders import CrawlSpider
from ..items import Doubanmovie250Item
from scrapy.http import Request
from scrapy.conf import settings #从settings文件中导入Cookie,这里也可以室友from scrapy.conf import settings.COOKIE
import random
import string
import re
class MovieSpider(CrawlSpider):
name = "douban_movie250_spider"
allowed_domains = ['movie.douban.com']
start_urls = ['https://movie.douban.com/top250']
cookie = settings['COOKIE'] # 带着Cookie向网页发请求
cookies = "bid=%s" % "".join(random.sample(string.ascii_letters + string.digits, 11))
def parse(self, response):
urls = ["https://movie.douban.com/top250?start={}&filter=".format(str(25*i)) for i in range(0, 10)]
for i, url in enumerate(urls):
#'dont_merge_cookies': True
yield Request(url, meta={'cookiejar': i,'dont_merge_cookies': True}, callback=self.parse_list)
def parse_list(self, response):
selector = Selector(response)
infos = selector.xpath('//ol[@class="grid_view"]/li')
#获取每页电影的部分数据,包括detail_url
for info in infos:
detail_url = info.xpath('div/div[2]/div[@class="hd"]/a/@href').extract()[0]
name = info.xpath('div/div[2]/div[@class="hd"]/a/span[1]/text()').extract()[0]
quote = info.xpath('div/div[2]/div[@class="bd"]/p/span[@class="inq"]/text()').extract()[0] if info.xpath('div/div[2]/div[@class="bd"]/p/span[@class="inq"]/text()') else "无"
score = float(info.xpath('//span[@property="v:average"]/text()').extract()[0])
yield Request(detail_url, meta={'cookiejar': response.meta['cookiejar'],'name': name,'detail_url': detail_url,'quote': quote,'score': score},callback=self.parse_item)
def parse_item(self, response):
item = Doubanmovie250Item()
item['name'] = response.meta['name']
item['detail_url'] = response.meta['detail_url']
item['top'] = int(response.xpath('//span[@class="top250-no"]/text()').extract()[0][3:])
item['score'] = response.meta['score']
item['quote'] = response.meta['quote']
item['year'] = int(response.xpath('//div[@id="content"]/h1/span[2]/text()').extract()[0][1:-1])
item['director'] = response.xpath('//a[@rel="v:directedBy"]/text()').extract()
item['scriptwriter'] = response.xpath('//div[@id="info"]/span[2]/span[@class="attrs"]/a/text()').extract()
item['actor'] = response.xpath('//a[@rel="v:starring"]/text()').extract()
item['classification'] = response.xpath('//div[@id="info"]/span[@property="v:genre"]/text()').extract()
item['showtime'] = response.xpath('//div[@id="info"]/span[@property="v:initialReleaseDate"]/text()').extract()
item['film_time'] = response.xpath('//div[@id="info"]/span[@property="v:runtime"]/text()').extract()
item['alias'] = response.xpath('//div[@id="info"]').re(r'</span> (.+)<br>\n')[-2]
item['IMDb_url'] = response.xpath('//div[@id="info"]/a/@href').extract()[0]
item['votes'] = int(response.xpath('//span[@property="v:votes"]/text()').extract()[0])
item['describe'] = response.xpath('//div[@id="link-report"]/span/text()').re(r'\S+')[0]
check_item = response.xpath('//div[@id="info"]').re(r'</span> (.+)<br>\n')[1]
result = self.check_contain_chinese(check_item)
# 有些电影详情页信息包含有官方网站,比如:https://movie.douban.com/subject/1291552/
if result:
item['made_country'] = response.xpath('//*[@id="info"]').re(r'</span> (.+)<br>\n')[1]
item['language'] = response.xpath('//*[@id="info"]').re(r'</span> (.+)<br>\n')[2]
else:
item['made_country'] = response.xpath('//*[@id="info"]').re(r'</span> (.*)<br>\n')[2]
item['language'] = response.xpath('//*[@id="info"]').re(r'</span> (.*)<br>\n')[3]
yield item
#判断字符串是否含有汉字
def check_contain_chinese(self, check_str):
#for ch in check_str.decode('utf-8'):
Pattern = re.compile(u'[\u4e00-\u9fa5]+')
match = Pattern.search(check_str)
if match:
return True
else:
return False
这里要注意的点:
1)、同一时间内多次快速访问豆瓣,可能会被ban,所以这里用的是构建一个cookie池,通过每次访问豆瓣分析发现,每次的cookie都不一样,是一个11位的字母+数字的随机字符串,所以自己构建了一个cookies,每次访问时随机传入这个cookie,就不会被禁了;
2)跑完之后,发现爬到一半会有报错的,把一些特殊的情况考虑进去;通过分析报错的电影,发现有些电影的字段是不一样的,所以针对这个情况写个判断,比如:made_country、language;
3)这里我用的选择器是xpath(个人喜好,也可以css、BeautifulSoup),
xpath():返回的一个selector list列表,
extract():序列化该节点为unicode字符串并返回列表,
parse_list()方法中,其中一个参数是response,这里可以通过构建一个selector对象,调用xpath方法解析网页,也可以直接用response调用xoath方法,官方文档里有写;
4)这里调试在xoath上可能会花费很长的时间,scrapy提供了一个方式验证xpath表达式是否正确,
新建一个命令窗口,输入
scrapy shell "https://movie.douban.com/top250"
返回的response为200,然后接着输入
response.xpath('//ol[@class="grid_view"]/li')
就可以测试xpath是否正常了,返回的是Unicode格式;
这里也可以用firepath测试,可以2者结合使用;
scrapy的命令行的操作:可直接看官方文档,文档写的已经很详细的,一看就懂;
4、编写 Pipeline 来存储提取到的 Item(即数据)
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
import pymongo
from scrapy.conf import settings
from scrapy.exceptions import DropItem
from scrapy import log
class DoubanMovie250Pipeline(object):
def __init__(self):
connection = pymongo.MongoClient(
settings['MONGODB_SERVER'],
settings['MONGODB_PORT']
)
db = connection[settings['MONGODB_DB']]
self.collection = db[settings['MONGODB_COLLECTION']]
def process_item(self, item, spider):
valid = True
for data in item:
if not data:
valid = False
raise DropItem("Missing {0}!".format(data))
if valid:
#插入数据到mongodb数据库
self.collection.insert(dict(item))
log.msg("Question added to MongoDB database!",
level=log.DEBUG, spider=spider)
return item
我这里是直接提取到mongodb数据库了,个人感觉用起来比较顺手,其他的也都OK,看自己习惯了。
其实每个pipeline都是一个独立的类,必须要实现process_item(self,item,spider)方法,
item对象是被爬取的item,
spider对象是被爬取改item的Spider;
所以,这个方法必须返回一个item对象,或者抛出DropItem异常,被丢弃的item将不会被pipeline执行;
定制pipeline还是不能执行的,需要激活的:
把pipeline的类添加到settings.py的ITEM_PIPELINES,
ITEM_PIPELINES = {
'douban_movie250.pipelines.DoubanMovie250Pipeline': 300,
}
ITEM_PIPELINES可以分配多个pipelines的组件,后面的数字代表了他们执行的顺序(从低到高),数字的范围是0~1000;
彩蛋:简单存储方式
使用scrapy的命令行实现快速存储的方式,存储的类型包括:json、jsonlines、csv、xml等
例如:scrapy crawl douban_movie250_spider -o movie250.csv
4、命令行启动爬虫
1)第一种方法
在爬虫代码所在目录,启动命令行,输入
scrapy crawl douban_movie250_spider
2)第二种方法
还有一种启动方法,就是在爬虫所在目前下新建一个main.py,写入以下代码,
from scrapy import cmdline
cmdline.execute("scrapy crawl douban_movie250_spider".split())
执行的时候直接运行这个文件就OK了;
3)第三种方法
在同一个进程中启动一个爬虫
使用CrawlProcess类,这个类内部将会开启Twisted reactor,配置log和设置Twist reactor自动关闭,因为Scrapy是在Twisted异步网络库上构建的,因此必须在Twisted reactor里运行。
main_spider.py启动脚本如下:
if __name__ = "__main__":
process = CrawlProcess({'USER_AGENT': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1'})
process.crawl(movie250)#传入的是Spider模块的文件名
process.start()
或者
初始化时传入设置settings的项目信息,
if __name__ = "__main__":
process = CrawlProcess(get_project_settings())
process.crawl(douban_movie250_spider)#传入的是爬虫名字
process.start()
运行结果: