0%

技术杂谈

问题

最近开发过程中遇到了一个小知识点,这里有一张 Base64 编码的图片,我想要把它复制到系统的粘贴板中,这个该怎么解决?

比如这里有一张图:

404

我可以通过 Base64 转换工具转换为 Base64 编码,转换网址为 https://www.base64-image.de/,转换结果如下:

图片就是类似 data:image/png;base64,ivBor2... 这样的编码。

问:现在有一张这样格式的图片,怎么把它复制到系统粘贴板中?

解决方案

首先这里需要用到一个 JavaScript 库,叫做 clipboard-polyfill,这个库的 npm 包地址为: https://www.npmjs.com/package/clipboard-polyfill,

看库的介绍有个关键的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import * as clipboard from "clipboard-polyfill";

async function handler() {
console.log("Previous clipboard contents:", await clipboard.read());

const item = new clipboard.ClipboardItem({
"text/html": new Blob(
["<i>Markup</i> <b>text</b>. Paste me into a rich text editor."],
{ type: "text/html" }
),
"text/plain": new Blob(
["Fallback markup text. Paste me into a rich text editor."],
{ type: "text/plain" }
),
});
await clipboard.write([item]);
}

window.addEventListener("DOMContentLoaded", function () {
const button = document.createElement("button");
button.textContent = "Copy";
button.addEventListener("click", handler);
document.body.appendChild(button);
});

这里可以创建一个 ClipboardItem 对象,并传入对应的 Blob 内容,同时指定对应的 content-type 即可。

OK,那这里就需要一个关键部分,那就是如何把 Base64 编码的图片转化为 Blob。

一般来说,Base64 编码的图片的开头是 data:image/jpg;base64,,后面跟的就是 Base64 真实编码。

这里提供一个方法,可以将 Base64 编码转化为 Blob,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function b64toBlob(b64Data, contentType = null, sliceSize = null) {
contentType = contentType || 'image/png'
sliceSize = sliceSize || 512
let byteCharacters = atob(b64Data)
let byteArrays = []
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
let slice = byteCharacters.slice(offset, offset + sliceSize)
let byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i)
}
var byteArray = new Uint8Array(byteNumbers)
byteArrays.push(byteArray)
}
return new Blob(byteArrays, {type: contentType})
}

这里其实就是将 Base64 编码先转化为了 Uint8Array,然后再将其转化为 Blob,并指定 contentType 即可。

接下来复制到粘贴板就很简单了,调用最上面的方法声明 ClipboardItem 对象即可:

1
2
3
4
5
function clip(b64Data) {
const item = new clipboard.ClipboardItem({
"image/png": this.b64toBlob(b64Data.replace('data:image/jpg;base64,', ''), 'image/png', 512)
});
}

这里 contentType 指定为了 image/png

至此,clip 方法传入 Base64 编码的图片即可复制到系统粘贴板了,亲测可以在 Windows、Mac 上生效。

技术杂谈

最近服务器都迁移到腾讯云 TKE 了,就是腾讯云 Kubernetes 服务,然后最近有个需求是获取客户端真实 IP。

我的情况:

  • 服务用 Django 写的,通过 HTTP 请求头获取真实 IP
  • Django 通过 uwsgi 运行,并通过 Nginx 转发出去
  • Ingress 使用的 Nginx Ingress,而不是 TKE Ingress

下面说下几个关键配置:

Django 中获取 IP

获取方式如下:

1
2
3
4
5
6
7
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip

uwsgi 配置

uwsgi.ini 配置如下:

1
2
3
4
5
6
7
8
[uwsgi]
module = core.wsgi
master = true
processes = 1
vacuum = true
static-map = /static=/app/app/static
http = 127.0.0.1:8000
log-master = true

Nginx 配置

Nginx 需要转发 uwsgi 出去,并且加上三个关键请求头:

1
2
3
4
5
6
7
8
9
10
11
server {
listen 7777;
server_name localhost;

location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://127.0.0.1:8000;
}
}

Supervisor 配置

Supervisor 需要启动 Uwsgi 和 Nginx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[supervisord]
nodaemon=true

[program:uwsgi]
command=uwsgi --ini /app/uwsgi.ini
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:nginx]
command=/usr/sbin/nginx -g "pid /run/nginx/nginx.pid; daemon off;"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

Dockerfile 配置

Dockkerfile 里面指定启动的命令为 supervisor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM python:3.7-alpine
ENV PYTHONUNBUFFERED 1
WORKDIR /app
COPY requirements.txt .
RUN apk update && apk add \
libuuid \
gcc \
nginx \
supervisor \
libc-dev \
linux-headers \
postgresql-libs \
postgresql-dev \
&& pip install pip -U \
&& pip install -r requirements.txt
COPY . /app
RUN mkdir -p /run/nginx
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY supervisord.conf /etc/supervisord.conf
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

TKE 配置

这里参考来源为:https://cloud.tencent.com/document/product/457/48949

最关键的就是:

这里我需要修改 Service 的负载均衡:

修改 externalTrafficPolicy 为 Local 即可:

大功告成,后面 IP 就能成功获取到了。

技术杂谈

之前配置 uwsgi 的时候,配完了之后 Python 的日志就输出不出来了,其实可以在 uwsgi.ini 里面加一个日志配置就行了,很简单:

1
log-master = true

示例配置如下:

1
2
3
4
5
6
7
8
[uwsgi]
module = core.wsgi
master = true
processes = 1
vacuum = true
static-map = /static=/app/app/static
http = 127.0.0.1:8000
log-master = true

加上这句之后,日志就可以顺利输出到控制台了。

说到加湿器,那就不得不先提一下空气净化器。

之前其实我家里是没有空气净化器的,最近刚搬新住处就入手了一个,因为我想打造一套全新的小米之家,哈哈哈。

这个空气净化器用和没用给我的感觉区别不大,因为我确实感觉不到室内空气好一点或者坏一点有啥区别。但是记得在我刚打开空气净化器的一瞬间,上面的空气质量指数显示为 180 多(实际上这个是说的 PM 2.5 的指数),重度污染!我直接好家伙,难道我没用空气净化器的二十多年一直都呼吸的是重度污染的空气吗?心中不由倒吸一口凉气。净化器开了一会,那个质量指数慢慢降低,最后一直降到 10 以下,我心里也慢慢舒服多了,这时候我就感觉自己处在一个洁净的环境里面。所以,虽然我自己并不能感受出来空气变好了,但是心里是舒服的、安心的。

那为什么又扯到加湿器呢?

这是因为当我在开着空气净化器的时候,和往常一样打开我的加湿器…这时候,空气净化器显示的空气质量指数直接爆表了,直接 200 以上,当场就把我吓坏了。我当时就跟我老婆说,老婆我们这个加湿器有问题啊,它加湿后的水雾怎么控制质量这么差,肯定对身体有害。说完了之后我就去搜新的加湿器去了。因为最近我在搞米家智能家居,所以第一个搜的就是小米的加湿器,搜到这么一款:

OK,除菌加湿器,很好,有了这个除菌功能,加湿的空气应该不会那么差了。当时我那款「美的」的加湿器是没有这个功能的,所以我就又跟老婆说,老婆我搜到这个小米的加湿器是有除菌功能的,我买了这个肯定就没事了,于是我成功说服了我老婆,于是我的米家智能家居又新增了一员。

然而,等到货之后我灌上水重新打开,情况居然跟我想的不一样!空气净化器依然非常高,直逼 200,这是怎么回事?难道这个除菌功能一点用都没有?

抱着好奇又震惊的心情,我就去网上查,原来,这个和水有关系。空气指数上升的原因可以简单这么解释:这么因为我用的是自来水,而自来水含有氯、重金属等杂质,我买的加湿器又属于超声波式加湿器,它的原理是利用超声波将水打散成直径只有 1-5 微米的细小颗粒,然后利用风动装置将这些小颗粒吹到空气中,所以自来水中的杂质也就跟着进入空气中了,这就导致了空气净化器指数急剧上升,污染空气,长期这样下去对人体并不好。

看到这里,那就牵扯到了几个概念,一个是水,一个是加湿器,这两者的不同组合可能产生不同的结果,下面且听我一一道来。

首先说水啊,其实我们平常见到的水有很多种,名字也不同,目前常见的水可以分为这么几种类别:自来水、白开水、纯净水、矿泉水、天然水、蒸馏水,咋这么多?听着都懵啊,下面来解释下。

自来水

这个大家都知道了,水龙头里面直接放出来的水就是自来水,它是经过自来水厂加工、消毒之后生产出来的复合国家饮用水标准的工人们生产生活用的水,消毒的时候,里面含有一些微生物(如大肠杆菌)、重金属、有机污染物、泥沙等物质,另外由于自来水厂喜欢用氯气消毒,所以自来水里面还含有一些氯离子,总之这些都是对人体健康不利的。看到这里不要害怕,那就不能喝了吗?没关系,把水烧开了就行了。

白开水

白开水就是把自然水直接烧开得到的水,因为刚才也说了,自来水里面含有各种对人体有害的物质,把水烧开后,大部分细菌病毒都会被杀死,一些氯也会被分解掉,另外钙、镁和一些重金属离子也会在烧开的过程中形成沉淀,也就是我们通常说的水垢。不过金属离子是不能完全沉淀的,所以水中还会有溶解有少量的重金属离子,所以并不是完完全全是干净的,但基本问题不大。另外白开水其实口感非常不好,喝起来很涩,反正我是完全喝不惯白开水的味道。

矿泉水

大家肯定听说过矿泉水,但你平时喝的还真不一定是矿泉水,目前比较常见的矿泉水有百岁山、恒大冰泉等,它是是天然含有矿物质离子或盐或硫化物或二氧化碳气体等物质的水,矿泉水对水源地要求很高,不同水源地的矿物质含量也不同,比如百岁山的河寨山水源说是富含了偏硅酸锂、锶、钙、钾等多种有益于人体健康的微量元素,是为数不多的优质水源,当然依云也是。

纯净水

纯净水就是水里不含任何其他物质,只有水分子的水,没有任何矿物质。它是经过一些离子交换、反渗透、蒸馏等工艺制成的,过滤掉了细菌、病毒等有害物质,而且几乎不含任何矿物质,水的硬度也很低,用它烧水也不会形成水垢。纯净水可以有两种获取方式,一种就是家里装净水器,出来的都是纯净水。另外就是直接购买,比如怡宝就是纯净水。

天然水

天然水,最常见的就是农夫山泉,它的标识叫做饮用天然水,为啥他不标识天然矿泉水呢?它其实和矿泉水几乎差不多,但是其中有益的元素含量并不能达到天然矿泉水的要求。百岁山的水源相对农夫山泉更好一些,农夫山泉的水源有好几个基地,一般来说千岛湖的最好喝,带有一点甘甜的味道。不过不仔细对比,这个和矿泉水大查不差,反正都含有一些钙、镁、钾等矿物质。

蒸馏水

蒸馏水相比大家在学习化学等时候听说过,其实就是把水烧开,把水蒸气收集下来,然后再把水蒸气液化,分离出来的水就是纯水了,和纯净水一样也是不含矿物质的。由于工艺复杂,制造设备昂贵,所以蒸馏水一般在实验室比较常用,家庭基本上没有喝的,当然要买也能买到,比如屈臣氏的瓶装蒸馏水,价格不菲。

好,说完了以上的水,那哪种水适合放在加湿器(目前指的是超声波加湿器)里面不会引起空气指数升高呢?答案是纯净水和蒸馏水,其他的都不行。纯净水和蒸馏水加上之后,我的空气净化器指数基本没有变化,保持超绿色状态,舒服了。

另外平时喝的话,矿泉水、纯净水、天然水、白开水其实都行,虽然说矿泉水相比纯净水多了一些对人体有益的矿物质,但人体矿物质的摄入也不一定是靠水嘛,所以基本没有什么影响。

好,说完了水,那就得再说说加湿器了,这个其实也是有讲究的。

加湿器

市面上的加湿器主要分为两种,一种是超声波式加湿器,一种是冷蒸发式加湿器。

超声波式加湿器

大部分人家里的加湿器可能都是一二三百块钱的吧,加湿的过程中你还会看到白雾对不对?没错,这种加湿器就是超声波式加湿器,这种加湿器刚才也说了,利用高效的超声波振动将水打散成直径只有1-5微米的细小颗粒,再利用风动装置,将这些小颗粒吹到空气中,这种加湿器具有加湿速度快耗电量小、使用寿命长的优点,而且价格相对比较便宜,但存在不足之处在于对水要求高。

冷蒸发式加湿器

这种加湿器和超声波最大的区别就是加湿过程看不见水雾,它的原理就是通过循环风将液态水变成气态水,其实这种加湿器的加湿效果和超声波式相比并不占优势,不过加湿过程中,水是真正变成水蒸气而进入空气中的,所以水中的一些物质并不会进入到空气中,所以这种加湿器我们可以放心使用自来水,而不会引起空气净化器指数上升。不过这种加湿器价格都挺贵的,比超声波式加湿器贵了两三倍不止。

所以呢,到这里,基本就通透了吧,我用的加湿器就是第一种,这种加湿器就必须得用纯净水或者蒸馏水才能保证空气净化器不会爆表。如果加湿器是第二种,那随便用什么水都没啥关系了。

另外友情提醒,如果你的加湿器是第一种,请不要再用自来水了,对身体健康不利,还是改用纯净水吧,或者直接换个蒸发式加湿器也可以的。

所以说,这一番下来,学到还真不少,水和加湿器原来还有这么多讲究,生活真是处处皆学问啊。

参考

想知道农夫山泉 景田百岁山和怡宝味道的差别? - 徐野的回答 - 知乎 https://www.zhihu.com/question/29367546/answer/109448432

白开水、纯净水、矿泉水和蒸馏水哪个最适合做长期饮用水? - 安吉尔的回答 - 知乎 https://www.zhihu.com/question/20418550/answer/1271349511

你的加湿器到底该加什么水?今天让你秒懂!http://jd.zol.com.cn/734/7346674.html

怎样选购加湿器? - 老爸评测的回答 - 知乎 https://www.zhihu.com/question/22406803/answer/555850897

TypeScript

上一节学习了 TypeScript 的基本类型,本节再来学习下接口 Interfaces 的使用。

TypeScript 的一个重要的特性就是帮助检查参数的字段是否是合法的,比如检查某个参数包含字段 foo,且类型需要是 number 类型,否则就会报错。通过接口,即 Interface 我们可以方便地实现这个操作。

第一个 Interface

最简单的实现方式参考下面的例子:

1
2
3
4
5
6
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

在这里我们声明了一个方法,叫做 printLabel,它接收一个参数叫做 labeledObj,在 labeledObj 后面声明了该参数需要的字段和类型,这里它需要一个字段 label,且类型必须要是 string 类型。在调用时,我们传入了一个 Object,它包含了两个字段,一个是 size,类型为 number,另一个是 label,类型为 string。这里值得注意的是传入的参数是比声明的参数多了一个字段 size 的,但是这并没有关系,编译器只检查传入的参数是否至少存在所需要的属性,对于多余的属性是不关心的。

运行结果如下:

1
[LOG]: "Size 10 Object"

如果此时我们将 label 属性的类型修改为 number:

1
2
3
function printLabel(labeledObj: { label: number }) {
console.log(labeledObj.label);
}

则会出现如下报错:

1
Argument of type '{ size: number; label: string; }' is not assignable to parameter of type '{ label: number; }'. Types of property 'label' are incompatible. Type 'string' is not assignable to type 'number'.

这里就提示 label 属性只能传入 number 类型,而不能是 string 类型。

但上面这个写法其实很不友好,如果属性比较多,那这个声明会非常复杂,而且不同方法如果都用到这个参数,难道还把它的声明都重复声明一遍?这也太不好了吧。

所以,为了更方便地实现声明,这里我们可以使用 Interface 来实现,上面的例子就可以改写为如下形式:

1
2
3
4
5
6
7
8
9
10
interface LabeledValue {
label: string;
}

function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

这里我们使用 interface 声明了一个类型声明,这样在 printLabel 就可以直接使用 Interface 的名称了。

怎么样?这种写法是不是感觉好多了。

Optional properties

某些情况下,某些字段并不是完全必要的,比如看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
interface LabeledValue {
label: string;
count: number,
message: string,
}

function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}

let myObj = { size: 10, count: 1, label: "Size 10 Object"};
printLabel(myObj);

其中 message 字段其实在 myObj 对象里面没有,而且这个字段也并不是必需的,但是该字段如果存在的话,必须是 string 类型。那像上面的写法,其实就会报错了:

1
Argument of type '{ size: number; count: number; label: string; }' is not assignable to parameter of type 'LabeledValue'. Property 'message' is missing in type '{ size: number; count: number; label: string; }' but required in type 'LabeledValue'.

这里说 message 字段没有传。

这时候我们可以将 message 标识为可选字段,只需要在字段后面加个 ? 就好了,写法如下:

1
2
3
4
5
interface LabeledValue {
label: string;
count: number,
message?: string,
}

这样就不会再报错了。

Readonly properties

在某些情况下,我们期望一个 Object 的某些字段不能后续被修改,只能在创建的时候声明,这个怎么做到呢?很简单,将其设置为只读字段就好了,示例如下:

1
2
3
4
5
6
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

这里我们声明了一个名为 Point 的 Interface,然后在创建 Point 的时候将 x 设置为 10。但后续如果我们想设置 x 的属性为 5 的时候,就会报错了:

1
Cannot assign to 'x' because it is a read-only property.

这样就可以保证某些字段不能在后续操作流程中被修改,保证了安全性。

另外我们可以使用 ReadonlyArray 来声明不可变的 Array,一旦初始化完成之后,后续所有关于 Array 的操作都会报错,示例如下:

1
2
3
4
5
6
7
8
9
10
11
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

ro[0] = 12; // error!
Index signature in type 'readonly number[]' only permits reading.
ro.push(5); // error!
Property 'push' does not exist on type 'readonly number[]'.
ro.length = 100; // error!
Cannot assign to 'length' because it is a read-only property.
a = ro; // error!
The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.

另外到这里大家可能疑惑 readonly 和 const 是什么区别,二者不都代表不可修改吗?其实区分很简单,readonly 是用来修改 Object 的某个属性的,而 const 是用来修饰某个变量的。

Function Types

除了用 interface 声明 Object 的字段,我们还可以声明方法的一些规范,示例如下:

1
2
3
interface SearchFunc {
(source: string, subString: string): boolean;
}

这里就是用 interface 声明了一个 Function,前半部分是接收的参数类型,后面 boolean 是返回值类型。

声明 interface 之后,我们便可以声明一个 Function 了,写法如下:

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc = (source: string, subString: string) => {
let result = source.search(subString);
return result > -1;
};

这里声明了一个 Function 叫做 mySearch,其中其参数和返回值严格按照 SearchFunc 这个 Interface 来实现,那就没问题。

如果我们将返回值改掉,改成非 boolean 类型,示例如下:

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc = (source: string, subString: string) => {
let result = source.search(subString);
return result;
};

这时候就会得到如下报错:

1
Type '(source: string, subString: string) => number' is not assignable to type 'SearchFunc'. Type 'number' is not assignable to type 'boolean'.

这里就说返回值是 number,而不是 boolean。

Class Types

除了声明 Function,interface 还可以用来声明 Class,主要作用就是声明 class 里面所必须的属性和方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}

class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}

这个简直跟其他语言的接口定义太像了。定义好了 ClockInterface 之后,class 需要使用 implements 来实现这个接口,同时必须要声明 currentTime 这个变量和 setTime 方法,类型也需要完全一致,不然就会报错。

Extending Interfaces

另外 Interface 之间也是可以继承的,相当于在一个 Interface 上进行扩展,示例如下:

1
2
3
4
5
6
7
8
9
10
11
interface Shape {
color: string;
}

interface Square extends Shape {
sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;

这里 Shape 这个 Interface 只有 color 这个属性,而 Square 则继承了 Shape,并且加了 sideLength 属性,那其实现在 Square 这个接口声明了 color 和 sideLength 这两个属性。

另外 Interface 还支持多继承,获取被继承的 Interface 的所有声明,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Shape {
color: string;
}

interface PenStroke {
penWidth: number;
}

interface Square extends Shape, PenStroke {
sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

但如果同名的字段不一致怎么办呢?比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Fruit {
color: string
}

interface Apple extends Fruit {
hasLeaf: boolean
}

interface Orange extends Fruit {
size: number,
hasLeaf: number
}

interface Watermalon extends Apple, Orange {

}

这里 hasLeaf 在 Apple 里面是 boolean 类型,在 Orange 里面是 number 类型,最后 Watermalon 继承了这两个 Interface 会怎样呢?

很明显,报错了,结果如下:

1
Interface 'Watermalon' cannot simultaneously extend types 'Apple' and 'Orange'. Named property 'hasLeaf' of types 'Apple' and 'Orange' are not identical.

意思就是说字段类型不一致。

所以,要多继承的话,需要被继承的 Interface 里面的属性不互相冲突,不然是无法同时继承的。

Interfaces Extending Classes

在某些情况下,Interface 可能需要继承 Class,Interface 扩展 Class 时,它将继承该 Class 的成员,但不继承其实现。这就类似该 Interface 声明了该类的所有成员而没有提供实现。

另外 Interface 甚至可以继承 Class 的私有成员和受保护成员。这意味着,当创建一个扩展带有私有或受保护成员的 Class 的 Interface 时,该 Interface 只能由该 Class 或其子 Class 实现。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Control {
private state: any;
}

interface SelectableControl extends Control {
select(): void;
}

class Button extends Control implements SelectableControl {
select() {}
}

class TextBox extends Control {
select() {}
}

class ImageControl implements SelectableControl {
// Error, Class 'ImageControl' incorrectly implements interface 'SelectableControl'.
// Types have separate declarations of a private property 'state'.
private state: any;
select() {}
}

上面我们可以知道,当创建一个扩展带有私有或受保护成员的 Class 的 Interface 时,该 Interface 只能由该 Class 或其子 Class 实现。在这里 ImageControl 由于没有继承 Control,但同时 Control 还包含了私有成员变量,所以 ImageControl 并不能继承得到 state 这个私有成员变量,所以会报错。

以上便是关于 Interface 的一些用法,后面会继续总结其他的用法,如 Functions、Classes 等详细用法。

TypeScript

之前的时候都是用 JavaScript 开发,但是最近发现随着项目的推进,一些项目越来越多地要求使用 TypeScript 来开发,所以不得不来专门学习一下 TypeScript 了,本篇文章就简单记录下学习 TypeScript 的过程。

介绍

先看看 TypeScript 的官方介绍:

Typed JavaScript at Any Scale.

TypeScript extends JavaScript by adding types. By understanding JavaScript, TypeScript saves you time catching errors and providing fixes before you run code. Any browser, any OS, anywhere JavaScript runs. Entirely Open Source.

我们知道 JavaScript 是弱类型的语言,而实际上说白了就是加上类型标识的 JavaScript 语言,加上类型标识之后,可以帮助我们减少开发的调试成本,尽早发现错误,节省更多时间。同时 TypeScript 是完全开源的,它编译后得到的 JavaScript 支持任何浏览器和操作系统运行。

截止本文撰写的时间,TypeScript 已经更新到 4.1 版本,官方文档地址:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html。

相关资料

学习 TypeScript 当然要看原汁原味的资料,推荐 TypeScript 官方的 Handbook,链接为:https://www.typescriptlang.org/docs/handbook/intro.html。

本文所总结的内容基于 Handbook 的内容加上个人的一些修改而成。

另外本节的代码推荐大家直接在 TypeScript 提供的 Playgroud 里面运行即可,不用自己再初始化 Node 和 TypeScript 环境,简单方便,链接为:https://www.typescriptlang.org/play,大家直接打开就行了,预览如下:

image-20201228234110415

比如默认情况下就有两行初始化代码:

1
2
const anExampleVariable = "Hello World";
console.log(anExampleVariable);

点击 Run 之后就可以直接运行,控制台输出结果显示在右侧,内容如下:

1
[LOG]: "Hello World"

如果你运行成功了,那我们就可以开始接下来的学习了。

下面正式开始介绍。

TypeScript 主要就是在 JavaScript 基础上扩展了一些类型,所以这里就分各种类型来进行介绍。

基本类型

Boolean

最常见的基本类型就是布尔类型了,其值就是 true 或者 false,类型声明用 boolean 就好了,在 TypeScript 中,声明一个 boolean 类型的变量写法如下:

1
2
let isDone: boolean = false;
console.log(isDone, typeof isDone);

这里注意到,声明类型可以在变量名的后面加上一个冒号,然后跟一个类型声明,和 Python 的 Type Hint 非常像。然后我们用 console 的 log 方法输出了这个变量,并用 typeof 输出了它的类型。

运行结果如下:

1
[LOG]: false,  "boolean"

可以看到运行结果就被输出出来了,同时 typeof 就是这个变量的类型,结果是一个字符串,就是 boolean。

是不是很简单?基本套路就是在变量的后面跟一个冒号再跟一个类型声明就好了。

Number

Number 即数值类型,对于这个类型,TypeScript 和 JavaScript 是一样的,Number 可以代表整数、浮点数、大整数。其中大整数需要单独用 bigint 来表示。另外 Number 还可以代表十六进制、八进制、二进制。

示例如下:

1
2
3
4
5
6
7
8
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
let big: bigint = 100n;

console.log(hex, typeof hex);
console.log(big, typeof big);

其中 bigint 需要 ES2020 的支持,需要在 TS Config 里面设置下:

image-20201228235911831

运行结果如下:

1
2
[LOG]: 61453,  "number" 
[LOG]: 100, "bigint"

这里输出了 hex 和 big 变量的值和类型,其中 hex 本身是用十六进制声明的,打印输出的时候被转化为了十进制,同时其类型为 number。

另外对于 bigint 类型来说,值的后面需要加一个 n,即 100 和 100n 代表的类型是不一样的,后者的类型是 bigint,前者的类型是 number。

假如把 bigint 类型的变量声明为 number 类型是会报错的,比如这样就是错误的:

1
let big: number = 100n;

报错内容如下:

1
Type 'bigint' is not assignable to type 'number'.

String

String 即字符串类型,需要用 string 来声明类型。字符串可以用单引号或者双引号或者斜引号声明,其中斜引号就是模板字符串。

示例如下:

1
2
3
4
5
6
let color: string = "blue";
color = 'red';
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${fullName}. I'll be ${age + 1} years old next month.`;
console.log(sentence, typeof sentence);

运行结果如下:

1
[LOG]: "Hello, my name is Bob Bobbington. I'll be 38 years old next month.",  "string"

嗯,没什么特殊的,就是字符串类型。

Array

Array 即数组,声明可以有两种方式,一种是 type[] 这样的形式,一种是 Generics 泛型的形式,示例如下:

1
2
3
4
5
6
let list: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
let list3: Array<any> = [1, "2", 3];
console.log(list, typeof list);
console.log(list2, typeof list2);
console.log(list3, typeof list3);

其中 list 就是使用了 type[] 这样的声明方式,声明为 number[],那么数组里面的每个元素都必须要是 number 类型,list2 则是使用了泛型类型的声明,和 list 的效果是一样的,另外 list3 使用了 any 泛型类型,所以其值可以不仅仅为 number,因此这里 list3 的第二个元素设置为了字符串 2。

运行结果如下:

1
2
3
[LOG]: [1, 2, 3],  "object" 
[LOG]: [1, 2, 3], "object"
[LOG]: [1, "2", 3], "object"

结果的 typeof 输出的结果可能让人意外,不应该是输出其声明的类型吗,怎么是 object?这是因为浏览器真正执行的是刚才的 TypeScript 编译生成的 JavaScript,而 JavaScript 本身的 Array 和 Object 等类型,typeof 都统一返回 object 类型,因此得到的结果就是 object 了。

Tuple

Tuple 即元组,它可以允许我们声明固定数量和顺序的 Array,来看个例子就懂了:

1
2
3
let x: [string, number];
x = [1, 2]; // Error
x = ["Hello", 2]; // Correct

比如这里声明了一个变量 x,x 必须是一个 Array,按照顺序来说,其第一个元素必须要是 string,第二个元素必须要是 number,所以第二行的声明就会报错,第三行的声明才是正确的。

有了类型声明之后,在编译阶段就能发现错误或者编辑器给出方法输入提示。比如:

1
2
console.log(x[0].substring(1)); // Correct
console.log(x[1].substring(1)); // Error, Property 'substring' does not exist on type 'number'.

比如这里第一行我们在敲 substring 的时候,编辑器根据类型判断出 x[0] 是 string 类型,那就可以给出 substring 的提示。而对于第二行代码的 x[1],编译器会直接检查出其中存在错误,这有助于我们在静态类型检查时候及时发现问题,减少 Bug 出现的概率。

Enum

Enum 即枚举类型,这个非常有用,有时候我们想定义的变量其实就只有某几种取值,那完全可以定义为枚举类型。

用法如下:

1
2
3
4
5
6
7
8
9
10
enum Stage {
Debug,
Info,
Warning,
Error,
Critical,
}

let stage: Stage = Stage.Critical;
console.log(stage, typeof stage);

这里我们声明了一个枚举类型,其名称叫做 Stage,值有五个。然后声明的变量直接用 Stage 修饰即可,值则可以直接取 Stage 的某个值,这里取值为 Stage.Critical

最后打印输出该变量,结果如下:

1
[LOG]: 4,  "number"

结果居然是 4,这个怎么情况?

原来是因为枚举类型,它会按照枚举值的声明顺序自动编号,比如 Debug 的值就是 0,Info 就是 1,以此类推。默认情况下是从 0 开始编号的,不过我们也可以手动更改编号的起始值,比如 Debug 从 1 开始编号,可以声明如下:

1
2
3
4
5
6
7
8
9
10
enum Stage {
Debug = 1,
Info,
Warning,
Error,
Critical,
}

let stage: Stage = Stage.Critical;
console.log(stage, typeof stage);

最后可以看到 Critical 的编号就变成 5 了,运行结果如下:

1
[LOG]: 5,  "number"

有点意思。

那既然它是按照顺序来赋值的,那我如果在前面的值设置大一点,会不会出现后面的值和前面的值相等的情况?比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Stage {
Debug = 1,
Info = 3,
Warning = 2,
Error,
Critical,
}

let stage: Stage = Stage.Error;
let stage2: Stage = Stage.Info;
console.log(stage, typeof stage);
console.log(stage2, typeof stage2);
console.log(stage == stage2);

看看这样会发生什么?这里 Error 按照常理来说会从 Warning 开始自增,值应该为 3,那 Info 我也设置为 3,二者会是相等吗?

运行结果如下:

1
2
3
[LOG]: 3,  "number" 
[LOG]: 3, "number"
[LOG]: true

果不其然,二者还都是 3,而且它们就是相等的。所以,对于枚举类型,我们一定要注意声明值的时候最好不要引起自增值和设定值之间的冲突,不然会引入不必要的麻烦。

另外对于枚举类型,我们还可以根据值进行查询,比如对于上述声明,我们不知道哪个值等于 2 或者 3,那可以直接将值传给 Stage 进行查询,得到的结果是一个字符串,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Stage {
Debug = 1,
Info = 3,
Warning = 2,
Error,
Critical,
}

let stage: string = Stage[3];
console.log(stage, typeof stage);

let stage2: string = Stage[2];
console.log(stage2, typeof stage2);

运行结果如下:

1
2
[LOG]: "Error",  "string" 
[LOG]: "Warning", "string"

比如这里 Stage[2] 就是查找枚举值等于 2 的枚举名称,结果是 Warning。那值相同的咋办呢?比如 3,看结果它返回的是 Error,看来 Error 把 Info 覆盖掉了,查询的结果是最新的一个枚举名称。注意这里返回的结果是字符串类型,不是枚举类型,它仅仅代表枚举的名称而已。

Unknown

Unknown 即未知类型,一般在类型不确定的情况下可以声明为 unknown 类型。比如说一个数据可能是 API 返回的,它可能是数值类型也可能是字符串类型,并不知道,这时候我们就可以将其声明为 unknown 类型。

示例如下:

1
2
3
4
5
let notSure: unknown = 4;
notSure = "maybe a string instead";

// OK, definitely a boolean
notSure = false;

比如这里 notSure 就声明了 unknown 类型,一开始它是数值类型,但当然也可以是字符串或者布尔类型。

那这样不就没啥用了吗?和 JavaScript 有啥不同吗?其实 unknown 还有其他用处,比如说编译器可以在一些判定条件下对 unknown 的值进行精确化处理,比如说一个 if 条件,判定了类型为布尔类型,那么其他和该类型相关的变量都会执行静态类型检查。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
declare const maybe: unknown;
// 'maybe' could be a string, object, boolean, undefined, or other types
const aNumber: number = maybe;
// Type 'unknown' is not assignable to type 'number'.

if (maybe === true) {
// TypeScript knows that maybe is a boolean now
const aBoolean: boolean = maybe;
// So, it cannot be a string
const aString: string = maybe;
// Type 'boolean' is not assignable to type 'string'.
}

if (typeof maybe === "string") {
// TypeScript knows that maybe is a string
const aString: string = maybe;
// So, it cannot be a boolean
const aBoolean: boolean = maybe;
// Type 'string' is not assignable to type 'boolean'.
}

这里使用 declare 声明了一个类型变量,然后通过类型变量里面的判定条件就能配合检查其他变量的类型了。

Any

和 Unknown 的情形类似,我们还可以使用 any 来代表任意的类型,示例如下:

1
2
3
declare function getValue(key: string): any;
// OK, return value of 'getValue' is not checked
const str: string = getValue("myString");

这里声明了一个方法,返回类型就是 any,这样的话返回类型就可以是任意的结果了。

那 any 和 unknown 有什么不同呢?any 的自由度会更高一点,如果声明为 any,那么静态类型检查都会通过,即使某个变量的属性不存在。示例如下:

1
2
3
4
5
6
7
8
9
let looselyTyped: any = 4;
// OK, ifItExists might exist at runtime
looselyTyped.ifItExists();
// OK, toFixed exists (but the compiler doesn't check)
looselyTyped.toFixed();

let strictlyTyped: unknown = 4;
strictlyTyped.toFixed();
// Error, Object is of type 'unknown'.

可以看到,前两行的静态类型检查是能过的,但是 unknown 就过不了。

所以,什么时候用 unknown 什么时候用 any 呢?可以从含义上来进行区分:如果某个变量我确实就不知道它的类型,即我没办法知道它的类型,那就用 unknown;如果某个值但其实我确实有办法知道可能的类型,但确实它的类型自由度比较高,那就可以被声明为 any。

但在不必要的情况下,尽量减少 any 类型的使用。

Void

Void 一般用于声明方法的返回值,如果一个方法不返回任何结果,那就用 Void,示例如下:

1
2
3
function warnUser(): void {
console.log("This is my warning message");
}

Null 和 Undefined

其实 TypeScript 还专门为 null 和 undefined 声明了类型,一般 null 就声明为 null 类型,undefined 就声明为 undefined 类型,不过这两个并不太常用,示例如下:

1
2
3
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

另外对于其他类型来说,其值也可以是 null 或者 undefined,比如 number 类型的变量,其值为 null 也是完全可以的。不过这里有个前提条件就是,TS Config 里面的 strictNullChecks 应该是关闭状态,不然会报错的。

设置如下:

image-20201229014015393

比如:

1
2
let a: number = undefined;
let b: string = null;

这样就是没问题的了。

Never

这个类型也比较特殊,它代表你永远得不到它的结果。比如一个方法始终抛出一个异常,或者处于一个无限循环中,那么就可以声明为 never,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Function returning never must not have a reachable end point
function error(message: string): never {
throw new Error(message);
}

// Inferred return type is never
function fail() {
return error("Something failed");
}

// Function returning never must not have a reachable end point
function infiniteLoop(): never {
while (true) {}
}

Object

Object 是 JavaScript 本身带有的类型,它表示非原始型的类型,即任何不是 number、string、boolean、bigint、symbol、null、undefined 的类型。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
declare function create(o: object | null): void;

// OK
create({ prop: 0 });
create(null);
create(42);
// Error, Argument of type '42' is not assignable to parameter of type 'object | null'.
create("string");
// Error, Argument of type '"string"' is not assignable to parameter of type 'object | null'.
create(false);
// Error, Argument of type 'false' is not assignable to parameter of type 'object | null'.
create(undefined);
// Error, Argument of type 'undefined' is not assignable to parameter of type 'object | null'.

不过,一般不用。

Type assertions

一般情况下,我们确确实实知道某个变量属于什么类型,或者在某种情况下确实需要这种类型的转化,则可以显式的声明某个类型,示例如下:

1
2
3
4
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;
let someValue2: unknown = "this is a string";
let strLength2: number = (<string>someValue2).length;

这里有两种使用方式,一种是 as,一种是尖括号声明。

注意

另外值得注意到是,以上的一些类型声明,使用大写形式 Number, String, Boolean, Symbol and Object 也是可以的,不过不推荐这么做,推荐还是用小写的形式。

比如这样其实是可以的:

1
2
3
4
5
function reverse(s: String): String {
return s.split("").reverse().join("");
}

reverse("hello world");

但更推荐使用小写,写成如下形式:

1
2
3
4
5
function reverse(s: string): string {
return s.split("").reverse().join("");
}

reverse("hello world");

总结

以上便是一些基本的类型声明方式,暂时先总结这么多,后面还会继续整理更高级的用法,比如 Function、Interface、Class 等。

技术杂谈

最近的服务都放到腾讯云上了,但是最近遇到了一个问题是 Docker 云磁盘满了,部署的适合提示 no space left 等等。

然后我就登录云主机看了下磁盘情况,好家伙:

1
2
3
4
5
6
7
8
9
ubuntu@VM-0-2-ubuntu:/var/lib/docker/containers$ df
Filesystem 1K-blocks Used Available Use% Mounted on
udev 3869584 0 3869584 0% /dev
tmpfs 777376 15124 762252 2% /run
/dev/vda1 103145380 9460508 89390616 10% /
tmpfs 3886864 24 3886840 1% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
tmpfs 3886864 0 3886864 0% /sys/fs/cgroup
/dev/vdb 10255636 10155252 - 99% /var/lib/docker

vdb 这个磁盘就是我的数据盘,里面放了容器数据,现在已经到了 99% 了,一共就 10G。

然后我就去腾讯云控制台搞了下数据盘的扩容,然后直接扩容到了 200G。

但是回过头来看了下磁盘还是 10G,一变不变。

但 fdisk 已经看到变了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@VM-0-2-ubuntu:/var/lib/docker/containers$ sudo fdisk -l
Disk /dev/vda: 100 GiB, 107374182400 bytes, 209715200 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x3fa1d255

Device Boot Start End Sectors Size Id Type
/dev/vda1 * 2048 209715166 209713119 100G 83 Linux


Disk /dev/vdb: 200 GiB, 214748364800 bytes, 419430400 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

这是因为之前的数据被格式化为了 ext4,但是新扩容的部分并没有。

问了腾讯云的工程师,这时候必须要重新格式化才能用上新的容量,没有好的解决方法。

最后的解决方案:

