爬虫文档学习 xpath bs4 selenium scrapy...

爬虫

一、介绍

1、什么是爬虫

1.1 爬虫(Spider)的概念

爬虫用于爬取数据, 又称之为数据采集程序

爬取的数据来源于网络,网络中的数据可以是由Web服务器(Nginx/Apache)、数据库服务器(MySQL、Redis)、索引库(ElastichSearch)、大数据(Hbase/Hive)、视频/图片库(FTP)、云存储等(OSS)提供的。

爬取的数据是公开的、非盈利的

1.2 Python爬虫

- 使用Python编写的爬虫脚本(程序)
- 可以完成定时、定量、指定目标(Web站点)的数据爬取。
- 主要使用多(单)线程/进程、网络请求库、数据解析、数据存储、任务调度等相关技术。
- Python爬虫工程师,可以完成接口测试、功能性测试、性能测试和集成测试。

2、爬虫与Web后端服务之间的关系

爬虫使用网络请求库,相当于客户端请求, Web后端服务根据请求响应数据。

爬虫即:

- 指定URL
- 向Web服务器发起HTTP请求,
- 正确地接收响应数据,
- 然后根据数据的类型(Content-Type)进行数据的解析及存储。

爬虫程序在发起请求前,需要伪造浏览器(UA伪装):

# UA:User-Agent(请求载体的身份标识)
# UA检测:门户网站的服务器会检测对应请求的载体身份标识,如果检测到请求的载体身份标识为某一款浏览器,
# 说明该请求是一个正常的请求。但是,如果检测到请求的载体身份标识不是基于某一款浏览器的,则表示该请求
# 为不正常的请求(爬虫),则服务器端就很有可能拒绝该次请求。# UA伪装:让爬虫对应的请求载体身份标识伪装成某一款浏览器

然后再向服务器发起请求, 响应200的成功率高很多。

3、Python爬虫技术的相关库

网络请求:

  • urllib: 内置
  • requests / urllib3 :第三方
  • tornado client: 实现异步请求
  • selenium(UI自动测试)/Splash(基于WebKit内核):动态js渲染
  • appium(手机App 的爬虫或UI测试)

数据解析:

  • re正则
  • xpath
  • bs4
  • json:RESTful接口数据

数据存储:

  • pymysql
  • mongodb
  • elasticsearch: ES搜索引擎 (JavaScript:ECMAScript(ES脚本) + BOM + DOM)

多任务库:

  • 多线程 (threading)、线程队列 queue
  • 协程(asynio、 gevent/eventlet)

爬虫框架

  • scrapy
  • scrapy-redis 分布式(多机爬虫)

4、常见反爬虫的策略

  • UA(User-Agent)策略
  • 登录限制(Cookie/Token)策略
  • 请求频次(IP代理)策略
  • 验证码(图片-云打码,文字或物件图片点选、滑块)策略
  • 动态js(Selenium/Splash/api接口)策略

二、爬虫库urllib【重要】

2.1、urllib库

from urllib.request import urlopen# 发起网络请求
resp = urllopen('http://www.hao123.com')
assert resp.code == 200
print('请求成功')
# 保存请求的网页
# f 变量接收open()函数返回的对象的__enter__()返回结果
with open('a.html', 'wb') as f:f.write(resp.read())

2.1.1 urllib.request 模块

urlopen(url | request: Request, data=None) data是bytes类型

from urllib.request import urlopen  # 导入urllib库下的request的urlopen模块
  • urlopen(url, data=None)可以直接发起url的请求, 如果data不为空时,则默认是POST请求,反之为GET请求。
  • urlopen()返回是response响应对象

urlretrieve(url, filename) 下载url的资源到指定的文件

from urllib.request import urlretrieve, url2pathname
# url2pathname通过url生成本地路径
import hashlib
import os# 下载图片
def download_img(url):# md5 生成文件名 + url获取文件的扩展名filename = hashlib.md5(url.encode('utf-8')).hexdigest() + os.path.splitext(url2pathname(url))[-1]urlretrieve(url, filename) # 保存文件图片if __name__ == '__main__':url = 'http://xxx.xxx.xxx/1.png'# print(url2pathname(url))  # P:\xxx.xxx.xxx\1.png# print(os.path.splitext(url2pathname(url))[-1])  # 分割出 url文件扩展名 .pngdownload_img('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1598329718420&di=f35bcbfa8ce0d360f4ac5ee60efe0b39&imgtype=0&src=http%3A%2F%2Fe.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2Fb3119313b07eca8031a31eeb902397dda1448313.jpg')

build_opener(*handlder) 构造浏览器对象

  • opener.open(url|request, data=None) 发起请求

Request 构造请求的类

  • 可以使用这个类来定制一个请求对象,来模拟浏览器登录

  • Request 构造请求类:

    ​ Request(url, data=None,headers=None,method=None)

from urllib.request import Request # 导入urllib库下的request的Request模块
from collections import namedtuple# 封装请求对象
request = Request(url, headers=default_headers) # 创建请求实例
resp:HTTPResponse = urlopen(request)  # 打开请求

HTTPHandler HTTP协议请求处理器

ProxyHandler(proxies={‘http’: ‘http://proxy_ip:port’}) 代理处理

HTTPCookieProcessor(CookieJar())

  • http.cookiejar.CookieJar 类

2.1.2 urllib.parse

url 解析

from urllib.parse import quote, urlencode
  • quote() 仅对中文字符串进行url编码(只针对一个汉字进行编码);

  • urlencode(query: dict) 将参数的字典中所有的values转成url编码,结果是key=value&key=value形式,即以 application/x-www-form-urlencoded作为url编码类型,可以针对多个参数进行编码)。

    【提示】

    • query: dict 参数加冒号,表示参数的数据类型
    • json上传数据时,Content-Type要设置为application/json类型
    • data 请求头的Content-Type默认类型是:application/x-www-form-urlencoded

2.1.3 response

from http.client import HTTPResponse   # 导入http.client库下的HTTPResponse模块response:HTTPResponse = urlopen(url, timeout=30)   #  打开请求
  • response.read()

    ​ 读取的是二进制数据,需要进行转码

    ​ 字节–>字符串,解码decode 【response.read().decode()】

    ​ 字符串–>字节,编码encode

  • response.code/ response.status/response.getcode()

    ​ 获取响应状态码

  • readline()

    ​ 读取当前行的数据-文本

  • readlines()

    ​ 读取所有行数据-文本 (按行读取字节数据,返回list列表[b’’,b’’])

  • geturl()

    ​ 请求的url

  • headers/getheaders()/info()

    ​ 获取响应头

2.1.4 解决SSL问题

解决Python低版本对SSL证书的支持

import ssl # 导入ssl包
ssl._create_default_https_context = ssl._create_unverified_context # 请求前设置https上下文

2.2、示例

spider01.py

需求:利用http://hao123.com网页测试相应方法

from http.client import HTTPResponse, HTTPMessage
from urllib.request import urlopen, Request, urlretrieve# 解决Python低版本对SSL证书的支持 3.7不用
import ssl
ssl._create_default_https_context = ssl._create_unverified_contexturl = 'https://www.baidu.com'  # http+ssl证书 (Socket三次握手、SSL的Socket的三次握手)
url2 = 'http://hao123.com'     # HTTP的请求版本(HTTP/1.0、HTTP/1.1、HTTP、2.0)# 发起HTTP的请求
# urlopen()默认请求的方法是GET,但data不为空时,则表示请求方式是POST
response:HTTPResponse = urlopen(url2, timeout=30)
print(type(response)) # 查看响应对象的类型
bytes = response.read()
# print(response.read().decode())  # 读取响应的字节数据并转成字符串
print(response.code, response.status, response.getcode()) # 获取响应状态码,
print(response.geturl()) # 响应成功的路径
print(response.headers)print('*'*30)
print(response.info())print('--'*30)
print(response.readlines())  # 按行读取字节数据,返回list列表[b'',b'']

spider02.py

import json
from urllib.request import urlopen, Request
from urllib.parse import quote, urlencode, urljoinfrom http.client import HTTPResponse, HTTPMessage
from collections import namedtuple# 声明不可修改的元组类: 可以修改属性名来访问不可变的属性
# resp = Response(text=, body=, charset=, ...)
Response = namedtuple('Response',('text','body','charset','mimetype','json','headers', 'status_code'))default_headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0'
}def download(url,data=None,headers=None):if headers:default_headers.update(headers)# 基于Request类,封装请求对象request = Request(url, data=data, headers=default_headers)  # 如果修改全局变量,在修改行之前使用global关键字,如 global headersresp: HTTPResponse = urlopen(request)bytes = resp.read()  # 读取字节的响应数据# resp.headers -> HTTPMessagecharset = resp.headers.get_content_charset() if resp.headers.get_content_charset() else 'utf-8'text = bytes.decode(charset)# 创建字典有哪些方式???params = {'body': bytes,'charset': charset,'mimetype': resp.headers.get_content_type(),'headers': dict(resp.headers),'text': text,'json': json.loads(text) if resp.headers.get_content_type() == 'application/json' else {},'status_code': resp.status}return Response(**params)def test_hao123():resp = download('http://hao123.com')print(resp.status_code)print(resp.headers)print(resp.text)def test_baidu():resp = download('https://www.baidu.com')print(resp.status_code)# print(resp.text)with open('baidu.html', 'w', encoding=resp.charset) as f:f.write(resp.text)print('file created')def baidu_search(wd):# quote只针对单一的汉字进行编码(转义)# urlencode() 可以针对多个参数进行编码, 以字典的方式传参url = 'https://www.baidu.com/s?wd=%s' % quote(wd)  # escapeheaders = {'Cookie': 'BAIDUID=75523031597EAA3E8BB1A69893E941A3:FG=1; ''BIDUPSID=287D2F5789E4D37673357E46D76A9234; ''PSTM=1597633936; BD_UPN=13314752; ''COOKIE_SESSION=15827_2_6_6_7_8_0_1_5_4_268_2_21926_17_0_22_1598118126_1598257071_1598257049%7C''9%23865216_4_1598257049%7C2; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598;'' H_PS_645EC=2e46hskUSTamKGaU0Dz33sTXubuzMrp9hK4vtJVqscDTlEg6LOtPFshX9hc; ''BDRCVFR[Fc9oatPmwxn]=srT4swvGNE6uzdhUL68mv3; BDRCVFR[gltLrB7qNCt]=mk3SLVN4HKm; ''BDRCVFR[S4-dAuiWMmn]=I67x6TjHwwYf0; BD_HOME=1; delPer=0; BD_CK_SAM=1; ''PSIN…V-oTH6aogfJ1O1b4WD4UoDdaEG0PSx8g0Kubrn8EogKKy2OTH9DF_2uxOjjg8UtVJeC6EG0Ptf8g0f5; ''H_BDCLCKID_SF=JbAtoKD-JKvJfJjkM4rHqR_Lqxby26nCa6T9aJ5nJDoVSITkhM6Keb8sXN5aJxvC5b7PQbovQpP-HJ7wb-ndeqtT3priW43TMTc8Kl0MLpbtbb0xyn_VMM3beMnMBMn8teOnaITg3fAKftnOM46JehL3346-35543bRTLnLy5KJYMDFRDj0Wej33jaRybTjLHDJKWJbatjrjDCvvQxQcy4LdjG5m2lbifnTHVPJj2CoDhqnqbUJj3Cue3-Aq54RM5eLD2KjtJU3UMM5vQ-OHQfbQ0bbOqP-jW5TaQJuy3R7JOpvwDxnxy5FvQRPH-Rv92DQMVU52QqcqEIQHQT3m5-5bbN3ht6T2-DA_oC8bJKJP; shifen[185669144806_26252]=1598257070; BDSVRTM=212'}resp = download(url, headers)if resp.status_code == 200:with open('%s.html' % wd , 'w', encoding=resp.charset) as f:f.write(resp.text)print('Save %s.html File OK' % wd)def baidu_fanyi(word):url = 'https://fanyi.baidu.com/sug' # post# urlencode 一次将多个参数进行转义(URL编码)data = urlencode({'kw': word})  # kw=%E4%BC%AF%E7%88%B5# 发起 post请求resp = download(url, data.encode('utf-8'))print(resp.status_code)print(resp.json)def xjgg(page=1, page_size=20):print('--download page %s ---' % page)url ='http://www.ccgp-xinjiang.gov.cn/front/search/category'data = json.dumps({'categoryCode': "ZcyAnnouncement2",'pageNo': page,'pageSize': page_size,'utm': "sites_group_front.5b1ba037.0.0.3e2f7230e5f011ea817d43a31c8c15bd"})# post 请求,上传的数据是json格式的字符串resp = download(url, data.encode('utf-8'), headers={'Content-Type': 'application/json;charset=utf-8'})print(resp.status_code, resp.mimetype)print(resp.json)# 保存为json文件ret = []for item in resp.json['hits']['hits']:item['_source']['id'] = item['_id']item['_source']['url'] = urljoin('http://www.ccgp-xinjiang.gov.cn', item['_source']['url'])ret.append(item['_source'])with open('top_%s.json' % page, 'w', encoding=resp.charset) as f:json.dump(ret, f)if page < 10:xjgg(page+1)if __name__ == '__main__':# test_hao123()# test_baidu()# baidu_search('基督山伯爵')# baidu_fanyi('雎')xjgg()

spider03.py


三、requests库

requests库也是一个网络请求库, 基于urllib和urllib3封装的便捷使用的网络请求库。

使用场景:

- 接口测试
- 封装基于RESTful的webserver操作,如ES搜索引擎(ElasticSearch)的SDK操作
- 第三方网络资源请求(ali云的短信验证码)
- 下载站点资源(图片、网页、音频和视频)

