Python爬虫 | Scrapy进阶

一、将爬取到的数据存储到磁盘文件和数据库中各一份

Pipeline

import pymysql
class QiubaiproPipeline(object):
fp = None

    def open_spider(self,spider):            #只会被调用一次
        print('开始爬虫')
        self.fp = open('./qiushi.txt','w',encoding='utf-8')

    #该方法被调用后,就可以将item(参数)包含的数据值进行持久化存储
    def process_item(self, item, spider):
        print('这是item的内容:',item)
        self.fp.write(item['name'])
        return item                    #返回给了下一个即将被执行的管道类

    def close_spider(self,spider):
        print('结束爬虫')
        self.fp.close()
        
class MysqlPileLine(object):
    conn = None
cursor = None

    def open_spider(self,spider):
        self.conn = pymysql.Connect(host='127.0.0.1',port=3306,user='root',password='',db='spider')

    def process_item(self,item,spider):
        self.cursor = self.conn.cursor()
        try:
            self.cursor.execute('insert into qb values("%s")'%(item['name']))
            self.conn.commit()
        except Exception as e:
            print(e)
            self.conn.rollback()

    def close_spider(self,spider):
        self.cursor.close()
        self.conn.close()

Settings

ITEM_PIPELINES = {
   'qiubaiPro.pipelines.QiubaiproPipeline': 300,                    # 先执行
   'qiubaiPro.pipelines.MysqlPileLine': 301,                       # 后执行
}

上述代码中,字典中的两组键值分别表示会执行管道文件中对应的两个管道类中的process_item方法,实现两种不同形式的持久化操作。

二、scrapy中selenium的应用

在通过scrapy框架进行某些网站数据爬取的时候,往往会碰到页面动态数据加载的情况发生,如果直接使用scrapy对其url发请求,是绝对获取不到那部分动态加载出来的数据值。但是通过观察我们会发现,通过浏览器进行url请求发送则会加载出对应的动态加载出的数据。那么如果我们想要在scrapy也获取动态加载出的数据,则必须使用selenium创建浏览器对象,然后通过该浏览器对象进行请求发送,获取动态加载的数据值。需要用到下载中间件中的process_response 方法。

selenium在scrapy中使用的原理分析:
  当引擎将url对应的请求提交给下载器后,下载器进行网页数据的下载,然后将下载到的页面数据,封装到response中,提交给引擎。引擎将response在转交给Spiders。Spiders接受到的response对象中存储的页面数据里是没有动态加载的新闻数据的。要想获取动态加载的新闻数据,则需要在下载中间件中对下载器提交给引擎的response响应对象进行拦截,且对其内部存储的页面数据进行篡改,修改成携带了动态加载出的数据,然后将被篡改的response对象最终交给Spiders进行解析操作。

selenium在scrapy中的使用流程:

  • 在spider的init方法中实例化一个浏览器对象(因为浏览器对象只需要被实例化一次);
  • 在spider的closed方法中关闭浏览器对象(该方法是在爬虫结束时被调用);
  • 在下载中间件类的process_response方法中接收spider中的浏览器对象;
  • 处理执行相关自动化操作(发起请求,获取页面数据)
  • 实例化一个新的响应对象(from scrapy.http import HtmlResponse),且将页面数据存储到该对象中
  • 返回新的响应对象
  • 配置文件中开启下载中间件

案例分析:

  • 需求:爬取网易新闻的国内板块下的新闻数据
  • 分析:当点击国内超链进入国内对应的页面时,会发现当前页面展示的新闻数据是被动态加载出来的,如果直接通过程序对url进行请求,是获取不到动态加载出的新闻数据的。则就需要我们使用selenium实例化一个浏览器对象,在该对象中进行url的请求,获取动态加载的新闻数据。

代码展示:

爬虫文件:

class WangyiSpider(RedisSpider):
    name = 'wangyi'
    #allowed_domains = ['www.xxxx.com']
    start_urls = ['https://news.163.com']            #如果爬取多个url,全写到里面,依次爬取

    def __init__(self):
        #实例化一个浏览器对象(实例化一次)
        self.bro = webdriver.Chrome(executable_path='/Users/bobo/Desktop/chromedriver')

    #必须在整个爬虫结束后,关闭浏览器
    def closed(self,spider):
        print('爬虫结束')
        self.bro.quit()

中间件文件:修改process_response中的方法

