0%

Python

导航目录:【2022 年】Python3 爬虫学习教程

在浏览网站的过程中,我们经常会遇到需要登录的情况,有些页面只有登录之后才可以访问,而且登录之后可以连续访问很多次网站,但是有时候过一段时间就需要重新登录。还有一些网站,在打开浏览器时就自动登录了,而且很长时间都不会失效,这种情况又是为什么?其实这里面涉及 Session 和 Cookie 的相关知识,本节就来揭开它们的神秘面纱。

1. 静态网页和动态网页

在开始之前,我们需要先了解一下静态网页和动态网页的概念。这里还是前面的示例代码,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>This is a Demo</title>
</head>
<body>
<div id="container">
<div class="wrapper">
<h2 class="title">Hello World</h2>
<p class="text">Hello, this is a paragraph.</p>
</div>
</div>
</body>
</html>

这是最基本的 HTML 代码,我们将其保存为一个 test.html 文件,然后把它放在某台具有固定公网 IP 的主机上,主机上装上 Apache 或 Nginx 等服务器,这样这台主机就可以作为服务器了,其他人便可以通过访问服务器看到这个页面,这就搭建了一个最简单的网站。

这种网页的内容是 HTML 代码编写的,文字、图片等内容均通过写好的 HTML 代码来指定,这种页面叫作静态网页。它加载速度快,编写简单,但是存在很大的缺陷,如可维护性差,不能根据 URL 灵活多变地显示内容等。例如,我们想要给这个网页的 URL 传入一个 name 参数,让其在网页中显示出来,是无法做到的。

因此,动态网页应运而生,它可以动态解析 URL 中参数的变化,关联数据库并动态呈现不同的页面内容,非常灵活多变。我们现在遇到的大多数网站都是动态网站,它们不再是一个简单的 HTML,而是可能由 JSP、PHP、Python 等语言编写的,其功能比静态网页强大、丰富太多了。此外,动态网站还可以实现用户登录和注册的功能。

再回到开头提到的问题,很多页面是需要登录之后才可以查看的。按照一般的逻辑来说,输入用户名和密码登录之后,肯定是拿到了一种类似凭证的东西,有了它,我们才能保持登录状态,访问登录之后才能看到的页面。

那么,这种神秘的凭证到底是什么呢?其实它就是 Session 和 Cookie 共同产生的结果,下面我们来一探究竟。

2. 无状态 HTTP

在了解 Session 和 Cookie 之前,我们还需要了解 HTTP 的一个特点,叫作无状态。

HTTP 的无状态是指 HTTP 协议对事务处理是没有记忆能力的,也就是说服务器不知道客户端是什么状态。当我们向服务器发送请求后,服务器解析此请求,然后返回对应的响应,服务器负责完成这个过程,而且这个过程是完全独立的,服务器不会记录前后状态的变化,也就是缺少状态记录。这意味着如果后续需要处理前面的信息,则必须重传,这导致需要额外传递一些前面的重复请求,才能获取后续响应,然而这种效果显然不是我们想要的。为了保持前后状态,我们肯定不能将前面的请求全部重传一次,这太浪费资源了,对于这种需要用户登录的页面来说,更是棘手。

这时两个用于保持 HTTP 连接状态的技术就出现了,它们分别是 Session 和 Cookie。Session 在服务端,也就是网站的服务器,用来保存用户的 Session 信息;Cookie 在客户端,也可以理解为浏览器端,有了 Cookie,浏览器在下次访问网页时会自动附带上它发送给服务器,服务器通过识别 Cookie 并鉴定出是哪个用户,然后再判断用户是否是登录状态,然后返回对应的响应。

我们可以理解为 Cookie 里面保存了登录的凭证,有了它,只需要在下次请求携带 Cookie 发送请求而不必重新输入用户名、密码等信息重新登录了。

因此,在爬虫中,有时候处理需要登录才能访问的页面时,我们一般会直接将登录成功后获取的 Cookie 放在请求头里面直接请求,而不必重新模拟登录。

好了,了解 Session 和 Cookie 的概念之后,我们在来详细剖析它们的原理。

3. Session

Session,中文称为会话,其本来的含义是指有始有终的一系列动作 / 消息。比如,打电话时,从拿起电话拨号到挂断电话这中间的一系列过程可以称为一个 Session。

而在 Web 中,Session 对象用来存储特定用户 Session 所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户 Session 中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有 Session,则 Web 服务器将自动创建一个 Session 对象。当 Session 过期或被放弃后,服务器将终止该 Session。

Cookie,也常用其复数形式 Cookies,Cookie 指某些网站为了辨别用户身份、进行 Session 跟踪而存储在用户本地终端上的数据。

Session 维持

那么,我们怎样利用 Cookies 保持状态呢?当客户端第一次请求服务器时,服务器会返回一个响应头中带有 Set-Cookie 字段的响应给客户端,用来标记是哪一个用户,客户端浏览器会把 Cookies 保存起来。当浏览器下一次再请求该网站时,浏览器会把此 Cookies 放到请求头一起提交给服务器,Cookies 携带了 Session ID 信息,服务器检查该 Cookies 即可找到对应的 Session 是什么,然后再判断 Session 来辨认用户状态。

在成功登录某个网站时,服务器会告诉客户端设置哪些 Cookies 信息。在后续访问页面时,客户端会把 Cookies 发送给服务器,服务器再找到对应的 Session 加以判断。如果 Session 中的某些设置登录状态的变量是有效的,那就证明用户处于登录状态,此时返回登录之后才可以查看的网页内容,浏览器再进行解析便可以看到了。

反之,如果传给服务器的 Cookies 是无效的,或者 Session 已经过期了,我们将不能继续访问页面,此时可能会收到错误的响应或者跳转到登录页面重新登录。

所以,Cookies 和 Session 需要配合,一个处于客户端,一个处于服务端,二者共同协作,就实现了登录 Session 控制。

属性结构

接下来,我们来看看 Cookies 都有哪些内容。这里以知乎为例,在浏览器开发者工具中打开 Application 选项卡,然后在左侧会有一个 Storage 部分,最后一项即为 Cookies,将其点开,如图所示。

Cookies 列表

可以看到,这里有很多条目,其中每个条目可以称为 Cookie。它有如下几个属性。

  • Name,即该 Cookie 的名称。Cookie 一旦创建,名称便不可更改。
  • Value,即该 Cookie 的值。如果值为 Unicode 字符,需要为字符编码。如果值为二进制数据,则需要使用 BASE64 编码。
  • Domain,即可以访问该 Cookie 的域名。例如如果设置为 .zhihu.com,则所有以 zhihu.com 结尾的域名都可以访问该 Cookie。
  • Path,即该 Cookie 的使用路径。如果设置为 /path/,则只有路径为 /path/ 的页面可以访问该 Cookie。如果设置为 /,则本域名下的所有页面都可以访问该 Cookie。
  • Max-Age,即该 Cookie 失效的时间,单位为秒,常和 Expires 一起使用,通过它可以计算出其有效时间。Max-Age 如果为正数,则该 Cookie 在 Max-Age 秒之后失效。如果为负数,则关闭浏览器时 Cookie 即失效,浏览器也不会以任何形式保存该 Cookie。
  • Size 字段,即此 Cookie 的大小。
  • HTTP 字段,即 Cookie 的 httponly 属性。若此属性为 true,则只有在 HTTP Headers 中会带有此 Cookie 的信息,而不能通过 document.cookie 来访问此 Cookie。
  • Secure,即该 Cookie 是否仅被使用安全协议传输。安全协议有 HTTPS 和 SSL 等,在网络上传输数据之前先将数据加密。其默认值为 false

从表面意思来说,会话 Cookie 就是把 Cookie 放在浏览器内存里,浏览器在关闭之后该 Cookie 即失效;持久 Cookie 则会保存到客户端的硬盘中,下次还可以继续使用,用于长久保持用户登录状态。

其实严格来说,没有会话 Cookie 和持久 Cookie 之分,只是由 Cookie 的 Max-Age 或 Expires 字段决定了过期的时间。

因此,一些持久化登录的网站其实就是把 Cookie 的有效时间和 Session 有效期设置得比较长,下次我们再访问页面时仍然携带之前的 Cookie,就可以直接保持登录状态。

5. 常见误区

在谈论 Session 机制的时候,常常听到这样一种误解 ——“只要关闭浏览器,Session 就消失了”。可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。对 Session 来说,也一样,除非程序通知服务器删除一个 Session,否则服务器会一直保留。比如,程序一般都是在我们做注销操作时才去删除 Session。

但是当我们关闭浏览器时,浏览器不会主动在关闭之前通知服务器它将要关闭,所以服务器根本不会有机会知道浏览器已经关闭。之所以会有这种错觉,是因为大部分网站都使用会话 Cookie 来保存 Session ID 信息,而关闭浏览器后 Cookies 就消失了,再次连接服务器时,也就无法找到原来的 Session 了。如果服务器设置的 Cookies 保存到硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 Cookies 发送给服务器,则再次打开浏览器,仍然能够找到原来的 Session ID,依旧还是可以保持登录状态的。

而且恰恰是由于关闭浏览器不会导致 Session 被删除,这就需要服务器为 Session 设置一个失效时间,当距离客户端上一次使用 Session 的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把 Session 删除以节省存储空间。

6. 总结

本节介绍了 Session 和 Cookie 的基本概念,这对后文进行网络爬虫的开发有很大的帮助,需要好好掌握。

由于涉及一些专业名词知识,本节部分内容的参考来源如下:

  • 文档 - Session - 百度百科:https://baike.baidu.com/item/session/479100
  • 文档 - Cookie - 百度百科:https://baike.baidu.com/item/cookie/1119
  • 文档 - HTTP Cookie 维基百科:https://en.wikipedia.org/wiki/HTTP_cookie
  • 博客 - Session 和几种状态保持方案理解:http://www.mamicode.com/info-detail-46545.html

Python

导航目录:【2022 年】Python3 爬虫学习教程

简而言之,爬虫可以帮助我们快速把网站上的信息快速提取并保存下来。

我们可以把互联网比作一张大网,而爬虫(即网络爬虫)便是在网上爬行的蜘蛛。把网的节点比作一个个网页,爬虫爬到这就相当于访问了该页面,就能把网页上的信息提取出来。我们可以把节点间的连线比作网页与网页之间的链接关系,这样蜘蛛通过一个节点后,可以顺着节点连线继续爬行到达下一个节点,即通过一个网页继续获取后续的网页,这样整个网的节点便可以被蜘蛛全部爬行到,网站的数据就可以被抓取下来了。

1. 爬虫有什么用?

通过上面的话,你可能已经初步知道了爬虫是做了什么事情,但一般要学一个东西,我们得知道学来干什么用吧?

其实,爬虫的用处可大了去了。

  • 比如,我们想要研究最近各大网站头条都有什么热点,那我们就可以用爬虫把这些网站的热门新闻用爬虫爬下来,这样我们就可以分析其中的标题、内容等知道热点关键词了。
  • 比如,我们想要对一些天气、金融、体育、公司等各种信息进行整理和分析,但这些内容都分布在各种不同的网站上,那我们就可以用爬虫把这些网站上的数据爬取下来,整理成我们想要的数据保存下来,就可以对其进行分析了。
  • 比如,我们在网上看到了很多美图,比如风景、美食、美女,或者一些资料、文章,想保存到电脑上,但一次次右键保存、复制粘贴显然非常费时费力,那我们就可以利用爬虫将这些图片或资源快速爬取下来,极大地节省时间和精力。

另外还有很多其他的,比如黄牛抢票、自助抢课、网站排名等等各种技术也都和爬虫分不开,爬虫的用处可谓是非常大,可以说人人都应该会点爬虫。

另外学爬虫还可以帮助我们顺便学好 Python。学爬虫,个人首推的就是 Python 语言,如果你对 Python 还不太熟,没关系,爬虫就非常适合作为入门 Python 的方向来学习,一边学爬虫,一边学 Python,最后一举两得。

不仅如此,爬虫技术和其他领域的几乎都有交集,比如前后端 Web 开发、数据库、数据分析、人工智能、运维、安全等等领域都和爬虫有所沾边,所以学好了爬虫,就相当于为其他的领域也铺好了一个台阶,以后想进军其他领域都可以更轻松地衔接。Python 爬虫可谓是学习计算机的一个很好的入门方向之一。

2. 爬虫的流程

简单来说,爬虫就是获取网页并提取和保存信息的自动化程序,下面概要介绍一下。

(1) 获取网页

爬虫首先要做的工作就是获取网页,这里就是获取网页的源代码。源代码里包含了网页的部分有用信息,所以只要把源代码获取下来,就可以从中提取想要的信息了。

我们用浏览器浏览网页时,其实浏览器就帮我们模拟了这个过程,浏览器向服务器发送了一个个请求,返回的响应体便是网页源代码,然后浏览器将其解析并呈现出来。所以,我们要做的爬虫其实就和浏览器类似,将网页源代码获取下来之后将内容解析出来就好了,只不过我们用的不是浏览器,而是 Python。

刚才说,最关键的部分就是构造一个请求并发送给服务器,然后接收到响应并将其解析出来,那么这个流程怎样用 Python 实现呢?

Python 提供了许多库来帮助我们实现这个操作,如 urllib、requests 等。我们可以用这些库来实现 HTTP 请求操作,请求和响应都可以用类库提供的数据结构来表示,得到响应之后只需要解析数据结构中的 body 部分即可,即得到网页的源代码,这样我们可以用程序来实现获取网页的过程了。

(2) 提取信息

获取网页的源代码后,接下来就是分析网页的源代码,从中提取我们想要的数据。首先,最通用的方法便是采用正则表达式提取,这是一个万能的方法,但是在构造正则表达式时比较复杂且容易出错。

另外,由于网页的结构有一定的规则,所以还有一些根据网页节点属性、CSS 选择器或 XPath 来提取网页信息的库,如 Beautiful Soup、pyquery、lxml 等。使用这些库,我们可以高效快速地从中提取网页信息,如节点的属性、文本值等。

提取信息是爬虫非常重要的部分,它可以使杂乱的数据变得条理、清晰,以便我们后续处理和分析数据。

(3) 保存数据

提取信息后,我们一般会将提取到的数据保存到某处以便后续使用。这里保存形式有多种多样,如可以简单保存为 TXT 文本或 JSON 文本,也可以保存到数据库,如 MySQL 和 MongoDB 等,还可保存至远程服务器,如借助 SFTP 进行操作等。

(4) 自动化程序

说到自动化程序,意思是说爬虫可以代替人来完成这些操作。首先,我们手工当然可以提取这些信息,但是当量特别大或者想快速获取大量数据的话,肯定还是要借助程序。爬虫就是代替我们来完成这份爬取工作的自动化程序,它可以在抓取过程中进行各种异常处理、错误重试等操作,确保爬取持续高效地运行。

3. 能爬怎样的数据?

在网页中我们能看到各种各样的信息,最常见的便是常规网页,它们对应着 HTML 代码,而最常抓取的便是 HTML 源代码。

另外,可能有些网页返回的不是 HTML 代码,而是一个 JSON 字符串(其中 API 接口大多采用这样的形式),这种格式的数据方便传输和解析,它们同样可以抓取,而且数据提取更加方便。

此外,我们还可以看到各种二进制数据,如图片、视频和音频等。利用爬虫,我们可以将这些二进制数据抓取下来,然后保存成对应的文件名。

另外,还可以看到各种扩展名的文件,如 CSS、JavaScript 和配置文件等,这些其实也是最普通的文件,只要在浏览器里面可以访问到,就可以将其抓取下来。

上述内容其实都对应各自的 URL,是基于 HTTP 或 HTTPS 协议的,只要是这种数据,爬虫都可以抓取。

4. 总结

本节结束,我们已经对爬虫有了基本的了解,接下来让我们一起接着迈入爬虫学习的世界吧!

Python

导航目录:【2022 年】Python3 爬虫学习教程

用浏览器访问网站时,页面各不相同,你有没有想过它为何会呈现这个样子呢?本节中,我们就来了解一下网页的组成、结构和节点等内容。

1. 网页的组成

网页可以分为三大部分 —— HTML、CSS 和 JavaScript。如果把网页比作一个人的话,HTML 相当于骨架,JavaScript 相当于肌肉,CSS 相当于皮肤,三者结合起来才能形成一个完善的网页。下面我们分别来介绍一下这三部分的功能。

(1)HTML

HTML,其英文叫做 HyperText Markup Language,中文翻译叫做超文本标记语言,但我们通常不会用中文翻译来称呼它,一般就叫 HTML。

HTML 是用来描述网页的一种语言,网页包括文字、按钮、图片和视频等各种复杂的元素,其基础架构就是 HTML。不同类型的元素通过不同类型的标签来表示,如图片用 img 标签表示,视频用 video 标签表示,段落用 p 标签表示,它们之间的布局又常通过布局标签 div 嵌套组合而成,各种标签通过不同的排列和嵌套才形成了网页的框架。

那 HTML 长什么样子呢?我们可以随意打开一个网站,比如淘宝 https://www.taobao.com,然后右键菜单点击“检查元素”或者按 F12 快捷键,即可打开浏览器开发者工具,切换到 Elements 面板,这时候就可以看到这里呈现的就是淘宝网对应的 HTML,它包含了一系列标签,浏览器解析这些标签后,便会在网页中渲染成一个个的节点,这便形成了我们平常看到的网页。比如这里可以看到一个输入框就对应一个 input 标签,可以用于输入文字。

不同的标签对应着不同的功能,这些标签定义的节点相互嵌套和组合形成了复杂的层次关系,就形成了网页的架构。

(2)CSS

HTML 定义了网页的结构,但是只有 HTML 页面的布局并不美观,可能只是简单的节点元素的排列。为了让网页看起来更好看一些,这里借助了 CSS。

CSS,全称叫作 Cascading Style Sheets,即层叠样式表。“层叠” 是指当在 HTML 中引用了数个样式文件,并且样式发生冲突时,浏览器能依据层叠顺序处理。“样式” 指网页中文字大小、颜色、元素间距、排列等格式。CSS 是目前唯一的网页页面排版样式标准,有了它的帮助,页面才会变得更为美观。

在上图中,Styles 面板呈现的就是一系列 CSS 样式,比如摘抄一段 CSS,内容如下:

1
2
3
4
5
6
#head_wrapper.s-ps-islite .s-p-top {
position: absolute;
bottom: 40px;
width: 100%;
height: 181px;
}

这就是一个 CSS 样式。大括号前面是一个 CSS 选择器。此选择器的意思是首先选中 idhead_wrapperclasss-ps-islite 的节点,然后再选中其内部的 classs-p-top 的节点。大括号内部写的就是一条条样式规则,例如 position 指定了这个节点的布局方式为绝对布局,bottom 指定节点的下边距为 40 像素,width 指定了宽度为 100%,表示占满父节点,height 则指定了节点的高度。也就是说,我们将位置、宽度、高度等样式配置统一写成这样的形式,然后用大括号括起来,接着在开头再加上 CSS 选择器,这就代表这个样式对 CSS 选择器选中的节点生效,节点就会根据此样式来展示了。

在网页中,一般会统一定义整个网页的样式规则,并写入 CSS 文件中(其后缀为 css)。在 HTML 中,只需要用 link 标签即可引入写好的 CSS 文件,这样整个页面就会变得美观、优雅。

(3)JavaScript

JavaScript,简称 JS,是一种脚本语言。HTML 和 CSS 配合使用,提供给用户的只是一种静态信息,缺乏交互性。我们在网页里可能会看到一些交互和动画效果,如下载进度条、提示框、轮播图等,这通常就是 JavaScript 的功劳。它的出现使得用户与信息之间不只是一种浏览与显示的关系,而是实现了一种实时、动态、交互的页面功能。

JavaScript 通常也是以单独的文件形式加载的,后缀为 js,在 HTML 中通过 script 标签即可引入,例如:

1
<script src="jquery-2.1.0.js"></script>

综上所述,HTML 定义了网页的内容和结构,CSS 描述了网页的样式,JavaScript 定义了网页的行为。

2. 网页的结构

我们首先用例子来感受一下 HTML 的基本结构。新建一个文本文件,名称叫做 test.html,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>This is a Demo</title>
</head>
<body>
<div id="container">
<div class="wrapper">
<h2 class="title">Hello World</h2>
<p class="text">Hello, this is a paragraph.</p>
</div>
</div>
</body>
</html>

这就是一个最简单的 HTML 实例。开头用 DOCTYPE 定义了文档类型,其次最外层是 html 标签,最后还有对应的结束标签来表示闭合,其内部是 head 标签和 body 标签,分别代表网页头和网页体,它们也需要结束标签。head 标签内定义了一些页面的配置和引用,如:

1
<meta charset="UTF-8" />

它指定了网页的编码为 UTF-8。

title 标签则定义了网页的标题,会显示在网页的选项卡中,不会显示在正文中。body 标签内则是在网页正文中显示的内容。div 标签定义了网页中的区块,它的 idcontainer,这是一个非常常用的属性,且 id 的内容在网页中是唯一的,我们可以通过它来获取这个区块。然后在此区块内又有一个 div 标签,它的 classwrapper,这也是一个非常常用的属性,经常与 CSS 配合使用来设定样式。然后此区块内部又有一个 h2 标签,这代表一个二级标题。另外,还有一个 p 标签,这代表一个段落。在这两者中直接写入相应的内容即可在网页中呈现出来,它们也有各自的 class 属性。

将代码保存后,双击该文件在浏览器中打开,可以看到如图所示的内容。

运行结果

可以看到,选项卡上显示了 This is a Demo 字样,这是我们在 head 中的 title 里定义的文字。而网页正文是 body 标签内部定义的各个元素生成的,可以看到这里显示了二级标题和段落。

这个实例便是网页的一般结构。一个网页的标准形式是 html 标签内嵌套 headbody 标签,head 内定义网页的配置和引用,body 内定义网页的正文。

3 节点树及节点间的关系

在 HTML 中,所有标签定义的内容都是节点,它们构成了一个 HTML 节点树,也称之为 HTML DOM 树。

我们先看下什么是 DOM。DOM 是 W3C(万维网联盟)的标准,其英文全称 Document Object Model,即文档对象模型。它定义了访问 HTML 和 XML 文档的标准。根据 W3C 的 HTML DOM 标准,HTML 文档中的所有内容都是节点。

  • 整个网站文档是一个文档节点。
  • 每个 html 标签对应一个根元素节点,即上例中的 html 标签,这属于一个跟元素节点。
  • 节点内的文本是文本节点,比如 a 节点代表一个超链接,它内部的文本也被认为是一个文本节点。
  • 每个节点的属性是属性节点,比如 a 节点有一个 href 属性,它就是一个属性节点。
  • 注释是注释节点,在 HTML 中有特殊的语法会被解析为注释,但其也会对应一个节点。

所以,HTML DOM 将 HTML 文档视作树结构,这种结构被称为节点树,如图所示:

节点树

通过 HTML DOM,树中的所有节点均可通过 JavaScript 访问,所有 HTML 节点元素均可被修改,也可以被创建或删除。

节点树中的节点彼此拥有层级关系。我们常用父(parent)、子(child)和兄弟(sibling)等术语描述这些关系。父节点拥有子节点,同级的子节点被称为兄弟节点。

在节点树中,顶端节点称为根(root)。除了根节点之外,每个节点都有父节点,同时可拥有任意数量的子节点或兄弟节点。图展示了节点树以及节点之间的关系。

节点树及节点间的关系

4. 选择器

我们知道网页由一个个节点组成,CSS 选择器会根据不同的节点设置不同的样式规则,那么怎样来定位节点呢?

在 CSS 中,我们使用 CSS 选择器来定位节点。例如,上例中 div 节点的 idcontainer,那么就可以表示为 #container,其中 # 开头代表选择 id,其后紧跟 id 的名称。另外,如果我们想选择 classwrapper 的节点,便可以使用.wrapper,这里以点(.)开头代表选择 class,其后紧跟 class 的名称。另外,还有一种选择方式,那就是根据标签名筛选,例如想选择二级标题,直接用 h2 即可。这是最常用的 3 种表示,分别是根据 idclass、标签名筛选,请牢记它们的写法。

另外,CSS 选择器还支持嵌套选择,各个选择器之间加上空格分隔开便可以代表嵌套关系,如 #container .wrapper p 则代表先选择 idcontainer 的节点,然后选中其内部的 classwrapper 的节点,然后再进一步选中其内部的 p 节点。另外,如果不加空格,则代表并列关系,如 div#container .wrapper p.text 代表先选择 idcontainerdiv 节点,然后选中其内部的 classwrapper 的节点,再进一步选中其内部的 classtextp 节点。这就是 CSS 选择器,其筛选功能还是非常强大的。