3.1 安装环境(库)

pip install requests -i https://mirrors.aliyun.com/pypi/simple

3.2 核心的函数

  • requests.request() 所有请求方法的基本方法

    以下是request()方法的参数说明

    • method: str 指定请求方法, GET, POST, PUT, DELETE,OPTIONS,HEAD,

    • url: str 请求的资源接口(API),在RESTful规范中即是URI(统一资源标签识符)

    • params: dict , 用于GET请求的查询参数(Query String params);如:/s?wd=python3

    • data: dict , 用于POST/PUT/PATCH 请求的表单参数(Form Data) ;封装到body(请求体)中 请求头的Content-Type默认类型是:application/x-www-form-urlencoded。借助urllib.parse.urlencode()方法序列化。

    • json: dict 用于上传json数据的参数, 封装到body(请求体)中,请求头的Content-Type默认设置为application/json,借助json.dumps()将字典序列化,把对象序列化成json字符串。

    • files: dict, 结构 {‘name’: file-like-object | tuple}, 如果是tuple, 则有三种情况:

      • (‘filename’, file-like-object),file-like-object理解为open()方法返回Stream流对象

      • (‘filename’, file-like-object, content_type) ,content_type表示打开文件的

        mimetype(image/png、image/gif、image/webp矢量图)

      • (‘filename’, file-like-object, content_type, custom-headers)

      指定files用于上传文件, 一般使用post请求,默认请求头的Content-Typemultipart/form-data类型。

    • headers/cookies : dict

    • proxies: dict , 设置代理

    • auth: tuple , 用于授权的用户名和口令, 形式(‘username’, ‘pwd’)

  • requests.get() 发起GET请求, 查询数据

    可用参数:

    • url
    • params 请求路径里带有参数时使用
    • json
    • headers/cookies/auth
    # 百度搜索
    import requestsdef search(wd):headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0','Cookies': 'BAIDUID=BB1900CD6732DD7E2D3AE34543595BDC:FG=1; BIDUPSID=BB1900CD6732DD7E94833ED9AE81FDDE; PSTM=1597146175; BDORZ=FFFB88E999055A3F8A630C64834BD6D0; BDRCVFR[Fc9oatPmwxn]=srT4swvGNE6uzdhUL68mv3; delPer=0; PSINO=1; H_PS_PSSID=1440_32621_32328_32348_32497_32481; BDRCVFR[dG2JNJb_ajR]=mk3SLVN4HKm; BDRCVFR[-pGxjrCMryR]=mk3SLVN4HKm'}resp = requests.get('http://www.baidu.com/s', params={'wd':wd}, headers=headers)print(resp.status_code)print(resp.text)with open('%s.html' % wd, 'wb') as f:f.write(resp.content)if __name__ == '__main__':search('python3.8')
    
  • requests.post() 发起POST请求, 上传/添加数据

    可用参数:

    • url
    • data/files
    • json
    • headers/cookies/auth
    # 百度翻译
    import requestsdef fanyi(kw):url = 'https://fanyi.baidu.com/sug'resp = requests.post(url, data={'kw':kw})# response响应对象存在属性:status_code/headers/cookies/encode/text/content/json()print(resp.json())if __name__ == '__main__':fanyi('good')
    
  • requests.put() 发起PUT请求, 修改或更新数据

  • requests.patch() HTTP幂等性的问题,可能会出现重复处理, 不建议使用。用于更新数据

  • requests.delete() 发起DELETE请求,删除数据

3.3 requests.Response

以上的请求方法返回的对象类型是Response, 对象常用的属性如下:

  • status_code 响应状态码

  • url 请求的url

  • headers : dict 响应的头, 相对于urllib的响应对象的getheaders(),但不包含cookie。

  • cookies: 可迭代的对象,元素是Cookie类对象(name, value, path)

  • text : 响应的文本信息

  • content: 响应的字节数据

  • encoding: 响应数据的编码字符集, 如utf-8, gbk, gb2312

  • json(): 如果响应数据类型为application/json,则将响应的数据进行反序化成python的list或dict对象。

    • 扩展-javascript的序列化和反序列化
      • JSON.stringify(obj) 序列化

      • JSON.parse(text) 反序列化

四、数据解析方式之re正则

字符的表示

  • . 任意一个字符, 除了换行
  • [a-f] 范围内的任意一个字符
  • \w 字母、数字和下划线组成的任意的字符
  • \W
  • \d
  • \D
  • \s
  • \S

量词(数量)表示

  • * 0或多个
  • + 1或多个
  • ? 0 或 1 个
  • {n} n 个
  • {n,} 至少n个
  • {n, m} n~m个

分组表示

  • ( ) 普通的分组表示, 多个正则分组时, search().groups() 返回是元组

  • (?P<name> 字符+数量)带有名称的分组, 多个正则分组时,search().groupdict()返回是字典, 字典的key即是分组名。

Python中的正则模块

  • re.compile() 一次生成正则对象,可以多次匹配查询
  • re.match(正则对象, 字符串)
  • re.search()
  • re.findall()
  • re.sub()
  • re.split()

4.1 示例

糗事百科糗图爬取:

import requests
import re
import osif __name__ == '__main__':headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36'}# 创建文件夹,保存所有图片if not os.path.exists('./qiutuLibs'):os.mkdir('./qiutuLibs')# 设置通用的url模板url = 'https://www.qiushibaike.com/imgrank/page/%d/'for pageNum in range(1,3):# 对应页码的urlnew_url = format(url%pageNum)# 使用通用爬虫对url对应的一整张页面进行爬取page_text = requests.get(url=new_url, headers=headers).text# 使用聚焦爬虫将页面中所有的糗图进行解析/提取ex = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>'img_src_list = re.findall(ex,page_text,re.S)# print(img_src_list)for src in img_src_list:# 拼接处一个完整的路径src = 'https:'+ src# 请求到了图片的二进制数据img_data = requests.get(url=src, headers=headers).content# 生成图片名称img_name = src.split('/')[-1]# 图片存储的路径imgPath = './qiutuLibs/' + img_namewith open(imgPath, 'wb') as fp:fp.write(img_data)print(img_name, '下载成功!!!')

五、BS4(BeatifulSoup)

5.1. 介绍

什么是BeatifulSoup
BeautifulSoup,和lxml一样,是一个html的解析器主要功能也是解析和提取数据
官网

https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html

优缺点:

效率没有lxml的效率高
接口设计人性化,使用方便

5.2. 使用

5.2.1. 数据解析原理

1、标签定位

2、提取标签、标签属性中存储的数据值

- bs4 数据解析的原理:- 1、实例化一个BeautifulSoup对象,并将页面源码数据加载到该对象中- 2、通过调用BeautifulSoup对象中相关的属性或者方法进行标签定位和数据提取

环境安装:

- pip install bs4
- pip install lxml(xpath中也要使用)

导入BeautifulSoup包:

from bs4 import BeautifulSoup

创建对象(对象实例化):

网上文件生成对象:soup = BeautifulSoup('网上下载的字符串', 'lxml')
本地文件生成对象:soup = BeautifulSoup(open('1.html'), 'lxml')

5.2.2. 数据解析的方法和属性

  1. soup.tagname 返回的是html中第一次出现的tagname标签

  2. soup.find():

    - find('tagName') == soup.div
    - 属性定位:- soup.find('div', class_/id/attr='xxx')- soup.find_all('tagName') 返回符合要求的所有标签(列表)
    
  3. select:

    - select('某种选择器 (id,class,标签...选择器)') 返回的是一个列表
    - 层级选择器:- soup.select('.xxx > ul > li > a'): > 表示的是一个层级(父子标签)- soup.select('.xxx > ul a'): 空格表示的是多个层级(不是相邻的)
    
  4. 获取标签之间的文本数据:

    - soup.a.text/string/get_text()- text/get_text():可以获取某一个标签中所有的文本内容- string:只可以获取该标签下面直系的文内容
    
  5. 获取标签的属性值

    -soup.a['href']
    

5.2.3 案例

示例:

需求:爬取三国演义小说所有的章节标题和章节内容

#!/usr/bin/env python 
# -*- coding:utf-8 -*-
import requests
from bs4 import BeautifulSoup
#需求:爬取三国演义小说所有的章节标题和章节内容http://www.shicimingju.com/book/sanguoyanyi.html
if __name__ == "__main__":#对首页的页面数据进行爬取headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36'}url = 'http://www.shicimingju.com/book/sanguoyanyi.html'page_text = requests.get(url=url,headers=headers).text#在首页中解析出章节的标题和详情页的url#1.实例化BeautifulSoup对象,需要将页面源码数据加载到该对象中soup = BeautifulSoup(page_text,'lxml')#解析章节标题和详情页的urlli_list = soup.select('.book-mulu > ul > li')fp = open('./sanguo.txt','w',encoding='utf-8')for li in li_list:title = li.a.stringdetail_url = 'http://www.shicimingju.com'+li.a['href']#对详情页发起请求,解析出章节内容detail_page_text = requests.get(url=detail_url,headers=headers).text#解析出详情页中相关的章节内容detail_soup = BeautifulSoup(detail_page_text,'lxml')div_tag = detail_soup.find('div',class_='chapter_content')#解析到了章节的内容content = div_tag.textfp.write(title+':'+content+'\n')print(title,'爬取成功!!!')

后期示例:

import requests
from bs4 import BeautifulSoupfrom utils.ua_ import get_uadef get(url, callback=None):headers = {'User-Agent': get_ua()}resp = requests.get(url, headers=headers, timeout=10)if resp.status_code == 200:resp.encoding = 'utf-8' if not resp.encoding else resp.encodingif callback is None:parse(url, resp.text)else:callback(url, resp.text)def start_spider():get('http://www.qiushibaike.com/text')def parse(url, html):# print(html)soup = BeautifulSoup(html, 'lxml')# 获取所有的文章:<div class="article block untagged mb15 typs_hot" ...>for article in soup.find_all('div', class_='article'):# article:  bs4.element.Tag类实例print(article.get('class'), type(article))img_src = 'https:'+article.find('img').get('src')# print(img_src)# 获取详情页面的URLinfo_url = 'http://www.qiushibaike.com'+article.find('a', class_="contentHerf").get('href')print(info_url)get(info_url, parse_content)  # 发起请求# 任务3:获取下一页的URL,并请求下一页# next_url = ''# get(next_url)def parse_content(url, html):soup = BeautifulSoup(html, 'lxml')#select()方法返回是 <class 'bs4.element.ResultSet'> 实例,可以索引下标操作或迭代author_a = soup.select('.side-left-userinfo')[0]img_tag = author_a.find('img')author_img = 'https:' + img_tag.get('src')author_name = img_tag.get('alt')  # author_a.find('span').string  # .text / .get_text()content = soup.find('div', class_="content")item_pipeline(dict(author_img=author_img, name=author_name, content=content.text))def item_pipeline(item):print(item)# 任务2:保存起来if __name__ == '__main__':start_spider()

六、数据解析方式之xpath

xpath属于xml/html解析数据的一种方式, 基于元素(Element)的树形结构(Node > Element)。选择某一元素时,根据元素的路径选择,如 /html/head/title获取<title>标签。

- XML 被设计用来传输和存储数据。XML 指可扩展标记语言(EXtensible Markup Language)
- HTML 被设计用来显示数据。

6.1. xpath解析原理:

- 1、实例化一个etree的对象,且需要将被解析的页面源码数据加载到该数据对象中
- 2、调用etree对象中的xpath 方法结合着xpath表达式标签的定位和内容的捕获

6.2. 环境安装:

- pip install lxml

6.3. 实例化一个etree对象:

- 1、导包:from lxml import etree
- 2、将本地的html文档中的源码数据加载到etree对象中:etree.parse(filePath)
- 3、可以将从互联网上获取的源码数据加载到该对象中etree.HTML('page_text')

6.4. xpath(‘xpath表达式’)

- /:表示的是从根节点开始定位。表示的是一个层级。
- //:表示的是多个层级。可以表示从任意位置开始定位。
- 属性定位://div[@class='song'] tag[@attrName="attrValue"]
- 索引定位://div[@class="song"]/p[3] 索引是从1开始的。
- 取文本:
- /text() 获取的是标签中直系的文本内容
- //text() 标签中非直系的文本内容(所有的文本内容)
- 取属性:/@attrName     ==>img/src
  1. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hx41BC9a-1602569751032)(E:\07-notes\picture\04-路径查询.png)]

    6.5. 示例1:

    需求:爬取58二手房中的房源信息和价格

    #!/usr/bin/python3
    # 需求:爬取58二手房中的房源信息和价格
    import requests
    from lxml import etreeif __name__ == '__main__':headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0'}# 爬取到页面源码数据url = 'https://xa.58.com/ershoufang/'page_text = requests.get(url=url, headers=headers).text# 数据解析tree = etree.HTML(page_text)# 存储的就是li标签对象li_list = tree.xpath('//ul[@class="house-list-wrap"]/li')fp = open('58二手房', 'w', encoding='utf-8')for li in li_list:# 局部解析title = li.xpath('./div[2]/h2/a/text()')[0]price = li.xpath('./div[3]/p//text()')[0]price2 = li.xpath('./div[3]/p//text()')[1]price3 = li.xpath('./div[3]/p//text()')[2]fp.write(title + '\n' + price + price2 + price3 + '\n')print(title + '\n' + price, price2, price3)
    

    示例2:

    需求:解析下载图片数据 http://pic.netbian.com/4kmeinv/
    
    import requests
    from lxml import etree
    import osif __name__ == '__main__':# 创建文件夹,保存所有图片if not os.path.exists('E:/03-图片/03-picture/picbz'):os.mkdir('E:/03-图片/03-picture/picbz')headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0'}# url = 'http://www.netbian.com/meinv/'url = 'http://pic.netbian.com/4kmeinv/index_%d.html'for pageNum in range(1,10):new_url = format(url % pageNum)response = requests.get(url=url, headers=headers)# 手动设定响应数据的编码格式# response.encoding = 'utf-8'page_text = response.text# 数据解析:src的属性值  alt属性tree = etree.HTML(page_text)li_list = tree.xpath('//ul[@class="clearfix"]/li')for li in li_list:img_src = 'http://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')# 请求图片进行持久化存储img_data = requests.get(url=img_src,headers=headers).contentimg_path = 'E:/03-图片/03-picture/picbz/' + img_name# print(img_name, img_src)with open(img_path, 'wb') as f:f.write(img_data)print(img_name, '下载成功')
    

