0%

技术杂谈

事情是这样的,最近发现我的博客 cuiqingcai.com 的评论功能出现了问题,登录功能不好用了。经过一番排查,我找到了一些解决方案,在这里记录一下问题排查过程。

另外这个排查过程中可以总结出一些思路,大家如果碰到类似的问题,也可以按照类似的思路来排查。

友情提示:大家一定要读到最后或者直接拉到最后,最后内容的可能对你更有价值。

起因

我的博客最近刚换上了 hexo 框架,于是评论功能就换成了 Gitalk。但最近发现登录功能不好用了,点击使用 GitHub 登录总是失败。

就点击这个按钮的时候,始终登录不上去:

image-20210212223917778

于是我就想着手解决一下这个问题。

思路

这里就记录一下我在排查过程中碰到的一些坑和解决思路。

首先,登录失败的问题,第一时间应该去排查的就是网络请求,打开控制台,查看 Network 面板,出现类似的结果:

image-20210212224407613

网络请求直接 403 了,拿不到 token 了,于是就登录不上了。

观察下,这个链接 cors-anywhere 似乎是用来解决跨域限制的,后面还跟了一个 GitHub 的 Access Token 获取地址,那没跑了,前面这个就是一个反向代理,后面是真实的请求 URL。

OK,看着这个也没啥思路啊,然后接着怎么办?

那就接着去搜这个 cors-anywhere.herokuapp.com,因为 herokuapp 很眼熟嘛,就是一个公用的网址 Host 平台,类似于 AWS、Azure 之类的,那么前面这个可能包含某些信息。万一是开源的那就好办了。

接着搜,cors-anywhere,然后就搜到了这个:https://github.com/Rob--W/cors-anywhere

介绍如下:

CORS Anywhere is a NodeJS proxy which adds CORS headers to the proxied request.

The url to proxy is literally taken from the path, validated and proxied. The protocol part of the proxied URI is optional, and defaults to “http”. If port 443 is specified, the protocol defaults to “https”.

This package does not put any restrictions on the http methods or headers, except for cookies. Requesting user credentials is disallowed. The app can be configured to require a header for proxying a request, for example to avoid a direct visit from the browser.

真是一个开源框架,和我猜的一样,就是一个解决跨域问题而生的反向代理。

然后我就在它的 README 中看到了这个:

image-20210212224946845

好家伙,这不就是我刚才用到的链接吗?

那肯定是这个玩意出了什么毛病。

咋看呢?这个果断就是找 Issue 了:

image-20210212225114466

一看,太明显了:

PSA: Public demo server (cors-anywhere.herokuapp.com) will be very limited by January 2021, 31st

意思就是从今年 1.31 开始这个网站的访问会受限,点进去看看:

