Scrapy爬虫框架学习
一、Scrapy框架简介
1. 下载页面 2. 解析 3. 并发 4. 深度
二、安装
linux下安装 pip3 install scrapy windows下安装 a.pip3 install wheel b.下载twisted和pywin32 http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted c.进入下载目录 执行pip3 install Twisted-18.7.0-cp36-cp36m-win_amd64.whl #cp36为适合python3.6 执行pip3 install pywin32-224-cp36-cp36m-win_amd64.whl d.pip3 install scrapy e.下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/ #找到适合本机python版本的64位
三、Scrapy整体架构图
3.1 Scrapy使用Twisted异步网络库来处理网络通讯
3.2 各个主要文件说明
spiders(蜘蛛)文件夹:如ip138.com
name #不能省略,最好不要修改 starts_urls #起始url allowed_domains #爬取允许域名列表,起始url不受影响。网页中的外链受此限制
四、使用
4.1 基本使用
1. 指定初始url 2. 解析器响应内容 - 给调度器 - 给item:pipeline;用于做格式化;持久化 基本步骤: a:scrapy startproject 项目名 #创建项目 b:进入项目目录 c:scrapy genspider baidu www.baidu.com #创建start_url d:打开项目名\spiders\baidu.py进行编辑 e:scrapy crawl baidu #执行,加--nolog可以不显示日志,如果没有内容显示,可能此IP已经有防爬机制,可换个不知名ip试试
#此处的baidu不是项目名,而是spiders文件夹下的baidu.py
*f:scrapy shell http://www.baidu.com #下载url里的内容,进入调试模式,对于捕捉过滤字段非常方便
4.2 scrapy.loader.ItemLoader的使用
a.Itemloader介绍
优点:避免重复代码,提高代码可读性
(解决在爬虫文件中代码结构杂乱,无序,可读性差的缺点)
b.Itemloader简单使用
#ke.py爬虫文件 from scrapy.loader import ItemLoader from .. import items #导入items.py文件里的类 class KeSpider(scrapy.Spider): name = 'ke' allowed_domains = ['ke.com'] start_urls = ['https://sz.ke.com/xiaoqu/'] def parse(self, response): # 通过变量item_loader加载item item_loader = ItemLoader(item=items.BeiKeItem(), response=response) item_loader.add_css() #使用css选择器 item_loader.add_xpath('name', '//div[@class="title"]/h1/text()') # 将response用xpath选择器指定的过滤字段,得到的结果赋值给items.py文件里BeiKeItem类里的name变量 item_loader.add_value('accessUrl', response.url) # 将response.url的值赋值给items.py文件里BeiKeItem类里的accessUrl变量 item_obj = item_loader.load_item() #将item_loader里的值装入Item yield item_obj
4.3 scrapy.loader.processors.MapCompose使用
a.MapCompose()介绍
MapCompose() #传入函数的列表的每一个元素都会经过第一个函数, #得到值在经过第二个函数,如果有返回值为None的,则抛弃, #最后返回一个列表
b.简单使用
#items.py文件 class BeiKeItem(scrapy.Item): name = scrapy.Field(input_processor=MapCompose(addName)) #input_processor输入时进行预处理,MapCompose为加载函数处理,每一个元素后面都加上"-伍木" name = scrapy.Field(input_processor=MapCompose(lambda x:x+"-伍木")) #input_processor输入时进行预处理,MapCompose为加载lambda处理,每一个元素后面都加上"-伍木" name = scrapy.Field(input_processor=MapCompose(lambda x:x+"-伍木1",addName)) #MapCompose第一个参数传入lambda,第二个参数传入函数,每一个元素先经过第一个lambda,结果在一一经过第二个函数
4.4 scrapy.loader.processors.TakeFirst使用
a.TakeFirst()介绍
只取列表里对象元素的第一个值
b.简单使用
#items.py文件 class BeiKeItem(scrapy.Item): name = scrapy.Field(out_processor = TakeFirst()) #tekeFirst()函数只取列表第一个元素,即便空列表也不报错
c.让每一个Item对象输出结果都默认只取第一个元素
笨办法(每个字段输出时都用一次TakeFirst()): #items.py from scrapy.loader.processors import TakeFirst class BeiKeItem(scrapy.Item): name = scrapy.Field(out_processor = TakeFirst()) #tekeFirst()函数只取列表第一个元素,即便空列表也不报错 avgPrice = scrapy.Field(out_processor = TakeFirst()) accessUrl = scrapy.Field(out_processor = TakeFirst()) address = scrapy.Field(out_processor = TakeFirst()) ############################################################################################################### 聪明办法(改写ItemLoader),一次改写,无需重复 #items.py from scrapy.loader.processors import TakeFirst from scrapy.loader import ItemLoader class BeiKeItemLoader(ItemLoader): #自定义itemloader default_output_processor = TakeFirst() #爬虫文件ke.py from scrapy.loader import ItemLoader class KeSpider(scrapy.Spider): name = 'ke' allowed_domains = ['ke.com'] start_urls = ['https://sz.ke.com/xiaoqu/'] def parse(self, response): item_loader = BeiKeItemLoader(item=items.BeiKeItem(), response=response) #将ItemLoader换成重写的BeiKeItemLoader item_loader.add_xpath('name', '//div[@class="title"]/h1/text()') item_loader.add_value('accessUrl', response.url) item_obj = item_loader.load_item() yield item_obj
4.5 scrapy.loader.processors.Join
a. Join()介绍
将元素那指定分隔符拼接,()括号内为指定分隔符,如","
b.简单使用
#items.py文件 from scrapy.loader.processors import Join class BeiKeItem(scrapy.Item): tag = scrapy.Field(out_processor = Join(',')) #将输出的元素以“,”分割,返回字符串类型
五、筛选器
5.1 Selector介绍
在scrapy中,可以使用Selector筛选器,代替BeautifulSoup
在scrapy项目里的spiders文件里的baidu.py爬虫文件编辑,导入Selecotr模块
from scrapy.selector import Selector
5.2 Selector(response=response).xpath()基本用法 或者使用response.xpath()
// #表示子子孙孙,即所有 .// #当前对象的子孙中 / #儿子 /div #儿子中的div标签 //div[@id] #所有标签含有id的div标签 /div[@id='i1'] #儿子中的div且id ='i1'的标签 obj.extract() #列表中每一个对象转换成字符串 ==>返回的是列表 obj.extract_first() #列表中每一各对象转换成字符串==>返回的是列表中第一个元素 //div/text() #获取所有div标签里的文本==》返回的是列表,元素是对象 //a/@href #获取所有a标签里的url==》返回的是列表,元素是对象 //a[starts-with(@href,"link")] #获取所有a标签,并且href属性是以link开头的 //a[re:test(@href,"/sitehome/p/\d+")]/@href #正则,获取所有a标签属性href符合/sitehome/p/数字的 //div[@id='i1'][@href=''xx] #[][]且的意思,即所有含有id='i1'且href='xxx'的div标签 //a[contains(@href, "link")] #所有href字段包含link字符串的a标签
5.3 简单示例
需求:在www.ip138.com网站里,打印如下a标签的文本内容和url地址
在爬虫文件(baidu.py)里的parse类方法里编辑
# -*- coding: utf-8 -*- import scrapy from scrapy.selector import Selector class BaiduSpider(scrapy.Spider): name = 'baidu' allowed_domains = ['ip138.com'] start_urls = ['http://www.ip138.com/'] def parse(self, response): #请求该页面下所有a标签==》列表,每个元素都是对象 #找到div标签里有class=mod-guide属性下的所有子孙a标签里文本内容 text_obj_list = Selector(response=response).xpath('//div[@class="module mod-guide"]//a/text()') # 找到div标签里有class=mod-guide属性下的所有子孙a标签里url url_obj_list = Selector(response=response).xpath('//div[@class="module mod-guide"]//a/@href') #将列表中对象转化成列表中字符串 text_str_list = text_obj_list.extract() url_str_list = url_obj_list.extract() for a_text,a_url in zip(text_str_list,url_str_list): print(a_text,a_url)
如何运行代码?
在cmd窗口,进入项目目录,运行scrapy crawl baidu
5.4 获取当前网页中的所有页码实例
需求:获取博客园里的首页页码url
在爬虫文件(ip138.py)里的parse类方法里编辑
# -*- coding: utf-8 -*- import scrapy from scrapy.selector import Selector class Ip138Spider(scrapy.Spider): name = 'ip138' allowed_domains = ['www.cnblogs.com'] start_urls = ['https://www.cnblogs.com/'] urls_set = set() def parse(self, response): # text_obj_list = Selector(response=response).xpath('//div[@class="module mod-guide"]//a/text()') # url_obj_list = Selector(response=response).xpath('//div[@class="module mod-guide"]//a/@href') # # text_str_list = text_obj_list.extract() # url_str_list = url_obj_list.extract() # # for text,url in zip(text_str_list,url_str_list): # print(text,url) #获取当前页里的所有页码的url对象列表 url_obj_list = Selector(response=response).xpath('//div[@class="pager"]/a/@href') ##或者2: starts-with(@属性,'值开头') #url_obj_list = Selector(response=response).xpath('//a[starts-with(@href,"/sitehome/p/")]/@href') ##或者3:正则表达式固定用法re:test(@属性值,"正则表达式") #url_obj_list = Selector(response=response).xpath('//a[re:test(@href,"/sitehome/p/\d+")]/@href') #将对象列表转换成字符串列表 url_str_list = url_obj_list.extract() for url in url_str_list: print(url) #1.通过集合去除重复url #2.使用加密的MD5存储url,好处:加密和等长 url_md5 = self.my_md5(url) if url_md5 not in self.urls_set: self.urls_set.add(url_md5) print(url_md5) else: print('%s已经存在'%url_md5) def my_md5(self,url): import hashlib obj = hashlib.md5() obj.update(bytes(url,encoding='utf-8')) return obj.hexdigest()
5.5 获得当前网页里的页码自动请求爬取实例
需求:自动爬取博客园的所有页码(基于5.4案例的基础)
在爬虫文件(ip138.py)里的parse类方法里编辑
# -*- coding: utf-8 -*- import scrapy from scrapy.selector import Selector from scrapy.http import Request class Ip138Spider(scrapy.Spider): name = 'ip138' allowed_domains = ['www.cnblogs.com'] start_urls = ['https://www.cnblogs.com/'] urls_set = set() def parse(self, response): #获取当前页里的所有页码的url对象列表 url_obj_list = Selector(response=response).xpath('//div[@class="pager"]/a/@href') ##或者2: starts-with(@属性,'值开头') #url_obj_list = Selector(response=response).xpath('//a[starts-with(@href,"/sitehome/p/")]/@href') ##或者3:正则表达式固定用法re:test(@属性值,"正则表达式") #url_obj_list = Selector(response=response).xpath('//a[re:test(@href,"/sitehome/p/\d+")]/@href') #将对象列表转换成字符串列表 url_str_list = url_obj_list.extract() for url in url_str_list: #1.通过集合去除重复url if url not in self.urls_set: #print(url) self.urls_set.add(url) #拼接完整的url地址 full_url = self.start_urls[0] + url #将url页码传给调度器去请求,并将下载的结果交给parse方法,yield的作用是将请求放入调度器 yield Request(url=full_url,callback=self.parse) for url in self.urls_set: print(url)
在setting.py中结尾新增一行DEPTH_LIMIT=1来指定递归的层次,默认是0,所有层次
实例中关键点概括
@href #取属性值 starts-with(@href,"xx") #属性href的值以xx开始 re:test(@href,"/sitehome/p/\d+") #正则re:test固定搭配 yield Request(url=full_url,callback=self.parse) #交给调度器 或者使用 yield response.follow(url=full_url,callback=self.parse),效果一样
六、item,pipeline使用
6.1 需求:将博客园的文章标题和url保存到一个文件a.txt文件里。
各文件代码如下:
# -*- coding: utf-8 -*- import scrapy from scrapy.selector import Selector from scrapy.http import Request from .. import items class Ip138Spider(scrapy.Spider): name = 'ip138' allowed_domains = ['www.cnblogs.com'] start_urls = ['https://www.cnblogs.com/'] urls_set = set() def parse(self, response): #获取当前页面里的标题和url==>返回字符串 title_str_list = Selector(response=response).xpath('//a[@class="titlelnk"]/text()').extract() href_str_list = Selector(response=response).xpath('//a[@class="titlelnk"]/@href').extract() for title_str,href_str in zip(title_str_list,href_str_list): #print(title_str,' ',href_str) #此处的参数title,和href来自于items.py文件里类Cnblogs里的属性 item_obj = items.Cnblogs(title=title_str,href=href_str) #将item对象传递给pipelines yield item_obj #获取当前页里的所有页码的url对象列表 page_obj_list = Selector(response=response).xpath('//div[@class="pager"]/a/@href') ##或者2: starts-with(@属性,'值开头') #page_obj_list = Selector(response=response).xpath('//a[starts-with(@href,"/sitehome/p/")]/@href') ##或者3:正则表达式固定用法re:test(@属性值,"正则表达式") #page_obj_list = Selector(response=response).xpath('//a[re:test(@href,"/sitehome/p/\d+")]/@href') #将对象列表转换成字符串列表 page_str_list = page_obj_list.extract() for url in page_str_list: #1.通过集合去除重复url if url not in self.urls_set: self.urls_set.add(url) print(url) #拼接完整的url地址 full_url = self.start_urls[0] + url #将url页码传给调度器去请求,并将下载的结果交给parse方法,yield的作用是将请求放入调度器 yield Request(url=full_url,callback=self.parse)
# -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html import scrapy class Cnblogs(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() title = scrapy.Field() href = scrapy.Field()
# -*- coding: utf-8 -*- # Define your item pipelines here # # Don't forget to add your pipeline to the ITEM_PIPELINES setting # See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html class Day0310Pipeline(object): def process_item(self, item, spider): content = "%s %s\n"%(item['title'],item['href']) f = open('a.json','a') f.write(content) f.close() # return item
ITEM_PIPELINES = { 'day0310.pipelines.Day0310Pipeline': 300, }
6.2 实例中关键点概
#items.py文件中 class Cnblogs(scrapy.Item): title = scrapy.Field() href = scrapy.Field() #ip138.py文件中 from .. import items item_obj = items.Cnblogs(title=title_str,href=href_str) #此处的参数title,和href来自于items.py文件里类Cnblogs里的属性 yield item_obj #将item对象传递给pipelines #pipelines.py文件中,将数据保存在文本a.txt里 class Day0310Pipeline(object): def process_item(self, item, spider): #item为ip138.py文件里的对象化的数据,spider为来自哪只蜘蛛对象 content = "%s %s\n"%(item['title'],item['href']) f = open('a.txt','a') f.write(content) f.close() #【可选】pipelines.py文件中,将数据保存在csv文件里 import csv class Day0310Pipeline(object): def process_item(self, item, spider): #item为ip138.py文件里的对象化的数据,spider为来自哪只蜘蛛对象 with open("a_gbk.csv", "a", encoding="gbk") as f: #此处encoding指定为gbk,是因此csv在windows中文系统下打开是GBK编码 #可参考文章https://www.cnblogs.com/lisenlin/p/14241416.html里的“四、常见问题”4.2的解决方法一 csv_writer = csv.writer(f) csv_writer.writerow(item.values()) #将字典里的每个值存放到csv的行里每个单元格 #setting.py文件中注册下pipelines的类 ITEM_PIPELINES = { 'day0310.pipelines.Day0310Pipeline': 300, } 数字越小越先进入管道