6.6 示例

6.6.1 要求:站长之家照片采集

# xpath  是解决HTML页面中数据的提取
import requests
from lxml import etreeurl = 'http://sc.chinaz.com/tupian/'resp = requests.get(url)
if resp.status_code ==200:print('--请求成功--')# 开始使用xpath# 1、获取网页根元素resp.encoding='utf-8' # 获取文本数据之前,可以设置字符集root = etree.HTML(resp.text)  # 将下载的html文本转成xpath的根元素的Element# with open('a.html', 'wb') as f:#     f.write(resp.content)   # 将网页写入文件,方便查找标签# 根据属性条件者直接或间接查找子元素img_elements = root.xpath('//div[@class="pic_wrap"]//img') # 返回列表类型list[<Element img at 0x24f3f4befc8>,...]print(img_elements)for img_element in img_elements:print(img_element)  # <Element img at 0x20fa767ed88>print(img_element.get('alt'))  # 霸气手持香烟美女图片print(img_element.xpath('./@alt')[0])  # 霸气手持香烟美女图片data = {}# data['title'] = img_element.xpath('./@alt')[0] # 返回的是list[<str>,....]# data['src'] = img_element.xpath('./@src2')[0]# data['src'], data['title'] = img_element.xpath('./@alt |./@src2')data['title'] = img_element.get('alt')  # 获取标签元素的属性data['src'] = img_element.get('src2')print(data)# 获取下一页的连接# xpath()返回的是list类型page_next_url = url + root.xpath('//a[@class="nextpage"]/@href')[0]print(page_next_url)

6.6.2 代码优化

​ —> 脚本封装为函数并采集到下一页数据

import os
import random
import time
from urllib.request import urlretrieveimport requests
from lxml import etreefrom utils.es import ESIndexindex = ESIndex('chinaz_zhb', '10.36.172.79' , 9200)
index.create_index()
# 1.开始
def start_spider(url, **kwargs):print('正在下载', url)resp = requests.get(url, **kwargs)if resp.status_code == 200:print('下载成功', url)# 开始xpath解析(封装函数)resp.encoding='utf-8'parse(url, resp.text)print(resp.text)  # //div[@class="pic_wrap"]//img# index = 1
# 2. 解析函数
def parse(url, html):root = etree.HTML(html)  # 获取当前网页的根节点# global index# with open('%s.html' % index, 'w', encoding='utf-8') as f:#     f.write(html)## index += 1xpath_str = '//div[@class="pic_wrap"]//img' if url.endswith('/')\else '//div[@id="container"]//img'print(xpath_str)for img in root.xpath(xpath_str):   # 返回列表类型list[<Element img at 0x24f3f4befc8>,...]item = {}item['title'] = img.get('alt')  # 获取标签元素的属性item['src'] = img.get('src2')# 保存数据item_pipeline(item)# 获取下一页的urlif url.endswith('/'):base_url = urlelse:base_url = url[:url.rfind('/')+1]next_page_url = base_url+root.xpath('//a[@class="nextpage"]/@href')[0]print(next_page_url)time.sleep(random.uniform(0.2, 3.5)) # 休眠随机时间# 开始循环拿数据start_spider(next_page_url, headers={'Referer': url,'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0'})# 3. 保存数据处理
def item_pipeline(item):print(item)filename = item['title'] + item['src'][item['src'].rfind('.'):]  # item['src'][item['src'].rfind('.'):]扩展名download_img(item['src'], filename)# 保存数据到ES搜索引擎index.add_doc('images', **item)# 下载图片的方法
def download_img(url, filename):urlretrieve(url, os.path.join('images', filename))if __name__ == '__main__':start_spider('http://sc.chinaz.com/tupian/')

6.6.3 古诗文网站数据采集

# 古诗文网站数据采集
import requests
from lxml import etree
from lxml.etree import _Elementclass GswSpider():# 起始的urlstart_url = ['https://so.gushiwen.org/shiwen/']base_url = 'https://so.gushiwen.org'headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0'}DEBUG = True# 打印函数def log(self, *args, sep='\n'):if self.DEBUG:print(*args, sep=sep)# 启动爬虫def start_spider(self):for url in self.start_url:self.get(url)# 封装请求函数(下载)def get(self, url, parse_func=None, **kwargs):resp = requests.get(url, headers=self.headers)  # 发起get请求self.log(url, resp.status_code, sep=' ')  # 打印url和状态码 https://so.gushiwen.org/shiwen/ 200if resp.status_code == 200:resp.encoding = 'utf-8'  # 字符集处理if parse_func is None:self.parse(url, resp.text)else:parse_func(url, resp.text, **kwargs)# 解析函数def parse(self, url, html):# 解析 右侧类型root = etree.HTML(html)  # 获取当前网页的根节点for a_element in root.xpath('//div[@class="main3"]/div[last()]/div[1]//a'):name = a_element.text  # 拿到text属性 小学古诗href = a_element.get('href')  # https://so.gushiwen.org/gushi/xiaoxue.aspx# self.log(name, href, sep='--->')  # 小学古诗--->https://so.gushiwen.org/gushi/xiaoxue.aspxself.get(href, self.parse_gsw_list)  # 第二次发起请求# 解析某一个分类下的所有古诗文def parse_gsw_list(self, url, html):root = etree.HTML(html)left_div: _Element = root.xpath('//div[@class="main3"]/div[1]')[0]# self.log(type(left_div))  # <class 'lxml.etree._Element'>cate_name = left_div.xpath('.//h1/text()')[0]  # 当前标签的文本 小学古诗文for subtype_element in left_div.xpath('.//div[@class="typecont"]'):try:subtype_name = subtype_element.xpath('.//strong/text()')[0]  # 一年级上册except:subtype_name = ''# self.log(cate_name, subtype_name, sep='=>')  # 小学古诗文=>一年级上册print(subtype_element.xpath('.//span/a'))for span_a in subtype_element.xpath('.//span/a'):href = span_a.get('href')  # https://so.gushiwen.org/shiwenv_ef9cd9ba44bb.aspxprint(href)if href:href = href if href.startswith('http') else self.base_url + hrefbook_name = span_a.text  # 诗名 江南self.log(cate_name, subtype_name, book_name, href, sep='=>')self.get(href, self.parse_gsw, category=cate_name, subtype=subtype_name) # 第三次发起请求# 解析某一古诗详情def parse_gsw(self, url, html, category=None, subtype=None):self.log('parse_gsw', category, subtype, sep=' ')root = etree.HTML(html)# 提取诗文的内容left_div = root.xpath('//div[@class="main3"]/div[1]')[0]gsw_element = left_div.xpath('./div[2]/div[1]')[0]name = gsw_element.xpath('.//h1/text()')[0]era, author = gsw_element.xpath('./p/a/text()')content = '\n'.join([c.replace('\n', '').strip() for c in gsw_element.xpath('./div[last()]/text()')])# self.log(name, author, era, sep='+')# print(content)self.item_pipeline(dict(name=name, author=author, era=era, category=category, subtype=subtype, content=content))def item_pipeline(self, item):print(item)# 写入到ES中if __name__ == '__main__':spider = GswSpider()# spider.DEBUG=Falsespider.start_spider()

【提示】:

1、解析类型数据获取标签举例:

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P9dVyAhw-1602569751036)(E:\07-notes\picture\06-古诗文采集标签举例.png)]

2、古诗文采集解析某一分类数据标签举例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VSEskUR9-1602569751039)(E:\07-notes\picture\07-古诗文采集解析某一分类数据标签举例.png)]

6.6.4 协程版古诗文数据采集

# 古诗文网站的数据采集
import requests
from lxml import etree
from lxml.etree import _Element
import asyncioclass GswSpider():start_urls = ['https://so.gushiwen.org/shiwen/']base_url = 'https://so.gushiwen.org'headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0'}DEBUG = True# 加装饰器@asyncio.coroutinedef log(self, *args, sep='\n'):if self.DEBUG:print(*args, sep=sep)@asyncio.coroutinedef start_spider(self):for url in self.start_urls:yield from self.get(url)@asyncio.coroutinedef get(self, url, parse_func=None, **kwargs):resp = requests.get(url, headers=self.headers)self.log(url, resp.status_code, sep=' ')if resp.status_code == 200:resp.encoding = 'utf-8'if parse_func is None:yield from self.parse(url, resp.text)else:yield from parse_func(url, resp.text, **kwargs)@asyncio.coroutinedef parse(self, url, html):root = etree.HTML(html)for a_element in root.xpath('//div[@class="main3"]/div[last()]/div[1]//a'):name = a_element.texthref = a_element.get('href')yield from self.log(name, href, sep='->')yield from self.get(href, self.parse_gsw_list)@asyncio.coroutinedef parse_gsw_list(self, url, html):# 解析某一个分类下的所有诗文root = etree.HTML(html)left_div: _Element = root.xpath('//div[@class="main3"]/div[1]')[0]# self.log(type(left_div))cate_name = left_div.xpath('.//h1/text()')[0]for subtype_element in left_div.xpath('.//div[@class="typecont"]'):try:subtype_name = subtype_element.xpath('.//strong/text()')[0]except:subtype_name = ''for span_a in subtype_element.xpath('.//span/a'):href = span_a.get('href')if href:href = href if href.startswith('http') else self.base_url + hrefbook_name = span_a.textyield from self.log(cate_name, subtype_name, book_name, href, sep='>')yield from self.get(href, self.parse_gsw, category=cate_name, subtype=subtype_name)@asyncio.coroutinedef parse_gsw(self, url, html, category=None, subtype=None):yield from self.log('parse_gsw', category, subtype, sep=' ')root = etree.HTML(html)left_div = root.xpath('//div[@class="main3"]/div[1]')[0]# 提取诗文内容gsw_element = left_div.xpath('./div[2]/div[1]')[0]name = gsw_element.xpath('.//h1/text()')[0]era, author = gsw_element.xpath('./p/a/text()')content = '\n'.join([c.replace('\n', '').strip() for c in gsw_element.xpath('./div[last()]//text()')])# self.log(name, author, era, sep='+')yield from self.item_pipeline(dict(name=name,author=author,era=era,category=category,subtype=subtype,content=content))@asyncio.coroutinedef item_pipeline(self, item):print(item)# 写入到ES中if __name__ == '__main__':spider = GswSpider()spider.DEBUG=False# asyncio.run(spider.start_spider()) # 获取事件模型对象loop = asyncio.get_event_loop()loop.run_until_complete(spider.start_spider())  # 单协程对象启动# tasks = [spider.start_spider(), spider.start_spider(), spider.start_spider()]# loop.run_until_complete(asyncio.wait(tasks))  # 多协程对象启动# spider.get('https://so.gushiwen.org/shiwenv_e4df1367a39a.aspx', spider.parse_gsw)

七、验证码

7.1 Cookie:

http/https协议特性:无状态。
没有请求到对应页面数据的原因:发起的第二次基于个人主页页面请求的时候,服务器端并不知道该此请求是基于登录状态下的请求。
cookie:用来让服务器端记录客户端的相关状态。- 手动处理:通过抓包工具获取cookie值,将该值封装到headers中。(不建议)- 自动处理:- cookie值的来源是哪里?- 模拟登录post请求后,由服务器端创建。session会话对象:- 作用:1.可以进行请求的发送。2.如果请求过程中产生了cookie,则该cookie会被自动存储/携带在该session对象中。- 创建一个session对象:session = requests.Session()- 使用session对象进行模拟登录post请求的发送(cookie就会被存储在session中)- session对象对个人主页对应的get请求进行发送(携带了cookie)

八、代理

代理:破解封IP这种反爬机制。
什么是代理:- 代理服务器。
代理的作用:- 突破自身IP访问的限制。- 隐藏自身真实IP
代理相关的网站:- 快代理- 西祠代理- www.goubanjia.com
代理ip的类型:- http:应用到http协议对应的url中- https:应用到https协议对应的url中代理ip的匿名度:- 透明:服务器知道该次请求使用了代理,也知道请求对应的真实ip- 匿名:知道使用了代理,不知道真实ip- 高匿:不知道使用了代理,更不知道真实的ip

九、多任务爬虫

在爬虫中使用异步实现高性能的数据爬取操作

1. 进程和线程

  • multiprocessing模块(进程)

    • Process 进程类
    • Queue 进程间通信的队列
      • put(item, timeout)
      • item = get(timeout)

    进程使用场景:

    • 服务程序与客户程序分离,如:mysql服务和mysql客户端、Docker、Redis、ElasticSearch等服务都属于进程使用场景

    • 服务框架中使用,如:scrapy、Django、Flask。

    • 分离业务中使用,如:进程池的定时任务和计划编排等,Celery框架

    • 使用场景图示:

      ​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8uyxkhNQ-1602569751043)(E:\07-notes\picture\08-进程使用场景.png)]

  • threading 模块(线程)

    • Thread 线程类
    • 线程间通信(访问对象)
      • queue.Queue 线程队列
      • 回调函数(主线程声明, 子线程调用函数)

