简介

GPG 是一个用于加解密的命令行工具,其中包含了对一些常用的加解密场景的支持命令。

GPG 是 GnuPG (GnuPrivacyGuard) 的缩写。其中的 PG 是指 PGP (Pretty Good Privacy):
https://en.wikipedia.org/wiki/Pretty_Good_Privacy

PGP 是商业用的加密软件,GPG 是自由软件基金开发的开源替代版本。

版本发布历史可参见官网 https://www.gnupg.org/

安装及入门用法

参见: https://www.ruanyifeng.com/blog/2013/07/gpg.html

MAC 环境直接 brew install gnupg

然后使用 gpg –version 确认安装成功以及查看版本信息

我当前使用版本为:

1
2
3
4
5
6
gpg (GnuPG) 2.3.2
libgcrypt 1.9.4
Copyright (C) 2021 Free Software Foundation, Inc.
License GNU GPL-3.0-or-later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

使用场景

秘钥管理

密钥管理大致包括如下几个方面:

  1. 密钥生成、查看、删除、导入、导出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 生成秘钥
gpg --full-generate-key

# 查看秘钥列表
gpg --list-keys --keyid-format long

# 删除秘钥
gpg --delete-secret-keys [Key ID]
gpg --delete-keys [Key ID]

# 添加子密钥
gpg --quick-add-key [Key ID] rsa2048 encr
gpg --quick-add-key [Key ID] rsa2048 sign
gpg --quick-add-key [Key ID] rsa2048 auth

# 导入秘钥
gpg --import private-key.txt

# 导出秘钥
# 公钥
gpg --armor --output pub-key.txt --export [Key ID]
# 私钥
gpg --armor --output private-key.txt --export-secret-keys [Key ID]
gpg --armor --output private-sub-key.txt --export-secret-subkey [Key ID]
  1. 密钥发布、搜索

密钥发布是指将自己的公钥发布到互联网上,使得别人可以搜索下载到。公钥服务器有很多,随便找一个发布就可以。公钥服务器之间会进行公钥的互相同步。

推荐使用 https://keys.openpgp.org/

搜索时可使用他人的 Key ID(邮箱或者HASH)进行搜索

1
2
3
4
5
6
7
8
9
10
# 发布公钥
gpg --keyserver hkps://keys.openpgp.org --send-keys [Key ID]
# or
gpg --export [Key ID] | curl -T - https://keys.openpgp.org
# or
# 在 https://keys.openpgp.org/upload 这里上传公钥文件

# 获取他人公钥
# 搜索 https://keys.openpgp.org/
gpg --sign-key [Key-ID]

3、密钥注销

密钥注销是指声明此密钥不可用,一般可能是由于私钥泄漏、私钥密码遗忘等原因

1
gpg --gen-revoke --output revoke.asc [Key ID]

资源发布

GPG 经常用于对资源进行签名,类似于校验码。

校验码常见的有 CRC、MD5 等,通过对资源内容做 HASH 值的计算,来确认资源内容是否被修改过。但校验码只能验证文件的完整性,是跟文件一一对应的,假如有人将文件内容和校验码同时修改了,仅使用校验码的手段是无法发现的。

GPG 的签名在校验时,可以同时确认文件完整性和签名所属用户,不同用户对同一文件的签名结果是不一样的。

举个例子:

在 Ubuntu 系统中使用 apt 命令安装软件,apt 会同时下载软件包和软件包的 GPG 签名,然后对软件包进行签名验证。

验证时会同时确认软件包的完整性和签名用户身份,只有两者都是可信的,才会信任此软件安装包并进行安装。

假如有人恶意将服务器上的软件包内容和签名都重新打包了,那么签名对应的用户信息必然会发生改变,校验时就能发现包被修改过。

完整的信任链是这样的:

  • 可信用户 -> 可信签名 -> 可信软件

之所以不简单使用如下的信任链:

  • 可信签名 -> 可信软件

是因为:

  1. 可信用户是可以枚举的,没有多少个,完全可以将可信官方的用户ID内置在系统中。

  2. 可信签名太多了,每个软件都不一样,无法枚举。

邮件加密

关于为什么要加密邮件,参见此网站:https://emailselfdefense.fsf.org/en/。

不过目前国内可能没有这个意识或者由于其他原因并不普及。

邮件加密的方式有两种:

  1. 有些邮件客户端支持GPG插件,可以自动对邮件内容进行加解密。

  2. 对于不支持的邮件客户端(国内常见的都不支持),可采用手工加密,然后将加密后的数据写进邮件发送的方式。

在 Git 中使用 GPG

Git 默认使用邮件地址来确认提交者身份,但邮件地址是可以随便修改的,假如别人使用你的邮件地址做了一个提交,那么 Git 是无法识别提交的真正用户的。

参考 https://zhuanlan.zhihu.com/p/76861431

为了防止提交被假冒,Git 引入了 GPG 签名机制。

基本原理如下:

  1. 用户将自己的 GPG 公钥上传到 Git 服务器

  2. 用户在 Commit 时使用私钥对 Commit 进行签名

  3. Git 服务器接收到 Commit 信息后,使用用户的公钥对签名进行校验,校验通过则标识为有效 Commit

邮件地址可以被伪造,但签名无法被伪造,伪造者使用伪造者的私钥进行的签名不会被 Git 服务器校验通过,因为公钥不匹配。

加解密

基本流程:

  1. 使用收件用户的公钥加密文件

  2. 将加密后的文件发送为收件人

  3. 收件人使用自己的私钥解密文件

加密

1
gpg --recipient [Key ID] --output a.encrypt.txt --encrypt a.txt