  • 将当前节点所有 Pod 驱逐到其他的节点。
  • 当前节点退出集群然后配置磁盘,重新操作 mount 和格式化。(但是这里劝别折腾了,终究还是要格式化的。
  • 把节点再加回来,这时候可以选重新格式化数据盘,mount 到 /var/lib/docker。
  • 再把之前的 Pod 收回到本节点就好了。

完毕。

感觉还是不完善啊,像 Azure Kubernetes 的扩容就方便多了,不用整这么多麻烦事。

技术杂谈

就昨天的时候,Python 之父 Guido 发了个推特,说自己觉得退休太无聊了,然后于是乎决定加入微软。

同时推特上大神 Anders Hejlsberg(Delphi、C# 和 TypeScript 之父)转发了推特,表示希望可以一起工作。

要知道,去年那会去年 10 月 30 日,Python 之父 Guido 宣布退休,当时他发推文说,“这件事感觉既苦涩又甜蜜:苦涩的是,我马上要离开 Dropbox,现在已经退休;甜蜜的是,在 Dropbox 做工程师期间,我学到很多,比如类型标注就来自这段经历。我会非常怀念在这里工作的日子。”

看到这个消息我既震惊又激动,Python 之父加入我司了?那我岂不是和他也算做同事了?哈哈哈。

当时我看到这个消息之后,第一时间就去公司的系统里面查了下,果真就查到了,这里放个图。

他的 Title 叫 DISTINUISHED ENGINEER,这个 Title 可不是一般的牛逼,在微软,有这样的 Title 的人可是屈指可数的,DISTINUISHED 意思就是杰出的,非常牛逼的意思,能有这种 Title 的是为业界或公司做出过特别特别大贡献的,特别有影响力的,可能普通人在微软呆个二十多年都不一定能到这个地位。他的职级和 Report Line 就不说了,他距离微软 CEO 纳德拉只差 3 级,稍微形象点说就是对标阿里 P11 或者 P12 的位置吧。

他的部门就直接写了 Python and Tools for AI,和 AI 相关,同时 Guido 又这么热爱开源,微软也在一直拥抱开源,期待 Python 之父将来不久之后又出新的杰作吧。

要知道 Python 之父 Guido 已经 64 岁了,或许大佬就是大佬吧,退休了感觉无聊,就来微软来玩玩。怎么说呢?他这个地位就是类似达成一些自我追求和实现自己的一些价值,顺便解解闷,这种境界完全不是我们能比的了。反过来想想中国这个环境,多少人都是用代码来换取一份工作谋求生存,然后 35 岁干不动了可能就被辞退。本来想展开说说的,但是感觉说来都是痛,就不多说了,希望我将来不会为生活所迫,能够自由地继续追求自己热爱的东西。

另外本想说说 Guido 的履历的,但看到其他的一些文章已经整理得挺好了,我就不再重复了,大家想了解更多的话可以去看看 https://mp.weixin.qq.com/s/ZQJClbYiKP5cAnKjB_ZfUg。

总之,我就是来宣布这个和 Python 之父成为“同事”的开心的消息,值得纪念下,然后顺便期待下 Python 之父将来出新的杰作!

技术杂谈

本章将告诉你该如何去对request模块进行二次封装,暂时并不会告诉你HTTP协议及原理、URL等相关。当然你会使用然后在来阅读此文章一定会另有所获。我已经迫不及待要告诉你这个小秘密,以及想与你交流了。没时间解释了,快来一起和我一起探讨相关的内容吧

官方文档对requests的定义为:Requests 唯一的一个非转基因的 Python HTTP 库,人类可以安全享用。

使用Python写做爬虫的小伙伴一定使用过requests这个模块,初入爬虫的小伙伴也一定写过N个重复的requests,这是你的疑问。当然也一直伴随着我,最近在想对requests如何进行封装一下,让他支持支持通用的函数。若需要使用,直接调用即可。

那么问题来了,如果要写个供自己使用通用的请求函数将会有几个问题

  • requests的请求方式(GET\POST\INPUT等等)
  • 智能识别网站的编码,避免出现乱码
  • 支持文本、二进制(图片、视频等为二进制内容)
  • 以及还需要傻瓜一点,那就是网站的Ua(Ua:User-Agent,基本上网站都会验证接受到请求的Ua。来初步判断是爬虫还是用户)

那么咱们就针对以上问题开干吧

Requests的安装

在确保python环境搭建完成后直接使用pip或者conda命令进行安装,安装命令如下:

1
2
3
4
5
pip install requests
conda install requests

# 或者下载过慢点话,可以使用国内的pip镜像源,例如:
pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple/

安装完成后,效果图如下:

初探requests基本使用

HTTP 中最常见的请求之一就是 GET 请求,下面我们来详细了解利用 requests 库构建 GET 请求的方法。

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
import requests

response = requests.get('http://httpbin.org/get')

# 响应状态码
print("response.status_code:", response.status_code)
# 响应头
print("response.headers:", response.headers)
# 响应请求头
print("response.request.headers:", response.request.headers)
# 响应二进制内容
print("response.content:", response.content)
# 响应文本
print("response.text", response.text)

# 返回如下
response.status_code: 200
response.headers: {'Date': 'Thu, 12 Nov 2020 13:38:05 GMT', 'Content-Type': 'application/json', 'Content-Length': '306', 'Connection': 'keep-alive', 'Server': 'gunicorn/19.9.0', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true'}
response.request.headers: {'User-Agent': 'python-requests/2.24.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
response.content: b'{\n "args": {}, \n "headers": {\n "Accept": "*/*", \n "Accept-Encoding": "gzip, deflate", \n "Host": "httpbin.org", \n "User-Agent": "python-requests/2.24.0", \n "X-Amzn-Trace-Id": "Root=1-5fad3abd-7516d60b3e951824687a50d8"\n }, \n "origin": "116.162.2.166", \n "url": "http://httpbin.org/get"\n}\n'
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.24.0",
"X-Amzn-Trace-Id": "Root=1-5fad3abd-7516d60b3e951824687a50d8"
},
"origin": "116.162.2.166",
"url": "http://httpbin.org/get"
}

requests基本使用已经经过简单的测试了,是否有一点点feel呢?接下来我们直接将它封装为一个函数以供随时调用

示例如下

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

urls = 'http://httpbin.org/get'


def downloader(url, headers=None):
response = requests.get(url, headers=headers)
return response


print("downloader.status_code:", downloader(url=urls).status_code)
print("downloader.headers:", downloader(url=urls).headers)
print("downloader.request.headers:", downloader(url=urls).request.headers)
print("downloader.content:", downloader(url=urls).content)
print("downloader.text", downloader(url=urls).text)

# 返回效果如上所示,此处省略

以上我们就把,请求方法封装成了一个函数。将基本的url,headers以形参的方式暴露出来,我们只需传入需要请求的url即可发起请求,至此一个简单可复用的请求方法咱们就完成啦。

完~~~

以上照顾新手的就基本完成了,接下来我们搞点真家伙。

二次封装

请求函数的封装

由于请求方式并不一定(有可能是GET也有可能是POST),所以我们并不能智能的确定它是什么方式发送请求的。

Requests中request方法以及帮我们实现了这个方法。我们将他的请求方式暴露出来,写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
urls = 'http://httpbin.org/get'


def downloader(url, method=None, headers=None):
_method = "GET" if not method else method
response = requests.request(url, method=_method, headers=headers)
return response


print("downloader.status_code:", downloader(url=urls).status_code)
print("downloader.headers:", downloader(url=urls).headers)
print("downloader.request.headers:", downloader(url=urls).request.headers)
print("downloader.content:", downloader(url=urls).content)
print("downloader.text", downloader(url=urls).text)

由于大部分都是GET方法,所以我们定义了一个默认的请求方式。如果需要修改请求方式,只需在调用时传入相对应的方法即可。例如我们可以这样

1
downloader(urls, method="POST")

文本编码问题

解决由于request的误差判断而造成解码错误,而得到乱码。

此误差造成的原因是可能是响应头的Accept-Encoding,另一个是识别错误

1
2
# 查看响应编码
response.encoding

此时我们需要借用Python中C语言编写的cchardet这个包来识别响应文本的编码。安装它

1
2
pip install cchardet -i  https://pypi.tuna.tsinghua.edu.cn/simple/
# 如果pip直接安装失败的话,直接用清华源的镜像。
1
2
3
4
5
6
7
8
9
10
# 实现智能版的解码:如下
encoding = cchardet.detect(response.content)['encoding']



def downloader(url, method=None, headers=None):
_method = "GET" if not method else method
response = requests.request(url, method=_method, headers=headers)
encoding = cchardet.detect(response.content)['encoding']
return response.content.decode(encoding)

区分二进制与文本的解析

在下载图片、视频等需获取到其二进制内容。而下载网页文本需要进行encode。

同理,我们只需要将一个标志传进去,从而达到分辨的的效果。例如这样

1
2
3
4
5
def downloader(url, method=None, headers=None, binary=False):
_method = "GET" if not method else method
response = requests.request(url, method=_method, headers=headers)
encoding = cchardet.detect(response.content)['encoding']
return response.content if binary else response.content.decode(encoding)

默认Ua

在很多时候,我们拿ua又是复制。又是加引号构建key-value格式。这样有时候仅仅用requests做个测试。就搞的麻烦的很。而且请求过多了,直接就被封IP了。没有自己的ip代理,没有钱又时候还真有点感觉玩不起爬虫。

为了减少被封禁IP的概率什么的,我们添加个自己的Ua池。Ua池的原理很简单,内部就是采用随机的Ua,从而减少被发现的概率.至于为什么可以达到这这样的效果,在这里仅作简单介绍。详细可能要从计算机网络原理说起。

结论就是你一个公司里大多采用的都是同一个外网ip去访问目标网址。那么就意味着可能你们公司有N个人使用同一个ip去访问目标网址。而封禁做区分的一般由ip访问频率和浏览器的指纹和在一起的什么鬼东东。简单理解为Ua+ip访问频率达到峰值,你IP就对方关小黑屋了。

构建自己的ua池,去添加默认的请求头,

Ua有很多,这里就不放出来了,如果有兴趣可以直接去源码里面拿。直接说原理:构造很多个Ua,然后随机取用。从而降低这个同一访问频率:同时也暴露端口方便你自己传入header

1
2
3
4
5
6
7
8
9
from powerspider.tools.Ua import ua
import requests

def downloader(url, method=None, header=None, binary=False):
_headers = header if header else {'User-Agent': ua()}
_method = "GET" if not method else method
response = requests.request(url, method=_method, headers=_headers)
encoding = cchardet.detect(response.content)['encoding']
return response.content if binary else response.content.decode(encoding)

那么基本的文件都已经解决了,不过还不完美。异常处理,错误重试,日志什么都没。这怎么行呢。活既然干了,那就干的漂漂亮亮的。

来让我们加入进来这些东西

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
import cchardet
from retrying import retry
from powerspider import logger
from powerspider.tools.Ua import ua
from requests import request, RequestException


@retry(stop_max_attempt_number=3, retry_on_result=lambda x: x is None, wait_fixed=2000)
def downloader(url, method=None, header=None, timeout=None, binary=False, **kwargs):
logger.info(f'Scraping {url}')
_header = {'User-Agent': ua()}
_maxTimeout = timeout if timeout else 5
_headers = header if header else _header
_method = "GET" if not method else method
try:
response = request(method=_method, url=url, headers=_headers, **kwargs)
encoding = cchardet.detect(response.content)['encoding']
if response.status_code == 200:
return response.content if binary else response.content.decode(encoding)
elif 200 < response.status_code < 400:
logger.info(f"Redirect_URL: {response.url}")
logger.error('Get invalid status code %s while scraping %s', response.status_code, url)
except RequestException as e:
logger.error(f'Error occurred while scraping {url}, Msg: {e}', exc_info=True)


if __name__ == '__main__':
print(downloader("https://www.baidu.com/", "GET"))

至此,我们的对Requests二次封装,构造通用的请求函数就已经完成了。

源码地址:https://github.com/PowerSpider/PowerSpider/tree/dev

期待下次再见

技术杂谈

前阵子有个朋友问我一个 Python 相关的问题,然后我把解决方案简单用代码写了一下发给他,结果他说跑不起来,我就很纳闷,确认了下他说 Python 库也都装了,但是运行的时候还是会报库找不到。果不其然,经过一段时间沟通发现,它的机器上装了两个 Python 环境,装 Python 库的时候是用的 pip 安装的,它对应了一个 Python 环境,但是运行的时候却是用的使用的 python3 命令却是对应另外一个 Python 环境,理所当然就跑不通了。这个问题其实挺常见的,有时候跟对方说检查下 Python 环境路径什么的,但是对方有时候并不知道怎么做,或者直接得再告诉他用 python3 -m pip install ... 命令来装包。

另外我还有个需求是,我写了一些代码,想直接把运行结果和代码发给别人看,一种方法可以直接借助于 GitHub,代码放上去,然后让对方 Clone 下来再运行。对方也得配置下相关的环境才能正确跑起来。另一种方法则是打包 Docker 镜像,但是这也得额外花一些时间。有没有更好的方案解决这个问题呢?

上面这就是两个问题:

  • 环境不一致的问题
  • 环境共享的问题

这俩问题有通用的解决方案吗?有!答案就是云 IDE。有了云 IDE,不仅以上两个问题可以解决,甚至都不用自己配置开发环境了,我们只需要一个浏览器就能在线编辑代码,同时这个 IDE 还能分享给他人,他人打开之后,看到的就是和我一模一样的内容,甚至还可以看到我正在编写代码的实时效果。

美哉美哉!

今天就给大家介绍一款云 IDE,是华为家的,叫做「华为云CloudIDE」,我试用了一下感觉还不错,它的官方介绍页面是这样的:

image-20201111005857785

官方介绍如下:

通过华为云CloudIDE,您仅需一个浏览器就可以编辑、调试、运行您的代码。

开发环境通常包括各种配置文件、特定的语言、框架版本、以及其他个人设备等等一系列个人配置。当您开发一个新的项目或者测试一些程序时,不得不对已有的个人配置做相应的调整。或者,您可以通过CloudIDE将个人的偏好设置转移到云端。CloudIDE将存储项目设置,实现您与他人共享,并通过浏览器授予访问的权限。您可以将所有文件保存在云环境中,并通过各种访问设备操作管理它们。开发环境在云端,您可以实现代码阅读、编写、调试、运行,也可以将代码存入代码仓库,随时、随地、随心!

和我刚才说的就是一样的,利用它我们可以解决诸多问题:如环境配置、环境统一、环境共享的问题,同时我们还可以随时随地编辑代码,丝毫不受机器的限制,上云之后,一切都是那么方便。

好,这里我就带大家来体验下「华为云CloudIDE」吧。

首先,目前「华为云CloudIDE」是可以免费体验的,每个人每天有 120 分钟的免费体验时间,如果要获得更好的体验可以创建付费实例。

目前免费体验需要有这些规定:

尊敬的用户,您已经开通CloudIDE服务,免费体验需要遵守如下约定:

  • 每天的免费体验时间为120分钟,少于5分钟不能创建新的体验;
  • 每个实例可使用时长为60分钟,60分钟后,实例将会被删除,并且数据将会被删除;
  • 同一时间只能体验一个实例,打开第二个实例将会自动关闭第一个实例;
  • 免费体验不能保证最佳使用体验,您可能需要排队等候,付费使用可以获得更好的体验

完全没问题,体验完全足够了。

我这里操作的 URL 是 https://devcloud.cn-north-4.huaweicloud.com/cloudide/trial,大家可以注册个华为云账号就能看到对应的入口页面了,就是上文发的图。

由于我对 Python 比较熟悉,那么我就来体验一个 Python 环境吧,找到下方的 Python 环境,然后点击「免费体验」。

这时候环境就会变成「启动中」的状态,如图所示:

image-20201111010644754

然后接下来我们就会进入到云 IDE 的环境,等待一顿初始化之后之后,我们就可以看到如下页面,如图所示:

image-20201111010740454

嗯?有没有熟悉的感觉?

没错,这和 VS Code 可太像了,简直就是一个在线版的 VS Code。不过仔细看看,其实还是有些不一样的,比如左上角的 Logo,插件仓库以及一些菜单选项。可以说是 VS Code 的魔改版。

我们看下左侧栏其他的菜单,搜索、版本控制、调试没啥好说的,看看插件仓库吧,打开之后是这样的:

image-20201111011151570

这里我们可以看到「华为云CloudIDE」已经为我们预装了一些插件,如 Python。另外下方还有一些推荐的插件,例如 Huawei Cloud Toolkit,这些插件的安装方式和 VS Code 是一模一样的。

接下来我们再看看菜单的一些配置,随便挑几个看看吧:

image-20201111011434518

image-20201111011453092

几乎所有功能菜单和 VS Code 是一样的,不过「华为云CloudIDE」添加了一些入口,比如 Close Instance 等等,点了之后就会关闭当前云 IDE 实例。

OK,接下来我们再回到代码看看,看看它给初始化了个什么,代码结构是这样的:

image-20201111011717465

就是一个基本的 Flask 的 Hello World。

那来跑跑试试吧,点击右上角的运行按钮,如图所示:

image-20201111011824402

哦,居然报错了,内容如下:

image-20201111011856800

这里报错说找不到 flask 这个包,那很明显就是没安装了,正好我们可以看到 requirements.txt 定义了相关的依赖环境,那我们来手动安装下吧。

在当前命令行下输入:

1
pip3 install -r requirements.txt

运行结果类似如下:

image-20201111012030928

看起来不错,另外可以观察到它是从 https://repo.huaweicloud.com/repository/pypi/ 这里下载的 Python 包,速度也挺快的。简直是意外收获呢,又发现了一个不错的 Pypi 加速镜像。

顺便看看 pip.conf 配置吧:

1
cat ~/.pip/pip.conf

结果如下:

1
2
3
4
5
[global]
index-url = https://repo.huaweicloud.com/repository/pypi/simple
trusted-host = repo.huaweicloud.com
timeout = 120
extra-index-url = https://obs-workspace-20180810.obs.cn-north-1.myhuaweicloud.com/pypi/simple

一目了然,可以看到 pip 全局配置了一个加速镜像,index-url 配置为了 https://repo.huaweicloud.com/repository/pypi/simple,这个是可以公开访问的,所以以后

我们装包的时候就可以这么来装了:

1
pip3 install requests -i https://repo.huaweicloud.com/repository/pypi/simple

速度还是挺不错的,美滋滋,意外收获。

OK,切回正题,这时候我们已经装完环境了,重新运行 app.py,这时候就可以发现运行成功了,结果类似如下:

image-20201111012729299

可以看到它就在 8080 端口上运行了一个服务,那咋访问呢?

在运行的同时,还弹出了这么一个窗口,如图所示:

image-20201111012846946

似乎是提示我们可以注册一个外部端口将服务暴露出去,我们点击 Register 和 OK。

接下来 OK 按钮就变成了 Access 按钮,如图所示:

image-20201111013057453

还带了一个二维码和 Copy 按钮,我们 Copy 一下看看,我 Copy 之后的 URL 是 https://sermgcaktl-8080-cce-5.lf.templink.dev/,然后我在浏览器中打开,显示如下:

image-20201111013157227

很不错啊,这是自动给我们生成了一个域名,然后还把服务暴露出去可供访问了,挺好。

接下来我们再看看 Workspace 吧,切换下代码文件夹,点击 File - Open ,可以看到如下页面:

image-20201111013747428

可以看到当前打开的是 python3-sample 这个文件夹,那能换其他文件夹吗?我在上方输入个根目录 / 看看,结果如图所示:

image-20201111013852795

出现了熟悉的 Linux 文件树结构,那可以断定这其实就是一个 Linux 环境了,那岂不是干啥事都行了。

找到 /home/user/projects 目录,看起来是用来专门存放项目的目录。

另外我再试试能否随便打开一个 GitHub 上的项目吧,比如在控制台进入刚才这个目录,然后 Clone 一个项目,命令如下:

1
2
cd /home/user/projects/
git clone https://github.com/Python3WebSpider/ScrapyTutorial.git

OK,简单 Clone 了一个 Scrapy 项目,成功!如图所示:

image-20201111014223373

然后打开下这个项目,如图所示:

image-20201111015026872

打开 Terminal,安装下依赖:

1
pip3 install scrapy pymongo

另外这个项目依赖于 MongoDB,不过我们这里仅仅测试,可以取消它的依赖,setting.py 中注释掉如下代码:

1
2
3
4
5
ITEM_PIPELINES = {
'tutorial.pipelines.TextPipeline': 300,
- 'tutorial.pipelines.MongoDBPipeline': 400,
+ #'tutorial.pipelines.MongoDBPipeline': 400,
}

然后接下来可以运行如下命令启动爬虫了:

1
scrapy crawl quotes

这时候就可以看到爬虫就成功运行了,如图所示:

image-20201111015327574

完美,Clone 的项目也成功运行了,美滋滋。

最后我们看看这个退出之后怎么样,先复制下当前的 URL,我的 URL 是 https://ide.devcloud.cn-north-4.huaweicloud.com/cce-5/sermgcaktl/#/home/user/projects/ScrapyTutorial,然后我们直接关掉当前页面选项卡,然后重新进入,看看现在的运行状态还在不在。

牛逼!重新进入之后还是原来的样子,控制台依然还是开的,跑的结果也依然有!

image-20201111015741812

很好!

那这个环境怎么分享给别人呢?看了下「华为云CloudIDE」是支持分享功能的,这里可以进入到 CloudIDE 的首页,点击管理,然后通过白名单或者黑名单的方式添加用户就可以了。

image-20201111020023870

好了,到现在为止「华为云CloudIDE」就体验完了,感觉整体上还是挺不错的,大家也可以来体验试试看吧!

技术杂谈

YAML文件概述

K8s集群文件中对资源管理和资源对象编排部署都可以通过声明样式yaml,文件来解决,也就是说可以把需要对资源对象操作编辑到yaml,文件中。

我们称之为资源清单资源清单文件通过kubectl命令直接使用资源清单文件就可以实现对大量资源对象进行编排部署

基本语法

  • 大小写敏感
  • 使用缩进表示层级关系,缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • ‘#’表示注释
  • ---表示新的yaml文件的开始

    数据类型

    YAML 支持以下几种数据类型:
  • 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
  • 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
  • 纯量(scalars):单个的、不可再分的值

    常量

    常量是最基本的,不可再分的值,包括:
  • 字符串
  • 布尔值
  • 整数
  • 浮点数
  • Null
  • 时间
  • 日期

    引用

    & 锚点和 * 别名,可以用来引用:

    & 用来建立锚点(defaults),<< 表示合并到当前数据,* 用来引用锚点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    defaults: &defaults
    adapter: postgres
    host: localhost
    development:
    database: myapp_development
    <<: *defaults
    test:
    database: myapp_test
    <<: *defaults
    ---相当于
    defaults:
    adapter: postgres
    host: localhost
    development:
    database: myapp_development
    adapter: postgres
    host: localhost
    test:
    database: myapp_test
    adapter: postgres
    host: localhost

    kubernetes中yaml组成部分

    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
    apiVersion: v1								# API版本 可使用命令kubeclt api--verison查看
    kind: ReplicationController # 资源类型 - 副本控制器RC
    metadata: # 资源元数据
    name: mysql # - RC的名称(全局唯一),符合目标的Pod拥有此标签
    spec: # 资源的规格(RC的相关属性的定义)
    replicas: 1 # 副本的期望数量
    selector: # 标签选择器
    app: mysql
    # *********************************************
    template: # Pod 模版
    metadata:
    name: mysql
    labels: # 标签 Pod 副本拥有的标签,对应RC的Selector
    app: mysql
    spec: # Pod规格
    containers: # 容器的配置
    - name: mysql # 容器名称
    image: mysql # 容器镜像(对应的Docker images)
    ports:
    - containerPort: 3306 # 容器引用监听的端口号
    env: # 环境配置
    - name: MYSQL_ROOT_PASSWORD
    value: "123456"


    ---
    apiVersion: v1
    kind: Service # 资源类型 服务
    metadata:
    name: mysql
    spec:
    selector:
    app: mysql
    ports:
    - port: 3306
  • 控制器部分
    控制器部分
  • 被控制的对象
    被控制的对象
  • RC
    RC

    快速编写yaml文件

    Part 1:使用命令生成yaml文件

    1
    2
    3
    4
    5
    6
    # kubectl create 
    kubectl create deployment web --image=nginx -o yaml --dry-run
    # -o 使用yaml格式展示
    # -dry-run 尝试运行,并不会真正运行
    # 保存至myweb.yaml
    kubectl create deployment web --image=nginx -o yaml --dry-run > myweb.yaml
  • kubectl create deployment web —image=nginx -o yaml —dry-run运行效果如下⬇️:
    kubectl create

    Part 2: 使用命令导出yaml文件

    1
    2
    3
    kubectl get
    kubectl get deploy # 查看部署
    kubectl get deploy nginx -o yaml --export > myweb.yaml

kubectl命令学习

这里简单记录下 Kubectl 部署的一些准备工作。

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
53
54
55
56
57
Basic Commands (Beginner):
create # Create a resource from a file or from stdin.
expose # 使用 replication controller, service, deployment 或者 pod 并暴露它作为一个 新的
Kubernetes Service
run # 在集群中运行一个指定的镜像
set # 为 objects 设置一个指定的特征

Basic Commands (Intermediate):
explain # 查看资源的文档
get # 显示一个或更多 resources
edit # 在服务器上编辑一个资源
delete # Delete resources by filenames, stdin, resources and names, or by resources and label selector

Deploy Commands:
rollout # Manage the rollout of a resource
scale # 为 Deployment, ReplicaSet, Replication Controller 或者 Job 设置一个新的副本数量
autoscale # 自动调整一个 Deployment, ReplicaSet, 或者 ReplicationController 的副本数量

Cluster Management Commands:
certificate # 修改 certificate 资源.
cluster-info # 显示集群信息
top # Display Resource (CPU/Memory/Storage) usage.
cordon # 标记 node 为 unschedulable
uncordon # 标记 node 为 schedulable
drain # Drain node in preparation for maintenance
taint # 更新一个或者多个 node 上的 taints

Troubleshooting and Debugging Commands:
describe # 显示一个指定 resource 或者 group 的 resources 详情
logs # 输出容器在 pod 中的日志
attach # Attach 到一个运行中的 container
exec # 在一个 container 中执行一个命令
port-forward # Forward one or more local ports to a pod
proxy # 运行一个 proxy 到 Kubernetes API server
cp # 复制 files 和 directories 到 containers 和从容器中复制 files 和 directories.
auth # Inspect authorization

Advanced Commands:
diff # Diff live version against would-be applied version
apply # 通过文件名或标准输入流(stdin)对资源进行配置
patch # 使用 strategic merge patch 更新一个资源的 field(s)
replace # 通过 filename 或者 stdin替换一个资源
wait # Experimental: Wait for a specific condition on one or many resources.
convert # 在不同的 API versions 转换配置文件
kustomize # Build a kustomization target from a directory or a remote url.

Settings Commands:
label # 更新在这个资源上的 labels
annotate # 更新一个资源的注解
completion # Output shell completion code for the specified shell (bash or zsh)

Other Commands:
api-resources # Print the supported API resources on the server
api-versions # Print the supported API versions on the server, in the form of "group/version"
config # 修改 kubeconfig 文件
plugin # Provides utilities for interacting with plugins.
version # 输出 client 和 server 的版本信息

Kubectl create

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
53
54
55
Create a resource from a file or from stdin.

JSON and YAML formats are accepted.

Examples:
# Create a pod using the data in pod.json.
kubectl create -f ./pod.json

# Create a pod based on the JSON passed into stdin.
cat pod.json | kubectl create -f -

# Edit the data in docker-registry.yaml in JSON then create the resource using the edited data.
kubectl create -f docker-registry.yaml --edit -o json

Available Commands:
clusterrole # Create a ClusterRole.
clusterrolebinding # 为一个指定的 ClusterRole 创建一个 ClusterRoleBinding
configmap # 从本地 file, directory 或者 literal value 创建一个 configmap
cronjob # Create a cronjob with the specified name.
deployment # 创建一个指定名称的 deployment.
job # Create a job with the specified name.
namespace # 创建一个指定名称的 namespace
poddisruptionbudget # 创建一个指定名称的 pod disruption budget.
priorityclass # Create a priorityclass with the specified name.
quota # 创建一个指定名称的 quota.
role # Create a role with single rule.
rolebinding # 为一个指定的 Role 或者 ClusterRole创建一个 RoleBinding
secret # 使用指定的 subcommand 创建一个 secret
service # 使用指定的 subcommand 创建一个 service.
serviceaccount # 创建一个指定名称的 service account

Options:
--allow-missing-template-keys=true: If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats.
--dry-run=false: If true, only print the object that would be sent, without sending it.
--edit=false: Edit the API resource before creating
-f, --filename=[]: Filename, directory, or URL to files to use to create the resource
-k, --kustomize='': Process the kustomization directory. This flag can't be used together with -f or -R.
-o, --output='': Output format. One of:
json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-file.
--raw='': Raw URI to POST to the server. Uses the transport specified by the kubeconfig file.
--record=false: Record current kubectl command in the resource annotation. If set to false, do not record the
command. If set to true, record the command. If not set, default to updating the existing annotation value only if one
already exists.
-R, --recursive=false: Process the directory used in -f, --filename recursively. Useful when you want to manage
related manifests organized within the same directory.
--save-config=false: If true, the configuration of current object will be saved in its annotation. Otherwise, the
annotation will be unchanged. This flag is useful when you want to perform kubectl apply on this object in the future.
-l, --selector='': Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)
--template='': Template string or path to template file to use when -o=go-template, -o=go-template-file. The
template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].
--validate=true: If true, use a schema to validate the input before sending it
--windows-line-endings=false: Only relevant if --edit=true. Defaults to the line ending native to your platform.

Usage:
kubectl create -f FILENAME [options]

技术杂谈

最近博客换上了 Hexo,但是发现经常莫名其妙构建失败,报内存溢出,Out of Memory,错误如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
==== JS stack trace =========================================

0: ExitFrame [pc: 0x1409219]
Security context: 0x36ffd21408d1 <JSObject>
1: get sticky [0x36ffd2149801](this=0x1e69c6f730e1 <JSRegExp <String[10]: [\t \n\r]+>>)
2: match [0x6ca6396ae39] [/github/workspace/node_modules/js-beautify/js/src/core/inputscanner.js:~110] [pc=0x1d19a16be4dc](this=0x1e69c6f73119 <InputScanner map = 0x1c206a47a549>,0x1e69c6f730e1 <JSRegExp <String[10]: [\t \n\r]+>>)
3: tokenize [0x6ca6396e171] [/gith...

1: 0xa17c40 node::Abort() [hexo]
2: 0xa1804c node::OnFatalError(char const*, char const*) [hexo]
3: 0xb95a7e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [hexo]
4: 0xb95df9 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [hexo]
5: 0xd53075 [hexo]
6: 0xd53706 v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [hexo]
7: 0xd5ffc5 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [hexo]
8: 0xd60e75 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [hexo]
9: 0xd6251f v8::internal::Heap::HandleGCRequest() [hexo]
10: 0xd10f85 v8::internal::StackGuard::HandleInterrupts() [hexo]
11: 0x106c5c6 v8::internal::Runtime_StackGuard(int, unsigned long*, v8::internal::Isolate*) [hexo]
12: 0x1409219 [hexo]

原因就是内存溢出了,这是因为 Hexo 在构建的时候存了一个特别大的数组,而默认 Node 运行时最大内存为 512 MB,详情可以见:https://github.com/hexojs/hexo/issues/2165。

真是服了,才几百篇文章就不行了,耗费内存至于这么多吗?

解决方案,可以直接设置 Node 的最大内存限制,比如我直接设置为 16G,在构建之前执行如下命令就行了:

1
export NODE_OPTIONS="--max-old-space-size=16384"

这样就设置了 Node 运行时的最大内存,就不会触发内存溢出了。

Markdown

在写文章的时候我经常会遇到这么一个需求,我想要跟大家说明某一行代码需要改动成另外一行代码。

比如我这里有一段代码:

1
2
3
4
5
6
FROM python:3.6
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
VOLUME ["/app/proxypool/crawlers/private"]
CMD ["supervisord", "-c", "supervisord.conf"]

仅仅作为示例,这里行号我也没标,行数也有可能很多行,比如上百行。

比如我就想告诉大家,我想把:

1
RUN pip install -r requirements.txt

改成:

1
RUN pip install -r requirements.txt -i https://pypi.douban.com/simple

这一行。

看,我已经说完了。

为了说这件事,我需要打好几行字,我得把原来的代码打上,然后说「改成」,然后再把新代码打上。

麻烦吧?

有没有什么更好的表述方式呢?

有。

看这里:

1
2
-RUN pip install -r requirements.txt
+RUN pip install -r requirements.txt -i https://pypi.douban.com/simple

是不是很直观?

红色代表删减,绿色代表增加。

经常 Code Review 的朋友一定倍感亲切。

那这个怎么实现的呢?其实这就是用了 Markdown 高亮的一种写法,只需要把语言改成 diff 就好了。

原语言如下:

这里为了防止 Markdown 解析冲突,我就用图片了。

反正就是语言标注改成 diff,然后需要删除的前面加个减号,需要增加的加个加号就行了。

完毕!

希望有帮助!

技术杂谈

kubernetes-install

操作系统初始化

  • 关闭防火墙(all)
1
2
3
4
5
6
# 临时关闭防火墙
systemctl stop firewalld
# 永久关闭防火墙
systemctl disable firewalld
# 验证
systemctl status firewalld
  • 关闭selinux(all)
1
2
3
4
# 临时关闭
setenforce 0
# 永久
sed -i 's/enforcing/disabled/' /etc/selinux/config
  • 关闭swap(all)
1
2
3
4
# 临时
swapoff -a
# 永久
sed -ri 's/.*swap.*/#&/' /etc/fstab
  • 设置主机名称(all)
1
2
3
4
# 设置名称(k8s-m-1)忽略大写字母
hostnamectl set-hostname k8s-m-1
# 验证
hostname
  • Master添加Hostname(master)
1
2
3
4
5
6
7
8
9
10
11
12
# 设置
cat >> /etc/hosts << EOF
masterIp master
node1Ip node1
node2Ip node2
EOF
# eg
cat >> /etc/hosts << EOF
192.168.50.212 k8s-m-1
192.168.50.87 k8s-n-1
192.168.50.85 k8s-n-2
EOF
  • 将桥接的IPV4 流量传递到iptables的链(all)
1
2
3
4
5
6
cat > /etc/sysctl.d/k8s.conf << EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
# 生效
sysctl --system
  • 时间同步(All)
1
2
3
yum install -y ntpdate 
ntpdate time.windows.com
# 三台机子输出如下则成功(相差几秒或几分为正常现象)

安装Docker

官方文档-安装

  • Docker安装sh Script:(All)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# You can use scripts for one click installation,You may need to type enter at the end
# remove docker
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
# Set up repository
sudo yum install -y yum-utils
# Use Aliyun Docker
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# install docker from yum
yum install -y docker-ce docker-ce-cli containerd.io
# restart docker
systemctl restart docker
# cat version
docker --version

  • 配置加速(all)
j
1
2
3
4
5
6
7
8
9
10
11
12
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://etdea28s.mirror.aliyuncs.com"]
}
EOF

# reload
sudo systemctl daemon-reload
sudo systemctl restart docker

# 检查阿里云加速

kubernetes安装