2.进程

2.1 进程的生命周期

# 进程的生命周期(状态):
# 1-> 创建 Xxx()
# 1->2: 启动 .start()# 2-> 就绪,等待执行# 3-> 运行,run()方法被执行,CPU分配时间执行片
# 3->2: 就绪, CPU执行片用完,则进入就绪状态
# 3->5 结束4# 4-> 阻塞,在run()方法中,执行程序的过程遇到IO操作(IO操作:文件的读写、网络流的读写、网络请求)
# 4->2: 阻塞结束后,进入就绪状态# 5-> 结束, 代码运行完毕,程序运行结束

图示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-58e83gBO-1602569751045)(E:\07-notes\picture\11-进程的生命周期图示.png)]

2.2 进程间的通信

# 进程之间的通信方式(进程间的内存是相互独立的):
# 1. multiprocessing.Queue 进程队列
# 2. multiprocessing.Pipe  进程管道
# 3. multiprocessing.Manager  共享内存 ,基于C实现的,Manager中所指放入数据类型都是c的
# 4. Linux下的socket.AF_UNIX  套接字
# 5. signals 信号(键盘事件监听等)

2.3 爬虫进程设计图示:

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7XzryDDS-1602569751047)(E:\07-notes\picture\09-爬虫进程设计.png)]

2.4 进程队列

2.4.1 说明

使用管道和少量的锁/信号量实现的进程共享的队列
当进程首先将一个项目放到队列中时,启动一个将线程从缓冲区转移到管道中的Feeder线程

2.4.2 Queue对列的方法
- Queue(maxsize=0) 最大进程任务数,<=0 表示不限制
- 输入: put(obj[, block[, timeout]])
- 获取输出: get([block[, timeout]])
- 队列大小: qsize()
- 是否为空: empty()
- 关闭: close()
# 需求: Boss派20项活给5个工人完成
from multiprocessing import Process, Queue
import time
import osdef boss(q):  # 大老板安排任务for i in range(20):msg = 'Boss按排的任务: %d ' %iq.put(msg)  # 如果msg量没有达到最高值,则直接存入,反之,则等待print(time.strftime('%x %X', time.localtime()), msg)  # 08/29/20 09:12:06 Boss按排的任务: 0time.sleep(0.5)print('--boss任务派发完成----')def worker(q):while True:msg = q.get(timeout=5)  # 5秒内没有消息,表示工作完成print('at {} 工人<{}> 收到: {}'.format(time.time(), os.getpid(), msg))time.sleep(2)if __name__ == '__main__':q = Queue(maxsize=2)  # 最大的消息数量workers = []for i in range(5):p = Process(target=worker, args=(q, ))p.start()workers.append(p)  # 将所有工人进程管理起来boss(q)  # 老板开始派活for worker in workers:worker.terminate()  # 解散工人q.close()print('--工作完成---over--')
2.4.3 晒了吗网站多任务数据爬取

2.5 进程管道 (半/全双工)

