聚焦式爬虫

[toc]

聚焦爬虫

  • 概述: 爬取页面中指定的页面内容

  • 编码流程

    1. 指定url

    2. 发起请求

    3. 获取响应数据

    4. 数据解析

    5. 持久化存储

1. 数据解析

1.1 数据解析分类

  • 正则

  • bs4

  • xpath(主要)

1.2 数据解析原理

  • 概述:

    • 解析的局部文本内容都会在标签之间或者标签对应的属性中进行存储

  • 方法:

    1. 进行指定标签的定位

    2. 标签或者标签对应的属性中存储的数据值进行提取(解析)

2. 正则解析

2.1 正则表达式

  • 正则:用来匹配字符串的一门表达式语言

2.2 常用正则表达式

正则

2.2 实战之暴走漫画

import re
import requests
import os
if __name__=='__main__':
    #创建一个文件夹,保存所有图片
    if not os.path.exists('./暴走'):
        os.mkdir('./暴走')
    url='http://admin2.baozoumanhua.com/'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Edg/97.0.1072.62'
    }
    #获取全部数据
    page_text=requests.get(url=url,headers=headers).text
    #使用正则对图片进行解析
    ex = '<img.*? src="(.*?)" .*?/>'
    img_src_list=re.findall(ex,page_text,re.S)
    for i in img_src_list:
    # content方法获取二进制数据类型
        img_data=requests.get(url=i,headers=headers).content
        #生成图片名称
        img_name=i.split('/')[-1]
        imgPath='./暴走/'+img_name
        with open(imgPath,'wb')as fp:
            fp.write(img_data)
            print(img_name+'下载成功')

bs4解析

  • 只能在python用

  • bs4数据解析原理

    • 实例化一个BeautifulSoup对象,并且将页面源码数据加载到该对象中

    • 通过调用BeautifulSoup对象中相关的属性或者方法进行标签定位和数据提取

  • 环境安装:

    • pip install bs4

    • pip install lxml

  • 如何实例化BeautifulSoup对象

    • from bs4 import BeautifulSoup

    • 对象实例化

      1. 将本地的HTML文档中的数据加载到该对象中

      from bs4 import BeautifulSoup
      if __name__=='__main__':
        #将本地的html文档中的数据加载到该对象中
        fp=open('D:\\html\\登录.html','r',encoding='utf-8')
        #第二个参数为BeautifulSoup对象使用lxml解析器进行解析
        soup=BeautifulSoup(fp,'lxml')
        print(soup)
      1. 将互联网上获取的页面源码加载到该对象中

        • 与第一种差不多,只要先把爬取到的全部数据存到本地文档在解析

  • 提供的用于数据解析解析的属性和方法:

    • soup.tagName:返回html中第一次出现的tagName标签

    • soup.find('tagName'):返回第一次出现的tagName对应的标签

      • 属性定位:

        • soup.find('tagName',class_/id/attr='')

    • soup.find_all('tagName'):返回所有出现的tagName对应的标签(列表)

    • soup.select("某种选择器"):返回一个列表

    • soup.select("某种选择器>某种选择器 某种选择器"):>表示一个层级,空格表示多个层级

    • 获取标签中的文本数据

      • soup.a.text/string/get_text()

        • soup.a.text/get_text():获取一个标签里的所有文本内容

        • soup.a.string:获取一个标签直系的文本内容

      • 获取标签中的属性值

        • soup.a['href']

xpath解析

  • 最常用切最便捷高效的一种方式

  • xpath解析原理:

    1. 实例化一个etree的对象,且需要将被解析的页面源码数据加载到该对象中

    2. 调用etree对象中的xpath方法结合xpath表达式实现标签的定位和内容的捕获

  • 环境安装

    • pip install lxml

  • 如何实例化一个etree对象

    1. 导包:from lxml import etree

    2. 将本地的HTML文档中的数据加载到该对象中:

      • etree.parse(filePath,etree.HTMLParser())

      • 解析本地本件第二个参数最好加上,不然可能报错

    3. 可以将互联网上获取的源码数据加载到该对象中

      • etree.HTML('page_text')

  • xpath('xpath表达式')

    • 标签定位:

      • xpath表达式只能用层级定位定位标签

      # 标签的定位
      #最前面的/表示从根节点开始
      # 一个标签返回一个element对象
      r=tree.xpath('/html/head/title')
    • 多层级定位

      #一个//表示一个多层级,也可以表示从任意位置开始定位
      r=tree.xpath('/html//title')
    • 精准定位

      # 精准定位class为song的divs
      # 选择器写法[@attrName='attrValue']
      r = tree.xpath('//div[@class="ong"]')
    • 索引定位

      # 索引定位,返回第几个元素,且索引从1开始
      r = tree.xpath('//div[@class="song"]/p[3]')
    • 取直系文本

      # 取文本,text()返回的是一个列表,取得是直系内容
      # /text()
        r = tree.xpath('//div[@class="song"]//li[5]/a/text()')
    • 取非直系文本

      #获取标签中非直系的文本内容
      r = tree.xpath('//li[7]//text()')
    • 取属性值

      #取属性值
      r = tree.xpath('//div[@class="song"]/img/@src')[0]
    • 以上所有xpath方法返回的都是列表

xpath实战之爬取58二手房

import requests
from lxml import etree
if __name__=='__main__':
    # 获取页面源码数据
    url='https://bj.58.com/ershoufang/'
    # UA伪装
    head={
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36 Edg/97.0.1072.69'
    }
    page_text=requests.get(url=url,headers=head).text
    #数据解析
    tree =etree.HTML(page_text)
    list=tree.xpath('//div[@class="property-content-detail"]')
    fp=open('58.txt','w',encoding='utf-8')
    for h3 in list:
        #./表示定位到的div标签
        title=h3.xpath('.//text()')[0]
        print(title)
        fp.write(title+'\n')
    fp.close()