  • 配置kubernetes源(all)
1
2
3
4
5
6
7
8
9
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

由于官网kubernetes源在国外有墙,直接使用官方源会导致安装失败。所以我们配置国内的阿里源

  • 安装 kubectl kubelet kubeadm(all)
1
2
3
4
# install kubectl kubelet kubeadm
yum install -y kubectl kubelet kubeadm
# set boot on opening computer
systemctl enable kubelet
  • 初始化k8s部署(Master)
1
2
3
4
5
6
7
8
9
10
11
12
13
kubeadm init \
--apiserver-advertise-address=youselfIp of Master \
--image-repository registry.aliyuncs.com/google_containers \
# 不冲突即可
--service-cidr=10.10.0.0/16 \
--pod-network-cidr=10.122.0.0/16

# eg
kubeadm init \
--apiserver-advertise-address=192.168.50.212 \
--image-repository registry.aliyuncs.com/google_containers \
--service-cidr=10.10.0.0/16 \
--pod-network-cidr=10.122.0.0/16

常见错误:running with swap on is not supported. Please disable swap

[preflight] If you know what you are doing, you can make a check non-fatal with `—ignore-preflight-

errors=…`

原因:系统自动进行分区

解决:

1
2
3
4
# 临时
swapoff -a
# 永久
sed -ri 's/.*swap.*/#&/' /etc/fstab
  • following as a regular user(Master)
1
2
3
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
  • join master node(node)
1
kubeadm join 172.16.164.136:6443 --token 9oilao.bpbxcm5zkk0jjcgm --discovery-token-ca-cert-hash sha256:609794bd03915be382bdb130c4c180e89cdc863d35cf99be79cf4ddcbfacee24

加入成功,如下图

此时我们在Master节点上使用命令kubectl get nodes查看节点信息:如下图所示

此时的kubectl get nodes的status都是NotNotReady:

查看kubernetes运行状态:

kubectl get pods -n kube-system

如图:

果然,两个Pending犹豫未决

此时我们部署CNI网络,配置如下

1
2
3
# 根据官方文档提示配置CNI网络
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
# 报错:The connection to the server raw.githubusercontent.com was refused - did you specify the right host or port? 原因:外网不可访问 -> 在https://www.ipaddress.com/查询raw.githubusercontent.com的真实IP。
1
2
3
sudo vi /etc/hosts
199.232.28.133 raw.githubusercontent.com
# 如下

1
2
3
4
5
# 开启IPVS,修改ConfigMap的kube-system/kube-proxy中的模式为ipvs

kubectl edit cm kube-proxy -n kube-system
# 将空的data -> ipvs -> mode中替换如下
mode: "ipvs"

在此运行kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml成功,如图

此时运行kubectl get nodes效果图如下->成功。(肯能并不一定会立马成功,上面👆确定没问题,请稍等片刻即可)

测试kubernetes

1
2
3
4
5
6
7
# 创建nginx镜像 Create a deployment with the specified name
# kubectl create deployment NAME --image=image -- [COMMAND] [args...] [options]
kubectl create deployment nginx --image=nginx
# 对外暴露端口
kubectl expose deployment nginx --port=80 --type=NodePort
# 查看pod服务
kubectl get pod,svc

成功

技术杂谈

最近刚遇到个问题,我要给自己做的网站加个无限 debugger 反爬,网站是基于 Vue.js 开发的,比如我就想在 main.js 里面加这么一段:

1
2
3
4
setInterval(() => {
debugger
console.log('debugger')
}, 1000)

当时在 Debug 环境下一切好好的,但是到了 build 之后,再运行发现 debugger 就没了,这就神奇了。

我搜了很久,最后终于找到了解决方案,这里记录下。

开发环境和生产环境

这里首先说下 Vue.js 是有开发环境和生产环境之分的,这里它用了一个关键词,叫做 mode。

我们先来看看两个常用的命令:

1
2
npm run serve
npm run build

这两个命令如果大家开发 Vue.js 的话一定不会陌生,但它们是怎么实现的呢?

顺着一找,其实他们定义在了 package.json 里面,是这样的:

1
2
3
4
5
6
7
8
9
10
{
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
...
}

这里其实就是调用了 vue-cli-service 的几个命令而已。

vue-cli-service 又是哪里来的呢?很简单,在刚开始初始化项目的时候装了个 Vue CLI:

1
2
3
npm install -g @vue/cli
# OR
yarn global add @vue/cli

它提供了 vue-cli-service 这个命令。

然后我们再来详细看看这个 serve 和 build 命令。

serve

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
Usage: vue-cli-service serve [options] [entry]

Options:

--open open browser on server start
--copy copy url to clipboard on server start
--mode specify env mode (default: development)
--host specify host (default: 0.0.0.0)
--port specify port (default: 8080)
--https use https (default: false)
--public specify the public network URL for the HMR client
--skip-plugins comma-separated list of plugin names to skip for this run

看到了吧,这里有个 mode,指的就是运行环境,这里默认为 development,即开发环境。

build

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Usage: vue-cli-service build [options] [entry|pattern]

Options:

--mode specify env mode (default: production)
--dest specify output directory (default: dist)
--modern build app targeting modern browsers with auto fallback
--no-unsafe-inline build app without introducing inline scripts
--target app | lib | wc | wc-async (default: app)
--formats list of output formats for library builds (default: commonjs,umd,umd-min)
--inline-vue include the Vue module in the final bundle of library or web component target
--name name for lib or web-component mode (default: "name" in package.json or entry filename)
--filename file name for output, only usable for 'lib' target (default: value of --name),
--no-clean do not remove the dist directory before building the project
--report generate report.html to help analyze bundle content
--report-json generate report.json to help analyze bundle content
--skip-plugins comma-separated list of plugin names to skip for this run
--watch watch for changes

这里也有一个 mode,默认就是 production 了,即生产环境。

所以,到这里我们就明白了,调用 build 命令之后,实际上是生产环境了,然后生产环境可能做了一些特殊的配置,把一些 debugger 给去除了,所以就没了。

还原

那咋还原呢?

这里我们就需要用到 Vue.js 的另外一个知识了。

Vue.js 同样是基于 Webpack 构建的,利用了 Webpack 的打包技术,不过为了更加方便开发者配置,Vue.js 在 Webpack 的基础上又封装了一层,一些配置我们不需要再实现 webpack.config.js 了,而是可以实现 vue.config.js,配置更加简单。

在 vue.config.js 里面,它为 Webpack 暴露了几个重要的配置入口,一个就是 configureWebpack,一个是 chainWebpack。

具体的教程大家可以参考官方文档:https://cli.vuejs.org/zh/guide/webpack.html。

比如前者可以这么配置:

1
2
3
4
5
6
7
module.exports = {
configureWebpack: {
plugins: [
new MyAwesomeWebpackPlugin()
]
}
}

后者可以这么配置:

1
2
3
4
5
6
7
8
9
10
11
12
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap(options => {
return options
})
}
}

这里,我们如果要修改 Webpack 构建的一些配置的话,可以利用 chainWebpack。

TerserPlugin

然后,这里又需要引入另外一个 Webpack 插件了,叫做 TerserPlugin,官方介绍链接为 https://webpack.js.org/plugins/terser-webpack-plugin/。

而这个库又是依赖 terser 的,官方介绍链接为 https://github.com/terser/terser。

官方介绍为:

A JavaScript parser and mangler/compressor toolkit for ES6+.

OK,反正就是类似一个 JavaScript 压缩转换器,比如它可以将一些 JavaScript 代码转码、混淆、压缩等等。

这里我们就需要借助于它来实现 debugger 的还原。

这里由于我使用的 Webpack 是 4.x 版本,所以 TerserPlugin 也需要是 4.x 版本,5.x 版本我测试过了不行。

安装配置如下,添加到 package.json 的 devDependencies 里面:

1
"terser-webpack-plugin": "^4.2.3",

然后:

1
npm install

接着 vue.config.js 改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
...
productionSourceMap: false,
chainWebpack: config => {
config.optimization.minimizer([
new TerserPlugin({
terserOptions: {
mangle: true,
compress: {
drop_debugger: false
}
}
})
]
)
}
}

这里我就保留了 chainWebpack 的配置,然后这里面通过 config 的 optimization 的 minimizer 方法配置了 plugins,然后这里 TerserPlugin 需要声明一个 terserOptions,然后 compress 里面的 drop_debugger 需要设置为 false。

这样,生产环境的 debugger 语句就不会丢了。

技术杂谈

2019 年,网红王尼玛做了一个五分钟的视频,是有关程序员的综艺节目《创造1024》,视频冲上热搜,被转发了几十万次,而这个视频,通篇是对程序员的刻板印象和标签化再加深。

视频里,程序员是这样子的:

视频里面每个程序员都是标配的黑眼圈,更有甚者那个连续七天七夜不眠不休最后累倒的程序员被推崇为 C 位大佬。「没女朋友」、「宅男」、「呆板」、「理工男」、「格子衫」、「没头发」 等等这些标签。

我不是吐槽视频本身,通过这个视频,站在另外的角度去思考,这里面确实有很多让我感觉不那么舒服的地方。

  • 第一,把女性程序员排除在外;
  • 第二,把整个行业的形象刻画的呆板、邋遢;
  • 第三,为什么程序员就一定会被标签化为这样的形象;

有的时候真的非常伤心,互联网时代真的来临了,程序员这个行业走上台前了。

然而走上台前的,不是曾经想用产品改变世界的梦想,而是秃头、加班、格子衫。

看到一些微博还有这样的:

所以一直到现在,我都对格子衫有着非常深刻的生理性反感,我有条纹衬衫,也有花衬衫,但就是不喜欢格子衫,因为每当我去穿格子衫的时候,我就觉得我走进了公众预设定的无聊框架中。因此,每当女朋友陪我买衣服的时候,路过格子衫的区域,我一定不会去选。

嘿嘿没错,我还有女朋友,谁说程序员没有女朋友的?况且,我女朋友还那么好看。

哦哦跑题了,说回格子衫,我现在选择不穿格子衫,其实也是被框架捆绑的一部分,可我又能怎么办呢?

当然,logo卫衣还是很赞。

当然,在提到程序员这个行业的时候,最大也是最让我讨厌的刻板影响,叫程序员安慰师。

程序员安慰师这个所谓的行业和所谓的热搜热转出来的时候,是我觉得最荒谬的事情,到底是谁觉得我们的智商和情商,我们对工作的热情和严谨,是被女性带动的。每个男性都喜欢漂亮的女孩,这是原始的性,但在这之上,我们如此优秀完成工作的事情,是我们聪明的大脑(掐腰)。

最后我想说,我仍然能看到有非常多的调研报告,有越来越多的小姐姐开始在相亲中去倾向程序员这个行业。

但我想说,所谓高薪,所谓单纯,所谓不会出轨,都是外界给我们的一个个贴下的标签,而在这标签下,我们仍然是独立而特别的个体。希望你最后的选择是因为我这个人,而并不是那些无聊的刻板印象。

我们程序员并不一定是秃头、呆滞、毫无生活情趣的死肥宅,至少说很多都不是,我们也有可爱的一面,也有自己热爱的生活,喜欢的人,喜欢的事,请行业不要再这么标签化我们。

1024,我既不想接受格子衫与人体工学椅的安利,也不想接受任何《程序员图鉴》的调侃。我希望节日能让公司反思过高强度的加班,我希望节日能给更多的人传输目前程序届的开源思想,我希望节日能让人们回忆起,在疫情时候,有那么多个体的程序员不记薪资帮助做统计和维护框架,我希望节日能让更多人意识到我们每一个有灵魂的个体本身。

最后,整篇发言稿都是我的一家之言,是我的三观输出,可能会有很多朋友不习惯或者不开心。用程序码出世界,始终是我的理想。

无论如何,祝大家 1024 节日快乐!

使用Hexo编写博客?

亲爱的伙伴您好,很荣幸能与您在此相遇.

本文主要用于记录,在一台完全的“新”机子上,书写Hexo部署博客。

我已经迫不及待想告诉你的几个容易错误的点,你准备好了么?

Let’s Go

在此之前说明,很多东西都是来自网络,当然无论如何还是建议你查阅官方文档。

Hexo官方文档

Fisrt 环境准备

  • [ ] Git
  • [ ] Node.js (Node.js 版本需不低于 10.13,建议使用 Node.js 12.0 及以上版本)
  • [ ] Hexo

Git:

可以参考此文章 https://cuiqingcai.com/9336.html

Mac 用户

如果在编译时可能会遇到问题,请先到 App Store 安装 Xcode,Xcode 完成后,启动并进入 Preferences -> Download -> Command Line Tools -> Install 安装命令行工具。

Node.JS:

Node.js 为大多数平台提供了官方的 安装程序。对于中国大陆地区用户,可以前往 淘宝 Node.js 镜像 下载。

其它的安装方法:

  • Windows:通过 nvs(推荐)或者nvm 安装。
  • Mac:使用 HomebrewMacPorts 安装。
  • Linux(DEB/RPM-based):从 NodeSource 安装。
  • 其它:使用相应的软件包管理器进行安装,可以参考由 Node.js 提供的 指导

Hexo:

所有必备的应用程序安装完成后,即可使用 npm 安装 Hexo。

1
2
3
4
# 和我一样小白的可使用以下命令来安装
npm install -g hexo-cli
# 对于熟悉 npm 的大牛,可以仅局部安装 hexo 包。
npm install hexo

验证安装

1
2
# 注意请不要在项目中使用,可能会因为限制从而导致验证误差
hexo version

Look

Hexo Version

Second 写作

新建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建一篇新文章或者新的页面
hexo new [layout] <title>
# EG 注意双引号中的是博客标题,不需要加.md等后缀。
hexo new "Your Title"

# hexo new --help
Usage: hexo new [layout] <title>

Description:
Create a new post.

Arguments:
layout Post layout. Use post, page, draft or whatever you want.
title Post title. Wrap it with quotations to escape.

Options:
-p, --path Post path. Customize the path of the post.
-r, --replace Replace the current post if existed.
-s, --slug Post slug. Customize the URL of the post.

Hexo 有三种默认布局:postpagedraft。在创建者三种不同类型的文件时,它们将会被保存到不同的路径;而您自定义的其他布局和 post 相同,都将储存到 source/_posts 文件夹。

布局 路径
post source/_posts
page source
draft source/_drafts

文内设置

用markdown等编辑器写博客,tags的写法如下:

1
2
# 注意冒号与方括号之间有一个空格,方括号中的标签用英文的”,”
tags: [hexo,备忘录]

添加“阅读全文”按钮

方法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 在文章任意你想添加的位置添加
<!--more-->
# EG

---
title: How to use hexo to create blog?
date: 2020-10-17 01:48:53
author:Payne
Mail:wuzhipeng1289690157@gamil.com
tags:[Hexo]
---
<!--more-->
后面的内容在首页不显示,只显示到<!--more-->这里

方法二:

1
2
3
4
# 设置首页文章以摘要形式显示,打开主题配置文件,找到auto_excerpt进行修改
auto_excerpt:
enable: true
length: 150

其中length代表显示摘要的截取字符长度。
注:这两种方法,在博客首页显示的效果不一样,根据自己的需要,选择自己喜欢的方法

请开始您的表演😊

Show Time 。。。

Third 自我查阅

1
2
3
4
5
6
7
8
9
10
11
12
# 构建 -> 用于生成博客的html文件
hexo g
# 预览 -> 用于在本地预览博客,打开浏览器,输入 localhost:4000/ 即可查看。
hexo s
# 检查博客格式等符合要求后,用此命令将博客推送到远端。(需要是自己的才行)
hexo d

# 实操
# 构建并查阅
hexo g && hexo s
# 推送(记得清除缓存!!!)
hexo clean && hexo g && hexo d

images

Fourth 发布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 详细流程如下
首先clone下来然后,创建分支
git clone "目标地址"
git checkout -b branch_name


# 编写博客



# 构建并查阅
hexo g && hexo s
# 推送(记得清除缓存!!!)
hexo clean && hexo g && hexo d

技术杂谈

Abstract

随着大数据人工智能时代的来临,互联网的快速发展。许许多多以前可能并不那么实际或需要的算法、技术也逐渐进入我们的眼中。例如分布式、集群、负载均衡、也越来越“平民”化。近期重新再一次的对于分布式理念、思想进行了学习。此随笔也因此而来。请多指教 为什么需要分布式? 什么是分布式? 分布式的核心理念是什么? 如何实现分布式、负载均衡、集群?

Why distributed?

为什么需要分布式、集群、负载均衡?

概念的提出或理论的出现,一定是为了解决对应的问题或避免相对应的问题。使其不断的完善化、稳定化。毕竟Human nature is lazy 此时此刻可能更需要什么?(3V3H)

  • Volume 海量
  • Variety 多样
  • velocity 实时
  • 高并发
  • 高可扩
  • 高性能

单机可能将会遇到的问题会有

  • 系统容量
  • 单点故障
  • 性能不足

该如何解决此问题? 有一种比较功利性的思想-缺啥补啥,当然也是比较较为明确的方式。至少我仅仅只需要解决以上中较少问题。 分布式(思想)要解决的问题主要是单机系统中系统容量不足及提高系统可用性。

What‘s Distributed?

什么是分布式?亦或者说满足如何的条件才算分布式?

分布式(系统),简而言之即是由多个处理机通过通信路线互联而构成的松散。一次性近乎解决了所有单点的所有问题。

A distributed system is a system whose components are located on different networked computers, which communicate and coordinate their actions by passing messages to one another.[1] The components interact with one another in order to achieve a common goal. Three significant characteristics of distributed systems are: concurrency of components, lack of a global clock, and independent failure of components.[1] Examples of distributed systems vary from SOA-based systems to massively multiplayer online games to peer-to-peer applications.

个人认为高并发、高可扩、高性能以此为主推到出 并发性:一个大的任务可以划分为若干个子任务,分别在不同的主机上执行 可扩性:可弹性增降容 自治性:分布式系统中的各个节点都包含自己的处理机和内存,各自具有独立的处理数据的功能。通常,彼此在地位上是平等的,无主次之分,既能自治地进行工作,又能利用共享的通信线路来传送信息,协调任务处理 全局唯一性:分布式系统中必须存在一个单一的、全局的进程通信机制,使得任何一个进程都能与其他进程通信,并且不区分本地通信与远程通信。

What is the core idea of distribution?

分布式的核心理念是什么?

较于以上的了解,那么最为重要的为: 一致性:各主从机权限可不同、但需有一致性。建立维持自己的通信 容错性:拥有较为自主的稳定性,可能最大保障系统的正常运行

How to realize distributed?

如何实现分布式?

经过了解与探究,相信你已经抓住的有单机到多机(分布式)的命脉,那就是一致性,信号一致性。如果能想到这里,那么恭喜您,你和我一样还是比较需要去学习的。并没有去否定它的正确与否,仅仅是太过于笼统。 那么既然需要实现分布式,那么仅仅需要建立及维护此通信即可。保障它,你我就从单机的电脑插上了网线。是否有点feel呢?

分布式优缺点剖析

部分先后,仅从不同角度侧重概述。合而为一即可

闪光点: 稳定性:资源共享。若干不同的节点通过通信网络彼此互联,一个节点上的用户可以使用其他节点上的资源。 性能:加快计算速度 机会点 存在通信网络饱和或信息丢失和网络安全问题

RAFT

简介

Raft首先选举出一个server作为Leader,然后赋予它管理日志的全部责任。Leader从客户端接收日志条目,复制给其他server,并告诉他们什么时候可以安全的将日志条目应用到自己的状态机上。拥有一个Leader可以简化replicated log的管理。例如,leader可以决定将新的日志条目放在什么位置,而无需询问其他节点,数据总是简单的从leader流向其他节点。Leader可能失败或者断开连接,这种情况下会选出一个新的leader。 通过leader,Raft将一致性问题分解成三个相当独立的子问题:

  • Leader Election:当集群启动或者leader失效时必须选出一个新的leader。
  • Log Replication:leader必须接收客户端提交的日志,并将其复制到集群中的其他节点,强制其他节点的日志与leader一样。
  • Safety:最关键的安全点就是图3.2中的State Machine Safety Property。如果任何一个server已经在它的状态机apply了一条日志,其他的server不可能在相同的index处apply其他不同的日志条目。后面将会讲述raft如何实现这一点。

基础

一个Raft集群会包含数个server,5是一个典型值,可以容忍两个节点失效。在任何时候每个server都会处于Leader、Candidate、Follower三种状态中的一种。在正常情况下会只有一个leader,其他节点都是follower,follower是消极的,他们不会主动发出请求而仅仅对来自leader和candidate的请求作出回应。leader处理所有来自客户端的请求(如果客户端访问follower,会把请求重定向到follower)。Candidate状态用来选举出一个leader。 多个candidate想要成为leader,如果一个candidate赢得选举,它将会在剩余的term中作为leader。在一些情况下选票可能会被瓜分,导致没有leader产生,这个term将会以没有leader结束,一个新的term将会很快产生。Raft确保每个term至多有一个leader。Term在Raft中起到了逻辑时钟的作用,它可以帮助server检测过期信息比如过期的leader。每一个server都存储有current term字段,会自动随时间增加。当server间通信的时候,会交换current term,如果一个节点的current term比另一个小,它会自动将其更新为较大者。如果candidate或者leader发现了自己的term过期了,它会立刻转为follower状态。如果一个节点收到了一个含有过期的term的请求,它会拒绝该请求。 Raft节点之间通过RPC进行通信,基本的一致性算法仅仅需要两种RPC。RequestVote RPC由candidate在选举过程中发出,AppendEntries RPC由leader发出,用于复制日志和提供心跳。每一个请求类型都有对应的response,Raft假定request和response都可能会丢失,因此要求请求者有超时重试的能力。为了性能,RPC请求会并行发出,而且不保证RPC的到达顺序。

Leader election

Raft使用心跳机制来触发leader选举。当server启动的时候是处于follower状态,当它可以收到来自leader或者candidate的有效RPC请求时就会保持follower的状态。Leader发送周期性的心跳(不含日志的AppendEntries RPC)给所有的follower来确保自己的权威。如果一个follower一段时间(称为election timeout)没有收到消息,它就会假定leader失效并开始新的选举。 为了开始新一轮选举,follower会提高自己当前的term并转为candidate状态。它会先给自己投一票然后并行向集群中的其他server发出RequestVote RPC,candidate会保持这个状态,直到下面三种事情之一发生:

  1. 赢得选举。当candidate收到了集群中相同term的多数节点的赞成票时就会选举成功,每一个server在给定的term内至多只能投票给一个candidate,先到先得。收到多数节点的选票可以确保在一个term内至多只能有一个leader选出。一旦一个candidate赢得选举,它就会成为leader。它之后会发送心跳消息来建立自己的权威,并阻止新的选举。
  2. 另一个server被确定为leader。在等待投票的过程中,candidate可能收到来自其他server的AppendEntries RPC,声明它才是leader。如果RPC中的term大于等于candidate的current term,candidate就会认为这个leader是合法的并转为follower状态。如果RPC中的term比自己当前的小,将会拒绝这个请求并保持candidate状态。
  3. 没有获胜者产生,等待选举超时。candidate没有选举成功或者失败,如果许多follower同时变成candidate,选票就会被瓜分,形不成多数派。这种情况发生时,candidate将会超时并触发新一轮的选举,提高term并发送新的RequestVote RPC。然而如果不采取其他措施的话,选票将可能会被再次瓜分。

Raft使用随机选举超时来确保选票被瓜分的情况很少出现而且出现了也可以被很快解决。election timeout的值会在一个固定区间内随机的选取(比如150-300ms)。这使得在大部分情况下仅有一个server会超时,它将会在其他节点超时前赢得选举并发送心跳。candidate在发起选举前也会重置自己的随机election timeout,也可以帮助减少新的选举轮次内选票瓜分的情况

Log Replication

一旦一个leader被选举出来,它开始为客户端请求服务。每一个客户端请求都包含着一个待状态机执行的命令,leader会将这个命令作为新的一条日志追加到自己的日志中,然后并行向其他server发出AppendEntries RPC来复制日志。当日志被安全的复制之后,leader可以将日志apply到自己的状态机,并将执行结果返回给客户端。如果follower宕机或运行很慢,甚至丢包,leader会无限的重试RPC(即使已经将结果报告给了客户端),直到所有的follower最终都存储了相同的日志。 日志按下图的方式进行组织,每一条日志储存了一条命令和leader接收到该指令时的term序号。日志中的term序号可以用来检测不一致的情况,每一条日志也拥有一个整数索引用于定位。

总结

RAFT算法终究实现了什么?

  • 实现了信号一致性
  • 动态的Leader、Accessible or work 角色及节点管理,最大限度的保障了稳定性。
  • 他的核心为保障大多数都可用,即可正常运行。

寄语: 如果您是一位非常有经验的管理者或您有相对稳健的算法与数据结构基础, 是否与您所存在的了解的结构有所大多相似呢?-有效管理 直接上司管辖直属同事,算法中的BFS 在稍稍加入较为智能的选Leader,若大多数leader停止了工作,worker 将会进行相对应的“休息”,并没有接受到任务,简单的计算机选择做不如不做。毕竟Human nature is lazy

技术杂谈

Docker File 解析:

构建简史

编写一个dockerfile的文件,符合dockerfile的规范 docker build 执行,获得一个自定义的镜像 docker 运行 Docker执行docker file文件的大致流程

docker 从基础镜像运行一个容器 执行一条指令并对容器作出修改 执行类似docker commit的操作提交一个新的镜像层 docker 在基于刚提交的镜像运行一个新容器 执行docker 中的下一条指令知道所有指令都执行完成

Docker File基础知识:

每条保留字指令必须为大写字母且后面要跟随至少一个参数 指令从上到下、从左至右执行 ‘#’ :表示注释 每条指令都会创建一个新的镜像层,并对镜像进行提交

Docker File 体系结构:

保留字指令:

FROM:基础镜像,当前这个新的镜像是基于哪个镜像(scratch) MAINTAINER:镜像作者+邮箱 RUN:容器构建时所需要运行的命令 EXPOSE:当前容器对外暴露的端口号 WORKDIR:指定在创建容器后,终端默认登陆的进来工作目录,一个落脚点 ENV:用于构建镜像过程中设置环境变量 ADD: 拷贝加解压缩:将宿主机目录下的文件拷贝进镜像且add命令会自动处理rul和解压tar压缩包 COPY:将从构建上下文目录中<源路径>的文件/目录复制到新的一层的镜像内的<目标路径>位置

  • COPY src dest
  • COPY [“src”,”dest”]

VOLUME:容器数据卷,用于数据保存和持久化工作 CMD:

  • 指定一个容器启动时要运行的命令
  • DockerFile 中可以有多个CMD指令,但只有最后一个生效,CMD会被docker run 之后的参数替换!!!

ENTRYPOINT:

  • 指定一个容器启动时要运行的命令
  • 目的与CMD一样,都是在指定容器启动程序及参数
  • 不会被替换,被追加

ONBUILD:触发器 当构建一个被继承Docker File时运行的命令,父镜像在被子继承后父镜像的onbuild被触发

关键字详解

Dockerfile 分为四部分:

基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。

基础镜像信息

FROM

1
2
# 格式
FROM <image> or FROM <image>:<tag>

如果在同一个Dockerfile中创建多个镜像时,可以使用多个 FROM 指令(每个镜像一次)

注意: Dockerfile每个保留字都会在docker容器中新建一层镜像层, 合理的减少镜像层以达节省资源的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# base
FROM python:3.7
RUN pip3 install requests
RUN pip3 install bs4
RUN pip3 install scrapy
RUN ...

# other
FROM python:3.7
RUN pip3 install requests bs64 scrapy # Python通过空格分隔,达到安装多个包的效果

# batter
# 外部建立所安装包
pip3 freeze >> requirements.txt
# 在Dockerfile 中
FROM python:3.7
RUN pip3 install -r requirements.txt

分析: 达到的效果虽相同,但所用资源却不同, base 所使用的资源约为other,batter的3/5

  • base 三层
  • other 一层
  • batter 一层
  1. 若所需要使用到的第三方库较少,建议使用other
  2. 在实际工程中用到的第三方库大多情况下都大于三个,方法二虽好.但所需导入的包一多,很可能出现单词拼写错误, 格式不符

维护者信息(非必须)

MAINTAINER

1
2
# 格式
MAINTAINR <name>

镜像操作指令(按需择选)

COPY

作用: 复制文件指令,从上下文 目录中复制或目录, 到容器中的指定路径 同等需求下建议使用COPY

1
2
3
4
5
6
# 格式
COPY <src> <dest>
COPY [--chown=<user>:<group>] <源路径1>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
# [--chown=<user>:<group>]:可选参数,用户改变复制到容器内文件的拥有者和属组。
# 复制本地主机的 `<src>`(为 Dockerfile 所在目录的相对路径)到容器中的 `<dest>`

<源路径>:源文件或者源目录,这里可以是通配符表达式,其通配符规则要满足 Go 的 filepath.Match 规则。

1
2
COPY hom* dir/
COPY hom?.txt dir/

<目标路径>:容器内的指定路径,该路径不用事先建好,路径不存在的话,会自动创建。

ADD

作用: 复制文件指令,从上下文 目录中复制或目录, 到容器中的指定路径 同等需求下建议使用COPY