管道也叫无名管道,它是 UNIX 系统 IPC(进程间通信 (Inter-Process Communication) 的最古老形式

管道用来连接不同进程之间的数据流

pipe.recv() 接收消息
pipe.send(str) 发送消息
2.5.1.1 半双工
import time
from multiprocessing import Process,current_process as cp,Pipedef send_msg(conn):print('--send msg--',cp().name)time.sleep(3)conn.send((1,2,3))   #管道通信尅发送任何类型对象# conn.send('are you ok?')print('--msg已发送--',cp().name)def receive_msg(conn):print('--receive msg--',cp().name)msg=conn.recv()   #阻塞到收到消息为止print(cp().name,'receives msg->',msg)if __name__ == '__main__':# duplex=Flase 表示单双工# conn1仅接收(只读)  conn2仅发conn1,conn2=Pipe(duplex=False)p1=Process(target=send_msg,args=(conn2,))p2 = Process(target=receive_msg, args=(conn1,))p2.start()p1.start()p1.join()p2.join()print('--over--')
2.5.1.1 全双工
import time
from multiprocessing import Process,current_process as cp,Pipedef a(conn):print(cp().name,'--a msg--')time.sleep(1)conn.send('are you ok?')print(cp().name,'a->b sended msg')msg=conn.recv()  #等待消息到达print(cp().name,'接收到b的消息',msg)def b(conn):print(cp().name,'--b msg--')msg=conn.recv()  #等待消息到达if msg.find('are you ok?')>-1:conn.send('ok!')else:conn.send('收到你的消息真开心')if __name__ == '__main__':# duplex=True 表示全双工# conn1/conn2  可读可写conn1,conn2=Pipe(duplex=True)p1=Process(target=a,args=(conn1,))p2 = Process(target=b, args=(conn2,))p2.start()p1.start()p1.join()p2.join()print('--over--')

3. 线程

3.1 线程的概念

1、一个进程里面至少有一个线程,进程的概念只是一种抽象的概念,真正在CPU上面调度的是进程里的线程
2、线程是真正干活的,线程用的是进程里面包含的一堆资源,线程仅仅是一个调度单位,不包含资源
3、每一个进程在启动的时候都会默认创建一个线程, 这个线程叫主线程(MainThread)
4、一个进程(任务)里面可能对应多个分任务,如果一个进程里面只开启一个线程的话,多个分任务之间实际上是串行的执行效果,即一个程序里面只含有一条执行路径

3.2 线程与进程的关系

一个程序启动起来以后,至少有一个进程,这个进程至少有一个线程

3.2.1 功能
  • 进程,能够完成多任务,比如 在一台电脑上能够同时运行多个QQ。
  • 线程,能够完成多任务,比如 一个QQ中的多个聊天窗口。
3.2.2 定义的不同
  • 进程是系统进行资源分配和调度的一个独立单位.
  • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
3.2.3 区别
  • 一个程序至少有一个进程,一个进程至少有一个线程.

  • 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。

  • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率

  • 线程不能够独立执行,必须依存在进程中

  • 可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人

优缺点

线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。

开发中多用 ---> 多进程 + 协程进程:占用资源多,效率相对较低,便于管理和维护线程:占用资源少,效率相对较高,不便于管理和维护存在一个平衡

例:

开4个进程 —> 一般有几个CPU就开几个进程(4核双线程就可以开8个进程),可以实现并行

开400个线程 —> 实现并发,一个CPU不断切换执行任务

3.3. 同步与异步

同步:即是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去。

异步:与同步相反,即进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。
当有消息返回时系统会通知进行处理,这样可以提高执行的效率。

3.4. 串行与并发

  • CPU地位:

    无论是串联、并行或并发,在用户看来都是同时运行的,不管是进程还是线程,都只是一个任务而已, 真正干活的是CPU,CPU来做这些任务,而一个cpu(单核)同一时刻只能执行一个任务

  • 串行:

    在执行多个任务时,一个任务接着一个任务执行,前一任务完成后,才能执行下一个任务。

  • 并行:

    多个任务同时运行,只有具备多个cpu才能实现并行,含有几个cpu,也就意味着在同一时刻可以执行几个任务

  • 并发:

    是伪并行,即看起来是同时运行的,实际上是单个CPU在多个程序之间来回的切换

3.5. 线程案例

import random
import time
from threading import Thread, current_thread as ct, Lock
from queue import Queueclass DownloadThread(Thread):def __init__(self):super(DownloadThread,self).__init__()def run(self) -> None:global sumprint(ct().name, 'running...')time.sleep(random.uniform(0.5, 3.5))n = random.randint(1, 100)# lock 在上下文中使用时# 进入上下文: 加锁 lock.acquire()# 退出上下文: 释放锁 lock.release()with lock:print(ct().name, '产生了', n, '当前的sum->', sum)sum += ntime.sleep(0.2)print(ct().name, '当前的sum->', sum)if __name__ == '__main__':sum = 100  # 可以在多个线程中使用lock = Lock()# 创建了10个线程ts = [DownloadThread()for i in range(10)]# 启动线程for t in ts:t.start()# 等待所有线程执行完成for t in ts:t.join()  # 阻塞方法
3.5.1. 安全锁
- 创建锁lock = threading.Lock()lock = threading.RLock()
- 加锁lock.acquire()
- 解锁lock.release()
3.5.2. 线程本地变量

理解:

​ ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的

一、对 ThreadLocal 的理解ThreadLocal,有的人叫它线程本地变量,也有的人叫它线程本地存储,其实意思一样。   ThreadLocal 在每一个变量中都会创建一个副本,每个线程都可以访问自己内部的副本变量。二、为什么会出现 ThreadLocal 的技术应用我们知道多线程环境下,每一个线程均可以使用所属进程的全局变量。如果一个线程对全局变量进行了修改,将会影响到其他所有的线程对全局变量的计算操作,从而出现数据混乱,即为脏数据。为了避免线程同时对变量进行修改,引入了线程同步机制,通过互斥锁、条件变量或者读写锁来控制对全局变量的访问。只用全局变量并不能满足多线程环境的需求,很多时候线程还需要拥有自己的私有数据,这些数据对于其他线程来说是不可见的。因此线程中也可以使用局部变量,局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问。有时候使用局部变量不太方便,因此 Python 还提供了ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。ThreadLocal 真正做到了线程之间的数据隔离,将线程的数据进行私有化
import time
from threading import Thread, current_thread as ct, localclass Download(Thread):def __init__(self, url):super(Download, self).__init__()self.url = urldef run(self) -> None:# 初始化请求头# 设置代理或Cookie等print(ct().name, '---set user-agent---', self.url)# id(current_thread())# headers 是线程的本地变量,添加属性时,使用当前的线程对象的ID作为key,属性作为value()# headers 本地变量实际的数据结构是 {id(thread):{属性名:属性值}}headers.user_agent = '%s Firefox 66.72' % self.urltime.sleep(1)print(ct().name, headers.user_agent)time.sleep(1)if __name__ == '__main__':headers = local()  # 线程的本地变量# Python提供了 threading.local 类,将这个类实例化得到一个全局对象,# 但是不同的线程使用这个对象存储的数据其它线程不可见(本质上就是不同的线程使用这个对象时为其创建一个独立的字典)。urls = ('http://www.baidu.com','http://hao123.com','http://jd.com')ts = [Download(url)for url in urls]for t in ts:t.start()for t in ts:t.join()# 输出结果:每一个线程读到的headers都不同,真正做到了线程之间的隔离
# Thread-1 ---set user-agent--- http://www.baidu.com
# Thread-2 ---set user-agent--- http://hao123.com
# Thread-3 ---set user-agent--- http://jd.com
# Thread-1 http://www.baidu.com Firefox 66.72
# Thread-3Thread-2 http://hao123.com Firefox 66.72
#  http://jd.com Firefox 66.72
3.5.3. 线程条件变量

条件变量(Condition)

作用:

实现多线程的数据安全问题,当数据不满足条件时,可以让线程挂起,反之唤醒其他等待线程。
内部使用线程锁

原理:

互斥锁,主要作用是并行访问共享资源时,保护共享资源,防止出现脏数据。python 条件变量Condition也需要关联互斥锁,同时Condition自身提供了wait/notify/notifyAll方法,用于阻塞/通知其他并行线程,可以访问共享资源了。可以这么理解,Condition提供了一种多线程通信机制,假如线程1需要数据,那么线程1就阻塞等待,这时线程2就去制造数据,线程2制造好数据后,通知线程1可以去取数据了,然后线程1去获取数据
  • 用法:
条件变量利用线程间共享的全局变量进行同步的一种机制
  • 两个动作:
1、一个 线程等待"条件变量的条件成立"而挂起
2、另一个线程使“条件成立”
  • 与互斥锁结合使用:
- 为了防止竞争,防止死锁
- 线程在改变条件状态前必须首先锁住互斥量(Lock)
- 把条件变量到等待条件的线程列表上
- 对互斥锁解锁
  • 常用函数:
# 创建条件变量
cond = threading.Condition(threading.Loca())
acquire(*args)  线程锁,注意线程条件变量中的所有相关函数使用必须在acquire() /release() 内部操作;
release()  条件变量解锁  
wait([timeout])  等待唤醒,timeout表示超时  
notify(n=1)  唤醒最大n个等待的线程  
notifyAll()、notify_all()   唤醒所有等待的线程  

示例:

import time
from threading import Thread, Condition, Lock, current_thread as ct
from queue import Queueclass ConQueue(Queue):def __init__(self, maxsize=10):  # 初始化super(ConQueue, self).__init__(maxsize)self.cond = Condition(Lock())  # 条件变量对象,传入一把锁def consume(self, **kwargs):# 消费的方法name = ct().name + '<%s>' % ct().ident  # 线程IDif self.cond.acquire():  # 加锁while self.empty():  # 如果仓库为空print(name, '当前仓库是空的')self.cond.wait()  # 等待、挂起item = self.get_nowait()   # 不用等待 获取数据self.cond.notify()  # 唤醒所有等待线程self.cond.release()  # 释放锁return itemdef product(self, item):# 生产的方法name = ct().name + '<%s>' % ct().ident  # 线程IDif self.cond.acquire():while self.full():print(name, '仓库已满')self.cond.wait()self.put_nowait(item)  # 不需要等待 存入仓库self.cond.notify()  # 唤醒其他(消费)线程self.cond.release()   # 释放锁class ProducterThread(Thread):  # 生产线程def __init__(self, con_queue):super(ProducterThread, self).__init__(name='Producyor')self.queue:ConQueue = con_queuedef run(self) -> None:name = ct().name + '<%s>' % ct().ident  # 线程IDglobal numwhile True:with lock:item = '%s 面包' % numself.queue.product(item)print(name, '生产了', item)num += 1time.sleep(2)class ConsumThead(Thread):   # 消费线程def __init__(self, con_queue):super(ConsumThead, self).__init__(name='Consumer')self.queue = con_queuedef run(self) -> None:name = ct().name + '<%s>' % ct().identwhile True:item = self.queue.consume()  # 可能存在等待状态print(name, '消费了', item)time.sleep(1)def start(*threads):for t in threads:t.start()def join(*threads):for t in threads:t.join()if __name__ == '__main__':queue = ConQueue(20)cs = [ConsumThead(queue) for _ in range(5)]ps = [ProducterThread(queue) for _ in range(2)]num = 1  # 面包的序号lock = Lock()start(*cs, *ps)  # 可以解包list或tuple元组# start(*ps)join(*cs, *ps)# join(*ps)print('--over--')

4. 协程

协程是线程的替代品, 区别在于线程由CPU调度, 协程由用户(程序)自己的调度的。

协程需要事件监听模型(事件循环器),它采用IO多路复用原理,在多个协程之间进行调度

4.1 协程的定义原理

- 1.协程是以协作式调度的单线程,协程又称之为“微线程”,它是在一个线程内完成函数或子程序之间调度
- 2.一个函数(子程序)在调用时都是按层级调用,如A调用B,B调用C,再依次返回结果
- 3.函数调用是通过栈实现的,栈中存放函数的局部变量
- 4.函数或子程序的调用一个入口, 一次返回,调用顺序是明确。
- 5.协程在执行子程序或函数时,函数内部是可以中断,转而执行别的子程序,在适当时再返回接着执行。

4.2 协程的事件模型

协程的事件模型(IO异步模型):

- selector 轮询
- poll 事件回调
- kqueue/epoll 增强式事件回调

4.3. 协程的三种方式

  • 基于生成器 generator (过渡)
    • yield
    • send()
  • Python3 之后引入了 asyncio模块
    • @asyncio.coroutine 协程装饰器, 可以在函数上使用此装饰器,使得函数变成协程对象
    • 在协程函数中,可以使用yield from 阻塞当前的协程,将执行的权限移交给 yield from 之后的协程对象。
    • asyncio.get_event_loop() 获取事件循环模型对象, 等待所有的协程对象完成之后结束。
  • Python3.5之后,引入两个关键字
    • async 替代 @asyncio.coroutine
    • await 替代 yield from

协程对象的运行方式:

- loop = asyncio.get_event_loop()
- loop.run_until_comlete(协程对象)- 自定义的协程函数,由@asyncio.coroutine装饰器装饰的函数,即协程对象- 通过asyncio.wait()协程函数封装多个自定义协程对象
4.3.1. 基于生成器
# 基于生成器方式实现协程
# yield/send# 斐波那契数列: 1,1,2,3,5,8,.....
import random
import timedef fib(n):a, b = 0, 1index = 0while index < n:wait = yield b  # 将b值输出给调用者,等待调用者输入(传入)等待时间waitprint(wait, ' 秒之后将会产出')time.sleep(wait)a, b = b, a+bindex += 1def main():f = fib(10)n = next(f)  # 从fib函数中获取数字while True:try:print('--->', n)wait_second = random.uniform(0.1, 2.0)n = f.send(wait_second)  # 向fib生成器函数发送等待时间(数据),等待fib函数产出下一个数字except:  # StopIteration异常:生成器产出完成breakif __name__ == '__main__':# for n in fib(10):#     print(n, end=' ')main()
4.3.2. 引入asyncio模块
# 斐波那契数列: 1,1,2,3,5,8,.....
import random
import timeimport asyncio
from asyncio import coroutine
from utils.ua_ import *
import requests@coroutine
def get(url):print('--正在GET请求-->', url)resp = requests.get(url, headers={'User-Agent': get_ua()})if resp.status_code == 200:resp.encoding = 'utf-8'items = yield from parse(url, resp.text)print(url, '===解析完成===>', items)def download(*urls):# 下载任务的入口函数# 创建异步协程的时间循环模型loop = asyncio.get_event_loop()  # 获取事件循环器# loop.run(协程对象) 单个协程运行# 生成批量的协程任务,并添加事件模型执行# 启动循环知道结束为止loop.run_until_complete(asyncio.wait([get(url)for url in urls]))@coroutine
def parse(url, html):print(url, '正在解析')# time.sleep()  # 当前线程挂起,阻塞yield from asyncio.sleep(random.uniform(0.1, 3.0))  # 当前协程挂起,阻塞return {'url': url,'data':time.localtime()}if __name__ == '__main__':# download('http://www.baidu.com','http://hao123.com','http://jd.com')# 单个运行协程的方式coroutine_1 = get('http://www.baidu.com')asyncio.get_event_loop().run_until_complete(coroutine_1)
4.3.3. async和await
# 斐波那契数列: 1,1,2,3,5,8,.....
import random
import timeimport asyncio
from asyncio import coroutine
from utils.ua_ import *
import requestsasync def get(url):print('--正在GET请求-->', url)resp = requests.get(url, headers={'User-Agent': get_ua()})if resp.status_code == 200:resp.encoding = 'utf-8'items = await parse(url, resp.text)print(url, '===解析完成===>', items)def download(*urls):# 下载任务的入口函数# 创建异步协程的时间循环模型loop = asyncio.get_event_loop()  # 获取事件循环器# loop.run(协程对象) 单个协程运行# 生成批量的协程任务,并添加事件模型执行loop.run_until_complete(asyncio.wait([get(url)for url in urls]))async def parse(url, html):print(url, '正在解析')# time.sleep()  # 当前线程挂起,阻塞await asyncio.sleep(random.uniform(0.1, 3.0))  # 当前协程挂起,阻塞return {'url': url,'data':time.localtime()}if __name__ == '__main__':# download('http://www.baidu.com','http://hao123.com','http://jd.com')# 单个运行协程的方式coroutine_1 = get('http://www.baidu.com')asyncio.get_event_loop().run_until_complete(coroutine_1)

十、selenium

Selenium是驱动浏览器(chrome, firefox, IE)进行浏览器相关操作(打开url, 点击网页中按钮功连接、输入文本)

10.1.什么是selenium模块

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

  • 支持通过各种driver(FirfoxDriver,IternetExplorerDriver,OperaDriver,ChromeDriver)驱动真实浏览器完成测试

    selenium也是支持无界面浏览器操作的。比如说HtmlUnit和PhantomJs。

10.2. 为什么使用selenium

  • 模拟浏览器功能,自动执行网页中的js代码,实现动态加载

  • 页面渲染
    在浏览器请求服务器的网页时, 执行页面的js,在js中将数据转成DOM元素(HTML标签)

  • UI自动测试

    • 定位输入DOM节点
    • 点击某一个DOM节点(Button/a标签)

10.3 使用selenium

安装环境:

pip install selenium

下载一个浏览器的驱动程序(谷歌浏览器)

- 下载路径:http://chromedriver.storage.googleapis.com/index.html
- 驱动程序和浏览器的映射关系:http://blog.csdn.net/huilan_same/article/details/51896672

导入模块:

from selenium import webdriver

实例化一个浏览器对象

  • 编写基于浏览器自动化的操作代码

    • 发起请求:get(url)

    • 标签定位:find系列的方法

    • 标签交互:send_keys(‘xxx’)

    • 执行js程序:excute_script(‘jsCode’)

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

    • 关闭浏览器:quit()

【总结】元素定位:

1、find_element_by_id  WebElement 元素
2、find_elements_by_name- 根据标签的name属性查找多个Dom元素- form表单中的字段标签都具有name属性- iframe标签具有name属性
3、find_elements_by_xpath- 基于xpath方式查找DOM元素- 依赖lxml库
4、find_elements_by_tag_name- 根据标签名查找多个Dom元素
5、find_elements_by_class_name
6、find_elements_by_css_selector- #id- .class_name- div- div p>a
7、find_elements_by_link_text- 根据a标签的文本,根据a标签

示例:

需求:打开淘宝搜索IPhone,再打开百度–>回退–>前进

# 需求:打开淘宝搜索IPhone,再打开百度-->回退-->前进
from selenium import webdriver
from time import sleep# 实例化浏览器(谷歌)
path = r'D:\01-soft\12-spider_chromedriver\chromedriver.exe'
bro = webdriver.Chrome(executable_path=path)# 发起请求
bro.get('https://www.taobao.com/')# 标签定位
search_input = bro.find_element_by_id('q')
# 标签交互
search_input.send_keys('Iphone')# 执行一组js程序
# scrollTo(0,document.body.scrollHeight) scrollTo(x,y)屏幕滚动,x表示左右滚动,y表示上下滚动
# document.body.scrollHeight表示向下滚动一屏
bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
sleep(2)
# 点击搜索按钮
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()
from selenium.webdriver import Chrome, Firefoxpath = r'D:\01-soft\12-spider_chromedriver\chromedriver.exe'
chrome = Chrome(path)
# chrome = Firefox(executable_path=r'D:\01-soft\12-spider_chromedriver\geckodriver.exe')# 打开必应
chrome.get('http://cn.bing.com')# 截图工具
chrome.save_screenshot('bing.png')chrome.close()  # 关闭页签,如果浏览器只有一个页签,即也会退出浏览器
chrome.quit()  # 退出程序(关闭浏览器)

10.4. selenium处理iframe

- selenium处理iframe- 如果定位的标签存在于iframe标签之中,则必须使用switch_to.frame(id)- 动作链(拖动):from selenium.webdriver import ActionChains- 实例化一个动作链对象:action = ActionChains(bro)- click_and_hold(div):长按且点击操作- move_by_offset(x,y)- perform()让动作链立即执行- action.release()释放动作链对象

示例1:

from selenium import webdriver
from time import sleep
#导入动作链对应的类
from selenium.webdriver import ActionChains
path = r'D:\01-soft\12-spider_chromedriver\chromedriver.exe'
bro = webdriver.Chrome(executable_path=path)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()立即执行动作链操作#move_by_offset(x,y):x水平方向 y竖直方向action.move_by_offset(17,0).perform()sleep(0.5)#释放动作链
action.release()bro.quit()

示例2:

需求:模拟登陆qq空间

from selenium import webdriver
from time import sleeppath = r'D:\01-soft\12-spider_chromedriver\chromedriver.exe'
bro = webdriver.Chrome(executable_path=path)bro.get('https://qzone.qq.com/')bro.switch_to.frame('login_frame')a_tag = bro.find_element_by_id("switcher_plogin")
a_tag.click()userName_tag = bro.find_element_by_id('u')
password_tag = bro.find_element_by_id('p')
sleep(1)
userName_tag.send_keys('1174333100')
sleep(1)
password_tag.send_keys('zhb1174333100')
sleep(1)
btn = bro.find_element_by_id('login_button')
btn.click()sleep(3)bro.quit()

10.5. 交互

1.点击 click()
2.输入 send_keys()
3.模拟 JS滚动var q = window.document.documentElement.scrollTop=10000execute_script() 执行js代码今日头条
# ......
# 模拟滚动操作
# document.documentElement 表示当前页面元素,指<html>
# 获取窗口高度
print(chrome.get_window_rect(), chrome.get_window_size())
for n in range(50):script = 'var q=window.document.documentElement.scrollTop=%s' % ((n+1)*500)chrome.execute_script(script)time.sleep(0.5)
# ......

10.6. 页面异步ajax的解决办法

原因:

由于网页中有ajax的异步执行的js, 导致driver.get()之后查找元素报 NoSuchElementException异常

导包:

from selenium.webdriver.common.by import By
from selenium.webdriver.support import ui
from selenium.webdriver.support import expected_conditions as EC

解决:

# 等待某一个Element出现为止,否则一直阻塞下去,不过可以设置一个超时时间ui.WebDriverWait(driver, 60).until(EC.visibility_of_all_elements_located((By.CLASS_NAME, 'soupager')))

10.7. switch的用法

原因:

- 当页面中出现对话框 alert,或内嵌窗口iframe
- 如果查找的元素节点在alert或iframe中的话,则需要切入到alert或iframe中

解决:

1. 查找iframe标签对象iframe = driver.find_element_by_id('login_frame')
2. 切换到iframe中driver.switch_to.frame(iframe)

10.8. 获取浏览器的页签

brower.window_handlers[0]  # 第一个页签,一般都存在
brower.window_handlers[1]  # 如果浏览器打开新的资源在新的页签时,可以获取,如果不存在第二个页签,则会报错

退出:browser.quit()

案例:

登录邮箱(内嵌窗口)

import timefrom selenium.webdriver import Chromechrome = Chrome(r'D:\01-soft\12-spider_chromedriver\chromedriver.exe')chrome.get('https://mail.qq.com')  # 阻塞方法,等待网页中的所有js执行完毕# 以下两个输入框是在<iframe>内嵌窗口中
login_frame = chrome.find_element_by_id('login_frame')
chrome.switch_to.frame(login_frame)  # 切入到内嵌窗口中uesr_input = chrome.find_element_by_id('u')
pwd_input = chrome.find_element_by_id('p')uesr_input.send_keys('123344556@qq.com')
pwd_input.send_keys('zdx1233456')# 查找登录按钮,并点击
chrome.find_element_by_id('login_button').click()

10.9 无头浏览器

from selenium import webdriver
from time import sleep
#实现无可视化界面
from selenium.webdriver.chrome.options import Options
#实现规避检测
from selenium.webdriver import ChromeOptions#实现无可视化界面的操作
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')#实现规避检测
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])#如何实现让selenium规避被检测到的风险
path = r'D:\01-soft\12-spider_chromedriver\chromedriver.exe'
bro = webdriver.Chrome(path,chrome_options=chrome_options,options=option)#无可视化界面(无头浏览器) phantomJs
bro.get('https://www.baidu.com')print(bro.page_source)
sleep(2)
bro.quit()

