前言
python爬虫的深度着实让我吃惊且吃力,仅仅笔记的第一篇就有3.7万字的强度,看来,想要在python爬虫领域登峰造极,要狠狠下一番功夫了!!!
为什么不直接把笔记全发出来呢?主要是才就3.7万字,CSND的编辑器就卡的不行了,所以分为四个篇章了。
爬虫自学阶段
以下为我整理的python爬虫学习分别对应的数个阶段,这里补充一下,这篇是python爬虫的第一篇,是很基础的一篇,知识点刚到下面初级爬虫阶段(初级完结),中级阶段的数据库之类我会单独列出来几篇。
一、初级爬虫
主要是掌握Python的基础语法和一些常用库的使用
初级爬虫的技能要求
- Python 【语言基础】
- requests 【请求相关】
- 多进程【python多任务基础】
- 多线程【python多任务基础】
- 协程【python多任务基础】
- lxml【解析相关】
- XPath 【解析相关】
- BeautifulSoup【解析相关】
- PyQuery 【解析相关】
- MySQL 【数据库】
- MongoDB【数据库】
- Elasticsearch【我没用过这个,不清楚用到他什么特性】
这个阶段最主要的就是掌握Python语法基础、常用库的使用;
请求库的话一般Requests能应付大部分简单网站的爬取,当然是在没有反爬机制的前提下,Selenium的话主要是用它来模拟真实浏览器对URL进行访问,从而对网页进行爬取,往往要配合PhantomJS使用,Selenium+PhantomJS可以抓取使用JS加载数据的网页。
解析常用 到XPath、BeautifulSoup、PyQuery 或者正则表达式,初级的话能够熟练两三种解析库基本也够用了。
正则一般用来满足特殊需求、以及提取其他解析器提取不到的数据,正常情况下我会用bs4,bs4无法满足就用正则。
单线程的爬虫简单是简单,但是速度慢啊!
碰上个网络不好啥的,茶都要等凉凉了,所以利用多进程、多线程、协程能大幅度提升爬虫的速度,相关的库有threading和multiprocessing。
不过需要注意的一点是别把人家网站搞挂了!
没有基础的话,在Python入门这一块需要消化的知识点还是不少的,除了Python之外,基础的计算机网络知识、CSS、HTML等这些都是需要补充学习的。
给零基础初学者的一点建议是:明确好自己的学习目标,掌握好自己的学习节奏!
那些陌生的密密麻麻的知识点介绍,有些同学看了可能会当场劝退!Python的语法还算是简单,虽然也很多,但一步一步来呗
初级水平的爬虫主要重在基础,能爬着基本的网站玩玩,碰到有反爬的网站就不太行了,只能说爬虫之路还任重而道远。
二、中级爬虫
职业爬虫师的基本水平
中级爬虫的技能要求:
- Ajax【能通过Ajax接口获取数据】
- Puppeteer【基于JS的爬虫框架,可直接执行JS】
- Pyppeteer【基于Puppeteer开发的python版本,需要python异步知识】
- Selenium【常见的自动化工具,支持多语言】
- Splash【JavaScript渲染服务】
- fiddler 【抓包工具】
- mitmproxy【中间人代理工具】
- appium【自动化工具】
- adb【安卓adb工具】
- Charles【抓包工具】
这个阶段就是爬虫技能的升级了,Ajax ---多线程 ---多进程等是重点的学习内容;
现在很多网站的数据可能都是通过接口的形式传输的,或者即使不是接口那也是一些 JSON 的数据,然后经过 JavaScript 渲染得出来的。
如果还是用requests来爬是行不通的,所以大多数情况需要分析 Ajax,知道这些接口的调用方式之后再用程序来模拟。但如果有些接口带着加密参数,比如 token、sign的话,这时候就得去分析网站的 JavaScript 逻辑,简单粗暴的方法就是死抠代码!找出里面的代码逻辑,不过这事费时间费精力也费脑子,它的加密做的特厉害的话,你几天几夜不睡觉研究可能也不一定解的出来。还有一种方法相对省事一点,就是用 Puppeteer、Selenium、Splash来模拟浏览器的方式来爬取,这样就不用死抠Ajax 和一些 JavaScript 逻辑的过程,提取数据自然就简单一点。
用 aiohttp、gevent、tornado 等等,基本上想搞多少并发就搞多少并发,速度是成倍提上了,同时也注意一下自己的爬虫别被反爬干掉了,比如封账号、封IP、验证码啥的,总之就是悠着点爬!
三、高级爬虫
进一步提高爬取效率
高级爬虫的技能要求:
- RabbitMQ【消息队列相关】
- Celery【消息队列相关】
- Kafka【消息队列相关】
- Redis【缓存数据库 -----》 其实mongodb也可以充当这个角色】
- Scrapy-Redis【scrapy的redis组件】
- Scrapy-Redis-BloomFilter 【scrapy的布隆过滤器】
- Scrapy-Cluster 【我没用过,分布式解决方案】
- 验证码破解
- IP代理池
- 用户行为管理
- cookies池 崔神建的代理池开源代码地址
- token池
- sign
- 账号管理
能达到这个层次的话,一般赚外快是不在话下了,赚的自然不少,这个阶段主要是两个重点:分布式爬虫和应对反爬的处理技巧。
分布式爬虫
分布式爬虫通俗的讲就是多台机器多个 spider 对多个 url 的同时处理问题,分布式的方式可以极大提高程序的抓取效率。
虽然听起来也很厉害,其实也是利用多线程的原理让多个爬虫同时工作,当你掌握分布式爬虫,实现大规模并发采集后,自动化数据获取会更便利。
需要掌握 Scrapy + MongoDB + Redis 这三种工具,但是分布式爬虫对电脑的CPU和网速都有一定的要求。
现在主流的 Python 分布式爬虫还是基于 Scrapy 的,对接 Scrapy-Redis、Scrapy-Redis-BloomFilter 或者用 Scrapy-Cluster 等等,他们都是基于 Redis 来共享爬取队列的,多多少少会遇到一些内存的问题。所以有些人也考虑对接到了其他的消息队列上面,比如 RabbitMQ、Kafka 等等,解决一些问题,效率也不差。
应对反爬
有爬虫就有反爬,什么滑块验证、实物勾选、IP检测(豆瓣和github,在检测到某一客户端频繁访问后,会直接封锁IP)、封号......反正各种奇葩的反爬都有,这时候就得知道如何去应付这些常见的反爬手段了。
常见的反爬虫措施有:
- 字体反爬
- 基于用户行为反爬虫
- 基于动态页面的反爬虫
- IP限制
- UA限制
- Cookie限制
应对反爬的处理手段有:
- 控制IP访问次数频率,增加时间间隔
- Cookie池保存与处理
- 用户代理池技术
- 字体反加密
- 验证码OCR处理
- 抓包
这里提示一点:技术学溜了,就不要去挑战反爬,搞过了你们懂得哈!
四、更高一级的爬虫
这几点技能是需要掌握的:
- JS逆向【分析目标站点JS加密逻辑】
- APP逆向【xposed可在不改变原应用代码基础上植入自己的代码】
- 智能化爬虫
- 运维
JS逆向
这就回到了前面讲过的这个 Ajax 接口会带着一些参数的这个问题,现在随着前端技术的进步和网站反爬意识的增强,很多网站选择在前端上下功夫,那就是在前端对一些逻辑或代码进行加密或混淆。用Selenium 等方式来爬行是行,效率还是低了,JS逆向则是更高级别的爬取技术。
但问题是难啊!JS逆向的修炼掉头发是少不了的!
APP逆向
网页可以逆向,APP也能逆向,现在越来越多的公司都选择将数据放到 App 上面,在一些兼职网站上APP数据爬取这一类的报价在几千左右,这块是酬劳比较高的。
基本的就是利用抓包工具,Charles、Fiddler等,抓到接口之后,直接拿来模拟。想实现自动化爬取的话,安卓原生的 adb 工具也行,现在Appium 是比较主流的了。
APP逆向听着好像很简单,实际跟JavaScript逆向一样的烧脑。
智能化爬虫
如果说我要爬取一万个新闻网站数据,要一个个写 XPath的话我估计会见不到明天的太阳,如果用智能化解析技术,在不出意外的情况下,分分钟可以搞定。
用智能化解析,不论是哪个网站,你只需要把网页的url传递给它,就可以通过算法智能识别出标题、内容、更新时间等信息,而不需要重复编写提取规则。
简而言之就是爬虫与机器学习技术相结合,使得爬虫更加智能化
运维
主要体现在部署和分发、数据的存储和监控这几个方面,Kubernetes 、Prometheus 、Grafana是爬虫在运维方面用的比较多的技术。
学海无涯、学无止境,好好珍惜头发!与君共勉!!!
一、python基础语法
关于python的基础语法和高级用法,不多赘述,请移步以下链接。
二、爬虫的概述
(1)、爬虫是什么?
爬虫主要是和网页打交道,了解Wed前端的知识是非常重要的,
爬虫:1、通过一个程序,根据Url(http://www.taobao.com)进行爬取网页,获取有用信息
2、使用程序模拟浏览器,去向服务器发生请求,获取响应信息
(2)、爬虫核心:1、爬取网页:爬取网页,包含了网页的所以内容
2、解析数据:将网页中你得到的数据,进行解析
3、难点:反爬虫
(3)、爬虫的用途
1、数据分析/人工数据集
2、社交软件冷启动
3、舆情监控
4、竞争对手的监控
(4)、爬虫分类
通用爬虫:
实例:百度、360、google等搜索引擎
功能:访问网站、抓取数据、数据存储、数据处理、提供检索服务
缺点:1、会爬到大量无用的数据
聚焦爬虫
功能:根据需求,实现爬虫程序,爬取需要的数据。
设计思路:1、确定要爬取的url
2、模拟浏览器通过http协议访问url,获取服务器返回的html代码
3、解析html字符串(根据一定规律提取需要的数据)
(5)、反爬手段
1、User-Agent:User Agent中午名为用户代理,简称UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统以及版本、CPU类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。
2、代理IP
3、验证码验证
前端基础知识
关于前端的HTML、CSS、Javascript语言的知识,请移步以下链接:
以下讲述的前端基础知识主要围绕:
1、开发者工具的使用及各参数的意义;
2、Web网页实现的基本原理
3、HTTP2.0协议所增加的功能和原理
URL和URI
URI(Uniform Resource Identifier)即统一资源标志符
URL(Universal Resource Locator)即统一资源定位符
例如https://github.com/faviconico即是一个URL,也是一个URI(网页来源于《Python3网络爬虫开发实战》崔庆才著)
即有这样的一个图标资源,用URL/URI来唯一指定了它的访问方式,这其中包括了访问协议HTTPS、访问路径(即根目录)和资源名称favicon.ico
URL是URI的一个子类。URI包括了URL和URN。
URN只命名资源而不指定如何定位资源
比如:urn:isbn:0451450523指定了一本书的ISBN,可以唯一标识这本书
下面是开发者工具中Network的详细说明
Name:请求的名称,一般会将URL的最后一部分当作名称
Status:响应的状态码,具体如下:
例如,状态码为200,表示请求成功已完成;状态码为404,表示服务器找不到给定的资源。
Type:请求的文档类型,百度第一条Type是document
Initiator:请求源,用来标记请求时由哪个对象或进程发起的
Size:从服务器下载的文件或请求的资源大小。如果资源是从缓存中取得的,则该列会显示from cache。
Time:从发起请求到获取响应所花的总时间。
Waterfall:网络请求的可视化瀑布流。
Web服务器的工作原理
可以概括为以下4个步骤。
(1)建立连接:客户端通过TCP/IP协议建立到服务器的TCP连接。
(2)请求过程:客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档。
(3)应答过程:服务器向客户端发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理后得到的数据返回给客户端。由客户端解释HTML文档,在客户端屏幕上渲染图形结果。
从图中的General概述关键信息如下。
Request URL:请求的URL地址,也就是服务器的URL地址。
Request Method:请求方式是GET。
Status Code:状态码是200,即成功返回响应。
Remote Address:服务器IP地址是101.201.120.85,端口号是80。
请求(request)
请求的类型
请求方法,用于标识请求客户端请求服务端的方式,常见的请求方法有两种:GET和POST。在浏览器中直接输人URL并回车,便发起了一个GET请求,请求的参数会直接包含到URL里。例如,在百度搜索引擎中搜索Python 就是一个GET请求,链接为https://www.baidu.com/s?wd-Python其中URL中包含了请求的query信息,这里的参数wd表示要搜寻的关键字。POST请求大多在提交表单时发起。例如,对于一个登录表单,输人用户名和密码后,单击“登录”按钮,这时通常会发起一个POST 请求,其数据通常以表单的形式传输,而不会体现在 URL 中。GET和POST请求方法有如下区别。□ GET 请求中的参数包含在URL里面,数据可以在URL 中看到;而 POST 请求的 URL 不会包含这些数据,数据都是通过表单形式传输的,会包含在请求体中。口GET 请求提交的数据最多只有1024字节,POST 方式则没有限制
登录时一般需要提交用户名和密码。其中密码是敏感信息,如果使用GET 方式请求,密码就会暴露在URL 里面,造成密码泄露,所以这时候最好以POST方式发送。上传文件时,由于文件内容比较大,因此也会选用POST 方式。我们平常遇到的绝大部分请求是GET或POST请求
请求头以及常见信息
Accept:请求报头域,用于指定客户端可接受哪些类型的信息
Accept-Language:指定客户端可接受的语言类型
Accept-Encoding:用于指定客户端可接受的内容编码
Host:用于指定请求资源的主机IP和端口号,其内容为请求URL的原始服务器或网关的位置。
Referer:用于标识请求是从哪个页面发过来的,服务器可以拿到这一信息并做相应的处理,如做来源统计、防盗链处理等等。
User-Agent:简称UA,这是一个特殊的字符串头,可以使服务器识别客户端使用的操作系统及版本、浏览器及版本等信息。做爬虫时如果加上此信息,可以伪装为浏览器;如果不加,很可能会被识别出来。
Content-Type:也叫互联网媒体类型(Intenet Media Type)或者MIME类型。在HTTP协议消息头中,它用来表示具体请求中的媒体类型信息。例如,text/html代表HTML格式,image/gif代表GIF 图片,application/json代表JSON类型。请求头是请求的重要组成部分,在写爬虫时,通常都需要设定请求头。
请求体
登录之前,需要先填写用户名和密码信息,登录时这些内容会以表单数据的形式提交给服务器,此时需要注意Request Headers 中指定Content-Type为application/x-www-form-urlencoded。只有这样设置Content-Type,内容才会以表单数据的形式提交。另外,也可以将Content-Type设置为application/json来提交JSON 数据,或者设置为multipart/form-data 来上传文件。
在爬虫中,构造POST请求需要使用正确的Content-Type,并了解设置各种请求库的各个参数是使用的都是哪种Content-Type,如若不然可能会导致POST提交后无法得到正常响应。
响应(Response)
由服务器返回给客户端,可以分为三部分:响应状态码(Response Status Code)、响应头(Response Headers)和响应体(Response Body)。
响应头以及常见信息
包含了服务器对请求的应答信息,如Content-TypeServerSet-Cookie等等。下面简要说明一些常用的响应头信息。
Date:用于标识响应产生的时间。(单位是GMT)
Last-Modified:用于指定资源的最后修改时间。
Content-Encoding:用于指定响应内容的编码。
Server:包含服务器的信息,例如名称、版本号等
Content-Type:文档类型,指定返回的数据是什么类型,如text/html代表返回HTML文档,application/x-JavaScript代表返回JavaScript文件,image/jpeg代表返回图片。
Set-Cookie:设置Cookie。响应头中的Set-Cookie用于告诉浏览器需要将此内容放在Cookie中,下次请求时将Cookie携带上。
Expires:用于指定响应的过期时间,可以让代理服务器或浏览器将加载的内容更新到缓存中。当再次访问相同的内容时,就可以直接从缓存中加载,达到降低服务器负载、缩短加载时间的目的。
响应体
可以说是最关键的部分了,响应的正文数据都存在与响应体中,例如请求网页时,响应体就是网页的HTML代码;请求一张图片时,响应体就是图片的二进制数据。我们做爬虫请求网页时,要解析的内容就是响应体。
在浏览器开发者工具中的Preview,就是网页的源代码,也就是响应体的内容,这是爬虫的解析目标。在做爬虫时,我们主要通过响应体得到网页的源代码、JSON数据等,然后从中提取相应内容。
补充:在响应头中的几乎所有相应时间都是GMT(格林威治标准时间)为单位的,而什么是GMT呢?
GMT(Greenwich Mean Time), 格林威治平时(也称格林威治时间)。
它规定太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间为中午12点。
格林威治皇家天文台为了海上霸权的扩张计划,在十七世纪就开始进行天体观测。为了天文观测,选择了穿过英国伦敦格林威治天文台子午仪中心的一条经线作为零度参考线,这条线,简称格林威治子午线。
1884年10月在美国华盛顿召开了一个国际子午线会议,该会议将格林威治子午线设定为本初子午线,并将格林威治平时 (GMT, Greenwich Mean Time) 作为世界时间标准(UT, Universal Time)。由此也确定了全球24小时自然时区的划分,所有时区都以和 GMT 之间的偏移量做为参考。
1972年之前,格林威治时间(GMT)一直是世界时间的标准。1972年之后,GMT 不再是一个时间标准了。
UTC(Coodinated Universal Time),协调世界时,又称世界统一时间、世界标准时间、国际协调时间。由于英文(CUT)和法文(TUC)的缩写不同,作为妥协,简称UTC。
UTC 是现在全球通用的时间标准,全球各地都同意将各自的时间进行同步协调。UTC 时间是经过平均太阳时(以格林威治时间GMT为准)、地轴运动修正后的新时标以及以秒为单位的国际原子时所综合精算而成。
UTC 由两部分构成:
1、原子时间(TAI, International Atomic Time):
结合了全球400个所有的原子钟而得到的时间,它决定了我们每个人的钟表中,时间流动的速度。
2、世界时间(UT, Universal Time):
也称天文时间,或太阳时,他的依据是地球的自转,我们用它来确定多少原子时,对应于一个地球日的时间长度。
GMT 和UTC的区别:GMT是前世界标准时,UTC是现世界标准时。UTC 比 GMT更精准,以原子时计时,适应现代社会的精确计时。但在不需要精确到秒的情况下,二者可以视为等同。每年格林尼治天文台会发调时信息,基于UTC。
GMT、UTC、时区和夏令时的概念等等,这些概念在JS项目中的一个非常实用的应用
简单地讲, GMT 是以前的世界时间标准;UTC 是现在在使用的世界时间标准;时区是基于格林威治子午线来偏移的,往东为正,往西为负;夏令时是地方时间制度,施行夏令时的地方,每年有2天很特殊(一天只有23个小时,另一天有25个小时)。
HTTP2.0
HTTP协议从2015 年起发布了2.0 版本,相比 HTTP 1.1 来说,HTTP 2.0变得更快,更简单,更稳定。HTTP2.0在传输层做了很多优化,它的主要目标是通过支持完整的请求与响应复用来减少延迟,并通过有效压缩HTTP请求头字段的方式将协议开销降至最低。同时增加对请求优先级和服务器推送的支持,这些优化一笔勾销了HTTP I.1为做传输优化想出的一系列“歪招”。
而为什么不叫HTTP1.2而是讲HTTP2.0呢?因为HTTP2.0内部实现了新的二进制分帧层,没法与之前HTTP1x的服务器和客户端兼容,所以直接修改主版本号为2.0。下面我们就来了解一下HTTP2.0相比HTTP1.1来说,都有那些优化吧
二进制分帧层
HTTP2.0所以性能增强的核心就在于这个新的二进制分帧层。在HTTP1x中,不管是请求(Request)还是响应(Response),它们都是用文本格式传输的,其头部(Headers)、实体(Body)之间也是用文本换行符分隔开的。HTTP2.0对其做了优化,将文本格式修改为了二进制格式,使得解析起来更加高效。
同时将请求和响应数据分割为更小的帧,并采用二进制编码。
补充:
帧:只存在于HTTP2.0中的概念,是数据通信的最小单位。比如一个请求被分为了请求头帧(Request Headers Frame)和请求体/数据帧(Request Data Frame)
数据流
一个虚拟通道,可以承载双向的消息,每个流都有一个唯一的整数ID来标识。
消息:与逻辑请求或响应消息对应的完整的一系列帧。
在HTTP 2.0 中,同域名下的所有通信都可以在单个连接上完成,该连接可以承载任意数量的双向数据流。数据流是用于承载双向消息的,每条消息都是一条逻辑HTTP消息(例如请求或响应)。它可以包含一个或多个帧。
简而言之HTTP2.0将HTTP协议通信分解为二进制编码帧的交换,这些帧对应着特定数据流中的消息,所有这些都在一个TCP连接内复用,这是HTTP2.0协议所有其他功能和性能优化的基础。
多路复用
在HTTP1x中,如果客户端想发起多个并行请求以提升性能。则必须使用多个 TCP 连接,而且浏览器为了控制资源,还会对单个域名有 6~8个TCP 连接请求的限制。但在HTTP2.0 中,由于有了二进制分帧技术的加持,HTTP 2.0 不用再以TCP连接的方式去实现多路并行了,客户端和服务器可以将HTTP消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来,达到以下效果。
1、并行交错地发送多个请求。请求之间互不影响。
2、并行交错地发送多个响应,响应之间互不干扰。
3、使用一个连接并行发送多个请求和响应。
4、不必再为绕过HTTP1x限制而做很多工作。
5、消除不必要的延迟和提高现有网络容量的利用率,从而减少页面加载时间。
这样一来,整个数据传输性能就有了极大提升。
6、同域名只需要占用一个TCP 连接,使用一个连接并行发送多个请求和响应,消除了多个 TCP连接带来的延时和内存消耗。
7、并行交错地发送多个请求和响应,而且它们之间互不影响。
8、在HTTP2.0 中,每个请求都可以带一个31 位的优先值,0 表示最高优先级,数值越大优先级越低。有了这个优先值。客户端和服务器就可以在处理不同的流时采取不同的策略了,以最优的方式发送流、消息和帧。
流控制
流控制是一种阻止发送方向接收方发送大量数据的机制,以免超出后者的需求或处理能力。可以理解为,接收方太繁忙了,来不及处理收到的消息了,但是发送方还在一直大量发送消息,这样就会出现一些问题。比如,客户端请求了一个具有较高优先级的大型视频流,但是用户已经暂停观看视频了,客户端现在希望暂停或限制从服务器的传输,以免提取和缓冲不必要的数据。再比如,一个代理服务器可能具有较快的下游连接和较慢的上游连接,并且也希望调节下游连接传输数据的速度以匹配上游连接的速度,从而控制其资源利用率等HTTP是基于TCP实现的,虽然TCP原生有流控制机制,但是由于HTTP2.0数据流在一个TCP 连接内复用,TCP 流控制既不够精细,也无法提供必要的应用级API 来调节各个数据流的传输。为了解决这一问题HTTP2.0提供了一组简单的构建块,这些构建块允许客户端和服务器实现它们自己的数据流和连接级流控制。
服务端推送
HTTP2.0新增的另一个强大的功能是:服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以向客户端推送额外资源,而无须客户端明确地请求。如果某些资源客户端是一定会请求的。这时就可以采取服务端推送的技术,在客户端发起一次请求后,提前给客户端推送必要的资源。这样就可以减少一点延迟时间。如图1-8所示,服务端接收到[插图]服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST STREAM帧来拒收。另外,主动推送也遵守同源策略,即服务器不能随便将第三方资源推送给客户端,而必须是经过服务器和客户端双方确认才行,这样也能保证一定的安全性。
urllib库的爬虫实现方法
urllib.request:用于实现基本HTTP请求的模块。
urllib.error:异常处理模块,如果在发送网络请求时出现了错误,可以捕获异常进行异常的有效处理。
urllib.parse:用于解析URL的模块。
urllib. robotparser:用于解析robots.txt文件,判断网站是否可以爬取信息。
urllib.request
urlopen方法
一、get请求
用于打开一个远程的url连接,并且向这个连接发出请求,获取响应结果。返回的结果是一个http响应对象,这个响应对象中记录了本次http访问的响应头和响应体
参数说明如下。
url:需要访问网站的URL完整地址。
data:该参数默认值为None,通过该参数确认请求方式,如果是None,那么表示请求方式为GET,否则请求方式为POST。在发送POST请求时,参数data需要以字典形式的数据作为参数值,并且需要将字典类型的参数值转换为字节类型的数据才可以实现POST请求。
timeout:以秒为单位,设置超时。
cafile、capath:指定一组HTTPS请求受信任的CA证书,cafile指定包含CA证书的单个文件,capath指定证书文件的目录。
cadefault:CA证书默认值。
context:描述SSL选项的实例。
实例代码如下:
import urllib.request
url = 'https://www.python.org'
# 方式一
response = urllib.request.urlopen(url)
print(type(response)) # <class 'http.client.HTTPResponse'>
# 方式二
request = urllib.request.Request(url)
res = urllib.request.urlopen(url)
print(type(res)) # <class 'http.client.HTTPResponse'>
print(response.status) # 200 获取响应状态码
print(response.reason) # OK
print(response.version) # 11
print(response) # 获取响应,结果为:<http.client.HTTPResponse object at 0x10be801d0>
print(response.headers) # 获取响应头
# Server: nginx
# Content-Type: text/html; charset=utf-8
# X-Frame-Options: DENY
# Via: 1.1 vegur
# Via: 1.1 varnish
# Content-Length: 48830
# Accept-Ranges: bytes
# Date: Thu, 12 Mar 2020 10:34:07 GMT
print(response.url) # https://www.python.org 获取响应url
print(response.read()) # 获取响应体 二进制字符串
print(response.read().decode("utf-8")) # 对响应体进行解码
# 按行读取
print(response.readline()) # 读取一行
print(response.readline()) # 读取下一行
print(response.readlines()) # 读取多行。得到一个列表 每个元素是一行
补充:
CA证书,也是根证书,是最顶级的证书,也是CA认证中心与用户建立信任关系的基础,用户的数字证书必须有一个受信任的根证书,用户的数字证书才是有效的。
ca数字证书作用一:验证你打开的HTTPS网站是不是可信
ca数字证书作用二:验证你所安装的文件是不是遭到篡改
通过结果可以发现response是一个HTTPResponsne类型的对象,它主要包含的方法有read()、readinto()、getheader()、getheader(name)、fileno()等函数和msg、version、status、reason、debuglevel、closed等等属性
二、发送post请求
urlopen()方法默认是get请求,而在发送post请求时,需要为其设置data参数,该参数时bytes类型,所以需要使用bytes()方法将参数值进行数据类型的转换。
三、设置网络超时:
urlopen()方法中的timeout参数,是用于设置请求超时的参数,该参数以秒为单位,表示如果在请求时,超出了设置的时间,还没有得到响应是就抛出异常。
注:根据网络环境的不同,可以将超时时间设置为一个合理的时间,如2秒、3秒等等
如果遇到了超时异常,爬虫程序将在此处停止。所以在实际开发中开发者可以将超时异常捕获,然后处理下面的爬虫任务。
四、复杂的网络请求
urlopen()方法能够发送一个最基本的网络请求,但这并不是一个完整的网络请求。如果要构建一个完整的网络请求,还需要在请求中添加Headers、Cookies以及代理IP等内容。Request类可以构建一个多功能的请求对象,其语法格式如下:
urllib.request.Request(url,data=None,headers={},origin_req_host=None, unverifiable=False, method=None)
参数说明如下。
url:需要访问网站的URL完整地址。
data:该参数默认值为None,通过该参数确认请求方式,如果是None,那么表示请求方式为GET,否则请求方式为POST。在发送POST请求时,参数data需要以字典形式的数据作为参数值,并且需要将字典类型的参数值转换为字节类型的数据才可以实现POST请求。
headers:设置请求头部信息,该参数为字典类型。添加请求头信息最常见的用法就是修改User-Agent来伪装成浏览器,例如,headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0;WOW64) AppleWebKit/537.36(KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'},表示伪装谷歌浏览器进行网络请求。
origin_req_host:用于设置请求方的host名称或者是IP。
unverifiable:用于设置网页是否需要验证,默认值是False。
method:用于设置请求方式,如GET、POST等,默认为GET请求。
设置请求头参数是为了模拟浏览器向网页后台发送网络请求,这样可以避免服务器的反爬措施。使用urlopen()方法发送网络请求时,其本身并没有设置请求头参数,所以向https://www.httpbin.org/post请求测试地址发送请求时,在返回的信息中,headers将显示如图所示的默认值。
五、Cookies的获取与设置
Cookie是服务器向客户端返回相应数据时所留下的标记,当客户端再次访问服务器时将携带这个标记。一般在实现登录一个页面时,登录成功后,会在浏览器的Cookie中保留一些信息,当浏览器再次访问时会携带Cookie中的信息,经过服务器核对后便可以确认当前用户已经登录过,此时可以直接将登录后的数据返回。
在使用爬虫获取网页登录后的数据时,除了使用模拟登录以外,还可以获取登录后的Cookie,然后利用这个Cookie再次发送请求时,就能以登录用户的身份获取数据。
六、代理IP
1、什么是代理?
代理IP是一个ip,指的是一个代理服务器。
2、正向代理和反向代理是什么?
知不知道服务器的地址作为判断标准:知道就是正向代理,不知道就是反向代理。
正向代理:
顺着请求的方向进行的代理,即代理服务器它是由你配置为你服务,去请求目标服务器地址。
举例一: 如我们现在想要访问谷歌,但是由于某些原因,无法直接访问到谷歌,我们可以通过连接一台代理服务器,代理服务将我们的请求提交到谷歌,然后再将谷歌的响应反馈给我们,对于谷歌而言,它只知道有一个请求过来,但是它并不会知道我们是无法直接访问它的。
正向代理的作用:
1. 访问原来无法访问的资源,如google
2. 可以做缓存,加速访问资源
3. 对客户端访问授权,上网进行认证
4. 代理可以记录用户访问记录(上网行为管理),对外隐藏用户信息
反向代理:
反向代理: 跟正向代理相反,它是为目标服务器进行服务的,但是请求的流程还是: clieng -> proxy -> server。
举例: 比如我们访问百度网站,百度的代理服务器对外的域名为 https://ww.baidu.com 。具体内部的服务器节点我们不知道。现实中我们通过访问百度的代理服务器后,代理服务器给我们转发请求到他们N多的服务器节点中的一个给我们进行搜索后将结果返回,此时,代理服务器对我们客户端来说就充当了提供响应的服务器,但是对于目标服务器来说,它只是进行了一个请求和转发的功能。
反向代理的作用:
1. 保证内网的安全,阻止web攻击,大型网站,通常将反向代理作为公网访问地址,Web服务器是内网。
2. 负载均衡,通过反向代理服务器来优化网站的负载。
两者的区别与联系
正向代理即是客户端代理, 代理客户端, 服务端不知道实际发起请求的客户端.
反向代理即是服务端代理, 代理服务端, 客户端不知道实际提供服务的服务端.
联系:
1、正向代理中,proxy和client同属一个LAN,对server透明;
2、反向代理中,proxy和server同属一个LAN,对client透明。
使用urllib模块设置代理IP是比较简单的,首先需要创建ProxyHandler对象,其参数为字典类型的代理IP,键名为协议类型(如HTTP或者HTTPS),值为代理链接。然后利用ProxyHandler对象与build_opener()方法构建一个新的opener对象,最后再发送网络请求即可。实例如下:
七、异常处理
在实现网络请求时,可能会出现很多异常错误,urllib模块中的urllib.error子模块包含了URLError与HTTPError两个比较重要的异常类。
URLError类中提供了一个reason属性,可以通过这个属性了解错误的原因。例如,这里向一个根本不存在的网络地址发送请求,然后调用reason属性查看错误原因。代码如下:
结果如下:
Not Found
HTTPError类时URLError类的子类,主要用于处理HTTP请求所出现的异常,该类有以下3个属性。
code:返回HTTP状态码
reason:返回错误原因
headers:返回请求头
使用HTTPError类捕获异常的示例代码如下:
结果如下:
双重异常的捕获:
由于HTTPError是URLError的子类,有时HTTPError类会有捕获不到的异常,所以可以先捕获子类HTTPError的异常,然后再去捕获父类URLError的异常,这样可以起到双重保险的作用。
结果为:
说明:从以上的运行结果中可以看出,此次超时(timeout)异常是由第二道防线URLError所捕获的。
urllib.parse(解析链接)
一、拆分URL
urlparse()方法
parse子模块中提供了urlparse()方法,用于实现URL分解成不同部分,其语法格式如下:
urllib.parse.urlparse(urlstring,scheme=” ”,allow_fragment= Ture)
参数说明如下:
urlstring:需要拆分的URL,该参数为必填参数。
scheme:可选参数,表示需要设置的默认协议。如果需要拆分的URL中没有协议(如https、http等),可以通过该参数设置一个默认的协议,该参数的默认值为空字符串。
allow_fragment:可选参数,如果该参数设置为False,泽表示忽略fragment这部分内容,默认值为True。
实例:使用urlparse()方法拆分URL。
上图可知:调用urlparse()方法将返回一个ParseResult对象,其中由6部分组成,scheme表示协议,netloc表示域名,path表示访问的路径,params表示参数,query表示查询条件,fragment表示片段标识符。
urlsplit()方法:
urlspilt()方法与urlparse()方法类似,都可以实现URL的拆分,只是urlsplit()方法不再单独拆分params这部分内容,而是将params合并到path中,所以返回的结果只有5部分内容,并且返回的数据类型为SplitResult。示例代码如下:
结果如下:
使用urlsplit()方法不再单独拆分params这部分内容,而是将params合并到path中,所以返回的结果只有5部分内容,并且返回的数据类型为SplitResuit,该类型的数据即可以使用属性获取对应的值,也可以使用索引进行值的获取。
二、组合URL
urlunparse()方法
parse子模块提供了拆分URL的方法,同样也提供了一个urlunparse()方法实现URL的组合。其语法格式如下:
urllib.parse.urlunparse(parts)
parts:表示用于组合url的可迭代对象。
结果如下:
使用urlunparse()方法组合URL时,需要注意可迭代参数中的元素必须是6个,如果参数中元素不足6个,报错!
urlunsplit()方法
urlunsplit()方法与urlunparse()类似,同样是用于实现URL的组合,其参数也同样是一个可迭代对象,,不管参数必须是5个。
结果如下:
三、连接URL
urljoin()方法
urlunparse()方法与urlunsplit()方法可以实现URL的组合,而parse子模块还提供了一个urljoin()方法来实现URL的连接。语法格式为:
urllib.parse.urljoin(base,url,allow_fragment = True)
参数说明如下:
base:表示基础链接
url:表示新的连接
allow_fragment:可选参数,如果该参数设置为False,那么表示fragment这部分内容,默认值为True
urljoin()方法在实现URL连接时,base参数只可以设置scheme、natloc以及path这3部分内容,
如果第二个参数(url)是一个不完整的URL,那么第二个参数的值会添加至第一个参数(base)的后面,并自动添加斜杠(/)
如果第二个参数(url)是一个完整的URL,那么会直接返回第二个参数所对应的值。
运行结果:
URL编码是GET请求中比较常见的,是将请求地址中的参数进行编码,尤其是对于中文参数。parse子模块提供了urlencode()方法与quote()方法用于实现URL的编码,而unquote()方法,可以实现对加密后的URL进行解码操作。
urlencode()方法
urlencode()方法接收一个字典类型的值,所以要想将URL进行编码需要先请求参数定义为字典类型,然后再调用urlencode()方法进行请求参数的编码。
示例代码如下:
运行结果为:
注:地址中“%E4%B8%AD%E5%9B%BD& “内容为中文转码后的效果。
quote()方法
quote()方法与urlencode()方法所实现的功能类似,但是urlencode()方法中只接收字典类型的参数,而quote()方法则可以将一个字符串进行编码。
运行结果为:
unquote()方法
unquote()方法可以将编码后的URL字符串逆向解码,无论是通过urlencode()方法,还是quote()方法所编码的URL字符串都可以使用unquote()方法进行解码。
解码结果如下:
URL参数的转换:
请求地址的URL是一个字符串,如果需要获取URL中某个参数时,可以将URL中的参数部分获取并使用parse_qs()方法将参数转换为字典类型的数据。示例如下:
程序运行结果:
使用parse_qsl()方法将参数转化为元组所组成的列表“
parse_qsl()方法会将字符串参数转换为元组所组成的列表。
urllib3的使用
get请求
使用urllib3模块发送网络请求时,首先需要创建PoolManger对象,通过该对象调用request()方法来实现网络请求的发送。
request()方法的语法格式如下:
Request(method,url,fields=None,headers=None,**urlopen_kw)
常用参数说明如下:
method:必选参数,用于指定请求方式,如GET、POST、PUT等等。
url:必选参数,用于设置需要请求的url地址
fields:可选参数,用于设置请求参数
headers:可选参数,用于设置请求头
示例:
使用request()方法实现GET请求的示例代码如下:
结果为 : #200
使用Pool Manger对象向多个服务器发送请求。
程序运行结果:
POST请求
使用urllib3模块向服务器发送POST请求时并不复杂,与发送GET请求相似,只需要在request()方法中将method参数设置为POST,然后将fields参数设置为字典类型的表单参数。代码如下:
程序运行结果如下:
从上图中可以看出,JSON信息中的from对应的数据为表单参数,只是country所对应的并不是“中国”而是一段unicode编码,
对于这样的情况,可以将请求结果的编码方式设置为unicode_escape。
即:print(r.data.decode(“unicode_escape”))
编码方式设置为unicode_escape之后,程序运行结果,返回的表单参数内容如图所示:
重试请求
urllib3可以自动重试请求,这种相同的机制还可以处理重定向。在默认情况下,request()方法的请求重试次数为3次,如果需要修改重试次数,那么可以设置retries参数。
示例如下:
运行结果为:
处理响应内容
获取响应头
发送网络请求后,将返回一个HTTPResponse对象,通过该对象中的info()方法即可获取HTTP响应头信息,该信息为字典(dict)类型的数据,所以需要通过for循环进行遍历才可清晰地看清每条响应头信息的内容。示例代码如下:
程序运行结果:
JSON信息
如果服务器返回一条JSON信息,而这条信息中只有某个数据为可用数据时,则可以先将返回的JSON数据转换为字典(dict)数据,接着直接获取指定键所对应的值即可。
程序运行结果如下:
处理返回的二进制数据
如果响应数据为二进制数据,则也可以做出相应的处理。例如,响应内容为某图片的二进制数据时,则可以使用open函数,将二进制数据转换为图片,示例代码如下:
复杂请求的发送
1、设置请求头
大多数浏览器都会检测请求头信息,判断当前请求是否来自浏览器的请求。
程序运行结果:
2、设置超时
在没有特殊要求的情况下,可以将设置超时的参数与时间填写在request()方法或者PoolManager()实例对象中。
示例代码如下:
程序结果如下:
如果需要更加精确地设置超时,可以使用Timeout实例对象,在该对象中可以单独设置连接超时与读取超时。
或者:
3、设置代理
在设置代理IP时,需要创建ProxyManager对象,在该对象中最后填写两个参数。一个时proxy_url,表示需要使用的代理IP;另一个参数为headers,就是为了模拟浏览器请求,避免后台服务器发现。
结果如下:
4、上传文件
request方法提供了两种比较常见的文件上传方式,一种是通过field参数以元组形式分别知道文件名,文件内容以及文件类型,这种方式适合上传文本文件时使用。
第二种方式,在request()方法中指定body参数,该参数所对应的值为图片的二进制数据,然后需要使用headers参数为其指定文件类型
request模块
Request是有史以来下载次数最多的python软件包之一
补充:request和urllib的区别为:
urllib库的response对象是先创建http,request对象,装载到request.urlopen里完成http请求。
返回的是http,response对象,实际上是html属性。 使用.read().decode()解码后转化成了str字符串类型,decode解码后中文字符能够显示出来。
requests库调用是requests.get方法传入url和参数,返回的对象是Response对象,打印出来是显示响应状态码。通过.text 方法可以返回是unicode 型的数据,一般是在网页的header中定义的编码形式,而content返回的是bytes,二级制型的数据,还有 .json方法也可以返回json字符串。如果想要提取文本就用text,但是如果你想要提取图片、文件等二进制文件,就要用content,当然decode之后,中文字符也会正常显示。
Python爬虫时,更建议用requests库。因为requests比urllib更为便捷,requests可以直接构造get,post请求并发起,而urllib.request只能先构造get,post请求,再发起
在使用urllib内的request模块时,返回体获取有效信息和请求体的拼接需要decode和encode后再进行装载。进行http请求时需先构造get或者post请求再进行调用,header等头文件也需先进行构造。
get请求
最常用的HTTP请求方式分别为GET和POST,在使用requests模块实现GET请求时,可以使用两种方式来实现,一种带参数,另一种为不带参数,以百度为例实现不带参数的网络请求。
结果为:
对响应结果进行utf-8编码
当响应状态码为200时说明本次网络请求以及成功,此时可以获取请求地址所对应的网页源码,代码如下:
运行结果为:
在没有对响应内容进行utf-8编码时,网页源码中的中文信息可能会出现乱码。如:
爬取二进制数据
使用requests模块中的函数不仅可以获取网页中的源码信息,还可以获取二进制文件。但是在获取二进制文件时,需要使用Response.content属性获取bytes类型的数据,然后将数据保存在本地文件中。
get(带参)请求
实现请求地址带参:
如果需要为get请求指定参数,则可以直接将参数添加在请求地址URL的后面,然后用问号(?)进行分隔,如果一个URL地址中有多个参数,参数之间用(&)进行连接。GET(带参)请求代码如下:
程序结果为:
这里通过http://httpbin.org/get网站进行演示,该网站可以作为练习网络请求的一个站点使用,该网站可以模拟各种请求操作。
配置params参数
requests模块提供了传递参数的方法,允许使用params关键字参数,以一个字符串字典来提供这些参数。例如,想传递key1=value1和key2=value2到httpbin.org/get,那么可以使用如下代码:
POST请求
POST请求方式也叫作提交表单,表单中的数据内容就是对应的请求参数。使用requests模块实现POST请求时需要设置请求参数data。POST请求的代码如下:
requests模块中GET与POST请求的参数分别是params和data,所以不要将两种参数填写错误。
复杂的网络请求
在使用requests模块实现网络请求时,不只有简单的GET与POST。还有复杂的请求头、Cookies以及网络超时等。不过,requests模块将这一系列复杂的请求方式进行了简化,只要在发送请求时设置对应的参数即可实现复杂的网络请求。
添加请求头
有时在请求一个网页内容时,发现无论通过GET或者POST以及其他请求方式,都会出现403错误。这种现象多数为服务器拒绝了访问,因为这些网页为了防止恶意采集信息,所以使用了反爬虫设置。此时可以通过模拟浏览器的头部信息来进行访问,这样就能解决以上反爬设置的问题。下面介绍requests模块添加请求头的方式,代码如下:
结果为200
验证Cookies
在爬取某些数据时,需要进行网页的登录,才可以进行数据的抓取工作。Cookies登录就像很多网页中的自动登录功能一样,可以让用户在第二次登录时,在不需要验证账号和密码的情况下进行登录。在使用requests模块实现Cookies登录时,首先需要在浏览器的开发者工具页面中找到可以实现登录的Cookies信息,然后将Cookies信息处理并添加至RequestsCookieJar的对象中,最后将RequestsCookieJar对象作为网络请求的Cookies参数,发送网络请求即可。以获取豆瓣网页登录后的用户名为例,具体步骤如下。
注意事项
使用requests = requests.get(url=url,headers=header)的时候,用text方法获取网页源代码时,必须事先进行解码,即:
而使用content方法的时候,可以在使用时进行解码。
多进程与多线程
进程可以理解为是一个可以独立运行的程序单位
比如:打开一个浏览器,就开启了一个浏览器进制。
打开一个编辑器,就开启了一个编辑器进程
一个进程可以同时处理很多事情,比如,浏览器中可以在多个选项卡中打开多个页面,有的页面在播放音乐,有的页面在播放视频。可以同时运行,互不干扰。
进程是线程的集合,是由一个或多个线程构成的。
线程是操作系统中进行运算调度的最小单位,是进程中的一个最小运行单元。在一个进程里面有会创建一个线程,线程是真正执行的单位,而进程是为线程提供资源的单位
如果你在python解释器中创建一个程序去执行,那么默认状态下会创建一个进程
注意:这里需要重申,多进程和多线程可以提高效率,是指在一个进程没有执行(在下载文件或者被其他情况耽搁)的时候,去执行其他进程,进而提高程序的效率。
补充:CPU密集型和IO密集型
CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运行大部分的状况是CPU Loading100%,CPU要读/写(I/O,即硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。
比如说要计算1+2+3+…+ 1亿、计算圆周率后几十位、数据分析。 都是属于CPU密集型程序。
此类程序运行的过程中,CPU占用率一般都很高。
假如在单核CPU的情况下,线程池又6个线程,但是由于是单核CPU,所以同一时间只能运行一个线程,考虑到线程之间还有上下文切换的时间消耗,还不如单个线程执行高效
所以!!单核CPU处理CPU密集型程序,就不要使用多线程了。
假如,是6个核心的CPU,理论上运行速度可以提升6倍。每个线程都有CPU来运行,并不会发生CPU空闲的情况,也没有线程切换的开销。
所以!!多核CPU处理CPU密集型程序才合适,而且中间可能没有线程的上下文切换(一个核心处理一个线程)
嗯,CPU密集型任务就是指CPU需要疯狂计算的任务。
IO密集型
IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O(硬盘/内存)的读/写操作,但CPU的使用率不高。所以用脚本语言像python去做IO密集型操作,效率就很高
简单来说,IO密集型任务就是需要大量的输入输出,如读文件、
写文件、传输文件、网络请求等等
区别和使用:
IO密集型:大量网络,文件操作
CPU密集型:大量计算,cpu占用接近100%,耗费多个核或多台机器。
线程数不是越多越好。
由于CPU的核心数有限,线程之间切换也需要开销,频繁的切换上下文会使性能降低,适得其反。
简单的总结就是:
Ncpu 表示 核心数。
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 Ncpu+1
如果是IO密集型任务,参考值可以设置为 2 * Ncpu
上面两个公式为什么是Ncpu+1 呢,而不是Ncpu+2 呢,为什么不是3 * Ncpu 呢?
在《Java并发编程实践》中,是这样来计算线程池的线程数目的:
一个基准负载下,使用 几种不同大小的线程池运行你的应用程序,并观察CPU利用率的水平。 给定下列定义:
Ncpu = CPU的数量 Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1 W/C = 等待时间与计算时间的比率
为保持处理器达到期望的使用率,最优的池的大小等于:
Nthreads = Ncpu x Ucpu x (1 + W/C)
CPU数量是确定的,CPU使用率是目标值也是确定的,W/C也是可以通过基准程序测试得出的。
对于计算密集型应用,假定等待时间趋近于0,是的CPU利用率达到100%,那么线程数就是CPU核心数,那这个+1意义何在呢?
《Java并发编程实践》这么说:
计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。
所以 Ncpu+1 是一个经验值。
对于IO密集型应用,假定所有的操作时间几乎都是IO操作耗时,那么 W/C的值就为1,Ucpu 要达到100%利用率。
根据 Nthreads = Ncpu x Ucpu x (1 + W/C),
那么对应的线程数确实为 2Ncpu 。
对于包含I/O操作或者其他阻塞的任务,由于线程不会一直执行,因此线程池的数量应该更多。
在《linux多线程服务器端编程》中有一个思路,CPU计算和IO的阻抗匹配原则。
如果线程池中的线程在执行任务时,密集计算所占的时间比重为P(0<P<=1),而系统一共有C个CPU,为了让CPU跑满而又不过载,线程池的大小经验公式 T = C / P。在此,T只是一个参考,考虑到P的估计并不是很准确,T的最佳估值可以上下浮动50%。
这个经验公式的原理很简单,T个线程,每个线程占用P的CPU时间,如果刚好占满C个CPU,那么必有 T * P = C。
如果一个web程序有CPU操作,也有IO操作,那该如何设置呢?
有一个估算公式:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
这个公式进一步转化为:
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
下面据说是个腾讯的面试题:
问题一:
假如一个程序平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么最佳的线程数应该是?
根据上面这个公式估算得到最佳的线程数:((0.5+1.5)/0.5)*8=32。
GIL锁
GIL在Python中是一个很有争议的话题,由于它的存在,多线程编程在Python中似乎并不理想,为什么这么说呢?先来了解一下GIL。GIL被称为为全局解释器锁(Global Interpreter Lock),是Python虚拟机上用作互斥线程的一种机制,它的作用是保证任何情况下虚拟机中只会有一个线程被运行,而其他线程都处于等待GIL锁被释放的状态。是cpython解释器特有的一个玩意,让一个进程中同一时刻只能有一个进程可以被CPU调用
不管是在单核系统还是多核系统中,始终只有一个获得了GIL锁的线程在运行,每次遇到I/O操作便会进行GIL锁的释放。
如下图所示:
上图是 Python虚拟机中I/O操作中GIL的变换过程
但如果是纯计算的程序,没有I/O操作,解释器则会根据sys.setcheckinterval的设置来自动进行线程间的切换,默认情况下每隔100个时钟(注:这里的时钟指的是Python的内部时钟,对应于解释器执行的指令)就会释放GIL锁从而轮换到其他线程的执行,示意图如下图所示。
上图是 无I/O操作时GIL的变换过程
在单核CPU中,GIL对多线程的执行并没有太大影响,因为单核上的多线程本质上就是顺序执行的。但对于多核CPU,多线程并不能真正发挥优势带来效率上明显的提升,甚至在频繁I/O操作的情况下由于存在需要多次释放和申请GIL的情形,效率反而会下降。
Python解释器中为什么要引入GIL呢?
来思考这样一个情形:我们知道Python中对象的管理与引用计数器密切相关,当计数器变为0的时候,该对象便会被垃圾回收器回收。当撤销对一个对象的引用时,Python解释器对对象以及其计数器的管理分为以下两步:
1)使引用计数值减1。
2)判断该计数值是否为0,如果为0,则销毁该对象。
注:以上垃圾回收机制属于计数回收机制,现在包括JavaScript等等都实现了标记回收机制,摒弃了计数回收机制。
假设线程A和B同时引用同一个对象obj,这时obj的引用计数值为2。如果现在线程A打算撤销对obj的引用。当执行完第一步的时候,由于存在多线程调度机制,A恰好在这个关键点被挂起,而B进入执行状态,如图6-8所示。但不幸的是B也同样做了撤销对obj的引用的动作,并顺利完成了所有两个步骤,这个时候由于obj的引用计数器为0,因此对象被销毁,内存被释放。但如果此时A再次被唤醒去执行第二步操作的时候会发现已经面目全非,则其操作结果完全未知
鉴于此,在Python解释器中引入了GIL,以保证对虚拟机内部共享资源访问的互斥性。GIL的引入确实使得多线程不能在多核系统中发挥优势,但它也带来了一些好处:大大简化了Python线程中共享资源的管理,在单核CPU上,由于其本质是顺序执行的,一般情况下多线程能够获得较好的性能。此外,对于扩展的C程序的外部调用,即使其不是线程安全的,但由于GIL的存在,线程会阻塞直到外部调用函数返回,线程安全不再是一个问题。
补充:在阅读许多资料和很多实验后,发现GIL锁的局限性主要是针对CPU密集型任务的多线程执行时的并行性和整体性能有影响,但对于IO密集型任务的影响不大,因为在等待I/O时GIL锁可以在多线程之间共享;并且如果python程序想充分利用计算机多核优势的话,就使用多进程。
并发和并行
并发(concurrency):同一时刻其实只有一个线程在执行。
指同一时刻只能有一条指令执行,但多个线程的对应被快速轮换地执行,宏观上看起来多个线程在同时运行,但微观上只是这个处理器在连续不断地在多个线程之间切换和执行
并行(parallel):指同一时刻有多条指令在多个处理器上同时执行,并行必须要依赖于多个处理器,无论宏观上还是微观上,多个线程都是在同一时刻一起执行的。
网络爬虫是一个非常典型的例子,爬虫在想服务器发起请求之后,有一段时间必须要等待服务器的响应返回,这种任务就属于IO密集型任务。
所以,如果爬虫在一个线程等待的时候,去执行另一个线程,就可以大大的提高爬虫的效率。
多线程(threading)
多线程类似于同时执行多个不同程序,多线程运行有以下优点:
1、可以把运行时间长的任务放到后台去处理
2、用户界面可以更加吸引人,比如用户点击了一个按钮去触发某个事件的处理,可以弹出一个进度条来显示处理的进度
3、程序的运行速度可能加快
4、在一些需要等待的任务实现上,如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源,如内存占用等等。
Threading模块
Python的标准库提供了两个模块:thread和threading,thread是低级模块,threading是高级模块,对thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
Threading模块的对象:
Threading模块的Thread类
线程的常见方法
t.start(),当前线程准备调度,具体时间是由CPU来决定的。
t.join(),等待当前线程的任务执行完毕后再向下继续执行。
注意以下代码:
说明:
在以上代码中,可能会造成一个现象(由于上述代码过于短小简陋,所以很稳定,实测不会出现,但一个大型项目出现的概率会很大):因为在加一的过程中,加一这个动作还没有加完(也就是说只完成了一半),python解释器(多线程)就切换到减一那个函数里面了 ;也就是说,比如原来number是100,本来要加一变成101的,但是加一这个动作还未执行,就切换到减一,number就变成了99,但是当再次切换到加一那个函数(_add)的时候,函数内部记录的number还会是100,也就是加一变成101了(number应该99,加一为100)。
也就是说,这个数据被两个线程同时在做一个操作的时候,切换的过程中,因为切片的力度不同,导致最终的结果数据可能是错误的。
线程安全
申请锁:解决线程安全问题
在python中,大部分数据类型都是内部就加锁的
所以,要注意官方文档的更新,注意到底哪些操作时线程安全的。
守护线程
在3.9之前(包括3.9)方法都是t.setdaemon(True)
但都是要放在start之前
t.setDaeon(True),设置为守护线程,主线程执行完毕后,子线程也自带关闭。(现在为t.Daeon = True)相反,False设置为非守护线程,主线程等待子线程,子线程执行完毕后,主线程才结束。(默认为False)
线程名称和设置
name = threading.current_thread().name 以及t.name = f'线程名称{i}'为现在方法。
自定义线程类,直接将线程需要做的事情写到run方法中。
死锁
以上代码时进行多次死锁操作所造成的错误。程序不会报错,但会一种卡死,不会向下运行。
由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。
线程池
线程并不是开的越多,效率就越高,开的多可能会导致系统的性能更低了
以下代码是不建议的:
建议:建立线程池
使用submit向线程池提交一个任务,让线程池分配一个线程去执行这个任务。
shutdown()等待线程池中的所有任务都执行完毕了,再继续执行。有点像join()方法的作用。
还有一个add_done_callback()方法,会将参数对应的函数再次传入进程池的同一进程中执行。
线程:单例模式
多进程
python实现多进程的方式主要有两种,一种方法是使用os模块中的fork方法,另一种方法是使用multiprocessing模块。这两种方法的区别在于前者仅适用于Unix/Linux操作系统,对window不支持,后者则是跨平台的实现方式。由于现在很大爬虫程序都是运行在Unix/Linus操作系统上的,所以,前者的方法也相当重要。
fork方式实现多进程
python的os模块封装了常见的系统调用,其中就有fork方法。fork方法来自于Unix/Linux操作系统中提供的一个fork系统调用,这个方法很特殊。普通的方法都是调用一次,返回一次,而fork方法是调用一次,返回两次,原因在于操作系统将当前进程(父进程)复制出一份进程(子进程),这两个进程几乎完全相同,于是fork方法分别在父进程和子进程中返回。子进程中永远返回0,父进程返回的是子进程的ID
os模块中的getpid方法用于获取当前进程的ID, getppid方法用于获取父进程的ID
结果为:
创建多进程
multiprocessing模块提供了一个Process类来描述一个进程对象。创建子进程时,只需要传入一个执行函数和函数的参数,即可完成一个Process实例的创建,用start()方法启动进程,用join()方法实现进程间的同步。
注意:创建进程池对象时,传入的args参数时一个元组
结果为:
注意:上述创建进制是在if __name__ = __main__下运行的(因为是window系统),因为不同操作系统在创建进程时,其内部机制不一样,Linux系统时基于fork来做的;而window是基于spawn来做的;mac则是两种都支持,但是python3.8后的版本都支持spawn。
以上是创建进程的两种方法,但是要启动大量的子进程,使用进程池子批量创建子进程的方式更加常见,以为当被操作对象数目不大时,可以之间利用multiprocessing中的Process动态生成多个进程,如果时上百个、上千个目标,手动去限制进程数量却又繁琐,就使用进程池Pool。
创建进程池对象
Pool可以提供指定数量的进程供用户调用,默认大小是CPU的核数。当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来处理它
两图一比较,差距就出来了。
上述程序先创建了容量为3的进程池,依次向进程池中添加了5个任务。从运行结果中可以看到虽然添加了5个任务,但是一开始只运行了3个,而且每次最多运行3个进程。当一个任务结束了,新的任务依次添加进来,任务执行使用的进程依然是原来的进程,这一点通过进程的pid就可以看出来。
注意:Pool对象调用join()方法会等待所以子进程执行完毕,调用join()之前必须先调用close(),掉用close只会就不能继续添加新的Process
进程常用功能
p.start()方法:当前进程准备就绪,等待CPU调度(工作单元其实是进程中的线程)。
p.join()方法:等待当前进程的任务的任务执行完毕后再向下继续执行。
p.daemon = 布尔值 ,守护进程(必须放在start之前)
进程的名称的设置和获取:
自定义进程类,直接将进程需要做的事写到run方法中。
进程间的通信
假如创建了大量的进程,那进程间的通信时必不可少的。Python提供了多种进程间的通信的方式,例如Queue、Pipe等等,Queue和Pipe的区别在于Pipe常用于在两个进程间通信,Queue用来在多个进程间实现通信。
Queue:多进程安全队列,可以使用Queue实现多进程之间的数据传递。Put和Get可以进行Queue操作:
Put方法用以插入数据到队列中,它还有两个可选参数:blocked和timeout。如果blocked默认为True(默认),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
Get方法可以从队列读取并且删除一个元素。同样,Get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,分两种情况:如果Queue有一个值可用,则立即返回该值;否则,如果队列为空,则立即抛出Queue.Empty异常。
最后介绍一下Pipe的通信机制,Pipe常用来在两个进程间进行通信,两个进程分别位于管道的两端。Pipe方法返回(conn1, conn2)代表一个管道的两个端。Pipe方法有duplex参数,如果duplex参数为True(默认值),那么这个管道是全双工模式,也就是说conn1和conn2均可收发。若duplex为False, conn1只负责接收消息,conn2只负责发送消息。send和recv方法分别是发送和接收消息的方法。例如,在全双工模式下,可以调用conn1.send发送消息,conn1.recv接收消息。如果没有消息可接收,recv方法会一直阻塞。如果管道已经被关闭,那么recv方法会抛出EOFError。下面通过一个例子进行说明:创建两个进程,一个子进程通过Pipe发送数据,一个子进程通过Pipe接收数据。程序示例如下:
正则表达式
re模块
match方法
match方法必须从字符串开头匹配,match方法尝试从字符串的起始位置匹配一个模块,如果不是起始位置匹配成功的话,matvh方法就会返回none
方法:re.match(pattern, string)
# pattern 匹配的正则表达式
# string 要匹配的字符串
例子如下:
re.match()方法返回一个匹配的对象,而不是匹配的内容。如果需要返回内容则需要调用group()方法。通过调用span()方法可以获得匹配结果的位置。而如果从起始位置开始没有匹配成功,即便其他部分包含需要匹配的内容,re.match()也会返回None。
单字符匹配
以下字符,都匹配单个字符数据。且开头(从字符串0位置开始)没有匹配到,即便字符串其他部分包含需要匹配的内容,match方法也会返回none,
注意:这是match方法的特性
. 匹配任意一个字符
[] 匹配[ ]中列举的字符
\d :匹配数字,即0-9
\D:匹配非数字
\s:匹配空白,就空格,tab键
\S:匹配非空白
\w:匹配单词字符,即a-z,A-Z,0-9、_
\W:匹配非单纯字符
[ ] 匹配[ ]中列举的字符
表示数量:
像上面写的那些都是匹配单个字符,如果我们要匹配多个字符的话,只能重复写匹配符。这样显然是不人性化的,所有我们还需要学习表达数量的字符
* 匹配前一个字符出现0次或无限次,即可有可无
+ 匹配前一个字符出现1次或者无限次,即至少有1次
?匹配前一个字符出现1次或者0次,即要么有1次,要么没有
{m} :匹配前一个字符出现m次
{m,}:匹配前一个字符出现至少m次
{m,n}:匹配前一个字符出现从m到n次
*出现0次或无数次
+ 至少出现一次
*与+在没有匹配到字符串后的区别
匹配边界:
^ 匹配字符串开头
$ 匹配字符串结尾
\b 匹配一个单词的边界
\B 匹配非单词边界
\b匹配一个单词的边界,表示字母数字与非字母数字的边界,非字母数字与字母数字的边界。即下面ve的右边不能有字母和数字
\B 匹配非单词边界,ve的右边需要有字母或者数字
匹配分组:
| 匹配左右任意一个表达式
(ab) 将括号中字符作为一个分组
\num 引用分组num匹配到的字符串
(?P<name>)分组起别名
(?P=name)引用别名为name分组匹配到的字符串。
| 只要 | 两边任意一个表达式符合要求就行。
(ab)将括号中的字符作为一个分组
()中的内容会作为一个元组字符装在元组中
search方法
匹配字符串,匹配到第一个结果就返回,不会匹配出多个结果来。
findall方法
findall是寻找所有能匹配到的字符,并以列表的方式返回。
补充:re.S属性
re.S属性是findall的另一个属性,在字符串中,包含换行符\n,在这种情况下,如果不使用re.S参数,则只在每一行内进行匹配,如果一行没有,就换下一行重新开始。
sub方法
查找字符串中所有相匹配的数据进行替换
sub(要替换的数据,替换成什么,要替换的数据所在的数据)
split方法
对字符串进行分割,并返回一个列表
贪婪
python正则表达式默认启动贪婪模式,关闭贪婪模式需要在末尾加?
正则表达式语法
Bs4解析方法
Bs4的概述
Bs4是一种获取网页源代码后的解析方法,这里我们默认为已经得到了网页的源代码。
首先,使用Bs4的第一步,创建一个BeautifulSoup对象。
语法:soup = BeautifulSoup(r1.text,‘lxml’)
#这里ri.text是网页源代码的内容,lxml是解释器
然后通过这个对象来实现对获取到的源码进行筛选和处理。
print(soup.prettify()) #格式化输出全部内容
print(soup.标签名)
Bs4对象的转换
将一段文档传入BeautifulSoup的构造方法,就能得到一个文档的对象
from bs4 import BeautifulSoup
soup = BeautifulSoup(open(文档路径, encoding=编码格式),features=解析器)
解析过程:BeautifulSoup会选择最合适的解析器来解析这段文档,如果手动指定解析器,那么BeautifulSoup会选择指定解析器来解析指定文档。
注意:默认解析器情况下,BeauSoup会将当前文档作为HTML格式解析,如果要解析XML文档,需要指定“xml“解析器。
BeautifulSoup将复杂的HTML文档转换为一个复杂的树形结构,每个节点都是python对象,所有对象可以归纳为4种:Tag、NavigableString、BeautifulSoup、Comment
1、Tag(标签)
tag对象与XML或HTML原生文档种的tag相同
最重要的两个属性:name和attributes
(1)、每个tag都有自己的名字,通过.name来获取
name属性可以被修改。
(2)、一个tag可能有多个属性,操作与字典相同,通过.attrs来获取、改变(增删改查)
多值属性:最常用的多值属性是class,多值属性的返回list
2、NavigableString(标签的值)
字符串常包含在tag内。beautifulSoup常用NavigableString类来包装tag中的字符串。但是字符串中不能包含其他tag。
tag中包含的字符串不能被编辑,但是可以被替换成其他字符串,用replace_with()方法
3.BeautifulSoup(文档)
BeautifulSoup 对象表示的是一个文档的全部内容。大部分时候,可以把它当作 Tag 对象。但是 BeautifulSoup 对象并不是真正的 HTM L或 XML 的 tag,它没有attribute属性,name 属性是一个值为“[document]”的特殊属性
4.Comment(注释及特殊字符串)
特殊类型的NavigableString,comment一般表示文档的注释部分。
选择元素
方法一
将result = soup.div修改为result = soup.div.a
如图:当有多个符合条件的标签时,会选择第一个符合的标签
所以,这种xxx.Tag_name的缺点是:选择性很低,无法增加附加条件进行更深层次的筛选。
依靠属性来获取,如content、children、descendants等等
1、content、children(子节点)
属性:content、children
作用:获取目标的直接子节点
并且注意:content返回的是列表,而children返回的是生成器。
但是,children的作用似乎与descendants一致,均可以返回所有子孙节点
2、descendants(子孙节点)
作用:获取目标的所以子孙元素
返回值和children一样,也是生成器
3、parent(祖先节点)
4、next_sibling(下一个兄弟节点)
previous_sibling(上一个兄弟节点)
next_siblings(下面的所有兄弟节点)
previous_siblings(之前的所有兄弟节点)
方法二
find_all方法:
作用:查询出所有符合条件的元素。
常用参数:name、attrs、text
name:想要获取的节点的节点名字
attrs:可以获取的节点的属性,根据这个属性来筛选
text:可以指定正则表达式或者字符串,去匹配元素的内容
使用一、name和attrs的配合使用
由图可得,使用name来筛选标签,attrs附加属性筛选。
使用二:使用text方法
除了find_all外的其他方法:
find()方法:返回第一个匹配成功的元素。
find_parent()方法:返回所有的祖先节点
find_parent()方法:返回直接父节点
find_next_sinlings():返回后面所有的兄弟节点
find_next_sinlings():返回下一个兄弟节点
find_previous_sibling():返回之前所有的兄弟节点
find_previous_siblings()返回上一个兄弟节点
方法三
写法:xxx.select(‘css代码’)
返回所有符合css条件的元素
获取元素信息
元素筛选成功之后,我们需要获取元素的一定信息,如:文本信息、属性信息等等。
获取文本信息
以下方法只
方法一:xxx.string:用来获取目标路径下第一个非标签字符串,得到的是整个字符串
方法二:xxx.stings:用来获取目标路径下所有的非标签字符串,得到的是生成器
方法三:xxx.stripped_strings:用来获取目标路径下所有的非标签字符串,会自动去掉空白字符串,得到的是生成器
方法四:xxx.get_text:用来获取目标路径下所有的非标签字符串,得到的是整个字符串(包含HTML的格式内容)
结果如下:
获取属性信息
方法一:xxx.attrs[‘属性名字’]
方法二:xxx[‘属性名字’]
结果如下:
Xpath
介绍:Xpath,全称XML path language,即XML路径语言,它是一门在XML文档中查找信息的语言。最初是用来搜寻XML文档的,单同样适用于HTML文档的搜索。所以在做爬虫的时候完全可以使用Xpath做相应的信息抽取。
Xpath的选择功能十分强大,它提供了非常简洁明了的路径选择表达式。另外,它还提供了超过100个内建函数,用于字符串、数值、时间的匹配以及节点、序列的处理等等,几乎所有想要的节点都可以用Xpath来选择。
常用规则:
基础使用
实例化一个etree的对象,且需要将被解析的页面源码数据加载到该对象中,有两种方式:
1、将本地的html文档中的源码数据加载到etree对象中
etree.parse(‘filePath’,etree.HTMLParser())#filePath是文件的路径。
实例:
from lxml import etree # 导包
html=etree.parse('./test.html',etree.HTMLParser())
# ./test.html为本地的html文件的路径
html.xpath('xpath表达式')
2、将从互联网上获取的源码数据加载到etree对象中。
etree.Html(‘page_data’) #page_data为从页面获取的源码数据
示例:
from lxml import etree # 导包
html = etree.Html('page_data') # page_data为从页面获取的源码数据
html.xpath('xpath表达式')
可以看出运行结果中有三个节点,正好是从上往下数前面3个a节点。我们也可以看出来上面使用了 / 来表示层级关系。
result2 = html.xpath('/html/body/div//a')
print(result2)
如图:
如果要取两个p节点,即可以使用//来取,这里只是演示通配符*的用法。
索引定位
比如我们想去到第一个a节点,就可以使用索引定位了。
注意:这里索引与计算机常用索引不同,它是从[1]开始索引的
属性定位
比如我们想取id = “aa“的a节点,就可以使用属性定位
nodename[@attrid=’value’]
代码如下:
取文本:
在Xpath中,使用text()即可取出网页中的文本信息
代码:
#输出为[‘百度’]
取属性,如果我们想取出下图的属性,可以使用@属性这个表达式。
Xpath解析的局限性
如果网页是通过Ajax动态加载的,我们就不能使用xpath表达式来提取信息
一个简单的判断方法:在网页中鼠标右击 ——> 查看网页源代码 ——> ctrl+F 搜索想要的信息 ——> 搜索无结果 ——> 不能使用xpath解析
避坑:有时,我们直接复制xpath表达式或者自己写xpath表达式会遇到提取信息后返回一个 空列表 这种情况,反复对照代码发现自己好像又没写错,这是怎么回事呢?
其实,很大可能是你没有以 网页源代码 为准来写xpath表达式,而是根据开发者工具展示的代码来写xpath表达式。原因就是开发者工具是实时的网页代码(比如通过js加载一些数据后的),而我们提取到的页面源码数据不一定是实时的网页代码。
一般情况下,我们可以直接根据开发者工具展示的代码来写xpath表达式,但是一定要结合网页源代码,以网页源代码为准!
重要的事情说三遍!以网页源代码为准!以网页源代码为准!以网页源代码为准!
PyQuery
不要把文件名字取得和要导入的库名字一样,,,血的教训
初始化
在使用pyquery库解析HTML‘文本的时候,需要先将其初始化为一个PyQuery对象。
初始化有很多种方法,比如直接传入字符串,传入url、文件名称等待
字符串初始化:
以上代码首先引用PyQuery这个对象,取别名为pq。然后声明一个长HTML字符串,并将其当作参数传递给PyQuery类,这样就成功完成了初始化。接着,将初始化的对象传入CSS选择器
URL初始化
初始化的参数除了能以字符串形式传递,还能是网页的URL,此时只需要指定PyQuery对象参数为url即可。
以上代码所建立的PyQuery对象会首先请求这个URL,然后用得到的HTML内存看出初始化,其实相对于把网页的源代码以字符串形式传递给pyQuery类,来完成初始化操作
下面代码效果相同:
文件初始化
除了上面两种,还可以传递本地的文件名,此时将参数指定为filename即可:
本地这里需要有一个html文件demo.html,其内容是待解析的HTML字符串。这样PyQuery对象会首先读取本地的文件内容,然后将文件内容以字符串的形式传递给PyQuery类型进行初始化。
CSS选择器
这里我们初始化PyQUery对象之后,传入了一个css选择器#contain .list li,他的意思是先选取id为container的节点,在选取其内部class为list的节点内部的所有li节点。
并且PyQuery类中也存在子节点和父节点等待CSS选择器的经典
如果要筛选所有的子节点中符合条件的节点,例如想筛选出子节点中class为active的节点,啧可以向children方法传入class选择器代码如下:
方法有children()【子节点】、parent()【父节点】、parents()【祖先】、silbings()【兄弟节点】、
但是如果结果是多个节点,就需要遍历获取了。调用item()方法
结果如下:
可以发现,调用items方法,会得到一个生成器,对其进行遍历,就可以逐个得到li节点对象,类型也是pyquery。还可以调用前面所说的方法对li节点进行选择,继续查询子节点或者祖先节点。
获取属性:
提取到某个pyquery类型的节点后,可以调用attr方法获取其属性
如果先选择item active a节点,在调用attr获取属性内容,如果print(a.attr.href)结果也是一样的。
但是如果我们得到的节点是多个节点,但是attr只有一个属性,调用attr方法,只会得到第一个节点的属性
如果想要获取全部属性则需要遍历。
节点操作
pyquery库提供了一系列方法进行动态修改,例如为某个节点添加一个class,移除某个节点等,有时候这些操作会为提供信息带来极大的便利。
addClass和removeClass
与js的DOM中的节点操作相同。