from scrapy.http import HtmlResponse    
    def process_response(self, request, response, spider):
        #响应对象中存储页面数据的篡改
        if request.url in['http://news.163.com/domestic/','http://news.163.com/world/','http://news.163.com/air/','http://war.163.com/']:
            spider.bro.get(url=request.url)
            js = 'window.scrollTo(0,document.body.scrollHeight)'
            spider.bro.execute_script(js)
            time.sleep(2)                          #一定要给与浏览器一定的缓冲加载数据的时间
            page_text = spider.bro.page_source            #页面数据就是包含了动态加载出来的新闻数据对应的页面数据
            time.sleep(2)  

            #篡改响应对象
            return HtmlResponse(url=spider.bro.current_url,body=page_text,encoding='utf-8',request=request)
        else:
            return response

配置文件:

DOWNLOADER_MIDDLEWARES = {
    'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
}

三、增量式爬虫

当我们在浏览`网页的时候会发现,某些网站定时会在原有网页数据的基础上更新一批数据,例如某电影网站会实时更新一批最近热门的电影。小说网站会根据作者创作的进度实时更新最新的章节数据等等。那么,类似的情景,当我们在爬虫的过程中遇到时,我们是不是需要定时更新程序以便能爬取到网站中最近更新的数据呢?

1. 增量式爬虫

概念:通过爬虫程序监测某网站数据更新的情况,以便可以爬取到该网站****更新出的新数据****。

如何进行增量式的爬取工作:

  • 在发送请求之前判断这个URL是不是之前爬取过
  • 在解析内容后判断这部分内容是不是之前爬取过
  • 写入存储介质时判断内容是不是已经在介质中存在

分析:
不难发现,其实增量爬取的核心是去重,至于去重的操作在哪个步骤起作用,只能说各有利弊。在我看来,前两种思路需要根据实际情况取一个(也可能都用)。

  • 第一种思路适合不断有新页面出现的网站,比如说小说的新章节,每天的最新新闻等等;
  • 第二种思路则适合页面内容会更新的网站。
  • 第三个思路是相当于是最后的一道防线。这样做可以最大程度上达到去重的目的。

去重方法

将爬取过程中产生的url进行存储,存储在redis的set中。当下次进行数据爬取时,首先对即将要发起的请求对应的url在存储的url的set中做判断,如果存在则不进行请求,否则才进行请求。

对爬取到的网页内容进行唯一标识的制定,然后将该唯一标识,存储至redis的set中。当下次爬取到网页数据的时候,在进行持久化存储之前,首先可以先判断该数据的唯一标识在redis的set中是否存在,再决定是否进行持久化存储。

2.项目案例

  • 需求:爬取4567tv网站中所有的电影详情数据。策略:对详情页url去重

爬虫文件

import scrapyfrom scrapy.linkextractors 
import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from redis import Redis
from incrementPro.items import IncrementproItem

class MovieSpider(CrawlSpider):
    name = 'movie'
    start_urls = ['http://www.4567tv.tv/frim/index7-11.html']                            #首页

    rules = (
        Rule(LinkExtractor(allow=r'/frim/index7-\d+\.html'), callback='parse_item', follow=True),    #正则中正常使用点号要转义
)

    conn = Redis(host='127.0.0.1',port=6379)                                    #创建redis链接对象

    def parse_item(self, response):
        li_list = response.xpath('//li[@class="p1 m1"]')
        for li in li_list:
            detail_url = 'http://www.4567tv.tv'+li.xpath('./a/@href').extract_first()            #获取详情页的url

       # 将详情页的url存入redis的set中,这里的第一个参数url,是给set集合起的名字
            ex = self.conn.sadd('urls',detail_url)                                
            if ex == 1:
                print('该url没有被爬取过,可以进行数据的爬取')
                yield scrapy.Request(url=detail_url,callback=self.parst_detail)
            else:
                print('数据还没有更新,暂无新数据可爬取!')

    #解析详情页中的电影名称和类型,进行持久化存储
    def parst_detail(self,response):
        item = IncrementproItem()
        item['name'] = response.xpath('//dt[@class="name"]/text()').extract_first()
        item['kind'] = response.xpath('//div[@class="ct-c"]/dl/dt[4]//text()').extract()
        item['kind'] = ''.join(item['kind'])
        yield item

管道文件:

from redis import Redis

class IncrementproPipeline(object):
conn = None

    def open_spider(self,spider):
        self.conn = Redis(host='127.0.0.1',port=6379)

    def process_item(self, item, spider):
        dic = {
            'name':item['name'],
            'kind':item['kind']
        }
        print(dic)
        self.conn.lpush('movieData',dic)
        return item

启动redis数据库的服务端和客户端


image
image
image

拷贝一下,作为正则

image
image
image
image

需求:爬取糗事百科中的段子和作者数据。策略:对爬取的内容去重。

爬虫文件

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from incrementByDataPro.items import IncrementbydataproItem
from redis import Redisimport hashlib

class QiubaiSpider(CrawlSpider):
    name = 'qiubai'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['https://www.qiushibaike.com/text/']

    rules = (
        Rule(LinkExtractor(allow=r'/text/page/\d+/'), callback='parse_item', follow=True),
        Rule(LinkExtractor(allow=r'/text/$'), callback='parse_item', follow=True),
)

    #创建redis链接对象
    conn = Redis(host='127.0.0.1',port=6379)
    def parse_item(self, response):
        div_list = response.xpath('//div[@id="content-left"]/div')

        for div in div_list:
            item = IncrementbydataproItem()
            item['author'] = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span[2]/h2/text()').extract_first()
            item['content'] = div.xpath('.//div[@class="content"]/span/text()').extract_first()

            #将解析到的数据值生成一个唯一的标识进行redis存储
            source = item['author']+item['content']
            source_id = hashlib.sha256(source.encode()).hexdigest()

            #将解析内容的唯一表示存储到redis的data_id中
            ex = self.conn.sadd('data_id',source_id)

            if ex == 1:
                print('该条数据没有爬取过,可以爬取......')
                yield item
            else:
                print('该条数据已经爬取过了,不需要再次爬取了!!!')

管道文件:

# -*- coding: utf-8 -*-
# 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

from redis import Redis
class IncrementbydataproPipeline(object):
    conn = None

    def open_spider(self, spider):
        self.conn = Redis(host='127.0.0.1', port=6379)

    def process_item(self, item, spider):
        dic = {
            'author': item['author'],
            'content': item['content']
        }
        # print(dic)
        self.conn.lpush('qiubaiData', dic)
        return item

因为span标签里面有br标签,所以用xpath获得的是列表,用extract提取,然后用jion转换成字符串

image

要排空值,产生的原因是xpath解析

image
image
image

四、如何提高scrapy的爬取效率

  • 增加并发
      默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改**CONCURRENT_REQUESTS **= 100值为100,并发设置成了为100。

  • 降低日志级别
    在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:LOG_LEVEL = ‘INFO’

  • 禁止cookie
    如果不是真的需要cookie,则在scrapy爬取数据时可以进制cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:COOKIES_ENABLED = False

  • 禁止重试
    对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:RETRY_ENABLED = False

  • 减少下载超时
    如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 10 超时时间为10s

测试案例:www.521609.com

import scrapyfrom xiaohua.items 
import XiaohuaItem

class XiahuaSpider(scrapy.Spider):

    name = 'xiaohua'
    allowed_domains = ['www.521609.com']
    start_urls = ['http://www.521609.com/daxuemeinv/']

    pageNum = 1
    url = 'http://www.521609.com/daxuemeinv/list8%d.html'

    def parse(self, response):
        li_list = response.xpath('//div[@class="index_img list_center"]/ul/li')
        for li in li_list:
            school = li.xpath('./a/img/@alt').extract_first()
            img_url = li.xpath('./a/img/@src').extract_first()

            item = XiaohuaItem()
            item['school'] = school
            item['img_url'] = 'http://www.521609.com' + img_url

            yield item

        if self.pageNum < 10:
            self.pageNum += 1
            url = format(self.url % self.pageNum)
            #print(url)
            yield scrapy.Request(url=url,callback=self.parse)
import scrapy

class XiaohuaItem(scrapy.Item):
    school=scrapy.Field()
    img_url=scrapy.Field()
import json
import os
import urllib.request

class XiaohuaPipeline(object):
    def __init__(self):
        self.fp = None

    def open_spider(self,spider):
        print('开始爬虫')
        self.fp = open('./xiaohua.txt','w')

    def download_img(self,item):
        url = item['img_url']
        fileName = item['school']+'.jpg'
        if not os.path.exists('./xiaohualib'):
            os.mkdir('./xiaohualib')
        filepath = os.path.join('./xiaohualib',fileName)
        urllib.request.urlretrieve(url,filepath)
        print(fileName+"下载成功")

    def process_item(self, item, spider):
        obj = dict(item)
        json_str = json.dumps(obj,ensure_ascii=False)
        self.fp.write(json_str+'\n')

        #下载图片
        self.download_img(item)
        return item

    def close_spider(self,spider):
        print('结束爬虫')
        self.fp.close()

配置文件:

USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

# Configure maximum concurrent requests performed by Scrapy (default: 16)
CONCURRENT_REQUESTS = 100
COOKIES_ENABLED = False
LOG_LEVEL = 'ERROR'
RETRY_ENABLED = False
DOWNLOAD_TIMEOUT = 3
#CONCURRENT_REQUESTS_PER_DOMAIN = 16
#CONCURRENT_REQUESTS_PER_IP = 16
DOWNLOAD_DELAY = 3

五、自定制命令

1.不在终端运行爬虫方法

不在终端运行爬虫,可以在最外层项目的下创建start.py文件
导入如下配置:直接运行就可以了

import   sys
from  scrapy.cmdline import  execute


if __name__ == '__main__':
    execute(['scrapy','crawl','baidu','--nolog'])##这里相当于是scrapy  crawl  baidu  --nolog  

    '''
    放在创建项目下的第一层目录下执行
    '''

2.自定制scrapy命令方法

在settings里面的配置:

COMMANDS_MODULE='scrapyproject1.commands'

首先在你要启动的项目下面(spider的同级目录)创建一个commands文件(名字可以自己修改,但是前提settings配置信息也要修改,否则找不到),然后在commands下面创建crawl_allspiders.py(名字可以修改,运行命令就是--scrapy 名字--,例如:scrapy crawl_spiders)

配置信息如下:

from   scrapy.commands  import  ScrapyCommand
from  scrapy.utils.project import  get_project_settings

class  Command(ScrapyCommand):
    requires_project = True
    def  syntax(self):
        return  '[option]'                          # 该命令的option

    def   short_desc(self):                         # 对该命令的描述
        return   'run'

    def  run(self, args, opts):
        spider_list=self.crawler_process.spiders.list()
        print(spider_list)                          # 拿到当前下的所有爬虫
        print(opts)
        print(args)
        '''['baidu', 'baidu1', 'baidu2']'''
        for  name    in  spider_list:
            self.crawler_process.crawl(name,**opts.__dict__)

        self.crawler_process.start()
xpath拿到的是selector对象,可以继续往下面找标签
extract()           拿到的是字符串
extarct_first()     拿到当前标签下面的第一个文本内容
extract()           拿到所有的标签文本的内容

./                  儿子(当前标签下面)
.//                 当前标签下面的孙子,子子孙孙都可以


# 取当前标签下面的属性和文本:
/text()             取当前标签的文本内容
/@href              拿到当前的属性

response.xpath('//a[2]')后面的2是按索引来找到第二个
response.xpath('//a[@href][@id]')是多个条件进行筛选

//a[contains(@href,"sina")]         包含的关系,只有这个标签里面有sina这个字段就可以了,也可以是其他字符
//a[start-with(@href,'sina')]       找到这个属性是否是以sina开头的

re:正则
//a[re:test(@id,'i\(d+)')]          更高级的用法,前面是固定的写法,后面是找到id属性,后main是匹配的规则,id=i1或id=i2>>>>d+是匹配数字
//a[re:test(@id,'i\(d+)')]/text()   拿文本,或者其他

*是匹配多个的写法,代表任意的标签
//*[@id="newsContent23123186"]/div[1]   找第几个,最后是索引

split('',)              切割
strip()                 去除空

内链就是同一网站域名下站内的链接,链接指向网站内部,好的内链结构是有助于网站收录的。最通俗的说法是自己在自己的网站上添加链接,就是在同一网站域名下的各内容页面间的互相链接。内链,其实在于用户体验,能从这个页面快速的进入下个页面就可以了。

网站的内链和外链的区别,外链可以算作是友情链接,就是你和别人交换相互之间的链接,从别人网站可以进入你的网站,内链在同一网站域名下的内容页面之间的互相链接,是网站自身内部结构。不管是内链还是外链,都对网站的SEO有重要的影响,所有不管你是做什么网站,都要注重内链和外链的布局,这样你的网站才能在搜索引擎上有排名.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容