scrapy爬虫总结

目录

  • 一. Scrapy
    • 1. 概述
    • 2. 流程
    • 3. 创建爬虫命令
  • 二. Selenium
    • 1. 概述
    • 2. Python + Selenium WebDriver
      • 2.1 基本使用
      • 2.2 优缺点
      • 2.3 启动正常浏览器绑定端口
      • 2.4 scrapy 结合 selenium
  • 三. 多线程
    • 1. Lock版本生产者和消费者模式
    • 2. Condition版的生产者与消费者模式
    • 3. Queue线程安全队列
    • 4. 多线程下载百思不得姐段子
    • 5. scrapy中的多线程
  • 四. 分布式爬虫
    • 1. 概念与原理

一. Scrapy

1. 概述

Scrapy ---- Python开发的一个快速、高层次的屏幕抓取和web抓取框架,用于抓取web站点并从页面中提取结构化的数据。Scrapy吸引人的地方在于它是一个框架,任何人都可以根据需求方便的修改。

2. 流程

scrapy爬虫总结_第1张图片
scrapy爬虫总结_第2张图片

  1. 首先爬虫Spiders将需要发送请求的url(Requests)经引擎Engine交给调度器Scheduler;
  2. Scheduler排序处理后,经EngineDownloaderMiddlewares(设置User_Agent, Proxy代理)交给Downloader;
  3. Downloader向互联网发送请求,并接收下载响应,将响应经Engine,可选交给Spiders(除了交给Spiders,还可以通过DownloaderMiddlewares设置重定向,重新请求等等);
  4. Spiders处理response,提取、处理数据并将处理后的数据经Engine交给ItemPipeline保存;
  5. 提取url重新经ScrapyEngine交给Scheduler进行下一个循环。直到无Url请求程序或触发异常,程序停止结束。

3. 创建爬虫命令

创建项目:scrapy startproject xxx(项目名)
进入项目:cd xxx #进入某个文件夹下
创建爬虫:scrapy genspider xxx(爬虫名) xxx.com (爬取域)
生成文件:scrapy crawl xxx -o xxx.json (生成某种类型的文件)
运行爬虫:scrapy crawl XXX
列出所有爬虫:scrapy list
获得配置信息:scrapy settings [options]

二. Selenium

1. 概述

Selenium是一个涵盖了一系列工具和库的开源框架,这些工具和库支持Web浏览器的自动化测试。
Selenium框架由多个工具组成,包括: Selenium IDE,Selenium RC,Selenium WebDriver和Selenium Grid。
参考链接:https://blog.csdn.net/wanglian2017/article/details/72843984

  • Selenium RC使用的是JavaScript注入技术与浏览器打交道,将操作Web元素的API调用转化为一段段Javascript,这种Javascript注入技术的缺点是速度不理想并且稳定性不高。
  • WebDriver利用浏览器原生的API,封装成一套更加面向对象的SeleniumWebDriver API,直接操作浏览器页面里的元素,甚至操作浏览器本身,速度大大提高,而且调用的稳定性交给了浏览器厂商本身,显然是更加科学。但不同的浏览器厂商,对Web元素的操作和呈现多少会有一些差异,这就直接导致了Selenium WebDriver要分浏览器厂商不同,而提供不同的实现。例如Firefox就有专门的FirefoxDriver,Chrome就有专门的ChromeDriver等等。
    scrapy爬虫总结_第3张图片

2. Python + Selenium WebDriver

2.1 基本使用

# 配置
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')  # 使用headless无界面浏览器模式

# browser = webdriver.Firefox()
browser = webdriver.Chrome()
# browser = webdriver.Chrome(options=chrome_options)

# 打开百度首页
browser.get('http://www.baidu.com/')

# 搜索框输入python,并按下确定键
browser.find_element_by_xpath('//input[@id="kw"]').send_keys("python")
time.sleep(2)
browser.find_element_by_xpath('//input[@type="submit"]').click()
time.sleep(2)

# 截图
browser.save_screenshot("baidu_python.png")

# 关闭浏览器
browser.close()

2.2 优缺点

selenium框架也就是模拟浏览器,相当于您用程序调动浏览器让浏览器打开您需要爬取的网站。

优点:

  • 反爬能力强。在用Python的requests库发出网络情况时候,您必须先构造http请求头。有些网站反爬很严格,可以直接识别出来您当前的访问是否正常用户行为。所以如果在用request请求时被目标网站反爬识别,导致无法爬取的话,那么这个时候只有使用这个selenium框架就是最好技术选择方式。
  • 适合爬取动态页面、需要点击提交的网站