xpath实战之4k图片解析下载

import requests
from lxml import etree
import os
if __name__=='__main__':
    # 获取页面源码数据
    url='https://pic.netbian.com/4kmeinv/'
    # UA伪装
    head = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36 Edg/97.0.1072.69'
    }
    response = requests.get(url=url, headers=head)
    #手动给响应数据设置编码
    # response.encoding='gbk'
    page_text=response.text
    tree=etree.HTML(page_text)
    li_list=tree.xpath('//div[@class="slist"]/ul/li')
    if not os.path.exists('./picLibs'):
        os.mkdir('./picLibs')
    for li in li_list:
        img_src='https://pic.netbian.com'+li.xpath('./a/img/@src')[0]
        img_name=li.xpath('./a/img/@alt')[0]+'.jpg'
        # 通用解决中文乱码的解决方案
        img_name=img_name.encode('iso-8859-1').decode('gbk')
        # print(img_name,img_src)
        img_data=requests.get(url=img_src,headers=head).content
        img_path='picLibs/'+img_name
        with open(img_path,'wb')as fp:
            fp.write(img_data)
            print(img_name+'下载完成!!')

xpath实战之全国城市名称爬取

import requests
from lxml import etree
import os
if __name__=='__main__':
    # 获取页面源码数据
    url='https://www.aqistudy.cn/historydata/'
    # UA伪装
    head = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36 Edg/97.0.1072.69'
    }
    page_text = requests.get(url=url, headers=head).text
    tree = etree.HTML(page_text)
    host_li_list=tree.xpath('//div[@class="bottom"]/ul/li')
    all_city_names=[]
    # 也可以一次获取全部
    # tree.xpath('//div[@class="bottom"]/ul/li/a | //div[@class="bottom"]/ul/div[2]/li/a')
    #解析热门城市名称
    for li in host_li_list:
        host_city_name=li.xpath('./a/text()')[0]
        all_city_names.append(host_city_name)
    city_names_list=tree.xpath('//div[@class="bottom"]/ul/div[2]/li')
    #解析全部城市名称
    for li in city_names_list:
        city_name=li.xpath('./a/text()')[0]
        all_city_names.append(city_name)
    print(all_city_names,len(all_city_names))

xpath实战之图片爬取

import requests
import os
from lxml import etree
if __name__=='__main__':
    lxm=0
    url = 'https://www.vilipix.com/ranking'
    head = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36 Edg/97.0.1072.69'
    }
    page_text=requests.get(url=url,headers=head).text
    tree = etree.HTML(page_text)
    img_list=tree.xpath('//div[@class="title"]/a')
    if not os.path.exists('./p站'):
        os.mkdir('./p站')
    for i in img_list:
        img_url='https://www.vilipix.com'+i.xpath('./@href')[0]
        img_data = requests.get(url=img_url, headers=head).text
        ptree=etree.HTML(img_data)
        p_list=ptree.xpath('//a[@href="javascript: void(0)"]/img')
        for img in p_list:
            lxm+=1
            img_p=img.xpath('./@src')[0]
            pp=requests.get(url=img_p,headers=head).content
            img_name=img.xpath('./@alt')[0]+str(lxm)+'.jpg'
            img_path='p站/'+img_name
            img_path=img_path.replace("?","L")
            with open(img_path,'wb') as fp:
                fp.write(pp)
                print(img_name+'下载完成!!')
    print("over!!!!!")

网站验证码

  • 反爬机制:验证码。识别验证码图片中的数据,用于模拟登录操作

  • 识别验证码的操作:

    • 人工肉眼识别(不推荐)

    • 第三方自动识别(推荐)

      • 超级鹰:http://www.chaojiying.com/demo.html

超级鹰使用

模拟登录

  • 爬取某些用户的相关信息

  • 点击登录后会发起一个post请求

  • post请求会携带相关信息,如果参数正确就能进行模拟登录

  • 获取的页面请求返回的对象status_code属性值如果是200的话就模拟登录成功

  • http/https特性:无状态

  • 没有得到基于第二次请求的个人页面,服务器并不知道该请求是基于登录状态下的请求

  • 需要cookie存储状态

  • cookie:用来让服务器端记录客户端的相关状态

  • 不推荐使用手动cookie:通过抓包工具获取cookie值,在封装到headers中

  • 自动cookie:

    • cookie值哪里来?

      • 模拟登录post请求后,由服务器创建

    • session会话对象:

      • 可以进行请求的发送

      • 如果请求过程中产生了cookie,则该cookie会被自动存储/携带在该对象中

      • 使用session对象进行模拟登录post请求的发送(cookie就会被存储在session中)

      • session=requests.Session()

      • session对象对个人页面对应的get请求进行发送

      # 使用session对象对全国kfc餐厅进行爬取
      import json
      import requests
      if __name__=='__main__':
          url='http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?'
          key=input('请输入城市:')
          par={
              'op':'keyword',
              'cname':'',
              'pid':'',
              'keyword': key,
              'pageIndex': '1',
              'pageSize': '100'
          }
          headers = {
              'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Edg/97.0.1072.62'
          }
          ss=requests.session()
          response = ss.get(url=url,params=par,headers=headers)
      
          print(type(response))
          page_text=response.text
          # 将字符串转换为json类型
          page_json=json.loads(page_text)
          fp=open('./kfc.json','w',encoding='utf-8')
          json.dump(page_json,fp=fp,ensure_ascii=False)
          print('over!!!')