10.10 12306模拟登陆

10.10.1. 超级鹰验证码使用

十一、scrapy框架

11.1. 介绍

什么是scrapy:

​ Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。

功能:高性能的持久化存储,异步的数据下载,高性能的数据解析,分布式

官方网站:

https://doc.scrapy.org/en/latest/
http://www.scrapyd.cn/doc/    中文
http://scrapy-chs.readthedocs.io/zh_CN/latest/   中文

11.2. scrapy框架的基本使用

- 环境的安装:```- mac or linux:pip install scrapy- windows:- pip install wheel- 下载twisted,下载地址为http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted- 安装twisted:pip install Twisted‑17.1.0‑cp36‑cp36m‑win_amd64.whl- pip install pywin32- pip install scrapy测试:在终端里录入scrapy指令,没有报错即表示安装成功!```
  • 创建一个工程:

    - scrapy startproject xxxPro
    
  • cd xxxPro

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

    - scrapy  genspider  spiderName  www.xxx.com
    
  • 执行工程:

    - scrapy crawl spiderName
    

11.3. 框架组成

11.3.1. 五个核心

  • engine 引擎, 协调其它四个组件之间的联系,即与其它四个组件进行通信,也是scrapy框架的核心。自动运行,无需关注,会自动组织所有的请求对象,分发给下载器

  • spider 爬虫类, 爬虫程序的编写代码所在, 也是发起请求的开始的位置。spider发起的请求,经过engine转入到scheduler中。

    请求成功之后的数据解析

    - scrapy.Spider 普通的爬虫
    - scrapy.CrawlSpider- 可设置规则的爬虫类- Rule 规则类
    - 开始的函数- start_requests()
    
  • scheduler 调度器, 调度所有的请求(优先级高,则会先执行)。当执行某一个请求时,由engine转入到downloader中。

  • donwloader 下载器, 实现请求任务的执行,从网络上请求数据,将请求到的数据封装成响应对象,并将响应的对象返回给engine。engine将数据响应的数据对象(以回调接口方式)回传给它的爬虫类对象进行解析。

  • itempipeline 数据管道, 当spider解析完成后,将数据经engine转入到此(数据管道)。再根据数据类型,进行数据处理(图片、文本)

    1. 清理HTML数据
    2. 验证爬取的数据(检查item包含某些字段)
    3. 查重(并丢弃)
    4. 将爬取结果保存到数据库中
    5. 对图片数据进行下载
    

    scrapy框架逻辑图:

流程:

1、爬虫引擎获得初始请求开始抓取。 
2、爬虫引擎开始请求调度程序,并准备对下一次的请求进行抓取。 
3、爬虫调度器返回下一个请求给爬虫引擎。 
4、引擎请求发送到下载器,通过下载中间件下载网络数据。 
5、一旦下载器完成页面下载,将下载结果返回给爬虫引擎。 
6、引擎将下载器的响应通过中间件返回给爬虫进行处理。 
7、爬虫处理响应,并通过中间件返回处理后的items,以及新的请求给引擎。 
8、引擎发送处理后的items到项目管道,然后把处理结果返回给调度器,调度器计划处理下一个请求抓取。 
9、重复该过程(继续步骤1),直到爬取完所有的url请求

11.3.3. scrapy使用

  • 创建项目命令
    • scrapy startproject 项目名称
  • 创建爬虫命令
    • scrapy genspider 爬虫名 域名
  • 启动爬虫命令
    • scrapy crawl 爬虫名
  • 调试爬虫命令
    • scrapy shell url
    • scrapy shell
      • fetch(url)

目录结构:

- spiders- __init__.py- 自定义的爬虫文件.py
- __init__.py
- items.py定义数据结构的地方,是一个继承自scrapy.Item类, 属性字段的类型是 scrapy.Field()注意: scrapy.Item类,实际是一个dict字典, 所以在spider的parse()函数返回的迭代元素应该是dict字典对象,且字典的key与       item的Field()相对应
- middlewares.py中间件, 用于调整业务逻辑
- pipelines.py管道文件,里面只有一个类,用于处理下载数据的后续处理
- settings.py配置文件比如:是否遵守robots协议User-Agent定义等

11.3.3. 爬虫文件

解析函数:

  • parse_detail(self, response: Response)

    • 解析数据的回调函数,response保存了下载的数据,可以在此函数内对其进行解析,通常使用xpath,parse()函数,如果有返回值,必须返回可迭代的对象

    • Response的类方法:

      - selector()
      - css()  样式选择器 , 返回Selector选择器的可迭代(列表)对象- scrapy.selector.SelectorList 选择器列表- x()/xpath()- scrapy.selector.Selector 选择器- 样式选择器提取属性或文本- `::text` 提取文本- `::attr("属性名")` 提取属性
      - xpath() xpath路径xpath路径,同lxml的xpath()写法
      - 选择器常用方法- css()/xpath()- extract()  提取选择中所有内容,返回是list- extract_first()/get() 提取每个选择器中的内容, 返回是文本
      
      - response 是 scrapy.http.response.HtmlResponse类对象
      - response.css('.class属性')  拿到class属性的标签
      - response.css('.contentHerf::attr("href")')  获取标签的href属性
      - response.xpath()scrapy.selector.Selector返回Selector对象内部写法:self.selector.xpath()
      - extract()/getall()  Selector对象的方法,用于获取Selector对象的内容 即提取是Selector对象中的data属性response.xpath('//title/text()').extract() 返回listresponse.css('').xpath() 先使用css选择标签元素,再通过xpath提取内容
      - extract_first()/get()提取第一条内容
      - css()方法中字符串:#id, .class, div,  div>p, ::attr(‘属性名’), ::text 标签文本
      
    • Request类

      scrapy.http.Request

      请求对象的属性:

      - url
      - callback 解释数据的回调函数对象
      - headers 请求头
      - priority 请求的优先
      Request()中的meta属性可以向下一个解析函数传递数据(元数据)注意:meta是dict字典格式,value不能是一个引用对象 (scrapy 1.5版本)
      Request()中的priority 请求在scheduler调度器中的优先级,值越高,级别越高,则优先下载
      Request()中的dont_filter为False表示过滤重复下载的请求,为True则不过滤
      

11.3.4. 示例:

# 示例
import scrapy
from scrapy import Request
from scrapy.http import Response, HtmlResponse
from scrapy.selector import SelectorListfrom qsbk import cookie_class TxtSpider(scrapy.Spider):  # 继承父类name = 'jokes'  # 糗事百科的段子allowed_domains = ['qiushibaike.com']  # 限制请求URL中的域(host)是否允许下载start_urls = ['https://www.qiushibaike.com/text/']  # 起始请求的url资源列表BASE_URL = 'https://www.qiushibaike.com'def parse(self, response: HtmlResponse):# 获取所有文章for article_div_selector in response.css('.article'):author_item = article_div_selector.css('.author img')[0].attribauthor_item['name'] = author_item.pop('alt')author_item['detail_href'] = article_div_selector.css('.contentHerf::attr("href")').get()# yield author_item# 发起详情的请求# Request()中的meta属性可以向下一个解析函数传递数据(元数据)# 注意:meta是dict字典格式,value不能是一个引用对象 (scrapy 1.5版本)# Request()中的priority 请求在scheduler调度器中的优先级,值越高,级别越高,则优先下载# Request()中的dont_filter为False表示过滤重复下载的请求,为True则不过滤yield Request(self.BASE_URL + author_item['detail_href'],callback=self.parse_detail,headers={'Referer': response.url},cookies=cookie_.get_cookies(),meta={'author': author_item['name'],'author_head': author_item['src']},priority=100,dont_filter=False)def parse_detail(self, response: Response):# response.request# print('parse_detail-->', response.request.meta)# print('parse_detail-->', response.meta)item = {'author': response.meta['author'],'author_head': response.meta['author_head']}item['title'] = response.css('.article-title::text').get()item['publish_time'] = response.css('.stats-time::text').get()item['content'] = '\n'.join([c.replace('\xa0', ' ')for c in response.css('.content::text').getall()])yield itemdef parse_test(self, response: HtmlResponse):# scrapy.http.response.html.HtmlResponse# print(type(response), response)# class:  contentHerf  ->a标签# css()/xpath() -> 'scrapy.selector.unified.SelectorList# scrapy.selector.SelectorList/Selector# -> xpath()/css() 查询子孙元素# -> get()/extract_first(),getall()/extract()  提取是Selector对象中的data属性# -> attrib 属性方法, 只有Selector类实例里面存在(要求:选择的是元素不是元素的属性)# css()方法中字符串:#id, .class, div,  div>p, ::attr(‘属性名’), ::text 标签文本a_elements: SelectorList = response.css('.contentHerf::attr("href")')  # list[<Selector>,...]author_elements = response.css('.author').xpath('.//img')for i, author_element in enumerate(author_elements):item: dict = author_element.attrib  # 拿到src和alt两个属性的dict# 修改alt key的名称为 nameitem['name'] = item.pop('alt')item['detail_href'] = a_elements[i].get()yield item

11.3.5. scrapy shell

终端调试工具:

- 终端输入scrapy shell "http://www.baidu.com" 在终端会得到一个response对象,可以直接使用
- response.xpath() 使用xpath路径查询特定元素,返回一个selector对象的列表
- response.css() 使用css_selector查询元素,返回一个selector对象

11.4. scrapy 持久化存储

11.4.1 基于终端命令存储

- 基于终端指令:- 要求:只可以将parse方法的返回值存储到本地的文本文件中- 注意:持久化存储对应的文本文件的类型只可以为:'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle- 指令:scrapy crawl xxx -o filePath- 好处:简介高效便捷- 缺点:局限性比较强(数据只可以存储到指定后缀的文本文件中)

11.4.2. 基于管道持久化存储

- 基于管道:- 编码流程:- 数据解析- 在item类中定义相关的属性- 将解析的数据封装存储到item类型的对象- 将item类型的对象提交给管道进行持久化存储的操作- 在管道类的process_item中要将其接受到的item对象中存储的数据进行持久化存储操作- 在配置文件中开启管道- 好处:- 通用性强。

11.4.3. 示例:

spider文件—> qiubai.py

import scrapy
from qiubaiPro.items import QiubaiproItemclass QiubaiSpider(scrapy.Spider):name = 'qiubai'# allowed_domains = ['www.xxx.com']start_urls = ['https://www.qiushibaike.com/text/']# 基于命令存储# def parse(self, response):#     #解析:作者的名称+段子内容#     div_list = response.xpath('//div[@id="content-left"]/div')#     all_data = [] #存储所有解析到的数据#     for div in div_list:#         #xpath返回的是列表,但是列表元素一定是Selector类型的对象#         #extract可以将Selector对象中data参数存储的字符串提取出来#         # author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()#         author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()#         #列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来#         content = div.xpath('./a[1]/div/span//text()').extract()#         content = ''.join(content)##         dic = {#             'author':author,#             'content':content#         }##         all_data.append(dic)###     return all_datadef parse(self, response):# 解析:作者的名称+段子内容div_list = response.xpath('//div[@id="content-left"]/div')all_data = []  # 存储所有解析到的数据for div in div_list:# xpath返回的是列表,但是列表元素一定是Selector类型的对象# extract可以将Selector对象中data参数存储的字符串提取出来# author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()author = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span/h2/text()').extract_first()# 列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来content = div.xpath('./a[1]/div/span//text()').extract()content = ''.join(content)item = QiubaiproItem()item['author'] = authoritem['content'] = contentyield item  # 将item提交给了管道

items.py 文件 —>在item类中定义相关的属性

import scrapyclass QiubaiproItem(scrapy.Item):# define the fields for your item here like:author = scrapy.Field()content = scrapy.Field()# pass

pipeline.py文件

import pymysqlclass QiubaiproPipeline(object):fp = None#重写父类的一个方法:该方法只在开始爬虫的时候被调用一次def open_spider(self,spider):print('开始爬虫......')self.fp = open('./qiubai.txt','w',encoding='utf-8')#专门用来处理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()#管道文件中一个管道类对应将一组数据存储到一个平台或者载体中
class mysqlPileLine(object):conn = Nonecursor = Nonedef open_spider(self,spider):self.conn = pymysql.Connect(host='116.85.7.220',port=3307,user='root',password='root',db='qiubai',charset='utf8')def process_item(self,item,spider):self.cursor = self.conn.cursor()try:self.cursor.execute('insert into qiubai values("%s","%s")'%(item["author"],item["content"]))self.conn.commit()except Exception as e:print(e)self.conn.rollback()return itemdef close_spider(self,spider):self.cursor.close()self.conn.close()

settings.py文件中开启管道

# ...
ITEM_PIPELINES = {'qiubaiPro.pipelines.QiubaiproPipeline': 300,'qiubaiPro.pipelines.mysqlPileLine': 301,#300表示的是优先级,数值越小优先级越高
}
# ...

【扩展】:

- 面试题:将爬取到的数据一份存储到本地一份存储到数据库,如何实现?- 管道文件中一个管道类对应的是将数据存储到一种平台- 爬虫文件提交的item只会给管道文件中第一个被执行的管道类接受- process_item中的return item表示将item传递给下一个即将被执行的管道类

11.4.4. 全站数据爬取

- 基于Spider的全站数据爬取- 就是将网站中某板块下的全部页码对应的页面数据进行爬取- 需求:爬取校花网中的照片的名称- 实现方式:- 将所有页面的url添加到start_urls列表(不推荐)- 自行手动进行请求发送(推荐)- 手动请求发送:- yield scrapy.Request(url,callback):callback专门用做于数据解析