解密

1
gpg --recipient [Key ID] --output a.decrypt.txt --decrypt a.encrypt.txt

签名

基本流程

  1. 使用自己的私钥对某个文件进行签名
  2. 将签名后的文件发送给收件人
  3. 收件人使用发送者公钥验证签名

签名

1
2
3
4
5
6
# 会将文件内容和签名信息合并然后生成新的 ascii 文件
gpg --clearsign a.txt
# 同上 但生成的是二进制文件
gpg --sign a.txt
# 生成独立的签名文件
gpg --detach-sign --armor a.txt

验证签名

1
gpg --verify a.txt.asc

总结一下

GPG 最重要的作用就是在加密的同时添加了加密人的信息,使得信任机制变得更有效。

参考文档

GPG 入门教程

&ensp;&ensp;https://www.ruanyifeng.com/blog/2013/07/gpg.html

简明 GPG 概念

&ensp;&ensp;https://zhuanlan.zhihu.com/p/137801979

在 Github 上使用 GPG 的全过程

&ensp;&ensp;https://zhuanlan.zhihu.com/p/76861431

震惊!竟然有人在 GitHub 上冒充我的身份

&ensp;&ensp;https://blog.spencerwoo.com/2020/08/wait-this-is-not-my-commit

Tips:
&ensp;&ensp;MySQL 8.0以下不支持窗口函数,非要使用的话参见:https://stackoverflow.com/questions/3333665/rank-function-in-mysql

简介

窗口函数,别名:开窗函数、Window函数。又称分析函数

常见使用场景为求排名、TopN、累加等涉及到对数据做二次处理且依赖上下文的环境的计算场景

历史:2003年被加入到SQL标准中,2011年的修订中添加了一些增强功能。

语法

1
2
3
4
5
function([expr]) over (
[partition by partition_expr]
[order by order_expr]
[frame]
)

分区:分区由 partition by 子句定义。如果没有指定 partition by 子句,则整个查询与分析结果集作为一个窗口分区。

排序:排序由 order by 子句定义

框架(窗口):框架在分区内对行进一步限制。框架元素不适用于排名函数。使用

1
ROWS BETWEEN start AND end

来定义,细节参见 https://dev.mysql.com/doc/refman/8.0/en/window-functions-frames.html

函数列表

专用窗口函数

  • row_number()

    • 取行号
  • rank()

    • 取排名 排名相同则值一样,但会占用后续排名的位置 , 比如 1、1、1、4
  • dense_rank()

  • percent_rank()

    • 计算分区内排名小于当前行的数据条数占总条数(不包含排名最高的行)的比例
  • cume_dist()

    • 计算分区内排名小于等于当前行的数据条数占总条数的比例
  • lag(expr [, N[, default]])

    • 返回分区内排名小于当前行的第前N行位置的值 expr 代表可以不仅仅可以取字段,也可以在取的同时对字段做操作 N默认为1 default默认为null
  • lead(expr [, N[, default]])

    • 与lag相反,取分区内排名大于当前行的第前N行位置的值
  • ntitle(N)

    • 将分区内的数据分为N组,返回值为组序号(最小为1)
  • first_value(expr)

    • 取当前窗口范围内的第一行,lag和leag是相对位置, first_value是绝对位置,
  • last_value(expr)

    • 取当前窗口范围内的最后一行
  • nth_value(expr, N)

    • 取当前窗口范围内的第N行

可用于窗口函数的聚合函数

  • avg
  • count
  • sum
  • min
  • max
  • bit_and
  • stddev 标准差
  • variance 方差