缺点:

  • 速度慢,driver.get(“url”) 需要等到页面全部加载渲染完成后才会执行后续的脚本。
  • 虽然说相对于requests库来说,反爬能力强,但通过selenium启动的浏览器还是有很多特征很容易被网站通过javascript检测(打开 https://bot.sannysoft.com/可查看),具体看下图(只截取一部分):
    scrapy爬虫总结_第4张图片
    一开始WebDriver就被标红了,说明网站成功检测到你使用模拟浏览器了,往下翻会更多,无头浏览器更加吓人。

2.3 启动正常浏览器绑定端口

那有没有办法使通过WebDriver驱动的浏览器像正常浏览器一样呢?
在搜索了很久之后,发现了一个办法:通过模拟浏览器驱动控制已经打开的正常浏览器
我们可以利用Chrome DevTools协议。它允许客户检查和调试Chrome浏览器,指定端口和配置文件目录:

chrome.exe --remote-debugging-port=9222 --user-data-dir="C:\selenum\AutomationProfile"
# 对于-remote-debugging-port值,可以指定任何打开的端口。
# 对于-user-data-dir标记,指定创建新Chrome配置文件的目录。为了确保在单独的配置文件中启动chrome,不会污染默认配置文件。

回顾Selenium WebDriver的原理,发现是通过WebDriver来打开浏览器然后绑定到特定端口,那么可以手动绑定我们已经指定好端口(9222)的浏览器的端口:

chrome_options = webdriver.ChromeOptions()
# chrome_options.add_argument('--headless')
chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")  # 绑定端口
browser = webdriver.Chrome(options=chrome_options)

browser.get("https://bot.sannysoft.com/")
time.sleep(10)
# 各种操作
browser.close()

打开网页会发现跟正常浏览器无异,也就可以大胆操作了,当然还是要控制频率或者结合使用动态代理。

2.4 scrapy 结合 selenium

关键:在DOWNLOADER_MIDDLEWARES实现自定义请求

编写my_spider.py

class MySpider(scrapy.Spider):
    name = "my_spider"

    def firefox_init(self):
        firefox_options = webdriver.FirefoxOptions()
        #firefox_options.add_argument('--headless')
        return webdriver.Firefox(options=firefox_options)

    def chrome_init(self):
        os.system('fuser -k -n tcp %d &' % self.port)
        time.sleep(2)
        os.system('google-chrome --remote-debugging-port=%d --user-data-dir="/python/spider/data/" --headless &' % (self.port))  #
        chrome_options = Options()
        chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:%d" % self.port)
        return webdriver.Chrome(chrome_options=chrome_options)

    def __init__(self):
        self.browsers = []
        self.port = 0

    def start_requests(self):
		start_urls = ['http://www.baidu.com']
		
		# 使用空闲的端口,例如9221
		self.port = 9221
		# 使用chrome浏览器
		self.browsers.append(self.chrome_init())
		# self.browsers.append(self.firefox_init())

        yield Request(start_urls[0], callback=self.parse, meta={'spider_name':'my_spider'}) #
	
	def parse(self, response):
		print(response.meta['spider_name'])
		# 具体的解析内容
		pass

    # 重写爬虫close函数
    def close(self, spider, reason):
        # 关闭浏览器
        if self.browsers:
            # firefox
            # for browser in self.browsers:
            #     browser.quit()

            # chrome
            os.system('fuser -k -n tcp %d &' % self.port)
        closed = getattr(spider, 'closed', None)
        if callable(closed):
            return closed(reason)

编写middlewares.py

class SeleniumMiddleware(object):
	def process_request(self, request, spider):
		# 获取浏览器实例
		browser = spider.browsers[0]
		# 其他模拟操作,例如 点击 等
		page_source = browser.page_source
        return HtmlResponse(url=request.url, body=page_source, encoding='utf-8', request=request)

编写settings.py

DOWNLOADER_MIDDLEWARES = {
    'teyecrawler.middlewares.SeleniumMiddleware': 543,
    'scrapy.downloadermiddlewares.retry.RetryMiddleware': 544,
}

三. 多线程

生产者和消费者模式是多线程开发中经常见到的一种模式。生产者的线程专门用来生产一些数据,然后存放到一个中间的变量中。消费者再从这个中间的变量中取出数据进行消费。但是因为要使用中间变量,中间变量经常是一些全局变量,因此需要使用锁来保证数据完整性。

1. Lock版本生产者和消费者模式

import threading
import random
import time

gMoney = 1000
gLock = threading.Lock()
# 记录生产者生产的次数,达到10次就不再生产
gTimes = 0

class Producer(threading.Thread):
    def run(self):
        global gMoney
        global gLock
        global gTimes
        while True:
            money = random.randint(100, 1000)
            gLock.acquire()
            # 如果已经达到10次了,就不再生产了
            if gTimes >= 10:
                gLock.release()
                break
            gMoney += money
            print('%s当前存入%s元钱,剩余%s元钱' % (threading.current_thread(), money, gMoney))
            gTimes += 1
            time.sleep(0.5)
            gLock.release()

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        global gLock
        global gTimes
        while True:
            money = random.randint(100, 500)
            gLock.acquire()
            if gMoney > money:
                gMoney -= money
                print('%s当前取出%s元钱,剩余%s元钱' % (threading.current_thread(), money, gMoney))
                time.sleep(0.5)
            else:
                # 如果钱不够了,有可能是已经超过了次数,这时候就判断一下
                if gTimes >= 10:
                    gLock.release()
                    break
                print("%s当前想取%s元钱,剩余%s元钱,不足!" % (threading.current_thread(),money,gMoney))
            gLock.release()

def main():
    for x in range(5):
        Consumer(name='消费者线程%d'%x).start()

    for x in range(5):
        Producer(name='生产者线程%d'%x).start()

if __name__ == '__main__':
    main()

2. Condition版的生产者与消费者模式

Lock版本的生产者与消费者模式可以正常的运行。但是存在一个不足,在消费者中,总是通过while True死循环并且上锁的方式去判断钱够不够。上锁是一个很耗费CPU资源的行为。因此这种方式不是最好的。

还有一种更好的方式便是使用threading.Condition来实现。
threading.Condition可以在没有数据的时候处于阻塞等待状态。一旦有合适的数据了,还可以使用notify相关的函数来通知其他处于等待状态的线程。这样就可以不用做一些无用的上锁和解锁的操作。可以提高程序的性能。

首先对threading.Condition相关的函数做个介绍,threading.Condition类似threading.Lock,可以在修改全局数据的时候进行上锁,也可以在修改完毕后进行解锁。

  • acquire:上锁。
  • release:解锁。
  • wait:将当前线程处于等待状态,并且会释放锁。可以被其他线程使用notify和notify_all函数唤醒。被唤醒后会继续等待上锁,上锁后继续执行下面的代码。
  • notify:通知某个正在等待的线程,默认是第1个等待的线程。
  • notify_all:通知所有正在等待的线程。notify和notify_all不会释放锁。并且需要在release之前调用。
import threading
import time
import random

gCondition = threading.Condition()
gMoney = 1000
gTotalTime = 10 # 总生产次数
gCount = 0

class Producer(threading.Thread):
    def run(self) -> None:
        global gMoney
        global gCount
        while True:
            money = random.randint(100,1000)
            gCondition.acquire()
            if gCount >= gTotalTime:
                gCondition.release()
                break
            gMoney += money
            gCondition.notify_all()
            print("%s生产了%d钱,剩余%d钱" % (threading.current_thread(), money, gMoney))
            gCount += 1
            gCondition.notify_all()
            gCondition.release()
            time.sleep(1)


class Consumer(threading.Thread):
    def run(self) -> None:
        global gMoney
        global gCount
        while True:
            money = random.randint(100,1000)
            gCondition.acquire()
            # 这里要给个while循环判断,因为等轮到这个线程的时候,条件有可能又不满足了
            # 我的理解:假如有多个(2个)线程同时判断 gMoney
            # 然而第一个执行完后,第二个执行时其实钱不够了,所有要加while,如果是if两个线程都会进入
            while gMoney < money:
                if gCount >= gTotalTime:
                    gCondition.release()
                    return
                print("%s要消费%d钱,但只剩余%d钱,不足!" % (threading.current_thread(), money, gMoney))
                gCondition.wait()

            gMoney -= money
            print("%s消费了%d钱,剩余%d钱" % (threading.current_thread(), money, gMoney))
            gCondition.release()
            time.sleep(1)


def main():
    for i in range(3):
        t = Consumer(name='消费者线程%d' % i)
        t.start()

    for i in range(2):
        t = Producer(name='生产者线程%d' % i)
        t.start()


if __name__ == '__main__':
    main()

3. Queue线程安全队列

在线程中,访问一些全局变量,加锁是一个经常的过程。如果你是想把一些数据存储到某个队列中,那么Python内置了一个线程安全的模块叫做queue模块。Python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后入先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。相关的函数如下:

  • 初始化Queue(maxsize):创建一个先进先出的队列。
  • qsize():返回队列的大小。
  • empty():判断队列是否为空。
  • full():判断队列是否满了。
  • get():从队列中取最后一个数据。
  • put():将一个数据放到队列中。

4. 多线程下载百思不得姐段子

import requests
from lxml import etree
import threading
from queue import Queue
import csv


class BSSpider(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
    }
    def __init__(self,page_queue,joke_queue,*args,**kwargs):
        super(BSSpider, self).__init__(*args,**kwargs)
        self.base_domain = 'http://www.budejie.com'
        self.page_queue = page_queue
        self.joke_queue = joke_queue

    def run(self):
        while True:
            if self.page_queue.empty():
                break
            url = self.page_queue.get()
            response = requests.get(url, headers=self.headers)
            text = response.text
            html = etree.HTML(text)
            descs = html.xpath("//div[@class='j-r-list-c-desc']")
            for desc in descs:
                jokes = desc.xpath(".//text()")
                joke = "\n".join(jokes).strip()
                link = self.base_domain+desc.xpath(".//a/@href")[0]
                self.joke_queue.put((joke,link))
            print('='*30+"第%s页下载完成!"%url.split('/')[-1]+"="*30)

class BSWriter(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
    }

    def __init__(self, joke_queue, writer,gLock, *args, **kwargs):
        super(BSWriter, self).__init__(*args, **kwargs)
        self.joke_queue = joke_queue
        self.writer = writer
        self.lock = gLock

    def run(self):
        while True:
            try:
                joke_info = self.joke_queue.get(timeout=40)
                joke,link = joke_info
                self.lock.acquire()
                self.writer.writerow((joke,link))
                self.lock.release()
                print('保存一条')
            except:
                break

def main():
    page_queue = Queue(10)
    joke_queue = Queue(500)
    gLock = threading.Lock()
    fp = open('bsbdj.csv', 'a',newline='', encoding='utf-8')
    writer = csv.writer(fp)
    writer.writerow(('content', 'link'))

    for x in range(1,11):
        url = 'http://www.budejie.com/text/%d' % x
        page_queue.put(url)

    for x in range(5):
        t = BSSpider(page_queue,joke_queue)
        t.start()

    for x in range(5):
        t = BSWriter(joke_queue,writer,gLock)
        t.start()

if __name__ == '__main__':
    main()

5. scrapy中的多线程

scrapy基于twisted异步IO框架,downloader是多线程的。但是,由于python使用GIL(全局解释器锁,保证同时只有一个线程在使用解释器),这极大限制了并行性,在处理运算密集型程序的时候,Python的多线程效果很差,而如果开多个线程进行耗时的IO操作时,Python的多线程才能发挥出更大的作用。(因为Python在进行长时IO操作时会释放GIL)
scrapy中settings.py

# 最大并发数(默认16个)
CONCURRENT_REQUESTS = 32

# Configure a delay for requests for the same website (default: 0)
# See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 3
# The download delay setting will honor only one of:
#CONCURRENT_REQUESTS_PER_DOMAIN = 16  # 针对域名
#CONCURRENT_REQUESTS_PER_IP = 16  # 针对ip

若设置最大并发数并且将 DOWNLOAD_DELAY 设为0,则就是最大同时发出32个请求;若设置了DOWNLOAD_DELAY 并发请求就会受到影响,DOWNLOAD_DELAY 会影响 CONCURRENT_REQUESTS,不能使并发显现出来。

当有CONCURRENT_REQUESTS,没有DOWNLOAD_DELAY 时,服务器会在同一时间收到大量的请求。

当有CONCURRENT_REQUESTS,有DOWNLOAD_DELAY 时,服务器不会在同一时间收到大量的请求。

虽然说downloader就是多线程的,但还是可以尝试在代码中使用多线程

import threading
import time
from concurrent.futures.thread import ThreadPoolExecutor
import requests
from queue import Queue

from lxml import etree
from scrapy import Request
import scrapy
import re


class ProductDetails:
    def __init__(self, href, company_brand, market_name):
        self.href = href
        self.company_brand = company_brand
        self.market_name = market_name

class TestSpider(scrapy.Spider):
    name = "test_spider"
    base_domain = 'https://www.alditalk.de'
    start_urls = ['https://www.alditalk.de/smartphones?page=0']
    pool = ThreadPoolExecutor(max_workers=3) # 使用线程池
    start_time = time.time()
    headers = {
        'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)'
    }

    def get_request(self, product_details: ProductDetails):
        #print(threading.current_thread().name, ': get_request')
        res = requests.get(product_details.href,headers=self.headers)
        return res.text

    def parse_details(self, future):
        #print(threading.current_thread().name, ': parse_details')
        product_details = future.result()
        # brand = product_details.company_brand
        # name = product_details.market_name
        text = etree.HTML(product_details).xpath('//ul[@class="product-detail-teaser__list"]//li/text()')
        # print(text)
        time.sleep(2)
        #print('brand: ', brand, ', name: ', name)

    def parse_details_by_spider(self, response):
        self.sleep()

    def sleep(self):
        time.sleep(2)

    def parse(self, response):
        products = response.xpath('//div[contains(@class, "product-detail-teaser js-product-container")]')
        for product in products:
            price = product.xpath('.//span[@class="current-price"]//text()').get()
            price = price.replace(',', '.').replace('-', '00').strip().split()[0]
            if float(price) > 800 or float(price) < 30:
                continue

            href = self.base_domain + product.xpath('.//a[@class="product__list--name"]/@href').get()
            title = product.xpath('.//a[@class="product__list--name"]/h3/text()').get()
            company_brand = title.split()[0]
            market_name = re.sub(',.*|Smartphone|\d{1,3}.?GB.*', '', title).replace('  ', ' ').strip()
            print(href, company_brand, market_name)
            #future = self.pool.submit(self.get_request, ProductDetails(href, company_brand, market_name))
            #future.add_done_callback(self.parse_details) # 线程数量3: 30,线程数越多,越快
            yield Request(href,callback=self.parse_details_by_spider) # 63s

        # next_url = response.xpath('//a[@class="util-icon--after util-icon--arrow-slide-right"]/@href').get()
        # if not next_url:
        #     return
        # else:
        #     yield Request(self.base_domain + '/smartphones' + next_url, callback=self.parse)

    def close(self, spider, reason):
        self.pool.shutdown(True)
        print(time.time() - self.start_time)

四. 分布式爬虫

参考链接:
Scrapy框架之分布式操作
基于Scrapy分布式爬虫的开发与设计

1. 概念与原理

分布式爬虫概念:多台机器上执行同一个爬虫程序,实现网站数据的分布爬取

1、原生的Scrapy无法实现分布式爬虫的原因?

  • 调度器无法在多台机器间共享:因为多台机器上部署的scrapy会各自拥有各自的调度器,这样就使得多台机器无法分配start_urls列表中的url。
  • 管道无法给多台机器共享:多台机器爬取到的数据无法通过同一个管道对数据进行统一的数据持久出存储。

2、scrapy-redis组件
scrapy-redis是专门为scrapy框架开发的一套组件。该组件可以解决上述两个问题,让Scrapy实现分布式。

pip install scarpy-redis

3、架构
scrapy爬虫总结_第5张图片

关键:

  1. scrapy改造了python本来的collection.deque(双向队列)形成了自己的Scrapy queue,但是scrapy多个spider不能共享待爬取队列scrapy queue, 即scrapy本身不支持爬虫分布式;scrapy-redis 的解决是把这个scrapy queue换成redis数据库(也是指redis队列),从同一个redis-server存放要爬取的request,便能让多个spider去同一个数据库里读取;
  2. scrapy中引擎将(Spider返回的) 爬取到的Item给Item Pipeline;
    scrapy-redis 的Item Pipeline将爬取到的 Item 存⼊redis的 items queue。
    修改过Item Pipeline可以很方便的根据 key 从 items queue 提取item,从⽽实现 items processes集群。
    所有的爬虫端共享一个数据库,新的请求的时候调度器交给redis比配是否已经爬过,如果没有爬,则交个调度器分配请求。也就是说redis数据库在内存中维护了请求队列、指纹队列和请求到的数据(便于统一存储)

scrapy爬虫总结_第6张图片

你可能感兴趣的:(大数据,爬虫)