scrapy案例实践
入门教程
- https://www.runoob.com/w3cnote/scrapy-detail.html
案例实践
- 爬取csdn某个主题的热门博客
新建项目
创建项目
- 创建一个工程blog,这里使用pycharm;
- 在工程目录下创建一个scrapy项目,执行命令"scrapy startproject 项目名称";
scrapy startproject 爬虫工程项目名
scrapy startproject blog
- 创建成功后,在工程目录下会生成一个blog文件夹
项目目录
- scrapy.cfg:项目的配置文件,开发无需用到。
- blog:项目中会有两个同名的文件夹。最外层表示 project,里面那个目录代表 module(项目的核心)。
- blog/items.py:以字段形式定义后期需要处理的数据。
- blog/middlewards.py:中间件处理文件。
- blog/pipelines.py:提取出来的 Item 对象返回的数据并进行存储。
- blog/settings.py:项目的设置文件。可以对爬虫进行自定义设置,比如选择深度优先爬取还是广度优先爬取,设置对每个IP的爬虫数,设置每个域名的爬虫数,设置爬虫延时,设置代理等等。
- blog/spider: 这个目录存放爬虫程序代码。
- init.py:python 包要求,对 scrapy 作用不大
settings常用配置
- USER_AGENT = ‘’ # User-Agent
- POBOTSTXT_OBEY = True|False # 是否遵守机器人协议,有的网站不允许爬虫爬取,如果遵守机器人协议则无法去爬取
- DEFAULT_REQUEST_HEADERS = {} # 默认请求头
- CONCURRENT_REQUESTS = 16 # 下载器最大处理的请求数
- DOWNLOAD_DELAY = 3 # 下载延时,秒,当访问到服务器之后,可以添加个延时再去服务器获取数据,模拟用户行为
- SPIDER_MIDDLEWARES # Spider中间件
- DOWNLOADER_MIDDLEWARES # Downloader中间件
- ITEM_PIPELINES # 管道文件
制作爬虫
- 在爬虫代码目录下创建一个爬虫,可以手动创建,也可以命令行创建
scrapy genspider 爬虫名称 "host地址"
scrapy genspider myblog "blog.csdn.net"
- blog/spider目录下会有一个myblog.py文件,查看文件内容
import scrapy
class MyblogSpider(scrapy.Spider):
name = 'myblog'
allowed_domains = ['blog.csdn.net']
start_urls = ['http://blog.csdn.net/']
def parse(self, response):
pass
- name: 爬虫名称,不可以与项目名称重名;
- allow_domains:搜索的域名范围,也就是爬虫的约束区域,规定爬虫只爬取这个域名下的网页,不存在的URL会被忽略;
- start_urls:爬取的URL元祖/列表。爬虫从这里开始抓取数据,所以,第一次下载的数据将会从这些urls开始。其他子URL将会从这些起始URL中继承性生成;
- parse(self, response) :解析的方法,每个初始URL完成下载后将被调用,调用的时候传入从每一个URL传回的Response对象来作为唯一参数,主要作用:负责解析返回的网页数据(response.body)、提取结构化数据(生成item)、生成需要下一页的URL请求
- 还有一种爬虫开始的执行方法
import scrapy
class MyblogSpider(scrapy.Spider):
name = 'myblog'
allowed_domains = ['blog.csdn.net']
# start_urls = ['http://blog.csdn.net/']
#
# def parse(self, response):
# print('callback********************')
# print(response.body)
# 爬虫开始,执行的方法, 相当于start_urls
def start_requests(self):
# 向调度器发送一个Request对象
yield scrapy.Request(
url='http://blog.csdn.net/', # 请求地址
callback=self.parse2 # 得到响应后的回调事件
)
def parse2(self):
print('callback********************')
print(response.body)
爬虫执行
在爬虫文件目录下执行命令
scrapy crawl 爬虫名
scrapy crawl myblog
- 将结果保存到文件中
scrapy crawl myblog -o blog.json
脚本执行
- 在同级目录下新建一个文件去执行爬虫
- 如果使用该方法执行爬虫,需要注意:在创建新的爬虫时,需要将该脚本内容注释,否则会导致创建失败
管道文件
- 在管道文件中pipelines.py可以对爬虫提取的数据进行处理和保存
-
启用管道文件
-
修改myblog.py文件
def start_requests(self):
# 向调度器发送一个Request对象
yield scrapy.Request(
url='http://blog.csdn.net/', # 请求地址
callback=self.parse2 # 得到响应后的回调事件
)
def parse2(self, response):
print('*********** parse2')
# 获取博客主页左侧边栏的主题分类
data = response.xpath('//div[@class="nav_com"]//li/a/text()').extract()
item = {}
item['data'] = data
yield item # 如果返回的是request请求,则发送给引擎->调度器;返回的是字典,则发送给引擎->管道文件
- 修该管道文件处理
爬取热门博客
- 爬虫处理 myblog.py
import scrapy
# 热门博客url地址
HOT_BLOG_URL = 'https://so.csdn.net/so/search/s.do?p={}&q={}&t=blog&viparticle=&domain=&o=&s=&u=&l=&f=&rbg=0'
class MyblogSpider(scrapy.Spider):
name = 'myblog'
allowed_domains = ['blog.csdn.net', 'so.csdn.net']
def __init__(self, topic='', pg_start=1, pg_end=1, *args, **kwargs):
super(MyblogSpider, self).__init__(*args, **kwargs)
self.topic = str(topic)
self.pg_start = int(pg_start)
self.pg_end = int(pg_end) + 1
# 爬虫开始,执行的方法
def start_requests(self):
# 向调度器发送一个Request对象
yield scrapy.Request(
url=HOT_BLOG_URL.format(1, self.topic), # 先获取第一页内容
callback=self.parse
)
# 对第一页的处理
def parse(self, response):
# 获取该主题的博客有多少页
pages = response.xpath('//div[@class="search-result-pagination"]/div/span/a/text()').extract()
if pages:
max_pg = int(pages[-1].strip())
if self.pg_end > (max_pg + 1):
self.pg_end = max_pg + 1
if self.pg_start < self.pg_end:
pg_start = self.pg_start
if 1 == self.pg_start:
# 获取第一页的博客
hrefs = response.xpath('//dl[@class="search-list J_search"]//a/@href').extract()
for href in hrefs:
yield scrapy.Request(
url=href,
callback=self.parse3
)
pg_start += 1
# 请求其他页的数据
for pg in range(pg_start, self.pg_end):
yield scrapy.Request(
url=HOT_BLOG_URL.format(self.topic, pg),
callback=self.parse2
)
# 获取每一页的博客数据
def parse2(self, response):
hrefs = response.xpath('//dl[@class="search-list J_search"]//a/@href').extract()
for href in hrefs:
yield scrapy.Request(
url=href,
callback=self.parse3
)
# 处理博客详情数据
def parse3(self, response):
item = {
'topic': self.topic,
'title': response.xpath('//h1[@class="title-article"]/text()').extract_first(),
'data': response.body
}
yield item
- 保存博客 pipelines.py
from itemadapter import ItemAdapter
import re
import os
class BlogPipeline:
def process_item(self, item, spider):
if item and item['title']:
# 创建结果存储文件夹
basePath = 'blog/{}/'.format(item['topic'])
if not os.path.exists(basePath):
os.makedirs(basePath)
title = re.sub(r'[/\\:*"<>|?]', '', item['title'])
with open('{}/{}.html'.format(basePath, title), 'wb') as f:
f.write(item['data'])
return item
- 执行爬虫
from scrapy import cmdline
cmdline.execute('scrapy crawl myblog -a topic=python -a pg_start=1 -a pg_end=2'.split())
- 注意事项
- 如果发现爬虫执行到一部分就停止了,检查下爬取的url是否在域名范围内
爬取数据定义
- 通常,对需要批量爬取的url请求,我们可以对需要获取的数据进行规范性定义。在 Item.py 文件中,我们以类的形式以及 Field 对象来声明。其中 Field 对象其实是一个字典类型,用于保存爬取到的数据。而定义出来的字段,可以简单理解为数据库表中的字段,但是它没有数据类型。Item 则复制了标准的 dict API,存放以及读取跟字典没有差别。
import scrapy
class MyBlogItem(scrapy.Item):
# define the fields for your item here like:
# 主题
topic = scrapy.Field()
# 标题
title = scrapy.Field()
# 详情url
detail_url= scrapy.Field()
数据保存
- 上面的案例是将博客内容保存为本地的html文件;
- 如果需要对爬取的数据进行数据库保存,可以在管道文件中使用mysql数据库进行持久化:
- 安装pymysql;
- 在mysql中先创建一个数据库,如blog;
- 在管道文件pipelines.py的***Pipeline类中,添加mysql数据库的连接;
import pymysql
class ZhaobiaoPipeline(object):
def __init__(self):
# 连接mysql数据库
self.mysql_conn = pymysql.Connect(
host='host地址,localhost',
port='mysql数据库端口,3306',
user='mysql数据库账号',
password='mysql数据库密码',
database='mysql数据库名称 blog',
charset='utf8', # 注意是utf8,不是utf-8
)
- 创建一个数据库表,如tbl_blog,可以预先创建好,也可以在管道文件的初始化中创建;
# 创建数据库表
def create_database_table(self):
# 创建光标对象
cs = self.mysql_conn.cursor()
# sql语句
sql_str = 'create table if not exists tbl_blog(' \
'id int primary key auto_increment,' \
'title varchar(100),' \
'topic varchar(100),' \
'detail_url varchar(100)' \
')'
# 执行sql语句
cs.execute(sql_str)
self.mysql_conn.commit()
- 填写爬取的数据格式,修改myblog.py
# 处理博客详情数据
def parse3(self, response):
item = {
'topic': self.topic,
'title': response.xpath('//h1[@class="title-article"]/text()').extract_first(),
'detail_url': response.url
}
yield item
- mysql数据库保存爬取的数据
def process_item(self, item, spider):
print("save item: ", item)
cs = self.mysql_conn.cursor()
keys = ','.join([key for key in item.keys()])
values = ','.join([f'"{value}"' for value in item.values()])
sql_str = 'insert into tbl_zhaobiao({}) value({})'.format(keys, values)
cs.execute(sql_str)
self.mysql_conn.commit()
return item
scrapy常用配置
中间件
DownloaderMiddle
- 启用该中间件,在settings.py文件中去除注释
- 添加请求头和代理ip
def process_request(self, request, spider):
print('######### BlogDownloaderMiddleware process_request start')
# 添加请求头,需要是字节类型,导入库from scrapy.http.request import Headers处理
request.headers = Headers({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
})
# 设置代理ip
proxy_ip = '27.204.91.22:35478' # 网上找个临时的代理ip用下,不一定可以使用
# 最好是使用代理ip接口生成一个
# proxy_ip = ur.urlopen('代理ip接口').read().decode('utf-8').strip()
request.meta['proxy'] = 'http://' + proxy_ip
print('######### BlogDownloaderMiddleware process_request end')
# Called for each request that goes through the downloader
# middleware.
# Must either:
# - return None: continue processing this request
# - or return a Response object
# - or return a Request object
# - or raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
return None
- 一般在这里是添加动态的请求头和代理IP,如果是固定的,可以在默认请求头DEFAULT_REQUEST_HEADERS中设置
新建中间件
- 新建一个获取动态User-Agent的中间件
- 在settings.py中添加User-Agent列表
UserAgent_List = [
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2226.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.4; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 4.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36",
"Mozilla/5.0 (X11; OpenBSD i386) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.3319.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2309.372 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2117.157 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1866.237 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/4E423F",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1",
"Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0",
"Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/31.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20130401 Firefox/31.0",
"Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0",
"Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16",
"Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14",
"Mozilla/5.0 (Windows NT 6.0; rv:2.0) Gecko/20100101 Firefox/4.0 Opera 12.14",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0) Opera 12.14",
"Opera/9.80 (Windows NT 5.1; U; zh-sg) Presto/2.9.181 Version/12.00"
]
- 在middlewares.py文件中新建RandomUserAgentMiddleware的代理中间件
import random
from blog.settings import UserAgent_List
class RandomUserAgentMiddleware(object):
'''动态随机设置 User-agent'''
def process_request(self, request, spider):
ua = random.choice(UserAgent_List)
if ua:
request.headers.setdefault('User-Agent', ua)
print(request.headers)
- 在settings.py文件中添加中间件配置
DOWNLOADER_MIDDLEWARES = {
'blog.middlewares.BlogDownloaderMiddleware': 543,
'blog.middlewares.RandomUserAgentMiddleware': 544, # 新加的代理中间件
}
默认请求头
- 在settings.py中启用DEFAULT_REQUEST_HEADERS,可以添加默认的User-Agent和Cookie等数据
禁用cookies
- 有些站点会使用 cookies 来发现爬虫的轨迹。因此,我们最好禁用 cookies,在 settings.py 文件中新增以下配置:
# 默认是被注释的, 也就是运行使用 cookies
# Disable cookies (enabled by default)
COOKIES_ENABLED = False
设置下载延迟
- 当 scrapy 的下载器在下载同一个网站下一个页面前需要等待的时间。我们设置下载延迟, 可以有效避免下载器获取到下载地址就立刻执行下载任务的情况发生。从而可以限制爬取速度, 减轻服务器压力。
- 在 settings.py 文件中新增以下配置:
# 默认是被注释的
# Configure a delay for requests for the same website (default: 0)
# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 3
# 单位是秒, 上述设置是延迟 3s。
# 同时还支持设置小数, 例 0.3, 延迟 300 ms
减小下载超时
- 如果对一个非常慢的连接进行爬取(一般对通用爬虫来说并不重要), 减小下载超时能让卡住的连接能被快速的放弃并解放处理其他站点的能力。
- 在 settings.py 文件中增加配置:
DOWNLOAD_TIMEOUT = 15
分布式爬虫
- 当爬取的页面较多时,使用单一的爬虫效率会比较低,可以利用Scrapy-Redis来部署分布式爬虫,这样就可以同时执行多个爬虫,或者在多个pc终端上执行,提高爬虫效率
- 安装redis和scraoy-redis;
- 在settings.py中添加相关配置
# Scrapy-Redis分布式爬虫相关配置
# 启动Scrapy-Redis的去重过滤器,取消Scrapy的去重功能
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
# 启动Scrapy-Redis的调度器,取消scrapy的调度器
SCHEDULER = 'scrapy_redis.scheduler.Scheduler'
# Scrapy-Redis的断点续爬
SCHEDULER_PERSIST = True
# 配置Redis数据库连接
REDIS_URL = 'redis://127.0.0.1:6379'
- 配置以上设置后,执行爬虫,会在redis数据库中生成一个以爬虫名称命名的文件夹,里面有2个数据库文件:
- dupefilter:去重过滤器,存储已发送的url请求,在爬虫发出一个请求时,会在这里查找是否已发送;
- requests:需要发送的url请求,从这里取出一个请求交给downloader
- 后面就可以同时始执行多个爬虫了
代理IP
- 编写爬虫时,为了对付网站的反爬机制,常常需要添加代理IP;
- 常常去第三方代理IP供应商获取一个代理IP的获取接口,请求该接口可以获取一些有用代理IP,形式为ip地址:端口号,如192.168.1.1:8000,然后使用这个ip去发送url请求;
- 对一些url请求较多的爬虫,频繁访问该代理IP的获取接口,会对这个接口造成较大的压力;对此可以设计一个代理IP池,里面不断更新代理IP(代理IP是具有有效期的),爬虫从里面获取一个代理IP去发送请求
import redis
import urllib.request as ur
import time
class ProxyPool():
def __init__(self):
# redis数据库连接
self.redis_conn = redis.StrictRedis(
host='localhost',
port=6379,
decode_responses=True
)
def set_proxy(self):
proxy_old = None
while True:
proxy_new = ur.urlopen('第三方代理ip获取接口').read().decode('utf-8').strip().split(' ')
if proxy_new != proxy_old:
proxy_old = proxy_new
self.redis_conn.delete('proxy')
self.redis_conn.sadd('proxy', *proxy_new)
time.sleep(2)
else:
time.sleep(1)
def get_proxy(self):
proxy = self.redis_conn.srandmember('proxy', 1)
if proxy:
return proxy[0]
else:
time.sleep(0.1)
return self.get_proxy()
if __name__ == '__main__':
ProxyPool().set_proxy()