Scrapy框架学习之路

安装scrapy框架

pip install scrapy
pip install pypiwin32

快速入门

  1. Spider:根据start_urls列表,自动调用start_requests()方法,想目标网站发送请求,默认是以parse作为回调函数,所以在类中有个parse函数让我们编写
  2. CrawlSpider:根据start_urls列表,发送请求;然后在rules里的规则进行过滤得到有效的连接在发送请求,各自有各自的回调函数进行处理

官方的案例

class AuthorSpider(scrapy.Spider):
    name = 'author'

    start_urls = ['http://quotes.toscrape.com/']

    def parse(self, response):
        author_page_links = response.css('.author + a')   # 获取作者链接

        yield from response.follow_all(author_page_links, self.parse_author) 
        # 解析作者内容

        pagination_links = response.css('li.next a')  
        # 获取下一页连接

        yield from response.follow_all(pagination_links, self.parse)  
        # 请求下一页来继续获取作者链接

    def parse_author(self, response):
        def extract_with_css(query):
            return response.css(query).get(default='').strip()

        yield {
            'name': extract_with_css('h3.author-title::text'),
            'birthdate': extract_with_css('.author-born-date::text'),
            'bio': extract_with_css('.author-description::text'),
        }

具体案例解析

创建项目

scrapy startproject ['项目名称']
cd ['项目名称']  && scrapy genspider itcast 'qiushibaike.com'
# 注意:爬虫名字不可以与项目名字重复,也不能以spider.py命名

##修改配置文件:
1: ROBOTSTXT_OBEY = False # 不准守机器人协议
2: DEFAULT_REQUEST_HEADERS = { # 设置请求头
‘Accept’: ‘text/html,application/xhtml+xml,application/ xml;q=0.9,/;q=0.8’,
‘Accept-Language’: ‘en’,
}

3. 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'  # 设置user—agent

spiders包:

爬虫包,包含所有的小爬虫

1. itcast.py: 要继承自scrapy.Spider类  # 爬虫通过 scrapy genspider [name] [address.com]
2. 实现三个类属性:name = 'itcast' # 爬虫的小蜘蛛的名字  
			  allow_domains = ['qiushibaike.com']  # 防止跑到其他网站,限制在这个域名内
			  start_urls = ['http://www.qiushibaike.com/']  
			  # 在start_requests函数中,而这个函数的默认解析函数就是parse
3. 根据开始的url,--> 引擎 --> 调度器(Schedule) --> 引擎 --
				--> 下载器 --> 引擎 --> spider的parse方法,并将response
				传给此方法

parse函数:

**对网页的解析操作 **

  1. 运行:在根目录新建一个start.py
from scrapy import cmdline

cmdline.execute("scrapy crawl itcast".split())

运行start.py就可以运行指定的spider

xpath语法

1. 层级: / 表示直接子集
		 // 表示子集或者孙子集
2. 属性: @[属性] 
		  例如:"//div[@class='red']" 
		     	[]表示集合,通常用在[]中用来过滤
		     	[last()]表示最后一个元素
3. 函数:contains(), text(), not() ...
		  例如:"//div[not(@class)]"  没有class属性的div
		  		"//div[contains(@class,"red green")]"  属性中包含red green的div
		  		"//div[@id='queue']//span/text()"  # 获取span的值,这里还是一个选择器,可以通过get获取
		  		一个,getall()获取全部,获取for循环中get()
		  		1. 地区:大陆,地区是固定的,但是后面的值可能是其他,定位方式:"//span[./text()='地区国家']/following::text[1]",following表示后面的内容
		  		2. attribute属性之类的都是当前节点的属性::
		  		3. 找某些特定标签然后[n]取不到值时,可能时因为你抓取的多个节点(并排),而不是一个列表(竖排),可以找父亲节点或者爷爷节点,在上一级[n]再进一步获取
		  注意:get()获取的内容是str,getall()是str-list,
		  	例如:text()函数可能会返回多个标签的文本信息,用getall()可以把多个标签的text都获取,然后通过join方法拼接成完整的字符串

yield [Request|item]

获取的数据通过yield的方式返回,可以省内存,引擎根据返回的对象,决定是发请求还是将item返回给pipeline