1
2
3
4
5
# 格式 
ADD <src> <dest>`

# 该命令将复制指定的 `<src>` 到容器中的 `<dest>`。
# 其中 `<src>` 可以是Dockerfile所在目录的一个相对路径;也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)。
  • ADD 的优点:在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制并解压到 <目标路径>。
  • ADD 的缺点:在不解压的前提下,无法复制 tar 压缩文件。会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。具体是否使用,可以根据是否需要自动解压来决定

小结:

  • ADD 与 COPY 功能无明显差异, 但针对性不同
  • 当使用本地文件为源目录时,建议使用Copy
  • 当需使用压缩包中文件时构建时,建议使用Copy

RUN

作用: 用于在容器内执行命令

1
2
3
# 格式
RUN <command> [option] # 相当于shell格式
RUN ["command", "option1", 'option2'] # 相当于 exec 格式

WORKDIR

作用:指定工作目录。用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在。(WORKDIR 指定的工作(主)目录,必须是提前创建好)。 docker build 构建镜像过程中的,每一个 RUN 命令都是新建的一层。只有通过 WORKDIR 创建的目录才会一直存在。

1
2
# 格式
WORKDIR <dir>

USE

作用:用于指定将使用命令的用户和用户组 此处只是切换后续命令执行的用户(用户和用户组必须提前已经存在)。 由于docker 无设置,默认需在root权限下运行 正所谓权限越大能力越大,若被入侵则造成损失较于其他权限为最大 安全与灵活性 二者相对斟酌

1
2
# 格式
USER <username>[:<usergroup>]

EXPOSE

作用:仅声明端口

  • 方便配置映射
  • 在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。
1
2
# 格式
EXPORT <port1> [<prot2> <prot3> ...]

执行指令

CMD

作用: 类似于 RUN命令,但运行处不同

  • RUN 在Docker build 前运行
  • CMD 在docker run 后运行

注意:

  • 当 Dockerfile 中存在多个 CMD 指令,仅最后一个生效。
  • CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。

当使用自定制镜像时,大致流程为

  • 编写业务代码
  • 构建定制镜像(docker build)
  • docker 中运行业务 (Docker run)
1
2
3
4
5
# 格式
CMD <ShellCommand1 [option]> [&&ShellCommand2[option] ]
CMD ["<可执行命令或文件>", "<option1>", "<option2>", ... ]
# 此写法为保留字 ENTRYPOINT 指令指定的程序提供默认参数
CMD ["<command1>", "<command2>", "<command3>", ...]

ENTRYPOINT

作用: 功能与CMD相似,但不会被docker run 后的指定参数所覆盖, 命令行参数会被当作参数送给 ENTRYPOINT 指令指定的程序。 但是当docker run 使用了 —entrypoint 选项此选项的参数,可当作要运行的程序覆盖 ENTRYPOINT 指令指定的程序。 ENTRYPOINT CMD对比 同: 多个 指令,仅最后一个生效。 异: ENTRYPOINT选项的参数可当作要运行的程序覆盖 ENTRYPOINT 指令指定的程序。

1
2
# 格式
ENTRYPOINT ["<可执行命令或文件>","option1", "option2"]

ENV

作用:配置容器内的环境变量,且保存,可以被后续 指令使用

1
2
3
# 格式
ENV <Key> <Values>
ENV <Key1>=<Values1>, <Key2>=<Values2> ...

ARG

作用:配置容器内的环境变量,且保存,可以被后续 指令使用(与ENV功能相似) 不同:作用域不一样。ARG 设置的环境变量仅对 Dockerfile 内有效 也就是说只有 docker build 的过程中有效,构建好的镜像内不存在此环境变量。

1
构建命令 docker build 中可以用 --build-arg <参数名>=<值> 来覆盖。
1
2
3
# 格式
ARG <key>[=默认值], <Value>
# 若不写Value 则Build时为自己写的默认值

VOLUME

作用:定义匿名数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。

  • 避免重要的数据,因容器重启而丢失
  • 避免容器不断变大
1
2
3
# 格式:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

在启动容器 docker run 的时候,我们可以通过 -v 参数修改挂载点。

HEALTHCHECK

作用: 用于指定某个程序或者指令来监控 docker 容器服务的运行状态。

格式:

1
2
3
HEALTHCHECK [option] CMD <command> #设置检查容器健康状况的命令
HEALTHCHECK NONE # 如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
HEALTHCHECK [option] CMD <command> # CMD 后面跟随的命令使用

ONBUILD

作用: 用于延迟构建命令的执行。 先构建一个父类镜像(ONBUILD 在父类中,但不立即执行),后子类继承此父类镜像(执行父类的ONBUILD 命令) 本次并不执行,当镜像调用它时,将执行父类中ONBUILD命令

1
2
# 格式
ONBUILD <其它指令>

技术杂谈

Docker容器数据卷

Docker理念:

  • 将运用与运行的环境打包形成容器运行,运行可以伴随着容器,但由于对数据要求希望是持久化的
  • 容器之间希望可以共享数据

一、Docker容器数据卷是什么?

  • docker容器产生的数据,如果不通过docker commit生成新的镜像,使数据做为镜像的一部分保存下来,那么删除容器之后,数据也随之被删除。为了能保存数据在docker中,我们使用容器卷。
  • 好比从电脑(docker)中拷贝数据(使用的U盘-容器卷)

二、Docker容器数据卷能干什么?

  • 数据的持久化
  • 容器间继承+共享数据

特点:

  • 容器之间共享过重用数据
  • 卷中更改可之间生效
  • 数据卷中的更改不会在镜像的更新中
  • 数据中的更改不会包含在镜像的更新中
  • 数据卷的生命周期一直持续到没有容器使用止

三、Docker容器数据卷

容器内添加:

1
2
docker run -it -v /[宿主机绝对路径目录]:[/容器内目录] [container_ID]
docker run -it -v /myDataVolume:/dataVolumeContainer [镜像名]

权限报错处理:

1
docker run -it -v /myDataVolume:/dataVolumeContainer --privileged=true [镜像名]

检测是否数据卷是否挂载成功

1
docker inspect [container_ID]

容器停止后,主机修改后数据是否同步 可以!但需为同一个容器!!![The same container_id]

1
2
3
使用docker ps -l 查看运行过的容器信息
# 查看运行容器信息
docker ps -l

命令(带权限):容器中只读,不可修改 docker run -it -v /宿主机绝对路径:/容器内目录:ro 镜像名

dockerfile 添加

javaEE:hello.java —-> hello.class Docker: images ===》 DockerFile

  1. 新建mydocker文件夹并进入
  2. 在dockerfile中使用volume指令来给镜像添加一个或多个数据卷
    • volume[“/dataVolumeContainer”,”/dataVolumeContainer2”,”/dataVolumeContainer3”]
  3. File构建

    1
    2
    3
    4
    5
    # volume test
    FROM centos
    VOLUME ["/dataVolumeContainer1","/dataVolumeContaine2"]
    CMD echo "finished,----sucess1"
    CMD /bin/bash
  4. build后生成镜像

    1
    2
    3
    4
    docker build -f /路径/文件名 -t 容器名:TAG . 
    -f: file-->指定为文件
    -t:为容器重新分配一个为输入终端;
    . : 分布执行file中命令

四、Docker容器数据卷容器

命名的容器挂载数据卷,其他容器通过挂载这个(父容器)实现数据共享,挂载数据卷的容器称之为数据卷容器 容器间传递共享(volumes-from)

dc01(主) 删除后 dc02(子1) dc03(子2)不受影响,dc02与dc3 继续传输

结论:容器之间配置信息的传递,数据卷的生命周期一直持续到没有容器使用为止

技术杂谈

搭建个人Leanote云笔记

Leanote是一款在线的云笔记应用,有如下特点:

  • 支持网页、PC、手机APP客户端和微信版,随时记录,方便分享,支持语音,图片输入。
  • 代码高亮,涵盖所有主流语言的代码高亮,随心所欲在Leanote里写代码,记知识。
  • Markdown 编辑器,实时同步预览。
  • 专业数学公式编辑,像Word和Latex能编辑数学公式。
  • 支持创建思维脑图,将散乱的想法以树状信息分层展示。
  • 详细历史纪录,每次保存都在后端备份,轻松查找,一键恢复。
  • 实时同步云端。

简介

基于Linux + Mongo + Leanote 快速搭建个人云笔记

安装MongoDB

MongoDB是一个基于分布式文件存储的高性能数据库,介于关系数据库和非关系数据库之间,它支持的数据结构非常松散是类似于json和bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。 Leanote云笔记使用MongoDB作为后端数据库,按照以下步骤按照MongoDB数据库。

1
2
3
4
5
6
# install MongoDB
yum -y install mongodb mongodb-server.x86_64 mariadb-devel.i686
# start MongoDB service
systemctl start mongod
# Verify mongodb running state
systemctl status mongod

安装Leanote

1
2
3
4
5
6
7
8
# Download Leanote installation package
wget https://nchc.dl.sourceforge.net/project/leanote-bin/2.6.1/leanote-linux-amd64-v2.6.1.bin.tar.gz
# Unzip
tar -zxvf leanote-linux-amd64-v2.6.1.bin.tar.gz
# setting leanote
vim leanote/conf/app.conf
# change app.secret
app.secret = Self configuration

配置服务

If the following message appears

Failed global initialization: BadValue Invalid or no user locale set. Please ensure LANG and/or LC_*

Please configure the environment variables as follows

1
export LC_ALL=C
1
2
3
4
# init MongoDB
mongorestore -h localhost -d leanote --dir /root/leanote/mongodb_backup/leanote_install_data/
# Start service
nohup bash /root/leanote/bin/run.sh > /root/leanote/run.log 2>&1

访问笔记

在浏览器中访问服务器弹性地址:端口(默认9000)

1
2
3
# 默认账号
username: admin
password: abc123

技术杂谈

Multiple solutions of Fibonacci (Python or Java)

本章是用英文写的,作为或想成为一名优秀的攻城狮,习惯阅读英文文档将使你受益良多。例如更好的查看最新版的官方文档、与国外友人交流、等等 其实英文的生词也并不多,其中90%的英文都在代码里,当然这其中的精华也在代码里,代码相信大部分伙计还是都可以看懂.所以,请不要惊慌。对于English,让我们一起取克服它、习惯它、拥抱它。然后把它锤倒在地,相信你可以的。 GO, Go, GO 如果实在不行,各种页面翻译来一手。莫慌,这都是小场面,啥都不是事儿,好吧

Violence law(Top-down)

It can be solved directly according to the known conditions (f (0) = 0, f (1) = 1 F(N) = F(N - 1) + F(N - 2), for N > 1)

Python Code

1
2
3
4
class Solution:
def fib(self, N: int) -> int:
if N == 1 or N == 2: return N
return self.fib(N - 1) + self.fib(N - 2)

Java Code

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int fib(int N) {
if (N == 1 || N == 2) return 1;
return fib(N - 1) + fib(N - 2);
}
}

class Solution {
public int fib(int N) {
return N < 2 ? N : fib(N - 1) + fib(N - 2);
}
}

Violence law add cache(Pruning)

We know that if we don’t do any processing, we will repeat too many calculations, which is very bad The processing idea will avoid repeated calculation

Python Code

1
2
3
4
5
class Solution2:
@functools.lru_cache()
def fib(self, N: int) -> int:
if N <= 1: return N
else: return self.fib(N - 1) + self.fib(N - 2)

Java Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
private Integer[] cache = new Integer[31];
public int fib(int N) {
if (N <= 1) return N;
cache[0] = 0;
cache[1] = 1;
return memoize(N);
}
public int memoize(int N) {
if (cache[N] != null) return cache[N];
cache[N] = memoize(N-1) + memoize(N-2);
return memoize(N);
}
}

Divide and conquer solution

Recursion, iteration, divide and conquer, backtracking, they do not have a clear distinction Recursion:The core idea is to govern separately and unify the officials

1
2
3
4
5
6
7
class Solution:
def fib(self, N: int) -> int:
memo = {}
if N < 2: return N
if N-1 not in memo: memo[N-1] = self.fib(N-1)
if N-2 not in memo: memo[N-2] = self.fib(N-2)
return memo[N-1] + memo[N-2]

Dynamic recursion(Bottom up)

Basic solutions

More initial value, continuous dynamic recursive

Python Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def fib(self, N: int) -> int:
if N < 2: return N
dp = [0 for _ in range(N + 1)]
dp[0], dp[1] = 0, 1
for i in range(2, N + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[- 1]

class Solution:
def fib(self, N: int) -> int:
if N == 0: return 0
memo = [0,1]
for _ in range(2,N+1):
memo = [memo[-1], memo[-1] + memo[-2]]
return memo[-1]
Java Code
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int fib(int N) {
if (N <= 1) return N;
if (N == 2) return 1;
int curr = 0, prev1 = 1, prev2 = 1;
for (int i = 3; i <= N; i++) {
curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return curr;
}
}

Use better base types (tuples) to improve performance

1
2
3
4
5
6
7
class Solution:
def fib(self, N: int) -> int:
if N == 0: return 0
memo = (0,1)
for _ in range(2,N+1):
memo = (memo[-1], memo[-1] + memo[-2])
return memo[-1]

Better solutions

Python Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def fib(self, N: int) -> int:
curr, prev1, prev2 = 0, 1, 1
for i in range(3, N + 1):
curr = prev1 + prev2
prev2 = prev1
prev1 = curr
return curr

class Solution5:
def fib(self, N: int) -> int:
prev, now = 0, 1
for i in range(N):
prev, now = now, now + prev
return prev

Java Code

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int fib(int N) {
if (N == 0) return 0;
if (N == 2 || N == 1) return 1;
int prev = 1, curr = 1;
for (int i = 3; i <= N; i++) {
int sum = prev + curr;
prev = curr;
curr = sum;
}
return curr;
}
}

Mathematical conclusion method

Python Code

1
2
3
4
5
class Solution:
def fib(self, N: int) -> int:
sqrt5 = 5 ** 0.5
fun = pow((1 + sqrt5) / 2, n + 1) - pow((1 - sqrt5) / 2, n + 1)
return int(fun / sqrt5)

Java Code

1
2
3
4
5
6
class Solution {
public int fib(int N) {
double sqrt5 = (1 + Math.sqrt(5)) / 2;
return (int)Math.round(Math.pow(sqrt5, N)/ Math.sqrt(5));
}
}

Other

在计算机的世界中由最基本的for loop、while loop、if…else无限衍生,无论多么复杂的逻辑最后大多可归纳为以上三种。当然除非原本逻辑无重复性,无条件分支。

一、循环(重复)

不断的重复、有始有终 循环实现

1
2
3
4
5
6
7
private loop(){
for(start; end; loop termination){
expression1;
expression2;
expression3;
}
}
1
2
3
4
5
def loop():
for start in end/loop_termination:
expression1;
expression2;
expression3;

二、递归


特征:自相似性、有始有终 实现:归去来兮、适可而止 何时想到递归?

子问题与原始问题做同样的事

递归实现:

1
2
3
4
5
6
7
8
9
10
11
12
private void recursion(int level,int param1,int param2...):{
// 终止条件(recursion terminato)
if(level > MAX_LEVEL){
# process_rsult
return
}
// 处理此层过程逻辑(process logic in current level)
process(level, data1, data2...)
// 进入下一层(dill down)
recursion(level: level + 1, newParam):
// 如果需要恢复此层状态
}
1
2
3
4
5
6
7
8
9
10
def recursion(level, param1, param2...):
# 终止条件(recursion terminato)
if level > MAX_LEVEL:
# process_rsult
return
# 处理此层过程逻辑(process logic in current level)
process(level, data1, data2...)
# 进入下一层(dill down)
self.recursion(level + 1, param1, param2...):
# 如果需要恢复此层状态

二、分治


定义:分而治之,群臣归一 何时想到分治?

当复杂的问题可以拆分成简单的子问题

分治实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static int divide_conquer(Problem, Param1, Param2...) {
// 终止条件
if (problem == NULL) {
int res = process_last_result();
return res;
}
// 拆分子问题
subProblems = split_problem(problem)

res0 = divide_conquer(subProblems[0])
res1 = divide_conquer(subProblems[1])
...
// 合并子问题结果
result = process_result(res0, res1);

return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
def divide_conquer(Problem, Param1, Param2...):
# 终止条件
if problem is None:
return
# 拆分子问题
subproblems = split_problem(problem, data)
subresult1 = self.divide_conquer(subproblems[0], p1, ...)
subresult2 = self.divide_conquer(subproblems[1], p1, ...)
subresult3 = self.divide_conquer(subproblems[2], p1, ...)
...
# 合并子问题结果
result = process_result(subresult1, subresult2, subresult3, …)

三:回溯


采用“试错”思想,尝试“分步”去解决问题。在分步的过程中。根据上层结果,尝试此层最优解决此问题,如果此层较于上层不是最优则回溯。

四、DP(Dynamic programming) 动态规划/动态递推


定义

In both contexts it refers to simplifying a complicated problem by breaking it down into simpler sub-problems in a recursive manner. While some decision problems cannot be taken apart this way, decisions that span several points in time do often break apart recursively. 在这两种情况下,它都是指通过递归的方式将复杂问题分解为更简单的子问题来简化它。虽然有些决策问题不能用这种方式分解,但是跨越多个时间点的决策通常会递归地分解。 Simplifying a complicated problem by breaking it down into simpler sub problem(in a recursibe manner) 把一个复杂的问题分解成更简单的子问题简化它(用一种递归的方式)

自低向上 动态规划关键点:

  • 最优子结构
  • 储存中间状态
  • 递推公式(状态转移方程,DP方程)
  • eg
1
2
3
4
5
# 一维
Fib:
opt[i] = opt[n - 1] + opt[n - 2]
# 二维
opt[i][j] = opt[i + 1][j] + opt[i][j + 1]

以斐波那契数列为例:

1
2
3
F(0) = 0, F(1) = 1 

F(N) = F(N - 1) + F(N - 2)(N >= 2

递归(傻递归): 若计算F(4);需计算

1
2
lin1 F(4) = f(3)、f(2), 
lin2 F(3):f(2)、f(1), F(2) = f(1) + f(0)

DP:

1
2
i(0) = 0, i(1) = 1
[0, 1, 1, 2, 3, 5]

总结

动态规划、递归、分治、无本质区别 共性: 重复子问题 异性:最优子结构、中途淘汰次优

技术杂谈

刚装了台新机器,Git 显示总是呈现这样的样子:

1
"\346\265\213\350\257\225.txt"

解决办法:

1
git config --global core.quotepath false

仅此记录,完毕。

技术杂谈

设计模式(Design Patterns),旨在软件设计(可重用的面向对象软件的要素)中,被反复使用的一种代码设计经验。设计模式旨在简化代码量、降低耦合度、高效使用可重用代码,提高代码可拓性和可维护性。

3V3H概念: 3V: Voluem(海量),Variety(多样)、Velocity(实时) 3H: High concurrency(高并发)、High performance(高性能)、High development(高可拓)

设计模式的由来:

设计模式这个术语是由上个世纪90年代Erich Gamma、Richard Helm、Raplh Johnson和Jonhn Vlissides四人总结提炼而出。并编写了Design_Patterns(设计模式)一书,他们四位统称为GOF(俗称四人帮)。 设计模式:即 将常使用的设计思想提炼出一个个模式,然后给每个模式命名,这样在使用的时候更方便交流。GOF把23个常用模式分为创建型模式、结构型模式和行为型模式三类

为何采用设计模式思想?(请注意思想二词!!!)

设计模式并非直接用来完成编码,而是描述在各种不同情况下,要怎么解决问题的一种方案一种思想 面向对象设计模式常以对象来描述其中的关系和相互作用,但不涉用来完成应用程序的特定类别或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。 学习设计模式,关键是学习设计思想,不为设计而设计,需合理平衡设计模式的复杂度和灵活性。 须知设计模式并不是万能的!!!

设计模式符合什么原则?

开闭原则(Open Colse Principle):

软件对拓展开发,对修改关闭。

理解:在增加新功能的时候,能不改代码就尽量不要改,如果只增加部分代码便可完成新功能,即是最好。

里氏替换原则(Liskov Substitution Principle):

面向对象设计原则(六大原则)

理解:若调用父类方法可成功,即调用成调用其子类亦可成功

面向对象六大设计原则:

英文名称

缩写

中文名称

Single Responsibility Principle

SRP

单一职责原则

Open Close Principle

OCP

开闭原则

Liskov Substitution Principle

LSP

里氏替换原则

Law of Demeter ( Least Knowledge Principle)

LoD

迪米特法则(最少知道原则)

Interface Segregation Principle

ISP

接口分离原则

Dependency Inversion Principle

DIP

依赖倒置原则

常用设计模式概况:

[gallery columns=”1” size=”full” ids=”9642”]

创建型模式(Creational Patterns):

如何创建对象? 核心思想是要把对象的创建和使用分离,从而使得二者可以相对独立地变换。这些模式更加关注对象之间的创建

  • 单例模式:Singleton Pattern
  • 多例模式:Multiton Pattern
  • 工厂模式:Factory Pattern
  • 静态工厂模式:Static Factory Pattern
  • 抽象工厂模式:Abstract Factory Pattern
  • 原型模式:Prototype Pattern
  • 建造者模式:Builder Pattern
  • 对象池模式:Pool Pattern

结构型模式(Structural Patterns):

如何组合各种对象,以便于更好、更灵活的结构?面向对象的继承机制虽提供了基本的子类继承与拓展父类的功能,但结构型模式却不仅是简单的继承,还有更多的通过组合,使之与运行期的动态组合实现更加灵活的功能。这些模式更加关注对象之间的组合

  • 组合模式(Composite)
  • 桥接模式(Bridge)
  • 适配器模式(Adapter)
  • 过滤器模式(Filter、Criteria Pattern
  • 装饰模式(Decorator)
  • 外观模式(Facade)
  • 门面模式:Facade
  • 享元模式(Flyweight)
  • 代理模式(Proxy)
  • 数据映射模式:Data Mapper
  • 依赖注入模式:Dependency Injection
  • 流接口模式:Fluent Interface
  • 注册模式:Registry

行为模式(Behavioral Patterns):

行为模式主要涉及对象与函数(算法)之间的职责分配,通过对象及函数灵活组合。此种模式更加关注对象之间的通信 责任链模式:Chain of Responsibility Pattern 命令模式:Command Pattern 解释器模式:Interpreter Pattern 迭代器模式:(Iterator Pattern 中介者模式:Mediator Pattern 备忘录模式:Memento Pattern 观察者模式:Observer Pattern 状态模式:State Pattern 空对象模式:Null Object Pattern 策略模式:Strategy Pattern 模板模式:Template Pattern 访问者模式:Visitor Pattern 规格模式:Specification

有不少的人说程序= 算法+ 数据结构,但个人认为程序 = 架构 + 设计模式 + 数据结构与算法 接下来将一起对本文中面向对象原则及设计模式进行详细的学习,同时我也会不断更新算法与数据结构相关的知识,让我们一起学习起来

技术杂谈

官方文档说明

在设置中找到ITEM_PIPELINES并加入以下代码

1
scrapy.pipelines.images.ImagesPipeline: 301

settings配置:

图片存储路径:

1
IMAGES_STORE = “your path”

图片存储天数

1
images_EXPIRES =  30

设置缩略图(固定值):

1
2
3
4
IMAGES_THUMBS = {
'small':(50,50)
'big':(270,270)
}

示例:

1
2
3
# 配置图片管道参数
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IMAGES_STORE = os.path.join(BASE_DIR,'images')
1
2
3
4
5
6
7
8
# 寻找此文件的父级目录
os.path.dirname()
# 当前脚本的绝对路径目录
os.path.abspath(__file__)
# __file__当前脚本的名字

IMAGES_STORE = os.path.join(BASE_DIR,'images')
将BASE_DIR新增IMAGES文件夹路径

设置spider中获取images_url的提取方法

1
2
3
item['image_urls'] = "提取语法"
# item['image_urls'] = response.css(".pic img:attr('src')").extract()
item['images'] = [] # 【】中不需要填写,下载图片之后,保存本地的文件位置

使用ImagesPipeline下载图片时,需要使用images_urls字段,images_urls一般是可迭代的列表或元组类型

如果遇到图片反扒请打开

1
2
3
4
5
# DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
# "referer":"自行配置"
# }

存入MongoDB,示例代码

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
 import pymongo
from itemadapter import ItemAdapter

class MongoPipeline:

collection_name = 'scrapy_items'

def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db

@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
)

def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]

def close_spider(self, spider):
self.client.close()

def process_item(self, item, spider):
self.db[self.collection_name].insert_one(ItemAdapter(item).asdict())
return item

更多详情请查阅官方文档:https://docs.scrapy.org/en/latest/topics/item-pipeline.html#take-screenshot-of-item

JavaScript

一、先试着英汉翻译一波:

1.按F12打开调试台,再点击Network,再点击Headers,可以找到i=good,这就是我们刚才输入需要翻译的词语good,from Data中的就是请求的参数,分别为:

i: good from: AUTO to: AUTO smartresult: dict client: fanyideskweb salt: 15972332870677 sign: 3a078c10344e67f95822ae9389e1363f lts: 1597233287067 bv: 85c050fb1c0b4d824d801d079db7371a doctype: json version: 2.1 keyfrom: fanyi.web action: FY_BY_CLICKBUTTION

2.在来翻译一个新的词语,看下这些参数有无变化

i: 我是中国人 from: AUTO to: AUTO smartresult: dict client: fanyideskweb salt: 15972365771410 sign: 744ebb7fd625f1d4d9d6a270e98536c2 lts: 1597236577141 bv: 85c050fb1c0b4d824d801d079db7371a doctype: json version: 2.1 keyfrom: fanyi.web action: FY_BY_CLICKBUTTION

发现有5个参数是变化的(实际是4 个),分别为:

i: salt: sign: lts: bv:

3.看看URL,#http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule() 请求连接:http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule 去掉translate_o中的_o,不去的话,轻轻就数据结果:某道词典 ,英语翻译汉语正常,汉语翻译英语时,出现{‘errorCode’: 50} ,post请求方式

4.点击Initiator,可以看到所有的js文件都是@fanyi.min.js:1

5、点击@fanyi.min.js:1进入,

点击中间的{ },格式化一下。

再找到XHR/fetch Breakpoints,添加断点, 你可以针对某一个请求或者请求的关键字设置断点:

再点一下‘+’输入url即可url为http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule

最后再去点击一下翻译按钮

6.翻译:我是中国人,点击t-translate,再点击Scope,再点击Local,再点击r,即可看到我们需要的参数,在吧鼠标放到中间的r上,r = v.generateSaltSign(n),就会弹出Object框。就是我们需要的参数。

7.可以看到8941行,鼠标放到v.gen erateSaltSign(n)上,会弹出f r(e)函数,点击这个函数会进入第8368行,可以看到我们需要的参数

salt: sign: lts: bv:

在8378行打上断点,再次点击翻译按钮,即可看多所有的参数值显示出来。

8.对于第一个 ts: r,相当于pythonzhong 的ts=r赋值, 可以看到r = “” + (new Date).getTime(),知道了r,就知道了ts,r是js中 new Date().getTime()得到的是毫秒数,也就是时间戳,单位为毫秒,13位数字的字符串。

即可用python中的时间戳构造:

13位时间戳获取方法:单位:毫秒 t1 = time.time() ts= int(t1 * 1000) ts=r

第二个参数:salt: i,相当于python中的salt= i赋值,i = r + parseInt(10 * Math.random(), 10),意思是随机产生一个整数 范围是0-9里面的一个随机数

ts=r,转化为字符转,parseInt(10 * Math.random(), 10)产生的随机数也转化为字符串,最后进行字符串拼接,而不是数字相加。请注意一下。跟实际的赋值对比,产生了一个14位数字的字符串,跟实际情况一样。

即可用python中的构造法: salt= str(int(ts))+str(random.randint(0,10)) print(salt)

第三个参数:bv: t,相当于python中的bv=t赋值, var t = n.md5(navigator.appVersion),navigator.appVersion的值竟然是一个 User-Agent,那么重点来了,网上教程都是md5加密相关都是直接用python原生来生成,而我就选择直接扣代码。用python3调用js的库之execjs 来扣,鼠标放到var t = n.md5(navigator.appVersion)中的n.md5()上,将会出现f md5(e)函数,点击进入,来到8196行, md5: function(e){ },把第8196行到8278行扣下来,再应用execjs解析出bv

注意:自调函数调用写法:

var aa=function(e){ }和function aa(e){ }写法都可以,一样的

import execjs f = open(r”text.js”,encoding=’utf-8’).read() ctx1 = execjs.compile(f) bv=ctx1.call(‘md5_1’,ua.random) print(bv)

扣出来的js代码:主体为function md51(e) { } ,应用execjs解析,缺什么参数找什么参数,即可_

var n = function(e, t) { return e << t | e >>> 32 - t } , r = function(e, t) { var n, r, i, a, o; return i = 2147483648 & e, a = 2147483648 & t, n = 1073741824 & e, r = 1073741824 & t, o = (1073741823 & e) + (1073741823 & t), n & r ? 2147483648 ^ o ^ i ^ a : n | r ? 1073741824 & o ? 3221225472 ^ o ^ i ^ a : 1073741824 ^ o ^ i ^ a : o ^ i ^ a } , i = function(e, t, n) { return e & t | ~e & n } , a = function(e, t, n) { return e & n | t & ~n } , o = function(e, t, n) { return e ^ t ^ n } , s = function(e, t, n) { return t ^ (e | ~n) } , l = function(e, t, a, o, s, l, c) { return e = r(e, r(r(i(t, a, o), s), c)), r(n(e, l), t) } , c = function(e, t, i, o, s, l, c) { return e = r(e, r(r(a(t, i, o), s), c)), r(n(e, l), t) } , u = function(e, t, i, a, s, l, c) { return e = r(e, r(r(o(t, i, a), s), c)), r(n(e, l), t) } , d = function(e, t, i, a, o, l, c) { return e = r(e, r(r(s(t, i, a), o), c)), r(n(e, l), t) } , f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } , p = function(e) { var t, n = "", r = ""; for (t = 0; t <= 3; t++) n += (r = "0" + (e >>> 8 * t & 255).toString(16)).substr(r.length - 2, 2); return n }, h = function(e) { e = e.replace(/\\x0d\\x0a/g, "\\n"); for (var t = "", n = 0; n < e.length; n++) { var r = e.charCodeAt(n); if (r < 128) t += String.fromCharCode(r); else if (r > 127 && r < 2048) t += String.fromCharCode(r >> 6 | 192), t += String.fromCharCode(63 & r | 128); else if (r >= 55296 && r <= 56319) { if (n + 1 < e.length) { var i = e.charCodeAt(n + 1); if (i >= 56320 && i <= 57343) { var a = 1024 * (r - 55296) + (i - 56320) + 65536; t += String.fromCharCode(240 | a >> 18 & 7), t += String.fromCharCode(128 | a >> 12 & 63), t += String.fromCharCode(128 | a >> 6 & 63), t += String.fromCharCode(128 | 63 & a), n++ } } } else t += String.fromCharCode(r >> 12 | 224), t += String.fromCharCode(r >> 6 & 63 | 128), t += String.fromCharCode(63 & r | 128) } return t }; f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } function md5_1(e) { var t, n, i, a, o, s, m, g, v, y = Array(); for (e = e, y = f, s = 1732584193, m = 4023233417, g = 2562383102, v = 271733878, t = 0; t < y.length; t += 16) n = s, i = m, a = g, o = v, s = l(s, m, g, v, y[t + 0], 7, 3614090360), v = l(v, s, m, g, y[t + 1], 12, 3905402710), g = l(g, v, s, m, y[t + 2], 17, 606105819), m = l(m, g, v, s, y[t + 3], 22, 3250441966), s = l(s, m, g, v, y[t + 4], 7, 4118548399), v = l(v, s, m, g, y[t + 5], 12, 1200080426), g = l(g, v, s, m, y[t + 6], 17, 2821735955), m = l(m, g, v, s, y[t + 7], 22, 4249261313), s = l(s, m, g, v, y[t + 8], 7, 1770035416), v = l(v, s, m, g, y[t + 9], 12, 2336552879), g = l(g, v, s, m, y[t + 10], 17, 4294925233), m = l(m, g, v, s, y[t + 11], 22, 2304563134), s = l(s, m, g, v, y[t + 12], 7, 1804603682), v = l(v, s, m, g, y[t + 13], 12, 4254626195), g = l(g, v, s, m, y[t + 14], 17, 2792965006), m = l(m, g, v, s, y[t + 15], 22, 1236535329), s = c(s, m, g, v, y[t + 1], 5, 4129170786), v = c(v, s, m, g, y[t + 6], 9, 3225465664), g = c(g, v, s, m, y[t + 11], 14, 643717713), m = c(m, g, v, s, y[t + 0], 20, 3921069994), s = c(s, m, g, v, y[t + 5], 5, 3593408605), v = c(v, s, m, g, y[t + 10], 9, 38016083), g = c(g, v, s, m, y[t + 15], 14, 3634488961), m = c(m, g, v, s, y[t + 4], 20, 3889429448), s = c(s, m, g, v, y[t + 9], 5, 568446438), v = c(v, s, m, g, y[t + 14], 9, 3275163606), g = c(g, v, s, m, y[t + 3], 14, 4107603335), m = c(m, g, v, s, y[t + 8], 20, 1163531501), s = c(s, m, g, v, y[t + 13], 5, 2850285829), v = c(v, s, m, g, y[t + 2], 9, 4243563512), g = c(g, v, s, m, y[t + 7], 14, 1735328473), m = c(m, g, v, s, y[t + 12], 20, 2368359562), s = u(s, m, g, v, y[t + 5], 4, 4294588738), v = u(v, s, m, g, y[t + 8], 11, 2272392833), g = u(g, v, s, m, y[t + 11], 16, 1839030562), m = u(m, g, v, s, y[t + 14], 23, 4259657740), s = u(s, m, g, v, y[t + 1], 4, 2763975236), v = u(v, s, m, g, y[t + 4], 11, 1272893353), g = u(g, v, s, m, y[t + 7], 16, 4139469664), m = u(m, g, v, s, y[t + 10], 23, 3200236656), s = u(s, m, g, v, y[t + 13], 4, 681279174), v = u(v, s, m, g, y[t + 0], 11, 3936430074), g = u(g, v, s, m, y[t + 3], 16, 3572445317), m = u(m, g, v, s, y[t + 6], 23, 76029189), s = u(s, m, g, v, y[t + 9], 4, 3654602809), v = u(v, s, m, g, y[t + 12], 11, 3873151461), g = u(g, v, s, m, y[t + 15], 16, 530742520), m = u(m, g, v, s, y[t + 2], 23, 3299628645), s = d(s, m, g, v, y[t + 0], 6, 4096336452), v = d(v, s, m, g, y[t + 7], 10, 1126891415), g = d(g, v, s, m, y[t + 14], 15, 2878612391), m = d(m, g, v, s, y[t + 5], 21, 4237533241), s = d(s, m, g, v, y[t + 12], 6, 1700485571), v = d(v, s, m, g, y[t + 3], 10, 2399980690), g = d(g, v, s, m, y[t + 10], 15, 4293915773), m = d(m, g, v, s, y[t + 1], 21, 2240044497), s = d(s, m, g, v, y[t + 8], 6, 1873313359), v = d(v, s, m, g, y[t + 15], 10, 4264355552), g = d(g, v, s, m, y[t + 6], 15, 2734768916), m = d(m, g, v, s, y[t + 13], 21, 1309151649), s = d(s, m, g, v, y[t + 4], 6, 4149444226), v = d(v, s, m, g, y[t + 11], 10, 3174756917), g = d(g, v, s, m, y[t + 2], 15, 718787259), m = d(m, g, v, s, y[t + 9], 21, 3951481745), s = r(s, n), m = r(m, i), g = r(g, a), v = r(v, o); return (p(s) + p(m) + p(g) + p(v)).toLowerCase() }

第四个参数:sign: n.md5(“fanyideskweb” + e + i + “]BjuETDhU)zqSxf-=B#7m”),相当于python中的sign=n.md5(“fanyideskweb” + e + i + “]BjuETDhU)zqSxf-=B#7m”), salt=i,e是你输入需要翻译的词语,即可写成sign=n.md5(“fanyideskweb” +str(e) + str(salt) + “]BjuETDhU)zqSxf-=B#7m”),继续选择直接扣代码。用python3调用js的库之execjs 来扣,鼠标放到 sign: n.md5(“fanyideskweb” + e + i + “]BjuETDhU)zqSxf-=B#7m”)中的n.md5()上,将会出现f md5(e)函数,点击进入,来到8196行, md5: function(e){ },把第8196行到8278行扣下来,竟然和刚才的扣bv的方法相同,仅仅是参数不同而已,bv的是 n.md5(navigator.appVersion)中的navigator.appVersion参数,现在的asin的是sign=n.md5(“fanyideskweb” +str(e) + str(salt) + “]BjuETDhU)zqSxf-=B#7m”)中的”fanyideskweb” +str(e) + str(salt) + “]BjuETDhU)zqSxf-=B#7m”参数,再应用execjs解析出sign.

注意:自调函数调用写法:

var aa=function(e){ }和function aa(e){ }写法都可以,一样的

import execjs f = open(r”text.js”,encoding=’utf-8’).read() ctx1 = execjs.compile(f) bv=ctx1.call(‘md5_2’,ua.random) print(bv)

扣出来的js代码:主体为function md52(e) { } ,应用execjs解析,缺什么参数找什么参数,即可_

var n = function(e, t) { return e << t | e >>> 32 - t } , r = function(e, t) { var n, r, i, a, o; return i = 2147483648 & e, a = 2147483648 & t, n = 1073741824 & e, r = 1073741824 & t, o = (1073741823 & e) + (1073741823 & t), n & r ? 2147483648 ^ o ^ i ^ a : n | r ? 1073741824 & o ? 3221225472 ^ o ^ i ^ a : 1073741824 ^ o ^ i ^ a : o ^ i ^ a } , i = function(e, t, n) { return e & t | ~e & n } , a = function(e, t, n) { return e & n | t & ~n } , o = function(e, t, n) { return e ^ t ^ n } , s = function(e, t, n) { return t ^ (e | ~n) } , l = function(e, t, a, o, s, l, c) { return e = r(e, r(r(i(t, a, o), s), c)), r(n(e, l), t) } , c = function(e, t, i, o, s, l, c) { return e = r(e, r(r(a(t, i, o), s), c)), r(n(e, l), t) } , u = function(e, t, i, a, s, l, c) { return e = r(e, r(r(o(t, i, a), s), c)), r(n(e, l), t) } , d = function(e, t, i, a, o, l, c) { return e = r(e, r(r(s(t, i, a), o), c)), r(n(e, l), t) } , f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } , p = function(e) { var t, n = "", r = ""; for (t = 0; t <= 3; t++) n += (r = "0" + (e >>> 8 * t & 255).toString(16)).substr(r.length - 2, 2); return n }, h = function(e) { e = e.replace(/\\x0d\\x0a/g, "\\n"); for (var t = "", n = 0; n < e.length; n++) { var r = e.charCodeAt(n); if (r < 128) t += String.fromCharCode(r); else if (r > 127 && r < 2048) t += String.fromCharCode(r >> 6 | 192), t += String.fromCharCode(63 & r | 128); else if (r >= 55296 && r <= 56319) { if (n + 1 < e.length) { var i = e.charCodeAt(n + 1); if (i >= 56320 && i <= 57343) { var a = 1024 * (r - 55296) + (i - 56320) + 65536; t += String.fromCharCode(240 | a >> 18 & 7), t += String.fromCharCode(128 | a >> 12 & 63), t += String.fromCharCode(128 | a >> 6 & 63), t += String.fromCharCode(128 | 63 & a), n++ } } } else t += String.fromCharCode(r >> 12 | 224), t += String.fromCharCode(r >> 6 & 63 | 128), t += String.fromCharCode(63 & r | 128) } return t }; f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a }

function md5_2(e) { var t, n, i, a, o, s, m, g, v, y = Array(); for (e = e, y = f(e), s = 1732584193, m = 4023233417, g = 2562383102, v = 271733878, t = 0; t < y.length; t += 16) n = s, i = m, a = g, o = v, s = l(s, m, g, v, y[t + 0], 7, 3614090360), v = l(v, s, m, g, y[t + 1], 12, 3905402710), g = l(g, v, s, m, y[t + 2], 17, 606105819), m = l(m, g, v, s, y[t + 3], 22, 3250441966), s = l(s, m, g, v, y[t + 4], 7, 4118548399), v = l(v, s, m, g, y[t + 5], 12, 1200080426), g = l(g, v, s, m, y[t + 6], 17, 2821735955), m = l(m, g, v, s, y[t + 7], 22, 4249261313), s = l(s, m, g, v, y[t + 8], 7, 1770035416), v = l(v, s, m, g, y[t + 9], 12, 2336552879), g = l(g, v, s, m, y[t + 10], 17, 4294925233), m = l(m, g, v, s, y[t + 11], 22, 2304563134), s = l(s, m, g, v, y[t + 12], 7, 1804603682), v = l(v, s, m, g, y[t + 13], 12, 4254626195), g = l(g, v, s, m, y[t + 14], 17, 2792965006), m = l(m, g, v, s, y[t + 15], 22, 1236535329), s = c(s, m, g, v, y[t + 1], 5, 4129170786), v = c(v, s, m, g, y[t + 6], 9, 3225465664), g = c(g, v, s, m, y[t + 11], 14, 643717713), m = c(m, g, v, s, y[t + 0], 20, 3921069994), s = c(s, m, g, v, y[t + 5], 5, 3593408605), v = c(v, s, m, g, y[t + 10], 9, 38016083), g = c(g, v, s, m, y[t + 15], 14, 3634488961), m = c(m, g, v, s, y[t + 4], 20, 3889429448), s = c(s, m, g, v, y[t + 9], 5, 568446438), v = c(v, s, m, g, y[t + 14], 9, 3275163606), g = c(g, v, s, m, y[t + 3], 14, 4107603335), m = c(m, g, v, s, y[t + 8], 20, 1163531501), s = c(s, m, g, v, y[t + 13], 5, 2850285829), v = c(v, s, m, g, y[t + 2], 9, 4243563512), g = c(g, v, s, m, y[t + 7], 14, 1735328473), m = c(m, g, v, s, y[t + 12], 20, 2368359562), s = u(s, m, g, v, y[t + 5], 4, 4294588738), v = u(v, s, m, g, y[t + 8], 11, 2272392833), g = u(g, v, s, m, y[t + 11], 16, 1839030562), m = u(m, g, v, s, y[t + 14], 23, 4259657740), s = u(s, m, g, v, y[t + 1], 4, 2763975236), v = u(v, s, m, g, y[t + 4], 11, 1272893353), g = u(g, v, s, m, y[t + 7], 16, 4139469664), m = u(m, g, v, s, y[t + 10], 23, 3200236656), s = u(s, m, g, v, y[t + 13], 4, 681279174), v = u(v, s, m, g, y[t + 0], 11, 3936430074), g = u(g, v, s, m, y[t + 3], 16, 3572445317), m = u(m, g, v, s, y[t + 6], 23, 76029189), s = u(s, m, g, v, y[t + 9], 4, 3654602809), v = u(v, s, m, g, y[t + 12], 11, 3873151461), g = u(g, v, s, m, y[t + 15], 16, 530742520), m = u(m, g, v, s, y[t + 2], 23, 3299628645), s = d(s, m, g, v, y[t + 0], 6, 4096336452), v = d(v, s, m, g, y[t + 7], 10, 1126891415), g = d(g, v, s, m, y[t + 14], 15, 2878612391), m = d(m, g, v, s, y[t + 5], 21, 4237533241), s = d(s, m, g, v, y[t + 12], 6, 1700485571), v = d(v, s, m, g, y[t + 3], 10, 2399980690), g = d(g, v, s, m, y[t + 10], 15, 4293915773), m = d(m, g, v, s, y[t + 1], 21, 2240044497), s = d(s, m, g, v, y[t + 8], 6, 1873313359), v = d(v, s, m, g, y[t + 15], 10, 4264355552), g = d(g, v, s, m, y[t + 6], 15, 2734768916), m = d(m, g, v, s, y[t + 13], 21, 1309151649), s = d(s, m, g, v, y[t + 4], 6, 4149444226), v = d(v, s, m, g, y[t + 11], 10, 3174756917), g = d(g, v, s, m, y[t + 2], 15, 718787259), m = d(m, g, v, s, y[t + 9], 21, 3951481745), s = r(s, n), m = r(m, i), g = r(g, a), v = r(v, o); return (p(s) + p(m) + p(g) + p(v)).toLowerCase() }

9.大功告成,四个可变参数全部搞定,即可搞定&

附上完整代码 python部分:

import time import execjs import random import requests import json from fake_useragent import UserAgent # var aa=function(e) function aa(e)

class YouDaoTanslate(): def init(self): #类方法调用 self.js_par() def js_par(self): translation_data = input(“请输入要翻译的词或者句子:”) # 13位时间戳获取方法:单位:毫秒 ua = UserAgent() print(ua.random) t1 = time.time() ts = int(t1 * 1000) print(ts) salt = str(int(ts)) + str(random.randint(0, 10)) print(salt)

f = open(r”text.js”, encoding=’utf-8’).read() ctx1 = execjs.compile(f) bv = ctx1.call(‘md5_1’, ua.random) print(bv)

ctx2 = execjs.compile(f) sign_data = ‘fanyideskweb’ + translation_data + salt + ‘mmbP%A-r6U3Nw(n]BjuEU’ sign = ctx2.call(‘md5_2’, sign_data) print(sign)

self.get_translateData(translation_data,ua,ts,salt,bv,sign) def get_translateData(self,translation_data,ua,ts,salt,bv,sign): url = ‘http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule‘ headers = { ‘Host’: ‘fanyi.youdao.com’, ‘Origin’: ‘http://fanyi.youdao.com‘, ‘Referer’: ‘http://fanyi.youdao.com/‘, ‘User-Agent’: ua.random, ‘X-Requested-With’: ‘XMLHttpRequest’, ‘Cookie’: ‘OUTFOX_SEARCH_USER_ID_NCOO=892433278.3204294; OUTFOX_SEARCH_USER_ID=”-1911793285@10.108.160.19”; _ntes_nnid=d2bb7793f13c9a83907e33d40665337a,1597158692753; JSESSIONID=aaamUZK4O580YaKjmjIpx; _rltest__cookies=1597227802599’ } data = { ‘i’: translation_data, ‘from’: ‘AUTO’, ‘to’: ‘AUTO’, ‘smartresult’: ‘dict’, ‘client’: ‘fanyideskweb’, ‘salt’: str(salt), ‘sign’: str(sign), ‘ts’: str(ts), ‘bv’: str(bv), ‘doctype’: ‘json’, ‘version’: ‘2.1’, ‘keyfrom’: ‘fanyi.web’, ‘action’: ‘FY_BY_CLICKBUTTION’ }

response = requests.post(url=url, data=data, headers=headers)

response = requests.post(url=url, data=data, headers=headers) translate_results = json.loads(response.text) # 找到翻译结果 if ‘translateResult’ in translate_results: translate_results = translate_results[‘translateResult’][0][0][‘tgt’] print(“翻译的结果是:%s” % translate_results)

else: print(translate_results)

self.js_par()

YouDaoTanslate()

js部分:

var n = function(e, t) { return e << t | e >>> 32 - t } , r = function(e, t) { var n, r, i, a, o; return i = 2147483648 & e, a = 2147483648 & t, n = 1073741824 & e, r = 1073741824 & t, o = (1073741823 & e) + (1073741823 & t), n & r ? 2147483648 ^ o ^ i ^ a : n | r ? 1073741824 & o ? 3221225472 ^ o ^ i ^ a : 1073741824 ^ o ^ i ^ a : o ^ i ^ a } , i = function(e, t, n) { return e & t | ~e & n } , a = function(e, t, n) { return e & n | t & ~n } , o = function(e, t, n) { return e ^ t ^ n } , s = function(e, t, n) { return t ^ (e | ~n) } , l = function(e, t, a, o, s, l, c) { return e = r(e, r(r(i(t, a, o), s), c)), r(n(e, l), t) } , c = function(e, t, i, o, s, l, c) { return e = r(e, r(r(a(t, i, o), s), c)), r(n(e, l), t) } , u = function(e, t, i, a, s, l, c) { return e = r(e, r(r(o(t, i, a), s), c)), r(n(e, l), t) } , d = function(e, t, i, a, o, l, c) { return e = r(e, r(r(s(t, i, a), o), c)), r(n(e, l), t) } , f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } , p = function(e) { var t, n = "", r = ""; for (t = 0; t <= 3; t++) n += (r = "0" + (e >>> 8 * t & 255).toString(16)).substr(r.length - 2, 2); return n }, h = function(e) { e = e.replace(/\\x0d\\x0a/g, "\\n"); for (var t = "", n = 0; n < e.length; n++) { var r = e.charCodeAt(n); if (r < 128) t += String.fromCharCode(r); else if (r > 127 && r < 2048) t += String.fromCharCode(r >> 6 | 192), t += String.fromCharCode(63 & r | 128); else if (r >= 55296 && r <= 56319) { if (n + 1 < e.length) { var i = e.charCodeAt(n + 1); if (i >= 56320 && i <= 57343) { var a = 1024 * (r - 55296) + (i - 56320) + 65536; t += String.fromCharCode(240 | a >> 18 & 7), t += String.fromCharCode(128 | a >> 12 & 63), t += String.fromCharCode(128 | a >> 6 & 63), t += String.fromCharCode(128 | 63 & a), n++ } } } else t += String.fromCharCode(r >> 12 | 224), t += String.fromCharCode(r >> 6 & 63 | 128), t += String.fromCharCode(63 & r | 128) } return t }; f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } function md5_1(e) { var t, n, i, a, o, s, m, g, v, y = Array(); for (e = e, y = f, s = 1732584193, m = 4023233417, g = 2562383102, v = 271733878, t = 0; t < y.length; t += 16) n = s, i = m, a = g, o = v, s = l(s, m, g, v, y[t + 0], 7, 3614090360), v = l(v, s, m, g, y[t + 1], 12, 3905402710), g = l(g, v, s, m, y[t + 2], 17, 606105819), m = l(m, g, v, s, y[t + 3], 22, 3250441966), s = l(s, m, g, v, y[t + 4], 7, 4118548399), v = l(v, s, m, g, y[t + 5], 12, 1200080426), g = l(g, v, s, m, y[t + 6], 17, 2821735955), m = l(m, g, v, s, y[t + 7], 22, 4249261313), s = l(s, m, g, v, y[t + 8], 7, 1770035416), v = l(v, s, m, g, y[t + 9], 12, 2336552879), g = l(g, v, s, m, y[t + 10], 17, 4294925233), m = l(m, g, v, s, y[t + 11], 22, 2304563134), s = l(s, m, g, v, y[t + 12], 7, 1804603682), v = l(v, s, m, g, y[t + 13], 12, 4254626195), g = l(g, v, s, m, y[t + 14], 17, 2792965006), m = l(m, g, v, s, y[t + 15], 22, 1236535329), s = c(s, m, g, v, y[t + 1], 5, 4129170786), v = c(v, s, m, g, y[t + 6], 9, 3225465664), g = c(g, v, s, m, y[t + 11], 14, 643717713), m = c(m, g, v, s, y[t + 0], 20, 3921069994), s = c(s, m, g, v, y[t + 5], 5, 3593408605), v = c(v, s, m, g, y[t + 10], 9, 38016083), g = c(g, v, s, m, y[t + 15], 14, 3634488961), m = c(m, g, v, s, y[t + 4], 20, 3889429448), s = c(s, m, g, v, y[t + 9], 5, 568446438), v = c(v, s, m, g, y[t + 14], 9, 3275163606), g = c(g, v, s, m, y[t + 3], 14, 4107603335), m = c(m, g, v, s, y[t + 8], 20, 1163531501), s = c(s, m, g, v, y[t + 13], 5, 2850285829), v = c(v, s, m, g, y[t + 2], 9, 4243563512), g = c(g, v, s, m, y[t + 7], 14, 1735328473), m = c(m, g, v, s, y[t + 12], 20, 2368359562), s = u(s, m, g, v, y[t + 5], 4, 4294588738), v = u(v, s, m, g, y[t + 8], 11, 2272392833), g = u(g, v, s, m, y[t + 11], 16, 1839030562), m = u(m, g, v, s, y[t + 14], 23, 4259657740), s = u(s, m, g, v, y[t + 1], 4, 2763975236), v = u(v, s, m, g, y[t + 4], 11, 1272893353), g = u(g, v, s, m, y[t + 7], 16, 4139469664), m = u(m, g, v, s, y[t + 10], 23, 3200236656), s = u(s, m, g, v, y[t + 13], 4, 681279174), v = u(v, s, m, g, y[t + 0], 11, 3936430074), g = u(g, v, s, m, y[t + 3], 16, 3572445317), m = u(m, g, v, s, y[t + 6], 23, 76029189), s = u(s, m, g, v, y[t + 9], 4, 3654602809), v = u(v, s, m, g, y[t + 12], 11, 3873151461), g = u(g, v, s, m, y[t + 15], 16, 530742520), m = u(m, g, v, s, y[t + 2], 23, 3299628645), s = d(s, m, g, v, y[t + 0], 6, 4096336452), v = d(v, s, m, g, y[t + 7], 10, 1126891415), g = d(g, v, s, m, y[t + 14], 15, 2878612391), m = d(m, g, v, s, y[t + 5], 21, 4237533241), s = d(s, m, g, v, y[t + 12], 6, 1700485571), v = d(v, s, m, g, y[t + 3], 10, 2399980690), g = d(g, v, s, m, y[t + 10], 15, 4293915773), m = d(m, g, v, s, y[t + 1], 21, 2240044497), s = d(s, m, g, v, y[t + 8], 6, 1873313359), v = d(v, s, m, g, y[t + 15], 10, 4264355552), g = d(g, v, s, m, y[t + 6], 15, 2734768916), m = d(m, g, v, s, y[t + 13], 21, 1309151649), s = d(s, m, g, v, y[t + 4], 6, 4149444226), v = d(v, s, m, g, y[t + 11], 10, 3174756917), g = d(g, v, s, m, y[t + 2], 15, 718787259), m = d(m, g, v, s, y[t + 9], 21, 3951481745), s = r(s, n), m = r(m, i), g = r(g, a), v = r(v, o); return (p(s) + p(m) + p(g) + p(v)).toLowerCase() }

var n = function(e, t) { return e << t | e >>> 32 - t } , r = function(e, t) { var n, r, i, a, o; return i = 2147483648 & e, a = 2147483648 & t, n = 1073741824 & e, r = 1073741824 & t, o = (1073741823 & e) + (1073741823 & t), n & r ? 2147483648 ^ o ^ i ^ a : n | r ? 1073741824 & o ? 3221225472 ^ o ^ i ^ a : 1073741824 ^ o ^ i ^ a : o ^ i ^ a } , i = function(e, t, n) { return e & t | ~e & n } , a = function(e, t, n) { return e & n | t & ~n } , o = function(e, t, n) { return e ^ t ^ n } , s = function(e, t, n) { return t ^ (e | ~n) } , l = function(e, t, a, o, s, l, c) { return e = r(e, r(r(i(t, a, o), s), c)), r(n(e, l), t) } , c = function(e, t, i, o, s, l, c) { return e = r(e, r(r(a(t, i, o), s), c)), r(n(e, l), t) } , u = function(e, t, i, a, s, l, c) { return e = r(e, r(r(o(t, i, a), s), c)), r(n(e, l), t) } , d = function(e, t, i, a, o, l, c) { return e = r(e, r(r(s(t, i, a), o), c)), r(n(e, l), t) } , f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } , p = function(e) { var t, n = “”, r = “”; for (t = 0; t <= 3; t++) n += (r = “0” + (e >>> 8 t & 255).toString(16)).substr(r.length - 2, 2); return n }, h = function(e) { e = e.replace(/\x0d\x0a/g, “\n”); for (var t = “”, n = 0; n < e.length; n++) { var r = e.charCodeAt(n); if (r < 128) t += String.fromCharCode(r); else if (r > 127 && r < 2048) t += String.fromCharCode(r >> 6 | 192), t += String.fromCharCode(63 & r | 128); else if (r >= 55296 && r <= 56319) { if (n + 1 < e.length) { var i = e.charCodeAt(n + 1); if (i >= 56320 && i <= 57343) { var a = 1024 (r - 55296) + (i - 56320) + 65536; t += String.fromCharCode(240 | a >> 18 & 7), t += String.fromCharCode(128 | a >> 12 & 63), t += String.fromCharCode(128 | a >> 6 & 63), t += String.fromCharCode(128 | 63 & a), n++ } } } else t += String.fromCharCode(r >> 12 | 224), t += String.fromCharCode(r >> 6 & 63 | 128), t += String.fromCharCode(63 & r | 128) } return t }; f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a }

function md5_2(e) { var t, n, i, a, o, s, m, g, v, y = Array(); for (e = e, y = f(e), s = 1732584193, m = 4023233417, g = 2562383102, v = 271733878, t = 0; t < y.length; t += 16) n = s, i = m, a = g, o = v, s = l(s, m, g, v, y[t + 0], 7, 3614090360), v = l(v, s, m, g, y[t + 1], 12, 3905402710), g = l(g, v, s, m, y[t + 2], 17, 606105819), m = l(m, g, v, s, y[t + 3], 22, 3250441966), s = l(s, m, g, v, y[t + 4], 7, 4118548399), v = l(v, s, m, g, y[t + 5], 12, 1200080426), g = l(g, v, s, m, y[t + 6], 17, 2821735955), m = l(m, g, v, s, y[t + 7], 22, 4249261313), s = l(s, m, g, v, y[t + 8], 7, 1770035416), v = l(v, s, m, g, y[t + 9], 12, 2336552879), g = l(g, v, s, m, y[t + 10], 17, 4294925233), m = l(m, g, v, s, y[t + 11], 22, 2304563134), s = l(s, m, g, v, y[t + 12], 7, 1804603682), v = l(v, s, m, g, y[t + 13], 12, 4254626195), g = l(g, v, s, m, y[t + 14], 17, 2792965006), m = l(m, g, v, s, y[t + 15], 22, 1236535329), s = c(s, m, g, v, y[t + 1], 5, 4129170786), v = c(v, s, m, g, y[t + 6], 9, 3225465664), g = c(g, v, s, m, y[t + 11], 14, 643717713), m = c(m, g, v, s, y[t + 0], 20, 3921069994), s = c(s, m, g, v, y[t + 5], 5, 3593408605), v = c(v, s, m, g, y[t + 10], 9, 38016083), g = c(g, v, s, m, y[t + 15], 14, 3634488961), m = c(m, g, v, s, y[t + 4], 20, 3889429448), s = c(s, m, g, v, y[t + 9], 5, 568446438), v = c(v, s, m, g, y[t + 14], 9, 3275163606), g = c(g, v, s, m, y[t + 3], 14, 4107603335), m = c(m, g, v, s, y[t + 8], 20, 1163531501), s = c(s, m, g, v, y[t + 13], 5, 2850285829), v = c(v, s, m, g, y[t + 2], 9, 4243563512), g = c(g, v, s, m, y[t + 7], 14, 1735328473), m = c(m, g, v, s, y[t + 12], 20, 2368359562), s = u(s, m, g, v, y[t + 5], 4, 4294588738), v = u(v, s, m, g, y[t + 8], 11, 2272392833), g = u(g, v, s, m, y[t + 11], 16, 1839030562), m = u(m, g, v, s, y[t + 14], 23, 4259657740), s = u(s, m, g, v, y[t + 1], 4, 2763975236), v = u(v, s, m, g, y[t + 4], 11, 1272893353), g = u(g, v, s, m, y[t + 7], 16, 4139469664), m = u(m, g, v, s, y[t + 10], 23, 3200236656), s = u(s, m, g, v, y[t + 13], 4, 681279174), v = u(v, s, m, g, y[t + 0], 11, 3936430074), g = u(g, v, s, m, y[t + 3], 16, 3572445317), m = u(m, g, v, s, y[t + 6], 23, 76029189), s = u(s, m, g, v, y[t + 9], 4, 3654602809), v = u(v, s, m, g, y[t + 12], 11, 3873151461), g = u(g, v, s, m, y[t + 15], 16, 530742520), m = u(m, g, v, s, y[t + 2], 23, 3299628645), s = d(s, m, g, v, y[t + 0], 6, 4096336452), v = d(v, s, m, g, y[t + 7], 10, 1126891415), g = d(g, v, s, m, y[t + 14], 15, 2878612391), m = d(m, g, v, s, y[t + 5], 21, 4237533241), s = d(s, m, g, v, y[t + 12], 6, 1700485571), v = d(v, s, m, g, y[t + 3], 10, 2399980690), g = d(g, v, s, m, y[t + 10], 15, 4293915773), m = d(m, g, v, s, y[t + 1], 21, 2240044497), s = d(s, m, g, v, y[t + 8], 6, 1873313359), v = d(v, s, m, g, y[t + 15], 10, 4264355552), g = d(g, v, s, m, y[t + 6], 15, 2734768916), m = d(m, g, v, s, y[t + 13], 21, 1309151649), s = d(s, m, g, v, y[t + 4], 6, 4149444226), v = d(v, s, m, g, y[t + 11], 10, 3174756917), g = d(g, v, s, m, y[t + 2], 15, 718787259), m = d(m, g, v, s, y[t + 9], 21, 3951481745), s = r(s, n), m = r(m, i), g = r(g, a), v = r(v, o); return (p(s) + p(m) + p(g) + p(v)).toLowerCase() }

Python

本篇目标

  • 了解为什么我们需要直接调用 JavaScript
  • 了解常见的 Python 调用 JavaScript 的库
  • 了解一种性能更高的操作方式
  • 知道什么场景下应该使用什么方式进行调用

通过本文的学习,在你写爬虫时,你应该会对调用 JavaScript 有一个更清晰的了解,并且你还要了解到一些你平时可能见不到的骚操作。

大家如果接触过 JavaScript 逆向的话,应该都知道,通常来说碰到 JS 逆向网站时会有这两种情况:

  • 简单 JS 破解:通过 Python 代码轻松实现
  • 复杂的 JS 破解:代码不容易重写,使用程序直接调用 JS 运行获取结果。

对于简单的 JS 来说,我们可以通过 Python 代码,直接重写,轻轻松松的就能搞定。 而对于复的 JS 代码而言呢,由于代码过于复杂,重写太费时费力,且碰到对方更新这就比较麻烦了。所以,我们一般直接使用程序去调用 JS,在 Python 层面就只是获取一个运行结果,这样做相比于重写而言就方便多了。 那么,接下来我带大家看一下两种比较简单的 JS 代码重写。 本文涉及的所有演示代码请到公众号:AI悦创,后台回复:PJS 。来获取即可!

1. Base64

首先,我们先来看一下 Base64 ,Base64 是我们再写爬虫过程中经常看到的一种编码方式。这边我们来写两个例子。

1
2
3
4
// 原字符
NightTeam
// 编码之后的:
TmlnaHRUZWFt

第一个例子如上,是 NightTeam 经过编码是如上面的结果(TmlnaHRUZWFt),如果我们只是通过这个结果来分析的话,它的特征不是很明显。如果是见的不多或者是新手小白的同学,并不会把它往 Base64 方向去想。 然后,我们来看一下第二个例子:

1
2
3
4
5
6
7
8
9
// 原字符
aiyuechuang
// 编码之后的:
YWl5dWVjaHVhbmc=

// 原字符
Python3
// 编码之后
UHl0aG9uMw==

第二个例子是 aiyuechuang 编码之后的结果,它的末尾有一个等号,Python3 编码之后末尾有两个等号,这个特征相对第一个就比较明显了。一般我们看到尾号有两个等号时应该大概可以猜到这个就是 Base64 了。 然后,直接解码看一看,如果没有什么特别的话,就可以使用 Python 进行重写了。 同学可以使用以下链接 Base64 编码解码的测试学习:http://tool.alixixi.com/base64/ 不过 Base64 也会有一些骚操作,碰到那种情况的时候,我们如果用 Python 重写可能有点麻烦。具体的内容我会在后面的的课程中,单独的跟大家详细的讲解。

2. MD5

第二个的话就是 MD5 ,MD5 在 Javascript 中并没有标准的库,一般我们都是使用开源库去操作。

注意:md5 的话是 哈希 并不是加密。

下面我来看一个 js 实现 md5 的一个例子: md5.js 上面的代码时被混淆过的,但是它的主要一个特征还是比较明显的,有一个入口函数:console.log(hex_md5("aiyuechuang")) 我们可以使用命令行运行一下结果,命令如下:

1
node md5.js

上面的代码自行复制保存为 md5.js 然后运行。

运行结果:

1
2
$ node md5.js
e55babec7f5d5cf7bac7872f0481bec1

我们数一下输出的结果的话,会发现这正好 是 32位,通常我们看到 32 位的一个英文数字混合的字符串,应该马上就能想到时 md5 了,这两个操作的话,因为在 Python 中都有对应的库,分别是:Base64 和 hashlib ,大家应该都知道这个我就不多说了。 例程:Base64 和 hashlib

1
2
3
4
5
6
import base64  
str1 = b'aiyuechuang'
str2 = base64.b64encode(str1)
print(str2)
str3 = base64.b64decode('YWl5dWVjaHVhbmc=')
print(str3)

输出

1
2
3
b'YWl5dWVjaHVhbmc='
b'aiyuechuang'
[Finished in 0.2s]
1
2
3
4
5
import hashlib

data = "aiyuechuang"
result = hashlib.md5(data.encode(encoding = "UTF-8")).hexdigest()
print(result)

输出

1
2
e55babec7f5d5cf7bac7872f0481bec1
[Finished in 0.1s]

像我们前面看到的那些代码,都是比较简单的,他们的算法部分也没有经过修改,所以我们可以使用其他语言和对应的库进行重写。 但是如果对方把算法部分做了一些改变呢? 如果代码量比较大也被混淆到看不出特征了,连操作后产生的字符串都看不出,我们就无法直接使用一个现成的库来复写操作了。 而且这种情况下的代码量太大了,直接对着代码重写成 Python 版本也不太现实,对方一更新你就得再重新看一遍,这样显然时非常麻烦的,也非常耗时。 那么有没有一种更高效的方法呢? 显然是有的,接下来我们来讲如何通过程序来直接调用 JavaScript 代码,也就是碰到复杂的 JS 时候的处理。

  1. 使用 Python 调用 JS
  2. 一种性能更高的调用方式
  3. 到底选择哪种方案比较好

首先,我会分享一些使用 Python 调用 JavaScript 的方式,然后会介绍一种性能更高的调用。以及具体使用哪种调用方式以及怎么选择性的使用,最后我会总结一下这些方案存在的小问题。并且会告诉你如何踩坑。

3. 使用 Python 调用 JS

我们接下来首先讲一下 Python 中调用 JavaScript。

  • PyV8
  • Js2Py
  • PyExecJS
  • PyminiRacer
  • Selenium
  • Pyppeteer

Python 调用 JS 库的话,光是我了解的话,目前就有这么一堆,接下来我们就来依次来介绍这些库。

3.1 PyV8

  • V8 是谷歌开源的 JavaScript 引擎,被使用在了 Chrome 中
  • PyV8 是 V8 引擎的一个 Python 层的包装,可以用来调用 V8 引擎执行 JS 代码
  • 网上有很多使用它来执行 JS 代码的文章
  • 年久失修,最新版本是 2010年的(https://pypi.org/project/PyV8/#history)
  • 存在内存泄漏问题,所以不建议使用

首先来看一下什么是 PyV8,V8 是谷歌开源的 JavaScript 引擎,被使用在了 Chrome 浏览器中,后来因为有人想在 Python 上调用它(V8),于是就有了 PyV8。 那 PyV8 实际上是 V8 引擎的一个 Python 层的包装,可以用来调用 V8 引擎执行 JS 代码,但是这个我不推荐使用它,那我既然不推荐大家使用,我为什么又要讲它呢? 其实,是这样的: 虽然目前网上有很多文章使用它执行 JS 代码,但是这个 PyV8 实际上已经年久失修了,而且它最新的一个正式版本还是 2010年的,可见是有多久远了,链接在上方可以执行访问查看。而且,如果你实际使用过的话,你应该会发现它存在一些内存泄漏的问题。 所以,这边我拿出来说一下,避免有人踩坑。接下来我们来说一下第二个 JS2Py。

3.2 Js2Py

  • Js2Py 是一个纯 Python 实现的 JavaScript 解释器和翻译器
  • 虽然 2019年依然有更新,但那也是 6月份的事情了,而且它的 issues 里面有很多的 bug 没有修复(https://github.com/PiotrDabkowski/Js2Py/issues)。

Js2Py 是一个纯 Python 实现的 JavaScript 解释器和翻译器,它和 PyV8 一样,也是有挺多文章提到这个库,然后来调用 JS 代码。 但是,Js2Py 虽然在2019年仍然更新,但那也是 6月份的事情了,而且它的 issues 里面有很多的 bug 没有修复(https://github.com/PiotrDabkowski/Js2Py/issues)。另外,Js2Py 本身也存在一些问题,就解释器部分来说: 解释器部分:

  • 性能不高
  • 存在一些 BUG

那不仅仅就解释器部分,还有翻译器部分:

  • 对于高度混淆的大型 JS 会转换失败
  • 而且转换出来的代码可读性差、性能不高

总之来讲,它在各个方面来说都不太适合我们的工作场景,所以也是不建议大家使用的。

3.3 PyMinRacer

  • 同样是 V8 引擎的包装,和 PyV8 的效果一样
  • 一个继任 PyExecJS 和 PyramidV8 的库
  • 一个比较新的库

这个库也是一个 PyV8 引擎包装,它的效果和 PyV8 的效果一样的。 而且作者号称这是一个继任 PyExecJS 和 PyramidV8 的库,乍眼一看挺唬人的,不过由于它是一个比较新的库,我这边就没有过多的尝试了,也没有再实际生产环境中使用过,所以不太清楚会有什么坑,感兴趣的朋友,大家可以自己去尝试一下。

3.4 PyExecJS

  • 一个最开始诞生于 Ruby 中的库,后来被移植到了 Python 上
  • 较新的文章一般都会说用它来执行 JS 代码
  • 有多个引擎可选,但一般我们会选择使用 NodeJS 作为引擎来执行代码

接下来我要说的是 PyExecJS ,这个库一个最开始诞生于 Ruby 中的库,后来人被移植到了 Python 上,目前看到一些比较新的文章都是用它来执行 JS 代码的,然后它是有多个引擎可以选择的,我们一般选择 NodeJS 作为它的一个引擎执行代码,毕竟 NodeJS 的速度是比较快的而且配置起来比较简单,那我带大家来看一下 PyExecjs 的使用。

3.4.5 PyExecJS 的使用

  1. 安装 JS 运行环境这里推荐安装 Node.js,安装方便,执行效率也高。

首先我们就是要安装引擎了,这个引擎指的就是 JS 的一个运行环境,这边推荐使用 Node.js。

注意:虽然 Windows 上有个系统自带的 JScript,可以用来作为 PyExecjs 的引擎,但是这个 JScript 很容易与其他的引擎有一个不一样的地方,容易踩到一些奇奇怪怪的坑。所以请大家务必要安装一个其他的引擎。比如说我们这里安装 Node.js 。

那上面装完 Nodejs 之后呢,我们就需要执行安装 PyExecjs 了:

  1. 安装 PyExecJS

    1
    pip install pyexecjs

这边我们使用上面的 pip 就可以进行安装了。 那么我们现在环境就准备好了,可以开始运行了。

  1. 代码示例(检测运行环境)

首先,我们打开 IPython 终端,执行一下一下两行代码,以下也给出了运行结果:

1
2
3
4
In [1]: import execjs

In [2]: execjs.get().name # 查看调用环境
Out[2]: 'Node.js (V8)'

execjs.get() # 查看调用的环境用此来看看我们的库能不能检测到 nodejs,如果不能的话那就需要手动设置一下,不过一般像我上面一样正常输出 node.js 就可以了。 如果,你检测出来的引擎不是 node.js 的话,那你就需要手动设置一下了,这里有两种设置形式,我在下方给你写出来了: 选择不同引擎进行解析

1
2
3
4
5
6
# 长期使用
os.environ["EXECJS_RUNTIME"]="Node"

# 临时使用
import execjs.runtime_names
node=execjs.get(execjs.runtime_names.Node)

由上边可知,我们有两种形式:一种是长期使用的,通过环境变量的形式,通过把环境变量改成大写的 EXECJS_RUNTIME 然后将其值赋值为 Node。 另一种的话,将它改成临时使用的一种方式,这种是直接使用 get,这种做法的话,你在使用的时候就需要使用 node 变量了,不能直接导入 PyExecjs 来直接开始使用,相对麻烦一些。 接下来,就让我们正式使用 PyExecJS 这个包吧。

1
2
3
4
5
6
In [8]: import execjs

In [9]: e = execjs.eval('a = new Array(1, 2, 3)') # 可以直接执行 JS 代码

In [10]: print(e)
[1, 2, 3]

PyExecjs 最简单的用法就是导入包,然后通过 eval 这个方法并传入简单的 JS 代码来执行。但是我们正常情况下肯定不会这么使用,因为我们的 JS 代码是比较复杂的而且 JS 代码内容也是比较多的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding: utf-8 -*-
# @Author: clela
# @Date: 2020-03-24 13:54:27
# @Last Modified by: aiyuechuang
# @Last Modified time: 2020-04-03 08:44:15
# @公众号:AI悦创

In [12]: import execjs

In [13]: jstext = """
...: function hello(str){return str;}
...: """

In [14]: ctx = execjs.compile(jstext) # 编译 JS 代码

In [15]: a = ctx.call("hello", "hello aiyc")

In [16]: print(a)
hello aiyc

这样的话,我们一般通过使用第二种方式,第二种方式是通过使用 compile 对 JS 字符串进行编译,这个编译操作其实就是把参数(jstext)里面的那段 JS 代码给放到一个叫 Context 的上下文中,它并不是我们平时编译程序所说的编译。然后我们 调用 call 方法进行执行。 第一个参数是我们调用 JS 中的的函数名,也就是 hello。然后后面跟着的 hello aiyc 就是参数,也就是我们 JS 中需要传入到 str 的参数。如果 JS 中存在多个参数,我们就直接在后面打个逗号,然后接着写下一个参数就好了。 接下来我们来看一个具体的代码: aes_demo.js 这边我准备了一个 CryptoJS 的一个 JS 文件,CryptoJS 它是一个包含各种加密哈希编码算法的一个开源库,很多网站都会用它提供的函数来生成参数,那么这边我是写了如上面这样的代码,用来调用它里面的 AES 加密参数,来加密一下我提供的字符串。

注意:JS 代码不要放在和 Python 代码同一个文件中,尽量放在单独的 js 文件中,因为我们的 JS 文件内容比较多。然后通过读取文件的方式,

run_aes.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Python 文件:run_aes.py
# -*- coding: utf-8 -*-
# @时间 : 2020-04-06 00:00
# @作者 : AI悦创
# @文件名 : run_aes.py
# @公众号: AI悦创
from pprint import pprint

import execjs
import pathlib
import os

js_path = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
js_path = js_path / "crypto.js"
with js_path.open('r', encoding="utf-8") as f:
script = f.read()

c = "1234"

# 传入python中的变量
add = ('''
aesEncrypt = function() {
result={}
var t = CryptoJS.MD5("login.xxx.com"),
i = CryptoJS.enc.Utf8.parse(t),
r = CryptoJS.enc.Utf8.parse("1234567812345678"),
u = CryptoJS.AES.encrypt(''' + "'{}'".format(c) + ''',i, {
iv: r
});
result.t=t.toString()
result.i =i.toString()
result.r =r.toString()
result.u =u.toString()
return result
};
''')
script = script + add
print("script",script)

x = execjs.compile(script)
result = x.call("aesEncrypt")
print(result)

这里我通过读取文件的方式,将 js 文件读取进来,把代码读取到我们的字符串里面,这样一方面方便我们管理,另一方面也可以直接通过代码检测自动补全功能,使用起来会比较方便。 然后,这里我们有一个小技巧,我们可以通过 format 字符串拼接的形式,将 Python 中的变量,也就是上面的变量 c 然后将这个变量写入到 Js 代码中,从而变相的实现了通过调用 JS 函数,在没有参数的情况下修改 JS 代码中的特定变量的值。最后我们拼接好了我我们的 JS 代码(add 和 script)。 拼完 JS 代码之后,我们这边再常规的进行一个操作,调用 Call 方法执行 aesEncrypt 这样一个函数,需要注意的是,这个代码里面 return 出来的 JS,它是一个 object,JS 中的 object 也就是 Python 中的字典 我们实际使用时,如果需要在 Python 中拿到 object 的话,建议把它转换成一个 json 字符串,而不是直接的把结果 return 出来。 因为,有些时候 PyExecjs 对 object 的转换会出现问题,所以我们可能会拿到一些类似于将字典直接用 str 函数包裹后转为字符串的一个东西,这样的话它是无法通过正常的方式去解析的。 或者说你也可能会遇到其情况的报错,总之大家最好先转一下 json 字符串,然后再 return 避免踩坑。这是我们的一个代码。 接下来我们来说一下,PyExecJS 存在的一些问题主要有以下两点:

  • 执行大型 JS 时会有点慢(这个是因为,每次执行 JS 代码的时候,都是从命令行去调用到的 JS,所以 JS 代码越复杂的话,nodejs 的初始化时间就越长,这个基本上是无解的)
  • 特殊编码的输入或输出参数会出现报错的情况(因为,是从命令行调用的,所以在碰到一些特殊字符输入或输出参数或者 JS 代码本身就有一些特殊字符的情况下,就会直接执行不了,给你抛出一个异常。不过这个跟系统的命令行默认编码有一定关系,具体的话这里就不深究了,直接就说解决方案吧。)
  • 可以把输入或输出的参数使用 Base64 编码一下(如果看报错是 JS 代码部分导致的,那就去看看能不能删除代码中的那部分字符或者你自己 new 一个上下文对象,将那个名叫 tempfile 的参数打开,这样在调用的时候,它就直接去执行那个文件了,不过大量调用的情况下,可能会对磁盘造成一定压力。

而如果参数不充分导致的话,有个很简单的方法:就是把参数使用 Base64 编码一下,因为编码之后出来的字符串,我们知道 Base64 编码之后是生成英文和数字组成的。这样就没有特殊符号了。所以就不会出现问题了。) 关于 PyExecejs 的相关东西就介绍到这里了,我们来看一些其他的内容。

3.5 其他使用 Python 调用 JS 的骚操作

前面说的都是非浏览器环境下直接调用 JS 的操作,但是还有一些市面上根本没人提到的骚操作,其实也挺好用的,接下来我给大家介绍一下:

  1. Selenium
  • 一个 web 自动化测试框架,可以驱动各种浏览器进行模拟人工操作
  • 用于渲染页面以方便提取数据或过验证码
  • 也可以直接驱动浏览器执行 JS

这个大家是比较熟悉的,它是一个外部自动化的测试框架,可以驱动各种浏览器进行模拟人工操作,很多文章或者培训班的课程,都会提到它在爬虫方面的一个使用,比如用它采集一些动态页面,或者用来过一些滑动验证码之类的。 不过我们这里不用它来做这些事,我们要做的是用它来执行 JS 代码,因为这样的话是直接在浏览器环境下执行的 ,所以的话它是省了很多事,那么 Selenium 执行 JS 的核心代码,实际上就下面一行:

1
2
js = "一大段 JS"
result = browser.execute_script(js)

我们来看一下实际的例子: SeleniumDemo 进入项目根目录,输入:python server.py

1
2
3
4
5
6
7
8
9
10
$ python server.py
* Serving Flask app "server" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 262-966-819
* Running on http://0.0.0.0:5002/ (Press CTRL+C to quit)

访问 localhost:5002 我们进入网页之后,有这样的一句话:


每次刷新都会显示不同的内容,查看源代码的话,会发现这个页面中的源代码里面没有对应页面显示的那句话,而是只有一个 input 标签。 我还能观察到,input 标签里面有两个属性,一个是 id、一个是 data,这两个是比较关键的属性,然后我们还发现这里面引用了一个 js 文件,所以这个网页最终结果实际上是通过 JS 文件,然后一系列的操作生成的,那接下来我就来看看 JS 文件,做了什么工作。 js 我们可以看见,这个 JS 文件最后一句,有一个 window.onload =doit 的这样一代码,这个我们知道,当页面加载完成之后,立即执行这个 JS 方法。

1
2
3
4
5
6
7
8
9
function doit() {
let browser_type=BrowserType();
console.log(browser_type)
let supporter =browser_type.supporter
if(supporter==="chrome"){
Base64.run('base64', 'data',supporter)
}

}

然后这个方法里面做了一个这样一个操作:let browser_type=BrowserType(); 首先去判断 supporter 是否等于 Chrome 这个 supporter 实际上有一个 browser_type 这个 browser_type 实际上就是检测浏览器等一系列参数,然后我们获取它里面的 supporter 属性,当 supportersupporter =browser_type.supporter )等于 Chrome 的时候,我们再去执行这个 run 函数。

1
2
3
4
5
6
7
8
run: function (id, attr,supporter) {
let all_str = $(id).getAttribute(attr)
let end_index=supporter.length+58
Base64._keyStr = all_str.substring(0, end_index)
let charset = all_str.substring(64, all_str.length)
let encoded = Base64.decode(charset,supporter);
$(id).value = encoded;
}

也就是 run 函数里面做了一系列操作,然后我传入的 id 可以通过看一下上面的函数 doit 可知传入的是 Base64 也就是说,实际上对 input 这个标签做了一个取值的操作,然后到这边我们就这整体一个过程将会用 JS 去模拟,所以这边我就不细说了。 最终会把这样的一个结果去通过 input.value 属性把值复制到 input 中,也就是我们最终看到的那样一个结果,到目前我就把这个 js 大概做了一件什么样的事情就已经讲的差不多了。接下来我们去看一下 Selenium 这边。 代码如下:

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
# -*- coding: utf-8 -*-
# @Time : 2020-04-01 20:56
# @Author : aiyuehcuang
# @File : demo.py
# @Software: PyCharm
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.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
import time

def get_text(id,attr):
###  拼接字符串注意{}要写出{{}}
script=("""
let bt=BrowserType();
let id='{id}';
let attr='{attr}';
let supporter =bt.supporter;
const run=function(){{
let all_str = $(id).getAttribute(attr)
let end_index=supporter.length+58
Base64._keyStr = all_str.substring(0, end_index)
let charset = all_str.substring(64, all_str.length)
let encoded = Base64.decode(charset,supporter);
return encoded
}}
return run()
""").format(id=id,attr=attr)
return script

chrome_option = Options()
chrome_option.add_argument("--headless")
chrome_option.add_argument("--disable-gpu")
chrome_option.add_argument('--ignore-certificate-errors') # SSL保存
browser = webdriver.Chrome(options=chrome_option)
wait = WebDriverWait(browser, 10)
# 启动浏览器,获取网页源代码
mainUrl = "http://127.0.0.1:5002/"
browser.get(mainUrl)
result=browser.execute_script(get_text("base64","data"))
print(result)
time.sleep(10)
browser.quit()

这边关键的一行代码是:通过 execute_script(get_text("base64","data")) 这样的一句话去执行这个函数,这个函数实际上就是返回一段 JS 代码,这边实际上就是去模拟构造 run 所需要的一些参数,然后把最终的结果返回回去。 这里有两点需要注意:

  1. 如果里面存在拼接字符串的时候,注意花括号实际上要写两个
  2. 如果需要在后面需要获取 JS 返回的值,所以我们上面的代码需要加上 return 来返回 run 函数的结果

我们可以运行一下代码,输出结果如下:

1
2
3
4
5
$ python demo.py

DevTools listening on ws://127.0.0.1:59507/devtools/browser/edbe51d8-744d-447d-9304-e9551a2a6421
[0407/184920.601:INFO:CONSOLE(286)] "[object Object]", source: http://127.0.0.1:5002/static/js/base64.js (286)
生活不是等待暴风雨过去,而是要学会在雨中跳舞。

我们可以看到,我们程够获取到了结果。 这个例子因为它用到了检测浏览器的属性,而且它检测完属性之后会把属性值一直往下传,我们可以从上面的代码中看到它有很多地方使用。 所以,如果我们用 PyExecjs 来写的话,就需要修改很多参数,这样就很不方便了。因为我们需要去模拟这些浏览器参数,我这边写的例子比较简单,像那种更加复杂的。像获取更多的浏览器的一个属性的话,用 PyExecjs 再去写的时候,可能没有浏览器这样的一个环境,所以 PyExecjs 没有 Selenium 有优势。 当然,除了 Selenium 以为,还有一个叫做 Pyppeteer 的库,也是比较常见。 为了控制文章篇幅,咱们下次再续咯,记得关注公众号:AI悦创!

技术杂谈

qimingpian接口加密分析

工具:Chrome + NodeJS + Pycharm 点击获取结果 如果能留下小星星就最好啦

抓包

调出开发者工具,直接到xhr(这里点击改变的时候并未发生网址变更、所以这是Ajax)

参数寻找

一共就两个包,但Preview里面没有数据,but几KB的包没有鬼?反正我不相信1.参数寻找

追根揭底

直接把encrypt_data,拉出来全局搜索(ctrl + shift + F),encrypt_data参数一共六个,但就只有这一个最可疑(我就是不告诉你为什么。。。),其实你看看周围的函数你就会发现,TmD一个个返回啥呀,不是错误就是上传失败。封IP的信息就放了。怕了怕了 2.有猫腻 在console里面打印一下Object(u.a)(e.encrypt_data) 初一看,好像是又好像不是(仅有部分信息)

只有标题,为什么没信息呢? 我告诉你为什么,因为数据被加密了,只给你看标题,充钱就给你看。 不慌,不慌。那个xx说过我离成功就一步了

点击 下一步没错,就是它。老板,求解密一下? ok,感谢老板。再次在console里面打印一下Object(u.a)(e.encrypt_data) 当当当~ 2.3有猫腻-揭开神秘面纱 ok,那它是怎么来的呢?

都晓得它是这里解密出来的,还不就进去搞他呗

3.紧随其后 当当当~,扣它,把这个函数扣出来(快到我怀里来~) 3.紧随其后-加密参数 到这里就基本上把主函数弄完了,但是还没有完 a.a.decode(t)这个鬼我们还不晓得,进去找他,扣它 4.缺啥补啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
decode = function (t) {
var e = (t = String(t).replace(f, "")).length;
e % 4 == 0 && (e = (t = t.replace(/==?$/, "")).length),
(e % 4 == 1 || /[^+a-zA-Z0-9/]/.test(t)) && l("Invalid character: the string to be decoded is not correctly encoded.");
for (var n, r, i = 0, o = "", a = -1; ++a < e;)
r = c.indexOf(t.charAt(a)),
n = i % 4 ? 64 * n + r : r,
i++ % 4 && (o += String.fromCharCode(255 & n >> (-2 * i & 6)));
return o
},

function o(t) {
return JSON.parse(s("5e5062e82f15fe4ca9d24bc5", a.a.decode(t), 0, 0, "012345677890123", 1))
}

这里的参数t,还不晓得,既然是外面传进来的,那么它要么是js生成的,要么就是全局的。全前面找,去console里面测一下,测多次。如果是不变的那么它就是一个全局参数。 拿过来就好,然后在console里面copy(t)。 同理,参数c,和f也是 但是c, 和f 就在decode函数前面,拿了就好 完成!

JavaScript

MiGu登录参数分析

目标:分析咪咕视频登录参数(enpasswordfingerPrintfingerPrintDetail

工具:NodeJs + Chrome 开发者工具

许久没有水文了,闲来无事特来混混脸熟 源码在此,欢迎白嫖,star就更好啦

enpassword

找到登录入口:

查找方式:

点击登录 —> 开启chrome开发者工具 -> 重载框架 —> 抓到登录包 如下:

加密参数寻找

清空之后,使用错误的账号密码登录。一共两个包两张图片。图片开源不看,具体看包,最后在authn包中看到了我们登录所加密过的三个参数,如下

海里捞针-找参数

在搜索框(ctrl + shift + F )下搜索enpassword参数,进入source File 发现link 93,name并未加密;那么就是在它的class 属性 J_RsaPsd中。再次找!

海里捞针-找参数、埋断点

找到三个 J_RsaPsd,每个都上断点,然后在点登录一下 encrypt:加密函数,b.val加密对象(输入的密码) 将其扣出来! 为什么扣这里?因为这里为加密处!由明文转为密文。那我们拿到这些就以为着拿到了加密的函数。就可以自己实现加密

c = new p.RSAKey; c.setPublic(a.result.modulus, a.result.publicExponent); var d = c.encrypt(b.val());

该写如下:(js丫)

1
2
3
4
5
6
function getPwd(pwd) {
c = new p.RSAKey;
c.setPublic(a.result.modulus, a.result.publicExponent);
var d = c.encrypt(b.val());
return d;
}

虽然我们加密的函数已经找到了,but,我们是在自己的环境下并不一定有这个函数(c.encrypt)。所以现在需要去找c.encrypt 新问题:p.RSAKey;没有定义;回到chrome进入p.RSAKey-(选中点击进入f db()) 进入f db()扣出这个方法,然后改写 寻找a.result.modulus, a.result.publicExponent两个参数, 其实是publickey包返回的结果那么至此enpassword加密完成 补两个环境参数

1
2
window = this;
navigator = {};

fingerPrintfingerPrintDetail参数破解

link480 下断点点击下一步,运行 运行一步, 进入RSAfingerPrint函数内,把o.page.RSAfingerPrint方法抠出来 在页面中观察a,b参数 观察发现: 其实a,b,就是我们的a.result.modulus, a.result.publicExponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rsaFingerprint = function () {
a = "00833c4af965ff7a8409f8b5d5a83d87f2f19d7c1eb40dc59a98d2346cbb145046b2c6facc25b5cc363443f0f7ebd9524b7c1e1917bf7d849212339f6c1d3711b115ecb20f0c89fc2182a985ea28cbb4adf6a321ff7e715ba9b8d7261d1c140485df3b705247a70c28c9068caabbedbf9510dada6d13d99e57642b853a73406817";
b = "010001";
var c = $.fingerprint.details
, d = $.fingerprint.result
, e = c.length
, f = ""
, g = new m.RSAKey;
console.log(a, b)
g.setPublic(a, b);
for (var h = g.encrypt(d), i = 0; e > i; i += 117)
f += g.encrypt(c.substr(i, 117));
return {
details: f,
result: h
}
}
rsaFingerprint()

继续寻找;这两个

1
2
c = $.fingerprint.details 
d = $.fingerprint.result

浏览器里面测一下,把他从console拿出来

Other

本文预计阅读需3min

你好,我是你老朋友Payne,大家都或许过我之前写的水文-JS解密入门,没看过的童鞋开源回头看看啊。里面主要讲述了Hash MD5的例子,以及加密与解密,相关的。那么今天我们去搞一下MD5的“父亲”, Hash。主要阐述了什么是哈希,哈希运用方向以及hash碰撞及解决方向,请查阅

Hash算法

哈希

(hash)也叫散列。Hash算法,是将一个不定长的输入,通过散列函数变换成一个定长的输出,即散列值。

应用

Hash主要应用在数据结构以及密码学领域。

数据结构:

使用Hash算法的数据结构叫做哈希表,也叫散列表,主要是为了提高查询的效率。它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数就是hash function,存放记录的数组叫做哈希表。 在数据结构中应用时,有时需要较高的运算速度而弱化考虑抗碰撞性,可以使用自己构建的哈希函数。

密码学:

这种hash散列变换是一种单向运算,具有不可逆性即不能根据散列值还原出输入信息,因此严格意义上讲Hash算法是一种消息摘要算法,不是一种加密算法。常见的hash算法有:SM3、MD5、SHA-1等 。 在不同的应用场景下,hash函数的选择也会有所侧重。比如在管理数据结构时,主要要考虑运算的快速性,并且要保证hash均匀分布;而应用在密码学中就要优先考虑 抗碰撞性 ,避免出现两段不同明文hash值相同的情况发生。

哈希碰撞:

不同的值经过Hash function得到同一值则产生哈希碰撞防止哈希碰撞的最有效方法,就是扩大哈希值的取值空间

1
2
3
16个二进制位的哈希值,产生碰撞的可能性是 65536 分之一。如果有65537个用户,就一定会产生碰撞。哈希值的长度扩大到32个二进制位,碰撞的可能性就会下降到 4,294,967,296 分之一。

更长的哈希值意味着更大的存储空间、更多的计算,将影响性能和成本。开发者s必须做出抉择,在安全与成本之间找到平衡。

hash碰撞解决办法:

1. 开放地址法

  1. 线性探测再散列
  2. 二次探测再散列
  3. 伪随机探测再散列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入 。

    开放寻址法:Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:

    1). di=1,2,3,…,m-1,称线性探测再散列;

    2). di=1^2,(-1)^2,2^2,(-2)^2,(3)^2,…,±(k)^2,(k<=m/2)称二次探测再散列;

    3). di=伪随机数序列,称伪随机探测再散列。

    用开放定址法解决冲突的做法是:当冲突发生时,使用某种探测技术(线性探测法、二次探测法(解决线性探测的堆积问题)、随机探测法(和二次探测原理一致,不一样的是:二次探测以定值跳跃,而随机探测的散列地址跳跃长度是不定值))在散列表中形成一个探测序列。

    沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止插入即可。
  4. 再哈希法

再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数去计算地址,直到无冲突。 虽然不易发生聚集,但是增加了计算时间。

  1. 链地址法(Java hashmap)

链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,将所有关键字为同义词的结点链接在同一个单链表中,

  1. 建立公共溢出区

建立公共溢出区的基本思想是:假设哈希函数的值域是 [1,m-1],则设向量 HashTable[0…m-1] 为基本表, 每个分量存放一个记录,另外设向量 OverTable[0…v] 为溢出表,所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。

Python

你好,我是悦创。 本篇将开启我自己啃代理池的心得,将逐步放送,因为代理池搭建较为复杂,这里我就尽可能把代理池分成几篇来讲,同时也保证,在我其他篇放出来之前,每一篇都是你们的新知识。 学习就像看小说一样,一次一篇就会显得额外的轻松! 当你把学习当作某个娱乐的事情来做,你会发现不一样的世界! 我们无法延长生命长度,但是我们延长生命宽度,学习编程就是扩展生命最有力的武器!

1. 看完之后你会得到什么

  • 返回 yield;
  • eval 的使用;
  • 多个代理网站同时抓取;
  • 使用异步测试代理是否可用;
  • Python 的元类编程简单介绍;
  • 正则表达式、PyQuery 提取数据;
  • 模块化编程;

废话不多说,马上步入正题!

2. 你需要的准备

在学习本篇文章时,希望你已经具备如下技能或者知识点:

  1. Python 环境(推荐 Python 3.7+);
  2. Python 爬虫常用库;
  3. Python 基本语法;
  4. 面向对象编程;
  5. yield、eval 的使用;
  6. 模块化编程;

3. 课前预习知识点

对于代理池的搭建呢,虽然我已经尽可能地照顾到绝大多数地小白,把代理地的搭建呢,进行了拆分,不过对于培训机构或者自学不是特别精进的小伙伴来说还是有些难度的,对于这些呢?我这里也给大家准备了知识点的扫盲。以下内容节选自我个人博客文章,这里为了扫盲就提供必要部分,想看完整的文章可以点击此链接:https://www.aiyc.top/archives/605.html

3.1 Python 的元类编程

你好,我是悦创。 好久不见,最近在啃数学、Java、英语没有来更新公众号,那么今天就来更新一下啦! 又到了每日一啃,啃代码的小悦。今天我遇到了 Python 原类,然而我啥也不懂。只能靠百度和谷歌了,主要还是用谷歌来查啦,百度前几条永远是广告准确度也不行(个人观点),也顺便参考了几个博客:廖雪峰网站,添加了一点自己的观点和理解。

3.1.1 type()

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。 比方说我们要定义一个 Hello 的 class,就写一个 hello.py 模块(这里我实际创建的是:the_test_code_project.py 模块):

1
2
3
class Hello(object):
def hello(self, name='world'):
print('Hello, %s.' % name)

当 Python 解释器载入 hello 模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个 Hello 的 class 对象,测试如下:

1
2
3
4
5
if __name__ == '__main__':
h = Hello()
h.hello()
print(type(Hello))
print(type(h))

运行结果如下:

1
2
3
Hello, world.
<class 'type'>
<class '__main__.Hello'>

其中,上面的输出结果:__main__.Hello 等价于 <class 'the_test_code_project.Hello'> 运行的方式不同显示的方式也不同,但含义是一样的。 type() 函数可以查看一个类型或变量的类型,为了让小白更轻松,我也写了个例子:

1
2
3
4
5
6
7
8
9
10
11
12
number = 12
string = 'Hello AIYC!'
float_number = 12.1
list_data = [1, '2', 3.5, 'AIYC'] # 可变
tuples = (1, '2', 3.5, 'AIYC') # 不可变

if __name__ == '__main__':
print(type(number))
print(type(string))
print(type(float_number))
print(type(list_data))
print(type(tuples))

运行结果:

1
2
3
4
5
<class 'int'>
<class 'str'>
<class 'float'>
<class 'list'>
<class 'tuple'>

Hello 是一个 class,它的类型就是 type ,而 h 是一个实例,它的类型就是class Hello。 我们说 class 的定义是运行时动态创建的,而创建 class 的方法就是使用 type() 函数。 type() 函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过 type() 函数创建出Hello 类,而无需通过 class Hello(object)... 的定义:

1
2
3
4
5
6
def fn(self, name='world'): # 先定义函数
print('Hello, %s.' % name)

Hello = type('Hello', (object,), dict(hello=fn)) # 创建 Hello class
# Hello = type('Class_Name', (object,), dict(hello=fn)) # 创建 Hello class
# type(类名, 父类的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))

我们接下来来调用一下代码,看输出的结果如何:

1
2
3
4
5
if __name__ == '__main__':
h = Hello()
h.hello()
print(type(Hello))
print(type(h))

这里推荐写成:if __name__ == '__main__': 使代码更加的规范。 运行结果:

1
2
3
Hello, world.
<class 'type'>
<class '__main__.Hello'>

要创建一个 class 对象,type() 函数依次传入 3 个参数:

  1. class 的名称;
  2. 继承的父类集合,注意 Python 支持多重继承,如果只有一个父类,别忘了 tuple 的单元素写法;(这个个 tuple 单元素写法起初本人不太理解,然后一查并认真观察了一下上面的代码就想起来 tuple 单元素写法需要加逗号(,),就是你必须这么写:tuple_1 = (1,) 而不能这么写:tuple_2 = (1)tuple_2 = (1) 的写法,Python 会自动认为是一个整数而不是一个元组)
  3. class 的方法名称与函数绑定,这里我们把函数 fn 绑定到方法名 hello 上。

通过 type() 函数创建的类和直接写 class 是完全一样的,因为 Python 解释器遇到 class 定义时,仅仅是扫描一下 class 定义的语法,然后调用 type() 函数创建出 class。(直接 Class 创建也是) 正常情况下,我们都用 class Xxx... 来定义类,但是,type() 函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。

3.1.2 metaclass

除了使用 type() 动态创建类以外,要控制类的 创建行为 ,还可以使用 metaclass。 metaclass,直译为元类,简单的解释就是:

  • 当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。

image-20200602213149088

  • 但是如果我们想创建出类呢?

那就必须根据 metaclass 创建出类,所以:先定义 metaclass ,然后创建类。连接起来就是:先定义 metaclass ,就可以创建类,最后创建实例。 所以,metaclass 允许你创建类或者修改类 。换句话说,你可以把类看成是 metaclass 创建出来的“实例”。

metaclass 是 Python 面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况,所以,以下内容看不懂也没关系,因为基本上你不会用到。(然而还是被我遇见了,而且还是看不懂,但经过大佬的指点就只是知道如何使用,但并不了解其中的原理,所以才有了此篇。)

我们先看一个简单的例子,这个 metaclass 可以给我们自定义的 MyList 增加一个 add 方法:

1
2
3
4
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value) # 加上新的方法
return type.__new__(cls, name, bases, attrs) # 返回修改后的定义

定义 ListMetaclass ,按照默认习惯,metaclass 的类名总是以 Metaclass 结尾,以便清楚地表示这是一个metaclass 。

有了 ListMetaclass ,我们在定义类的时候还要指示使用 ListMetaclass 来定制类,传入关键字参数 metaclass

1
2
class MyList(list, metaclass=ListMetaclass):
pass

当我们传入关键字参数 metaclass 时,魔术就生效了,它指示 Python 解释器在创建 MyList 时,要通过ListMetaclass.__new__() 来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。 __new__() 方法接收到的参数依次是:

  1. 当前准备创建的类的对象;
  2. 类的名字;
  3. 类继承的父类集合;
  4. 类的方法集合。

测试一下 MyList 是否可以调用 add() 方法:

1
2
3
4
5
6
L = MyList()
L.add(1)
print(L)

# 输出
[1]

而普通的 list 没有 add() 方法:

1
2
3
4
5
\>>> L2 = list()
>>> L2.add(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'

这时候,我想你应该和我会有同样的问题,动态修改有什么意义? 直接在 MyList 定义中写上 add() 方法不是更简单吗? 正常情况下,确实应该直接写,我觉得通过 metaclass 修改纯属变态。

3.2 Python eval() 函数

对于 Python 的 eval 呢,大部分人是这么定义的:eval() 函数用来执行一个字符串表达式,并返回表达式的值。 这句话,有可能对于新手来说并不是非常好理解,我们还是用例子来顿悟一下吧。 以下是 eval() 方法的语法:

1
eval(expression[, globals[, locals]])

参数

  • expression — 表达式。
  • globals — 变量作用域,全局命名空间,如果被提供,则必须是一个字典对象。
  • locals — 变量作用域,局部命名空间,如果被提供,可以是任何映射对象。

返回值 返回表达式计算结果。 实际操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [3]: x = 7

In [4]: eval( '3 * x' )
Out[4]: 21

In [5]: eval('pow(2,2)')
Out[5]: 4

In [6]: eval('2 + 2')
Out[6]: 4

In [7]: n=81

In [8]: eval("n + 4")
Out[8]: 85

再来个函数的操作:

1
2
3
4
In [1]: str1 = "print('Hello World')"

In [2]: eval(str1)
Hello World

ok,到此零基础小白关怀文章就完成了,我就不继续赘述啦! 看到这里,你们的身体还行吗? 真叫人头大 我正在的干货要开始了! 进入我们的 show time 环节,起飞了、起飞了,做好准备了!

4. 抓取免费代理

这里呢,我就不再带带大家手摸手的教学如何抓取代理了,因为当你能看这篇文章时,相信你已经对爬虫是有所入门了,如果还没入门的小伙伴可以关注本公众号,往期文章也有零基础教学和基础的课程资源,可以公众号后台回复:Python爬虫。即可获取资源,如果失效也别担心加小悦好友即可!(资源多多噢!)

小白不建议报名培训机构,毕竟现在培训结构收智商税比较多,还是需要多多鉴别噢!

4.1 目标的代理网站

  1. 快代理:https://www.kuaidaili.com/
  2. 西刺代理:https://www.xicidaili.com
  3. 66代理:http://www.66ip.cn/
  4. 无忧代理:http://www.data5u.com
  5. 开心代理-高匿:http://www.kxdaili.com/dailiip/
  6. 云代理:http://www.ip3366.net/free/

对于每个网站的代码呢,具体实现也是比较简单的,这里我就不做过多的赘述啦! 接下来我都直接上代码,不过在上代理的爬虫代码前,我们需要来写个元类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ProxyMetaclass(type):
"""
定义 ProxyMetaclass ,按照默认习惯,metaclass 的类名总是以 Metaclass 结尾,以便清楚地表示这是一个metaclass :
元类,在 FreeProxyGetter 类中加入
CrawlFunc_list 和 CrawlFuncCount
两个参数,分别表示爬虫函数,和爬虫函数的数量。
"""
def __new__(cls, name, bases, attrs):
count = 0
attrs['CrawlFunc_List'] = [] # 添加:CrawlFunc_List 列表方法
for k, v in attrs.items():
if 'crawl_' in k:
# 判断这个函数里面是否携带 crawl_ 也就是利用 value in xxx
attrs['CrawlFunc_List'].append(k)
count += 1
attrs['CrawlFuncCount'] = count # 检测添加的函数的总数量
return type.__new__(cls, name, bases, attrs) # 返回所修改的

详细的代码含义,已经写在上面了,具体的这里 就不赘述了,如果泥有任何不理解的可以去点击阅读原文在我的博客网站下留言,和后台回复数字“3”,加小编好友,拉你入交流群。

4.2 代码编写

4.2.1 请求函数编写

单独保存成一个文件:utils.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
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
"""
project = 'Code', file_name = 'utils.py', author = 'AI悦创'
time = '2020/6/1 12:34', product_name = PyCharm, 公众号:AI悦创
code is far away from bugs with the god animal protecting
I love animals. They taste delicious.
"""
import requests
import asyncio
import aiohttp
from requests.exceptions import ConnectionError
from fake_useragent import UserAgent,FakeUserAgentError
import random

def get_page(url, options={}):
"""
构造随机请求头,如果不理解的可以阅读此文章:
两行代码设置 Scrapy UserAgent:https://www.aiyc.top/archives/533.html
"""
try:
ua = UserAgent()
except FakeUserAgentError:
pass
# 生成随机的请求头,加 try...except... 使代码更加健壮
base_headers = {
'User-Agent': ua.random,
'Accept-Encoding': 'gzip, deflate, sdch',
'Accept-Language': 'zh-CN,zh;q=0.8'
}
# 如果使用者有传入请求头,则将此请求头和随机生成的合成在一起
headers = dict(base_headers, **options)
# 当前请求的 url
print('Getting', url)
try:
r = requests.get(url, headers=headers)
print('Getting result', url, r.status_code)
if r.status_code == 200:
return r.text
return None
except ConnectionError:
print('Crawling Failed', url)
return None

class Downloader(object):
"""
一个异步下载器,可以对代理源异步抓取,但是容易被 BAN。
self._htmls: 把请求的 html 放入列表当中
升级版的请求类
"""

def __init__(self, urls):
self.urls = urls
self._htmls = []

async def download_single_page(self, url):
"""
下载单页
"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
self._htmls.append(await response.text())

def download(self):
loop = asyncio.get_event_loop()
tasks = [self.download_single_page(url) for url in self.urls]
loop.run_until_complete(asyncio.wait(tasks))

@property
def htmls(self):
self.download()
return self._htmls

上面请求函数编写完成之后,我们就需要在抓取代理的代码文件中,进行导包,代码如下:

1
2
# files:Spider.py
from utils import get_page

4.2.2 代理抓取代码

导入所需要的库:

1
2
3
4
import requests
from utils import get_page
from pyquery import PyQuery as pq
import re

接下来我们需要创建一个类 FreeProxyGetter 并使用元类创建。

1
2
class FreeProxyGetter(object, metaclass=ProxyMetaclass):
pass
1. 快代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def crawl_kuaidaili(self):
"""
快代理
:return:
"""
for page in range(1, 4):
# 国内高匿代理
start_url = 'https://www.kuaidaili.com/free/inha/{}/'.format(page)
html = get_page(start_url)
# print(html)
pattern = re.compile(
'<td data-title="IP">(.*)</td>s*<td data-title="PORT">(w+)</td>'
)
# s * 匹配空格,起到换行作用
# ip_addres = re.findall(pattern, html) # 写法一
ip_addres = pattern.findall(str(html))
for adress, port in ip_addres:
# print(adress, port)
result = f"{adress}:{port}".strip()
yield result
2. 西刺代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def crawl_xicidaili(self):
"""
西刺代理
:return:
"""
for page in range(1, 4):
start_url = 'https://www.xicidaili.com/wt/{}'.format(page)
html = get_page(start_url)
# print(html)
ip_adress = re.compile(
'<td class="country"><img src="//fs.xicidaili.com/images/flag/cn.png" alt="Cn" /></td>s*<td>(.*?)</td>s*<td>(.*?)</td>'
)
# s* 匹配空格,起到换行作用
re_ip_adress = ip_adress.findall(str(html))
for adress, port in re_ip_adress:
result = f"{adress}:{port}".strip()
yield result
3. 66代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def crawl_daili66(self, page_count=4):
"""
66代理
:param page_count:
:return:
"""
start_url = 'http://www.66ip.cn/{}.html'
urls = [start_url.format(page) for page in range(1, page_count + 1)]
for url in urls:
print('Crawling', url)
html = get_page(url)
if html:
doc = pq(html)
trs = doc('.containerbox table tr:gt(0)').items()
for tr in trs:
ip = tr.find('td:nth-child(1)').text()
port = tr.find('td:nth-child(2)').text()
yield ':'.join([ip, port])
4. 无忧代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def crawl_data5u(self):
"""
无忧代理
:return:
"""
start_url = 'http://www.data5u.com'
html = get_page(start_url)
# print(html)
ip_adress = re.compile(
'<ul class="l2">s*<span><li>(.*?)</li></span>s*<span style="width: 100px;"><li class=".*">(.*?)</li></span>'
)
# s * 匹配空格,起到换行作用
re_ip_adress = ip_adress.findall(str(html))
for adress, port in re_ip_adress:
result = f"{adress}:{port}"
yield result.strip()
5. 开心代理-高匿
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def crawl_kxdaili(self):
"""
开心代理-高匿
:return:
"""
for i in range(1, 4):
start_url = 'http://www.kxdaili.com/dailiip/1/{}.html'.format(i)
try:
html = requests.get(start_url)
if html.status_code == 200:
html.encoding = 'utf-8'
# print(html.text)
ip_adress = re.compile('<tr.*?>s*<td>(.*?)</td>s*<td>(.*?)</td>')
# s* 匹配空格,起到换行作用
re_ip_adress = ip_adress.findall(str(html.text))
for adress, port in re_ip_adress:
result = f"{adress}:{port}"
yield result.strip()
return None
except:
pass
6. 云代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def IP3366Crawler(self):
"""
云代理
parse html file to get proxies
:return:
"""
start_url = 'http://www.ip3366.net/free/?stype=1&page={page}'
urls = [start_url.format(page=i) for i in range(1, 8)]
# s * 匹配空格,起到换行作用
ip_address = re.compile('<tr>s*<td>(.*?)</td>s*<td>(.*?)</td>')
for url in urls:
html = get_page(url)
re_ip_address = ip_address.findall(str(html))
for adress, port in re_ip_address:
result = f"{adress}:{port}"
yield result.strip()

至此,代理网站的代码已经全部编写完成,接下来我们需要了解的是然后调用此类,直接调用吗? 显然不是,我们还需要编写一个运行调用的函数,代码如下:

1
2
3
4
5
6
7
8
9
10
def run(self):
# print(self.__CrawlFunc__)
proxies = []
callback = self.CrawlFunc_List
for i in callback:
print('Callback', i)
for proxy in eval("self.{}()".format(i)):
print('Getting', proxy, 'from', i)
proxies.append(proxy)
return proxies

以上代理的代码中,我并没有抓取每个网站全部页数,如果有需要可以自行调节。 这里我们可以直接写一个调用代码,来测试代码是否正常,写在:Spider.py 也就是代理的代码中,代码如下:

1
2
3
if __name__ == '__main__':
Tester = FreeProxyGetter()
Tester.run()

运行结果如下: 结果较多,省略大部分结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Getting 14.115.70.177:4216 from crawl_xicidaili
Getting 116.22.48.220:4216 from crawl_xicidaili
Getting 223.241.3.120:4216 from crawl_xicidaili
......
Getting 60.188.1.27:8232 from crawl_data5u
Callback crawl_kxdaili
Getting 117.71.165.208:3000 from crawl_kxdaili
Getting 223.99.197.253:63000 from crawl_kxdaili
Getting 60.255.186.169:8888 from crawl_kxdaili
Getting 180.168.13.26:8000 from crawl_kxdaili
Getting 125.124.51.226:808 from crawl_kxdaili
Getting 47.99.145.67:808 from crawl_kxdaili
Getting 222.186.55.41:8080 from crawl_kxdaili
Getting 36.6.224.30:3000 from crawl_kxdaili
Getting 110.43.42.200:8081 from crawl_kxdaili
Getting 39.137.69.8:80 from crawl_kxdaili

ok,至此代理成功抓取,接下来就是我们的异步检测了。

4.2.3 异步检测

这里,我们需要编写一个 error.py 来自定义包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ResourceDepletionError(Exception):

def __init__(self):
Exception.__init__(self)

def __str__(self):
return repr('The proxy source is exhausted')

class PoolEmptyError(Exception):

def __init__(self):
Exception.__init__(self)

def __str__(self):
return repr('The proxy pool is empty')

异步检测代码,这里我就不做任何数据存储,有需求的可以自行添加,下一篇将添加一个存入 redis 数据库的,敬请关注!

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# files:schedule.py
import asyncio
import time
from multiprocessing.context import Process

import aiohttp
from db import RedisClient
from Spider import FreeProxyGetter
from error import ResourceDepletionError
from aiohttp import ServerDisconnectedError, ClientResponseError, ClientConnectorError
from socks import ProxyConnectionError

get_proxy_timeout = 9

class ValidityTester(object):
"""
代理的有效性测试
"""
def __init__(self):
self.raw_proxies = None
self.usable_proxies = [] # 可用代理
self.test_api = 'http://www.baidu.com'

def set_raw_proxies(self, proxies):
self.raw_proxies = proxies # 临时存储一些代理

async def test_single_proxy(self, proxy):
"""
python3.5 之后出现的新特性
text one proxy, if valid, put them to usable_proxies.
"""
try:
async with aiohttp.ClientSession() as session:
try:
if isinstance(proxy, bytes):
proxy = proxy.decode('utf-8')
real_proxy = 'http://' + proxy
print('Testing', proxy)
async with session.get(self.test_api, proxy=real_proxy, timeout=get_proxy_timeout) as response:
if response.status == 200:
print('Valid proxy', proxy)
except (ProxyConnectionError, TimeoutError, ValueError):
print('Invalid proxy', proxy)
except (ServerDisconnectedError, ClientResponseError, ClientConnectorError) as s:
print(s)
pass

def test(self):
"""
aio test all proxies.
"""
print('ValidityTester is working')
try:
loop = asyncio.get_event_loop()
tasks = [self.test_single_proxy(proxy) for proxy in self.raw_proxies]
loop.run_until_complete(asyncio.wait(tasks))
except ValueError:
print('Async Error')

class Schedule(object):
@staticmethod
def valid_proxy():
"""
Get half of proxies which in Spider
"""
tester = ValidityTester()
free_proxy_getter = FreeProxyGetter()
# 获取 Spider 数据库数据
# 调用测试类
tester.set_raw_proxies(free_proxy_getter.run())
# 开始测试
tester.test()

def run(self):
print('Ip processing running')
valid_process = Process(target=Schedule.valid_proxy) # 获取代理并筛选
valid_process.start()
if __name__ == '__main__':
schedule = Schedule().run()

对于上面的部分代理代码,我为了输出结果更加直观,把 print() 输出的,包括 utils.pyprint() 全部注释了, 运行结果(省略部分输出)

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Testing 182.46.252.84:9999
Testing 183.63.188.250:8808
Testing 39.137.69.10:8080
Testing 119.190.149.226:8060
Testing 219.159.38.201:56210
Testing 58.246.143.32:8118
Testing 114.99.117.13:3000
Testing 114.229.229.248:8118
Testing 119.57.108.53:53281
Testing 223.247.94.164:4216
Testing 219.159.38.208:56210
Testing 59.44.78.30:42335
Testing 112.74.87.172:3128
Testing 182.92.235.109:3128
Testing 58.62.115.3:4216
Testing 183.162.171.37:4216
Testing 223.241.5.46:4216
Testing 223.243.4.231:4216
Testing 223.241.5.228:4216
Testing 183.162.171.56:4216
Testing 223.241.4.146:4216
Testing 223.241.4.200:4216
Testing 223.241.4.182:4216
Testing 118.187.50.114:8080
Testing 223.214.176.170:3000
Testing 125.126.123.208:60004
Testing 163.125.248.39:8088
Testing 119.57.108.89:53281
Testing 222.249.238.138:8080
Testing 125.126.97.77:60004
Testing 222.182.54.210:8118
Valid proxy 58.220.95.90:9401
Valid proxy 60.191.45.92:8080
Valid proxy 222.186.55.41:8080
Valid proxy 60.205.132.71:80
Valid proxy 124.90.49.146:8888

Valid proxy 218.75.109.86:3128
Valid proxy 27.188.64.70:8060

Valid proxy 27.188.62.3:8060
Valid proxy 221.180.170.104:8080
Valid proxy 39.137.69.9:8080

Valid proxy 39.137.69.6:80
Valid proxy 39.137.69.10:8080
Valid proxy 114.229.6.215:8118
Valid proxy 221.180.170.104:8080
Valid proxy 218.89.14.142:8060
Valid proxy 116.196.85.150:3128

Valid proxy 122.224.65.197:3128
Valid proxy 112.253.11.103:8000
Valid proxy 112.253.11.103:8000
Valid proxy 112.253.11.113:8000
Valid proxy 124.90.52.200:8888

Cannot connect to host 113.103.226.99:4216 ssl:default [Connect call failed ('113.103.226.99', 4216)]
Cannot connect to host 36.249.109.59:8221 ssl:default [Connect call failed ('36.249.109.59', 8221)]
Cannot connect to host 125.126.121.66:60004 ssl:default [Connect call failed ('125.126.121.66', 60004)]
Cannot connect to host 27.42.168.46:48919 ssl:default [Connect call failed ('27.42.168.46', 48919)]
Cannot connect to host 163.125.113.220:8118 ssl:default [Connect call failed ('163.125.113.220', 8118)]
Cannot connect to host 114.55.63.34:808 ssl:default [Connect call failed ('114.55.63.34', 808)]
Cannot connect to host 119.133.207.10:4216 ssl:default [Connect call failed ('119.133.207.10', 4216)]
Cannot connect to host 58.62.115.3:4216 ssl:default [Connect call failed ('58.62.115.3', 4216)]
Cannot connect to host 117.71.164.232:3000 ssl:default [Connect call failed ('117.71.164.232', 3000)]

到此,多站抓取异步检测,就已经完全实现,也是实现了模块化编程。我也 顺便把项目结构分享一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
C:.
error.py
info.txt
schedule.py
Spider.py
the_test_code_crawler.py
utils.py

└─__pycache__
db.cpython-37.pyc
error.cpython-37.pyc
Spider.cpython-37.pyc
utils.cpython-37.pyc

本篇我想你主要学习到的目标也就是开头所说的,这里就不赘述了,下一篇我将带大家把抓取到可用的代理进行存储到 redis 数据库中,尽请关注! 源代码文件获取:公众号后台回放:ProxyPool-1 也欢迎加入我的交流群,一起交流!

技术杂谈

之前开发了一个工具包 GerapyPyppeteer,GitHub 地址为 https://github.com/Gerapy/GerapyPyppeteer,这个包实现了 Scrapy 和 Pyppeteer 的对接,利用它我们就可以方便地实现 Scrapy 使用 Pyppeteer 爬取动态渲染的页面了。 另外,很多朋友在运行爬虫的时候可能会使用到 Docker,想把 Scrapy 和 Pyppeteer 打包成 Docker 运行,但是这个打包和测试过程中大家可能会遇到一些问题,在这里对 Pyppeteer 打包 Docker 的坑简单做一下总结。

概述

Pyppeteer 打包 Docker 主要是有这么几个坑点:

  • 依赖没有安装,导致无法正确安装和启动 Pyppeteer。
  • 没有关闭沙盒模式,导致可能出现 Browser closed unexpectedly 错误
  • 没有提前安装好 Pyppeteer,导致每次启动时都要重新安装

下面我们分别对三个问题做下简单的 Troubleshooting。

安装依赖

首先说第一个,安装依赖。 因为 Docker 大部分都是基于 Linux 系统的,比如我常用的基础镜像就是 python:3.7,剩余 Debian 系列,当然还有很多其他的版本,具体可以查看 https://hub.docker.com/_/python 了解下。 但是对于 Pyppeteer 来说,python:3.7 内置的依赖库并不够,我们还需要额外进行安装,安装完毕之后还需要清空下 apt list,一句 Dockerfile 命令如下:

1
2
3
4
5
6
7
RUN apt-get update && 
apt-get -y install libnss3 xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2
libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget &&
rm -rf /var/lib/apt/lists/*

关闭沙盒模式

在 Docker 中如果直接启动 Pyppeteer,我们还需要关闭沙盒模式,否则可能会遇到如下错误:

1
2
pyppeteer.errors.BrowserError: Browser closed unexpectedly:
[0924/153706.301300:ERROR:zygote_host_impl_linux.cc(89)] Running as root without --no-sandbox is not supported. See https://crbug.com/638180

这里提示我们要关闭沙盒模式,这里只需要在启动 Pyppeteer 的时候,给 launch 方法的 args 参数多加一个 \--no-sandbox 即可,写法如下:

1
browser = await pyppeteer.launch(options={'args': ['--no-sandbox']})

这样就不会再遇到上面的错误了。

提前安装

另外建议在打包 Docker 的时候就提前把 Pyppeteer 提前安装好,可以单独使用一句 RUN 命令安装即可。

1
RUN pip install -U pip && pip install pyppeteer && pyppeteer-install

这里是提前安装了一下 Pyppteer 这个 Python 库,然后利用 Python 库提供的 pyppeteer-install 命令提前下载了 Chromium 浏览器。 这样后面启动的时候就可以直接唤起 Chromium 浏览器进行爬取了。

总结

最后看下完整 Dockerfile,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM python:3.7

RUN apt-get update &&
apt-get -y install libnss3 xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2
libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget &&
rm -rf /var/lib/apt/lists/*

RUN pip install -U pip && pip install pyppeteer && pyppeteer-install

WORKDIR /code
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD python3 run.py

这里首先就是安装了必须的依赖库,然后安装了 Pyppeteer 并下载了 Chromium 浏览器,最后拷贝项目运行即可。 当然最后的一句 CMD 大家可以随意指定入口。 最后大家可以体验一个实例来感受下 Scrapy 和 Pyppeteer 对接后在 Docker 中的运行效果:

1
docker run germey/gerapy-pyppeteer-example

如果大家对 Scrapy 和 Pyppeteer 感兴趣也可以看下我写的这个库 GerapyPyppeteer,GitHub 地址为 https://github.com/Gerapy/GerapyPyppeteer,感谢支持

技术杂谈

解析页面是做爬虫的过程中的重要环节,而且如果站点多了,解析也会变得非常复杂,所以智能化解析就可能是一个不错的解决方案。如果我们能够容忍一定的错误率,那么我们可以利用智能化解析算法帮我们提取一些内容,简单高效。 那有没有办法做到一个网站的全自动化解析呢? 比如来了一个博客网站,我能首先识别出来这是一个列表页还是文章(详情)页,然后提取列表页的每篇文章的链接,然后跳转到每篇文章(详情)页再提取文章相关信息。 那么这里面可能就有四个关键部分:

  • 判断当前所在的页面是列表页还是文章(详情)页
  • 识别出列表页下一页的链接
  • 识别出列表页所有列表链接
  • 识别出文章(详情)页的文章内容和其他信息

如果我们能把这四步都用算法实现出来,那么我们只需要一个网站的主站链接就能轻松地把内容规整地爬取下来了。 那么这篇文章我们就来简单说下第一步,如何判断当前所在的页面的列表页还是文章(详情)页。

注:后文中文章页统一称之为详情页。

示例

列表页和详情页不知道大家有没有基本的概念了,列表页就是导航页,里面带有好多文章或新闻或详情链接,我们选一个链接点进去就是详情页。 比如说这里拿新浪体育来说,首页如图所示: image-20200720193407237 看到这里面有很多链接,就是一些页面导航集合,这个页面就是列表页。 然后我们随便点开一篇新闻报道,如图所示: image-20200720193504042 这里就是一篇新闻报道,带有醒目的标题、发布时间、正文等内容,这就是详情页。 现在我们要做的就是用一个算法来凭借 HTML 代码区分出来哪个是列表页,哪个是详情页。 最后的输入输出如下:

  • 输入:一个页面的 HTML 代码
  • 输出:这个页面是列表页还是详情页,并输出二者的判定概率。

模型选用

首先我们确认下这个问题是个什么问题。 很明显,结果要么是列表页,要么是详情页,就是个二分类问题。 那二分类问题怎么解决呢?实现一个基本的分类模型就好了。大范围就是传统机器学习和现在比较流行的深度学习。总体上来说,深度学习的精度和处理能力会强一点,但是想想我们现在的应用场景,后者要追求精度的话可能需要更多的标注数据,而前者也有比较不错的易用的模型了,比如 SVM。 所以,我们不妨先选用 SVM 模型来实现一个基本的二分类模型来试试看,效果如果已经很好了或者提升空间不大了,那就直接用就好了,如果效果比较差,那我们再选用其他模型来优化。 好,那就定下来了,我们用 SVM 模型来实现一下试试。

数据标注

既然要做分类模型,那么最重要的当然就是数据标注了,我们分两组就好了,一组是列表页,一组是详情页,我们先用手工配合爬虫找一些列表页和详情页的 HTML 代码,然后将其保存下来。 结果类似如下: image-20200720195132784 每个文件夹几百个就行了,数量不用太多,五花八门的页面混起来更好。

特征提取

既然要做 SVM,那么我们得想清楚要分清两个类别需要哪些特征。既然是特征,那我们就要选出二者不同的特征,这样更加有区分度。 比如这里我大体总结了有这么几个特征:

  • 文本密度:正文页通常会包含密集的文字,比如一个 p 节点内部就包含几十上百个文字,如果用单个节点内的文字数目来表示文本密度的话,那么详情页的部分内容文本密度会很高。
  • 超链接节点的数量和比例:一般来说列表页通常会包含多个超链接,而且很大比例都是超链接文本,而详情页却有很多的文字并不是超链接,比如正文。
  • 符号密度:一般来说列表页通常会是一些标题导航,一般可能都不会包含句号,而详情页的正文内容通常就会包含句号等内容,如果按照单位文字所包含的标点符号数量来表示符号密度的话,后者的符号密度也会高一些。
  • 列表簇的数目:一般来说,列表页通常会包含多个具有公共父节点的条目,多个条目构成一个列表簇,虽然说详情页侧栏也会包含一些列表,但至少这个数量也可以成为一个特征来判别。
  • meta 信息:有一些特殊的 meta 信息是列表页独有的,比如只有详情页才会有发布时间,而列表页通常是没有的。
  • 正文标题和 title 标题相似度:一般来说,详情页的正文标题和 title 标题很可能是相同的内容,而列表页通常则是站点的名称。

以上便是我简单列的几个特征,还有很多其他的特征也可以来挖掘,比如视觉特征等等。

模型训练

真正代码实现的过程就是将现有的 HTML 文本进行预处理,把上面的一些特征提取出来,然后直接声明一个 SVM 分类模型即可。 这里声明了一个 feature 名字和对应的处理方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
self.feature_funcs = {
'number_of_a_char': number_of_a_char,
'number_of_a_char_log10': self._number_of_a_char_log10,
'number_of_char': number_of_char,
'number_of_char_log10': self._number_of_char_log10,
'rate_of_a_char': self._rate_of_a_char,
'number_of_p_descendants': number_of_p_descendants,
'number_of_a_descendants': number_of_a_descendants,
'number_of_punctuation': number_of_punctuation,
'density_of_punctuation': density_of_punctuation,
'number_of_clusters': self._number_of_clusters,
'density_of_text': density_of_text,
'max_density_of_text': self._max_density_of_text,
'max_number_of_p_children': self._max_number_of_p_children,
'has_datetime_meta': self._has_datetime_mata,
'similarity_of_title': self._similarity_of_title,
}
self.feature_names = self.feature_funcs.keys()

以上方法就是特征和对应的获取方法,具体根据实际情况实现即可。 然后关键的部分就是对数据的处理和模型的训练了,关键代码如下:

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
list_file_paths = list(glob(f'{DATASETS_LIST_DIR}/*.html'))
detail_file_paths = list(glob(f'{DATASETS_DETAIL_DIR}/*.html'))

x_data, y_data = [], []

for index, list_file_path in enumerate(list_file_paths):
logger.log('inspect', f'list_file_path {list_file_path}')
element = file2element(list_file_path)
if element is None:
continue
preprocess4list_classifier(element)
x = self.features_to_list(self.features(element))
x_data.append(x)
y_data.append(1)

for index, detail_file_path in enumerate(detail_file_paths):
logger.log('inspect', f'detail_file_path {detail_file_path}')
element = file2element(detail_file_path)
if element is None:
continue
preprocess4list_classifier(element)
x = self.features_to_list(self.features(element))
x_data.append(x)
y_data.append(0)

# preprocess data
ss = StandardScaler()
x_data = ss.fit_transform(x_data)
joblib.dump(ss, self.scaler_path)
x_train, x_test, y_train, y_test = train_test_split(x_data, y_data, test_size=0.2, random_state=5)

# set up grid search
c_range = np.logspace(-5, 20, 5, base=2)
gamma_range = np.logspace(-9, 10, 5, base=2)
param_grid = [
{'kernel': ['rbf'], 'C': c_range, 'gamma': gamma_range},
{'kernel': ['linear'], 'C': c_range},
]
grid = GridSearchCV(SVC(probability=True), param_grid, cv=5, verbose=10, n_jobs=-1)
clf = grid.fit(x_train, y_train)
y_true, y_pred = y_test, clf.predict(x_test)
logger.log('inspect', f'n{classification_report(y_true, y_pred)}')
score = grid.score(x_test, y_test)
logger.log('inspect', f'test accuracy {score}')
# save model
joblib.dump(grid.best_estimator_, self.model_path)

这里首先对数据进行预处理,然后将每个 feature 存 存到 x_data 中,标注结果存到 y_data 中。接着我们使用 StandardScaler 对数据进行标准化处理,然后进行随机切分。最后使用 GridSearch 训练了一个 SVM 模型然后保存了下来。 以上便是基本的模型训练过程,具体的代码可以再完善一下。

使用

以上的流程我已经实现了,并且发布了一个开源 Python 包,名字叫做 Gerapy AutoExtractor,GitHub 地址为 https://github.com/Gerapy/GerapyAutoExtractor。 大家如需使用可以使用 pip 安装:

1
pip3 install gerapy-auto-extractor

这个库针对于以上算法提供了四个方法:

  • is_detail:判断是否是详情页
  • is_list:判断是否是列表页
  • probability_of_detail:是详情页的概率,结果是 0-1
  • probability_of_list:是列表页的概率,结果是 0-1

例如,我们随便可以找几个网址存下来,比如把上文的列表页和详情页的 HTML 代码存下来分别保存为 list.html 和 detail.html。 测试代码如下:

1
2
3
4
5
6
7
8
9
10
from gerapy_auto_extractor import is_detail, is_list, probability_of_detail, probability_of_list
from gerapy_auto_extractor.helpers import content, jsonify

html = content('detail.html')
print(probability_of_detail(html), probability_of_list(html))
print(is_detail(html), is_list(html))

html = content('list.html')
print(probability_of_detail(html), probability_of_list(html))
print(is_detail(html), is_list(html))

这里我们就调用了以上四个方法来实现了页面类型和置信度的判断。 类似的输出结果如下:

1
2
3
4
0.9990605314033392 0.0009394685966607814
True False
0.033477426883441685 0.9665225731165583
False True

这样我们就成功得到了两个页面的类别和置信度了。 以上便是判断列表页和详情页的原理和实现,如需了解更多请关注项目 Gerapy Auto Extractor,GitHub 链接为 https://github.com/Gerapy/GerapyAutoExtractor,多谢支持

技术杂谈

Airtest

首先需要安装 Airtest,使用 pip3 即可:

1
pip3 install airtest

初始化 device

如果设备没有被初始化的话会进行初始化,并把初始化的设备作为当前设备。 用法如下:

1
2
3
4
5
6
7
8
9
def init_device(platform="Android", uuid=None, **kwargs):
"""
Initialize device if not yet, and set as current device.

:param platform: Android, IOS or Windows
:param uuid: uuid for target device, e.g. serialno for Android, handle for Windows, uuid for iOS
:param kwargs: Optional platform specific keyword args, e.g. `cap_method=JAVACAP` for Android
:return: device instance
"""

示例如下:

1
2
device = init_device('Android')
print(device)

运行结果如下:

1
<airtest.core.android.android.Android object at 0x1018f3a58>

可以发现它返回的是一个 Android 对象。 这个 Android 对象实际上属于 airtest.core.android 这个包,继承自 airtest.core.device.Device 这个类,与之并列的还有 airtest.core.ios.ios.IOSairtest.core.linux.linux.Linuxairtest.core.win.win.Windows 等。这些都有一些针对 Device 操作的 API,下面我们以 airtest.core.android.android.Android 为例来总结一下。

  • get_default_device:获取默认 device
  • uuid:获取当前 Device 的 UUID
  • list_app:列举所有 App
  • path_app:打印输出某个 App 的完整路径
  • check_app:检查某个 App 是否在当前设备上
  • start_app:启动某个 App
  • start_app_timing:启动某个 App,然后计算时间
  • stop_app:停止某个 App
  • clear_app:清空某个 App 的全部数据
  • install_app:安装某个 App
  • install_multiple_app:安装多个 App
  • uninstall_app:卸载某个 App
  • snapshot:屏幕截图
  • shell:获取 Adb Shell 执行的结果
  • keyevent:执行键盘操作
  • wake:唤醒当前设备
  • home:点击 HOME 键
  • text:向设备输入内容
  • touch:点击屏幕某处的位置
  • double_click:双击屏幕某处的位置
  • swipe:滑动屏幕,由一点到另外一点
  • pinch:手指捏和操作
  • logcat:日志记录操作
  • getprop:获取某个特定属性的值
  • get_ip_address:获取 IP 地址
  • get_top_activity:获取当前 Activity
  • get_top_activity_name_and_pid:获取当前 Activity 的名称和进程号
  • get_top_activity_name:获取当前 Activity 的名称
  • is_keyboard_shown:判断当前键盘是否出现了
  • is_locked:设备是否锁定了
  • unlock:解锁设备
  • display_info:获取当前显示信息,如屏幕宽高等
  • get_display_info:同 display_info
  • get_current_resolution:获取当前设备分辨率
  • get_render_resolution:获取当前渲染分辨率
  • start_recording:开始录制
  • stop_recording:结束录制
  • adjust_all_screen:调整屏幕适配分辨率

了解了上面的方法之后,我们可以用一个实例来感受下它们的用法:

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
from airtest.core.android import Android
from airtest.core.api import *
import logging

logging.getLogger("airtest").setLevel(logging.WARNING)

device: Android = init_device('Android')
is_locked = device.is_locked()
print(f'is_locked: {is_locked}')

if is_locked:
device.unlock()

device.wake()

app_list = device.list_app()
print(f'app list {app_list}')

uuid = device.uuid
print(f'uuid {uuid}')

display_info = device.get_display_info()
print(f'display info {display_info}')

resolution = device.get_render_resolution()
print(f'resolution {resolution}')

ip_address = device.get_ip_address()
print(f'ip address {ip_address}')

top_activity = device.get_top_activity()
print(f'top activity {top_activity}')

is_keyboard_shown = device.is_keyboard_shown()
print(f'is keyboard shown {is_keyboard_shown}')

这里我们调用了设备的一些操作方法,获取了一些基本状态,运行结果如下:

1
2
3
4
5
6
7
8
is_locked: False
app list ['com.kimcy929.screenrecorder', 'com.android.providers.telephony', 'io.appium.settings', 'com.android.providers.calendar', 'com.android.providers.media', 'com.goldze.mvvmhabit', 'com.android.wallpapercropper', 'com.android.documentsui', 'com.android.galaxy4', 'com.android.externalstorage', 'com.android.htmlviewer', 'com.android.quicksearchbox', 'com.android.mms.service', 'com.android.providers.downloads', 'mark.qrcode', ..., 'com.google.android.play.games', 'io.kkzs', 'tv.danmaku.bili', 'com.android.captiveportallogin']
uuid emulator-5554
display info {'id': 0, 'width': 1080, 'height': 1920, 'xdpi': 320.0, 'ydpi': 320.0, 'size': 6.88, 'density': 2.0, 'fps': 60.0, 'secure': True, 'rotation': 0, 'orientation': 0.0, 'physical_width': 1080, 'physical_height': 1920}
resolution (0.0, 0.0, 1080.0, 1920.0)
ip address 10.0.2.15
top activity ('com.microsoft.launcher.dev', 'com.microsoft.launcher.Launcher', '16040')
is keyboard shown False

连接 device

连接 device 需要传入设备的 uri,格式类似 android://adbhost:adbport/serialno?param=value&param2=value2,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
def connect_device(uri):
"""
Initialize device with uri, and set as current device.

:param uri: an URI where to connect to device, e.g. `android://adbhost:adbport/serialno?param=value&param2=value2`
:return: device instance
:Example:
* ``android:///`` # local adb device using default params
* ``android://adbhost:adbport/1234566?cap_method=javacap&touch_method=adb`` # remote device using custom params
* ``windows:///`` # local Windows application
* ``ios:///`` # iOS device
"""

示例如下:

1
2
3
4
5
6
from airtest.core.android import Android
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
device: Android = connect_device(uri)
print(device)

运行结果如下:

1
<airtest.core.android.android.Android object at 0x110246940>

其实返回结果和 init_device 是一样的,最后 connect_device 方法就是调用了 init_device 方法。

获取当前 device

就是直接调用 device 方法,定义如下:

1
2
3
4
5
6
7
def device():
"""
Return the current active device.

