文章目录
  1. 1. Scrapy是什么?
  2. 2. 平台
  3. 3. Scrapy安装
    1. 3.1. Python 3.5下Scrapy安装
    2. 3.2. Python 2.7下Scrapy的安装
  4. 4. 开始项目
    1. 4.1. 创建Scrapy项目
    2. 4.2. 定义items.py
    3. 4.3. 编写主爬程序
    4. 4.4. 使用Item
    5. 4.5. 获取打赏描述
    6. 4.6. 爬取多页
    7. 4.7. 使用Pipeline
    8. 4.8. 合并打赏描述,根据打赏数排序
  5. 5. 结语
  6. 6. 参考资料

Scrapy是什么?

Scrapy是一款网络爬虫框架,官方文档的描述如下:

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。

其最初是为了 页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。

以前写小型爬虫的话还可以自己写,用urllib,BeautifulSoup,Requests什么的就能解决了,后来我发现遇到一个新问题又得重新来一遍这些代码,又得去看前面是怎么写的,而且自己写容易怎么高兴怎么来,代码写的太乱,不好维护,过段时间再来看又要花时间才能看懂。

用框架的好处就是代码结构清晰,代码重用,不用对新的问题又重新来一遍代码,而且功能更强大,能快速解决自己手写代码所不能短时间解决的问题。

平台

  • Windows 8.1
  • Python 2.7.10
  • 简书

Scrapy安装

Scrapy完美支持Python 2.x,虽然现在已经慢慢在支持Python 3.x了,但是可能还会遇到不少情况。我刚开始学习Scrapy想用Python 3.5的,都安装好了,但是运行的时候还是有引包错误:

1
ImportError: cannot import module '_win32stdio'

搜了一些,也没有解决,而且后面可能还会有很多问题,就暂时等一等它们的更新吧,先用回2.7,解决问题再说。
(By the way,看到了下面这个)

在Windows,Python 3.x下不能简单的pip install scrapy来一条龙安装scrapy,因为中间会出一些错误。
我参考了 【1】 以及 【2】,采用安装wheel文件的方式极其有效。

Python 3.5下Scrapy安装

1.安装Python,这个不说了
2.去http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml下载合适你的Python版本的lxml的wheel文件,我下载的是lxml-3.4.4-cp35-none-win32.whl,下载3.6.0版本好像不得行,在我的平台上报错:
lxml-3.6.0-cp35-cp35m-win32.whl is not a supported wheel on this platform,不支持我的平台。

下载完后,将whl文件拷贝到Python安装目录下,然后cmd进入到你的Python安装目录,运行

1
pip3 install lxml-3.4.4-cp35-none-win32.whl

然后运行:

1
pip3 install scrapy

在cmd中输入scrapy,如果输出版本信息并没有报错,那么恭喜你,搞定了,是不是很爽!

Python 2.7下Scrapy的安装

Python2.7下直接pip install scrapy,如果报错,看报错的内容是什么,找出问题出在哪个依赖包上,在网上搜索该包的whl文件(符合版本),直接pip install whl文件 来安装就好了。我是问题处在twisted包上,所以去网上下载了老版本的twisted安装的。

开始项目

我们将所学的马上利用到实际问题中来。[5]

爬取简书首页文章的打赏描述和打赏数,以企获得打赏描述对打赏数的影响

其实打赏数这个东西和文章的质量是最相关的,但是通过大量数据的挖掘统计,是否能将这种相关性弱化一下,从而显露出打赏描述和打赏数的关系呢?这就有趣了,值得研究。而且还可以同时学习框架和做有趣的事,岂不是人生一大乐趣。

创建Scrapy项目

通过如下语句创建Scrapy项目:

1
scrapy startproject jianshu2

然后会生成一个目录jianshu,目录结构如下:

1
2
3
4
5
6
7
8
9
jianshu/
scrapy.cfg
jianshu/
__init__.py
items.py
pipelines.py
settings.py
spiders/
__init__.py

  • spiders目录存放主爬取代码,是整个项目的核心。需要在spider下自己新建自己的爬取程序。
  • scrapy.cfg是项目的配置文件。
  • settings.py是项目的设置文件。
  • items.py定义我们要爬取的字段信息。
  • pipelines.py是项目的管道文件。

定义items.py

首先定义我们需要爬取的字段:

1
2
3
4
5
6
7
8
# -*- coding: utf-8 -*-
import scrapy

