首先,我们先快速复习一下 Sui Move 中使用到的基础类型。
Move 包含六种无符号整型:u8
,u16
u32
,u64
,u128
和 u256
。值的范围从 0 到 与类型大小相关的最大值。
这些类型的字面值为数字序列(例如 112)或十六进制文字,例如 0xFF
。 字面值的类型可以选择添加为后缀,例如 112u8
。 如果未指定类型,编译器将尝试从使用文字的上下文中推断类型。 如果无法推断类型,则假定为 u64
。
对无符号整型支持的运算包括:
+
-
*
%
/
&
|
^
>>
<<
>
<
>=
<=
==
!=
as
简单示例:
let a: u64 = 4; |
Move 布尔值包含两种,true
和 false
。支持与 &&
,或||
和非 !
运算。可以用于 Move 的控制流和 assert!
中。 assert!
是 Move 提供的用于断言,当判断的值是 false
时,程序会抛出错误并停止。
if (bool) { ... } |
address 也是 Move 的原生类型,可以在地址下保存模块和资源。Sui 中地址的长度为 20 字节。
在表达式中,地址需要使用前缀 @
,例如:
let a1: address = @0xDEADBEEF; // shorthand for 0x00000000000000000000000000000000DEADBEEF |
Tuples 和 Unit ()
在 Move 中主要用作函数返回值。只支持解构(destructuring)运算。
module ds::tuples { |
接下来,我们从 Vector 开始,介绍 Sui 和 Sui Framework 中支持的集合类型。
vector<T>
是 Move 提供的唯一的原生集合类型。vector<T>
是由一组相同类型的值组成的数组,比如 vector<u64>
, vector<address>
等。
vector
支持的主要操作有:
push_back
pop_back
borrow
,borrow_mut
contains
swap
index_of
module ds::vectors { |
编译并运行示例:
# 编译并发布 |
下面我们介绍几种基于 vector
的数据类型。
Move 没有字符串的原生类型,但它使用 vector<u8>
表示字节数组。目前, vector<u8>
字面量有两种:字节字符串(byte strings)和十六进制字符串(hex strings)。
字节字符串是以 b
为前缀的字符串文字,例如 b"Hello!\n"
。
十六进制字符串是以 x
为前缀的字符串文字,例如 x"48656C6C6F210A"
。每一对字节的范围从 00
到 FF
,表示一个十六进制的 u8
。因此我们可以知道: b"Hello" == x"48656C6C6F"
。
在 vector<u8>
的基础上,Move 提供了 string
包处理 UTF8 字符串的操作。
我们以创建 Name NFT 的为例:
module ds::strings { |
编译后命令行中调用:
$ sui client call \ |
可以在 Transaction Effects 中看到新创建的对象,ID 为 0xf53891c8d200125bcfdba69557b158395bdf9390
,通过 Sui 提供的 RPC-API 接口 sui_getObject
可以看到其中保存的内容:
curl -H 'Content-Type: application/json' https://fullnode.devnet.sui.io:443 -d '{ |
输出结果
{ |
Sui 在 vector
的基础上实现了两种数据结构,映射 vec_map
和集合 vec_set
。
vec_map
是一种映射结构,保证不包含重复的键,但是条目按照插入顺序排列,而不是按键的顺序。所有的操作时间复杂度为 0(N)
,N 为映射的大小。vec_map
只是为了提供方便的操作映射的接口,如果需要保存大型的映射,或者是需要按键的顺序排序的映射都需要另外处理。可以考虑使用之后介绍的 table
数据结构。
主要操作包括:
empty
insert
get
, get_mut
remove
contains
size
into_keys_values
keys
destroy_empty
get_entry_by_idx
,get_entry_by_idx_mut
module ds::v_map { |
vec_set
结构保证其中不包含重复的键。所有的操作时间复杂度为 O(N)
,N 为映射的大小。同样, vec_set
提供了方便的集合操作接口,按插入顺序进行排序,如果需要使用按键进行排序的集合,也需要另外处理。
主要操作包括:
empty
insert
remove
contains
size
into_keys
module ds::v_set { |
还有一种基于 vector
构建的数据结构:优先队列,他使用基于 vector
实现的大顶堆(max heap)来实现。
大顶堆是一种二叉树结构,每个节点的值都大于或等于其左右孩子节点的值,这样,这个二叉树的根节点始终都是所有节点中值最大的节点。
在优先队列中,我们为每一个节点赋予一个权重,我们基于权重构建一个大顶堆,从大顶堆顶部弹出根节点则为权重最大的节点。这样就形成过了一个按优先级弹出的队列。
优先队列主要包含的操作为:
create_entries
,结果作为 new
方法参数new
insert
pop_max
示例:
module ds::pq { |
Move语言中,结构体是包含类型化字段的用户定义数据结构。 结构可以存储任何非引用类型,包括其他结构。示例:
module ds::structs { |
对象是 Sui Move 中新引入的概念,也是 Sui 安全和高并发等众多特性的基础。定义一个对象,需要为结构体添加 key
能力,同时结构体的第一个字段必须是 UID
类型的 id。
对象结构中除了可以使用基础数据结构外,也可以包含另一个对象,即对象可以进行包装,在一个对象中使用另一个对象。
对象有不同的所有权形式,可以存放在一个地址下面,也可以设置成不可变对象或者全局对象。不可变对象永远不能被修改,转移或者删除,因此它不属于任何人,但也可以被任何人访问。比如合约包对象,Coin Metadata 对象。
我们可以通过 transfer
包中的方法对对象进行处理:
transfer
:将对象放到某个地址下freeze_object
:创建不可变对象share_object
:创建共享对象module ds::objects { |
编译后调用:
sui client call \ |
sui client call \ |
sui client call \ |
可以看到,不同所有权类型的对象会在创建时显示不同的类型结果。
sui client call \ |
可以在结果中看到 Mutated Objects
中对象已经发生了变化。
对象虽然可以进行包装,但是也有一些局限,一是对象中的字段是有限的,在结构体定义是已经确定;二是包含其他对象的对象可能非常大,可能会导致交易 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 的名称可以是任何拥有 copy
,drop
和 store
能力的值,这些值包括 Move 中的基本类型(整数,布尔值,字节串),以及拥有 copy
,drop
和 store
能力的结构体。
下面我们通过例子来看看具体的操作:
add
borrow
, borow_mut
module ds::fields { |
编译并调用 initialize
和 add_child
方法:
sui client call \ |
sui client call \ |
可以通过 sui_getDynamicFields
方法查看添加的字段:
curl -H 'Content-Type: application/json' https://fullnode.devnet.sui.io:443 -d '{ |
结果:
{ |
其中 name
为 “child”
。同时,对于对象 ID 0x55536ca8123ffb606398da9f7d2472888ca5bfd1
,我们仍然能从链上追踪对应信息。
curl -H 'Content-Type: application/json' https://fullnode.devnet.sui.io:443 -d '{ |
接下来,我们介绍几种基于 dynamic field 的集合数据类型。
前面介绍过,带有 dynamic field 的对象可以被删除,但是这对于链上集合类型来说这是不希望发生的,因为链上集合类型可能将无限多的键值对作为 dynamic field 保存。因此,在 Sui 提供了两种集合类型: Table
和 Bag
,两者都基于 dynamic field 构建的映射类型的数据结构,但是额外支持计算它们包含的条目数,并防止在非空时意外删除。
Table
和 Bag
的区别在于,Table 是同质(*homogeneous)*映射,所以的键必须是同一个类型,所以的值也必须是同一个类型,而 Bag 是异质(heterogeneous)映射,可以存储任意类型的键值对。
同时,Sui 标准库中还包含对象版本的 Table
和 Bag
: ObjectTable
和 ObjectBag
,区别在于前者可以将任何 store
能力的值保存,但从外部存储查看时,作为值存储的对象将被隐藏,后者只能将对象作为值存储,但可以从外部存储中通过 ID 访问这些对象。
与之前介绍过的 vec_map
相比,table
更适合用来处理包含大量映射的情况。
下面我们通过示例来展示对 table 的基本操作:
add
borrow
,borrow_mut
delete
length
contains
Object table 的操作与 table 类似。
module ds::tables { |
Bag 的操作与 table 的操作接口类似:
add
borrow
,borrow_mut
delete
length
contains
这里我们仅展示添加不同类型的键值对。
Object_bag
的操作与 bag
类似。
module ds::bags { |
linked_table
是另一种使用 dynamic field 实现的数据结构,它与 table
类似,除此之外,它还支持值的有序插入和删除。因此,除了 table 类似的基础操作方法,还包含 front
,back
,push_front
,push_back
,pop_front
,pop_back
等操作,对于每一个键,也可以通过 prev
和 next
获取前一个和后一个插入的键。
module ds::linked_tables { |
最后,我们介绍一种基于 table
的数据结构 table_vec
。从名字就可以看出,table_vec
是使用 table
实现的可扩展 vector
,它使用元素在 vector
的索引作为 table
中的键进行存储。table_vec
提供了与 vector
类似的操作方法。
module ds::table_vecs { |
编译并运行示例:
sui client call \ |
至此,我们介绍完了 Sui Move 中主要的数据类型及其使用方法,希望大家学习和理解 Sui Move 有一定的帮助。
项目代码:
在线 Demo: https://fox-game-interface.vercel.app/
狼羊游戏是以太坊上的 NFT 游戏,玩家通过购买NFT,然后将 NFT 质押来获取游戏代币 $WOOL,游戏代币 $WOOL 可用于之后的 NFT 铸造。有趣的是,狼羊游戏在这个过程中引入了随机性,让单纯的质押过程增加了不确定性,因而吸引了大量玩家参与到游戏中,狼羊游戏的可玩性也是建立在这个基础之上。具体的游戏规则为:
你有90%的概率铸造一只羊,每只羊都有独特的特征。以下是他们可以采取的行动:
进入谷仓(Stake)
每天累积 10,000 羊毛 $WOOL
剪羊毛 $WOOL (Claim)
收到的羊毛80%累积在羊的身上,狼对剪下的羊毛收取20%的税,作为不攻击谷仓的回报。征税的 $WOOL 分配给目前在谷仓中质押的所有狼,数量与他们的 Alpha 分数成正比。
离开谷仓(Unstake)
羊被从谷仓中移除,所有 $WOOL 都被剪掉了。只有当羊积累了2天价值的 $WOOL 时才能离开谷仓,离开谷仓时你所有累积的 $WOOL 有50%的几率被狼全部偷走。被盗 $WOOL 分配给当前在谷仓中质押的所有狼,数量与他们的 Alpha 分数成正比。
使用 $WOOL 铸造一个新羊
铸造的 NFT 有10%的可能性实际上是狼!新的羊或狼有10%的几率被质押的狼偷走。每只狼的成功机会与他们的 Alpha 分数成正比。
你有 10% 的机会铸造一只狼,每只狼都有独特的特征,包括 5~8 的 Alpha 值。Alpha值越高,狼从税收中赚取的 $WOOL 部分越高,偷一只新铸造的羊或狼的概率也越高。只有被质押的狼才能偷羊或赚取 $WOOL 税。
例子:狼A的 Alpha 为8,狼B的 Alpha 为6,并且他们都被质押。
本次项目实践,我们将在 Sui 区块链上通过 Move 智能合约语言来实现游戏铸造,质押和获取 NFT 过程,并使用新的游戏元素:狐狸,鸡和鸡蛋,其中狐狸对应狼,鸡对应羊,鸡蛋对应羊毛,其他过程不变,我们将这个游戏命名为狐狸游戏。
我们首先进行智能合约的编写,大致分为以下几个部分:
首先我们定义狐狸和鸡的 NFT 的结构,我们使用一个结构体 FoxOrChicken
来表示这个 NFT, 通过 is_chicken
来进行区分:
// 文件:token_helper.move |
其中, url
既可以是指向 NFT 图片的链接,也可以是 base64 编码的字符串,比如 data:image/svg+xml;base64,PHN2Zy......
。link
是一个指向 NFT 的页面。
整个创建 NFT 的逻辑大致就是根据随机种子生成对应属性索引,根据属性索引构建对应的属性列表和图片,从而创建 NFT。
创建 NFT 使用到 FoCRegistry
结构体,这个数据结构用于记录关于 NFT 的一些数据,比如 foc_born
记录生产的 NFT 总数,foc_hash
用于在生产 NFT 时产生随机数,该随机数用于生成 NFT 的属性,foc_hash
可以看作是 NFT 的基因。具体的属性值记录如下:
// 文件:token_helper.move |
创建 NFT 方法 create_foc
如下:
// 文件:token_helper.move |
其中 genetate_traits
用于根据 foc_hash
生成 NFT 的属性值,此处属性为对应属性值的索引,select_trait
根据 A.J. Walker’s Alias 算法根据预先设置好的每一个属性的随机概率(rarities
)来快速生成对应的属性索引。详情可以参考文章 https://zhuanlan.zhihu.com/p/436785581 中 A.J. Walker’s Alias 算法一节****。****
// 文件: token_helper.move |
而 get_attributes
则是根据属性索引值对应从 trait_types
和 trait_data
中将属性的真实值取出并构建成属性数组。
fun get_attributes(reg: &mut FoCRegistry, fc: &Traits): vector<Attribute> |
而 img_url
则通过上述生成的特征构建出对应的 base64 编码的 svg 图片。
/// Construct an image URL for the NFT. |
至此,我们可以通过 create_foc
方法创建一个 FoxOrChicken NFT。
接下来我们看到铸造 NFT 过程,大致过程为:
// 文件: fox.move |
质押 NFT 时,我们通过 NFT 的属性值 is_chicken
来将不同的NFT放置到不同的容器中。其中,狐狸放置在 Pack 中,鸡放置在 Barn 中。每一个 NFT 在放置的同时记录对应的 owner 地址和用于计算质押收益的时间戳。
对于 Barn
,除了记录 NFT 对象 ID
与 Stake
之间对应关系的 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 的设计目的在于:
FoxOrChicken
成为 Stake
的一个属性时,在区块链上无法追踪,因此,只能通过 Stake
的 Object ID 进行追踪,items 都是为了保证能直接通过 NFT 的 Object ID 来对应到 Stake;dynamic_field
可以方便查询。// struct to store a stake's token, owner, and earning values |
我们接下来看到如何质押一个 Chicken 的 NFT,方法调用层级为 stake_many_to_barn_and_pack -> stake_chicken_to_barn -> add_chicken_to_barn, record_staked
:
// 文件: Token_helper.move |
同理,质押 Fox 进入 Pack 中的过程也是类似的,这里就不再赘述,方法调用层级为 stake_many_to_barn_and_pack ->
stake_fox_to_pack ->``add_fox_to_pack, record_staked
。
提取 Chicken NFT 时,方法调用层级为 claim_many_from_barn_and_pack -> claim_chicken_from_barn -> remove_chicken_from_barn, remove_staked
主要的过程为:
// 文件: token_helper.move |
同理,从 Pack 中提取 Fox 中的过程也是类似的,这里就不再赘述。
EGG 代币创建过程使用了 one-time-witness 模式,具体可以参考:Move 高阶语法 | 共学课优秀笔记 中的 Witness 模式一节。
代币的铸造能力 treasury_cap: TreasuryCap<EGG>
保存为共享对象,但是 mint
和 burn
方法t通过 friend
关键字限制了只能在 fox
和 barn
模块中调用,因此控制了代币的产生和销毁的权限。
// 文件: egg.move |
fox
模块作为整个包的入口模块,将对所有模块进行初始化,并提供 entry 方法。
我们在 fox 模块中设置了 Global
作为全局参数的结构体,用来保存不同模块需要用到的不同对象,一来方便我们看到系统需要处理的对象信息,二来减少了方法调用时需要传入的参数个数,通过Global对象将不同模块的对象进行分发,可以有效减少代码复杂度。
// 文件: fox.move |
除了之前介绍过的 mint 方法,我们还提供用于质押和提取 NFT 的 entry 方法:
// 文件: fox.move |
目前 Sui 区块链还没有完全实现区块时间,而目前提供的 tx_context::epoch()
的精度为24小时,无法满足游戏需求。因此在游戏中,我们通过手动设置时间戳来模拟时间增加,以确保游戏顺利进行。
// 文件: barn.move |
在初始化时,将设置时间的能力给到了一个预先生成的专门用于设置时间戳的地址 0xa3e46ec682bb5082849c240d2f2d20b0f6e054aa
。
// 文件: config.move |
之后,我们可以设置定时任务进行时间戳更新,通过调用设置时间的命令进行,详细结果可以查看 3.2 节合约命令行调用:
sui client call --function set_timestamp --module fox --package ${fox_game} --args ${time_cap} ${global} \"$(date +%s)\" --gas-budget 30000 |
至此,我们介绍了合约部分的主要功能,详细的代码可以阅读项目仓库。
下面,我们首先将部署合约,并通过命令行进行方法的调用。
通过以下命令可以编译和部署合约:
sui move build |
输出结果为:
$ sui client publish . --gas-budget 300000 |
可以通过交易哈希 5FZi4YxiiBJsCj67JSSzkVZvHdJjKKPtMMMrfGbmPXvH
在 sui explorer 中查看部署的合约信息:
通过 sui client object <object_id>
可以查看创建的 object 的属性,可以知道:
0x17db4feb4652b8b5ce9ebf6dc7d29463b08e234e
为代币 EGG 的 TreasuryCap 的 ObjectId0x1d525318e381f93dd2b2f043d2ed96400b4f16d9
为 EGG 的 CoinMetadata0x59a85fbef4bc17cd73f8ff89d227fdcd6226c885
为部署的地址0xe364474bd00b7544b9393f0a2b0af2dbea143fd3
为 TimeManagerCap0xe4ffefc480e20129ff7893d7fd550b17fda0ab0f
为 Global 对象0xe572b53c8fa93602ae97baca3a94e231c2917af6
为 FoCManagerCap 对象这些对象将在之后的命令行调用和前端项目中使用到。其他省略的创建的对象为 Trait 对象,在之后不会使用到。
设置环境变量
export fox_game=0x59a85fbef4bc17cd73f8ff89d227fdcd6226c885 |
设置时间戳
# 需要切换到时间戳的管理地址 0xa3e46ec682bb5082849c240d2f2d20b0f6e054aa |
之后的每一步操作前都需要同步一次时间戳,保证数据正确。
铸造 NFT
使用以下命令进行铸造:
# 查看当前gas |
其中:- `\"1\"` 表示铸造的数量为 1;- `false` 表示不质押,如果要铸造的同时进行质押,可以修改为 `true`;- `\[0x3cd2bb1e03326e5141203cc008e6d2eb44a0df05\]` 是用于支付 0.0099 SUI 铸造费用的 SUI 对象;- `\[\]` 表示用于支付 `EGG` 的对象。可以看到生成的对象中, `0x84fe8e597bcb9387b2911b5ef39b90bb111e71a2` 在地址 `0x659f89084673bf4a993cdea89a94dabf93a2ddb4` 之下,查看属性可以看到对应的 type 为 `0x59a85fbef4bc17cd73f8ff89d227fdcd6226c885::token_helper::FoxOrChicken` ,这个就是我们铸造得到的 NFT,相应的其他属性也可以查看到,命令输出结果可以查看此 [gist](https://gist.github.com/qiwihui/86e7385c635f88b539ed2f032018ca28)。或者,我们可以通过 `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"
]
}'
质押 NFT
通过以下命令对前一步铸造的 NFT 进行质押:
sui client call --function add_many_to_barn_and_pack --module fox --package ${fox_game} --args ${global} \[0x84fe8e597bcb9387b2911b5ef39b90bb111e71a2\] --gas-budget 100000 |
获取收益和 提取 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 提取出来。
至此,命令行操作完成。
这个项目基于 NonceGeek DAO 的 scaffold-move 开发脚手架,这个脚手架目前包含 Aptos 和 Sui 两个公链的前端开发实例,可以可以在这个基础上快速进行 Sui 的前端部分开发。
通过运行以下步骤可以设置开发环境:
git clone https://github.com/NonceGeek/scaffold-move.git |
项目页面主要包括三部分,位于 src/pages
目录:index,game 和 whitepapers:
我们之后的部分主要聚焦在 game 页面。game 页面功能主要包括三部分:
其中,质押和提取时进行的多选操作,可以通过设置选择变量进行过滤来实现:
const [unstakedSelected, setUnstakedSelected] = useState<Array<string>>([]) |
我们使用 Suiet 钱包开发的 @suiet/wallet-kit
包连接 Sui 钱包,从包对应的 WalletContextState 可以看出, useWallet
包含了我们在构建 App 时会使用到的基本信息和功能,比如钱包信息,链信息,连接状态信息,以及发送交易,签名信息等。
export interface WalletContextState { |
在 src/components/SuiConnect.tsx
中,我们可以很方便的设置钱包连接功能:
import { |
之后,我们将需要使用的信息在 src/pages/game.tsx
中引入:
import { |
其中, signAndExecuteTransaction
方法用来签名并执行交易,支持 moveCall
, transferSui
, transferObject
等交易。
我们使用官方提供的 @mysten/sui.js
库调用 Sui 的 RPC 接口,这个库支持了大部分 Sui JSON-RPC,同时,还提供了一些额外的方法方便开发,例如:
selectCoinsWithBalanceGreaterThanOrEqual
:获取大于等于指定数量的coin对象ID数组selectCoinSetWithCombinedBalanceGreaterThanOrEqual
:获取总和大于等于指定数量的coin对象ID数组这两个方法在需要在 NFT 铸造时支付 SUI 或者其他代币时十分有用。我们在 game.tsx
中引入 JsonProvider 进行初始化:
// 文件: src/pages/game.tsx |
其他方法的介绍可以参考库的文档,这里不多赘述。
我们首先看到如何铸造 NFT:
// 文件: src/pages/game.tsx |
其中 arguments
参数对应 mint 方法所需要的参数。
同理,其他的 entry 方法的调用和签名也与 Mint 方法类似,分别为:
// 铸造并质押 |
对于 Sui 公链,除了调用合约,另一块难点是合约数据的读取。相对于 EVM 合约,Move的合约数据结构更复杂,更难读取。由于在 Sui 中,Object 对象被包装后可能无法进行追踪(详情可以参考官方 Object 教程系列),因此在之前的数据结构设计中,Pack 和 Barn 中存储的 NFT 需要使用能进行追踪的数据结构。因此,ObjectTable 被做为基本的键值存储结构区别于不可追踪的 Table 数据类型。相应地,可以使用 sui_getDynamicFieldObject
来读取其中的数据,例如,通过读取保存在 PackStaked 中的 NFT 对象质押列表,从而通过 getObjectBatch
可以获取当前地址所有的质押的 NFT。
// 读取 Pack 中质押的 Fox NFT |
其中, packStakedObject
对象ID通过 GLOBAL
对象 ID 获取得到。
const globalObject: any = await provider.getObject(GLOBAL) |
对于当前地址所拥有的未质押的NFT,需要通过读取全部对象ID后进行类型过滤才能得到:
// 获取所有对象 |
最后,对于当前地址中包含的 EGG 代币的余额,可以通过 getCoinBalancesOwnedByAddress
获得所有余额对象并进行求和得到。
const balanceObjects = await provider.getCoinBalancesOwnedByAddress(account!.address, `${PACKAGE_ID}::egg::EGG`) |
至此,我们完成了狐狸游戏合约和前端代码的介绍。我们实现的狐狸游戏虽然功能上只有铸造,质押和提取这几个主要的功能,但是涉及 NFT 创建以及 Sui Move 的诸多语法,整体项目具有一定的难度。
这篇文章希望对有兴趣于 Sui 上的 NFT 的操作的同学有所帮助,也希望大家提出宝贵的建议和意见。项目目前只完成了初步的逻辑功能,还需要继续补充测试和形式验证,欢迎有兴趣的同学提交 Pull Request。
基于恒定乘积的自动化做市商(AMM),去中心化交易所。
v1 版本:
v2 版本:
x*y=k
的情况下,做市的价格区间在 (0, +∞) 的分布,当用户交易时,交易的量相比我们的流动性来说是很小的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%,这个问题对于稳定币池来说更加严重。v3版本:
- 多层级手续费率(0.05%,0.3%,1%),升级的预言机,区间订单(range order)等。
例子:
假设 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%。
模型分析:
根据恒定乘积公式 $xy=k$,令 $k=L^2$,其中 L 表示流动性,则有 $xy=L^2$,再根据价格 $S=x/y$,可以得到 $x=L/\sqrt{S}$,$y=L\sqrt{S}$。
考虑 LP 在流动性池 X-Y 中添加流动性 $L$,池的初始价格为 $S_0$,所以 LP 需要向流动性池中提供 $x_0=L/\sqrt{S_0}$的 X 代币和 $y_0=L\sqrt{S_0}$ 的 Y 代币。
当池的价格变为 $S_1$时,LP 的资产价值为
$$V_{v2,pos}(L, S_1)=S_1 \cdot x_1+y_1=\frac{L}{\sqrt{S_1}}S_1+L\sqrt{S_1}=2L\sqrt{S_1}$$
其中 $x_1$和 $y_1$是LP在池中的资产。
LP 初始时的资产如果一直拿手里,则价值为
$$V_{v2,hold}(L,S_0,S_1)=S_1 \cdot x_0 + y_0=\frac{L}{\sqrt{S_0}}S_1+L\sqrt{S_0}$$
所以,无常损失为:
$$\begin{aligned}\mathrm{IL}{\mathrm{v} 2}\left(S_0, S_1\right) &=\frac{V{\mathrm{v} 2, \text { pos }}-V_{\mathrm{v} 2, \text { hold }}}{V_{\mathrm{v} 2, \text { hold }}} \&=\frac{2 L \sqrt{S_1}-\left(\frac{L}{\sqrt{S_0}} S_1+L \sqrt{S_0}\right)}{\frac{L}{\sqrt{S_0}} S_1+L \sqrt{S_0}} \&=\left(\frac{2 \cdot \sqrt{\frac{S_1}{S_0}}}{1+\frac{S_1}{S_0}}-1\right)\end{aligned}$$
令 $r=S_1/S_0$,则有:
$$\mathrm{IL}_{v2} = \frac{2 \cdot \sqrt{r}}{1+r}-1$$
用之前的例子计算,r=1300/1500=0.87时,IL=0.0026=0.26%,r=2200/1500=1.47时,IL=0.018=1.8%,与上述计算相符合。
图像:
https://www.desmos.com/calculator/aza5py3g95
可以看到,当 $S_0=S_1$时无常损失为0,其他时候无常损失都为负数。列一个表:
价格变化 | 无常损失 |
---|---|
0.25x | 20.0% |
0.5x | 5.7% |
0.75x | 1.0% |
1 | 0 |
1.25x | 0.6% |
1.5x | 2.0% |
1.75x | 3.8% |
2x | 5.7% |
3x | 13.4% |
4x | 20.0% |
5x | 25.5% |
用同样的过程,我们分析 Uniswap v3的无常损失。假设 LP 向价格区间 $[P_a,P_b]$提供流动性 $L$,初始价格为 $P_0(\in[P_a,P_b])$,之后价格变为 $P_1(\in[P_a,P_b])$。
首先我们从Uniswap v3 的白皮书中可以知道,集中流动性的资产储备曲线(橙色)的公式为:
$$\left(x+\frac{L}{\sqrt{p_b}}\right)\left(y+L \sqrt{p_a}\right)=L^2$$
(推导:曲线相当于v2的曲线向左向下平移动)
对于虚拟曲线: $x_{virtual} \cdot y_{virtual} = L^2$,可以得到:
$$\begin{aligned}&y=y_{\text {virtual }}-L \sqrt{p_a}=L\left(\sqrt{P}-\sqrt{p_a}\right) \&x=x_{\text {virtual }}-\frac{L}{\sqrt{p_b}}=L\left(\frac{1}{\sqrt{P}}-\frac{1}{\sqrt{p_b}}\right)\end{aligned}$$
初始时资产价值为:
$$\begin{aligned}V_{v3}(P_0) &=y_0+x_0 \cdot P_0 \&=L\left(\sqrt{P_0}-\sqrt{p_a}\right)+L\left(\sqrt{P_0}-\frac{P_0}{\sqrt{p_b}}\right) \&=2 L \sqrt{P_0}-L\left(\sqrt{p_a}+\frac{P_0}{\sqrt{p_b}}\right)\end{aligned}$$
同样,则在价格 $P_1$时流动池中的资产价值为(令 $r=P_1/P_0$):
$$\begin{aligned}V_{v3,pos}(P_1) &=2 L \sqrt{P_1}-L\left(\sqrt{p_a}+\frac{P_1}{\sqrt{p_b}}\right) \ &=2 L \sqrt{rP_0}-L\left(\sqrt{p_a}+\frac{rP_0}{\sqrt{p_b}}\right)\end{aligned}$$
在价格为 $P_1$ 时的,选择 HODL 的资产价值为:
$$\begin{aligned}V_{\text {v3,hold}}(P_1) &=y_0+x_0 P_1 \&=L\left(\sqrt{P_0}-\sqrt{p_a}\right)+P_1 \cdot L\left(\frac{1}{\sqrt{P_0}}-\frac{1}{\sqrt{p_b}}\right) \&=L\left(\sqrt{P_0}-\sqrt{p_a}\right)+L \cdot rP _0\left(\frac{1}{\sqrt{P_0}}-\frac{1}{\sqrt{p_b}}\right) \&=L \sqrt{P_0}(1+r)-L\left(\sqrt{p_a}+\frac{rP_0 }{\sqrt{p_b}}\right)\end{aligned}$$
所以无常损失为(不失一般性,取 $P_0$为 $P$):
$$\begin{aligned}\mathrm{IL}{a, b}® &=\frac{V{pos}-V_{\text {hold }}}{V_{\text {hold }}} \&=\frac{2 L \sqrt{rP}-L \sqrt{P}(1+r)}{L \sqrt{P}(1+r)-L\left(\sqrt{p_a}+\frac{rP}{\sqrt{p_b}}\right)} \&=\frac{2 \sqrt{r}-1-r}{1+r-\sqrt{\frac{p_a}{P}}-r \sqrt{\frac{P}{p_b}}} \&=\operatorname{IL}® \cdot\left(\frac{1}{1-\frac{\sqrt{\frac{p_a}{P}}+r \sqrt{\frac{P}{p_b}}}{1+r}}\right)\end{aligned}$$
( $P_1$ 在价格区间 $[0,P_b]$,$[P_a,+\infty]$时的无常损失也同样可以计算。)
我们可以通过价格区间 $[P_a, P_b]$ 的变化看到:
$$p_a=0, p_b \rightarrow \infty, \mathrm{IL}{v3}=\frac{2 \cdot \sqrt{r} -1-r}{1+r}=\mathrm{IL}{v2}$$
趋近于 $\mathrm{IL}_{v2}$。
画图
https://www.desmos.com/calculator/ha322rtufc
同样我们可以看到:当价格区间越小时,无常损失越大:
(这是一个动图)
数值比较
我们比较在不同的价格区间下 Uniswap v3的无常损失:
具体数据():
价格区间% | -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?
我们的计算忽略了两个问题:
例如:ETH-USDC-0.3%池的流动性
一些流行的 token 对的深度比中心化交易所(Binance, Coinbase)更高。link
large-cap: ETH/dollar
mid-cap - cross-chain pairs
- 稳定币与稳定币对: USDC/USDT
比较以下五种资产持有策略
比较这五种策略的资产价值。(使用 https://defi-lab.xyz/uniswapv3simulator)
无手续费时:
包含手续费时:
Uniswap V3 既是投资者收益的放大器,也是风险的放大器。在享受更高投资收益的同时,也必然要承担当价格脱离安全范围时更多的无常损失。
在不主动调整情况下,全范围(full range)的 Uniswap v3 头寸和价格限定的稳定币头寸的手续费回报平均比 Uniswap v2 好约 54%。其中
通常建议 LPers 选择 v3。link
选择哪个池?
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,它有两个主要参数:
该策略始终保持两个有效的范围订单:
每24小时,进行再平衡,根据价格和token数量提交订单。如果策略表现优秀,则时间区间可以被减少。再平衡并不能保证完全50/50。
举例:
比如,ETH目前价格 150USDC,B=50,R=20,策略拥有资金 1ETH 和160USDC。则在 [100, 200] 放置一个基础订单,使用 1ETH 和 150 USDC。剩余的 10 USDC 用来在 [130,150] 放置一个在平衡订单,用来购买ETH以达到50/50。
如果价格提升到 180, 再平衡之后,基础订单为 [130, 230],若此时策略有 1.2 ETH 和 90USDC,则策略会使用 0.5EHT 和 90USDC 放入基础订单中,剩余 0.7ETH 会用于在 [180, 200] 之间的再平衡订单。
实际操作:
https://dune.com/queries/78325/155734?Number of days=200
效果
蓝色曲线
实际效果:
https://dune.com/mxwtnb/Alpha-Vaults-Performance?Number+of+days=200&Number+of+days_t4072e=500
从历史数据中预测未来10分钟的价格走势,得到一个价格范围区间,在这个价格范围区间中提供流动性。直到当前价格超出价格范围,重复上述过程,重新预测价格范围并添加流动性。这个价格范围称为“预期价格范围”。同时我们可以在当前价格没有完全超出预期价格范围时调整价格区间,称这个价格范围为“移动策略范围(move strategy ranges)”,这个范围指示了什么时候需要移动。
如何设置
2018年3月~2020年4月的十分钟数据得出价格移动分布在 [-3%, 3%] 之间。可以设置百分比作为价格波动区间。
进一步策略:在预期价格范围内不采用一致的流动性,而是采用多个连续的流动性多头,每个多头存入不同数量的资产。
三种策略:
比例策略:
蓝线为概率分布,使用小的价格区间实现
结论:
比例策略对于风险偏向 LP 提供者是最优的( $\alpha$大 ),而均匀分配对于风险规避LP提供者是最优的( $\alpha$ 小)。
这意味着,在 Uniswap v3 中被动管理的头寸可能不足以以资本效率和平衡风险赚取费用,积极的流动性提供策略既是机遇也是挑战。
其他主动策略 dapp
用户将资金存入Tornado Cash,然后将资金提取到另一个地址中,在区块链上记录上,这两个地址之间的联系就大概率断开了。那 Tornado Cash 是如何做到的呢?
首先我们看一下存款过程。用户在存款时需要生产两个随机数 secret 和 nullifier,并计算这两个数的一个哈希 commitment = hash(secret, nullifier),然后用户将需要混币的金额(比如 1 ETH)和 commitment 发送给 TC 合约的 deposit 函数,TC合约将保存这两个数据,commitment之后会用于提取存入的资金。
同时,用户会得到一个凭证,通过这个凭证,用户(或者任何人)就可以提取存入的资金。
如果不同的用户会存入不同的金额,比如 Alice 和 Bob 存入 1 ETH,Chris 存入 73 ETH,当取出存款时,某个地址提取了 73 ETH,我们会有很大程度怀疑这个地址属于 Chris。因此,在TC 合约中规定了每次存入的金额为 1 ETH,这样就不会有地址与其他地址不一致。
实际上,TC 有不同金额的 ETH 存款池,分别为 0.1,1,10,100,以满足不同数量的存取款需求。
当进行取款时,一种错误方法是将之前随机生成的 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 哈希已经使用过了,这样就避免了二次提款。
使用 Merkle 树。Merkle树具体参见之前的介绍文章。
TC 会首先初始化一组叶子节点为 keccak256("tornado")
,并以这些叶子节点构建一颗 Merkle 树。当用户存款时,对应的 commitment 存入 Merkle 树的第一个叶子节点,然后合约更新整棵 Merkle 树,然后是第二个用户的commitment 存入第二个叶子节点并更新整棵 Merkle 树,依次类推。
如何证明 commitment 在这棵 Merkle 树中呢?
假设需要证明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 的值。
因此我们将证明 proof 和 Merkle 树根 root 发送给合约,一旦合约验证成功,我们就可以取出之前存入的存款。
TC 合约包含三个部分:
zk-SNARK 验证器合约由 circom 编写的验证电路通过 snarkjs 库生成。
白皮书:https://github.com/MystenLabs/sui/blob/main/doc/paper/sui.pdf
水平可扩展性,高吞吐,低延迟。
技术特征:
代币经济白皮书:The Sui Smart Contracts Platform: Economics and Incentives
代币:SUI
总供应量: 100亿,分配给创始团队、投资者、公售、Sui 基金会和未来的释放。
代币作用:
角色:
五个核心组件:
为用户提供可预测的低的交易费用、激励验证者优化其交易处理操作以及防止拒绝服务攻击。
Gas 费用包含两个部分:计算执行费用和存储费用,并为两部分费用分别计价。
计算价格定价机制:
存储价格:
网络堵塞期间 Gas 费如何保持在低位:因为网络的吞吐量与更多的参与者成线性关系,验证节点可以根据网络需求的增加按比例增加更多的参与者,以此使 Gas 价格接近参考价格。
存储基金:
SUI 持有者可以将自己的 SUI 委托给给验证者进行质押,在每个纪元结束时可获取对应份额的奖励。
验证者在总质押奖励中的份额是与质押数量相关的,因为它决定了每个验证者在处理交易中的投票权份额。每笔Sui的交易只需要2/3的验证者按权益份额进行处理,因此拥有质押数量越多的验证者将拥有更多的份额,从而处理更多的交易,获取到更多的奖励。同时在计算总奖励时,Sui 也会对存储基金进行分配,因此验证者就会相对于 SUI 委托人获得更多的质押奖励。
同时在每个纪元开始前,SUI 持有者可自由地选择验证者进行 SUI 的质押,因此对于处理速度快的验证者将处理更多的交易,获取到更多的执行 gas 奖励,持有者也更愿意选择这种验证者进行质押,从而提升了整个 Sui 网络验证者的质量。
这是一个修订后的提案,它建立在最初的 Across 代币发布提案的基础上,增加了社区反馈和实施细节。
Across 代币的推出将发展和团结 Across 社区,激励流动性提供者,提高 Across 的知名度,并进一步实现成为最快和最便宜的 L2 桥的使命。 该提案概述了代币发布计划,大致可分为两部分:
将铸造 1,000,000,000 ($ACX) Across 代币 。 700,000,000 $ACX 将保留在 Across DAO 国库中,一部分将保留用于激励奖励。 300,000,000 $ACX 将作为初始供应,并按以下方式分配:
$ACX 空投 ── 总共 100,000,000 $ACX 将奖励给以下团体:
$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)借款时用作抵押,以及用于奖励以促进中继者网络的去中心化。
700,000,000 $ACX 储备的很大一部分将通过此激励计划发放,社区成员可以通过执行以下任何操作来赚取 $ACX:
流动性提供者(LP) ── 奖励锁定是传统流动性挖矿的增强版本,它阻止耕种(farm)和抛售活动,同时奖励协议的忠实贡献者。流动性提供者 (LP) 有一个他们获得奖励的个性化利率。 LP 保持未提取(和未售出)累积奖励的时间越长,LP 获得额外奖励的速度就越快。
每个受激励的流动性池将有一个基本的发放率,每个 LP 将有一个针对每个池的独特收益增值率(multiplier)。 LP 将按比例获得基准发放量乘以 LP 的独特收益增值率的份额。 LP 的收益增值率从第 0 天的 1 开始,当奖励未提取 100 天时可以线性增长到最大值 3。下表说明了这个简单的过程。例如,持有 60 天未领取的奖励的 LP 的收益增值率为 2.2。一旦 LP 领取任何奖励,收益增值率立即重置为 1,LP 将需要重赚取该乘数。
持有天数 | 收益增值率 |
---|---|
0 | 1.0 |
25 | 1.5 |
50 | 2.0 |
75 | 2.5 |
100 | 3.0 |
最初的奖励锁定计划预计将运行 6 个月,届时将及时审查是否有任何更改。 该计划将从以下基本发放率开始:
这相当于大约 4MM 到 10MM $ACX,具体取决于 LP 的行为。 $ACX 持有者可以随时提议并投票添加新资产或更改这些参数。
Across 推荐计划 ── 推荐计划将 Across 社区转变为销售队伍。要参与推荐计划,Across 支持者可以输入他们的钱包地址以生成唯一的推荐链接。单击该链接并在 Across 上完成桥接转移的用户会将 $ACX 奖励分配给推荐人。鼓励支持者与朋友分享他们的链接,并在 Twitter 等社交媒体上宣传 Across。这也可以用于与其他项目的集成。跨链聚合器或 DEX 可以创建推荐链接以将 Across 连接到他们的 dApp。单击该链接并完跨链转移后,奖励将分配给该项目。该钱包所有的未来转账将继续向推荐人发放奖励,除非钱包用户点击不同的推荐链接或推荐人领取了他们的奖励。
与 LP 的奖励锁定类似,推荐人可以通过保持奖励未认领并达到特定数量的推荐或确保一定数量的数量来提高他们赚取推荐费的比率。推荐费是在 $ACX 中授予推荐人的跨链 LP 费用的百分比。如果没有领取奖励并且完成了一定数量的推荐或交易量,那么推荐费就会上涨。推荐人分为五层:
推荐奖励每周分配一次,推荐人每周只能增加一个等级。一旦推荐人领取奖励,推荐人的等级立即重置回铜,并且所有推荐链接都失效。这意味着推荐人需要让用户再次点击他们的推荐链接才能继续赚取推荐费,并且推荐人需要重新获得他们的等级,这需要至少 5 周的时间才能达到白金级别。
奖励锁定 = DeFi 的游戏化
奖励锁定的好处是显而易见的。保持奖励锁定不鼓励耕种和抛售活动,但更重要的是,它使 LP 和推荐人与协议更加紧密。如果您被鼓励参与该协议,您自然会想了解更多有关它的信息,并且您会被激励加入社区并进一步履行其使命。
鉴于您可以为每个流动性池赚取的各种独特收益增值率,以及您作为推荐人可以获得的不同层级,为协议做出贡献的每个钱包都将发展出一个个性化的身份。类似于角色扮演游戏中的角色,各种统计数据可以转化为经验值,使钱包可以升级并获得协议中的状态。
奖励锁定可以通过精心涉及的用户界面和用户体验进一步游戏化,使其看起来像一个真正的游戏。它可以像 RPG 一样构建,用户可以在达到某些里程碑时获得特殊的 NFT 或物品。社区成员可以构建这个和/或使用这些统计数据的实际游戏并相互进行战斗。同样,排行榜可以识别所有忠诚的 Across 用户的成就。这一切都会使用户非常不愿意领取他们的奖励并降低身份。
质押 $ACX ── 随着协议的成熟,社区可以考虑为 $ACX 设置质押机制,该机制可以授予进一步的治理权,并分享整个协议的收入。治理可以决定激励奖励的方向,以确定哪些代币和哪些 L2 应该获得更多流动性。这种类似投票锁定的机制可以为 $ACX 和 Across 协议增加更多价值。
除了建立社区和激励项目目标外,Across 代币的发布旨在为拥有 $ACX 创造价值和意义。 目标是让 $ACX 代币持有者在启动后立即通过他们的代币与协议进行交互。 事实上,通过概述在空投之前将获得奖励的行为,这个协议现在正在鼓励整个 LP 活动。 Across 奖励锁定激励计划将吸引社区成员并使用 $ACX 作为货币来游戏化和激励对协议的贡献。 $ACX 代币将代表 Across 协议在经济和治理方面的真正所有权。
非常欢迎对此提案提出反馈意见。 可以而且应该讨论机制和数字,以便社区在此代币发布之前感到舒适。
以太坊跨链协议 Across 是一种新颖的跨链方法,它结合了乐观预言机(Optimistic Oracle)、绑定中继者和单边流动性池,可以提供从 Rollup 链到以太坊主网的去中心化即时交易。目前,Across 协议通过集成以太坊二层扩容方案Optimism、Arbitrum和Boba Network支持双向桥接,即可将资产从L1发送至L2,亦可从L2发送至L1。
来源于:https://docs.across.to/bridge/how-does-across-work-1/architecture-process-walkthrough
Across 协议中,存款跨链有几种可能的流程,最重要的是,存款人在任何这些情况下都不会损失资金。在每一种情况下,在 L2 上存入的任何代币都会通过 Optimism 或 Arbitrum 的原生桥转移到 L1 上的流动池,用以偿还给流动性提供者。
从上面的流程中,我们可以看到 Across 协议流程包括以下几种:
Across 协议中主要包括几类角色:
Across 的合约源码地址为 https://github.com/across-protocol/contracts-v1,目前 Across Protocol 正在进行 v2 版本合约的开发,我们这一篇文章主要分析 v1 版本的合约源码。首先我们下载源码:
git clone https://github.com/across-protocol/contracts-v1 |
合约源码的主要的目录结构为:
contract-v1 |
在这篇解析中,我们主要关注 contracts
和 deploy
目录下的文件。
合约目录 contracts
的目录结构为:
contracts/ |
其中,各个目录包含的内容为:
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 层资金池。这个合约是管理员合约,部署在L1层,并有权限管理 L1 层上的流动性池和 L2 上的存款箱(DepositBoxes)。可以注意的是,这个合约的管理帐号是一个多钱钱包,避免了一些安全问题。
首先我们看到合约中的几个状态变量:
contract BridgeAdmin is BridgeAdminInterface, Ownable, Lockable { |
其中:
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( |
不同L2的消息方法分别在对应链的 CrossDomainEnabled.sol
合约中,比如:
contracts/insured-bridge/avm/Arbitrum_CrossDomainEnabled.sol
;contracts/insured-bridge/ovm/OVM_CrossDomainEnabled.sol
;接下来我们看到 BridgeDepositBox.sol
,抽象合约 BridgeDepositBox
合约中主要有两个功能。
bridgeTokens
第一个是 bridgeTokens
方法,用于将 L2 层代币通过原生代币桥转移到 L1 上,这个方法需要在不同的 L2 层合约上实现,目前支持的 L2 层包括 Arbitrum,Optimism 和 Boba,分别对应的文件为:
contracts/insured-bridge/avm/AVM_BridgeDepositBox.sol
contracts/insured-bridge/ovm/OVM_BridgeDepositBox.sol
contracts/insured-bridge/ovm/OVM_OETH_BridgeDepositBox.sol
以 Arbitrum 链上的 bridgeToken
为例:
// BridgeDepositBox.sol 文件中 |
bridgeTokens
上有一个装饰器 canBridge
包含两个判断, isWhitelistToken
用于判断对应 L2 层代币是否已经在 L1 层上添加了桥接池, _hasEnoughTimeElapsedToBridge
用来减少频繁跨连导致的费用消耗问题,因此设置了最小的跨链接时间。
bridgeTokens
主要就是调用了 L2 层原生的跨链方法,比如 outboundTransfer
。
deposit
第二个是 deposit
方法用于将 L2 层资产转移到以太坊 L1 层上,对应与前端页面 Deposit 操作。对应代码为:
function bridgeTokens(address l2Token, uint32 l2Gas) public virtual; |
其中,合约区分了 ETH 和 ERC20 代币的存入方式。
存入资产后,合约产生了一个事件 FundsDeposited
,用于中继者程序捕获并进行资产跨链,事件信息包含合约部署的 L2 链ID,存款ID numberOfDeposits
,L1层接收者,存款者,L1和L2层代币地址,数量和费率,以及时间戳。
BridgePool
合约部署在 Layer 1 上,提供了给中继者完成 Layer2 上存款订单的函数。主要包含以下功能:
addLiquidity
, removeLiquidity
;relayDeposit
relayAndSpeedUp
, speedUpRelay
disputeRelay
settleRelay
在合约初始时,合约设置了对应的桥管理员地址,L1代币地址,每秒的 LP 费率,以及标识是否为 WETH 池。同时,通过 syncUmaEcosystemParams
和 syncWithBridgeAdminParams
两个方法同步了 Optimistic Oracle 地址信息,Store 的地址信息,以及对应的 ProposerBondPct
, OptimisticOracleLiveness
等参数。
function syncUmaEcosystemParams() public nonReentrant() { |
我们首先看到添加和删除流动性,添加流动性即流动性提供者向连接池中提供 L1 代币,并获取相应数量的 LP 代币作为证明,LP 代币数量根据现行汇率计算。
function addLiquidity(uint256 l1TokenAmount) public payable nonReentrant() { |
由于合约支持 WETH 作为流动性池,因此添加流动性区分了 WETH 和其他 ERC20 代币的添加方法。
此处的难点在于 LP 代币和 L1 代币之间的汇率换算 _exchangeRateCurrent
的实现,我们从合约中提取出了 _exchangeRateCurrent
所使用的函数,包括 _updateAccumulatedLpFees
和 _sync
:
|
换算汇率等于当前合约中代币的储备与总 LP 供应量的比值,计算步骤如下:
_updateAccumulatedLpFees
possibleUnpaidFees
,等于未分配的 Lp 费用 undistributedLpFees
* 每秒 LP 费率 *(当前时间-上次更新时间),目前 WETH 桥接池中每秒LP费率为 0.0000015。unallocatedAccumulatedFees
,如果 possibleUnpaidFees
小于未分配的 Lp 费用,则所有未分配的 LP 费用都将用于累积费用;liquidReserves
,则被使用的储备 utilizedReserves
= 原先被使用的储备 -(当前合约中的代币储备 - 流动储备);利用换算汇率,可以计算得到添加 l1TokenAmount
数量的代币时所能得到的 LP 代币的数量。
对于移除流动性,过程与添加流动性相反,这里不再赘述。
function removeLiquidity(uint256 lpTokenAmount, bool sendEth) public nonReentrant() { |
慢速中继,以及之后要讨论的即时中继,都会用到 DepositData
和 RelayData
这两个数据,前者表示存框交易的数据,后者表示中继交易的信息。
// 来自 L2 存款交易的数据。 |
下面我们看到 relayDeposit
方法,这个方法由中继者调用,执行从 L2 到 L1 的慢速中继。对于每一个存款而言,只能有一个待处理的中继,这个待处理的中继不包括有争议的中继。
function relayDeposit(DepositData memory depositData, uint64 realizedLpFeePct) |
可以看到,存款哈希与 depositData
有关,中继哈希与 depositData
和 relayData
都有关。最后我们可以看到, relayDeposit
还未实际付款给用户的 L1 地址,需要等待中继者处理,或者通过加速处理中继。
speedUpRelay
方法立即将存款金额减去费用后转发给 l1Recipient
,即时中继者在待处理的中继挑战期后获得奖励。
// 我们假设调用者已经执行了链外检查,以确保他们尝试中继的存款数据是有效的。 |
relayAndSpeedUp
执行即时中继。这个方法的函数内容与 relayDeposit
和 speedUpRelay
方法是一致的,这里就不具体注释了,可以参考前文中的注释。这个函数的代码几乎是直接将 relayDeposit
和 speedUpRelay
的代码进行了合并,代码冗余。
// 由 Relayer 调用以执行从 L2 到 L1 的慢 + 快中继,完成相应的存款订单。 |
当对待处理的中继提出争议时,争议者需要想 Optimistic Oracle 提交提案,并等待争议解决。
// 由 Disputer 调用以对待处理的中继提出争议。 |
其中, _requestProposeDispute
的函数内容如下:
// 向 optimistic oracle 提议与 `customAncillaryData` 相关的中继事件的新价格为真。 |
最后,我们来看看 settleRelay
。
// 如果待处理中继价格请求在 OptimisticOracle 上有可用的价格,则奖励中继者,并将中继标记为完成。 |
至此,我们分析完了 Across 合约的主要功能的代码。
部署合约目录 deploy
下包含 8 脚本,依次部署了管理合约,WETH 桥接池,Optimism,Arbitrum和Boba的信使,以及 Arbitrum,Optimism 和 Boba 的存款合约。由于过程比较简单,这里就不仔细分析了。
deploy/ |
Across 协议整体结构简单,流程清晰,支持了 Across 协议安全,快速的从 L2 向 L1 的资金转移。
代码中调用了 Optimistic Oracle 的接口来出和解决争议,对应的逻辑有空之后详说。
在第8课和第3节中,我们讨论了 solidity 重入缺陷。在这个问题中,我们将看一个有趣的现实世界的例子。考虑下面16384个NFT中使用的 solidity 代码片段。通过调用此NFT合约上的 mintNFT()
函数,用户一次最多可以铸造20个NFT。您可以假设所有内部变量都由构造函数正确初始化(未显示)。
function mintNFT(uint256 numberOfNfts) public payable { |
让我们证明 _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 的数量为 $14+13+\dots+2+1=105$。
B) 假设现在总供给的价值是16370,请写出实施对(a)部分进行攻击的恶意Solidity合约代码。
答:
contract Attacker is IERC721Receiver { |
其中 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 { |
这样,当合约被重入攻击时,由于 _totalSupply
还没有增加,因此在第二次进入 mintNFT
函数时 mintIndex
的值是第一次 mint 的值,会导致触发 'ERC721: token already minted'
这个错误,有效保证合约安全。
for (uint256 i = 0; i < numberOfNfts; i++) { |
验证交易: https://rinkeby.etherscan.io/tx/0xa5f70a226c5fd64132eee800f8902ddb9b4ff562ff7f37820d11746fbde52acb
感谢 discord yyczz#5837 对于这个问题的指导。
你作为Uniswap V2的流动性提供者,为DAI/ETH池贡献5个ETH及5000个DAI。假设1个DAI值1美元,那么你的出资总额为1万美元。
A) 几个月后,1个ETH的价格上升到2000 DAI。在DAI/ETH池适应这个新的汇率稳定下来以后,您决定撤回作为流动性提供者的全部份额。假设系统不收费(∅= 1),你会收到多少ETH和DAI ?
答: 假设初始时流动性池中 ETH 和 DAI 的数量为$x$,$y$,提供的5个 ETH 和5000个 DAI 流动性占比为 $w$,则此时边际价格(marginal price)为$M_p=\frac{y}{x}=\frac{5000}{5}=1000$;
设价格变化之后流动性池中 ETH 和 DAI 的数量为 $x’$,$y’$,则有 $M_p’=\frac{y’}{x’}=2000=2M_p$;
根据恒定乘积公式 $xy=x’y’=k$,以及 $M_p=\frac{y}{x}$,$M_p’=\frac{y’}{x’}$,可以推出:
$x’=\sqrt{\frac{k}{M_p’}}=\sqrt{\frac{k}{2M_p}}=\frac{1}{\sqrt{2}}x$,$y’=\sqrt{2kM_p’}=\sqrt{2}y$,
由于流动性占比不变,所以取回的 ETH 为 $wx’=\frac{1}{\sqrt{2}}wx=5\frac{1}{\sqrt{2}}=3.5355$,DAI 为 $wy’=\sqrt{2}wy=5000\sqrt{2}=7071.0678$
故可以收到 3.5355 ETH 和 7071.0678 DAI
B) 如果你自己持有你的5 ETH和5000 DAI,你的资产现在将价值15K DAI,获取了5000 DAI的利润。在这几个月里,作为Uniswap V2的流动性提供者,与“自己持有”策略相比,你的损失是多少? 将损失以美元的绝对值表示,假设1 DAI = 1 USD。这被称为暂时性损失,尽管在这种情况下,这种损失是相当永久性的。
答: 按目前的价格,收回的 ETH 和 DAI 的价值为 $3.5355*2000+7071.0678=14142.0678$,损失为 $15000-14142.0678=857.9322$,损失率为 $\frac{857.9322}{15000}=0.057=5.7%$,故损失 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交易之后被立即执行,具体步骤为:
由于以太坊中矿工在打包交易时是根据交易的gas价格高低进行的,这样将会使得 Alice 广播的交易有机会处于 Bob 交易的后的第一个交易,从而达到获利机会。
E) 假设10个不同的套利者,为捕获Bob的交易创造的套利机会, 在同一时间执行了相同的尾随操作策略。他们都使用了你在(D)部分中所描述的相同机制,那么这10个中的哪一个会获胜呢?
答: 由于以太坊中矿工在打包交易时是根据交易的 gas 价格高低进行的,因此对于所有 gas 价格和Bob的交易的 Gas 价格一致的交易,都有能被排序在Bob交易之后,所以这些交易中处在 Bob 交易之后的第一个交易将获利,对应的套利者获胜。
这个挑战的目的是创建一个自己的ERC20代币,并编写承销商合约,实现用户对代币的购买和卖出。下面,我们一步步完成这个过程。
首先,我们下载项目,并初始化环境。
git clone https://github.com/scaffold-eth/scaffold-eth-typescript-challenges.git challenge-2-token-vendor |
安装好依赖包之后,我们可以看到项目的主要目录为 packages
,包含一下子目录
packages/ |
其中:
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
即可。
现在我们进入合约编写部分。我们的目标是编写一个 ERC20 代币合约,并为创建者铸造 1000 个代币。
代币可以在以太坊中表示任何东西,比如信誉积分,黄金等,而 ERC-20 提供了一个同质化代币的标准,每个代币与另一个代币(在类型和价值上)完全相同。
ERC20是各个代币的标准接口,包含以下方法:
// 名称 |
其中,合约必需设置 totalSupply
、 balanceOf
、 transfer
、 transferFrom
、 approve
以及 allowance
这六个函数,其他如 name
、 symbol
和 decimalsze
则是可选实现。
如果从上述的合约标准开始,我们需要实现这六个函数的方法,幸运的是,OpenZeppelin 库是一个成熟的合约开发库,为我们实现了 ERC20 代币基本功能,我们可以基于这个库开发我们的 ERC20 代币,这将大大减少我们的工作量。我们可以在 ERC20 标准 页面查到相关的使用方法。
除了 ERC20,OpenZeppelin 库还提供了其他合约标准的实现,比如 ERC721,ERC777等,以及大量的经过安全审计的库,这些对于我们快速开发和实现安全的合约代码提供了支持。
我们使用 ERC20.sol 来实现我们的合约,创见一个名为 GOLD
的代币,代币符号为 GLD
,并为创建者铸造 1000 个代币:
pragma solidity >=0.8.0 <0.9.0; |
其中, _mint
方法是 ERC20 提供的方法,该方法创建相应数量的代币,并将代币发送给账户:
/** @dev Creates `amount` tokens and assigns them to `account`, increasing |
接着我们使用脚本进行部署,并向地址发送 1000 代币,地址可以在 http://localhost:3000
中连接我们的 Metamask 得到。部署脚本地址:packages/hardhat-ts/deploy/00_deploy_your_token.ts
。
... |
然后我们运行 yarn deploy --reset
部署合约。
使用 Debug 页面功能进行检查,查看用户账户中的代币余额,可以看到账户中有 1000 个代币;
使用 transfer()
将代币转给另一个账户;
在 Debug 中,使用 transfer
功能,输入目标钱包地址 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33
,以及发送的数量 1000000000000000000000
(1000*1E18,1后边有21个0),点击发送。等交易完成之后,可以分别查看原来账户和目标账户的代币数量,可以看到原来的变成了 0,目标账户是 1000。
注意:
00_deploy_your_token.ts
中的 transfer 代码注释了,不然会影响之后的步骤。接下来,我们创建一个承销商合约,这个合约允许用户通过以太购买代币。
为了完成这个功能,我们需要:
tokensPerEth=100
,也就是 1个以太可以兑换 100 GLD;buyTokens
函数,这个函数必须是 payable
,可以接受发送的以太,计算对应的 GLD
数量,然后使用 transfer
将相应的 GLD
代币发送给购买者 msg.sender
;BuyTokens
事件,记录购买者,使用的 ETH 数量以及购买的 GLD 数量;withdraw
,用来将合约中的 ETH 全部提取到合约的所有者(owner)地址。我们可以使用两种方式设置合约的所有者:在这个教程中,我们使用第二个方式,这样我们可以不用将我们控制的地址的私钥添加到项目配置中,降低暴露。
pragma solidity >=0.8.0 <0.9.0; |
其中, Ownable
可以进行权限控制,合约提供的onlyOwner
修改器可以用来限制某些特定合约函数的访问权限。在这里,我们的 withdraw
函数必需限制合约的所有这才能提取所有的资金。同时,这个合约提供了 transferOwnership
函数,可以用来转移合约的所有者,这个将在我们的脚本部分中使用。
对于部署脚本,我们需要完成以下功能:
vendor.address
,而不是我们之前的地址;ownership
转移到我们能控制的地址,比如我们在前端使用的地址。脚本位置: packages/hardhat-ts/deploy/01_deploy_vendor.ts
// You might need the previously deployed yourToken: |
完成上述代码之后,我们重新部署我们的合约:
yarn deploy --reset |
对应的输出结果为:
$ yarn deploy --reset |
可以从命令行输出中看到合约部署的地址为:
0x9A676e781A523b5d0C0e43731313A708CB607508
0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82
我们通过以下步骤进行验证:
通过 Debug 页面查看承销商 (Vendor)合约地址初始时是否有 1000 个代币;
使用 0.1 ETH 购买 10 个 GLD:我们使用 Buy Tokens 功能购买 10 个代币,可以看到此时的价格约为 0.1 ETH(ETH 价格为 2766.7 美元)。
将购买的代币发送给另一个账户:同样使用页面 Transfer Tokens 功能完成;
使用所有者账户,查看是否能全部取出合约中的 ETH:在 Debug 页面,我们使用 withdraw
功能,尝试将承销商合约中的 ETH 全部取出,可以看到,当交易完成以后,合约的余额变为了0:
![vendorBalanceBefore](https://user-images.githubusercontent.com/3297411/155913554-5f0e78f5-0836-4d53-b0b8-adb44a37413c.png)变为:![vendorBalanceAfter](https://user-images.githubusercontent.com/3297411/155913548-289343ef-d51a-4fbb-8134-071918589636.png)
接下来我们添加承销商合约的回购代币功能,也就是允许用户通过发送代币给承销商合约,承销商合约将对应的ETH发给用户账户。但是在以太坊中,合约只能通过 payable 接受 ETH,无法接受直接发送代币,如果直接向合约发送代币,代币将会永久消失。所以在 ERC20 标准中,我们需要使用 approve
和 tranferFrom
者两个函数来完成这个过程。
approve(address spender, uint256 amount) -> bool |
首先,用户通过调用 approve
函数授权承销商合约( spender
)处理 amount
数量的代币,然后,调用 transferFrom
函数将代币从用户账户( from
)转移 amount
数量的代币给承销商合约( to
)。这其中的难点在于 approve
和 transferFrom
函数。我们来看一下这两个函数在 OpenZeppelin 中具体实现,首先是 approve
:
mapping(address => mapping(address => uint256)) private _allowances; |
从上面可以看出, approve
函数调用了 _approve
, _approve
中用 _allowances
这个哈希记录了 owner
和 spender
之间的授权数量 amount
。因此可以推断, transferFrom
函数以及其他需要授权情况的函数都使用了 _allowances
这个变量,比如 allowance
函数。
/** |
在 transferFrom
函数中,先使用 _spendAllowance
进行授权数量检查并更新授权数量,然后再使用 _transfer
进行代币划转,而 _spendAllowance
中正是调用了 allowance
这个函数。
合约的函数实现如下:
... |
我们再次部署新的合约:
$ yarn deploy --reset |
此时,合约地址变为:
0x3Aa5ebB10DC797CAC828524e59A333d0A371443c
0x68B1D87F95878fE05B998F19b66F4baba5De1aed
验证过程需要包含两步:
先在 Debug 页面使用代币的 approve
允许承销商合约处理 10 个代币:
在 编辑权限
中,我们可以查看到授权的代币数量:
使用承销商的 sellTokens
将 10 个代币换成 ETH。如果上一步没有使用 approve
的话,程序会报错。
到这一步,我们就完成了合约的编写。
我们将部署合约到测试网络中,使用的测试网络是 rinkeby
:
rinkeby
:packages/hardhat-ts/hardhat.config.ts
的 defaultNetwork
变量,packages/vite-app-ts/src/config/providersConfig.ts
中的 targetNetworkInfo
变量yarn account
,如果没有找到可用账户,则使用 yarn generate
生成;yarn deploy
进行合约部署:$ yarn deploy |
可以看到,合约部署成功,此时我们可以在线上测试网络查看到具体的合约部署情况:
并且部署完成了初始化代币分发和所有权转换。详情可以查看部署账户信息: https://rinkeby.etherscan.io/address/0xccb20d43f62f31dd94436f04a1e90d7d08569e57。
接下来,我们将发布我们的前端项目到 Surge (或者使用 s3, ipfs 上)。Surge.sh 提供了免费的网站的部署,对于我们的测试网站来时再合适不过。
yarn build
yarn surge
$ yarn surge |
Surge 在运行命令的过程中就设置了账户名称,以及可以自定义域名:qiwihui-scaffold-2.surge.sh,当完成部署之后,我们就可以在浏览器中访问这个页面,和我们本地运行的结果是一致的。
当我们向测试网络部署合约时,部署的是合约编译之后的字节码,合约源码不会发布。实际生产中,有时我们需要发布我们的源代码,以保证我们的代码真实可信。此时,我们就可以借助 etherscan 提供的功能进行验证。
首先,我们获取 etherscan 的 API key,地址为 https://etherscan.io/myapikey,比如 PSW8C433Q667DVEX5BCRMGNAH9FSGFZ7Q8
;
更新 packages/hardhat-ts/package.json
中对应的 api-key 参数:
... |
由于项目中的一个 bug,需要在根目录下的 packages.json
中添加以下命令才能直接使用之后的命令:
"verify": "yarn workspace @scaffold-eth/hardhat etherscan-verify", |
运行 yarn verify --network rinkeby
,这个命令将通过 etherscan 接口进行合约验证,输出结果为:
$ yarn verify --network rinkeby |
验证完成后,我们可以看到 etherscan 中的合约页面已经加上了一个蓝色小钩,在合约中,也可以看到我们合约的源代码:
至此,我们就完成了合约的验证。
最后,当我们完成上述的所有步骤之后,我们可以将我们的结果提交到 speedrunethereum.com 上,选择对应的挑战,并提交部署的前端地址和承销商合约的链接即可:
Congratulations! 你已经完成了这个教程
通过篇教程,我们可以学习到如下内容:
approve
和 transferFrom
的使用;在合约中有一个修改器 canMintWitches()
用来检查地址是否能够在公开发售阶段铸造更多的 NFT:
uint256 public maxWitches; // 初始化为 9,999 |
这里面的 bug 只会在特定的条件下触发。问题在于应该有 9749 个女巫在公开函数中铸造,250个在 owner-only 函数中铸造,共计9999个。这个逻辑在公开发售阶段如果没有女巫被赠送,则完全正常。然而,项目方在这期间铸造并赠送了女巫,这意味着在上面的条件检查中,右边的总数应该也要变化才正确。铸造赠送越多,相应能允许的 tokenId
越高。
在公开发售结束的时候,有93个女巫被赠送,这意味着 tokenCounter.current()
到达 9749 使得公开发售结束时,总共只有 9656 个女巫被铸造。
canGiftWitches()
函数的作用是为了限制可以赠送的女巫数量最大为 250,所以我们不能通过以下的方式规避:
uint256 public maxWitches; // 初始化为 9,999 |
结果是,有93个女巫永久消失,合约总共铸造了9906个女巫。
我们可以通过 numGiftedWitches
记录已经赠送的女巫数量来修正。
uint256 public maxWitches; // 初始化为 9,999 |
Crypto Coven 认为拥有链上版税很重要,而不仅仅是使用特定于平台的链下实现,这就使得他们使用了 EIP-2981。 支持该标准的代码很简单:
function royaltyInfo(uint256 tokenId, uint256 salePrice) |
它是如何工作的呢? 市场调用该函数来读取接收方地址和版税金额的数据,然后相应地发送版税。 在上述例子中,接收方是合约地址,版税金额是 5%。然而,从 Solifidy 0.6.x 开始,合约必需要实现 receive()
方法才能接收以太,而女巫合约没有实现。并且,合约的测试在检查 royaltyInfo()
函数时,检查了是否返回正确的值,但是没有测试接收版税,所以如果市场尝试发送版税给合约会引起 revert
。
幸运的是,在这种情况下,补救措施非常简单,这要归功于 Royalty Registry。 项目方配置了一个覆盖指向不同的接收者 receiver
地址(在本例中,是他们的多重签名钱包),所以现在从 Royalty Registry 读取的市场将使用覆盖后的值。
修复此错误以支持 EIP-2981 的最简单方法是简单地返回接收提款的所有者地址,而不是合约地址。 另一种选择是添加一个 royalReceiverAddress
变量和一个 setter
函数来配置这个值。
如果确实想将以太接收到合约地址,你需要做的就是在合约中添加一个 receive()
函数:
receive() external payable {} |
学习在 Solidity 中进行开发可能是一场考验——无论是小错误还是大错误,都会永远存在于区块链上,而且通常要付出巨大的代价。 但是,这僵化、无情的空间却有它自己的魅力,在约束中诞生的创造力,通过共同的不眠之夜形成的团结。 对于任何在荒野中闯出自己道路的初出茅庐的奥术师:我希望这里所提供的知识能够进一步照亮这条道路。
loogies-svg-nft 是 scaffold-eth 提供的一个简单的 NFT 铸造和展示的项目,在本教程中,我们将带领大家一步步分析和实现这个项目。
由于项目的 loogies-svg-nft
分支与 master
分支在组件库和主页上有一些变化,故先将 master
分支代码与 loogies-svg-nft
分支进行了合并,解决冲突,得到一份基新组件库的全新的代码。可以参考项目地址: https://github.com/qiwihui/scaffold-eth.git 的 loogies-svg-nft
分支。本文以下内容将基于这些代码进行部署和分析。
首先我们先运行项目查看我们将要分析实现的功能。
首先我们在本地运行项目:
clone 项目并切换到 loogies-svg-nft
分支:
git clone https://github.com/qiwihui/scaffold-eth.git loogies-svg-nft |
安装依赖包
yarn install |
运行前端
yarn start |
在第二个终端窗口中,运行本地测试链
yarn chain |
在第三个终端窗口中,运行部署合约
yarn deploy |
此时在浏览器中访问 http://localhost:3000
,就可以看到程序了。
首先在 MetaMask 钱包中添加本地网络,并切换到本地网络;
Localhost 8545
http://localhost:8545
31337
ETH
创建一个新的本地钱包账号;
复制钱包地址,在页面左下角给这个地址发送一些测试 ETH;
点击在页面右上角 connect
连接钱包;
点击 Mint 铸造;
当交易成功后,可以看到新铸造的 NFT;
下面,我们开始对项目合约进行分析。
NFT,全称为Non-Fungible Token,指非同质化代币,对应于以太坊上 ERC-721 标准。 一般在智能合约中,NFT 的定义包含 tokenId
和 tokenURI
,每一个 NFT 的 tokenId
是唯一的, tokenURI
对于保存了NFT的元数据,可以是图像URL、描述、属性等。如果一个 NFT 想在 NFT 市场上进行展示和销售,则 tokenURI
内容需要对应符合 NFT 市场的标准,比如,在 NFT 市场 OpenSea 元数据标准中,就指出了 NFT 展示需要设置的属性。
OpenSea 中 NFT 元数据与展示对应关系
loogies-svg-nft 项目的合约文件在 packages/hardhat/contracts/
路径下,包含以下三个文件:
packages/hardhat/contracts/ |
HexString.sol
:生成地址字符串;ToColor.sol
:生成颜色编码字符串;YourCollectible.sol
: Loogies
NFT的合约文件,主要功能涉及合约铸造和元数据生成。合约的主要结构和方法为:
contract YourCollectible is ERC721, Ownable { |
constructor() public ERC721("Loogies", "LOOG") { |
代币符号: Loogies
代币名称: LOOG
合约继承自 OpenZeppelin 的 ERC721.sol
,这是 OpenZeppelin 提供的基本合约代码,可以方便开发者使用。
合约中分别对 uint256
, uint160
和 bytes3
等应用了不同库函数,扩展对应功能:
// 使 uint256 具有 toHexString 功能 |
以下代码是 Mint 时间限制:
uint256 mintDeadline = block.timestamp + 24 hours; |
合约在部署之后的24小时内可以铸造,超过24小时则会引发异常。这个机制类似于预售,由于这个合约比较简单,所以没有使用白名单机制,一般在实际情况,会使用预售和白名单的方式来控制 NFT 的发行。
铸造 NFT 其实就是在合约中设置两个信息:
tokenId
及其 owner
tokenId
及其 tokenURI
我们首先看铸造函数 mintItem
:
// 用于保存每一个铸造的 Loogies 的特征,其中,color 表示颜色,chubbiness 表示胖瘦 |
其中:
tokenId
在每次铸造时会自增,确保 tokenId
唯一;_mint
函数绑定 tokenId
及其 owner
;tokenId
对应的属性通过随机方式生成,具体为:blockhash(block.number-1)
),当前铸造账户( msg.sender
),合约地址( address(this)
)和 tokenId
生成哈希 predictableRandom
;predictableRandom
前三位得到颜色,颜色表示用 bytes3 表示,其中 bytes2(predictableRandom[0])
对应最低位蓝色数值, ( bytes2(predictableRandom[1]) >> 8 )
对应中间位绿色数值, ( bytes3(predictableRandom[2]) >> 16 )
对应最高位红色数值;35+((55*uint256(uint8(predictableRandom[3])))/255);
,uint8(predictableRandom[3])
介于0~255,故最小值为35,最大值为 35+55 = 90;例如: color
为 0x4cc4c1
, chubbiness
为 88 时对应的 NFT 图片为:
函数 tokenURI
接受 tokenId
参数,返回编码之后的元数据字符串:
function tokenURI(uint256 id) public view override returns (string memory) { |
其中, generateSVGofTokenById
函数返回 tokenId
对应的颜色和胖瘦属性生成的 SVG 字符串, renderTokenById
用户绘制图像。
我们可以看到,NFT 元数据中包含的属性有:
这里,我们通过实际数据了解一下什么是 SVG。tokenId
为 1 时对应的 tokenURI
结果为:
data:application/json;base64,eyJuYW1lIjoiTG9vZ2llICMxIiwiZGVzY3JpcHRpb24iOiJUaGlzIExvb2dpZSBpcyB0aGUgY29sb3IgIzRjYzRjMSB3aXRoIGEgY2h1YmJpbmVzcyBvZiA4OCEhISIsImV4dGVybmFsX3VybCI6Imh0dHBzOi8vYnVybnlib3lzLmNvbS90b2tlbi8xIiwiYXR0cmlidXRlcyI6W3sidHJhaXRfdHlwZSI6ImNvbG9yIiwidmFsdWUiOiIjNGNjNGMxIn0seyJ0cmFpdF90eXBlIjoiY2h1YmJpbmVzcyIsInZhbHVlIjo4OH1dLCJvd25lciI6IjB4MTY5ODQxYWEzMDI0Y2ZhNTcwMDI0ZWI3ZGQ2YmY1Zjc3NDA5MjA4OCIsImltYWdlIjoiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCM2FXUjBhRDBpTkRBd0lpQm9aV2xuYUhROUlqUXdNQ0lnZUcxc2JuTTlJbWgwZEhBNkx5OTNkM2N1ZHpNdWIzSm5Mekl3TURBdmMzWm5JajQ4WnlCcFpEMGlaWGxsTVNJK1BHVnNiR2x3YzJVZ2MzUnliMnRsTFhkcFpIUm9QU0l6SWlCeWVUMGlNamt1TlNJZ2NuZzlJakk1TGpVaUlHbGtQU0p6ZG1kZk1TSWdZM2s5SWpFMU5DNDFJaUJqZUQwaU1UZ3hMalVpSUhOMGNtOXJaVDBpSXpBd01DSWdabWxzYkQwaUkyWm1aaUl2UGp4bGJHeHBjSE5sSUhKNVBTSXpMalVpSUhKNFBTSXlMalVpSUdsa1BTSnpkbWRmTXlJZ1kzazlJakUxTkM0MUlpQmplRDBpTVRjekxqVWlJSE4wY205clpTMTNhV1IwYUQwaU15SWdjM1J5YjJ0bFBTSWpNREF3SWlCbWFXeHNQU0lqTURBd01EQXdJaTgrUEM5blBqeG5JR2xrUFNKb1pXRmtJajQ4Wld4c2FYQnpaU0JtYVd4c1BTSWpOR05qTkdNeElpQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlHTjRQU0l5TURRdU5TSWdZM2s5SWpJeE1TNDRNREEyTlNJZ2FXUTlJbk4yWjE4MUlpQnllRDBpT0RnaUlISjVQU0kxTVM0NE1EQTJOU0lnYzNSeWIydGxQU0lqTURBd0lpOCtQQzluUGp4bklHbGtQU0psZVdVeUlqNDhaV3hzYVhCelpTQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlISjVQU0l5T1M0MUlpQnllRDBpTWprdU5TSWdhV1E5SW5OMloxOHlJaUJqZVQwaU1UWTRMalVpSUdONFBTSXlNRGt1TlNJZ2MzUnliMnRsUFNJak1EQXdJaUJtYVd4c1BTSWpabVptSWk4K1BHVnNiR2x3YzJVZ2NuazlJak11TlNJZ2NuZzlJak1pSUdsa1BTSnpkbWRmTkNJZ1kzazlJakUyT1M0MUlpQmplRDBpTWpBNElpQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlHWnBiR3c5SWlNd01EQXdNREFpSUhOMGNtOXJaVDBpSXpBd01DSXZQand2Wno0OEwzTjJaejQ9In0= |
通过 base64 解码 data:application/json;base64,
之后的字符串可以得到如下 json(以下 json 经过了格式化,方便阅读):
{ |
我们对 image
字段进行解码并格式化就得到图片的 SVG:
|
SVG是一种用 XML 定义的语言,用来描述二维矢量及矢量/栅格图形。它可以任意放大图形显示,也不会牺牲图像质量,它可以使用代码进行描述,方便编辑,因此被广泛使用。
从上面的代码结合以下的图像可以看出,这个 SVG 包含如下内容:
eye1
:由两个椭圆(ellipse)绘制的眼圈和黑色眼珠;head
:填充 #4cc4c1
颜色的椭圆作为身体;eye2
:与 eye1
一致,位置不同;eye1
,head
和eye2
依次叠加得到最终的图形:
uint2str
将 uint
转变为字符串,例如 123
变为 '123'
function uint2str(uint _i) internal pure returns (string memory _uintAsString) { |
ToColor.sol
库:将 byte3
类型转换为前端颜色字符串,例如:输入 0x4cc4c1
输出 '4cc4c1'
library ToColor { |
HexStrings.sol
库:主要作用是将 uint
按 length
位提取,对应于生成公钥时截取前20位的功能: (*uint160*(ownerOf(id))).toHexString(20)
,此表达式生成对应 tokenId
所有者的地址。library HexStrings { |
至此,合约源码分析完成。
下面我们将对前端的逻辑进行简要分析,然后我们将一步步实现 NFT 铸造和展示的功能。将代码切换到前端代码提交之前,按照以下的步骤一步步添加功能。
git checkout a98156f6a03a0bc8fc98c8c77cef6fbf59f03b31 |
项目前端文件在 packages/react-app
内,以下文章中涉及文件的位置都将在这个文件中寻找。
我们首先来看一下 src/App.jsx
,这是项目的主要页面,我们可以利用代码编辑器查看这个文件的主要部分:
其中包含的功能和组件包括:
Header
:标题栏,显示标题NetworkDisplay
:所处网络状态Menu
, Switch
:菜单切换ThemeSwitch
:右下角明暗主题切换Account
:右上角账户信息组件Row
对应左下角的 Gas 显示、支持和本地的水龙头下面我们主要看一下 NetworkDisplay
和 Account
的逻辑实现,以及 Menu
, Switch
中的功能。
NetworkDisplay
组件位置: src/components/NetworkDisplay.jsx
主要包含两个功能:
31337
。function NetworkDisplay({ |
Account
组件位置: src/components/Account.jsx
主要包含两个功能:
Connect
或者 Logout
其中,当用户点击 Connect
时,前端调用 loadWeb3Modal
,代码如下,这个函数的需要功能是与MetaMask等钱包进行连接,并监听钱包的 chainChanged
,accountsChanged
和 disconnect
事件,即当我们在钱包中切换网络,选择连接账户以及取消连接时对应修改显示状态。
const loadWeb3Modal = useCallback(async () => { |
同理,在连接钱包情况下,用户点击 Logout
会调用 logoutOfWeb3Modal
功能,
const logoutOfWeb3Modal = async () => { |
这两个分别对应显示菜单和对应切换菜单功能,这些菜单包括:
App Home
:项目希望我们将需要实现的功能放在这个菜单中,比如我们将要实现的 NFT 的铸造和展示功能;Debug Contracts
:调试自己编写的合约功能,将会根据合约的 ABI 文件 展示可以合约的状态变量和可以调用的函数;Hints
:编程提示ExampleUI
:示例UI,可以做为编程使用Mainnet DAI
:以太坊主网 DAI
的合约状态和可用函数,与 Debug Contracts
功能一直Subgraph
:使用 The Graph 协议对合约中的事件进行监听和查询。App.jsx
中还包含了打印当前页面状态的调试信息,可以在开发的过程中实时查看当前状态变量。
// |
查看完主页的基本功能,下面我们开始实现 NFT 铸造和展示 NFT 列表这两个功能。
我们将主要实现以下三个部分功能:
首先我们找到 App Home
对应使用的组件,从下面的代码中可以看到,对应使用 Home
组件,所在位置为 src/views/Home.jsx
。
... |
删除 Home.jsx
中内容,添加以下 Mint 按钮:
import React, { useState } from "react"; |
同时将 Switch
中对应组件使用修改为:
... |
效果图为:
点击 Mint 之后,我们可以看到交易成功发出,这时,虽然我们成功 mint 了 NFT,但是我们还需要添加列表来展示我们的 NFT。
添加列表展示,其中包含 NFT 的转移功能可以将对应的 NFT 发送给其他地址。
import React, { useState } from "react"; |
对应组件使用修改为:
... |
效果图为:
但是我们发现,当我们再次 mint 时,列表并不会更新,还是原来的样子,因此我们需要在 App.jsx
中添加事件监听,一旦我们铸造 NFT 之后,列表将刷新:
// 跟踪当前 NFT 数量 |
此时,当我们再次 Mint 时,就是自动更新列表,显示最新铸造的 NFT 了。
这个功能比较简单,只需要修改对应 debug 部分即可:
<Route exact path="/debug"> |
更新之后,可以在 Debug Contracts
菜单下看到合约的可以调用的函数。
至此,我们就完成了一个简单 NFT 铸造和展示的 DApp 了。
通过这个项目,我们可以学习并了解以下知识:
加密女巫的合约代码堪称艺术品。代码出自工程师 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 一样的作品。
/* |
使用 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 { |
合约中使用修改器对权限进行控制,其中包括:
publicSaleActive
公开销售状态communitySaleActive
社区销售状态maxWitchesPerWallet
每个钱包最大 token 数量canMintWitches
控制token总数量canGiftWitches
isCorrectPayment
判断购买时价格是否正确isValidMerkleProof
用于白名单机制中的 Merkle 验证这些修改器可以使得权限控制更简便,代码的可读性也大大提升。
// ============ ACCESS CONTROL/SANITY MODIFIERS ============ |
NFT 项目都需要包含图片的存储,合约将 NFT 对应的元信息存储在 IPFS 中,并将对应的图片存储都在 Amazon S3 存储中。
string private baseURI; |
比如 tokenId
为 1
的 NFT,对应的 tokenURI
为 ipfs://QmZHKZDavkvNfA9gSAg7HALv8jF7BJaKjUc9U2LSuvUySB/1.json
,在 IPFS 中可以看到这里面的内容为:
{ |
其中包含女巫的ID,名称,图片地址,属性等信息。
不得不说,如果 Amazon S3 出问题了,可能这些图片就没法显示了。
对于预售,项目方使用白名单方式进行,而对于白名单验证,合约中使用 Merkle 证明的方式进行验证。
在 mint 时,只需发送正确的 Merkle 证明来验证即可实现白名单功能,这个方法不仅效率高,而且省去了在合约中存储所有白名单地址造成的 Gas 消耗。
modifier isValidMerkleProof(bytes32[] calldata merkleProof, bytes32 root) { |
详细细节可以参考我之前的一篇文章:
可以看到在 OpenSea 上列出这些 NFT 费用为 0 gas,因为合约预先批准了 OpenSea 合约以节省用户的 gas,同时合约还包括一个紧急功能来消除这种行为!
/** |
为了防止 Opensea 关闭或者被入侵,合约可以通过 setIsOpenSeaProxyActive
方法关闭预先批准。
// function to disable gasless listings for security in case |
这是一种发布并能检测到一个智能合约实现了什么接口的标准,用于实现对合约实现的接口的查询。这个标准需要实现 suppoetsInterface
方法:
interface ERC165 { |
加密女巫实现复写这个方法是因为它额外实现了 EIP2981 这个标准,需要指出。
function supportsInterface(bytes4 interfaceId) |
EIP-2981 实现了标准化的版税信息检索,可被任何类型的 NFT 市场接受。EIP-2981 支持所有市场检索特定 NFT 的版税支付信息,从而实现无论 NFT 在哪个市场出售或转售都可以实现准确的版税支付。
NFT 市场和个人可通过检索版税支付信息 royaltyInfo()
来实施该标准,它指定为特定的 NFT 销售价格向指定的单一地址支付特定比例的金额。对于特定的 tokenId
和 salePrice
,在请求时需提供一个版税接收者的地址和要支付的预期版税金额(百分比表示)。
女巫合约规定了 5% 的版税,但是这个标准并不是强制性的,需要靠市场去实施此标准。
/** |
不太确定此函数中的注释 See {IERC165-royaltyInfo}.
是否正确,需要确认。
没有 tokensOfOwner
方法
可能是基于女巫NFT的具体场景与优化 Gas 做的权衡,查询 token 所有者的功能需要靠 Opensea 的 API 或者 The Graph 去实现。
在没有外部调用的函数中也加了 nonReentrant
msg.sender
可能是合约
对 onlyOwner
也加了 nonReentrant
,避免可能的被利用。
使用 Merkle 树做 NFT 白名单验证,简单来说就是将所有的白名单钱包地址做为 Merkle 树的叶节点生成一棵 Merkle 树,在部署的NFT 合约中只存储 Merkle 树的 root hash,这样避免了在合约中存储所有白名单地址带来的高额 gas 费用。在 mint 时,前端生成钱包地址的 Merkle proof,调用合约进行验证即可。
一次验证过程前端和合约运行过程如图:
图片来自 [3]
详情请参见:https://en.wikipedia.org/wiki/Merkle_tree
图片来自 [1]
比如,以水果单词作为叶节点,生成 Merkle 树的结构如下:
图片来自 [2]
我们简单实现 Merkle 验证的过程,此合约包含以下功能:
setSaleMerkleRoot
isValidMerkleProof
mint
//SPDX-License-Identifier: MIT |
调用合约验证的 Merkle proof 需要在前端生成。生成过程需要用到 merkletreejs
和 keccak256
两个库,前者用于创建 Merkle 树,后者用于生成哈希。
npm install --save merkletreejs keccak256 |
第一步,生成白名单地址的 Merkle 树:
const { MerkleTree } = require('merkletreejs'); |
我们可以看到根哈希值为 0xc7ec7ffb250de2b95a1c690751b2826ec9d2999dd9f5c6f8816655b1590ca544
,这个值在调用合约函数 setSaleMerkleRoot
时需要用到,会保存在合约中。生成的 Merkle 证明需要存储在页面中,也可以存在 IPFS 中,在使用时加载使用。
第二步,需要生成参与 mint 地址的 Merkle 证明,假设使用 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33
地址进行 mint 操作:
let leaf = keccak256('0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33'); |
对应生成的证明为
Proof of 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33: [ |
同时我们将生成一个假的证明:
// another proof, for example |
此过程将使用 Remix IDE 进行部署和测试:
使用 Remix 将合约部署到以太坊测试网 Rinkeby
中,得到合约地址为: 0xb3E2409199855ea9676dc5CFc9DefFd4A1b93eFe
;
调用 setSaleMerkleRoot
设置 Merkle 根哈希为 0xc7ec7ffb250de2b95a1c690751b2826ec9d2999dd9f5c6f8816655b1590ca544
;
调用 mint
,传入非法 Merkle 证明:["0x1575cc1dded49f942913392f94716824d29b8fa45876b2db6295d16a606533a4","0x6c42c6099e51e28eef8f19f71765bb42c571d5c7177996f177606138f65c0c2b"]
,可以看到交易失败,显示 Fail with error 'Address does not exist in list
;
验证 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33
地址对应 mint 状态是否为 false;
调用 mint
,传入合法的 Merkle 证明:
["0x1575cc1dded49f942913392f94716824d29b8fa45876b2db6295d16a606533a4","0x6c42c6099e51e28eef8f19f71765bb42c571d5c7177996f177606138f65c0c2b","0xb0d6f760008340e3f60414d84b305702faa6418f44f31de07b10e05bf369eb3b","0x4c880bf401add28c4e51270dfe16b28c3ca1b3d263ff7c5863fc8214b4046364"]
,可以看到交易成功;
验证 0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33
地址对应 mint 状态是否为 true
。
。
为此,翻译人员必须了解源语言和目标语言的语法结构。
有些编译器会编译成低级机器码,可以直接在系统上执行。其他编译器会编译成一种中间语言,由虚拟机执行。
选择编译器时的一个考虑因素是系统可移植性要求。Java和 .NET CLR将编译成一种中间语言,以便编译后的代码可以跨多个系统架构移植。C、Go、C++ 和 Pascal 将编译成可执行的二进制文件。此二进制文件是为编译它的平台构建的。
Python 应用程序通常作为源代码分发。Python 解释器的作用是将Python源代码进行转换并一步执行。CPython 运行时在第一次运行时会编译你的代码。这一步对普通用户是不可见的。
Python 代码不会被编译成机器码;它被编译成一种称为 字节码 的低级中间语言。此字节码存储在 .pyc
文件中并缓存以供执行。如果在不更改源代码的情况下两次运行同一个 Python 应用程序,则第二次执行速度会更快。这是因为它加载编译后的字节码而不是每次都重新编译。
CPython 中的 C 是对 C 编程语言的引用,这意味着这个 Python 发行版是用 C 语言编写的。
这种说法大多是正确的:CPython 中的编译器是用纯 C 编写的。但是,许多标准库模块是用纯 Python 或 C 和 Python 组合编写的。
那么为什么 CPython 编译器是用 C 而不是 Python 编写的呢?
答案在于编译器的工作方式。 有两种类型的编译器:
如果你要从头开始编写新的编程语言,则需要一个可执行应用程序来编译你的编译器!你需要一个编译器来执行任何事情,所以当开发新语言时,它们通常首先用更老的、更成熟的语言编写。
还有一些可用的工具可以读取语言规范并创建解析器。流行的编译器-编译器(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> : |
编译器在尝试执行之前需要严格的语言语法结构规则。
对于本书的其余部分,
./python
将指代 CPython 的编译版本。 但是,实际命令将取决于你的操作系统。对于 Windows:
> > python.exe
>
对于 Linux:
> $ ./python
>
对于 macOS:
> $ ./python.exe
>
CPython 源代码中包含 Python 语言的定义。这个文档是所有 Python 解释器使用的参考规范。
该规范采用人类可读和机器可读的格式。文档里面是对 Python 语言的详细解释。包含允许的内容以及每个语句的行为方式。
位于 Doc/reference
目录中的是 Python 语言中每个功能的 reStructured-Text 解释。这些文件构成了 docs.python.org/3/reference 上的官方 Python 参考指南。
目录里面是你需要了解整个语言、结构和关键字的文件:
cpython/Doc/reference/ |
在 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)规范和正则表达式语法。因此,在语法文件中,你可以使用:
*
用于重复+
至少重复一次[]
用于可选部分|
对于替代品()
用于分组例如,考虑如何定义一杯咖啡:
在 EBNF 中定义的咖啡订单可能如下所示:
coffee: 'cup' ('espresso')+ ['water'] [milk] |
在本章中,语法是用铁路图形象化的。 这张图是咖啡语句的铁路图:
在铁路图中,每个可能的组合必须从左到右排成一条线。 可选语句可以被绕过,有些语句可以形成循环。
如果在语法文件中搜索 with_stmt
,可以看到定义:
with_stmt: 'with' with_item (',' with_item)* ':' suite |
引号中的任何内容都是字符串文字,称为终端(terminal)。终端是识别关键字的方式。with_stmt
指定为:
with
开始with_item
,它可以是 test
,和(可选的) as
以及一个表达式 expr
with_item
,每个都用逗号隔开:
结尾suite
在这两行中引用了其他三个定义:
• suite
是指包含一个或多个语句的代码块• test
指的是一个被评估的简单的语句• expr
指的是一个简单的表达式
在铁路图中可视化,with
语句如下所示:
作为一个更复杂的例子,try
语句定义为:
try_stmt: ('try' ':' suite |
try
语句有两种用途:
try
和一个或多个 except
子句,然后是一个可选的 else
,然后是一个可选的 finally
try
和只有一个 finally
语句或者,在铁路图中可视化:
try
语句是更复杂结构的一个很好的例子。
如果你想详细了解 Python 语言,语法在 Grammar/Grammar
中定义。
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 更简单,路径减少了:
NFA 和 DFA 图仅用于调试复杂语法的设计。
我们将使用铁路图代替 DFA 或 NFA 图来表示语法。例如,此图表示 decorator
语句可以采用的路径:
要查看 pgen
的运行情况,让我们更改部分 Python 语法。在 Grammar/Grammar
中搜索 pass_stmt
以查看 pass
语句的定义:
pass_stmt: 'pass' |
通过添加选择 |
和 proceed
字面量,更改该行以接受终端(关键字)'pass'
或 'proceed'
作为关键字:
pass_stmt: 'pass' | 'proceed' |
接下来,通过运行 pgen
重建语法文件。CPython 带有脚本来自动化 pgen
。
在 macOS 和 Linux 上,运行 make regen-grammar
:
$ make regen-grammar |
对于 Windows,从 PCBuild 目录调出命令行并使用 --regen
标志运行 build.bat
:
build.bat --regen |
你应该会看到一个输出,显示新的 Include/graminit.h
和 Python/graminit.c
文件已重新生成。
使用重新生成的解析器表,当你重新编译 CPython 时,它将使用新语法。
如果代码编译成功,你可以执行新的 CPython 二进制文件并启动 REPL。
在 REPL 中,你现在可以尝试定义一个函数。不要使用 pass 语句,而是使用你编译到 Python 语法中的 proceed
关键字替代 pass
:
$ ./python -X oldparser |
恭喜,你已经更改了 CPython 语法并编译了你自己的 CPython 版本。
接下来,我们将探索标记(tokens)及其与语法的关系。
除了 Grammar
文件夹中的语法文件之外,还有 Grammar/Tokens
文件,其中包含在分析树中作为叶节点找到的每个唯一类型。每个标记还有一个名称和一个生成的唯一 ID。名称用于使在分词器(tokenizer)中更容易引用。
Grammar/Tokens
文件是 Python 3.8 中的一项新功能。
例如,左括号称为 LPAR
,分号称为称为 SEMI
。 你将在本书后面看到这些标记:
LPAR '(' |
和 Grammar
文件一样,如果你修改了 Grammar/Tokens
文件,你需要重新运行 pgen
。
要查看操作中的标记,你可以使用 CPython 中的 tokenize
模块。
CPython 源代码中有两个标记器。这里演示了一个用 Python 编写的分词器,另一个用 C 编写。用 Python 编写的分词器是一个实用程序,Python 解释器使用用 C 编写的那个。它们具有相同的输出和行为。用 C 编写的版本是为性能而设计的,而 Python 中的模块是为调试而设计的。
cpython-book-samples/13/test_tokens.py
:
# Demo application |
将 test_tokens.py
文件输入到标准库中内置的名为 tokenize
的模块中。你将按行和字符看到标记列表。使用 -e
标志输出确切的标记名称:
$ ./python -m tokenize -e test_tokens.py |
在输出中,第一列是行/列坐标的范围,第二列是标记的名称,最后一列是标记的值。
在输出中, tokenize
模块隐含了一些标记:
utf-8
的 ENCODING
标记DEDENT
关闭函数声明ENDMARKER
结束文件最佳做法是在 Python 源文件的末尾有一个空行。如果省略它,CPython 会为你添加它。
tokenize
模块是用纯 Python 编写的,位于 Lib/tokenize.py
中。
要查看 C 分词器的详细读数,您可以使用 -d
标志运行 Python。使用之前创建的 test_tokens.py
脚本,使用以下命令运行它:
$ ./python -d test_tokens.py |
在输出中,你可以看到它突出显示了作为关键字的 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] |
或者,在铁路图中可视化为:
if
语句的新语法已更改为用 namedexpr_test
替换 test
:
if_stmt: 'if' namedexpr_test ':' suite ('elif' namedexpr_test ':' suite) |
在铁路图中可视化:
为了区分 :=
和现有的 COLON
(:
) 和 EQUAL
(=
) 标记,将以下标记也添加到 Grammar/Tokens
中:
COLONEQUAL ':=' |
这不是支持赋值表达式所需的唯一更改。 如 Pull Request 中所示,这一变化改变了 CPython 编译器的许多部分。
有关 CPython 解析器生成器的更多信息,
pgen
的作者在 PyCon Europe 2019 上录制了有关实现和设计的演示文稿:“野兽之魂”。
在本章中,你已经了解了 Python 语法定义和解析器生成器。在下一章中,你将扩展该知识以构建更复杂的语法功能,即“几乎等于”运算符。
在实践中,必须仔细考虑和讨论对 Python 语法的更改。审查水平有两个原因:
如果 Python 核心开发人员提议对语法进行更改,则必须将其作为 Python 增强提案 (PEP) 提出。所有 PEP 都在 PEP 索引上进行编号和索引。PEP 5 记录了语言发展的指南,并指定必须在 PEP 中提出更改。
成员还可以通过 python-ideas 邮件列表建议对核心开发组之外的语言进行更改。
你可以在 PEP 索引中查看 CPython 未来版本的起草的、拒绝的和接受的 PEP。一旦 PEP 达成共识,并且草案已定稿,指导委员会必须接受或拒绝它。PEP 13 中定义的指导委员会的任务规定,他们应努力“维护 Python 语言和 CPython 解释器的质量和稳定性”。
与 Python 文件不同,C 源代码每次更改时都必须重新编译。
在前一章中,我们已经设置开发环境,并设置了运行“Build”阶段的选项,该选项将重新编译 CPython。在构建步骤工作之前,你需要一个 C 编译器和一些构建工具。使用的工具取决于你使用的操作系统。
如果你担心这些步骤中的任何一个会干扰您现有的 CPython 安装,请不要担心。CPython 源目录的行为就像一个虚拟环境。
对于编译 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 \ |
一旦安装完成,你就可以使用 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" \ |
运行 configure
将在存储库的根目录中生成一个 Makefile
,你可以使用它来自动化构建过程。
你现在可以通过运行一下命令来构建 CPython 二进制文件:
$ make -j2 -s |
在构建过程中,你可能会收到一些错误。在构建摘要中,make
会通知你并非所有包都已构建。例如,ossaudiodev
、spwd
和 _tkinter
将无法使用这组指令进行构建。如果你不打算针对这些软件包进行开发,那也没关系。如果是,请查看官方开发指南网站以获取更多信息。
构建将需要几分钟并生成一个名为 python.exe
的二进制文件。每次对源代码进行更改时,你都会需要使用相同的标志重新运行 make
。python.exe
二进制文件是 CPython 的调试二进制文件。执行 python.exe
以查看有效的 REPL:
$ ./python.exe |
是的,没错,macOS 版本的文件扩展名为
.exe
。 这个扩展不是因为它是 Windows 二进制文件!因为 macOS 有一个不区分大小写的文件系统,并且在使用二进制文件时,开发人员不希望人们不小心引用Python/
目录,因此附加了.exe
以避免歧义。如果你稍后运行make install
或make 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 配置文件引导的优化包括以下检查和改进:
if..else if
或 switch
语句,并找出最常用的路径。例如,如果 switch
语句中有 10 个 case
,其中一个使用了 95% 的时间,那么它会被移到顶部,以便在代码路径中立即执行。在本章中,你已经了解了如何将 CPython 源代码编译成可工作的解释器。在探索和改编源代码时,你可以在整本书中使用这些知识。
在使用 CPython 时,你可能需要重复编译步骤数十次甚至数百次。如果你可以调整你的开发环境来创建重新编译的快捷方式,最好现在就这样做,这样可以节省大量时间。
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
这本书将涵盖 CPython 内部实现的主要概念,并学习如何:
遵循 Creative Commons Public Domain (CC0) License。
这书中的代码已经在 Windows 10、macOS 10.15 和 Linux 上使用 Python 3.9.0b1 进行了测试。
CPython 源代码大约 65% 是 Python(测试是重要的部分)、24% C,其余是其他语言的混合。
从 code.visualstudio.com 下载 Visual Studio Code,并在本地安装。
推荐的扩展:
VS Code 在工作区目录中创建一个文件夹 .vscode
。 在此文件夹中,你可以创建:
tasks.json
用于执行项目命令的快捷方式launch.json
用于配置调试器在 .vscode
目录中创建一个 tasks.json
文件,并添加以下内容:
cpython-book-samples/11/tasks.json
:
{ |
使用任务资源管理器插件,你将在 vscode 组中看到你配置的任务列表。
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
外部引入:通过 link 的方式引用 CSS 样式
<head> |
内部引入,在 HTML 中的 head 位置添加 style 标签
<head> |
内联样式
<p style="color: red; font-size: 18px;">内容</p> |
white-space
属性作用就是告诉浏览器遇到「空格」该如何处理,这里的空格不是单纯意义上的空格。normal
nowrap
overflow
属性overflow-x
, overflow-y
/* 默认值。内容不会被修剪,会呈现在元素框之外 */ |
/*清除标签默认边距*/ |
注意:写 CSS 代码的时候,即使某个属性写错,浏览器也不会报错,只会忽略无法识别的 CSS 样式。
标签选择器: 如 p
, li
等
class 选择器:如 .first
ID 选择器: #firstid
通用选择器: *
,作用于所有的标签
属性选择器:根据属性来匹配HTML元素
/* 匹配所有使用属性 "lefe" 的元素 */ |
类似于“正则表达式”的属性选择器,比如: [attr^=val]
匹配以 val 开头的元素, [attr$=val]
,匹配以 val 结尾的元素, [attr*=val]
匹配包含 val 的字符串的元素
伪选择器(pseudo-selectors):它包含伪类(pseudo-classes)和伪元素(pseudo-elements)。这类选择器不是真正意义上的选择器,它作为选择器的一部分,起到选择器匹配元素的限定条件。
/* 匹配超链接样式 */ |
伪元素(pseudo-elements)选择器,它以“ :: ” 为标识符
p::first-letter{ |
/* Selects any <p> that is the first element |
组合选择器(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 样式,不同元素使用逗号隔开。伪类选择器:作用是选中某个元素中符合某些条件的元素。作用于现有元素,相当于给现有元素添加某些属性。使用单个冒号 :
:first-child |
伪元素选择器:作用就是给现有元素添加某些新的内容,就好比给某个元素添加了一个新的标签,使用2个冒号 ::
::first-letter 表示对首字母进行操作 |
@规则在CSS中用于传递元数据、条件信息或其他描述性信息。它们以at符号(@)开头,后跟一个标识符来说明它是什么类型的规则,然后是某种类型的语法块,以分号(;)结尾。由标识符定义的每种类型的 at 规则都有其自己的内部语法和语义。
@charset and @import (metadata) |
下面这个 CSS 只适用于屏幕超过 800px 的设备:
@media (min-width: 801px) { |
@media
语法
@media mediaType and|not|only (media feture) { |
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: 文字样式,比如斜体;
两种盒子类型
通过 display
修改盒子的显示方式
.title { |
盒模型
模式
标准的盒子模型
对于这种盒子模式,给它设置的 width 和 height 是 content 的宽高,当给盒子添加 padding 和 border 的时候,会增加盒子的整体大小。「外边距不会计入盒子的大小,它只是表示外部的边距」。
诡异盒子模型(The alternative CSS box model)
对于这种盒子模式,给它设置的 width 和 height 是盒子的宽高,也就是说内容 content 的宽需要减去 border 和 padding 的宽。
谷歌浏览器默认的是标准的盒模型,可以通过:
box-sizing: border-box; |
来修改盒模型为诡异盒模型。
display:inline
display:block
display:inline-block
这种布局方式结合了 inline 和 block 这两种元素的特性,它与块级元素不同的是:元素不会单独占用一行;相同的是:可以使用 width 和 height,可以通过 padding、margin 和 border 来控制元素的显示位置。
说白了就是除了不会单独占一行,其余的与块级元素一致。
display:none
隐藏元素
display:flex
一维
display:grid
二维
设置背景图
background-color: antiquewhite; |
top | left | bottom | right
,在某个边缘的位置,另一个维度为 50%。比如 top,背景图的起始位置为顶部,在X轴方向为 50%,居中显示;no-repat
不重复, repeat
重复, repat-x
X轴上重复,还有其它关键字。img
标签
行内(inline)元素
<img class="logo" src="./images/1.png" alt="图片"> |
.logo { |
text-overflow 和 -webkit-line-clamp
.singal-line { |
.two-line { |
text-overflow:只对块级元素起作用,表示对超出指定区域的内容该如何显示
-webkit-box:webkit 的 CSS 扩展属性
*
:通用选择器,权重最低,就是 0,第 1 张图就是此意;.myClass, [type=chekbox], :only-of-type
: 类、属性、伪类选择器。第 5 张图,一个类选择器,权重值表示为 0-1-0;5-15张图能看懂了吧;#myDiv
:id选择器,一条鲨鱼,权重比较高,权重值为 1-0-0;`style
:权重值更高,权重值为 1-0-0-0;!important
: 无敌,我是老大,告诉浏览器必须使用我定义的属性;g
:直接在元素中使用属性,权重最高,可以看做 1-0-0-0;z
:id选择器,权重次子,可以看做 0-1-0-0;y
:类、伪类、属性选择器,权重低,可以看做 0-0-1-0;x
:元素、伪元素选择器,权重最低,可以看做 0-0-0-1;主要有两种方式
@keyframes
定义的名字。 @keyframes
指定某一帧的动画如何变化,可通过 % 来控制各个阶段的属性值.move-box-animation { |
.move-transition { |
less,sass
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
《爱是一种选择》和《非暴力沟通》都没有读完
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
日剧和电影看得多一些啊。
六月开始涉及一些心理学的书籍,但是都在读。
1. 早起和锻炼争取能在六点钟起床,做半小时自己喜欢的,有未来意义的事情,然后出门跑步锻炼;
2. 心理咨询继续和咨询师探讨自己家庭给自己带来的困扰;
3. 准备雅思考试完全定下雅思考试时间,复习规划;
4. 工作技能
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
数据密集型(data-intensive)而非计算密集型(compute-intensive):
影响数据系统设计的因素很多,包括参与人员的技能和经验、历史遗留问题、系统路径依赖、交付时限、公司的风险容忍度、监管约束等。
重要的三个问题;
可靠性(Reliability)
系统在困境(adversity)(硬件故障、软件故障、人为错误)中仍可正常工作(正确完成功能,并能达到期望的性能水准)。
可扩展性(Scalability)
有合理的办法应对系统的增长(数据量、流量、复杂性)。
可维护性(Maintainability)
随着时间的推移,许多新的人员参与到系统开发和运维, 以维护现有功能或适配 新场景等,系统都应高效运转。
期望:
故障(fault)和失效(failure):
系统不见得一直可靠,比如负载增加,系统需要持续变化。
软件开发本身开销并不算大,日后的维护升级需要花费更多。
软件系统的三个设计原则:
可运维性(Operability)
方便运营团队来保持系统平稳运行。
简单性(Simplicity)简化系统复杂性,使新工程师能够轻松理解系统。注意这与用户界面的简单性并不一样。
可演化性(Evolvability)后续工程师能够轻松地对系统进行改进,井根据需求变化将其适配到非典型场景,也称为可延伸性、易修改性或可塑性。
保证代码简洁,让新加入的码农也能理解代码。一个很有效的方法是抽象,用高级类来描述统一的行为。
方便后续重构、加入新功能那个,与前面的简单性息息相关。
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
random
模块random.seed()
再生产数字secrets
模块创建密码学上强的随机数numpy.random
创建随机 nd 数组random
模块该模块为各种版本实现伪随机数生成器。它使用Mersenne Twister算法(https://en.wikipedia.org/wiki/Mersenne_Twister)作为其核心生成器。 之所以称其为伪随机数,是因为数字看起来是随机的,但是是可重现的。
import random |
0.10426373452067317 |
使用 random.seed()
,可以使结果可重复,并且 random.seed()
之后的调用链将产生相同的数据轨迹。 随机数序列变得确定,或完全由种子值确定。
print('Seeding with 1...\n') |
Seeding with 1... |
secrets
模块secrets
模块用于生成适合于管理数据(例如密码,帐户身份验证,安全令牌和相关机密)的密码学上强的随机数。
特别是,应优先使用secrets
而不是 random
模块中默认的伪随机数生成器,后者是为建模和仿真而设计的,而不是安全或加密技术。
import secrets |
6 |
为多维数组创建随机数。NumPy伪随机数生成器与Python标准库伪随机数生成器不同。
重要的是,设置Python伪随机数生成器种子不会影响NumPy伪随机数生成器,必须单独设置和使用。
import numpy as np |
[4.17022005e-01 7.20324493e-01 1.14374817e-04] |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
json
模块。 只需导入它,就可以使用JSON数据了:import json |
JSON的一些优点:
{ |
JSON支持基本类型(字符串,数字,布尔值)以及嵌套的数组和对象。 根据以下转换,将简单的Python对象转换为JSON:
使用 json.dumps()
方法将Python对象转换为JSON字符串。
import json |
{"name": "John", "age": 30, "city": "New York", "hasChildren": false, "titles":["engineer", "programmer"]} |
或将Python对象转换为JSON对象,然后使用 json.dump()
方法将其保存到文件中。
import json |
使用 json.loads()
方法将JSON字符串转换为Python对象。 结果将是一个Python字典。
import json |
{'age': 30, 'city': 'New York', 'hasChildren': False, 'name': 'John', 'titles': ['engineer', 'programmer']} |
或从文件加载数据,然后使用 json.load()
方法将其转换为Python对象。
import json |
{'name': 'John', 'age': 30, 'city': 'New York', 'hasChildren': False, 'titles': ['engineer', 'programmer']} |
使用默认的 JSONEncoder
编码自定义对象将引发 TypeError
。 我们可以指定一个自定义的编码函数,该函数将类名和所有对象变量存储在字典中。 将此函数用作 json.dump()
方法中的 default
参数。
import json |
{"complex": true, "real": 5.0, "imag": 9.0} |
你还可以创建一个自定义的 Encoder 类,并覆盖 default()
方法。 将其用于 json.dump()
方法中的 cls
参数,或直接使用编码器。
from json import JSONEncoder |
{"complex": true, "real": 5.0, "imag": 9.0} |
可以使用默认 JSONDecoder 解码自定义对象,但是它将被解码为字典。 编写一个自定义解码函数,该函数将以字典作为输入,并在可以在字典中找到对象类名称的情况下创建自定义对象。 将此函数用于 json.load()
方法中的 object_hook
参数。
# 可能但解码为字典 |
<class 'dict'> |
如果在 __init__
方法中提供了所有类变量,则此方法适用于所有自定义类。
class User: |
{ |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
import logging |
有5种不同的日志级别指示事件的严重程度。 默认情况下,系统仅记录 警告(WARNING) 级别及更高级别的事件。
import logging |
WARNING:root:This is a warning message |
使用 basicConfig(**kwargs)
,你可以自定义根记录器。 最常见的参数是 level
, format
和 filename
。查看全部可能的参数: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 |
在具有多个模块的应用程序中,最佳实践是使用 __name__
全局变量创建内部记录器。 这将使用你的模块名称创建一个记录器,并确保没有名称冲突。 日志记录模块创建记录器的层次结构,从根记录器开始,然后将新的记录器添加到该层次结构中。 如果随后将模块导入另一个模块,则可以通过记录器名称将日志消息与正确的模块关联。 请注意,更改根记录器的 basicConfig
还将影响层次结构中其他(下部)记录器的日志事件。
# helper.py |
默认情况下,除了附加到创建的记录器的任何处理程序外,所有创建的记录器还将日志事件传递给高级记录器的处理程序。 你可以通过设置 propagate = False
来禁用此功能。 有时,当你想知道为什么看不到来自另一个模块的日志消息时,则可能是此属性。
# ------------------------------------- |
处理程序对象负责将适当的日志消息调度到处理程序的特定目标。 例如,你可以使用不同的处理程序通过HTTP或通过电子邮件将消息发送到标准输出流,文件。 通常,你为每个处理程序配置一个级别( setLevel()
),一个格式化程序( setFormatter()
)和一个可选的过滤器( addFilter()
)。 有关可能的内置处理程序,请参见 https://docs.python.org/3/howto/logging.html#useful-handlers。 当然,你也可以通过派生这些类来实现自己的处理程序。
import logging |
class InfoFilter(logging.Filter): |
我们已经看到了如何配置日志,从而在代码中显式地创建日志记录器,处理程序和格式化程序。 还有其他两种配置方法:
fileConfig()
函数读取它。 请参见下面的示例。dictConfig()
函数。 有关更多信息,请参见https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig。创建一个 .conf(或有时存储为 .ini)文件,定义记录器,处理程序和格式化程序,并提供名称作为键。 定义其名称后,可以通过在其名称之间用下划线分隔之前添加单词 logger, handler 和 formatter 进行配置。 然后,你可以为每个记录器,处理程序和格式化程序设置属性。 在下面的示例中,将使用 StreamHandler 配置根记录器和名为 simpleExample 的记录器。
# logging.conf |
# 在代码中使用配置文件 |
将跟踪记录记录在异常日志中对于解决问题非常有用。 你可以通过将 excinfo 参数设置为True来捕获 logging.error()
中的回溯。
import logging |
ERROR:root:list index out of range |
如果未捕获正确的 Exception,则还可以使用 traceback.formatexc() 方法记录该异常。
当你有一个大型应用程序将许多事件记录到一个文件中,而你只需要跟踪最近的事件时,请使用RotatingFileHandler来使文件保持较小。 当日志达到一定数量的字节时,它将被“滚动”。 你还可以保留多个备份日志文件,然后再覆盖它们。
import logging |
如果你的应用程序将长时间运行,则可以使用 TimedRotatingFileHandler。 这将根据经过的时间创建一个轮换日志。 when 参数的可能时间条件是:
import logging |
如果你的应用程序从不同的模块(特别是在微服务体系结构中)生成许多日志,那么定位重要的日志以进行分析可能会很困难。 因此,最佳实践是以JSON格式记录你的消息,并将其发送到集中式日志管理系统。 然后,你可以轻松地搜索,可视化和分析日志记录。
我建议使用此开源JSON记录器:https://github.com/madzak/python-json-logger
pip install python-json-logger |
import logging |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
multiprocessing
模块。lock
来防止竞态情Queue
进行进程安全的数据/任务处理Pool
来管理多个工作进程。你可以使用 multiprocessing.Process()
创建一个进程。 它包含两个重要的参数:
target
:进程启动时要调用的可调用对象(函数)args
:目标函数的(函数)参数。 这必须是一个元组。使用 process.start()
启动一个进程
调用 process.join()
告诉程序在继续执行其余代码之前,应等待该进程完成。
from multiprocessing import Process |
由于进程不在同一个内存空间中,因此它们无法访问相同(公共)数据。 因此,它们需要特殊的共享内存对象来共享数据。
可以使用 Value
或者 Array
将数据存储在共享内存变量中。
Value(type, value)
:创建类型为 type
的 ctypes
对象。 使用 .target
访问该值。Array(type, value)
:使用类型为 type
的元素创建一个 ctypes
数组。 用 []
访问值。任务:创建两个进程,每个进程都应该有权访问一个共享变量并对其进行修改(在这种情况下,只是将其重复增加1达100次)。 创建另外两个共享一个数组的进程,然后修改(增加)该数组中的所有元素。
from multiprocessing import Process, Value, Array |
Value at beginning: 0 |
请注意,在上面的示例中,两个进程应将共享值增加1达100次。 这样一来,总共进行了200次操作。 但是为什么终值不是200?
这里发生了竞态情况。当两个或多个进程或线程可以访问共享数据并且它们试图同时更改它们时,就会发生竞态情况。在我们的示例中,两个进程必须读取共享值,将其增加1,然后将其写回到共享变量中。如果这同时发生,则两个进程将读取相同的值,将其增加并写回。因此,两个进程都将相同的增加的值写回到共享对象中,并且该值未增加2。有关竞态条件的详细说明,请参见 16. 多线程 — Python 进阶 。
锁(也称为互斥锁)是一种同步机制,用于在存在许多执行进程/线程的环境中强制限制对资源的访问。锁具有两种状态:锁定和解锁。如果状态为锁定,则在状态再次被解锁之前,不允许其他并发进程/线程进入此代码段。
两个函数很重要:
lock.acquire()
:这将锁定状态并阻塞lock.release()
:这将再次解锁状态。重要提示:块获得后,你应始终再次释放它!
在我们的示例中,读取并增加了共享变量的关键代码部分现已锁定。这样可以防止第二个进程同时修改共享库。我们的代码没有太大变化。所有新更改都在下面的代码中进行了注释。
# import Lock |
Value at beginning: 0 |
在 lock.acquire()
之后,你应该永远不要忘记调用 lock.release()
来解锁代码。 你还可以将锁用作上下文管理器,这将安全地锁定和解锁你的代码。 建议以这种方式使用锁:
def add_100(number, lock): |
数据也可以通过队列在进程之间共享。 队列可用于多线程和多进程环境中的线程安全/进程安全数据交换和数据处理,这意味着你可以避免使用任何同步原语(例如锁)。
队列队列是遵循先进先出(FIFO)原理的线性数据结构。 一个很好的例子是排队等候的客户队列,其中首先服务的是第一位的客户。
from multiprocessing import Queue |
1 |
带有队列的操作是进程安全的。 除了 task_done()
和 join()
之外,多进程队列实现了 queue.Queue
的所有方法。 重要方法是:
q.get()
:删除并返回第一项。 默认情况下,它会阻塞,直到该项可用为止。q.put(item)
:将元素放在队列的末尾。 默认情况下,它会阻塞,直到有空闲插槽可用为止。q.empty()
:如果队列为空,则返回True。q.close()
:指示当前进程不会再将更多数据放入此队列。# 使用多进程队列在进程之间进行通信 |
1 |
进程池对象控制可以向其提交作业的工作进程池。它支持带有超时和回调的异步结果,并具有并行映射实现。它可以自动管理可用的处理器,并将数据拆分为较小的块,然后由不同的进程并行处理。有关所有可能的方法,请参见 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 |
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729] |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
threading
模块。lock
)来防止竞态情况Queue
进行线程安全的数据/任务处理。你可以使用 threading.Thread()
创建一个线程。 它包含两个重要的参数:
target
:线程启动时要调用的该线程的可调用对象(函数)args
:目标函数的(函数)参数。 这必须是一个元组使用 thread.start()
启动线程
调用 thread.join()
告诉程序在继续执行其余代码之前,应等待该线程完成。
from threading import Thread |
由于线程位于相同的内存空间中,因此它们可以访问相同的(公共)数据。 因此,例如,你可以简单地使用所有线程都具有读取和写入访问权限的全局变量。
任务:创建两个线程,每个线程应访问当前数据库值,对其进行修改(在这种情况下,仅将其增加1),然后将新值写回到数据库值中。 每个线程应执行10次此操作。
from threading import Thread |
Start value: 0 |
请注意,在上面的示例中,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 |
Start value: 0 |
在 lock.acquire()
之后,你应该永远不要忘记调用 lock.release()
来解锁代码。 你还可以将锁用作上下文管理器,这将安全地锁定和解锁你的代码。 建议以这种方式使用锁:
def increase(lock): |
队列可用于多线程和多进程环境中的线程安全/进程安全的数据交换和数据处理。
队列是遵循先进先出(FIFO)原理的线性数据结构。 一个很好的例子是排队等候的客户队列,其中首先服务的是第一位的客户。
from queue import Queue |
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 |
in Thread1 got 0 |
在以上示例中,使用了守护线程。 守护线程是后台线程,它们在主程序结束时自动消失。 这就是为什么可以退出 worker 方法内的无限循环的原因。 没有守护进程,我们将不得不使用诸如 threading.Event
之类的信号机制来停止 worker。 但请注意守护进程:它们会突然停止,并且它们的资源(例如打开的文件或数据库事务)可能无法正确释放/完成。
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
进程是程序的一个实例,例如Python解释器。它们彼此独立,并且不共享相同的内存。
关键事实:
线程是可以调度执行的进程(也称为“轻量级进程”)中的实体。一个进程可以产生多个线程。主要区别在于,进程中的所有线程共享同一内存。
关键事实:
使用 threading
模块。
注意:由于受CPU限制,以下示例通常不会从多个线程中受益。 它应显示如何使用线程的示例。
from threading import Thread |
尽管使用了GIL,但在程序必须与速度较慢的设备(例如硬盘驱动器或网络连接)进行通讯时,它仍可用于 I/O 密集型任务。 通过线程化,程序可以花费时间等待这些设备并同时智能地执行其他任务。
示例:从多个站点下载网站信息。 为每个站点使用一个线程。
使用 multiprocessing
模块。 语法与上面非常相似。
from multiprocessing import Process |
这对于必须对大量数据执行大量CPU操作且需要大量计算时间的CPU密集型任务很有用。通过多进程,你可以将数据分成相等的部分,然后在不同的CPU上进行并行计算。
示例:计算从1到1000000的所有数字的平方数。将数字分成相等大小的部分,并对每个子集使用一个过程。
这是一个互斥锁(或锁),仅允许一个线程控制Python解释器。这意味着即使在多线程体系结构中,GIL一次也只允许一个线程执行。
之所以需要它,是因为CPython(Python的引用实现)的内存管理不是线程安全的。 Python使用引用计数进行内存管理。这意味着在Python中创建的对象具有引用计数变量,该变量跟踪指向该对象的引用数。当此计数达到零时,将释放对象占用的内存。问题在于该引用计数变量需要保护,以防止两个线程同时增大或减小其值的竞争条件。如果发生这种情况,则可能导致从未释放的内存泄漏,或者在仍然存在对该对象的引用的情况下错误地释放了内存。
GIL在Python社区中引起很大争议。避免GIL的主要方法是使用多线程而不是线程。另一个(但是很不舒服)的解决方案是避免CPython实现,而使用 Jython
或 IronPython
之类的自由线程Python实现。第三种选择是将应用程序的部分移到二进制扩展模块中,即使用Python作为第三方库的包装器(例如在C / C ++中)。这是 numpy
和 scipy
采取的路径。
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
with open()
语句:with open('notes.txt', 'w') as f: |
这将打开一个文件,并确保在程序执行离开with语句的上下文之后自动将其关闭。 它还处理异常,并确保即使在发生异常的情况下也能正确关闭文件。 在内部,上面的代码翻译成这样的东西:
f = open('notes.txt', 'w') |
我们可以看到,使用上下文管理器和 with
语句更短,更简洁。
from threading import Lock |
为了支持我们自己的类的 with
语句,我们必须实现 __enter__
和 __exit__
方法。 当执行进入 with
语句的上下文时,Python调用 __enter__
。 在这里,应该获取资源并将其返回。 当执行再次离开上下文时,将调用 __exit__
并释放资源。
class ManagedFile: |
init notes.txt |
如果发生异常,Python将类型,值和回溯传递给 __exit__
方法。 它可以在这里处理异常。 如果 __exit__
方法返回的不是 True
,则 with
语句将引发异常。
class ManagedFile: |
init notes.txt |
我们可以在 __exit__
方法中处理异常并返回 True
。
class ManagedFile: |
init notes2.txt |
除了编写类,我们还可以编写一个生成器函数,并使用 contextlib.contextmanager
装饰器对其进行装饰。 然后,我们也可以使用 with
语句调用该函数。 对于这种方法,函数必须在 try
语句中 yield
资源,并且释放资源的 __exit__
方法的所有内容现在都在相应的 finally
语句内。
from contextlib import contextmanager |
生成器首先获取资源。 然后,它暂时挂起其自己的执行并 产生 资源,以便调用者可以使用它。 当调用者离开 with
上下文时,生成器继续执行并释放 finally
语句中的资源。
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
obj_b = obj_a
)不会创建真实副本。 它仅使用相同的引用创建一个新变量。 因此,当你想制作可变对象(列表,字典)的实际副本并且想要在不影响原始对象的情况下修改副本时,必须格外小心。对于“真实”副本,我们可以使用 copy
模块。 但是,对于复合/嵌套对象(例如嵌套列表或字典)和自定义对象,浅拷贝和深拷贝之间存在重要区别:
这只会创建具有相同引用的新变量。 修改其中一个会影响另一个。
list_a = [1, 2, 3, 4, 5] |
[-10, 2, 3, 4, 5] |
一层深。 在级别1上进行修改不会影响其他列表。 使用 copy.copy()
或特定于对象的复制函数/复制构造函数。
import copy |
[1, 2, 3, 4, 5] |
但是对于嵌套对象,在2级或更高级别上进行修改确实会影响其他对象!
import copy |
[[-10, 2, 3, 4, 5], [6, 7, 8, 9, 10]] |
注意:你还可以使用以下内容来创建浅拷贝:
# 浅拷贝 |
完全独立的克隆。 使用 copy.deepcopy()
。
import copy |
[[-10, 2, 3, 4, 5], [6, 7, 8, 9, 10]] |
你可以使用 copy
模块来获取自定义对象的浅拷贝或深拷贝。
class Person: |
28 |
# 浅拷贝 |
27 |
现在让我们创建一个嵌套对象:
class Company: |
56 |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
*
)可用于Python中的不同情况:*args
, **kwargs
和仅关键字参数# 乘法 |
35 |
# list |
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
*args
, **kwargs
和仅关键字参数*args
**kwargs
*
,后跟更多函数参数以强制使用仅关键字的参数def my_function(*args, **kwargs): |
Hey |
*
拆成函数参数。**
拆包。def foo(a, b, c): |
1 2 3 |
将列表,元组或集合的元素拆包为单个和多个剩余元素。 请注意,即使被拆包的容器是元组或集合,也将多个元素组合在一个列表中。
numbers = (1, 2, 3, 4, 5, 6, 7, 8) |
[1, 2, 3, 4, 5, 6, 7] |
由于PEP 448(https://www.python.org/dev/peps/pep-0448/),从Python 3.5开始,这是可能的。
# 将可迭代对象合并到列表中 |
[1, 2, 3, 4, 5, 6] |
但是,请注意以下合并解决方案。 如果字典中有任何非字符串键,则它将不起作用:https://stackoverflow.com/questions/38987/how-to-merge-two-dictionaries-in-a-single-expression/39858#39858
dict_a = {'one': 1, 'two': 2} |
--------------------------------------------------------------------------- |
推荐进一步阅读:
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
*args
和 **kwargs
)def print_name(name): # name 是形参 |
我们可以将参数作为位置参数或关键字参数传递。 关键字参数的一些好处可能是:
def foo(a, b, c): |
1 2 3 |
函数可以具有带有预定义值的默认参数。 可以忽略此参数,然后将默认值传递给函数,或者可以将参数与其他值一起使用。 注意,必须将默认参数定义为函数中的最后一个参数。
# 默认参数 |
1 2 3 4 |
*args
和 **kwargs
)*
)标记参数,则可以将任意数量的位置参数传递给函数(通常称为 *args
)**
)标记参数,则可以将任意数量的关键字参数传递给该函数(通常称为 **kwargs
)。def foo(a, b, *args, **kwargs): |
1 2 |
有时你想要仅使用关键字的参数。 你可以执行以下操作:
*,
,则此后的所有参数都必须作为关键字参数传递。def foo(a, b, *, c, d): |
1 2 3 4 |
*
)拆包为参数。**
)的参数,其长度和键与函数参数匹配。def foo(a, b, c): |
4 5 6 |
可以在函数体内访问全局变量,但是要对其进行修改,我们首先必须声明 global var_name
才能更改全局变量。
def foo1(): |
number in function: 0 |
如果我们不写 global var_name
并给与全局变量同名的变量赋一个新值,这将在函数内创建一个局部变量。 全局变量保持不变。
number = 0 |
number before foo3(): 0 |
Python使用一种称为“对象调用”或“对象引用调用”的机制。必须考虑以下规则:
这意味着:
# 不可变对象 -> 不变 |
var before foo(): 10 |
# 可变对象 -> 可变 |
my_list before foo(): [1, 2, 3] |
# 不可变对象包含在可变对象内 -> 可变 |
# 重新绑定可变引用 -> 不变 |
my_list before foo(): [1, 2, 3] |
对于可变类型,请小心使用 +=
和 =
操作。 第一个操作对传递的参数有影响,而后者则没有:
# 重新绑定引用的另一个例子 |
my_list before foo(): [1, 2, 3] |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
生成器的定义类似于普通函数,但是使用 yield
语句而不是 return
。
def my_generator(): |
调用该函数不会执行它,而是函数返回一个生成器对象,该对象用于控制执行。 生成器对象在调用 next()
时执行。 首次调用 next()
时,执行从函数的开头开始,一直持续到第一个 yield
语句,在该语句中返回语句右边的值。 随后对 next()
的调用从 yield
语句继续(并循环),直到达到另一个 yield
。 如果由于条件而未调用 yield
或到达末尾,则会引发 StopIteration
异常:
def countdown(num): |
Starting |
# 你可以使用 for 循环来遍历一个生成器对象 |
Starting |
# 你可以将其用于接受可迭代对象作为输入的函数 |
Starting |
最大的优点:迭代器节省内存!
由于这些值是延迟生成的,即仅在需要时才生成,因此可以节省大量内存,尤其是在处理大数据时。 此外,我们不必等到所有元素生成后再开始使用它们。
# 如果没有生成器,则必须将完整序列存储在此处的列表中 |
499999500000 |
# 使用生成器,不需要额外的序列来存储数字 |
499999500000 |
def fibonacci(limit): |
[0, 1, 1, 2, 3, 5, 8, 13, 21] |
就像列表推导一样,生成器可以用相同的语法编写,除了用括号代替方括号。 注意不要混淆它们,因为由于函数调用的开销,生成器表达式通常比列表理解要慢(https://stackoverflow.com/questions/11964130/list-comprehension-vs-generator-expressions-weird-timeit-results/11964478#11964478)。
# 生成器表达式 |
120 bytes |
这个类将生成器实现为可迭代的对象。 它必须实现 __iter__
和 __next__
使其可迭代,跟踪当前状态(在这种情况下为当前数字),并注意 StopIteration
。 它可以用来理解生成器背后的概念。 但是,有很多样板代码,其逻辑并不像使用 yield
关键字的简单函数那样清晰。
class firstn: |
499999500000 |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
装饰器有2种:
函数用 @
符号修饰:
|
为了理解装饰器模式,我们必须了解Python中的函数是一级对象,这意味着像其他任何对象一样,它们可以在另一个函数内定义,作为参数传递给另一个函数或从其他函数返回 。 装饰器是一个将另一个函数作为参数的函数,将其行为包装在内部函数中,并返回包装的函数。 结果,修饰的函数便具有了扩展的功能!
# 装饰器是一个将另一个函数作为参数的函数,将其行为包装在内部函数中,并返回包装的函数。 |
Alex |
除了包装函数并将其分配给自身之外,我们还可以通过用 @
装饰函数来实现相同的目的。
|
Start |
如果我们的函数具有输入参数,并且我们尝试使用上面的装饰器将其包装,则它将引发 TypeError
,因为我们在包装器内调用函数时也必须使用此参数。 但是,我们可以通过在内部函数中使用 *args
和 **kwargs
来解决此问题:
def start_end_decorator_2(func): |
Start |
请注意,在上面的示例中,我们没有取回结果,因此,下一步,我们还必须从内部函数返回值:
def start_end_decorator_3(func): |
Start |
如果我们看一下装饰函数的名称,并使用内置的 help
函数对其进行检查,我们会注意到Python认为我们的函数现在是装饰器函数的包装内部函数。
print(add_5.__name__) |
wrapper |
要解决此问题,请使用 functools.wraps
装饰器,该装饰器将保留有关原始函数的信息。 这有助于进行自省,即对象在运行时了解其自身属性的能力:
import functools |
Start |
现在,我们已经有了所有部分,用于任何装饰器的模板如下所示:
import functools |
请注意, functools.wraps
是一个装饰器,它自己接受一个参数。 我们可以将其视为2个内部函数,即内部函数里的内部函数。 为了更清楚地说明这一点,我们来看另一个示例:以数字作为输入的 repeat
装饰器。 在此函数内,我们有实际的装饰函数,该函数包装函数并在另一个内部函数内扩展其行为。 在这种情况下,它将输入函数重复给定的次数。
def repeat(num_times): |
Hello Alex |
我们可以通过将多个装饰器彼此堆叠来将其应用到一个函数。 装饰器将按照其列出的顺序执行。
# 装饰器函数,它输出有关包装函数的调试信息 |
Calling say_hello(name='Alex') |
我们也可以使用一个类作为装饰器。 因此,我们必须实现 __call__()
方法以使我们的对象可调用。 类装饰器通常用于维护状态,例如: 在这里,我们跟踪函数执行的次数。 __call__
方法与我们之前看到的 wrapper()
方法本质上是相同的。 它添加了一些功能,执行了该函数,并返回其结果。 请注意,这里我们使用 functools.update_wrapper()
代替 functools.wraps
来保留有关函数的信息。
import functools |
Call 1 of 'say_hello' |
time.sleep()
降低代码速度以检查网络行为GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
当解析器检测到语法不正确的语句时发生语法错误。 语法错误可以是例如拼写错误,缺少括号,没有新行(请参见下面的代码)或错误的标识(这实际上会引发它自己的IndentationError,但它是SyntaxError的子类)。
a = 5 print(a) |
File "<ipython-input-5-fed4b61d14cd>", line 1 |
即使一条语句在语法上是正确的,执行该语句也可能导致错误,这称为 异常错误。 有几种不同的错误类别,例如,尝试对数字和字符串求和将引发 TypeError
。
a = 5 + '10' |
--------------------------------------------------------------------------- |
如果要在满足特定条件时强制发生异常,则可以使用 raise
关键字。
x = -5 |
--------------------------------------------------------------------------- |
你还可以使用 assert
语句,如果你的断言不是 True
,则将引发 AssertionError
。 这样,你可以主动测试必须满足的某些条件,而不必等待程序中途崩溃。 断言还用于单元测试。
x = -5 |
--------------------------------------------------------------------------- |
你可以使用 try
和 except
块来捕获和处理异常。 如果你可以捕获异常,则你的程序将不会终止,并且可以继续。
# 这将捕获所有可能的异常 |
Some error occured. |
else
语句如果没有发生异常,则可以使用else语句运行。
try: |
Everything is ok |
finally
语句你可以使用始终运行的 finally
语句,无论是否存在异常。 例如,这可用于进行一些清理操作。
try: |
A TypeError occured: unsupported operand type(s) for +: 'float' and 'str' |
你可以在此处找到所有内置的异常:https://docs.python.org/3/library/exceptions.html
ImportError
:如果无法导入模块NameError
:如果你尝试使用未定义的变量FileNotFoundError
:如果你尝试打开一个不存在的文件或指定了错误的路径ValueError
:当某个操作或函数收到类型正确但值不正确的参数时,例如尝试从不存在的列表中删除值TypeError
:将操作或函数应用于不适当类型的对象时引发。IndexError
:如果你尝试访问序列的无效索引,例如列表或元组。KeyError
:如果你尝试访问字典中不存在的键。# ImportError |
你可以定义自己的异常类,该异常类应从内置的 Exception
类派生。 与标准异常的命名类似,大多数异常都以“错误”结尾的名称定义。 可以像定义其他任何类一样定义异常类,但是它们通常保持简单,通常仅提供一定数量的属性,这些属性允许处理程序提取有关错误的信息。
# 自定义异常类的最小示例 |
Value is too low. The value is: 1 |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
lambda arguments: expression |
当简单函数仅在代码中使用一次或短时间时,可以使用Lambda函数。 最常见的用途是作为高阶函数(将其他函数作为参数的函数)的参数。 它们还与诸如 map()
, filter()
, reduce()
之类的内置函数一起使用。
# 一个给参数加10的lambda函数 |
15 110 |
从另一个函数返回定制的lambda函数,并根据需要创建不同的函数变体。
def myfunc(n): |
12 |
key函数会在排序之前转换每个元素。
points2D = [(1, 9), (4, 1), (5, -3), (10, 2)] |
[(5, -3), (4, 1), (10, 2), (1, 9)] |
map(func, seq)
,使用函数转换每个元素。
a = [1, 2, 3, 4, 5, 6] |
[2, 4, 6, 8, 10, 12] |
filter(func, seq)
,返回其 func
计算为 True
的所有元素。
a = [1, 2, 3, 4, 5, 6, 7, 8] |
[2, 4, 6, 8] |
reduce(func, seq)
,重复将 func
应用于元素并返回单个值。func
需要2个参数。
from functools import reduce |
24 |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
itertools
模块是用于处理迭代器的工具集合。 简而言之,迭代器是可以在for循环中使用的数据类型。 Python中最常见的迭代器是列表。有关所有可能的 itertools,请参见 https://docs.python.org/3/library/itertools.html。
该工具计算输入可迭代项的笛卡尔积。
它等效于嵌套的for循环。 例如,product(A, B)
返 回的结果与 ((x,y) for x in A for y in B)
相同。
from itertools import product |
[(1, 3), (1, 4), (2, 3), (2, 4)] |
此工具以所有可能的顺序,以可迭代的方式返回元素的连续长度排列,并且没有重复的元素。
from itertools import permutations |
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)] |
长度r的元组,按排序顺序。 因此,如果对输入的可迭代对象进行排序,则将按排序顺序生成组合元组。 combinations()
不允许重复的元素,但 combinations_with_replacement()
允许。
from itertools import combinations, combinations_with_replacement |
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] |
使迭代器返回累加的总和或其他二进制函数的累加结果。
from itertools import accumulate |
[1, 3, 6, 10] |
创建一个迭代器,从迭代器返回连续的键和组。 键是为每个元素计算键值的函数。 如果未指定或为None,则键默认为标识函数,并返回不变的元素。 通常,可迭代项需要已经在相同的键函数上进行了排序。
from itertools import groupby |
True [1, 2] |
from itertools import count, cycle, repeat |
10 |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
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。
计数器是一个将元素存储为字典键的容器,而它们的计数则存储为字典值。
from collections import Counter |
Counter({'a': 5, 'b': 4, 'c': 3, 'd': 2, 'e': 1}) |
namedtuple
是容易创建,轻量级的对象类型。 它们为元组中的每个位置分配含义,并允许使用更具可读性的带文档代码。 它们可以在使用常规元组的任何地方使用,并且它们增加了按名称而不是位置索引访问字段的能力。
from collections import namedtuple |
Point(x=1, y=-4) |
OrderedDict 就像常规dict一样,但是它们记住条目插入的顺序。 在 OrderedDict 上进行迭代时,将按照条目的键首次添加的顺序返回项。 如果新条目覆盖了现有条目,则原始插入位置将保持不变。 既然内置dict类获得了记住插入顺序的能力(自python 3.7起),它们的重要性就变得不那么重要了。 但是仍然存在一些差异,例如 OrderedDict 被设计为擅长重新排序操作。
from collections import OrderedDict |
{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5} |
defaultdict是一个与通常的dict容器相似的容器,但是唯一的区别是,如果尚未设置该键,则defaultdict将具有默认值。 如果不使用defaultdict,则你必须检查该键是否存在,如果不存在,则将其设置为所需的键。
from collections import defaultdict |
dict_items([('yellow', 1), ('blue', 2)]) |
deque是双端队列。 它可用于在两端添加或删除元素。 deque支持从队列的任一侧线程安全,内存高效地追加和弹出,在任一方向上大致相同的 O(1)
性能。 更常用的栈和队列是双端队列的退化形式,其中输入和输出限制为单端。
from collections import deque |
deque(['a', 'b']) |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
my_string = 'Hello' |
Python字符串是不可变的,这意味着它们在创建后就无法更改。
# 使用单引号后者双引号 |
I' m a 'Geek' |
my_string = "Hello World" |
H |
# 使用 + 拼接字符串 |
Hello Tom |
# 使用for循环迭代列表 |
H |
if "e" in "Hello": |
yes |
my_string = " Hello World " |
Hello World |
新样式使用 format()
方法,旧样式使用 %
操作符。
# 使用大括号做占位符 |
Hello Bob and Tom |
从 Python 3.6 起,可以直接在花括号内使用变量。
name = "Eric" |
Hello, Eric. You are 25. |
# 因为字符串不可变,所以使用 + 或者 += 拼接字符串总是生成新的字符串 |
concat string with + : 0.34527 |
# a[start:stop:step], 默认步长为 1 |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
my_set = {"apple", "banana", "cherry"} |
使用花括号或内置的 set
函数。
my_set = {"apple", "banana", "cherry"} |
{'banana', 'apple', 'cherry'} |
my_set = set() |
{True, 42, 'Hello'} |
# remove(x): 移除 x, 如果元素不存在则引发 KeyError 错误 |
{'banana', 'cherry'} |
my_set = {"apple", "banana", "cherry"} |
yes |
# 使用 for 循环迭代集合 |
banana |
odds = {1, 3, 5, 7, 9} |
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} |
setA = {1, 2, 3, 4, 5, 6, 7, 8, 9} |
{4, 5, 6, 7, 8, 9} |
setA = {1, 2, 3, 4, 5, 6, 7, 8, 9} |
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} |
set_org = {1, 2, 3, 4, 5} |
{1, 2, 3, 4, 5, 6, 7} |
setA = {1, 2, 3, 4, 5, 6} |
False |
Frozenset 只是普通集和的不变版本。 尽管可以随时修改集合的元素,但 Frozenset 的元素在创建后保持不变。 创建方式:
my_frozenset = frozenset(iterable) |
a = frozenset([0, 1, 2, 3, 4]) |
frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
:
)分隔,并且各项之间以逗号分隔。my_dict = {"name":"Max", "age":28, "city":"New York"} |
使用大括号或者内置的 dict
函数创建。
my_dict = {"name":"Max", "age":28, "city":"New York"} |
{'name': 'Max', 'age': 28, 'city': 'New York'} |
name_in_dict = my_dict["name"] |
Max |
只需添加或访问键并分配值即可。
# 添加新键 |
{'name': 'Max', 'age': 28, 'city': 'New York', 'email': 'max@xyz.com'} |
# 删除键值对 |
popped value: 28 |
检查键
my_dict = {"name":"Max", "age":28, "city":"New York"} |
Max |
# 遍历键 |
name Max |
复制索引时请注意。
dict_org = {"name":"Max", "age":28, "city":"New York"} |
{'name': 'Lisa', 'age': 28, 'city': 'New York'} |
# 使用 update() 方法合两个字典 |
{'name': 'Lisa', 'age': 27, 'email': 'max@xyz.com', 'city': 'Boston'} |
任何不可变的类型(例如字符串或数字)都可以用作键。 另外,如果元组仅包含不可变元素,则可以使用它作为键。
# 使用数字做键,但要小心 |
9 36 81 |
值也可以是容器类型(例如列表,元组,字典)。
my_dict_1 = {"name": "Max", "age": 28} |
{'dictA': {'name': 'Max', 'age': 28}, 'dictB': {'name': 'Alex', 'age': 25}} |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
my_tuple = ("Max", 28, "New York") |
用圆括号和逗号分隔的值创建元组,或使用内置的 tuple
函数。
tuple_1 = ("Max", 28, "New York") |
('Max', 28, 'New York') |
可以通过引用索引号访问元组项。 请注意,索引从0开始。
item = tuple_1[0] |
Max |
不可能,会触发 TypeError
错误。
tuple_1[2] = "Boston" |
--------------------------------------------------------------------------- |
del tuple_2 |
# 使用 for 循环迭代元组 |
Max |
if "New York" in tuple_1: |
yes |
my_tuple = ('a','p','p','l','e',) |
5 |
和字符串一样,使用冒号(:
)访问列表的子部分。
# a[start:stop:step], 默认步长为 1 |
(2, 3) |
元组解包
# 变量个数必需与元组元素个数相同 |
Max |
a = ((0, 1), ('age', 'height')) |
((0, 1), ('age', 'height')) |
元组的不可变性使Python可以进行内部优化。 因此,在处理大数据时,元组可以更高效。
# 比较大小 |
104 bytes |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
my_list = ["banana", "cherry", "apple"]
列表使用方括号创建,或者内置的 list 函数。
list_1 = ["banana", "cherry", "apple"] |
['banana', 'cherry', 'apple'] |
可以通过索引号访问列表项。 请注意,索引从0开始。
item = list_1[0] |
banana |
只需访问索引并分配一个新值即可。
# 列表创建之后可以被修改 |
['banana', 'cherry', 'lemon'] |
查看Python文档以查看所有列表方法:https://docs.python.org/3/tutorial/datastructures.html
my_list = ["banana", "cherry", "apple"] |
Length: 3 |
复制引用(references)时要小心。
list_org = ["banana", "cherry", "apple"] |
['banana', 'cherry', 'apple', True] |
# 使用for循环迭代列表 |
banana |
if "banana" in list_1: |
yes |
和字符串一样,使用冒号( :
)访问列表的子部分。
# a[start:stop:step], 默认步长为 1 |
[2, 3] |
一种从现有列表创建新列表的简便快捷方法。
列表推导方括号内包含一个表达式,后跟for语句。
a = [1, 2, 3, 4, 5, 6, 7, 8] |
[1, 4, 9, 16, 25, 36, 49, 64] |
嵌套列表
a = [[1, 2], [3, 4]] |
[[1, 2], [3, 4]] |
起因是公司的项目为了安全和执行速度,在发布时会使用 Cython 转为 C 语言并编译成动态连接库进行调用,但是有个函数在 Python 执行时正常,但是在动态连接库中却执行错误。
测试用例 test.py
:
def ip_str(ips: str): |
编译 compile.py
:
from setuptools import setup |
运行编译:
python compile.py build |
编译之后进入对应 so 文件目录 build/lib-{平台架构}
,运行对比:
>>> import test |
经过调查发现,当函数变量做了类型注释时,不能重新赋值为其他类型,否则会在 Cython 编译后执行时报错。
Cython 可以在编译时推断出部分简单的错误,比如:
def ip1(): |
Error compiling Cython file: |
但如果代码比较复杂,则只能在运行时才会出错。所以上述错误只能在执行的时候才被抛出。
Cython 将 Python 转为 C 代码比较后类型注释与否代码比较:
没有类型注释:
/* "test.py":2 |
有类型注释:
/* "test.py":2 |
主要区别在于,类型注释增加了变量检测 __Pyx_ArgTypeTest
,以及之后赋值 ips 时的类型检测 PyString_CheckExact
;没有变量类型注释则进行了变量推测,判断是否为List( PyList_CheckExact
)或者Tuple ( PyTuple_CheckExact
),还是可迭代类型。
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
项目地址: https://podcastx.qiwihui.com
目前支持的功能:
待支持的功能:
这个项目的下一个阶段的功能,是提供从文字到播客的功能,目的在于为设备或者口语能力有限的人提供制作博客的方便方法。看到很多技术达人在制作播客,作者本人也想制作一些博客,碍于口语表达。所以这个项目的目的就在于将这个过程通过比较成熟的tts来实现。
项目UI:
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
之前用过 Google Codelabs,对于它提供的指导性,动手的编码过程非常喜欢。Codelabs 用步骤性的教学方式,一步步地引导,非常适合用来书写教程。但是这种教程的编写需要两个过程:一是需要使用特定的格式书写,这个格式比 markdown 稍多一些内容;二是需要使用特定的工具 claat 进行转换。
这个项目的作用就是提供模板和自动化过程,使用 github actions 完成自动构建和部署过程,是教程书写专注于内容。你所需要做的就是在这个项目模板的基础上,在 markdowns
目录中,按不同需要增加教程即可。
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
原文《使用 Checks API 创建 CI 测试》中使用 Ruby,现使用 Python 完成文档示例。由于教程已经将大部分内容详细描述了,本文只列出与原来教程有不同的步骤,以及对应的 Python 代码。
项目地址:qiwihui/githubappcheckruns
访问 smee.io 并创建一个新的 channel,比如 https://smee.io/LgDQ8xrhy0q2GeET,然后使用 pysmee
命令运行如下命令:
安装 |
或者使用项目目录 smee 中的 node 脚本运行
npm i |
主要为以下权限:
对应于 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__) |
其中,GithubAppFlask
提供三个功能:
on
装饰器,对于不同 github 动作分发处理;使用 PyGithub 库的 create_check_run
处理
|
|
原教程使用 RuboCop 作为 ruby 代码语法检查和格式化工具,相对应,我们使用 pylint
作为 python 代码语法检查,使用 autopep8
作为格式化工具。同样,对于git项目的操作,我们使用 GitPython
简化操作。
添加要操作的 python 文件即可。
使用 GitPython 库处理,使用临时目录进行克隆。
def clone_repository(full_repo_name, repository, ref, installation_token, clean=False): |
运行 CI 测试:
# ***** RUN A CI TEST ***** |
pylint 运行并输出json结果。
command = f"pylint {repo_dir}/{repository}/**/*.py -f json" |
pylint结果与 rubocop
类似,收集并解析结果:
# lint |
整理结果,并添加修复动作:
summary = ( |
沿用 fix_rubocop_notices
这个 ID,使用 autopep8
做 python 文件的修正,将结果以 PR 的方式提交。
|
在以上步骤的基础上,可以构建更复杂的测试过程,完成不同的需求。
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
原文地址: https://github.com/jwasham/coding-interview-university基于2020年10月30日的原文版本翻译和校对
原先我为了成为一个软件工程师而建立这份简单的学习主题清单, 但这份清单随着时间而膨胀成今天这样。在做完这份清单上的每个目标后,我成为了 Amazon 的软件开发工程师! 你或许不需要像我一样学习这么多。但是,让你成为一位称职工程师所需要的知识都在这里了。
我每天自学8~12小时,这样持续了好几个月。这是我的故事:为什么我为了 Google 面试而自学了8个月。
在这份清单内的主题会让你拥有足够的知识去面对几乎每家软件公司的技术面试,包括科技巨头:Amazon、Facebook、Google,以及 Microsoft。
祝你好运!
这是我为了从 web 开发者(自学、非计算机科学学位)蜕变至 Google 软件工程师所制定的计划,其内容历时数月。
这份清单适用于 新手软件工程师,或者想从软件/网站开发转向软件工程(需要计算机科学知识)的人员。如果你有多年的经验,并且声称拥有多年的软件工程经验,并且期待一次更艰难的面试。
如果你具有多年的软件/网页开发经验,请注意,大型软件公司(例如 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、C++ 和 Python,因此在下面你会看到部分关于它们的学习资料。相关书籍请看文章的底部。
为了节省你的时间,以下是比我使用过的更缩减的书单。
选择以下之一:
你需要选择面试语言(请参见上文)。
这是我按语言给出的建议。我没有所有语言的资源,欢迎贡献。
如果你通读其中之一,你应该具备了开始解决编程问题所需的所有数据结构和算法知识。除非你需要复习,否则你可以跳过此项目中的所有视频讲座。
我没有读过这两本书,但是它们颇受好评,作者是 Sedgewick,他非常厉害。
如果你有更好的 C++ 书籍,请告诉我。我正在搜集全面的资源。
或者:
该列表已经持续更新了很长的一段时间,所以,我们的确很容易会对其失去控制。
这里列出了一些我所犯过的错误,希望你不要重滔覆辙。
就算我观看了数小时的视频,并记录了大量的笔记,几个月后的我,仍然会忘却其中大部分的东西。所以,我花了3天翻阅我的笔记,并制作成抽认卡(flashcard)帮助我复习:
请阅读以下的文章以免重蹈覆辙:
有人推荐给我的课程(但我还沒看过):学习如何学习。
为了解决善忘的问题,我制作了一个抽认卡的网页,用于添加两种抽认卡:一般的及带有代码的。每种卡都会有不同的格式设计。
而且,我还以移动设备为先去设计这些网页,以使得在任何地方,我都能通过我的手机及平板去回顾知识。
你也可以免费制作属于你自己的抽认卡网站:
有一点需要记住的是,我做事有点过头,以至于卡片都覆盖到所有的东西上,从汇编语言和 Python 的细枝末节,到机器学习和统计都被覆盖到卡片上。而这种做法,对于要求来说是多余的。
在抽认卡上做笔记: 若你第一次发现你知道问题的答案时,先不要急着把其标注成“已知”。反复复习这张抽认卡,直到每次都能答对后才是真正学会了这个问题。反复地问答可帮助你深刻记住该知识点。
这里有个替代我抽认卡的网站 Anki,很多人向我推荐过它。这个网站用同一个字卡重复出现的方式让你牢牢地记住知识。这个网站非常容易使用,支持多平台,并且有云端同步功能。在 iOS 平台上收费25美金,其他平台免费。
这是我用 Anki 这个网站里的格式所储存的抽认卡资料库: ankiweb.net/shared/info/25173560 (感谢 @xiewenya)
我留有一组 ASCII 码表、OSI 堆栈、Big-O 记号及更多的抽认卡,以便在空余的时候可以学习。
编程累了就休息半个小时,并去复习你的抽认卡。
在学习的过程中,往往会有许多令人分心的事占据着我们宝贵的时间。因此,专注和集中注意力是非常困难的。放点纯音乐能帮上一些忙。
有一些熟悉且普遍的技术在此未被谈及到:
部分问题可能会花费一天的时间去学习,而有些则会花费多天。当然,有些学习并不需要我们懂得如何实现。
因此,每一天我都会在下面所列出的列表中选择一项,并观看相关的视频。然后,使用以下的一种语言去实现:
你不需要学会所有的编程语言,你只需要专注在一种编程语言上。
为何要在这些语言上分别实现一次?
就算我没有时间去每一项都这么做,但我也会尽我所能。
在这里你可以查看到我的代码:
你不需要记住每一个算法的内部原理。
在一个白板上写代码,而不要直接在计算机上编写。在测试完部分简单的输入后,到计算机上再测试一遍。
[ ] 学习C语言
[ ] 计算机是如何处理一段程序:
[ ] 视频:
[ ] 在线课程:
[ ] 使用线性探测的数组去实现
[ ] 笔记:
关于堆排序,请查看前文堆的数据结构部分。堆排序很强大,不过是非稳定排序。
[ ] 冒泡排序(视频)
[ ] 冒泡排序分析(视频)
[ ] 插入排序 & 归并排序(视频)
[ ] 插入排序(视频)
[ ] 归并排序(视频)
[ ] 快排(视频)
[ ] 选择排序(视频)
[ ] 归并排序代码:
[ ] 快速排序代码:
[ ] 实现:
[ ] 有兴趣的话,还有一些补充,但并不是必须的:
总结一下,这是15种排序算法的可视化表示。如果你需要有关此主题的更多详细信息,请参阅“一些主题的额外内容”中的“排序”部分。
图论能解决计算机科学里的很多问题,所以这一节会比较长,像树和排序的部分一样。
笔记:
MIT(视频):
[ ] Skiena 教授的课程 - 很不错的介绍:
[ ] 图 (复习和其他):
完整的 Coursera 课程:
我会实现:
可以从 Skiena 的书(参考下面的书推荐小节)和面试书籍中学习更多关于图的实践。
如果你需要有关此主题的更多详细信息,请参阅“一些主题的额外内容”中的“字符串匹配”部分。
如果你已经拥有了4年以上的编程经验,那你可以来看看有关系统设计的问题
这一部分有一些短视频,你可以快速的观看和复习大多数重要概念。这对经常性的巩固很有帮助。
现在你已经了解了上面所有的计算机科学主题,是时候练习回答编程问题了。
编程问题的实践并不是要记住编程问题的答案。
为什么需要练习编程问题:
这里有个很棒的入门教学,内容是如何在面试中有条不紊,并且有互动沟通地解决问题。这种能力可以从面试书籍中获得,但我觉得这个也很棒:算法设计画布。
家里没有白板?那讲得通。我是一个怪人,有一个很大的白板。从白板商店买了一个大的绘图板,而不是白板。你可以坐在沙发上练习。这是我的“沙发白板”。我在照片中添加了笔以便进行缩放。如果你使用笔,则希望可以擦除。快速变得凌乱。我用铅笔和橡皮擦。
补充:
阅读并练习编程问题(按此顺序):
请参阅上方的书单。
一旦你学会了理论基础,就应该把它们拿出来练练。尽量坚持每天做编码练习,越多越好。
编码面试问题视频:
编码练习平台:
语言学习网站,附带编码挑战:
编码挑战项目:
模拟面试:
随着下面列举的问题思考下你可能会遇到的 20 个面试问题,每个问题准备 2-3 种回答。准备点故事,不要只是摆一些你完成的事情的数据,相信我,人人都喜欢听故事。
我会问的一些:(可能我已经知道了答案但我想听听面试官的看法或者了解团队的前景):
恭喜你!
继续学习。
活到老,学到老。
**********************************************************************************************************************************************************************************************************下面的内容都是可选的。通过学习这些内容,你将会得到更多的有关 CS 的概念,并将为所有的软件工程工作做更好的准备。你将会成为一个更全面的软件工程师。**********************************************************************************************************************************************************************************************************
你可以从以下的书单挑选你有兴趣的主题来研读。
我把它们加进来是为了让你成为更全方位的软件工程师,并且留意一些技术以及算法,让你拥有更大的工具箱。
掌握至少一种平衡查找树(并懂得如何实现):
“在各种平衡查找树当中,AVL 树和2-3树已经成为了过去,而红黑树(red-black trees)看似变得越来越受人青睐。这种令人特别感兴趣的数据结构,亦称伸展树(splay tree)。它可以自我管理,且会使用轮换来移除任何访问过根节点的键。” —— Skiena
因此,在各种各样的平衡查找树当中,我选择了伸展树来实现。虽然,通过我的阅读,我发现在面试中并不会被要求实现一棵平衡查找树。但是,为了胜人一筹,我们还是应该看看如何去实现。在阅读了大量关于红黑树的代码后,我才发现伸展树的实现确实会使得各方面更为高效。
我希望能阅读到更多关于 B 树的资料,因为它也被广泛地应用到大型的数据集当中。
AVL 树
伸展树
红黑树
2-3查找树
2-3-4树 (亦称2-4树)
N 叉树(K 叉树、M 叉树)
B 树
–
我为前面提到的某些主题增加了一些额外的内容,之所以没有直接添加到前面,是因为这样很容易导致某个主题内容过多。毕竟你想在本世纪找到一份工作,对吧?
SOLID
Union-Find
动态规划的更多内容 (视频)
图形处理进阶 (视频)
MIT 概率论 (过于数学,进度缓慢,但这对于数学的东西却是必要之恶) (视频):
字符串匹配
排序
斯坦福大学关于排序算法的视频:
Shai Simonson 视频,Aduni.org:
Steven Skiena 关于排序的视频:
坐下来享受一下吧。“netflix 和技能” :P
CSE373 - 算法分析 (25 个视频)
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
在测试前,先简单重构,方便构建测试。
routes.rs
use crate::handlers; |
并将原来的 hello
视图移至 handlers.rs
中,此时 main.rs
中路由修改为如下:
+mod routes; |
init_pool
方法首先我们添加一个配置错误处理:
errors.rs
:
@@ -10,6 +12,7 @@ pub enum Error { |
在 config.rs
中增加 init_pool
方法:
use crate::errors::Error; |
首先,增加运行时环境包:
# ... |
创建 tests
目录,并添加如下文件,
src/tests/ |
在 main.rs 中增加 tests
模块:
|
其中,handlers.rs
用于集成测试,helpers.rs
提供基本的测试方法。
mod.rs
:
mod handlers; |
我们先测试一下 /
路由下 hello world
的功能。
helpers.rs
中增加基本的 get
测试方法:
use crate::routes::routes; |
handlers.rs
:
use crate::tests::helpers::assert_get; |
运行测试:
cargo test |
可以看到这个测试成功了。
POST
接口我们增加 lazy_static
和 serde_json
库,前者用于延后执行,后者用于方便处理 json 数据。
# ... |
在集成测试中,我们将使用数据库链接进行测试,首先 helpers.rs
中增加 AppState
用于测试:
lazy_static! { |
以及对应的 post 测试断言:
// 其中 `AppState` 需要增加 `Clone` 宏 |
然后,我们增加一个创建 todo_list 的测试,包含创建并检测是否存在:
tests/handlers.rs
:
|
其中 CreateTodoList
需要增加 Clone
宏,才能在传入 params
参数时正常使用。
运行测试,查看结果
cargo test |
GET
接口最后添加 GET
集成测试:
use crate::db::create_todo; |
运行结果:
$ cargo test |
GET
和 POST
测试。GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
diesel 是一个用 rust 写的 ORM 库,支持多种数据库,同时 diesel 提供了对数据库结构的管理功能。我们将使用 diesel 对我们的数据库结构变化进行管理。
首先,安装命令行工具 diesel_cli
,并初始化数据库设置
安装 diesel_cli,支持 postgres |
在生成的 migrations
目录中,填入数据库变化的 sql 语句,up.sql
用于修改,down.sql
用于撤销修改。
up.sql
:
create table todo_list ( |
down.sql
:
drop table if exists todo_item; |
由于之前已经有对应的数据表结构,需要将原来的表结构删除,再运行数据库变更:
删除原有的数据表之后 |
其中,对应生成的 schema 为:
table! { |
此时,数据库中的表机构就和我们之前是一样的,同时增加了一个用于记录已经做过的 migrations 的数据库。
鉴于 diesel 没有 async 版本,以及 quaint 不是 type-safe,不做 ORM 的支持。
自定义错误,并将常见的错误统一处理。
新增 errors.rs
:
use actix_web::http::StatusCode; |
修改 db.rs
:
+use crate::errors::Error; |
修改 handlers.rs
,其中一个请求处理
+use crate::errors::Error; |
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
添加根据 id 获取数据操作:
use std::io::{Error, ErrorKind}; |
设置请求参数,设置使用 Info
作为请求路径参数序列化结构体:
use crate::db::{get_todo, get_todos}; |
添加路由:
use handlers::{todo, todos}; |
运行并获取结果:
curl 127.0.0.1:8000/todos/1 |
为了方便查看操作过程,可以增加日志记录,使用 env_logger
方便从环境变量中设置日志记录级别,log
用于记录不同级别日志,比如 info
,debug
。
Cargo.toml
:
[dependencies] |
在 .env
中可以手动设置日志记录级别,比如
RUST_LOG=info |
main.rs
:
use actix_web::{get, middleware, web, App, HttpServer, Responder}; |
运行以查看日志
cargo run |
首先,我们增加插入数据操作,sql 语句中的 returning id, title
用于返回插入成功的数据
db.rs
:
pub async fn create_todo(client: &Client, title: String) -> Result<TodoList, Error> { |
增加请求处理,CreateTodoList
用于序列化请求的数据:
handles.rs
|
其中,state: web::Data<AppState>
将 原来的 pool 做了简单的封装,好处在于可以传入多个数据作为 web::Data
。
// .. 省略 |
同时添加路由:
App::new() |
完成后运行 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" |
继续添加其他操作,如获取创建单个项,以及完成项目(check_todo
)
pub async fn get_items(client: &Client, list_id: i32) -> Result<Vec<TodoItem>, Error> { |
以及对应的路由和请求处理:
.route("/todos/{list_id}/items{_:/?}", web::get().to(handlers::items)) |
|
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
actix 是 Rust 生态中的 Actor 系统。actix-web 是在 actix actor 框架和 Tokio 异步 IO 系统之上构建的高级 Web 框架。
本篇博客实践使用 actix-web 实现一个简单的 todo 应用。基本要求:了解 rust 基本语法,了解一定的 sql 和 docker 知识。
首先,新建一个 todo-list
项目,并在其中增加 actix-web
依赖,我们使用最新的 actix 3.0。
cargo new todo-list |
Cargo.toml
:
[package] |
在 main.rs
中,使用类似于 python flask 的语法,增加一个最简单的 service。
use actix_web::{get, App, HttpServer, Responder}; |
运行并测试:
cargo run |
在另一个终端中
curl 127.0.0.1:8000 |
项目中将使用 postgres 作为数据库存储,为了方便操作和管理,我们使用 docker-compose 进行管理。
docker-compose.yml
version: "3" |
创建数据库:
docker-compose up -d |
然后,我们设计整体数据库表结构,并创建一些基础数据作为测试。表结构如下:
|
在 database.sql
中手动创建表结构并插入数据:
drop table if exists todo_list; |
创建数据表并查看结果
psql -h 127.0.0.1 -p 5432 -U actix actix < database.sql |
psql -h 127.0.0.1 -p 5432 -U actix actix |
首先,添加我们需要的库,其中 serde
用于序列化,tokio-postgres
是一直支持异步的 PostgreSQL 客户端,deadpool-postgres
用于连接池的管理。
[dependencies] |
增加 models.rs
用于管理数据模型,并支持序列化和反序列化。
use serde::{Deserialize, Serialize}; |
增加 db.rs
用于管理数据操作,例如 get_todos
从数据库中获取数据并序列化为 TodoList
的数组:
use crate::models::{TodoItem, TodoList}; |
增加 handlers.rs
用于处理服务:
use crate::db::get_todos; |
最后,在 main.rs
中创建连接池并添加路由:
mod db; |
运行并测试:
cargo run |
添加 .env
配置数据库连接信息和服务端口:
SERVER.HOST=127.0.0.1 |
同时,通过环境变量获取对应配置。首先增加 dotenv
和 config
依赖:
[dependencies] |
然后增加 config.rs
,增加从环境变量中获取配置并生成连接池方法 from_env
:
use config::{self, ConfigError}; |
在 main.rs
中使用环境变量创建连接池:
mod config; |
db.rs
中 row_to_todo
函数太麻烦,使用 tokio_pg_mapper
做处理,简化操作:[dependencies] |
在 models.rs
中添加 PostgresMapper
,
use serde::{Deserialize, Serialize}; |
使用 from_row_ref
方法将记录进行转换:
|
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
from: qiwihui on: 10/20/2020
cool
]]>之前部署了 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 |
为了方便部署,使用 docker-compose 完成整个部署过程,项目地址 qiwihui/ssv2ray。
Nginx 关键配置:
|
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
你通常需要运行一些与其他服务(通常是数据库)进行通信的集成测试。你可以通过编写 docker run
命令来拉下容器,启动容器并映射必要的端口,从而编写脚本,但这在最佳情况下很烦人。而且,如果你要在容器中进行构建,则自己运行docker会变得非常棘手。
使用容器服务可以使GitHub Actions基础架构为你执行。你只需指定容器和要映射的任何端口,它将在作业开始时启动服务容器,并使该容器可用于作业中的步骤。
services: |
这将启动 redis:latest
容器并将容器中的端口6379映射到虚拟机运行程序上的端口。这等同于运行 docker run redis:latest -p 6379/tcp
,就像你要运行该命令一样,映射到本地运行程序上的端口不是确定性的。GitHub Actions可在job.services上下文中提供此信息。
你可以查看 $
以标识本地端口号。(就像运行 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
例如:你可能有一个项目,该项目在多个平台上创建二进制文件,将这些二进制文件作为文件上载,然后发布到最后运行作业以将这些不同的二进制文件聚合到一个程序包中。
或者,你可能想散开──拥有一个创建单个文件的作业,然后在不同平台上运行多个作业以测试该文件。
在这里,我有一个测试我的本机代码的工作流程:首先,我构建本机代码测试运行器,该运行器使用 clar 单元测试框架,以便它编译一个以 testapp
命名的包含我所有单元测试的二进制文件。该二进制文件作为名为的文件上传 tests
。然后,我将创建一个依赖于第一个build
作业的矩阵作业。它将使用最新版本的Ubuntu,Debian,CentOS和Alpine建立一个在容器内执行的矩阵。每个作业将下载 tests
构建作业中生成的文件,然后将设置 testapp 为可执行文件(因为文件不保留Unix权限),最后运行测试应用程序。
当我运行它时,构建将产生一个文件,并且当该构建完成时,我的测试作业将全部开始,下载该文件,然后运行它。
你可以看到,上传文件对于生成构建输出非常有用,你可以在后续构建步骤中下载和使用这些输出。
原文链接:https://www.edwardthomson.com/blog/github_actions_19_downloading_artifacts.html
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
要将文件作为构建的一部分进行上传,可以使用 upload-artifact
操作。你可以指定为其创建文件的路径–你可以指定单个文件或文件夹,以及文件的名称。你指定的路径将以你指定的工件名称存档到一个zip文件中。
例如,我可以构建和测试我的项目,然后创建一个nuget包,最后将该nuget包作为文件上传。
现在,当我的工作流程运行时,我将在该运行的右上角获得一个选项,向我展示我的文件并让我下载它们。
将构建输出作为文件上载可以与包仓库一起使用:我喜欢将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
但是有时你希望能够设置依赖于其他作业的作业。例如,你可能有一些要测试的服务。但是为了节省成本,你只想在实际运行测试时运行那些服务。因此,你可能想要一个启动服务的作业,一个运行测试的工作业,然后是一个停止服务的作业。
要指定作业之间的依赖关系,可以使用 needs
关键字指示哪些作业依赖于其他作业的完成。
现在,这似乎不是一个很好的例子–我们可能不使用单独的作业,而可能只在一个作业中完成了这三个步骤。但是使用作业可以使我们“成长”:实际上,我们可以在一个作业中设置测试基础结构,然后并行运行多个作业以对其进行测试,然后最后运行清理作业。
这样一来,我们就可以在多个平台上并行运行测试作业,并通过设置将这些作业预定下来,然后停止作业。我们可以通过定义我们的安装作业,然后定义依赖于它的许多作业,然后依赖于这些作业的最终的工作。这通常称为“扇出”和“扇入”。
通过此工作流程,我们的设置作业将运行,然后将使用矩阵在Windows,macOS和Linux上运行构建和测试作业,最后,我们将关闭启动的那些测试资源。
你可以通过相互指定作业来轻松地构建高级工作流, needs
以指定工作流的依赖关系图。
原文连接:https://www.edwardthomson.com/blog/github_actions_17_dependent_jobs.html
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
但是你不必将自己局限于仅在你的步骤中使用 env
上下文。你还可以在工作流本身中使用 env
上下文,并根据在先前步骤中设置的数据来设置条件。
例如,你可能有一个每天要运行的工作流,并且你希望对该工作流在星期一的运行方式进行较小的修改。你可以使用 schedule
触发器每天运行工作流程。你可以复制该工作流程,并添加只希望在星期一运行的特殊更改。但是,呵呵,维持两个相似但只有一点点不同的工作流程是一个严重的难题。
取而代之的是,你可以查看星期几并在此基础上设置一个环境变量──在这里,我将使用bash语法运行 date
命令以打印缩写的星期几,并将其放入我的 echo
语句中,将 DAY_OF_WEEK
在我们的 env
上下文中设置变量 。然后,我将其 env.DAY_OF_WEEK
作为后续步骤的条件。
使用此配置,我将每天在世界标准时间05:00运行工作流。与今天一样,在星期一,将运行仅星期一的步骤。
但是在本周的剩余时间里,该步骤将被跳过。
这是另一个很好的例子,说明GitHub Actions如何为你提供简单的原语,你可以将它们组合在一起以创建功能强大的工作流。
原文链接:https://www.edwardthomson.com/blog/github_actions_16_conditionals_with_shared_data.html
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
run
,仅运行你提供的脚本的步骤。但是通常你希望与之前执行的步骤进行交互,例如,你可能希望运行一个步骤来更新软件的版本号,以使其准备好发布。然后,你可能需要在实际的发布步骤中使用该版本号。
但是,如何来回获取这些数据?GitHub Actions在其自己的流程中运行你的每个步骤。这意味着你不能只在一个步骤中设置环境变量,然后在另一步骤中引用它。换句话说,这将无法正常工作:
steps: |
但是,GitHub Actions 确实为你提供了将数据持久保存在执行环境中的工具。你可以通过写入标准输出(即,仅使用echo)来向GitHub Actions编写命令──包括指示 GitHub Actions 在后续运行步骤中设置环境变量的命令。
在当前shell中设置环境变量之后,可以对GitHub Actions 使用命令 set-env
,这将是环境变量被注入到以后的步骤中:
steps: |
现在,实际上可以在后续步骤中获取环境变量 FOO
中的数据。
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
这两个功能很自然地结合在一起──当你跨不同的操作系统,平台或语言版本构建矩阵时,可能只需要在该矩阵的一个子集上运行一些步骤。例如:在Linux上运行时,可能需要安装其他编译器,或者对于不同的操作系统,可能需要安装稍有不同的依赖项。
我可以结合一些以前的概念来为我的一个项目(C语言中的系统库)构建工作流。它将使用跨平台和工具安装的矩阵工作流来执行CI的构建和测试步骤。
目标是安装Ninja构建系统,然后使用CMake创建构建脚本以利用这一优势──CMake和Ninja可以很好地协同工作,以生成快速,跨平台的本机构建。最后,我们将使用 cmake
进行构建,并使用 ctest
进行测试。
运行此命令时,条件将确保仅对特定平台运行适当的“安装依赖项”步骤。其他平台的其他步骤将被跳过。
现在,我们开始了解如何将GitHub Actions的简单片段组合到更复杂,功能更强大的工作流程中。
原文链接:https://www.edwardthomson.com/blog/github_actions_14_conditionals_with_a_matrix.html
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
利用这些数据的一种有用方法是有条件地使用它来运行工作流步骤。
例如,你可能想在执行步骤之前检查工作流在其中运行的存储库的名称。如果你正在开发一个开源项目,这将很有帮助──因为fork你的存储库的人拥有具有不同权限的令牌,因此你可以跳过fork的发布步骤。
这使fork的存储库仍可以执行连续的集成构建,并确保在运行构建和测试通过时工作流成功,并且不会由于发布步骤上的权限问题而失败。
你可以设置一个条件,以确保你位于正确的存储库上并在CI构建中运行(来自push事件)。
现在,当此工作流在fork中运行时,将跳过“发布文档”步骤。
使用条件语句使你可以构建可在分支或分支之间共享的高级工作流,但其中某些步骤是针对特定触发器或环境量身定制的。
原文链接:https://www.edwardthomson.com/blog/github_actions_13_conditionals.html
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
GITHUB_TOKEN
。但这还不是全部。GitHub Actions还为你提供什么其他信息?其实很多!
GitHub Actions 设置了许多信息“上下文”,其中包含有关你的工作流程运行的数据。例如,github 上下文包含信息,例如你的工作流在其中运行的存储库的名称 github.repository
,启动工作流的用户 github.actor
。你可以使用与 处理矩阵 和 密码 相同的双弯括号扩展语法来引用它们。
如果你想在上下文中查看GitHub Actions提供的所有信息,则可以实际使用方便的 toJson
函数来输出整个对象:
如果这样做,你会注意到GitHub上下文中有很多信息。特别是,github.event
对象本身就是一块巨大的json数据。它基本上包含与触发器相对应的 Webhook 信息。
相同的事件信息已保存到磁盘上的 github.event_path
。因此,你可以通过检查json blob来获取工作流程中的所有信息。幸运的是,非常方便的 jq 工具已安装在 runner 上。你可以使用它在命令行上分解json数据。
例如,如果我想获取存储库中的星标数量和fork数量,则可以 jq
用来解压缩保存在的json数据 github.event_path
。
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支持将这些作为密码保存在存储库中。
要设置密码,请转到“存储库设置”页面,然后选择“密码”。你的密码名称将在你的工作流中用于引用数据,你可以将密码本身放入值中。
要使用该密码,你可以在工作流中使用上下文 secrets
来引用它。如果你有一个密码的名字 SECRET_KEY
,你可以将其称为 $
。
GITHUB_TOKEN
GitHub Actions会为每次运行的工作流自动在存储库中设置一个密码 GITHUB_TOKEN
。该令牌使你可以与存储库进行交互,而无需自己创建新令牌或设置密码。
该令牌为你提供了对存储库本身,issue和GitHub Packages进行读写的有限访问权限。但是它不能完全访问所有内容──你无法与组织中的其他存储库一起使用,也无法发布到GitHub Pages──因此,对于某些工作流,你可能仍需要设置令牌。
GitHub试图防止你的密码被窥视。在输出日志中,你定义的所有密码都会被清除,并在输出日志之前用星号替换。
这有助于保护你的密码,防止他人窥视,尤其是利用那些导出值的工具。但这当然不是完美的,你应该谨慎保护密码。
如果你的项目使用fork来接受来自贡献者的pull request(例如,如果你正在开发一个开源项目),则可能对在工作流程中使用密码有所警惕。
GitHub明确 禁用 了对来自fork的工作流提供密码的功能。这意味着,当用户从fork打开对你的项目的pull request时,不会向此工作流提供任何密码。
这有助于防止用户修改工作流程本身──或工作流程调用的任何脚本──试图获取你的密码副本。这些密码根本无法获得。
(GITHUB_TOKEN
仍然为fork提供了特殊功能,以便它们可以克隆你的存储库(以便构建它),但已将其降级为只读令牌,以防止fork工作流在你的存储库中进行更改。)
原文链接:https://www.edwardthomson.com/blog/github_actions_11_secrets.html
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
from: Volcano-Yang on: 4/28/2021
from: Volcano-Yang on: 4/28/2021
现在还支持设置环境secrets 限制不同分支对secret的读取权限
]]>push
或 pull_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 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
在服务行业工作的任何人都会理解此问题。这就像一个厨师的 场面调度连接 的地方──在他们与他们的配料厨房的设置。
如果让你的现场发生故障,变脏和混乱,你会很快发现自己旋转到位并需要备份。我和一位厨师一起工作,他曾经在匆忙中走到排队的肮脏厨师的工作台旁,解释为什么违规的厨师落后了。他将手掌压在切菜板上,切菜板上撒满了胡椒粒,飞溅的酱汁,一些香菜,面包屑以及通常会漂浮在工作台上的漂浮物和抛弃物,如果不时常用潮湿的侧毛巾将其擦掉。“你看到了吗?” 他打了个招呼,抬起他的手掌,这样厨师就可以看到灰尘和碎屑粘在厨师的手掌上。“那就是你现在的脑袋。”
Anthony Bourdain,厨房机密
当GitHub着手创建Actions平台时,他们希望构建一些对CI/CD工作流程非常有用的东西──构建项目,运行测试并部署它──但这也可以帮助你自动化项目中的常见任务。在这种情况下,请保持存储库的美观和整洁。
启动程序工作流程的底部是关闭陈旧issue和 pull request 的工作流程。
它会按计划触发运行,因此在每天UTC午夜:
on: |
当它运行时,它将运行过时的操作,该操作将查看存储库中的issue和pull request,并找到几个月没有执行任何操作的请求。然后它将在问题中发布一条消息,并添加一个标签,指示该问题是过时的。如果该问题再保持一周的陈旧状态,则将其关闭。
这样可以确保识别出每一个过时的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首先要考虑的是存储库中的代码类型。GitHub Actions使用成熟的语言工具来了解你的存储库包含哪种代码。这是为GitHub许多其他部分提供支持的工具,其中包括存储库主页上的语言统计栏。
对于这个拥有大量JavaScript的存储库,GitHub Actions将选择两个可能的工作流程──运行 npm run build
和 npm test
的Node.js CI/CD工作流程(这对应用程序有用),以及执行相同构建和测试运行的打包工作流程,然后将程序包发布到GitHub Packages中。
GitHub Actions不仅具有构建和测试项目的能力,还有工作流可以帮助你开始将应用程序部署到云中,无论是AWS,Azure还是Google Cloud。
而且,当然,尽管和语言学家一样好,它也不是完美的。许多人在同一存储库中混合了不同的项目,因此你还可以扩展整个启动程序工作流列表。
如果你想帮助改善入门工作流程──无论是对现有工作流程进行更改,还是添加全新的语言,都可以在GitHub上进行提交。
原文链接:https://www.edwardthomson.com/blog/github_actions_7_starter_workflows.html
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
但是,如果你开始设置第一个矩阵工作流程,那么你需要注意:默认情况下,矩阵工作流程会快速失败。这就是说:如果矩阵扩展中的一个作业失败,则其余作业将被取消。
这种行为通常非常有益:如果你正在运行 pull request 验证构建,而矩阵中的构建之一失败,则你可能根本不在乎其余的构建是否成功。任何失败都足以表明存在使你无法合并 PR 的问题。
但是,当你从头开始创建工作流时,可能需要迭代一下才能使其第一次正常工作。当作业失败是由于工作流设置中存在问题而不是代码本身存在问题时,关闭快速故障行为作为调试工具会很有帮助。
假设你有一个在 Linux 上运行良好的工作流程,并且希望使用矩阵将其扩展到可以在 macOS 和 Windows 上运行。对于简单的工作流程,这可能会正常工作。但是对于更复杂的事情,你可能需要先设置一些依赖项或安装一些工具,然后才能起作用。因此,很可能你的Linux上运行的工作流如果不做一些修改就无法在 macOS 或 Windows 上运行。
那么,当你第一次运行此新矩阵工作流时会发生什么?你的 Linux,macOS 和 Windows 作业将全部启动,并且 macOS 作业或 Windows 作业将失败,其余工作流程将被取消。
想象一下,首先失败的是 Windows 作业。你会看到的:
好的,因此你决定需要修复 Windows 工作流程。 因此,你可以查看出了什么问题,更新工作流程,然后推送更改以将新构建放入队列。 但是,由于排队和调度不是很确定,因此也许这次 macOS 构建首先完成──失败。 现在,你的 Windows 运行被取消,甚至无法找出它是否有效:
现在,在调试工作流时,可以通过设置 fail-fast: false
来关闭此行为:
strategy: |
现在,工作流不会在第一个失败的作业时被取消。它将允许 Windows 和 macOS 作业运行完成。
关闭 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
通过创建一个包含所有需要的开发工具以及项目依赖项的容器,你不必操心在工作流运行开始时就对那些设置和安装步骤进行管理。
此外,你还将获得基于容器的开发的优势:你可以在用于CI构建的同一个容器中进行本地构建,因此你可以高度自信地确保 GitHub Actions 中的构建与构建时所看到的与本地匹配。
语法非常简单明了──我不需要自己运行任何 docker pull
或 docker 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
但是这些环境上实际安装了什么? 原来有很多安装。
团队试图通过许多不同的平台使我们的运行器(runners)保持最新状态。 因此,你会发现许多不同版本的Python,Ruby,.NET Core等。 但是──仅仅依靠这些各种各样的开发工具──他们不可能绝对安装所有东西。
有时你需要自己安装。而且由于你拥有一台完整的虚拟机,因此对于每项作业执行,你都可以在其上安装任何所需的软件。
例如,你可能要安装非常好的“ninja”构建工具。
Linux虚拟环境运行Ubuntu,因此你可以使用 apt 安装可能需要的任何其他工具。 默认情况下,你以非 root 用户身份运行,但是可以使用无密码 sudo。这样你就可以:
run: sudo apt-get install ninja-build |
Chocolatey 是 Windows 的首选软件包管理器,它已安装并可以在 GitHub Actions 虚拟环境中使用。
run: choco install ninja |
在 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
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
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 变量的当前值。
以下是一个示例工作流,该工作流安装每个依赖项,并运行我的 autoconf 设置,然后运行 make:
当你运行此工作流程时,你可以快速查看它如何扩展到12个不同的作业。在工作流运行的左侧,你可以看到它们中的每一个。 这样,简单的工作流程就可以迅速扩展。
在其中一个运行中打开步骤时,你可以看到确实我们能够安装依赖项。 如果打开 build (clang, openssl, libkrb5)
任务,实际上正在运行 clang
(由 ${CC} --version
显示),libcurl的OpenSSL版本(由 curl-config
显示)和 MIT krb5(由 krb5-config
显示)。
因此,你可以看到,你只需使用工作流中的几行矩阵定义就可以构建具有多种配置的强大工作流。
原文链接:https://www.edwardthomson.com/blog/github_actions_2_matrixes.html
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
我将在这个月更多地讨论那些项目库自动化方案,但是你要知道,这种灵活性将有助于理解如何进行 CI/CD 构建设置。 GitHub Actions 允许你定义一个 触发器 来控制工作流程的运行时间。每当你的项目库中发生与该触发器匹配的操作时,工作流运行都会进入排队队列中准备。
对于 CI/CD 工作流,我喜欢使用 push 和 pull_request 触发器,并将其范围限定在我感兴趣的分支上。例如:
on: |
这个触发器将在对master分支进行任何更改时运行你的工作流──(即使它的名字是 push
触发器,也将在你运行 git push
或将 pull request 合并到 master 分支时运行)。对于针对master分支打开的任何 pull request,工作流也将运行,并且将在 pull request 中向你显示验证。
如果你熟悉YAML语法,就可能会注意到分支采用数组。 因此,你可以轻松地设置工作流在多个分支中运行,这在你维护单独的发布轨道追踪时非常有用。 例如:
on: |
每当对 master
分支或名称 以 releases/
开头的分支 打开 pull request 时,将运行你的工作流。
通过 push
和 pull_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
每天一个tips:
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
这篇文章是 reddit 上用户 unknownguy0518 发表的他在学习Django是所犯的初学者错误,以及他的一些建议。更多具体的内容可以前往对应的 话题 查看。
我是 Django Web 框架的新手。我也不是专业的程序员。我没有任何人的帮助,我学到的一切都是通过反复试验而得出的。我犯了无数的错误,当我回首时,我现在笑了。在艰难学习了很多基础知识之后,我成功地创建了一个简单的网站来添加/更新/删除联系人。它已部署在 Heroku(免费服务器)中。我在这里写的内容纯属我个人观点。如果你是初学者,并且热衷于探索 Django 的世界,则应该阅读这篇文章。它可能会帮助你解决问题。
那些想浏览我的网站的人(仍然需要一些工作),它是:https://djangophonebook.herokuapp.com
以下是我在创建自己的网站的过程中面临的主要挑战:
很多具有 Django 经验或其他编码经验的人都会告诉你阅读官方文档,以了解有关这个出色 Web 框架的更多信息。实际是,并非所有人都喜欢这么高的技术细节。乍一看,任何指定这样的框架内部工作的文档都会吓到新手。毫无疑问,Django 文档是非常详细并且组织得很好,但是我建议你观看一些出色的视频教程,使我们对所有可用功能以及如何在实际项目中实现这些功能有所了解。我亲自浏览了 YouTube上 “Corey Schafer” 的 Django 教程。这是我在互联网上找到的最好的教程之一。还有很多其他人,但我总是回头去看他的视频。我还发现 YouTube 上 “CodingEntrepreneurs” 的“尝试 Django” 系列也是一个很好的教程。一旦有了方向感,官方文档就会变得更加有意义。
现在有很多针对 Django 1.x 版本的教程。尽管我们的项目很想使用相同的版本,但我强烈建议你使用 Django 的最新稳定版本(译者注:翻译时是2020年1月,最新版本为是 2.2.9)。它不仅消除了重写代码,而且还使我们能够使用旧版本可能没有的新功能。
刚开始处理项目时,我忽略了使用 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,该怎么办?如果你希望在注册时从用户那里收集自己的某些字段,例如城市,省份,性别等,该怎么办?你可以通过创建自己的自定义用户模型来做到这一点。那时你应该考虑遍历 AbstractUser
和 AbstractBaseUser
类。我通常参考两个网站── https://simpleisbetterthancomplex.com/ 和 https://wsvincent.com/ 来实现此目的。 YouTube 上的 CodingEntrepreneurs(https://www.youtube.com/watch?v=HshbjK1vDtY)在其视频之一中还介绍了创建自定义用户模型。我强烈建议你观看它,以了解其真正工作原理,而不仅仅是复制粘贴代码。
当今大多数网站都提供了使用多种社交登录之一(例如,使用 Google 登录,使用 Facebook 登录等)登录或注册的选项。以我的个人经验,浏览我网站的大多数用户都使用了社交登录我提供的登录选项,而不是标准的注册过程。在将其付诸实践之前,在你的项目中实现它非常有意义。 “Django-Allauth” 库是一个非常好的开始,我曾经用它来实现 Google 登录。
在部署项目之前,考虑一下要存储在数据库中的数据类型始终是一个好主意。哪些字段应该是必填字段,哪些字段可以是可选字段,在用户注册时要捕获的信息,所有这些都必须事先进行仔细考虑。网站上线后,对模型进行任何更改都会证明是一件非常昂贵的事情,因为我犯了这个错误。
对于像我们这样的初学者来说,这始终是一个难题。根据我的经验,我发现基于通用类的视图非常容易编写,所用的代码行数少得多,并且使事情看起来更加整洁。这是我们真正可以看到所有魔术发生的地方,因为 Django 在后端为我们完成了所有繁重的工作。但是,我还发现,使用 CBV 实现任何自定义逻辑不是非常用户友好。我在互联网上也找不到太多有关如何使用和覆盖现有 CBV 方法的资料。这正是基于函数的视图蓬勃发展的地方。它们需要编写更多的代码行,解释起来可能会更复杂,但是当我们必须实现任何自定义逻辑时,它们就可能会显得很强大。了解 CBV 和 FBV 的工作原理确实有帮助。但是对于大多数用例来说,CBV 可以轻松完成工作。这是我创建视图时的首选路径。
除了设计模型之外,在创建项目时规划所有路由也很有意义。清楚了解各种 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 的原因之一是因为它内置了许多安全功能。其中之一就是功能齐全的管理界面。用户访问管理页面后,他/她实际上就可以滥用数据。创建超级用户时,请确保不要使用诸如 admin
或 manager
之类的通用名称作为登录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 本身并不是一个单独的话题,但像我们这样的初学者应该知道,为什么对其进行计划很重要,如何创建 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 3中:bytes实例包含原始的8位值,str实例包含Unicode字符。把Unicode字符表示为二进制数据,最常见的编码方式是UTF-8。
encode
decode
Python程序中,一定要把编码和解码放在最外围来操作。程序的核心部分应该使用Unicode字符类型,而且不要对字符编码做任何假设。
定义 to_str
和 to_bytes
方法。
Python 3中,open
默认以utf-8
编码打开,而不是二进制。
list
,str
,bytes
以及实现了 __getitem__
和 __setitem__
的类;>> a = [1,2,3,4,5,6,7] |
start
,end
和stride
;会变得难理解
数据量较大时,列表推导式会占用大量内存
()
a_long_list = [1,2,3,4,5] |
=>
value = (len(x) for x in a_long_list) |
roots = ((v, v ** 0.5)for v in value) |
range
=> enumerate
enumerate
可以把各种迭代器包装成生成器,以便稍后产生输出值。enumerate
计数下表默认从 0
开始,可以修改。
zip
同是遍历两个迭代器itertools.zip_longest
for
和 while
循环后面写 else
与 if/else
, try/except/else
的 else
不同,容易误解
try/except/else/finally
finally
块:既要将异常向上传播,又要在异常发生时做清理工作ry/except/else
:except使异常传播变得清晰,else便于自己处理代码None
比如除以0时,抛出异常
nonlocal
,但是nonlocal
不能延伸到模块级别;nonlocal
yield
iter
,next
__iter__
iter(target) == iter(target)
为True,则为迭代器*args
),*
操作符None
和文档字符串描述具有动态默认值的参数{}
,[]
等动态值会出现奇怪行为。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): |
*
标识着位置参数结束,之后的参数只能以关键字形式指定。
collections.namedtuple
;list
类型的sort
方法defaultdict
__call__
使类的实例像普通函数那样调用;__call__
方法,而不要定义带状态的闭包。@classmedtod
形式的多态去通用地构建对象__init__
;@@classmedtod
机制可以用一种与构造器相似的方式构造类对象;super
初始化父类super
在Python2和Python3不一致;Python 3 中以下两种方式效果相同
class Explicit(MyBaseClass): |
super
函数来初始化父类;mro
方法可以查看方法解析顺序:MyClass.mro()
__init__
构造器;_{类名称}__{原private属性名称}
);collections.abc
以实现自定义容器类型编写自定义容器类型时,从 collections.abc
模块的抽象基类中继承,那些基类可以确保子类具有适当的接口和行为。
os
,sys
repr
输出调试信息print
易于阅读字符串repr
可供打印字符串,eval
还原为初始值%s
=> str; %r
=> repr__repr__
自定义可供打印字符串;__dict__
任意对象查询实例字典;示例:
>> print(5) |
要确保 Python 程序能正常运行,唯一的方法就是编写测试。Python 语言动态特性,一方面阻碍了静态类型检测,另一方面却有利于开发者进行测试。
assertEqual
,assertTrue
,assertRaises
setUp
,tearDown
pdb
交互调试import pdb; pdb.set_trace()
bt
,up
,down
step
,next
,return
,continue
profile
:profile
,cProfile
runcall
Stats
tracemalloc
内存使用及泄漏CPyhton:引用计数,gc
gc.get_objects()
tracemalloc.take_snapshot()
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
故事起因于自我懈怠的感知,我在去年年底有很长一段时间不知道自己改做什么,处在十分疲惫的状态,以至于在12月的时候连着一周没有任何提交,知道我觉得自己应该做些什么改变。我想起了很早之前的一遍博客:《我泡在GitHub上的177天》,于是,我决定试着实践一次在 Github 上进行连续提交,以改变自己的状态。
我试着按照博客中的约定给自己定下了几条提交的要求:
不可否认,在这个计划刚开始的时候热情十足,有过40个提交的一天,以及好些个十几个提交的日子,虽然40个提交只是整理博客带来的,但也是让我很是激动。即使我在前半端的时间里有过两次终端也没有对降低我的热情。
在第60天之后,这个挑战的难度就开始显现了,一是我开始出现的疲惫,另一个是因为我开始出现提交枯竭。我在这次挑战中的提交大概有以下几类:
博客刚开始写得勤,一周一到两篇,后来能两三周一篇就已经算很多了;项目的想法不可能一直有;学习笔记越到后边越困难,渐渐一章一节的时间就加长了;对开源的项目的提交并不能都得到合并;阅读的文章也不能总是读到很优秀的文章。所以后来,每天的提交频率基本处于保底。
今天决定这是这次挑战的最后一个提交,写成一遍简短的博客做为结束。终于如释重负,放下了心头的重担。
正如那篇博客提及的一样,如果一个好习惯被刻意过度遵循的话也会变成坏事;每天完成的一个目标连起来形成一条链时,它会激励你去继续完成目标,保持这条链不断裂。
最后,我觉得对于 Github 的提交统计计算也有一些小的提示和想法:
我最后的建议是进行一次这样的挑战,可以给自己带来一个好的习惯。
本文整理简单整理一下HMM的理解思路。
性质:- 有限历史假设- 时间不变性
模型定义:1、初始状态概率向量 $\pi=(\pi_i)$,其中 $\pi_{i}=P(i_{1}=q_{i}), \quad i=1,2, \cdots, N$2、状态转移概率矩阵 $A=\left[a_{i j}\right]{N \times N}$,其中 $a{i j}=P\left(i_{t+1}=q_{j} | i_{t}=q_{i}\right), \quad i=1,2, \cdots, N ; j=1,2, \cdots, N$3、观测概率矩阵 $B=\left[b_{j}(k)\right]{N \times M}$,其中 $b{j}(k)=P\left(o_{t}=v_{k} | i_{t}=q_{j}\right), \quad k=1,2, \cdots, M ; j=1,2, \cdots, N$4、观测序列 $O=(o_{1}, o_{2}, \cdots, o_{T})$,状态序列 $I=(i_{1}, i_{2}, \cdots, i_{T})$5、状态集合 $Q=\left{q_{1}, q_{2}, \cdots, q_{N}\right}$,观测集合 $V=\left{v_{1}, v_{2}, \cdots, v_{M}\right}$
模型三元组 $\lambda=(A, B, \pi)$
状态转移概率矩阵A与初始状态概率向量确定了隐藏的马尔科夫链,生成不可观测的序列。观测概率矩阵B确定了如何从状态生成规则,与状态序列综合确定了如何产生观测序列。
模型基本假设:
例子:
给定模型 $\lambda=(A, B, \pi)$ 和观测序列 $O=o_{1}, o_{2}, \ldots, o_{T}$,计算在模型 $\pi$ 下观测序列 $O$ 出现的概率 $P(O | \lambda)$。- 穷举搜索,O(TN^T)
- 前向算法,O(N^2T)
- 后向算法
已知观测序列 $O=o_{1}, o_{2}, \ldots, o_{T}$ 和模型 $\lambda=(A, B, \pi)$,求给定观测序列条件概率 $P(I|O)$ 最大的状态序列 $I=\left(i_{1}, i_{2}, \ldots, i_{T}\right)$,即给定观测序列,求最有可能的对应的状态序列。- 穷举搜索- 近似计算- 维特比(Viterbi)算法:动态规划
已知观测序列 $O=o_{1}, o_{2}, \ldots, o_{T}$,估计模型 $\lambda=(A, B, \pi)$,使 $P(O | \lambda)$ 最大。- 监督算法:利用极大似然估计- 非监督算法:Baum-Welch算法(EM算法在HMM中的具体实现)
语音识别,中文分词,手写识别
在超市买菜的时候随手买了生姜和牛奶,回到家放进冰箱也就忘了。昨天想起来开始做时已经过了五天了,开始第一次尝试。首先把生姜切成小块,放碗里用擀面杖捣碎了,因为生姜有点干了,加了点水挤出来小半碗姜汁,然后把牛奶加入白糖煮开了,倒入装姜汁的碗中,然后将碗中的撞奶放到锅上蒸了五分钟。但是,结果并没有像电视上那样好,牛奶还是牛奶,加了姜汁和糖的牛奶。为什么会失败呢?
认真分析了一下原因,我想大概是是这样的:姜汁撞奶的过程就是蛋白质变性的过程,牛奶中的蛋白质和姜汁中的某些物质混合导致蛋白质变性,和做豆腐脑的过程差不多,但是因为原料的品质没有达到变性的要求:
基于以上的分析,我决定再试一次。
首先选材,生姜选老一些的,这样是蛋白质变性的物质含量高,牛奶选全脂的,蛋白质含量高。其次,做法上,生姜用板子磨成末挤姜汁水,牛奶煮的时候煮到快开再关火等10秒,这样大概就是80度,然后与姜汁混合。混合的时候把姜汁分成了两个碗,每个碗倒入原来一般的牛奶,这样就比之前更好控制。混合之后不搅动,并用盘子盖住保持温度,等10分钟左右。
经过这些改进,我终于得到了形似豆腐脑的姜汁装奶!虽然没有把握好糖的量使得牛奶有些淡,但2.0版本味道确实不错。
做一件事要了解原理,掌握每一步的技术细节和对结果的影响,这样才能做好一件事!这就是两次姜汁撞奶的总结。
太平洋标准时间5月13日中午12点30分(北京时间5月14日凌晨3:30),中国电信经历了持续近5个小时的重大故障,后续效应持续了几个小时。正如在美国对贸易政策的紧张局势加剧之时所做的那样,可能很容易让人联想到对潜在地缘政治动机的猜测。然而,这种猜测忽略了许多人都不知道的关于中国和互联网的一些重要的基本现实。这次中断是深入了解中国互联网连接状况的绝佳机会。
昨天,从下午早些时候开始,我们的全球优势点开始检测到中国电信骨干网上进出中国的互联网流量出现大量数据包丢失。数据包丢失持续数小时,主要影响中国大陆的网络基础设施,但也影响中国电信在新加坡的网络和美国的多个接入点,包括洛杉矶。
在长时间中断的过程中,通过受影响的基础设施的任何流量都被丢弃,这意味着中国境内外的一些互联网用户将遇到连接到各种网站和应用程序的服务中断。中国境内的用户试图访问中国以外的网站会受到影响,同时中国以外的用户尝试连接到中国境内的网站也是如此。
这些不仅仅影响西方网站和服务,许多美国主要网站,如Apple,Amazon,Microsoft,Slack,Workday,SAP等在中断期间都受到了影响。下图显示了中断的附带损害的部分范围。
图1:受中国电信中断影响的基于云的服务。
在最高峰时,我们检测到因中国电信全球骨干网络状况而中断的100多项服务。我们还检测到中断对地理影响的变化,如下面的两个热图所示,在中断期间的不同点捕获。
图2:中国电信中断的地理范围在中断期间有所不同。
最严重的中断期是在太平洋标准时间12:30开始后的前三个小时内发生的,但许多服务在数小时后继续受到影响。一项特定服务的中断的扩展性质可以在下面的图3中看到。Amazon Web Services的数据包丢失从太平洋标准时间12:30开始飙升,持续约5个小时。下面的路径可视化显示了在17:30-17:45(太平洋标准时间)之间的网络状况,此时前往AWS的流量继续被中国电信路由器丢弃。
图3:中国电信用户在12:30-17:45(太平洋标准时间)之间受影响的AWS服务。
受中断影响的另一家美国服务是Cloudflare的托管DNS服务。例如,WeWork的域名由Cloudflare托管,在下面的路径可视化中,您可以看到尝试访问Cloudflare的DNS服务器的流量(由最右边的绿色节点表示,IP地址为173.245.58.135,这是地理位置位于美国的路由前缀173.245.58.0/24的一部分。)被中国境内的中国电信路由器丢弃。结果是来自中国境内的许多用户无法访问WeWork网站。
图4:Cloudflare的托管DNS服务受到影响,阻止了WeWork域的DNS解析。
中断现已解决,但在ThousandEyes,我们相信每次互联网停运都是一次学习机会,这也不例外。这里有两个要点可能一般不太清楚。
首先,大多数人认为“长城防火墙”是一套统一管理的规则,使中国用户与全球其他地方隔离。他们想象一方面是中国庞大的用户和为他们提供服务的网络基础设施,另一方面,互联网的其余部分。但现实情况是,中国与外部网站和服务的联系非常紧密──至少是那些服务于商业利益的网站和服务。
中国互联网服务提供商(ISP)的标准操作程序允许访问大多数西方基于云的业务服务。通过允许国内和外国公司更有效地开展业务,从中国境内进入这些类型的网站符合中国的利益。通俗地说,您可以从中国境内访问许多美国服务。
其次,大多数人也可能没有意识到中国的互联网服务提供商将拥有全球性的存在。但是,如上图2所示,中国电信控制和管理的基础设施范围远远超出了中国的地理边界。该提供商还维护全球互联,与全球许多地方的服务提供商保持联系。
那些回忆起去年影响谷歌服务的BGP路由泄漏的读者,可能还记得中国电信在那一集中的客串。中国电信与负责尼日利亚工厂路线泄漏的ISP(MainOne)保持联系。中国电信未能过滤通往谷歌的广告路线(而是将其传播给其他同行),导致一些用户试图访问谷歌的服务中断。
无论互联网在哪里,无论是在一个自由的民主国家,还是在高度国家控制的环境中,它都是根本无法预测的。这是由于它被构建为自愿互连但单独管理的网络,基础技术的自动化性质(例如BGP路由)以及完全缺乏集中式运营管理的的集合。
当互联网的某个部分出现糟糕的事情时,尤其是在像中国这样的后续国家,互联网的其他部分会产生连锁反应。在这种情况下,超过一百种商业服务受到影响,毫无疑问,因此产生了生产力和收入损失。
当你考虑互联网是多么不可预测的时候,今天的企业依赖互联网的程度相当惊人。这是最终的不受控制的环境。这就引出了一个问题,如果你无法直接控制它,你是否拥有尽可能最好地管理它的可见性?
因为ThousandEyes本质上是(不断变化的)互联网的实时地图,我们能够跟踪互联网中断及其全球影响,无论是在中国,俄罗斯还是其他地方,但我们不能也不会推测这种网络事件的地缘政治和可能的动机(如果有的话)。
然而,值得注意的是,中国的互联网服务提供商非常精通对进出网络的流量实施细粒度控制。如果有意展示禁用美国应用程序和服务的能力,那么从技术角度来看,它肯定没有得到很明确的体现。毕竟,被破坏的网站和服务是不分青红皂白的,影响到中国境内外的人。
主要的互联网中断是企业团队关注企业所依赖的大量外部依赖关系的警钟。如果你无法深入了解四面墙以外的应用和网络层条件,那么当你的云和互联网生态系统发生不可预测的事情时,您将陷入困境。
用来将多个连续的提交合并为一个,以下面的提交记录为例,master
是主分支,分支 featureY
提交了一系列的修改:
git lg |
其中,lg
是如下命令:
[alias] |
这里我们需要合并 featureY
功能分支上的 Y1
到 Y5
这五个提交为一个。git提供了如下命令:
git rebase --interactive HEAD~[N] |
其中 [N]
表示需要合并的数量,从最近一个提交开始数,这里为5
。在命令行输入 git rebase --interactive HEAD~5
进入编辑器进行选择。注意,这里的提交顺序是 反 的,从最早的 Y1
开始:
pick 61b5ff9 Y1 |
对应需要合并的提交,将pick
改成squash
(或者简化为s
),修改之后为:
pick 61b5ff9 Y1 |
保存并关闭编辑器,这是编辑器会自动跳出并需要你提交一个新的提交:
This is a combination of 5 commits. |
可以看到,Git提供了详细的信息指导提交,只需要修改成你需要的信息即可,比如 featureY
,然后保存。这时就完成了修改,修改之后的提交信息如下:
git lg |
用于在合并分支时,最后只在合并后的分支上保留一个提交。同样以上面的代码提交为例子。
git lg |
git checkout master |
此时,分支featureY
保持不变,同时在master
上多了一个未被提交的更改:
git status |
这些更改是分支featureY
中所有提交的合并,现在只需要提交这些更改就可以了:
git commit -m "featureY" |
从以上的擦坐过程可以看出两者之间的差别:Rebase Squash会合并之前的提交,之前的记录会消失,而Merge Squash只会在合并的分支上新生成提交,原来的那些提交熬还会保留。
如果需要合并的提交数量很多,数数容易出错,可以使用提交哈希来识别:
git rebase --interactive [commit-hash] |
这个[commit-hash]
是需要合并的提交之前的一个提交:
git lg |
这里,需要使用 220e45c
而不是 61b5ff9
。
heroku login |
cd RSSHub |
Settings
中 Heroku Git URL
找到:heroku git:remote -a rss-qiwihui |
git push heroku master |
HEROKU_SLUG_COMMIT
:heroku config:set HEROKU_SLUG_COMMIT=$(git rev-parse --short HEAD) |
前往相应的页面验证,可以看到在Debug中的 githash值已经是当前最新的hash值了。
每隔一段时间我就会参与一个关于依赖管理和版本的对话,通常是在工作中,其中会出现“依赖地狱”的主题。如果你对这个术语不熟悉,那么我建议你查一下。简要总结可能是:“处理应用程序依赖版本和依赖冲突所带来的挫败感”。带着这个,让我们先获得关于依赖解析的一些技术。
在讨论包应该具有哪种依赖关系以及哪些依赖关系可能导致问题时,本主题通常会进入讨论。作为一个真实的例子,在 Widen Enterprises,我们有一个内部的,可重用的Java框架,它由几个软件包组成,为我们提供了创建许多内部服务的基础(如果你愿意的话,微服务)。这很好,但是如果你想创建一个依赖于框架中某些东西的可重用共享代码库呢?如果你尝试在应用程序中使用这样的库,最终可能会得到如下依赖关系图:
就像在这个例子中一样,每当你试图在服务中使用库时,你的服务和库很可能依赖于不同版本的框架,这就是“依赖地狱”的开始。
现在,在这一点上,一个好的开发平台将为你提供以下两种选择的组合:
framework
版本21.1.1
和21.2.0
相互冲突。这两个看起来都合理,对吧?如果两个软件包确实彼此不兼容,那么我们根本无法在不修改其中一个的情况下将它们一起使用。这是一个艰难的情况,但替代方案往往更糟糕。事实上,Java是不该学习的一个很好的例子:
app
升级到framework 21.2.0
。这看起来像是一个双输的情况,所以你可以想象,这对添加依赖项非常不利,并且使之成为一个事实上的策略,除了实际的应用程序之外什么都不允许依赖我们的核心框架。
在进行这些讨论时,我会经常提到这是一个不适用于所有语言的问题,作为一个例子,Rust“解决”了这个问题。我常常拿Rust如何解决世界上所有的问题开玩笑,但在那里通常有一个真实的核心。因此,当我说Rust“解决”了这个问题以及它是如何工作的时候,让我们深入了解一下我的意思。
Rust的解决方案涉及相当多的动人的部分,但它基本上归结为挑战我们在此之前做出的核心假设:
最终应用程序中只应存在任何给定包的一个版本。
Rust挑战了这一点,以便重构问题,看看是否有一个在依赖地狱之外更好的解决方案。Rust平台主要有两个功能可以协同工作,为解决这些依赖问题提供基础,现在我们将分别研究并看看最终结果是怎样的。
难题的第一部分当然是Cargo,Rust官方依赖管理器。Cargo类似于NPM或Maven之类的工具,并且有一些有趣的功能使它成为一个真正高质量的依赖管理器(这里我最喜欢的是Composer,一个非常精心设计的PHP依赖管理器)。Cargo负责下载项目依赖的Rust库,称为crates,并协调调用Rust编译器以获得最终结果。
请注意,crates是编译器中的第一类构造。这在以后很重要。
与NPM和Composer一样,Cargo允许你根据语义版本控制的兼容性规则指定项目兼容的一系列依赖项版本。这允许你描述与你的代码兼容(或可能)兼容的一个或多个版本。例如,我可能会添加
[dependencies] |
到Cargo.toml
文件,表明我的代码适用于0.4
系列中log
包的任何补丁版本。也许在最终的应用程序中,我们得到了这个依赖树
因为在my-project
中我声明了与log
版本0.4.*
的兼容性,我们可以安全地为log
选择版本0.4.4
,因为它满足所有要求。(如果log
包遵循语义版本控制的原则,这个原则对于已发布的库而言并不总是如此,那么我们可以确信这个发布不包括任何会破坏我们代码的重大更改。)你可以在Cargo文档中找到一个更好地解释版本范围以及它们如何应用于Cargo。
太棒了,所以我们可以选择满足每个项目版本要求的最新版本,而不是选择避开遇到版本冲突或只是选择更新的版本并祈祷。但是,如果我们遇到无法解决的问题,例如:
没有可以选择满足所有要求的log
版本!我们接下来做什么?
为了回答这个问题,我们需要讨论名字修饰。一般来说,名字修饰是一些编译器用于各种语言的过程,它将符号名称作为输入,并生成一个更简单的字符串作为输出,可用于在链接时消除类似命名符号的歧义。例如,Rust允许你在不同模块之间重用标识符:
mod en { |
这里我们有两个不同的函数,名为greet()
,但当然这很好,因为它们在不同的模块中。这很方便,但通常应用程序二进制格式没有模块的概念;相反,所有符号都存在于单个全局命名空间中,非常类似于C中的名称。由于greet()
在最终二进制文件中不能显示两次,因此编译器可能使用比源代码更明确的名称。例如:
en::greet()
成为en__greet
es::greet()
成为es__greet
问题解决了!只要我们确保这个名字修饰方案是确定性的并且在编译期间到处使用,代码就会知道如何获得正确的函数。
现在这不是一个完全完整的名字修饰方案,因为我们还没有考虑很多其他的东西,比如泛型类型参数,重载等等。此功能也不是Rust独有的,并且确实在C++和Fortran等语言中使用了很长时间。
名字修饰如何帮助Rust解决依赖地狱?这一切都在Rust的名字管理体系中,这似乎在我所研究的语言中相当独特。那么让我们来看看?
在Rust编译器中查找名字修饰的代码很简单;它位于一个名为symbol_names.rs
的文件中。如果你想学习更多内容,我建议你阅读这个文件中的注释,但我会包括重点。似乎有四个基本组件包含在一个修饰符号名称中:
使用Cargo时,Cargo本身会将“歧义消除器”提供给编译器,所以让我们看一下compilation_files.rs
包含的内容:
这个复杂系统的最终结果是,即使是不同版本的crate中的相同功能也具有不同的修饰符号名称,因此只要每个组件知道要调用的函数版本,就可以在单个应用程序中共存。
现在回到我们之前的“无法解决的”依赖图:
借助依赖范围的强大功能,以及Cargo和Rust编译器协同工作,我们现在可以通过在我们的应用程序中包含log 0.5.0
和log 0.4.4
来实际解决此依赖关系图。app
内部使用log
的任何代码都将被编译以达到从0.5.0
版生成的符号,而my-project
中的代码将使用为0.4.4
版生成的符号。
现在我们看到了大局,这实际上看起来非常直观,并解决了一大堆依赖问题,这些问题会困扰其他语言的用户。这个解决方案并不完美:
log 0.5.0
的LogLevel
并将其传递给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
词的独热表示(One-Hot Representation)
缺点:
词的分布式表示(Distributed Representation)
文法语言模型,统计语言模型
核心是上下文的表示以及上下文与目标词之间的关系的建模。
语言模型就是计算一个句子的概率大小的模型。一个句子的打分概率越高,越说明他是更合乎人说出来的自然句子。常见的统计语言模型有N元文法模型(N-gram Model),最常见的是unigram model、bigram model、trigram model等等。还有N-pos模型。
2001年,Bengio 等人正式提出神经网络语言模型( Neural Network Language Model ,NNLM),该模型在学习语言模型的同时,也得到了词向量。所以请注意:词向量可以认为是神经网络训练语言模型的副产品。
做法:
1、将one-hot中的vector每一个元素由整形改为浮点型,变为整个实数范围的表示;2、将原来稀疏的巨大维度压缩 嵌入 到一个更小维度的空间。
a. Neural Network Language Model ,NNLMb. Log-Bilinear Language Model, LBLc. Recurrent Neural Network based Language Model,RNNLMd. Collobert 和 Weston 在2008 年提出的 C&W 模型e. Mikolov 等人提出了 CBOW( Continuous Bagof-Words,连续词袋模型)和 Skip-gram 模型
CBOW和Skip-gram:
实现CBOW和Skip-gram语言模型的工具(正如C&W模型的实现工具是SENNA)。
文本分类,个性化推荐,广告点击等
Answer to the Ultimate Question of Life, the Universe, and Everything. – 42
以下是论文正文翻译:
作者:ANDREW R. BOOKER
摘要 受到Tim Browning和Brady Haran的Numberphile视频"未解决的33问题"的启发,我们研究了方程 $x^3+y^3+z^3=k$ 在一些小的 $k$ 值的解。 我们找到了 $k=33$的第一个已知解。
令 $k$ 为正整数,其中 $k \equiv ±4(\mod 9)$。 然后Heath-Brown[HB92] 推测 有无限多的三元组$(x,y,z) \in \mathbb{Z}^3$ 满足
$$k = x^3 + y^3 + z^3. \quad \text{(1)}$$
早在1954年就开始对(1)进行各种数值研究[MW55];请参阅[BPTYJ07],了解截至2000年的这些研究的历史。自那时起进行的计算由于Elkies [Elk00]而被算法所主导。我们所知道的最新内容是Huisman[Hui16] 的论文, 该论文确定了(1)的所有解,其中$k \le 1000$ 且 $\max{|x|,|y|,|z|}\le 10^15$。特别是,Huisman报告说除了13个 $k \le 1000$的值以外的所有解决方案都是已知的:
$$33, 42, 114, 165, 390, 579, 627, 633, 732, 795, 906, 921, 975. \quad \text{(2)}$$
Elkies的算法通过使用格基减少(lattice basis reduction)在Fermat曲线$X^3+Y^3=1$ 附近寻找有理点来工作;它非常适合同时找到许多 $k$ 值的解。在本文中,我们描述了一种在k值确定时更有效的不同方法。它的优点是可以找到所有具有 最小坐标界限的解,而不是Elkies算法中的最大坐标。这总是产生搜索范围的非平凡的扩张(nontrivialexpansion),因为除了可以单独考虑的有限多个例外之外,还有
$$\max {|x|,|y|,|z|} > \sqrt[3]{2} \min {|x|,|y|,|z|}$$
此外,根据经验,通常情况是其中一个变量比其他变量小得多,因此我们希望实际上增益更大。
我们的策略类似于一些早期的方法(特别参见[HBLtR93],[Bre95],[KTS97]和 [BPTYJ07]), 并且基于观察:$k-z^3=x^3+y^3$的任何解都具有 $x+y$ 作为一个因子。相对于早期研究,我们的主要贡献是注意到,通过一些时间空间权衡,运行时间在高度边界内非常接近线性,并且在现代64位计算机上实现时非常实用。
更详细地说,假设 $(x,y,z)$ 是(1)的解,并且不失一般性,假设$|x| \ge |y| \ge |z|$。 然后我们有
$$k-z^{3}=x^{3}+y^{3}=(x+y)(x^{2}-x y+y^{2})$$
如果 $k-z^3=0$ 则 $y=-x$,并且 $x$ 的每个值都产生一个解。 否则,设$d=|x+y|=|x|+y \operatorname{sgn} x$, 我们看到 $d$ 可以除 $|k-z^3|$并且
$$\begin{aligned}\begin{aligned}\frac{\left|k-z^{3}\right|}{d} &=x^{2}-x y+y^{2}=x(2 x-(x+y))+y^{2} \&=|x|(2|x|-d)+(d-|x|)^{2}=3 x^{2}-3 d|x|+d^{2}\end{aligned}\end{aligned}$$
得到
$${x, y}=\left{\frac{1}{2} \operatorname{sgn}\left(k-z^{3}\right)\left(d \pm \sqrt{\frac{4|k-z^{3}|-d^{3}}{3 d}}\right)\right}$$
因此,给定 $z$ 的候选值,通过遍历 $|k-z^3|$ 的所有除数,有一个有效的程序来查找 $x$ 和 $y$ 的所有相应值。这个基本算法在假设整数分解的时间复杂度的标准启发式(standardheuristics)下,已经能在 时间 $O(B^{1+\varepsilon})$ 内找到满足$\min{|x|,|y|,|z|}\ge B$ 的所有解。在下一节中,我们将解释如何避免因子分解并更有效地实现相同目的。
感谢 感谢Roger Heath-Brown提供了有用的意见和建议。
为了便于表示,我们假设$k \equiv ±3(\mod 9)$;请注意,这适用于(2)中的所有 $k$。由于上述基本算法对于寻找小解是合理的,因此我们将假设 $|z|>\sqrt{k}$。此外,如果我们将(1)专门用于 $y=z$ 的解,那么我们得到Thue方程$x^3+2y^3=k$,这是有效可解的。 使用 PARI/GP[The18] 中的Thue求解器,我们验证了(2)中的 $k$不存在这样的解。 因此,我们可以进一步假设 $y \ne z$。
由于 $|z|>\sqrt{k} \ge \sqrt[3]{k}$,我们有
$$\operatorname{sgn} z=-\operatorname{sgn}(k-z^{3})=-\operatorname{sgn}(x^{3}+y^{3})=-\operatorname{sgn} x.$$
同样,因为 $x^3 + z^3 = k-y^3$ 和 $|y|\ge |z|$, 我们有$\operatorname{sgn} y=-\operatorname{sgn} x=\operatorname{sgn} z$。将(1)的两边乘以 $-\operatorname{sgn} z$,我们得到
$$|x|^{3}-|y|^{3}-|z|^{3}=-k \operatorname{sgn} z \quad \text{(4)}$$
令 $\alpha=\sqrt[3]{2}-1$,并且 $d=|x+y|=|x|-|y|$。 如果$d \ge \alpha |z|$ 则
$$\begin{aligned}\begin{aligned}-k \operatorname{sgn} z &=|x|^{3}-|y|^{3}-|z|^{3} \geq(|y|+\alpha|z|)^{3}-|y|^{3}-|z|^{3} \&=3 \alpha(\alpha+2)(|y|-|z|) z^{2}+3 \alpha(|y|-|z|)^{2}|z| \& \geq 3 \alpha(\alpha+2)|y-z| z^{2}\end{aligned}\end{aligned}$$
由于 $3 \alpha(\alpha+2)>1$, 这与我们的假设不相容,即 $y \ne z$ 和$|z|>\sqrt{k}$。 因此我们必然有 $0<d<\alpha|z|$。
接下来,减少(4)模3并回想我们的假设 $k \equiv ±3(\mod 9)$,我们有
$$d=|x|-|y| \equiv|z| \quad(\mod 3).$$
设 $\epsilon\in{±1}$ 使得 $k \equiv 3 \epsilon(\mod 9)$。然后,由于每个立方数都与 $0$ 或 $±1(mod 9)$ 相等, 我们必然有$x \equiv y \equiv z \equiv \epsilon(\mod 3)$, 因此$\operatorname{sgn} z=\epsilon(\frac{|z|}{3})=\epsilon(\frac{d}{3})$。基于(3),当且仅当 $d | z^{3}-k$ 以及$3d(4|z^{3}-k|-d^3) = 3d(4\epsilon(\frac{d}{3})(z^{3}-k)-d^{3})$是平方数时, 我们得到(1)的解。
总之,找到(1)的所有解并且满足$|x| \ge |y| \ge |z|>\sqrt{k}$,$y \ne z$ 和$|z|\le B$,对于每个与3互质的 $d\in\mathbb{Z}\cap(0,\alpha B)$,解决以下系统就足够了:
$$\begin{aligned}\begin{aligned}&{\frac{d}{\sqrt[3]{2}-1}<|z| \le B, \quad \operatorname{sgn} z=\epsilon\left(\frac{d}{3}\right), \quad z^{3} \equiv k \quad(\mod d)} \&{3 d\left(4 \epsilon\left(\frac{d}{3}\right)(z^{3}-k)-d^{3}\right)=\square} & \text{(5)}\end{aligned}\end{aligned}$$
我们解决这个问题的方法很简单:我们通过它们的主要因子分解递归地计算 $d$的值, 并应用中国剩余定理来将 $z^{3} \equiv k(\mod d)$的解减少到素数模幂的情况下, 其中标准算法可以适用。设$r_{d}(k)=# \left{z(\mod d):z^{3} \equiv k(\mod d)\right}$ 表示 $k$ 模 $d$的立方根数。通过标准分析估计,由于 $k$ 不是立方数,我们有
$$\sum_{d \le \alpha B} r_{d}(k) \ll_{k} B$$
启发式地,计算对所有素数 $p\le \alpha B$ 的 $z^{3} \equiv k(\mod p)$的解 可以用 $[0, \alpha B]$ 上的整数在 $O(B)$ 算术运算来完成; 见例如[[NZM91],§2.9,练习8]中描述的算法。假设这一点,可以看出,使用Montgomery的批量反转技巧[[Mon87],§10.3.1],计算对所有正整数$p\le \alpha B$ 的 $z^{3} \equiv k(\mod p)$ 的根的剩余工作可以再次用$O(B)$ 算术运算完成。
因此,我们可以在线性时间内计算满足(5)的第一行的所有 $z$,作为算术进展(arithmeticprogressions)的并集。为了检测最后一行的解,有一个快速的方法来确定$\Delta :=3d\left(4\epsilon(\frac{d}{3})(z^{3}-k)-d^{3}\right)$是一个平方数 至关重要。我们首先注意到对于固定$d$,这种情况减少到在椭圆曲线上找到积分点; 特别是,令 $X=12d|z|$ 和$Y=(6d^2|x-y|$,从(3)中我们看到(X,Y)位于Mordell曲线上
$$Y^{2}=X^{3}-2(6 d)^{3}\left(d^{3}+4 \epsilon\left(\frac{d}{3}\right) k\right). \quad \text{(6)}$$
因此,对于固定 $d$,存在至多有限多个解,并且它们可以被有效地约束。 对于$d$的一些小值,找到(6)上的所有积分点并检查是否产生任何满足(1)的解是切实可行的。例如,使用Magma[[BCFS18],§128.2.8]中的积分点函数(functionality),我们验证了如(2)中的 $k$ 和 $d \le 40$ 情况下没有解, 除了$(k, d)\in{(579,29),(579,34),(975,22)}$。
接下来我们自然注意到一些同余和可分性约束:
引理 设 $z$ 为(5)的解,设 $p$ 为素数, 设$s=ord_p d$,$t=ord_p(z^3-k)$。则
(i) $z \equiv \frac{4}{3} k\left(2-d^{2}\right)+9(k+d)(\mod 18)$;(ii) 如果 $p \equiv 2 (\mod 3)$ 则 $t \le 3s$;(iii) 如果 $t \le 3s$ 则 $s \equiv t (\mod 2)$;(iv) 如果 $ord_p k \in {1,2}$ 则 $s \in {0,ord_p k}$。
证明 令 $\Delta=3d\left(4\epsilon(\frac{d}{3})(z^3-k)-d^3\right)$, 令$\delta=(\frac{d}{3})$,我们有 $|z| \equiv d \equiv \delta(\mod 3)$,观察到 $(\delta+3 n)^{3} \equiv \delta+9 n(\mod 27)$,模27,我们有
$$\begin{aligned}\begin{aligned}\frac{\Delta}{3 d} &=4 \epsilon \delta\left(z^{3}-k\right)-d^{3}=4|z|^{3}-d^{3}-4 \epsilon \delta k \& \equiv 4[\delta+3(|z|-\delta)]-[\delta+3(d-\delta)]-4 \epsilon \delta k=3(4|z|-d)-\delta[18+4(\epsilon k-3)] \& \equiv 3(4|z|-d)-d[18+4(\epsilon k-3)]=12|z|-9 d-4 \epsilon d k \& \equiv 3|z|-4 \epsilon d k\end{aligned}\end{aligned}$$
这消失了模9,所以为了使 $\Delta$ 成为平方数,它也必须消除mod 27。 于是
$$z=\epsilon \delta|z| \equiv \frac{4 \delta d k}{3} \equiv \frac{4(2-d^{2}) k}{3} \quad(\mod 9)$$
减少(1)模2我们得到 $z \equiv k+d(\mod 2)$,这得到(i)。
接下来设 $u=p^{-s} d$ 和 $v=p^{-t} \epsilon \delta(z^{3}-k)$,这样就有
$$\Delta=3\left(4 p^{s+t} u v-p^{4 s} u^{4}\right)$$
如果 $3s<t$ 则 $p^{-4 s} \Delta \equiv-3 u^{4}(\mod 4 p)$, 但是当$p \equiv 2(\mod 3)$ 时这是不可能的,因为 $-3$ 不是 $4p$ 的平方模。因此,在这种情况下我们必须 $t<3s$。
接下来假设 $t<3s$。 我们考虑以下情况,涵盖所有可能性:
因此,在任何情况我们得出结论 $s \equiv t(\mod 2)$。
最后,假设 $p|k$ 和 $p \not | 3k$。如果 $s=0$则无需证明的,所以假设不然。 由于 $d | z^{3}-k$,我们必须有$d | k$,因为
$$0 < s \le t=\operatorname{ord}{p}(z^{3}-k)=\operatorname{ord}{p} k<3 s$$
通过部分(iii)得出 $s \equiv \operatorname{ord}{p} k(\mod 2)$, 因此$s=\operatorname{ord}{p} k$。
因此,一旦 $z(\mod d)$ 的残差类(residue class)固定, 则其残差模$lcm(d,18)$ 是确定的。还要注意,条件(ii)和(iii)对于测试 $p=2$是有效的。
然而,即使有这些优化,也有 $\ll B\log B$ 对 $d, z$满足(5)的第一行和引理的结论(i)和(iv)。 因此,为了实现比$O(B\log B)$ 更好的运行时间,需要从一开始就消除一些 $z$ 值。我们通过标准的时间空间交换来实现这一目标。确切地说,设置$P=3(\log \log B)(\log \log \log B)$, 并且让$M=\prod_{5 \le p \le P} p$ 是区间 $[5, P]$ 之间的素数的乘积。根据素数定理,我们得到 $\log M=(1+o(1)) P$。如果 $\Delta$ 是平方数,那么对于任意素数 $p|M$ 我们有
$$\left(\frac{\Delta}{p}\right)=\left(\frac{3 d}{p}\right)\left(\frac{|z|^{3}-c}{p}\right) \in{0,1} \quad \text{(7)}$$
其中 $c \equiv \epsilon\left(\frac{d}{3}\right) k+\frac{d^{3}}{4}$。 当$\operatorname{lcm}(d, 18) \le \alpha B / M$ 时, 我们首先为每个残差类$|z|(\bmod M)$ 计算该函数, 并且仅选择对于每个 $p|M$满足(7)的那些残基。 由Hasse约束,允许的残差的数量最多为
$$\frac{M}{2^{\omega(M /(M, d))}} \prod_{p | \frac{M}{(M, d)}}\left(1+O\left(\frac{1}{\sqrt{p}}\right)\right)=\frac{M}{2^{\omega(M /(M, d))}} e^{O(\sqrt{P} / \log P)}$$
因此,要考虑的 $z$ 值的总数最多为
$$\begin{aligned}\begin{array}{l}{\sum_{\operatorname{lcm}(d, 18) \le \frac{\alpha B}{M}} r_{d}(k)\left[M+\frac{e^{O(\sqrt{P} / \log P)}}{2^{\omega(M /(M, d))}} \frac{\alpha B}{d}\right] +\sum_{d \le \alpha B, {lcm}(d, 18) \le \frac{\alpha B}{M}} \frac{r_{d}(k) \alpha B}{d}} \{\ll_{k} B \log M+\frac{e^{O(\sqrt{P} / \log P)}}{2^{\omega(M)}} \sum_{g | M} \frac{2^{\omega(g)} r_{g}(k)}{g} \sum_{d^{\prime} \le \frac{\alpha B}{9 g M}} \frac{r_{d^{\prime}}(k) \alpha B}{d^{\prime}}} \{\ll_{k} B \log M+B \log B \frac{e^{O(\sqrt{P} / \log P)}}{2^{\omega(M)}} \prod_{p | M}\left(1+\frac{2 r_{p}(k)}{p}\right)} \{\ll B P+\frac{B \log B}{2^{(1+o(1)) P / \log P}} \ll B(\log \log B)(\log \log \log B)}\end{array}\end{aligned}$$
对于没有以这种方式消除的 $z$,我们遵循类似的策略, 其中一些其他辅助模$M^{\prime}$ 由较大的素数组成,以加速平方测试。 我们预先计算模为$M^{\prime}$ 的立方数表和Legendre符号模 $p|M^{\prime}$,因此将测试(7)简化为了表查找。只有当所有这些测试都通过时,我们才能在多精度算术中计算 $\Delta$并应用一般的平方检验,这种情况对于一小部分候选值来说都是如此。事实上,我们期望Legendre测试的数量平均有限,所以总的来说,找到所有解决方案的 $|z| \le B$ 应该要求不超过$O_k(B(\log \log B)(\log \log \log B))$ 次表查找和对 $[0, B]$中整数的算术运算。
因此,当 $B$符合机器字大小时,我们预计运行时间几乎是线性的,这就是我们在实践中观察到的$B<2^{64}$。
我们在C中实现了上述算法,其中有一些内联汇编程序来源于由Ben Buhrow[Buh19] 编写的Montgomery算法[Mon85], 以及Kim Walisch的用于枚举素数的primesieve 库 [Wal19]。
该算法自然地在具有超过 $\sqrt{\alpha B}$ 的素因子和 具有$\sqrt{\alpha B}$ -平滑的素数的 $d$ 的值之间分配。 前一组 $d$消耗超过运行时间的三分之二,但更容易并行化。我们在布里斯托大学高级计算研究中心的大规模并行集群Bluecrystal Phase3上运行了这一部分。 对于平滑的$d$,我们使用了一个单独的32核和64核节点的小集群。
我们搜索了满足 $k \in {33,42}$ 和 $\min{|x|, |y|, |z|} \le 10^16$的(1)的解,找到了以下结果:
$$33 = 8 866 128 975 287 528^3 +(-8 778 405 442 862 239)^3 +(-2 736 111 468 807 040)^3$$
总计算在三个星期的实际时间中大约使用了15个核年。
(略)
School of Mathematics, University of Bristol, University Walk, Bristol,BS8 1TW, United Kingdom
E-mail address: andrew.booker@bristol.ac.uk
]]>from: qiwihui on: 4/11/2019
以下材料为本次申请签证所需材料:
主申人:
随行:
因为选择的代办旅行社没有要求赴日行程和机票酒店预订记录,所以没有提供。
最后,最最重要的,就是开心就好,不用太拘泥于景点有没有都游览一遍。
from: qiwihui on: 3/13/2019
]]>from: qiwihui on: 4/11/2019
获取模型的第一步当然是训练一个模型,但是这不是本篇的重点,所以我们使用一个已经训练好的模型,比如ResNet。TensorFlow Serving 使用SavedModel这种格式来保存其模型,SavedModel是一种独立于语言的,可恢复,密集的序列化格式,支持使用更高级别的系统和工具来生成,使用和转换TensorFlow模型。这里我们直接下载一个预训练好的模型:
mkdir /tmp/resnet |
如果是使用其他框架比如Keras生成的模型,则需要将模型转换为SavedModel格式,比如:
from keras.models import Sequential |
下载完成后,文件目录树为:
tree /tmp/resnet |
使用Docker部署模型服务:
docker pull 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 |
我们可以看到,TensorFlow Serving使用1538687457
作为模型的版本号。我们使用curl命令来查看一下启动的服务状态,也可以看到提供服务的模型版本以及模型状态。
curl http://localhost:8501/v1/models/resnet |
很多时候我们需要查看模型的输出和输出参数的具体形式,TensorFlow提供了一个saved_model_cli
命令来查看模型的输入和输出参数:
saved_model_cli show --dir /tmp/resnet/1538687457/ --all |
注意到signature_def
,inputs
的名称,类型和输出,这些参数在接下来的模型预测请求中需要。
TensorFlow Serving提供REST API和gRPC两种请求方式,接下来将具体这两种方式。
我们下载一个客户端脚本,这个脚本会下载一张猫的图片,同时使用这张图片来计算服务请求时间。
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 |
输出结果为
python resnet_client.py |
让我们下载另一个客户端脚本,这个脚本使用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 |
脚本内容:
from __future__ import print_function |
输出的结果可以看到图片的分类,概率和使用的模型信息:
python resnet_client_grpc.py |
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. 编译开发版本 |
之后,使用新编译的$USER/tensorflow-serving
重新启动服务即可。
上面我们快速实践了使用TensorFlow Serving和Docker部署机器学习服务的过程,可以看到,TensorFlow Serving提供了非常方便和高效的模型管理,配合Docker,可以快速搭建起机器学习服务。
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI
项目代码:qiwihui/sphinx-doc-starter
Sphinx 是一个基于 Python 的文档生成项目。最早只是用来生成 Python 的项目文档,使用 reStructuredText 格式。但随着项目的逐渐完善,很多非 Python 的项目也采用 Sphinx 作为文档写作工具,甚至完全可以用 Sphinx 来写书。
使用 Sphinx 生成文档的优点包括:
这个过程包括如下步骤:
Sphinx 依赖于 Python,并提供了 Python 包,所以使用 pip 安装既可。这里我只安装了 sphinx-doc
这个包。
pip install sphinx-doc |
这时,通过 bash 自动补全(连续两下 tab
),可以看到有几个命令,Sphinx 推荐使用 sphinx-quickstart
,这是一个设置向导。
sphinx- |
运行 sphinx-quickstart
,以下主要设置项目名称,作者名称以及语言(zh_CN
)即可,其他默认。
sphinx-quickstart |
解释1,整个设置过程包括:
是否分离源文件目录 source
和生成文件目录 build
,默认否;
模板目录 templates
和静态文件目录 static
前缀,默认为_
;
项目名称;
项目作者;
项目版本,默认为空;
项目语言,默认为 en
;
文档扩展名,默认为 .rst
;
首页文件名,默认为 index
;
开启的扩展,均默认为否:
生成 Makefile,默认是;
生成 Windows 用命令行,默认是。
解释2,项目目录文件结构如下:
sphinx-test |
其中:
Makefile
:可以看作是一个包含指令的文件,在使用 make 命令时,可以使用这些指令来构建文档输出。build
:生成的文件的输出目录。make.bat
:Windows 用命令行。_static
:静态文件目录,比如图片等。_templates
:模板目录。conf.py
:存放 Sphinx 的配置,包括在 sphinx-quickstart
时选中的那些值,可以自行定义其他的值。index.rst
:文档项目起始文件。接下来看看默认生成的内容:
make html |
然后直接在浏览器中打开 build/html/index.html
这个文件。
默认风格为 alabaster
,可以改成 ReadTheDocs 的风格: sphinx_rtd_theme
。
# -- Options for HTML output ------------------------------------------------- |
我们以一下文档为例:
This is a Title |
将之写入 example.rst
中,并修改 index.rst
为:
Welcome to 一本书's documentation! |
重新编译,这时文档已经改变。
ReadTheDocs 可是直接用于托管 sphinx 生成的网页文档。将之前的文档用 Git 管理,并推送到 Github,然后在 ReadTheDocs 中 Import a Project
即可。
另外,可以设置自定义域名:
readthedocs.io
,比如 onebook.qiwihui.com
Admin
-> Domains
中设置上一步添加的域名,开启 HTTPS,保存即可。过程很简单。
Sphinx 生成 PDF 的过程先将 rst 转换为 tex,再生成 PDF。这个过程遇到了比较多的坑,最后总结下来过程如下:
首先,安装 Tex 环境。在 Mac 上,推荐安装 MacTex
而不是 BasicTex
,对于新手来说 BasicTex 上需要自己处理很多依赖问题。完成后使用 tlmgr
更新 TexLive。
brew cask install mactex |
然后,在 con.py 中设置 latex_engine
和 latex_elements
两个参数,同时也可以设置 latex_documents
参数来设置文档。因为 ReadTheDocs 上只有 pdflatex 引擎,如果需要同时在 ReadTheDocs 和本地化都能顺利编译中文pdf的话,可以在 conf.py 中添加如下配置:
# -- Options for LaTeX output ------------------------------------------------ |
最后,编译:
make latexpdf |
make latexpdf
会完成 rst转换为 tex 并将 tex 生成 PDF,可以手动分开:
make latex |
在 build/latex
下可以查看到生成的 PDF 文档。
使用 fc-list
来获取字体信息,修改相应字体设置即可。
brew install fontconfig |
"! LaTeX Error: File '*.sty' not found."
类的问题:解决:使用 sudo tlmgr install
安装相应的包即可。
简单过了一下整个文档的流程,总体来说,Sphinx非常适合用来编写项目文档,reStructuredText 比起 Markdown 也有太多的优势,值得推荐。
from: qiwihui on: 3/1/2019
添加 gooogle analytics
]]>此清单可以指导你完成机器学习项目。主要有八个步骤:
显然,你应该根据你的需求调整此清单。
注意:尽可能自动化,以便你轻松获取新数据。
注意:尝试从领域专家那获取有关这些步骤的见解。
注意:
注意:
注意:
在测量泛化误差后不要调整模型:你会开始过度拟合测试集的。
由 Kaggle 主持的泰坦尼克号生存挑战赛是一项竞赛,其目标是基于一组描述乘客的变量,例如他的年龄,性别或乘客在船上的等级,来预测特定乘客是生存或死亡。
我一直在玩 Titanic 数据集,我最近在公共排行榜上获得了0.8134的准确度分数。当我写这篇文章时,我在所有 Kagglers 中排名前4%。
这篇文章将分享我的解决方案。
为了使本教程更具“学术性”以便任何人都能从中受益,我将首先从探索性数据分析(EDA)开始,然后我将遵循特征工程并最终呈现我设置的预测模型。
在这个 jupyter 笔记本中,我将在每个级别的管道中使用 Python。
本教程涉及的主要库是:
安装这些软件包的一种非常简单的方法是下载并安装 Conda,它是将以上所有包封装起来的发行版。此发行版适用于所有平台(Windows,Linux 和 Mac OSX)。
这是我作为博主和机器学习从业者的第一次尝试。
如果您对我所做的代码或假设有疑问,请不要犹豫,在下面的评论部分发表评论。
如果您对如何改进笔记本电脑也有建议,请联系我。
本教程可在我的 github 帐户中找到。
译者注:本翻译在 qiwihui 下。
希望你已经在计算机上设置了所有内容。让我们开始吧。
正如在不同的数据项目中,我们将首先开始深入研究数据并建立我们的第一个直觉。
在本节中,我们将做四件事。
我们稍微调整了这款笔记本的风格,以便画图居中。
from IPython.core.display import HTML |
导入有用的包。
%matplotlib inline |
有两个数据集:训练集和测试集。我们将使用训练集来构建我们的预测模型,用测试集来对其进行评分并生成输出文件以在Kaggle评估系统上提交。
我们将在本文末尾看到这个过程是如何完成的。
现在让我们开始加载训练集。
data = pd.read_csv('./data/train.csv') |
print data.shape |
(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,QPandas 允许您对数字特征进行高级简单的统计描述。这可以使用 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), |
看起来男性乘客更容易死亡。让我们绘制相同的图形,但用比例代替。
data.groupby('Sex').agg('mean')[['Survived', 'Died']].plot(kind='bar', figsize=(25, 7), |
性别变量似乎是一种歧视性特征。女性更有可能生存。
现在让我们将生存与年龄变量联系起来。
fig = plt.figure(figsize=(25, 7)) |
正如我们在上面的图表中看到并通过以下方式验证:
现在,我们看到:
年龄为男性乘客的生存:
年龄似乎没有对女性生存产生直接影响
以下小提琴情节证实,在遇到威胁的情况下,水手和船长遵守一条旧的行为准则:“妇女和儿童优先!”。
对吗?
现在让我们关注每位乘客的票价,看看它如何影响生存。
figure = plt.figure(figsize=(25, 7)) |
票价较低的乘客更容易死亡。换句话说,拥有更昂贵门票,因此更重要的社会地位的乘客似乎首先获救。
好的,这很好。 现在让我们将年龄,票价和生存结合在一张图表上。
plt.figure(figsize=(25, 7)) |
圆圈的大小与票价成正比。
在 x 轴上,我们有年龄,在 y 轴,我们考虑票价。
我们可以观察不同的集群:
事实上,票价与我们在下面的图表中看到的类别相关。
ax = plt.subplot() |
现在让我们看看登船地点如何影响生存。
fig = plt.figure(figsize=(25, 7)) |
似乎登船地点 C 的票价范围更广,因此支付最高价格的乘客是那些幸存的乘客。
我们也看到这种情况发生在登船地点 S 而不是登船地点 Q。
现在让我们停止数据探索并切换到下一部分。
在前一部分中,我们调查了数据并发现了一些有趣的相关性。
在这一部分中,我们将看到如何处理和转换这些变量,使数据变得可以通过机器学习算法进行管理。
我们还将创建或“设计”在构建模型时有用的其他功能。
我们将在此过程中看到如何处理文本变量(如乘客姓名)并将此信息集成到我们的模型中。
为了更加清晰,我们将代码分散在单独的函数中。
但首先,让我们定义一个打印函数,断言是否已经处理了一个特征。
def status(feature): |
启动机器学习问题的一个技巧是将训练集一起附加到测试集。
我们将使用训练集进行特征工程以防止信息泄漏。然后我们将这些变量添加到测试集中。
让我们加载训练集和测试集并将它们合在一起。
def get_combined_data(): |
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 |
在查看乘客姓名时,人们可能想知道如何处理它们以提取有用的信息。
如果你仔细看看这些第一个例子:
你会注意到每个名字都有一个称谓!这可能是一个简单的小姐(Miss.)或太太(Mrs.),但它有时可能像 Master,Sir 或 Dona 那样更复杂。在这种情况下,我们可以通过简单地解析称谓并提取标题并转换为二进制变量来引入有关社会地位的其他信息。
让我们看看我们将如何在下面的函数中执行此操作。
让我们先来看看在训练集中有什么不同的称谓。
titles = set() |
print titles |
set(['Sir', 'Major', 'the Countess', 'Don', 'Mlle', 'Capt', 'Dr', 'Lady', 'Rev', 'Mrs', 'Jonkheer', 'Master', 'Ms', 'Mr', 'Mme', 'Miss', 'Col'])
Title_Dictionary = { |
此函数解析名称并提取称谓。 然后,它将称谓映射到称谓类别。我们选择:
让我们运行一下!
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
变量缺少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.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 将帮助我们根据不同的标准估算缺失的年龄值。
查看中位年龄列,看看这个值如何根据 Sex
,Pclass
和 Title
组合在一起。
例如:
让我们创建一个函数,根据这些不同的属性填充 组合 中的缺失年龄。
def fill_age(row): |
combined = process_age() |
Processing age : ok
完美。失踪的年龄已被取代。
但是,我们注意到票价(Fare)中缺少1个值,登船位置(Embarked)有两个缺失值,而船舱位置(Cabin)有很多缺失值。我们稍后会处理这些变量。
我们现在处理名字。
def process_names(): |
此函数会删除 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 |
如你看到的 :
让我们通过在训练集上计算的平均票价估算缺失的票价值。
def process_fares(): |
此函数用平均值替换一个缺失的票价(Fare)值。
combined = process_fares() |
Processing fare : ok
def process_embarked(): |
此函数用最常用的 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 |
train_cabin, test_cabin = set(), set() |
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(): |
此函数将 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 |
def process_sex(): |
此函数将字符串值 male
和 female
分别映射到1和0。
combined = process_sex() |
Processing Sex : ok
def process_pclass(): |
此函数使用虚拟编码对 Pclass(1,2,3)的值进行编码。
combined = process_pclass() |
Processing Pclass : ok
让我们首先看看我们的数据集中不同的船票号码前缀
def cleanTicket(ticket): |
tickets = set() |
print len(tickets) |
37
def process_ticket(): |
combined = process_ticket() |
Processing Ticket : ok
这部分包括根据家庭的大小创建新变量(大小是我们创建的另一个变量)。
这种新变量的创建是在一个现实的假设下完成的:大家庭聚集在一起,因此他们比单独旅行的人更有可能获救。
def process_family(): |
此函数引入了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 |
在这一部分中,我们根据我们创建的特征建立统计模型。您可以将此模型视为一个盒子,它可以处理任何新乘客的信息,并决定他是否能够幸存。
有各种各样的模型可供使用,从逻辑回归到决策树,以及更复杂的模型,如随机森林和梯度提升树。
我们将使用随机森林。Random Froests 在 Kaggle 比赛中证明了很高的有效性。
有关为什么集合方法表现良好的更多详细信息,您可以参考这些帖子:
回到我们的问题,我们现在必须:
1.将组合数据集分成训练集和测试集。2.使用训练集建立预测模型。3.使用训练集评估模型。4.使用测试集测试模型,并生成并输出提交文件。
请记住,我们必须重复 2 和 3 直到达到可接受的评估分数。
让我们首先导入需要用到的函数包。
from sklearn.pipeline import make_pipeline |
为了评估我们的模型,我们将使用5折交叉验证(5-fold cross validation),因为它是在比赛排行榜中使用的指标。
为此,我们将定义一个小的评分函数。
def compute_score(clf, X, y, scoring='accuracy'): |
从组合数据集中恢复训练集和测试集是一项简单的任务。
def recover_train_test_target(): |
train, test, targets = recover_train_test_target() |
到目前为止,我们已经提出了30多个特征。这个数字非常大。
在完成特征工程时,我们通常倾向于通过选择捕获基本特征的“正确”数量的特征来减少维度。
事实上,特征选择带来许多好处:
基于树的估算器可用于计算特征重要性,而这些重要性又可用于丢弃不相关的特征。
clf = RandomForestClassifier(n_estimators=50, max_features='sqrt') |
让我们看看每个特征的重要性。
features = pd.DataFrame() |
features.plot(kind='barh', figsize=(25, 25)) |
<matplotlib.axes._subplots.AxesSubplot at 0x117ff2a10>
正如您可能注意到的那样,与 Title_Mr
,Age
,Fare
和 Sex
相关联非常重要。
与 Passenger_Id
也有重要的相关性。
现在让我们将我们的训练集和测试集转换为更紧凑的数据集。
model = SelectFromModel(clf, prefit=True) |
(891, 12)
test_reduced = model.transform(test) |
(418, 12)
好极了! 现在我们的特征减少了很多。
我们将看看我们是否会使用训练集的减少版或完整版。
logreg = LogisticRegression() |
for model in models: |
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. |
现在通过扫描超参数的几个组合来构建模型,我们可以生成一个输出文件以在 Kaggle 上提交。
output = model.predict(test).astype(int) |
我没有亲自上传基于模型混合的提交,但这是你可以这么做:
trained_models = [] |
predictions_df.to_csv('./predictions/blending_base_models.csv', index=False) |
为了获得良好的混合提交,基本模型应该是不同的,并且它们的相关性是不相关的。
在本文中,我们探讨了 Kaggle 带给我们的一个有趣的数据集。
我们浏览了数据科学管道的基本要点:
如果您想测试和使用它,可以将此博客下载为笔记本:我的 github repo
译者注:此中文翻译地址为: qiwihui 的 github repo
关于这一挑战的文章很多,所以显然还有改进的余地。
以下是我建议的后续步骤:
如果你能找到改善我的解决方案的方法,我会非常高兴。这可以让我更新文章,绝对给你信任。所以请随时发表评论。
重命名本地分支:
git branch -m old-name new-name |
若当前在 old-name
分支上,则可以省略 old-name
:
git branch -m new-name |
删除远程老分支:
git push origin :old-name |
推送新的本地分支,并设置本地新分支追踪远程分支:
git push origin -u new-name |
DNS 隧道,是隐蔽信道的一种,通过将其他协议封装在DNS协议中传输建立通信。
本文是在 Mac OS X 上实践的 DNS 隧道的一个记录,关于这个方法的原理,请具体参考 DNS Tunneling及相关实现。
准备一台 VPS 以及一个域名(比如 qiwihui.com
),在域名的 DNS 配置中添加两条记录:
Name | Type | TTL | Data |
---|---|---|---|
dns | A | 1h | vps-ip |
dt | NS | 1h | dns.qiwihui.com |
以 Debian 系统为例,安装:
apt update |
使用
$ iodined -f -c -P password 172.18.0.1 dt.qiwihui.com |
其中,password
是客户端和服务器之前的密码,172.18.0.1
为虚拟局域网的IP地址,可自行设定,但不要与现有网络重复了。此时,服务端已经就绪。
iodine
tuntap
brew cask install tuntap |
iodine
因为官方没有提供 Mac OS X 的可执行文件,需要从源码编译,或者使用我已经设置好的 Homebrew tap 进行安装。从源码编译:
wget -c http://code.kryo.se/iodine/iodine-0.7.0.tar.gz |
或者使用 Homebrew:
brew tap qiwihui/core |
使用:
$ sudo iodine -f -P password dns.qiwihui.com dt.qiwihui.com |
此时,客户端配置完成。
在本地尝试 ping 172.18.0.1 即可:
$ ping 172.18.0.1 |
这时,只要通过这个 DNS 隧道就可就传递其他数据了。
当防火墙限制了一些网站的访问,但是能进行 DNS 查询时,可使用这种方法进行绕过,比如在公司,又或者在某些国家,犯罪分子也常用这中方式进行内网数据传出。
from: shaohuihu on: 4/25/2019
mac上使用 iodined: open_tun: Failed to open tunneling device: No such file or directory楼主mac上如何解决的TUN/TAP?
from: qiwihui on: 4/25/2019
brew cask install tuntap
from: shaohuihu on: 4/25/2019
from: qiwihui on: 4/25/2019
@shaohuihu 在直连模式下可看youtube 720p, 中继模式下没试过,看相关文章速度也是在其他几种DNS tunneling中是最快的。
from: shaohuihu on: 4/26/2019
from: shaohuihu on: 4/28/2019
from: qiwihui on: 4/28/2019
@shaohuihu 这个问题原因很多,可是试试重置网卡
sudo route -n get 172.18.0.1 |
sudo ifconfig tun0 down |
from: qiwihui on: 4/28/2019
可以参考:
from: badtoken on: 5/9/2019
@shaohuihu 这个问题原因很多,可是试试重置网卡
- 确定使用的网卡
$ sudo route -n get 172.18.0.1
route to: 172.18.0.1
destination: 172.18.0.0
mask: 255.255.255.224
gateway: 172.18.0.2
interface: tun0
flags: <UP,GATEWAY,DONE,STATIC,PRCLONING>
recvpipe sendpipe ssthresh rtt,msec rttvar hopcount mtu expire
0 0 0 0 0 0 1130 0
- 重置
sudo ifconfig tun0 down
sudo ifconfig tun0 up
你好,请问有win的解决方案吗。我现在也是ping不通,win客户端ping隧道的服务端内网地址,死活不通(ping隧道服务端外网地址是通的)
from: qiwihui on: 5/9/2019
@badtoken 应该也有吧,比如这个 https://kb.wisc.edu/helpdesk/page.php?id=6653 ,具体我也没有试过
]]>同样,需要在服务器端编译安装 hans
:
wget -c https://github.com/friedrich/hans/archive/v1.0.tar.gz |
编译完成后会产生 hans
执行程序,按以下命令以 root
启动,程序会进入 Deamon 模式。如果要看到输出,可以加上 -f
参数。
./hans -s 10.1.2.0 -p password |
其中,password
为设置的密码。然后使用 netstat -rn
可以看到多了一个 tun0
设备
$ netstat -rn |
在 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 |
理论上这时服务器 IP 是无法 ping 通了:
$ ping <server-ip> |
此时,就建立了一条从本地到服务器的 hanstunnel tunnel 了。
现在只需要将本地 ss-local
的配置中的 server
参数改为 tun0
的 gateway 地址(本例为10.1.2.1
)即可,其他不需要做任何修改。
在服务器网卡上抓包可以不断看到 ICMP 的 `echo request` 和 `echo reply` 包,在 tun0
上可以看到实际的数据包。
$ tcpdump -ni ens3 |
以上就是使用 TCP over ICMP 的方法进行数据传输的配置过程。
我发现 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: |
如果你从本地计算机构建和部署博客,并且永远不会删除(或意外丢失)你的博客源代码,你可能永远不会遇到此问题。当你从未被擦除的工作空间执行此操作时,则存在具有完整历史记录的文件夹 .deploy_git
,并且 hexo deploy
仅推送实际修改的那些文件。当你迁移到像 Travis CI
这样的 CI 服务器时,这就变了,因为它使用干净的工作区和仓库的新克隆执行构建。在这种情况下,.deploy_git
文件夹根本不存在,将从头开始重新创建。
我发现解决方案非常简单。以前我负责部署的 .travis.yml
文件部分看起来像这样:
清单2. 以前的 Travis CI
部署配置
deploy: |
只要我将更改推送到 develop
分支,它就会触发 hexo deploy
。在这种情况下,它最终创建了一个新的 .deploy_git
文件夹并强制将初始提交推送到 GitHub 仓库。然后,我做了一个小改进 - 我创建了一个简短的 bash 脚本。
清单3. 部署博客使用的脚本
#!/bin/bash |
这个脚本完全按照它在注释中所说的那样做:
master
分支从远程存储库克隆到 .deploy_git
以获取现有提交历史记录。.deploy_git
中删除所有非 git 对象存储库文件,因此从 public
文件夹复制文件将跟踪已删除的文件。hexo deploy
命令。最后,这是在引入部署bash脚本后的部署配置部分:
清单4. 当前的 Travis CI
部署配置
deploy: |
由于这个解决方案,我能够保留站点更新的历史记录,并跟踪使用给定站点更新实际修改的文件的更改。
我希望你发现这篇文章很有用。它描述了 Hexo + Travis CI + GitHub 用例的解决方案,但它可以解决从 CI 服务器环境运行时其他类似静态站点生成器可能遇到的问题。
sed
会和 GNU 上不太一致,在此记录。sed
不可忽略备份扩展在 OS X 上进行文本替换时,必须要指定备份扩展,即使扩展可以为空。比如:
sed -i 's/foo/bar/g' target |
上面这行代码,可以在 GNU 上运行,作用是将 foo
替换为 bar
,并且直接修改目标文件(-i
)。但是如果在 OS X 上,这行命令会报错:
$ sed -i 's/foo/bar/g' target |
原因是在 OS X 上,sed 命令必须指定备份的扩展格式:
$ man sed |
所以需要修改为
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 |
又或者,使用其他命令:
perl -i -pe's/foo/bar/g' target |
Apex
域名(qiwihui.com),二级域名(www.qiwihui.com)以及开启 HTTPS。最后,所有指向 www.qiwihui.com
的请求将会被重定向至 https://qiwihui.com
。支持的自定义域名类型 | 域名例子 |
---|---|
www subdomain | www.example.com |
one apex domain & one www subdomain | example.com & www.example.com |
apex domain | example.com |
custom subdomain | blog.example.com |
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/projectname ) | project.example.com |
公司拥有的 Project Pages 站 | orgname.github.io/projectname | 自动重定向到 Organization Pages 站自定义域名的子目录(org.example.com/projectname ) | project.example.com |
在项目 Settings
中,找到 GitHub Pages
这一区域,选择 Source
为对应的要部署的分支,这里我选择 gh-pages branch
:
其中,选择 master branch
会视 /README.md
为 web 的 index.html
,选择 master branch /docs folder
会视 /docs/README.md
为 web 的 index.html
。
在 Custom domain
中添加自己的域名并保存:
或者,在项目分支中添加 CNAME
文件,CNAME
文件的内容为
qiwihui.com |
这里推荐第二种,尤其对于有设置 CI 的项目,因为 CI 上将第一种设置覆盖。这一步是比较重要却又容易忽视的一步:
qiwihui.com
,那么 www.qiwihui.com
会被重定向到 qiwihui.com
;www.qiwihui.com
,那么 qiwihui.com
会被重定向到 www.qiwihui.com
;这里我选择重定向到 www.qiwihui.com
,所以设置为 qiwihui.com
为了能设置Apex
域名,需要在 DNS 中配置 A 记录指向 github 的 IP:
185.199.108.153 |
同时,设置 CNAME
记录将 www.qiwihui.com
指向 qiwihui.github.io
,即 <你的 github 用户名>.github.io
。对于公司来说,这个地址是 <公司名称>.github.io
。
以下是设置好之后的 DNS 记录情况:
$ dig +noall +answer qiwihui.com |
$ dig www.qiwihui.com +nostats +nocomments +nocmd |
勾选 Enforce HTTPS
Github 会自动保持 HTTPS 证书的有效。
当给项目设置 Pages 时,一般都已经有一个个人或者公司的 Pages 了,如果没有,就可以按以上的过程添加。如果已经设置了,则只需要很简单的两步即可:
以下以个人项目 [qiwihui/fullstackpython.com](https://github.com/qiwihui/fullstackpython.com)
,设置地址为 fullstackpython.qiwihui.com
CNAME
文件指向 fullstackpython.qiwihui.com
:fullstackpython.qiwihui.com
指向 qiwihui.github.io
,即 <你的 github 用户名>.github.io
。对于公司来说,这个地址是 <公司名称>.github.io
。一段时间后即可。
原文:How to read Apple’s developer documentation
对于很多人来说,这篇文章听起来很奇怪,因为我们已经习惯了 Apple 的 API 文档的工作方式,因此我们精神上已经经过调整以快速找到我们想要的东西。
但这是一个有趣的事实:去年我最热门的文章请求之一是帮助人们真正阅读 Apple 的代码文档。您如何找到您需要的 iOS API,如何浏览所有材料以找到您真正想要的内容,以及您如何深入了解为什么事情按照他们的方式工作?
所以,如果你曾经寻求帮助来理解 Apple 的开发者文档,首先我要让你知道你并不孤单 - 许多人都在努力解决这个问题。但其次,我希望这篇文章会有所帮助:我会尽力解释它的结构,它有什么好处(以及不好的地方),以及如何使用它。
更重要的是,我将向您展示经验丰富的开发人员寻找额外信息的位置,这些信息通常比Apple的在线文档更有价值。
任何书面的 API 文档通常采用以下五种形式之一:
粗略地说,苹果公司第一点做了很多,其次是第二点和第三点,第四点很少,第五点几乎没有。
所以,如果你正在寻找“如何用 Y 做 X ”的具体例子,你最好从我的 Swift 知识库开始 - 这正是它的用途。
了解 Apple 的文档解决的问题,可以帮助您从中获得最大收益。它并不是一个结构化的教程,它不会向您介绍一系列概念来帮助您实现目标,而是作为 Apple 支持的数千个 API 的参考指南。
Apple的在线文档位于 https://developer.apple.com/documentation/ ,虽然您能在 Xcode 中使用本地副本,但我会说大多数人使用在线版本只是因为他们可以更容易地找到内容。
绝大多数 Apple 的文档都描述了接口,而这正是大多数时候你会看到的。我想使用一个实际的例子,所以请先在您的网络浏览器中打开https://developer.apple.com/documentation/ ,这是所有Apple开发者文档的主页。
您会看到所有 Apple 的 API 分为 App Frameworks
或 Graphics and Games
等类别,您已经看到了一件重要的事情:所有深蓝色文本都是可点击的,并会带您进入特定框架的API文档。是的,它使用相同的字体和大小,没有下划线,说实话,深蓝色链接和黑色文本之间没有太大区别,但你仍然需要留意这些链接 - 有很多他们,你会用它们来深入挖掘主题。
现在请从 App Frameworks
类别中选择 UIKit
,您将看到它的功能(为iOS创建用户界面)的简要概述,标有“重要
”(Important
)的大黄色框,然后是类别列表。那些黄色的盒子确实值得关注:虽然它们经常被使用,它们几乎总能阻止你犯下根本错误,这些错误导致出现奇怪的问题。
此页面上重要的是共同描述 UIKit
的类别列表。这是人们经常迷路的地方:他们想要了解像 UIImage
这样的东西,所以他们必须仔细查看该列表以找到它可能出现的合适位置。
在这种情况下,您可能会查看“资源管理”(Resource Management
),因为它的副标题“管理存储在主可执行文件之外的图像,字符串,故事板和 nib 文件”听起来很有希望。但是,您会感到失望 - 您需要向下滚动到 “图像和 PDF”(Images and PDF
)部分才能找到 UIImage
。
这就是为什么我谈过的大多数人只是使用自己喜欢的搜索引擎。他们输入他们关心的类,并且 - 只要它有“UI”,“SK”或类似的前缀 - 它可能是第一个结果。
不要误会我的意思:我知道这种做法并不理想。但是面对搜索一个类或者去 https://developer.apple.com/documentation/ ,选择一个框架,选择一个类别,然后选择一个类,第一个就是更快。
重要提示:无论您选择哪种方法,最终都会在同一个地方,所以只做最适合您的方法。现在,请找到并选择 UIImage
。
一旦选择了您关心的类,该页面就有四个主要组件:概述,版本摘要,接口和关系。
概述是“API的文本描述,描述了它应该做什么以及一般指导用例”,我之前提到过 - 我要求你选择 UIImage
,因为它是文本描述何时运行良好的一个很好的例子。
当它是我第一次使用的类时,特别是如果它刚刚推出时,我通常会阅读概述文本。但是对于其他一切 - 我之前至少使用过一次的任何类 - 我跳过它并尝试找到我所得到的具体细节。请记住,Apple 文档确实不是一种学习工具:当您考虑到特定目的时,它最有效。
如果您不总是为所选 Apple 平台的最新版本开发,则版本摘要 - 页面右侧的侧栏 - 非常重要。在这种情况下,您将看到 iOS 2.0 +
,tvOS 9.0+
和 watchOS 2.0+
,它告诉我们何时 UIImage
类首次在这三个操作系统上可用,并且它仍然可用 - 如果它已被弃用(不再可用)你会看到像 iOS 2.0-9.0
这样的东西。
此页面上的实际内容 - 以及作为 Apple 框架中特定类的主页的所有页面 - 都列在“主题”标题下。这将列出该类支持的所有属性和方法,再次分解为使用类别:“获取图像数据”,“获取图像大小和比例”等。
如果您选择的类具有任何自定义初始化方法,则始终会首先显示它们。 UIImage
有很多自定义初始化方法,你会看到它们都被列为签名 - 只是描述它所期望的参数的部分。所以,你会看到这样的代码:
init?(named: String) |
**提示:**如果您看到 Objective-C
代码,请确保将语言更改为 Swift
。您可以在页面的右上角执行此操作,也可以在重要的 iOS 测试版引入更改时启用 API 更改选项。
记住,初始化方法写成 init?
而不是 init
的是容易出错的 - 它们返回一个可选项,以便在初始化失败时它们可以返回 nil
。
在初始化器的正下方,您有时会看到一些用于创建类的高度专业化实例的方法。这些不是 Swift 意义上的初始化器,但它们确实创建了类的实例。对于 UIImage
,你会看到这样的事情:
class func animatedImageNamed(String, duration: TimeInterval) -> UIImage? |
class func
部分意味着你将使用 UIImage.animatedImageNamed()
方式调用。
在初始化程序之后,事情变得有点不那么有条理:你会发现属性方法和枚举自由混合在一起。虽然您可以滚动查找您要查找的内容,但我可以认为大多数人只需要 Cmd + F
在页面上查找文字就可以了!
有三点需要注意:
UIImage
包含嵌套的枚举 ResizingMode
。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
类别中找到它。
这不是一个复杂的方法,但它确实展示了这些页面的重要部分:
class func animatedResizableImageNamed
- 然后是方法页面标题中显示的形式(animatedResizableImageNamed(_:capInsets:resizingMode:duration:)
),以及方法页面的声明部分中的形式。iOS 6.0
中引入。因此,虽然主要的 UIImage
类从第1天开始就已存在,但这种方法是在几年后推出的。UIImage.ResizingMode
,你将去哪里取决于你是否点击了“UIImage”或“ResizingMode”。 (提示:您通常需要单击右侧的那个。)Discussion
)部分详细介绍了此方法的具体使用说明。这几乎总是 - 每个页面中最有用的部分,因为在这里您可以看到“不要调用此方法”或“小心…”See Also
部分,但这有点受欢迎 - 这里只是我们在上一页的方法列表。现在,UIImage
是一个老类,并没有太大变化,因此它的文档处于良好状态。但是一些较新的 API - 以及许多没有像 UIKit
那样被喜欢的旧 API - 仍然记录不足。例如,来自 SceneKit
的 SCNAnimation
或来自 UIKit
的 UITextDragPreviewRenderer
:两者都是在 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”,你会发现:
/** |
这只是一个开始。 是的,该类及其所有内部都有文档,包括用法说明,默认值等。 所有这一切都应该在在线文档中,但无论出于什么原因它仍然没有,所以要准备好查找代码作为一个有用的补充。
此时,您应该能够查找在线文档以获取您喜欢的任何代码,并查找头文件注释以获取额外的使用说明。
但是,在准备好面对全部 Apple 文档之前,还有两件事需要了解。
首先,您经常会遇到标记为“已归档(archived”)”,“遗留(“legacy”)”或“已退休(“retired)”的文档 - 即使对于相对较新的事物也是如此。当它真的老了,你会看到诸如“这篇文章可能不代表当前发展的最佳实践”之类的消息。下载和其他资源的链接可能不再有效。“
尽管 Apple 是世界上最大的公司之一,但 Apple 的工程和 devpubs 团队几乎没有人员 - 他们不可能在保留所有内容的同时覆盖新的 API。因此,当你看到“存档”文档或类似文件时,请运用你的判断:如果它在某个版本的 Swift 中至少你知道它最近是模糊的,但即使不是,你仍然可能会发现那里有很多有价值的信息。
其次,Apple 拥有一些特别有价值的出色文档。这些都列在 https://developer.apple.com 的页脚中,但主要是人机界面指南。这将引导您完整地为 Apple 平台设计应用程序的所有部分,包括说明关键点的图片,并提供大量具体建议。即使这个文档是构建 iOS 应用程序时最重要的一个,但很少有开发人员似乎已经阅读过它!
我之前曾写过关于 Apple 文档的问题 - 我担心那里没有鼓励,但至少如果你在努力,它可能会让你觉得不那么孤单。
幸运的是,我有很多可能更有用的材料:
您认为阅读Apple文档最有效的方法是什么? 在Twitter上发送你的提示:@twostraws。
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。
分成三步:
Demo中使用的是"结巴"中文分词的 C++ 版本 yanyiwu/cppjieba。将其中的 include/cppjieba
和依赖 limonp
合并,并加入 dict
中的 hmm_model
和 jiaba.dict
作为基础数据,并暴露 JiebaInit
和 JiebaCut
接口:
// |
以及
// |
目录如下:
$ tree iosjieba |
接下来开始在项目中集成。首先创建一个空项目 iOSJiebaDemo
,将 iosjieba
加入项目中。
单页应用 | SwiftJiebaDemo | 添加 SwiftJiebaDemo |
---|---|---|
添加 iosjieba:
见代码: https://github.com/qiwihui/SwiftJiebaDemo/commit/caeb6c2f9fb005a9bc518ee67890814481676807
这个过程是将 C++ 的接口进行 Objective-C 封装,向 Swift 暴露。这个封装只暴露了 objcJiebaInit
和 objcJiebaCut
两个接口。
// |
// |
见代码: https://github.com/qiwihui/SwiftJiebaDemo/commit/7d196bb2c33280a4f419be21b47961a521618221
在 Swift 中调用 Objecttive-C 的接口,这个在官方文档和许多博客中都有详细介绍。
{project_name}-Bridging-Header.h
头文件,即 SwiftJiebaDemo_Bridging_Header_h
,引入之前封装的头文件,并在 Targets -> Build Settings -> Objective-C Bridging Header
中设置头文件路径 SwiftJiebaDemo/SwiftJiebaDemo_Bridging_Header_h
。// |
.m
改为 .mm
: iosjiebaWrapper.m
改为 iosjiebaWrapper.mm
。见代码:https://github.com/qiwihui/SwiftJiebaDemo/commit/94852b1357b0a0a4b2e8b92384fbdb1b16c80ed8
使用时需要先初始化 Jiaba
分词,然后再进行分词。
class Classifier { |
控制台输出结果:
可以看到,测试用例 小明硕士毕业于中国科学院计算所,后在日本京都大学深造
经过分词后为〔拼音〕["小明", "硕士", "毕业", "于", "中国科学院", "计算所", ",", "后", "在", "日本", "京都大学", "深造"]
,完成集成。
见代码: https://github.com/qiwihui/SwiftJiebaDemo/commit/bc42e1312dff6a9f7171cc69403136bc8a82204c
由于自己对于编译链接原理不了解,以及是 iOS 开发初学,因此上面的这个过程中遇到了很多问题,耗时两周才解决,故将遇到的一些问题记录于此,以便日后。
"cassert" file not found
将 .m
改为 .mm
即可。
compiler not finding <tr1/unordered_map>
设置 C++ Standard Library
为 LLVM libc++
参考: mac c++ compiler not finding <tr1/unordered_map>
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++
use of unresolved identifier
这个问题在于向项目中加入文件时,Target Membership
设置不正确导致。需要将对于使用到的 Target 都勾上。
相关参考: Understanding The “Use of Unresolved Identifier” Error In Xcode
CoaoaPods
是一套整体解决方案,我们在 Podfile
中指定好我们需要的第三方库。然后 CocoaPods
就会进行下载,集成,然后修改或者创建我们项目的 workspace
文件,这一系列整体操作。
相比之下,Carthage
就要轻量很多,它也会一个叫做 Cartfile
描述文件,但 Carthage
不会对我们的项目结构进行任何修改,更不多创建 workspace
。它只是根据我们描述文件中配置的第三方库,将他们下载到本地,然后使用 xcodebuild
构建成 framework
文件。然后由我们自己将这些库集成到项目中。Carthage
使用的是一种非侵入性的哲学。
另外 Carthage
除了非侵入性,它还是去中心化的,它的包管理不像 CocoaPods
那样,有一个中心服务器(cocoapods.org),来管理各个包的元信息,而是依赖于每个第三方库自己的源地址,比如 Github。
(可选)使用 taobao ruby-china 源替换默认 gem 源: gem source blabla..
$ gem sources -l |
sudo gem install cocoapods
(可选)切换 pod 源
$ pod repo |
或者$ 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
如果Podfile文件中有
source 'https://github.com/CocoaPods/Specs.git' |
也需要把它换成repo的源,否则依然是使用GitHub源
cd <project_folder>
pod init
编辑 Podfile, example
# 平台,必需 |
版本支持: - `>`, `>=`, `<`, `<=` - `~>`: **up to** next major | minor | patch - `:path` 本地绝对路径 - `:git` git项目地址,还可使用 `:branch`, `:tag`, `:commit`
pod install
pod install [package_name]
: 安装特定版本的 podspod update [package_name]
: 升级 pods 到最新版本brew install carthage |
编辑 Cartfile
,比如 SwiftyJSON
github "SwiftyJSON/SwiftyJSON" |
carthage update [–platform ios]
$ carthage update |
Carthage
目录下:
$ tree -L 3 Carthage/ |
添加生成的文件: 项目 “General” -> “Linked Frameworks and Libraries” -> 将 Carthage/Build/iOS
中的 .framework
文件添加到项目中
“Build Phases” -> “+” -> “New Run Script Phase”
添加这个 Run Script 的作用是为了让运行时能够找到这个动态库。还可以将 Carthage 所集成的第三方库生成的符号文件添加到项目中,这样我们在调试的时候,就可以步入第三方库内部的代码:Build Phrases
-> New Copy Files Phrase
,将 Carthage/Build/iOS 目录中的 SwiftyJSON.framework.dSYM
符号文件拖动进来
做为一个程序员,在保持自己技术水平同时,应当不断地学习,总结和思考新的技术,才能在这个行业不至于被淘汰。互联网寒冬来临之时,对于还不能掌握形式的我也只能先增强自身能力以期减少这个寒冬带来影响。
从2017年年底开始涉足机器学习深度学习,先是学习然后在工作项目中实践,至今一年有余。机器学习和深度学习从吴恩达的《机器学习》和《深度学习》课程开始,吴恩达的课程简洁易懂,逻辑清楚,虽然都是英文,但也没有太大关系。不过这两个课程注重算法胡实现,缺少项目,因此可以需要 fast.ai 的机器学习和深度学习课程来巩固掌握。主要使用的框架是 Scikit-learn
,Keras
和 Tensorflow
,这三个掌握不足。之后在公司安全项目中使用了一些分析和算法,对于机器学习和深度学习的理解和应用也只能说是皮毛。
年初(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 的体验轻便,方便梳理,是迄今用过的最舒服的项目管理工具。
Fork并修改了一个 Chrome 扩展:Octo Previewer,用来实时预览 Github 上的 PRs,Issues,Gists 的 Markdown 评论。
Trello 卡片 上记录这个过程。理论上是可行的,只是在有些工具上卡住了前进的路线。
这个基于极路由的翻墙项目没有持续维护,一则自己对于 lua 和 前端不熟悉,开发起来困难,二则我自己的极路由在搬家之后就没有了,再者极路由似乎大势已去,所以这个项目基本也就三四个月更新一次。这个项目给我带来的最大感触就是维护开源项目真的不容易。前些天看到 kalasoo 的文章《开源即责任》也是有感触。
2018年上半年忙与机器学习和项目,几乎没有读什么书,下半年(9月)开始入坑科幻小说。《三体》是一部好的科幻小说,但在国内《三体》造就的伪科幻迷群体以及随之而来的各种各种视为真理的概念着实令人难受,于是入坑科幻洗洗脑。科幻类基本都是名篇,看过之后,对于《三体》所带来的震撼减轻了不少,但是我更加佩服大刘了。
关于素数的两本书,主要是因为菲尔兹奖与阿贝尔奖双料得主迈克尔·阿蒂亚爵士讲述他对黎曼猜想的证明,这次事件间引起了我详细了解黎曼猜想的兴趣,为此还专门购买了经典教材《复变函数论方法》,期待进一步了解。这两本书适合一起看,互相补充。不幸的是,迈克尔·阿蒂亚爵士,于 2019 年 1 月 11 日上午逝世,享年八十九岁。他最后的尝试,令人佩服!
「我一直在尝试理解事物运行的原因。我对不能理解背后原理的公式不感兴趣。我总是试图挖掘事物背后的原理,所以如果我有一个公式,我就会去理解它为什么是这样。理解是一个非常困难的概念。人们认为数学的开始是你写下一个定理并附带证明。这不是开始,这是结束。对我来说,数学的创造性在你动手在纸上写字之前,在你尝试写公式之前。你描绘不同的事物,在脑海中反复思考。你尝试的创造,就像音乐家试图创作音乐,或诗人写诗一样。这个过程没有可以遵循的规律,你必须找到自己的方法。但到了最后,就像作曲家必须写下乐谱一样,你必须把它写下来。但最重要的一步是理解。证明公式本身可能不能让你理解。你可能有一个很长的证明,但到最后却不知道它为何是这样。但为了理解,你必须找到类似于直觉的能力,你必须感受它。」
——迈克尔·阿蒂亚爵士
今年看的非技术书基本都是在上下班坐着公交看的,积少成多,也就多了。今年的体会就是读书如抽丝。多读书涨见识。
平常看电影电视有时候喜欢写着代码,可能也不太记得太多,讲讲喜欢的吧。
年初买了 Nitendo Switch,不过没有太多时间花在玩游戏上,以至于《塞尔达传说:荒野之息》和《超级马里奥:奥德赛》都没有通关,只是偶尔用来玩玩 AoV,不过 NS 的体验确实非常不错,不论是个人还是联机,值得推荐。个人觉得游戏在于娱乐放松,若影响正常工作和情绪则视为不可,曾记得大学玩游戏还冲别人发过脾气也是太年轻。
我在王者荣耀S13赛季又重新玩了一段时间,每天三五局,升个一星两星就停止,掉个一星两星也停止,就这样达到了个人历史最好成绩,不过这个过程给我带来的影响也不少。一个是我发现在白天的队友比较坑,而到了夜晚会好很多,估计是小学生都去睡觉了,所以我每到十一二点就开始玩,有时会因为连胜而玩到很晚,以致停止一段时间之后仍不能好好早睡,严重影响精神状态;二是一局结束又开一局,犹如赌博,赢则更想赢,输则不服气,往往计较于一城得失,实在是影响心气。这个游戏不能投入太多时间。
很不错的游戏,只是都还没有玩通关,需要补上。
就和养儿子一样(虽然作者说的是丈夫),总是担心种种,吃没吃好,有没有被雨淋,有没有被欺负,路上还有钱吗。这就是为人父母的体验吧。
接触不多,不过和《王者荣耀》这种是一样的感受。看过一些游戏主播的视频,佩服一个LOL职业玩家转吃鸡的主播,在战场上沉稳,有判断。
已经弃坑了,一则是因为满级了,并没有刷成就的习惯,同时也因为没有太多时间出门,二则官方一直没有太多的积极活动,每次就是换一个地方刷牌子,再则在手机升级到 iOS 11 之后,官方推出了 Ingress Prime,游戏体验万分糟糕,之前的版本却不升级,遂弃。Ingress玩了四五年,也疯狂过,但是还是离开吧。
今年一大事就是和老婆领证了,还没有办酒席,两人都商量着简办,请亲戚吃个饭就感觉已经很隆重了。参加了几次同学的婚礼,无非接亲,闹新郎,宣个誓言,在众亲友面前挥泪感恩,对于这样重复的婚礼,也没有太大的兴趣。梦想人生的婚礼应当刺激,可以在远山,或者,招待一群好友,准备一堆食物,准备一段给大家的表演,大家也可以上台表演,发表自己的感想。
11月,迎来家庭另一个成员,一只英国短毛蓝猫,取名“狗狗”。虽然有时会觉得这和取狗蛋差不多,也不知道会不会让其他猫看不起。第一次养猫,总是担心这担心那,生怕他生病了,像极了父母。久了我也发现他还是很粘人的,不知道年后给他找了女朋友之后会不会还是这么粘人。
十月,堂弟找我借钱周转,并承诺四天还,鉴于有承诺而且是亲戚,也就爽快借了。第一次到期没任何动静,还钱还是再接着一句话也没说,我问了才换来一句“明天,明天一定换”,接着第二次就是“最迟不会超过后天下午”,然后是到期还了一半,我没收,要全款,就继续拖着,一星期没动静,问了几句就变成在外地学习没开手机,“明天下午三点之前”,然后“再给一个小时”,“我手机没电了”。最后钱是还了,但是我对他的信任一点都没有了,直接拉黑!这件事让伙我认识到一点:在钱面前,亲戚的嘴脸也是令人恶心的。我借钱是因为我信任,却不代表你可以践踏。想起一个村民中奖却被村里人借得倾家荡产,村里人却觉得他有钱为什么就不能借点,可怕。
绝不是flag。
人生需要有目标,需要一个积极乐观的心态,和一群志同道合的同伴。
当Jupyter开始一个内核的时候,它会传递它一个连接文件。它指定了如何与前端开始通信。
以下是实践:
conda create -n py365400 python=3.6.5 jupyter ipykernel |
在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 |
在内核文件夹下,现在会使用三种类型的文件。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 |
当内核开始的时候将会传入一个连接文件的路径,这个文件只对当前用户可用,会包含类似下面的一个JSON字典。
{ |
transport
, ip
和设定了该使用 ZeroMQ 绑定的五个_port。比如 shell 套接字的地址应该是:tcp://127.0.0.1:57503
。在每个内核开始的时候会指定随意的端口。signature_scheme
和 key
用来加密信息,因此系统的其他用户不能发送代码来运行内核。
现在我需要自己定义一个内核,这个内核可以执行我们定义的逻辑。
这是简单的重用 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__': |
现在创建一个JSON的内核说明文件,然后通过 jupyter kernelspec install </path/to/kernel>
。将你的内核模块放在Python可以导入的地方,一般是当前目录(做测试)。最后,你可以使用 jupyter console --kernel <mykernelname>
来运行你的内核。
例子:
ls echo/ |
echokernel.py
:
from ipykernel.kernelbase import Kernel |
kernel.json
:
{ |
安装
$ jupyter kernelspec install echo --user |
这里,只为当前用户添加这个kernel。
jupyter notebook |
选择新创建的内核创建 notebook,并运行代码。
echokernel
模块:[I 15:48:27.754 NotebookApp] Kernel started: 77759cfa-db55-4b70-be23-c14d69f8d87d |
需要将 echokernel.py
放置在 python PATH 中 ,这样在执行命令时才能访问到。
$ jupyter kernelspec help |
jupyter kernelspec uninstall echo |
curl -X<REST Verb> <Node>:<Port>/<Index>/<Type>/<ID>
curl -XPOST 'localhost:9200/bank/_search?pretty' -d ' |
bool must
: 所有的查询都必须为真bool should
: 只要有一个查询匹配bool must_not
: 查询列表中的的所有查询都必须都不为真
_score
: 指定的搜索查询匹配程度的一个相对度量。得分越高,文档越相关,得分越低文档的相关度越低。Elasticsearch中的所有的查询都会触发相关度得分的计算。对于那些我们不需要相关度得分的场景下,Elasticsearch以过滤器的形式提供了另一种查询功能。
过滤器在概念上类似于查询,但是它们有非常快的执行速度,这种快的执行速度主要有以下两个原因:
通常情况下,要决定是使用过滤器还是使用查询,你就需要问自己是否需要相关度得分。如果相关度是不重要的,使用过滤器,否则使用查询。
curl -XPOST 'localhost:9200/bank/_search?pretty' -d ' |
doc[‘my_field’].value和_source.my_field之间的不同:
你参加一个游戏,在你面前有4张1000万支票,其中一张是真的。游戏开始,你选了一张,之后主持人在剩下的3张里,选择一个展示出来,验证后发现是假的。
问题:请分情况理性分析,此时,你的参赛权的价格
回答:请用下面两种方法分别作答
方式1(理论推导)
情况1: 不能重新选择时获奖的概率是1/4情况2: 可以重新选择时是3/8理由:
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 |
结果:
>> python bh.py |
与理论计算一致
三门问题(Monty Hall Problem)电影《决胜21点》
写好博客之后,部署总会占去一段时间:编译、部署、推送和检查。手动部署多了也就烦了,一则容易出错,比如把 master 分支用 gh-pages 分支覆盖了,二则劳动是重复的,重复的劳动就应该自动化去解决。
使用 GitHub Webhooks 实现自动部署,这就需要有一台服务器,在服务器上启动服务接受 Github 的回调,然后拉取代码,编译,将编译后的代码要么部署在同一台服务器上,要么推送到代码 gh-pages 分支上。前者额外需要编写服务,配置博客 Nginx,可能还需要配置 HTTPS,以及对服务器进行加固,总归就是需要额外的更多东西来支持。所以还是觉得用已经存在的线上自动化服务方便一些(其实就是懒)。
持续集成(Continuous Integration,CI)的 SaaS 服务,好处不言而喻。
gem install travis |
language: node_js |
ERROR Deployer not found: git
npm install hexo-deployer-git --save
往 Github 仓库中提交代码是需要认证的,不管是用用户密码,Access Token还是SSH key。一种方法是直接将认证写在 .config.yml
中,不是说不行,是太年轻。好在 Travis CI 不仅支持加密文件,也支持加密 Keys,这就为认证这一块扫清了道路,我决定使用 OAuth 认证 Git 来提交代码到仓库中。
操作步骤:
生成 Github Personal Access Token;
使用 Travis CI 命令行加密 Personal Access Token;
travis encrypt GH_TOKEN=<token> --add |
在 .travis.yml
中添加配置
before_install: |
env: |
胜利完成!
V2Ray 是一个模块化的代理软件包,它的目标是提供常用的代理软件模块,简化网络代理软件的开发。
简单说 v2ray 就是翻墙代理软件(但不止于软件,是一个平台)。V2RayX 就是 macOS 下一个简单的 v2ray 的GUI程序。
macOS上强大的包管理工具,类似于Ubuntu的apt。
安装:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" |
使用,比如下载 curl
:
brew install curl |
不知道为啥,官方的Homebrew Formula不接受 v2ray 源,所以只能自己写了,见 qiwihui/homebrew-v2ray。
安装:
brew tap qiwihui/v2ray |
使用:
首先,需要配置 /usr/local/etc//v2ray.config.json
;其次,配置v2ray登录时自动开启:
brew services start v2ray-core |
或者,可以手动运行:
v2ray -config=/usr/local/etc//v2ray.config.json |
我向官方 Homebrew-Cask 提交了一个Formula,可以直接使用如下命令安装
brew cask install v2rayx |
不过GUI毕竟不能覆盖命令行的全部功能,所以能用命令行v2ray的话,就尽量不使用V2RayX吧。
(https, http over ssl)
(free, easy)
(directory tree)
我使用的是Debian 7,其他系统类似。
letsencrypt-auto
安装:$ git clone https://github.com/letsencrypt/letsencrypt |
实验前,我已将www.qiwihui.com
站点移到了要安装的服务器上,nginx已经在运行,因此可以使用 webroot 模式来获取证书,先安装webroot插件,这是一个可以不用停止 Web 服务就能让 Let’s Encrypt 验证域名的插件:
location ~ /.well-known { |
安装证书命令如下:
$ ./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: |
重要提示:需要将站点的DNS指向对用的服务器,否则会提示申请不过。
首先生成2048位 DH parameters:
$ mkdir -p /var/www/ssl/ |
Nginx的配置如下:
server { |
如果想要开启全站 https 的话,需要将 http 转向到 https,再添加一个 server 就好了:
server { |
修改完成后reload nginx 就可以了:nginx -s reload
https://qiwihui.com
,可以查看到证书信息:Let’s Encrypt 的有效期只有90天,官方客户端不支持持续更新,所以要设置自动更新,让证书一直有效。
在crontab 中设置定时任务:
30 2 * * 1 /root/letsencrypt/letsencrypt-auto renew >> /var/log/le-renew.log |
上述配置会再每周一凌晨2:30执行letsencrypt-auto renew
,在2点35分重新加载nginx配置,同时更新日志会在写在/var/log/le-renewal.log
中。
Let’s Encrypt TLS/SSL is free.
HTTP/2 安装需要以下前提:
不同 Linux 系统对于 ALPN
和 NPN
的支持可以参见下表
Operating System | OpenSSL Version | ALPN and NPN Support |
---|---|---|
CentOS/Oracle Linux/RHEL 5.10+ | 0.9.8e | Neither |
CentOS/Oracle Linux/RHEL 6.5+, 7.0+ | 1.0.1e | NPN |
Ubuntu 12.04 LTS | 1.0.1 | NPN |
Ubuntu 14.04 LTS | 1.0.1f | NPN |
Ubuntu 16.04 LTS | 1.0.2g | ALPN and NPN |
Debian 7.0 | 1.0.1e | NPN |
Debian 8.0 | 1.0.1k | NPN |
所以要么升级使用带有 OpenSSL 1.0.2 的 Ubuntu 16.04 LTS,要么从头编译 Nginx.
我的服务器系统是 Debian 7, OpenSSL 版本是1.0.1t, 所以需要重新编译 Nginx 和 OpenSSL.
下载并安装 OpenSSL:
# cd ~ |
使用 openssl version
来查看安装好的 OpenSSL 的版本。
需要编译 PCRE
库和 zlib
库[]:
# wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.40.tar.gz |
# wget http://zlib.net/zlib-1.2.11.tar.gz |
首先,下载最新的 nginx,我使用 1.10.3.
cd ~ |
其实,获取 Nginx 配置参数,使新版 Nginx 和之前的配置一样
# nginx -V |
上述配置用已经有 --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 |
最后,编译并安装
# make |
之后就可以看到已经安装好了新版 Nginx了。
请参考之前博客 使用免费的let’s encrypt证书为网站开启https
第一步完成后就设置好了一个 HTTPS 的网站了,在此基础之上开始 HTTP/2。首先,开启 HTTP/2:
listen 443 ssl http2 default_server; |
其次,去除HTTP/2不支持的旧的不安全的密码套件[5]:
ssl_prefer_server_ciphers on; |
最后,检查配置并重启 Nginx:
# nginx -t |
至此,不出问题的话你的服务器已经开始支持 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 { |
[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
在旧版本的极路由已经有很不错的翻墙设置方式,感谢前人的大树:三流火的shadwosocks插件.在旧版本的极路由上设置shadowsocks翻墙可已参考极路由Shadowsocks家庭无痛翻墙实践.
最近极路由更新了新版本,管理界面风格大变导致之前的插件不能使用,在网上找了一段时间也没有看到有人对之前的插件进行更新,所以决定自己写一个(其实后来才知道stary.love也有可用的插件,比我修改的插件功能强大很多).所谓的自己写也只是在前人的基础上修改为适应新的极路由后台管理假面. 这过程要感谢stary.love的帮助,为我提供了早期插件的一些源代码,以及许多帮着测试使用的人.
项目地址: qiwihui/hiwifi-ss, 现在插件的状态:
(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账号就可以了.
未来要做的一些工作:
怎么说呢, 我在最开始的时候, 寻找免费的vpn是获得翻墙的唯一方式, 这种方式的不好之处在于: vpn不稳定, 经常换,而且花费在寻找上的精力和时间算下来不合算. 之后精力了地下铁路vpn
的消失之后, 自己搭建翻墙才成为我的主要翻墙方式. 一个月花费的费用不到10美元, 带来的时稳定的流量和方式. VPS+shadowsocks/v2ray就可以提供稳定持久的方式.
不怎么使用vpn(免费或者收费)以及一些其他的收费翻墙服务,一则担心不安全, 流量劫持或者流量分析都有可能,甚者蜜罐,二则是重点观察对象, 服务失效的可能性还是存在的. 因此, 加密翻墙流量和混淆翻墙行为时十分重要的过程.
因为GFW, 墙内封闭的环境使得获取技术知识的广度和及时性都受到了很严重的影响, 翻墙让搞技术的我们与世界更接近.
分享 @lepture的一个tweet:
「我的互联网,上谷歌维基搜知识,上Reddit看看头条,上YouTube学习和开眼界,上Twitter关注一些正在改变世界的人和事,去Quora上看看好的问题和回答,去SlideShare上学习以及了解不同的想法和观点」
翻墙在于不断折腾.
from: wanjunlengfeng on: 1/8/2019
我把你的启动方案修改了!!只开启61080端口,结束全局GFW, 让各自的电脑去链接61080vi /etc/init.d/gw-shadowsocks`#service_start /usr/bin/ss-redir -s $server -p $server_port -b 0.0.0.0 -l $rs_port -k $password -t $timeout -m $method $params
#/etc/init.d/gw-redsocks start`我把这3行代码注销了!!希望作者能增加http代理协议,并且在路由器后台可以自定义是否全局翻墙.这样就完美了
from: nonskim on: 5/13/2019
学校的网络,本来可以用外文文献库,用了这个,上不了怎么解决
from: stonelf on: 10/28/2019
支持哪几款极路由?
]]>##0x00 准备
下载iso文件:在Arch官网上下载最新的镜像,这里我选用了163.com节点的资源,下载archlinux-2015.01.01-dual.iso;
检查文件的完整性:在MAC中使用md5或者sha1检验文件的完整行,并和下载站点提供的值进行比较。
$ openssl sha1 archlinux-2014.12.01-dual.iso |
##0x01 创建虚拟机
给虚拟机一个名字
内存:最小为256MB
创建新的虚拟磁盘
文件类型
动态分配
文件位置和大小,最小10GB
如果要安装桌面环境的话就勾选Enable 3D Acceleration
第一次启动虚拟机时,选取之前下载的ISO文件
##0x01 开始安装
选择32位或者64位Arch
一旦看到如下提示,就可以开始进行配置了:
更改键盘布局和设置语言:
默认键盘布局为us
,非us布局可以用如下命令修改:
# loadkeys layout |
layout可以是uk
, dvorak
等。设置语言:
磁盘分区
先看一下磁盘状态:
# ls /dev |
开始啦,一般创建四个分区:/
, /boot
, /home
, swap
# gdisk /dev/sda |
当出现下面命令时, 开始分区,一下以/boot
分区为例:
Command (? for help): |
a. 创建新分区:'n’b. 分区号码:回车默认从0开始递增c. “first sector”: 回车默认从上一个分区结束处开始,初始为0d. “last sector”: '+250MB’e. “hex code”: 回车默认(8300 为"Linux File System"),swap
分区输入8200, 见这儿
依次给swap
分配’+1G’(和分配的RAM一样大),/boot
分配’+8G’,/home
分配’+1G’,再次看一下磁盘的状态,可以看到已经分配好了,键入’w’并回车即可保存修改。
格式化分区
再次回到命令行:
root@archiso ~ # |
格式化分区:
# mkfs -t ext4 /dev/sda1 |
挂载新分区
# swapon /dev/sda2 |
安装Arch
# cd / |
生成fstab
文件
# genfstab -p /mnt >> /mnt/etc/fstab |
可以看看fstab里面的内容:
# more /mnt/etc/fstab |
初始化安装Boot Loader
# pacstrap /mnt syslinux |
配置安装
运行以下命令:
# 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 |
完成语言设置:
# 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 |
完成Bootloader安装
# cd /boot/syslinux/ |
打开syslinux.cfg文件,找到"comboot modules"一段:
# more syslinux.cfg |
将其中列举的文件copy到本地,同时还要加上’libutil.c32’:
# cp /usr/lib/syslinux/bios/menu.c32 . |
一旦完成上述设置,
# extlinux --install /boot/syslinux |
完成安装
最后,更改root密码:
# passwd |
输入两次exit
退回到:
[root@archiso /]# |
umount所有的分区:
# umount /mnt/boot |
在重启之前最后一步,设置/boot
分区的BIOS标识为’bootable’:
# sgdisk /dev/sda --attributes=1:set:2 |
重启Arch
# reboot |
重启之后会再次进入CD启动,这时,去除安装CD,再次重启:
Devices > CD/DVD Devices > Remove disk from virtual drive |
等待一小会:
Congradulations!
后续工作
链接网络:
dhcpcd |
安装’sudo’:
# pacman -S sudo |
添加’sudoer’:
# nano /etc/sudoers |
## |
保存,并log out:
# exit |
以新的ID和密码重新登录。
最后,每次登录的时候自动获取ip:
# sudo systemctl enable dhcpcd@eth0.service |
这样最基本的Arch Linux就好了,Desktop Environment就不装了。
curl
命令可以给你设计请求,放入管道并查看相应的能力。curl
能力的缺点在于它能覆盖多广的命令选项。使用 curl --help
会展示出150条不同的选项。这篇文章演示了9个基本的,现实程序用到的 curl
命令。在这篇教程中我们会使用httpkit的 echo 服务做为端点,回显服务的响应是它收到 HTTP 请求的 JSON 表示。
我们从最简单的 curl
命令开始。
请求
curl http://echo.httpkit.com |
响应
{ |
就这样,我们用 curl
创建了一个请求,curl
使用的 HTTP 动词默认为 GET
,请求的资源指向的是httpkit 的 echo 服务:http://echo.httpkit.com
。
你可以添加路径和查询变量:
请求
curl http://echo.httpkit.com//path?query=string |
响应
{ ... |
curl
默认的请求方法为 GET
,可以用 -X
参数设置成任何你想要的方法,通常为 POST
,PUT
,DELETE
方法,甚至是自定义的方法。
请求
curl -X POST echo.httpkit.com |
响应
{ |
正如你看到的,http://
协议前缀可以不使用,因为这是默认假定的。接着实施 DELETE
方法:
请求
curl -X DELETE echo.httpkit.com |
响应
{ |
请求头部允许客户端给服务器提供诸如授权,内容类型等信息。比如,OAuth2 使用 Authorization
头来传递访问令牌(access tokens)。curl
使用 -H
选项设置自定义头部。
请求
curl -H "Authorization: OAuth 2c4419d1aabeec" \ |
响应
{... |
可以使用 -H
多次来设置多个头部。
请求
curl -H "Accept: application/json" \ |
响应
{ ... |
现今许多有名的 HTTP API 使用 application/json
和 application/xml
来 POST
和 PUT
资源,而不是用HTML化的数据。我们试试 PUT
一些 JSON 数据到服务器上。
请求
curl -X PUT \ |
响应
{ |
将 JSON/XML 写到命令行中是令人头疼的,尤其有时这个文件很大时。幸运的是, curl
的 @readfile
可以很容易地读取文件的文本。如果上面例子中的 JSON 保存为文件 example.json
, 我们可以这么做:
请求
curl -X PUT \ |
如果不能发送带有数据的请求体,可以设置类似 POST
的方法真是没什么用。也许我们可以试试发送 HTML表单数据。使用 -d
选项,我们可以制定 URL 编码的名称和值。
请求
curl -d "firstName=Kris" \ |
响应
{ |
注意到 POST
这个方法,即使我们没有指明方法,当 curl
看到表单数据时它会指定 POST
方法。可以使用 -X
选项来覆盖这个方法。请求的 Content-Type
也被自动设置为 application/x-www-form-urlencoded
,这样服务器就知道怎么解析数据了。最终,请求体由编码了每一个表单域的 URL 构成。
当涉及到文件上传的表单时,正如你从写上传文件表单时知道的那样,这些使用 multipart/form-data
文本类型,带有 enctype
属性。cURL 使用 -F
配合上面介绍的 @readFile
宏来处理。
请求
curl -F "firstName=Kris" \ |
响应
{ |
像 -d
选项一样,当使用 -d
选项时 curl
会自动地默认使用 POST
方法,multipart/form-data
文件类型头部,计算长度并组成请求体。请注意 @readFile
宏是怎样读取一个文件的文本为任何字符的,这个不是一个单独的操作,;text/plain
指定了文件的 MIME 文本类型。在未指定的情况下,curl
会尝试嗅探文本类型。
通常,在不修改 DNS 覆盖主机的情况下测试一个虚拟主机或者是缓存代理时很有用的。只需使用 cURL 将请求指向主机的 IP 地址 并覆写 Host
头。
请求
curl -H "Host: google.com" 50.112.251.120 |
响应
{ |
API 正越来越多的利用响应头部来提供授权,速率限制,缓存等方面的信息。cURL 使用 -i
选项来查看响应头部和响应体。
请求
curl -i echo.httpkit.com |
响应
HTTP/1.1 200 OK |
原文:9 uses for cURL worth knowing
之前使用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
,这个会在更新之前被调用。
环境要求:
我们的实践过程会按照下边的过程实施:
|
$ cd ~ |
$ cd ~ |
$ cd ~/remoteRepo/webapp.git/hooks |
post-receive
中的命令:
#!/bin/sh |
为post-receive
添加可执行权限
chmod +x post-receive |
cd <your-local-repository-folder> |
或者从头开始创建一个项目:
git init |
这样,当我们在本地完成更新并push到server上时,这些代码就会被自动更新。
可以在最初在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 |
有一种情况是当本地更新了webapp,结果push到远程仓库后这个更新被reset了(虽然我觉得这个问题应该避免,但是还是有可能发生),这是,简单地在hook中使用git push deploy master
是无法完成这个过程的,因为远端的代码版本低于deploy端的代码版本,再使用pull的时候就不能实现同步,这时就应该使用另一种方式更新代码:
git fetch --all |
即git reset
把HEAD 指向了新下载的未合并的节点,也就是在local machine上reset之后的。
以下涉及到的内容有:
因为之前并没有在我的服务器上创建过其他用户,如果直接用root用户的话不好,所以需要专门的一个账户来负责部署。
登陆服务器:ssh root@<server-ip>
创建一个用户deploy
: sudo adduser deploy
将用户加入sudoers中: sudo usermod -a -G sudo deploy
添加远程连接的权限,这样就省去了输入密码了:
sudo su - deploy |
其中,700
表示只有文件拥有者才能读,写以及打开文件,600
表示只能读和写。
接着将自己的公钥加入authorized_keys
文件中,这个公钥在自己本机~.ssh/id_rsa.pub
中。没有的话可以用ssh-keygen -t rsa -C "qwh005007@gmail.com"
来生成。
自动部署的好处就是省去了每次都要上服务器。可以参见之前的一篇博客使用 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 |
zhu327/rss 项目的部署在openshift,为了将其部署在自己服务器上,修改是必须的。
删除了项目中的openshift hooks部分
将其中用到openshift环境变量OPENSHIFT_DIY_IP
和OPENSHIFT_DIY_PORT
修改为对应的localhost
和8000
端口
将diy/templates/
中的https://diy-devz.rhcloud.com
修改为之后要用到的地址 http://rss.daozhang.info
然后将修改好的代码在本地的virtualenv环境中测试,并生成需要的python的模块文件requirement.txt
。如下:
Jinja2==2.7.3 |
这些都好了之后就可以将本地的文件第一次push到服务器上了。因为之前已经设置好了git hook,所以可以在服务器上的deployment/rss
看到项目的代码更新了。
supervisor
是Linux中非常好用的进程管理工具,我们将使用它和Nginx一起来组成我们的服务的部署。
安装supervisor:pip install supervisor
或者 sudo apt-get install supervisor
创建一个目录来装supervisor的配置文件:mkdir -p ~/local/etc/supervisord
创建superviosr的出要的配置文件:touch ~/local/etc/supervisord.conf
,并加入如下内容:
[unix_http_server] |
其中我们都适用用户目录下创建的local
,logs
和tmp
文件夹来装这些文件。
创建一个rss.ini的文件用来作为rss服务:touch ~/local/etc/supervisord/rss.ini
,放入如下内容:
[program:rss] |
其中,start.py
是这个tornado项目的入口。
启动服务:supervisord -c /home/deploy/local/etc/supervisord.conf
,因为用的是非默认的配置文件,这里指定相应的配置文件位置。
一旦我们在之后修改了项目push了之后,我们需要重新启动rss:supervisorctl restart rss
,因此,为了方便,可以将这条命令加入项目git hooks中的post-receive
文件末尾。
Nginx很好很强大,我们用它来做为我们的HTTP服务器。
安装Nginx,这里,我们适用从源代码安装Nginx,并配置一些log,pid等的目录到deploy的用户目录下,这里,写一个安装的脚本install.sh
:
mkdir -p ~/src |
在Nginx的安装过程中会列出这些配置信息:
Configuration summary |
添加路径到PATH中:
export PATH=/home/you/local/sbin:$PATH |
创建配置文件:~/local/etc/nginx.conf
,在其中添加我们服务的配置:
#user deploy; |
其中,upstream rsstornado
指向了我们的rss的端口。
之后使用/home/deploy/local/sbin/nginx -t
来检查这些配置,期望的输出为:
nginx: the configuration file /home/deploy/local/etc/nginx.conf syntax is ok |
运行服务:/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来生成这个序列,所以我暂时也不知道怎么处理这个,有待进一步研究。
把树莓派用网线连接到路由器上,插上SD卡,打开树莓派电源,等大约90秒.
在Mac上打开命令行终端,输入arp -a
命令,可以看到树莓派的ip地址为 192.168.199.199
.当然也可以从路由器后台看到这个IP地址.
$ arp -a |
输入"ssh pi@192.168.199.199", 根据要求输入密码,默认为raspberry
.
$ ssh pi@192.168.199.199 |
/etc/network/interfaces
编辑这个文件:
$ sudo nano /etc/network/interfaces |
添加如下内容:
auto lo |
wpa_supplicant.conf
配置文件编辑文件wpa_supplicant.conf
设置连接的网络热点.
$ sudo nano /etc/wpa_supplicant/wpa_supplicant.conf |
为:
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev |
其中:
proto
可以是 RSN
(WPA2) 或者 WPA
(WPA1).key_mgmt
可以是 WPA-PSK
(大部分) 或者 WPA-EAP
(企业网络)pairwise
可以是 CCMP
(WPA2) 或者 TKIP
(WPA1)auth_alg
常为 OPEN
, 其他可选为 LEAP
和 SHARED
重启树莓派,之后就会自动连上wifi了.
这片文章会写得很慢,为了解释一些基础和默认的东西。
我们会使用字符串切片举例,因为这事我们首先接触的,不过这对于列表切片和设定范围是一样的。我们有:
a = '0123456789' |
其中第k个位置的字符为k。
我们使用如下方式对a进行切片:
b = a[start:stop:step] |
或者直接地:
'0123456789'[start:stop:step] |
在Python中,字符串和指向字符串的变量都是对象,所以都可以进行切片(事实上,Python中所有东西都是对象:数字,变量,字符串,函数,文件)。
有三件事情需要记住:
start
是我们想要的第一项(当然)stop
是我们第一个不想要的项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" |
我们用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],那么它前面的位置会是-len(b)-1 = -43。
所以,在这个例子中len(b) = 42
:
b[0] = b[-len(b)] = b[-42] = 'm' |
并且一般地,
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[:] = a[0:len(a):1] = '0123456789' # a +1 step 默认 |
所以,只要我们从字符串头部或者尾部使用切片,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' |
有时候这样的混合式非常方便的:
url = '<a href="http://udacity.com">'[9:-2] |
我们需要记住的是使用负数索引不意味着我们就是向后移动,只是我们从字符串末尾开始索引。向前还是向后是仅由step变量的符号决定的。
为了向后移动,我们需要在我们的意识中反转这个字符串:
|<- <- <-| |
所以:
a[::-1] = a[-1::-1] = '9876543210' # 我们只是学习如何反转字符串 |
再一次,只要我们从字符串的头部或者尾部对字符串切片,我们可以使用空的start和stop变量,Python会使用默认值。
只用6个字符就反转了一个字符串,厉害吧!可惜的是这个只在Python中有用,许多其他的语言并不支持这种方式。这类问题只是用来是我们熟悉这种结构,不只是在Python中,也包括其他语言在内。因此,考虑需要反转字符串的这类问题(比如回文问题)可以让我们学习如何使用循环,索引,并且尝试不同切片来解决这些问题。这样,你就有两手准备了。
所以现在,我们已经掌握了Python的索引,应该能明白底下这些了:
'0123456789'[8:2:-2] = '864' |
Good luck!
原文在这儿
** 1) 那么下一个问题:为什么在C中索引从0开始?**
C语言中的主要数据结构是数组,数组时一些相同类型元素的集合。在C中,字符串时字符数组,如果你想存储字符串“HELLO”,C会在内存中寻找一块连续的地方存储这些字符。比如,从内存地址7000开始存储,那么这个字符串在内存中的地址就是:
7000 'H' |
你可能会问:最后一个’/0’是什么?这个不是字符串"HELLO"的一部分。这个称作空值终止字符串。我们知道字符串从7000开始,但是我们不知道在哪里结束,因此C在字符串的末尾加了一个空值使得我们遍历字符串时知道它在哪里结束。让我们再回到原来的问题。
让我们认真看一下这些字符的地址,如果我们想要这个字符串的第一个字符,我们要做的就是得到这个字符串的初始内存地址。
'H' 地址在 7000 因为字符串从 7000 开始 |
如果我们要字符’E’,只需要地址偏移加1:
'E' is at 7000+1 |
我们可以是用偏移来得到所以的字符:
'H' is at 7000 + 0 |
啊哈!看到了吗?我们很自然地会使得索引等于便宜,这样我们就可以找到数组中的所有元素。如果我们赋值如下:
greeting = 'HELLO' |
则
greeting[0] = 'H' |
所以,这是我们问题的回答。字符从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 reflog
查看历史记录的版本号idgit reset --hard HEAD^
git reset --hard HEAD~100
git reset --hard <one commit>
git checkout -- <file>
git reset HEAD <file>
git remote add origin https://github.com/username/project_name.git
关联一个远程库git push –u origin master
(第一次要用-u, 以后不需要)git merge --no-ff -m "comments" <branch_name>
git stash
: 可以把当前工作现场 ”隐藏起来”,等以后恢复现场后继续工作。git stash list
: 查看git stash apply
恢复,恢复后,stash内容并不删除,你需要使用命令git stash drop
来删除。git stash pop
,恢复的同时把stash内容也删除了。git checkout –b dev origin/dev
, edit something, git push origin dev
git branch --set-upstream dev origin/dev
, git pull
, edit something, git push origin dev
git push origin <branch-name>
推送自己的修改.git push origin <branch-name>
推送。git push origin —delete <branch_name>
git branch --set-upstream dev origin/dev
直到我暂停出行的计划,更多地和朋友接触,我才觉得,智能手机在给我带来很大方便的同时,也使得我的生活变得狭窄,交际变得狭隘。于是我决定:离开智能手机一到两个月,就像去年手机坏掉一样。
我把手机交给了朋友保管,在微信上和QQ上留下了电话和邮箱,开始了我一个月的非智能生活。第一天是很艰难的,尤其是在突然离开手机之后,我获取信息的方式直接转移到了电脑,这就意味着我要很多时候开着电脑,背单词,阅读文章,收邮件等等都从手机向电脑迁移,无可选择。
第一天计划的实施还是有点水分的,总是不自觉地拿起那台很古老的手机,打开又关闭,才能继续回来学习工作,然后在iPad上上了一会微信,然后删了微信,整理了所有的不需要的会上瘾的软件,这才算是正式开始了。
理了头发表决心。
在接下来的一两个月的时间里,计划是这样的:
尽最大的诚意,但不知道何时会消失,如果可以,就不要再试一次。
探索,或者说是寻求,就是当你得到这个消息的时候,不是盲目地相信,而是先开始收集和甄别资料。在这个信息化一直信息爆炸的时代,越来越多的误导和虚假消息充斥在我们的身边,新闻电视,媒体资讯,社交网络,信息方便的同时也可能是误导和虚假消息滋生的温床。信息本无对错,只是当它们被少数人利用,曲解的时候,信息的对错才开始变得有利可图。
思考,结合自己的知识背景和收集到的信息,思考信息的对错,以及其中个所包含的其他有价值的信息,这是一步非常重要的过程。善恶只在一念之间,这一念就是你的思考,你的想法。思考,可以是道理更深刻,事物更透彻,思考的好处不言而喻,古今之集大成者,莫不是善于思考的人。
发言,又或者可以是行动(Move),就是在思考之后表达自己的见解,采取一定的行动支持这个见解,以达到说服自己,说服别人,是别人达成对你的共识,就如同演讲家和行动派表现出来的一样。
我更喜欢诸如Twitter一类的自由开放社交工具,一个重要的原因是消息的对冲。QQ,微信之类的熟人社交,消息相对封闭,而且熟人会弱化我们对消息的思考,又如微博,却因别有用心的控制儿失去了本来的对冲能力。但这种情况发生的时候, “Seek,think then speak” 就变更加重要了。比如在MH370消失的三四后,微博上开始有人谣传MH370安全返航,微博上很快澄清,儿微信圈和QQ圈就无法及时跟上,seek在这个过程中让我没有轻易相信这这不实的信息。
Speak, 在这个容易因为言获罪的时期,发言和行动变得愈加重要。沉默是金,那是因为真理,对于非正义,speak才是真理。
《平凡的世界》讲述了以孙少安和孙少平为代表的普通人在大时代历史进程中走过的艰难曲折的道路。时间在上个实际70到80年代,从混乱到改革开放的时期,时代的变革深刻地影响这每一代人的命运。
哥哥少安一直在家劳动,与村支书田福堂的女儿田润叶青梅竹马,两人互有爱慕之心,却遭到田福堂的反对,经过痛苦的煎熬,少安与山西勤劳的姑娘秀莲结婚,润叶也只能含泪与倾慕她的李向前结婚,改革开放后,机灵的少安看到机会,先是带领生产对实施责任制,后又进城拉砖,用赚的钱办砖窑,成为冒尖户。
少平原来在县城高中读书,毕业后回乡做了一名老师,但他没有消沉,与县革委副主任田福军女儿田晓霞建立了友情,青春的梦想和追求也激励着他到外面去“闯荡世界”,他从漂泊的揽工汉成为正式的建筑工人,最后又获得了当煤矿工人的好机遇,而田晓霞毕业后也到省城成为了一名记者。在两人产生了强烈的感情时候,田晓霞却因在抗洪采访中为抢救灾民光荣牺牲,少平悲痛不已。后来少平在一次事故中毁容,他没有被不幸压跨,重新回到矿山迎接新的挑战。
《平凡的世界》是一部很长的小说,但是文字十分流畅,很快就可以带入我们进入这两个在黄土高原上闪亮的两个人的故事。在那个时代变革明显的时代,个人的命运也和时代的命运紧紧地联系在一起,与此同时,个人的追求和梦想也在一步一步地影响这他们的轨迹。少安看准了时代的先机,少平追求不一样的外面世界,这也深深地影响他们的命运和感情。
他们的感情都有着悲剧性的一面。少安与田润叶,少平与田晓霞,最终没能在一起,甚是惋惜。书中在许多细节上的描写令人感动,比如少平与少安相约两年之后再相见的那段,以及少平在得知田晓霞牺牲之后的感情变化,让人心中为之而动。
我喜欢书中提到的叶赛宁的一首诗:不惋惜,不呼唤,我也不啼哭……金黄的落叶堆满我心间,我已经不再是青春少年……
我喜欢道家之数“三”,道家曰:一生二,二生三,三生万物。天下之事情有三而生,即是刚刚开始,故而在300天左右的时候是很适合分享的。
背单词的最初起因里带有种愤,气愤,大体就是成为前女友眼中的“极品前任”。虽然这在最出的几天很激励,但是这种感觉很快就消失了,之间有一段迷茫期,不知到自己为什么背单词。直到有一天我找到了另一种坚持,一个自己一直想去的地方:Multnomah Falls,这是在美国Oregon这州的一个瀑布,我被她绚丽的落差所折服,有生之年不然是要前往的,虽然觉得去旅游和背单词不是很搭边,没什么联系。
就是这个,犹如在远方等待着我的少女,让我坚持到了现在:
扇贝网是我知道的为数不多的几个背单词的网站,除了单词,还有新闻文章,书籍,以及一起背单词的小组和论坛,这在一定程度上激励着你一直坚持背下去,小组的作用更加的明显,不打卡就踢人的制度很合适。对单词的单词的理解程度也完全靠自己的自觉。
背单词最大的感受是你不能只背单词,只背单词如同嚼蜡,刚开始就会觉得很舒服,没有营养又损害身体,带来的效果也很小。单词背的同时结合着文章的阅读效果是很明显的,以前老师常说的在语境中理解单词的含义单体就是这个意思。其次是要让自己处在一种英语的氛围中,可以用英语阅读写文章,使用英文和朋友交流,使用英文的办公环境…如此种种,都是很有效果的。再者,别人和自己的经验告诉我:背单词应该是意见很快乐的事情,如果在这个过程过程中觉得很痛苦,那么是应该考虑一下自己的方法了。
李笑来在《把时间当作朋友中》中提到了背单词的方法:
在背单词的时候,事实上,在做所有类似的必须记住大量信息的工作的时候,一定要想办法由衷地把这件事当成一件快乐的事情来做。
我的一个朋友曾跟我分享他的做法:当年他终于搞明白要拿到奖学金就得获得GRE高分的时候,背单词量要求吓了一跳。他说,他用两天才说服自己这应该是件快乐的事情。
一共要搞定20000个单词,而因此可能获得的奖学金是40000美元左右 且连续4年没有失业的可能,那么每个单词就值20元人民币,这还只不过是算了一年的收入而已。
所以,他终于明白背单词是很快乐的,他每天都强迫自己背下200个单词,每在确定记住了一个单词前面画上一个勾时,他就想象一下刚刚数过一张20元人民币的钞票。每天睡觉的时候总感觉心满意足,因为今天又赚了4000块!
在这样的坚持了300+天以后,我觉得在更多的地方都体会到了一个坚持力量(很鸡汤的一句话),但是确实是,坚持锻炼,或者开始每天/每两天更新一千字博客。
In WeTalk Stage, one of the 8 stages, Fan Zhang, who is the brain behindMidi 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 boywho 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 otherafter 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.
Here is the song"The Brightest Star in the Sky" in English. Hope you like it.
安全方向;
大数据;
机器学习初学:线性回归,神经网路,SVM;
深度学习入门
语言:Python, iOS, Go
阅读/读书:非技术的书阅读较少,五本左右
知识整理系统:RSS, PinBoard -> Pocket -> IFTTT -> Evernote记录,github分析
开源项目,维护乏力,hiwifi-ss
我的专长:
目标:
产品和实现
2016年主要玩了两款游戏:Ingress 和 Minecraft(我的世界)。在Ingress上花费了很多的夜晚时间,在 Minecraft 上花费了一些周末时间。
入坑 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 的服务器,和朋友共同建设了一个世界。
我在8月1号开始维护开源项目:hiwifi-ss, 这是一个基于前人工作做的极路由翻墙插件,主要完成了在新版本极路由上的界面更新和功能修复。因为工作和 Ingress 的原因,这个项目在10月底就暂时没有继续维护了。
还想做一些其他的项目和实现一些其他的想法,比如一个RSS在线阅读器。
2016年缺乏系统地学习。之前获取和记录知识的过程(书,RSS, 博客等 -> Pocket暂存 -> Evernote记录和归纳 -> 博文输出)并没有很好的实行和保持,使得代码虽然会写,但是没有系统地去理解为什么,没有去理解怎样更好。拿搬砖来讲,只是回垒墙,还没有上升到造房子或者造更好的房子的程度。
今年没有读太多的书,准确说很少,以至于在最近的一段时间明显感觉到粗口增加,思考迷茫。看的文章很碎,而且没有及时思考,使得这一年没有太多的思想收入。
最近一段时间的英语学习质量也明显下降,多次任务没有认真及时完成,总体感觉能力没有提升。日语学习中断,停滞不前。
生活依旧是生活。
今年喜欢上了星星,很多时间在晚上活动,很多时候熬夜到很晚不肯睡。其实这也没有什么不对。只是如无必要,不要晚睡。
2016年想去很多地方,然而总是错过,广州,深圳,杭州,上海,以及一直以来的衡山,都成为了今年的遗憾。唯一的努力是完成了十月计划而耽搁的日本之行,第一次出国成就达成。但是计划不足和日语能力也使得这次出行有些仓促,没有达到自己的预期。
逃避问题和冷漠处理问题都是感情的敌人。(来自一个人的反思)
逃避可耻且没有用。然而一个人却可以做很多事情而不需要进行顾虑太多。
有很多的时间花在了刷 Twitter 上,没事了刷,吃完饭刷,中午刷,晚上刷,甚至有时走路都在刷。认识了一些新朋友,但是除了游戏和社交软件上的对话,没有更多深入的交流了。
计划有余而行动不足,是这一年的总结。很多事情有了开始,但是没有很好地坚持下去
日拱一卒,功不唐捐。
这是以后的每一年计划的一个宗旨:至少要坚持干完一件事情!每年都想做很多事情,但可能没有时间,也可能没有精力,不期待速成,但求每天都有进步。
详:
简:
年初三月离开了毕业后的第一份正式工作, 七月底加入青松, 中间的四个月从迷茫焦虑, 到完成第一次知识整合和补充, 算是一次小的飞跃。 在新团队的这半年是自己能力和知识增长最快的半年。
感谢一路陪伴的岩, 许多事情不再纠结, 更有勇气去做一件事情。
整理和输出的东西太少, Evernote 和 Pocket 上记录的文章基本未有效整理, 博客自上次更新已是半年, 这点需要改进。
个人项目上, COMICS项目, 微信RSS项目和自己的公众号(我都忘了叫啥了)相继停止了维护/更新。 开源项目基本维持在阅读和 fork 别人项目的水平上, 对开源项目的贡献不够。自己开发和信息收集的方法工具没有整理。
2015年阅书寥寥, 《三体》和《量子物理史话》是为数不多的能记住的, 倒是知乎上迄今645万字的阅读量确实令我咋舌, 读书的质和量都有待提高。
语言能力上, 英语继续保持之前的学习量, 只是意语刚开始没多久就放弃了。
每年都会学一项不一样的技能或者挑战一件不一样的事情, 2015年一个人背包旅行了一个月, 见识了江南的风景, 新增的技能就算滑雪了。相较于之前, 2015年的技能成长比较缓慢。
2015年半壮半胖得长了十斤, 体重达到了历史最高点, 这是一段时间失衡与调整的结果。 2016年需要停止增长, 增加体能和力量训练。
2015年是变革与变化, 机遇和挑战。新的一年, 新的成长, 新的奋斗, 不变的梦想!
** 学习环境 **
** Linux **
** Vim + Git **
** C **
** Python **
** Web **
** Mac/iOS **
** 读书 **
** 设备 **
** 生活 **
以上就像列家常一样把2014年能记得的东西都写了个遍,那么问题来了,挖掘机…不:
因此2015年在这写方面确实要改善和加强。故制定2015年的主要目标如下(比较宽的目标,无先后,要细分):
规划:
学习:
杂项
2015年想来事情也是比较多的,加油!
Wait, wait! 虽然2015年还有15天到来,但是可以做的事情还是很多,好好想想,and期待惊喜的发生!
P.S. 明年总结的时候这个也会是比较二的一篇,除非我没有进步!
2013年的总结:年度总结 - 过去的2013年。突然觉得这个好矫情啊!!
Here is one picture of it.
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
Yes, as you may guess, it is one look-and-say sequence(sequence A006715 inOEIS.
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 ofthe same digit. For example:
If we start with any digit d from 0 to 9 then d will remainindefinitely as the last digit of the sequence. For d different from 1, thesequence 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 8thmember and every member after it in the sequence is made up of one or moreof 92 “basic” non-interacting subsequences. The 92 basic subsequence showsin the following table(from here.
The fouth column in the table says what othersubsequences the given subsequence evolves into. He also show that thenumber of the digits in each member of the sequence grows a constant fromone member to the next. If Ln is the number of the digits in thenth member in the sequence, then Ln+1/Ln toa limitation when n to infinity. It is 1.303577269… , which we call itas 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-saysequence. Just like 92 elements. Heregives the periodic table of atoms associated with the look-and-say sequenceas named by Conway(1987). As we can see, 3113112211322112 links to Br, and311311 links to Ba.
Breaking Bad. That is the answer.
That is perfect from the begining to the end. Many thanks to the problemmaker, and the screenwriters, also everyexcellent actors in Breaking Bad.