:return: current device instance
"""
return G.DEVICE

获取所有 device

在 airtest 中有一个全局变量 G,获取所有 device 的方法如下:

1
2
3
4
5
6
7
from airtest.core.android import Android
from airtest.core.api import *

print(G.DEVICE_LIST)
uri = 'Android://127.0.0.1:5037/emulator-5554'
device: Android = connect_device(uri)
print(G.DEVICE_LIST)

运行结果如下:

1
2
[]
[<airtest.core.android.android.Android object at 0x10ba03978>]

这里需要注意的是,在最开始没有调用 connect_device 方法之前,DEVICE_LIST 是空的,在调用之后 DEVICE_LIST 会自动添加已经连接的 device,DEVICE_LIST 就是已经连接的 device 列表

切换 device

我们可以使用 set_current 方法切换当前连接的 device,传入的是 index,定义如下:

1
2
3
4
5
6
7
8
9
def set_current(idx):
"""
Set current active device.

:param idx: uuid or index of initialized device instance
:raise IndexError: raised when device idx is not found
:return: None
:platforms: Android, iOS, Windows
"""

这个方法没有返回值,调用 set_current 方法切换 device 之后,再调用 device 方法就可以获取当前 device 对象了。

执行命令行

可以使用 shell 方法传入 cmd 来执行命令行,定义如下:

1
2
3
4
5
6
7
8
9
10
@logwrap
def shell(cmd):
"""
Start remote shell in the target device and execute the command

