通常情况下,我们都会自己去编写完整的代码来进行数据抓取,但是对于爬取量较大的时候,我们所编写的程序在时间上说不定就没有什么优势了,虽然可以通过多进程、异步等操作来减少爬取时间,但是由于异步编写起来过于复杂(多进程还是挺简单的),并且需要大量的重构代码。那有没有什么简单的方法来实现大量数据的快速抓取呢?当然有,那就是使用Scrapy
框架,其是一个快速、高层次的屏幕抓取和web抓取框架,能够实现快速的数据抓取。
之前我们讲了使用requests.Session()
来实现,虽然比较简单,但是现在各大公司在招聘员工时都需要熟悉Scrapy
框架,因此,今天就来谈一谈如何用Scrapy
来模拟登陆并对数据进行抓取。
创建项目
在Scrapy
中可直接用Scrapy
命令来生产,命令如下:
1
scrapy startproject Alita
这里Alita是我们所创建的文件夹名称。
创建Spider
Spider是我们自己定义的类,Scrapy
用它来对网页进行抓取并进行解析。同样,Spider的创建页可以通过命令行来实现,在这里,我们要在刚刚创建的alita文件夹中执行命令
Tip:可直接进入到该文件夹下,按住Shift,在按下鼠标左键,可快速进入该文件夹下的命令窗口 命令如下:
1 2 3
scrapy genspider alita douban.com 或者 scrapy genspider -t crawl alita douban.com # 这个在创建时使用的是模板crawl
这里需要注意的是Spider的名称不能和项目的名称重复。
创建后的alita.py
的内容为:
1 2 3 4 5 6 7 8 9 10
import scrapyclass (scrapy.Spider) : name = 'alita' allowed_domains = ['douban.com' ] start_urls = ['http://douban.com/' ] def parse (self, response) : pass
创建容器Item
创建容器,顾名思义就是要创建一个来存放爬取的数据东西,也就是创建Item,它的使用方法和字典相似。在这里,我们要抓取的是’用户昵称’,’评分’,’评论’,’觉得有用的人数’。
由于在创建项目时,创建了 items.py
, 因此我们只需要对其内容进行修改,改为我们所需要的字段,代码如下:
1 2 3 4 5 6 7
import scrapyclass AlitaItem (scrapy.Item) : user_nick = scrapy.Field() score = scrapy.Field() content = scrapy.Field() userful_num = scrapy.Field()
解析Response,使用Item
本来这里应该是先讲模拟登陆,生成Request请求的,但考虑到内容过多,就放到后面来讲。在Scrapy
将网页下载下来后,对response变量的内容解析,并将我们所需要的内容提取出来赋值给Item字段,alita.py
中部分代码如下:
1 2 3 4 5 6 7 8 9
def parse_item (self, response) : results = response.css('div.comment-item' ) for result in results: item = AlitaItem() item['user_nick' ] = result.css('span.comment-info > a::text' ).extract_first() item['score' ] = result.css('span.rating::attr(title)' ).extract_first() item['content' ] = result.css('span.short::text' ).extract_first() item['userful_num' ] = result.css('span.votes::text' ).extract_first() yield item
我们使用了CSS选择器来对数据进行提取,具体的表达式再此就不进行阐述了。大家可在w3cschool 中找到详细的讲解。
模拟登陆
用Scrapy
来进行模拟登陆的方法主要有三种:
1.自己直接登陆网站,将登陆成功的cookies
保存下来,供Scrapy
直接携带
对于该方法不进行展开,毕竟有些网站的cookies
会发生变化,短时间内保存下来的cookies
再进行模拟登陆时会成功,但是过段时间就不行,因此,对于这种方法并不推荐。当然,没有其他办法的时候,还是可以使用该类方法的(如登陆时需要验证码,验证码较为复杂,或者由一些加密的难以破解的数据)
2.使用scrapy.Formrequest.from_response()
进行登陆, 自动解析当前登陆url,找到表单,发送post请求
该方法对于大对数网站来说应该都可以实现,因为它能够实现自动解析得到表单,并发送post请求,这能给我们的编程带来极大的便利。 该方法的关键就是对表单进行填写,然后通过scrapy.Formrequest.from_response()
进行提交便可以了,在这过程中from_response()
用来模拟表单上的提交单击。
你可能会问,表单中的内容如何填写呢?不用担心,直接在parse
函数中写入即可。注意的是在这里我们要对start_urls
进行设置,将其设置为登陆请求的url,parse
将对start_urls
中返回的response进行解析,进行进一步处理 。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class (scrapy.Spider) : name = 'alita' allowed_domains = ['douban.com' ] start_urls = ['https://accounts.douban.com/j/mobile/login/basic' ] base_url = 'https://movie.douban.com/subject/1652592/comments?start={}&limit=20&sort=new_score&status=P' def parse (self, response) : postData = { 'ck' : '' , 'name' : '****' , 'password' : '****' , 'remember' : 'false' , 'ticket' : '' } return [FormRequest.from_response(response, formdata = postData, callback = self.after_login, dont_filter = True )]
这里callback回调函数设置的是需要登陆成功后才能进行的一些网页请求。
很不幸的是,在使用该方法进行模拟登陆时,Scrapy
的某条Debug显示为403
1
2019-02-25 22:52:55 [scrapy.core.engine] DEBUG: Crawled (403) <GET https://accounts.douban.com/j/mobile/login/basic> (referer: None)
并且报错信息(截取部分)为:
1 2
raise ValueError("No <form> element found in %s" % response) ValueError: No <form> element found in <403 https://accounts.douban.com/j/mobile/login/basic>
通过报错信息我们知道时,在解析的response中并没有表单信息存在,这是因为from_response()
会对得到的response进行form表单的提取,因此在使用该方法时,一定要确保response中有form表单,否则,都会出现上述的错误 ,比如我们这里的豆瓣登陆界面就时不存在form表单的,用浏览器打开请求链接,我们只能看到如下的界面(一般都是空白的界面)使用该方法,确保response中有form表单!!! 使用该方法,确保response中有form表单!!! 使用该方法,确保response中有form表单!!!
因此,在这里想让该方法可行,只需要将start_urls
内容修改为我们可见的登陆界面https://accounts.douban.com/passport/login即可。
3.使用scrapy.FormRequest()进行登陆,对设置的url发送post请求,对得到的cookies
进行存储
既然让函数自己去找表单行不通,那我们就自己来呗。不过在这里同样需要用到FormRequest()
,只是不再使用from_response()
。
直接上代码:
而是用了FormRequest实例,手动指定post地址,meta参数同样是要带上的,将response.meta[‘cookiejar’]赋值给cookiejar,供后面的Request使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
def start_requests (self) : return [Request(url = 'https://movie.douban.com' , meta = {'cookiejar' :1 }, callback = self.post_login)] def post_login (self, response) : return FormRequest( url = 'https://accounts.douban.com/j/mobile/login/basic' , method = 'POST' , formdata = { 大专栏 Scrapy模拟登陆豆瓣抓取数据 'ck' : '' , 'name' : '***' , 'password' : '***' , 'remember' : 'false' , 'ticket' : '' }, meta = {'cookiejar' :response.meta['cookiejar' ]}, dont_filter = True , callback = self.after_login ) def after_login (self, response) : for i in range(22 , 24 ): url = self.base_url.format(i * 20 ) yield Request(url = url, meta = {'cookiejar' :1 }, callback = self.parse_item, dont_filter = True )
在start_requests
中,会默认使用start_urls
里面的url来构造Request,在这里我们直接在Request中指定了url,让spider先去访问豆瓣首页(以获取一些隐藏的表单项,在豆瓣登陆里其实并没有什么隐藏的表单项)。start_requests
必须返回应该可迭代的对象,因此我们在return后面加了‘[ ]’ 。之后通过回调函数来发送post请求。
在post_login
中,我们指定访问方式为post,通过meta参数将response.meta[‘cookiejar’]赋值给cookiejar,供后续的Request使用,并不进行过滤处理。在登陆成功之后,通过回调函数用得到的cookies
来生成我们要抓取的页面的Request,同样不进行过滤处理。
alita.py
中的完整代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
import scrapyfrom scrapy.linkextractors import LinkExtractorfrom scrapy.spiders import CrawlSpider, Rule, Requestfrom alita.items import AlitaItemfrom scrapy.http import FormRequestclass (scrapy.Spider) : name = 'alita' allowed_domains = ['douban.com' ] base_url = 'https://movie.douban.com/subject/1652592/comments?start={}&limit=20&sort=new_score&status=P' def start_requests (self) : return [Request(url = 'https://movie.douban.com' , meta = {'cookiejar' :1 }, callback = self.post_login)] def post_login (self, response) : return FormRequest( url = 'https://accounts.douban.com/j/mobile/login/basic' , method = 'POST' , formdata = { 'ck' : '' , 'name' : '***' , 'password' : '***' , 'remember' : 'false' , 'ticket' : '' }, meta = {'cookiejar' :response.meta['cookiejar' ]}, dont_filter = True , callback = self.after_login ) def after_login (self, response) : for i in range(22 , 24 ): url = self.base_url.format(i * 20 ) yield Request(url = url, meta = {'cookiejar' :1 }, callback = self.parse_item, dont_filter = True ) def parse_item (self, response) : results = response.css('div.comment-item' ) for result in results: item = AlitaItem() item['user_nick' ] = result.css('span.comment-info > a::text' ).extract_first() item['score' ] = result.css('span.rating::attr(title)' ).extract_first() item['content' ] = result.css('span.short::text' ).extract_first() item['userful_num' ] = result.css('span.votes::text' ).extract_first() yield item
到目前为止,我们的工作已经完成99%了,就差最后一步了。为了能够让Request加入直接登陆后的cookies
信息,我们需要在settings.py
中的DOWNLOADER_MIDDLEWARES
开启中间件scrapy.downloadermiddlewares.cookies.CookiesMiddleware
关于中间件的更多信息可参阅http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/settings.html#std:setting-DOWNLOADER_MIDDLEWARES_BASE
由于豆瓣中存在着反爬虫机制,所以我们还需要增加User-Agent来伪装成浏览器,在这里为了省事儿,我们直接在settings.py
中进行了修改
最后得到的settings.py
代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
BOT_NAME = 'alita' SPIDER_MODULES = ['alita.spiders' ] NEWSPIDER_MODULE = 'alita.spiders' USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36' ROBOTSTXT_OBEY = False DOWNLOADER_MIDDLEWARES = { 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware' : 700 , }
运行程序
进入目录,运行如下命令:
由于Scrapy
的运行结果过长,我们仅截取了部分关键信息放在这里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
2019-02-26 09:11:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://movie.douban.com> (referer: None) 2019-02-26 09:11:00 [scrapy.core.engine] DEBUG: Crawled (200) <POST https://accounts.douban.com/j/mobile/login/basic> (referer: https://movie.douban.com) 2019-02-26 09:11:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://movie.douban.com/subject/1652592/comments?start=460&limit=20&sort=new_score&status=P> (referer: https://accounts.douban.com/j/mobile/login/basic) 2019-02-26 09:11:01 [scrapy.core.scraper] DEBUG: Scraped from <200 https://movie.douban.com/subject/1652592/comments?start=460&limit=20&sort=new_score&status=P> 2019-02-26 09:11:01 [scrapy.core.scraper] DEBUG: Scraped from <200 https://movie.douban.com/subject/1652592/comments?start=460&limit=20&sort=new_score&status=P> {'content': '**********', 'score': '还行', 'user_nick': '*****', 'userful_num': '***'} 2019-02-26 09:11:01 [scrapy.core.scraper] DEBUG: Scraped from <200 https://movie.douban.com/subject/1652592/comments?start=460&limit=20&sort=new_score&status=P> {'content': '*********', 'score': '力荐', 'user_nick': '*****', 'userful_num': '***'} ....
这里为保护用户信息,我们将评论等内容替换为了‘***’
至此,我们便完整的实现了使用Scrapy
模拟登陆豆瓣并对数据进行抓取。后续还可以将数据保存到本地,数据分析,可视化等操作。
好好学习,天天向上。