您的位置:首页 > 其它

爬虫 Scrapy 学习系列之一:Tutorial

2018-02-07 00:00 316 查看

前言

笔者打算写一系列的文章,记录自己在学习并使用Scrapy的点滴;作者打算使用python3.6作为Scrapy的基础运行环境;

本文为作者的原创作品,转载需注明出处;本文转载自本人的博客,伤神的博客:http://www.shangyang.me/2017/06/29/scrapy-learning-1-tutorial/

Scrapy安装

我本地安装有两个版本的python,2.7和3.6;而正如前言所描述的那样,笔者打算使用Python3.6的环境来搭建Scrapy;

$pipinstallScrapy
默认安装的支持Python2.7版本的Scrapy;

$pip3installScrap
安装的是支持python3.x版本的Scrapy;不过安装过程中,遇到了些问题,HTTPSConnectionPool(host=’pypi.python.org’,port=443):Readtimedout.解决办法是,在安装的过程中,延长超时的时间,

$pip3install-U--timeout1000Scrapy

ScrapyTutorial

创建tutorial项目

使用

1
2
3
$scrapystartprojecttutorial
NewScrapyproject'tutorial',usingtemplatedirectory'/usr/local/lib/python2.7/site-packages/scrapy/templates/project',createdin:
/Users/mac/workspace/scrapy/tutorial
可见默认使用的python2.7,但是如果需要创建一个支持python3.x版本的tutoiral项目呢?如下所示,使用python3-m

1
2
3
$python3-mscrapystartprojecttutorial
NewScrapyproject'tutorial',usingtemplatedirectory'/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/scrapy/templates/project',createdin:
/Users/mac/workspace/scrapy/tutorial

导入PyCharm

直接open项目工程/Users/mac/workspace/scrapy/tutorial;这里需要注意的是默认的PyCharm使用的解释器Interpretor是我本地的Python2.7;这里需要将解释器改为Python3.6;下面记录下修改的步骤,

点击左上角PyCharmCommunityEdition,进入Preferences

点击Project:tutorial,然后选择ProjectInterpreter,然后设置解释器的版本,如下




工程结构

通过命令构建出来的项目骨架如图所示





第一个Spider

