scrapy项目测试

项目地址:https://github.com/snjl/python.spider.scrapy_test.git

新建项目

在开始爬取之前,必须创建一个新的Scrapy项目。进入自定义的项目目录中,运行下列命令:

1
scrapy startproject tutorial

项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tutorial/
scrapy.cfg # 部署配置文件

tutorial/ # Python模块,代码写在这个目录下
__init__.py

items.py # 项目项定义文件

pipelines.py # 项目管道文件

settings.py # 项目设置文件

spiders/ # 我们的爬虫/蜘蛛 目录
__init__.py

创建第一个爬虫类:tutorial/spiders/QuotesSpider

1
scrapy genspider QuotesSpider quotes.toscrape.com

会生成代码

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


class QuotesspiderSpider(scrapy.Spider):
name = 'QuotesSpider'
allowed_domains = ['quotes.toscrape.com']
start_urls = ['http://quotes.toscrape.com/']

def parse(self, response):
pass

setting里面会生成:

1
2
3
4
BOT_NAME = 'tutorial'

SPIDER_MODULES = ['tutorial.spiders']
NEWSPIDER_MODULE = 'tutorial.spiders'

可以将QuotesSpider里面的name换成quotes,在文件夹里使用命令行输入

1
scrapy crawl quotes

运行爬虫,调试信息如下:

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
49
50
51
52
53
54
55
56
2019-01-27 21:50:34 [scrapy.utils.log] INFO: Scrapy 1.5.0 started (bot: tutorial)
2019-01-27 21:50:34 [scrapy.utils.log] INFO: Versions: lxml 4.2.1.0, libxml2 2.9.5, cssselect 1.0.3, parsel 1.4.0, w3lib 1.19.0, Twisted 17.9.0, Python
3.6.4 (v3.6.4:d48eceb, Dec 19 2017, 06:54:40) [MSC v.1900 64 bit (AMD64)], pyOpenSSL 17.5.0 (OpenSSL 1.1.0h 27 Mar 2018), cryptography 2.2.2, Platform
Windows-10-10.0.17134-SP0
2019-01-27 21:50:34 [scrapy.crawler] INFO: Overridden settings: {'BOT_NAME': 'tutorial', 'NEWSPIDER_MODULE': 'tutorial.spiders', 'ROBOTSTXT_OBEY': True,
'SPIDER_MODULES': ['tutorial.spiders']}
2019-01-27 21:50:34 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
'scrapy.extensions.telnet.TelnetConsole',
'scrapy.extensions.logstats.LogStats']
2019-01-27 21:50:34 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware',
'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware',
'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware',
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware',
'scrapy.downloadermiddlewares.retry.RetryMiddleware',
'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware',
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware',
'scrapy.downloadermiddlewares.redirect.RedirectMiddleware',
'scrapy.downloadermiddlewares.cookies.CookiesMiddleware',
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware',
'scrapy.downloadermiddlewares.stats.DownloaderStats']
2019-01-27 21:50:34 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
'scrapy.spidermiddlewares.offsite.OffsiteMiddleware',
'scrapy.spidermiddlewares.referer.RefererMiddleware',
'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware',
'scrapy.spidermiddlewares.depth.DepthMiddleware']
2019-01-27 21:50:34 [scrapy.middleware] INFO: Enabled item pipelines:
[]
2019-01-27 21:50:34 [scrapy.core.engine] INFO: Spider opened
2019-01-27 21:50:34 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2019-01-27 21:50:34 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2019-01-27 21:50:36 [scrapy.core.engine] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2019-01-27 21:50:36 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/> (referer: None)
2019-01-27 21:50:36 [scrapy.core.engine] INFO: Closing spider (finished)
2019-01-27 21:50:36 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 446,
'downloader/request_count': 2,
'downloader/request_method_count/GET': 2,
'downloader/response_bytes': 2701,
'downloader/response_count': 2,
'downloader/response_status_count/200': 1,
'downloader/response_status_count/404': 1,
'finish_reason': 'finished',
'finish_time': datetime.datetime(2019, 1, 27, 13, 50, 36, 441026),
'log_count/DEBUG': 3,
'log_count/INFO': 7,
'response_received_count': 2,
'scheduler/dequeued': 1,
'scheduler/dequeued/memory': 1,
'scheduler/enqueued': 1,
'scheduler/enqueued/memory': 1,
'start_time': datetime.datetime(2019, 1, 27, 13, 50, 34, 851055)}
2019-01-27 21:50:36 [scrapy.core.engine] INFO: Spider closed (finished)