示例:spider.py文件

import scrapyclass XiaohuaSpider(scrapy.Spider):name = 'xiaohua'# allowed_domains = ['www.xxx.com']start_urls = ['http://www.521609.com/meinvxiaohua/']#生成一个通用的url模板(不可变)url = 'http://www.521609.com/meinvxiaohua/list12%d.html'page_num = 2def parse(self, response):li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li')for li in li_list:img_name = li.xpath('./a[2]/b/text() | ./a[2]/text()').extract_first()print(img_name)if self.page_num <= 11:new_url = format(self.url%self.page_num)self.page_num += 1#手动请求发送:callback回调函数是专门用作于数据解析yield scrapy.Request(url=new_url,callback=self.parse)

11.5. 请求传参

- 请求传参- 使用场景:如果爬取解析的数据不在同一张页面中。(深度爬取)- 需求:爬取boss的岗位名称,岗位描述

示例:boos.py文件

import scrapy
from bossPro.items import BossproItemclass BossSpider(scrapy.Spider):name = 'boss'# allowed_domains = ['www.xxx.com']start_urls = ['https://www.zhipin.com/job_detail/?query=python&city=101010100&industry=&position=']url = 'https://www.zhipin.com/c101010100/?query=python&page=%d'page_num = 2# 回调函数接受item # 详情页数据解析def parse_detail(self, response):item = response.meta['item']job_desc = response.xpath('//*[@id="main"]/div[3]/div/div[2]/div[2]/div[1]/div//text()').extract()job_desc = ''.join(job_desc)# print(job_desc)item['job_desc'] = job_descyield item# 解析首页中的岗位名称def parse(self, response):li_list = response.xpath('//*[@id="main"]/div/div[3]/ul/li')for li in li_list:item = BossproItem()job_name = li.xpath('.//div[@class="info-primary"]/h3/a/div[1]/text()').extract_first()item['job_name'] = job_name# print(job_name)detail_url = 'https://www.zhipin.com' + li.xpath('.//div[@class="info-primary"]/h3/a/@href').extract_first()# 对详情页发请求获取详情页的页面源码数据# 手动请求的发送# 请求传参:meta={},可以将meta字典传递给请求对应的回调函数yield scrapy.Request(detail_url, callback=self.parse_detail, meta={'item': item})# 分页操作if self.page_num <= 3:new_url = format(self.url % self.page_num)self.page_num += 1yield scrapy.Request(new_url, callback=self.parse)

11.4. 图片数据 Imagepipeline

- 图片数据爬取之ImagesPipeline- 基于scrapy爬取字符串类型的数据和爬取图片类型的数据区别?- 字符串:只需要基于xpath进行解析且提交管道进行持久化存储- 图片:xpath解析出图片src的属性值。单独的对图片地址发起请求获取图片二进制类型的数据- ImagesPipeline:- 只需要将img的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制类型的数据,而且还会帮我们进行持久化存储。- 使用流程:- 数据解析(图片的地址)- 将存储图片地址的item提交到制定的管道类- 在管道文件中自定制一个基于ImagesPipeLine的一个管道类- get_media_request- file_path- item_completed # 将item返回给下一个管道方法- 在配置文件中:- 指定图片存储的目录:IMAGES_STORE = './imgs_zhb'- 指定开启的管道:自定制的管道类

示例:

需求:爬取站长素材中的高清图片

1、爬虫脚本(解析数据)img.py

import scrapy
from imgspro.items import ImgsproItemclass ImgSpider(scrapy.Spider):name = 'img'# allowed_domains = ['www.xxx.com']start_urls = ['http://sc.chinaz.com/tupian/']def parse(self, response):div_list = response.xpath('//div[@id="container"]/div')for div in div_list:# 注意:使用伪装属性src = div.xpath('./div/a/img/@src2').extract_first()# print(src)# 实例化item对象item = ImgsproItem()item['src'] = srcyield item # 提交item到管道

2、items.py 文件 —>在item类中定义相关的属性

import scrapyclass ImgsproItem(scrapy.Item):# define the fields for your item here like:src = scrapy.Field()# pass

3、在管道文件中自定制一个基于ImagesPipeLine的一个管道类

pipeline.py文件

from scrapy.pipelines.images import ImagesPipeline
import scrapy
class ImgsPileLine(ImagesPipeline):#就是可以根据图片地址进行图片数据的请求def get_media_requests(self, item, info):yield scrapy.Request(item['src'])#指定图片存储的路径def file_path(self, request, response=None, info=None):imgName = request.url.split('/')[-1]return imgNamedef item_completed(self, results, item, info):return item #返回给下一个即将被执行的管道类

4、在配置文件中:

settings.py文件

...
LOG_LEVEL = 'ERROR'USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) # Obey robots.txt rules
ROBOTSTXT_OBEY = False...
ITEM_PIPELINES = {'imgsPro.pipelines.ImgsPileLine': 300,
}
...
#指定图片存储的目录
IMAGES_STORE = './imgs_zhb'

11.5. 中间件

中间件- 爬虫中间件- 下载中间件【重要】- 位置:引擎和下载器之间- 作用:批量拦截到整个工程中所有的请求和响应- 拦截请求:- UA伪装:process_request- 代理IP:process_exception:return request- 拦截响应:- 篡改响应数据,响应对象- 需求:爬取网易新闻中的新闻数据(标题和内容)- 1.通过网易新闻的首页解析出五大板块对应的详情页的url(没有动态加载)- 2.每一个板块对应的新闻标题都是动态加载出来的(动态加载)- 3.通过解析出每一条新闻详情页的url获取详情页的页面源码,解析出新闻内容

11.5.1. 拦截请求:

middlewares.py

from scrapy import signalsimport randomclass MiddleproDownloaderMiddleware(object):# 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.# 定义一个'User-Agent'池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)#为了验证代理的操作是否生效request.meta['proxy'] = 'http://183.146.213.198:80'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 IgnoreRequestreturn response#拦截发生异常的请求def process_exception(self, request, exception, spider):if request.url.split(':')[0] == 'http':#代理request.meta['proxy'] = 'http://'+random.choice(self.PROXY_http)else:request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https)return request  #将修正之后的请求对象进行重新的请求发送

11.5.2. 拦截响应 :

需求:爬取网易新闻数据

wangyi.py文件

import scrapy
from selenium import webdriver
from wangyiPro.items import WangyiproItemclass WangyiSpider(scrapy.Spider):name = 'wangyi'# allowed_domains = ['www.cccom']start_urls = ['https://news.163.com/']models_urls = []  # 存储五个板块对应详情页的url# 解析五大板块对应详情页的url# 实例化一个浏览器对象def __init__(self):self.bro = webdriver.Chrome(executable_path=r'D:\01-soft\12-spider_chromedriver\chromedriver.exe')def parse(self, response):li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')alist = [3, 4, 6, 7, 8]for index in alist:model_url = li_list[index].xpath('./a/@href').extract_first()self.models_urls.append(model_url)# 依次对每一个板块对应的页面进行请求for url in self.models_urls:  # 对每一个板块的url进行请求发送yield scrapy.Request(url, callback=self.parse_model)# 每一个板块对应的新闻标题相关的内容都是动态加载def parse_model(self, response):  # 解析每一个板块页面中对应新闻的标题和新闻详情页# response.xpath()div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div/div/ul/li/div/div')for div in div_list:title = div.xpath('./div/div[1]/h3/a/text()').extract_first()new_detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()item = WangyiproItem()item['title'] = title# 对新闻详情页的url发起请求yield scrapy.Request(url=new_detail_url, callback=self.parse_detail, meta={'item': item})def parse_detail(self, response):  # 解析新闻内容content = response.xpath('//*[@id="endText"]//text()').extract()content = ''.join(content)item = response.meta['item']item['content'] = contentyield item# 退出浏览器def closed(self, spider):self.bro.quit()

middleware.py文件

from scrapy.http import HtmlResponse
from time import sleep
class WangyiproDownloaderMiddleware(object):# 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.def process_request(self, request, spider):# Called for each request that goes through the downloader# middleware.# Must either:# - return None: continue processing this request# - or return a Response object# - or return a Request object# - or raise IgnoreRequest: process_exception() methods of#   installed downloader middleware will be calledreturn None#该方法拦截五大板块对应的响应对象,进行篡改def process_response(self, request, response, spider):#spider爬虫对象bro = spider.bro #获取了在爬虫类中定义的浏览器对象#挑选出指定的响应对象进行篡改#通过url指定request#通过request指定responseif request.url in spider.models_urls:bro.get(request.url) #五个板块对应的url进行请求sleep(3)page_text = bro.page_source  #包含了动态加载的新闻数据#response #五大板块对应的响应对象#针对定位到的这些response进行篡改#实例化一个新的响应对象(符合需求:包含动态加载出的新闻数据),替代原来旧的响应对象#如何获取动态加载出的新闻数据?#基于selenium便捷的获取动态加载数据new_response = HtmlResponse(url=request.url,body=page_text,encoding='utf-8',request=request)return new_responseelse:#response #其他请求对应的响应对象return responsedef process_exception(self, request, exception, spider):# Called when a download handler or a process_request()# (from other downloader middleware) raises an exception.# Must either:# - return None: continue processing this exception# - return a Response object: stops process_exception() chain# - return a Request object: stops process_exception() chainpass

items.py文件

import scrapyclass WangyiproItem(scrapy.Item):# define the fields for your item here like:title = scrapy.Field()content = scrapy.Field()

settings.py文件

# 打开下载中间件
...
DOWNLOADER_MIDDLEWARES = {'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
}

11.5.3. 爬虫中间件

SpiderMiddleware

@classmethod
from_crawler(cls, crawler) 当创建了spider之后创建当前的中间件类实例同时, 连接打开爬虫类的信号处理process_spider_input(self, response, spider): 可以返回 None 和 raise Exception返回None,表示放行,不拦截响应被解析raise Exception 抛出异常,到达了process_spider_exception()方法中process_spider_output(self, response, result, spider)可以返回 item和request默认: for r in result: yield rprocess_spider_exception(self, response, exception, spider)可以返回 None/Request/Itemprocess_start_requests(self, start_requests, spider)必须返回Request

11.5.4. 下载中间件

process_request(self, request,spider): 返回对象 :None|Request|Response|raise IgnoreRequest1. 可以返回哪些对象?? 返回None,继续处理这个请求,或者返回一个响应对象或者返回一个请求对象,或者或触发IgnoreRequest2. 什么时候使用此函数 下载器向引擎返回响应的时候
process_response(self,request, response, spider)1. 可以返回对象 Request|Response|raise IgnoreRequest2. 使用场景是什么? 从下载器返回响应时调用process_exception(self,request,exception,  spider)返回对象:None

11.6. 总结核心模块和类

scrapy.Spider 普通爬虫类的父类- name  爬虫名, 在scrapy crawl 命令中使用- start_urls 起始的请求URL资源列表- allowed_domains 允许访问的服务器域名列表- start_requests() 方法,爬虫启动后执行的第一个方法(流程中的第一步:发起请求)- logger  当前爬虫的日志记录器- parse() 默认请求成功后,对响应的数据默认解析的方法
scrapy.Spider  普通爬虫类的父类- name 爬虫名称- start ——urls 起始的请求URL资源列表
scrapy.Request-初始化时参数: url, method, body, encoding, callback, headers, cookiesn dont_filter, priority, meta- meta dict格式, 可以设置proxy 代理
scrapy.http.Response/TextResponse/HtmlResponse- status 响应状态码- meta 响应的原信息,包含request中的meta信息- url  请求的URL- request 请求对象- headers 响应头- body 字节数据- text 文本数据- css()/xpath() 提取HTML元素信息(基于lxml/bs4)
scrapy.Item 类, 类似于dict, 作用: 解析出不同结构的数据时,使用不同的Item类,便于数据管道处理。
scrapy.Filed 类,用于Item子类中声明字段属性(数据属性)
scrapy.signals 信号- spider_opened 打开爬虫- spider_closed 关闭爬虫- spider_error  爬虫出现异常
优先级:
- 请求优先级: 值高 == 优先级大, 值低 == 优先级小配置settings中的优先级
- 管道优先级: 值高 == 优先级小, 值低 == 优先级大
- 中间件优先级: 值高 == 优先级小, 值低 == 优先级大

十二、规则爬虫

crawlspider

CrawlSpider是一个类,它的父类就是scrapy.Spider,所以CrawlSpider不仅有Spider的功能,还有自己独有的功能

CrawlSpider可以定义规则,再解析html内容的时候,可以根据链接规则提取出指定的链接,然后再向这些链接发送请求,所以,如果有需要跟进链接的需求,就可以使用CrawlSpider来实现

12.1. 流程

- CrawlSpider:类,Spider的一个子类- 全站数据爬取的方式- 基于Spider:手动请求- 基于CrawlSpider- CrawlSpider的使用:- 创建一个工程- cd XXX- 创建爬虫文件(CrawlSpider):- scrapy genspider -t crawl xxx www.xxxx.com- 链接提取器:- 作用:根据指定的规则(allow)进行指定链接的提取- 规则解析器:- 作用:将链接提取器提取到的链接进行指定规则(callback)的解析#需求:爬取sun网站中的编号,新闻标题,新闻内容,标号- 分析:爬取的数据没有在同一张页面中。- 1.可以使用链接提取器提取所有的页码链接- 2.让链接提取器提取所有的新闻详情页的链接

#需求:爬取sun网站中的编号,新闻标题,新闻内容,标号