代理理论

  • 反爬封ip:同一ip访问过多会封ip

  • 代理:代理服务器

  • 代理的作用:

    • 可以突破自身ip访问的限制

    • 可以隐藏自身真实ip

  • 相关代理网站:

    • 快代理

    • 西祠代理

    • www.goubanjia.com

  • 代理ip类型

    • http:只能应用于http协议的url

    • https:只能应用于https协议的url

  • 代理ip的匿名度:

    • 透明:服务器知道该次请求使用了代理,也知道请求对应的真实ip

    • 匿名:知道使用了代理,不知道真实ip

    • 高匿:不知道使用了代理,更不知道真实ip

  • 实现:

    • 在请求中的proxies参数中输入代理ip就行

异步爬虫

  • 目的:在爬虫中使用异步实现

  • 实现方式:

    1. 多进程,多线程(不建议)

      • 好处:可以为相关阻塞单独开启线程或者进程,阻塞操作就可以异步执行

      • 弊端:无法无限制开启多线程或者多进程

    2. 进程池:(适当使用)

      • 好处:可以减低系统对进程创建和销毁的一个频率,从而很好的减低系统的开销

      • 弊端:进程池线程或进程的数量是有限的

      • 实例:

      import time
      from multiprocessing.dummy import Pool
      start_time=time.time()
      def get_page(str):
          print('正在下载:',str)
          time.sleep(2)
          print('下载成功',str)
      name_list=['aa','bb','cc']
      #实例化线程池对象
      pool=Pool(4)
      #将每一个name_list里面的数据传递给get_page处理
      pool.map(get_page,name_list)
      end_time=time.time()
      print("%d second"%(end_time-start_time))
    3. 单线程+异步协程(推荐)

    4. event_loop:事侏循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足某些条件的时候,函数就会被循环执行。

    5. coroutine:协程对象,我们可以将协程对象注册到事件循环中,它会被事件循环调用。我们可以使用async 关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象。

    6. task:任务,它是对协程对象的进一步封装,包含了任务的各个状态。

    7. future:代表将来执行或还没有执行的任务,实际上和task没有本质区别.

    8. async定义一个协程.

    9. await用来挂起阻塞方法的执行。

    10. 协程用法:

    import asyncio
    async def request(url):
        print('正在请求:',url)
        print('请求成功:',url)
        return url
    #asunc修饰的函数,调用之后返回一个协程对象
    c=request('www.baidu.com')
    #创建一个事件循环对象
    # loop=asyncio.get_event_loop()
    # #将协程对象注册到loop中,然后启动loop
    # loop.run_until_complete(c)
    #task使用
    loop=asyncio.get_event_loop()
    #基于loop创建了一个task对象
    # task=loop.create_task(c)
    # print(task)
    # loop.run_until_complete(task)
    #future使用
    # loop=asyncio.get_event_loop()
    # task=asyncio.ensure_future(c)
    # print(task)
    # loop.run_until_complete(task)
    # print(task)
    def callback_func(task):
        #打印协程函数的返回值
        print(task.result())
    #绑定回调
    loop=asyncio.get_event_loop()
    task=asyncio.ensure_future(c)
    # 将回调函数绑定到任务对象中
    task.add_done_callback(callback_func)
    loop.run_until_complete(task)
    
    • 协程实例:

    import asyncio
    import time
    
    async  def request(url):
        print('正在下载',url)
        #在异步协程中如果出现了同步模块相关的代码,那么久无法实现异步
        # time.sleep(2)
        #当在asyncio中欲打破阻塞操作时必须进行手动挂起
        await asyncio.sleep(2)
        print('下载完毕',url)
    start=time.time()
    urls=[
        'www.baidu.com',
        'www.sogou.com',
        'www.goubanjia.com'
    ]
    stasks=[]
    for url in urls:
        c=request(url)
        task=asyncio.ensure_future(c)
        stasks.append(task)
    loop=asyncio.get_event_loop()
    #多任务列表要封装wait()
    loop.run_until_complete(asyncio.wait(stasks))
    print(time.time()-start)
    • 协程aiohttp模块

      • 模块安装:pip install aiohttp

      • 实现异步爬虫

      import requests
      import asyncio
      import time
      import aiohttp
      # 使用该模块的ClientSession
      start=time.time()
      urls=[
          'http://baidu.com',
          'http://sogou.com',
          'http://baidu.com',
      ]
      async def get_page(url):
          print('正在下载',url)
          # requests模块基于同步
          # aiohttpj基于异步网络请求
          # response=requests.get(url=url)
          async with aiohttp.ClientSession() as session:
              # async  with await session.post(url) as response:post请求
              # UA伪装方式和请求参数处理以及代理ip和requests库一样,但ip赋值为字符串,不再是字典
            async  with await session.get(url) as response:
                # text()返回字符串的响应数据
                # read()返回的是二进制的响应数据
                # json()返回的就是json类型的数据
                # 注意:在获取响应数据之前一定要使用await进行手动挂起
                page_text=await response.text()
          print('下载完毕', page_text)
      tasks=[]
      for url in urls:
          c=get_page(url)
          task=asyncio.ensure_future(c)
          tasks.append(task)
      loop=asyncio.get_event_loop()
      loop.run_until_complete(asyncio.wait(tasks))
      print(time.time()-start)
      