我们可以在浏览器中测试 CSS 选择器的效果,依然还是打开浏览器的开发者工具,然后按快捷键 Ctrl + F(如果你用的是 Mac,则是 Command + F),这时候在左下角便会出现一个搜索框,如图所示。

这时候我们输入 .title 就是选中了 class 为 title 的节点,这时候该节点就会被选中并在网页中高亮显示,如图所示:

输入 div#container .wrapper p.text 就逐层选中了 id 为 container 中 class 为 wrapper 节点中的 p 节点,如图所示:

另外,CSS 选择器还有一些其他语法规则,具体如下表所示。

CSS 选择器的其他语法规则

选 择 器 例  子 例子描述
.class .intro 选择 class="intro" 的所有节点
#id #firstname 选择 id="firstname" 的所有节点
* * 选择所有节点
element p 选择所有 p 节点
element,element div,p 选择所有 div 节点和所有 p 节点
element element div p 选择 div 节点内部的所有 p 节点
element>element div>p 选择父节点为 div 节点的所有 p 节点
element+element div+p 选择紧接在 div 节点之后的所有 p 节点
[attribute] [target] 选择带有 target 属性的所有节点
[attribute=value] [target=blank] 选择 target="blank" 的所有节点
[attribute~=value] [title~=flower] 选择 title 属性包含单词 flower 的所有节点
:link a:link 选择所有未被访问的链接
:visited a:visited 选择所有已被访问的链接
:active a:active 选择活动链接
:hover a:hover 选择鼠标指针位于其上的链接
:focus input:focus 选择获得焦点的 input 节点
:first-letter p:first-letter 选择每个 p 节点的首字母
:first-line p:first-line 选择每个 p 节点的首行
:first-child p:first-child 选择属于父节点的第一个子节点的所有 p 节点
:before p:before 在每个 p 节点的内容之前插入内容
:after p:after 在每个 p 节点的内容之后插入内容
:lang(language) p:lang 选择带有以 it 开头的 lang 属性值的所有 p 节点
element1~element2 p~ul 选择前面有 p 节点的所有 ul 节点
[attribute^=value] a[src^="https"] 选择其 src 属性值以 https 开头的所有 a 节点
[attribute$=value] a[src$=".pdf"] 选择其 src 属性以 .pdf 结尾的所有 a 节点
[attribute*=value] a[src*="abc"] 选择其 src 属性中包含 abc 子串的所有 a 节点
:first-of-type p:first-of-type 选择属于其父节点的首个 p 节点的所有 p 节点
:last-of-type p:last-of-type 选择属于其父节点的最后一个 p 节点的所有 p 节点
:only-of-type p:only-of-type 选择属于其父节点唯一的 p 节点的所有 p 节点
:only-child p:only-child 选择属于其父节点的唯一子节点的所有 p 节点
:nth-child(n) p:nth-child 选择属于其父节点的第二个子节点的所有 p 节点
:nth-last-child(n) p:nth-last-child 同上,从最后一个子节点开始计数
:nth-of-type(n) p:nth-of-type 选择属于其父节点第二个 p 节点的所有 p 节点
:nth-last-of-type(n) p:nth-last-of-type 同上,但是从最后一个子节点开始计数
:last-child p:last-child 选择属于其父节点最后一个子节点的所有 p 节点
:root :root 选择文档的根节点
:empty p:empty 选择没有子节点的所有 p 节点(包括文本节点)
:target #news:target 选择当前活动的 #news 节点
:enabled input:enabled 选择每个启用的 input 节点
:disabled input:disabled 选择每个禁用的 input 节点
:checked input:checked 选择每个被选中的 input 节点
:not(selector) :not 选择非 p 节点的所有节点
::selection ::selection 选择被用户选取的节点部分

另外,还有一种比较常用的选择器 XPath,这种选择方式后面会详细介绍。

5. 总结

本节介绍了网页的结构和节点间的关系,了解了这些内容,我们才有更加清晰的思路去解析和提取网页内容。

本节参考来源:

  • 文档 - HTML - MDN Web Docs:https://developer.mozilla.org/en-US/docs/Web/HTML
  • 文档 - JavaScript - MDN Web Docs:https://developer.mozilla.org/en-US/docs/Web/JavaScript
  • 文档 - HTML DOM 节点 - W3School:http://www.w3school.com.cn/htmldom/dom_nodes.asp
  • 文档 - HTML - 维基百科:https://en.wikipedia.org/wiki/HTML
  • 文档 - CSS Selector - W3School:https://www.w3schools.com/cssref/css_selectors.asp

Python

导航目录:【2022 年】Python3 爬虫学习教程

在正式学习网络爬虫之前,我们需要详细了解 HTTP 的基本原理,了解在浏览器中敲入 URL 到获取网页内容之间发生了什么。了解这些内容,有助于我们进一步了解爬虫的基本原理。

1.1 HTTP 基本原理

在本节中,我们会详细了解 HTTP 的基本原理,了解在浏览器中敲入 URL 到获取网页内容之间发生了什么。了解这些内容,有助于我们进一步了解爬虫的基本原理。

1. URI 和 URL

这里我们先了解一下 URI 和 URL。URI 的全称为 Uniform Resource Identifier,即统一资源标志符;而 URL 的全称为 Universal Resource Locator,即统一资源定位符。举例来说,https://github.com/favicon.ico 是一个 URL,也是一个 URI。即有这样一个图标资源,我们用 URL/URI 来唯一指定了它的访问方式,这其中包括了访问协议 https、访问路径(即根目录)和资源名称 favicon.ico。通过这样一个链接,我们便可以从互联网上找到这个资源,这就是 URL/URI。

URL 是 URI 的子集,也就是说每个 URL 都是 URI,但不是每个 URI 都是 URL。那么,怎样的 URI 不是 URL 呢?URI 还包括一个子类,叫作 URN,它的全称为 Universal Resource Name,即统一资源名称。URN 只命名资源而不指定如何定位资源,比如 urn:isbn:0451450523 指定了一本书的 ISBN,可以唯一标识这本书,但是没有指定到哪里定位这本书,这就是 URN。URL、URN 和 URI 的关系可以用图 1-1 表示。

URL、URN 和 URI 关系图

但是在目前的互联网,URN 使用得非常少,几乎所有的 URI 都是 URL,所以对于一般的网页链接,我们既可以称之为 URL,也可以称之为 URI,我个人习惯称之为 URL。

但 URL 也不是随便写的,它也是需要遵循一定的格式规范的,基本的组成格式如下:

1
scheme://[username:password@]hostname[:port][/path][;parameters][?query][#fragment]

其中这里中括号包括的内容代表非必要部分,比如 https://www.baidu.com 这个 URL,这里就只包含了 scheme 和 host 两部分,其他的 port、path、parameters、query、fragment 都没有。

这里我们分别介绍下几部分代表的含义和作用:

  • scheme:协议。比如常用的协议有 http、https、ftp 等等,另外 scheme 也被常称作 protocol,都代表协议的意思。
  • username、password:用户名和密码。在某些情况下 URL 需要提供用户名和密码才能访问,这时候可以把用户名密码放在 host 前面。比如 https://ssr3.scrape.center 这个 URL 需要用户名密码才能访问,那么可以直接写为 https://admin:admin@ssr3.scrape.center 则可以直接访问。
  • hostname:主机地址。可以是域名或 IP 地址,比如 https://www.baidu.com 这个 URL 中的 hostname 就是 www.baidu.com,这就是百度的二级域名。比如 https://8.8.8.8 这个 URL 中 hostname 就是 8.8.8.8,它是一个 IP 地址。
  • port:端口。这是服务器设定的服务端口,比如 https://8.8.8.8:12345 这个 URL 中的端口就是 12345。但是有些 URL 中没有端口信息,这是使用了默认的端口,http 协议的默认端口是 80,https 协议的默认端口是 443。所以 https://www.baidu.com 其实相当于 https://www.baidu.com:443,而 http://www.baidu.com 其实相当于 http://www.baidu.com:80。
  • path:路径。指的是网络资源在服务器中的指定地址,比如 https://github.com/favicon.ico 这里 path 就是 favicon.ico,指的就是访问 GitHub 上的根目录下的 favicon.ico 这个资源。
  • parameters:参数。用来制定访问某个资源的时候的附加信息,比如 https://8.8.8.8:12345/hello;user 这里的 user 就是 parameters。但是 parameters 现在用得很少,所以目前很多人会把该参数后面的 query 部分称为参数,甚至把 parameters 和 query 混用。严格意义上来说,parameters 是分号 ; 后面的内容。
  • query:查询。用来查询某类资源,如果有多个查询,则用 & 隔开。query 其实非常常见,比如 https://www.baidu.com/s?wd=nba&ie=utf-8,这里的 query 部分就是 wd=nba&ie=utf-8,这里指定了 wd 是 nba,ie 是 utf-8。由于 query 比刚才所说的 parameters 使用频率高太多,所以平时我们见到的参数、GET 请求参数、parameters、params 等称呼多数情况指代的也是 query。严格意义上来说,其实应该用 query 来表示。
  • fragment:片段。它是对资源描述的部分补充,可以理解为资源内部的书签。目前它有两个主要应用,一个是用作单页面路由,比如 现代前端框架 Vue、React 都可以借助它来做路由管理;另外一个应用是用作 HTML 锚点,用它可以控制一个页面打开时自动下滑滚动到某个特定的位置。

以上我们就简单了解了 URL 的基本概念和构成,后文我们会结合多个实战案例练习来帮助加深其理解。

2. HTTP 和 HTTPS

刚才我们了解了 URL 的基本构成,其支持的协议有很多,比如 http、https、ftp、sftp、smb 等等。

在爬虫中,我们抓取的页面通常基于 http 或 https 协议,这里首先我们先来了解一下这两个协议的含义。

HTTP 的全称是 Hyper Text Transfer Protocol,中文名叫作超文本传输协议。HTTP 协议是用于从网络传输超文本数据到本地浏览器的传送协议,它能保证高效而准确地传送超文本文档。HTTP 由万维网协会(World Wide Web Consortium)和 Internet 工作小组 IETF(Internet Engineering Task Force)共同合作制定的规范,目前广泛使用的是 HTTP 1.1 版本,当然 HTTP 2.0 现在不少网站也增加了支持。

其发展历史见下表:

版本 产生时间 主要特点 发展现状
HTTP/0.9 1991 年 不涉及数据包传输,规定客户端和服务器之间通信格式,只能 GET 请求 没有作为正式的标准
HTTP/1.0 1996 年 传输内容格式不限制,增加 PUT、PATCH、HEAD、 OPTIONS、DELETE 命令 正式作为标准
HTTP/1.1 1997 年 持久连接(长连接)、节约带宽、HOST 域、管道机制、分块传输编码 正式作为标准并广泛使用
HTTP/2.0 2015 年 多路复用、服务器推送、头信息压缩、二进制协议等 逐渐覆盖市场

HTTPS 的全称是 Hyper Text Transfer Protocol over Secure Socket Layer,是以安全为目标的 HTTP 通道,简单讲是 HTTP 的安全版,即在 HTTP 下加入 SSL 层,简称为 HTTPS。

HTTPS 的安全基础是 SSL,因此通过它传输的内容都是经过 SSL 加密的,它的主要作用分为以下两种。

  • 建立一个信息安全通道,保证数据传输的安全性。
  • 确认网站的真实性。凡是使用了 https 的网站,都可以通过点击浏览器地址栏的锁头标志来查看网站认证之后的真实信息,也可以通过 CA 机构颁发的安全签章来查询。

现在越来越多的网站和 App 都已经向 HTTPS 方向发展,举例如下。

  • 苹果公司强制所有 iOS App 在 2017 年 1 月 1 日前全部改为使用 HTTPS 加密,否则 App 就无法在应用商店上架。
  • 谷歌从 2017 年 1 月推出的 Chrome 56 开始,对未进行 HTTPS 加密的网址亮出风险提示,即在地址栏的显著位置提醒用户 “此网页不安全”。
  • 腾讯微信小程序的官方需求文档要求后台使用 HTTPS 请求进行网络通信,不满足条件的域名和协议无法请求。

因此,HTTPS 已经是大势所趋。

注:HTTP 和 HTTPS 协议都属于计算机网络中的应用层协议,其下层是基于 TCP 协议实现的,TCP 协议属于计算机网络中的传输层协议,包括建立连接时的三次握手和断开时的四次挥手等过程。但本书主要讲的是网络爬虫相关,主要爬取的是 HTTP/HTTPS 协议相关的内容,所以这里就不再展开深入讲解 TCP、IP 等相关知识了,感兴趣的读者可以搜索相关资料了解下,如《计算机网络》、《图解 HTTP》等书籍。

3. HTTP 请求过程

我们在浏览器中输入一个 URL,回车之后便会在浏览器中观察到页面内容。

实际上,这个过程是浏览器向网站所在的服务器发送了一个请求,网站服务器接收到这个请求后进行处理和解析,然后返回对应的响应,接着传回给浏览器。

由于响应里包含页面的源代码等内容,浏览器再对其进行解析,便将网页呈现了出来,流程如图 1-3 所示。

模型图

此处客户端即代表我们自己的 PC 或手机浏览器,服务器即要访问的网站所在的服务器。

为了更直观地说明这个过程,这里用 Chrome 浏览器开发者模式下的 Network 监听组件来做下演示,它可以显示访问当前请求网页时发生的所有网络请求和响应。

打开 Chrome 浏览器,访问百度 http://www.baidu.com/,这时候鼠标右键并选择“检查”菜单(或直接按快捷键 F12),即可打开浏览器的开发者工具,如下图所示:

打开浏览器的开发者工具

我们切换到 Network 面板,然后重新刷新网页,这时候就可以看到在 Network 面板下方出现了很多个条目,其中一个条目就代表一次发送请求和接收响应的过程,如图所示:

请求和接收响应的过程

我们先观察第一个网络请求,即 www.baidu.com,其中各列的含义如下。

  • 第一列 Name:请求的名称,一般会将 URL 的最后一部分内容当作名称。
  • 第二列 Status:响应的状态码,这里显示为 200,代表响应是正常的。通过状态码,我们可以判断发送了请求之后是否得到了正常的响应。
  • 第三列 Protocol:请求的协议类型,这里 http/1.1 代表是 HTTP 1.1 版本,h2 代表 HTTP 2.0 版本。
  • 第四列 Type:请求的文档类型。这里为 document,代表我们这次请求的是一个 HTML 文档,内容就是一些 HTML 代码。
  • 第五列 Initiator:请求源。用来标记请求是由哪个对象或进程发起的。
  • 第六列 Size:从服务器下载的文件和请求的资源大小。如果是从缓存中取得的资源,则该列会显示 from cache。
  • 第七列 Time:发起请求到获取响应所用的总时间。
  • 第八列 Waterfall:网络请求的可视化瀑布流。

我们点击这个条目,即可看到其更详细的信息,如图所示。

更详细的信息

首先是 General 部分,其中 Request URL 为请求的 URL,Request Method 为请求的方法,Status Code 为响应状态码,Remote Address 为远程服务器的地址和端口,Referrer Policy 为 Referrer 判别策略。

再继续往下可以看到,有 Response Headers 和 Request Headers,它们分别代表响应头和请求头。请求头里带有许多请求信息,例如浏览器标识、Cookie、Host 等信息,这是请求的一部分,服务器会根据请求头内的信息判断请求是否合法,进而作出对应的响应。图 1-5 中看到的 Response Headers 就是响应的一部分,其中包含了服务器的类型、文档类型、日期等信息,浏览器接收到响应后,会解析响应内容,进而呈现网页内容。

下面我们分别来介绍一下请求和响应都包含哪些内容。

4. 请求(Request)

请求,英文为 Request,由客户端向服务器发出,可以分为 4 部分内容:请求方法(Request Method)、请求的网址(Request URL)、请求头(Request Headers)、请求体(Request Body)。

下面我们分别予以介绍。

请求方法(Request Method)

请求方法,英文为 Request Method,用于标识请求客户端请求服务端的方式,常见的请求方法有两种: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 请求。另外,还有一些请求方法,如 GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE 等,我们简单将其总结为下表。

方  法 描  述
GET 请求页面,并返回页面内容
HEAD 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头
POST 大多用于提交表单或上传文件,数据包含在请求体中
PUT 从客户端向服务器传送的数据取代指定文档中的内容
DELETE 请求服务器删除指定的页面
CONNECT 把服务器当作跳板,让服务器代替客户端访问其他网页
OPTIONS 允许客户端查看服务器的性能
TRACE 回显服务器收到的请求,主要用于测试或诊断

本表参考:http://www.runoob.com/http/http-methods.html

请求的网址(Request URL)

请求的网址,英文为 Reqeust URL,它可以唯一确定我们想请求的资源。关于 URL 的构成和各个部分的功能我们在前文已经提及到了,这里就不再赘述。

请求头(Request Headers)

请求头,英文为 Request Headers,用来说明服务器要使用的附加信息,比较重要的信息有 Cookie、Referer、User-Agent 等。

下面简要说明一些常用的头信息:

  • Accept:请求报头域,用于指定客户端可接受哪些类型的信息。
  • Accept-Language:指定客户端可接受的语言类型。
  • Accept-Encoding:指定客户端可接受的内容编码。
  • Host:用于指定请求资源的主机 IP 和端口号,其内容为请求 URL 的原始服务器或网关的位置。从 HTTP 1.1 版本开始,请求必须包含此内容。
  • Cookie:也常用复数形式 Cookies,这是网站为了辨别用户进行会话跟踪而存储在用户本地的数据。它的主要功能是维持当前访问会话。例如,我们输入用户名和密码成功登录某个网站后,服务器会用会话保存登录状态信息,后面我们每次刷新或请求该站点的其他页面时,会发现都是登录状态,这就是 Cookie 的功劳。Cookie 里有信息标识了我们所对应的服务器的会话,每次浏览器在请求该站点的页面时,都会在请求头中加上 Cookie 并将其发送给服务器,服务器通过 Cookie 识别出是我们自己,并且查出当前状态是登录状态,所以返回结果就是登录之后才能看到的网页内容。
  • Referer:此内容用来标识这个请求是从哪个页面发过来的,服务器可以拿到这一信息并做相应的处理,如做来源统计、防盗链处理等。
  • User-Agent:简称 UA,它是一个特殊的字符串头,可以使服务器识别客户使用的操作系统及版本、浏览器及版本等信息。在做爬虫时加上此信息,可以伪装为浏览器;如果不加,很可能会被识别为爬虫。
  • Content-Type:也叫互联网媒体类型(Internet Media Type)或者 MIME 类型,在 HTTP 协议消息头中,它用来表示具体请求中的媒体类型信息。例如,text/html 代表 HTML 格式,image/gif 代表 GIF 图片,application/json 代表 JSON 类型,更多对应关系可以查看此对照表:http://tool.oschina.net/commons

因此,请求头是请求的重要组成部分,在写爬虫时,大部分情况下都需要设定请求头。

请求体(Request Body)

请求体,即 Request Body 一般承载的内容是 POST 请求中的表单数据,而对于 GET 请求,请求体则为空。

例如,这里我登录 GitHub 时捕获到的请求和响应如图 1-6 所示。

请求和响应

登录之前,我们填写了用户名和密码信息,提交时这些内容就会以表单数据的形式提交给服务器,此时需要注意 Request Headers 中指定 Content-Type 为 application/x-www-form-urlencoded。只有设置 Content-Type 为 application/x-www-form-urlencoded,才会以表单数据的形式提交。另外,我们也可以将 Content-Type 设置为 application/json 来提交 JSON 数据,或者设置为 multipart/form-data 来上传文件。

如下表是 Content-Type 和 POST 提交数据方式的关系

Content-Type 提交数据的方式
application/x-www-form-urlencoded 表单数据
multipart/form-data 表单文件上传
application/json 序列化 JSON 数据
text/xml XML 数据

在爬虫中,如果要构造 POST 请求,需要使用正确的 Content-Type,并了解各种请求库的各个参数设置时使用的是哪种 Content-Type,不然可能会导致 POST 提交后无法正常响应。

5. 响应(Response)

响应,即 Response,由服务器返回给客户端,可以分为三部分:响应状态码(Response Status Code)、响应头(Response Headers)和响应体(Response Body)。

响应状态码(Response Status Code)

响应状态码,即 Response Status Code,表示服务器的响应状态,如 200 代表服务器正常响应,404 代表页面未找到,500 代表服务器内部发生错误。在爬虫中,我们可以根据状态码来判断服务器响应状态,如状态码为 200,则证明成功返回数据,再进行进一步的处理,否则直接忽略。下表列出了常见的错误代码及错误原因。

常见的错误代码及错误原因

状态码 说  明 详  情
100 继续 请求者应当继续提出请求。服务器已收到请求的一部分,正在等待其余部分
101 切换协议 请求者已要求服务器切换协议,服务器已确认并准备切换
200 成功 服务器已成功处理了请求
201 已创建 请求成功并且服务器创建了新的资源
202 已接受 服务器已接受请求,但尚未处理
203 非授权信息 服务器已成功处理了请求,但返回的信息可能来自另一个源
204 无内容 服务器成功处理了请求,但没有返回任何内容
205 重置内容 服务器成功处理了请求,内容被重置
206 部分内容 服务器成功处理了部分请求
300 多种选择 针对请求,服务器可执行多种操作
301 永久移动 请求的网页已永久移动到新位置,即永久重定向
302 临时移动 请求的网页暂时跳转到其他页面,即暂时重定向
303 查看其他位置 如果原来的请求是 POST,重定向目标文档应该通过 GET 提取
304 未修改 此次请求返回的网页未修改,继续使用上次的资源
305 使用代理 请求者应该使用代理访问该网页
307 临时重定向 请求的资源临时从其他位置响应
400 错误请求 服务器无法解析该请求
401 未授权 请求没有进行身份验证或验证未通过
403 禁止访问 服务器拒绝此请求
404 未找到 服务器找不到请求的网页
405 方法禁用 服务器禁用了请求中指定的方法
406 不接受 无法使用请求的内容响应请求的网页
407 需要代理授权 请求者需要使用代理授权
408 请求超时 服务器请求超时
409 冲突 服务器在完成请求时发生冲突
410 已删除 请求的资源已永久删除
411 需要有效长度 服务器不接受不含有效内容长度标头字段的请求
412 未满足前提条件 服务器未满足请求者在请求中设置的其中一个前提条件
413 请求实体过大 请求实体过大,超出服务器的处理能力
414 请求 URI 过长 请求网址过长,服务器无法处理
415 不支持类型 请求格式不被请求页面支持
416 请求范围不符 页面无法提供请求的范围
417 未满足期望值 服务器未满足期望请求标头字段的要求
500 服务器内部错误 服务器遇到错误,无法完成请求
501 未实现 服务器不具备完成请求的功能
502 错误网关 服务器作为网关或代理,从上游服务器收到无效响应
503 服务不可用 服务器目前无法使用
504 网关超时 服务器作为网关或代理,但是没有及时从上游服务器收到请求
505 HTTP 版本不支持 服务器不支持请求中所用的 HTTP 协议版本

响应头(Response Headers)

响应头,即 Response Headers,包含了服务器对请求的应答信息,如 Content-Type、Server、Set-Cookie 等。下面简要说明一些常用的头信息。

  • Date:标识响应产生的时间。
  • Last-Modified:指定资源的最后修改时间。
  • Content-Encoding:指定响应内容的编码。
  • Server:包含服务器的信息,比如名称、版本号等。
  • Content-Type:文档类型,指定返回的数据类型是什么,如 text/html 代表返回 HTML 文档,application/x-javascript 则代表返回 JavaScript 文件,image/jpeg 则代表返回图片。
  • Set-Cookie:设置 Cookie。响应头中的 Set-Cookie 告诉浏览器需要将此内容放在 Cookie 中,下次请求携带 Cookie 请求。
  • Expires:指定响应的过期时间,可以使代理服务器或浏览器将加载的内容更新到缓存中。如果再次访问时,就可以直接从缓存中加载,降低服务器负载,缩短加载时间。

响应体(Response Body)

响应体,即 Response Body,这可以说是最关键的部分了,响应的正文数据都在响应体中,比如请求网页时,它的响应体就是网页的 HTML 代码;请求一张图片时,它的响应体就是图片的二进制数据。我们做爬虫请求网页后,要解析的内容就是响应体,如图 1-7 所示。

响应体

在浏览器开发者工具中点击 Preview,就可以看到网页的源代码,也就是响应体的内容,它是解析的目标。

在做爬虫时,我们主要通过响应体得到网页的源代码、JSON 数据等,然后从中做相应内容的提取。

本节中,我们了解了 HTTP 的基本原理,大概了解了访问网页时背后的请求和响应过程。本节涉及的知识点需要好好掌握,后面分析网页请求时会经常用到。

6. HTTP/2.0