sun.py文件

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from sunPro.items import SunproItem,DetailItem#需求:爬取sun网站中的编号,新闻标题,新闻内容,标号
class SunSpider(CrawlSpider):name = 'sun'# allowed_domains = ['www.xxx.com']start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=']#链接提取器:根据指定规则(allow="正则")进行指定链接的提取link = LinkExtractor(allow=r'type=4&page=\d+')link_detail = LinkExtractor(allow=r'question/\d+/\d+\.shtml')rules = (#规则解析器:将链接提取器提取到的链接进行指定规则(callback)的解析操作Rule(link, callback='parse_item', follow=True),#follow=True:可以将链接提取器 继续作用到 连接提取器提取到的链接 所对应的页面中Rule(link_detail,callback='parse_detail'))#http://wz.sun0769.com/html/question/201907/421001.shtml#http://wz.sun0769.com/html/question/201907/420987.shtml#解析新闻编号和新闻的标题#如下两个解析方法中是不可以实现请求传参!#如法将两个解析方法解析的数据存储到同一个item中,可以以此存储到两个itemdef parse_item(self, response):#注意:xpath表达式中不可以出现tbody标签tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')for tr in tr_list:new_num = tr.xpath('./td[1]/text()').extract_first()new_title = tr.xpath('./td[2]/a[2]/@title').extract_first()item = SunproItem()item['title'] = new_titleitem['new_num'] = new_numyield item#解析新闻内容和新闻编号def parse_detail(self,response):new_id = response.xpath('/html/body/div[9]/table[1]//tr/td[2]/span[2]/text()').extract_first()new_content = response.xpath('/html/body/div[9]/table[2]//tr[1]//text()').extract()new_content = ''.join(new_content)# print(new_id,new_content)item = DetailItem()item['content'] = new_contentitem['new_id'] = new_idyield item

items.py

import scrapyclass SunproItem(scrapy.Item):# define the fields for your item here like:title = scrapy.Field()new_num = scrapy.Field()class DetailItem(scrapy.Item):new_id = scrapy.Field()content = scrapy.Field()

pipeline.py

class SunproPipeline(object):def process_item(self, item, spider):#如何判定item的类型#将数据写入数据库时,如何保证数据的一致性if item.__class__.__name__ == 'DetailItem':print(item['new_id'],item['content'])passelse:print(item['new_num'],item['title'])return item

settings.py

...
ITEM_PIPELINES = {'sunPro.pipelines.SunproPipeline': 300,
}
...

十三、分布式爬虫

- 分布式爬虫- 概念:我们需要搭建一个分布式的机群,让其对一组资源进行分布联合爬取。- 作用:提升爬取数据的效率- 如何实现分布式?- 安装一个scrapy-redis的组件- 原生的scarapy是不可以实现分布式爬虫,必须要让scrapy结合着scrapy-redis组件一起实现分布式爬虫。- 为什么原生的scrapy不可以实现分布式?- 调度器不可以被分布式机群共享- 管道不可以被分布式机群共享- scrapy-redis组件作用:- 可以给原生的scrapy框架提供可以被共享的管道和调度器- 实现流程- 创建一个工程- 创建一个基于CrawlSpider的爬虫文件- 修改当前的爬虫文件:- 导包:from scrapy_redis.spiders import RedisCrawlSpider- 将start_urls和allowed_domains进行注释- 添加一个新属性:redis_key = 'sun' 可以被共享的调度器队列的名称- 编写数据解析相关的操作- 将当前爬虫类的父类修改成RedisCrawlSpider- 修改配置文件settings- 指定使用可以被共享的管道:ITEM_PIPELINES = {'scrapy_redis.pipelines.RedisPipeline': 400}- 指定调度器:# 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"# 使用scrapy-redis组件自己的调度器SCHEDULER = "scrapy_redis.scheduler.Scheduler"# 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据SCHEDULER_PERSIST = True- 指定redis服务器:- redis相关操作配置:- 配置redis的配置文件:- linux或者mac:redis.conf- windows:redis.windows.conf- 代开配置文件修改:- 将bind 127.0.0.1进行删除- 关闭保护模式:protected-mode yes改为no- 结合着配置文件开启redis服务- redis-server 配置文件- 启动客户端:- redis-cli- 执行工程:- scrapy runspider xxx.py- 向调度器的队列中放入一个起始的url:- 调度器的队列在redis的客户端中- lpush xxx www.xxx.com- 爬取到的数据存储在了redis的proName:items这个数据结构中

示例:

day10 —> dm530项目

存储MondoDB

—>NoSQL仓库.xmind

十四、增量式爬虫

增量式爬虫- 概念:监测网站数据更新的情况,只会爬取网站最新更新出来的数据。- 分析:- 指定一个起始url- 基于CrawlSpider获取其他页码链接- 基于Rule将其他页码链接进行请求- 从每一个页码对应的页面源码中解析出每一个电影详情页的URL- 核心:检测电影详情页的url之前有没有请求过- 将爬取过的电影详情页的url存储- 存储到redis的set数据结构- 对详情页的url发起请求,然后解析出电影的名称和简介- 进行持久化存储

movie.py

# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rulefrom redis import Redis
from moviePro.items import MovieproItem
class MovieSpider(CrawlSpider):name = 'movie'# allowed_domains = ['www.ccc.com']start_urls = ['https://www.4567tv.tv/frim/index1.html']rules = (Rule(LinkExtractor(allow=r'/frim/index1-\d+\.html'), callback='parse_item', follow=True),)# 创建redis链接对象conn = Redis(host='127.0.0.1', port=6379)#用于解析每一个页码对应页面中的电影详情页的urldef parse_item(self, response):li_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li')for li in li_list:# 获取详情页的urldetail_url = 'https://www.4567tv.tv' + li.xpath('./div/a/@href').extract_first()# 将详情页的url存入redis的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 = MovieproItem()item['name'] = response.xpath('/html/body/div[1]/div/div/div/div[2]/h1/text()').extract_first()item['desc'] = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[2]//text()').extract()item['desc'] = ''.join(item['desc'])yield item

pipelines.py

class MovieproPipeline(object):conn = Nonedef open_spider(self,spider):self.conn = spider.conndef process_item(self, item, spider):dic = {'name':item['name'],'desc':item['desc']}# print(dic)self.conn.lpush('movieData',dic)return item

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/491953.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用人工神经网络控制真实大脑,MIT的科学家做到了

来源&#xff1a;网络大数据三位研究者分别是 MIT 大脑与行为科学系主任 James DiCarlo、MIT 博士后 Pouya Bashivan 和 Kohitij Kar。相关论文发表在 5 月 2 日 Science 的网络版上。论文链接&#xff1a; http s://www.biorxiv.org/content/10.1101/461525v1研究人员表示&…

学习卫星菜单

学会坚持的自己写的底部中间菜单 转自http://www.cnblogs.com/persist-confident/p/4487386.html 看了hyman老师的视频&#xff0c;听起来有点迷糊&#xff0c;所以就想把实现卫星菜单的实现总结一下。长话短说&#xff0c;下面总结一下&#xff1a; 一、自定义ViewGroup1&…

Python 的垃圾回收回收机制(源码)

python内存管理及垃圾回收 1. 引用计数器 1.1 环状双向连表 refchain 在python程序中创建的任何对象都会放在refchain链表中&#xff0c;并且可以通过这个对象访问到上一个和下一个对象。 name 张三 age 18 hobby [美女,吃饭]内部会建立一些数据 -打包 C语言叫做结构体-…

未来五年人工智能将实现的五大突破

来源&#xff1a;资本实验室不论是可以和你对话的智能音箱&#xff0c;还是能够自己作画的虚拟艺术家&#xff1b;不论是能够帮助农民准确判断种植和施肥时间的农场管理系统&#xff0c;又或者是能够在演唱会现场快速识别罪犯的人脸识别程序&#xff0c;人工智能已经开始在各行…

python面试常问

一、Python基础部分 1. 数据类型 数字类型(Numbers)&#xff1a; 整数(int), 浮点数(float), 复数(complex) 布尔(Booleans)&#xff1a; True和False 字符串(Str)&#xff1a;Uniconde字符序列, 在引号内包含 列表(list)&#xff1a; 有序的值的序列 元组(Tuples)&#x…

springJDBC一对多关系,以及Java递归,jsp递归的实现

maven编译&#xff0c;springMVCspringspringJDBC框架。 要实现的功能是一个文件夹下&#xff0c;可能显示n个文件夹&#xff0c;每个文件夹下又可能显示n个文件夹。。。。 前台效果&#xff1a; controller中的方法如下&#xff1a; RequestMapping(value"/index",m…

未来全球15大热门研究方向出炉!

转自&#xff1a;科学网&#xff08;sciencenet-cas&#xff09;要点速览伦敦、纽约、新加坡、香港、巴黎、北京、东京、迪拜、上海、柏林、波士顿&#xff0c;这些国际性大都市在科技创新方面的表现如何&#xff1f;它们主要关注哪些研究方向&#xff1f;15大科技创新策源点&a…

Django-rest framework

Django-rest Framework 1. FBV CBV 1.1 开发模式 普通开发方式&#xff08;前后端放在一起写&#xff09;前后端分离 1.2 后端开发 为前端提供URL&#xff08;API/接口的开发&#xff09; 注&#xff1a;永远返回HttpResponse 1.3 Django FBV、CBV # FBV(function base …

常用的网络营销方法有哪些

索引擎营销 电子邮件营销 即时通讯营销 病毒式营销 BBS营销 博客营销 播客营销 RSS营销 SN营销 创意广告营销 知识型营销 事件营销 口碑营销 转载于:https://www.cnblogs.com/happyday56/p/4739488.html

AI产业链分布图曝光:1040个玩家,BAT率先步入应用

来源&#xff1a;网络大数据5月9日&#xff0c;在苏州举办的全球人工智能产品应用博览会上&#xff0c;《新一代人工智能发展年度报告(2018)》重磅发布。发布方是中国经济信息社与新一代人工智能产业技术创新战略联盟。报告相当于对2018年以来全球AI领域融资、国内AI企业分布、…

什么是混合云?

来源&#xff1a;光联集团混合云是使那些正常的进化措施看起来更酷&#xff0c;是IT前沿术语之一。亚马逊&#xff0c;谷歌和微软等云供应商倡导企业关闭内部数据中心并将所有基础架构迁移到云端&#xff0c;这就是“超融合”数据中心战略。1转移基础设施对于刚刚起步的公司而言…

Shell—grep、sed、awk

Shell学习 Shell 是一个用 C 语言编写的程序&#xff0c;它是用户使用 Linux 的桥梁。Shell 既是一种命令语言&#xff0c;又是一种程序设计语言。 Shell 是指一种应用程序&#xff0c;这个应用程序提供了一个界面&#xff0c;用户通过这个界面访问操作系统内核的服务。 She…

【科普】AI的分类与演进

来源&#xff1a;物联网智库摘要&#xff1a;AI是人工通过高强度的计算能力&#xff0c;并基于大量的环境数据、行为数据、历史数据等大数据支持&#xff0c;或是一定规则的自学习机制&#xff0c;来分析特定输入的情况下&#xff0c;事物的相关性、影响和可能处理方法&#xf…

AngularJs入门学习

http://www.ituring.com.cn/article/13471 安装并配置好所有依赖环境之后&#xff0c;只需要在cmd进入angular-phonecat目录。接着指令操作npm start&#xff1b;开启服务器。如下图&#xff1a; 打开angular-phonecat的gitbash&#xff1b; 接下来就是用编译器打开angular-pho…

nginx+uWSGI + django部署项目

项目部署 nginxuWSGI django 1. WSGI WSGI是Web服务器网关接口。它是一个规范&#xff0c;描述了Web服务器(返回静态资源的就是web服务器&#xff0c;Nginx)如何与Web应用程序(django、Flask)通信&#xff0c;以及Web应用程序如何链接在一起以处理一个请求&#xff0c;&…

71页《乌镇智库:全球人工智能发展报告(2018)》PDF下载

来源&#xff1a;专知【导读】人工智能热潮之下&#xff0c;斯坦福、阿里等纷纷出台人工智能报告。乌镇智库已连续发布三年《全球人工智能发展报告》&#xff0c;以宏观视角纵览全球人工智能发展&#xff0c;从产业、融资、技术、教育和应用等多个角度展现人工智能在全球的发展…

Windows实用技巧

Windows常用命令&#xff0c;亲测可用 创建文件夹Test&#xff0c;命令为&#xff1a;mkdir Test 盘符&#xff1a; 进入系统中的某个盘&#xff0c;例如E&#xff1a;进入E盘cd 文件夹位置 进入某个文件夹&#xff0c;需要先进入盘符&#xff0c;即完成第一步&#xff0c;例如…

python进程、线程、协程

通过学习bi站 蚂蚁学Python 老师视频总结文档&#xff0c;仅用于学习。。。 python进程、线程、协程 多线程&#xff1a;threading&#xff0c;利用CPU和IO可以同时执行的原理&#xff0c;不会让CPU干巴巴的等待IO完成 多进程&#xff1a;multiprocessing&#xff0c;利用多核…

一文读懂计算计仿真技术

来源&#xff1a;传感器技术计算机仿真作为分析和研究系统运行行为、揭示系统动态过程和运动规律的一种重要手段和方法, 随着系统科学研究的深入、控制理论、计算技术、计算机科学与技术的发展而形成的一门新兴学科。近年来, 随着信息处理技术的突飞猛进, 使仿真技术得到迅速发…

Chrome Cookie SameSite 属性设置

Chrome Cookie SameSite 设置 Chrome 51 开始&#xff0c;浏览器的 Cookie 新增加了一个SameSite属性&#xff0c;用来防止 CSRF 攻击和用户追踪。 Cookie 的SameSite属性用来限制第三方 Cookie&#xff0c;从而减少安全风险。 它可以设置三个值。 StrictLaxNone Chrome 默认…