:param cmd: command to be run on device, e.g. "ls /data/local/tmp"
:return: the output of the shell cmd
:platforms: Android
"""
return G.DEVICE.shell(cmd)

直接调用 adb 命令就好了,例如获取内存信息就可以使用如下命令:

1
2
3
4
5
6
7
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)

result = shell('cat /proc/meminfo')
print(result)

运行结果如下:

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
MemTotal:        3627908 kB
MemFree: 2655560 kB
MemAvailable: 2725928 kB
Buffers: 3496 kB
Cached: 147472 kB
SwapCached: 0 kB
Active: 744592 kB
Inactive: 126332 kB
Active(anon): 723292 kB
Inactive(anon): 16344 kB
Active(file): 21300 kB
Inactive(file): 109988 kB
Unevictable: 0 kB
Mlocked: 0 kB
HighTotal: 2760648 kB
HighFree: 2073440 kB
LowTotal: 867260 kB
LowFree: 582120 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 720100 kB
Mapped: 127720 kB
Shmem: 19428 kB
Slab: 76196 kB
SReclaimable: 7392 kB
SUnreclaim: 68804 kB
KernelStack: 7896 kB
PageTables: 8544 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 1813952 kB
Committed_AS: 21521776 kB
VmallocTotal: 122880 kB
VmallocUsed: 38876 kB
VmallocChunk: 15068 kB
DirectMap4k: 16376 kB
DirectMap4M: 892928 kB

启动和停止 App

启动和停止 App 直接传入包名即可,其实它们就是调用的 device 的 start_app 和 stop_app 方法,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@logwrap
def start_app(package, activity=None):
"""
Start the target application on device

