项目地址: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
14tutorial/
scrapy.cfg # 部署配置文件
tutorial/ # Python模块,代码写在这个目录下
__init__.py
items.py # 项目项定义文件
pipelines.py # 项目管道文件
settings.py # 项目设置文件
spiders/ # 我们的爬虫/蜘蛛 目录
__init__.py
创建第一个爬虫类:tutorial/spiders/QuotesSpider1
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
4BOT_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
562019-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
6class 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
24import 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
52019-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
31import 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
4scrapy 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
4self.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
2MONGO_URI = 'localhost'
MONGO_DB = 'quotes'
在settings.py中配置:1
2
3
4ITEM_PIPELINES = {
'tutorial.pipelines.TutorialPipeline': 300,
'tutorial.pipelines.MongoPipeline': 500,
}
这样可以先执行上一个管道,再执行第二个,在MongoDB中可以看到数据已经是进行了截断处理。