爬虫
1 Scrapy框架
1.1 介绍
Scrapy框架是基于异步爬虫的应用框架,用于高性能数据解析,高性能持久化存储,全站数据爬取,增量式爬虫和分布式爬虫等。
1.2 环境安装
Windows
1. pip install wheel
2. 下载twisted
http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
3. 进入twisted目录下,执行 pip3 install twisted文件名
例如 pip install Twisted-20.3.0-cp38-cp38-win_amd64.whl
4. pip install pywin32
5. pip install scrapy
Linux
pip install scrapy
说明
twisted插件是scrapy框架实现异步操作的第三方组件。
1.3 基本使用
操作 | 命令 |
---|---|
创建项目 | scrapy startproject xxx (爬虫项目名) |
创建爬虫文件 | scrapy genspider xxx (爬虫名) xxx.com (爬取域) |
生成文件 | scrapy crawl xxx -o xxx.json (生成某种类型的文件) |
运行爬虫 | scrapy crawl xxx (爬虫名) |
列出所有爬虫 | scrapy list |
获得配置信息 | scrapy settings [options] |
1.3.1 创建工程
scrapy startproject DemoScrapyProject
1.3.2 工程目录
DemoScrapyProject
│ scrapy.cfg
│
└─DemoScrapyProject
│ items.py
│ middlewares.py
│ pipelines.py
│ settings.py
│ __init__.py
│
├─spiders
│ │ __init__.py
│ │
│ └─__pycache__
└─__pycache__
在工程目录中的项目名文件夹下,有一个名为spiders的文件夹,相当于爬虫包。
spiders爬虫文件夹中需要至少创建一个爬虫文件。
1.3.3 创建爬虫文件
cd DemoScrapyProject
scrapy genspider test www.test.com
在项目目录下执行创建命令,会自动在spiders爬虫文件夹中创建名为test的爬虫文件。
import scrapy
class TestSpider(scrapy.Spider):
name = 'test'
allowed_domains = ['www.test.com']
start_urls = ['http://www.test.com/']
def parse(self, response):
pass
类属性 | 说明 |
---|---|
name | 爬虫文件名称,是这个爬虫文件的唯一标识,不能重复。 |
allowed_domains | 允许域名,只爬取该限定域名下的网页。 |
start_urls | 起始url列表,用于存储即将发起请求的目标url。 |
parse | 用于数据解析,参数response是自动传入的响应对象。 |
import scrapy
class TestSpider(scrapy.Spider):
name = 'test'
allowed_domains = ['www.test.com']
start_urls = ['https://www.sougou.com/', 'https://www.baidu.com/']
def parse(self, response):
pass
1.3.4 修改配置文件
settings.py
- 修改日志输出级别
LOG_LEVEL = 'ERROR'
- 不需要服从robots协议
ROBOTSTXT_OBEY = False
- UA伪装
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'
1.3.5 执行工程
scrapy crawl test
1.3.6 数据解析
目标:爬取糗事百科中的段子。
方式:使用xpath定位标签,再提取数据。
提取数据的两种方式:
extract() 用于取Selector对象中的data;
extract_first() 返回字符串,取列表中的第一个Selector对象中的data。
selector_obj.extract() 用于取出selector_obj中的data,返回字符串;
[selector_obj1, selector_obj2...].extract() 用于取出列表中每个selector_obj中的data,返回列表。
[selector_obj1, selector_obj2...].extract_first() 用于取出列表中第一个元素selector_obj1中的data,返回字符串。
def parse(self, response):
div_list = response.xpath('//*[@id="content"]/div/div[2]/div')
for each_div in div_list:
# 返回的是Selector对象
# 提取方式1:使用extract()
author_selector_list1 = each_div.xpath('./div[1]/a[2]/h2/text()') # [<Selector xpath='./div[1]/a[2]/h2/text()' data='\n吃了两碗又盛\n'>]
author_list1 = author_selector_list1.extract() # ['\n吃了两碗又盛\n']
author_str1 = author_list1[0] # '吃了两碗又盛'
author_selector_obj2 = each_div.xpath('./div[1]/a[2]/h2/text()')[0] # <Selector xpath='./div[1]/a[2]/h2/text()' data='\n吃了两碗又盛\n'>
author_str2 = author_selector_obj2.extract() # '吃了两碗又盛'
# 提取方式2:使用extract_first()
author_str2 = each_div.xpath('./div[1]/a[2]/h2/text()').extract_first()
print(author_str2)
import scrapy
class TestSpider(scrapy.Spider):
name = 'test'
# allowed_domains = ['www.test.com']
start_urls = ['https://www.qiushibaike.com/text/']
def parse(self, response):
div_list = response.xpath('//*[@id="content"]/div/div[2]/div')
for each_div in div_list:
author_str = each_div.xpath('./div[1]/a[2]/h2/text()').extract_first()
content_list = each_div.xpath('./a/div/span//text()').extract()
content_str = ''.join(content_list)
1.3.7 持久化存储
有两种方式:
- 基于终端指令的持久化存储;
- 基于管道的持久化存储。
1.3.7.1 基于终端指令的持久化存储
局限性:
- 只能对parse方法的返回值进行持久化存储;
- 只能存储到指定类型的文件中,包括:json,jsonlines,jl,csv,xml,marshal,pickle;
- 不支持将数据写入到数据库中。
import scrapy
class TestSpider(scrapy.Spider):
name = 'test'
# allowed_domains = ['www.test.com']
start_urls = ['https://www.qiushibaike.com/text/']
def parse(self, response):
result_list = []
div_list = response.xpath('//*[@id="content"]/div/div[2]/div')
for each_div in div_list:
author_str = each_div.xpath('./div[1]/a[2]/h2/text()').extract_first()
content_list = each_div.xpath('./a/div/span//text()').extract()
content_str = ''.join(content_list)
temp_dict = {
'author': author_str,
'content': content_str
}
result_list.append(temp_dict)
return result_list
终端指令
scrapy crawl test -o result.csv
1.3.7.2 基于管道的持久化存储
步骤:
- 进行数据解析;
- 在items.py中定义相关的属性,且属性个数需要与解析出的字段个数一致;
- 将解析出来的数据存储到Item类型的对象中;
- 将Item对象提交给管道;
- 在管道中接收Item对象,将该Item对象中存储的数据做任意形式的持久化存储;
- 在配置文件中开启管道机制。
步骤1 进行数据解析。
test.py
import scrapy
class TestSpider(scrapy.Spider):
name = 'test'
# allowed_domains = ['www.test.com']
start_urls = ['https://www.qiushibaike.com/text/']
def parse(self, response):
div_list = response.xpath('//*[@id="content"]/div/div[2]/div')
for each_div in div_list:
author_str = each_div.xpath('./div[1]/a[2]/h2/text()').extract_first()
content_list = each_div.xpath('./a/div/span//text()').extract()
content_str = ''.join(content_list)
步骤2 在items.py中定义相关的属性。
items.py
import scrapy
class DemoscrapyprojectItem(scrapy.Item):
author = scrapy.Field()
content = scrapy.Field()
步骤3 将解析出来的数据存储到Item类型的对象中。
test.py
import scrapy
from DemoScrapyProject.items import DemoscrapyprojectItem
class TestSpider(scrapy.Spider):
name = 'test'
# allowed_domains = ['www.test.com']
start_urls = ['https://www.qiushibaike.com/text/']
def parse(self, response):
div_list = response.xpath('//*[@id="content"]/div/div[2]/div')
for each_div in div_list:
author_str = each_div.xpath('./div[1]/a[2]/h2/text()').extract_first()
content_list = each_div.xpath('./a/div/span//text()').extract()
content_str = ''.join(content_list)
# 实例化Item对象
item_obj = DemoscrapyprojectItem()
item_obj['author'] = author_str
item_obj['content'] = content_str
步骤4 将Item对象提交给管道。
test.py
import scrapy
from DemoScrapyProject.items import DemoscrapyprojectItem
class TestSpider(scrapy.Spider):
name = 'test'
# allowed_domains = ['www.test.com']
start_urls = ['https://www.qiushibaike.com/text/']
def parse(self, response):
div_list = response.xpath('//*[@id="content"]/div/div[2]/div')
for each_div in div_list:
author_str = each_div.xpath('./div[1]/a[2]/h2/text()').extract_first()
content_list = each_div.xpath('./a/div/span//text()').extract()
content_str = ''.join(content_list)
# 实例化Item对象
item_obj = DemoscrapyprojectItem()
item_obj['author'] = author_str
item_obj['content'] = content_str
# 将Item对象提交给管道
yield item_obj
步骤5 在管道中接收Item对象,持久化存储数据。
pipelines.py
class DemoscrapyprojectPipeline:
fp = None
# 重写父类方法,用于打开文件。
# 在整个工程的执行过程中该方法只会在爬虫开始时被执行一次。
def open_spider(self, spider):
self.fp = open('./result.txt', 'w', encoding='utf-8')
def process_item(self, item, spider):
'''
用于接收Item对象,该方法调用的次数取决于爬虫文件向管道提交Item对象的次数。
:param item: 接收到的Item对象
:param spider: 爬虫类实例化的对象
:return:
'''
author = item['author']
content = item['content']
self.fp.write(author)
self.fp.write(content)
return item
# 重写父类方法,用于关闭文件。
# 在整个工程的执行过程中该方法只会在爬虫结束时被执行一次。
def close_spider(self, spider):
self.fp.close()
方法中的参数spider指test.py中爬虫类实例化的对象,用于在管道文件中访问爬虫类的数据。
步骤6 在配置文件中开启管道机制。
settings.py
ITEM_PIPELINES = {
'DemoScrapyProject.pipelines.DemoscrapyprojectPipeline': 300,
}
1.3.7.3 管道细节分析
- 优先级
数字300表示管道的优先级,数值越小,则管道的优先级越高。
管道的优先级越高,则表示该管道会被优先执行。
settings.py
ITEM_PIPELINES = {
'DemoScrapyProject.pipelines.DemoscrapyprojectPipeline': 300,
}
-
多个管道类
多个管道类一般用于数据备份,每一个管道类表示将数据存储到一种形式的载体中。
如果需要将数据同时存储到MySQL和Redis中,则需要两个管道类来实现。 -
return item
爬虫文件只会将Item对象提交给优先级最高的管道。
管道类的方法process_item中的return item
可以将item对象交给下一个即将被执行的管道类。
例如封装一个管道类,将数据存储到MySQL数据库中。
pipelines.py
import pymysql
class MySQLPipeline:
conn = None # 数据库连接对象
cursor = None # 游标对象
def open_spider(self, spider):
self.conn = pymysql.Connect(
host='127.0.0.1',
port=3306,
user='root',
password='123456',
db='spider_db',
charset='utf8'
)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
author = item['author']
content = item['content']
sql = 'insert into qiushi values ("%s", "%s")' % (author, content)
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
print(e)
self.conn.rollback()
return item # 用于向下一个管道类传递item对象。
def close_spider(self, spider):
self.cursor.close()
self.conn.close()
在配置文件中注册这个管道类
settings.py
ITEM_PIPELINES = {
'DemoScrapyProject.pipelines.DemoscrapyprojectPipeline': 300,
'DemoScrapyProject.pipelines.MySQLPipeline': 301,
}
2 Redis数据库简介
2.1 介绍
Redis是一个非关系型数据库。
启动服务端 redis-server
启动客户端 redis-cli
2.2 基本存储单元
- set集合
- list列表
2.2.1 set集合
- 插入数据
语法:sadd 集合名 值
注意:集合不能存储重复数据。
将字符串Ben和Elliot存储到集合name_set中。
127.0.0.1:6379> sadd name_set Ben
(integer) 1
127.0.0.1:6379> sadd name_set Elliot
(integer) 1
127.0.0.1:6379> sadd name_set Ben
(integer) 0
- 查看数据
语法:smembers 集合名
查看集合name_set中的数据。
127.0.0.1:6379> smembers name_set
1) "Elliot"
2) "Ben"
2.2.2 list列表
- 插入数据
语法:lpush 列表名 值
列表允许存储重复数据。
将字符串Ben和Elliot存储到列表name_list中。
127.0.0.1:6379> lpush name_list Ben
(integer) 1
127.0.0.1:6379> lpush name_list Elliot
(integer) 2
127.0.0.1:6379> lpush name_list Ben
(integer) 3
- 查看列表长度
语法:llen 列表名
查看列表name_list的长度。
127.0.0.1:6379> llen name_list
(integer) 3
- 查看数据
语法:lrange 列表名 起始下标 终止下标
127.0.0.1:6379> lrange name_list 0 1
1) "Ben"
2) "Elliot"
127.0.0.1:6379> lrange name_list 0 -1
1) "Ben"
2) "Elliot"
3) "Ben"
127.0.0.1:6379> lrange name_list 1 1
1) "Elliot"
2.2.3 通用命令
- 查看所有数据
127.0.0.1:6379> keys *
1) "name_list"
2) "name_set"
- 删除所有数据
127.0.0.1:6379> flushall
OK
2.3 Scrapy与Redis配合
安装redis模块,需要指定版本为2.10.6。
其它版本的redis模块无法直接向Redis数据库的列表中存入字典类型数据。
pip install redis-2.10.6
2.3.1 封装Redis的管道类
pipelines.py
import redis
class RedisPipeline:
conn = None
def open_spider(self, spider):
self.conn = redis.Redis(
host='127.0.0.1',
port=6379
)
def process_item(self, item, spider):
self.conn.lpush('qiushiData', item)
return item
2.3.2 在配置文件中注册管道类
settings.py
ITEM_PIPELINES = {
'DemoScrapyProject.pipelines.DemoscrapyprojectPipeline': 300,
'DemoScrapyProject.pipelines.MySQLPipeline': 301,
'DemoScrapyProject.pipelines.RedisPipeline': 302,
}
3 全站数据爬取
目标:对所有页码的数据进行爬取并存储。
3.1 爬取第一页数据
>>> scrapy startproject duanziProject
>>> cd duanziProject
>>> scrapy genspider duanzi www.duanziwang.com/
settings.py
BOT_NAME = 'duanziProject'
SPIDER_MODULES = ['duanziProject.spiders']
NEWSPIDER_MODULE = 'duanziProject.spiders'
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
ITEM_PIPELINES = {
'duanziProject.pipelines.DuanziprojectPipeline': 300,
}
items.py
import scrapy
class DuanziprojectItem(scrapy.Item):
title = scrapy.Field()
content = scrapy.Field()
duanzi.py
import scrapy
from duanziProject.items import DuanziprojectItem
class DuanziSpider(scrapy.Spider):
name = 'duanzi'
# allowed_domains = ['www.duanziwang.com/']
start_urls = ['https://duanziwang.com/category/一句话段子/1/']
def parse(self, response):
article_list = response.xpath('/html/body/section/div/div/main/article')
for each_article in article_list:
title_str = each_article.xpath('./div[1]/h1/a/text()').extract_first()
content_str = each_article.xpath('./div[2]/p/text()').extract_first()
item = DuanziprojectItem()
item['title'] = title_str
item['content'] = content_str
yield item
piplines.py
class DuanziprojectPipeline:
fp = None
def open_spider(self, spider):
self.fp = open('./duanzi.txt', 'w', encoding='utf-8')
def process_item(self, item, spider):
self.fp.write(item['title'])
self.fp.write(item['content'])
return item
def close_spider(self, spider):
self.fp.close()
3.2 手动请求发送
不使用Scrapy内置方式,通过代码的形式进行请求发送。
3.2.1 手动请求get发送
yield scrapy.Request(url, callback)
向指定url发送get请求,通过回调函数callback对响应数据进行解析。
duanzi.py
import scrapy
from duanziProject.items import DuanziprojectItem
class DuanziSpider(scrapy.Spider):
name = 'duanzi'
# allowed_domains = ['www.duanziwang.com/']
start_urls = ['https://duanziwang.com/category/一句话段子/1/']
url_model = 'https://duanziwang.com/category/一句话段子/%d/'
page_num = 2
def parse(self, response):
article_list = response.xpath('/html/body/section/div/div/main/article')
for each_article in article_list:
title_str = each_article.xpath('./div[1]/h1/a/text()').extract_first()
content_str = each_article.xpath('./div[2]/p/text()').extract_first()
item = DuanziprojectItem()
item['title'] = title_str
item['content'] = content_str
yield item
# 结束递归条件
if self.page_num < 10:
new_url = format(self.url_model % self.page_num)
self.page_num += 1
# 手动发送请求,并调用回调函数对请求到的数据进行解析。
# 回调函数是parse,发送递归调用。
yield scrapy.Request(url=new_url, callback=self.parse)
3.2.2 手动请求post发送
yield scrapy.FormRequest(url, formdata, callback)
参数formdata指post请求携带的请求参数,格式是字典。
3.2.3 父类方法start_requests
问题:如何向start_url列表中的每一个url发送post请求?
关键:重写父类方法start_requests
父类方法start_requests对start_url列表中的每一个url默认发送get请求。
duanzi.py
import scrapy
from duanziProject.items import DuanziprojectItem
class DuanziSpider(scrapy.Spider):
name = 'duanzi'
start_urls = ['https://duanziwang.com/category/一句话段子/1/']
...
# 父类方法的原始实现,请求方式是get。
def start_requests(self):
for url in start_urls:
yield scrapy.Request(url=url, callback=self.parse)
def parse(self, response):
...
重写父类方法start_requests
duanzi.py
import scrapy
from duanziProject.items import DuanziprojectItem
class DuanziSpider(scrapy.Spider):
name = 'duanzi'
start_urls = ['https://duanziwang.com/category/一句话段子/1/']
...
# 重写父类方法,请求方式改为post。
def start_requests(self):
for url in start_urls:
yield scrapy.FormRequest(url=url, callback=self.parse)
def parse(self, response):
...