:param package: name of the package to be started, e.g. "com.netease.my"
:param activity: the activity to start, default is None which means the main activity
:return: None
:platforms: Android, iOS
"""
G.DEVICE.start_app(package, activity)

@logwrap
def stop_app(package):
"""
Stop the target application on device

:param package: name of the package to stop, see also `start_app`
:return: None
:platforms: Android, iOS
"""
G.DEVICE.stop_app(package)

用法示例如下:

1
2
3
4
5
6
7
8
9
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)

package = 'com.tencent.mm'
start_app(package)
sleep(10)
stop_app(package)

这里我指定了微信的包名,然后调用 start_app 启动了微信,然后等待了 10 秒,然后调用了 stop_app 停止了微信。

安装和卸载

安装和卸载也是一样,也是调用了 device 的 install 和 uninstall 方法,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@logwrap
def install(filepath, **kwargs):
"""
Install application on device

:param filepath: the path to file to be installed on target device
:param kwargs: platform specific `kwargs`, please refer to corresponding docs
:return: None
:platforms: Android
"""
return G.DEVICE.install_app(filepath, **kwargs)

@logwrap
def uninstall(package):
"""
Uninstall application on device

:param package: name of the package, see also `start_app`
:return: None
:platforms: Android
"""
return G.DEVICE.uninstall_app(package)

截图

截图使用 snapshot 即可完成,可以设定存储的文件名称,图片质量等。 定义如下:

1
2
3
4
5
6
7
8
9
10
11
def snapshot(filename=None, msg="", quality=ST.SNAPSHOT_QUALITY):
"""
Take the screenshot of the target device and save it to the file.