# itcast.py [spieder]
class ItcastSpider(scrapy.Spider):
    name = 'itcast'  #  爬虫
    allowed_domains = ['qiushibaike.com']  # 允许范围
    start_urls = ['https://www.qiushibaike.com/text/page/1/']

    def parse(self, response):
        # 先获取整个div,然后在里面找文章信息和作者信息
        # duanzidivs = response.xpath("//div[contains(@class,'article block untagged mb15')]")
        divs = response.xpath("//div[@class='col1 old-style-col1']/div")


        # 是selector类型,还可以xpath

        # duanzi = response.xpath("//div[contains(@class,'article block untagged mb15')]/a//span[not(@class)]")
        # author = response.xpath("//div[contains(@class,'article block untagged mb15')]//h2/text()").getall()


        for duanzidiv in divs:
            author = duanzidiv.xpath(".//h2/text()").get().strip()
            content = duanzidiv.xpath(".//div[@class='content']//text()").getall()
            content = ''.join(content).strip()
            # 不加//text,还不是列表就是一个selector对象,
            # 加上//text获取下面的所有标签的文本,所以要用getll(),开发人员老阴B,把内容放在多个标签中
            duanzi = {'author':author,"content":content}

            yield duanzi
            # 段子进过引擎到达pipeline中

数据Json格式保存 - pipelines管道的使用

import json
# pipelines.py
class MyspiderPipeline:

    def __init__(self):
        self.fp = open('duanzi.json','w',encoding='utf-8')

    def open_spider(self,spider):
        # 爬虫一开始就会运行
        print('爬虫开始了')
        pass

    def process_item(self, item, spider):
        # 当spider向这里yield数据的时候会运行
        item_json = json.dumps(item,ensure_ascii=False) # 保证中文的正常保存---※※※※
        self.fp.write(item_json+'\n')
        return item

    def close_spider(self,spider):
        # 爬虫结束就会运行
        self.fp.close()
        print('爬虫结束了')
注意: 要想pipeline起作用,就要把settings中的配置打开
```
ITEM_PIPELINES = {
	'myspider.pipelines.MyspiderPipeline': 300,  # 后面是优先级,小为高
}
```

使用模型类构建数据模型

  1. 定义爬取的数据的模型
class MyspiderItem(scrapy.Item):  # 一般在生成项目的时候会有一个这样的类
	author = scrapy.Field()
	content = scrapy.Field()
  1. 返回内容的时候不用字典,用模型
from myspider.myspider import items 
item = items.MyspiderItem(author=author,content=content)
yield item   # 更加专业

保存数据 - 实际中可能用的方法

(1) 在初始化中声明一个属性为exporter = JsonLineItemExporter()
(2) 在传入值的时候执行self.exporter.export_item(item)
from scrapy.exporters import JsonLinesItemExporter
class MyspiderPipeline:

    def __init__(self):
        self.fp = open('duanzi.json', 'wb')  # 'wb' 打开二进制
        self.exporter = JsonLinesItemExporter(self.fp, ensure_ascii=False, encoding='UTF-8')

    def open_spider(self, spider):
        print('爬虫开始了')

    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item

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

其中process_item()必须将item返回

爬取第二页,第三页的数据

  1. 提取下一页的连接
    (1)使用xpath提取,例如
    next_url = response.xpath(".//ur[@class=‘pagination’]/li[last()]/a/@href").get()

     如果有值:# 对于这个url可能会拼接一下
     	yield scrapy.Request(next_url,self.parse)
    

    (2)为了防止网站崩溃,打开settings中的delay
    python DOWNLOAD_DELAY = 1 # 一秒一回头

  2. 某些网站使用ajax请求渲染第二页的数据,
    (1)找到请求的地址:一般是当前地址&page=\d&sort=\d的格式
    (2)通过正则找到当前的页面和总页面:re.findall(r"var current_page=(.+?);",response.body.decode())[0]
    (3) 构造请求:根据请求的地址和当前的页码+1构造出类似的请求地址yield Request

Request对象

url
callback
method
header
mata:用户中间件和扩展之间的通信,也可以使用这个在请求和下一步的响应之间进行通信
cb_kwargs:是一个字典,其中包含要传递给回调函数的关键字参数。
在多个请求之间通信的方式可以通过将属性加到request对象上面
encoding,dot_filter重复请求的过滤,在验证码的时候可能会多次请求要设置False

Response对象

url:
meta:上一次请求和本次响应传递数据, 注意:传入的值如果是可变类型,可能会要用到深copy,否则多线程会影响
meta = {‘item’:deepcopy(item)}:
只有在大分类层中创建了item,在小分类中共用这个item的时候才会用到深拷贝:这时候建议不共用字典item,用变量代替。
encoding:
text():unicode的字符串
body():bites字符串返回
xpath():xpath选择器
css():css选择器
follow():scrapy.Request(url,callback=self.parse)的简写
follow_all():如果获取的连接是列表就可以使用follow_all()方法,省去for循环