selenium模块

  • 为什么需要使用selenium模块?

    • selenium可以更快捷的获取网站中动态加载的数据

    • 便捷的实现模拟登录

  • 什么是selenium模块?

    • 基于浏览器自动化的一个模块

  • selenium使用

    • 环境安装:pip install selenium

    • 下载一个浏览器的驱动程序

      • https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/(edge浏览器)

      • 可以自己查找对应浏览器的驱动下载网站

    • 实例化一个selenium对象

    • 编写浏览器自动化代码

    • 初始用

    from selenium import webdriver
    from lxml import etree
    from time import sleep
    # 实例化一个浏览器对象(传入浏览器的驱动器)
    # 直接写路径也行,也可以不用写
    # bro=webdriver.Edge('msedgedriver.exe')
    bro=webdriver.Edge('msedgedriver.exe')
    # 让浏览器发起一个指定url对应请求
    bro.get('http://scxk.nmpa.gov.cn:81/xk/')
    # 获取浏览器当前页面的页面源码数据
    page_text=bro.page_source
    tree=etree.HTML(page_text)
    li_list=tree.xpath('//ul[@id="gzlist"]/li')
    for li in li_list:
        name=li.xpath('./dl/@title')[0]
        print(name)
    # 睡眠5秒后关闭浏览器
    sleep(5)
    bro.quit()
    • 相关基础语法

      • 发起请求:get()

        • 在selenium对象发送请求时的url必须带上超文本协议前缀

      • 标签定位:find系列方法

      • 标签交互:send_keys('xxx')

      • 执行js程序:excut_script('就js代码')

      • 前进、后退:forward()、back()

      • 关闭浏览器:quit()

    • 实战之操纵淘宝

    from selenium import webdriver
    from time import sleep
    bro=webdriver.Edge()
    bro.get('https://www.taobao.com/')
    # 标签定位
    serch_input=bro.find_element_by_id('q')
    # 标签交互,搜索框输入内容
    serch_input.send_keys('Iphone')
    # 执行一组js程序
    # 向下滚动
    bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
    btn=bro.find_element_by_css_selector('.btn-search')
    # 点击搜索按钮
    btn.click()
    bro.get('https://www.baidu.com')
    # 后退
    sleep(2)
    bro.back()
    # 前进
    sleep(2)
    bro.forward()
    sleep(5)
    bro.quit()
  • selenium处理iframe以及动作链

    • 如果定位的标签在iframe中则必须要使用switch_to.frame('id')

    • 动作链(拖动):from selenium.webdriver import ActionChains导入动作链包

      • 实例化一个动作链对象

        • action=ActionChains(bro)

      • 触发长按且点击操作

        • action.click_and_hold(div)

      • 进行拖动

        • action.move_by_offset(17,10)

      • 动作立即执行

        • perform()

      • 释放动作链

        • action.release()

    • 实操:

    from selenium import webdriver
    from time import sleep
    # 导入动作链的类
    from selenium.webdriver import ActionChains
    bro=webdriver.Edge()
    bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
    # 如果要定位的标签在iframe标签中,则进行操作
    bro.switch_to.frame('iframeResult')# 切换定位的作用域
    div=bro.find_element_by_id('draggable')
    # 动作链
    action=ActionChains(bro)
    # 点击长安指定的标签
    action.click_and_hold(div)
    for i in range(5):
        # perform()立即 执行动作链操作
        action.move_by_offset(17,10).perform()
        sleep(0.2)
    # 释放动作链
    action.release()
    bro.quit()
    • 模拟登录

    • 实战之QQ空间登录

    from selenium import webdriver
    from time import sleep
    bro=webdriver.Edge()
    bro.get('https://qzone.qq.com/')
    bro.switch_to.frame('login_frame')
    a_tag=bro.find_element_by_id("img_out_2609320892")
    a_tag.click()
    • 无头浏览器和规避检测

    from selenium import webdriver
    from time import sleep
    # 实现无可视化界面
    # 实现规避检测
    from selenium.webdriver.edge.options import Options
    edge_options=Options()
    edge_options.add_argument('--headless')
    edge_options.add_argument('--disable-gpu')
    # 如何实现让selenium规避被检测到的风险
    edge_options.add_experimental_option('excludeSwitches',['enable-automation'])
    bro=webdriver.Edge(options=edge_options)
    # 无可视化界面(无头浏览器),phantomJs
    bro.get('https://baidu.com')
    print(bro.page_source)
    bro.quit()

scrapy框架

  • 什么是框架?

    • 就是一个集成了很多功能并且具有很强通用性的一个项目模板

  • 如何学习框架?

    • 什么是scrapy?

    • 爬虫中封装好的一个明星框架

    • 高性能的持久化存储

    • 异步的数据下载

    • 高性能的数据解析

    • 分布式

  • 如何安装scrapy框架?

    • mac or linux:pip install scrapy

    • windows:pip install scrapy

  1. 创建工程

    • scrapy startproject [工程名]

  2. 在spiders子目录中创建一个爬虫文件

    • 必须进入工程文件

    • scrapy genspider [爬虫文件名] [初始url]

    • 初始url必须要写

  3. 执行工程

    • scrapy crawl [爬虫文件名]

  4. 显示指定类型的日志文件

    • 在配置文件settings.py中加上,LOG_LEVEL='ERROR'

    • 查看指定错误的日志文件才显示

  5. 设置不遵守爬虫协议

    • 在配置文件中ROBOTSTXT_OBEY 默认为true,遵守爬虫协议,更改为False

import scrapy

class SpidernameSpider(scrapy.Spider):
    # 爬虫文件的名称:就是爬虫源文件的一个唯一标识
    name = 'spiderName'
    # 允许的域名:用来限定start_url列表中那些url可以进行请求的发送,一般不使用
    # allowed_domains = ['www.baidu.com']
    # 起始的url列表:该列表存放的url会被scrapy自动进行请求的发送
    start_urls = ['https://ww.baidu.com/','https://www.sogou.com']

    # 用作于数据解析:response参数就是请求成功后对应的响应对象
    def parse(self, response):
        print(response)