前面我们也提到了 HTTP 协议从 2015 年起发布了 2.0 版本,相比 HTTP/1.1 来说,HTTP/2.0 变得更快、更简单、更稳定,HTTP/2.0 在传输层做了很多优化,HTTP/2.0 的主要目标是通过支持完整的请求与响应复用来减少延迟,并通过有效压缩 HTTP 请求头字段将协议开销降至最低,同时增加对请求优先级和服务器推送的支持,这些优化一笔勾销了 HTTP/1.1 为做传输优化想出的一系列“歪招”。

有读者这时候可能会问,为什么不叫 HTTP/1.2 而叫 HTTP/2.0 呢?因为 HTTP/2.0 在内部实现上新的二进制分帧层,这是没法与之前的 HTTP/1.x 的服务器和客户端实现向后兼容的,所以直接修改了主版本号为 2.0。

下面我们就来了解下 HTTP/2.0 相比 HTTP/1.1 来说做了哪些优化吧。

二进制分帧层

HTTP/2.0 所有性能增强的核心就在于这个新的二进制分帧层。在 HTTP/1.x 中,不管是请求(Request)还是响应(Response),它们都是用文本格式传输的,其头部(Headers)、实体(Body)之间也是用文本换行符分隔开的。HTTP/2.0 对其做了优化,将文本格式修改为了二进制格式,使得解析起来更加高效。同时将请求和响应数据分割为更小的帧,并采用二进制编码。

所以这里就引入了几个新的概念:

  • 帧:只存在于 HTTP/2.0 中的概念,是数据通信的最小单位,比如一个请求被分为了请求头帧(Request Headers frame)和请求体/数据帧(Request Data frame)。
  • 数据流:一个虚拟通道,可以承载双向的消息,每个流都有一个唯一的整数 ID 来标识。
  • 消息:与逻辑请求或响应消息对应的完整的一系列帧。

在 HTTP/2.0 中,同域名下的所有通信都可以在单个连接上完成,该连接可以承载任意数量的双向数据流,数据流是用于承载双向消息的,每条消息都是一条逻辑 HTTP 消息(例如请求或响应),它可以包含一个或多个帧。

简而言之,HTTP/2.0 将 HTTP 协议通信分解为二进制编码帧的交换,这些帧对应着特定数据流中的消息,所有这些都在一个 TCP 连接内复用,这是 HTTP/2.0 协议所有其他功能和性能优化的基础。

多路复用

在 HTTP/1.x 中,如果客户端要想发起多个并行请求以提升性能,则必须使用多个 TCP 连接,而且浏览器位了控制资源,还会对单个域名有 6-8 个 TCP 连接请求的限制。但在 HTTP/2.0 中,由于又了二进制分帧技术的加持,HTTP/2.0 不用再以来 TCP 连接去实现多路并行了,客户端和服务器可以将 HTTP 消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来,让我们可以:

  • 并行交错地发送多个请求,请求之间互不影响。
  • 并行交错地发送多个响应,响应之间互不干扰。
  • 使用一个连接并行发送多个请求和响应。
  • 不必再为绕过 HTTP/1.x 限制而做很多工作。
  • 消除不必要的延迟和提高现有网络容量的利用率,从而减少页面加载时间。

这样以来,整个数据传输使性能就有了极大提升:

  • 同个域名只需要占用一个 TCP 连接,使用一个连接并行发送多个请求和响应,消除了因多个 TCP 连接而带来的延时和内存消耗。
  • 并行交错地发送多个请求和详情,而且之间互不影响。
  • 在 HTTP/2.0 中,每个请求都可以带一个 31bit 的优先值,0 表示最高优先级, 数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。

流量控制

流控制是一种阻止发送方向接收方发送大量数据的机制,以免超出后者的需求或处理能力。可以理解为,接收方已经太繁忙了,来不及处理收到的消息了,但是发送方还在一直大量发送消息,这样就会出现一些问题。

比如说,客户端可能请求了一个具有较高优先级的大型视频流,但是用户已经暂停视频,客户端现在希望暂停或限制从服务器的传输,以免提取和缓冲不必要的数据。 再比如,一个代理服务器可能具有较快的下游连接和较慢的上游连接,并且也希望调节下游连接传输数据的速度以匹配上游连接的速度来控制其资源利用率等等。

由于 HTTP 是基于 TCP 实现的,虽然 TCP 原生有流量控制机制,但是由于 HTTP/2.0 数据流在一个 TCP 连接内复用,TCP 流控制既不够精细,也无法提供必要的应用级 API 来调节各个数据流的传输。

为了解决这一问题,HTTP/2.0 提供了一组简单的构建块,这些构建块允许客户端和服务器实现其自己的数据流和连接级流控制:

  • 流控制具有方向性。 每个接收方都可以根据自身需要选择为每个数据流和整个连接设置任意的窗口大小。
  • 流控制基于信用。 每个接收方都可以公布其初始连接和数据流流控制窗口(以字节为单位),每当发送方发出 DATA 帧时都会减小,在接收方发出 WINDOW_UPDATE 帧时增大。
  • 流控制无法停用。 建立 HTTP/2.0 连接后,客户端将与服务器交换 SETTINGS 帧,这会在两个方向上设置流控制窗口。 流控制窗口的默认值设为 65535 字节,但是接收方可以设置一个较大的最大窗口大小(2^31-1 字节),并在接收到任意数据时通过发送 WINDOW_UPDATE 帧来维持这一大小。
  • 流控制为逐跃点控制,而非端到端控制。 即,可信中介可以使用它来控制资源使用,以及基于自身条件和启发式算法实现资源分配机制。

由此可见,HTTP/2.0 提供了简单的构建块实现了自定义策略来调节资源使用和分配,以及实现新传输能力,同时提升了网页应用的实际性能和感知性能。

服务端推送

HTTP/2.0 新增的另一个强大的新功能是,服务器可以对一个客户端请求发送多个响应。 换句话说,除了对最初请求的响应外,服务器还可以向客户端推送额外资源,而无需客户端明确地请求。

如果某些资源客户端是一定会请求的,这时就可以采取服务端推送的技术,在客户端发起一次请求后,额外提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。例如,服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 时再发送这些请求。

服务端推送

服务端可以主动推送,当然客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送 RST_STREAM 帧来拒收。

另外主动推送也遵守同源策略,即服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行,这样也能保证一定的安全性。

HTTP/2.0 发展现状

HTTP/2.0 的普及是一件任重而道远的事情,一些主流的网站现在已经支持了 HTTP/2.0,主流浏览器现在都已经实现了 HTTP/2.0 的支持,但总的来看,目前大部分网站依然还是以 HTTP/1.1 为主。

另外一些编程语言的库还没有完全支持 HTTP/2.0,比如对于 Python 来说,hyper、httpx 等库已经支持了 HTTP/2.0,但广泛使用的 requests 库依然还是只支持 HTTP/1.1。

7. 总结

本节介绍了关于 HTTP 的一些基础知识,内容不少,需要好好掌握,这些知识对于后面我们编写和理解网络爬虫具有非常大的帮助。

由于本节的内容多数为概念介绍,内容参考了很多书籍、文档、博客,来源如下:

  • 书籍 - 《HTTP 权威指南》- 作者 David Gourley / Brian Totty
  • 文档 - HTTP - 维基百科:https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol
  • 文档 - HTTP - 百度百科:https://baike.baidu.com/item/HTTP/243074
  • 文档 - HTTP - MDN Web Docs:https://developer.mozilla.org/en-US/docs/Web/HTTP
  • 文档 - HTTP/2 简介 - Google 开发文档:https://developers.google.com/web/fundamentals/performance/http2
  • 博客 - 一文读懂 HTTP/2 及 HTTP/3 特性:https://blog.fundebug.com/2019/03/07/understand-http2-and-http3/
  • 博客 - 一文读懂 HTTP/2 特性:https://zhuanlan.zhihu.com/p/26559480

Markdown

相信绝大多数朋友做 PPT(幻灯片 / Slides / Deck 等各种称呼了)都是用的 PowerPoint 或者 KeyNote 吧?功能是比较强大,但你有没有遇到过这样的痛点:

  • 各种标题、段落的格式不统一,比如字体大小、行间距等等各个页面不太一样,然后得用格式刷来挨个刷一下。
  • 想给 PPT 做版本控制,然后就保存了各种复制版本,比如“一版”、“二版”、“终版”、“最终版”、“最终不改版”、“最终稳定不改版”等等,想必大家都见过类似这样的场景吧。
  • 想插入代码,但是插入之后发现格式全乱了或者高亮全没了,然后不得不截图插入进去。
  • 想插入个公式,然后发现 PPT、Keynote 对 Latex 兼容不太好或者配置稍微麻烦,就只能自己重新敲一遍或者贴截图。
  • 想插入一个酷炫的交互组件,比如嵌入一个微博的网页页面实时访问、插入一个可以交互的组件、插入一个音乐播放器组件,原生的 PPT 功能几乎都不支持,这全得依赖于 PowerPoint 或者 KeyNote 来支持才行。