:param filename: name of the file where to save the screenshot. If the relative path is provided, the default
location is ``ST.LOG_DIR``
:param msg: short description for screenshot, it will be recorded in the report
:param quality: The image quality, integer in range [1, 99]
:return: absolute path of the screenshot
:platforms: Android, iOS, Windows
"""

示例如下:

1
2
3
4
5
6
7
8
9
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)

package = 'com.tencent.mm'
start_app(package)
sleep(3)
snapshot('weixin.png', quality=30)

运行之后在当前目录会生成一个 weixin.png 的图片,如图所示: weixin

唤醒和首页

唤醒和回到首页分别也是调用了 device 的 wake 和 home 方法,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@logwrap
def wake():
"""
Wake up and unlock the target device

:return: None
:platforms: Android

.. note:: Might not work on some models
"""
G.DEVICE.wake()

@logwrap
def home():
"""
Return to the home screen of the target device.

:return: None
:platforms: Android, iOS
"""
G.DEVICE.home()

直接调用即可。

点击屏幕

点击屏幕是 touch 方法,可以传入一张图或者绝对位置,同时可以指定点击次数,定义如下:

1
2
3
4
5
6
7
8
9
10
11
@logwrap
def touch(v, times=1, **kwargs):
"""
Perform the touch action on the device screen

:param v: target to touch, either a Template instance or absolute coordinates (x, y)
:param times: how many touches to be performed
:param kwargs: platform specific `kwargs`, please refer to corresponding docs
:return: finial position to be clicked
:platforms: Android, Windows, iOS
"""

例如我现在的手机屏幕是这样子: image-20200723104955681 这里我截图下来一张图片,如图所示: tpl 然后我们把这个图片声明成一个 Template 传入,示例如下:

1
2
3
4
5
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
touch(Template('tpl.png'))

启动之后它就会识别出这张图片的位置,然后点击。 或者我们可以指定点击的绝对位置,示例如下:

1
2
3
4
5
6
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
touch((400, 600), times=2)

另外上述的 touch 方法还可以完全等同于 click 方法。 如果要双击的话,还可以使用调用 double_click 方法,传入参数也可以是 Template 或者绝对位置。

滑动

滑动可以使用 swipe 方法,可以传入起始和终止位置,两个位置都可以传入绝对位置或者 Template,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@logwrap
def swipe(v1, v2=None, vector=None, **kwargs):
"""
Perform the swipe action on the device screen.

There are two ways of assigning the parameters
* ``swipe(v1, v2=Template(...))`` # swipe from v1 to v2
* ``swipe(v1, vector=(x, y))`` # swipe starts at v1 and moves along the vector.

:param v1: the start point of swipe,
either a Template instance or absolute coordinates (x, y)
:param v2: the end point of swipe,
either a Template instance or absolute coordinates (x, y)
:param vector: a vector coordinates of swipe action, either absolute coordinates (x, y) or percentage of
screen e.g.(0.5, 0.5)
:param **kwargs: platform specific `kwargs`, please refer to corresponding docs
:raise Exception: general exception when not enough parameters to perform swap action have been provided
:return: Origin position and target position
:platforms: Android, Windows, iOS
"""

比如这里我们可以定义手指向右滑动:

1
2
3
4
5
6
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
swipe((200, 300), (900, 300))

放大缩小

放大缩小是使用的 pinch 方法,可以指定放大还是缩小,同时还可以指定中心位置点和放大缩小的比率。 定义如下:

1
2
3
4
5
6
7
8
9
10
11
@logwrap
def pinch(in_or_out='in', center=None, percent=0.5):
"""
Perform the pinch action on the device screen

:param in_or_out: pinch in or pinch out, enum in ["in", "out"]
:param center: center of pinch action, default as None which is the center of the screen
:param percent: percentage of the screen of pinch action, default is 0.5
:return: None
:platforms: Android
"""

示例如下:

1
2
3
4
5
6
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
pinch(in_or_out='out', center=(300, 300), percent=0.4)

键盘事件

可以使用 keyevent 来输入某个键,例如 home、back 等等,keyevent 的定义如下:

1
2
3
4
5
6
7
8
9
def keyevent(keyname, **kwargs):
"""
Perform key event on the device

:param keyname: platform specific key name
:param **kwargs: platform specific `kwargs`, please refer to corresponding docs
:return: None
:platforms: Android, Windows, iOS
"""

示例如下:

1
keyevent("HOME")

这样就表示按了 HOME 键。

输入内容

输入内容需要使用 text 方法,当然前提是这个 widget 需要是 active 状态,text 的定义如下:

1
2
3
4
5
6
7
8
9
10
@logwrap
def text(text, enter=True, **kwargs):
"""
Input text on the target device. Text input widget must be active first.

:param text: text to input, unicode is supported
:param enter: input `Enter` keyevent after text input, default is True
:return: None
:platforms: Android, Windows, iOS
"""

等待和判断

可以使用 wait 方法等待某个内容加载出来,需要传入的是 Template,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@logwrap
def wait(v, timeout=None, interval=0.5, intervalfunc=None):
"""
Wait to match the Template on the device screen

:param v: target object to wait for, Template instance
:param timeout: time interval to wait for the match, default is None which is ``ST.FIND_TIMEOUT``
:param interval: time interval in seconds to attempt to find a match
:param intervalfunc: called after each unsuccessful attempt to find the corresponding match
:raise TargetNotFoundError: raised if target is not found after the time limit expired
:return: coordinates of the matched target
:platforms: Android, Windows, iOS
"""

同时也使用 exists 方法判断某个内容是否存在,定义如下:

1
2
3
4
5
6
7
8
9
@logwrap
def exists(v):
"""
Check whether given target exists on device screen

:param v: target to be checked
:return: False if target is not found, otherwise returns the coordinates of the target
:platforms: Android, Windows, iOS
"""

断言

另外 Airtest 还提供了几个断言语句来判断结果是否存在或者相同,定义如下:

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
53
54
55
56
57
58
59
60
61
62
63
@logwrap
def assert_exists(v, msg=""):
"""
Assert target exists on device screen

:param v: target to be checked
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion fails
:return: coordinates of the target
:platforms: Android, Windows, iOS
"""
try:
pos = loop_find(v, timeout=ST.FIND_TIMEOUT, threshold=ST.THRESHOLD_STRICT)
return pos
except TargetNotFoundError:
raise AssertionError("%s does not exist in screen, message: %s" % (v, msg))

@logwrap
def assert_not_exists(v, msg=""):
"""
Assert target does not exist on device screen

:param v: target to be checked
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion fails
:return: None.
:platforms: Android, Windows, iOS
"""
try:
pos = loop_find(v, timeout=ST.FIND_TIMEOUT_TMP)
raise AssertionError("%s exists unexpectedly at pos: %s, message: %s" % (v, pos, msg))
except TargetNotFoundError:
pass

@logwrap
def assert_equal(first, second, msg=""):
"""
Assert two values are equal

:param first: first value
:param second: second value
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion fails
:return: None
:platforms: Android, Windows, iOS
"""
if first != second:
raise AssertionError("%s and %s are not equal, message: %s" % (first, second, msg))

@logwrap
def assert_not_equal(first, second, msg=""):
"""
Assert two values are not equal

:param first: first value
:param second: second value
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion
:return: None
:platforms: Android, Windows, iOS
"""
if first == second:
raise AssertionError("%s and %s are equal, message: %s" % (first, second, msg))

这几个断言比较常用的就是 assert_exists 和 assert_not_exists 判断某个目标是否存在于屏幕上,同时还可以传入 msg,它可以被记录到 report 里面。 以上就是 Airtest 的 API 的用法,它提供了一些便捷的方法封装,同时还对接了图像识别等技术。 但 Airtest 也有一定的局限性,比如不能根据 DOM 树来选择对应的节点,依靠图像识别也有一定的不精确之处,所以还需要另外一个库 —— Poco。

Poco

利用 Poco 我们可以支持 DOM 选择,例如编写 XPath 等来定位某一个节点。 首先需要安装 Poco,使用 pip3 即可:

1
pip3 install pocoui

安装好了之后我们便可以使用它来选择一些节点了,示例如下:

1
2
3
4
5
6
7
8
9
from airtest.core.api import *
from poco.drivers.android.uiautomation import AndroidUiautomationPoco

poco = AndroidUiautomationPoco()

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
poco(text='Weather').click()

比如这里我们就声明了 AndroidUiautomationPoco 这个 Poco 对象,然后调用了 poco 传入一些选择器,选中之后执行 click 方法。 这个选择器非常强大,可以传入 name 和各个属性值,具体的使用方法见:https://poco.readthedocs.io/en/latest/source/poco.pocofw.html

1
2
3
4
5
6
7
8
9
10
11
from airtest.core.api import *
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
from poco.proxy import UIObjectProxy

poco = AndroidUiautomationPoco()

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
object: UIObjectProxy = poco("com.microsoft.launcher.dev:id/workspace")
print(object)

poco 返回的是 UIObjectProxy 对象,它提供了其他的操作 API,例如选取子节点,兄弟节点,父节点等等,同时可以调用各个操作方法,如 click、pinch、scroll 等等。 具体的操作文档可以参见:https://poco.readthedocs.io/en/latest/source/poco.proxy.html 下面简单总结:

  • attr:获取节点属性
  • child:获取子节点
  • children:获取所有子节点
  • click:点击
  • double_click:双击
  • drag_to:将某个节点拖拽到另一个节点
  • exists:某个节点是否存在
  • focus:获得焦点
  • get_bounds:获取边界
  • get_name:获取节点名
  • get_position:获取节点位置
  • get_size:获取节点大小
  • get_text:获取文本内容
  • long_click:长按
  • offspring:选出包含直接子代的后代
  • parent:父节点
  • pinch:缩放
  • scroll:滑动
  • set_text:设置文字
  • sibling:兄弟节点
  • swipe:滑动
  • wait:等待
  • wait_for_appearance:等待某个节点出现
  • wait_for_disappearance:等待某个节点消失

以上的这些方法混用的话就可以执行各种节点的选择和相应的操作。

技术杂谈

最近遇到一个问题,那就是需要给别人共享一下 Kubernetes 的某个资源的使用和访问权限,这个仅仅存在于某个 namespace 下,但是我又不能把管理员权限全都给它,我想只给他授予这一个 Namespace 下的权限,那应该怎么办呢? 比如我这边是需要只想授予 postgresql 这个 Namespace 的权限,这里我就需要利用到 Kubernetes 里面的 RBAC 机制来实现了,下面记录了我的操作流程。

创建 Namespace

1
kubectl create namespace postgresql

首先没有 Namespace 的话需要创建一个 Namespace,这里我创建的是 postgresql,大家可以自行修改。

创建 ServiceAccount

接下来需要创建一个 ServiceAccount,yaml 如下:

1
2
3
4
5
apiVersion: v1
kind: ServiceAccount
metadata:
name: postgresql
namespace: postgresql

创建 Role

然后还要创建一个 Role,来控制相应的权限,yaml 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: postgresql
namespace: postgresql
rules:
- apiGroups: ["", "extensions", "apps"]
resources: ["*"]
verbs: ["*"]
- apiGroups: ["batch"]
resources:
- jobs
- cronjobs
verbs: ["*"]

这里由于 Role 是 Namespace 级别的,所以只能在特定 Namespace 下生效,这里我要让授予本 Namespace 下的所有权限,这里 rules 就添加了所有的 API类型、资源类型和操作类型。

创建 RoleBinding

最后需要将 Role 和 ServiceAccount 绑定起来,yaml 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: postgresql
namespace: postgresql
subjects:
- kind: ServiceAccount
name: postgresql
namespace: postgresql
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: postgresql

创建 kubeconfig 文件

现在我们执行上述 yaml 文件之后,ServiceAccount 其实就已经创建好了,它会对应一个 secret,我们来看下详情,执行:

1
kubectl get serviceaccount postgresql -n postgresql -o yaml

好,运行结果类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"annotations":{},"name":"postgresql","namespace":"postgresql"}}
creationTimestamp: "2020-07-30T16:10:38Z"
name: postgresql
namespace: postgresql
resourceVersion: "17800240"
selfLink: /api/v1/namespaces/postgresql/serviceaccounts/postgresql
uid: 6327db1f-6a93-4f1e-b988-31842989bbbc
secrets:
- name: postgresql-token-v26k7

这里它实际关联了一个 secret,叫做 postgresql-token-v26k7,这里面就隐藏了 ServiceAccount 的 token 和证书。 好,那么我们就可以利用这个 secret 来制作 kubeconfig 文件了,命令如下:

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
server=https://your-server:443
name=postgresql-token-v26k7
namespace=postgresql

ca=$(kubectl get secret/$name -n $namespace -o jsonpath='{.data.ca.crt}')
token=$(kubectl get secret/$name -n $namespace -o jsonpath='{.data.token}' | base64 --decode)

echo "apiVersion: v1
kind: Config
clusters:
- name: test
cluster:
certificate-authority-data: ${ca}
server: ${server}
contexts:
- name: test
context:
cluster: test
user: postgresql
current-context: test
users:
- name: postgresql
user:
token: ${token}
" > postgresql.kubeconfig

这里我们需要指定三个变量:

  • server:就是 Kubernetes Server API 的地址
  • name:就是 ServiceAccount 对应的 secret
  • namespace:就是当前操作的 Namespace

运行之后就会生成一个 portgresql.kubeconfig 文件。

使用

那么怎么使用呢?很简单,设置下环境变量切换下就好了。

1
export KUBECONFIG=postgresql.kubeconfig

这里我们就将 KUBECONFIG 设置了下,这样再执行 kubectl 就会读取到当前的 kubeconfig 文件,就会生效了。 这时候我们来测试下:

1
kubectl get nodes

运行结果如下:

1
Error from server (Forbidden): nodes is forbidden: User "system:serviceaccount:postgresql:postgresql" cannot list resource "nodes" in API group "" at the cluster scope

可以看到这里就提示没有列出节点的权限了。 然后我们操作下 postgresql 下的权限试试:

1
kubectl get svc -n postgresql

运行结果如下:

1
2
3
4
5
NAME                  TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
postgresql ClusterIP 10.0.193.137 <none> 5432/TCP 9d
postgresql-headless ClusterIP None <none> 5432/TCP 9d
postgresql-metrics ClusterIP 10.0.60.88 <none> 9187/TCP 9d
postgresql-read ClusterIP 10.0.236.184 <none> 5432/TCP 9d

这里就可以看到对 postgresql 这个命名空间的操作就成功了。 到此为止我们就成功实现了特定 Namespace 的限制,大功告成。

技术杂谈

之前也写过不少关于爬虫的博客了,比如我拿一个案例来写了一篇博客,当时写的时候好好的,结果过了一段时间这个页面改版了,甚至直接下线了,那这篇案例就废掉了。 另外如果拿别人的站或者 App 来做案例的话,比较容易触犯到对方的利益,风险比较高,比如把某个站的 JavaScript 逆向方案公布出来,比如把某个 App 的逆向方案公布出来。如果此时此刻没有对方联系你的话,一个很大原因可能是规模太小了别人没注意到,但不代表以后不会。我还是选择爱护自己的羽毛,关于逆向实际网站和 App 的案例我都不会发的。在这种情况下比较理想的方案是自建案例,只用这个案例讲明白对应的知识点就可以了。 所以,为此,这段时间我也在写一些爬虫相关的案例,比如:

  • 无反爬的服务端渲染网站
  • 带有参数加密的 SPA 网站
  • 各类形式验证码网站
  • 反 WebDriver 网站
  • 字体反爬等网站
  • 模拟登录网站
  • App 案例,如代理检测,SSL Pining 等

今天发布一下。

案例列表

本案例平台自爬数据、自建页面、自接反爬,案例稳定后永不过期,适合教学与练习。

SSR 网站

  • ssr1:猫眼电影数据网站,数据通过服务端渲染,适合基本爬虫练习。
  • ssr2:HTTPS 无效证书网站,适合做跳过验证 HTTPS 案例。
  • ssr3:HTTP Basic Authentication 网站,适合做 HTTP 认证案例,用户名密码均为 admin。
  • ssr4:每个响应增加了 5 秒延迟,适合测试慢速网站爬取或做爬取速度测试,减少本身网速干扰。

SPA 网站

  • spa1:猫眼电影数据网站,数据通过 Ajax 加载,页面动态渲染,适合 Ajax 分析和动态页面渲染爬取。
  • spa2:猫眼电影数据网站,数据通过 Ajax 加载,数据接口参数加密且有时间限制,适合动态页面渲染爬取或 JavaScript 逆向分析。
  • spa3:猫眼电影数据网站,数据通过 Ajax 加载,无页码翻页,适合 Ajax 分析和动态页面渲染抓取。
  • spa4:新闻网站索引,数据通过 Ajax 加载,无页码翻页,适合 Ajax 分析和动态页面渲染抓取以及智能页面提取分析。
  • spa5:豆瓣图书网站,数据通过 Ajax 加载,有翻页,无反爬,适合大批量动态页面渲染抓取。
  • spa6:电影数据网站,数据通过 Ajax 加载,数据接口参数加密且有时间限制,源码经过混淆,适合 JavaScript 逆向分析。

验证码网站

  • captcha1:对接滑动拼图验证码,适合滑动拼图验证码分析处理。
  • captcha2:对接图标点选验证码,适合图标点选验证码分析处理。
  • captcha3:对接图文点选验证码,适合图文点选验证码分析处理。
  • captcha4:对接语序分析验证码,适合语序分析验证码分析处理。
  • captcha5:对接空间推理验证码,适合空间推理验证码分析处理。
  • captcha6:对接九宫格识图验证码,适合九宫格识图验证码分析处理。

模拟登录网站

  • login1:登录时用户名和密码经过加密处理,适合 JavaScript 逆向分析。
  • login2:对接 Session + Cookies 模拟登录,适合用作 Session + Cookies 模拟登录练习。
  • login3:对接 JWT 模拟登录方式,适合用作 JWT 模拟登录练习。

反爬型网站

  • antispider1:WebDriver 反爬网站,检测到 WebDriver 就不显示页面。
  • antispider2:对接 User-Agent 反爬,检测到常见爬虫 User-Agent 就会拒绝响应,适合用作 User-Agent 反爬练习。
  • antispider3:对接文字偏移反爬,所见顺序并不一定和源码顺序一致,适合用作文字偏移反爬练习。
  • antispider4:对接字体文件反爬,显示的内容并不在 HTML 内,而是隐藏在字体文件,设置了文字映射表,适合用作字体反爬练习。
  • antispider5:限制 IP 访问频率为最多 1 秒一个,如果过多则会封禁 IP。

App

  • app1:最基本的 App 案例,数据通过接口加载,无反爬,无任何加密参数,适合做抓包分析和请求模拟。
  • app2:设置了接口请求不走系统代理,因此无法直接抓包,适合做抓包特殊处理。
  • app3:对系统代理进行了检测,如果设置了代理则无法正常请求数据,适合做抓包特殊处理。
  • app4:设置了 SSL Pining,如果设置了非法证书则无法正常请求数据,适合做反 SSL Pining 处理。
  • app5:接口增加了加密参数,适合做抓包实时处理或可视化爬取或逆向分析。
  • app6:接口增加了加密参数,同时对源码进行了混淆,适合做抓包实时处理或可视化爬取或逆向分析。
  • app7:接口增加了加密参数,同时对安装包进行了加固处理,适合做抓包实时处理或可视化爬取或逆向分析。

暂且是这么多,后续还会继续增加,大家可以试着爬爬看。

汇总链接

为了方便,我专门申请了一个域名,scrape.center,意思名为「爬取中心」,似乎听起来意义上还说的过去? 案例平台首页 URL:https://scrape.center,截图如下: 案例首页 大家可以点击任意一个网站来爬取练习。

案例预览

下面是一些部分案例的截图: SSR1案例截图 SPA4案例截图 SPA5案例截图 SPA6案例截图 上面是一些案例的效果,基本上是使用 Django + Vue.js 开发的,主题使用了红色调,整个案例平台风格比较统一。另外还有一些 App 也是类似的风格,大家可以自行下载体验试试。 当然这里面最主要的还是案例的功能,比如各种加密、反爬、检测等等。

源代码

有朋友可能会问这个案例平台的源代码在哪里。 这里解释一下,由于这个案例平台以后会用于案例的讲解,并且可能会出现在课程、书本中,所以为了避免盗版和抄袭的问题,这里我选择了闭源,也算是对自己的知识成果的另一种保护吧。 不过这并不意味着爬取代码是闭源的,这块还是会开源出来的。

怎么爬

还有朋友会问,这一个个网站这么多类型和反爬,到底怎么爬呢? 其实现在针对这个练习平台一些案例讲解我已经做好了,课程也基本 OK 了,如果感兴趣大家可以点击看看。 https://t.lagou.com/cRC3RGRjSu706 谢谢大家。

技术杂谈

Persistenced:

RDB(Redis Database):

简介:

将时间段间隔内的内存数据以快照的形式写入磁盘,它恢复时是将快照文件直接读到内存里(snapshot)

原理:

Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,在用这个临时文件替换上次持久化号的文件。主进程是不进行任何的IO操作,确保了极高的性能。如果需要进行大规模的数据恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能会丢失

Fork:

作用:创建一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)都与原进程一致。但为一个全新的进程,并作为原进程的子进程

RDB保存的是dump.rdb文件

配置位置:

在redis.conf 中的snapshoting

快照:

save,save只管保存,其他不管全部阻塞 bgsave:redis会在后台异步进行快照操作 快照同时还可以响应客户端请求,可以通过lastsave命令获取最后一次成功执行快照的时间 执行flush命令也会产生dump.rdb文件,但里面是空的,无意义

如何恢复:

将备份文件(dump.rdb)移动到redis安装目录,并启动服务即可 在连接完成之后的终端 使用 config get dir 获取目录 异常恢复: redis-check-rdb —fix {}

优势:

适合大规模的数据备份 对数据完整性和一致性要求不高

劣势:

在一定时间间隔内做一次备份,所以如果redis意外down了就会丢失最后一次快照后的所有更改 Fork的时候,内存中的数据被克隆了一份,内存等将会2倍膨胀性,需考虑!!!

如何停止:

​ 动态所有停止rdb保存规则方法:redis-cli config set save “”

AOF:(Append only File):

导入:

为什么还会出现AOF?(新技术的出现必定弥补老技术的不足,新技术一定会借鉴老技术,是老技术的子类) 如果一个系统里面同时存在RDB是冲突呢还是协作? 为什么AOF会在RDB之后产生 AOF它会有什么优缺点?

原理:以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只需追加文件但不可改写文件,Redis启动之初会读取该文件重新构建数据, 换言之,redis重启就根据日志文件的内容将写指令从前到后执行一次以完成数据恢复工作 配置位置: redis.conf中的APPEND ONLY MODE 配置说明: Appendfsunc:

always: 同步持久化,每次发生更改立即记录到磁盘,性能差但数据完整性比较好 everysec:出厂默认推荐,异步操作,每秒记录,如果一秒内宕机,有数据丢失 No

No-appendfsy-on-rewrite:重写时是否可以运用Appendfsync,默认no即可,保障数据安全 Auto-aof-rewrite-min-size :设定重写基准值 Auto-aof-rewrite-percentage : 设定重写基准值

探讨dump.rdb,与aof的二者是否能共存及选择顺序

当aof损坏时,rdb完全,二者可以和平共存 二者先选择aof

AOF启动/修复/恢复:

正常恢复:

启动:将redis.conf中APPEND ONLY MODE 下appendonly no改为yes 将有数据的aof文件复制一份保存到对于目录(config get dir) 恢复: 重启redis然后重新加载即可

异常恢复:

备份被写坏的AOF文件, 修复: Redistribution-check-aof —fix 进行修复 重启redis,重新加载即可

Rewrite:重写

AOF采用文件追加的方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof

重新原理:

AOF文件持续增长而过大时,会fork出一条新进程来讲文件重写(也是先写零食文件最后在rename),遍历新进程的内存数据,每条记录有一条的set语句.重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件.和快照有点类似

触发机制: REDIS会记录上次重写时的AOF大小,默认配置时当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发 优势:

每秒同步:appendfsync always 同步持久化 每次发送数据变更会被立即记录到磁盘,性能较差完整性比较友好 每修改同步: appendfsync everysec 异步操作,每秒记录,如果一秒内宕机,有数据丢失 不同步:appendfsync no 从不同步

劣势: 相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb AOF运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同

Which one?:

RDB持久化方式:能够在指定的时间间隔内对数据进行快照存储 AOF持久化方式:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重新,使得AOF文件的体积不至于过大 只做缓存:

如果希望数据在服务器运行的时候存在,也可以不使用热河持久化方式.

同时开启两种持久化方式:

当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比rdb文件保存的数据集更完整 RDB的数据不实时,同时使用两者时服务器重启也指挥找AOF文件 那干脆直接使用AOF?不建议 因为RDB更适合用于数据库(AOF在不断变化不好备份),快速重启,而且不担保有AOF可能潜在的BUG,留着作为一个万一的手段

Other

浅谈排序算法与优化(仅部分,Updating)

欢迎查阅与star的源码 写在最前面,此文章少了各排序算法的对比,但多了一份由浅入深的个人理解,以及代码、及算法的优化的思路 阅读文章约 需 5min

列表排序

排序

将一组“无序”的记录序列调整为“有序”的记录序列

列表排序

将无序的列表变为有序列表 输入:列表; 输出:有序列表

升序与降序 内置排序函数:sort(),基于timsort排序算法

Timsort是一种混合稳定排序算法,源自归并排序(merge sort)和插入排序(insertion sort)

有兴趣的伙计可以看看这两篇文章 sort算法运用原理1 sort算法运用原理2 TimSort

常见排序算法

LOW:

冒泡:

列表每两个相邻的数,如果前面比后面大,则交换这两个数 一趟排序完成后,则无序区减少一个数,有序区增加一个数

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
@cal_time
def bubble_sort(li: List[List]) -> List:
print('初始列表', li)
print('初始列表长度:', len(li))
for i in range(len(li) - 1): # 第i躺,总次数
i += 1
for j in range(len(li) - i - 1): # 指针
if li[j] > li[j + 1]: # 如果指针所对应的值大于对比的值,则二者交换位置
li[j], li[j + 1] = li[j + 1], li[j]
else:
break
print(f'冒泡排序第{i}次', li)

li = [np.random.randint(0, 1000) for i in range(5)]
bubble_sort(li)

数据来源于numpy.randmo.randint()随机取数,运行流程:
冒泡排序第1次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第2次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第3次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第4次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第5次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第6次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第7次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第8次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第9次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]

# 排序次数:列表长度-1,
# 缘由将无序区转化为有序区:(再看一遍以下这段代码, 如果li[j]>li[j+1],则二者交换位置)
if li[j] > li[j + 1]:
li[j], li[j + 1] = li[j + 1], li[j]

# 当一维数组长度为10000的时候所需时间为:

Runtime 优化: 在一次回头看看冒泡排序具体实现思路,将列表每两个相邻的数,如果前面比后面大,则交换这两个数,一趟排序完成后,则无序区减少一个数,有序区增加一个数。 如果无序区已经是有序的呢?按照代码执行流程可知, 如果前者比后者大,那么则两个交换位置。(假设为降序,前面为无序区,后面为有序区) 否则不执行任何交换操作,但会执行便利(也可以理解为此运算”不执行任何交换操作,无实际意义”) 优化目标:(将“不执行任何交换的操作去掉”),咱们在回头看看,具体实际的运算是在第二层for循环里面的,也就说所谓的“无用功”也是在这里产生的 无用功体现为:无论是否进行了位置交换,都会在往有序区在遍历检查一遍 思路如下: 主要优化的地方在跳出循环,在它不交换位置的时候,直接跳出此次的循环 假设它全部都是有序的,并设个标记True, 当如果循环内发生了位置交换,则改变标记为False。 流程控制,if。当if True的时则会执行if内部代码(设立return,或者break)主要跳出循环。避免对有序区进行有一次的排序操作 具体代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
def bubble_sort(li: List):
print("The len of List:", len(li)) # 输入数组长度
for i in range(len(li) - 1): # 保证输入的数组在计算范围类,此处的“-1”是因为索引=长度-1
flag = True
for j in range(len(li) - i - 1):
if li[j] > li[j + 1]:
li[j + 1], li[j] = li[j], li[j + 1]
flag = False
print("Sorting times:%d, Status:%s" % (i + 1, li))
if flag:
break

选择: 插入:

Stronger

快速排序: 堆排序 归并排序

其他排序:

希尔排序 计数排序 基数排序

1
break

选择: 插入:

Stronger

快速排序: 堆排序 归并排序

其他排序:

希尔排序 计数排序 基数排序

技术杂谈

欢迎Star的源码

新入手,小白的我,在我眼里Request爬虫永远只有四大步,不服来辩?

  1. 确定URL,构造请求头
  2. 发送请求,获取响应
  3. 解析响应,获取数据
  4. 保存数据

目标:根据视频BV,获取B站视频弹幕

代码地址如下: 抓包确定URL:

导入:

视频都有一个唯一区分视频:BV号 那么视频的URL规则为:’https://wwww.bilibili.com/video/BV{BVID}’ 找一下弹幕的地址,直接search,即可!如下

由以上抓包可知,弹幕的URL:’https://api.bilibili.com/x/v1/dm/list.so?oid=oid‘, 我们获取到oid那么这一步就完成了 来,回头去找一下oid从何而来呢? 据老夫多年经验指引,他一定在视频URL里面。(其实当时也找了挺久的,甚至逆向那一手,断点调试、调用堆栈等等什么都用出来了。最终还是功夫不负有心人,找到了) 其实回头看,oid是等于video_URL页面里面的cid参数的(验证了Payne式猜想)。过程是难受的

URL,其参数规则也找到了,那么还不就随我为所欲为了。只要拿到视频地址,那不就可以直接拿到弹幕了么。of course!

此处省略3万字(请求,解析,网络原理。。。)

其实当时知道两个方法都去试了,JS那个就不说了,有兴趣的盆友,可以去搞一下 说说这个提取cid参数吧,我用的是正则,这种情况最好是用正则,不过也看个人喜好吧。 可以回头看第二张图,初一乍看我好像不会,啊哈哈~

经过优化后(主要是看了其他视频的那啥之后):写出这个神奇的正则

1
cid = re.findall('.*?cid":+(d{9})+', text)[0] 

谢谢你认真看到了末尾,那我也写点私活吧。欢迎查阅源码与star

1
2
其实关于很多网站(普通)的参数就算是有JS加密,以及混淆。并不代表就一定需要去解密,去逆向。有时候真的只需要serch一下惊喜多多,望诸君切记、切记。
咱们解密,逆向,有时候完全即可做一个动态维持即可。例如我源码中的scrape 拉勾