模拟登陆

1. 使用cookie模拟登录

def start_requests(self):
    cookies = "获取的cookie"
    cookie = dict(i.split('=') for i in cookies.split('; '))
    yield scrapy.Request(
        self.start_urls[0],
        callback = self.parse,
        cookies = cookies,
        # 将cookie放到headers中然后传入headers参数是没有用的,浏览器直接找cookie字段,
        # 而不是去header中找cookie
        )

2. FormRequest

**2.1 如果想要在一开始就发送post请求就需要重写Spider类的start_request(self)方法

**2.2对于某些网站登录后只返回json信息,可以在回调函数中在此请求网站的首页,因为是同一个request,所以登录信息会记住。

class RenSpider(scrapy.Spider):
    name = 'ren'
    allowed_domains = ['duboku.co']
    start_urls = ['https://www.duboku.co/']

    def start_requests(self):

        url = 'https://www.duboku.co/user/login.html'
        data = {
            'user_name':'shuaiqi',
            'user_pwd':'pythonspider'
        }
        yield scrapy.FormRequest(url,formdata=data,callback=self.parse_page)

    def parse_page(self, response):
        # 此响应式json信息,在此请求改网站
        url = 'https://www.duboku.co/'
        yield scrapy.Request(url,callback=self.parse_newpage)


    def parse_newpage(self,response):
        with open('main.html','wb') as f:
            f.write(response.body)

3. 对于某些暴露form表单的网站,可以使用FormRequest.from_response():

  1. 这个是自动的,所以准确性没有手动的高,必传参数是response。

验证码登录

使用第三方的图片识别平台,自动识别验证码,模拟登陆豆瓣网

  1. 获取验证码图片,按照base64进行加密,然后作为参数请求识别平台,将获取的内容填写到文本框中

图片批量下载

使用imagepipeline下载

重写imagepipeline实现下载的路径分类

  1. 主要方法:file_path() 在保存图片的时候执行此方法,获取路径
    :get_media_requests() 获取下载的图片链接,通过item对象的image_urls获取

  2. 这两个方法是先执行get_mediarequests() 得到图片信息,然后在保存的时候执行file_path()方法

  3. 我们主要是重写file_path方法,返回自定义的图片的路径

from scrapy.pipelines.images import ImagePipeline
import os
from project import settings
class ImagePipeLine(ImagePipeline):
	def get_media_requests(self,item,info):
		request_objs = super().get_media_requests(item,info)
		# 为request对象添加item属性,也算是一种标识
		for request_obj in request_objs:
			request_obj.item = item
		return request_objs
	def file_path(self,request,response=None,info=None):
		path = super().file_path(request,response,info)
		# 这是应该返回的路径,但是这个多了full
		path = path.replace('full','')
		category = request.item.get('category')
		image_store = settings.IMAGES_STORE
		#  将image存储的路径设置到全局变量中
		category_path = os.path.join(image_store,category)
		image_path = os.path.join(category_path,path)
		return image_path

CrawlSpider 爬取规律性网站

  • 使用CrawlSpider爬取汽车之家高清图片:适合爬取url规则的网站

Rule: 规 则 列 表

LinkExtractor
  1. allow设置匹配的url正则,要能限制在我们想要的url上面,不要跟其他url产生相同正则即可

  2. follow:在爬取页面的时候,如果想要在页面上既要寻找符合正则url就是让他为True,否则False

  3. callback: 想要获取此页面内容,设置解析函数,如果只是想找url,则不指定

    callback = 'parse_page’或者 callback = self.parse_page

    1. 在回调函数中获取类别和该类别图片的urls,交给imagepipeline处理保存
from bmw.items import BmwItem
from scrapy.spiders import CrawlSpider,Rule
from scrapy.linkextractors import LinkExtractor
class Bmw5Spider(CrawlSpider):
    '''
    使用CrawlSpider在详情页面爬取,详情页面
    '''
    name = 'bmw5'
    allowed_domains = ['car.autohome.com.cn']
    start_urls = ['https://car.autohome.com.cn/pic/series/65.html']

    rules = (
        Rule(LinkExtractor(allow=r'https://car\.autohome\.com\.cn/pic/series/65.+'),callback="parse_page",follow=True),
        # 有点,问号的地方要注意转义
    )

    def parse_page(self,response):
        category = response.xpath("//div[@class='uibox']/div/text()").get()
        srcs = response.xpath("//div[contains(@class,'uibox-con')]/ul/li/a/img/@src").getall()
        
        srcs = list(map(lambda x: response.urljoin(x.replace('240x180_0_q95_c42_','')),srcs))
        yield BmwItem(category=category,image_urls=srcs)