如果你遇到这些痛点,那请你一定要看下去。如果你没有遇到,那也请你看下去吧(拜托。

好,说回正题,我列举了那么多痛点,那这些痛点咋解决呢?

能!甚至解决方案更加轻量级,那就是用 Markdown 来做 PPT!

你试过用 Markdown 写 PPT 吗?没有吧,试试吧,试过之后你就发现上面的功能简直易如反掌。

具体怎么实现呢?

接下来,就有请今天的主角登场了!它就是 Slidev。

什么是 Slidev?

简而言之,Slidev 就是可以让我们用 Markdown 写 PPT 的工具库,基于 Node.js、Vue.js 开发。

利用它我们可以简单地把 Markdown 转化成 PPT,而且它可以支持各种好看的主题、代码高亮、公式、流程图、自定义的网页交互组件,还可以方便地导出成 pdf 或者直接部署成一个网页使用。

官方主页:https://sli.dev/

GitHub:https://github.com/slidevjs/slidev

安装和启动

下面我们就来了解下它的基本使用啦。

首先我们需要先安装好 Node.js,推荐 14.x 及以上版本,安装方法见 https://setup.scrape.center/nodejs

接着,我们就可以使用 npm 这个命令了。

然后我们可以初始化一个仓库,运行命令如下:

1
npm init slidev@latest

这个命令就是初始化一个 Slidev 的仓库,运行之后它会让我们输入和选择一些选项,如图所示:

比如上图就是先输入项目文件夹的名称,比如这里我取名叫做 slidevtest。

总之一些选项完成之后,Slidev 会在本地 3000 端口上启动,如图所示:

接着,我们就可以打开浏览器 http://localhost:3000 来查看一个 HelloWorld 版本的 PPT 了,如图所示:

我们可以点击空格进行翻页,第二页展示了一张常规的 PPT 的样式,包括标题、正文、列表等,如图所示:

那这一页的 Markdown 是什么样的呢?其实就是非常常规的 Markdown 文章的写法,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# What is Slidev?

Slidev is a slides maker and presenter designed for developers, consist of the following features

- 📝 **Text-based** - focus on the content with Markdown, and then style them later
- 🎨 **Themable** - theme can be shared and used with npm packages
- 🧑‍💻 **Developer Friendly** - code highlighting, live coding with autocompletion
- 🤹 **Interactive** - embedding Vue components to enhance your expressions
- 🎥 **Recording** - built-in recording and camera view
- 📤 **Portable** - export into PDF, PNGs, or even a hostable SPA
- 🛠 **Hackable** - anything possible on a webpage

<br>
<br>

Read more about [Why Slidev?](https://sli.dev/guide/why)

是不是?我们只需要用同样格式的 Markdown 语法就可以轻松将其转化为 PPT 了。

快捷键操作

再下一页介绍了各种快捷键的操作,这个就很常规了,比如点击空格、上下左右键来进行页面切换,如图所示:

更多快捷键的操作可以看这里的说明:https://sli.dev/guide/navigation.html,一些简单的快捷键列举如下:

  • f:切换全屏
  • right / space:下一动画或幻灯片
  • left:上一动画或幻灯片
  • up:上一张幻灯片
  • down:下一张幻灯片
  • o:切换幻灯片总览
  • d:切换暗黑模式
  • g:显示“前往…”

代码高亮

接下来就是代码环节了,因为 Markdown 对代码编写非常友好,所以展示自然也不是问题了,比如代码高亮、代码对齐等都是常规操作,如图所示:

那左边的代码定义就直接这么写就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Code

Use code snippets and get the highlighting directly![^1]

```ts {all|2|1-6|9|all}
interface User {
id: number
firstName: string
lastName: string
role: string
}

function updateUser(id: number, update: User) {
const user = getUser(id)
const newUser = {...user, ...update}
saveUser(id, newUser)
}
```

由于是 Markdown,所以我们可以指定是什么语言,比如 TypeScript、Python 等等。

网页组件

接下来就是非常酷炫的环节了,我们还可以自定义一些网页组件,然后展示出来。

比如我们看下面的一张图。左边就呈现了一个数字计数器,点击左侧数字就会减 1,点击右侧数字就会加 1;另外图的右侧还嵌入了一个组件,这里显示了一个推特的消息,通过一个卡片的形式呈现了出来,不仅仅可以看内容,甚至我们还可以点击下方的喜欢、回复、复制等按钮来进行一些交互。

这些功能在网页里面并不稀奇,但是如果能做到 PPT 里面,那感觉就挺酷的。

那这一页怎么做到的呢?这个其实是引入了一些基于 Vue.js 的组件,本节对应的 Markdown 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Components

<div grid="~ cols-2 gap-4">
<div>

You can use Vue components directly inside your slides.

We have provided a few built-in components like `<Tweet/>` and `<Youtube/>` that you can use directly. And adding your custom components is also super easy.

```html
<Counter :count="10" />
```

<!-- ./components/Counter.vue -->
<Counter :count="10" m="t-4" />

Check out [the guides](https://sli.dev/builtin/components.html) for more.

</div>
<div>

```html
<Tweet id="1390115482657726468" />
```

<Tweet id="1390115482657726468" scale="0.65" />

</div>
</div>

这里我们可以看到,这里引入了 Counter、Tweet 组件,而这个 Counter 就是 Vue.js 的组件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<script setup lang="ts">
import { ref } from 'vue'

const props = defineProps({
count: {
default: 0,
},
})

const counter = ref(props.count)
</script>

<template>
<div flex="~" w="min" border="~ gray-400 opacity-50 rounded-md">
<button
border="r gray-400 opacity-50"
p="2"
font="mono"
outline="!none"
hover:bg="gray-400 opacity-20"
@click="counter -= 1"
>
-
</button>
<span m="auto" p="2">{{ counter }}</span>
<button
border="l gray-400 opacity-50"
p="2"
font="mono"
outline="!none"
hover:bg="gray-400 opacity-20"
@click="counter += 1"
>
+
</button>
</div>
</template>

这就是一个标准的基于 Vue.js 3.x 的组件,都是标准的 Vue.js 语法,所以如果我们要添加想要的组件,直接自己写就行了,什么都能实现,只要网页能支持的,统统都能写!

主题定义

当然,一些主题定制也是非常方便的,我们可以在 Markdown 文件直接更改一些配置就好了,比如就把 theme 换个名字,整个主题样式就变了,看如下的对比图:

上面就是一些内置主题,当然我们也可以去官方文档查看一些别人已经写好的主题,见:https://sli.dev/themes/gallery.html

另外我们自己写主题也是可以的,所有的主题样式都可以通过 CSS 等配置好,想要什么就可以有什么,见:https://sli.dev/themes/write-a-theme.html。

公式和图表

接下来就是一个非常强大实用的功能,公式和图表,支持 Latex、流程图,如图所示:

比如上面的 Latex 的源代码就是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Inline $\sqrt{3x-1}+(1+x)^2$

Block
$$
\begin{array}{c}

\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &
= \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\

\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\

\nabla \cdot \vec{\mathbf{B}} & = 0

\end{array}
$$

其语法也是和 Latex 一样的。

其背后是怎么实现的呢?其实是因为 Slidev 默认集成了 Katex 这个库,见:https://katex.org/,有了 Katex 的加持,所有公式的显示都不是事。

页面分隔

有的朋友就好奇了,既然是用 Markdown 写 PPT,那么每一页之间是怎么分割的呢?

其实很简单,最常规的,用三条横线分割就好了,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
---
layout: cover
---

# 第 1 页

This is the cover page.

---

# 第 2 页

The second page

当然,除了使用三横线,我们还可以使用更丰富的定义模式,可以给每一页制定一些具体信息,就是使用两层三横线。

比如这样:

1
2
3
4
5
---
theme: seriph
layout: cover
background: 'https://source.unsplash.com/1600x900/?nature,water'
---

上面这样的配置可以替代三横线,是另一种可以用作页面分隔的写法,借助这种写法我们可以定义更多页面的具体信息。

备注

当然我们肯定也想给 PPT 添加备注,这个也非常简单,通过注释的形式写到 Markdown 源文件就好了:

1
2
3
4
5
6
7
8
9
---
layout: cover
---

# 第 1 页

This is the cover page.

<!-- 这是一条备注 -->

这里可以看到其实就是用了注释的特定语法。

演讲者头像

当然还有很多酷炫的功能,比如说,我们在讲 PPT 的时候,可能想同时自己也出镜,Slidev 也可以支持。

因为开的是网页,而网页又有捕捉摄像头的功能,所以最终效果可以是这样子:

是的没错!右下角就是演讲者的个人头像,它被嵌入到了 PPT 中!是不是非常酷!

演讲录制

当然,Slidev 还支持演讲录制功能,因为它背后集成了 WebRTC 和 RecordRTC 的 API,一些录制配置如下所示:

所以,演讲过程的录制完全不是问题。

具体的操作可以查看:https://sli.dev/guide/recording.html

部署

当然用 Slidev 写的 PPT 还可以支持部署,因为这毕竟就是一个网页。

而且部署非常简单和轻量级,因为这就是一些纯静态的 HTML、JavaScript 文件,我们可以轻松把它部署到 GitHub Pages、Netlify 等站点上。

试想这么一个场景:别人在演讲之前还在各种拷贝 PPT,而你打开了一个浏览器直接输入了一个网址,PPT 就出来了,众人惊叹,就问你装不装逼?

具体的部署操作可以查看:https://sli.dev/guide/hosting.html

让我们看几个别人已经部署好的 PPT,直接网页打开就行了:

  • https://demo.sli.dev/composable-vue
  • https://masukin.link/talks/simply-publish-your-package-to-npm

就是这么简单方便。

版本控制

什么?你想实现版本控制,那再简单不过了。

Markdown 嘛,配合下专业版本管理工具 Git,版本控制再也不是难题。

总结

以上就是对 Slidev 的简单介绍,确实不得不说有些功能真的非常实用,而且我本身特别喜欢 Markdown 和网页开发,所以这个简直对我来说太方便了。

如果你感兴趣的话,不妨也来试试吧~

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

Python

有时候我们会有这样的一个需求:

我们定义了一个 Python 的方法,方法接收一些参数,但是调用的时候想将这些参数用命令行暴露出来。

比如说这里有个爬取方法:

1
2
3
4
5
import requests

def scrape(url, timeout=10):
response = requests.get(url, timeout=timeout)
print(response.text)

这里定义了一个 scrape 方法,第一个参数接收 url,即爬取的网址,第二个参数接收 timeout,即指定超时时间。

调用的时候我们可能这么调用:

1
scrape('https:///www.baidu.com', 10)

如果我们想改参数换 url,那就得改代码对吧。

所以有时候我们就想把这些参数用命令行暴露出来,这时候我们可能就用上了 argparse 等等的库,挨个声明各个参数是干嘛的,非常繁琐,代码如下:

1
2
3
4
5
6
7
8
9
parser = argparse.ArgumentParser(description='Scrape Function')
parser.add_argument('url', type=str,
help='an integer for the accumulator')
parser.add_argument('timeout', type=int,
help='sum the integers (default: find the max)')

if __name__ == '__main__':
args = parser.parse_args()
scrape(args.url, args.timeout)

这样我们才能顺利地使用命令行来调用这个脚本:

1
python3 main.py https://www.baidu.com 10

是不是感觉非常麻烦?argparse 写起来又臭又长,想想就费劲。

Fire

但接下来我们要介绍一个库,用它我们只需要两行代码就可以做到如上操作。

这个库的名字叫做Fire,它可以快速为某个 Python 方法或者类添加命令行的参数支持。

先看看安装方法,使用 pip3 安装即可:

1
pip3 install fire

这样我们就安装好了。

使用

下面我们来看几个例子。

方法支持

第一个代码示例如下:

1
2
3
4
5
6
7
import fire

def hello(name="World"):
return "Hello %s!" % name

if __name__ == '__main__':
fire.Fire(hello)

这里我们定义了一个 hello 方法,然后接收一个 name 参数,默认值是 World,接着输出了 Hello 加 name 这个字符串。

然后接着我们导入了 fire 这个库,调用它的 Fire 方法并传入 hello 这个方法声明,会发生什么事情呢?

我们把这段代码保存为 demo1.py,接着用 Python3 来运行一下:

1
python3 demo1.py

运行结果如下:

1
Hello World!

看起来并没有什么不同。

但我们这时候如果运行如下命令,就可以看到一些神奇的事情了:

1
python3 demo1.py --help

运行结果如下:

1
2
3
4
5
6
7
8
9
NAME
demo1.py

SYNOPSIS
demo1.py <flags>

FLAGS
--name=NAME
Default: 'World'

可以看到,这里它将 name 这个参数转化成了命令行的一个可选参数,我们可以通过 —-name 来替换 name 参数。

我们来试下:

1
python3 demo1.py --name 123

这里我们传入了一个 name 参数是 123,这时候我们就发现运行结果就变成了如下内容:

1
Hello 123!

是不是非常方便?我们没有借助 argparse 就轻松完成了命令行参数的支持和替换。

那如果我们将 name 这个参数的默认值取消呢?代码改写如下:

1
2
3
4
5
6
7
import fire

def hello(name):
return "Hello %s!" % name

if __name__ == '__main__':
fire.Fire(hello)

这时候重新运行:

1
python3 demo1.py --help

就可以看到结果变成了如下内容:

1
2
3
4
5
6
7
8
9
10
11
NAME
demo1.py

SYNOPSIS
demo1.py NAME

POSITIONAL ARGUMENTS
NAME

NOTES
You can also use flags syntax for POSITIONAL ARGUMENTS

这时候我们发现 name 这个参数就变成了必传参数,我们必须在命令行里指定这个参数内容,调用就会变成如下命令:

1
python3 demo1.py 123

运行结果还是一样的。

类支持

当然 fire 这个库不仅仅支持给方法添加命令行的支持,还支持给一个类添加命令行的支持。

下面我们再看一个例子:

1
2
3
4
5
6
7
8
import fire

class Calculator(object):
def double(self, number):
return 2 * number

if __name__ == '__main__':
fire.Fire(Calculator)

我们把这个代码保存为 demo2.py,然后运行:

1
python3 demo2.py

运行结果如下:

1
2
3
4
5
6
7
8
9
10
NAME
demo2.py

SYNOPSIS
demo2.py COMMAND

COMMANDS
COMMAND is one of the following:

double

可以看到,这里它将 Calculator 这个类中的方法识别出来了,COMMAND 之一就是 double,我们试着调用下:

1
python3 demo2.py double

运行结果如下:

1
2
3
4
5
ERROR: The function received no value for the required argument: number
Usage: demo2.py double NUMBER

For detailed information on this command, run:
demo2.py double --help

这里就说了,这里必须要指定另外一个参数,叫做 NUMBER,同时这个参数还是必填参数,我们试着加下:

1
python3 demo2.py double 4

运行结果如下:

1
8

这时候就可以达到正确结果了。

所以说,综合来看,fire 可以为一个类命令行,每个命令都对应一个方法的名称,同时在后面添加额外的可选或必选参数,加到命令行参数的后面。

重新改写

最后,让我们回过头来,给我们一开始定义的 scrape 方法添加命令行的参数支持:

1
2
3
4
5
6
7
8
9
10
import requests
import fire

def scrape(url, timeout=10):
response = requests.get(url, timeout=timeout)
print(response.text)


if __name__ == '__main__':
fire.Fire(scrape)

这样就可以了!省去了冗长的 argparse 的代码,是不是非常方便?

调用就是如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
NAME
main.py

SYNOPSIS
main.py URL <flags>

POSITIONAL ARGUMENTS
URL

FLAGS
--timeout=TIMEOUT
Default: 10

这里说了,URL 是必传参数,timeout 是可选参数。

最后调用下:

1
python3 main.py https://www.baidu.com

这样我们就可以轻松将 url 通过命令行传递过去了。

当然 timeout 还是可选值,我们可以通过 —-timeout 来指定 timeout 参数:

1
python3 main.py https://www.baidu.com --timeout 5

这样两个参数就都能顺利赋值了,最后效果就是爬取百度,5 秒超时。

怎么样?是不是很方便?大家快用起来吧!

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

个人随笔

之前跟一个朋友聊天,思考到一点,分享一下。

我这个朋友在创业,大家知道,创业都是非常艰难的,几乎是九死一生。

他创业一年多了,业务现在也挺好的,一直很有拼劲。

我就问他是什么让他一直保持这样持续向前向上的状态的。

我们聊了很多,但我发现其中有一个很重要的点就是那种笃定的态度,核心就在于两个字,笃定。

这个笃定分为两个方面,一个是做事上的笃定,一个是信念上的笃定。

分别说说。

  • 关于做事上的笃定。就是能认定了一件事之后,不会轻言放弃或更改,不能今天干了这一点,第二天就换去干另外一个了,也不能看到一点点困难就立即退缩不干了。他说他历经了一些困难的时刻,但经过反复复盘和思考,不断做到了螺旋式上升,现在经验越来越多,做得也越来越顺。所以在做事的时候,内心要有一种笃定的态度,认准了目标不断进发,虽然说努力了不一定成功,但成功几率一定比不努力或直接放弃大的多多多。
  • 关于信念上的笃定。就是能够在心中有一种信念,认定做某件事是有意义的有价值的,不会轻易被动摇。现在很多人做事都是,听到别人说做什么做什么挺赚钱,那就跟风去做了。或者做一个产品,他觉得会有市场需求有用户去用,完全迎合用户,但到头来发现是众口难调。真正信念上的笃定是一个人觉得做这件事是真正有意义的,有价值的,那就够了,不会被轻易动摇,不会一味迎合他人。只要他认定的正确的事,那就去做,这就是信念上的笃定。

当然可能有人会反驳了,这么笃定,都不听取别人的意见吗?一味向前冲不是死脑筋吗?这里要解释一下。这里说的笃定是一种广义上的笃定,这种态度信念是要有的,是一种大目标,总体的态度是要这样,但不代表不知变通或者死脑筋。在做具体的事情的时候,肯定还是反思、复盘,去思考什么才是达成大目标的方案,达成大目标的一步步小目标是怎样的,不断提升和完善的过程中慢慢地去接近笃定的大目标,这才是正确的。

与君共勉。

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

Python

上一篇我写过一篇《万物皆可 API》,这个项目就是把一些脚本的执行结果输出到了网页里面。

但是这个还是有很多改进空间,比如说 UI 能好看些,甚至能执行交互命令该多好,最后思来想去,它的究极形态不就是一个 Web 版的 Terminal (终端)吗?

然后本来我还想着对项目进行改造来着,但是想想,最终如果要改造成一个 Web 版的 Terminal,这个肯定已经有开源实现了。

于是我就开始搜,最后搜到几个还不错的。

Web Terminal

  • ttyd:https://github.com/tsl0922/ttyd,一款可以将命令行转到 Web 执行的工具,基于 C 编写的。
  • gotty:https://github.com/yudai/gotty,和 ttyd 一样,只不过是 Go 语言写的,但最新更新是在 2017 年了,估计失修了。
  • wetty:https://github.com/butlerx/wetty,基于 Node.js 开发的,也可以将命令行转到 Web 执行,但是需要基于 SSH 登录,其实就是个 Web 版的 SSH 终端。
  • Secure Shell (Chrome App):Google 浏览器插件,也可以提供网页版 SSH 终端。
  • tmate:https://tmate.io/,从 tmux 修改而来,可以支持 Terminal 分享。

经过一番试用,我个人首推的还是 ttyd,其他的几个要么是基于 SSH 的,要么不怎么好用或停止维护了。

下面我就来介绍下 ttyd 的简单用法。

安装

安装其实非常简单,我用的是 Mac,所以用 HomeBrew 直接安装即可:

1
brew install ttyd

如果你用的是 Windows、Linux,依然也可以支持,安装可以参考 https://github.com/tsl0922/ttyd#installation 章节。

使用

ttyd 支持不少功能配置,完整命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ttyd is a tool for sharing terminal over the web

USAGE:
ttyd [options] <command> [<arguments...>]

VERSION:
1.6.3

OPTIONS:
-p, --port Port to listen (default: 7681, use `0` for random port)
-i, --interface Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock)
-c, --credential Credential for Basic Authentication (format: username:password)
-u, --uid User id to run with
-g, --gid Group id to run with
-s, --signal Signal to send to the command when exit it (default: 1, SIGHUP)
-a, --url-arg Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)
-R, --readonly Do not allow clients to write to the TTY
-t, --client-option Send option to client (format: key=value), repeat to add more options
-T, --terminal-type Terminal type to report, default: xterm-256color
-O, --check-origin Do not allow websocket connection from different origin
-m, --max-clients Maximum clients to support (default: 0, no limit)
-o, --once Accept only one client and exit on disconnection
-B, --browser Open terminal with the default system browser
-I, --index Custom index.html path
-b, --base-path Expected base path for requests coming from a reverse proxy (eg: /mounted/here)
-P, --ping-interval Websocket ping interval(sec) (default: 300)
-6, --ipv6 Enable IPv6 support
-S, --ssl Enable SSL
-C, --ssl-cert SSL certificate file path
-K, --ssl-key SSL key file path
-A, --ssl-ca SSL CA file path for client certificate verification
-d, --debug Set log level (default: 7)
-v, --version Print the version and exit
-h, --help Print this text and exit

Visit https://github.com/tsl0922/ttyd to get more information and report bugs.

可以看到,这里可以使用 -p 来指定运行端口,使用 -c 指定登录密码等等。

基本使用

我们来试下,最基本的命令如下:

1
ttyd bash

这样就使用启动了一个 Web 版的 bash,运行结果如下:

这里显示是在 7681 上运行的,那我们就可以打开 http://localhost:7681/,就可以直接运行命令了:

非常丝滑。

看了下背后的传输协议是 WebSocket,所以稳定性还是有保障的:

当然,我们也可以不用 bash,用自己喜欢的 Shell,比如 zsh,命令如下:

1
ttyd zsh

这样的话浏览器里面的 Shell 就是 zsh 啦:

绑定端口

当然我们也可以更换端口,比如 8000,则可以使用如下命令:

1
ttyd -p 8000 zsh

这样 ttyd 就可以在 8000 端口运行 HTTP 服务,我们打开 http://localhost:8000/ 就可以执行命令了。

Basic Auth

当然这么直接暴露出去似乎也不太安全,我们可以设置 Basic Auth,使用 -c 这个选项即可指定用户名密码,格式为 username:password,例如我们指定用户名和密码都是 admin,那命令就这么写:

1
ttyd -p 8000 -c admin:admin zsh

这样打开 http://localhost:8000/ 之后就需要输入用户名密码才可以登录了:

自动打开浏览器

我们还可以使用 -B 命令让它自动打开浏览器:

1
ttyd -p 8000 -B zsh

这样运行之后,默认的浏览器就会自动打开 http://localhost:8000/,不用我们再去敲网址了,十分方便。

所以,上面这个命令甚至我们还可以做成一个 alias,比如:

1
alias webcmd="ttyd -p 8000 -B zsh";

这样输入 webcmd 就可以轻松打开一个 Web 版命令行了。

Docker 支持

另外 ttyd 还提供了 Docker 镜像,如果你不想安装的话,可以直接启 Docker,比如这样的话就可以在 7681 上启动:

1
docker run -it --rm -p 7681:7681 tsl0922/ttyd

但这实际上是把容器内部的命令行暴露出来了,如果要暴露宿主机的命令行还需要 mount 下磁盘:

SSH 终端

ttyd 还支持 SSH 终端,命令如下:

1
ttyd login

这样的话,打开浏览器之后就需要 SSH 登录,输入正确的 SSH 用户名和密码后才能使用。

SSL 支持

如果你想配置 SSL 支持,即支持 HTTPS 的话,可以自己生成证书并添加对应的参数来启动 ttyd,参考链接是:https://github.com/tsl0922/ttyd/wiki/SSL-Usage

更多

上面的用法基本能满足日常需要了,如果想要了解更多用法,可以参考其 Wiki,链接是:https://github.com/tsl0922/ttyd/wiki/Example-Usage

公网暴露

当然,我们如果想把它公网暴露出来,还可以配合 Ngrok,比如 ttyd 运行在 8000 端口上,我可以使用 Ngrok 将其暴露出来:

1
ngrok http 8000

运行结果如下:

这样我就可以通过指定的 URL 访问这个终端了,比如这里我就可以使用 https://11b4-2404-f801-8050-3-bf-00-55.ngrok.io/ 来访问我的终端了:

非常 Nice!

总结

好了,以上就是 ttyd 的基本使用了,有了它,我们就可以轻松将某台机器上的终端转到 Web 上来执行了,还是非常方便有用的。

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

Python

今天看到一个开源项目,叫做 Command2API,感觉挺有意思的,分享给大家。

起源

关于这个项目为什么诞生,原 Repo 有这么一段:

以近期 Log4j 的 RCE 举例,在内网的安全测试中,由于网络环境限制导致没有 DNSLog 平台可用,这时候做 Log4j 的漏洞验证就考虑直接查看 LDAP 服务是否有连接进来,但是现成的 JNDI 注入工具开启服务并没有 API 可以直接拉取对应服务的结果,这就导致需要人工去查看,很费时间,再加上已经写好 BurpSuite 被动插件进行扫描了,为了节省时间就简单写了这个脚本用于获取 JNDI 工具的执行结果并通过 API 的形式返回,便于插件拉取结果进行漏洞验证。

反正大意就是说,有些命令的执行结果如果能够通过 HTTP的 API 暴露出来,我们就能更方便地获取到命令的执行结果,在某些场景下会非常方便。

所以,这里作者写了这个项目。

原理

这个原理其实非常简单,就是用一个 Python 线程开启 Web 服务,一个线程执行命令,通过全局变量与 Web 服务共享执行命令的结果。

运行

这里我们来运行下看看效果吧。

首先需要下载下项目:

1
git clone https://github.com/gh0stkey/Command2API.git

然后接着指定想运行的命令和 API 运行的端口就好了,样例如下:

1
python Command2Api.py "执行的命令" Web运行的端口

注意,这里的 python 使用的 Python2,而不是 Python3,因为原项目引用了一个包叫 BaseHTTPServer,Python3 是没有的。

这里我们执行一个 ping 命令来试试:

1
python Command2Api.py "ping www.baidu.com" 8888

运行结果如下:

可以看到,这里首先输出了一个运行的地址:

1
URL: http://HOST:8888/c1IvlLF9

这时候我们打开 http://localhost:8888/c1IvlLF9 看下。

可以看到控制台结果就呈现在网页里面了。

但是这个页面没法自动刷新,需要点击刷新来获取最新的结果。

介绍完了。

所以,这个项目在某些情况下还是挺有用的。

比如说:

  • 内网安全测试中,可以用于获取 JNDI 工具的执行结果并通过API的形式返回,可以更方便地观测执行结果。
  • 我们想监控或实时获取某个命令行程序的输出结果,比如 Scrapy 爬虫、比如 Web Server 等等,可以将其暴露出来。
  • 我们想快速分享某个程序的执行结果,则可以通过这个命令配合 Ngrok 生成一个网站分享出去。

等等。

源码解析

我们再来看看源码吧,其实非常简单,一共就这些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import subprocess
import BaseHTTPServer
import SimpleHTTPServer
import cgi
import threading
import sys
import string
import random

l = []

uri = '/' + ''.join(random.sample(string.ascii_letters+string.digits,8))

class thread(threading.Thread):
def __init__(self, threadname, command):
threading.Thread.__init__(self, name='Thread_' + threadname)
self.threadname = int(threadname)
self.command = command

def run(self):
global l
ret = subprocess.Popen(
self.command,
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
for i in iter(ret.stdout.readline, b""):
res = i.decode().strip()
print(res)
l.append(res)

class ServerHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def do_GET(self):
global l
if self.path == uri:
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(l)

if __name__ == '__main__':
# New Thread: Get Command Result
t1 = thread('1', sys.argv[1])
t1.start()
# Webserver
port = int(sys.argv[2])
print("URL: http://HOST:{0}{1}".format(port, uri))
Handler = ServerHandler
httpd = BaseHTTPServer.HTTPServer(('0.0.0.0', port), Handler)
httpd.serve_forever()

可以看到这个命令就是 Popen 执行的,然后通过 PIPE 将结果捕获出来赋值为变量,然后同时另外一个线程启动服务器,将这个结果写入到 Response 里面。

就是这么简单的代码,实现了如此便捷的功能。

优化

不过我看这个项目还是有很多优化空间的,简单总结下:

  • 现在支持的是 Python2 而不是 Python3。
  • 网页结果不能自动刷新。
  • 网页结果是一个列表,和控制台的结果格式不太统一。
  • 不能通过 pip 来安全这个工具包。
  • 输出结果的 HOST 可以优化一下,直接复制出来不好访问。
  • 可以配合 Ngrok 将结果进行公开暴露。
  • 如果能通过网页来对命令进行交互控制就更好了。

我看看如果有时间的话,我可以试着将这个项目改写下并实现如上的一些优化功能哈,到时候写完了发出来。

谢谢阅读~

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

深度学习

本节我们就来了解下使用深度学习识别滑动验证码的方法。

1. 准备工作

我们这次主要侧重于完成利用深度学习模型来识别验证码缺口的过程,所以不会侧重于讲解深度学习模型的算法,另外由于整个模型实现较为复杂,本节也不会从零开始编写代码,而是倾向于把代码提前下载下来进行实操练习。

所以在最后,请提前代码下载下来,仓库地址为:https://github.com/Python3WebSpider/DeepLearningSlideCaptcha2,利用 Git 把它克隆下来:

1
git clone https://github.com/Python3WebSpider/DeepLearningSlideCaptcha2.git

运行完毕之后,本地就会出现一个 DeepLearningImageCaptcha2 的文件夹,就证明克隆成功了。

克隆完毕之后,请切换到 DeepLearningImageCaptcha2 文件夹,安装必要的依赖库:

1
pip3 install -r requirements.txt

运行完毕之后,本项目运行所需要的依赖库就全部安装好了。

以上准备工作都完成之后,那就让我们就开始本节正式的学习吧。

2. 目标检测

识别滑动验证码缺口的这个问题,其实可以归结为目标检测问题。那什么叫目标检测呢?在这里简单作下介绍。

目标检测,顾名思义,就是把我们想找的东西找出来。比如给一张「狗」的图片,如图所示:

image-20191107024841075

我们想知道这只狗在哪,它的舌头在哪,找到了就把它们框选出来,这就是目标检测。

经过目标检测算法处理之后,我们期望得到的图片是这样的:

image-20191107025008947

可以看到这只狗和它的舌头就被框选出来了,这就完成了一个不错的目标检测。

现在比较流行的目标检测算法有 R-CNN、Fast R-CNN、Faster R-CNN、SSD、YOLO 等,感兴趣可以了解一下,当然不太了解对本节要完成的目标也没有什么影响。

当前做目标检测的算法主要有两种方法,有一阶段式和两阶段式,英文叫做 One stage 和 Two stage,简述如下:

  • Two Stage:算法首先生成一系列目标所在位置的候选框,然后再对这些框选出来的结果进行样本分类,即先找出来在哪,然后再分出来是啥,俗话说叫「看两眼」,这种算法有 R-CNN、Fast R-CNN、Faster R-CNN 等,这些算法架构相对复杂,但准确率上有优势。
  • One Stage:不需要产生候选框,直接将目标定位和分类的问题转化为回归问题,俗话说叫「看一眼」,这种算法有 YOLO、SSD,这些算法虽然准确率上不及 Two stage,但架构相对简单,检测速度更快。

所以这次我们选用 One Stage 的有代表性的目标检测算法 YOLO 来实现滑动验证码缺口的识别。

YOLO,英文全称叫做 You Only Look Once,取了它们的首字母就构成了算法名,

目前 YOLO 算法最新的版本是 V5 版本,应用比较广泛的是 V3 版本,这里算法的具体流程我们就不过多介绍了,感兴趣的可以搜一下相关资料了解下,另外也可以了解下 YOLO V1-V3 版本的不同和改进之处,这里列几个参考链接:

  • YOLO V3 论文:https://pjreddie.com/media/files/papers/YOLOv3.pdf
  • YOLO V3 介绍:https://zhuanlan.zhihu.com/p/34997279
  • YOLO V1-V3 对比介绍:https://www.cnblogs.com/makefile/p/yolov3.html

3. 数据准备

像上一节介绍的一样,要训练深度学习模型也需要准备训练数据,数据也是分为两部分,一部分是验证码图像,另一部分是数据标注,即缺口的位置。但和上一节不一样的是,这次标注不再是单纯的验证码文本了,因为这次我们需要表示的是缺口的位置,缺口对应的是一个矩形框,要表示一个矩形框,至少需要四个数据,如左上角点的横纵坐标 x、y,矩形的宽高 w、h,所以标注数据就变成了四个数字。

所以,接下来我们就需要准备一些验证码图片和对应的四位数字的标注了,比如下图的滑动验证码:

好,那接下来我们就完成这两步吧,第一步就是收集验证码图片,第二步就是标注缺口的位置并转为我们想要的四位数字。

在这里我们的示例网站是 https://captcha1.scrape.center/,打开之后点击登录按钮便会弹出一个滑动验证码,如图所示:

image-20210504182925384

我们需要做的就是单独将滑动验证码的图像保存下来,也就是这个区域:

image-20210504183039997

怎么做呢?靠手工截图肯定不太可靠,费时费力,而且不好准确定位边界,会导致存下来的图片有大有小。为了解决这个问题,我们可以简单写一个脚本来实现下自动化裁切和保存,就是仓库中的 collect.py 文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException
import time
from loguru import logger

COUNT = 1000

for i in range(1, COUNT + 1):
try:
browser = webdriver.Chrome()
wait = WebDriverWait(browser, 10)
browser.get('https://captcha1.scrape.center/')
button = wait.until(EC.element_to_be_clickable(
(By.CSS_SELECTOR, '.el-button')))
button.click()
captcha = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, '.geetest_slicebg.geetest_absolute')))
time.sleep(5)
captcha.screenshot(f'data/captcha/images/captcha_{i}.png')
except WebDriverException as e:
logger.error(f'webdriver error occurred {e.msg}')
finally:
browser.close()

在这里我们先定义了一个循环,循环次数为 COUNT 次,每次循环都使用 Selenium 启动一个浏览器,然后打开目标网站,模拟点击登录按钮触发验证码弹出,然后截取验证码对应的节点,再用 screenshot 方法将其保存下来。

我们将其运行:

1
python3 collect.py

运行完了之后我们就可以在 data/captcha/images/ 目录获得很多验证码图片了,样例如图所示:

image-20210504194022826

获得验证码图片之后,我们就需要进行数据标注了,这里推荐的工具是 labelImg,GitHub 地址为 https://github.com/tzutalin/labelImg,使用 pip3 安装即可:

1
pip3 install labelImg

安装完成之后可以直接命令行运行:

1
labelImg

这样就成功启动了 labelImg:

image-20210504194644729

点击 Open Dir 打开 data/captcha/images/ 目录,然后点击左下角的 Create RectBox 创建一个标注框,我们可以将缺口所在的矩形框框选出来,框选完毕之后 labelImg 就会提示保存一个名称,我们将其命名为 target,然后点击 OK,如图所示:

image-20210504194608969

这时候我们可以发现其保存了一个 xml 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<annotation>
<folder>images</folder>
<filename>captcha_0.png</filename>
<path>data/captcha/images/captcha_0.png</path>
<source>
<database>Unknown</database>
</source>
<size>
<width>520</width>
<height>320</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
<object>
<name>target</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>321</xmin>
<ymin>87</ymin>
<xmax>407</xmax>
<ymax>167</ymax>
</bndbox>
</object>
</annotation>

其中可以看到 size 节点里有三个节点,分别是 width、height、depth,分别代表原验证码图片的宽度、高度、通道数。另外 object 节点下的 bndbox 节点就包含了标注缺口的位置,通过观察对比可以知道 xmin、ymin 指的就是左上角的坐标,xmax、ymax 指的就是右下角的坐标。

我们可以用下面的方法简单进行下数据处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import xmltodict
import json

def parse_xml(file):
xml_str = open(file, encoding='utf-8').read()
data = xmltodict.parse(xml_str)
data = json.loads(json.dumps(data))
annoatation = data.get('annotation')
width = int(annoatation.get('size').get('width'))
height = int(annoatation.get('size').get('height'))
bndbox = annoatation.get('object').get('bndbox')
box_xmin = int(bndbox.get('xmin'))
box_xmax = int(bndbox.get('xmax'))
box_ymin = int(bndbox.get('ymin'))
box_ymax = int(bndbox.get('ymax'))
box_width = (box_xmax - box_xmin) / width
box_height = (box_ymax - box_ymin) / height
return box_xmin / width, box_ymin / height, box_width / width, box_height / height

这里我们定义了一个 parse_xml 方法,这个方法首先读取了 xml 文件,然后使用 xmltodict 库就可以将 XML 字符串转为 JSON,然后依次读取出验证码的宽高信息,缺口的位置信息,最后返回了想要的数据格式—— 缺口左上角的坐标和宽高相对值,以元组的形式返回。

都标注完成之后,对每个 xml 文件调用此方法便可以生成想要的标注结果了。

在这里,我已经将对应的标注结果都处理好了,可以直接使用,路径为 data/captcha/labels,如图所示:

image-20210504200730482

每个 txt 文件对应一张验证码图的标注结果,内容类似如下:

1
0 0.6153846153846154 0.275 0.16596774 0.24170968

第一位 0 代表标注目标的索引,由于我们只需要检测一个缺口,所以索引就是 0;第 2、3 位代表缺口的左上角的位置,比如 0.615 则代表缺口左上角的横坐标在相对验证码的 61.5% 处,乘以验证码的宽度 520,结果大约就是 320,即左上角偏移值是 320 像素;第 4、5 代表缺口的宽高相对验证码图片的占比,比如第 5 位 0.24 乘以验证码的高度 320,结果大约是 77,即缺口的高度大约为 77 像素。

好了,到此为止数据准备阶段就完成了。

4. 训练

为了更好的训练效果,我们还需要下载一些预训练模型。预训练的意思就是已经有一个提前训练过的基础模型了,我们可以直接使用提前训练好的模型里面的权重文件,我们就不用从零开始训练了,只需要基于之前的模型进行微调就好了,这样既可以节省训练时间,又可以有比较好的效果。

YOLOV3 的训练要加载预训练模型才能有不错的训练效果,预训练模型下载命令如下:

1
bash prepare.sh

注意:在 Windows 下请使用 Bash 命令行工具如 Git Bash 来运行此命令。

执行这个脚本,就能下载 YOLO V3 模型的一些权重文件,包括 yolov3 和 weights 还有 darknet 的 weights,在训练之前我们需要用这些权重文件初始化 YOLO V3 模型。

接下来就可以开始训练了,执行如下脚本:

1
bash train.sh

注意:在 Windows 下请同样使用 Bash 命令行工具如 Git Bash 来运行此命令。

同样推荐使用 GPU 进行训练,训练过程中我们可以使用 TensorBoard 来看看 loss 和 mAP 的变化,运行 TensorBoard:

1
tensorboard --logdir='logs' --port=6006 --host 0.0.0.0

注意:请确保已经正确安装了本项目的所有依赖库,其中就包括 TensorBoard,安装成功之后便可以使用 tensorboard 命令。

运行此命令后可以在 http://localhost:6006 观察到训练过程中的 loss 变化。

loss_1 变化类似如下:

loss 变化

val_mAP 变化类似如下:

mAP 变化

可以看到 loss 从最初的非常高下降到了很低,准确率也逐渐接近 100%。

这是训练过程中的命令行的一些输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---- [Epoch 99/100, Batch 27/29] ----
+------------+--------------+--------------+--------------+
| Metrics | YOLO Layer 0 | YOLO Layer 1 | YOLO Layer 2 |
+------------+--------------+--------------+--------------+
| grid_size | 14 | 28 | 56 |
| loss | 0.028268 | 0.046053 | 0.043745 |
| x | 0.002108 | 0.005267 | 0.008111 |
| y | 0.004561 | 0.002016 | 0.009047 |
| w | 0.001284 | 0.004618 | 0.000207 |
| h | 0.000594 | 0.000528 | 0.000946 |
| conf | 0.019700 | 0.033624 | 0.025432 |
| cls | 0.000022 | 0.000001 | 0.000002 |
| cls_acc | 100.00% | 100.00% | 100.00% |
| recall50 | 1.000000 | 1.000000 | 1.000000 |
| recall75 | 1.000000 | 1.000000 | 1.000000 |
| precision | 1.000000 | 0.800000 | 0.666667 |
| conf_obj | 0.994271 | 0.999249 | 0.997762 |
| conf_noobj | 0.000126 | 0.000158 | 0.000140 |
+------------+--------------+--------------+--------------+
Total loss 0.11806630343198776

