何为框架,就至关于一个封装了不少功能的结构体,它帮咱们把主要的结构给搭建好了,咱们只需往骨架里添加内容就行。scrapy框架是一个为了爬取网站数据,提取数据的框架,咱们熟知爬虫总共有四大部分,请求、响应、解析、存储,scrapy框架都已经搭建好了。scrapy是基于twisted框架开发而来,twisted是一个流行的事件驱动的python网络框架,scrapy使用了一种非阻塞(又名异步)的代码实现并发的,Scrapy之因此能实现异步,得益于twisted框架。twisted有事件队列,哪个事件有活动,就会执行!Scrapy它集成高性能异步下载,队列,分布式,解析,持久化等。css
1.五大核心组件html
引擎(Scrapy)python
框架核心,用来处理整个系统的数据流的流动, 触发事务(判断是何种数据流,而后再调用相应的方法)。也就是负责Spider、ItemPipeline、Downloader、Scheduler中间的通信,信号、数据传递等,因此被称为框架的核心。git
调度器(Scheduler)github
用来接受引擎发过来的请求,并按照必定的方式进行整理排列,放到队列中,当引擎须要时,交还给引擎。能够想像成一个URL(抓取网页的网址或者说是连接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址。web
下载器(Downloader)ajax
负责下载引擎发送的全部Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理。Scrapy下载器是创建在twisted这个高效的异步模型上的。shell
爬虫(Spiders)数据库
用户根据本身的需求,编写程序,用于从特定的网页中提取本身须要的信息,即所谓的实体(Item)。用户也能够从中提取出连接,让Scrapy继续抓取下一个页面。跟进的URL提交给引擎,再次进入Scheduler(调度器)。express
项目管道(Pipeline)
负责处理爬虫提取出来的item,主要的功能是持久化实体、验证明体的有效性、清除不须要的信息。
2.工做流程
Scrapy中的数据流由引擎控制,其过程以下:
(1)用户编写爬虫主程序将须要下载的页面请求requests递交给引擎,引擎将请求转发给调度器;
(2)调度实现了优先级、去重等策略,调度从队列中取出一个请求,交给引擎转发给下载器(引擎和下载器中间有中间件,做用是对请求加工如:对requests添加代理、ua、cookie,response进行过滤等);
(3)下载器下载页面,将生成的响应经过下载器中间件发送到引擎;
(4) 爬虫主程序进行解析,这个时候解析函数将产生两类数据,一种是items、一种是连接(URL),其中requests按上面步骤交给调度器;items交给数据管道(数据管道实现数据的最终处理);
官方文档
英文版:https://docs.scrapy.org/en/latest/
http://doc.scrapy.org/en/master/
中文版:https://scrapy-chs.readthedocs.io/zh_CN/latest/intro/overview.html
https://www.osgeo.cn/scrapy/topics/architecture.html
1. 安装
Linux:pip3 install scrapy
Windows:
a. pip3 install wheel
b. 下载twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
c. shift右击进入下载目录,执行 pip3 install typed_ast-1.4.0-cp36-cp36m-win32.whl
d. pip3 install pywin32
e. pip3 install scrapy
2.scrapy基本命令行
(1)建立一个新的项目 scrapy startproject ProjectName (2)生成爬虫 scrapy genspider +SpiderName+website (3)运行(crawl) # -o output scrapy crawl +SpiderName scrapy crawl SpiderName -o file.json scrapy crawl SpiderName-o file.csv (4)check检查错误 scrapy check (5)list返回项目全部spider名称 scrapy list (6)view 存储、打开网页 scrapy view https://www.baidu.com (7)scrapy shell,进入终端 scrapy shell https://www.baidu.com (8)scrapy runspider scrapy runspider zufang_spider.py
以麦田租房信息爬取为例,网站http://bj.maitian.cn/zfall/PG1
1.建立项目
scrapy startproject houseinfo
生成项目结构:
scrapy.cfg 项目的主配置信息。(真正爬虫相关的配置信息在settings.py文件中)
items.py 设置数据存储模板,用于结构化数据,如:Django的Model
pipelines 数据持久化处理
settings.py 配置文件
spiders 爬虫目录
2.建立爬虫应用程序
cd houseinfo
scrapy genspider maitian maitian.com
而后就能够在spiders目录下看到咱们的爬虫主程序
3.编写爬虫文件
步骤2执行完毕后,会在项目的spiders中生成一个应用名的py爬虫文件,文件源码以下:
1 # -*- coding: utf-8 -*- 2 import scrapy 3 4 5 class MaitianSpider(scrapy.Spider): 6 name = 'maitian' # 应用名称 7 allowed_domains = ['maitian.com'] #通常注释掉,容许爬取的域名(若是遇到非该域名的url则爬取不到数据) 8 start_urls = ['http://maitian.com/'] #起始爬取的url列表,该列表中存在的url,都会被parse进行请求的发送 9 10 #解析函数 11 def parse(self, response): 12 pass
咱们能够在此基础上,根据需求进行编写
1 # -*- coding: utf-8 -*- 2 import scrapy 3 4 class MaitianSpider(scrapy.Spider): 5 name = 'maitian' 6 start_urls = ['http://bj.maitian.cn/zfall/PG100'] 7 8 9 #解析函数 10 def parse(self, response): 11 12 li_list = response.xpath('//div[@class="list_wrap"]/ul/li') 13 results = [] 14 for li in li_list: 15 title = li.xpath('./div[2]/h1/a/text()').extract_first().strip() 16 price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip() 17 square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡','') # 将面积的单位去掉 18 area = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[0] # 以空格分隔 19 adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2] 20 21 dict = { 22 "标题":title, 23 "月租金":price, 24 "面积":square, 25 "区域":area, 26 "地址":adress 27 } 28 results.append(dict) 29 30 print(title,price,square,area,adress) 31 return results
须知:
二者等同,都是将列表中的内容提取出来
title = li.xpath('./div[2]/h1/a/text()').extract_first().strip()
title = li.xpath('./div[2]/h1/a/text()')[0].extract().strip()
4. 设置修改settings.py配置文件相关配置:
1 #假装请求载体身份 2 USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' 3 4 #能够忽略或者不遵照robots协议 5 ROBOTSTXT_OBEY = False
5.执行爬虫程序:scrapy crawl maitain
爬取全站数据,也就是所有页码数据。本例中,总共100页,观察页面之间的共性,构造通用url
方式一:经过占位符,构造通用url
1 import scrapy 2 3 class MaitianSpider(scrapy.Spider): 4 name = 'maitian' 5 start_urls = ['http://bj.maitian.cn/zfall/PG{}'.format(page) for page in range(1,4)] #注意写法 6 7 8 #解析函数 9 def parse(self, response): 10 11 li_list = response.xpath('//div[@class="list_wrap"]/ul/li') 12 results = [] 13 for li in li_list: 14 title = li.xpath('./div[2]/h1/a/text()').extract_first().strip() 15 price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip() 16 square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡','') 17 # 也能够经过正则匹配提取出来 18 area = li.xpath('./div[2]/p[2]/span/text()[2]')..re(r'昌平|朝阳|东城|大兴|丰台|海淀|石景山|顺义|通州|西城')[0] 19 adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2] 20 21 dict = { 22 "标题":title, 23 "月租金":price, 24 "面积":square, 25 "区域":area, 26 "地址":adress 27 } 28 results.append(dict) 29 30 return results
若是碰到一个表达式不能包含全部状况的项目,解决方式是先分别写表达式,最后经过列表相加,将全部url合并成一个url列表,例如
start_urls = ['http://www.guokr.com/ask/hottest/?page={}'.format(n) for n in range(1, 8)] + [ 'http://www.guokr.com/ask/highlight/?page={}'.format(m) for m in range(1, 101)]
方式二:经过重写start_requests方法,获取全部的起始url。(不用写start_urls
)
1 import scrapy 2 3 class MaitianSpider(scrapy.Spider): 4 name = 'maitian' 5 6 def start_requests(self): 7 pages=[] 8 for page in range(90,100): 9 url='http://bj.maitian.cn/zfall/PG{}'.format(page) 10 page=scrapy.Request(url) 11 pages.append(page) 12 return pages 13 14 #解析函数 15 def parse(self, response): 16 17 li_list = response.xpath('//div[@class="list_wrap"]/ul/li') 18 19 results = [] 20 for li in li_list: 21 title = li.xpath('./div[2]/h1/a/text()').extract_first().strip(), 22 price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip(), 23 square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡',''), 24 area = li.xpath('./div[2]/p[2]/span/text()[2]').re(r'昌平|朝阳|东城|大兴|丰台|海淀|石景山|顺义|通州|西城')[0], 25 adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2] 26 27 dict = { 28 "标题":title, 29 "月租金":price, 30 "面积":square, 31 "区域":area, 32 "地址":adress 33 } 34 results.append(dict) 35 36 return results
只要是数据持久化存储,parse方法必须有返回值(也就是return后的内容)
1. 基于终端指令的持久化存储
执行输出指定格式进行存储:将爬取到的数据写入不一样格式的文件中进行存储,windows终端不能使用txt格式
以麦田为例,spider中的代码不变,将返回值写到qiubai.csv中。本地没有,就会本身建立一个。本地有就会追加
scrapy crawl maitian -o maitian.csv
就会在项目目录下看到,生成的文件
查看文件内容
2.基于管道的持久化存储
scrapy框架中已经为咱们专门集成好了高效、便捷的持久化操做功能,咱们直接使用便可。要想使用scrapy的持久化操做功能,咱们首先来认识以下两个文件:
持久化流程:
① 爬虫文件爬取到数据解析后,须要将数据封装到items对象中。
② 使用yield关键字将items对象提交给pipelines管道,进行持久化操做。
③ 在管道文件中的process_item方法中接收爬虫文件提交过来的item对象,而后编写持久化存储的代码,将item对象中存储的数据进行持久化存储(在管道的process_item方法中执行io操做,进行持久化存储)
④ settings.py配置文件中开启管道
2.1保存到本地的持久化存储
爬虫文件:maitian.py
1 import scrapy 2 from houseinfo.items import HouseinfoItem # 将item导入 3 4 class MaitianSpider(scrapy.Spider): 5 name = 'maitian' 6 start_urls = ['http://bj.maitian.cn/zfall/PG100'] 7 8 #解析函数 9 def parse(self, response): 10 11 li_list = response.xpath('//div[@class="list_wrap"]/ul/li') 12 13 for li in li_list: 14 item = HouseinfoItem( 15 title = li.xpath('./div[2]/h1/a/text()').extract_first().strip(), 16 price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip(), 17 square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡',''), 18 area = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[0], 19 adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2] 20 ) 21 22 yield item # 提交给管道,而后管道定义存储方式
items文件:items.py
1 import scrapy 2 3 class HouseinfoItem(scrapy.Item): 4 title = scrapy.Field() #存储标题,里面能够存储任意类型的数据 5 price = scrapy.Field() 6 square = scrapy.Field() 7 area = scrapy.Field() 8 adress = scrapy.Field()
管道文件:pipelines.py
1 class HouseinfoPipeline(object): 2 def __init__(self): 3 self.file = None 4 5 #开始爬虫时,执行一次 6 def open_spider(self,spider): 7 self.file = open('maitian.csv','a',encoding='utf-8') # 选用了追加模式 8 self.file.write(",".join(["标题","月租金","面积","区域","地址","\n"])) 9 print("开始爬虫") 10 11 # 由于该方法会被执行调用屡次,因此文件的开启和关闭操做写在了另外两个只会各自执行一次的方法中。 12 def process_item(self, item, spider): 13 content = [item["title"], item["price"], item["square"], item["area"], item["adress"], "\n"] 14 self.file.write(",".join(content)) 15 return item 16 17 # 结束爬虫时,执行一次 18 def close_spider(self,spider): 19 self.file.close() 20 print("结束爬虫")
配置文件:settings.py
1 #假装请求载体身份 2 USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' 3 4 #能够忽略或者不遵照robots协议 5 ROBOTSTXT_OBEY = False 6 7 #开启管道 8 ITEM_PIPELINES = { 9 'houseinfo.pipelines.HouseinfoPipeline': 300, #数值300表示为优先级,值越小优先级越高 10 }
要求:
1.爬取豆瓣top 250电影名字、演员列表、评分和简介
2.设置随机UserAgent和Proxy
3.爬取到的数据保存到MongoDB数据库
items.py
# -*- coding: utf-8 -*- import scrapy class DoubanItem(scrapy.Item): # define the fields for your item here like: # 标题 title = scrapy.Field() # 信息 bd = scrapy.Field() # 评分 star = scrapy.Field() # 简介 quote = scrapy.Field()
doubanmovie.py
# -*- coding: utf-8 -*- import scrapy from douban.items import DoubanItem class DoubamovieSpider(scrapy.Spider): name = "doubanmovie" allowed_domains = ["movie.douban.com"] offset = 0 url = "https://movie.douban.com/top250?start=" start_urls = ( url+str(offset), ) def parse(self, response): item = DoubanItem() movies = response.xpath("//div[@class='info']") for each in movies: # 标题 item['title'] = each.xpath(".//span[@class='title'][1]/text()").extract()[0] # 信息 item['bd'] = each.xpath(".//div[@class='bd']/p/text()").extract()[0] # 评分 item['star'] = each.xpath(".//div[@class='star']/span[@class='rating_num']/text()").extract()[0] # 简介 quote = each.xpath(".//p[@class='quote']/span/text()").extract() if len(quote) != 0: item['quote'] = quote[0] yield item if self.offset < 225: self.offset += 25 yield scrapy.Request(self.url + str(self.offset), callback = self.parse)
pipelines.py
# -*- coding: utf-8 -*- import pymongo from scrapy.conf import settings class DoubanPipeline(object): def __init__(self): host = settings["MONGODB_HOST"] port = settings["MONGODB_PORT"] dbname = settings["MONGODB_DBNAME"] sheetname= settings["MONGODB_SHEETNAME"] # 建立MONGODB数据库连接 client = pymongo.MongoClient(host = host, port = port) # 指定数据库 mydb = client[dbname] # 存放数据的数据库表名 self.sheet = mydb[sheetname] def process_item(self, item, spider): data = dict(item) self.sheet.insert(data) return item
settings.py
DOWNLOAD_DELAY = 2.5 COOKIES_ENABLED = False DOWNLOADER_MIDDLEWARES = { 'douban.middlewares.RandomUserAgent': 100, 'douban.middlewares.RandomProxy': 200, } USER_AGENTS = [ 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0)', 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2)', 'Opera/9.27 (Windows NT 5.2; U; zh-cn)', 'Opera/8.0 (Macintosh; PPC Mac OS X; U; en)', 'Mozilla/5.0 (Macintosh; PPC Mac OS X; U; en) Opera 8.0', 'Mozilla/5.0 (Linux; U; Android 4.0.3; zh-cn; M032 Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', 'Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13' ] PROXIES = [ {"ip_port" :"121.42.140.113:16816", "user_passwd" : "****"}, #{"ip_prot" :"121.42.140.113:16816", "user_passwd" : ""} #{"ip_prot" :"121.42.140.113:16816", "user_passwd" : ""} #{"ip_prot" :"121.42.140.113:16816", "user_passwd" : ""} ] ITEM_PIPELINES = { 'douban.pipelines.DoubanPipeline': 300, } # MONGODB 主机名 MONGODB_HOST = "127.0.0.1" # MONGODB 端口号 MONGODB_PORT = 27017 # 数据库名称 MONGODB_DBNAME = "Douban" # 存放数据的表名称 MONGODB_SHEETNAME = "doubanmovies"
middlewares.py
#!/usr/bin/env python # -*- coding:utf-8 -*- import random import base64 from settings import USER_AGENTS from settings import PROXIES # 随机的User-Agent class RandomUserAgent(object): def process_request(self, request, spider): useragent = random.choice(USER_AGENTS) #print useragent request.headers.setdefault("User-Agent", useragent) class RandomProxy(object): def process_request(self, request, spider): proxy = random.choice(PROXIES) if proxy['user_passwd'] is None: # 没有代理帐户验证的代理使用方式 request.meta['proxy'] = "http://" + proxy['ip_port'] else: # 对帐户密码进行base64编码转换 base64_userpasswd = base64.b64encode(proxy['user_passwd']) # 对应到代理服务器的信令格式里 request.headers['Proxy-Authorization'] = 'Basic ' + base64_userpasswd request.meta['proxy'] = "http://" + proxy['ip_port']
爬取多级页面,会遇到2个问题:
问题1:如何对下一层级页面发送请求?
答:在每个解析函数的末尾,经过Request方法对下一层级的页面手动发起请求
# 先提取二级页面url,再对二级页面发送请求。多级页面以此类推 def parse(self, response): next_url = response.xpath('//div[2]/h2/a/@href').extract()[0] # 提取二级页面url yield scrapy.Request(url=next_url, callback=self.next_parse) # 对二级页面发送请求,注意要用yield,回调函数不带括号
问题2:解析的数据不在同一张页面中,最终如何将数据传递
答:涉及到请求传参,能够在对下一层级页面发送请求的时候,经过meta参数进行数据传递,meta字典就会传递给回调函数的response参数。下一级的解析函数经过response获取item(先经过 response.meta返回接收到的meta字典,再得到item字典)
# 经过meta参数进行Request的数据传递,meta字典就会传递给回调函数的response参数 def parse(self, response): item = Item() # 实例化item对象 Item["field1"] = response.xpath('expression1').extract()[0] # 列表中只有一个元素 Item["field2"] = response.xpath('expression2').extract() # 列表 next_url = response.xpath('expression3').extract()[0] # 提取二级页面url # meta参数:请求传参.经过meta参数进行Request的数据传递,meta字典就会传递给回调函数的response参数 yield scrapy.Request(url=next_url, callback=self.next_parse,meta={'item':item}) # 对二级页面发送请求 def next_parse(self,response): # 经过response获取item. 先经过 response.meta返回接收到的meta字典,再得到item字典 item = response.meta['item'] item['field'] = response.xpath('expression').extract_first() yield item #提交给管道
案例1:麦田,对全部页码发送请求。不推荐将每个页码对应的url存放到爬虫文件的起始url列表(start_urls)中。这里咱们使用Request方法手动发起请求。
# -*- coding: utf-8 -*- import scrapy from houseinfo.items import HouseinfoItem # 将item导入 class MaitianSpider(scrapy.Spider): name = 'maitian' start_urls = ['http://bj.maitian.cn/zfall/PG1'] #爬取多页 page = 1 url = 'http://bj.maitian.cn/zfall/PG%d' #解析函数 def parse(self, response): li_list = response.xpath('//div[@class="list_wrap"]/ul/li') for li in li_list: item = HouseinfoItem( title = li.xpath('./div[2]/h1/a/text()').extract_first().strip(), price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip(), square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡',''), area = li.xpath('./div[2]/p[2]/span/text()[2]').re(r'昌平|朝阳|东城|大兴|丰台|海淀|石景山|顺义|通州|西城')[0], # 也能够经过正则匹配提取出来 adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2] ) ['http://bj.maitian.cn/zfall/PG{}'.format(page) for page in range(1, 4)] yield item # 提交给管道,而后管道定义存储方式 if self.page < 4: self.page += 1 new_url = format(self.url%self.page) # 这里的%是拼接的意思 yield scrapy.Request(url=new_url,callback=self.parse) # 手动发起一个请求,注意必定要写yield
案例2:这个案例比较好的一点是,parse函数,既有对下一页的回调,又有对详情页的回调
import scrapy class QuotesSpider(scrapy.Spider): name = 'quotes_2_3' start_urls = [ 'http://quotes.toscrape.com', ] allowed_domains = [ 'toscrape.com', ] def parse(self,response): for quote in response.css('div.quote'): yield{ 'quote': quote.css('span.text::text').extract_first(), 'author': quote.css('small.author::text').extract_first(), 'tags': quote.css('div.tags a.tag::text').extract(), } author_page = response.css('small.author+a::attr(href)').extract_first() authro_full_url = response.urljoin(author_page) yield scrapy.Request(authro_full_url, callback=self.parse_author) # 对详情页发送请求,回调详情页的解析函数 next_page = response.css('li.next a::attr("href")').extract_first() # 经过css选择器定位到下一页 if next_page is not None: next_full_url = response.urljoin(next_page) yield scrapy.Request(next_full_url, callback=self.parse) # 对下一页发送请求,回调本身的解析函数 def parse_author(self,response): yield{ 'author': response.css('.author-title::text').extract_first(), 'author_born_date': response.css('.author-born-date::text').extract_first(), 'author_born_location': response.css('.author-born-location::text').extract_first(), 'authro_description': response.css('.author-born-location::text').extract_first(),
案例3:爬取www.id97.com电影网,将一级页面中的电影名称,类型,评分,二级页面中的上映时间,导演,片长进行爬取。(多级页面+传参)
# -*- coding: utf-8 -*- import scrapy from moviePro.items import MovieproItem class MovieSpider(scrapy.Spider): name = 'movie' allowed_domains = ['www.id97.com'] start_urls = ['http://www.id97.com/'] def parse(self, response): div_list = response.xpath('//div[@class="col-xs-1-5 movie-item"]') for div in div_list: item = MovieproItem() item['name'] = div.xpath('.//h1/a/text()').extract_first() item['score'] = div.xpath('.//h1/em/text()').extract_first() item['kind'] = div.xpath('.//div[@class="otherinfo"]').xpath('string(.)').extract_first() item['detail_url'] = div.xpath('./div/a/@href').extract_first() #meta参数:请求传参.经过meta参数进行Request的数据传递,meta字典就会传递给回调函数的response参数 yield scrapy.Request(url=item['detail_url'],callback=self.parse_detail,meta={'item':item}) def parse_detail(self,response): #经过response获取item. 先经过 response.meta返回接收到的meta字典,再得到item字典 item = response.meta['item'] item['actor'] = response.xpath('//div[@class="row"]//table/tr[1]/a/text()').extract_first() item['time'] = response.xpath('//div[@class="row"]//table/tr[7]/td[2]/text()').extract_first() item['long'] = response.xpath('//div[@class="row"]//table/tr[8]/td[2]/text()').extract_first() yield item #提交item到管道
案例4:稍复杂,可参考连接进行理解:https://github.com/makcyun/web_scraping_with_python/tree/master/,https://www.cnblogs.com/sanduzxcvbnm/p/10277414.html
1 #!/user/bin/env python 2 3 """ 4 爬取豌豆荚网站全部分类下的所有 app 5 数据爬取包括两个部分: 6 一:数据指标 7 1 爬取首页 8 2 爬取第2页开始的 ajax 页 9 二:图标 10 使用class方法下载首页和 ajax 页 11 分页循环两种爬取思路, 12 指定页数进行for 循环,和不指定页数一直往下爬直到爬不到内容为止 13 1 for 循环 14 """ 15 16 import scrapy 17 from wandoujia.items import WandoujiaItem 18 19 import requests 20 from pyquery import PyQuery as pq 21 import re 22 import csv 23 import pandas as pd 24 import numpy as np 25 import time 26 import pymongo 27 import json 28 import os 29 from urllib.parse import urlencode 30 import random 31 import logging 32 33 logging.basicConfig(filename='wandoujia.log',filemode='w',level=logging.DEBUG,format='%(asctime)s %(message)s',datefmt='%Y/%m/%d %I:%M:%S %p') 34 # https://juejin.im/post/5aee70105188256712786b7f 35 logging.warning("warn message") 36 logging.error("error message") 37 38 39 class WandouSpider(scrapy.Spider): 40 name = 'wandou' 41 allowed_domains = ['www.wandoujia.com'] 42 start_urls = ['http://www.wandoujia.com/'] 43 44 def __init__(self): 45 self.cate_url = 'https://www.wandoujia.com/category/app' 46 # 首页url 47 self.url = 'https://www.wandoujia.com/category/' 48 # ajax 请求url 49 self.ajax_url = 'https://www.wandoujia.com/wdjweb/api/category/more?' 50 # 实例化分类标签 51 self.wandou_category = Get_category() 52 53 def start_requests(self): 54 yield scrapy.Request(self.cate_url,callback=self.get_category) 55 56 def get_category(self,response): 57 # # num = 0 58 cate_content = self.wandou_category.parse_category(response) 59 for item in cate_content: 60 child_cate = item['child_cate_codes'] 61 for cate in child_cate: 62 cate_code = item['cate_code'] 63 cate_name = item['cate_name'] 64 child_cate_code = cate['child_cate_code'] 65 child_cate_name = cate['child_cate_name'] 66 67 68 # # 单类别下载 69 # cate_code = 5029 70 # child_cate_code = 837 71 # cate_name = '通信社交' 72 # child_cate_name = '收音机' 73 74 # while循环 75 page = 1 # 设置爬取起始页数 76 print('*' * 50) 77 78 # # for 循环下一页 79 # pages = [] 80 # for page in range(1,3): 81 # print('正在爬取:%s-%s 第 %s 页 ' % 82 # (cate_name, child_cate_name, page)) 83 logging.debug('正在爬取:%s-%s 第 %s 页 ' % 84 (cate_name, child_cate_name, page)) 85 86 if page == 1: 87 # 构造首页url 88 category_url = '{}{}_{}' .format(self.url, cate_code, child_cate_code) 89 else: 90 params = { 91 'catId': cate_code, # 大类别 92 'subCatId': child_cate_code, # 小类别 93 'page': page, 94 } 95 category_url = self.ajax_url + urlencode(params) 96 97 dict = {'page':page,'cate_name':cate_name,'cate_code':cate_code,'child_cate_name':child_cate_name,'child_cate_code':child_cate_code} 98 99 yield scrapy.Request(category_url,callback=self.parse,meta=dict) 100 101 # # for 循环方法 102 # pa = yield scrapy.Request(category_url,callback=self.parse,meta=dict) 103 # pages.append(pa) 104 # return pages 105 106 def parse(self, response): 107 if len(response.body) >= 100: # 判断该页是否爬完,数值定为100是由于无内容时长度是87 108 page = response.meta['page'] 109 cate_name = response.meta['cate_name'] 110 cate_code = response.meta['cate_code'] 111 child_cate_name = response.meta['child_cate_name'] 112 child_cate_code = response.meta['child_cate_code'] 113 114 if page == 1: 115 contents = response 116 else: 117 jsonresponse = json.loads(response.body_as_unicode()) 118 contents = jsonresponse['data']['content'] 119 # response 是json,json内容是html,html 为文本不能直接使用.css 提取,要先转换 120 contents = scrapy.Selector(text=contents, type="html") 121 122 contents = contents.css('.card') 123 for content in contents: 124 # num += 1 125 item = WandoujiaItem() 126 item['cate_name'] = cate_name 127 item['child_cate_name'] = child_cate_name 128 item['app_name'] = self.clean_name(content.css('.name::text').extract_first()) 129 item['install'] = content.css('.install-count::text').extract_first() 130 item['volume'] = content.css('.meta span:last-child::text').extract_first() 131 item['comment'] = content.css('.comment::text').extract_first().strip() 132 item['icon_url'] = self.get_icon_url(content.css('.icon-wrap a img'),page) 133 yield item 134 135 # 递归爬下一页 136 page += 1 137 params = { 138 'catId': cate_code, # 大类别 139 'subCatId': child_cate_code, # 小类别 140 'page': page, 141 } 142 ajax_url = self.ajax_url + urlencode(params) 143 144 dict = {'page':page,'cate_name':cate_name,'cate_code':cate_code,'child_cate_name':child_cate_name,'child_cate_code':child_cate_code} 145 yield scrapy.Request(ajax_url,callback=self.parse,meta=dict) 146 147 148 149 # 名称清除方法1 去除不能用于文件命名的特殊字符 150 def clean_name(self, name): 151 rule = re.compile(r"[\/\\\:\*\?\"\<\>\|]") # '/ \ : * ? " < > |') 152 name = re.sub(rule, '', name) 153 return name 154 155 def get_icon_url(self,item,page): 156 if page == 1: 157 if item.css('::attr("src")').extract_first().startswith('https'): 158 url = item.css('::attr("src")').extract_first() 159 else: 160 url = item.css('::attr("data-original")').extract_first() 161 # ajax页url提取 162 else: 163 url = item.css('::attr("data-original")').extract_first() 164 165 # if url: # 不要在这里添加url存在判断,不然空url 被过滤掉 致使编号对不上 166 return url 167 168 169 # 首先获取主分类和子分类的数值代码 # # # # # # # # # # # # # # # # 170 class Get_category(): 171 def parse_category(self, response): 172 category = response.css('.parent-cate') 173 data = [{ 174 'cate_name': item.css('.cate-link::text').extract_first(), 175 'cate_code': self.get_category_code(item), 176 'child_cate_codes': self.get_child_category(item), 177 } for item in category] 178 return data 179 180 # 获取全部主分类标签数值代码 181 def get_category_code(self, item): 182 cate_url = item.css('.cate-link::attr("href")').extract_first() 183 184 pattern = re.compile(r'.*/(\d+)') # 提取主类标签代码 185 cate_code = re.search(pattern, cate_url) 186 return cate_code.group(1) 187 188 # 获取全部子分类标签数值代码 189 def get_child_category(self, item): 190 child_cate = item.css('.child-cate a') 191 child_cate_url = [{ 192 'child_cate_name': child.css('::text').extract_first(), 193 'child_cate_code': self.get_child_category_code(child) 194 } for child in child_cate] 195 196 return child_cate_url 197 198 # 正则提取子分类 199 def get_child_category_code(self, child): 200 child_cate_url = child.css('::attr("href")').extract_first() 201 pattern = re.compile(r'.*_(\d+)') # 提取小类标签编号 202 child_cate_code = re.search(pattern, child_cate_url) 203 return child_cate_code.group(1) 204 205 # # 能够选择保存到txt 文件 206 # def write_category(self,category): 207 # with open('category.txt','a',encoding='utf_8_sig',newline='') as f: 208 # w = csv.writer(f) 209 # w.writerow(category.values())
以上4个案例都只贴出了爬虫主程序脚本,因篇幅缘由,因此item、pipeline和settings等脚本未贴出,可参考上面案例进行编写。
问题:在以前代码中,咱们历来没有手动的对start_urls列表中存储的起始url进行过请求的发送,可是起始url的确是进行了请求的发送,那这是如何实现的呢?
解答:实际上是由于爬虫文件中的爬虫类继承到了Spider父类中的start_requests(self)这个方法,该方法就能够对start_urls列表中的url发起请求:
def start_requests(self): for u in self.start_urls: yield scrapy.Request(url=u,callback=self.parse)
注意:该方法默认的实现,是对起始的url发起get请求,若是想发起post请求,则须要子类重写该方法。不过,通常状况下不用scrapy发post请求,用request模块。
例:爬取百度翻译
# -*- coding: utf-8 -*- import scrapy class PostSpider(scrapy.Spider): name = 'post' # allowed_domains = ['www.xxx.com'] start_urls = ['https://fanyi.baidu.com/sug'] def start_requests(self): data = { # post请求参数 'kw':'dog' } for url in self.start_urls: yield scrapy.FormRequest(url=url,formdata=data,callback=self.parse) # 发送post请求 def parse(self, response): print(response.text)
- 在使用scrapy crawl spiderFileName运行程序时,在终端里打印输出的就是scrapy的日志信息。
- 日志信息的种类:
ERROR : 通常错误
WARNING : 警告
INFO : 通常的信息
DEBUG : 调试信息
- 设置日志信息指定输出:
在settings.py配置文件中,加入
LOG_LEVEL = ‘指定日志信息种类’便可。
LOG_FILE = 'log.txt'则表示将日志信息写入到指定文件中进行存储。
其余经常使用设置:
BOT_NAME 默认:“scrapybot”,使用startproject命令建立项目时,其被自动赋值 CONCURRENT_ITEMS 默认为100,Item Process(即Item Pipeline)同时处理(每一个response的)item时最大值 CONCURRENT_REQUEST 默认为16,scrapy downloader并发请求(concurrent requests)的最大值 LOG_ENABLED 默认为True,是否启用logging DEFAULT_REQUEST_HEADERS 默认以下:{'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en',} scrapy http request使用的默认header LOG_ENCODING 默认utt-8,logging中使用的编码 LOG_LEVEL 默认“DEBUG”,log中最低级别,可选级别有:CRITICAL,ERROR,WARNING,DEBUG USER_AGENT 默认:“Scrapy/VERSION(....)”,爬取的默认User-Agent,除非被覆盖 COOKIES_ENABLED=False,禁用cookies
实际开发中,一般在同一个项目里会有多个爬虫,多个爬虫的时候是怎么将他们运行起来呢?
运行单个爬虫
import sys from scrapy.cmdline import execute if __name__ == '__main__': execute(["scrapy","crawl","maitian","--nolog"])
而后运行py文件便可运行名为‘maitian‘的爬虫
同时运行多个爬虫
步骤以下:
- 在spiders同级建立任意目录,如:commands
- 在其中建立 crawlall.py 文件 (此处文件名就是自定义的命令)
- 在settings.py 中添加配置 COMMANDS_MODULE = '项目名称.目录名称'
- 在项目目录执行命令:scrapy crawlall
crawlall.py代码
1 from scrapy.commands import ScrapyCommand 2 from scrapy.utils.project import get_project_settings 3 4 class Command(ScrapyCommand): 5 6 requires_project = True 7 8 def syntax(self): 9 return '[options]' 10 11 def short_desc(self): 12 return 'Runs all of the spiders' 13 14 def run(self, args, opts): 15 spider_list = self.crawler_process.spiders.list() 16 for name in spider_list: 17 self.crawler_process.crawl(name, **opts.__dict__) 18 self.crawler_process.start()