反 反爬虫

更换随机的请求头

  1. httpbin.org可以获取你请求的参数信息,所以爬这个网站看你的请求头是否更换
    httpbin.org/user-agent
    httpbin.org/ip
    httpbin.org/headers
  2. 代码:注意使用dont_filter为True,否则因为是重复的请求会被停掉
import scrapy
import json

class HttpbinSpider(scrapy.Spider):
    name = 'httpbin'
    allowed_domains = ['httpbin.org']
    start_urls = ['http://httpbin.org/user-agent']

    def parse(self, response):
        user_agent = json.loads(response.text)['user-agent']
        print('='*30)
        print(user_agent)
        # yield response.follow(self.start_urls[0])
        yield scrapy.Request(self.start_urls[0],self.parse,dont_filter=True)
  1. 中间件,使用random选取随机请求头,赋值给request.headers[‘user-agent’]
import random
class UseAgentDownloadMiddleware():
    USER_AGENTS = [
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36',
        'Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2919.83 Safari/537.36',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A',
        'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25',
        'Mozilla/5.0 (Windows; U; Windows NT 6.1; tr-TR) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27',
    ]
    def process_request(self,request,spider):
        user_agent = random.choices(self.USER_AGENTS)
        request.headers['User-Agent'] = user_agent

特殊的

  • 上面的spider是有很多信息的,包含这个爬虫的配置信息;例如可以使用spider.settings[‘USER_AGENTS’]获取当前爬虫的当前settings配置中的USER_AGENTS字段的值,前提是在settings值有设置

ip代理池

  1. 开放代理可以直接赋值
    request.meta[‘proxy’] = proxy
  2. 独享代理要有认证信息
    user_password = ‘用户名:密码’
    request.meta[‘proxy’] = proxy
    b64_user_password = base64.b64encode(user_password.encode(‘utf-8’))
    request.headers[‘Proxy-Authorization’] = ‘Basic’ + b64_user_password.decode(‘utf-8’)
代理模型
''''
创建代理模型类
'''
fro datetime import datetime,timedelta
class Proxy():
	def __init__(data):  # 将之前的json传过来,得到一个代理实例
		self.ip = data['ip']
		self.port = data['port']
		self.expire_str = data['expire_time']
		self.proxy = f"https:{ip}:{port}"
		self._expire_time = None
		self.blacked = False  # 是否被拉黑
	@property
	def expire_time(self):
		if not self._expire_time:			# 防止多次调用expire_time方法,单例的运用
			# 将expire_str进行解析得到datetime类型的过期时间
			return 'datetime类型的时间'
		return self._expire_time
	def is_expire(self):
		if self.expire_time-datetime.now() < timedelta(second=5):
			return True
		else:
			return False
  • 一般不要每次都去换代理去访问,访问网站返回403或者404的时候再去换代理
import requests
import json
import Proxy  # 下面定义的代理模型类
from twisted.internet.defer import DeferredLock

class IPProxyDownLoadMiddleware():
	PROXYS_URL = '第三方平台请求接口'

	def __init__(self):
		super().__init__()
		self.current = None   # 用这个来保存当前代理,获取代理的时候要为这个属性设置值
		self.lock = DeferredLock()

    def process_reuest(self,request,spider):
        if 'proxy' not in request.meta or self.current.is_expire or self.current.blacked:  # 没有代理或者代理将要过期或拉黑,所以要找个地方保存当前的代理
        	# 请求代理
        	# p = self.get_proxy
        	self.update_proxy()  # 不需要返回值,直接放到current里面了
        	request.meta['proxy'] = self.current.proxy
    def process_response(self,request,response,spider):
    	if response.status != 200 or 'captcha' in response.url:  # 返回的状态码不是200 或者返回的是验证码页面
    		if not self.current.blacked:
    			self.current.blacked = True  # 符合条件就是被拉黑了
    		self.update_proxy
    		# 继续本次请求
    		return request  # 不返回这个请求,此次的数据就没有了,所以要回去request
    	else:
    		return response # 如果是正常的请求,要返回response,否则后面接收不到数据 

    def update_proxy(self):			# 获取代理,同时为他的current设置值
    	with self.lock.acquire():
    		if self.current or self.current.is_expire:
		    	response = requests.get(self.PROXYS_URL)
		    	ret = json.loads(response.text)  # 得到的是一个json格式
		    	# 因为会多次用到代理类,索性就创建一个模型
		    	data = ret['data'][0]  # 创建代理类需要的数据
		    	p = Proxy(data)
		    	self.current = p