这里显示了训练过程中各个指标的变化情况,如 loss、recall、precision、confidence 等,分别代表训练过程的损失(越小越好)、召回率(能识别出的结果占应该识别出结果的比例,越高越好)、精确率(识别出的结果中正确的比率,越高越好)、置信度(模型有把握识别对的概率,越高越好),可以作为参考。

5. 测试

训练完毕之后会在 checkpoints 文件夹生成 pth 文件,这就是一些模型文件,和上一节的 best_model.pkl 是一样的原理,只不过表示形式略有不同,我们可直接使用这些模型来预测生成标注结果。

要运行测试,我们可以先在测试文件夹 data/captcha/test 放入一些验证码图片:

样例验证码如下:

captcha_435

要运行测试,执行如下脚本:

1
bash detect.sh

该脚本会读取测试文件夹所有图片,并将处理后的结果输出到 data/captcha/result 文件夹,控制台输出了一些验证码的识别结果。

同时在 data/captcha/result 生成了标注的结果,样例如下:

可以看到,缺口就被准确识别出来了。

实际上,detect.sh 是执行了 detect.py 文件,在代码中有一个关键的输出结果如下:

1
2
bbox = patches.Rectangle((x1 + box_w / 2, y1 + box_h / 2), box_w, box_h, linewidth=2, edgecolor=color, facecolor="none")
print('bbox', (x1, y1, box_w, box_h), 'offset', x1)

这里 bbox 指的就是最终缺口的轮廓位置,同时 x1 就是指的轮廓最左侧距离整个验证码最左侧的横向偏移量,即 offset。通过这两个信息,我们就能得到缺口的关键位置了。

有了目标滑块位置之后,我们便可以进行一些模拟滑动操作从而实现通过验证码的检测了。

6. 总结

本节主要介绍了训练深度学习模型来识别滑动验证码缺口的整体流程,最终我们成功实现了模型训练过程,并得到了一个深度学习模型文件。

利用这个模型,我们可以输入一张滑动验证码,模型便会预测出其中的缺口的位置,包括偏移量、宽度等,最后可以通过缺口的信息绘制出对应的位置。

和上一节一样,本节介绍的内容也可以进一步优化:

  • 当前模型的预测过程是通过命令行执行的,但在实际使用的时候可能并不太方便,可以考虑将预测过程对接 API 服务器暴露出来,比如对接 Flask、Django、FastAPI 等把预测过程实现为一个支持 POST 请求的接口,接口可以接收一张验证码图片,返回验证码的文本信息,这样会使得模型更加方便易用。

本节代码:https://github.com/Python3WebSpider/DeepLearningSlideCaptcha2。

个人随笔

之前跟一位朋友聊天,他说了有一点让我触动很大,这里专门来写写。

就我个人来讲,我在我的这个年纪一直在尝试各种各样的事情,除了工作之外,我还尝试了多种副业,比如写公众号、写书、录课、做视频、写各种开源项目等等,其实整体说来,还是比较“杂”的。

他给了我一个建议,叫做“想想做事的终局”,我感觉挺有道理的。

下面是我的一些思考。

其实年轻多尝试没有什么坏事,比如在三十岁之前,我们可能并不清楚我们真正擅长的或喜欢的到底是什么,所以就得多尝试不同的方向,探索一些未知的领域。找到真正喜欢或者适合的事情之后,后面的几十年可以潜心放在这上面,深入去发展它。

当然很多时候,我们可能并不知道我们在做的这件事以后究竟会是怎样的,这里面有两个方面:

  • 验证这件事究竟合不合适、能不能做
  • 思考这件事以后到底能做到什么地步

这里针对这两点分别说下。

首先就是验证这件事究竟合不合适、能不能做。我们比较好的办法就是进行快速试错,比如一个项目,我们做一些 POC 来验证可行性,或者前期调研的时候通过调查问卷了解行情,这些都是十分重要的。另外我们也不能想当然,比如做一件事情之前,凭空一想就把自己否定了,那就会少掉很多机会。该行动还是要行动的,但前期也要尽量控制一些成本。

另外一个重要的就是想想这件事到底能做到什么地步,也就是终局在哪里。比如说,一件事,它本身的可扩展性、可复用性就很小,这个就得好好考虑下要不要投入大的成本一直做下去。如果一件事投入很多的精力,以后可做的事情非常多,或者能做的很大很大,甚至无限大,这种事情就值得多去投入些。比如说,我做一个开源项目,投入大量精力,最后的用户可能就是很有限的,而且场景也很有限,那这个以后就做不大。如果我写作、课程,如果我能把我学习和进步的都输出出来,不断积累,那以后的扩展的面就会有很多很多,这种相对来说就能做大。这就是一些事的终局。所以,平时我们也需要考虑下这件事的终局在哪里。

嗯,总之,找到适合做、喜欢做的事,同时这件事的终局也够大,那就是非常难得的,持续努力,结果会回报我们的。

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

安装配置

Pika 是 Python 中用于连接和操作 RabbitMQ 的库,本节我们了解下 Pika 的安装方式。

相关链接

  • 官方文档:https://pika.readthedocs.io
  • GitHub:https://github.com/pika/pika

先置条件

安装之前请先安装下 RabbitMQ,安装参考说明:https://setup.scrape.center/rabbitmq

安装方法

pip 安装

推荐使用 pip3 安装,命令如下:

1
pip3 install pika

命令执行完毕之后即可完成安装。

验证安装

安装完成之后,可以在 Python 命令行下测试。

1
2
$ python3
>>> import pika

如果没有错误报出,则证明库已经安装好了。

安装配置

RabbmitMQ 是一个非常强大易用的消息队列,支持 Windows、Mac、Linux 等各个平台。

每个平台都有多种安装方式,这里进行简单归纳总结,具体的安装方式请移步官方文档说明。

Windows 上的安装

Windows 主要有两种安装方式,一种是通过 Chocolatey 工具安装,另一种是通过官方安装包安装。

详细说明请移步:https://rabbitmq.com/install-windows.html 查看。

Chocolatey

Chocolatey 是一个 Windows 上管理软件的工具,RabbitMQ 发布了很多 Chocolatey 安装包,具体说明可以参考:https://rabbitmq.com/install-windows.html#chocolatey

官方安装包

就是下载一个二进制安装文件,具体的最新下载地址请移步 https://rabbitmq.com/install-windows.html#installer 查看。

Linux 上的安装

Linux 上的安装方式又分了多种了,根据发行平台的不同有如下安装说明。

Debian 和 Ubuntu

请移步:https://rabbitmq.com/install-debian.html 查看。

RedHat Enterprise Linux, CentOS, Fedora, openSUSE

请移步:https://rabbitmq.com/install-rpm.html 查看。

Mac

Mac 就是主要用 Homebrew 来安装了,具体安装说明请移步:https://rabbitmq.com/install-homebrew.html

更多

更多平台的安装方式请移步:https://rabbitmq.com/download.html 查看。

安装配置

Selenium 是一个自动化测试工具,利用它我们可以驱动浏览器执行特定的动作,如点击、下拉等等操作,对于一些 JavaScript 渲染的页面来说,此种抓取方式非常有效,下面我们来看下 Selenium 的安装过程。

相关链接

  • 官方网站:http://www.seleniumhq.org
  • GitHub:https://github.com/SeleniumHQ/selenium/tree/master/py
  • PyPi:https://pypi.python.org/pypi/selenium
  • 官方文档:http://selenium-python.readthedocs.io
  • 中文文档:http://selenium-python-zh.readthedocs.io

ChromeDriver 安装

通常来说,我们需要配合 Chrome 浏览器使用 Selenium,所以我们还需要额外安装下 ChromeDriver 和 Chrome 浏览器。

安装参考:https://setup.scrape.center/chromedriver

安装

pip 安装

推荐直接使用 pip3 安装,执行如下命令即可:

1
pip3 install selenium

wheel 安装

除了 pip 安装,也可以到 PyPi 下载对应的 wheel 文件进行安装,https://pypi.python.org/pypi/selenium/#downloads, 如假设最新版本为 3.4.3,则下载 selenium-3.4.3-py2.py3-none-any.whl。

然后进入 wheel 文件目录,使用 pip 安装。

1
pip3 install selenium-3.4.3-py2.py3-none-any.whl

验证安装

进入 Python 命令行交互模式,导入一下 Selenium 包,如果没有报错,则证明安装成功。

1
2
$ python3
>>> import selenium

当然也可以运行一个脚本:

1
2
3
4
5
6
7
from selenium import webdriver
from time import sleep

browser = webdriver.Chrome()
browser.get('https://www.baidu.com')
sleep(2)
browser.close()

如果运行完毕之后弹出来了一个 Chrome 浏览器并加载了百度页面,2 秒之后就关闭了,那就证明没问题了。

Python

大家好!我是崔庆才。

今天告诉大家一个好消息:《Python3网络爬虫开发实战(第二版)》上架了!!!!

没错,就是这本:

2018 年 5 月我的《Python3网络爬虫开发实战》的第一版出版了,从上市到现在三年多销量约 10w 册,真的非常感谢各位读者的支持。后来,由于一些技术更迭,我开始策划编写本书的第二版。

2021 年11月,这本书历经各种反复修改、审稿等阶段,到今天终于上架了!

这几个月我收到了太多读者的询问,第二版什么时候出来,真的抱歉实在是让大家久等了。

没错,就是今天,它来了!

第二版更新内容

大家第一个问题可能就会问,第二版比第一版更新了哪些内容?

因为技术总是在不断发展和进步的,爬虫技术也是一样,它在爬虫和反爬虫不断斗争的过程中也在不断演进。比如现在越来越多的网页采取了各种防护措施,比如前端代码的压缩和混淆、API 的参数加密、WebDriver 的检测,要做到高效的数据爬取,我们就需要懂得一些 JavaScript 逆向分析相关技术。App 也是一样,App 的抓包防护、加壳保护、Native 化、风控检测使得越来越多的 App 数据难以爬取,所以我们也不得不了解一些逆向相关技术,如 Xposed、Frida、IDA Pro 等工具的使用。除此之外,近几年深度学习和人工智能发展得也是如火如荼,所以爬虫也可以和人工智能结合起来,比如基于深度学习的验证码识别、网页内容的智能化解析和提取等技术我们也可以进行学习和了解。另外,一些大规模爬虫的管理和运维技术也在不断发展,当前 Kubernetes、Docker、Prometheus 等云原生技术也非常火爆,基于 Kubernetes 等云原生技术的爬虫管理和运维解决方案也已经很受青睐。然而,之前第一版书对以上提到的这些新兴技术几乎没有提及。