窗口函数与聚合函数的对比

  1. 两者在执行前都会对数据进行分区,窗口函数使用 PARTITION BY,聚合函数使用 GROUP BY。

    如下图,数据被分为两个区

  2. 窗口函数相比于聚合函数多了一个窗口的概念,即分区之后又进行了一次虚拟的切分,把一个分区内的数据分成了多个窗口,并且多个窗口间数据是有重复的。

    场景1:窗口宽度不设置,则默认为从第一行到当前行

    1
    2
    3
    4
    5
    6
    7
    8
    SELECT
    RANK() OVER (
    PARTITION BY `color`
    ORDER BY `id`
    # Frame 默认为空 等价于 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    )
    FROM
    color_table

    场景2:窗口宽度为前后各一行

    1
    2
    3
    4
    5
    6
    7
    8
    SELECT
    RANK() OVER (
    PARTITION BY `color`
    ORDER BY `id`
    ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
    )
    FROM
    color_table

    场景3:窗口宽度为3包含前两行

    1
    2
    3
    4
    5
    6
    7
    8
    SELECT
    RANK() OVER (
    PARTITION BY `color`
    ORDER BY `id`
    ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    )
    FROM
    color_table
  3. 窗口函数的每次操作数据范围为窗口数据的行数,移动步长为1。聚合函数的每次操作数据范围为分区数据的行数,移动步长为分区行数。

  4. 聚合函数也可以在窗口函数中使用,但作用范围缩小为对每个窗口内的数据进行聚合操作

窗口范围定义要点总结

ROWS 和 RANGE 的区别

  • ROWS:框架由开始行和结束行的位置来定义,偏移量是行号与当前行号的差异。这个比较常用。
  • RANGE:框架由值范围内的行定义,偏移量是行值与当前行值的差异。这个不太常用。

ORDER BY 对窗口范围的影响

  • 没有ORDER BY, 则窗口范围是整个分区。因为没有排序,所以每一行都是对等的。
  • 有ORDER BY,则窗口范围为分区内第一行到当前行。

窗口范围定义实例

  • 前后各N行

    1
    ROWS BETWEEN N PRECEDING AND N FOLLOWING
  • 前N行到前M行

    1
    ROWS BETWEEN N PRECEDING AND M PRECEDING
  • 第一行到后N行

    1
    ROWS BETWEEN UNBOUNDED PRECEDING AND N FOLLOWING
  • 前N行到最后一行

    1
    ROWS BETWEEN N PRECEDING AND UNBOUNDED FOLLOWING

使用窗口函数的一些场景

默认数据表为 scores

id name value
1 Alice 1
2 Bob 2
3 Alice 3
4 Bob 4

一、求当前行值的占比

利用了不排序的窗口特性

1
2
3
4
5
6
7
SELECT
*,
sum( `value` ) over w AS `sum`,
`value` / sum( `value` ) over w AS `percent`
FROM
scores
WINDOW w AS ()
id name value sum percent
1 Alice 1 10 0.1000
2 Bob 2 10 0.2000
3 Alice 3 10 0.3000
4 Bob 4 10 0.4000

二、累乘

使用对数+累加实现

1
2
3
4
5
6
7
SELECT
*,
power(2,
sum(log2( `value` )) OVER ( ORDER BY `value` )
) cum_multi
FROM
scores
id name value cum_multi
1 Alice 1 1
2 Bob 2 2
3 Alice 3 6
4 Bob 4 24

参考文档

mysql8.0 窗口函数文档
&ensp;&ensp;&ensp;&ensp;https://dev.mysql.com/doc/refman/8.0/en/window-functions.html
StackOverflow上关于MySQL如何实现rank()的说明
&ensp;&ensp;&ensp;&ensp;https://stackoverflow.com/questions/3333665/rank-function-in-mysql
通俗易懂的学会:SQL窗口函数
&ensp;&ensp;&ensp;&ensp;https://zhuanlan.zhihu.com/p/92654574
维基百科:SQL
&ensp;&ensp;&ensp;&ensp;https://zh.wikipedia.org/wiki/SQL
数据分析|SQL窗口函数最全使用指南
&ensp;&ensp;&ensp;&ensp;https://zhuanlan.zhihu.com/p/120269203
窗口函数
&ensp;&ensp;&ensp;&ensp;https://help.aliyun.com/document_detail/63972.html

前言

&ensp;&ensp;ES的官方文档中关于增量快照的说明是:
你的第一个快照会是一个数据的完整拷贝,但是所有后续的快照会保留的是已存快照和新数据之间的差异。

&ensp;&ensp;看到这个解释后,脑海中产生出两个疑问:

  1. 删除历史快照会对新的快照造成影响吗?
  2. 如果每次保存的都是差异,那我的快照数据量是不是会越来越大?
  3. 恢复完整数据的时候要如何恢复?需要从第一个快照开始一个一个恢复吗?

&ensp;&ensp;经过一番思考和搜索,总结一下我对这些问题的理解。

基础知识

&ensp;&ensp;首先,有一些关键知识点需要了解一下:

  • ES 的底层使用了 Lucene, ES 快照实际上是对 Lucene 快照文件的一次保存。
  • Lucene 快照包含了当前时间点上与需要快照的索引相关的全部文件。
  • Lucene 的索引是由多个分段文件组成的,且分段文件是不能被修改的,只会新增和删除。
  • 只有两种情况下会产生新的分段文件:1、新增、修改、删除索引内的数据 2、将已存在的分段文件合并
  • 快照删除时仅删除没有被任何快照引用的文件

快照过程梳理

&ensp;&ensp;首次快照示意图,快照名称记为:快照 A

&ensp;&ensp;与快照 A 关联的文件列表为:1、2、3然后我们对ES中的数据做了一些修改,导致Lucene文件发生了一些变化。由于Lucene索引分段文件的特性,只会新增和删除而不会修改,因此此时的Lucene中文件可能如下

&ensp;&ensp;然后进行第二次快照,快照名称记为:快照 B

&ensp;&ensp;与快照 B 关联的文件列表为:2、3、4
&ensp;&ensp;然后删除快照 A

&ensp;&ensp;因为
&ensp;&ensp;&ensp;&ensp;与快照 A 关联的文件是:1、2、3
&ensp;&ensp;&ensp;&ensp;与快照 B 关联的文件是:2、3、4
&ensp;&ensp;所以 只有文件 1 可以被删除。

疑问解答

  1. 删除历史快照会对新的快照造成影响吗?
    答:不会的,以上面的流程为例,只会清理不被任何快照关联的文件。而每个快照关联的文件列表都能还原当时的全量数据。

  2. 如果每次保存的都是差异,那我的快照数据量是不是会越来越大?
    答:如果定期清理历史的快照,那么快照的数据量是不会越来越大的。虽然每次都保存的是差异,但这个差异并不是绝对的新增数据,而是对历史数据做了修改后的一个局部全量数据。也就是说,新文件和旧文件之间是有重叠数据的,所以清理历史文件即可。

  3. 恢复完整数据的时候要如何恢复?需要从第一个快照开始一个一个恢复吗?
    答:不需要,只需恢复指定时间点的快照即可,因为每个快照都保留了那个时间点的全量数据。

参考文档

&ensp;&ensp;https://www.easyice.cn/archives/302

&ensp;&ensp;https://www.elastic.co/guide/cn/elasticsearch/guide/current/backing-up-your-cluster.html

&ensp;&ensp;https://elasticsearch.cn/question/10818

前言

  • 目标:ClickHouse单机集群版安装
  • 本文面向ClickHouse初学者
  • 服务器环境 Ubuntu18.04

集群安装步骤如下:

  1. 安装java
  2. 安装Zookeeper(依赖Java)
  3. 安装单机ClickHouse
  4. 修改ClickHouse配置为集群版

安装步骤

一、安装Java

在线安装

1
2
sudo apt update
sudo apt install openjdk-8-jdk

离线安装

  1. 下载安装包:https://www.oracle.com/cn/java/technologies/javase/javase-jdk8-downloads.html 在当前页面寻找 jdk-8u301-linux-x64.tar.gz 并下载
  2. 将文件上传至服务器并解压到指定目录
    1
    2
    3
    tar -zxvf jdk-8u301-linux-x64.tar.gz
    mkdir /usr/local/src/jdk/
    mv jdk1.8.0_301/ /usr/local/src/jdk/jdk1.8
  3. 在文件 /etc/profile 中添加环境变量
    1
    2
    3
    export JAVA_HOME=/usr/local/src/jdk/jdk1.8
    export PATH=$PATH:$JAVA_HOME/bin
    export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
  4. 执行source /etc/profile 使得环境变量生效
  5. 使用java -version命令验证是否安装成功

二、安装Zookeeper

  1. 下载安装包:https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz
  2. 将文件上传至服务器指定目录并解压
    1
    2
    3
    4
    5
    6
    7
    8
    # 自定义安装路径为 /usr/local/zookeeper
    ZK_PATH=/usr/local/zookeeper
    mkdir -p $ZK_PATH
    tar -zxvf apache-zookeeper-3.7.0-bin.tar.gz
    mv apache-zookeeper-3.7.0-bin/* $ZK_PATH
    # 修改配置
    cd $ZK_PATH
    cp conf/zoo_sample.cfg conf/zoo.cfg
  3. 修改配置文件内容为如下
    1
    2
    3
    4
    5
    6
    tickTime=2000
    initLimit=10
    syncLimit=5
    # 主要修改dataDir属性
    dataDir=/data/zookeeper
    clientPort=2181
  4. 启动服务:$ZK_PATH/bin/zkServer.sh start
  5. 此后命令如果不需要配置认证则可跳过
  6. 进入ZK命令行:$ZK_PATH/bin/zkCli.sh
  7. 执行添加用户命令:
    1
    2
    addauth digest username:password
    setAcl / auth:username:cdrwa
  8. 重启zk服务:$ZK_PATH/bin/zkServer.sh restart

三、安装单机ClickHouse

  1. 安装CK命令如下,在安装过程中会提示输入默认用户(用户名为default)的密码
    1
    2
    3
    4
    5
    6
    7
    sudo apt-get install apt-transport-https ca-certificates dirmngr
    sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E0C56BD4
    # 此处使用清华源 加快安装速度
    echo "deb https://mirrors.tuna.tsinghua.edu.cn/clickhouse/deb/stable/ main/" | sudo tee /etc/apt/sources.list.d/clickhouse.list
    sudo apt-get update
    sudo apt-get install -y clickhouse-server clickhouse-client
    sudo service clickhouse-server start
  2. 安装完成进行登录测试:clickhouse-client -u default –password password

四、修改ClickHouse配置为集群版

  1. 备份默认配置文件:cp /etc/clickhouse-server/config.xml /etc/clickhouse-server/config.xml.bak
  2. 然后编辑默认配置文件 /etc/clickhouse-server/config.xml 并删除集群相关的配置

    文件中 remote_servers、zookeeper、macros 标签里的全部内容

  3. 添加自定义配置文件:vi /etc/clickhouse-server/config.d/config.xml 内容如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <yandex>
    <zookeeper>
    <node index="1">
    <host>此处改为ZK的HOST</host>
    <port>2181</port>
    </node>
    <identity>zk_username:zk_password</identity>
    <session_timeout_ms>600000</session_timeout_ms>
    </zookeeper>

    <remote_servers>
    <test>
    <shard>
    <internal_replication>true</internal_replication>
    <replica>
    <host>本机IP</host>
    <port>9000</port>
    <user>default</user>
    <password>password</password>
    </replica>
    </shard>
    </test>
    </remote_servers>

    <networks>
    <ip>::/0</ip>
    </networks>

    <macros>
    <shard>01</shard>
    <replica>node1</replica>
    </macros>
    </yandex>

关键步骤解释

集群配置文件解释

zookeeper 配置

  1. 如果Zookeeper为集群版,直接增加node节点即可

remote_servers 配置

  1. remote_servers下级节点为集群,可配置多个集群
  2. 集群下级节点为分片(shard),可配置多个shard,不同shard不能用同一个ClickHouse实例
  3. 分片下级为副本(replica),可对分片配置多个副本,默认最少0个,不同副本不能用同一个ClickHouse实例
  4. internal_replication 用来控制当数据写入时(必须是Replicated*的表),由分片自己负责副本间的数据复制,否则分布式表的副本数据写入需要由Distributed引擎来负责

macros 配置

  1. 参数解释 https://stackoverflow.com/questions/68272747/what-is-macros-in-clickhouse-and-what-is-use-of-macros-in-clickhouse
  2. 本质上就是针对当前实例的全局变量的定义,可以被某些地方来引用
  3. 此配置需要在集群中全局唯一
  4. 此处的参数会在创建Replicated*的表时被引用
  5. shard的值为当前节点在在集群中的分片编号,需要在集群中唯一
  6. replica是副本的唯一标识,需要在单个分片的多个副本中唯一

参考文档

ClickHouse官方文档:

StackOverFlow关于macros的解释

一本书

  • 《ClickHouse原理解析与应用实践》

前言

用了这么长时间的python,一直觉得collections库里面的东西真是太好用了,然而发现很多人竟然还没听过,忍不住想安利一下。

简介

collections是python标准库中的一员。官方文档解释为:Container datatypes。提供了对python的基础(容器)数据类型dict、list、set、tuple的替代选择。

模块内成员有:

  • namedtuple # 命名元组
  • deque # 双端队列
  • ChainMap # 链式字典
  • Counter # 计数器
  • OrderedDict # 有序字典
  • defaultdict # 默认值字典
  • UserDict
  • UserList
  • UserString

namedtuple(命名元组)

1
collections.namedtuple(typename, field_names, *, rename=False, defaults=None, module=None)

导入

1
from collections import namedtuple

创建

1
2
3
4
5
Point = namedtuple('Point', ['x', 'y'])
# 文档字符串查看
print(Point.__doc__)
# >>> 'Point(x, y)'
p = Point(11, y=22)

用法

1
2
3
4
5
6
7
8
9
10
11
12
# 类tuple操作
print(p[0] + p[1])
# >>> 33

# 解包
x, y = p
print(x, y)
# >>> (11, 22)

# 属性访问
print(p.x)
# >>> 11

用途

因为namedtuple更节省内存

  1. 代替某些仅需要访问数据的类
    1
    2
    3
    4
    5
    6
    7
    8
    class Stock(object):
    def __init__(self,name,shares,price):
    self.name = name
    self.shares = shares
    self.price = price
    # or
    import collections
    Stock = collections.namedtuple('Stock','name shares price')
  2. 代替不需要改变的字典

原理

1.为啥会节省内存?

TODO

2.如何实现的属性访问?

查看源码,发现其实就是给每个参数做了个别名,实质还是通过索引去访问数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print(p._source)
'''
class Point(tuple):
'Point(x, y)'
__slots__ = ()
_fields = ('x', 'y')
def __new__(_cls, x, y):
'Create new instance of Point(x, y)'
return _tuple.__new__(_cls, (x, y))
...
x = _property(_itemgetter(0), doc='Alias for field number 0')
y = _property(_itemgetter(1), doc='Alias for field number 1')

'''

deque(双端队列)

1
class collections.deque([iterable[, maxlen]])

Deque队列是由栈或者queue队列生成的(发音是 “deck”,”double-ended queue”的简称)。Deque 支持线程安全,内存高效添加(append)和弹出(pop),从两端都可以,两个方向的大概开销都是 O(1) 复杂度。

虽然 list 对象也支持类似操作,不过这里优化了定长操作和 pop(0) 和 insert(0, v) 的开销。它们引起 O(n) 内存移动的操作,改变底层数据表达的大小和位置。

如果 maxlen 没有指定或者是 None ,deques 可以增长到任意长度。否则,deque就限定到指定最大长度。一旦限定长度的deque满了,当新项加入时,同样数量的项就从另一端弹出。限定长度deque提供类似Unix filter tail 的功能。它们同样可以用与追踪最近的交换和其他数据池活动。

创建

1
test_deque = collections.deque([1, 2, 3, 4], 1000)

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from collections import deque

test_deque = deque()
test_deque.append(1)
test_deque.append(2)
test_deque.append(3)
test_deque.appendleft(0)
# >>> deque([0, 1, 2, 3])

test_deque.pop()
# >>> deque([0, 1, 2])

test_deque.popleft()
# >>> deque([1, 2])

用途

  1. deque特点在于双端操作速度都快,如果要将数据插入列表头部或者从头部提取数据(保持处理顺序)则使用deque能够提升很大效率
  2. deque支持限制队列长度,在某些类型的应用,比如保留1000条日志时很有用。

性能测试结果

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
# coding:utf8
import time
from concurrent.futures import ThreadPoolExecutor
from collections import deque

pool = ThreadPoolExecutor(100)

data_length = 100000

# 测试
def test(_type):
start = time.time()

if _type == "list":
l = []

def work(i):
l.insert(0, i)

elif _type == "deque":
l = deque()

def work(i):
l.appendleft(i)

r = list(pool.map(work, range(data_length)))
print(f"{_type}: {time.time() - start}")
return


test("deque")
test("list")

"""
deque: 3.510190725326538
list: 6.319669008255005
"""

ChainMap

一个 ChainMap 将多个字典或者其他映射组合在一起,创建一个单独的可更新的视图。 如果没有 maps 被指定,就提供一个默认的空字典,这样一个新链至少有一个映射。

创建

1
2
3
4
5
6
7
8
9
from collections import ChainMap

cm = ChainMap({1: 1}, {2: 2}, {3: 3})

print(cm[1])
# >>> 1
print(cm[2])
# >>> 2

用途

  1. 模拟python内部lookup链
    1
    2
    import builtins
    pylookup = ChainMap(locals(), globals(), vars(builtins))

Counter

一个计数器工具提供快速和方便的计数

创建

1
2
3
from collections import Counter
c = Counter("aaacccddd")
c = Counter(["a", "a", "b", "c", "d"])

用法

  1. 计数
    1
    2
    3
    4
    5
    6
    c = Counter(["a", "a", "b", "c", "d"])
    # >>> Counter({'a': 2, 'b': 1, 'c': 1, 'd': 1})
    c += Counter(["a", "a", "b", "c", "d"])
    # >>> Counter({'a': 4, 'b': 2, 'c': 2, 'd': 2})
    c -= Counter(["a", "a", "b", "c", "d"])
    # >>> Counter({'a': 2, 'b': 1, 'c': 1, 'd': 1})
  2. 统计重复最多的元素
    1
    2
    3
    c = Counter(["a", "a", "b", "c", "d"])
    c.most_common(1)
    # >>> [('a', 4)]

defaultdict

1
class collections.defaultdict([default_factory[, ...]])

返回一个新的类似字典的对象。 defaultdict 是内置 dict 类的子类。它重载了一个方法并添加了一个可写的实例变量

创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from collections import defaultdict

d = defaultdict(int)
print(d["xxx"])
# >>> 0


d = defaultdict(dict)
print(d["xxx"])
# >>> {}

d = defaultdict(lambda: defaultdict(int))
print(d["xxx"]["xxx"])
# >>> 0

d = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(int))))
print(d["xxx"]["xxx"]["xxx"]["xxx"])
# >>> 0

用法

  1. 提供默认值

    1
    2
    3
    4
    5
    6
    7
    d = defaultdict(int)
    print(d["xxx"])
    # >>> 0

    d = {}
    print(d.get("xxx", 0))
    # >>> 0
  2. 对多层嵌套数据结构提供默认值

    1
    2
    3
    4
    # 预定义数据结构
    d = defaultdict(lambda: {"name": "", "child": defaultdict(str)})

    print(d["xxx"]["child"]["tom"])
  3. 省去if判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    d = defaultdict(list)
    d["name"].append("xxx")
    d["age"].append("111")
    print(d)
    # >>> defaultdict(<class 'list'>, {'name': ['xxx'], 'age': ['111']})

    d = {}
    if "name" not in d:
    d["name"] = []
    d["name"].append("xxx")

前言

由于某些很坑的原因,需要将一台mysql里的全部数据进行迁移,并且需要迁移用户及权限。下面记录下用户及权限是如何迁移的。

原理

首先,我没找到现成的工具。。。因此只好自己搞了,好在也没多复杂。

用户迁移

mysql里的用户都存在于mysql.user这张表里。可以通过SQL查询这张表拿到host、user、authentication_string(加密后的密码)。

然后通过SQL在新库中创建用户,并更新新库中的mysql.user表里的authentication_string字段。这样就不需要知道账号的明文密码了。

需要注意的是,修改完mysql.user表中的加密密码字段后,需要执行

1
FLUSH PRIVILEGES

然后新的密码才能生效

权限迁移

利用SQL语句

1
SHOW GRANTS FOR 'user'@'host';

可查询指定用户被授权过的权限,得到的直接就是一条授权语句。将此授权语句在新库中执行即可

需要注意的是授权语句中如果有针对某个表的授权,那么当表不存在时会报错,因此授权的迁移只能放在数据的迁移之后

完整示例脚本

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
# coding:utf8
import pymysql


source_mysql_client = pymysql.connect(
host=None, user=None, password="", database=None, port=3306
)
source_mysql_client.autocommit(True)
source_mysql_cursor = source_mysql_client.cursor()

target_mysql_client = pymysql.connect(
host=None, user=None, password="", database=None, port=3306
)
target_mysql_client.autocommit(True)
target_mysql_cursor = target_mysql_client.cursor()

# 需要忽略的用户
ignore_users = ["mysql.session", "root", "mysql.sys"]

#
target_sql_list = []

sql = "select host, user, authentication_string from mysql.user"
source_mysql_cursor.execute(sql)

for host, user, authentication_string in source_mysql_cursor.fetchall():
if user in ignore_users:
continue
# 创建用户
create_sql = "CREATE USER IF NOT EXISTS '{}'@'{}' IDENTIFIED BY 'skvnajnvr92jkfads'".format(
user, host
)
# 修改密码
change_password_sql = "UPDATE mysql.user SET authentication_string='{}' WHERE host='{}' AND user='{}'".format(
authentication_string, host, user
)

target_sql_list.append(create_sql)
target_sql_list.append(change_password_sql)

# 授权语句获取
grant_sql = "show grants for '{}'@'{}'".format(user, host)
source_mysql_cursor.execute(grant_sql)
grant_sql_result = source_mysql_cursor.fetchall()
target_sql_list.extend([x[0] for x in grant_sql_result])

source_mysql_cursor.close()
source_mysql_client.close()


#
target_sql_list.append("FLUSH PRIVILEGES")
for sql in target_sql_list:
r = target_mysql_cursor.execute(sql)
print(sql)
print(r)

target_mysql_cursor.close()
target_mysql_client.close()

清理浏览器书签有感而发

前言

平时浏览些网站、看些博客、查点问题的时候,总是随手会加个书签。偶尔心血来潮的时候,还会建个目录,收集一系列相关文章、资料。

然而,等你真的碰到了问题的时候,你根本想不起来到书签里、笔记里去找答案。

除非,这个笔记是你自己写的,而非随手收藏的。

网盘里的数T各种视频、文档,根本不会去看。

今天突然看到了自己的书签好乱,整理了一下,发现好多东西都可以删掉了。

删删删

某些网站的首页

唉、不知道当时为啥要存这个东西,还单独建了个文件夹。难道百度不香吗?

  • CSDN
  • StackOverflow
  • SegmentFault
  • Ruby
  • ubuntu中文论坛
  • O’reilly
  • WebSpidersScreenScrapers
  • 小程序
  • 账号网
  • 豌豆荚
  • 图灵
  • shadon
  • 开源中国
  • 梦幻西游

某些已经不需要了的东西

  • Sublime的相关教程

    专业编辑器很香。不过Sublime的多行编辑操作很爽,一直在用

  • Vim的相关教程

    曾经也是重度用户,不过现在感觉会简单操作就可以了。

  • WingIDE

    Pycharm香

  • flash

那些变了的网站

生活不易

  • 伯乐在线竟然改成了财经网站?
  • api之家:http://www.apihome.cn/ 咋成了OneinStack的官网?
  • http://jm.taobao.org/ 这个还在更新吗?
  • 乌云
  • 百度云盘:由于产品的升级与调整,个人主页关闭并停止提供服务~。
  • firebug 停了好久好久了

那些被下架的资源

  • 51CTO之前看的几本书的链接都失效了
  • 淘宝收藏的几个卖小号的链接也都没用了

某些资源聚合站

收藏了很多很杂的东西,现在看起来,其实没啥卵用,如果一个东西你不去用起来的话,学习效率很低不说,很快就忘了。根本没必要收藏这些乱七八糟的东西,等用的时候搜索就好了

  • 比如书册网、实验楼

不明所以

收藏的目的已经想不起来了,而且现在看起来卵用没有

  • 几个搜狗微信的公众号的链接。。。
  • 几个知乎的问题

吐槽一下

  • CSDN

    曾经真的以为是国内最大的技术博客网站。后来发现里面好多乱七八糟的文章(格式乱)。而且好多从别的站爬过来的文章。还有,啥时候CSDN能改下UI,不难看吗???

  • 百度云盘

    竟然有个内容商城:https://pan.baidu.com/mall/home?from=panhome 是在下孤陋寡闻了

后记

收藏应该是收藏那些值得收藏的东西,并且当以后用到的时候可以很快触达。

那些可以被搜索引擎替代的东西,就不要来占空间了。附:如何用好搜索引擎

还是自己写的笔记印象会更深刻一些。自己的笔记,可以按照自己的思维方式把资源重新整合,以后翻查也可以更快的唤起记忆。

前言

某日,准备良久后,开始入K8S的坑。以下经历,惨不忍睹。

K8S 安装

K8S官方安装方式比较繁琐,有大神出了一键部署包。https://github.com/fanux/sealos。

安装最新版的K8S免费且简单。想安装其他版本的话,就收费了。离线安装包50一位。

想着自己只是测试,然后客户那边是v1.17。我装个v1.18的应该没啥问题,于是就直接开搞。安装无比顺利,两台机器,master是自己的开发机:ubuntu18.04。node新开了一台centos7.4。(当时没多想,后来坑死了)

服务部署

应用很简单。前后端两个Deployment、前后端各一个Service, 主要为了能在外部访问。
前期不太熟悉Service的部署方式,略微耗费了点时间,也还好,没啥大问题。

然后开心的启动服务,一切正常。 然后扭头搞别的事情去了。

问题出现

过了大概1个小时?突然发现服务访问不了了。在Service里配置了使用NodePort的方式,因此正常情况下我可以用任意一台node或master的IP来访问服务,但现在的情况是master可以,node不行。

一番苦思冥想,各种测试,外加谷歌大法,终于找到了个大神的文章:http://www.mydlq.club/article/78/。

恍然大悟,原来是v1.18的版本太新了,需要升级linux内核才能匹配。不过升级内核太麻烦,干脆换系统吧。于是一顿操作把node节点系统换成了centos8.1。这下内核版本没问题了,重启服务,发现两个节点访问都没问题。

很开心。

新的问题

过了30秒吧,突然发现master节点的服务不能访问了。node节点的访问也会报500。

进入pod中发现无法访问master的ip,无法访问任何外网。

于是又开始了疯狂DEBUG, 1个小时后。。。

会不会是因为master和node的系统不一致???

于是一通操作,将node改成ubuntu18.04,重启服务。测试、等待、测试、等待。

终于,没问题了。

心情是这样的

欲哭无泪,没 人 说 节 点 的 系 统 必 须 得 一 致 吧!!!

简介

99%的网络爬虫技术都要与HTTP协议打交道,而在打交道的过程中,你会发现很多好玩的事情。本文主要介绍那些常用的HTTP状态码是如何被反爬虫技术使用的。

常用状态码与反爬虫

2xx

2xx系列状态码一般代表响应成功。

200

OK

最常见的状态码,代表一切正常。

202

Accepted

这个并不常见。原意为请求已经收到,不过服务端采用了异步处理的方式在处理,所以结果没有返回给你。

206

Partial Content

当你访问的资源支持断点下载,并且你使用了Range头部的时候,服务端会返回206,然后你可以继续换下一个范围获取之后的数据。

3xx

资源重定向。需要根据返回头信息中的Location字段重新发起请求。

301

Moved Permanently

资源 永久 重定向。注意是永久。最常见的比如网站升级了HTTPS,则当你使用HTTP访问的时候,会自动重定向到HTTPS的地址。

302

Found

资源 临时 重定向。此种情况下,就不要改你的书签或者访问地址了,可能过一会儿临时地址就失效了。
常见的情况是,某些网站首次访问会有个302跳转,进入某个中间页面做一些奇奇怪怪的操作。比如当你首次访问新浪微博的时候,会触发302跳转,通过JS添加一个访客的Cookie。

304

Not Modified

资源没有改变。这个主要是用来优化网络流量传输的。一般我们访问某个网站时,会访问这个网站的多个页面,这些页面所使用的一些静态资源,比如CSS、JS文件等都是共用的,并且在浏览器内缓存。但为了避免服务端有更新,而客户端没有及时更新,所以每次打开一个网页,浏览器都会重新请求一下这个资源,如果资源没变,那么服务端不用返回资源内容,只要返回304就好了,然后浏览器就自动使用本地缓存。如果服务端资源内容改了,那么就返回200,同时返回新的资源内容。

4xx

4xx一般代表请求有问题。

400

Bad Request

你的请求不知道哪里出了问题。一般是参数格式不对、Cookie不对等情况。

401

Unauthorized

没有登录。一般网站会自动跳转到登录页面。某些服务会直接在页面弹窗出登录框,比如FTP服务。

402

Payment Required

这个一点都不常用。不过很好玩。含义是:你所访问的资源是需要付费的。这个状态码设计的不错,不过真的有人会用吗?

403

Forbidden

禁止访问。这个大概是除了200之外,在爬虫里面最常见的了 ⊙﹏⊙∥∣°,一般就是你的IP被封了。

不过正常情况下,一般代表没有权限访问某个资源。

404

Not Found

资源不见了。一般是资源失效、被删除了。某些情况下,也可能是你没权限…

405

Method Not Allowed

请求方法不对。常见于对某个需要POST访问的资源使用了GET方法。小小提示一下:某些网站是不限制这个的,或者说做了兼容。你可以把需要POST发送的数据作为URL中的参数用GET的方式访问。

413

Payload Too Large

请求体太长了。常见于上传文件过大的情况。

414

URI Too Long

URL太长了。有个不成文的约定,URL长度不能超过1024。不过这个并非标准,各个网站、浏览器的实现也都不一致。所以出现这个情况一般代表你构造的URL有问题。为啥是你构造的,因为肯定是你构造的。如果不是你构造的,这个问题怎么可能被触发呢。
还有个特殊情况,比如新浪微博在某些情况下会返回414来逗你,让你懵逼一会。你的请求没问题,只不过是被识别到你是个爬虫了。

418

I’m a teapot

我是个茶壶。嗯,就是逗你玩。你已经被发现了。

429

Too Many Requests

访问太频繁了。歇一会吧(或者换个代理),你已经被发现了。

5xx

5xx一般代表服务端有问题。

这个就不分开说了,我也没仔细研究过区别。一般是后端代码有问题,处理请求出现了错误导致的。或者是网络不通等。

不过,某些网站会有些迷之行为。明明是正常的请求、正常的响应,偏偏返回个501。

总结一下

常用状态码及其含义跟大家介绍了。

但是,还有个重点:
状态码只是规范,并非强制要求

所以你会看到某些网站在状态码上搞事情。走的路多了,总会遇见鬼的。

HTTP状态码含义全集

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status

概述

记录一下Scala中关于正则表达式的一些方法的使用,之前用惯了Python,发现差别还是挺大的。。。

基本函数/方法使用

正则表达式定义

1
2
val pattern = "\\d+".r
// 类似于python中的 pattern = re.compile("\d+")

还有另一种定义方式,可以为每个分组指定名称,在使用scala.util.matching.Regex.Match对象时使用(下文介绍)

1
val pattern = "(\\d+)-(\\d+)-(\\d+)".r("year", "month", "day")

各种函数

findFirstIn vs findFirstMatchIn

这两个函数都用于查找匹配上的第一个元素,区别在于返回值,findFirstIn返回:Option[String], findFirstMatchIn返回:Option[Match]

1
2
3
4
val pattern = "\\d+".r
val text = "2020-04-16"
//output => 2020
println(pattern.findFirstIn(text).get)

findAllIn vs findAllMatchIn

这两个函数都用于查找匹配上的全部元素

1
2
3
4
val pattern = "\\d+".r
val text = "2020-04-16"
//output => List(2020, 04, 16)
println(pattern.findAllIn(text).toList)

findPrefixOf vs findPrefixMatchOf

此函数用于从目标字符串开始位置开始匹配,若匹配上则返回匹配到的值,发偶咋返回None,类似Pyhton的 re.match

1
2
3
4
val pattern = "\\d+".r
val text = "2020-04-16"
//output => 2020
println(pattern.findPrefixOf(text).get)

replaceFirstIn vs replaceAllIn vs replaceSomeIn

这几个函数的作用是替换,类似python的re.sub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val pattern = "\\d+".r
val text = "2020-01-01"
// output => xxx-01-01
println(pattern.replaceFirstIn(text, "xxx").get)
// output => xxx-xxx-xxx
println(pattern.replaceAllIn(text, "xxx").get)
// 根据匹配到的对象自定义函数去进行不同的替换
// output => year-xxx-xxx
val replace_f = (m: Match) => {
var r = Option("")
m.group(1) match {
case "2020" => r = Some("year")
case _ => r = Some("xxx")
}
r
}
println(pattern.replaceSomeIn(text, replace_f))

关于scala.util.matching.Regex.Match

细节参见文档。简单用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
val pattern = "(\\d+)".r("year")
val text = "2020-01-01"
val _match = pattern.findFirstMatchIn(text).get
// 匹配到的文本 output => 2020
println(_match.source)
// 匹配的起始位置 output => 0
println(_match.start)
// 匹配的分组 output => 2020
println(_match.group(0))
// 匹配的分组 output => 2020
println(_match.group("year"))

一些不太习惯的地方

#1

我以为会是: 2020

1
2
3
4
5
val text = "Hello, I am Scala Regex. date:2020-01-01"
val pattern = "date:(\\d+)".r

// output => date:2020
println(pattern.findFirstIn(text).get)

所以当你想获取2020时,可以这么写

1
println(pattern.findFirstMatchIn(text).get.group(1))

#2

总感觉这个语法很诡异。。。

1
2
3
4
5
6
val text = "Hello, I am Scala Regex. date:2020-01-01"
val pattern = ".*date:(\\d+).*".r

val pattern(year) = text
// output => 2020
println(year)