可以在里面使用BeautifulSoup,引入后提取出text,author,tags:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

import scrapy
from bs4 import BeautifulSoup


class QuotesspiderSpider(scrapy.Spider):
name = 'quotes'
allowed_domains = ['quotes.toscrape.com']
start_urls = ['http://quotes.toscrape.com/']

def parse(self, response):
bs_obj = BeautifulSoup(response.text)
items = bs_obj.find_all('div', {'class': 'quote'}) # 获取列表
for item in items:
text = item.find('span', {'itemprop': 'text', 'class': 'text'}).text
author = item.find('small', {'class': 'author'}).text
tags = item.find_all('a', {'class': 'tag'})
tags = [tag.text for tag in tags]
print('text', text)
print("author", author)
print('tags', tags)

如果需要进行测试,可以在命令行输入:

1
scrapy shell quotes.toscrape.com

进入命令行交互模式后,引入bs4包可以进行测试。

修改items类

修改items.py代码:

1
2
3
4
5
6
class TutorialItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
text = scrapy.Field()
author = scrapy.Field()
tags = scrapy.Field()

使用TutorialItem存储需要的三个字段,用来接收爬虫数据。

爬虫QuotesSpider.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
import scrapy
from bs4 import BeautifulSoup

from tutorial.items import TutorialItem


class QuotesspiderSpider(scrapy.Spider):
name = 'quotes'
allowed_domains = ['quotes.toscrape.com']
start_urls = ['http://quotes.toscrape.com/']

def parse(self, response):
bs_obj = BeautifulSoup(response.text)
items = bs_obj.find_all('div', {'class': 'quote'}) # 获取列表
for item in items:
item_field = TutorialItem() # 每一个item信息存储到item_field里
text = item.find('span', {'itemprop': 'text', 'class': 'text'}).text
author = item.find('small', {'class': 'author'}).text
tags = item.find_all('a', {'class': 'tag'})
tags = [tag.text for tag in tags] # 获取tags列表里的每一个tag的文本
item_field['text'] = text # 存储数据到item_field
item_field['author'] = author
item_field['tags'] = tags
yield item_field # 使用生成器,每次调用都会从结束处开始,会生成新的item_field,爬取和计算后会返回数据

此处使用的是BeautifulSoup,也可以用css选择器或者xpath,可以参考github项目

此项目包含两个爬虫,您可以使用list 命令列出它们:

1
2
3
$ scrapy list
toscrape-css
toscrape-xpath

两个爬虫都从同一网站提取相同的数据,但toscrape-css 使用CSS选择器,而toscrape-xpath使用XPath表达式。

可以使用scrapy crawl命令运行爬虫,如:

1
$ scrapy crawl toscrape-css

如果要将已抓取的数据保存到文件,可以传递-o选项:

1
$ scrapy crawl toscrape-css -o quotes.json

每一条提取的数据看起来像这个示例:

1
2
3
4
5
{
'author': 'Douglas Adams',
'text': '“I may not have gone where I intended to go, but I think I ...”',
'tags': ['life', 'navigation']
}

运行爬虫:

1
scrapy crawl quotes

发现中间会输出内容,每一条结果类似如下:

1
2
3
4
5
2019-01-27 23:51:18 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/>
{'author': 'Eleanor Roosevelt',
'tags': ['misattributed-eleanor-roosevelt'],
'text': '“A woman is like a tea bag; you never know how strong it is until '
"it's in hot water.”"}

获取下一页进行爬取

使用

1
next_page = response.css('.paper .next a::attr(href)').extract_first()

获取下一页,回调该函数,就可以爬取所有页面,整个QuotesSpider.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
import scrapy
from bs4 import BeautifulSoup

from tutorial.items import TutorialItem


class QuotesspiderSpider(scrapy.Spider):
name = 'quotes'
allowed_domains = ['quotes.toscrape.com']
start_urls = ['http://quotes.toscrape.com/']

def parse(self, response):
bs_obj = BeautifulSoup(response.text)
items = bs_obj.find_all('div', {'class': 'quote'}) # 获取列表
for item in items:
item_field = TutorialItem() # 每一个item信息存储到item_field里
text = item.find('span', {'itemprop': 'text', 'class': 'text'}).text
author = item.find('small', {'class': 'author'}).text
tags = item.find_all('a', {'class': 'tag'})
tags = [tag.text for tag in tags] # 获取tags列表里的每一个tag的文本
item_field['text'] = text # 存储数据到item_field
item_field['author'] = author
item_field['tags'] = tags
yield item_field # 使用生成器,每次调用都会从结束处开始,会生成新的item_field,爬取和计算后会返回数据