除此之外,第一版书在讲解数据爬取的过程中引用了很多案例和服务,比如猫眼电影网站、淘宝网站、代理服务网站,然而几年过去了,有些案例网站和服务早已经改版或者停止维护,这就导致第一版书中的很多案例已经不能正常运行了。这其实是一个很大的问题,因为程序运行不通会大大降低学习的积极性和成就感,而且会浪费不少时间。另外,即使案例对应的爬虫代码及时更新了,那我们也不知道这些案例网站和服务什么时候会再次改版,因为这都是不可控的。所以,为了彻底解决这个问题,我花费了近半年的时间构建了一个爬虫案例平台(https://scrape.center),平台包含了几十个爬虫案例,包括服务端渲染(SSR)网站、单页面应用(SPA)网站、各类反爬网站、验证码网站、模拟登录网站、各类 App 等,覆盖了现在爬虫和反爬虫相关的大多数技术,整个平台都是我来维护的,书中几乎所有案例都是从案例平台来的,从而解决了页面改版的问题。

所以,本书相比第一版来说,更新的内容主要如下:

  • 绝大多数都迁移到了自建的案例平台,以后再也不用担心案例有过期或改版问题。

  • 替换了原本第一章环境安装的章节,将环境配置的部分全部汇总并迁移到案例平台(https://setup.scrape.center)并在书中以外链的形式附上,以确保环境的配置和安装说明能够被及时更新。

  • 增加了一些新的请求库、解析库、存储库等的介绍,如 httpx、parsel、Elasticsearch 等库的介绍。

  • 增加了异步爬虫的介绍,如协程的基本原理、aiohttp 的使用和爬取实战介绍。

  • 增加了一些新兴自动化工具的介绍,如 Pyppeteer、Playwright 的介绍。

  • 增加了深度学习相关内容,如图形验证码、滑动验证码的识别方案。

  • 丰富了模拟登录章节的内容,如增加了 JWT 模拟登录的介绍和实战、大规模账号池的优化。

  • 增加了 JavaScript 逆向的章节,包括网站加密和混淆技术、JavaScript 逆向调试技巧、JavaScript 的各种模拟执行方式、AST 还原混淆代码、WebAssembly 等相关技术的介绍。

  • 丰富了 App 自动化爬取技术的章节,如新兴框架 Airtest 的介绍、手机群控和云手机技术的介绍。

  • 增加了 Android 逆向章节,如反编译、反汇编、Hook、脱壳、so 文件分析和模拟执行等技术的介绍。

  • 增加了网页智能化解析章节,包括列表页、详情页内容提取算法和分类算法。

  • 丰富了 Scrapy 相关章节的介绍,如 Pyppeteer 的对接、RabbitMQ 的对接、Prometheus 的对接等。

  • 增加了基于 Kubernetes、Docker、Prometheus、Grafana 等云原生技术爬虫管理和运维解决方案的介绍。

以上就是第二版的主要更新内容。

章节介绍

为了让大家更直接地了解到全书的内容,这里就直接放目录了:

没错!全书一共 900 多页,量了下有4.3 厘米厚,定价是 139.8 元。

可以直接看第二版吗?

当然,有朋友也会担心,我需不需要先学习第一版,然后才能学第二版呢?

答案是:可以直接学第二版,第二版书爬虫的内容知识体系是完整的,一些旧的技术已经在第一版中移除,第二版的书籍是对所有爬虫知识体系的全新升级。

没有基础可以学吗?

有朋友也可能会问,没有爬虫或者 Python 基础可以学吗?

答案是:可以,本书就是专为零爬虫基础的朋友准备的,本书从最基础的环境配置、基础知识的讲解开始,循序渐进地对爬虫的各个知识点进行介绍,所以完全不用担心没有爬虫基础学不会的问题。如果没有 Python 基础,那也没关系(当然有会更好),书中也会提及 Python 环境的配置并附上一些 Python 入门学习资料(链接),同时也会通过各个 Python 代码片段来进行讲解,很多案例也很简单易懂,学爬虫的时候 Python 也就会逐渐掌握了。

大咖推荐

这本书同时还获得了 Python 之父的推荐(没错就是 Python 的创始人,Guido van Rossum)。另外我还有幸获得了微软亚洲互联网工程院副院长曾文峰、知名爬虫专家梁斌penny、中国人民大学高瓴人工智能学院长聘副教授宋睿华的推荐。

下面是推荐语的内容:

宣传彩页

另外编辑还为本书制作了几张宣传彩页,是对整本书的一个宣传介绍,大家可以看下:

有没有电子版?

看到这里,大家可能也会问了,有没有电子版呢?可能有的朋友习惯用电子版书籍来学习,有的朋友可能在海外也不方便购买,所以想要电子版。

但还是很遗憾地说:没有电子版。

因为你知道的,如果出了电子版,那么马上就会有各种盗版袭来,网上也会造成各种恶意传播。

所以,为了保护版权,这本书是没有上电子版的。

购买链接

是的,最后就是大家最关心的部分了,到哪里能够买到呢?

上架之前,我与编辑经过各种沟通,原本是想给广大读者和粉丝们有个专属优惠的,但是这个比较难操作,所以最终决定,整本书现在全网统一 7 折销售了!

也就是说,原价 139.8 元,现在只需要 97.9 元就能买到了。

不过这个也是限时的,7 折优惠只到下周五,也就是 12 月 3 日,之后会恢复 84 折销售,也就是 117 元。

另外还有一个消息,前几天我不是签名了 1000 本书吗?所以,现在这个阶段,卖的全都是签名版,一共 1000 本,卖完即止,先到先得。

如果不想要签名版的朋友可以再等等,等签名版的卖完了就是非签名版的了。

购买链接:https://item.jd.com/13527222.html

为了方便购买,我把这个链接转成了二维码,大家可以直接扫码购买:

谢谢大家支持!

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

Python

是的,今天我终于完成了我的新书《Python3网络爬虫开发实战(第二版)》的签名,一共 1000 本,整个过程感觉着实不容易啊。

签完我就来写这篇文章了,感觉左右胳膊完全不是一个感觉。

1

先说下事情的来龙去脉吧,因为很多读者想要一本签名版的书籍,毕竟有需求就有市场。于是编辑和我商讨之后,让我对其中的一部分进行签名。

其实当时第一版出来的时候也签了一些,那会签的少,我记得只有三百多本。

遥想那会,还是 2018 年,那会第一版的书刚刚印刷出来,那会我是去河北的一个印厂签名的。印厂把新鲜出炉的扉页的那一沓拿给我,我一个个进行签名,我还有当年签名的照片:

我去,我当年这么瘦的吗?

这里简单说下扉页这一沓是怎么回事,有朋友就会好奇问为啥不直接拿给你一张纸,而是一沓纸呢?这是因为印刷的时候是用的一张大纸,比如正反面一张大纸就能覆盖书的二三十页内容,然后印刷完毕之后,这张大纸就会被折叠,最后就得到了这小小的一沓。

所以,每一本书,我只需要在这一小沓纸上签名,最后印厂再拿回去进行锁线、装订那就得到完整的书了。

2

好了,再说回签名的事。

今年和往年的签名流程不一样了。

印厂还是在河北,然而由于疫情原因,北京疫情管控非常严格,进京必须要 2 日之内的核算证明。另外去的话还不能光我一人吧,加上编辑还有同行同事肯定至少也得三个人,就感觉比较麻烦。

于是我们就决定这次不去印厂签名了(所以就没法拍照片了)。

那怎么办呢?

我们就决定让印厂把书的扉页都拿给我,然后我签完了再运回印厂,印厂再进行组装。

于是乎,在周二的时候,我收到了整整两大箱子书扉页。

好家伙,这数量着实不少啊,而且特别重。

另外我住的地方其实是没有电梯的 6 楼,光搬上去就废了好大的劲。

后来,从周二开始,周二、三、四、五,我每天下班之后都签一点,每天两百多本。

差不多下班后要花将近两小时签名吧,我也没仔细计算,可能中间有少许休息时间,所以这几天光签名得花了得有七八小时?

3

然后一直到今晚上,哦不,应该是凌晨,终于是签完了。

感觉手都签麻了。

这是一开始的时候签的:

这是最后的时候签的:

哪个好看?一定是第二张好看对不对?!更加飘逸对不对?(逃

最后晒一下完整成果吧,整整两箱子,都签好啦!

我联系了印厂,印厂周六会有师傅过来我家取。

等师傅取回印厂之后,印厂就会安排锁线、装订、裁切等各个环节,最后完整的书就有啦。

4

然后到这里,就会有朋友问了,现在书到底啥时候能买到啊?

其实非签名版的书已经印刷好啦,前天的时候编辑已经拿到了样书,今天还有给书的专门拍摄,这里给大家看看样子哈。

这是编辑发我的图,我其实还没拿到实体书。

然后我就问编辑,这书厚不厚啊?

编辑说,厚!

然后给我发了一张测量图:

好家伙,我直接好家伙,4.3 cm 厚,大家感受一下吧。

加上用的纸还算比较好的,好像说有两三斤重?

emm,具体到手之后再体验下吧。

印刷完了之后,接下来就是入库销售了。

然后具体的售卖时间,编辑说周末或下周一会知道入库时间是什么时候,但我预估下周应该大家就可以开始购买啦。

签名版的话可能会先预售,就是发货较晚的意思,因为我刚刚签完,组装还需要几天时间,到时候应该会在京东上直接销售签名版。

售价的话,应该会打 7 折,原价 139.8,所以打完折是 98 元。

嗯嗯!总之,大家马上就可以买到啦,实在让大家久等了,到时候我会在公号第一时间发布最新消息,非常感谢大家的支持!

撒花!

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

爬虫

Playwright 是微软在 2020 年初开源的新一代自动化测试工具,它的功能类似于 Selenium、Pyppeteer 等,都可以驱动浏览器进行各种自动化操作。它的功能也非常强大,对市面上的主流浏览器都提供了支持,API 功能简洁又强大。虽然诞生比较晚,但是现在发展得非常火热。

1. Playwright 的特点

  • Playwright 支持当前所有主流浏览器,包括 Chrome 和 Edge(基于 Chromium)、Firefox、Safari(基于 WebKit) ,提供完善的自动化控制的 API。
  • Playwright 支持移动端页面测试,使用设备模拟技术可以使我们在移动 Web 浏览器中测试响应式 Web 应用程序。
  • Playwright 支持所有浏览器的 Headless 模式和非 Headless 模式的测试。
  • Playwright 的安装和配置非常简单,安装过程中会自动安装对应的浏览器和驱动,不需要额外配置 WebDriver 等。
  • Playwright 提供了自动等待相关的 API,当页面加载的时候会自动等待对应的节点加载,大大简化了 API 编写复杂度。

本节我们就来了解下 Playwright 的使用方法。

2. 安装

要使用 Playwright,需要 Python 3.7 版本及以上,请确保 Python 的版本符合要求。

要安装 Playwright,可以直接使用 pip3,命令如下:

1
pip3 install playwright

安装完成之后需要进行一些初始化操作:

1
playwright install

这时候 Playwrigth 会安装 Chromium, Firefox and WebKit 浏览器并配置一些驱动,我们不必关心中间配置的过程,Playwright 会为我们配置好。

具体的安装说明可以参考:https://setup.scrape.center/playwright。

安装完成之后,我们便可以使用 Playwright 启动 Chromium 或 Firefox 或 WebKit 浏览器来进行自动化操作了。

3. 基本使用

Playwright 支持两种编写模式,一种是类似 Pyppetter 一样的异步模式,另一种是像 Selenium 一样的同步模式,我们可以根据实际需要选择使用不同的模式。

我们先来看一个基本同步模式的例子:

1
2
3
4
5
6
7
8
9
10
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
for browser_type in [p.chromium, p.firefox, p.webkit]:
browser = browser_type.launch(headless=False)
page = browser.new_page()
page.goto('https://www.baidu.com')
page.screenshot(path=f'screenshot-{browser_type.name}.png')
print(page.title())
browser.close()

首先我们导入了 sync_playwright 方法,然后直接调用了这个方法,该方法返回的是一个 PlaywrightContextManager 对象,可以理解是一个浏览器上下文管理器,我们将其赋值为变量 p。

接着我们调用了 PlaywrightContextManager 对象的 chromium、firefox、webkit 属性依次创建了一个 Chromium、Firefox 以及 Webkit 浏览器实例,接着用一个 for 循环依次执行了它们的 launch 方法,同时设置了 headless 参数为 False。

注意:如果不设置为 False,默认是无头模式启动浏览器,我们看不到任何窗口。

launch 方法返回的是一个 Browser 对象,我们将其赋值为 browser 变量。然后调用 browser 的 new_page 方法,相当于新建了一个选项卡,返回的是一个 Page 对象,将其赋值为 page,这整个过程其实和 Pyppeteer 非常类似。接着我们就可以调用 page 的一系列 API 来进行各种自动化操作了,比如调用 goto,就是加载某个页面,这里我们访问的是百度的首页。接着我们调用了 page 的 screenshot 方法,参数传一个文件名称,这样截图就会自动保存为该图片名称,这里名称中我们加入了 browser_type 的 name 属性,代表浏览器的类型,结果分别就是 chromium, firefox, webkit。另外我们还调用了 title 方法,该方法会返回页面的标题,即 HTML 中 title 节点中的文字,也就是选项卡上的文字,我们将该结果打印输出到控制台。最后操作完毕,调用 browser 的 close 方法关闭整个浏览器,运行结束。

运行一下,这时候我们可以看到有三个浏览器依次启动并加载了百度这个页面,分别是 Chromium、Firefox 和 Webkit 三个浏览器,页面加载完成之后,生成截图、控制台打印结果就退出了。

这时候当前目录便会生成三个截图文件,都是百度的首页,文件名中都带有了浏览器的名称,如图所示:

控制台运行结果如下:

1
2
3
百度一下,你就知道
百度一下,你就知道
百度一下,你就知道

通过运行结果我们可以发现,我们非常方便地启动了三种浏览器并完成了自动化操作,并通过几个 API 就完成了截图和数据的获取,整个运行速度是非常快的,者就是 Playwright 最最基本的用法。

当然除了同步模式,Playwright 还提供异步的 API,如果我们项目里面使用了 asyncio,那就应该使用异步模式,写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio
from playwright.async_api import async_playwright

async def main():
async with async_playwright() as p:
for browser_type in [p.chromium, p.firefox, p.webkit]:
browser = await browser_type.launch()
page = await browser.new_page()
await page.goto('https://www.baidu.com')
await page.screenshot(path=f'screenshot-{browser_type.name}.png')
print(await page.title())
await browser.close()

asyncio.run(main())

可以看到整个写法和同步模式基本类似,导入的时候使用的是 async_playwright 方法,而不再是 sync_playwright 方法。写法上添加了 async/await 关键字的使用,最后的运行效果是一样的。

另外我们注意到,这例子中使用了 with as 语句,with 用于上下文对象的管理,它可以返回一个上下文管理器,也就对应一个 PlaywrightContextManager 对象,无论运行期间是否抛出异常,它能够帮助我们自动分配并且释放 Playwright 的资源。

4. 代码生成

Playwright 还有一个强大的功能,那就是可以录制我们在浏览器中的操作并将代码自动生成出来,有了这个功能,我们甚至都不用写任何一行代码,这个功能可以通过 playwright 命令行调用 codegen 来实现,我们先来看看 codegen 命令都有什么参数,输入如下命令:

1
playwright codegen --help

结果类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Usage: npx playwright codegen [options] [url]

open page and generate code for user actions

Options:
-o, --output <file name> saves the generated script to a file
--target <language> language to use, one of javascript, python, python-async, csharp (default: "python")
-b, --browser <browserType> browser to use, one of cr, chromium, ff, firefox, wk, webkit (default: "chromium")
--channel <channel> Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc
--color-scheme <scheme> emulate preferred color scheme, "light" or "dark"
--device <deviceName> emulate device, for example "iPhone 11"
--geolocation <coordinates> specify geolocation coordinates, for example "37.819722,-122.478611"
--load-storage <filename> load context storage state from the file, previously saved with --save-storage
--lang <language> specify language / locale, for example "en-GB"
--proxy-server <proxy> specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"
--save-storage <filename> save context storage state at the end, for later use with --load-storage
--timezone <time zone> time zone to emulate, for example "Europe/Rome"
--timeout <timeout> timeout for Playwright actions in milliseconds (default: "10000")
--user-agent <ua string> specify user agent string
--viewport-size <size> specify browser viewport size in pixels, for example "1280, 720"
-h, --help display help for command

Examples:

$ codegen
$ codegen --target=python
$ codegen -b webkit https://example.com

可以看到这里有几个选项,比如 -o 代表输出的代码文件的名称;—target 代表使用的语言,默认是 python,即会生成同步模式的操作代码,如果传入 python-async 就会生成异步模式的代码;-b 代表的是使用的浏览器,默认是 Chromium,其他还有很多设置,比如 —device 可以模拟使用手机浏览器,比如 iPhone 11,—lang 代表设置浏览器的语言,—timeout 可以设置页面加载超时时间。

好,了解了这些用法,那我们就来尝试启动一个 Firefox 浏览器,然后将操作结果输出到 script.py 文件,命令如下:

1
playwright codegen -o script.py -b firefox

这时候就弹出了一个 Firefox 浏览器,同时右侧会输出一个脚本窗口,实时显示当前操作对应的代码。

我们可以在浏览器中做任何操作,比如打开百度,然后点击输入框并输入 nba,然后再点击搜索按钮,浏览器窗口如下:

可以看见浏览器中还会高亮显示我们正在操作的页面节点,同时还显示了对应的选择器字符串 input[name="wd"],右侧的窗口如图所示:

在操作过程中,该窗口中的代码就实时变化,可以看到这里生成了我们一系列操作的对应代码,比如在搜索框中输入 nba,就对应如下代码:

1
page.fill("input[name=\"wd\"]", "nba")

操作完毕之后,关闭浏览器,Playwright 会生成一个 script.py 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from playwright.sync_api import sync_playwright

def run(playwright):
browser = playwright.firefox.launch(headless=False)
context = browser.new_context()

# Open new page
page = context.new_page()

# Go to https://www.baidu.com/
page.goto("https://www.baidu.com/")

# Click input[name="wd"]
page.click("input[name=\"wd\"]")

# Fill input[name="wd"]
page.fill("input[name=\"wd\"]", "nba")

# Click text=百度一下
with page.expect_navigation():
page.click("text=百度一下")

context.close()
browser.close()

with sync_playwright() as playwright:
run(playwright)

可以看到这里生成的代码和我们之前写的示例代码几乎差不多,而且也是完全可以运行的,运行之后就可以看到它又可以复现我们刚才所做的操作了。

所以,有了这个功能,我们甚至都不用编写任何代码,只通过简单的可视化点击就能把代码生成出来,可谓是非常方便了!

另外这里有一个值得注意的点,仔细观察下生成的代码,和前面的例子不同的是,这里 new_page 方法并不是直接通过 browser 调用的,而是通过 context 变量调用的,这个 context 又是由 browser 通过调用 new_context 方法生成的。有读者可能就会问了,这个 context 究竟是做什么的呢?

其实这个 context 变量对应的是一个 BrowserContext 对象,BrowserContext 是一个类似隐身模式的独立上下文环境,其运行资源是单独隔离的,在做一些自动化测试过程中,每个测试用例我们都可以单独创建一个 BrowserContext 对象,这样可以保证每个测试用例之间互不干扰,具体的 API 可以参考 https://playwright.dev/python/docs/api/class-browsercontext

5. 移动端浏览器支持

Playwright 另外一个特色功能就是可以支持移动端浏览器的模拟,比如模拟打开 iPhone 12 Pro Max 上的 Safari 浏览器,然后手动设置定位,并打开百度地图并截图。首先我们可以选定一个经纬度,比如故宫的经纬度是 39.913904, 116.39014,我们可以通过 geolocation 参数传递给 Webkit 浏览器并初始化。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
iphone_12_pro_max = p.devices['iPhone 12 Pro Max']
browser = p.webkit.launch(headless=False)
context = browser.new_context(
**iphone_12_pro_max,
locale='zh-CN',
geolocation={'longitude': 116.39014, 'latitude': 39.913904},
permissions=['geolocation']
)
page = context.new_page()
page.goto('https://amap.com')
page.wait_for_load_state(state='networkidle')
page.screenshot(path='location-iphone.png')
browser.close()

这里我们先用 PlaywrightContextManager 对象的 devices 属性指定了一台移动设备,这里传入的是手机的型号,比如 iPhone 12 Pro Max,当然也可以传其他名称,比如 iPhone 8,Pixel 2 等。

前面我们已经了解了 BrowserContext 对象,BrowserContext 对象也可以用来模拟移动端浏览器,初始化一些移动设备信息、语言、权限、位置等信息,这里我们就用它来创建了一个移动端 BrowserContext 对象,通过 geolocation 参数传入了经纬度信息,通过 permissions 参数传入了赋予的权限信息,最后将得到的 BrowserContext 对象赋值为 context 变量。

接着我们就可以用 BrowserContext 对象来新建一个页面,还是调用 new_page 方法创建一个新的选项卡,然后跳转到高德地图,并调用了 wait_for_load_state 方法等待页面某个状态完成,这里我们传入的 state 是 networkidle,也就是网络空闲状态。因为在页面初始化和加载过程中,肯定是伴随有网络请求的,所以加载过程中肯定不算 networkidle 状态,所以这里我们传入 networkidle 就可以标识当前页面和数据加载完成的状态。加载完成之后,我们再调用 screenshot 方法获取当前页面截图,最后关闭浏览器。

运行下代码,可以发现这里就弹出了一个移动版浏览器,然后加载了高德地图,并定位到了故宫的位置,如图所示:

输出的截图也是浏览器中显示的结果。

所以这样我们就成功实现了移动端浏览器的模拟和一些设置,其操作 API 和 PC 版浏览器是完全一样的。

6. 选择器

前面我们注意到 click 和 fill 等方法都传入了一个字符串,这些字符串有的符合 CSS 选择器的语法,有的又是 text= 开头的,感觉似乎没太有规律的样子,它到底支持怎样的匹配规则呢?下面我们来了解下。

传入的这个字符串,我们可以称之为 Element Selector,它不仅仅支持 CSS 选择器、XPath,Playwright 还扩展了一些方便好用的规则,比如直接根据文本内容筛选,根据节点层级结构筛选等等。

文本选择

文本选择支持直接使用 text= 这样的语法进行筛选,示例如下:

1
page.click("text=Log in")

这就代表选择文本是 Log in 的节点,并点击。

CSS 选择器

CSS 选择器之前也介绍过了,比如根据 id 或者 class 筛选:

1
2
page.click("button")
page.click("#nav-bar .contact-us-item")

根据特定的节点属性筛选:

1
2
page.click("[data-test=login-button]")
page.click("[aria-label='Sign in']")

CSS 选择器 + 文本

我们还可以使用 CSS 选择器结合文本值进行海选,比较常用的就是 has-text 和 text,前者代表包含指定的字符串,后者代表字符串完全匹配,示例如下:

1
2
page.click("article:has-text('Playwright')")
page.click("#nav-bar :text('Contact us')")

第一个就是选择文本中包含 Playwright 的 article 节点,第二个就是选择 id 为 nav-bar 节点中文本值等于 Contact us 的节点。

CSS 选择器 + 节点关系

还可以结合节点关系来筛选节点,比如使用 has 来指定另外一个选择器,示例如下:

1
page.click(".item-description:has(.item-promo-banner)")

比如这里选择的就是选择 class 为 item-description 的节点,且该节点还要包含 class 为 item-promo-banner 的子节点。

另外还有一些相对位置关系,比如 right-of 可以指定位于某个节点右侧的节点,示例如下:

1
page.click("input:right-of(:text('Username'))")

这里选择的就是一个 input 节点,并且该 input 节点要位于文本值为 Username 的节点的右侧。

XPath

当然 XPath 也是支持的,不过 xpath 这个关键字需要我们自行制定,示例如下:

1
page.click("xpath=//button")

这里需要在开头指定 xpath= 字符串,代表后面是一个 XPath 表达式。

关于更多选择器的用法和最佳实践,可以参考官方文档:https://playwright.dev/python/docs/selectors。

7. 常用操作方法

上面我们了解了浏览器的一些初始化设置和基本的操作实例,下面我们再对一些常用的操作 API 进行说明。

常见的一些 API 如点击 click,输入 fill 等操作,这些方法都是属于 Page 对象的,所以所有的方法都从 Page 对象的 API 文档查找就好了,文档地址:https://playwright.dev/python/docs/api/class-page。

下面介绍几个常见的 API 用法。

事件监听

Page 对象提供了一个 on 方法,它可以用来监听页面中发生的各个事件,比如 close、console、load、request、response 等等。

比如这里我们可以监听 response 事件,response 事件可以在每次网络请求得到响应的时候触发,我们可以设置对应的回调方法获取到对应 Response 的全部信息,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
from playwright.sync_api import sync_playwright

def on_response(response):
print(f'Statue {response.status}: {response.url}')

with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.on('response', on_response)
page.goto('https://spa6.scrape.center/')
page.wait_for_load_state('networkidle')
browser.close()

这里我们在创建 Page 对象之后,就开始监听 response 事件,同时将回调方法设置为 on_response,on_response 对象接收一个参数,然后把 Response 的状态码和链接都输出出来了。

运行之后,可以看到控制台输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Statue 200: https://spa6.scrape.center/
Statue 200: https://spa6.scrape.center/css/app.ea9d802a.css
Statue 200: https://spa6.scrape.center/js/app.5ef0d454.js
Statue 200: https://spa6.scrape.center/js/chunk-vendors.77daf991.js
Statue 200: https://spa6.scrape.center/css/chunk-19c920f8.2a6496e0.css
...
Statue 200: https://spa6.scrape.center/css/chunk-19c920f8.2a6496e0.css
Statue 200: https://spa6.scrape.center/js/chunk-19c920f8.c3a1129d.js
Statue 200: https://spa6.scrape.center/img/logo.a508a8f0.png
Statue 200: https://spa6.scrape.center/fonts/element-icons.535877f5.woff
Statue 301: https://spa6.scrape.center/api/movie?limit=10&offset=0&token=NGMwMzFhNGEzMTFiMzJkOGE0ZTQ1YjUzMTc2OWNiYTI1Yzk0ZDM3MSwxNjIyOTE4NTE5
Statue 200: https://spa6.scrape.center/api/movie/?limit=10&offset=0&token=NGMwMzFhNGEzMTFiMzJkOGE0ZTQ1YjUzMTc2OWNiYTI1Yzk0ZDM3MSwxNjIyOTE4NTE5
Statue 200: https://p0.meituan.net/movie/da64660f82b98cdc1b8a3804e69609e041108.jpg@464w_644h_1e_1c
Statue 200: https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg@464w_644h_1e_1c
....
Statue 200: https://p1.meituan.net/movie/b607fba7513e7f15eab170aac1e1400d878112.jpg@464w_644h_1e_1c

注意:这里省略了部分重复的内容。

可以看到,这里的输出结果其实正好对应浏览器 Network 面板中所有的请求和响应内容,和下图是一一对应的:

这个网站我们之前分析过,其真实的数据都是 Ajax 加载的,同时 Ajax 请求中还带有加密参数,不好轻易获取。

但有了这个方法,这里如果我们想要截获 Ajax 请求,岂不是就非常容易了?

改写一下判定条件,输出对应的 JSON 结果,改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from playwright.sync_api import sync_playwright

def on_response(response):
if '/api/movie/' in response.url and response.status == 200:
print(response.json())

with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.on('response', on_response)
page.goto('https://spa6.scrape.center/')
page.wait_for_load_state('networkidle')
browser.close()

控制台输入如下:

1
2
3
{'count': 100, 'results': [{'id': 1, 'name': '霸王别姬', 'alias': 'Farewell My Concubine', 'cover': 'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c', 'categories': ['剧情', '爱情'], 'published_at': '1993-07-26', 'minute': 171, 'score': 9.5, 'regions': ['中国大陆', '中国香港']}, 
...
'published_at': None, 'minute': 103, 'score': 9.0, 'regions': ['美国']}, {'id': 10, 'name': '狮子王', 'alias': 'The Lion King', 'cover': 'https://p0.meituan.net/movie/27b76fe6cf3903f3d74963f70786001e1438406.jpg@464w_644h_1e_1c', 'categories': ['动画', '歌舞', '冒险'], 'published_at': '1995-07-15', 'minute': 89, 'score': 9.0, 'regions': ['美国']}]}

简直是得来全不费工夫,我们直接通过这个方法拦截了 Ajax 请求,直接把响应结果拿到了,即使这个 Ajax 请求有加密参数,我们也不用关心,因为我们直接截获了 Ajax 最后响应的结果,这对数据爬取来说实在是太方便了。

另外还有很多其他的事件监听,这里不再一一介绍了,可以查阅官方文档,参考类似的写法实现。

获取页面源码

要获取页面的 HTML 代码其实很简单,我们直接通过 content 方法获取即可,用法如下:

1
2
3
4
5
6
7
8
9
10
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto('https://spa6.scrape.center/')
page.wait_for_load_state('networkidle')
html = page.content()
print(html)
browser.close()

运行结果就是页面的 HTML 代码。获取了 HTML 代码之后,我们通过一些解析工具就可以提取想要的信息了。

页面点击

刚才我们通过示例也了解了页面点击的方法,那就是 click,这里详细说一下其使用方法。

页面点击的 API 定义如下:

1
page.click(selector, **kwargs)

这里可以看到必传的参数是 selector,其他的参数都是可选的。第一个 selector 就代表选择器,可以用来匹配想要点击的节点,如果传入的选择器匹配了多个节点,那么只会用第一个节点。

这个方法的内部执行逻辑如下:

  • 根据 selector 找到匹配的节点,如果没有找到,那就一直等待直到超时,超时时间可以由额外的 timeout 参数设置,默认是 30 秒。
  • 等待对该节点的可操作性检查的结果,比如说如果某个按钮设置了不可点击,那它会等待该按钮变成了可点击的时候才去点击,除非通过 force 参数设置跳过可操作性检查步骤强制点击。
  • 如果需要的话,就滚动下页面,将需要被点击的节点呈现出来。
  • 调用 page 对象的 mouse 方法,点击节点中心的位置,如果指定了 position 参数,那就点击指定的位置。

click 方法的一些比较重要的参数如下:

  • click_count:点击次数,默认为 1。
  • timeout:等待要点击的节点的超时时间,默认是 30 秒。
  • position:需要传入一个字典,带有 x 和 y 属性,代表点击位置相对节点左上角的偏移位置。
  • force:即使不可点击,那也强制点击。默认是 False。

具体的 API 设置参数可以参考官方文档:https://playwright.dev/python/docs/api/class-page/#pageclickselector-kwargs。

文本输入

文本输入对应的方法是 fill,API 定义如下:

1
page.fill(selector, value, **kwargs)

这个方法有两个必传参数,第一个参数也是 selector,第二个参数是 value,代表输入的内容,另外还可以通过 timeout 参数指定对应节点的最长等待时间。

获取节点属性

除了对节点进行操作,我们还可以获取节点的属性,方法就是 get_attribute,API 定义如下:

1
page.get_attribute(selector, name, **kwargs)

这个方法有两个必传参数,第一个参数也是 selector,第二个参数是 name,代表要获取的属性名称,另外还可以通过 timeout 参数指定对应节点的最长等待时间。

示例如下:

1
2
3
4
5
6
7
8
9
10
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto('https://spa6.scrape.center/')
page.wait_for_load_state('networkidle')
href = page.get_attribute('a.name', 'href')
print(href)
browser.close()

这里我们调用了 get_attribute 方法,传入的 selector 是 a.name,选定了 class 为 name 的 a 节点,然后第二个参数传入了 href,获取超链接的内容,输出结果如下:

1
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx

可以看到对应 href 属性就获取出来了,但这里只有一条结果,因为这里有个条件,那就是如果传入的选择器匹配了多个节点,那么只会用第一个节点。

那怎么获取所有的节点呢?

获取多个节点

获取所有节点可以使用 query_selector_all 方法,它可以返回节点列表,通过遍历获取到单个节点之后,我们可以接着调用单个节点的方法来进行一些操作和属性获取,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto('https://spa6.scrape.center/')
page.wait_for_load_state('networkidle')
elements = page.query_selector_all('a.name')
for element in elements:
print(element.get_attribute('href'))
print(element.text_content())
browser.close()

这里我们通过 query_selector_all 方法获取了所有匹配到的节点,每个节点对应的是一个 ElementHandle 对象,然后 ElementHandle 对象也有 get_attribute 方法来获取节点属性,另外还可以通过 text_content 方法获取节点文本。

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
霸王别姬 - Farewell My Concubine
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIy
这个杀手不太冷 - Léon
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIz
肖申克的救赎 - The Shawshank Redemption
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI0
泰坦尼克号 - Titanic
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI1
罗马假日 - Roman Holiday
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI2
唐伯虎点秋香 - Flirting Scholar
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI3
乱世佳人 - Gone with the Wind
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI4
喜剧之王 - The King of Comedy
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI5
楚门的世界 - The Truman Show
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIxMA==
狮子王 - The Lion King

获取单个节点

获取单个节点也有特定的方法,就是 query_selector,如果传入的选择器匹配到多个节点,那它只会返回第一个节点,示例如下:

1
2
3
4
5
6
7
8
9
10
11
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto('https://spa6.scrape.center/')
page.wait_for_load_state('networkidle')
element = page.query_selector('a.name')
print(element.get_attribute('href'))
print(element.text_content())
browser.close()

运行结果如下:

1
2
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
霸王别姬 - Farewell My Concubine

可以看到这里只输出了第一个匹配节点的信息。

网络劫持

最后再介绍一个实用的方法 route,利用 route 方法,我们可以实现一些网络劫持和修改操作,比如修改 request 的属性,修改 response 响应结果等。

看一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from playwright.sync_api import sync_playwright
import re

with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()

def cancel_request(route, request):
route.abort()

page.route(re.compile(r"(\.png)|(\.jpg)"), cancel_request)
page.goto("https://spa6.scrape.center/")
page.wait_for_load_state('networkidle')
page.screenshot(path='no_picture.png')
browser.close()

这里我们调用了 route 方法,第一个参数通过正则表达式传入了匹配的 URL 路径,这里代表的是任何包含 .png.jpg 的链接,遇到这样的请求,会回调 cancel_request 方法处理,cancel_request 方法可以接收两个参数,一个是 route,代表一个 CallableRoute 对象,另外一个是 request,代表 Request 对象。这里我们直接调用了 route 的 abort 方法,取消了这次请求,所以最终导致的结果就是图片的加载全部取消了。

观察下运行结果,如图所示:

可以看到图片全都加载失败了。

这个设置有什么用呢?其实是有用的,因为图片资源都是二进制文件,而我们在做爬取过程中可能并不想关心其具体的二进制文件的内容,可能只关心图片的 URL 是什么,所以在浏览器中是否把图片加载出来就不重要了。所以如此设置之后,我们可以提高整个页面的加载速度,提高爬取效率。

另外,利用这个功能,我们还可以将一些响应内容进行修改,比如直接修改 Response 的结果为自定义的文本文件内容。

首先这里定义一个 HTML 文本文件,命名为 custom_response.html,内容如下:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
<title>Hack Response</title>
</head>
<body>
<h1>Hack Response</h1>
</body>
</html>

代码编写如下:

1
2
3
4
5
6
7
8
9
10
11
12
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()

def modify_response(route, request):
route.fulfill(path="./custom_response.html")

page.route('/', modify_response)
page.goto("https://spa6.scrape.center/")
browser.close()

这里我们使用 route 的 fulfill 方法指定了一个本地文件,就是刚才我们定义的 HTML 文件,运行结果如下:

可以看到,Response 的运行结果就被我们修改了,URL 还是不变的,但是结果已经成了我们修改的 HTML 代码。

所以通过 route 方法,我们可以灵活地控制请求和响应的内容,从而在某些场景下达成某些目的。

8. 总结

本节介绍了 Playwright 的基本用法,其 API 强大又易于使用,同时具备很多 Selenium、Pyppeteer 不具备的更好用的 API,是新一代 JavaScript 渲染页面的爬取利器。

本节代码:https://github.com/Python3WebSpider/PlaywrightTest。

Other

以往各大云服务商在双十一大促都会推出很多活动,例如前几年会有 1核2G1M 69 元这类新用户活动。没想到今年腾讯云活动相当猛,2核CPU+4G内存+8M带宽首年70 块钱!如果按 3 年买,也只是 198 元,要知道平时买这样的服务器,怎么说也要 1000+ 元。

如果你不懂如何操作云服务器,可以观看【社区公开课】视频-工程师必知必会的 Linux 云服务器

活动主会场

除此之外,腾讯云那边联系到社区一起办活动,给了一部分奖励,大家从社区链接购买他们本次活动产品的可以找社区小助手返现金,具体活动规则如下:

1、腾讯云新用户 (个人或企业)购买金额大于 100 元,社区将按购买金额的 5% 进行返现;
2、腾讯云新用户 (个人或企业)购买金额大于 50 、小于 100 元,社区一次性返现 5 元;
3、腾讯云新用户 (个人或企业)购买金额大于 1000 元,社区将按购买金额的 10% 进行返现;
4、腾讯云老用户 (个人或企业)购买金额大于 300 元,社区将按购买金额的 5% 进行返现;

5、如果购买多个产品,请在同一个订单中结算(即一次性购买);

社区专属活动链接(可点击此处跳转也可复制下方地址):

1
https://cloud.tencent.com/act/double11?spread_hash_key=92518f4cf8e4cb326251fe0f7537bc23&cps_key=f31586a82039422aee085aac372348bd

attachmentId-956

活动截止时间:活动截止日期为 2021-11-30 日 12:00:00;

奖励是否叠加:不叠加;

返现发放时间:预计 30 个工作日内;

返现发放方式:个人微信/支付宝打款、企业对公打款;

⚠️ 活动参与要求 :必须是穿甲兵技术社区用户;通过社区提供的链接前往腾讯云双十一活动主会场,购买主会场中推荐的腾讯云产品;活动 COOKIE 持续 30 分钟,点击后如果还在观察活动、未购买的朋友,在购买前请再点击一次社区专属活动链接 ,以免后续在后台查询不到订单信息,也就拿不到返现。

参与活动的朋友,请联系穿甲兵小助手 (微信 elasticworm )计算返现;

社区补贴活动刚开始 2 天,已经有 150 多人参加了,他们都联系了小助手要补贴,火热火热的!错过了这次大促活动,以后买服务器可贵了

参与名单

除了云服务器活动给力之外,新用户注册 .com 域名首年也只需要 1 元!

腾讯云还为大家准备了代金券,简直省到家了

代金券

⚠️注意:社区云产品规则、适用范围等请阅读腾讯云相关文档;

Python

前些天我发起了一个投票,让大家帮忙为 《Python3网络爬虫开发实战(第二版)》选几个封面,大家也纷纷出谋划策。

其实第二版和第一版整体差别不大,这次设计主要就是换个封面的图片,然后加一个第二版的标识就好了。

之前第一版的封面是这样的:

但这个蜘蛛我觉得还有改进的空间,于是就想换一个。

然后我就自己设计了一个,然后同时设计师也设计了一个,最后出来这么几个版本:

当时就是拿不定主意,然后就发起了一个投票活动,邀请群里的各个小伙伴们来投票。

大家参与也很积极,三百多个人参与了投票,结果是这样的:

当时选项一就是刚才设计的第一张图片,选项二和三就是刚才设计的第二张图片,只是背景元素稍微换了换。

结果大家几乎清一色的都投了选项三。

但其实我对这个选项的蜘蛛不太满意,它长这样:

我就感觉,这怎么跟闹着玩似的?而且腿也太细了吧,也没点过渡和弯折,比较奇怪。但是第一个设计的根据大家的反馈,说蜘蛛的形态比较吓人,还有的人说看起来不像蜘蛛,像章鱼,我也是醉了。

反正上面这俩图我都越想越不满意,总体上还是因为轮廓的原因吧。

然后我就像自己再找一些蜘蛛轮廓,准备再做一个。

怎么做呢?

如果找到好看的轮廓,其实就是生成一个词云图就好了。

我找啊找,又搜到了几个不错的轮廓,接下来就是把这个轮廓生成词云图了。

大家可能会想,这个词云图生成也太麻烦了吧,有对应的关键字,还要符合对应的轮廓,还要同时好看,这搞起来有点难啊,目前市面上我问了下找设计师搞个词云图也得 500 块钱呢。

当然,其实 Python 也有对应的词云图生成库,但功能我觉得还是没有那么精细化。

难道要自己画?

其实不用这么麻烦,我找到了一个网站,叫做微词云,这个还是比较简单好用的,我们只需要输入关键词和每个词的频率,还能自定义想要的轮廓,它就能自动生成词云图了,如图所示:

这就是确定的一个新轮廓,这个蜘蛛是不是看起来就舒服多了?看起来既不吓人,又没那么奇怪。

最后我找了好多小伙伴也看了看最终的效果,大家都觉得这个放在封面上挺不错的。

嗯!最后就是它了!经过几天的修改,最后的效果如下:

同时在第一版封面上还加上了 Python 之父推荐的相关字样,另外「第 2 版」三个字就直接标识到了图书标题的下面。

最后的完整全封如下:

背面是四位专家的推荐语。

定价是 139.8 元,没错,这本书最后 900 多页,139.8 元,其实这个价格在这个页码基础上已经算很合算了。

书现在是什么状态呢?

现在稿子已经送到印检部啦,印前检查没问题就直接印刷啦:

等印刷出来之后我还会去印厂签上千本名,到时候就会有签名版的书搞活动送给大家哈。

请大家耐心等待几天,书很快就可以跟大家见面啦~

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

Python

别急,这书现在还没上市哈,但很快了。

最近朋友们一直在催:你的第二版爬虫书怎么还不出来啊,我都等了好几年了!你不是前几个月就完稿了吗?咋这么慢。

别急,这下是真的很快就要上市了。

为啥我的第二版书“难产”了呢?原因有好多:

  • 一个就是工作原因,之前第一版书是读研期间写的,工作之后发现书中的一些案例已经过期了,于是就决定写第二版。但工作毕竟是工作,工作的内容还是需要放在第一位的,所以第二版书的内容基本都是利用下班之后或者周末的时间写出来的,所以进度很慢。

  • 另外一个就是为了解决案例过期的问题,之前耗费了很多精力自己制作了爬虫案例平台,https://scrape.center/,里面做了几十个爬虫案例,书中对这些案例进行了配合讲解,这就解决了一些案例过期的问题,也能更好地帮助读者进行练习,前前后后这个案例平台做了也有小半年的时间。

等书籍完稿之后,其实还有不少的事情,比如审稿、修改、封面设计、推荐语等等,总之步骤是很繁琐的,改天再写个文章专门介绍下写书这件事。

单独说说推荐语。

推荐语就是印在书的背面的各位专家对本书的评价和推荐内容,一般就是几句话加上专家的 Title。就在推荐语这一块上,我还憋了个“大招”,那就是让 Python 之父 Guido van Rossum 给我写个推荐语。

Python 之父想必大家应该知道是谁吧?就是 Guido van Rossum,Python 就是他在 1989 年编写出来的。

有人说,你还真敢想,还想让 Python 之父给你这本“名不见经传”的书写推荐语?闹呢?人家怎么可能会理你?

其实想想确实不可思议的,但我还是想试试,如果真的能拿到 Python 之父的推荐,那简直是荣幸之至!

另外由于我在微软工作,Python 之父 Guido 2020 年也宣布加入微软。所以,我和他也多少是同一个公司的了(但显然职位差距过大)。

所以,没准还是有机会的呢?

好,那么问题来了,我这书都是中文写的,Python 之父是看不懂中文的,咋办呢?难道我要把全书都翻译一遍吗?

是的,还真得是这样,我不能给个中文内容或者啥也不给就干巴巴地要求他帮我写推荐语吧?这也太滑稽了。

在此之前,有好几个月时间整本书的内容还在审稿和修改,等稿子审完的时候差不多就九月份了,然后九月份左右,我就拿到了编辑那边给到的全书审核完毕的 Word 文档,但还没正式排版。看了看,这个 Word 文档一共有 1000 多页,好家伙,我真的不敢相信我写了这么多。

但也没办法,那也得翻译一遍。

如果我全部手工翻译也太麻烦了,于是我就从网上找了一些工具,比如 Word 全文翻译工具,其背后就是对接了 Google 翻译,但这些工具还有上传大小限制,于是我就又把各个章节进行了拆分,一部分一部分地翻译完了再合并起来。

当然大家知道,Google 翻译肯定质量不能保证的吧,比如一些说法和名词就翻译不太准确,我就得进行手工审查和修改。所以我又花了好多天时间对全文进行检查和修改,包括不准确的标题、表述、名词等等。然后我还对整个目录索引重新规整了一遍。

总之整个翻译准备过程差不多也花了半个月的时间,最后翻译完了英文版差不多 1600 页,如图所示,比如目录最后一节就是 1561 页了:

全书翻译

但是,这个其实还不够,如果把这个书丢过去,人家没时间看怎么办?我总得好好介绍下整本书的情况吧。

于是我又把书的介绍和前言又翻译了一遍,内容包括:写书的初衷、整本书的介绍、整体章节的规划,这样的话能帮助 Guido 更好地理解整本书的脉络和内容。

于是又有了如下的内容:

书的介绍

好像还是不够,万一 Guido 觉得写推荐语很麻烦怎么办?如果我能给他提供几个 Draft Candidate 候选是不是更有帮助一些?这样他可以找些灵感或者稍微修改下就好了,于是我又草拟了一些候选推荐语,整理了一个文档。

嗯,好像就准备差不多了,一共三个东西:

  • 全书的翻译内容

  • 书的内容介绍

  • 候选推荐语

接下来就是联系 Guido 了,心中一阵忐忑。

为了更正式一点,我发了一封邮件给 Guido,邮件整体的内容就是:先表示下对他的感谢和敬佩,然后介绍下自己的基本情况和书的基本情况,比如我是做什么的,第一版书在中国的销量等等,接着开门见山地说想要请他帮忙写一段推荐语。然后后面就附上我的书的一些详细信息,比如我整理的全文翻译书稿、内容介绍、候选推荐语等。

内容如下:

我怀着十分忐忑而又激动的心情,按下了发送键。

接下来就是漫长的等待。

我每天早上醒来都会看看邮件有没有收到 Guido 的回信。

没回。

还是没回。

还是还是没回。

还是还是还是没回。

好像要凉了。

好像凉了。

好像真凉了。

真的凉了。

可能真的是太忙或者没看到我的邮件吧。

后来,我就联系了我们部门的总 Director(这里就不透露具体信息了),看看他能不能帮我 connect 一下 Guido,他爽快地答应了。

不太清楚我的Director怎么联系的,他说单独找了下 Guido,可能是发信或者内部联系。

然后第二天,他就告诉我,得到 Guido 回复了!Guido 说确实近期比较忙,也很希望能有一个 Draft Candidate 推荐语提供给他,他可以结合着书的内容来改写一下会更好。

我的 Director 人也非常好,他也给我出了很多建议,还帮我修改了下之前我写的 Draft Candidate 推荐语给他,转发了我的邮件还帮我又介绍了下。

最后,Guido 给了最终的推荐语!!

Guido 的回信,遮盖了部分邮件内容

收到这封邮件的时候我真的开心地要跳起来了,太开心了!我的书得终于拿到了 Python 之父的推荐啦!

正文如下:

I am happy to see that Python is so widely used in the Chinese IT community. I hope this book will help more people understand Python and web crawling/scraping. *

—Guido van Rossum, creator of Python, Distinguished Engineer, Microsoft

大意就是,我非常高兴 Python 能够在中国社区得到这么广泛的应用,希望本书能够帮助更多的朋友学习 Python 和网络爬虫。

嗯,整个过程比较漫长,但真的非常开心最终有了一个好的结果,真的非常荣幸能获得 Python 之父的推荐,同时也非常感谢我的 Director 帮我联系和出建议。

得到 Guido 的推荐语之后,后面的流程就很快了,还有其他各位专家的推荐语和序我也都联系好了,真的非常感谢各位专家的推荐,书中都会一一表示感谢。

现在内容也审核完毕了,最后把推荐语添加到封面上就投入印刷了!

请大家再耐心等待一小段时间,很快《Python3网络爬虫开发实战(第二版)》就要跟大家见面啦~

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

个人随笔

大家有没有这样的一个经历:

周末和节假日出去疯玩了几天,期间没有任何学习或总结,到了周一前或者到了晚上的时候,会不会感觉到有点焦虑?比如就会想:我怎么今天又玩了一天,我还有什么什么事还没做呢,还有什么什么东西还没学呢,今天都没有什么进步,诸如此类的吧,然后就产生了一种焦虑感,想去做点什么事情找补一下。

我也有这样的情况。

我觉得我可能是一个自我 Push 比较强的人。比如有一天,我觉得自己做的东西没什么意义,或者疯玩了一天也没有学习和进步,到了一天结束的时候就会感到有些焦虑。我就想通过一些事情找补点什么回来,于是可能就想熬夜看点或者学习点东西。但实际上,有时候我也不知道应该看点什么,但总觉得需要学点或者看点什么,以“欺骗”自己的大脑今天我学习了,进步了,然后焦虑就会缓解一些。当然也有时候确实也知道自己每天还有什么事情没有完成,做完了焦虑就缓解了。

但长期以来,我感觉这样其实是不健康的,我也一直在思考我应该做点什么来解决这个问题。

经历了一些思考和尝试之后,我发现了其中的一个原因:当我给自己没有制定明确的计划和目标的时候,这种焦虑就会很频繁发生。

这个计划和目标分为长期的和短期的,长期目标和规划比较大、比较远或者说比较空洞,而短期目标和规划就是为了长期目标而制定的具体的执行方案,更能使我们焦虑的,就是短期目标和规划的缺失。

假如我没有为自己制定短期的计划,到了周末不上班的时候,就会突然不知道自己应该做些什么,至少在那一天我不知道应该做些什么。由于没有当天的规划,所以那天可能就漫无目的地休闲娱乐,比如说赖床、躺尸、刷手机(比如微博、知乎、朋友圈、抖音等)、玩游戏等等,当然也有时候无聊了就约朋友出去玩,但时间就这么一点点过去了。等到晚上到来,我就会发现,这一天怎么这么快就过去了?我好像今天也没有干什么事情,我就会焦虑,觉得今天也没有做有意义的事情,觉得我拥有的能够达成我的长期目标的时间和机会又变少了。因为长期计划会隐藏在我的潜意识里,我想达成某个大的目标,但经历了无所事事的一天之后,就会觉得我又浪费了一天,就会感觉到我能达成长期目标的几率仿佛又变小了,焦虑感就油然而生了。

为了验证我的这个想法,我就有意识地进行一些测试,比如在某一段时间我就不给自己制定短期和具体计划,任凭想到啥做啥,然后就发现那个时间段焦虑感很强。然后我就又尝试梳理自己的一些目标和具体每天应该做的一些事情,每天按照我的计划去执行,每天完成了既定计划之后,就会感觉很踏实。甚至完成了既定计划之后还能超额再做点什么的时候,就会感觉成就感爆棚。

嗯,总之,经过我的一些尝试之后,就感觉 —— 为自己制定短期的目标和规划真的很有帮助,这个短期的目标和规划时间段可能在一个月或者几周,先想这个月或者几周应该去做些什么,然后细化到每天应该去做些什么,这个一定要列详细,不要空洞,然后最好还能标记好优先级。就比如说,我规划一个月可能要学一门课程,那么我就分配一下,我哪一天需要具体学从第几节到第几节的内容,记录到我的 Todo List 里面。我是用的「滴答清单」这款软件,在里面我就给我每天需要做的事情做好分配,这样每天我就知道自己需要做什么了。

但这时候有朋友可能就说,难道你周末规划的也全是学习和工作吗?娱乐、放松呢?这个当然也是有的。我觉得人不能把自己逼的太紧了,每天都紧绷着做各种各样的事情,弦会断的。所以,有时候我就会给自己规划有些时间段就是可以放松和玩的,比如周五晚上就玩游戏、周六上午就赖床多睡会、周末也会规划时间出去玩等等。

所以,做好了规划,不论是学习、工作还是放松、娱乐,这都是在自己的期望和规划之中。比如我就规划了某个时间段去放空自己玩两天,那两天就全身心放松玩两天,那也不会有什么焦虑,因为这就在我的期望之内,我知道玩完这两天,我后面的一些规划时间和事情仍然可以正常完成我做达成的短期和长期目标,焦虑感自然就不会产生了。

嗯,就是这样。如果你也遇到了文章一开始我提到的问题,不妨尝试给自己制定一个详细的短期目标和规划吧。

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

安装配置

Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。

如果你还不了解 YML 文件配置,可以先阅读 YAML 入门教程

Compose 使用的三个步骤:

  • 使用 Dockerfile 定义应用程序的环境。
  • 使用 docker-compose.yml 定义构成应用程序的服务,这样它们可以在隔离环境中一起运行。
  • 最后,执行 docker-compose up 命令来启动并运行整个应用程序。

安装

当前,最新的 Docker 已经把 Compose 作为 Docker 的命令一部分了,可以直接安装 Docker:https://setup.scrape.center/docker

另外如果要单独安装 Docker-Compose,可以参考 https://www.runoob.com/docker/docker-compose.html

安装配置

Helm 是 Kubernetes 的包管理器。

相关链接

  • 官网:https://helm.sh/
  • 中文文档:https://helm.sh/zh/docs/

该指南展示了如何安装 Helm CLI。Helm 可以用源码或构建的二进制版本安装。

下面的内容来自:https://helm.sh/zh/docs/intro/install/,最新内容以该链接为准。

用 Helm 项目安装

Helm 项目提供了两种获取和安装 Helm 的方式。这是官方提供的获取 Helm 发布版本的方法。另外, Helm 社区提供了通过不同包管理器安装 Helm 的方法。这些方法可以在下面的官方方法之后看到。

用二进制版本安装

每个 Helm 版本都提供了各种操作系统的二进制版本,这些版本可以手动下载和安装。

  1. 下载 需要的版本
  2. 解压(tar -zxvf helm-v3.0.0-linux-amd64.tar.gz)
  3. 在解压目中找到helm程序,移动到需要的目录中(mv linux-amd64/helm /usr/local/bin/helm)

然后就可以执行客户端程序并 添加稳定仓库: helm help.

注意 针对 Linux AMD64,Helm 的自动测试只有在 CircleCi 构建和发布时才会执行。测试其他操作系统是社区针对系统问题请求 Helm 的责任。

使用脚本安装

Helm 现在有个安装脚本可以自动拉取最新的 Helm 版本并在 本地安装

您可以获取这个脚本并在本地执行。它良好的文档会让您在执行之前知道脚本都做了什么。

1
2
3
$ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
$ chmod 700 get_helm.sh
$ ./get_helm.sh

如果想直接执行安装,运行curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

通过包管理器安装

Helm 社区提供了通过操作系统包管理器安装 Helm 的方式。但 Helm 项目不支持且不认为是可信的第三方。

使用 Homebrew (macOS)

Helm 社区成员贡献了一种在 Homebrew 构建 Helm 的方案,这个方案通常是最新的。

1
brew install helm

(注意:还有一个 emacs-helm 的方案,当然这是另一个项目了。)

使用 Chocolatey (Windows)

Helm 社区成员贡献了一个 Helm 包Chocolatey中构建, 包通常是最新的。

1
choco install kubernetes-helm

使用 Apt (Debian/Ubuntu)

Helm 社区成员贡献了针对 Apt 的一个 Helm 包,包通常是最新的。

1
2
3
4
5
curl https://baltocdn.com/helm/signing.asc | sudo apt-key add -
sudo apt-get install apt-transport-https --yes
echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm

使用 Snap

Snapcrafters社区维护了 Helm 包的 Snap 版本:

1
sudo snap install helm --classic

使用 pkg (FreeBSD)

FreeBSD 社区成员贡献了一个 Helm 页面来构建 FreeBSD 端口集。通常都是最新的包。

1
pkg install helm

开发版本构建

另外您可以下载和安装 Helm 的开发版本。

使用 Canary 构建

“Canary”版本是从 Helm 最新的 master 分支构建。这些不是官方版本,可能不稳定。但是这提供测试边缘特性的条件。

Canary Helm 二进制包存储在 get.helm.sh。以下是一般构建的链接:

  • Linux AMD64
  • macOS AMD64
  • 实验性 Windows AMD64

使用源码 Source (Linux, macOS)

从源码构建 Helm 的工作要稍微多一点,但如果你想测试最新(预发布)的 Helm 版本,这是最好的方式。

您必须有可用的 Go 环境。

1
2
3
$ git clone https://github.com/helm/helm.git
$ cd helm
$ make

如果需要,会拉取依赖并缓存,然后验证配置。然后会编译helm并放在bin/helm

总结

大多数情况下,安装只需要简单地获取一个构建好的helm二进制包。本文档为想使用 Helm 做更复杂事情的人提供额外示例。

一旦你成功安装了 Helm 客户端,就可以继续使用 Helm 管理 chart 和 添加稳定的仓库

安装配置

Kubernetes,又被简称作 K8s(K 和 s 中间含有 8 个字母),它是用于编排容器化应用程序的云原生系统。Kubernetes 诞生自 Google,现在已经由 CNCF (云原生计算基金会)维护更新。Kubernetes 是目前最受欢迎的集群管理方案之一,可以非常容易地实现容器的管理编排。

刚刚我们提到,Kubernetes 是一个容器编排系统,对于“编排”二字,可能不太容易理解其中的含义。为了对它有更好的理解,我们先回过头来看看容器的定位是什么以及容器解决了什么问题,不能解决什么问题,然后我们再来了解下 Kubernetes 能够弥补容器哪些缺失的内容。

好,首先来看容器。最常见的容器技术就是 Docker 了,容器它提供了相比传统虚拟化技术更轻量级的机制来创建隔离的应用程序的运行环境。比如对于某个应用程序,我们使用容器运行时,不必担心它与宿主机之间产生资源冲突,不必担心多个容器之间产生资源冲突。同时借助于容器技术,我们还能更好地保证开发环境和生产环境的运行一致性。另外由于每个容器都是独立的,因此可以将多个容器运行在同一台宿主机上,以提高宿主机资源利用率,从而也进一步降低了成本。总之,使用容器带来的好处很多,可以为我们带来极大的便利。

不过单单依靠容器技术并不能解决所有的问题,也可以说容器技术也引入了新的问题,比如说:

  • 如果容器突然运行异常了怎么办?
  • 如果容器所在的宿主机突然运行异常了怎么办?
  • 如果有多个容器,他们之间怎么有效地传输数据?
  • 如果单个容器达到了瓶颈,如何平稳且有效地进行扩容?
  • 如果生产环境是由多台主机组成的,我们怎样更好地决定使用哪台主机来运行哪个容器?

以上列举了一些单纯依靠容器技术或者单纯依靠 Docker 不能解决的问题,而 Kubernetes 作为容器编排平台,提供了一个可弹性运行的分布式系统框架,各个容器可以运行在 Kubernetes 平台上,容器的管理、调度、部署、扩容等各个操作都可以经由 Kubernetes 来有效实现。比如说,Kubernetes 可以管理单个容器的声明周期,并且可以根据需要来扩展和释放资源,如果某个容器意外关闭,Kubernetes 可以根据对应的策略选择重启该容器,以保证服务的正常运行。再比如说,Kubernetes 是一个分布式的平台,当容器所在的主机突然发生异常,Kubernetes 可以将异常主机上运行的容器转移到其他正常的主机上运行,另外 Kubernetes 还可以根据容器运行所需要占用的资源自动选择合适的主机来运行。总之,Kubernetes 对容器的调度和管理提供了非常强大的支持,可以帮我们解决上述的诸多问题。

相关资料

  • 官方文档:https://kubernetes.io/docs/home/

安装方式

安装 Kubernetes 有好多方式,比如 Minicube、Docker 自带、自建集群、云服务商提供。

Minicube

参考:https://kubernetes.io/docs/tutorials/hello-minikube/

Docker 自带

现在 Docker for Windows 和 Docker for Mac 已经自带了 Kubernetes 的集群功能,只需要打开对应开关即可,如图所示:

image-20211004003229922

这里只需要把 Enable Kubernetes 勾选即可。

集群搭建

搭建 Kubernetes 集群是比较麻烦的,参考链接:

  • https://segmentfault.com/a/1190000040107263
  • https://kubernetes.io/zh/docs/setup/production-environment/tools/kubeadm/high-availability/
  • https://www.kubernetes.org.cn/7315.html

云服务商

很多云服务商已经提供了 Kubernetes,请移步对应云服务商的功能支持说明,参考链接:

  • Azure:https://azure.microsoft.com/en-us/services/kubernetes-service/
  • Google:https://cloud.google.com/kubernetes-engine
  • Amazon:https://aws.amazon.com/cn/eks/
  • 腾讯云:https://cloud.tencent.com/product/tke
  • 阿里云:https://cn.aliyun.com/product/kubernetes
  • 华为云:https://support.huaweicloud.com/kubernetes.html
  • 百度云:https://cloud.baidu.com/product/cce.html

安装配置

用 Splash 做页面抓取时,如果爬取的量非常大,任务非常多,用一个 Splash 服务来处理的话,未免压力太大了,此时可以考虑搭建一个负载均衡器来把压力分散到各个服务器上。这相当于多台机器多个服务共同参与任务的处理,可以减小单个 Splash 服务的压力。

配置 Splash 服务

要搭建 Splash 负载均衡,首先要有多个 Splash 服务。假如这里在 4 台远程主机的 8050 端口上都开启了 Splash 服务,它们的服务地址分别为 41.159.27.223:8050、41.159.27.221:8050、41.159.27.9:8050 和 41.159.117.119:8050,这 4 个服务完全一致,都是通过 Docker 的 Splash 镜像开启的。访问其中任何一个服务时,都可以使用 Splash 服务。

具体的 Splash 搭建流程可以参考:https://setup.scrape.center/splash

配置负载均衡

接下来,可以选用任意一台带有公网 IP 的主机来配置负载均衡。首先,在这台主机上装好 Nginx,然后修改 Nginx 的配置文件 nginx.conf,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
http {
upstream splash {
least_conn;
server 41.159.27.223:8050;
server 41.159.27.221:8050;
server 41.159.27.9:8050;
server 41.159.117.119:8050;
}
server {
listen 8050;
location / {proxy_pass http://splash;}
}
}

这样我们通过 upstream 字段定义了一个名字叫作 splash 的服务集群配置。其中 least_conn 代表最少链接负载均衡,它适合处理请求处理时间长短不一造成服务器过载的情况。

当然,我们也可以不指定配置,具体如下:

1
2
3
4
5
6
upstream splash {
server 41.159.27.223:8050;
server 41.159.27.221:8050;
server 41.159.27.9:8050;
server 41.159.117.119:8050;
}

这样默认以轮询策略实现负载均衡,每个服务器的压力相同。此策略适合服务器配置相当、无状态且短平快的服务使用。

另外,我们还可以指定权重,配置如下:

1
2
3
4
5
6
upstream splash {
server 41.159.27.223:8050 weight=4;
server 41.159.27.221:8050 weight=2;
server 41.159.27.9:8050 weight=2;
server 41.159.117.119:8050 weight=1;
}

这里 weight 参数指定各个服务的权重,权重越高,分配到处理的请求越多。假如不同的服务器配置差别比较大的话,可以使用此种配置。

最后,还有一种 IP 散列负载均衡,配置如下:

1
2
3
4
5
6
7
upstream splash {
ip_hash;
server 41.159.27.223:8050;
server 41.159.27.221:8050;
server 41.159.27.9:8050;
server 41.159.117.119:8050;
}

服务器根据请求客户端的 IP 地址进行散列计算,确保使用同一个服务器响应请求,这种策略适合有状态的服务,比如用户登录后访问某个页面的情形。对于 Splash 来说,不需要应用此设置。

我们可以根据不同的情形选用不同的配置,配置完成后重启一下 Nginx 服务:

1
sudo nginx -s reload

这样直接访问 Nginx 所在服务器的 8050 端口,即可实现负载均衡了。

配置认证

现在 Splash 是可以公开访问的,如果不想让其公开访问,还可以配置认证,这仍然借助于 Nginx。可以在 serverlocation 字段中添加 auth_basicauth_basic_user_file 字段,具体配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http {
upstream splash {
least_conn;
server 41.159.27.223:8050;
server 41.159.27.221:8050;
server 41.159.27.9:8050;
server 41.159.117.119:8050;
}
server {
listen 8050;
location / {
proxy_pass http://splash;
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/conf.d/.htpasswd;
}
}
}

这里使用的用户名和密码配置放置在 /etc/nginx/conf.d 目录下,我们需要使用 htpasswd 命令创建。例如,创建一个用户名为 admin 的文件,相关命令如下:

1
htpasswd -c .htpasswd admin

接下来,就会提示我们输入密码。输入两次之后,就会生成密码文件,其内容如下:

1
2
cat .htpasswd
admin:5ZBxQr0rCqwbc

配置完成后重启一下 Nginx 服务,运行如下命令:

1
sudo nginx -s reload

这样访问认证就成功配置好了。

测试

最后,我们可以用代码来测试一下负载均衡的配置,看看到底是不是每次请求会切换 IP。利用 http://httpbin.org/get 测试即可,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
from urllib.parse import quote
import re

lua = '''
function main(splash, args)
local treat = require("treat")
local response = splash:http_get("http://httpbin.org/get")
return treat.as_string(response.body)
end
'''

url = 'http://splash:8050/execute?lua_source=' + quote(lua)
response = requests.get(url, auth=('admin', 'admin'))
ip = re.search('(\d+\.\d+\.\d+\.\d+)', response.text).group(1)
print(ip)

这里 URL 中的 splash 字符串请自行替换成自己的 Nginx 服务器 IP。这里我修改了 Hosts,设置了 splash 为 Nginx 服务器 IP。

多次运行代码之后,可以发现每次请求的 IP 都会变化,比如第一次的结果:

1
41.159.27.223

第二次的结果:

1
41.159.27.9

这就说明负载均衡已经成功实现了,配置负载均衡后,可以多个 Splash 服务共同合作,减轻单个服务的负载,这还是比较有用的。

当然,我们也可以借助于 Kubernetes + Docker 来实现负载均衡,管理起来更加简单方便,感兴趣可以搜索相关内容试验一下。

安装配置

爬虫过程中难免会遇到各种各样的验证码,而大多数验证码还是图形验证码,这时候我们可以直接用 OCR 来识别。

OCR

OCR,即 Optical Character Recognition,光学字符识别。是指通过扫描字符,然后通过其形状将其翻译成电子文本的过程。那么对于图形验证码来说,它都是一些不规则的字符,但是这些字符确实是由字符稍加扭曲变换得到的内容。

例如这样的验证码,如图所示:

对于这种验证码,我们便可以使用 OCR 技术来将其转化为电子文本,然后爬虫将识别结果提交给服务器,便可以达到自动识别验证码的过程。

Tesserocr 是 Python 的一个 OCR 识别库,但其实是对 Tesseract 做的一层 Python API 封装,所以它的核心是 Tesseract,所以在安装 Tesserocr 之前我们需要先安装 Tesseract,本节我们来了解下它们的安装方式。

相关链接

  • Tesserocr GitHub:https://github.com/sirfz/tesserocr
  • Tesserocr PyPi:https://pypi.python.org/pypi/tesserocr
  • Tesseract 下载地址:http://digi.bib.uni-mannheim.de/tesseract
  • Tesseract GitHub:https://github.com/tesseract-ocr/tesseract
  • Tesseract 语言包:https://github.com/tesseract-ocr/tessdata
  • Tesseract 文档:https://github.com/tesseract-ocr/tesseract/wiki/Documentation

Windows 下的安装

本小节内容来自于:https://segmentfault.com/a/1190000039929696,可以移步此链接查看原文。

另外也可以查看其他博文,如:https://cloud.tencent.com/developer/article/1616037 来安装和排查。

为了增大成功安装的几率,推荐使用 Python 3.7 版本。

在 Windows 下,首先需要下载 Tesseract,它为 Tesserocr 提供了支持,下载链接为:http://digi.bib.uni-mannheim.de/tesseract/

点击进入之后可以看到有各种 exe 的下载列表,在这里可以选择下载 4.0 版本 tesseract-ocr-setup-4.00.00dev.exe,如图所示:

其中文件名中带有 dev 的为开发版本,不带 dev 的为稳定版本。

下载完成之后双击安装即可,在安装过程中可以勾选上 Additional language data 选项,安装 OCR 识别支持的语言包,这样 OCR 便可以识别多国语言。

复制你的安装路径,我的安装路径 D:\Python\Tesseract-OCR,界面如下:

打开我的电脑系统属性->高级->环境变量,把该路径配置到环境变量:

然后将下载好的字库放到 Tesseract-OCR 项目的 tessdata 文件夹里面。

接下来再安装 Tesserocr 即可,直接使用 Pip 安装:

1
pip3 install tesserocr pillow

另外如果安装过程中出现错误,请移步官方安装说明排查问题:https://github.com/sirfz/tesserocr

Linux 下的安装

对于 Linux 来说,不同系统已经有了不同的发行包了,它可能叫做 tesseract-ocr 或者 tesseract,直接用对应的命令安装即可。

Ubuntu、Debian、Deepin

安装命令如下:

1
sudo apt-get install -y tesseract-ocr libtesseract-dev libleptonica-dev

CentOS、RedHat

安装命令如下:

1
yum install -y tesseract

不同发行版本运行如上命令即可完成 Tesseract 的安装。

安装完成之后便可以调用 tesseract 命令了。

我们查看一下其支持的语言:

1
tesseract --list-langs

运行结果示例:

1
2
3
4
List of available languages (3):
eng
osd
equ

结果显示其只支持几种语言,如果我们想要安装多国语言还需要安装语言包,官方叫做 tessdata。

tessdata 的下载链接为:https://github.com/tesseract-ocr/tessdata

利用 Git 命令将其下载下来并迁移到相关目录即可,不同的版本迁移命令如下:

Ubuntu、Debian、Deepin

1
2
git clone https://github.com/tesseract-ocr/tessdata.git
sudo mv tessdata/* /usr/share/tesseract-ocr/tessdata

CentOS、RedHat

1
2
git clone https://github.com/tesseract-ocr/tessdata.git
sudo mv tessdata/* /usr/share/tesseract/tessdata

这样就可以将下载下来的语言包全部安装了。

这时我们重新运行列出所有语言的命令:

1
tesseract --list-langs

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List of available languages (107):
afr
amh
ara
asm
aze
aze_cyrl
bel
ben
bod
bos
bul
cat
ceb
ces
chi_sim
chi_tra
...

即可发现其列出的语言就多了非常多,比如 chi_sim 就代表简体中文,这就证明语言包安装成功了。

接下来再安装 Tesserocr 即可,直接使用 Pip 安装:

1
pip3 install tesserocr pillow

Mac 下的安装

Mac 下首先使用 Homebrew 安装 Imagemagick 和 Tesseract 库:

1
2
brew install imagemagick
brew install tesseract --all-languages

接下来再安装 Tesserocr 即可:

1
pip3 install tesserocr pillow

这样我们便完成了 Tesserocr 的安装。

验证安装

接下来我们可以使用 Tesseract 和 Tesserocr 来分别进行测试。

下面我们以如下的图片为样例进行测试,如图所示:

图片链接为:https://raw.githubusercontent.com/Python3WebSpider/TestTess/master/image.png,可以直接保存或下载。

我们首先用命令行进行测试,将图片下载保存为 image.png,然后用 Tesseract 命令行测试,命令如下:

1
tesseract image.png result -l eng && cat result.txt

运行结果:

1
2
Tesseract Open Source OCR Engine v3.05.01 with Leptonica
Python3WebSpider

我们调用了 tesseract 命令,第一个参数为图片名称,第二个参数 result 为结果保存的目标文件名称,-l 指定使用的语言包,在此使用 eng 英文,然后再用 cat 命令将结果输出。

第二行的运行结果便是图片的识别结果,Python3WebSpider。

我们可以看到这时已经成功将图片文字转为电子文本了。

然后我们还可以利用 Python 代码来测试,这里就需要借助于 Tesserocr 库了,测试代码如下:

1
2
3
4
import tesserocr
from PIL import Image
image = Image.open('image.png')
print(tesserocr.image_to_text(image))

在这里我们首先利用 Image 读取了图片文件,然后调用了 tesserocr 的 image_to_text() 方法,再将将其识别结果输出。

运行结果:

1
Python3WebSpider

另外我们还可以直接调用 file_to_text() 方法,也可以达到同样的效果:

1
2
import tesserocr
print(tesserocr.file_to_text('image.png'))

运行结果:

1
Python3WebSpider

如果成功输出结果,则证明 Tesseract 和 Tesserocr 都已经安装成功。

安装配置

Frida 是一个基于 Python 和 JavaScript 的 Hook 与调试框架,是一款易用的跨平台 Hook 工具,无 论 Java 层的逻辑,还是 Native 层的逻辑,它都可以 Hook。Frida 可以把代码插入原生 App 的内存空 间,然后动态地监视和修改其行为,支持 Windows、Mac、Linux、Android、iOS 全平台。

相关链接

  • 官网:https://frida.re/
  • GitHub:https://github.com/frida/frida

安装

pip 安装

推荐使用 pip3 安装,命令如下:

1
pip3 install frida frida-tools

命令执行完毕之后即可完成安装。

验证安装

安装完成之后,可以在 Python 命令行下测试。

1
2
$ python3
>>> import frida

如果没有错误报出,则证明库已经安装好了。

更多安装说明参考:https://frida.re/docs/installation/

安装配置

IDA Pro 的英文全称是 Interactive Disassembler Professional,即交互式反汇编器专业版,大家也称 之为 IDA。它由一家总部位于比利时的 Hex-Rayd 公司开发,功能十分强大,是目前流行的反汇编软 件之一,也是安全分析人士必备的一款软件。

IDA Pro 最重要的功能便是可以将二进制文件中的机器代码(如 010101)转化成汇编代码,甚至 可以进一步根据汇编代码的执行逻辑还原出高级语言(如 C/C++)编写的代码,从而大大提高代码的 可读性。IDA Pro 不仅仅局限于分析 Android 中的 so 文件,它可以处理和分析几乎所有的二进制文件, Windows、DOS、Unix、Linux、Mac、Java、.NET 等平台的二进制文件都不在话下。另外,IDA Pro 提 供了图形界面和强大的调试功能,利用它我们可以直观地实时调试和分析二进制文件。除了这些,IDA Pro 还提供开放式的插件架构,我们可以编写自定义的插件轻松扩展其功能。

总之,IDA Pro 是一款极其强大的反汇编软件,已经成为业界安全分析必不可少的一个工具,更多介绍可以查看 IDA Pro 的官网。

安装

IDA Pro 是收费的,但是有不少大佬已经破解了,可以移步相关资源查看:https://bbs.pediy.com/thread-263559.htm

其他的安装教程:

  • https://blog.csdn.net/qq_34732729/article/details/109184623
  • https://www.bilibili.com/video/BV1uA411V7LK/

安装配置

VirtualXposed 是基于VirtualAppepic非 ROOT环境下运行 Xposed 模块的实现(支持 5.0~10.0)。

与 Xposed 相比,目前 VirtualXposed 有两个限制:

  1. 不支持修改系统(可以修改普通 APP 中对系统 API 的调用),因此重力工具箱,应用控制器等无法使用。
  2. 暂不支持资源 HOOK,因此资源钩子不会起任何作用;使用资源 HOOK 的模块,相应的功能不会生效。

相关链接

  • 官网:https://vxp.app/
  • GitHub:https://github.com/android-hacker/VirtualXposed
  • 中文文档:https://github.com/android-hacker/VirtualXposed/blob/vxp/CHINESE.md

安装

可以到 https://github.com/android-hacker/VirtualXposed/releases 下载最新的安装包,如图所示:

image-20211004001344424

下载下来 apk 之后直接安装即可:

1
adb install VirtualXposed_0.20.3.apk

使用说明见:https://vxposed.com/,中文文档见:https://github.com/android-hacker/VirtualXposed/blob/vxp/CHINESE.md

安装配置

强烈建议看官方文档:https://developer.android.com/guide,因为这才是原汁原味的搭建流程指导。

一些中文教程也有,2021 年请移步:

  • https://www.cnblogs.com/kiba/p/14830060.html
  • https://www.jianshu.com/p/690a69bc031b

安装配置

EdXposed 在 Xposed 的基础上提供了 Android 8.0+ 的支持,同时提供了白名单、黑名单、兼容模式、无需重启等特性。

相关链接

  • 官网:https://edxp.meowcat.org/
  • GitHub:https://github.com/ElderDrivers/EdXposed

安装

由于 EdXposed 一直在更新,请移步最新官方文档 https://edxp.meowcat.org/ 查看最新安装说明:

安装配置

PyExecJS 是一个可以模拟调用 JavaScript 脚本的 Python 库。

相关链接

  • GitHub:https://github.com/doloopwhile/PyExecJS
  • PyPi:https://pypi.org/project/PyExecJS/

准备工作

PyExecJS 需要 Node.js 执行环境,请先安装 Node.js,参考:https://setup.scrape.center/nodejs。

安装方法

pip 安装

推荐使用 pip3 安装,命令如下:

1
pip3 install PyExecJS

命令执行完毕之后即可完成安装。

验证安装

安装完成之后,可以在 Python 命令行下测试。

1
2
$ python3
>>> import execjs

如果没有错误报出,则证明库已经安装好了。

安装配置

Elasticsearch 也有对应的 Python 库,名称也叫 Elasticsearch。

相关链接

  • GitHub:https://github.com/elastic/elasticsearch-py
  • 官方文档:https://elasticsearch-py.readthedocs.io/

准备工作

Elasticsearch 的 Python 库需要和 Elasticsearch 配合使用,在此之前请安装 Elasticsearch,安装教程参考: https://setup.scrape.center/elasticsearch。

安装方法

pip 安装

推荐使用 pip3 安装,命令如下:

1
pip3 install elasticsearch

命令执行完毕之后即可完成安装。

如果要支持异步 async,可以安装如下包:

1
pip3 install elasticsearch[async]

验证安装

安装完成之后,可以在 Python 命令行下测试。

1
2
$ python3
>>> import elasticsearch

如果没有错误报出,则证明库已经安装好了。

安装配置

Motor 是 PyMongo 的支持异步的库。

相关链接

  • 文档:https://motor.readthedocs.io/en/stable/
  • GitHub:https://github.com/mongodb/motor
  • PyPi: https://pypi.org/project/motor/

安装

pip 安装

推荐使用 pip3 安装,命令如下:

1
pip3 install motor

命令执行完毕之后即可完成安装。

验证安装

安装完成之后,可以在 Python 命令行下测试。

1
2
$ python3
>>> import motor

如果没有错误报出,则证明库已经安装好了。

安装配置

Numpy 是 Python 中的一个科学计算库,功能非常强大。

相关链接

  • 官网:https://numpy.org/
  • 文档:https://numpy.org/doc/stable/
  • 中文教程:https://www.runoob.com/numpy/numpy-tutorial.html

安装方法

pip 安装

推荐使用 pip3 安装,命令如下:

1
pip3 install numpy

命令执行完毕之后即可完成安装。

验证安装

安装完成之后,可以在 Python 命令行下测试。

1
2
$ python3
>>> import numpy

如果没有错误报出,则证明库已经安装好了。

安装配置

Pyppeteer 是一个自动化测试框架,是 Puppteer 的 Python 版本。

相关链接

  • GitHub:https://github.com/pyppeteer/pyppeteer
  • 文档:https://pyppeteer.github.io/pyppeteer/

安装

pip 安装

推荐使用 pip3 安装,命令如下:

1
pip3 install pyppeteer

命令执行完毕之后即可完成安装。

安装完成之后可以运行如下命令进行一些初始化操作:

1
pyppeteer-install

运行之后 Pyppeteer 会下载一个 Chromium 浏览器并配置好环境变量。

验证安装

安装完成之后,可以在 Python 命令行下测试。

1
2
$ python3
>>> import pyppeteer

如果没有错误报出,则证明库已经安装好了。

另外还可以运行测试脚本:

1
2
3
4
5
6
7
8
9
10
11
import asyncio
from pyppeteer import launch

async def main():
browser = await launch()
page = await browser.newPage()
await page.goto('https://www.baidu.com')
await page.screenshot({'path': 'baidu.png'})
await browser.close()

asyncio.get_event_loop().run_until_complete(main())

运行之后,如果之前没有运行过 pyppeteer-install 命令的话,Pyppeteer 会进行一些初始化配置,运行完毕之后,就会启动浏览器,然后访问百度,生成截图。

安装配置

Redis 是一个基于内存的高效的非关系型数据库,本节我们来了解下 Redis 在各个平台的安装过程。

相关链接

  • 官方网站:https://redis.io
  • 官方文档:https://redis.io/documentation
  • 中文官网:http://www.redis.cn
  • GitHub:https://github.com/antirez/redis
  • 中文教程:http://www.runoob.com/redis/redis-tutorial.html
  • Redis Desktop Manager:https://redisdesktop.com
  • Redis Desktop Manager GitHub:https://github.com/uglide/RedisDesktopManager

Windows 下的安装

Redis 在 Windows 下可以直接到 https://redis.io/downloadhttps://nowjava.com/download/31283 下载。

安装过程比较简单,直接点击下一步安装即可,安装完成之后 Redis 便会启动。

在系统服务里可以观察到多了一个正在运行到 Redis 服务,如图所示:

img

另外推荐下载一个 Redis Desktop Manager 可视化管理工具,来管理 Redis。

可以到官方网站下载,链接为:https://redisdesktop.com/download 也可以到 GitHub 下载最新发行版本,链接为:https://github.com/uglide/RedisDesktopManager/releases

安装之后直接连接本地 Redis 即可,简单方便。

Linux 下的安装

这里依然还是分为两类平台介绍。

Ubuntu、Debian、Deepin

使用 apt-get 命令行安装:

1
sudo apt-get -y install redis-server

运行如上命令即可完成 Redis 的安装,然后输入 redis-cli 即可进入 Redis 命令行模式。

1
2
3
4
5
$ redis-cli
127.0.0.1:6379> set 'name' 'Germey'
OK
127.0.0.1:6379> get 'name'
"Germey"

这样就证明 Redis 成功安装了,但是现在 Redis 还是无法远程连接的,依然需要修改配置文件,配置文件路径为 /etc/redis/redis.conf。

注释这一行:

1
bind 127.0.0.1

另外推荐给 Redis 设置密码,取消注释这一行:

1
requirepass foobared

foobared 即当前密码,可以自行修改。

然后重启 Redis 服务,使用如下命令:

1
sudo /etc/init.d/redis-server restart

现在就可以使用密码远程连接 Redis 了。

另外停止和启动 Redis 服务的命令如下:

1
2
sudo /etc/init.d/redis-server stop
sudo /etc/init.d/redis-server start

CentOS、RedHat

首先添加 EPEL 仓库,然后更新 Yum 源:

1
2
sudo yum install epel-release
sudo yum update

然后安装 Redis 数据库:

1
sudo yum -y install redis

安装好之后启动 Redis 服务:

1
sudo systemctl start redis

同样可以使用 redis-cli 进入 Redis 命令行模式操作。

另外为了可以使 Redis 能被远程连接,需要修改配置文件,路径为 /etc/redis.conf。

注释这一行:

1
bind 127.0.0.1

另外推荐给 Redis 设置密码,取消注释这一行:

1
requirepass foobared

foobared 即当前密码,可以自行修改。

之后保存重启 Redis 服务:

1
sudo systemctl restart redis

这样就可以远程连接 Redis 了。

Mac 下的安装

推荐使用 Homenbrew 安装,执行 brew 命令即可。

1
brew install redis

启动 Redis 服务:

1
2
brew services start redis
redis-server /usr/local/etc/redis.conf

这样就启动了 Redis 服务。

同样可以使用 redis-cli 进入 Redis 命令行模式。

Mac 下 Redis 的配置文件路径是 /usr/local/etc/redis.conf,可以通过修改它来配置访问密码。

修改配置文件后需要重启 Redis 服务,停止、重启 Redis 服务的命令如下:

1
2
brew services stop redis
brew services restart redis

另外在 Mac 下也可以安装 Redis Desktop Manager 可视化管理工具来管理 Redis。

安装配置

retrying 提供了一些装饰器,使得我们非常方便地配置重试机制。

本节我们了解下 retrying 的安装方式。

相关链接

  • GitHub:https://github.com/rholder/retrying
  • PyPi:https://pypi.org/project/retrying/

安装方法

pip 安装

推荐使用 pip3 安装,命令如下:

1
pip3 install retrying

命令执行完毕之后即可完成安装。

验证安装

安装完成之后,可以在 Python 命令行下测试。

1
2
$ python3
>>> import retrying

如果没有错误报出,则证明库已经安装好了。

安装配置

Xposed 框架是一套开源的,在 Android 高权限模式下运行的框架服务,它可以在不修改 App 源码的情况下影响程序运行(修改系统)的框架服务。基于 Xposed,可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。

其实现原理我们稍作了解即可:Xposed 框架的原理是通过替换系统级别的 /system/bin/app_process 程序控制 zygote 进程,使得app_process 在启动过程中加载 XposedBridge.jar,这个 jar 包里面定义了对系统方法、属性的一系列 Hook 操作,同时还提供了几个 Hook API 供我们编写 Xposed 模块使用。我们在编写 Xposed 模块时,引用 Xposed 提供的几个 Hook 方法就可以实现对系统级别任意方法和属性的修改了。

安装

请移步:https://cn.sync-computers.com/how-install-xposed-framework

安装配置

ElasticSearch 是一个分布式的,高性能,高可用的,可伸缩的搜索和分析系统

(1)可以作为大型分布式集群(数百台服务器)技术,处理 PB 级的数据,服务大公司;也可以运行在单机上服务于小公司

(2)Elasticsearch 不是什么新技术,主要是将全文检索、数据分析以及分布式技术,合并在了一起,才形成了独一无二的 ES:lucene(全文检索),商用的数据分析软件,分布式数据库

(3)对用户而言,是开箱即用的,非常简单,作为中小型应用,直接 3 分钟部署一下 ES,就可以作为生产环境的系统来使用了,此时的场景是数据量不大,操作不是太复杂

(4)数据库的功能面对很多领域是不够用的(事务,还有各种联机事务型的操作);

特殊的功能,比如全文检索,同义词处理,相关度排名,复杂数据分析,海量数据的近实时处理,Elasticsearch 作为传统数据库的一个补充,提供了数据库所不能提供的很多功能。

安装教程

最官方的安装指南当属官网了,到 https://www.elastic.co/cn/downloads/elasticsearch 直接下载即可。

这里提供了各种系统的下载安装包,目前最新版本是 7.x。

下载完成之后,可以直接参考官网的教程来启动:

其实基本内容就是两步:

  • 解压下载的 zip 压缩文件
  • 直接运行 bin 目录下的 elasticsearch 脚本即可启动

启动之后,Elasticsearch 就会在 9200 端口上运行,这时候我们通过浏览器打开就会看到类似如下的输出:

这就证明 Elasticsearch 安装成功了。

另外 Elasticsearch 还有一个配套的可视化管理工具,叫做 Kibana,安装教程可以参考 https://www.elastic.co/cn/downloads/kibana,安装方式同 Elasticsearch。