class Jianshu2Item(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
url = scrapy.Field()
likeNum = scrapy.Field()

编写主爬程序

1
2
3
4
5
6
7
8
9
10
11
12
13
import scrapy

class postSpider(scrapy.spiders.Spider):

name = 'post'
start_urls = ['http://www.jianshu.com']

def parse(self, response):
articles = response.xpath('//ul[@class="article-list thumbnails"]/li')
for article in articles:
url = article.xpath('div/h4/a/@href').extract()
likeNum = article.xpath('div/div/span[2]/text()').extract()
print(url,likeNum)

然后试着运行:

1
scrapy crawl post

来运行我们的爬虫,中间又报了一次”No module named win32api”错误,直接pip install pypiwin32即可。
然后可以看到正确运行了,爬取了20篇文章后,爬虫自动停止,cmd中打印正常。
中间用到了XPath来解析HTML,找到元素具体的位置,我们找到首页的HTML的第一篇文章:
First Post

1
articles = response.xpath('//ul[@class="article-list thumbnails"]/li')

这句找到所有文章的HTML段,response是我们爬取时服务器返回的HTML。
我们看到所有文章都包含在<ul class="article-list thumbnails">中,并且以<li class=have-img>开头,所以就不难理解XPath中为什么这么写了。
有BeautifulSoup基础的同学应该很好理解XPath了。

使用Item

我们爬取数据肯定不是为了打印出来看一下就算了,而是想要保存数据,一般来说,Spider爬取到数据之后通过items返回,还记得我们之前定义的items么,这时候就可以派上用场了。
写出完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import scrapy
from jianshu2.items import Jianshu2Item

class postSpider(scrapy.spiders.Spider):

name = 'post'
start_urls = ['http://www.jianshu.com']

def parse(self, response):
articles = response.xpath('//ul[@class="article-list thumbnails"]/li')

for article in articles:
url = article.xpath('div/h4/a/@href').extract()
likeNum = article.xpath('div/div/span[2]/text()').extract()
item = Jianshu2Item()
item['url'] = 'http://www.jianshu.com/'+url[0]
if likeNum == []:
#print(url,likeNum)
item['likeNum'] = 0
else:
#print(url,int(likeNum[0].split(' ')[-1]))
item['likeNum'] = int(likeNum[0].split(' ')[-1])
yield item

执行scrapy crawl post -o items.json就把数据保存到json中了。
yield 语句提交item。
注意打赏有可能没有,所以span也没有,这里判断一下。

数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[
[
{"url": "http://www.jianshu.com//p/6d7bf7d611aa", "likeNum": 1},
{"url": "http://www.jianshu.com//p/e47d86ce78d4", "likeNum": 0},
{"url": "http://www.jianshu.com//p/e69606806d6c", "likeNum": 0},
{"url": "http://www.jianshu.com//p/d7159874c59c", "likeNum": 2},
{"url": "http://www.jianshu.com//p/d38e8074ae94", "likeNum": 0},
{"url": "http://www.jianshu.com//p/6c8a0d0447cd", "likeNum": 0},
{"url": "http://www.jianshu.com//p/beff4ff80b25", "likeNum": 0},
{"url": "http://www.jianshu.com//p/d7e626cf02d7", "likeNum": 0},
{"url": "http://www.jianshu.com//p/524b13db9ce3", "likeNum": 1},
{"url": "http://www.jianshu.com//p/39449bcf9c28", "likeNum": 0},
{"url": "http://www.jianshu.com//p/970412b3c34d", "likeNum": 0},
{"url": "http://www.jianshu.com//p/2f98170f6eda", "likeNum": 1},
{"url": "http://www.jianshu.com//p/e91ab8e7a517", "likeNum": 0},
{"url": "http://www.jianshu.com//p/59a6caf3d965", "likeNum": 1},
{"url": "http://www.jianshu.com//p/ee5432e57dd3", "likeNum": 0},
{"url": "http://www.jianshu.com//p/00b7662bd335", "likeNum": 0},
{"url": "http://www.jianshu.com//p/1815b4071362", "likeNum": 1},
{"url": "http://www.jianshu.com//p/b00f7a2f0295", "likeNum": 0},
{"url": "http://www.jianshu.com//p/7f5fc5a01b75", "likeNum": 0},
{"url": "http://www.jianshu.com//p/84c10f2cf100", "likeNum": 0}
]

我们想将数据直接存在CSV这样的文件中怎么办呢?方法就是使用Feed exports,在settings.py文件中添加:

1
2
FEED_URI=u'D:\Python27\jianshu2\jianshu2\spiders\data.csv'
FEED_FORMAT='CSV'

第一次运行scrapy crawl post -o data.csv,然后后面不用加-o data.csv,即可输出到data.csv中。

获取打赏描述

我们已经获得了url和打赏数,这已经是一个巨大的进步了。
然而我们还需要根据这个url再进一步爬到文章里面去,并且我们希望在一个爬虫里面就解决了,不想搞很多爬虫。
这时候问题转化为: 如何爬取需要的属性在不同页面的items?
这时候我们加一个属性’quote’,这个属性在打开url的页面中。
这时候,看到这里,仿照它的写法,通过meta传递item参数,即相当于

主函数先确定一些参数(‘url’,’likeNum’),剩下的交给另一个函数去做,然后另一个函数算出’quote’参数后把item还给主函数,主函数整合一下item,然后yield生成就好了。

部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
request = Request(posturl,callback=self.parse_donate)
request.meta['item'] = item
yield request

...

def parse_donate(self, response):
donate = response.xpath('//div[@class="support-author"]/p/text()').extract()
item = response.meta['item']
if len(str(donate)) == 0:
item['quote'] = ""
else:
item['quote'] = str(donate[0].encode('utf-8'))

return item

爬取多页

这时候我们发现爬的太少了,只有20篇。又看到首页下面有一个【点击查看更多】按钮,我们试着在代码中‘按下’这个按钮,然后获取下面内容的url,递归调用parse即可添加更多的文章。

1
2
3
4
next_link = selector.xpath('//*[@id="list-container"]/div[@class="load-more"]/button/@data-url').extract()[0]
if next_link:
next_link = self.url + str(next_link)
yield Request(next_link,callback=self.parse)

使用Pipeline

有了item之后,item会被传递给Item Pipeline,我们可以在pipelines.py中对item做一些操作,比如写到json文件中。

Item Pipeline的典型应用如下,更多见中文文档。

  • 清洗HTML数据
  • 验证item中的数据
  • 查重或者丢弃
  • 保存结果到文件(json,数据库,csv等)

于是我们编写pipelines.py如下,将item数据写入到json文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
import json
import codecs

class Jianshu2Pipeline(object):

def __init__(self):
self.file = codecs.open('items.json','wb','utf-8')

def process_item(self, item, spider):

line = json.dumps(dict(item)) + "\n"
self.file.write(line.decode("unicode_escape"))
return item

不得不用codecs来解决编码问题。Python在Windows下的编码真让人头疼。
这时候我们写道json中,其实url都可以去掉了,我们并不关心。
效果如下:
Result

合并打赏描述,根据打赏数排序

修改pipelines.py文件,用一个全局的字典dict记录每种语句的打赏数之和,然后根据打赏数排序,写到新的csv文件中。

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
# -*- coding: utf-8 -*-
import json
import codecs
from operator import itemgetter

class Jianshu2Pipeline(object):

def __init__(self):
self.file = codecs.open('items.json','wb','utf-8')
self.quote = {}
self.filecsv = codecs.open('items.csv','w','utf-8')

def process_item(self, item, spider):

line = json.dumps(dict(item)) + "\n"
self.file.write(line.decode("unicode_escape"))

if item['quote'] in self.quote.keys():
self.quote[item['quote']] += item['likeNum']
else:
self.quote[item['quote']] = item['likeNum']

self.filecsv.seek(0)
lis = sorted(self.quote.items(),key=itemgetter(1),reverse=True)
for i in range(len(lis)):
line2 = lis[i][0] + '\t' + str(lis[i][1]) + '\r\n'
self.filecsv.write(line2.decode("utf-8"))

return item

排序后结果

结语

由结果可以看出,第一条打赏数最多,不难理解,因为这句是默认的打赏描述,所以使用的基数很大,所以不能说明什么。由于数据量太少,只能爬6页,所以还不是很能说明问题。但是学习scrapy,了解scrapy的目的已经初步达到了,虽然还只是初步学习。但是找出统计上相对能够吸引人打赏的描述的目的还没有达到,需要加大数据量。

由结果还可以看出,其实打赏描述的个性化挺强的,很多都是个人信息。所以呢,还是要大数据。

查看源码点击进入我的Github: 本文源码

参考资料

[1] Windows上Python3.5安装Scrapy(lxml)
[2] How to install LXML for Python 3 on 64-bit Windows
[3] Python爬虫框架Scrapy快速入门
[4] Scrapy中文文档
[5] 本文应用目标

数据科学 | Data Science