加锁的原因

可能会多次调用update_proxy()导致不仅费代理,而且第三方的供应商也会告诉你请求频率过高,请x秒后在尝试,所以这个函数应该加锁
加锁之后,在某个协程成功请求后,要判断:若当前代理存在并且没有过期,就不再执行了

简书网 全站爬取

  • 抓取js渲染的数据,并将数据保存到mysql数据库,动态网站爬取的思路。
  1. 普通的请求不会渲染数据,所以在中间件中截获这个请求交给selenium,然后通过webdriver获取页面信息,
    在将此信息包装成httpresponse返回,此response带有渲染的数据。

  2. 在spider中定义爬取的网站,和爬取的规则,以及处理响应的回调函数,在回调函数中通过xpath语法
    获取内容,将数据yield。
    为了保证响应中有动态加载的数据,所以在中间件中:定义一个selenium的webdriver,
    在请求发出之前(process_request)通过这个webdriver请求加载的地址,有些数据需要点击’更多’,就需要获取元素触发click事件;有些数据
    还会在滚动条下拉到最后的时候才会出现,可以通过webdriver触发js事件,然后得到的响应就是最普通的数据
    使用HtmlResonse(url,body,request) 创建一个response对象并返回。

# jianshu_spider.py
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from jianshu_spider.items import Article
import re
class JianshuSpider(CrawlSpider):
    name = 'jianshu'
    allowed_domains = ['jianshu.com']
    start_urls = ['https://www.jianshu.com/']

    rules = (
        Rule(LinkExtractor(allow=r'.*/p/[0-9a-z]{12}.*'), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        title = response.xpath("//h1[@class='_1RuRku']/text()").get()
        avatar = response.xpath("//img[@class='_13D2Eh']/@src").get()
        author = response.xpath("//div[@class='rEsl9f']//a[@class='_1OhGeD']").get()
        content = response.xpath("//article").get()
        article_id = response.url.split('?')[0].split('/')[-1]
        origin_url = response.url.split('?')[0]
        pub_time = response.xpath("//div[@class='s-dsoj']//time/@datetime").get()
        word_count = response.xpath("//div[@class='s-dsoj']/span[2]/text()").get()
        read_count = response.xpath("//div[@class='s-dsoj']/span[3]/text()").get()
        like_count = response.xpath("//span[@class='_1LOh_5']").get()
        subject = response.xpath("//div[@class='_2Nttfz']/a/span/text()").getall()
        subject = ','.join(subject)
        yield Article(title=title,avatar=avatar,author=author,content=content,
        article_id=article_id,origin_url=origin_url,pub_time=pub_time,read_count=read_count,word_count=word_count,like_count=like_count,subject=subject
        )

# items.py
class Article(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field()
    content = scrapy.Field()
    article_id = scrapy.Field()
    origin_url = scrapy.Field()
    author = scrapy.Field()
    avatar = scrapy.Field()
    pub_time = scrapy.Field()
    read_count = scrapy.Field()
    like_count = scrapy.Field()
    word_count = scrapy.Field()
    subject = scrapy.Field()

# middlewares.py
from selenium import webdriver
import time
from scrapy.http.response.html import HtmlResponse
class SeleniumDownLoadMiddleware():
    def __init__(self):
        self.driver = webdriver.Chrome(executable_path=r'D:\\WorkPlace\\chromedriver.exe')
    def process_request(self,request,spider):
        # 在这里可以拿到请求,用selenim处理这个请求,得到响应就返回
        # 将请求拦截,并给scrapy一个selenium的响应  妙
        self.driver.get(request.url)
        time.sleep(1)
        js="document.documentElement.scrollTop=999999"  
        self.driver.execute_script(js)
        time.sleep(0.2)
        self.driver.execute_script(js)
        try:
            while True:
                bt = self.driver.find_element_by_xpath("//div[@class='H7E3vT']")
                bt.click()
                time.sleep(0.5)
                if not bt:
                    break
        except:
            pass
        source = self.driver.page_source
        response = HtmlResponse(self.driver.current_url,encoding='utf-8',body=source,request=request)    
        return response


# pipelines.py
import pymysql
class JianshuSpiderPipeline:
    '''
    将数据存储到mysql
    '''
    def __init__(self):
        params = {
            'host':'127.0.0.1',
            'port':3306,
            'user':'root',
            'password':'root',
            'database':'jianshu',
            'charset':'utf8',

        }
        self.conn = pymysql.connect(**params)
        self.cursor = self.conn.cursor()
        self._sql = None
    def process_item(self, item, spider):
        self.cursor.execute(self.sql,(item["title"],item["content"],item["author"],item["avatar"],item["pub_time"],item["origin_url"],item["article_id"]))
        self.conn.commit()
        return item

    @property
    def sql(self):
        if not self._sql:
            self._sql ='''INSERT INTO ARTICLE (id,title,content,author,avatar,pub_time,origin_url,article_id) VALUES (null,%s,%s,%s,%s,%s,%s,%s)'''
            return self._sql
        return self._sql

简书全站爬取的执行流程

  1. 首先执行start_requests方法,开始请求,这一部分不用注意中间件,然后将响应中的url链接Rule获取,然后
    发送请求
  2. 在中间件中进行截断,process_request()方法中通过selenium请求获取响应,在此方法中返回,得到的数据是js渲染后的数据
  3. 在Rule的parse_item方法中,处理响应,此时的的数据是通过js渲染的,所以不会有动态加载的问题,
  4. 将数据yield返回给pipeline,在pipeline中,;连接数据库,并执行insert语句,完成数据库的添加

分布式爬虫:

专注于爬虫和模型类的构建,保存的工作交给redis,根据情况处理响应信息。

redis的作用

  1. 登录会话存储
  2. 计数器,排名,经常访问的
  3. 消息队列
  4. 在线人数
  5. 做网站的缓存
  6. 好友关系
  7. 订阅发布

将项目改为分布式爬虫

fang.com

源码:https://github.com/shangdidaren/spider

  1. 安装scrapy-redis
  2. 将Spider改为scrapy_redis.spiders.RedisSpider
    或者将CrawlSpider改为scrapy_redis.spiders.RedisCrawlSpider
  3. 将start_urls删除,把url放在redis上;添加一个redis_key:redis_key = ‘fang:start_urls’
  4. 配置文件中:调度器和去重和管道改为Scrapy_Redis的组件
# 调度器
SCHEDULER = 'scrapy_redis.scheduler.Scheduler'
# 链接去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 将数据保存到redis中
ITEM_PIPELINES = {
	'scrapy_redis.pipelines.RedisPipeline':300,
	# 
}
# 在redis保持队列不被清空,可以暂停和恢复
SCHEDULER_PERSIST = True

# 设置redis的信息
# Redis独一台服务器,或者是做集群保证高可用
REDIS_HOST = 'Redis服务器的ip'
REDIS_PORT = 6379
  1. 将项目放到爬虫服务器上,安装相应的依赖包,运行scrapy runspider name.py ;
    此时爬虫服务器应处于等待状态
  2. 连接redis服务器,使用lpush命令插入start_urls,键为爬虫中的redis_key,值为目标网站的地址。

scrapy_redis源码分析

保存数据pipelines.py
  1. 在process_item()方法中return deferToThread(self._process_item,item,spider)
    • 这个方法大致就是使用调用线程来处理
  2. _process_item()方法是前者的具体实现多线程实现
key = self.item_key(item,spider)  # 获取key
data = self.serialize(item)  # 将itme序列化
self.server.rpush(key,data)  # 存储在redis中
数据过滤dupefilter.py
  1. request-fingerprint(request,include_headers=None)
    (1). url不是唯一的标识:www.example.com/?id=1&car=222
    www.example.com/?cat=222&id=1
    这两个url应该是同一个地址,所以会对url做排序处理
    (2). 如果我们有两个用户,我们已经有了唯一标识指纹,所以请求的cookie应该也是一样的,
    带了headers可能会导致不一样,所以就默认不带cookie。
    (3).生成指纹是hashlib.sha1()方法
fp = hashlib.sha1()
fp.update(to_bytes('对象'))
fp.update(to_bytes(cannonicalize_url(response.url)))
fp.update(request.body or b'')

(4).将指纹插入到redis,使用的是redis.sadd(key,fp)
如果数据存在返回0,即request_seen
否则返回1,request没有看过

调度器schedule.py
  1. close()方法中判断,如果persist继续下载的开关为False,执行flush
    flush()会将所有的指纹删除,下次再执行的时候就是全新的
  2. enqueue_request():
    dont_filter为True:一定会入队
    url在start_urls中会入队:因为在构造start_reqeusts时,dont_filer默认为True
    dont_filter为False:全新url:入队
    看过的url:不入队

简书网 全站爬取

  • 通过获取json数据,构造合法请求获取数据

打开F12查看异步请求:

在动态请求中有以下几种:

  1. audio:没用
  1. book:没用
  1. current:没用
  1. includeed_collections?page=1&coount=10:返回的是收入的专题
  • https://www.jianshu.com/shakespeare/notes/69052585/included_collections?count=31notes后面是文章的id,所以要获取文章的id
{

  *"id"*: 1720632,

  *"slug"*: "3771548474a2",

  *"title"*: "其他零散知识点",

  *"avatar"*: "https://upload.jianshu.io/collections/images/1720632/crop1550280817442.jpg",

  *"owner_name"*: "鹏_0d3c"

},

{

  *"id"*: 1864630,

  *"slug"*: "ae71e1311508",

  *"title"*: "工具集合",

  *"avatar"*: "https://upload.jianshu.io/collections/images/1864630/1584356274.jpg",

  *"owner_name"*: "Jackzhou3927"

}
  • 返回的是json数据,取title得到当前文章所在的专题

  • 取slug拼接jianshu.com/c/{slug}得到专题列表,使用Link Extractor提取文章链接。(特殊的,这个列表也也是动态加载,只有当进度条翻到下面才会加载,除了selenium暂时没发现怎么反 反爬),不过可以拿到10条足够了

  1. mark_viewed:没用:提升文章的阅读数目
  1. reconmmendations:推荐的文章的信息:
{

  *"id"*: 68512684,

  *"slug"*: "dc09f86ca4ba",

  *"list_image_url"*: "https://upload-images.jianshu.io/upload_images/5275417-d267dca5c31e05e5",

  *"title"*: "为什么说Redis是单线程的以及Redis为什么这么快!Redis、面试、缓存、雪崩、分布式锁实现一篇文章搞定!",

  *"views_count"*: 1338

},

{

  *"id"*: 72990466,

  *"slug"*: "384ca5efba36",

  *"list_image_url"*: "https://upload-images.jianshu.io/upload_images/19895418-012de5aa9699764d",

  *"title"*: "Spring Boot “内存泄漏”?看看美团大牛是如何排查的",

  *"views_count"*: 967

}

- id应该是暂时没用的,slug拼接文章的详情页面jianshu.com/p/{slug}得到详情页面又可以获取列出的信息

  1. reward_section:没用
  1. user_notes:用户的热门文章信息,类似于专题
{

  *"id"*: 73043642,

  *"slug"*: "e5cb75bacb8a",

  *"title"*: "连RabbitMQ的5种核心消息模式都不懂,也敢说自己会用消息队列!",

  *"view_count"*: 71,

  *"user"*: {*"id"*: 1939592,*"nickname"*: "梦想de星空",*"slug"*: "9bdcaae6d6b7",*"avatar"*: "https://upload.jianshu.io/users/upload_avatars/1939592/c9a25006-52d5-4cb5-8812-ca4c0b8d2bdb.png"

  }

}
  • slug拼接文章的详情页面jianshu.com/p/{slug}

  • 用户的信息中又可以拼接用户的个人中心,得到文章的列表页,但是文章的列表也是动态加载的,所以只能获取前几条, 重复会很多

  • 动态加载是www.jianshu.com/u/9bdcaae6d6b7?order_by=added_at&page=10page无线大没用,最多加载20条数据,然后就去请求https://www.jianshu.com/users/9bdcaae6d6b7/timeline?max_id=639979257&page=2,和专题一样,没想到怎么反 反爬

  1. response:里面有详情页所有的数据,包括用户的部分数据,在id为__NEXT_DATA__的 script 标签中,json格式数据提取出来。可以获取信息。

思路二源码:未解决

# -*- coding: utf-8 -*-
import scrapy
import re
import json
import time
from copy import deepcopy
from scrapy.linkextractors import LinkExtractor
from collections import Iterable
class JianshuSpider(scrapy.Spider):
    name = 'jianshu'
    allowed_domains = ['jianshu.com']
    # start_urls = ['https://www.jianshu.com/p/2c228cccca31']
    start_urls = ['https://www.jianshu.com']
    def parse_item(self, response):
        item = {}
        source_data = response.xpath("//script[@id='__NEXT_DATA__']//text()").getall()

        data = (''.join(source_data))
        data = json.loads(data)

        # with open('save.json', 'w',encoding='utf-8') as f:
        #     f.write(''.join(data))
        try:
            data = data['props']['initialState']['note']['data']

            item['title'] = data['public_title']
            item['slug'] = data['slug']
            item['views_count'] = data['views_count']
            # item['free_content'] = data['free_content']
            item['likes_count'] = data['likes_count']
            item['wordage'] = data['wordage']
            item['avatar'] = data['user']['avatar']
            last_updated_at = int(data['last_updated_at'])
            item['last_updated_at'] = t1 = time.strftime(r'%Y-%m-%d %H:%M:%S',time.localtime(last_updated_at))
            item['user_name'] = data['user']['nickname']

            id = data['id'] # 用于构建专题url

            # 被什么专题收入:https://www.jianshu.com/shakespeare/notes/{文章id}/included_collections?page=1&count=7
            subject_json = f"https://www.jianshu.com/shakespeare/notes/{id}/included_collections?page=1&count=50"
            
            yield scrapy.Request(  # 专题
                url = subject_json,
                callback=self.parse_sub,
                meta={'item':deepcopy(item)},
                )

            # # 推荐:https://www.jianshu.com/shakespeare/notes/{文章slug}/recommendations
            reconmmend_json = 'https://www.jianshu.com/shakespeare/notes/' + item['slug'] + '/recommendations'
            yield scrapy.Request(  # 推荐文章
                url=reconmmend_json,
                callback=self.parse_recon,
                )  # 这里想到将id,slug,title,views_count传过来,但是没必要。
            
            # 自己的热门文章:https://www.jianshu.com/shakespeare/notes/{文章id}/user_notes
            hot_json = f'https://www.jianshu.com/shakespeare/notes/{id}/user_notes'
            yield scrapy.Request(  # HOT文章
                url = hot_json,
                callback=self.parse_hot
            )
        except:
            print('文章审核!!!') # 该文章在审核中

    def parse_sub(self,response):
        '''专题解析函数:取出json数据,完善item, 构建到达专题列表页的请求'''
        # with open('sub.json','w',encoding='utf-8')  as fp:
        #     fp.write(response.body.decode())
        item = response.meta['item']
        data = json.loads(response.body.decode())

        item['subjects'] = ' '.join([collection['title'] for collection in data['collections']])
        yield item  # 得到专题信息后,item就完成了
        print(item)
        pre = 'https://www.jianshu.com/c/'
        sub_url_list = [pre+collection['slug'] for collection in data['collections']]
        
        if len(sub_url_list) == 1:
            yield scrapy.Request(
                url = sub_url_list[0],
                callback =self.parse_subject_detail
            )
        elif len(sub_url_list) == 0:               # 可能是空列表,或者长列表
            pass

        for url in sub_url_list:
            yield scrapy.Request(
                    url=url,
                    callback=self.parse_subject_detail
                )
        

        # 在此访问主页就是下一页的请求:除非它一直推送相同的文章
        yield scrapy.Request(url='https://www.jianshu.com',callback=self.parse,dont_filter=True)
        print('True')

    def parse_recon(self,response):
        '''
            取出 推荐 列表数据,构建请求,这是文章详情页了,所以交给parse
        '''
        pre = 'https://www.jianshu.com/p/'
        data_list = json.loads(response.body.decode())  # 推荐的list,内含字典
        article_url_list = [pre+data['slug'] for data in data_list]
        if len(article_url_list) == 1:
            yield scrapy.Request(url=article_url_list,callback=self.parse)
        elif len(article_url_list) == 0:
            # do nothing
            pass
        for url in article_url_list:
            yield scrapy.Request(
                url=url,
                callback=self.parse,
            )
            

    def parse_hot(self,response):
        '''
            自己的热门文章,取出列表,构建请求
        '''
        pre = 'https://www.jianshu.com/p/'
        data_list = json.loads(response.body.decode())  # 内含字典
        article_url_list = [pre+data['slug'] for data in data_list]
        if len(article_url_list) > 1: # 一般都是3个,这步判断没啥意义
            if isinstance(article_url_list,Iterable):
                for url in article_url_list:
                    yield scrapy.Request(
                            url=url,
                            callback=self.parse,
                            )
                
        elif len(article_url_list) == 1:
            yield scrapy.Request(url=article_url_list[0])

    def parse(self,response):   # 解析列表页 包括主页和专题页
        extractor = LinkExtractor(restrict_xpaths=('//ul/li/div/a'))
        link_list = extractor.extract_links(response)

        yield from response.follow_all(urls=link_list,callback=self.parse_item)
    
        
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值