我们来新建一个Spider类,名叫quotes_spider.py,并将其放置到tutorial/spiders目录中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
importscrapy
classQuotesSpider(scrapy.Spider):
name="quotes"
defstart_requests(self):
urls=[
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
forurlinurls:
yieldscrapy.Request(url=url,callback=self.parse)
defparse(self,response):
page=response.url.split("/")[-2]
filename='quotes-%s.html'%page
withopen(filename,'wb')asf:
f.write(response.body)
self.log('Savedfile%s'%filename)
可以看到,我们新建的QuotesSpider类是继承自scrapy.Spider类的;下面看看其属性和方法的意义,

name
是Spider的标识符,用于唯一标识该Spider;它必须在整个项目中是全局唯一的;

start_requests()
必须定义并返回一组可以被Spider爬取的Requests,Request对象由一个URL和一个回调函数构成;

parse()
就是Request对象中的回调方法,用来解析每一个Request之后的Response;所以,parse()方法就是用来解析返回的内容,通过解析得到的URL同样可以创建对应的Requests进而继续爬取;

再来看看具体的实现,

start_request(self)方法分别针对http://quotes.toscrape.com/page/1/和http://quotes.toscrape.com/page/2/创建了两个需要被爬取的Requests对象;并通过yield进行迭代返回;备注,yield是迭代生成器,是一个Generator;

parse(self,response)方法既是对Request的反馈的内容Response进行解析,这里的解析的逻辑很简单,就是分别创建两个本地文件,然后将response.body的内容放入这两个文件当中。

如何执行

执行的过程需要使用到命令行,注意,这里需要使用到scrapy命令来执行;

1
2
$cd/Users/mac/workspace/scrapy/tutorial
$python3-mscrapycrawlquotes
大致会输出如下内容

1
2
3
4
5
6
7
8
9
10
11
...
2016-12-1621:24:05[scrapy.core.engine]INFO:Spideropened
2016-12-1621:24:05[scrapy.extensions.logstats]INFO:Crawled0pages(at0pages/min),scraped0items(at0items/min)
2016-12-1621:24:05[scrapy.extensions.telnet]DEBUG:Telnetconsolelisteningon127.0.0.1:6023
2016-12-1621:24:05[scrapy.core.engine]DEBUG:Crawled(404)<GEThttp://quotes.toscrape.com/robots.txt>(referer:None)
2016-12-1621:24:05[scrapy.core.engine]DEBUG:Crawled(200)<GEThttp://quotes.toscrape.com/page/1/>(referer:None)
2016-12-1621:24:05[scrapy.core.engine]DEBUG:Crawled(200)<GEThttp://quotes.toscrape.com/page/2/>(referer:None)
2016-12-1621:24:05[quotes]DEBUG:Savedfilequotes-1.html
2016-12-1621:24:05[quotes]DEBUG:Savedfilequotes-2.html
2016-12-1621:24:05[scrapy.core.engine]INFO:Closingspider(finished)
...
可以看到,通过爬取,我们在本地生成了两个html文件quotes-1.html和quotes-2.html

如何提取

通过命令行的方式提取

Scrapy提供了命令行的方式可以对需要被爬取的内容进行高效的调试,通过使用Scrapyshell进入命令行,然后在命令行中可以快速的对要爬取的内容进行提取;

如何进入Scrapyshell环境

我们试着通过Scrapyshell来提取下“http://quotes.toscrape.com/page/1/“中的数据,通过执行如下命令,进入shell

1
$scrapyshell"http://quotes.toscrape.com/page/1/"
输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[...Scrapyloghere...]
2016-09-1912:09:27[scrapy.core.engine]DEBUG:Crawled(200)<GEThttp://quotes.toscrape.com/page/1/>(referer:None)
[s]AvailableScrapyobjects:
[s]scrapyscrapymodule(containsscrapy.Request,scrapy.Selector,etc)
[s]crawler<scrapy.crawler.Crawlerobjectat0x7fa91d888c90>
[s]item{}
[s]request<GEThttp://quotes.toscrape.com/page/1/>[s]response<200http://quotes.toscrape.com/page/1/>[s]settings<scrapy.settings.Settingsobjectat0x7fa91d888c10>
[s]spider<DefaultSpider'default'at0x7fa91c8af990>
[s]Usefulshortcuts:
[s]shelp()Shellhelp(printthishelp)
[s]fetch(req_or_url)Fetchrequest(orURL)andupdatelocalobjects
[s]view(response)Viewresponseinabrowser
>>>
这样,我们就进入了Scrapyshell的环境,上面显示了连接请求和返回的相关信息,response返回statuscode200表示成功返回;

通过CSS标准进行提取

这里主要是遵循CSS标准https://www.w3.org/TR/selectors/来对网页的元素进行提取,

通过使用css()选择我们要提取的元素;下面演示一下如何提取元素<title/>

1
2
>>>response.css('title')
[<Selectorxpath=u'descendant-or-self::title'data=u'<title>QuotestoScrape</title>'>]
可以看到,它通过返回一个类似SelectorList的对象成功的获取到了http://quotes.toscrape.com/page/1/页面中的<title/>的信息,该信息是封装在Selector对象中的data属性中的;

提取Selector元素的文本内容,一般有两种方式用来提取,

通过使用extract()或者extract_first()方法来提取元素的内容;下面演示如何提取#1返回的元素<title/>中的文本内容text;

1
2
>>>response.css('title::text').extract_first()
'QuotestoScrape'
extract_first()表示提取返回队列中的第一个Selector对象;同样也可以使用如下的方式,

1
2
>>>response.css('title::text')[0].extract()
'QuotestoScrape'
不过extract_first()方法可以在当页面没有找到的情况下,避免出现IndexError的错误;

通过re()方法来使用正则表达式的方式来进行提取元素的文本内容

1
2
3
4
5
6
>>>response.css('title::text').re(r'Quotes.*')
['QuotestoScrape']
>>>response.css('title::text').re(r'Q\w+')
['Quotes']
>>>response.css('title::text').re(r'(\w+)to(\w+)')
['Quotes','Scrape']
备注,最后一个正则表示式返回了两个匹配的Group;

使用XPath

除了使用CSS标准来提取元素意外,我们还可以使用XPath标准来提取元素,比如,

1
2
3
4
>>>response.xpath('//title')
[<Selectorxpath='//title'data='<title>QuotestoScrape</title>'>]
>>>response.xpath('//title/text()').extract_first()
'QuotestoScrape'
XPath比CSS的爬取方式更为强大,因为它不仅仅是根据HTML的结构元素去进行检索(Navigating),并且它可以顺带的对文本(text)进行检索;所以它可以支持CSS标准不能做到的场景,比如,检索一个包含文本内容”NextPage”的link元素;这就使得通过XPath去构建爬虫更为简单;

提取quotes和authors

下面我们将来演示如何提取http://quotes.toscrape.com首页中的内容,先来看看首页的结构





可以看到,里面每个段落包含了一个名人的一段语录,那么我们如何来提取所有的相关信息呢?

我们从提取第一个名人的信息入手,看看如何提取第一个名人的名言信息;可以看到,第一个名人的语句是爱因斯坦的,那么我们试着来提取名言、作者以及相关的tags;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<divclass="quote">
<spanclass="text">“Theworldaswehavecreateditisaprocessofour
thinking.Itcannotbechangedwithoutchangingourthinking.”</span>
<span>
by<smallclass="author">AlbertEinstein</small>
<ahref="/author/Albert-Einstein">(about)</a>
</span>
<divclass="tags">
Tags:
<aclass="tag"href="/tag/change/page/1/">change</a>
<aclass="tag"href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
<aclass="tag"href="/tag/thinking/page/1/">thinking</a>
<aclass="tag"href="/tag/world/page/1/">world</a>
</div>
</div>
下面我们就来试着一步一步的去提取相关的信息,

首先,进入ScrapyShell,

1
$scrapyshell'http://quotes.toscrape.com'
然后,获取<divclass="quote"/>元素列表

1
>>>response.css("div.quote")
这里会返回一系列的相关的Selectors,不过因为这里我们仅仅是对第一个名言进行解析,所以我们只取第一个元素,并将其保存在quote变量中

1
>>>quote=response.css("div.quote")[0]
然后,我们来分别提取title、author和tags

提取title

1
2
3
>>>title=quote.css("span.text::text").extract_first()
>>>title
'“Theworldaswehavecreateditisaprocessofourthinking.Itcannotbechangedwithoutchangingourthinking.”'
提取author

1
2
3
>>>author=quote.css("small.author::text").extract_first()
>>>author
'AlbertEinstein'
提取tags,这里需要注意的是,tags是一系列的文本,

1
2
3
>>>tags=quote.css("div.tagsa.tag::text").extract()
>>>tags
['change','deep-thoughts','thinking','world']
Ok,上述完成了针对其中一个名言信息的提取,那么,我们如何提取完所有名人的名言信息呢?

1
2
3
4
5
6
7
8
>>>forquoteinresponse.css("div.quote"):
...text=quote.css("span.text::text").extract_first()
...author=quote.css("small.author::text").extract_first()
...tags=quote.css("div.tagsa.tag::text").extract()
...print(dict(text=text,author=author,tags=tags))
{'tags':['change','deep-thoughts','thinking','world'],'author':'AlbertEinstein','text':'“Theworldaswehavecreateditisaprocessofourthinking.Itcannotbechangedwithoutchangingourthinking.”'}
{'tags':['abilities','choices'],'author':'J.K.Rowling','text':'“Itisourchoices,Harry,thatshowwhatwetrulyare,farmorethanourabilities.”'}
...afewmoreofthese,omittedforbrevity
写个循环,将所有的信息的信息放入Pythondictionary;

通过Python程序来进行提取

本小计继续沿用提取quotes和authors小节的例子,来看看如何通过python程序来做相同的爬取动作;

提取数据

修改该之前的quotes_spider.py内容,如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
importscrapy
classQuotesSpider(scrapy.Spider):
name="quotes"
start_urls=[
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
defparse(self,response):
forquoteinresponse.css('div.quote'):
yield{
'text':quote.css('span.text::text').extract_first(),
'author':quote.css('small.author::text').extract_first(),
'tags':quote.css('div.tagsa.tag::text').extract(),
}
执行上述的名为quotes的爬虫,

1
$scrapycrawlquotes
执行结果如下,

1
2
3
4
2016-09-1918:57:19[scrapy.core.scraper]DEBUG:Scrapedfrom<200http://quotes.toscrape.com/page/1/>{'tags':['life','love'],'author':'AndréGide','text':'“Itisbettertobehatedforwhatyouarethantobelovedforwhatyouarenot.”'}
2016-09-1918:57:19[scrapy.core.scraper]DEBUG:Scrapedfrom<200http://quotes.toscrape.com/page/1/>{'tags':['edison','failure','inspirational','paraphrased'],'author':'ThomasA.Edison','text':"“Ihavenotfailed.I'vejustfound10,000waysthatwon'twork.”"}
可以看到,我们通过python创建的爬虫quotes一条一条的返回了爬取的信息;

保存数据

最简单保存方式被爬取的数据是通过使用Feedexports,通过使用如下的命令,

使用JSON格式

1
$scrapycrawlquotes-oquotes.json
上述命令会生成一个文件quotes.json,该文件中包含了所有被爬取的数据;不过由于历史的原因,Scrapy是往一个文件中追加被爬取的信息,而不是覆盖更新,所以如果你执行上述命令两次,将会得到一个损坏了的json文件;

使用JSONLines格式

1
$scrapycrawlquotes-oquotes.jl
这样,保存的文件就是JSONLines的格式了,注意,这里的唯一变化是文件的后缀名改为了.jl;

补充,JSONLines是另一种JSON格式的定义,基本设计是每行是一个有效的JSONValue;比如它的格式比CSV格式更友好,

1
2
3
4
5
["Name","Session","Score","Completed"]
["Gilbert","2013",24,true]
["Alexa","2013",29,true]
["May","2012B",14,false]
["Deloise","2012A",19,true]
同时也可以支持内嵌数据,

1
2
3
4
{"name":"Gilbert","wins":[["straight","7♣"],["onepair","10♥"]]}
{"name":"Alexa","wins":[["twopair","4♠"],["twopair","9♠"]]}
{"name":"May","wins":[]}
{"name":"Deloise","wins":[["threeofakind","5♣"]]}
JSONLines格式非常适合处理含有大量数据的文件,通过迭代,每行处理一个数据对象;不过,要注意的是,使用JSONlines的方式,Scrapy同样的是以追加的方式添加内容,只是因为JSONLines逐行的方式添加被爬取的数据,所以以追加的方式并不会想使用JSON格式那样导致文件格式错误;

如果是一个小型的项目,使用JSONLines的方式就足够了;但是,如果你面临的是一个更复杂的项目,而且有更复杂的数据需要爬取,那么你就可以使用ItemPipeline;一个demoPipelines已经帮你创建好了,tutorial/pipelines.py;

提取下一页(提取链接信息)

如何提取章节详细的描述了如何爬取页面的信息,那么,如何爬取该网站的所有信息呢?那么就必须爬取相关的链接信息;那么我们依然以http://quotes.toscrape.com为例,来看看我们该如何爬取链接信息,

我们可以看到,下一页的链接HTML元素,

1
2
3
4
5
<ulclass="pager">
<liclass="next">
<ahref="/page/2/">Next<spanaria-hidden="true">→</span></a>
</li>
</ul>
我们可以通过shell来抓取它,

1
2
>>>response.css('li.nexta').extract_first()
'<ahref="/page/2/">Next<spanaria-hidden="true">→</span></a>'
这样,我们得到了这个anchor元素,但是我们想要得到的是其href属性;Scrapy支持CSS扩展的方式,因此我们可以直接爬取其属性值,

1
2
>>>response.css('li.nexta::attr(href)').extract_first()
'/page/2/'
好的,我们现在已经知道该如何获取下一页链接的相对地址了,那么我们如何修改我们的python程序使得我们可以自动的爬取所有页面的数据呢?

使用scrapy.Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
importscrapy
classQuotesSpider(scrapy.Spider):
name="quotes"
start_urls=[
'http://quotes.toscrape.com/page/1/',
]
defparse(self,response):
forquoteinresponse.css('div.quote'):
yield{
'text':quote.css('span.text::text').extract_first(),
'author':quote.css('small.author::text').extract_first(),
'tags':quote.css('div.tagsa.tag::text').extract(),
}
next_page=response.css('li.nexta::attr(href)').extract_first()
ifnext_pageisnotNone:
next_page=response.urljoin(next_page)
yieldscrapy.Request(next_page,callback=self.parse)
这里简单的描述下程序的执行逻辑,通过for循环处理完当前页面的爬取操作,然后执行获取下一页的相关操作,首先获得下一页的相对路径并保存到变量next_page中,然后通过response.urljon(next_page)方法得到绝对路径;最后,通过该绝对路径再生成一个scrapy.Request对象返回,并加入爬虫队列中,等待下一次的爬取;由此,你就可以动态的去爬取所有相关页面的信息了;

基于此,你就可以建立起非常复杂的爬虫了,同样,可以根据不同链接的类型,构建不同的Parser,那么就可以对不同类型的返回页面进行分别处理;

使用response.follow

不同于使用scrapyRequest,需要通过相对路径构造出绝对路径,response.follow可以直接使用相对路径,因此就不需要调用urljoin方法了;注意,response.follow直接返回一个Request实例,可以直接通过yield进行返回;所以,上述代码可以简化为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
importscrapy
classQuotesSpider(scrapy.Spider):
name="quotes"
start_urls=[
'http://quotes.toscrape.com/page/1/',
]
defparse(self,response):
forquoteinresponse.css('div.quote'):
yield{
'text':quote.css('span.text::text').extract_first(),
'author':quote.css('spansmall::text').extract_first(),
'tags':quote.css('div.tagsa.tag::text').extract(),
}
next_page=response.css('li.nexta::attr(href)').extract_first()
ifnext_pageisnotNone:
yieldresponse.follow(next_page,callback=self.parse)
另外,response.follow在处理<a>元素的时候,会直接使用它们的href属性;所以上述代码还可以简化为,

1
2
3
next_page=response.css('li.nexta').extract_first()
ifnext_pageisnotNone:
yieldresponse.follow(next_page,callback=self.parse)
因此匹配的时候不需要显示的声明<a>的属性值了;

定义更多的Parser

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
importscrapy
classAuthorSpider(scrapy.Spider):
name='author'
start_urls=['http://quotes.toscrape.com/']
defparse(self,response):
#followlinkstoauthorpages
forhrefinresponse.css('.author+a::attr(href)'):
yieldresponse.follow(href,self.parse_author)
#followpaginationlinks
forhrefinresponse.css('li.nexta::attr(href)'):
yieldresponse.follow(href,self.parse)
defparse_author(self,response):
defextract_with_css(query):
returnresponse.css(query).extract_first().strip()
yield{
'name':extract_with_css('h3.author-title::text'),
'birthdate':extract_with_css('.author-born-date::text'),
'bio':extract_with_css('.author-description::text'),
}
该例子创建了两个解析方法parse()和parse_author(),一个是用来控制整个爬取流程,一个是用来解析author信息的;首先,我们来分析一下执行的流程,

进入parse(),从当前的页面中爬取得到所有相关的authorhref属性值既是一个链接,然后针对该链接,通过response.follow创建一个新的Request继续进行爬取,通过回调parse_author()方法对爬取的内容进行进一步的解析,这里就是对爬取到的Author的信息进行提取;

当#1有关当前页面所有的Author信息都已经爬取成功以后,便开始对下一页进行爬取;

从这个例子中,我们需要注意的是,当爬取当前页面的时候,我们依然可以通过创建子的Requests对子链接进行爬取直到所有有关当前页面的信息都已经被爬取完毕以后,方可进入下一个页面继续进行爬取;

另外,需要注意的是,在爬取整个网站信息的时候,必然会有多个相同Author的名言,那么势必要爬取到许多的重复的Author的信息;这无疑是增加了爬取的压力同时也需要处理大量的冗余数据,基于此,Scrapy默认实现了对重复的已经爬取过的链接在下次爬取的时候自动过滤掉了;不过,你也可以通过DUPEFILTER_CLASS来进行设置是否启用该默认行为;

使用Spider参数

你可以通过commondline的方式为你的Spider提供参数,

1
$scrapycrawlquotes-oquotes-humor.json-atag=humor
该参数将会被传入Spider的__init__方法中,并默认成为当前Spiderquotes的属性;在quotesSpider的python应用程序中,可以通过使用self.tag来获取该参数信息;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
importscrapy
classQuotesSpider(scrapy.Spider):
name="quotes"
defstart_requests(self):
url='http://quotes.toscrape.com/'
tag=getattr(self,'tag',None)
iftagisnotNone:
url=url+'tag/'+tag
yieldscrapy.Request(url,self.parse)
defparse(self,response):
forquoteinresponse.css('div.quote'):
yield{
'text':quote.css('span.text::text').extract_first(),
'author':quote.css('small.author::text').extract_first(),
}
next_page=response.css('li.nexta::attr(href)').extract_first()
ifnext_pageisnotNone:
yieldresponse.follow(next_page,self.parse)
通过getattr(self,'tag',None)便可以获取从commonline中传入的tag参数,并构造出需要爬取的URL链接http://quotes.toscrape.com/tag/humor

Reference

Scrapy爬虫框架:http://python.jobbole.com/86405/
InstallationGuide:https://doc.scrapy.org/en/latest/intro/install.html
explainhowvirtualenvused:https://stackoverflow.com/questions/41151141/how-to-get-scrapy-to-use-python-3-when-both-python-versions-are-installed
tutorialguide:https://doc.scrapy.org/en/latest/intro/tutorial.html
ScrapyClusters:http://scrapy-cluster.readthedocs.io/en/latest/
ScrapyDeployment:https://scrapyd.readthedocs.io/en/latest/deploy.html
Scrapy0.24文档:http://scrapy-chs.readthedocs.io/zh_CN/0.24/index.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  爬虫 Scrapy