scrapy数据解析

import scrapy


class ShujclSpider(scrapy.Spider):
    name = 'shujcl'
    # allowed_domains = ['www.baidu.com']
    start_urls = ['https://www.izuiyou.com/home']

    def parse(self, response):
        # 解析:作者的名称+段子内容
        div_list=response.xpath('//div[@class="Post"]/div')
        for div in div_list:
            # xpath返回的是列表,但是列表元素一定是Selector类型的对象
            # extract可以将Selector对象中data参数存储的字符串提取出来
            # extract_first提取第零个元素
            	
          author=div.xpath('./div[1]/div[classs="Post__name"]/text()').extract_first()
            # 列表调用extract之后,则表示将列表中每一个Selector对象中的data对应的字符串提取出来
          content=div.xpath('./div[2]/div[class="Post__container"]//text()').extract()
            print(author,content)

scrapy持久化存储

基于终端指令

  • 要求:只可以将parse方法的返回值存储到本地的文本文件中

  • 指令:scrapy crawl [爬虫文件名] -o [文件路径]

  • 这种持久化存储方式只能存储['json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle']文件格式

  • 优点:简介高效便捷

  • 缺点:局限性比较强(数据只能存储到指定后缀文件中)

基于管道

  • 编码流程:

    1. 数据解析

    2. 在item类中定义相关属性(items.py文件中的类中用name=scrapy.Field()定义属性)

    3. 将解析的数据封装存储到item类型的对象

    4. 将item类型的对象交给管道进行持久化存储的操作(pipelines.py文件)

    5. 在管道类的process_item方法中要将其接受到的item对象中存储的数据进行持久化存储操作

    6. 在配置文件中开启管道(ITEM_PIPELINES = { 'shuju.pipelines.ShujuPipeline': 300, }注释解除)

    7. 数值300是优先级,数值越小优先级越高

    8. 好处:通用性强

    9. 管道文件中一个管道类对应将一组数据存储到一个平台或载体中,在管道文件中加入一个管道类在设置文件中也要相应加入管道类设置

    10. 在管道文件的process_item方法中最好带上返回值,优先级别高的管道类接收item数据后会把返回值传递给下一个管道类

'''爬虫文件'''
import scrapy
from zuiyou.items import ZuiyouItem

class ShujclSpider(scrapy.Spider):
    name = 'shujcl'
    # allowed_domains = ['www.baidu.com']
    start_urls = ['https://www.izuiyou.com/home']

    def parse(self, response):
        # 解析:作者的名称+段子内容
        div_list = response.xpath('//div[@class="Post"]/div')
        for div in div_list:
            # xpath返回的是列表,但是列表元素一定是Selector类型的对象
            # extract可以将Selector对象中data参数存储的字符串提取出来
            # extract_first提取第零个元素

            author = div.xpath('./div[1]/div[classs="Post__name"]/text()').extract_first()
            # 列表调用extract之后,则表示将列表中每一个Selector对象中的data对应的字符串提取出来
            content = div.xpath('./div[2]/div[class="Post__container"]//text()').extract()
            # 创建item类
            zui = ZuiyouItem()
            # 必须使用中括号形式访问item类中定义的属性
            zui['author'] = author
            zui['content'] = content

            yield zui # 表示将item类提交给管道

'''items文件'''
import scrapy


class ZuiyouItem(scrapy.Item):
    # define the fields for your item here like:
    author = scrapy.Field()
    content = scrapy.Field()
    
    
'''pipelines文件'''
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html


# useful for handling different item types with a single interface
from itemadapter import ItemAdapter


class ZuiyouPipeline:
    fp = None
    # 重写父类的一个方法:该方法只在开始爬虫的时候被调用一次
    def open_spider(self,spider):
        print('开始爬虫。。。。。。。。')
        self.fp = open('./zuiyou.txt','w',encoding='utf8')
    # 专门用来处理item类型对象
    # 该方法可以接受爬虫文件提交过来的item对象
    # 该方法每接收到一个item就会被调用一次
    def process_item(self, item, spider):
        author = item['author']
        content = item['content']

        self.fp.write(author + ':' + content + '\n')

        return item # 会把返回值交给下一个即将执行的管道类 

    # 只在爬虫结束的时候调用一次
    def close_spider(self,spider):
        print("结束爬虫!!!")
        self.fp.close()

全站数据爬取

  • 对一个网站所有的页码对应的页面数据进行爬取

  • 实现方式:

    1. 将所有页码url添加到start_urls列表(比较呆板)

    2. 自动手动进行请求发送(推荐)

import scrapy
from zuiyou.items import ZuiyouItem

class ShujclSpider(scrapy.Spider):
    name = 'shujcl'
    # allowed_domains = ['www.baidu.com']
    # 第一页数据
    start_urls = ['https://www.izuiyou.com/home']
    
    # 生成一个通用爬虫模块
    url = 'https://www.izuiyou.com/home/%d'
    # 第二页开始手动爬取
    page_num = 2

    def parse(self, response):
        # 解析:作者的名称+段子内容
        div_list = response.xpath('//div[@class="Post"]/div')
        for div in div_list:

            author = div.xpath('./div[1]/div[classs="Post__name"]/text()').extract_first()
            content = div.xpath('./div[2]/div[class="Post__container"]//text()').extract()
        if page_num <= 11:    
            new_uel = format(self.url%self.page_num)    
            self.page_num += 1
            # 进行手动爬取,并在成功获取响应对象时调用parse方法进行数据解析
            # callback主要是用来作数据解析
            yield scrapy.Request(url=new_uel,callback=self.parse)