# 获取下一页,由于使用BeautifulSoup比较麻烦,而且错误处理比较不方便,所以使用css选择器
next_page = response.css('.pager .next a::attr(href)').extract_first()
# 使用urljoin获取绝对地址
next_url = response.urljoin(next_page)
# 回调函数,继续调用该parse函数,传入next_url进行请求
yield scrapy.Request(url=next_url, callback=self.parse)

现在,在提取数据之后,该parse()方法寻找到下一页的链接,使用该urljoin()方法构建完整的绝对URL (因为链接可以是相对的)并且产生对下一页的新请求,将其注册为回调以处理针对下一页的数据提取,以及保持爬行通过所有页面。

问题:这样做可能会出现一些问题,例如没有下一页,会启用自动过滤,从而停止运行,但是可以通过判断是否有最后一页来增加程序的健壮性和合理性:

1
2
3
4
5
6
7
8
9
10
···
# 获取下一页,由于使用BeautifulSoup比较麻烦,而且错误处理比较不方便,所以使用css选择器
next_page = response.css('.pager .next a::attr(href)').extract_first()
# 如果next_page存在
if next_page:
# 使用urljoin获取绝对地址
next_url = response.urljoin(next_page)
# 回调函数,继续调用该parse函数,传入next_url进行请求
yield scrapy.Request(url=next_url, callback=self.parse)
···

这里看到的是Scrapy的向下链接的机制:当你在回调方法中产生一个请求时,Scrapy会调度要发送的请求,并注册一个回调方法,在上次请求完成时执行。

保存方法

正常能获取100条数据,可以使用下面命令存储到文件中:

1
2
3
4
scrapy crawl quotes -o quotes.json 生成json格式的数据
scrapy crawl quotes -o quotes.jl 生成json格式的单行数据,即json line
scrapy crawl quotes -o quotes.csv 生成csv形式文件
scrapy crawl quotes -o quotes.xml 生成xml格式的数据

也可以传输到ftp服务器。

注意:文件是a+写入的,并不会覆盖

log使用

内置了logger,可以使用

1
self.logger.info(MESSAGE)

输出MESSAGE信息。
使用

1
self.logger.error(MESSAGE)

可以输出错误、调试等信息。

1
2
3
4
self.logger.info('info on %s', response.url)
self.logger.warning('WARNING on %s', response.url)
self.logger.debug('info on %s', response.url)
self.logger.error('info on %s', response.url)

pipeline处理(例如存储数据库,或者item处理)

在pipelines.py中写一些工具,用于对数据的处理。

处理抓取数据长度

例如,如果字符长度大于50,则截取并且在后面加上···,需要在pipeline.py中写一个类,并且在settings.py中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# pipeline.py
from scrapy.exceptions import DropItem


# 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 TutorialPipeline(object):
def __init__(self):
self.limit = 50

def process_item(self, item, spider):
if item['text']:
if len(item['text']) > self.limit:
item['text'] = item['text'][0:self.limit].rstrip() + '...'
return item
else:
print('text is null')
return DropItem("Missing Text")

返回item或者返回报错的DropItem(也可以用raise DropItem)。

在settings.py中需要设置

1
2
3
4
# settings.py
ITEM_PIPELINES = {
'tutorial.pipelines.TutorialPipeline': 300,
}

300表示优先级,如果有多个管道,数字越小优先级越高。

将抓取数据存入MongoDB

在pipelines.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
# MongoDB存储数据管道
class MongoPipeline(object):
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db

# 从settings中拿到配置
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DB')
)

# 爬虫启动时需要进行的操作,初始化MongoDB对象
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]

# 最重要的process_item
def process_item(self, item, spider):
# 使用这样的name比较灵活
name = item.__class__.__name__
self.db[name].insert(dict(item))
return item

# 管道完毕时自动运行
def close_spider(self, spider):
self.client.close()

因为代码中获取了settings中的配置,所以settings.py中需要加入代码进行配置:

1
2
MONGO_URI = 'localhost'
MONGO_DB = 'quotes'

在settings.py中配置:

1
2
3
4
ITEM_PIPELINES = {
'tutorial.pipelines.TutorialPipeline': 300,
'tutorial.pipelines.MongoPipeline': 500,
}

这样可以先执行上一个管道,再执行第二个,在MongoDB中可以看到数据已经是进行了截断处理。

-------------本文结束 感谢您的阅读-------------