The demo server of CORS Anywhere (cors-anywhere.herokuapp.com) is meant to be a demo of this project. But abuse has become so common that the platform where the demo is hosted (Heroku) has asked me to shut down the server, despite efforts to counter the abuse (rate limits in #45 and #164, and blocking other forms of requests). Downtime becomes increasingly frequent (e.g. recently #300, #299, #295, #294, #287) due to abuse and its popularity.

To counter this, I will make the following changes:

  1. The rate limit will decrease from 200 (#164) per hour to 50 per hour.
  2. By January 31st, 2021, cors-anywhere.herokuapp.com will stop serving as an open proxy.
  3. From February 1st. 2021, cors-anywhere.herokuapp.com will only serve requests after the visitor has completed a challenge: The user (developer) must visit a page at cors-anywhere.herokuapp.com to temporarily unlock the demo for their browser. This allows developers to try out the functionality, to help with deciding on self-hosting or looking for alternatives.

好吧,意思就是说这个网站本来是演示用的,但是现在已经被滥用了,然后从 1.31 开始用户手动必须手动先访问这个网站获取临时的访问权限,然后才能使用。另外推荐开发者自己来维护一个网站。

接着下面的评论第一个就更滑稽了:

image-20210212225414735

这个人直接艾特了 gitalk,哈哈哈,因为 Gitalk 就如刚才所说的那样,也用了这个。

那就顺便去 Gitalk https://github.com/gitalk/gitalk,逛一下 issue,看看是不是也有人遇到了同样的问题,果不其然了:

image-20210212225919986

最近几个 issue 都是关于 403 的,真热闹。

点进去看看,有个大收获,里面有个好心人说:

这次直接去嫖了一个CORS proxy,把gitalk.js的6794行改为 proxy: ‘https://netnr-proxy.cloudno.de/https://github.com/login/oauth/access_token‘, 就可以了。具体能用多久我也没普,且用且珍惜。

真是得来全不费功夫,本来还想着自己部署着,这次那就换了就行了。

然而,这样不行,得需要改 gitalk.js 的源码,并不太好吧。

好,这时候就遇到了一个问题,要修改某些开源软件的源码应该怎么办?

首选的思路当然不是硬改,改了之后还要自己 host 一个新的 js 文件,那显然是很费精力的。

其实一半程序在编写的时候应该是预留一些接口和配置的,我们应该能很轻易地通过某些配置就能实现某些配置的复写。

那就接着看看吧,既然要改,那就得先看看 Gitalk 是怎么用的吧。

看文档,Gitalk 调用方式如下:

1
2
3
4
5
6
7
8
9
10
11
const gitalk = new Gitalk({
clientID: 'GitHub Application Client ID',
clientSecret: 'GitHub Application Client Secret',
repo: 'GitHub repo', // The repository of store comments,
owner: 'GitHub repo owner',
admin: ['GitHub repo owner and collaborators, only these guys can initialize github issues'],
id: location.pathname, // Ensure uniqueness and length less than 50
distractionFreeMode: false // Facebook-like distraction free mode
})

gitalk.render('gitalk-container')

看来这个在声明的时候是有参数的,那刚才 URL 配置没看到在哪里配啊,既然如此,那就看看 Gitalk 这个对象支持多少参数吧。

接着就去找 Gitalk 的构造参数说明,找到这么一个:

  • proxy:String

Default: https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/access_token.

果然找到了,所以这里如果我们要修改,那就改 proxy 参数就行了,初始化 Gitalk 的时候复写掉 proxy 就行。

OK,基本思路有了,那我怎么改到我的源码里呢?

我的博客是基于 Hexo 的 Next 主题的,根据经验,Gitalk 是 Next 主题自带的,所以 Gitalk 的声明应该就在 Next 主题源码里面。

那怎么找呢?

这时候就需要借助于一些搜索技巧了,搜什么?既然要用 Gitalk,那一定有刚才初始化的调用,那就搜 Gitalk 这个关键字就行了。另外还需要缩小一下搜索范围。

于是我就把范围限定到了 next 主题目录,搜索 Gitalk。

image-20210212230931998

简直不要太舒服,一搜就有了,文件是 themes/next/layout/_third-party/comments/gitalk.swig。

这里我们只需要把 proxy 参数加上不就行了,值是什么呢?仿照写就行了,配置风格保持统一,那就加一条:

1
proxy       : '{{ theme.gitalk.proxy }}',

OK,那这个配置的值很明显是主题配置文件,那就把配置文件里面加上 proxy 这个参数就好了。

找到 themes/next/_config.yml,添加行:

1
proxy: https://netnr-proxy.cloudno.de/https://github.com/login/oauth/access_token

结果如下:

image-20210212231224403

好了,大公告成!

重新部署 Hexo,现在评论又能重新使用了,问题就解决了!

测试地址:https://cuiqingcai.com/message/,大家来给我留言吧~

好了,这就是我排查问题的整个过程,做一下记录。

经验

另外,其实这篇文章的用意不仅仅是单纯解决这个问题,因为这个问题大家可能并没有遇到过,因此这个解决方案仅仅是给极少数遇到这个问题的朋友提供的。

但是,这并不代表这篇文章没有价值,因为其中有的思路是通用的,在这里稍微做一下总结,希望对大家有帮助:

  • 当遇到网页功能异常的时候,排查问题就主要看两个——控制台、网络请求,这里面往往能找到主要问题。
  • 结合一些基本知识进行合理的推断,比如刚才我就推断了 cors- anywhere 的作用并结合 herokuapp 推断这个可能还会是个公用的服务。
  • 当碰到没有思路或者不确定的时候,去谷歌它!不要百度,另外还可以在 GitHub 或者 Gitee 上搜。
  • 如果找到对应的 GitHub 仓库,Issue 区往往能找到一些有效答案,比如刚才我就在 Issue 区找到了一个可替代的 cors 网站。
  • 修改代码功能的时候要想着尽量复写,也就是 overwrite,而不是直接改,前者更具有灵活性,而且某些情况下会省去一些麻烦。
  • 复写的时候去找一些参数配置,比如找一些初始化参数、默认参数配置,看看能否实现改写的需求。
  • 找不到入口的时候善用全局搜索功能,比如刚才 Gitalk 找哪里调用的时候,就直接全局搜索。
  • 根据功能限制某些搜索范围,比如刚才我就知道 Gitalk 这个功能是 Next 主题提供的,所以我就直接限制搜索范围是 Next 主题的源码。
  • 以上步骤多尝试,熟能生巧。

技术杂谈

最近遇到了一个问题,就是 Vue.js 中的 prop 如何实现双向绑定比较好。

之前我都是把 prop 传递到子组件,然后子组件里面直接把 prop 直接改了,这样虽然能把结果反映到父组件,但并不是一个很好的解决方案。

比如我就经常遇到这样的 Warning:

Avoid mutating a prop directly since the value will be overwritten…

实际上,用事件传递的方式把修改从子组件传到父组件是比较不错的方式,这就需要子组件 $emit 一个事件,然后父组件监听这个事件,然后将接收到的值修改就好了。

官方文档的说明如下:

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件都没有明显的变更来源。

这也是为什么我们推荐以 update:myPropName 的模式触发事件取而代之。举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:

1
this.$emit('update:title', newTitle)

所以这里推荐的就是使用 update:propName 来实现。

这样的话,父组件原生的写法如下:

1
2
3
4
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>

这里 prop 的名字叫做 title,子组件需要接收到这个值,然后各种操作进行修改都没关系。

比如子组件可以这么写:

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
<template>
<el-input v-model="titleData"></el-input>
</template>

<script>
export default {
name: "Child",
props: {
title: {
type: String,
default: "",
}
},
data() {
return {
titleData: this.title
}
},
watch: {
titleData: {
handler(val) {
this.$emit('update:title', val)
}
}
}
}
</script>

这样当 titleData 更新的时候,父组件的 title 就更新了。

这时候父组件可以简写为:

1
<text-document v-bind:title.sync="doc.title"></text-document>

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似 v-model

另外当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

1
<text-document v-bind.sync="doc"></text-document>

这样会把 doc 对象中的每一个 property (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。

v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

以上,完毕。

技术杂谈

写这篇文章的缘由来自看到了知乎上的一个问题——在 GitHub 上保持 365 天全绿是一种怎样的体验?

解释

大家可能有的不明白啥意思啊,这个绿指的是就是 GitHub 的 Contribution,如果你每天都提交代码到 GitHub,至少一次 commit,那么 GitHub 就会在你当天对应的 Contribution 格子上点上绿色,比如我的就是这样子:

因为我经常在 GitHub 上提交代码,所以我的 Contribution 页面就显示为绿色,commit 多的就深,少的就浅,灰色的就是当然没有任何代码提交的。

所以这个问题问的就是,每天都在 GitHub 上提交代码是什么体验,也就是 365 天每天一天不落地撸代码是什么体验?

然后有一个回答看得我笑出声:

曾经保持了200多天全绿,但是冷落了女朋友,一直绿到现在。

原回答如下:

这哥们真的是太秀了,秀到我实在忍不住给他默默点了个赞…

不过咱们还是言归正传啊,说回这个问题。

其实说实话真的保持 365 天全绿真的是一件很难的事情,每周都会有周末吧,周末得陪女朋友吧?什么,你没有女朋友,那忽略这一条。

那即使没有女朋友,一年不得有几天是过年过节的,还撸啥代码啊?即使不是逢年过节,那也总有几天状态不好或者生病的吧,强如铁人那坚持 365 天天撸代码也是够神的。

不过我还真见过几个,实打实的大神,比如 Taylor Otweel,PHP Laravel 框架的开发者,一年 8000 多次 commit,他的 Contribution 是这样的:

这个是真的强,而且人家撸的代码质量肯定也高啊,不像我们可能改了点 README 啥的。

但强如 Taylor Otweel,你也能看到有些天是没有贡献的,毕竟人家周末可能就真的不撸代码或者有其他的安排。

那怎么才能做到 365 天全绿呢?

既然人不行,那就靠机器人吧。

GitHub 不是这两年出了个 GitHub Actions 的功能吗?这就是 CI 嘛,借助于它,我们想做到 GitHub 365 天全绿,就轻而易举了。

到底怎么做?难不难。

不难,可能只需要两分钟就搞定了。

想不想知道怎么做的?想知道的接着往下看。

方案

这里介绍一个 GitHub 的库,地址为:https://github.com/justjavac/auto-green,借助它,咔咔几步就能搞定了。

先说步骤。

首先打开这个仓库,点 Use this template,注意千万不要点 Fork,不然是不生效的,如图所示:

点了之后就是提示你用这个 Template 创建一个自己的 Git 残酷,这时候就会让你填写的的仓库名称,比如我就创建了一个名字叫做 AutoGreen 的仓库,如图所示:

至此,已经成功了一半了。

现在你会观察到,在 Actions 这个选项卡自动执行了一个任务。啪!很快啊,执行完了:

执行完了你会发现这个仓库多了一次 commit,是来自 justjavac 的一次 commit:

点进去看看,commit 的信息还叫 “a commit a day keeps your girlfriend away”,啥意思?

跟代码做好朋友,永远没有女朋友???Oh,No,不是这样的!

commit 如图所示:

神奇的是这次 commit 还没有任何 file change,还是个空的 commit。

行了,不管这么多了。

到现在就结束了吗?GitHub 以后就会天天绿了吗,当然不会绿啊,为啥啊?

扒一扒源码看一下,打开 .github/workflows/ci.yml 文件,结果如下:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
name: ci

on:
push:
branches:
- master
schedule:
- cron: "0 0 * * *"

jobs:
autogreen:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2

- name: Auto green
run: |
git config --local user.email "justjavac@gmail.com"
git config --local user.name "迷渡"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git pull --rebase
git commit --allow-empty -m "a commit a day keeps your girlfriend away"
git push

看看这个文件,意思就是每天执行一下下面的这几行代码,比如 git 的 config、commit、push 等,就相当于模拟了一次提交。

其中有几行代码比较有意思:

1
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}

这里就是指定远程仓库等地址,这里有几个占位符,github 的 actor 和 repository 对象,其实这些我们不用管,在运行的时候会被自动赋值为当前仓库的信息,另外还有 GITHUB_TOKEN 也是在该任务运行的时候自动添加的。

另外还有一行代码:

1
git commit --allow-empty -m "a commit a day keeps your girlfriend away"

这里的 commit 操作加上了一个 --allow-empty 选项,意思就是允许空的提交,这也就解释了上文空提交的缘由了。

OK,但这里我们再回过头来看看配置就知道为啥不算我们的提交了:

因为这里配置的是原作者的 GitHub 邮箱,所以这次提交当然就会算作原作者的了。

那怎么才能让我们的绿呢?那把邮箱改成我们自己的就好了,比如我的就修改为了这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
name: ci

on:
push:
branches:
- master
schedule:
- cron: "0 0 * * *"

jobs:
autogreen:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2

- name: Auto green
run: |
git config --local user.email "cqc@cuiqingcai.com"
git config --local user.name "Germey"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git pull --rebase
git commit --allow-empty -m "a commit a day keeps your girlfriend away"
git push

直接在 GitHub 网页上点击修改,修改好了保存就行了。

修改完了之后我们就自动有了一次 commit,接着 Actions 还会自动触发一次 commit,最后结果如图所示:

可以看到,最后这次 commit 已经变成我自己了。

以后,我每天都可以自动绿了。

(这话怎么听着这么奇怪?

福利

最后,原作者还预留了一个定时任务,可以使得你想绿就绿,不仅可以让你每天都绿,还能让你每小时都绿,每分钟都能绿。

想绿就绿,其乐无穷。

绿的方式很简单,套用原作者的介绍了,修改 yml 文件的定时配置就好了:

计划任务语法有 5 个字段,中间用空格分隔,每个字段代表一个时间单位。

1
2
3
4
5
6
7
8
9
┌───────────── 分钟 (0 - 59)
│ ┌───────────── 小时 (0 - 23)
│ │ ┌───────────── 日 (1 - 31)
│ │ │ ┌───────────── 月 (1 - 12 或 JAN-DEC)
│ │ │ │ ┌───────────── 星期 (0 - 6 或 SUN-SAT)
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
* * * * *

每个时间字段的含义:

符号 描述 举例
* 任意值 * * * * * 每天每小时每分钟
, 值分隔符 1,3,4,7 * * * * 每小时的 1 3 4 7 分钟
- 范围 1-6 * * * * 每小时的 1-6 分钟
/ */15 * * * * 每隔 15 分钟

比如修改为 0 * * * * ,你就能做到每小时都绿了,修改为 * * * * * 你就能每分钟都绿了,真舒服啊!

:由于 GitHub Actions 的限制,如果设置为 * * * * * 实际的执行频率为每 5 分执行一次,也就是每 5 分钟绿一次,也还不错嘛~

技术杂谈

大家想必听说过一些变量命名格式,比如大驼峰式、小驼峰式,其他的你还知道什么?

今天看文档的时候,提到了一个 kebab case,这个你知道是什么命名格式吗?不知道的接着往下看。

这篇文章我们把所有变量命名格式梳理下。

Lower Camel Case

又名:小驼峰式

特征:第一个单词首字母小写,后续单词首字母大写。

例如:firstName、lastName

Upper Camel Case

又名:大驼峰式、Pascal Case

特征:每个单词的首字母都大写。

例如:FirstName、LastName

Snake Case

特征:每个单词小写,单词之间用下划线连接。

例如:first_name、last_name

Kebab Case

又名:Spinal Case、Train Case、Lisp Case

特征:每个单词小写,单词之间用中划线连接。

例如:first-name、last-name

Studly Case

特征:混合大小写,对大写字母的使用没有语义语法意义。有时只有元音是大写的,其他时候大写和小写是交替的,但通常只是随机的。这个名字来自讽刺或讽刺意味,它是作者试图传达自己的冷静的一种尝试。它也可以用来模拟营销人员在命名计算机软件包时违反标准英语案例惯例的行为,即使在没有技术要求的情况下也是如此。

例如:tHeqUicKBrOWnFoXJUmpsoVeRThElAzydOG

参考来源

技术杂谈

问题

最近开发过程中遇到了一个小知识点,这里有一张 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 节日快乐!