五大核心组件

  • 调度器(包含过滤器,队列)

    • 使用过滤器对url进行去重

    • 用来接受引擎过来的请求,例如队列中,并在引擎再次请求的时候返回,可以想象成一个url(抓取网页的网址)的优先队列,由它来决定下一个要抓取的网址是什么,同时取出重复网址

  • 管道

    • 负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体,验证实体的有效性,清楚不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并进过几个特定的次序处理数据

  • 引擎

    • 用来处理整个系统的数据流处理,触发事物(框架核心)

  • 下载器

    • 用于下载网页内容,并将网页内容返回给蜘蛛(scrapy下载器是建立在twisted这个高效的异步模型上的)

  • spider

    1. 产生url

    2. 进行数据解析

    3. 爬虫是主要干活的,用于从特定的网页中提取自己需要的信息,即所谓的实体(item)。用户也可以从中提取出链接,让scrapy继续抓取下一个页面

image-20220808153912625

请求传参

  • 使用场景:

    • 如果要爬取解析的数据不在同一张页面中。(深度爬取)

    • 传递参数传递的是item对象

    • 在Request函数中使用meta参数进行参数传递

'''
该例子为:
	爬取猎聘网站的python工作信息
	且爬取详情页的工作具体描述
'''
import scrapy
from lieping.items import LiepingItem


class LieurlSpider(scrapy.Spider):
    name = 'lieurl'
    start_urls = ['https://www.liepin.com/zhaopin/?inputFrom=www_index&workYearCode=0&key=python ']

    def parse_url(self, response):
        # 回调函数接收参数
        item = response.meta['item']
        job_desc = response.xpath('/html/body/main/content/section[2]/dl/dd/text()').extract_first()
        item['job_desc'] = job_desc
        yield item

    def parse(self, response):
        name = response.xpath('//div[@class="job-title-box"]/div[1]/@title').extract()
        url = response.xpath('//div[@class="job-detail-box"]/a[1]/@href').extract()
        print(response)
        for i in range(len(name)):
            item = LiepingItem()
            item['name'] = name[i]
            new_url = url[i].split('&')[0]
            # 请求传参
            # 在请求方法中,使用meta={}参数,可以将meta字典传递给请求对应的回调函数
            yield scrapy.Request(new_url, callback=self.parse_url, meta={'item': item})

图片爬取值ImagesPipeline类

  • 基于scrapy爬取字符串类型的数据和爬取图片类型的数据区别:

    • 字符串:只需要基于xpath今夕解析且提交管道进行持久化存储

    • 图片:xpath解析除图片src的属性值。单独的对图片地址发起请求获取图片二进制类型的数据

  • 基于ImagesPipeline类:

    • 只需要将img的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制类型的数据,还会进行持久化存储

  • 需求:爬取二次元网站的cos图片

  • 使用流程:

    1. 数据解析(图片的地址)

    2. 将存储图片地址的item提交到制定的管道类

    3. 在管道文件中自定一个基于ImagesPipeLine的一个管道类,并重写三个方法

      1. get_media_requests:根据图片地址进行图片数据的请求

      2. file_path:指定图片的存储路径

      3. item_completed:把item返回给下一个即将被执行的管道类

    4. 在配置文件中配置图片下载路径

# 爬虫文件
import scrapy
from erciyuan.items import ErciyuanItem


class ErciSpider(scrapy.Spider):
    name = 'erci'
    # allowed_domains = ['t2cy.com']
    start_urls = ['https://t2cy.com/acg/cos/cosplay/2022-07-25/1569.html']

    def parse(self, response):
        print(response)
        p_list = response.xpath('/html/body/div/div[1]/div[1]/div[2]/p')
        for p in p_list:
            # 该网站使用了图片懒加载
            # 所以除了src还要获取data-loadsrc属性
            src = 'https://t2cy.com'+p.xpath('./img/@src | ./img/@data-loadsrc').extract_first()
            item = ErciyuanItem()
            item['src'] = src
            yield item


# 管道文件
from scrapy.pipelines.images import ImagesPipeline
import scrapy
class imagePileLine(ImagesPipeline):

    # 可以根据图片地址进行图片数据的请求
    def get_media_requests(self, item, info):

        yield scrapy.Request(item['src'])

    # 指定图片进行持久化存储的路径
    def file_path(self, request, response=None, info=None, *, item=None):
        image_name = request.url.split('/')[-1]
        return image_name

    def item_completed(self, results, item, info):
        return item

中间件

主要有爬虫中间件与下载中间件(重点)

下载中间件:处于下载器和引擎之间。

  • 作用:可以批量拦截整个工程中发起的所有的的请求和响应

  • 拦截请求:

    • UA伪装

    • 代理IP

  • 拦截响应:

    • 篡改响应数据,响应对象

  • 使用中间件记得在设置文件中开启对应的设置

  • 拦截请求

    '''
    需求:爬取百度ip搜索页
    '''
    # 爬虫文件
    import scrapy
    
    
    class LanjieSpider(scrapy.Spider):
        name = 'lanjie'
        start_urls = ['http://www.baidu.com/s?wd=ip']
    
        def parse(self, response):
            print(response)
            page_text = response.text
    
            with open('ip.html', 'w', encoding='utf8') as fp:
                fp.write(page_text)
    
    
    # middlewars文件
    
    from scrapy import signals
    
    # useful for handling different item types with a single interface
    from itemadapter import is_item, ItemAdapter
    import random
    
    class QqiuDownloaderMiddleware:
        # Not all methods need to be defined. If a method is not defined,
        # scrapy acts as if the downloader middleware does not modify the
        # passed objects.
        # 封装一个UA池
        user_agent_list = [
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 "
            "(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
            "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 "
            "(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 "
            "(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 "
            "(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
            "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 "
            "(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
            "(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
            "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 "
            "(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 "
            "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
            "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 "
            "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
        ]
        # 代理池
        PROXY_http = [
            '153.180.102.104:80',
            '195.208.131.189:56055',
        ]
        PROXY_https = [
            '120.83.49.90:9000',
            '95.189.112.214:35508',
        ]
        # 拦截正常请求
        def process_request(self, request, spider):
    
            # UA伪装
            request.headers['User-Agent'] = random.choice(self.user_agent_list)
            # 代理ip设置,为了验证,一般卸载process_exception
            request.meta['proxy'] = 'http://47.113.90.161'
    
            return None
    
        # 拦截所有的响应
        def process_response(self, request, response, spider):
            # Called with the response returned from the downloader.
    
            # Must either;
            # - return a Response object
            # - return a Request object
            # - or raise IgnoreRequest
            return response
    
        # 拦截发生异常的请求
        def process_exception(self, request, exception, spider):
            # 代理
            request.meta['proxy'] = 'http://' + random.choice(self.PROXY_http) if request.url.split(':')[0] == 'http' else 'https://' + random.choice(self.PROXY_https)
            return request # 将修正之后的请求对象进行重新的请求发送
    
    
  • 拦截响应对象

    '''
    需求:爬取网易新闻中的新闻数据(标题和内容)
        1.通过网易新闻的首页解析出四大板块对应的详情页的url
        2.每一个板块对应的新闻标题都是动态加载出来的
        3.通过解析除每一天新闻详情页的url获取详情页的页面源码,解析出新闻内容
    '''
    
    # 爬虫文件
    import scrapy
    from selenium import webdriver
    from wanyipro.items import WanyiproItem
    
    class WanyiSpider(scrapy.Spider):
        name = 'wanyi'
        allowed_domains = ['news.163.com']
        start_urls = ['https://news.163.com/']
        moders_urls = [] # 存储各个板块的url
        # 实例化浏览器对象
        def __init__(self):
            self.bro = webdriver.Edge('爬虫/msedgedriver.exe')
        def parse(self, response):
            li_list = response.xpath('//*[@id="index2016_wrap"]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
            a_list = [1,2,4,5]
            for index in a_list:
                model_url = li_list[index].xpath('./a/@href').extract_first()
                self.moders_urls.append(model_url)
            # 依次对每个板块的页面进行请求
            for url in self.moders_urls:
                yield scrapy.Request(url,callback=self.parse_model)
    
        # 解析每一个板块页面对应新闻的标题和详情页的url
        def parse_model(self,response):
            div_list = response.xpath('/html/body/div/div[3]/div[3]/div[1]/div[1]/div/ul/li/div/div[3]')
            for div in div_list:
                title = div.xpath('.//h3/a/text()').extract_first()
                new_detail_url = div.xpath('.//h3/a/@href').extract_first()
                item = WanyiproItem()
                item['title'] = title
    
                # 新闻详情页的url发起请求
                yield scrapy.Request(new_detail_url,callback=self.parse_detail,meta={'item':item})
    
        def parse_detail(self,response):
            item = response.meta['item']
            content = response.xpath('//*[@id="content"]/div[2]//text()').extract()
            content = ''.join(content)
            item['content'] = content
            yield item
    
        # 重写父类方法,只在爬虫关闭时调用一次
        def closed(self,spider):
            self.bro.quit()
            
    # middlewares文件
    # 拦截响应对象,进行篡改
        def process_response(self, request, response, spider):
            bro = spider.bro
            # 挑选出指定的响应对象进行篡改
            # 通过url指定request
            # 通过request指定的response
            if request.url in spider.models_urls:
                bro.get(request.url)
                sleep(2)
                page_text = bro.page_source
                # 实例化一个新的响应对象,替代原来不满足需求的响应对象
                # 基于selenium便捷的获取动态加载数据
                # 把selenium和scrapy进行结合
                new_response = HtmlResponse(url=request.url, body=page_text, encoding='utf8', request=request)
    
                return new_response # 替换旧的response
            else:
                return response

CrawlSpider类

CrawlSpider类:spider的子类,专门用作全站数据的爬取

  • 全站数据爬取方式:

    1. 基于spider:手动请求

    2. 基于crawlspider

  • crawl的使用:只有创建爬虫文件时有所不同

    • scrapy genspider -t crawl 文件名 url

    • 链接提取器(LinkExtractor):根据指定规则(allow='正则')进行指定链接的提取

    • 规则解析器(Rule):将链接提取器提取到的链接进行指定规则的解析操作

'''
需求:爬取全部页码的古诗文网的诗词题目和详情页的内容
'''
# 爬虫文件
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from sunpro.items import SunproItem,ContentItem

class SunSpider(CrawlSpider):
    name = 'sun'
    start_urls = ['https://so.gushiwen.cn/shiwens/']

    rules = (
        # 规则解析器
        # LinkExtractor链接提取器
        # follow=True:可以将链接提取器继续作用到链接提取器提取到的链接所对应的页面中
        # 使用follow会产生大量重复链接,调度器的过滤器会去重
        Rule(LinkExtractor(allow=r'page=\d+'), callback='parse_item', follow=True),
        Rule(LinkExtractor(allow=r'/shiwenv\w+\.aspx'), callback='parse_detail',follow=False),
    )
    # 如下两个解析方法中是不可以实现请求传参的
    # 无法将两个解析方法解析的数据存储到一个item中,可以存储到两个item
    def parse_item(self, response):
        # 注意:在xpath表达式中不可以出现tbody标签
        div_list = response.xpath('//*[@id="leftZhankai"]/div[@class="sons"]')
        for div in div_list:
            title = div.xpath('./div[1]//p//text()').extract_first()
            item = SunproItem()
            item['title'] = title
            yield item

    # 解析详情页内容
    def parse_detail(self,response):
        content = response.xpath('//*[@class="contson"]/text()').extract_first()
        item = ContentItem()
        item['content'] = content
        yield item
        
# 管道文件
class SunproPipeline:
    def process_item(self, item, spider):
        # 如何判定item类型
        if item.__class__.__name__ == 'SunproItem':
            print(item['title'])
        else:
            print(item['content'])
        return item

scrapy设置文件

  1. 设置日志等级

    LOG_LEVEL = 'ERROR'
    # 只输出ERROR日志信息
  2. 设置是否遵守爬虫协议

    ROBOTSTXT_OBEY = False
  3. 设置开启管道

    ITEM_PIPELINES = {
    'shuju.pipelines.ShujuPipeline': 300,
    }
    # 在设置文件中解除注释
  4. 设置图片存储路径

    # 图片会都存再imgs文件夹中
    # 会自行创建
    IMAGES_STORE = './imgs'
  5. 设置开启下载中间件

    # 在设置文件中把这解除注释
    DOWNLOADER_MIDDLEWARES = {
       'qqiu.middlewares.QqiuDownloaderMiddleware': 543,
    }

分布式爬虫

  • 概念:需要搭建一个分布式的机群,让其对一组资源进行分布联合爬取

  • 作用:提升爬取数据的效率

  • 如何实现分布式?

    • 安装scrapy-redis的组件

    • 原生的scrapy不可以实现分布式爬虫,必须要让scrapy结合scrapy-redis组件一起实现分布式爬虫

      • 调度器不可以被分布式机群共享

      • 管道不可以被分布式机群共享

    • scrapy-redis组件作用:

      • 可以给原生的scrapy框架提供可以被共享的管道和调度器

    • 实现流程

      1. 创建一个工程

      2. 创建一个基于CrawlSpider的爬虫文件

      3. 修改当前爬虫文件:

        1. 导包:from scrapy_redis.spider import RedisCrawlSpider,并且把爬虫类的父类改成RedisCrawlSpider

        2. 将start_urls和allowed_domains进行注释

        3. 添加redis_key = 'baidu'属性,为可以被共享的调度器队列的名称

        4. 编写数据解析相关操作

      4. 修改配置文件

        1. 指定可以被共享的管道

          ITEM_PIPELINES = {
          	'scrapy_reids.pipelines.RedisPipeline':400
          }
        2. 指定调度器

          # 增加一个去重容器类的配置,作用使用Redis的set集合来存储请求的指纹数据,从而实现请求去重的持久化存储
          DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
          # 使用scrapy_redis组件自己的调度器
          SCHEDULER = "scrapy_redis.scheduler.Scheduler"
          # 配置调度器是否要持久化,也就是当爬虫结束了,要不要清空Redis中请求队列和去重指纹的set
          SCHEDULER_PERSIST = True
        3. 指定redis服务器

          # 指定redis数据库  
          REDIS_HOST = 'xxx.xxx.xxx.xxx'
          REDIS_PORT = xxx
      5. redis相关操作配置

        1. 配置redis的配置文件

          1. 把默认绑定bind注释掉:否则只能自己电脑能访问

          2. 关闭保护模式:protected-mode:no:否则其他电脑访问只能读数据,不能写数据

        2. 结合配置文件开启redis服务

          1. redis-server 配置文件

        3. 启动客户端

          1. redis-cli

      6. 执行工程:scrapy runspider [爬虫源文件名称]

      7. 向调度器的队列中放入起始url

        • 调度器的队列在redis的客户端中

          • lpush [队列名称] [起始URL]

# 爬虫文件
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import  Rule
from scrapy_redis.spiders import RedisCrawlSpider
from fenbu.items import FenbuItem

class FbsSpider(RedisCrawlSpider):
    name = 'fbs'
    # allowed_domains = ['www.xx.com']
    # start_urls = ['http://www.xx.com/']
    # 可以被共享的调度器队列的名称
    redis_key = 'baidu'

    rules = (
        # 规则解析器
        # LinkExtractor链接提取器
        # follow=True:可以将链接提取器继续作用到链接提取器提取到的链接所对应的页面中
        Rule(LinkExtractor(allow=r'page=\d+'), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        # 注意:在xpath表达式中不可以出现tbody标签
        div_list = response.xpath('//*[@id="leftZhankai"]/div[@class="sons"]')
        for div in div_list:
            title = div.xpath('./div[1]//p//text()').extract_first()
            item = FenbuItem()
            item['title'] = title
            yield item

            
'''
其实代码编写步骤和scrapy框架差不多
很多都是配置上的步骤和redis的知识
'''

关于开发者工具与页面源代码不同

  • 有的网站反爬会把某些数据动态加载,这时候我们爬不到自己想要的页面,这时候我们可以读取页面源代码获取我们想要的数据

  • 可以通过抓包工具判断是否为动态加载的数据,再决定是否使用selenium模块获取动态数据也可以

关于一次获取多个xpath数据

  • xpath语句中可以用'|'符号分隔以获取多个xpath语句的属性

关于中文乱码解决方案

多页面爬取

  • post请求:在参数中有页数参数,通过修改可以改变页面

  • get请求:url参数中有页面参数,通过修改可以改变页面

最后更新于