
PostgreSQL是最先进的开源数据库,其中一个非常给力的特性就是FDW:外部数据包装器(Foreign Data Wrapper)。通过FDW,用户可以用统一的方式从Pg中访问各类外部数据源。file_fdw就是其中随数据库附赠的两个fdw之一。随着pg10的更新,file_fdw也添加了一颗赛艇的功能:从程序输出读取。 小霸王妙用无穷,我们能通过file_fdw,轻松查看操作系统信息,拉取网络数据,把各种各样的数据源轻松喂进数据库里统一查看管理。 安装与配置 file_fdw是Pg自带的组件,不需要奇怪的配置,在数据库中执行以下命令即可启用file_fdw: CREATE EXTENSION file_fdw; 启用FDW插件之后,需要创建一个实例,也是一行SQL搞定,创建一个名为fs的FDW Server实例。 CREATE SERVER fs FOREIGN DATA WRAPPER file_fdw; 创建外部表 举个栗子,如果我想从数据库中读取操作系统中正在运行的进程信息,该怎么做呢? 最典型,也是最常用的外部数据格式就是CSV啦。不过系统命令输出的结果并不是很规整: >>> ps ux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND vonng 2658 0.0 0.2 148428 2620 ? S 11:51 0:00 sshd: vonng@pts/0,pts/2 vonng 2659 0.0 0.2 115648 2312 pts/0 Ss+ 11:51 0:00 -bash vonng 4854 0.0 0.2 115648 2272 pts/2 Ss 15:46 0:00 -bash vonng 5176 0.0 0.1 150940 1828 pts/2 R+ 16:06 0:00 ps -ux vonng 26460 0.0 1.2 271808 13060 ? S 10月26 0:22 /usr/local/pgsql/bin/postgres vonng 26462 0.0 0.2 271960 2640 ? Ss 10月26 0:00 postgres: checkpointer process vonng 26463 0.0 0.2 271808 2148 ? Ss 10月26 0:25 postgres: writer process vonng 26464 0.0 0.5 271808 5300 ? Ss 10月26 0:27 postgres: wal writer process vonng 26465 0.0 0.2 272216 2096 ? Ss 10月26 0:31 postgres: autovacuum launcher process vonng 26466 0.0 0.1 126896 1104 ? Ss 10月26 0:54 postgres: stats collector process vonng 26467 0.0 0.1 272100 1588 ? Ss 10月26 0:01 postgres: bgworker: logical replication launcher 可以通过awk,将ps的命令输出规整为分隔符为\x1F的csv格式。 ps aux | awk '{print $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,substr($0,index($0,$11))}' OFS='\037' 正戏来啦!通过以下DDL创建一张外表定义 CREATE FOREIGN TABLE process_status ( username TEXT, pid INTEGER, cpu NUMERIC, mem NUMERIC, vsz BIGINT, rss BIGINT, tty TEXT, stat TEXT, start TEXT, time TEXT, command TEXT ) SERVER fs OPTIONS ( PROGRAM $$ ps aux | awk '{print $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,substr($0,index($0,$11))}' OFS='\037' $$ , FORMAT 'csv', DELIMITER E'\037', HEADER 'TRUE'); 这里,关键是通过CREATE FOREIGN TABLE OPTIONS (xxxx)中的OPTIONS提供相应的参数,在PROGRAM参数中填入上面的命令,pg就会在查询这张表的时候自动执行此命令,并读取其输出。FORMAT参数可以指定为CSV,DELIMITER参数指定为之前使用的\x1F,并通过HEADER 'TRUE'忽略CSV的第一行 那么结果如何呢? 有什么用 最简单的场景,原本系统指标监控需要编写各种监测脚本,部署在奇奇怪怪的地方。然后定期执行拉取metric,再存进数据库。现在通过file_fdw的方式,可以将感兴趣的指标直接录入数据库表,一步到位,而且维护方便,部署简单,更加可靠。在外表上加上视图,定期拉取聚合,将原本一个监控系统完成的事情,在数据库中一条龙解决了。 因为可以从程序输出读取结果,因此file_fdw可以与linux生态里各类强大的命令行工具配合使用,发挥出强大的威力。 其他栗子 诸如此类,实际上后来我发现Facebook貌似有一个类似的产品,叫OSQuery,也是干了差不多的事。通过SQL查询操作系统的指标。但明显PostgreSQL这种方法最简单粗暴高效啦,只要定义表结构,和命令数据源就能轻松对接指标数据,用不了一天就能做出一个功能差不多的东西来。 用于读取系统用户列表的DDL: CREATE FOREIGN TABLE etc_password ( username TEXT, password TEXT, user_id INTEGER, group_id INTEGER, user_info TEXT, home_dir TEXT, shell TEXT ) SERVER fs OPTIONS ( PROGRAM $$ awk -F: 'NF && !/^[:space:]*#/ {print $1,$2,$3,$4,$5,$6,$7}' OFS='\037' /etc/passwd $$ , FORMAT 'csv', DELIMITER E'\037' ); 用于读取磁盘用量的DDL: CREATE FOREIGN TABLE disk_free ( file_system TEXT, blocks_1m BIGINT, used_1m BIGINT, avail_1m BIGINT, capacity TEXT, iused BIGINT, ifree BIGINT, iused_pct TEXT, mounted_on TEXT ) SERVER fs OPTIONS (PROGRAM $$ df -ml| awk '{print $1,$2,$3,$4,$5,$6,$7,$8,$9}' OFS='\037' $$ , FORMAT 'csv', HEADER 'TRUE', DELIMITER E'\037' ); 当然,用file_fdw只是一个很Naive的FDW,譬如这里就只能读,不能改。 自己编写FDW实现增删改查逻辑也非常简单,例如Multicorn就是使用Python编写FDW的项目。 SQL over everything,让世界变的更简单~
Go使用SQL与类SQL数据库的惯例是通过标准库database/sql。这是一个对关系型数据库的通用抽象,它提供了标准的、轻量的、面向行的接口。不过database/sql的包文档只讲它做了什么,却对如何使用只字未提。快速指南远比堆砌事实有用,本文讲述了database/sql的使用方法及其注意事项。 1. 顶层抽象 在Go中访问数据库需要用到sql.DB接口:它可以创建语句(statement)和事务(transaction),执行查询,获取结果。 sql.DB并不是数据库连接,也并未在概念上映射到特定的数据库(Database)或模式(schema)。它只是一个抽象的接口,不同的具体驱动有着不同的实现方式。通常而言,sql.DB会处理一些重要而麻烦的事情,例如操作具体的驱动打开/关闭实际底层数据库的连接,按需管理连接池。 sql.DB这一抽象让用户不必考虑如何管理并发访问底层数据库的问题。当一个连接在执行任务时会被标记为正在使用。用完之后会放回连接池中。不过用户如果用完连接后忘记释放,就会产生大量的连接,极可能导致资源耗尽(建立太多连接,打开太多文件,缺少可用网络端口)。 2. 导入驱动 使用数据库时,除了database/sql包本身,还需要引入想使用的特定数据库驱动。 尽管有时候一些数据库特有的功能必需通过驱动的Ad Hoc接口来实现,但通常只要有可能,还是应当尽量只用database/sql中定义的类型。这可以减小用户代码与驱动的耦合,使切换驱动时代码改动最小化,也尽可能地使用户遵循Go的惯用法。本文使用PostgreSQL为例,PostgreSQL的著名的驱动有: github.com/lib/pq github.com/go-pg/pg github.com/jackc/pgx。 这里以pgx为例,它性能表现不俗,并对PostgreSQL诸多特性与类型有着良好的支持。既可使用Ad-Hoc API,也提供了标准数据库接口的实现:github.com/jackc/pgx/stdlib。 import ( "database/sql" _ "github.com/jackx/pgx/stdlib" ) 使用_别名来匿名导入驱动,驱动的导出名字不会出现在当前作用域中。导入时,驱动的初始化函数会调用sql.Register将自己注册在database/sql包的全局变量sql.drivers中,以便以后通过sql.Open访问。 3. 访问数据 加载驱动包后,需要使用sql.Open()来创建sql.DB: func main() { db, err := sql.Open("pgx","postgres://localhost:5432/postgres") if err != nil { log.Fatal(err) } defer db.Close() } sql.Open有两个参数: 第一个参数是驱动名称,字符串类型。为避免混淆,一般与包名相同,这里是pgx。 第二个参数也是字符串,内容依赖于特定驱动的语法。通常是URL的形式,例如postgres://localhost:5432。 绝大多数情况下都应当检查database/sql操作所返回的错误。 一般而言,程序需要在退出时通过sql.DB的Close()方法释放数据库连接资源。如果其生命周期不超过函数的范围,则应当使用defer db.Close() 执行sql.Open()并未实际建立起到数据库的连接,也不会验证驱动参数。第一个实际的连接会惰性求值,延迟到第一次需要时建立。用户应该通过db.Ping()来检查数据库是否实际可用。 if err = db.Ping(); err != nil { // do something about db error } sql.DB对象是为了长连接而设计的,不要频繁Open()和Close()数据库。而应该为每个待访问的数据库创建一个sql.DB实例,并在用完前一直保留它。需要时可将其作为参数传递,或注册为全局对象。 如果没有按照database/sql设计的意图,不把sql.DB当成长期对象来用而频繁开关启停,就可能遭遇各式各样的错误:无法复用和共享连接,耗尽网络资源,由于TCP连接保持在TIME_WAIT状态而间断性的失败等…… 4. 获取结果 有了sql.DB实例之后就可以开始执行查询语句了。 Go将数据库操作分为两类:Query与Exec。两者的区别在于前者会返回结果,而后者不会。 Query表示查询,它会从数据库获取查询结果(一系列行,可能为空)。 Exec表示执行语句,它不会返回行。 此外还有两种常见的数据库操作模式: QueryRow表示只返回一行的查询,作为Query的一个常见特例。 Prepare表示准备一个需要多次使用的语句,供后续执行用。 4.1 获取数据 让我们看一个如何查询数据库并且处理结果的例子:利用数据库计算从1到10的自然数之和。 func example() { var sum, n int32 // invoke query rows, err := db.Query("SELECT generate_series(1,$1)", 10) // handle query error if err != nil { fmt.Println(err) } // defer close result set defer rows.Close() // Iter results for rows.Next() { if err = rows.Scan(&n); err != nil { fmt.Println(err) // Handle scan error } sum += n // Use result } // check iteration error if rows.Err() != nil { fmt.Println(err) } fmt.Println(sum) } 整体工作流程如下: 使用db.Query()来发送查询到数据库,获取结果集Rows,并检查错误。 使用rows.Next()作为循环条件,迭代读取结果集。 使用rows.Scan从结果集中获取一行结果。 使用rows.Err()在退出迭代后检查错误。 使用rows.Close()关闭结果集,释放连接。 一些需要详细说明的地方: db.Query会返回结果集*Rows和错误。每个驱动返回的错误都不一样,用错误字符串来判断错误类型并不是明智的做法,更好的方法是对抽象的错误做Type Assertion,利用驱动提供的更具体的信息来处理错误。当然类型断言也可能产生错误,这也是需要处理的。 if err.(pgx.PgError).Code == "0A000" { // Do something with that type or error } rows.Next()会指明是否还有未读取的数据记录,通常用于迭代结果集。迭代中的错误会导致rows.Next()返回false。 rows.Scan()用于在迭代中获取一行结果。数据库会使用wire protocal通过TCP/UnixSocket传输数据,对Pg而言,每一行实际上对应一条DataRow消息。Scan接受变量地址,解析DataRow消息并填入相应变量中。因为Go语言是强类型的,所以用户需要创建相应类型的变量并在rows.Scan中传入其指针,Scan函数会根据目标变量的类型执行相应转换。 例如某查询返回一个单列string结果集,用户可以传入[]byte或string类型变量的地址,Go会将原始二进制数据或其字符串形式填入其中。但如果用户知道这一列始终存储着数字字面值,那么相比传入string地址后手动使用strconv.ParseInt()解析,更推荐的做法是直接传入一个整型变量的地址(如上面所示),Go会替用户完成解析工作。如果解析出错,Scan会返回相应的错误。 rows.Err()用于在退出迭代后检查错误。正常情况下迭代退出是因为内部产生的EOF错误,使得下一次rows.Next() == false,从而终止循环;在迭代结束后要检查错误,以确保迭代是因为数据读取完毕,而非其他“真正”错误而结束的。遍历结果集的过程实际上是网络IO的过程,可能出现各种错误。健壮的程序应当考虑这些可能,而不能总是假设一切正常。 rows.Close()用于关闭结果集。结果集引用了数据库连接,并会从中读取结果。读取完之后必须关闭它才能避免资源泄露。只要结果集仍然打开着,相应的底层连接就处于忙碌状态,不能被其他查询使用。 因错误(包括EOF)导致的迭代退出会自动调用rows.Close()关闭结果集(和释放底层连接)。但如果程序自行意外地退出了循环,例如中途break & return,结果集就不会被关闭,产生资源泄露。rows.Close方法是幂等的,重复调用不会产生副作用,因此建议使用 defer rows.Close()来关闭结果集。 以上就是在Go中使用数据库的标准方式。 4.2 单行查询 如果一个查询每次最多返回一行,那么可以用快捷的单行查询来替代冗长的标准查询,例如上例可改写为: var sum int err := db.QueryRow("SELECT sum(n) FROM (SELECT generate_series(1,$1) as n) a;", 10).Scan(&sum) if err != nil { fmt.Println(err) } fmt.Println(sum) 不同于Query,如果查询发生错误,错误会延迟到调用Scan()时统一返回,减少了一次错误处理判断。同时QueryRow也避免了手动操作结果集的麻烦。 需要注意的是,对于单行查询,Go将没有结果的情况视为错误。sql包中定义了一个特殊的错误常量ErrNoRows,当结果为空时,QueryRow().Scan()会返回它。 4.3 修改数据 什么时候用Exec,什么时候用Query,这是一个问题。通常DDL和增删改使用Exec,返回结果集的查询使用Query。但这不是绝对的,这完全取决于用户是否希望想要获取返回结果。例如在PostgreSQL中:INSERT ... RETURNING *;虽然是一条插入语句,但它也有返回结果集,故应当使用Query而不是Exec。 Query和Exec返回的结果不同,两者的签名分别是: func (s *Stmt) Query(args ...interface{}) (*Rows, error) func (s *Stmt) Exec(args ...interface{}) (Result, error) Exec不需要返回数据集,返回的结果是Result,Result接口允许获取执行结果的元数据 type Result interface { // 用于返回自增ID,并不是所有的关系型数据库都有这个功能。 LastInsertId() (int64, error) // 返回受影响的行数。 RowsAffected() (int64, error) } Exec的用法如下所示: db.Exec(`CREATE TABLE test_users(id INTEGER PRIMARY KEY ,name TEXT);`) db.Exec(`TRUNCATE test_users;`) stmt, err := db.Prepare(`INSERT INTO test_users(id,name) VALUES ($1,$2) RETURNING id`) if err != nil { fmt.Println(err.Error()) } res, err := stmt.Exec(1, "Alice") if err != nil { fmt.Println(err) } else { fmt.Println(res.RowsAffected()) fmt.Println(res.LastInsertId()) } 相比之下Query则会返回结果集对象*Rows,使用方式见上节。其特例QueryRow使用方式如下: db.Exec(`CREATE TABLE test_users(id INTEGER PRIMARY KEY ,name TEXT);`) db.Exec(`TRUNCATE test_users;`) stmt, err := db.Prepare(`INSERT INTO test_users(id,name) VALUES ($1,$2) RETURNING id`) if err != nil { fmt.Println(err.Error()) } var returnID int err = stmt.QueryRow(4, "Alice").Scan(&returnID) if err != nil { fmt.Println(err) } else { fmt.Println(returnID) } 同样的语句使用Exec和Query执行有巨大的差别。如上文所述,Query会返回结果集Rows,而存在未读取数据的Rows其实会占用底层连接直到rows.Close()为止。因此,使用Query但不读取返回结果,会导致底层连接永远无法释放。database/sql期望用户能够用完就把连接还回来,所以这样的用法很快就会导致资源耗尽(连接过多)。所以,应该用Exec的语句绝不可用Query来执行。 4.4 准备查询 在上一节的两个例子中,没有直接使用数据库的Query和Exec方法,而是首先执行了db.Prepare获取准备好的语句(prepared statement)。准备好的语句Stmt和sql.DB一样,都可以执行Query、Exec等方法。 准备语句的优势 在查询前进行准备是Go语言中的惯用法,多次使用的查询语句应当进行准备(Prepare)。准备查询的结果是一个准备好的语句(prepared statement),语句中可以包含执行时所需参数的占位符(即绑定值)。准备查询比拼字符串的方式好很多,它可以转义参数,避免SQL注入。同时,准备查询对于一些数据库也省去了解析和生成执行计划的开销,有利于性能。 占位符 PostgreSQL使用$N作为占位符,N是一个从1开始递增的整数,代表参数的位置,方便参数的重复使用。MySQL使用?作为占位符,SQLite两种占位符都可以,而Oracle则使用:param1的形式。 MySQL PostgreSQL Oracle ===== ========== ====== WHERE col = ? WHERE col = $1 WHERE col = :col VALUES(?, ?, ?) VALUES($1, $2, $3) VALUES(:val1, :val2, :val3) 以PostgreSQL为例,在上面的例子中:"SELECT generate_series(1,$1)" 就用到了$N的占位符形式,并在后面提供了与占位符数目匹配的参数个数。 底层内幕 准备语句有着各种优点:安全,高效,方便。但Go中实现它的方式可能和用户所设想的有轻微不同,尤其是关于和database/sql内部其他对象交互的部分。 在数据库层面,准备语句Stmt是与单个数据库连接绑定的。通常的流程是:客户端向服务器发送带有占位符的查询语句用于准备,服务器返回一个语句ID,客户端在实际执行时,只需要传输语句ID和相应的参数即可。因此准备语句无法在连接之间共享,当使用新的数据库连接时,必须重新准备。 database/sql并没有直接暴露出数据库连接。用户是在DB或Tx上执行Prepare,而不是Conn。因此database/sql提供了一些便利处理,例如自动重试。这些机制隐藏在Driver中实现,而不会暴露在用户代码中。其工作原理是:当用户准备一条语句时,它在连接池中的一个连接上进行准备。Stmt对象会引用它实际使用的连接。当执行Stmt时,它会尝试会用引用的连接。如果那个连接忙碌或已经被关闭,它会获取一个新的连接,并在连接上重新准备,然后再执行。 因为当原有连接忙时,Stmt会在其他连接上重新准备。因此当高并发地访问数据库时,大量的连接处于忙碌状态,这会导致Stmt不断获取新的连接并执行准备,最终导致资源泄露,甚至超出服务端允许的语句数目上限。所以通常应尽量采用扇入的方式减小数据库访问并发数。 查询的微妙之处 数据库连接其实是实现了Begin,Close,Prepare方法的接口。 type Conn interface { Prepare(query string) (Stmt, error) Close() error Begin() (Tx, error) } 所以连接接口上实际并没有Exec,Query方法,这些方法其实定义在Prepare返回的Stmt上。对于Go而言,这意味着db.Query()实际上执行了三个操作:首先对查询语句做了准备,然后执行查询语句,最后关闭准备好的语句。这对数据库而言,其实是3个来回。设计粗糙的程序与简陋实现驱动可能会让应用与数据库交互的次数增至3倍。好在绝大多数数据库驱动对于这种情况有优化,如果驱动实现sql.Queryer接口: type Queryer interface { Query(query string, args []Value) (Rows, error) } 那么database/sql就不会再进行Prepare-Execute-Close的查询模式,而是直接使用驱动实现的Query方法向数据库发送查询。对于查询都是即拼即用,也不担心安全问题的情况下,直接Query可以有效减少性能开销。 5. 使用事务 事物是关系型数据库的核心特性。Go中事务(Tx)是一个持有数据库连接的对象,它允许用户在同一个连接上执行上面提到的各类操作。 事务基本操作 通过db.Begin()来开启一个事务,Begin方法会返回一个事务对象Tx。在结果变量Tx上调用Commit()或者Rollback()方法会提交或回滚变更,并关闭事务。在底层,Tx会从连接池中获得一个连接并在事务过程中保持对它的独占。事务对象Tx上的方法与数据库对象sql.DB的方法一一对应,例如Query,Exec等。事务对象也可以准备(prepare)查询,由事务创建的准备语句会显式绑定到创建它的事务。 事务注意事项 使用事务对象时,不应再执行事务相关的SQL语句,例如BEGIN,COMMIT等。这可能产生一些副作用: Tx对象一直保持打开状态,从而占用了连接。 数据库状态不再与Go中相关变量的状态保持同步。 事务提前终止会导致一些本应属于事务内的查询语句不再属于事务的一部分,这些被排除的语句有可能会由别的数据库连接而非原有的事务专属连接执行。 当处于事务内部时,应当使用Tx对象的方法而非DB的方法,DB对象并不是事务的一部分,直接调用数据库对象的方法时,所执行的查询并不属于事务的一部分,有可能由其他连接执行。 Tx的其他应用场景 如果需要修改连接的状态,也需要用到Tx对象,即使用户并不需要事务。例如: 创建仅连接可见的临时表 设置变量,例如SET @var := somevalue 修改连接选项,例如字符集,超时设置。 在Tx上执行的方法都保证同一个底层连接执行,这使得对连接状态的修改对后续操作起效。这是Go中实现这种功能的标准方式。 在事务中准备语句 调用Tx.Prepare会创建一个与事务绑定的准备语句。在事务中使用准备语句,有一个特殊问题需要关注:一定要在事务结束前关闭准备语句。 在事务中使用defer stmt.Close()是相当危险的。因为当事务结束后,它会释放自己持有的数据库连接,但事务创建的未关闭Stmt仍然保留着对事务连接的引用。在事务结束后执行stmt.Close(),如果原来释放的连接已经被其他查询获取并使用,就会产生竞争,极有可能破坏连接的状态。 6. 处理空值 可空列(Nullable Column)非常的恼人,容易导致代码变得丑陋。如果可以,在设计时就应当尽量避免。因为: Go语言的每一个变量都有着默认零值,当数据的零值没有意义时,可以用零值来表示空值。但很多情况下,数据的零值和空值实际上有着不同的语义。单独的原子类型无法表示这种情况。 标准库只提供了有限的四种Nullable type::NullInt64, NullFloat64, NullString, NullBool。并没有诸如NullUint64,NullYourFavoriteType,用户需要自己实现。 空值有很多麻烦的地方。例如用户认为某一列不会出现空值而采用基本类型接收时却遇到了空值,程序就会崩溃。这种错误非常稀少,难以捕捉、侦测、处理,甚至意识到。 空值的解决办法 使用额外的标记字段 database\sql提供了四种基本可空数据类型:使用基本类型和一个布尔标记的复合结构体表示可空值。例如: type NullInt64 struct { Int64 int64 Valid bool // Valid is true if Int64 is not NULL } 可空类型的使用方法与基本类型一致: for rows.Next() { var s sql.NullString err := rows.Scan(&s) // check err if s.Valid { // use s.String } else { // handle NULL case } } 使用指针 在Java中通过装箱(boxing)处理可空类型,即把基本类型包装成一个类,并通过指针引用。于是,空值语义可以通过指针为空来表示。Go当然也可以采用这种办法,不过标准库中并没有提供这种实现方式。pgx提供了这种形式的可空类型支持。 使用零值表示空值 如果数据本身从语义上就不会出现零值,或者根本不区分零值和空值,那么最简便的方法就是使用零值来表示空值。驱动go-pg提供了这种形式的支持。 自定义处理逻辑 任何实现了Scanner接口的类型,都可以作为Scan传入的地址参数类型。这就允许用户自己定制复杂的解析逻辑,实现更丰富的类型支持。 type Scanner interface { // Scan 从数据库驱动中扫描出一个值,当不能无损地转换时,应当返回错误 // src可能是int64, float64, bool, []byte, string, time.Time,也可能是nil,表示空值。 Scan(src interface{}) error } 在数据库层面解决 通过对列添加NOT NULL约束,可以确保任何结果都不会为空。或者,通过在SQL中使用COALESCE来为NULL设定默认值。 7. 处理动态列 Scan()函数要求传递给它的目标变量的数目,与结果集中的列数正好匹配,否则就会出错。 但总有一些情况,用户事先并不知道返回的结果到底有多少列,例如调用一个返回表的存储过程时。 在这种情况下,使用rows.Columns()来获取列名列表。在不知道列类型情况下,应当使用sql.RawBytes作为接受变量的类型。获取结果后自行解析。 cols, err := rows.Columns() if err != nil { // handle this.... } // 目标列是一个动态生成的数组 dest := []interface{}{ new(string), new(uint32), new(sql.RawBytes), } // 将数组作为可变参数传入Scan中。 err = rows.Scan(dest...) // ... 8. 连接池 database/sql包里实现了一个通用的连接池,它只提供了非常简单的接口,除了限制连接数、设置生命周期基本没有什么定制选项。但了解它的一些特性也是很有帮助的。 连接池意味着:同一个数据库上的连续两条查询可能会打开两个连接,在各自的连接上执行。这可能导致一些让人困惑的错误,例如程序员希望锁表插入时连续执行了两条命令:LOCK TABLE和INSERT,结果却会阻塞。因为执行插入时,连接池创建了一个新的连接,而这条连接并没有持有表锁。 在需要时,而且连接池中没有可用的连接时,连接才被创建。 默认情况下连接数量没有限制,想创建多少就有多少。但服务器允许的连接数往往是有限的。 用db.SetMaxIdleConns(N)来限制连接池中空闲连接的数量,但是这并不会限制连接池的大小。连接回收(recycle)的很快,通过设置一个较大的N,可以在连接池中保留一些空闲连接,供快速复用(reuse)。但保持连接空闲时间过久可能会引发其他问题,比如超时。设置N=0则可以避免连接空闲太久。 用db.SetMaxOpenConns(N)来限制连接池中打开的连接数量。 用db.SetConnMaxLifetime(d time.Duration)来限制连接的生命周期。连接超时后,会在需要时惰性回收复用。 9. 微妙行为 database/sql并不复杂,但某些情况下它的微妙表现仍然会出人意料。 9.1 资源耗尽 不谨慎地使用database/sql会给自己挖许多坑,最常见的问题就是资源枯竭(resource exhaustion): 打开和关闭数据库(sql.DB)可能会导致资源枯竭; 结果集没有读取完毕,或者调用rows.Close()失败,结果集会一直占用池里的连接; 使用Query()执行一些不返回结果集的语句,返回的未读取结果集会一直占用池里的连接; 不了解准备语句(Prepared Statement)的工作原理会产生许多额外的数据库访问。 9.2 Uint64 Go底层使用int64来表示整型,使用uint64时应当极其小心。使用超出int64表示范围的整数作为参数,会产生一个溢出错误: // Error: constant 18446744073709551615 overflows int _, err := db.Exec("INSERT INTO users(id) VALUES", math.MaxUint64) 这种类型的错误非常不容易发现,它可能一开始表现的很正常,但是溢出之后问题就来了。 9.3 不合预期的连接状态 连接的状态,例如是否处于事务中,所连接的数据库,设置的变量等,应该通过Go的相关类型来处理,而不是通过SQL语句。用户不应当对自己的查询在哪条连接上执行作任何假设,如果需要在同一条连接上执行,需要使用Tx。 举个例子,通过USE DATABASE改变连接的数据库对于不少人是习以为常的操作,执行这条语句,只影响当前连接的状态,其他连接仍然访问的是原来的数据库。如果没有使用事务Tx,后续的查询并不能保证仍然由当前的连接执行,所以这些查询很可能并不像用户预期的那样工作。 更糟糕的是,如果用户改变了连接的状态,用完之后它成为空连接又回到了连接池,这会污染其他代码的状态。尤其是直接在SQL中执行诸如BEGIN或COMMIT这样的语句。 9.4 驱动的特殊语法 尽管database/sql是一个通用的抽象,但不同的数据库,不同的驱动仍然会有不同的语法和行为。参数占位符就是一个例子。 9.5 批量操作 出乎意料的是,标准库没有提供对批量操作的支持。即INSERT INTO xxx VALUES (1),(2),...;这种一条语句插入多条数据的形式。目前实现这个功能还需要自己手动拼SQL。 9.6 执行多条语句 database/sql并没有对在一次查询中执行多条SQL语句的显式支持,具体的行为以驱动的实现为准。所以对于 _, err := db.Exec("DELETE FROM tbl1; DELETE FROM tbl2") // Error/unpredictable result 这样的查询,怎样执行完全由驱动说了算,用户并无法确定驱动到底执行了什么,又返回了什么。 9.7 事务中的多条语句 因为事务保证在它上面执行的查询都由同一个连接来执行,因此事务中的语句必需按顺序一条一条执行。对于返回结果集的查询,结果集必须Close()之后才能进行下一次查询。用户如果尝试在前一条语句的结果还没读完前就执行新的查询,连接就会失去同步。这意味着事务中返回结果集的语句都会占用一次单独的网络往返。 10. 其他 本文主体基于[[Go database/sql tutorial]]([Go database/sql tutorial]),由我翻译并进行一些增删改,修正过时错误的内容。转载保留出处。
Go的作者Ken Thompson是UTF-8的发明人(也是C,Unix,Plan9等的创始人),因此在关于字符编码上,Go有着独到而周全的设计。本文介绍了Go语言中的三种内置文本类型:string, byte,rune的内部表示与相互转换。 1. 概览 Go中,字符串string是内置类型,与文本处理相关的内置类型还有符文rune和字节byte。 UTF-8编码在Go语言中有着特殊的位置,无论是源代码的文本编码,还是字符串的内部编码都是UTF-8。Go绕开前辈语言们踩过的坑,使用了UTF8作为默认编码是一个非常明智的选择。相比之下,Java,Javascript都使用 UCS-2/UTF16作为内部编码,早期还有随机访问的优势,可当Unicode增长超出BMP之后,这一优势也荡然无存了。相比之下,字节序,Surrogate , 空间冗余带来的麻烦却仍让人头大无比。 标准库 与C语言类似,大多数关于字符串处理的函数都放在标准库里。Go将大部分字符串处理的函数放在了strings,bytes这两个包里。因为在字符串和整型间没有隐式类型转换,字符串和其他基本类型的转换的功能主要在标准库strconv中提供。unicode相关功能在unicode包中提供。encoding包提供了一系列其他的编码支持。 摘要 Go语言源代码总是采用UTF-8编码 字符串string可以包含任意字节序列,通常是UTF-8编码的。 字符串字面值,在不带有字节转义的情况下一定是UTF-8编码的。 Go使用rune代表Unicode码位。一个字符可能由一个或多个码位组成(复合字符) Go string是建立在字节数组的基础上的,因此对string使用[]索引会得到字节byte而不是字符rune。 Go语言的字符串不是正规化(normalized)的,因此同一个字符可能由不同的字节序列表示。使用unicode/norm解决此类问题。 基础数据结构 数组与切片 要讨论[]byte和[]rune,就必需先解释Go语言中的数组(Array)与切片(Slice),数组很好理解,和C语言中的数组概念一致,切片则是对数组的引用。 数组Array是固定长度的数据结构,不存放任何额外的信息。很少直接使用,往往用作切片的底层存储。 切片Slice描述了数组中一个连续的片段,Go语言的切片操作与Python较为类似。在底层实现中,切片可以看成一个由三个word组成的结构体,这里word是CPU的字长。这三个字分别是ptr,len,cap,分别代表数组首元素地址,切片的长度,当前切片头位置到底层数组尾部的距离。 因此,在函数参数中传递十个元素的数组,那么就会在栈上复制这十个元素。而传递一个切片,则实际上传递的是这个3Word结构体。传递切片本身就是传递引用。 字节byte 字节byte实际上是uint8的别名,只是为了和其他8bit类型相区别才单独起了别名。通常出现的更多的是字节切片[]byte与字节数组[...]byte。 字面值 字节可以用单引号扩起的单个字符表示,不过这种字面值和rune的字面值很容易搞混。赋予字节变量一个超出范围的值,如果在编译期能检查出来就会报overflows byte编译错误。 底层结构 对于字节数组[]byte,实质上可以看做[]uint8,即一个整形切片,所以字节数组的本体结构定义如下: type SliceHeader struct { Data uintptr Len int Cap int } 字符串string 字符串通常是UTF8编码的文本,由一系列8bit字节组成。raw string literal和不含转义符号的string literal一定是UTF-8编码的,但string其实可以含有任意的字节序列。 字符串是不可变对象,可以空(s=""),但不会是nil。 底层结构 string在Go中的实现与Slice类似,但因为字符串是不可变类型,因此底层数组的长度就是字符串的长度,所以相比切片,string结构的本体少了一个Cap字段。只有一个指针和一个长度值,由两个Word组成。64位机器上占用16个字节。 type StringHeader struct { Data uintptr Len int } 虽然字符串是不可变类型,但通过指针和强制转换,还是可以进行一些危险但高效的操作的。不过要注意,编译器作为常量确定的string会写入只读段,是不可以修改的。相比之下,fmt.Sprintf生成的字符串分配在堆上,就可以通过黑魔法进行修改。 关于string,有这么几点需要注意。 string常量会在编译期分配到只读段,对应数据地址不可写入。 相同的string常量不会重复存储,但动态生成的字符串即使内容一样,数据也是在不同的空间。 常量空字符串有数据地址,动态生成的字符串没有设置数据地址 ,只有动态生成的string可以unsafe魔改。 Golang string和[]byte转换,会将数据复制到堆上,返回数据指向复制的数据。所以string(bytes)存在开销 string和[]byte通过复制转换,性能损失接近4倍 符文rune 符文rune其实是int32的别名,表示一个Unicode的码位。 注意一个字符(Character)可以由一个或多个码位(Code Point)构成。例如带音调的e,即é,既可以由\u00e9单个码位表示,也可以由e和口音符号\u0301复合而成。这涉及到normalization的问题。但通常情况下一个字符就是一个码位。 >>> print u'\u00e9', u'e\u0301',u'e\u0301\u0301\u0301' é é é́́ 符文的字面值是用单引号括起的一个或多个字符,例如a,啊,\a,\141,\x61,\u0061,\U00000061,都是合法的rune literal。其格式定义如下: rune_lit = "'" ( unicode_value | byte_value ) "'" . unicode_value = unicode_char | little_u_value | big_u_value | escaped_char . byte_value = octal_byte_value | hex_byte_value . octal_byte_value = `\` octal_digit octal_digit octal_digit . hex_byte_value = `\` "x" hex_digit hex_digit . little_u_value = `\` "u" hex_digit hex_digit hex_digit hex_digit . big_u_value = `\` "U" hex_digit hex_digit hex_digit hex_digit hex_digit hex_digit hex_digit hex_digit . escaped_char = `\` ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | `\` | "'" | `"` ) . 其中,八进制的数字范围是0~255,Unicode转义字符通常要排除0x10FFFF以上的字符和surrogate字符。 看上去这样用单引号括起来的字面值像是一个字符串,但当源代码转换为内部表示时,它其实就是一个int32。所以var b byte = '蛤',其实就是为uint8赋了一个int32的值,会导致溢出。相应的,一个rune也可以在不产生溢出的条件下赋值给byte。 文本类型转换 三种基本文本类型之间可以相互转换,当然,有常规的做法,也有指针黑魔法。 string与[]byte的转换 string和bytes的转换是最常见的,因为通常通过IO得到的都是[]byte,例如io.Reader接口的方法签名为:Read(p []byte) (n int, err error)。但日常字符串操作使用的都是string,这就需要在两者之间进行转换。 常规做法 通常[]byte和string可以直接通过类型名强制转化,但实质上执行了一次堆复制。理论上stringHeader只是比sliceHeader少一个cap字段,但因为string需要满足不可变的约束,而[]byte是可变的,因此在执行[]byte到string的操作时会进行一次复制,在堆上新分配一次内存。 // byte to string s := string(b) // string index -> byte s[i] = b // []byte to string s := string(bytes) // string to []byte bytes := []byte(s) 黑魔法 利用unsafe.Pointer和reflect包可以实现很多禁忌的黑魔法,但这些操作对GC并不友好。最好不要尝试。 type Bytes []byte // 将string转换为[]byte,'可以修改',很危险,因为[]byte结构要多一个cap字段。 func StringBytes(s string) Bytes { return *(*Bytes)(unsafe.Pointer(&s)) } // 不拷贝地将[]byte转换为string func BytesString(b []byte) String { // 因为[]byte的Header只比string的Header多一个Cap字段。可以直接强制成`*String` return *(*String)(unsafe.Pointer(&b)) } // 获取&s[0],即存储字符串的字节数组的地址指针,Go里不允许这种操作。 func StringPointer(s string) unsafe.Pointer { p := (*reflect.StringHeader)(unsafe.Pointer(&s)) return unsafe.Pointer(p.Data) } // r获取&b[0],即[]byte底层数组的地址指针,Go里不允许这种操作 func BytesPointer(b []byte) unsafe.Pointer { p := (*reflect.SliceHeader)(unsafe.Pointer(&b)) return unsafe.Pointer(p.Data) } string与rune的转换 string是UTF8编码的字符串,因此对于非含有ASCII字符的字符串,是没法简单的直接索引的。例如 fmt.Printf("%x","hello"[0]),会取出第一个字节h的相应字节表示uint8,值为:0x68。然而 fmt.Printf("%s","你好"[0]),也是同理,在UTF-8编码中,汉字"你"被编码为0xeE4BDA0由三个字节组成,因此使用下标0去索引字符串,并不会取出第一个汉字字符的int32码位值0x4f60来,而是这三个字节中的第一个0xE4。 没有办法随机访问一个中文汉字是一件很蛋疼的事情。曾经Java和Javascript之类的语言就出于性能考虑使用UCS2/UTF-16来平衡时间和空间开销。但现在Unicode字符远远超过65535个了,这点优势已经荡然无存,想要准确的索引一个字符(尤其是带Emoji的),也需要用特制的API从头解码啦,啪啪啪打脸苍天饶过谁……。 常规方式 string和rune之间也可以通过类型名直接转换,不过string不能直接转换成单个的rune。 // rune to string str := string(r) // range string -> rune for i,r := range str // string to []rune runes := []rune(str) // []rune to string str := string(runes) 特殊支持 Go对于UTF-8有特殊的支持和处理(因为UTF-8和Go都是Ken发明的……。),这体现在对于string的range迭代上。 const nihongo = "日本語" for index, runeValue := range nihongo { fmt.Printf("%#U starts at byte position %d\n", runeValue, index) } U+65E5 '日' starts at byte position 0 U+672C '本' starts at byte position 3 U+8A9E '語' starts at byte position 6 直接索引string会得到字节序号和相应字节。而对string进行range迭代,获得的就是字符rune的索引与相应的rune。 byte与rune的转换 byte其实是uint8,而rune实际就是int32,所以uint8和int32两者之间的转换就是整数的转换。 但是[]uint8和[]int32是两个不同类型的整形数组,它们之间是没有直接强制转换的方法的,好在通过string来曲线救国:runes := []rune(string(bytes))
Parallel与Hierarchy是架构设计的两大法宝,缓存是Hierarchy在IO领域的体现。单线程场景下缓存机制的实现可以简单到不可思议,但很难想象成熟的应用会只有一个实例。在使用缓存的同时引入并发,就不得不考虑一个问题:如何保证每个实例的缓存与底层数据副本的数据一致性。 分布式系统受到CAP定理的约束,分区一致性P是一般来说是不允许牺牲的,不可能让两个实例对同样的请求却给出不同的结果。用缓存是为了更好的性能,所以如果还要追求可用性A,就一定会牺牲C。我们能做的,就是通过巧妙设计让AP系统的一致性损失最小化。 传统方法 最简单粗暴的办法就是定时重新拉取,例如每个整点,所有应用一起去数据库拉取一次最新版本的数据。很多应用都是这么做的。当然问题也很多:拉的间隔长了,变更不能及时应用,用户体验差;拉的频繁了,IO压力大。而且实例数目和数据大小一旦膨胀起来,对于宝贵的IO资源是很大的浪费。 异步通知是一种更好的办法,尤其是在读请求远多于写请求的情况下。接受到写请求的实例,通过发送广播的方式通知其他实例。Redis的PubSub就可以很好地实现这个功能。如果原本下层存储就是Redis自然是再方便不过,但如果下层存储是关系型数据库的话,为这样一个功能引入一个新的组件似乎有些得不偿失。况且考虑到后台管理程序或者其他应用如果在修改了数据库后也要去redis发布通知,实在太麻烦了。一种可行的办法是通过数据库中间件来监听RDS变动并广播通知,淘宝不少东西就是这么做的。但如果DB本身就能搞定的事情,为什么要加一个中间件呢?通过PostgreSQL的Notfiy-Listen机制,可以方便地实现这种功能。 目标 无论从任何渠道产生的数据库记录变更(增删改)都能被所有相关应用实时感知,用于维护自身缓存与数据库内容的一致性。 原理 PostgreSQL行级触发器 + Notify机制 + 自定义协议 + Smart Client 行级触发器:通过为我们感兴趣的表建立一个行级别的写触发器,对数据表中的每一行记录的Update,Delete,Insert都会出发自定义函数的执行。 Notify:通过PostgreSQL内建的异步通知机制向指定的Channel发送通知 自定义协议:协商消息格式,传递操作的类型与变更记录的标识 Smart Client:客户端监听消息变更,根据消息对缓存执行相应的操作。 实际上这样一套东西就是一个超简易的WAL(Write After Log)实现,从而使应用内部的缓存状态能与数据库保持实时一致(compare to poll)。 实现 DDL 这里以一个最简单的表作为示例,一张以主键标识的users表。 -- 用户表 CREATE TABLE users ( id TEXT, name TEXT, PRIMARY KEY (id) ); 触发器 -- 通知触发器 CREATE OR REPLACE FUNCTION notify_change() RETURNS TRIGGER AS $$ BEGIN IF (TG_OP = 'INSERT') THEN PERFORM pg_notify(TG_RELNAME || '_chan', 'I' || NEW.id); RETURN NEW; ELSIF (TG_OP = 'UPDATE') THEN PERFORM pg_notify(TG_RELNAME || '_chan', 'U' || NEW.id); RETURN NEW; ELSIF (TG_OP = 'DELETE') THEN PERFORM pg_notify(TG_RELNAME || '_chan', 'D' || OLD.id); RETURN OLD; END IF; END; $$ LANGUAGE plpgsql SECURITY DEFINER; 这里创建了一个触发器函数,通过内置变量TG_OP获取操作的名称,TG_RELNAME获取表名。每当触发器执行时,它会向名为<table_name>_chan的通道发送指定格式的消息:[I|U|D]<id> 题外话:通过行级触发器,还可以实现一些很实用的功能,例如In-DB Audit,自动更新字段值,统计信息,自定义备份策略与回滚逻辑等。 -- 为用户表创建行级触发器,监听INSERT UPDATE DELETE 操作。 CREATE TRIGGER t_user_notify AFTER INSERT OR UPDATE OR DELETE ON users FOR EACH ROW EXECUTE PROCEDURE notify_change(); 创建触发器也很简单,表级触发器对每次表变更执行一次,而行级触发器对每条记录都会执行一次。这样,数据库的里的工作就算全部完成了。 消息格式 通知需要传达出两个信息:变更的操作类型,变更的实体标记。 变更的操作类型就是增删改:INSERT,DELETE,UPDATE。通过一个打头的字符'[I|U|D]'就可以标识。 变更的对象可以通过实体主键来标识。如果不是字符串类型,还需要确定一种无歧义的序列化方式。 这里为了省事直接使用字符串类型作为ID,那么插入一条id=1的记录,对应的消息就是I1,更新一条id=5的记录消息就是U5,删除id=3的记录消息就是D3。 完全可以通过更复杂的消息协议实现更强大的功能。 SmartClient 数据库的机制需要客户端的配合才能生效,客户端需要监听数据库的变更通知,才能将变更实时应用到自己的缓存副本中。对于插入和更新,客户端需要根据ID重新拉取相应实体,对于删除,客户端需要删除自己缓存副本的相应实体。以Go语言为例,编写了一个简单的客户端模块。 本例中使用一个以User.ID作为键,User对象作为值的并发安全字典Users sync.Map作为缓存。 作为演示,启动了另一个goroutine对数据库写入了一些变更。 package main import "sync" import "strings" import "github.com/go-pg/pg" import . "github.com/Vonng/gopher/db/pg" import log "github.com/Sirupsen/logrus" type User struct { ID string `sql:",pk"` Name string } // Users 内部数据缓存 var Users sync.Map // 辅助函数:加载全部用户,初始化时使用 func LoadAllUser() { var users []User Pg.Query(&users, `SELECT ID,name FROM users;`) for _, user := range users { Users.Store(user.ID, user) } } // 辅助函数:根据ID重载单个用户,当插入和更新时执行 func LoadUser(id string) { user := User{ID: id} Pg.Select(&user) Users.Store(user.ID, user) } // 打印缓存内部的Key列表 func PrintUsers() string { var buf []string Users.Range(func(key, value interface{}) bool { buf = append(buf, key.(string)); return true }) return strings.Join(buf, ",") } // ListenUserChange 会监听PostgreSQL users数据表中的变动通知,并维护缓存状态 func ListenUserChange() { go func(c <-chan *pg.Notification) { for notify := range c { action, id := notify.Payload[0], notify.Payload[1:] switch action { case 'I': fallthrough case 'U': LoadUser(id); case 'D': Users.Delete(id) } log.Infof("[NOTIFY] Action:%c ID:%s Users: %s", action, id, PrintUsers()) } }(Pg.Listen("users_chan").Channel()) } // MakeSomeChange 会向数据库写入一些变更 func MakeSomeChange() { Pg.Exec(`TRUNCATE TABLE users;`) Pg.Insert(&User{"001", "张三"}) Pg.Insert(&User{"002", "李四"}) Pg.Insert(&User{"003", "王五"}) // 插入 Pg.Update(&User{"003", "王麻子"}) // 改名 Pg.Delete(&User{ID: "002"}) // 删除 } func main() { LoadAllUser() ListenUserChange() go MakeSomeChange() <-make(chan struct{}) } 运行结果如下: [NOTIFY] Action:I ID:001 Users: 001 [NOTIFY] Action:I ID:002 Users: 001,002 [NOTIFY] Action:I ID:003 Users: 002,003,001 [NOTIFY] Action:U ID:003 Users: 001,002,003 [NOTIFY] Action:D ID:002 Users: 001,003 可以看出,缓存确是与数据库保持了同样的状态。 应用场景 读远大于写的场景。
Python是时髦的机器学习御用开发语言,Golang是大红大紫的新时代后端开发语言。Python很适合让搞算法的写写模型,而Golang很适合提供API服务,两位同志都红的发紫,这里就介绍一下正确搅基的办法。 原理 Python提供了丰富的C-API。而C和Go又可以通过cgo无缝集成。所以,直接通过Golang调用libpython,就可以实现Go调Python的功能了。确实没啥神奇,只要会用C调Python,马上就知道怎么用了。但问题是,如果有的选择,这个年代还有多少人愿意去裸写C和C++呢?诚心默念Golang大法好。 准备工作 Python :确保Python正确安装,所谓正确安装,就是在系统中能找到libpython.so(dylib),找到Python.h。一般linux直接安装python-devel,mac直接用homebrew安装就可以。 Golang安装:Golang不需要什么特殊的处理,能找到go即可。 安装libpython-go-binding 虽然直接用cgo调用libpython也不是不可以,但是有native-binding用起来肯定要爽的多。Github上有一个现成的Binding库go-python。 go get github.com/sbinet/go-python 如果Python安装正确,这里会自动编译并显示提示,事就这样成了。 Have a try 首先写一个测试Python脚本 import numpy import sklearn a = 10 def b(xixi): return xixi + "haha" 然后写一个Go脚本: package main import ( "github.com/sbinet/go-python" "fmt" ) func init() { err := python.Initialize() if err != nil { panic(err.Error()) } } var PyStr = python.PyString_FromString var GoStr = python.PyString_AS_STRING func main() { // import hello InsertBeforeSysPath("/Users/vonng/anaconda2/lib/python2.7/site-packages") hello := ImportModule("/Users/vonng/Dev/go/src/gitlab.alibaba-inc.com/cplus", "hello") fmt.Printf("[MODULE] repr(hello) = %s\n", GoStr(hello.Repr())) // print(hello.a) a := hello.GetAttrString("a") fmt.Printf("[VARS] a = %#v\n", python.PyInt_AsLong(a)) // print(hello.b) b := hello.GetAttrString("b") fmt.Printf("[FUNC] b = %#v\n", b) // args = tuple("xixi",) bArgs := python.PyTuple_New(1) python.PyTuple_SetItem(bArgs, 0, PyStr("xixi")) // b(*args) res := b.Call(bArgs, python.Py_None) fmt.Printf("[CALL] b('xixi') = %s\n", GoStr(res)) // sklearn sklearn := hello.GetAttrString("sklearn") skVersion := sklearn.GetAttrString("__version__") fmt.Printf("[IMPORT] sklearn = %s\n", GoStr(sklearn.Repr())) fmt.Printf("[IMPORT] sklearn version = %s\n", GoStr(skVersion.Repr())) } // InsertBeforeSysPath will add given dir to python import path func InsertBeforeSysPath(p string) string { sysModule := python.PyImport_ImportModule("sys") path := sysModule.GetAttrString("path") python.PyList_Insert(path, 0, PyStr(p)) return GoStr(path.Repr()) } // ImportModule will import python module from given directory func ImportModule(dir, name string) *python.PyObject { sysModule := python.PyImport_ImportModule("sys") // import sys path := sysModule.GetAttrString("path") // path = sys.path python.PyList_Insert(path, 0, PyStr(dir)) // path.insert(0, dir) return python.PyImport_ImportModule(name) // return __import__(name) } 打印输出为: repr(hello) = <module 'hello' from '/Users/vonng/Dev/go/src/gitlab.alibaba-inc.com/cplus/hello.pyc'> a = 10 b = &python.PyObject{ptr:(*python._Ctype_struct__object)(0xe90b1b8)} b('xixi') = xixihaha sklearn = <module 'sklearn' from '/Users/vonng/anaconda2/lib/python2.7/site-packages/sklearn/__init__.pyc'> sklearn version = '0.18.1' 这里简单解释一下。首先将这个脚本的路径添加到sys.path中。然后调用PyImport_ImportModule导入包 使用GetAttrString可以根据属性名获取对象的属性,相当于python中的.操作。调用Python函数可以采用Object.Call方法,,列表参数使用Tuple来构建。返回值用PyString_AS_STRING从Python字符串转换为C或Go的字符串。 更多用法可以参考Python-C API文档。 但是只要有这几个API,就足够 Make python module rock & roll。充分利用Golang和Python各自的特性,构建灵活而强大的应用了。
神经网络表示 神经元模型 神经网络从大脑的工作原理得到启发,可用于解决通用的学习问题。神经网络的基本组成单元是神经元(neuron)。每个神经元具有一个轴突和多个树突。每个连接到本神经元的树突都是一个输入,当所有输入树突的兴奋水平之和超过某一阈值,神经元就会被激活。激活的神经元会沿着其轴突发射信号,轴突分出数以万计的树突连接至其他神经元,并将本神经元的输出并作为其他神经元的输入。数学上,神经元可以用感知机的模型表示。 一个神经元的数学模型主要包括以下内容: 名称 符号 说明 输入 (input) $x$ 列向量 权值 (weight) $w$ 行向量,维度等于输入个数 偏置 (bias) $b$ 标量值,是阈值的相反数 带权输入 (weighted input) $z$ $z=w · x + b$ ,激活函数的输入值 激活函数 (activation function) $σ$ 接受带权输入,给出激活值。 激活值 (activation) $a$ 标量值,$a = σ(vec{w}·vec{x}+b)$ 激活函数表达式 $$ a = \sigma( \left[ \begin{matrix} w_{1} & ⋯ & w_{n} \\ \end{matrix}\right] · \left[ \begin{array}{x} x_1 \\ ⋮ \\ ⋮ \\ x_n \end{array}\right] + b ) $$ 激活函数通常使用S型函数,又称为sigmoid或者logsig,因为该函数具有良好的特性:光滑可微,形状接近感知机所使用的硬极限传输函数,函数值与导数值计算方便。 $$ σ(z) = \frac 1 {1+e^{-z}} $$ $$ σ'(z) = σ(z)(1-σ(z)) $$ 也有一些其他的激活函数,例如:硬极限传输函数(hardlim),对称硬极限函数(hardlims),线性函数(purelin) , 对称饱和线性函数(satlins) ,对数-s形函数(logsig) ,正线性函数(poslin),双曲正切S形函数(tansig),竞争函数(compet),有时候为了学习速度或者其他原因也会使用,表过不提。 单层神经网络模型 可以并行操作的神经元组成的集合,称为神经网络的一层。 现在考虑一个具有$n$个输入,$s$个神经元(输出)的单层神经网络,则原来单个神经元的数学模型可扩展如下: 名称 符号 说明 输入 $x$ 同层所有神经元共用输入,故输入保持不变,仍为$(n×1)$列向量 权值 $W$ 由$1 × n$行向量,变为$s × n$矩阵,每一行表示一个神经元的权值信息 偏置 $b$ 由$1 × 1$标量变为$s × 1$列向量 带权输入 $z$ 由$1 × 1$标量变为$s × 1$列向量 激活值 $a$ 由$1 × 1$标量变为$s × 1$列向量 激活函数向量表达式 $$ \left[ \begin{array}{a} a_1 \\ ⋮ \\ a_s \end{array}\right] = \sigma( \left[ \begin{matrix} w_{1,1} & ⋯ & w_{1,n} \\ ⋮ & ⋱ & ⋮ \\ w_{s,1} & ⋯ & w_{s,n} \\ \end{matrix}\right] · \left[ \begin{array}{x} x_1 \\ ⋮ \\ ⋮ \\ x_n \end{array}\right] + \left[ \begin{array}{b} b_1 \\ ⋮ \\ b_s \end{array}\right] ) $$ 单层神经网络能力有限,通常都会将多个单层神经网络的输出和输入相连,组成多层神经网络。 多层神经网络模型 多层神经网络的层数从1开始计数,第一层为输入层,第$L$层为输出层,其它的层称为隐含层。 每一层神经网络都有自己的参数$W,b,z,a,⋯$,为了区别,使用上标区分:$W^2,W^3,⋯$。 整个多层网络的输入,即为输入层的激活值$x=a^1$,整个网络的输出,即为输出层的激活值:$y'=a^L$。 因为输入层没有神经元,所以该层所有参数中只有激活值$a^1$作为网络输入值而存在,没有$W^1,b^1,z^1$等。 现在考虑一个$L$层的神经网络,其各层神经元个数依次为:$d_1,d_2,⋯,d_L$。则该网络的数学模型可扩展如下: 名称 符号 说明 输入 $x$ 输入仍然保持不变,为$(d_1×1)$列向量 权值 $W$ 由$s × n$矩阵扩展为$L-1$个矩阵组成的列表:$W^2_{d_2 × d_1},⋯,W^L_{d_L × d_{L-1}}$ 偏置 $b$ 由$s × 1$列向量扩展为$L-1$个列向量组成的列表:$b^2_{d_2},⋯,b^L_{d_L}$ 带权输入 $z$ 由$s × 1$列向量扩展为$L-1$个列向量组成的列表:$z^2_{d_2},⋯,z^L_{d_L}$ 激活值 $a$ 由$s × 1$列向量扩展为$L$个列向量组成的列表:$a^1_{d_1},a^2_{d_2},⋯,a^L_{d_L}$ 激活函数矩阵表达式 $$ \left[ \begin{array}{a} a^l_1 \\ ⋮ \\ a^l_{d_l} \end{array}\right] = \sigma( \left[ \begin{matrix} w^l_{1,1} & ⋯ & w^l_{1,d_{l-1}} \\ ⋮ & ⋱ & ⋮ \\ w^l_{d_l,1} & ⋯ & w^l_{d_l,d_{l-1}} \\ \end{matrix}\right] · \left[ \begin{array}{x} a^{l-1}_1 \\ ⋮ \\ ⋮ \\ a^{l-1}_{d_{l-1}} \end{array}\right] + \left[ \begin{array}{b} b^l_1 \\ ⋮ \\ b^l_{d_l} \end{array}\right]) $$ 权值矩阵的涵义 多层神经网络的权值由一系列权值矩阵表示 第$l$层网络的权值矩阵可记作$W^l$,表示前一层($l-1$)到本层($l$ )的连接权重 $W^l$的第$j$行可记作$W^l_{j*}$ ,表示从$l-1$层所有$d_{l-1}$个神经元出发,到达$l$ 层$j$号神经元的连接权重 $W^l$的第$k$列可记作$W^l_{*k}$ ,表示从$l-1$层第$k$号神经元出发,到达$l$ 层所有$d_l$个神经元的连接权重 $W^l$的$j$行$k$列可记作$W^l_{jk}$,表示从$l-1$层$k$号神经元出发,到达$l$ 层$j$神经元的连接权重 如图,$w^3_{24}$表示从2层4号神经元到3层2号神经元的连接权值: 只要记住,权值矩阵$W$的行标表示本层神经元的标号,列标表示上层神经元的标号即可。 神经网络推断 前馈(feed forward)是指神经网络接受输入,产生输出的一次计算过程。又称为一次推断(inference)。 计算过程如下: $$ \begin{align} a^1 &= x \\ a^2 &= σ(W^2a^1 + b^2) \\ a^3 &= σ(W^3a^2 + b^3) \\ ⋯ \\ a^L &= σ(W^La^{L-1} + b^L) \\ y &= a^L \\ \end{align} $$ 推断实际上就是一系列矩阵乘法与向量运算,一个训练好的神经网络可以高效地使用各种语言实现。神经网络的功能是通过推断而体现的。推断实现起来很简单,但如何训练神经网络才是真正的难点。 神经网络训练 神经网络的训练,是调整网络中的权值参数与偏置参数,从而提高网络工作效果的过程。 通常使用梯度下降(Gradient Descent)的方法来调整神经网络的参数,首先要定义一个代价函数(cost function)用以衡量神经网络的误差,然后通过梯度下降方法计算合适的参数修正量,从而最小化网络误差。 代价函数 代价函数是用于衡量神经网络工作效果的函数,是定义在一个或多个样本上的实值函数,通常应满足以下条件: 误差是非负的,神经网络效果越好,误差越小 代价可以写成神经网络输出的函数 总体代价等于个体样本代价的均值:$C=\frac{1}{n} \sum_x C_x$ 最常用的一个简单的代价函数是:二次代价函数,又称为均方误差(MeanSquareError) $$ C(w,b) = \frac{1}{2n} \sum_x{{\|y(x)-a\|}^2} $$ 前面的系数$\frac 1 2$是为了求导后简洁的形式而添加的,$n$是使用样本的数量,这里$y$和$x$都是已知的样本数据。 理论上任何可以反映网络工作效果的指标都可以作为代价函数。但之所以使用MSE,而不是诸如“正确分类图像个数”的指标,是因为只有一个光滑可导的代价函数才可以使用梯度下降(Gradient Descent)调整参数。 样本的使用 代价函数的计算需要一个或多个训练样本。当训练样本非常多时,如果每轮训练都要重新计算网络整个训练集上所有样本的误差函数,开销非常大,速度难以接受。若只使用总体的一小部分,计算就能快很多。不过这样做依赖一个假设:随机样本的代价,近似等于总体的代价。 按照使用样本的方式,梯度下降又分为: 批量梯度下降法(Batch GD):最原始的形式,更新每一参数都使用所有样本。可以得到全局最优解,易于并行实现,但当样本数量很多时,训练速度极慢。 随机梯度下降法(Stochastic GD):解决BGD训练慢的问题,每次随机使用一个样本。训练速度快,但准确度下降,且并不是全局最优,也不易于并行实现。 小批量梯度下降法(MiniBatch GD):在每次更新参数时使用b个样本(例如每次10个样本),在BGD与SGD中取得折中。 每次只使用一个样本时,又称为在线学习或递增学习。 当训练集的所有样本都被使用过一轮,称为完成一轮迭代。 梯度下降算法 若希望通过调整神经网络中的某个参数来减小整体代价,则可以考虑微分的方法。因为每层的激活函数,以及最终的代价函数都是光滑可导的。所以最终的代价函数$C$对于某个我们感兴趣的参数$w,b$也是光滑可导的。轻微拨动某个参数的值,最终的误差值也会发生连续的轻微的变化。不断地沿着参数的梯度方向,轻微调整每个参数的值,使得总误差值向下降的方向前进,最终达到极值点。就是梯度下降法的核心思想。 梯度下降的逻辑 现在假设代价函数$C$为两个变量$v_1,v_2$的可微函数,梯度下降实际上就是选择合适的$Δv$,使得$ΔC$为负。由微积分可知: $$ ΔC ≈ \frac{∂C}{∂v_1} Δv_1 + \frac{∂C}{∂v_2} Δv_2 $$ 这里$Δv$是向量:$Δv = left[ begin{array}{v} Δv_1 \ Δv_2 end{array}right]$,$ΔC$是梯度向量$left[ begin{array}{C} frac{∂C}{∂v_1} \ frac{∂C}{∂v_2} end{array} right]$,于是上式可重写为 $$ ΔC ≈ ∇C \cdot Δv $$ 怎样的$Δv$才能令代价函数的变化量为负呢?一种简单办法是令即$Δv$取一个与梯度$∇C$共线反向的小向量,此时$Δv = -η∇C$ ,则损失函数变化量$ΔC ≈ -η{∇C}^2$,可以确保为负值。按照这种方法,通过不断调整$v$:$v → v' = v -η∇C$,使得$C$最终达到极小值点。 这即梯度下降的涵义所在:所有参数都会沿着自己的梯度(导数)方向不断进行轻微下降,使得总误差到达极值点。 对于神经网络,学习的参数实际上是权重$w$与偏置量$b$。原理是一样的,不过这里的$w,b$数目非常巨大 $$ w →w' = w-η\frac{∂C}{∂w} \\ b → b' = b-η\frac{∂C}{∂b} $$ 真正棘手的问题在于梯度$∇C_w,∇C_b$的计算方式。如果使用微分的方法,通过$frac {C(p+ε)-C} {ε}$来求参数的梯度,那么网络中的每一个参数都需要进行一次前馈和一次$C(p+ε)$的计算,在神经网络汪洋大海般的参数面前,这样的办法是行不通的。 反向传播(Back propagation)算法可以解决这一问题。通过巧妙的简化, 可以在一次前馈与一次反传中,高效地计算整个网络中所有参数梯度。 反向传播 反向传播算法接受一个打标样本$(x,y)$作为输入,给出网络中所有参数$(W,b)$的梯度。 反向传播误差δ 反向传播算法需要引入一个新的概念:误差$δ$。误差的定义源于这样一种朴素的思想:如果轻微修改某个神经元的带权输入$z$,而最终代价$C$已不再变化,则可认为$z$已经到达极值点,调整的很好了。于是损失函数$C$对某神经元带权输入$z$的偏导$frac {∂C}{∂z}$可以作为该神经元上误差$δ$的度量。故定义第$l$层的第$j^{th}$个神经元上的误差$δ^l_j$为: $$ δ^l_j ≡ \frac{∂C}{∂z^l_j} $$ 与激活值$a$,带权输入$z$一样,误差也可以写作向量。第$l$层的误差向量记作$δ^l$。虽然看上去差不多,但之所以使用带权输入$z$而不是激活值输出$a$来定义本层的误差,有着形式上巧妙的设计。 引入反向传播误差的概念,是为了通过误差向量来计算梯度$∇C_w,∇C_b$。 反向传播算法一言蔽之:计算出输出层误差,通过递推方程逐层回算出每一层的误差,再由每一层的误差算出本层的权值梯度与偏置梯度。 这需要解决四个问题: 递推首项:如何计算输出层的误差:$δ^L$ 递推方程:如何根据后一层的误差$δ^{l+1}$计算前一层误差$δ^l$ 权值梯度:如何根据本层误差$δ^l$计算本层权值梯度$∇W^l$ 偏置梯度:如何根据本层误差$δ^l$计算本层偏置梯度$∇b^l$ 这四个问题,可以通过四个反向传播方程得到解决。 反向传播方程 方程 说明 编号 $δ^L = ∇C_a ⊙ σ'(z^L)$ 输出层误差计算公式 BP1 $δ^l = (W^{l+1})^T δ^{l+1} ⊙ σ'(z^l)$ 误差传递公式 BP2 $∇C_{W^l} = δ^l × {(a^{l-1})}^T $ 权值梯度计算公式 BP3 $∇C_b = δ^l$ 偏置梯度计算公式 BP4 当误差函数取MSE:$C = \frac 1 2 \|\vec{y} -\vec{a}\|^2= \frac 1 2 [(y_1 - a_1)^2 + \cdots + (y_{d_L} - a_{d_L})^2]$,激活函数取sigmoid时: 计算方程 说明 编号 $δ^L = (a^L - y) ⊙(1-a^L)⊙ a^L$ 输出层误差需要$a^L$和$y$ BP1 $δ^l = (W^{l+1})^T δ^{l+1} ⊙(1-a^l)⊙ a^l $ 本层误差需要:后层权值$W^{l+1}$,后层误差$δ^{l+1}$,本层输出$a^l$ BP2 $∇C_{W^l} = δ^l × {(a^{l-1})}^T $ 权值梯度需要:本层误差$δ^l$,前层输出$a^{l-1}$ BP3 $∇C_b = δ^l$ 偏置梯度需要:本层误差$δ^l$ BP4 反向传播方程的证明 BP1:输出层误差方程 输出层误差方程给出了根据网络输出$a^L$与标记结果$y$计算输出层误差$δ$的方法: $$ δ^L = (a^L - y) ⊙(1-a^L)⊙ a^L $$ 证明 因为$a^L = σ(z^L)$,本方程可以直接从反向传播误差的定义,通过$a^L$作为中间变量链式求导推导得出: $$ \frac{∂C}{∂z^L} = \frac{∂C}{∂a^L} \frac{∂a^L}{∂z^L} = ∇C_a σ'(z^L) $$ 而因为误差函数$C = frac 1 2 |vec{y} -vec{a}|^2= frac 1 2 [(y_1 - a_1)^2 + ⋯ + (y_{d_L} - a_{d_L})^2]$,方程两侧对某个$a_j$取偏导则有: $$ \frac {∂C}{∂a^L_j} = (a^L_j-y_j) $$ 因为误差函数中,其他神经元的输出不会影响到误差函数对神经元$j$输出的偏导,系数也正好平掉了。写作向量形式即为:$ (a^L - y) $。另一方面,易证$σ'(z^L) = (1-a^L)⊙ a^L$。 QED BP2:误差传递方程 误差传递方程给出了根据后一层误差计算前一层误差的方法: $$ δ^l = (W^{l+1})^T δ^{l+1} ⊙ σ'(z^l) $$ 证明 本方程可以直接从反向传播误差的定义,以后一层所有神经元的带权输入$z^{l+1}$作为中间变量进行链式求导推导出: $$ δ^l_j = \frac {∂C}{∂z^l_j} = \sum_{k=1}^{d_{l+1}} \frac{∂C}{∂z^{l+1}_k} \frac{∂z^{l+1}_k}{∂z^{l}_j} = \sum_{k=1}^{d_{l+1}} (δ^{l+1}_k \frac{∂z^{l+1}_k}{∂z^{l}_j}) $$ 通过链式求导,引入后一层带权输入作为中间变量,从而在方程右侧引入后一层误差的表达形式。现在要解决的就是$frac{∂z^{l+1}_k}{∂z^{l}_j}$ 是什么的问题。由带权输入的定义$z = wx + b$可知: $$ z^{l+1}_k = W^{l+1}_{k,*} ·a^l + b^{l+1}_k = W^{l+1}_{k,*} · σ(z^l) + b^{l+1}_k = \sum_{j=1}^{d_{l}}(w_{kj}^{l+1} σ(z^l_j)) + b^{l+1}_k $$ 两边同时对$z^{l}_j$求导可以得到: $$ \frac{∂z^{l+1}_k}{∂z^{l}_j} = w^{l+1}_{kj} σ'(z^l) $$ 回代则有: $$ \begin{align} δ^l_j & = \sum_{k=1}^{d_{l+1}} (δ^{l+1}_k \frac{∂z^{l+1}_k}{∂z^{l}_j}) \\ & = σ'(z^l) \sum_{k=1}^{d_{l+1}} (δ^{l+1}_k w^{l+1}_{kj}) \\ & = σ'(z^l) ⊙ [(δ^{l+1}) · W^{l+1}_{*.j}] \\ & = σ'(z^l) ⊙ [(W^{l+1})^T_{j,*} · (δ^{l+1}) ]\\ \end{align} $$ 这里,对后一层所有神经元的误差权值之积求和,可以改写为两个向量的点积: 后一层$k$个神经元的误差向量 后一层权值矩阵的第$j$列,即所有从本层$j$神经元出发前往下一层所有$k$个神经元的权值。 又因为向量点积可以改写为矩阵乘法:以行向量乘以列向量的方式进行,所以将权值矩阵转置,原来拿的是列,现在则拿出了行向量。这时候再改写回向量形式为: $$ δ^l = σ'(z^l) ⊙ (W^{l+1})^Tδ^{l+1} $$ QED BP3:权值梯度方程 每一层的权值梯度$∇C_{W^l}$可以根据本层的误差向量(列向量),与上层的输出向量(行向量)的外积得出。 $$ ∇C_{W^l} = δ^l × {(a^{l-1})}^T $$ 证明 由误差的定义,以$w^l_{jk}$作为中间变量求偏导可得: $$ \begin{align} δ^l_j & = \frac{∂C}{∂z^l_j} = \frac{∂C}{∂w^l_{jk}} \frac{∂ w_{jk}}{∂ z^l_j} = ∇C_{w^l_{jk}} \frac{∂w_{jk}}{∂ z^l_j} \end{align} $$ 由定义可得,第$l$层第$j$个神经元的带权输入$z^l_j$: $$ z^l_j = \sum_k w^l_{jk} a^{l-1}_k + b^l_j $$ 两侧对$w_{jk}^l$求导得到: $$ \frac{\partial z_j}{\partial w^l_{jk}} = a^{l-1}_k $$ 代回则有: $$ ∇C_{w^l_{jk}} = δ^l_j \frac{∂ z^l_j}{∂w_{jk}} = δ^l_j a^{l-1}_k $$ 观察可知,向量形式是一个外积: $$ ∇C_{W^l} = δ^l × {(a^{l-1})}^T $$ 本层误差行向量:$δ^l$,维度为($d_l \times 1$) 上层激活列向量:$(a^{l-1})^T$,维度为($1 times d_{l-1}$) QED BP4:偏置梯度方程 $$ ∇C_b = δ^l $$ 证明 由定义可知: $$ δ^l_j = \frac{∂C}{∂z^l_j} = \frac{∂C}{∂b^l_j} \frac{∂b_j}{∂z^l_j} = ∇C_{b^l_{j}} \frac{∂b_j}{∂z^l_j} $$ 因为$z^l_j = W^l_{*,j} \cdot a^{l-1} + b^l_j$,两侧对$z_j^l$求导得到$1=frac{∂b_j}{∂z^l_j}$。于是回代得到:$∇C_{b^l_{j}} =δ^l_j $ , QED 至此,四个方程均已证毕。只要将其转换为代码即可工作。 神经网络的实现 作为概念验证,这里给出了MNIST手写数字分类神经网络的Python实现。 # coding: utf-8 # author: vonng(fengruohang@outlook.com) # ctime: 2017-05-10 import random import numpy as np class Network(object): def __init__(self, sizes): self.sizes = sizes self.L = len(sizes) self.layers = range(0, self.L - 1) self.w = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])] self.b = [np.random.randn(x, 1) for x in sizes[1:]] def feed_forward(self, a): for l in self.layers: a = 1.0 / (1.0 + np.exp(-np.dot(self.w[l], a) - self.b[l])) return a def gradient_descent(self, train, test, epoches=30, m=10, eta=3.0): for round in range(epoches): # generate mini batch random.shuffle(train) for batch in [train_data[k:k + m] for k in xrange(0, len(train), m)]: x = np.array([item[0].reshape(784) for item in batch]).transpose() y = np.array([item[1].reshape(10) for item in batch]).transpose() n, r, a = len(batch), eta / len(batch), [x] # forward & save activations for l in self.layers: a.append(1.0 / (np.exp(-np.dot(self.w[l], a[-1]) - self.b[l]) + 1)) # back propagation d = (a[-1] - y) * a[-1] * (1 - a[-1]) #BP1 for l in range(1, self.L): # l is reverse index since last layer if l > 1: #BP2 d = np.dot(self.w[-l + 1].transpose(), d) * a[-l] * (1 - a[-l]) self.w[-l] -= r * np.dot(d, a[-l - 1].transpose()) #BP3 self.b[-l] -= r * np.sum(d, axis=1, keepdims=True) #BP4 # evaluate acc_cnt = sum([np.argmax(self.feed_forward(x)) == y for x, y in test]) print "Round {%d}: {%s}/{%d}" % (round, acc_cnt, len(test_data)) if __name__ == '__main__': import mnist_loader train_data, valid_data, test_data = mnist_loader.load_data_wrapper() net = Network([784, 100, 10]) net.gradient_descent(train_data, test_data, epoches=100, m=10, eta=2.0) 数据加载脚本:mnist_loader.py 。输入数据为二元组列表:(input(784,1), output(10,1)) $ python net.py Round {0}: {9136}/{10000} Round {1}: {9265}/{10000} Round {2}: {9327}/{10000} Round {3}: {9387}/{10000} Round {4}: {9418}/{10000} Round {5}: {9470}/{10000} Round {6}: {9469}/{10000} Round {7}: {9484}/{10000} Round {8}: {9509}/{10000} Round {9}: {9539}/{10000} Round {10}: {9526}/{10000} 一轮迭代后,网络在测试集上的分类准确率就达到90%,最终收敛至96%左右。 对于五十行代码,这个效果是值得惊叹的。然而96%的准确率在实际生产中恐怕仍然是无法接受的。想要达到更好的效果,就需要对神经网络进行优化。 神经网络优化 神经网络的基础知识也就这么多,但改善其表现却是一个无尽的挑战。每一种优化的手段,都可以当做一个进阶的Topic深入研究。优化手段也是八仙过海,有数学,有科学,有工程学,也有哲学,还有玄学…… 改进神经网络的学习效果有几种主要的方法: 选取更好的代价函数:例如交叉熵(cross-entropy) 规范化(regularization):L2规范化、弃权、L1规范化 采用其他的激活神经元:线性修正神经元(ReLU),双曲正切神经元(tansig) 修改神经网络的输出层:柔性最大值(softmax) 修改神经网络输入的组织方式:递归神经网络(Recurrent NN),卷积神经网络(Convolutional NN)。 添加层数:深度神经网络(Deep NN) 通过尝试,选择合适的超参数(hyper-parameters),按照迭代轮数或评估效果动态调整超参数。 采用其他的梯度下降方法:基于动量的梯度下降 使用更好的初始化权重 人为扩展已有训练数据集 这里介绍两种方法,交叉熵代价函数与L2规范化。因为它们: 实现简单,修改一行代码即可实现,还减小了计算开销。 效果立竿见影,将分类错误率从4%降低到2%以下。 代价函数:交叉熵 MSE是一个不错的代价函数,然而它存在一个很尴尬的问题:学习速度。 MSE输出层误差的计算公式为: $$ δ^L = (a^L - y)σ'(z^L) $$ sigmoid又称为逻辑斯蒂曲线,其导数$σ'$是一个钟形曲线。所以当带权输入$z$从大到小或从小到大时,梯度的变化会经历一个“小,大,小”的过程。学习的速度也会被导数项拖累,存在一个“慢,快,慢”的过程。 MSE Cross Entropy 若采用交叉熵(cross entropy)误差函数: $$ C = - \frac 1 n \sum_x [ y ln(a) + (1-y)ln(1-a)] $$ 对于单个样本,即 $$ C = - [ y ln(a) + (1-y)ln(1-a)] $$ 虽然看起来很复杂,但输出层的误差公式变得异常简单,变为:$δ^L = a^L - y$ 比起MSE少掉了导数因子,所以误差直接和(预测值-实际值)成正比,不会遇到学习速度被激活函数的导数拖慢的问题,而且计算起来更简单了! 证明 $C$对网络输出值$a$求导,则有: $$ ∇C_a = \frac {∂C} {∂a^L} = - [ \frac y a - \frac {(1-y)} {1-a}] = \frac {a - y} {a (1-a)} $$ 反向传播的四个基本方程里,与误差函数$C$相关的只有BP1:即输出层误差的计算方式。 $$ δ^L = ∇C_a ⊙ σ'(z^L) $$ 现在$C$换了计算方式,将新的误差函数$C$对输出值$a^L$的梯度$frac {∂C} {∂a^L}$带回BP1,即有: $$ δ^L = \frac {a - y} {a (1-a)}× a(1-a) = a-y $$ 规范化 拥有大量的自由参数的模型能够描述特别神奇的现象。 费米说:"With four parameters I can fit an elephant, and with five I can make him wiggle his trunk"。神经网络这种动辄百万的参数的模型能拟合出什么奇妙的东西是难以想象的。 一个模型能够很好的拟合已有的数据,可能只是因为模型中足够的自由度,使得它可以描述几乎所有给定大小的数据集,而不是真正洞察数据集背后的本质。发生这种情形时,模型对已有的数据表现的很好,但是对新的数据很难泛化。这种情况称为过拟合(overfitting)。 例如用3阶多项式拟合一个带随机噪声的正弦函数,看上去就还不错;而10阶多项式,虽然完美拟合了数据集中的所有点,但实际预测能力就很离谱了。它拟合的更多地是数据集中的噪声,而非数据集背后的潜在规律。 x, xs = np.linspace(0, 2 * np.pi, 10), np.arange(0, 2 * np.pi, 0.001) y = np.sin(x) + np.random.randn(10) * 0.4 p1,p2 = np.polyfit(x, y, 10), np.polyfit(x, y, 3) plt.plot(xs, np.polyval(p1, xs));plt.plot(x, y, 'ro');plt.plot(xs, np.sin(xs), 'r--') plt.plot(xs, np.polyval(p2, xs));plt.plot(x, y, 'ro');plt.plot(xs, np.sin(xs), 'r--') 3阶多项式 10阶多项式 一个模型真正的测验标准,是它对没有见过的场景的预测能力,称为泛化能力(generalize)。 如何避免过拟合?按照奥卡姆剃刀原理:两个效果相同的解释,选择简单的那一个。 当然这个原理只是我们抱有的一种信念,并不是真正的定理铁律:这些数据点真的由拟合出的十阶多项式产生,也不能否认这种可能… 总之,如果出现非常大的权重参数,通常就意味着过拟合。例如拟合所得十阶多项式系数就非常畸形: -0.001278386964370502 0.02826407452052734 -0.20310716176300195 0.049178327509096835 7.376259706365357 -46.295365250182925 135.58265224859255 -211.767050023543 167.26204130954324 -50.95259728945658 0.4211227089756039 通过添加权重衰减项,可以有效遏制过拟合。例如$L2$规范化为损失函数添加了一个$frac λ 2 w^2$的惩罚项: $$ C = -\frac{1}{n} \sum_{xj} \left[ y_j \ln a^L_j+(1-y_j) \ln (1-a^L_j)\right] + \frac{\lambda}{2n} \sum_w w^2 $$ 所以,权重越大,损失值越大,这就避免神经网络了向拟合出畸形参数的方向发展。 这里使用的是交叉熵损失函数。但无论哪种损失函数,都可以写成: $$ C = C_0 + \frac {λ}{2n} \sum_w {w^2} $$ 其中原始的代价函数为$C_0$。那么,原来损失函数对权值的偏导,就可以写成: $$ \frac{∂C}{∂w} = \frac{ ∂C_0}{∂w}+\frac{λ}{n} w $$ 因此,引入$L2$规范化惩罚项在计算上的唯一变化,就是在处理权值梯度时首先要乘一个衰减系数: $$ w → w' = w\left(1 - \frac{ηλ}{n} \right)- η\frac{∂C_0}{∂ w} $$ 注意这里的$n$是所有的训练样本数,而不是一个小批次使用的训练样本数。 改进实现 # coding: utf-8 # author: vonng(fengruohang@outlook.com) # ctime: 2017-05-10 import random import numpy as np class Network(object): def __init__(self, sizes): self.sizes = sizes self.L = len(sizes) self.layers = range(0, self.L - 1) self.w = [np.random.randn(y, x) / np.sqrt(x) for x, y in zip(sizes[:-1], sizes[1:])] self.b = [np.random.randn(x, 1) for x in sizes[1:]] def feed_forward(self, a): for l in self.layers: a = 1.0 / (1.0 + np.exp(-np.dot(self.w[l], a) - self.b[l])) return a def gradient_descent(self, train, test, epoches=30, m=10, eta=0.1, lmd=5.0): n = len(train) for round in range(epoches): random.shuffle(train) for batch in [train_data[k:k + m] for k in xrange(0, len(train), m)]: x = np.array([item[0].reshape(784) for item in batch]).transpose() y = np.array([item[1].reshape(10) for item in batch]).transpose() r = eta / len(batch) w = 1 - eta * lmd / n a = [x] for l in self.layers: a.append(1.0 / (np.exp(-np.dot(self.w[l], a[-1]) - self.b[l]) + 1)) d = (a[-1] - y) # cross-entropy BP1 for l in range(1, self.L): if l > 1: # BP2 d = np.dot(self.w[-l + 1].transpose(), d) * a[-l] * (1 - a[-l]) self.w[-l] *= w # weight decay self.w[-l] -= r * np.dot(d, a[-l - 1].transpose()) # BP3 self.b[-l] -= r * np.sum(d, axis=1, keepdims=True) # BP4 acc_cnt = sum([np.argmax(self.feed_forward(x)) == y for x, y in test]) print "Round {%d}: {%s}/{%d}" % (round, acc_cnt, len(test_data)) if __name__ == '__main__': import mnist_loader train_data, valid_data, test_data = mnist_loader.load_data_wrapper() net = Network([784, 100, 10]) net.gradient_descent(train_data, test_data, epoches=50, m=10, eta=0.1, lmd=5.0) Round {0}: {9348}/{10000} Round {1}: {9538}/{10000} Round {2}: {9589}/{10000} Round {3}: {9667}/{10000} Round {4}: {9651}/{10000} Round {5}: {9676}/{10000} ... Round {25}: {9801}/{10000} Round {26}: {9799}/{10000} Round {27}: {9806}/{10000} Round {28}: {9804}/{10000} Round {29}: {9804}/{10000} Round {30}: {9802}/{10000} 可见只是简单的变更,就使准确率有了显著提高,最终收敛至98%。 修改Size为[784,128,64,10]添加一层隐藏层,可以进一步提升测试集准确率至98.33%,验证集至98.24%。 对于MNIST数字分类任务,目前最好的准确率为99.79%,那些识别错误的case,恐怕人类想要正确识别也很困难。神经网络的分类效果最新进展可以参看这里:classification_datasets_results。 本文是tensorflow官方推荐教程:Neural Networks and Deep Learning的笔记整理,原文Github Page。
Pure PostgreSQL实现推荐系统 推荐系统大家都熟悉哈,猜你喜欢,淘宝个性化什么的,前年双十一搞了个大新闻,拿了CEO特别贡献奖。 今天就来说说怎么用PostgreSQL 3分钟实现一个最简单ItemCF推荐系统,以推荐系统最喜闻乐见的movielens数据集为例。 原理 Item CF,全称Item Collaboration Filter,即基于物品的协同过滤,是目前业界应用最多的推荐算法。ItemCF不需要物品与用户的标签、属性,只要有用户对Item的行为日志就可以了,同时具有很好的可解释性。所以无论是亚马逊,Hulu,YouTube,balabala,用的都是该算法。 ItemCF算法的核心思想是:给用户推荐那些和他们之前喜欢的物品相似的物品。 这里有两个Point:用户喜欢物品怎么表示?物品之间的相似度怎样表示? 用户评分表 用户评分表有三个核心字段:user_id, movie_id, rating,分别是用户ID,物品ID,用户对物品的评分。 这个表怎么来呢?如果本来就是个评论打分网站,直接有用户对电影,音乐,小说的评分记录,那是最好不过。对于其他的场景,比如电商,社交网络,则可以通过行为日志生成这张评分表。例如,浏览,点击,收藏,购买,点击“我不喜欢”按钮,可以分别设一个喜好权重:0.1, 0.2, 0.3, 0.4, -100。然后最终计算出每个用户对每个物品的评分来。事就成了一半了。 物品相似度 还需要解决的一个问题是物品相似度的计算与表示。 假设一共有N个物品,则物品相似度数据可以表示为一个NxN的矩阵,第i行j列的值表示物品i与物品j之间的相似度。这样相似度表示的问题就解决了。 然后就是物品相似度矩阵的计算了,在此之前必须要做的一件事,就是定义什么是相似度? 两个物品之间的相似度有很多种定义与计算方式,如果我们知道物品的各种属性的话,就可以方便的根据各种“距离”来定义相似度。但ItemCF有一种更简单的定义方法,令N(i)为喜欢物品i的用户集合: $$ w_{ij} = \frac{|N(i) \cap N(j)|}{ \sqrt{ |N(i)| * |N(j)|}} $$ 即:同时喜欢物品i和物品j的人数,除以喜爱物品i人数和喜爱物品j人数的几何平均数。 可以简单的认为,只要用户给电影打分超过某一阈值,就是喜爱该电影。 推荐物品 现在有一个用户u,他对物品$i_1,i_2,…,i_n$的评分分别为$w_1,w_2,…,w_n$,则按照该评分权重累积所有物品的相似度,就可以得到用户对所有物品的评分了。 $$ \displaystyle p_{uj} = \sum_{i \in N(u) \cap S(i, K)} w_{ji}r_{ui} $$ 排序取TopN,就得到了推荐物品列表 实践 说了这么多废话,赶紧燥起来。 第一步:准备数据 下载数据集,开发测试的话选小规模的(100k)就可以。 对于ItemCF来说,有用的数据其实就是用户行为表,即ratings.csv -- movielens 用户评分数据集 CREATE TABLE mls_ratings ( user_id INTEGER, movie_id INTEGER, rating TEXT, timestamp INTEGER, PRIMARY KEY (user_id, movie_id) ); -- 从CSV导入数据,并将评分乘以2变为2~10的整数便于处理,将Unix时间戳转换为日期类型 COPY mls_ratings FROM '/Users/vonng/Dev/recsys/ml-latest-small/ratings.csv' DELIMITER ',' CSV HEADER; ALTER TABLE mls_ratings ALTER COLUMN rating SET DATA TYPE INTEGER USING (rating :: DECIMAL * 2) :: INTEGER; ALTER TABLE mls_ratings ALTER COLUMN timestamp SET DATA TYPE TIMESTAMPTZ USING to_timestamp(timestamp :: DOUBLE PRECISION); 数据大概长这样,第一列用户ID列表,第二列电影ID列表,第三列是评分,最后是时间戳。一共100k条 movielens=# select * from mls_ratings limit 10; user_id | movie_id | rating | timestamp ---------+----------+--------+------------------------ 1 | 31 | 5 | 2009-12-14 10:52:24+08 1 | 1029 | 6 | 2009-12-14 10:52:59+08 1 | 1061 | 6 | 2009-12-14 10:53:02+08 1 | 1129 | 4 | 2009-12-14 10:53:05+08 1 | 1172 | 8 | 2009-12-14 10:53:25+08 1 | 1263 | 4 | 2009-12-14 10:52:31+08 1 | 1287 | 4 | 2009-12-14 10:53:07+08 1 | 1293 | 4 | 2009-12-14 10:52:28+08 1 | 1339 | 7 | 2009-12-14 10:52:05+08 1 | 1343 | 4 | 2009-12-14 10:52:11+08 第二步:计算物品相似度 中间结果表: 计算物品相似度,要计算两个中间数据: 每个物品被用户喜欢的次数:N(i) 每对物品共同被同一个用户喜欢的次数 N(i)∩N(j) 如果是用编程语言,那可以一次性解决两个问题,不过SQL就要稍微麻烦点了,先创建两张中间临时表。 -- 中间表1:每个电影被用户看过的次数:N(i) CREATE TABLE mls_occur ( movie_id INTEGER PRIMARY KEY, n INTEGER ); -- 这个好算的很,按照movie_id聚合一下就知道每个电影被多少用户看过了:47ms INSERT INTO mls_occur SELECT movie_id, count(*) AS n FROM mls_ratings GROUP BY movie_id; -- 中间表2:同时看过电影i和j的人数: N(i)∩N(j) CREATE TABLE mls_common ( i INTEGER, j INTEGER, n INTEGER, PRIMARY KEY (i, j) ); -- 计算物品共现矩阵,这个比较慢。大表自己Join自己比较省事:2m 23s INSERT INTO mls_common SELECT a.movie_id AS i, b.movie_id AS j, count(*) AS n FROM mls_ratings a INNER JOIN mls_ratings b ON a.user_id = b.user_id GROUP BY i, j; 物品相似度表 有了中间结果,就可以应用距离公式,计算最终的物品相似度啦 -- 物品相似度表,这是把矩阵用<i,j,M_ij>的方式在数据库中表示。 CREATE TABLE mls_similarity ( i INTEGER, j INTEGER, p FLOAT, PRIMARY KEY (i, j) ); -- 计算物品相似度矩阵:1m 24s INSERT INTO mls_similarity SELECT i, j, n / sqrt(n1 * n2) AS p FROM mls_common c, LATERAL (SELECT n AS n1 FROM mls_occur WHERE movie_id = i) n1, LATERAL (SELECT n AS n2 FROM mls_occur WHERE movie_id = j) n2; 物品相似度表大概长这样,实际上还可以修剪修剪,比如非常小的相似度干脆可以直接删掉。也可以用整个表中相似度的最大值作为单位1,进行归一化。不过这里都不弄了。 第三步:进行推荐! 现在假设我们为ID为10的用户推荐10部他没看过的电影,该怎么做呢? SELECT j AS movie_id, sum(rating * p) AS score FROM (SELECT movie_id, rating FROM mls_ratings WHERE user_id = 10 ) seed LEFT OUTER JOIN mls_similarity b ON seed.movie_id = b.i WHERE i != j GROUP BY j ORDER BY score DESC LIMIT 100; 首先取用户评过分的电影作为种子集合,Join物品相似度表。按权重算出所有出现物品的预测评分并依此排序取TOP10,就得到推荐结果啦!大概长这样 movie_id | score ----------+------------------ 1270 | 121.487735902517 1196 | 118.399962228869 1198 | 117.518394778743 1036 | 116.841317175111 1240 | 116.432450924524 1214 | 116.146138947698 1580 | 116.015331936539 2797 | 115.144083402858 1265 | 114.959033115913 1291 | 114.934944010088 包装一下,把它变成一个存储过程 CREATE OR REPLACE FUNCTION get_recommendation(userid INTEGER) RETURNS JSONB AS $$ BEGIN RETURN (SELECT jsonb_agg(movie_id) FROM ( SELECT j AS movie_id, sum(rating * p) AS score FROM (SELECT movie_id, rating FROM mls_ratings WHERE user_id = userid) seed LEFT OUTER JOIN mls_similarity b ON seed.movie_id = b.i WHERE i != j GROUP BY j ORDER BY score DESC LIMIT 100) res); END $$ LANGUAGE plpgsql STABLE; SELECT get_recommendation(11); 用起来更方便啦 movielens=# SELECT get_recommendation(11) as res; res ----------------------------------------------------------------------- [80489, 96079, 79132, 59315, 91529, 69122, 58559, 59369, 1682, 71535] 是的,就是这么简单……。 还能再给力点吗 就算这样,还是有些蛋疼,比如说计算相似度矩阵的时候,竟然花了一两分钟,才100k条记录就这样,不太给力呀。而且这么多SQL写起来也烦,有没有更快更省事的方法? 这儿还有个基于PostgreSQL源码魔改的推荐数据库:RecDB,直接用C实现了推荐系统相关的功能扩展,性能杠杠地。同时还包装了SQL语法糖,一行SQL建立推荐系统!再一行SQL开始使用~。 -- 计算推荐所需的信息 CREATE RECOMMENDER MovieRec ON ml_ratings USERS FROM userid ITEMS FROM itemid EVENTS FROM ratingval USING ItemCosCF -- 进行推荐! SELECT * FROM ml_ratings R RECOMMEND R.itemid TO R.userid ON R.ratingval USING ItemCosCF WHERE R.userid = 1 ORDER BY R.ratingval LIMIT 10
Capslock Make CapsLock Great Again! 显著提高APM!! 功能强大:基于⇪CapsLock的新功能键Hyper可以与任何按键组合,提供强大的定制性。 精心设计:在作者两年使用实践中不断雕琢,将最常使用的操作预置于核心键区,显著提高操作效率。 轻量跨平台:分别基于Karabiner() 与AutoHotKey(⊞)以脚本方式实现。 部署方便:放在随身装机U盘中,还是托管在Github上,简单几步即可复刻环境。 CapsLock() : 使用姿势参考 CapsLock(⊞) 前言 ⇪CapsLock,即大写锁定键,其起源可追溯至打字机时代。打字机是纯机械的设备,当按下Shift时,整套设备会与墨条纸带发生位移,使小写字母“上档”为大写字母。此等操作对小拇指是不小的负担,以致连续输入两到三个大写字母都比较吃力。于是在今天键盘上⇪Capslock的位置上出现了其原型Shiftlock功能键:可以在大小写状态之间切换与保持。这样的设计解决了一指禅选手的困境,更减轻了打字员的负担。 然而随着科技的进步,这样的问题已经不存在了。除了一指禅选手,Shift能够毫不费力的满足人们的需求。而⇪Capslock这个'多余'的按键,则因为历史惯性保留在了绝大多数键盘的黄金位置。 历史上有很多人注意到了这一点,不少人都琢磨着让这个占据宝地的按键发挥出更大的作用。一些键盘直接去掉了⇪Capslock键,而另一些则直接把它作为⌃Ctrl使用。在一些Unix Like(e.g )环境中,⇪Capslock还被用作语言切换的按键。有些人将⇪Capslock改为复合功能键:单独键入时是⎋ESC,而按住不放时是⌃Ctrl。 不过为什么要浪费这样一个绝佳的机会呢?我们所能做的远比这更多: 譬如创造一个类似⇧ Shift、⌃ Ctrl、⌥ Option、⊞ Win、⎇ Alt 、 ⌘ Command等修饰键的全新功能键: Hyper 功能说明 以Mac为例: 右手核心区 基本映射 按住⇪CapsLock,映射为 Hyper 按下⇪CapsLock,映射为⌥␢ 输入法切换(二选一,默认) 按下⇪CapsLock,映射为⎋ ESC (二选一) 导航键映射 按下 Hyper启用光标移动,按下⌘时启用光标选择 H ↦ ← (VI导航键:H映射为左) J ↦ ↓ (VI导航键:J映射为左) K ↦ ↑ (VI导航键:K映射为左) L ↦ → (VI导航键:L映射为右) U ↦ ⇞ (U映射为PageUp) I ↦ ↖ (I映射为Home) O ↦ ↘ (O映射为End) P ↦ ⇟ (P映射为Pagedown) 删除键 N ↦ ⌥⌫ (删除光标左侧一个单词) M ↦ ⌫ (删除光标左侧一个字符) , ↦ ⌦ (删除光标右侧一个字符) . ↦ ⌥⌦ (删除光标右侧一个单词) 左手核心区 窗口操作 ⇥ ↦ ⌃⇥ (Tab 映射为正向切换标签页) ⌘⇥↦ ⌃⇧⇥ (⌘Tab 映射为反向切换标签页) Q ↦ ⌘Q (关闭窗口) W ↦ ⌘W (关闭标签页) A ↦ ⌃⌥⇧⌘A (Moom-Meta键,※一个窗口操作软件) ⌘A ↦ F11 (回到桌面) S ↦ ⌃⇥ (切换标签页) ⌘S ↦ ⌃⇧⇥ (反向切换标签页 ) Bash控制 Z ↦ ⌃Z (作业停止信号:SIGTSTP)) X ↦ ⌃B (Tmux-Prefix,※一个终端复用软件) C ↦ ⌃C (作业中断信号:SIGINT) V ↦ , (Vim-Leader键) D ↦ ⌃D (发送EOF) 应用快捷方式 E ↦ Google Chrome ⌘E ↦ Finder R ↦ iTerm ⌘R ↦ ^R (IDE-Run) T ↦ Sublime Text ⌘T ↦ Typora F ↦ Dash ⌘F ↦ Dictionary G ↦ IntelliJ IDEA G ↦ CLion 键盘第一行:功能键 功能键映射 F1 ↦ BrightnessDown F2 ↦ BrightnessUp F3 ↦ ExposeAll F4 ↦ LaunchPad F5 ↦ KeyboardLightDown F6 ↦ KeyboardLightUp F7 ↦ MusicPrev F8 ↦ MusicPlay F9 ↦ MusicNext F10 ↦ Mute F11 ↦ VolumeDown F12 ↦ VolumeUp 键盘第二行:数字键 上档键映射 [12...-=] ↦ ⇧[12...-=] (将数字键、减号、等于号上档为对应符号) [] ↦ ⇧90 '()' (将方括号映射为圆括号) ' ↦ ⇧- '_' (将单引号映射为下划线) 主键区其他按键 主键区其他杂项 ⎋ ↦ ⇪ (Escape 将Hyper转义回⇪CapsLock) ␢ ↦ ⎋ (空格 映射为⎋ESC) \ ↦ ⌃/ (斜杠 映射为注释/解注释) ` ↦ ⌃⇧⌘4(反引号 映射为选取截图区域并存储) ⌘ ↦ ⌃⇧4` (⌘反引号 映射为选取截图区域至剪贴板) ' ↦ = (单引号 映射为等于号) 特殊键区 PC键盘特殊按键映射 Ins ↦ ⇧⌥F1 (Insert 微调提升屏幕亮度) Del ↦ ⇧⌥F2 (Delete 微调降低屏幕亮度) ↘ ↦ ⇧⌥F5 (Home 微调提升键盘背光亮度) ↖ ↦ ⇧⌥F6 (End 微调降低键盘背光亮度) ⇟ ↦ ⇧⌥F11 (PgUp 微调提升音量) ⇞ ↦ ⇧⌥F12 (PgDn 微调降低音量) PrintScreen ↦ ⌃⇧⌘3 (保存全屏截图至剪贴板) PrintScreen ↦ ⌃⇧⌘3 (保存全屏截图至桌面文件) ScrollLock ↦ VolumeMute (ScrollLock静音) Pause ↦ MusicPlay (Pause映射为音乐播放) 光标键映射 ↑ ↦ MouseUp ↓ ↦ MouseDown ← ↦ MouseLeft → ↦ MouseRight ↩ ↦ MouseLButton \ ↦ MouseRButton 符号参考表 Modifiers: Mac Sym Key Hyper ⌃ Control ⌥ Option ⇧ Shift ⌘ Command Modifiers: ⊞Windows Sym Key Hyper ⌃ Control ⊞ Windows ⇧ Shift ⎇ Alter Normal Keys GLYPH NAME Apple ⌘ Command, Cmd, Clover, (formerly) Apple ⌃ Control, Ctl, Ctrl ⌥ Option, Opt, (Windows) Alt ⎇ Alt ⇧ Shift ⇪ Caps lock ⏏ Eject ↩, ↵, ⏎ Return, Carriage Return ⌤ Enter ⌫ Delete, Backspace ⌦ Forward Delete ⎋ Escape, Esc → Right arrow ← Left arrow ↑ Up arrow ↓ Down arrow ⇞ Page Up, PgUp ⇟ Page Down, PgDn ↖ Home ↘ End ⌧ Clear ⇥ Tab, Tab Right, Horizontal Tab ⇤ Shift Tab, Tab Left, Back-tab ␢ Space, Blank ␣ Space, Blank ⃝ Power ⇭ Num lock ?⃝ Help Context menu Mac版本脚本 CapsLock() 更新中 <?xml version="1.0"?> <root> <!-- Introduction --> <item> <name style="important">CapsLock Enhancement</name> <appendix></appendix> <appendix>Make CapsLock Great Again!</appendix> <appendix></appendix> <appendix>※Author: Vonng (fengruohang@outlook.com)</appendix> <appendix>※Document: https://github.com/Vonng/Capslock/tree/master/osx</appendix> <appendix>※Environment: Mac OS X darwin64 via Karabiner</appendix> <appendix>※License: WTFPL</appendix> <appendix>※Prequisite: Maps ⇪CapsLock to F19(KeyCode:80) first. (via Seil)</appendix> </item> <!-- Hyper declaration --> <modifierdef>HYPER</modifierdef> <item> <name style="important">CapsLock setup</name> <appendix>Hold CapsLock to Hyper</appendix> <appendix>⇪[Hold] ↦ </appendix> <appendix></appendix> <appendix>Press CapsLock to ?</appendix> <appendix><![CDATA[* ⎋ ESC ]]></appendix> <appendix><![CDATA[* ⌥␢ Language-Switcher]]></appendix> </item> <!-- CapsLock to Hyper/Language Switcher(Recommend) --> <item> <name>Press CapsLock to Language-Switcher</name> <appendix>⇪[Press] ↦ ⌥␢</appendix> <identifier>private.capslock-langswitcher</identifier> <autogen> --KeyOverlaidModifier-- KeyCode::F19, KeyCode::VK_MODIFIER_HYPER, KeyCode::SPACE, ModifierFlag::CONTROL_L </autogen> </item> <!-- CapsLock to Escape --> <item> <name>Press CapsLock to Escape</name> <appendix>⇪[Press] ↦ ⎋</appendix> <identifier>private.capslock-escape</identifier> <autogen> --KeyOverlaidModifier-- KeyCode::F19, KeyCode::VK_MODIFIER_HYPER, KeyCode::ESCAPE </autogen> </item> <!-- Hyper Navigator --> <item> <name>Hyper Navigator</name> <appendix><![CDATA[H ↦ ← (CursorLeft)]]></appendix> <appendix><![CDATA[J ↦ ↓ (CursorDown)]]></appendix> <appendix><![CDATA[K ↦ ↑ (CursorUp)]]></appendix> <appendix><![CDATA[L ↦ → (CursorRight)]]></appendix> <appendix><![CDATA[U ↦ ⇞ (PageUp)]]></appendix> <appendix><![CDATA[I ↦ ↖ (Home)]]></appendix> <appendix><![CDATA[O ↦ ↘ (End)]]></appendix> <appendix><![CDATA[P ↦ ⇟ (Pagedown)]]></appendix> <appendix></appendix> <appendix>※Hint: Hold additional ⌘(Left) key for selection</appendix> <identifier>private.hyper-navigator</identifier> <!-- H: Left --> <autogen> --KeyToKey-- KeyCode::H, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::CURSOR_LEFT | ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::H, ModifierFlag::HYPER, KeyCode::CURSOR_LEFT </autogen> <!-- J: Down --> <autogen> --KeyToKey-- KeyCode::J, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::CURSOR_DOWN | ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::J,ModifierFlag::HYPER, KeyCode::CURSOR_DOWN </autogen> <!-- K: Up --> <autogen> --KeyToKey-- KeyCode::K, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::CURSOR_UP | ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::K,ModifierFlag::HYPER, KeyCode::CURSOR_UP </autogen> <!-- L: Right --> <autogen> --KeyToKey-- KeyCode::L, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::CURSOR_RIGHT | ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::L,ModifierFlag::HYPER, KeyCode::CURSOR_RIGHT </autogen> <!-- U: PageUp --> <autogen> --KeyToKey-- KeyCode::U, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::PAGEUP | ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::U,ModifierFlag::HYPER, KeyCode::PAGEUP </autogen> <!-- I: HOME --> <autogen> --KeyToKey-- KeyCode::I, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::HOME | ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::I,ModifierFlag::HYPER, KeyCode::HOME </autogen> <!-- O: END --> <autogen> --KeyToKey-- KeyCode::O, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::END | ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::O,ModifierFlag::HYPER, KeyCode::END </autogen> <!-- P: PageDown --> <autogen> --KeyToKey-- KeyCode::P, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::PAGEDOWN | ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::P,ModifierFlag::HYPER, KeyCode::PAGEDOWN </autogen> </item> <!-- Hyper Deletion --> <item> <name>Hyper Deletion</name> <appendix><![CDATA[N ↦ ⌥⌫ (Delete a word ahead)]]></appendix> <appendix><![CDATA[M ↦ ⌫ (Delete a char ahead)]]></appendix> <appendix><![CDATA[, ↦ ⌦ (Forward Delete a char)]]></appendix> <appendix><![CDATA[. ↦ ⌥⌦ (Forward Delete a word)]]></appendix> <identifier>private.hyper-deletion</identifier> <autogen> --KeyToKey-- KeyCode::N, ModifierFlag::HYPER, KeyCode::DELETE, ModifierFlag::OPTION_L, </autogen> <autogen> --KeyToKey-- KeyCode::M,ModifierFlag::HYPER, KeyCode::DELETE </autogen> <autogen> --KeyToKey-- KeyCode::COMMA,ModifierFlag::HYPER, KeyCode::FORWARD_DELETE </autogen> <autogen> --KeyToKey-- KeyCode::DOT,ModifierFlag::HYPER, KeyCode::FORWARD_DELETE, ModifierFlag::OPTION_L </autogen> </item> <!-- Hyper Window Control --> <item> <name>Hyper Window-Manipulation</name> <appendix><![CDATA[Q ↦ ⌘Q (Close Window)]]></appendix> <appendix><![CDATA[W ↦ ⌘W (Close Tab)]]></appendix> <appendix><![CDATA[A ↦ ⌃⌥⇧⌘A (Moom Prefix)]]></appendix> <appendix><![CDATA[⌘A ↦ F11 (Desktop)]]></appendix> <appendix><![CDATA[S ↦ ⌃⇥ (Tab Switch)]]></appendix> <appendix><![CDATA[⌘S ↦ ⌃⇧⇥ (Reverse Tab Switch)]]></appendix> <identifier>private.hyper-window-manipulation</identifier> <!-- Tab to SwitchTab--> <autogen> --KeyToKey-- KeyCode::TAB, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::TAB,ModifierFlag::CONTROL_L | ModifierFlag::SHIFT_L </autogen> <!-- ⌘Tab to Reverse SwitchTab--> <autogen> --KeyToKey-- KeyCode::TAB, ModifierFlag::HYPER, KeyCode::TAB,ModifierFlag::CONTROL_L </autogen> <!-- ⌘Q: Quit Window --> <autogen> --KeyToKey-- KeyCode::Q, ModifierFlag::HYPER, KeyCode::Q, ModifierFlag::COMMAND_L </autogen> <!-- ⌘W Close Tab --> <autogen> --KeyToKey-- KeyCode::W, ModifierFlag::HYPER , KeyCode::W, ModifierFlag::COMMAND_L </autogen> <!-- Hyper-A Moom-Prefix (Set to Physical Hyper-A) --> <autogen> --KeyToKey-- KeyCode::A,ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::F11 </autogen> <autogen> --KeyToKey-- KeyCode::A,ModifierFlag::HYPER , KeyCode::A,ModifierFlag::COMMAND_L | ModifierFlag::OPTION_L | ModifierFlag::CONTROL_L | ModifierFlag::SHIFT_L </autogen> <!-- ^Tab: Switch Tag --> <autogen> --KeyToKey-- KeyCode::S,ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::TAB,ModifierFlag::CONTROL_L | ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::S,ModifierFlag::HYPER, KeyCode::TAB,ModifierFlag::CONTROL_L </autogen> </item> <!-- Hyper Bash Control --> <item> <name>Hyper Bash-Control</name> <appendix><![CDATA[Z ↦ ⌃Z (SIGTSTP)]]></appendix> <appendix><![CDATA[X ↦ ⌃B (Tmux-Prefix)]]></appendix> <appendix><![CDATA[C ↦ ⌃C (SIGINT)]]></appendix> <appendix><![CDATA[V ↦ , (VIM Leader)]]></appendix> <appendix><![CDATA[D ↦ ⌃D (EOF)]]></appendix> <identifier>private.hyper-bash-control</identifier> <!-- ^Z SIGTSTP --> <autogen> --KeyToKey-- KeyCode::Z, ModifierFlag::HYPER , KeyCode::Z, ModifierFlag::CONTROL_L </autogen> <!-- ^X Tmux Prefix --> <autogen> --KeyToKey-- KeyCode::X, ModifierFlag::HYPER , KeyCode::B, ModifierFlag::CONTROL_L </autogen> <!-- ^C SIGINT --> <autogen> --KeyToKey-- KeyCode::C, ModifierFlag::HYPER, KeyCode::C, ModifierFlag::CONTROL_L </autogen> <!-- ^V Vim --> <autogen> --KeyToKey-- KeyCode::V, ModifierFlag::HYPER , KeyCode::COMMA </autogen> <!-- ^D EOF +⌘=^R (Debug) --> <autogen> --KeyToKey-- KeyCode::D, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::D, ModifierFlag::CONTROL_L </autogen> <autogen> --KeyToKey-- KeyCode::D, ModifierFlag::HYPER , KeyCode::D, ModifierFlag::CONTROL_L </autogen> </item> <!--Hyper Application--> <item> <name>Hyper Application</name> <appendix></appendix> <appendix>E: Explore</appendix> <appendix><![CDATA[E ↦ Google Chrome]]></appendix> <appendix><![CDATA[⌘E ↦ Finder]]></appendix> <appendix></appendix> <appendix>R: Run</appendix> <appendix><![CDATA[R ↦ iTerm]]></appendix> <appendix><![CDATA[⌘R ↦ ^R (IDE-Run)]]></appendix> <appendix></appendix> <appendix>T: Text</appendix> <appendix><![CDATA[T ↦ Sublime Text]]></appendix> <appendix><![CDATA[⌘T ↦ Typora]]></appendix> <appendix></appendix> <appendix>F: Find</appendix> <appendix><![CDATA[F ↦ Dash]]></appendix> <appendix><![CDATA[⌘F ↦ Dictionary]]></appendix> <appendix></appendix> <appendix>G: Grand</appendix> <appendix><![CDATA[G ↦ Intellij IDEA]]></appendix> <appendix><![CDATA[G ↦ CLion]]></appendix> <identifier>private.hyper-application</identifier> <!--Define your own application here--> <vkopenurldef> <name>KeyCode::VK_OPEN_URL_APP_Google_Chrome</name> <url type="file">/Applications/Google Chrome.app</url> </vkopenurldef> <vkopenurldef> <name>KeyCode::VK_OPEN_URL_APP_iTerm</name> <url type="file">/Applications/iTerm.app</url> </vkopenurldef> <vkopenurldef> <name>KeyCode::VK_OPEN_URL_APP_Sublime_Text</name> <url type="file">/Applications/Sublime Text.app</url> </vkopenurldef> <vkopenurldef> <name>KeyCode::VK_OPEN_URL_APP_INTELLIJ_IDEA</name> <url type="file">/Applications/IntelliJ IDEA.app/</url> </vkopenurldef> <vkopenurldef> <name>KeyCode::VK_OPEN_URL_APP_CLion</name> <url type="file">/Applications/CLion.app/</url> </vkopenurldef> <vkopenurldef> <name>KeyCode::VK_OPEN_URL_APP_Dash</name> <url type="file">/Applications/Dash.app/</url> </vkopenurldef> <vkopenurldef> <name>KeyCode::VK_OPEN_URL_APP_Typora</name> <url type="file">/Applications/Typora.app</url> </vkopenurldef> <autogen> --KeyToKey-- KeyCode::E, ModifierFlag::HYPER | ModifierFlag::COMMAND_L , KeyCode::VK_OPEN_URL_APP_Finder </autogen> <autogen> --KeyToKey-- KeyCode::E, ModifierFlag::HYPER, KeyCode::VK_OPEN_URL_APP_Google_Chrome </autogen> <autogen> --KeyToKey-- KeyCode::R, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::R, ModifierFlag::CONTROL_L </autogen> <autogen> --KeyToKey-- KeyCode::R, ModifierFlag::HYPER, KeyCode::VK_OPEN_URL_APP_iTerm </autogen> <autogen> --KeyToKey-- KeyCode::T, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::VK_OPEN_URL_APP_Typora </autogen> <autogen> --KeyToKey-- KeyCode::T, ModifierFlag::HYPER, KeyCode::VK_OPEN_URL_APP_Sublime_Text </autogen> <autogen> --KeyToKey-- KeyCode::F, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::VK_OPEN_URL_APP_Dictionary </autogen> <autogen> --KeyToKey-- KeyCode::F, ModifierFlag::HYPER, KeyCode::VK_OPEN_URL_APP_Dash </autogen> <autogen> --KeyToKey-- KeyCode::G, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::VK_OPEN_URL_APP_CLion </autogen> <autogen> --KeyToKey-- KeyCode::G, ModifierFlag::HYPER, KeyCode::VK_OPEN_URL_APP_INTELLIJ_IDEA </autogen> </item> <!-- Hyper Functional--> <item> <name>Hyper Functional</name> <appendix>Maps F[N] to corresponding functionality</appendix> <appendix><![CDATA[F1 ↦ BrightnessDown]]></appendix> <appendix><![CDATA[F2 ↦ BrightnessUp]]></appendix> <appendix><![CDATA[F3 ↦ ExposeAll]]></appendix> <appendix><![CDATA[F4 ↦ LaunchPad]]></appendix> <appendix><![CDATA[F5 ↦ KeyboardLightDown]]></appendix> <appendix><![CDATA[F6 ↦ KeyboardLightUp]]></appendix> <appendix><![CDATA[F7 ↦ MusicPrev]]></appendix> <appendix><![CDATA[F8 ↦ MusicPlay]]></appendix> <appendix><![CDATA[F9 ↦ MusicNext]]></appendix> <appendix><![CDATA[F10 ↦ Mute]]></appendix> <appendix><![CDATA[F11 ↦ VolumeDown]]></appendix> <appendix><![CDATA[F12 ↦ VolumeUp]]></appendix> <appendix></appendix> <appendix>※Hint: Enable system settings: preference -> keyboard -> 'Use F1,F2,etc.. as standard function key' </appendix> <identifier>private.hyper-function</identifier> <!-- F1 --> <autogen> __KeyToKey__ KeyCode::F1, ModifierFlag::HYPER, ConsumerKeyCode::BRIGHTNESS_DOWN ,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> <!-- F2 --> <autogen> __KeyToKey__ KeyCode::F2, ModifierFlag::HYPER, ConsumerKeyCode::BRIGHTNESS_UP ,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> <!-- F3 --> <autogen>__KeyToKey__ KeyCode::F3, ModifierFlag::HYPER, KeyCode::EXPOSE_ALL</autogen> <!-- F4 --> <autogen>__KeyToKey__ KeyCode::F4, ModifierFlag::HYPER, KeyCode::LAUNCHPAD</autogen> <!-- F5 --> <autogen> __KeyToKey__ KeyCode::F5, ModifierFlag::HYPER, ConsumerKeyCode::KEYBOARDLIGHT_LOW ,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> <!-- F6 --> <autogen> __KeyToKey__ KeyCode::F6, ModifierFlag::HYPER, ConsumerKeyCode::KEYBOARDLIGHT_HIGH ,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> <!-- F7 --> <autogen> __KeyToKey__ KeyCode::F7, ModifierFlag::HYPER, ConsumerKeyCode::MUSIC_PREV </autogen> <!-- F8 --> <autogen> __KeyToKey__ KeyCode::F8, ModifierFlag::HYPER, ConsumerKeyCode::MUSIC_PLAY </autogen> <!-- F9 --> <autogen> __KeyToKey__ KeyCode::F9, ModifierFlag::HYPER, ConsumerKeyCode::MUSIC_NEXT </autogen> <!-- F10 --> <autogen> __KeyToKey__ KeyCode::F10, ModifierFlag::HYPER, ConsumerKeyCode::VOLUME_MUTE </autogen> <!-- F11 --> <autogen> __KeyToKey__ KeyCode::F11, ModifierFlag::HYPER, ConsumerKeyCode::VOLUME_DOWN,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> <!-- F12 --> <autogen> __KeyToKey__ KeyCode::F12, ModifierFlag::HYPER, ConsumerKeyCode::VOLUME_UP,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> </item> <!-- Hyper Shifter--> <item> <name>Hyper Shifter</name> <appendix>Maps hyper numkeys, minus, equal to corresponding punctuation.</appendix> <appendix>[12...-=] ↦ ⇧[12...-=] '!@..._+'</appendix> <appendix></appendix> <appendix>Maps square bracket to round breacket.</appendix> <appendix>[ ↦ ⇧9 '('</appendix> <appendix>] ↦ ⇧0 ')'</appendix> <appendix></appendix> <appendix>Maps Hyper semicolon to low dash</appendix> <appendix>; ↦ ⇧- '_'</appendix> <identifier>private.hyper-shifter</identifier> <autogen> --KeyToKey-- KeyCode::1, ModifierFlag::HYPER, KeyCode::1, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::2, ModifierFlag::HYPER, KeyCode::2, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::3, ModifierFlag::HYPER, KeyCode::3, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::4, ModifierFlag::HYPER, KeyCode::4, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::5, ModifierFlag::HYPER, KeyCode::5, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::6, ModifierFlag::HYPER, KeyCode::6, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::7, ModifierFlag::HYPER, KeyCode::7, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::8, ModifierFlag::HYPER, KeyCode::8, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::9, ModifierFlag::HYPER, KeyCode::9, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::0, ModifierFlag::HYPER, KeyCode::0, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::MINUS, ModifierFlag::HYPER, KeyCode::MINUS, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::EQUAL, ModifierFlag::HYPER, KeyCode::EQUAL, ModifierFlag::SHIFT_L </autogen> <autogen> --KeyToKey-- KeyCode::BRACKET_LEFT, ModifierFlag::HYPER, KeyCode::9 , ModifierFlag::SHIFT_R, </autogen> <autogen> --KeyToKey-- KeyCode::BRACKET_RIGHT, ModifierFlag::HYPER, KeyCode::0 , ModifierFlag::SHIFT_R, </autogen> <!-- SemiColon to Minus --> <autogen> --KeyToKey-- KeyCode::SEMICOLON, ModifierFlag::HYPER, KeyCode::MINUS , ModifierFlag::SHIFT_R, </autogen> </item> <!-- Hyper Other --> <item> <name>Hyper Other</name> <appendix>Escape Hyper back to CapsLock</appendix> <appendix><![CDATA[⎋ ↦ ⇪ (CapsLock)]]></appendix> <appendix></appendix> <appendix>Hyper Space to Escape</appendix> <appendix><![CDATA[␢ ↦ ⎋ (Escape)]]></appendix> <appendix></appendix> <appendix><![CDATA[\ ↦ ⌃/ (Comment/Uncomment)]]></appendix> <appendix><![CDATA[` ↦ ⌃⇧⌘4 (ScreenShot by area)]]></appendix> <appendix><![CDATA[⌘` ↦ ⌃⇧4 (ScreenShot by area to file)]]></appendix> <appendix><![CDATA[' ↦ = (Quote to equal)]]></appendix> <identifier>private.hyper-other</identifier> <!-- Escape to Capslock--> <autogen>__KeyToKey__ KeyCode::ESCAPE, ModifierFlag::HYPER, KeyCode::CAPSLOCK</autogen> <!-- Space to Escape--> <autogen> --KeyToKey-- KeyCode::SPACE, ModifierFlag::HYPER, KeyCode::ESCAPE </autogen> <!-- Slash to comment --> <autogen> --KeyToKey-- KeyCode::SLASH, ModifierFlag::HYPER, KeyCode::SLASH, ModifierFlag::COMMAND_L </autogen> <!-- ` to area screenshot --> <autogen> --KeyToKey-- KeyCode::BACKQUOTE, ModifierFlag::HYPER, KeyCode::4, ModifierFlag::CONTROL_L | ModifierFlag::SHIFT_L | ModifierFlag::COMMAND_L </autogen> <!-- ⌘` to area screenshot to file --> <autogen> --KeyToKey-- KeyCode::BACKQUOTE, ModifierFlag::HYPER | ModifierFlag::COMMAND_L, KeyCode::4, ModifierFlag::SHIFT_L | ModifierFlag::COMMAND_L </autogen> <!-- Quote to Equal --> <autogen> --KeyToKey-- KeyCode::QUOTE, ModifierFlag::HYPER, KeyCode::EQUAL </autogen> </item> <!-- Hyper Special Control--> <item> <name>Hyper Special to fine grained functional keys</name> <appendix><![CDATA[Ins ↦ ⇧⌥F1 (Fine grained brightness down)]]></appendix> <appendix><![CDATA[Del ↦ ⇧⌥F2 (Fine grained brightness up)]]></appendix> <appendix><![CDATA[↘ ↦ ⇧⌥F5 (Fine grained keyboard light down)]]></appendix> <appendix><![CDATA[↖ ↦ ⇧⌥F6 (Fine grained keyboard light up)]]></appendix> <appendix><![CDATA[⇟ ↦ ⇧⌥F11 (Fine grained volume down)]]></appendix> <appendix><![CDATA[ ↦ ⇧⌥F12 (Fine grained volume up)]]></appendix> <appendix></appendix> <appendix><![CDATA[PrintScreen ↦ ⌃⇧⌘3 (Save full screen to clipboard)]]></appendix> <appendix><![CDATA[PrintScreen ↦ ⌃⇧3 (Save full screen to file)]]></appendix> <appendix><![CDATA[ScrollLock ↦ VolumeMute (Save full screen to clipboard)]]></appendix> <appendix><![CDATA[Pause ↦ MusicPlay (Save full screen to clipboard)]]></appendix> <identifier>private.hyper-special</identifier> <!-- Brightness: Insert/Delete --> <autogen> __KeyToKey__ KeyCode::PC_INSERT, ModifierFlag::HYPER, ConsumerKeyCode::BRIGHTNESS_UP,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> <autogen> __KeyToKey__ KeyCode::PC_DEL, ModifierFlag::HYPER, ConsumerKeyCode::BRIGHTNESS_DOWN,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> <!-- Keyboard Light: Home/End --> <autogen> __KeyToKey__ KeyCode::HOME, ModifierFlag::HYPER, ConsumerKeyCode::KEYBOARDLIGHT_LOW,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> <autogen> __KeyToKey__ KeyCode::END, ModifierFlag::HYPER, ConsumerKeyCode::KEYBOARDLIGHT_HIGH,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> <!-- PgUp --> <autogen> __KeyToKey__ KeyCode::PAGEUP, ModifierFlag::HYPER, ConsumerKeyCode::VOLUME_UP,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> <autogen> __KeyToKey__ KeyCode::PAGEDOWN, ModifierFlag::HYPER, ConsumerKeyCode::VOLUME_DOWN,ModifierFlag::SHIFT_L | ModifierFlag::OPTION_L </autogen> <!-- PrintScreen & Hyper PrintScreen--> <autogen> __KeyToKey__ KeyCode::PC_PRINTSCREEN, KeyCode::3, ModifierFlag::CONTROL_L | ModifierFlag::SHIFT_L | ModifierFlag::COMMAND_L </autogen> <autogen> __KeyToKey__ KeyCode::PC_PRINTSCREEN, ModifierFlag::HYPER, KeyCode::3, ModifierFlag::CONTROL_L | ModifierFlag::SHIFT_L </autogen> <!--Scroll to mute --> <autogen> __KeyToKey__ KeyCode::PC_SCROLLLOCK, ModifierFlag::HYPER, ConsumerKeyCode::VOLUME_MUTE </autogen> <!--Pause to music_pley--> <autogen> __KeyToKey__ KeyCode::PC_PAUSE, ModifierFlag::HYPER, ConsumerKeyCode::MUSIC_PLAY </autogen> </item> <!-- Hyper Mouse key --> <item> <name>Hyper MouseKey</name> <appendix><![CDATA[↑ ↦ MouseUp]]></appendix> <appendix><![CDATA[↓ ↦ MouseDown]]></appendix> <appendix><![CDATA[← ↦ MouseLeft]]></appendix> <appendix><![CDATA[→ ↦ MouseRight]]></appendix> <appendix><![CDATA[↩ ↦ MouseLButton]]></appendix> <appendix><![CDATA[\ ↦ MouseRButton]]></appendix> <identifier>private.hyper-mousekey</identifier> <autogen> --KeyToKey-- KeyCode::CURSOR_UP, ModifierFlag::HYPER, KeyCode::VK_MOUSEKEY_UP </autogen> <autogen> --KeyToKey-- KeyCode::CURSOR_DOWN, ModifierFlag::HYPER, KeyCode::VK_MOUSEKEY_DOWN </autogen> <autogen> --KeyToKey-- KeyCode::CURSOR_LEFT, ModifierFlag::HYPER, KeyCode::VK_MOUSEKEY_LEFT </autogen> <autogen> --KeyToKey-- KeyCode::CURSOR_RIGHT, ModifierFlag::HYPER, KeyCode::VK_MOUSEKEY_RIGHT </autogen> <autogen> --KeyToKey-- KeyCode::RETURN, ModifierFlag::HYPER, KeyCode::VK_MOUSEKEY_BUTTON_LEFT </autogen> <autogen> --KeyToKey-- KeyCode::BACKSLASH, ModifierFlag::HYPER, KeyCode::VK_MOUSEKEY_BUTTON_RIGHT </autogen> </item> </root> Win版本脚本 CapsLock(⊞) 这个是两年前写的,不过功能大体上是差不多的。 ;=====================================================================o ; Feng Ruohang's AHK Script | ; CapsLock Enhancement | ;---------------------------------------------------------------------o ;Description: | ; This Script is wrote by Feng Ruohang via AutoHotKey Script. It | ; Provieds an enhancement towards the "Useless Key" CapsLock, and | ; turns CapsLock into an useful function Key just like Ctrl and Alt | ; by combining CapsLock with almost all other keys in the keyboard. | ; | ;Summary: | ;o----------------------o---------------------------------------------o ;|CapsLock; | {ESC} Especially Convient for vim user | ;|CaspLock + ` | {CapsLock}CapsLock Switcher as a Substituent| ;|CapsLock + hjklwb | Vim-Style Cursor Mover | ;|CaspLock + uiop | Convient Home/End PageUp/PageDn | ;|CaspLock + nm,. | Convient Delete Controller | ;|CapsLock + zxcvay | Windows-Style Editor | ;|CapsLock + Direction | Mouse Move | ;|CapsLock + Enter | Mouse Click | ;|CaspLock + {F1}~{F6} | Media Volume Controller | ;|CapsLock + qs | Windows & Tags Control | ;|CapsLock + ;'[] | Convient Key Mapping | ;|CaspLock + dfert | Frequently Used Programs (Self Defined) | ;|CaspLock + 123456 | Dev-Hotkey for Visual Studio (Self Defined) | ;|CapsLock + 67890-= | Shifter as Shift | ;-----------------------o---------------------------------------------o ;|Use it whatever and wherever you like. Hope it help | ;=====================================================================o ;=====================================================================o ; CapsLock Initializer ;| ;---------------------------------------------------------------------o SetCapsLockState, AlwaysOff ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Switcher: ;| ;---------------------------------o-----------------------------------o ; CapsLock + ` | {CapsLock} ;| ;---------------------------------o-----------------------------------o CapsLock & `:: ;| GetKeyState, CapsLockState, CapsLock, T ;| if CapsLockState = D ;| SetCapsLockState, AlwaysOff ;| else ;| SetCapsLockState, AlwaysOn ;| KeyWait, `` ;| return ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Escaper: ;| ;----------------------------------o----------------------------------o ; CapsLock | {ESC} ;| ;----------------------------------o----------------------------------o CapsLock::Send, {ESC} ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Direction Navigator ;| ;-----------------------------------o---------------------------------o ; CapsLock + h | Left ;| ; CapsLock + j | Down ;| ; CapsLock + k | Up ;| ; CapsLock + l | Right ;| ; Ctrl, Alt Compatible ;| ;-----------------------------------o---------------------------------o CapsLock & h:: ;| if GetKeyState("control") = 0 ;| { ;| if GetKeyState("alt") = 0 ;| Send, {Left} ;| else ;| Send, +{Left} ;| return ;| } ;| else { ;| if GetKeyState("alt") = 0 ;| Send, ^{Left} ;| else ;| Send, +^{Left} ;| return ;| } ;| return ;| ;-----------------------------------o ;| CapsLock & j:: ;| if GetKeyState("control") = 0 ;| { ;| if GetKeyState("alt") = 0 ;| Send, {Down} ;| else ;| Send, +{Down} ;| return ;| } ;| else { ;| if GetKeyState("alt") = 0 ;| Send, ^{Down} ;| else ;| Send, +^{Down} ;| return ;| } ;| return ;| ;-----------------------------------o ;| CapsLock & k:: ;| if GetKeyState("control") = 0 ;| { ;| if GetKeyState("alt") = 0 ;| Send, {Up} ;| else ;| Send, +{Up} ;| return ;| } ;| else { ;| if GetKeyState("alt") = 0 ;| Send, ^{Up} ;| else ;| Send, +^{Up} ;| return ;| } ;| return ;| ;-----------------------------------o ;| CapsLock & l:: ;| if GetKeyState("control") = 0 ;| { ;| if GetKeyState("alt") = 0 ;| Send, {Right} ;| else ;| Send, +{Right} ;| return ;| } ;| else { ;| if GetKeyState("alt") = 0 ;| Send, ^{Right} ;| else ;| Send, +^{Right} ;| return ;| } ;| return ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Home/End Navigator ;| ;-----------------------------------o---------------------------------o ; CapsLock + i | Home ;| ; CapsLock + o | End ;| ; Ctrl, Alt Compatible ;| ;-----------------------------------o---------------------------------o CapsLock & i:: ;| if GetKeyState("control") = 0 ;| { ;| if GetKeyState("alt") = 0 ;| Send, {Home} ;| else ;| Send, +{Home} ;| return ;| } ;| else { ;| if GetKeyState("alt") = 0 ;| Send, ^{Home} ;| else ;| Send, +^{Home} ;| return ;| } ;| return ;| ;-----------------------------------o ;| CapsLock & o:: ;| if GetKeyState("control") = 0 ;| { ;| if GetKeyState("alt") = 0 ;| Send, {End} ;| else ;| Send, +{End} ;| return ;| } ;| else { ;| if GetKeyState("alt") = 0 ;| Send, ^{End} ;| else ;| Send, +^{End} ;| return ;| } ;| return ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Page Navigator ;| ;-----------------------------------o---------------------------------o ; CapsLock + u | PageUp ;| ; CapsLock + p | PageDown ;| ; Ctrl, Alt Compatible ;| ;-----------------------------------o---------------------------------o CapsLock & u:: ;| if GetKeyState("control") = 0 ;| { ;| if GetKeyState("alt") = 0 ;| Send, {PgUp} ;| else ;| Send, +{PgUp} ;| return ;| } ;| else { ;| if GetKeyState("alt") = 0 ;| Send, ^{PgUp} ;| else ;| Send, +^{PgUp} ;| return ;| } ;| return ;| ;-----------------------------------o ;| CapsLock & p:: ;| if GetKeyState("control") = 0 ;| { ;| if GetKeyState("alt") = 0 ;| Send, {PgDn} ;| else ;| Send, +{PgDn} ;| return ;| } ;| else { ;| if GetKeyState("alt") = 0 ;| Send, ^{PgDn} ;| else ;| Send, +^{PgDn} ;| return ;| } ;| return ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Mouse Controller ;| ;-----------------------------------o---------------------------------o ; CapsLock + Up | Mouse Up ;| ; CapsLock + Down | Mouse Down ;| ; CapsLock + Left | Mouse Left ;| ; CapsLock + Right | Mouse Right ;| ; CapsLock + Enter(Push Release) | Mouse Left Push(Release) ;| ;-----------------------------------o---------------------------------o CapsLock & Up:: MouseMove, 0, -10, 0, R ;| CapsLock & Down:: MouseMove, 0, 10, 0, R ;| CapsLock & Left:: MouseMove, -10, 0, 0, R ;| CapsLock & Right:: MouseMove, 10, 0, 0, R ;| ;-----------------------------------o ;| CapsLock & Enter:: ;| SendEvent {Blind}{LButton down} ;| KeyWait Enter ;| SendEvent {Blind}{LButton up} ;| return ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Deletor ;| ;-----------------------------------o---------------------------------o ; CapsLock + n | Ctrl + Delete (Delete a Word) ;| ; CapsLock + m | Delete ;| ; CapsLock + , | BackSpace ;| ; CapsLock + . | Ctrl + BackSpace ;| ;-----------------------------------o---------------------------------o CapsLock & ,:: Send, {Del} ;| CapsLock & .:: Send, ^{Del} ;| CapsLock & m:: Send, {BS} ;| CapsLock & n:: Send, ^{BS} ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Editor ;| ;-----------------------------------o---------------------------------o ; CapsLock + z | Ctrl + z (Cancel) ;| ; CapsLock + x | Ctrl + x (Cut) ;| ; CapsLock + c | Ctrl + c (Copy) ;| ; CapsLock + v | Ctrl + z (Paste) ;| ; CapsLock + a | Ctrl + a (Select All) ;| ; CapsLock + y | Ctrl + z (Yeild) ;| ; CapsLock + w | Ctrl + Right(Move as [vim: w]);| ; CapsLock + b | Ctrl + Left (Move as [vim: b]);| ;-----------------------------------o---------------------------------o CapsLock & z:: Send, ^z ;| CapsLock & x:: Send, ^x ;| CapsLock & c:: Send, ^c ;| CapsLock & v:: Send, ^v ;| CapsLock & a:: Send, ^a ;| CapsLock & y:: Send, ^y ;| CapsLock & w:: Send, ^{Right} ;| CapsLock & b:: Send, ^{Left} ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Media Controller ;| ;-----------------------------------o---------------------------------o ; CapsLock + F1 | Volume_Mute ;| ; CapsLock + F2 | Volume_Down ;| ; CapsLock + F3 | Volume_Up ;| ; CapsLock + F3 | Media_Play_Pause ;| ; CapsLock + F5 | Media_Next ;| ; CapsLock + F6 | Media_Stop ;| ;-----------------------------------o---------------------------------o CapsLock & F1:: Send, {Volume_Mute} ;| CapsLock & F2:: Send, {Volume_Down} ;| CapsLock & F3:: Send, {Volume_Up} ;| CapsLock & F4:: Send, {Media_Play_Pause} ;| CapsLock & F5:: Send, {Media_Next} ;| CapsLock & F6:: Send, {Media_Stop} ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Window Controller ;| ;-----------------------------------o---------------------------------o ; CapsLock + s | Ctrl + Tab (Swith Tag) ;| ; CapsLock + q | Ctrl + W (Close Tag) ;| ; (Disabled) Alt + CapsLock + s | AltTab (Switch Windows) ;| ; Alt + CapsLock + q | Ctrl + Tab (Close Windows) ;| ; CapsLock + g | AppsKey (Menu Key) ;| ;-----------------------------------o---------------------------------o CapsLock & s::Send, ^{Tab} ;| ;-----------------------------------o ;| CapsLock & q:: ;| if GetKeyState("alt") = 0 ;| { ;| Send, ^w ;| } ;| else { ;| Send, !{F4} ;| return ;| } ;| return ;| ;-----------------------------------o ;| CapsLock & g:: Send, {AppsKey} ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Self Defined Area ;| ;-----------------------------------o---------------------------------o ; CapsLock + d | Alt + d(Dictionary) ;| ; CapsLock + f | Alt + f(Search via Everything);| ; CapsLock + e | Open Search Engine ;| ; CapsLock + r | Open Shell ;| ; CapsLock + t | Open Text Editor ;| ;-----------------------------------o---------------------------------o CapsLock & d:: Send, !d ;| CapsLock & f:: Send, !f ;| CapsLock & e:: Run http://cn.bing.com/ ;| CapsLock & r:: Run Powershell ;| CapsLock & t:: Run C:\Program Files (x86)\Notepad++\notepad++.exe ;| ;---------------------------------------------------------------------o ;=====================================================================o ; CapsLock Char Mapping ;| ;-----------------------------------o---------------------------------o ; CapsLock + ; | Enter (Cancel) ;| ; CapsLock + ' | = ;| ; CapsLock + [ | Back (Visual Studio) ;| ; CapsLock + ] | Goto Define (Visual Studio) ;| ; CapsLock + / | Comment (Visual Studio) ;| ; CapsLock + \ | Uncomment (Visual Studio) ;| ; CapsLock + 1 | Build and Run(Visual Studio) ;| ; CapsLock + 2 | Debuging (Visual Studio) ;| ; CapsLock + 3 | Step Over (Visual Studio) ;| ; CapsLock + 4 | Step In (Visual Studio) ;| ; CapsLock + 5 | Stop Debuging(Visual Studio) ;| ; CapsLock + 6 | Shift + 6 ^ ;| ; CapsLock + 7 | Shift + 7 & ;| ; CapsLock + 8 | Shift + 8 * ;| ; CapsLock + 9 | Shift + 9 ( ;| ; CapsLock + 0 | Shift + 0 ) ;| ;-----------------------------------o---------------------------------o CapsLock & `;:: Send, {Enter} ;| CapsLock & ':: Send, = ;| CapsLock & [:: Send, ^- ;| CapsLock & ]:: Send, {F12} ;| ;-----------------------------------o ;| CapsLock & /:: ;| Send, ^e ;| Send, c ;| return ;| ;-----------------------------------o ;| CapsLock & \:: ;| Send, ^e ;| Send, u ;| return ;| ;-----------------------------------o ;| CapsLock & 1:: Send,^{F5} ;| CapsLock & 2:: Send,{F5} ;| CapsLock & 3:: Send,{F10} ;| CapsLock & 4:: Send,{F11} ;| CapsLock & 5:: Send,+{F5} ;| ;-----------------------------------o ;| CapsLock & 6:: Send,+6 ;| CapsLock & 7:: Send,+7 ;| CapsLock & 8:: Send,+8 ;| CapsLock & 9:: Send,+9 ;| CapsLock & 0:: Send,+0 ;| ;---------------------------------------------------------------------o Contact GitHub API Training Shop Blog About FAQ Q: Why using as symbol of hyper key? A:Cause asterisk have the ascii code 42, which is the answer to life, the universe, and everything! while itself has meaning 'star'. (Heavy-Asterisk) is a pretty version of *(Asterisk). Actually I would choose
是的,没有打错,标题中是/0而不是0。那么问题就来了:除以0会发生什么? 限定条件是必须的:在CS领域,*nix | win操作系统下任意编程语言中,整数除法运算中除数为零的情况。 答案并不是固定的,在不同的操作系统,不同的编程语言,甚至不同的编译器下,答案都可能是不同的。 除0异常 譬如, 在OS X下,使用C语言,Clang编译,引发除零并不会报错,会返回一个垃圾值。 $ echo 'void main(){printf("%d",1/0);}' > a.c && gcc a.c 2> /dev/null && ./a.out 1512003000 同样的代码在Linux下,使用C语言,GCC编译,就会引发Float point exception。 $ echo 'void main(){printf("%d",1/0);}' > a.c && gcc a.c 2> /dev/null && ./a.out Floating point exception C++在两种环境中与C表现是一致的。至于Windows,手头没有Windows机器且VS只支持C++,但没记错的话/Od下Windows是会通过SEH抛Exception的,而/O2则会返回垃圾值。But who cares windows here…. 相比之下Python与Java在不同的系统中表现是一致的: $ python -c 'print(1/0)' Traceback (most recent call last): File "<string>", line 1, in <module> ZeroDivisionError: integer division or modulo by zero $ echo "class DZ{public static void main(String[] args){System.out.println(2/0);}}" > DZ.java && javac DZ.java && java DZ Exception in thread "main" java.lang.ArithmeticException: / by zero at DZ.main(DZ.java:1) Js这种只有浮点数的奇葩‘巧妙’地用Inf绕开了这个问题,就不讨论了。备注:浮点数除数为0是合法的。 硬件级异常 那么在除0的时候,究竟发生了什么呢?查阅Intel芯片手册可以发现,在x86机器上DIV或IDIV指令除数为0时,会引发0号中断,编号#DE(Divide Error),即所谓除零异常。 如果做过王爽《汇编语言》里面的小实验:编写零号中断处理程序,就能知道在硬件机器码与汇编编程的洪荒年代里,异常是怎样处理的:程序员需要自己写一段代码,作为硬件中断的处理程序。 当然,在没有操作系统的环境里,所谓“异常”,其实就是硬件级异常,翻来覆去也就哪几种:除零、溢出、越界、非法指令等等。异常的种类虽然不多,但想找出异常的原因,或者编写合适的处理函数确实是相当让人抓狂的工作。 许多我们耳熟能详的概念,例如进程与文件,都是伴随操作系统的发明而引入的。 在拥有文件概念的现代操作系统中,数据被存放在文件里,有独立的从零开始的寻址空间,程序员只需要通过文件路径就能拿到这坨数据;如果文件不存在,可以通过open的返回值-1和全局的errno来判断究竟是什么原因导致了错误。想一想这是多么幸福的事啊!在洪荒年代,整个计算机就那么一两个寻址空间,对应着内存或者硬盘,数据就放在固定的偏移量,没什么所谓的文件(其实在固定偏移量维护一点元数据,这就是所谓的文件系统了)。如果读取不出有意义的数据那就只能报错挂掉呗,根本没有所谓的“FileNotExistException”。 除了文件,进程也是一样。在没有操作系统的世界里,连栈的概念都不存在。控制流的玩弄可以称得上随心所欲,只要不越界,不跳到非代码段,整个世界真是天高任鸟飞,随你怎么跳。 在洪荒年代里,异常处理就是处理硬件异常。硬件异常的种类只手可数,不除零,不越界,不干蠢事,几乎可以说是百无禁忌。当然这并不一定是好事,人们往往声称向往自由;但在真正的自由面前,很少的人才能把握方向,其他人只能在无穷的选择面前感到焦虑迷茫。 程序员们呼唤着新秩序的到来,于是就有了操作系统。 操作系统级异常 时代在发展,C语言和操作系统出现了,程序员们从洪荒年代进入了远古时代。终于告别了直接和硬件异常打交道的苦日子。但从C语言的错误处理方式中,我们还是能看到那个时代的缩影。 操作系统引入诸多新颖抽象,随之而来的则是各种新颖的异常:文件打开失败,进程fork失败。这些异常,不同于硬件级异常,属于操作系统的异常。POSIX标准中很多系统调用使用返回-1的方式告知调用者出现异常,通过设置全局errno的方式传递异常的具体原因。于是我们经常能看见这样的代码: if (somecall() == -1) { printf("somecall() failed\n"); if (errno == ...) { ... } } 但还有一个问题:原来的硬件异常怎么办? 譬如喜闻乐见的野指针越界:Segmentation fault: $ echo 'void main(){int* p;printf("%d",*p);}' > a.c && gcc a.c 2> /dev/null && ./a.out Segmentation fault 虽然printf并不是系统调用,只是一个库函数。即便如此,发生硬件异常时,库函数并没有如同发生普通的操作系统异常一样返回-1 ,而是直接CoreDump给程序员一个Surprise~,Tada~。 因为这种异常并不是操作系统产生的,操作系统面对硬件异常也要挠头。怎么办?显然,让程序员自己编写0号中断处理程序是不现实的,操作系统能做的就是把接受这个硬件中断包装成一个操作系统的中断,即“信号”的概念,然后发送给进程。这几个异常信号进程要是不处理,默认的行为就是挂掉。 但是到了操作系统的时代,编写除零、越界信号的处理程序往往是没有太大意义的……,因为程序员在此类异常发生后往往无能为力。不然怎么着,越界读写是准备重试?还是不读了跳过?除零错误是准备加个小的抖动偏移量除出一个天文数字?还是准备拿着垃圾值凑数?如果有这个闲工夫写这种Handler,为啥不在错误语句事前加上条件判断呢……。程序能做到的最好程度,无非是handle SIG之后打好日志,保留现场然后老老实实的挂掉……。 所以,在操作系统级(C,C++),我们还是可以清晰地看到硬件异常与操作系统异常处理方式的差异,前者通过信号(Linux),后者通过返回值和错误码。 在Linux下C语言处理硬件异常的方式: #include <signal.h> #include <stdio.h> void handler(int a) { printf("SIGNAL: %d",a); } int main() { signal(SIGFPE, handler); int a = 1/0; } $ gcc a.c 2> /dev/null && ./a.out SIGNAL: 8 高级语言中的异常 C和C++是所谓的“中级”语言,由于标准库的功能非常有限,在不同操作系统中,程序员还是需要与不少Ad-Hoc的细节打交道。Java的出现可以说解决了(Well, at least part of)这一问题。我们可以看到Java中整数除0发生的是java.lang.ArithmeticException,看上去和其他异常并没有什么不同。只是所属的uncheked RuntimeException好像又隐隐地告诉着我们这个异常和其他异常有点不太一样。 虽然说JVM提供了中间字节码的解释器,但最终JVM还是使用C或汇编将字节码映射为系统调用与机器指令。那么操作系统异常与硬件异常仍然是不可避免的。但是JVM会帮程序员打理好这一切:当发生硬件级异常,比如除零错误时,Java捕获SIGFPE,SIGSEGV等异常信号(Linux下),并将其转化为语言内部的异常抛出;相比之下,诸如文件没找着这种系统调用失效,也都会被Java包装相应的异常;在Java的语言概念中,至少在处理方式上并没有对这些异常(硬件异常,操作系统异常,应用逻辑异常)进行区分,程序员想捕获都能用同一种方式来捕捉处理。 世界大同了吗?Java这一类高级语言虽然在形式上消弭了硬件异常、操作系统异常、应用异常的区分,但从语义设计、编程规范、工程实践的方式,却制定了另一种分类方式: 另一种异常划分的方式 先来看一下Java异常与错误的继承关系。这个继承树中有三大类叶子节点: Error,RuntimeException,Blahblah...Exception。 BlahblahException就是程序或者库定义的普通异常,需要显式在代码中处理。 Error是JVM运行时产生的致命错误,不允许去处理。不过实际上去catch throwable也是可以的……。 RuntimeException,又称为unchecked Exception。是不推荐程序员去捕获的异常。 事实上我们可以还原出这样对异常分类的设计初衷,如下表所示: 原因能否处理 程序员能处理的(checked) 程序员处理不了的(unchecked) 设计缺陷 假命题 RuntimeException 操作失效 普通Exception,需要显式处理 Error 老朋友除零异常换了身马甲:java.lang.ArithmeticException藏在了RuntimeException中。 程序员能处理的设计缺陷,本身就是一个矛盾的陈述。 程序员能处理的操作失效,就是Java中普通的异常。这类异常设计的初衷就是提供一种Fancy的控制流,程序员在调用链条中玩起抛绣球游戏,让错误处理变得方便一些。 程序员处理不了的设计缺陷,属于所谓的RuntimeException。这一点需要解释一下:大家都知道防止NPE是程序员的基本修养。除非文档显式指明,拿到参数或者返回值,首先要做的就是检查是否为空。同理,程序员也有义务在逻辑上保证除法的除数不为0。如果程序员没有这么做,那么这就是一个设计缺陷。任何硬件异常,或者可能导致硬件异常的条件(譬如:除0,数组越界、野指针、栈溢出),都应当在运行时抛出RuntimeException。 程序员处理不了的操作失效:另一方面,JVM本身也是一个程序。人固有一死,程序固有一挂。无论是因为JVM自己的BUG也好,还是环境条件不符合预期,当JVM陷入严重错误时,程序员对此是毫无办法的(自己去改Jvm不算!),这类异常是所谓的程序员处理不了的操作失效,即Error。 对于程序员处理不了的异常,Java处理为unchecked Exception,也就是无需在函数签名后显式列出此类异常。这很好理解,如果这类异常需要指明,那每个使用到指针和除法的地方都可能会抛异常,也就是说几乎每个函数都要在签名后面加上throws RuntimeException,蛋疼无比。所以uncheck是RuntimeException所必须的性质。 这就引出另一个问题了,Error也是非检查的异常。Error就是一种特殊的RuntimeException,它只是运行时异常的一个细分子类。其实在程序员看来,只有两种异常:我能处理的,我不能处理的。无论是JVM挂了还是程序员设计缺陷,这些异常都不是程序员能处理的,也不是程序员该处理的。进行细分实在没有必要,无故将事情复杂化。这一点上我觉得Java设计还是蛮恶心的。另外Java的RuntimeException真的是个垃圾筐,什么垃圾异常都往里装。比较合理的设计应当参考C# Runtime Exception 。运行时只会抛出几种异常,都可以与硬件异常对应上,其他的异常都是普通异常。 总结 从程序员的视角,异常分为两种:能处理的应用异常,处理不了的运行时异常 应用异常是程序员或库作者所使用的错误处理方式。这种异常设计就是为了被捕获处理。 运行时异常属于系统异常,产生原因应当包括两个:应用设计缺陷导致的硬件异常。环境条件导致的JVM或者CRT严重操作失效。不管怎样,这种异常设计就是为了让程序赶紧当掉避免造成更大损失的。 从异常的原因来说,异常分为:设计缺陷与操作失效 设计缺陷是因为程序员或者库作者的考虑不周导致的,应当立即挂掉暴露出错误来。 操作失效是因为环境条件不满足导致的异常,不太严重的操作失效是可以抢救一下的,例如IO Timeout可以等一段时间重试几次,不行再挂掉,或者可选步骤出错可以直接跳过。严重的操作失效,比如JVM自己尿了,那就没办法了,早死早超生吧。 最后,回到最初的问题 除零会发生什么呢? 在Intel x86_64 Linux下: CPU执行div指令,遇到操作数为0,产生0号中断(#DE) Linux内核捕获0号中断,给相应进程产生一个SIGFPE (8) 进程接受到信号 不处理:产生CoreDump 程序自行处理:例如C中注册SIGFPE信号的handler,实现异常捕获。 运行时压制:比如一些C运行时就偷偷忽略或者压制了这个异常,提着垃圾高高兴兴回家了。(C++标准中,整数除以0是未定义行为,读者可以自行实验。) 运行时包装并抛出:Java和Python的运行时接受到信号后,转换为相应的语言内异常抛出。runtimeException一般不捕获,所以一般来说程序就挂了。
结论:Mac: airport, tcpdumpWindows: OmnipeekLinux: tcpdump, airmon-ng 以太网里抓包很简单,各种软件一大把,什么Wireshark,Ethereal,Sniffer Pro 一抓一大把。不过如果是无线数据包,就要稍微麻烦一点了。网上找了一堆罗里吧嗦的文章,绕来绕去的,其实抓无线包一条命令就好了。 Windows下因为无线网卡驱动会拒绝进入混杂模式,所以比较蛋疼,一般是用Omnipeek去弄,不细说了。 Linux和Mac就很方便了。只要用tcpdump就可以,一般系统都自带了。最后-i选项的参数填想抓的网络设备名就行。Mac默认的WiFi网卡是en0。tcpdump -Ine -i en0 主要就是指定-I参数,进入监控模式。-I :Put the interface in "monitor mode"; this is supported only on IEEE 802.11 Wi-Fi interfaces, and supported only on some operating systems.进入监控模式之后计算机用于监控的无线网卡就上不了网了,所以可以考虑买个外置无线网卡来抓包,上网抓包两不误。 抓了包能干很多坏事,比如WEP网络抓几个IV包就可以用aircrack破密码,WPA网络抓到一个握手包就能跑字典破无线密码了。如果在同一个网络内,还可以看到各种未加密的流量……什么小黄图啊,隐私照啊之类的……。 假如我已经知道某个手机的MAC地址,那么只要tcpdump -Ine -i en0 | grep $MAC_ADDRESS 就过滤出该手机相关的WiFi流量。 具体帧的类型详情参看802.11协议,《802.11无线网络权威指南》等。 顺便解释以下混杂模式与监控模式的区别:混杂(promiscuous)模式是指:接收同一个网络中的所有数据包,无论是不是发给自己的。监控(monitor)模式是指:接收某个物理信道中所有传输着的数据包。 RFMONRFMON is short for radio frequency monitoring mode and is sometimes also described as monitor mode or raw monitoring mode. In this mode an 802.11 wireless card is in listening mode (“sniffer” mode). The wireless card does not have to associate to an access point or ad-hoc network but can passively listen to all traffic on the channel it is monitoring. Also, the wireless card does not require the frames to pass CRC checks and forwards all frames (corrupted or not with 802.11 headers) to upper level protocols for processing. This can come in handy when troubleshooting protocol issues and bad hardware. RFMON/Monitor Mode vs. Promiscuous ModePromiscuous mode in wired and wireless networks instructs a wired or wireless card to process any traffic regardless of the destination mac address. In wireless networks promiscuous mode requires that the wireless card be associated to an access point or ad-hoc network. While in promiscuous mode a wireless card can transmit and receive but will only captures traffic for the network (SSID) to which it is associated. RFMON mode is only possible for wireless cards and does not require the wireless card to be associated to a wireless network. While in monitor mode the wireless card can passively monitor traffic of all networks and devices within listening range (SSIDs, stations, access points). In most cases the wireless card is not able to transmit and does not follow the typical 802.11 protocol when receiving traffic (i.e. transmit an 802.11 ACK for received packet). Both modes have to be supported by the driver of the wired or wireless card. 另外在研究抓包工具时,发现了Mac下有一个很好用的命令行工具airport,可以用来抓包,以及摆弄Macbook的WiFi。位置在/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport 可以创建一个符号链接方便使用:sudo ln -s /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport /usr/sbin/airport 常用的命令有:显示当前网络信息:airport -I扫描周围无线网络:airport -s断开当前无线网络:airport -z强制指定无线信道:airport -c=$CHANNEL 抓无线包,可以指定信道:airport en0 sniff [$CHANNEL]抓到的包放在/tmp/airportSniffXXXXX.cap,可以用tcpdump, tshark, wireshark等软件来读。 最实用的功能还是扫描周围无线网络。
测试用API基准性能测试 API编写 为了测试API Gateway的性能,我们首先要有一个测试用API。 用Go语言,采用基于fasthttp的iris框架,编写一个简单的HelloWorld Server。如下所示: package main import "github.com/kataras/iris" func main() { api := iris.New() api.Get("/hi", hi) api.Listen(":8080") } func hi(ctx *iris.Context){ ctx.Write("Hi %s", "iris") } 测试API是否可用 $ curl http://127.0.0.1:8080/hi Hi iris Bench测试用API 并发 RPS TPR 1 21391.66 0.047 2 29864.03 0.067 3 63258.51 0.047 4 73325.61 0.055 5 80241.88 0.062 8 99025.29 0.081 10 105148.38 0.095 16 99378.09 0.161 20 99064.04 0.202 25 96601.46 0.259 30 94350.84 0.318 32 97307.03 0.329 40 93983.53 0.426 50 95480.85 0.524 64 94770.72 0.675 80 90437.65 0.885 100 93304.23 1.072 120 91837.84 1.307 128 91585.78 1.398 150 92307.99 1.625 180 92827.5 1.939 200 93157.63 2.147 500 93920.95 5.324 1000 90560.25 11.042 2000 73470.44 27.222 5000 72345.1 69.113 10000 70525.77 141.792 结论是,在合理的并发数下,QPS基本可以稳定在9W左右。最佳并发数为10。 对于测试,该API不可能成为瓶颈。 单实例API网关 注册为OpenAPI curl -X POST http://localhost:8888/apis/ \ -d "name=overseas_index" \ -d "upstream_url=http://10.182.20.100:8080/" \ -d "request_path=/test" \ -d "strip_request_path=true" {"upstream_url":"http:\/\/10.182.20.100:8080\/","request_path":"\/test","id":"36cc77c7-89cd-49d3-b153-e79c632ccc44","created_at":1471509004000,"preserve_host":false,"strip_request_path":true,"name":"overseas_index"} 测试OpenAPI连通情况 $ curl http://127.0.0.1/test/hi Hi iris 从同机房通过API Gateway进行测试: c n rps tpr 50 100000 13780.78 3.628 100 100000 16499.59 6.061 200 100000 15778.13 12.676 500 100000 14109.51 35.437 使用OpenAPI,添加key-auth权限认证,ACL,从外网接口访问: c n rps tpr 50 100000 14006.29 3.570 100 100000 16087.16 6.216 200 100000 16033.10 12.474 500 100000 14080.34 35.511 (3 Nginx Node, 3x24 EchoServer) 在没有进行任何优化的条件下,峰值约16000 QPS。应对目前Cplus需求非常充裕。但对于DMP还需要进一步优化。优化空间很大。
假如你在生产环境有一个内网可访问的端口,Let’s say: 80,而且有生产机器的应用管理员权限。在这样的情况下,其实是可以做到从内网直接连接到线上环境任意端口的。链接SSH,链接数据库都不在话下。当然,安全性和便利性永远是不可调和的一对矛盾。为了避免有人用它来干坏事,我们也至少应当对这种方式有所了解。 x00 环境描述 假设有生产环境机器10.x.x.1。可在内网通过VIP:123.123.123.1访问80端口。 坏蛋拥有线上环境sudo权限,现在希望在生产环境中直接访问任意端口。 x01 访问SSH root权限或者sudo执行以下命令…… sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 22 此命令将创立从80端口到22端口的路由转发。 # 在任意一台办公网的机器 ssh xxx@123.123.123.1 -p 80 x02 访问数据库 # 清除路由规则 sudo iptables -t nat -F # 将80端口重定向至5432:PostgreSQL sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 5432 # 将80端口重定向至6379:Redis sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 6379 x03 结语 临时的远程调试,IDE开发需求可以短时间内使用这种方法。 但一定要记住,请一定只转发至有应用权限验证的端口。 用完之后必须及时恢复,不然安全会来找你喝茶的。 另外实验请限于内网端口,不要作死在对公网开放的端口上使用。
JSON(JavaScript Object Notation)(RFC 4627)是目前Web数据交换的事实标准。本文记叙JSON的格式,以及Javascript与Python的JSON库使用实践。 语法 JSON语法可以表述以下三种类型的值: 简单值 使用JavaScript相同的语法,可以表示字符串、数值、布尔值、null、(没有undefined)。例如5,"Hello World",null,都属于简单值。 对象 复杂数据类型,一组无序的键值对,每个键值对的键必须是双引号括起的字符串,值可以是简单值也可以是复杂数据类型。例如 { "name": "haha" "age": 74 } 数组 复杂数据类型,表示有序的值的列表,可通过数值索引访问,数组的值可以是简单值也可以是复杂数据类型,且同一数组内可以同时出现任意类型的值。例如 ["a", 2, {"b":3},[4, 5, 6] ] JSON对象中的键应当是独一无二的,但在JSON表示中没有强制这一点,通常的做法都是取最后一次出现的value作为重复key的值。 编码 RFC规定了JSON必须使用UTF-8,UTF-16,UTF-32中的编码来表示。一般使用UTF-8编码。在JSON的字符串中最好不要使用unicode字面值,使用\uxxxx的形式表示能有效降低乱码的可能性。 RFC禁止JSON的字符表示前出现BOM标记。 RFC没有明确禁止JSON的string中出现非法Unicode字节序列,比如“unpaired UTF-16 surrogates”。 解析与序列化(Javascript) 早期的JSON解析器基本上就是使用eval()函数进行的。 但这样做存在安全隐患,所以在浏览器或者Node中定义有全局模块JSON。 序列化: JSON.stringify JSON.stringify(value[, replacer[, space]]) JSON.stringify方法会忽略所有原型成员,以及所有值为undefined的属性。 该方法可以指定两个可选参数:过滤器函数与缩进字符。 过滤器函数 If you return a Number, the string corresponding to that number is used as the value for the property when added to the JSON string. If you return a String, that string is used as the property’s value when adding it to the JSON string. If you return a Boolean, “true” or “false” is used as the property’s value, as appropriate, when adding it to the JSON string. If you return any other object, the object is recursively stringified into the JSON string, calling the replacer function on each property, unless the object is a function, in which case nothing is added to the JSON string. If you return undefined, the property is not included in the output JSON string. function replacer(key, value) { if (typeof value === "string") { return undefined; } return value; } var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7}; var jsonString = JSON.stringify(foo, replacer); 可以在过滤器函数中滤除不必要的属性 space参数 如果指定为数字,使用空格作为indent,最大为10. 也可以指定为其他字符。 JSON.stringify({ uno: 1, dos: 2 }, null, '\t'); // returns the string: // '{ // "uno": 1, // "dos": 2 // }' toJSON JSON.stringify的解析顺序如下 如果存在toJSON方法且能通过该方法取得有效值,调用该方法,否则返回对象本身。 如果提供第二个参数:函数过滤器,使用它,传入第一步的返回值。 对第二步返回的每个值进行相应的序列化。 执行格式化。 var obj = { foo: 'foo', toJSON: function() { return 'bar'; } }; JSON.stringify(obj); // '"bar"' JSON.stringify({ x: obj }); // '{"x":"bar"}' 解析JSON.parse() JSON.parse(text[, reviver]) 从字符串中解析JSON,可以选定一个reviver函数作为Hooki。 JSON.parse('{}'); // {} JSON.parse('true'); // true JSON.parse('"foo"'); // "foo" JSON.parse('[1, 5, "false"]'); // [1, 5, "false"] JSON.parse('null'); // null Reviver函数 JSON.parse('{"p": 5}', function(k, v) { if (typeof v === 'number') { return v * 2; // return v * 2 for numbers } return v; // return everything else unchanged }); 解析与序列化(Python) python的JSON模块基本与Js类似。 类型映射 JSON PYTHON JSON Python object dict array list string unicode number (int) int, long numbe (real) float true True false False null None JSON序列化 >>> import json >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) '["foo", {"bar": ["baz", null, 1.0, 2]}]' >>> print json.dumps("\"foo\bar") "\"foo\bar" >>> print json.dumps(u'\u1234') "\u1234" >>> print json.dumps('\\') "\\" >>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) {"a": 0, "b": 0, "c": 0} >>> print json.dumps(None)' null 其格式可以通过indent参数控制。 >>> import json >>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) '[1,2,3,{"4":5,"6":7}]' >>> import json >>> print json.dumps({'4': 5, '6': 7}, sort_keys=True, ... indent=4, separators=(',', ': ')) { "4": 5, "6": 7 } JSON解析 >>> import json >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] >>> json.loads('"\\"foo\\bar"') u'"foo\x08ar' JSON格式化工具 (Shell) $ echo '{"json":"obj"}' | python -mjson.tool { "json": "obj" }
原地址:https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/01.2.md 1.2 GOPATH与工作空间 前面我们在安装Go的时候看到需要设置GOPATH变量,Go从1.1版本开始必须设置这个变量,而且不能和Go的安装目录一样,这个目录用来存放Go源码,Go的可运行文件,以及相应的编译之后的包文件。所以这个目录下面有三个子目录:src、bin、pkg GOPATH设置 go 命令依赖一个重要的环境变量:$GOPATH Windows系统中环境变量的形式为%GOPATH%,本书主要使用Unix形式,Windows用户请自行替换。 (注:这个不是Go安装目录。下面以笔者的工作目录为示例,如果你想不一样请把GOPATH替换成你的工作目录。) 在类似 Unix 环境大概这样设置: export GOPATH=/home/apple/mygo 为了方便,应该新建以上文件夹,并且上一行加入到 .bashrc 或者 .zshrc 或者自己的 sh 的配置文件中。 Windows 设置如下,新建一个环境变量名称叫做GOPATH: GOPATH=c:\mygo GOPATH允许多个目录,当有多个目录时,请注意分隔符,多个目录的时候Windows是分号,Linux系统是冒号,当有多个GOPATH时,默认会将go get的内容放在第一个目录下。 以上 $GOPATH 目录约定有三个子目录: src 存放源代码(比如:.go .c .h .s等) pkg 编译后生成的文件(比如:.a) bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中,如果有多个gopath,那么使用${GOPATH//://bin:}/bin添加所有的bin目录) 以后我所有的例子都是以mygo作为我的gopath目录 代码目录结构规划 GOPATH下的src目录就是接下来开发程序的主要目录,所有的源码都是放在这个目录下面,那么一般我们的做法就是一个目录一个项目,例如: $GOPATH/src/mymath 表示mymath这个应用包或者可执行应用,这个根据package是main还是其他来决定,main的话就是可执行应用,其他的话就是应用包,这个会在后续详细介绍package。 所以当新建应用或者一个代码包时都是在src目录下新建一个文件夹,文件夹名称一般是代码包名称,当然也允许多级目录,例如在src下面新建了目录$GOPATH/src/github.com/astaxie/beedb 那么这个包路径就是”github.com/astaxie/beedb”,包名称是最后一个目录beedb 下面我就以mymath为例来讲述如何编写应用包,执行如下代码 cd $GOPATH/src mkdir mymath 新建文件sqrt.go,内容如下 // $GOPATH/src/mymath/sqrt.go源码如下: package mymath func Sqrt(x float64) float64 { z := 0.0 for i := 0; i < 1000; i++ { z -= (z*z - x) / (2 * x) } return z } 这样我的应用包目录和代码已经新建完毕,注意:一般建议package的名称和目录名保持一致 编译应用 上面我们已经建立了自己的应用包,如何进行编译安装呢?有两种方式可以进行安装 1、只要进入对应的应用包目录,然后执行go install,就可以安装了 2、在任意的目录执行如下代码go install mymath 安装完之后,我们可以进入如下目录 cd $GOPATH/pkg/${GOOS}_${GOARCH} //可以看到如下文件 mymath.a 这个.a文件是应用包,那么我们如何进行调用呢? 接下来我们新建一个应用程序来调用这个应用包 新建应用包mathapp cd $GOPATH/src mkdir mathapp cd mathapp vim main.go $GOPATH/src/mathapp/main.go源码: package main import ( "mymath" "fmt" ) func main() { fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2)) } 可以看到这个的package是main,import里面调用的包是mymath,这个就是相对于$GOPATH/src的路径,如果是多级目录,就在import里面引入多级目录,如果你有多个GOPATH,也是一样,Go会自动在多个$GOPATH/src中寻找。 如何编译程序呢?进入该应用目录,然后执行go build,那么在该目录下面会生成一个mathapp的可执行文件 ./mathapp 输出如下内容 Hello, world. Sqrt(2) = 1.414213562373095 如何安装该应用,进入该目录执行go install,那么在$GOPATH/bin/下增加了一个可执行文件mathapp, 还记得前面我们把$GOPATH/bin加到我们的PATH里面了,这样可以在命令行输入如下命令就可以执行 mathapp 也是输出如下内容 Hello, world. Sqrt(2) = 1.414213562373095 这里我们展示如何编译和安装一个可运行的应用,以及如何设计我们的目录结构。 获取远程包 go语言有一个获取远程包的工具就是go get,目前go get支持多数开源社区(例如:github、googlecode、bitbucket、Launchpad) go get github.com/astaxie/beedb go get -u 参数可以自动更新包,而且当go get的时候会自动获取该包依赖的其他第三方包 通过这个命令可以获取相应的源码,对应的开源平台采用不同的源码控制工具,例如github采用git、googlecode采用hg,所以要想获取这些源码,必须先安装相应的源码控制工具 通过上面获取的代码在我们本地的源码相应的代码结构如下 $GOPATH src |--github.com |-astaxie |-beedb pkg |--相应平台 |-github.com |--astaxie |beedb.a go get本质上可以理解为首先第一步是通过源码工具clone代码到src下面,然后执行go install 在代码中如何使用远程包,很简单的就是和使用本地包一样,只要在开头import相应的路径就可以 import "github.com/astaxie/beedb" 程序的整体结构 通过上面建立的我本地的mygo的目录结构如下所示 bin/ mathapp pkg/ 平台名/ 如:darwin_amd64、linux_amd64 mymath.a github.com/ astaxie/ beedb.a src/ mathapp main.go mymath/ sqrt.go github.com/ astaxie/ beedb/ beedb.go util.go 从上面的结构我们可以很清晰的看到,bin目录下面存的是编译之后可执行的文件,pkg下面存放的是应用包,src下面保存的是应用源代码
Variables & Fields 变量与字段 The Java programming language defines the following kinds of variables: Instance Variables (Non-Static Fields) Technically speaking, objects store their individual states in “non-static fields”, that is, fields declared without the static keyword. Non-static fields are also known as instance variables because their values are unique to each instance of a class (to each object, in other words); the currentSpeed of one bicycle is independent from the currentSpeed of another. 实例变量,每个实例不同。 Class Variables (Static Fields) A class variable is any field declared with the static modifier; this tells the compiler that there is exactly one copy of this variable in existence, regardless of how many times the class has been instantiated. A field defining the number of gears for a particular kind of bicycle could be marked as static since conceptually the same number of gears will apply to all instances. The code static int numGears = 6; would create such a static field. Additionally, the keyword final could be added to indicate that the number of gears will never change. 类变量由static定义,静态变量,全局静态存储,类共享。 Local Variables Similar to how an object stores its state in fields, a method will often store its temporary state in local variables. The syntax for declaring a local variable is similar to declaring a field (for example, int count = 0;). There is no special keyword designating a variable as local; that determination comes entirely from the location in which the variable is declared — which is between the opening and closing braces of a method. As such, local variables are only visible to the methods in which they are declared; they are not accessible from the rest of the class. 局部变量定义于方法中,存储于栈里。 Parameters You’ve already seen examples of parameters, both in the Bicycle class and in the main method of the “Hello World!” application. Recall that the signature for the main method is public static void main(String[] args). Here, the args variable is the parameter to this method. The important thing to remember is that parameters are always classified as “variables” not “fields”. This applies to other parameter-accepting constructs as well (such as constructors and exception handlers) that you’ll learn about later in the tutorial. 参数,参数是方法签名中定义的变量,存储于堆栈。 The Java programming language uses both “fields” and “variables” as part of its terminology. Instance variables (non-static fields) are unique to each instance of a class. Class variables (static fields) are fields declared with the static modifier; there is exactly one copy of a class variable, regardless of how many times the class has been instantiated. Local variables store temporary state inside a method. Parameters are variables that provide extra information to a method; both local variables and parameters are always classified as “variables” (not “fields”). When naming your fields or variables, there are rules and conventions that you should (or must) follow. 变量 和字段是两种不同的东西。变量包括局部变量和参数,初始化于栈中字段包括实例字段和类字段,初始化于堆中 Naming 名字 命名规则基本与C一致 原子数据类型 byte,short,int,long,float,char 原子数据类型是语言内建支持的,不是从类创建的。 string不是原子类型,但是语言有特殊支持。 Array 数组 An array is a container object that holds a fixed number of values of a single type. The length of an array is established when the array is created. After creation, its length is fixed. You have seen an example of arrays already, in the main method of the “Hello World!” application. This section discusses arrays in greater detail. //数组可以这样声明,但不推荐 int[] anArray; //数组初始化 anArray = new int[10]; //列表初始化 int[] anArray = { 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000 }; 多维数组 You can also declare an array of arrays (also known as a multidimensional array) by using two or more sets of brackets, such as String[][] names. Each element, therefore, must be accessed by a corresponding number of index values. In the Java programming language, a multidimensional array is an array whose components are themselves arrays. This is unlike arrays in C or Fortran. A consequence of this is that the rows are allowed to vary in length, as shown in the following MultiDimArrayDemo program: class MultiDimArrayDemo { public static void main(String[] args) { String[][] names = { {"Mr. ", "Mrs. ", "Ms. "}, {"Smith", "Jones"} }; // Mr. Smith System.out.println(names[0][0] + names[1][0]); // Ms. Jones System.out.println(names[0][2] + names[1][1]); } } The output from this program is: Mr. Smith Ms. Jones Finally, you can use the built-in length property to determine the size of any array. The following code prints the array’s size to standard output: System.out.println(anArray.length);
Happybase是Python通过Thrift访问HBase的库,方便快捷。 基本使用 import happybase connection = happybase.Connection('hostname') table = connection.table('table-name') table.put('row-key', {'family:qual1': 'value1', 'family:qual2': 'value2'}) row = table.row('row-key') print row['family:qual1'] # prints 'value1' for key, data in table.rows(['row-key-1', 'row-key-2']): print key, data # prints row key and data for each row for key, data in table.scan(row_prefix='row'): print key, data # prints 'value1' and 'value2' row = table.delete('row-key') 链接 # lazy connection connection = happybase.Connection('somehost', autoconnect=False) # and before first use: connection.open() # show all tables print connection.tables() # Using table namespace connection = happybase.Connection('somehost', table_prefix='myproject') 表 connection.create_table( 'mytable', {'cf1': dict(max_versions=10), 'cf2': dict(max_versions=1, block_cache_enabled=False), 'cf3': dict(), # use defaults } ) table = connection.table('mytable')
pgpool是一个PostgreSQL链接池,有很多相当不错的特性。 下载,编译,安装 官方中文文档 ./configure make && sudo make install 假设安装到了默认目录 # 编辑配置文件 sudo cp /usr/local/etc/pgpool.conf.sample /usr/local/etc/pgpool.conf vi /usr/local/etc/pgpool.conf 主要是设置后面几个PostgreSQL的实例地址。 PostgreSQL实例需要完成的工作 cd sql/pgpool-regclass; sudo make install; cd ../pgpool-recovery sudo make install; cd ..; psql -f insert_lock.sql template1 psql -f insert_lock.sql elysium psql template1 CREATE EXTENSION pgpool_regclass; CREATE EXTENSION pgpool_recovery; \c elysium CREATE EXTENSION pgpool_regclass; CREATE EXTENSION pgpool_recovery;
MySQL是最流行的开源数据库,而PostgreSQL是最先进的开源数据库。虽然我现在自己已经全面投入PostgreSQL的怀抱中了,但是还有许多迷途的羔羊执迷不悟,或者无力抽身,不求上进,满足于MySQL。所以目前来看还有是有MySQL的使用需求的。本文描述了*nix下MySQL的源码安装方法。 从源码编译安装MySQL tar -zxvf mysql-5.7.9-osx10.10-x86_64.tar.gz mv mysql-5.7.9-osx10.10-x86_64 /usr/local/mysql chown -R root:wheel mysql bin/mysqld --initialize --user=mysql cd /usr/local sudo chown -R root:wheel mysql cd /usr/local/mysql sudo bin/mysqld --initialize --user=mysql # Remember the root password cp support-files/my-default.cnf /etc/my.cnf # Add Following content to /etc/my.cnf [client] default-character-set=utf8 [mysqld] default-storage-engine=INNODB character-set-server=utf8 collation-server=utf8_general_ci # Admin support-files/mysql.server start support-files/mysql.server restart support-files/mysql.server stop support-files/mysql.server status # Change Root Password bin/mysqladmin -u root -p password <newpassword> $ <Input temp password here> # login with root bin/mysql -p # Create Main User CREATE USER 'vonng'@'%' IDENTIFIED BY 'xxxx'; grant all privileges on *.* to 'vonng'@'%' with grant option; create database vonng; create database test; # Create server user CREATE USER 'vonngserver'@'localhost' IDENTIFIED BY 'xxxx'; grant all privileges on vonng.* to 'vonngserver'@'localhost'; grant all privileges on test.* to 'vonngserver'@'localhost'; flush privileges; # Uninstall sudo rm -rf /usr/local/mysql sudo rm -rf /usr/local/mysql* sudo rm -rf /Library/StartupItems/MySQLCOM sudo rm -rf /Library/PreferencePanes/My* sudo rm -rf /Library/Receipts/mysql* sudo rm -rf /Library/Receipts/MySQL* sudo rm -rf /var/db/receipts/com.mysql.* # Dump: /path/to/mysql/bin/mysqldump -u<username> -p <databasename> > dumpfile_name # Example: /usr/local/mysql/bin/mysqldump -uvonng -p cnzzdb > ~/Data/mysql/cnzzdb.sql # Recover mysql -u<username> -p -D <dbname> < dump_file_name # Example mysql -p -D testdb< ~/Data/mysql/cnzzdb.sql 在Mac上设置开机自动启动 sudo vi /Library/LaunchDaemons/com.mysql.mysql.plist <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>KeepAlive</key> <true/> <key>Label</key> <string>com.mysql.mysqld</string> <key>ProgramArguments</key> <array> <string>/usr/local/mysql/bin/mysqld_safe</string> <string>--user=root</string> </array> </dict> </plist> sudo launchctl load -w /Library/LaunchDaemons/com.mysql.mysql.plist
本文描述了这个博客背后归档系统的设计与实现思路。 逻辑模型设计 通常博客所用的归档方式有三种: 按发布时间归档 分类体系 标签体系 这三种归档体系是可以同时存在的,而且这样的设计也非常常见。 其中按时间归档实在是Trivial,而且归档逻辑性也相对较弱,故在此略去不提。 通常分类与标签体系最大的区别在于,文章和分类是多对一的关系,而标签和文章是多对多的关系。 实际上在使用中,我发现分类确实不如标签好用。 在使用OneNote整理笔记的时候,经常出现一类很让人头疼的Delimma。比如,用Python做的可视化演示,究竟是应当放入Python分类中呢?还是放在可视化分类中呢? 避免这个问题,需要分类设计做到完全正交,然而真正能做到完全正交的分类体系又往往不甚实用……。更重要的一点是,每当我检索笔记的时候,往往是通过类似标签形式的关键词搜索去定位文章的……分类体系更是显得累赘了…… 不过完全不要分类体系好不好?当然也不好,当然,多级分类应该拍扁成一级分类,这样分类就可以做成一个字段放入文章表中了。 所以在我看来,博客的归档系统,应当以标签系统为主,分类系统为辅。 分类系统必须是扁平的,不可有过多的类目,且尽量做到正交。 我的博客目前拟采用以下分类 INSERT INTO categories (cate_name) VALUES ('未分类'), ('随笔文章'), ('生活记录'), ('工作记录'), ('文档剪藏'), ('读书笔记'), ('前端戏法'), ('后端杂技'), ('算法心得'), ('数据库'), ('部署运维'),('知识积累'), ('游戏杂谈'); 而标签就会显得比较随意了。 数据模型设计 传统方法 传统上,按照数据库设计理论Blabla,应当这样设计: ------------- 基础表定义 ------------- -- 登录信息表 CREATE TABLE IF NOT EXISTS login ( user_id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE, hashed_password TEXT NOT NULL ); -- 用户信息表 CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY REFERENCES login (user_id), email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, avatar TEXT ); -- 博文分类表 CREATE TABLE IF NOT EXISTS categories ( cate_id SERIAL PRIMARY KEY, cate_name TEXT NOT NULL UNIQUE ); -- 博客文章表 CREATE TABLE IF NOT EXISTS articles ( article_id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES users (user_id), title TEXT NOT NULL, content TEXT NOT NULL, description TEXT, thumb TEXT, cate_id INTEGER NOT NULL REFERENCES categories (cate_id) DEFAULT 1, private BOOLEAN DEFAULT FALSE, ctime TIMESTAMP NOT NULL DEFAULT current_timestamp, mtime TIMESTAMP NOT NULL DEFAULT current_timestamp, read_cnt INT DEFAULT 0, upvote_cnt INT DEFAULT 0 ); -- 标签表 CREATE TABLE IF NOT EXISTS tags ( tag_id SERIAL PRIMARY KEY, tag_name TEXT NOT NULL UNIQUE ); -- 标签映射表 CREATE TABLE IF NOT EXISTS tag_mapping ( article_id INTEGER REFERENCES articles (article_id) ON DELETE CASCADE, tag_id INTEGER REFERENCES tags (tag_id), PRIMARY KEY (article_id, tag_id) ); ---------------- 复合类型定义 ---------------- -- 文章类型,包括文章的详细信息 CREATE TYPE article AS ( user_id INTEGER, cate_id INTEGER, article_id INTEGER, title TEXT, content TEXT, description TEXT, thumb TEXT, private BOOLEAN, ctime TIMESTAMP, mtime TIMESTAMP, read_cnt INTEGER, upvote_cnt INTEGER, cate_name TEXT, email TEXT, name TEXT, avatar TEXT, tags JSONB ); -- 文章摘要,除了content字段外的所有信息 CREATE TYPE article_entry AS ( user_id INTEGER, cate_id INTEGER, article_id INTEGER, title TEXT, description TEXT, thumb TEXT, private BOOLEAN, ctime TIMESTAMP, mtime TIMESTAMP, read_cnt INTEGER, upvote_cnt INTEGER, cate_name TEXT, email TEXT, name TEXT, avatar TEXT, tags JSONB ); -- 标签信息 CREATE TYPE tag AS ( tag_id INTEGER, tag_name TEXT, article_cnt INTEGER, articles INTEGER [] ); -- 类目信息 CREATE TYPE category AS ( cate_id INTEGER, cate_name TEXT, article_cnt INTEGER, articles INTEGER [] ); -- 用户信息 CREATE TYPE user_entry AS ( user_id INTEGER, email TEXT, hashed_password TEXT, nickname TEXT, avatar TEXT ); 这样设计的好处,当时只要对映射表轻松地聚合,就可以实现查询文章所有的标签和查询标签对应的所有文章,正查倒查效率都很高。当然坏处就是特么的查个标签最后我要Join三张表…… JSON方法 事实上,如果把标签作为一个Json字段放在文章表里,就可以省却很多这类破事了。 尤其是当PostgreSQL支持JSON索引的时候,直接通过where子句筛选标签字段,同样可以实现传统方法的功能,而且不需要额外的Join了。 不过后来,我发现JSON的设计还是不给力,因为如果我想统计每个标签下面究竟挂着多少文章,JSON的设计就蛋疼了。仔细想想,关系型数据库的崛起确实是因为它的确具有优越性。当然,最后定义了视图里面,还是采用了一些JSON字段的。
Tornado的确很给力,知乎、Facebook一直在用。不过Tornado也有自己的局限性,比如它就没有实现完整的HTTP协议,甚至一些REST方法都不支持。不过这也难为它了,本来就是一个Web Framework顺便兼职干一点Web Server的事情而已,有就不错了。好在Tornado现在有了好伙伴Nginx,像HTTPS,负载均衡,静态文件这种破事都可以交给Nginx去处理了。 下载 从源代码编译安装Nginx需要处理一些依赖Nginx官网PCRE官网ZLib官网 下载好之后解压至同一根目录下。 编译 $ ./configure --sbin-path=/usr/local/nginx/nginx --conf-path=/usr/local/nginx/nginx.conf --pid-path=/usr/local/nginx/nginx.pid --with-http_ssl_module --with-pcre=../pcre-8.38 --with-zlib=../zlib-1.2.8 --with-openssl=../openssl-1.0.2g #上面只是为了好看.... $ ./configure --sbin-path=/usr/local/nginx/nginx --conf-path=/usr/local/nginx/nginx.conf --pid-path=/usr/local/nginx/nginx.pid --with-http_ssl_module --with-pcre=../pcre-8.38 --with-zlib=../zlib-1.2.8 --with-openssl=../openssl-1.0.2g $ make -j8 && sudo make install 需要注意的是,Mac下和Linux下默认生成的目录是不一样的。 nginx运行一般要求root权限。 nginx命令行参数相当简单,因为功夫全在配置文件里了……。 #启动Nginx实例 $ nginx #使用指定的配置文件启动nginx $ nginx -c <conf_file> #向Nginx发出信号 $ nginx -s [stop| quit| reopen| reload] Mac下的Nginx配置 user nobody nobody; worker_processes 1; error_log /var/log/nginx/error.log; pid /var/run/nginx.pid; events { worker_connections 1024; use kqueue; } http { # Enumerate all the Tornado servers here upstream frontends { server 127.0.0.1:8888; } include /usr/local/nginx/conf/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; keepalive_timeout 65; proxy_read_timeout 200; sendfile on; tcp_nopush on; tcp_nodelay on; gzip on; gzip_min_length 1000; gzip_proxied any; # Only retry if there was a communication error, not a timeout # on the Tornado server (to avoid propagating "queries of death" # to all frontends) proxy_next_upstream error; server { listen 80; # Allow file uploads client_max_body_size 50M; location ^~ /static/ { root /Volumes/Data/vserver; if ($query_string) { expires max; } } location ^~ /upload/ { root /Volumes/Data/vserver; if ($query_string) { expires max; } } location = /favicon.ico { access_log off; rewrite (.*) /static/other/favicon.ico; } location = /robots.txt { rewrite (.*) /static/other/robots.txt; } # Ali heartbeat dectection location = /status.taobao { access_log off; rewrite (.*) /static/other/status.taobao; } location / { proxy_pass_header Server; proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme $scheme; proxy_pass http://frontends; } } } Linux下Nginx的配置 # user nobody nobody; worker_processes 1; #error_log /var/log/nginx/error.log; #pid /var/run/nginx.pid; events { worker_connections 1024; } http { # Enumerate all the Tornado servers here upstream frontends { server 127.0.0.1:8888; } include /usr/local/nginx/mime.types; default_type application/octet-stream; #access_log /var/log/nginx/access.log; keepalive_timeout 65; proxy_read_timeout 200; sendfile on; tcp_nopush on; tcp_nodelay on; gzip on; gzip_min_length 1000; gzip_proxied any; # Only retry if there was a communication error, not a timeout # on the Tornado server (to avoid propagating "queries of death" # to all frontends) proxy_next_upstream error; server { listen 80; # Allow file uploads client_max_body_size 50M; location ^~ /static/ { root /home/vonng/vserver; if ($query_string) { expires max; } } location ^~ /upload/ { root /home/vonng/vserver; if ($query_string) { expires max; } } location = /favicon.ico { access_log off; rewrite (.*) /static/other/favicon.ico; } location = /robots.txt { rewrite (.*) /static/other/robots.txt; } # Ali heartbeat dectection location = /status.taobao { access_log off; rewrite (.*) /static/other/status.taobao; } location / { proxy_pass_header Server; proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme $scheme; proxy_pass http://frontends; } } }
Tornado的确很给力,知乎、Facebook一直在用。不过Tornado也有自己的局限性,比如它就没有实现完整的HTTP协议,甚至一些REST方法都不支持。不过这也难为它了,本来就是一个Web Framework顺便兼职干一点Web Server的事情而已,有就不错了。好在Tornado现在有了好伙伴Nginx,像HTTPS,负载均衡,静态文件这种破事都可以交给Nginx去处理了。 下载 从源代码编译安装Nginx需要处理一些依赖Nginx官网PCRE官网ZLib官网 下载好之后解压至同一根目录下。 编译 $ ./configure --sbin-path=/usr/local/nginx/nginx --conf-path=/usr/local/nginx/nginx.conf --pid-path=/usr/local/nginx/nginx.pid --with-http_ssl_module --with-pcre=../pcre-8.38 --with-zlib=../zlib-1.2.8 --with-openssl=../openssl-1.0.2g #上面只是为了好看.... $ ./configure --sbin-path=/usr/local/nginx/nginx --conf-path=/usr/local/nginx/nginx.conf --pid-path=/usr/local/nginx/nginx.pid --with-http_ssl_module --with-pcre=../pcre-8.38 --with-zlib=../zlib-1.2.8 --with-openssl=../openssl-1.0.2g $ make -j8 && sudo make install 需要注意的是,Mac下和Linux下默认生成的目录是不一样的。 nginx运行一般要求root权限。 nginx命令行参数相当简单,因为功夫全在配置文件里了……。 #启动Nginx实例 $ nginx #使用指定的配置文件启动nginx $ nginx -c <conf_file> #向Nginx发出信号 $ nginx -s [stop| quit| reopen| reload] Mac下的Nginx配置 user nobody nobody; worker_processes 1; error_log /var/log/nginx/error.log; pid /var/run/nginx.pid; events { worker_connections 1024; use kqueue; } http { # Enumerate all the Tornado servers here upstream frontends { server 127.0.0.1:8888; } include /usr/local/nginx/conf/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; keepalive_timeout 65; proxy_read_timeout 200; sendfile on; tcp_nopush on; tcp_nodelay on; gzip on; gzip_min_length 1000; gzip_proxied any; # Only retry if there was a communication error, not a timeout # on the Tornado server (to avoid propagating "queries of death" # to all frontends) proxy_next_upstream error; server { listen 80; # Allow file uploads client_max_body_size 50M; location ^~ /static/ { root /Volumes/Data/vserver; if ($query_string) { expires max; } } location ^~ /upload/ { root /Volumes/Data/vserver; if ($query_string) { expires max; } } location = /favicon.ico { access_log off; rewrite (.*) /static/other/favicon.ico; } location = /robots.txt { rewrite (.*) /static/other/robots.txt; } # Ali heartbeat dectection location = /status.taobao { access_log off; rewrite (.*) /static/other/status.taobao; } location / { proxy_pass_header Server; proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme $scheme; proxy_pass http://frontends; } } } Linux下Nginx的配置 # user nobody nobody; worker_processes 1; #error_log /var/log/nginx/error.log; #pid /var/run/nginx.pid; events { worker_connections 1024; } http { # Enumerate all the Tornado servers here upstream frontends { server 127.0.0.1:8888; } include /usr/local/nginx/mime.types; default_type application/octet-stream; #access_log /var/log/nginx/access.log; keepalive_timeout 65; proxy_read_timeout 200; sendfile on; tcp_nopush on; tcp_nodelay on; gzip on; gzip_min_length 1000; gzip_proxied any; # Only retry if there was a communication error, not a timeout # on the Tornado server (to avoid propagating "queries of death" # to all frontends) proxy_next_upstream error; server { listen 80; # Allow file uploads client_max_body_size 50M; location ^~ /static/ { root /home/vonng/vserver; if ($query_string) { expires max; } } location ^~ /upload/ { root /home/vonng/vserver; if ($query_string) { expires max; } } location = /favicon.ico { access_log off; rewrite (.*) /static/other/favicon.ico; } location = /robots.txt { rewrite (.*) /static/other/robots.txt; } # Ali heartbeat dectection location = /status.taobao { access_log off; rewrite (.*) /static/other/status.taobao; } location / { proxy_pass_header Server; proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme $scheme; proxy_pass http://frontends; } } }
本文讲述Linux生产环境(RHEL5.5 Kernel2.6.32)下安装PostgreSQL9.5.2的过程。安装所有contrib扩展,安装Python扩展。 安装详细细节,或者其他需求请参考官方文档安装说明:http://www.postgresql.org/docs/9.5/interactive/installation.html 处理依赖 PostgreSQL的依赖都比较常见,一般系统中均已自带: GNU Make 3.8+,ISO/ANSI C compiler,tar, zlib, GNU Readline Lib 6。 GNU Readline Lib 这里提一下GNU Readline Lib。这个主要是用于psql shell一些查看历史记录之类功能。 因为生产环境的Readline版本可能较老,如果要手动安装,请参考GNU ReadLine document: https://cnswww.cns.cwru.edu/php/chet/readline/INSTALL 如果安装的时候因为readline报错了,可以在configure的时候用—without-readline去掉。 Anaconda 安装Python扩展需要Python解释器,libpython,和Python头文件。 这里以使用Anaconda发行版安装Python: 从http://continuum.io/downloads下载对应的版本。 执行bash Anaconda2-4.0.0-Linux-x86_64.sh 依据提示选择安装路径。这里为/usr/local/anaconda,没有权限请sudo。 等待安装完成,将anaconda目录下bin, inlucde, lib目录分别加入各自的搜索路径中。 export PATH="/usr/local/anaconda/bin/:$PATH" export C_INCLUDE_PATH="/usr/local/anaconda/include/:$C_INCLUDE_PATH" export CPLUS_INCLUDE_PATH="/usr/local/anaconda/include/:$CPLUS_INCLUDE_PATH" export LD_LIBRARY_PATH="/usr/local/anaconda/lib/:$LD_LIBRARY_PATH" export LIBRARY_PATH="/usr/local/anaconda/lib/:$LIBRARY_PATH" 2. PostgreSQL的安装 # 编译安装主体 tar -jxf postgresql-9.5.2.tar.bz2 cd postgresql-9.5.2 ./configure PYTHON=/usr/local/anaconda/bin/python --with-python make -j8 sudo make install make install-docs # 编译安装标准扩展 $ cd contrib $ make -j8 $ sudo make install 3. 数据库的初始化与启动 # 创建一个用户,postgres主进程应当由一个独立的用户持有。 $ adduser postgres # 创建一个数据目录,并指定上面创建的用户所有 $ mkdir /usr/local/pgsql/data $ chown postgres /usr/local/pgsql/data $ su - postgres # 初始化数据目录 $ /usr/local/pgsql/bin/initdb -D /usr/local/pgsql/data $ 启动数据库 /usr/local/pgsql/bin/postgres -D /usr/local/pgsql/data >logfile 2>&1 & # 另一种启动数据库的方式是使用pg_ctl,推荐这种方式: $ /usr/local/pgsql/bin/pg_ctl init -D /usr/local/pgsql/data -l /usr/local/pgsql/data/logfile # 最后通过pg_ctl启动数据库 /usr/local/pgsql/bin/pg_ctl -D /usr/local/pgsql/data -l logfile start 4. 配置PostgreSQL 创建数据库、角色、授权 PostgreSQL安装完成后会自带一个postgres数据库,用户postgres可直接使用psql连接。 每个操作系统用户可以直接连接自己同名的数据库。psql [-U<username>] [database] $ psql # 创建数据库 $ create database vonng; # 创建角色: $ CREATE USER vonng login Superuser password 'xxxxxxx'; #授予权限: $ GRANT ALL PRIVILEGES ON DATABASE vonng to vonng; 允许从外部主机访问PostgreSQL # Pg默认只接受本机的连接。需要配置HBA允许外部链接,具体细节参见文档。 # 这里假设我们希望在10.0.0.0-10.255.255.255的A类局域网段内允许任何用户连接任何数据库。 # 打开数据文件夹中的pg_hba.conf文件. $ vi /usr/local/pgsql/data/pg_hba.conf # 在最下方添加一行 host all all 10.0.0.0/8 trust # 打开数据文件夹中的postgresql.conf # 找到 #listen_addresses = 'localhost',修改为 listen_addresses="*" # 重启PostgreSQL: $ pg_ctl stop -D /usr/local/pgsql/data $ pg_ctl start -D /usr/local/pgsql/data -l logfile # 在另一台机器上测试 $ psql -h<Your Pgserver host> -U<Your Pgserver username>。 5. PostgreSQL的常用运维命令 #备份一个数据库 $ pg_dump [connection-option...] [option...] [dbname] #恢复一个数据库 $ psql [connectino-option] < dumpfile.sql
根据PEP-0342 Coroutines via Enhanced Generators,原来仅仅用于生成器的yield关键字被扩展,成为Python协程实现的一部分。而之所以使用协程,主要是出于性能的考虑:一个活跃的Python线程大约占据8MB内存,而一个活跃线程只使用1KB不到内存。对于IO密集型的应用,显然轻量化的协程更适用。 概述 原来,yield是一个statement,即和return一样的语句,但是在PEP-0342后,yield statement被改造为了yield expression。其语法如下: Yield expressions: yield_atom ::= "(" yield_expression ")" yield_expression ::= "yield" [expression_list] yield表达式只能在函数体内使用,会导致该函数变为一个 生成器函数 当生成器函数被调用,它会返回一个 生成器 ,用以控制生成器函数的执行. 生成器的第一次执行,必须使用gen_func.next()方法,函数会执行到第一条yield的位置并返回该处yield语句之后的表达式值。 接下来,可以交替使用gen_func.next与gen_func.send方法对协程的执行。send方法会传入一个参数,该参数即yield表达式的值,可以在生成器函数里面被接收。而next则不传入参数。当send or next被执行时,生成器函数会从yield表达式处继续执行。直到下一次出现yield语句,再次返回yield之后表达式的值。 一个简单的例子:使用协程返回值 最简单的协程使用,通过next与send控制协程的运行。 def coroutine_print(): while True: message = yield print message it = coroutine_print() next(it) it.send('Hello') it.send('World') # result: # Hello # World 稍微复杂一点:向协程发送数据计算平均值 稍微复杂的一个例子,使用yield的返回值。 假设我们希望执行一个流式计算,不断有数据到达,我们希望计算这些数据的平均数。 def avg_coroutine(): cnt = 0 sum = 0 new_value = yield while True: sum += new_value cnt += 1 new_value = yield float(sum) / cnt In [6]: it = avg_coroutine() In [7]: it.next() In [8]: it.send(10) Out[8]: 10.0 In [9]: it.send(20) Out[9]: 15.0 In [10]: it.send(30) Out[10]: 20.0 In [11]: it.send(110) Out[11]: 42.5 如本例所示,在使用协程时,首先调用next方法停止在第一条yield处,并使用send方法向协程内发送数据,并获取更新后的平均值。 使用throw方法与协程通信 一旦协程开启,仅仅通过send与yield只能对协程进行简单的控制。throw方法提供了在协程内引发异常的接口。通过主叫者调用throw在协程内引发异常,协程捕获异常的方式,可以实现主叫者与协程之间的通信。 需要注意的是,使用throw方法在协程内引发的异常,如果没有被捕获,或者内部又重新raise了不同的异常,那么这个异常会传播到主叫者。 同时throw方法同send与next一样,都会使协程继续运行,并返回下一个yield表达式中的表达式值。且同样的,如果不存在下一个yield表达式协程就结束了,主叫方会收到StopIteration Exception。 使用close方法来结束协程 close方法与throw方法类似,都是在协程暂停的位置引发一个异常,从而向协程发出控制信息。不同于throw方法可以抛出自定义异常,close方法会固定抛出GeneratorExit异常。当这个异常没有被捕获或者引发StopIteration Exception时,close方法会正常返回。这个比较好理解,协程设计者捕获了GeneratorExit异常并完成清理,保证清理干净了,所以最后不会再有yield返回值引发StopItertation。如果因为设计错误导致仍然有下一个yield,那么就会抛出RuntimeError. Python文档中给出的应用四个协程API的样例。 >>> def echo(value=None): ... print "Execution starts when 'next()' is called for the first time." ... try: ... while True: ... try: ... value = (yield value) ... except Exception, e: ... value = e ... finally: ... print "Don't forget to clean up when 'close()' is called." ... >>> generator = echo(1) >>> print generator.next() Execution starts when 'next()' is called for the first time. 1 >>> print generator.next() None >>> print generator.send(2) 2 >>> generator.throw(TypeError, "spam") TypeError('spam',) >>> generator.close() Don't forget to clean up when 'close()' is called. 这和原来的生成器。这时候使用gen_func.send()会报错,因为没有接受 当某个生成器函数被执行时,生成器函数会开始执行,直到第一条yield表达式为止。 这条表达式会有一个返回值,它取决于使用了生成器的哪个方法去恢复生成器函数的控制流。 next方法会恢复gen func的执行,这里yield表达式会返回值None。 而gen func会继续运行到下一条yield语句,并将yield后面的值反回。 send和next类似,不同的是她可以传入一个值作为yield表达式的计算值。如果用send去开启gen func的执行,那么参数必须为None,因为这时候没有yield语句在等待返回值。 Python文档中的描述 PEP 342 — Coroutines via Enhanced Generators New in version 2.5. The yield expression is only used when defining a generator function, and can only be used in the body of a function definition. Using a yield expression in a function definition is sufficient to cause that definition to create a generator function instead of a normal function. When a generator function is called, it returns an iterator known as a generator. That generator then controls the execution of a generator function. The execution starts when one of the generator’s methods is called. At that time, the execution proceeds to the first yield expression, where it is suspended again, returning the value of expression_list to generator’s caller. By suspended we mean that all local state is retained, including the current bindings of local variables, the instruction pointer, and the internal evaluation stack. When the execution is resumed by calling one of the generator’s methods, the function can proceed exactly as if the yield expression was just another external call. The value of the yield expression after resuming depends on the method which resumed the execution. All of this makes generator functions quite similar to coroutines; they yield multiple times, they have more than one entry point and their execution can be suspended. The only difference is that a generator function cannot control where should the execution continue after it yields; the control is always transferred to the generator’s caller. 5.2.10.1. Generator-iterator methods This subsection describes the methods of a generator iterator. They can be used to control the execution of a generator function. Note that calling any of the generator methods below when the generator is already executing raises a ValueError exception. generator.next() Starts the execution of a generator function or resumes it at the last executed yield expression. When a generator function is resumed with a next() method, the current yield expression always evaluates to None. The execution then continues to the next yield expression, where the generator is suspended again, and the value of the expression_list is returned to next()‘s caller. If the generator exits without yielding another value, a StopIteration exception is raised. generator.send(value) Resumes the execution and “sends” a value into the generator function. The value argument becomes the result of the current yield expression. The send() method returns the next value yielded by the generator, or raises StopIteration if the generator exits without yielding another value. When send() is called to start the generator, it must be called with Noneas the argument, because there is no yield expression that could receive the value. generator.throw(type[, value[, traceback]]) Raises an exception of type type at the point where generator was paused, and returns the next value yielded by the generator function. If the generator exits without yielding another value, a StopIteration exception is raised. If the generator function does not catch the passed-in exception, or raises a different exception, then that exception propagates to the caller. generator.close() Raises a GeneratorExit at the point where the generator function was paused. If the generator function then raises StopIteration (by exiting normally, or due to already being closed) or GeneratorExit (by not catching the exception), close returns to its caller. If the generator yields a value, a RuntimeError is raised. If the generator raises any other exception, it is propagated to the caller. close() does nothing if the generator has already exited due to an exception or normal exit. Here is a simple example that demonstrates the behavior of generators and generator functions: >>> def echo(value=None): ... print "Execution starts when 'next()' is called for the first time." ... try: ... while True: ... try: ... value = (yield value) ... except Exception, e: ... value = e ... finally: ... print "Don't forget to clean up when 'close()' is called." ... >>> generator = echo(1) >>> print generator.next() # Execution starts when 'next()' is called for the first time. 1 >>> print generator.next() None >>> print generator.send(2) 2 >>> generator.throw(TypeError, "spam") TypeError('spam',) >>> generator.close() # Don't forget to clean up when 'close()' is called.
KONG是一个基于Nginx的API Gateway。提供了诸如身份认证,权限控制,流量控制,日志等一系列API相关的组件,可谓相当方便。KONG项目首页KONG入门KONG的Github地址KONG Admin API一览KONG插件列表 安装 以Linux RedHat 6.2为例: 从此处下载对应二进制rpm包,按操作安装即可KONG RedHat Release 注意如果yum出现无法获取metalink的错误,sudo vi /etc/yum.repos.d/epel.repo 注释掉所有mirrorlist,取消所有baseurl的注释即可。 简介 KONG是一个API Gateway,顾名思义,就是API的出口。 内部系统的API可以随意编写,但如果要对外服务,就一定需要各种权限控制。 测试环境 首先,我们写一个Mock API,进行测试之用。 import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): call_cnt = 0 def get(self): MainHandler.call_cnt+= 1 self.write("GET / %s"%MainHandler.call_cnt) def post(self): MainHandler.call_cnt+= 1 self.write("POST / %s"%MainHandler.call_cnt) class TestHandler(tornado.web.RequestHandler): call_cnt = 0 def get(self): TestHandler.call_cnt += 1 self.write("GET /test %s"%TestHandler.call_cnt) def post(self): TestHandler.call_cnt += 1 self.write("POST /test %s"%TestHandler.call_cnt) def make_app(): return tornado.web.Application([ (r"/", MainHandler), (r"/test", TestHandler) ]) if __name__ == "__main__": app = make_app() app.listen(8812) tornado.ioloop.IOLoop.current().start() 这是一个用Tornado写的简易REST测试 API,用以下命令后台启动nohup python test_api.py 1>/dev/null 2>log &; # 测试Endpoint:/ curl -i -X GET http://localhost:8812/ curl -i -X POST http://localhost:8812/ curl -i -X DELETE http://localhost:8812/ # 测试Endpoint:/test curl -i -X GET http://localhost:8812/test curl -i -X POST http://localhost:8812/test curl -i -X DELETE http://localhost:8812/test 现在我们已经有一个正常工作的REST API了。是时候使用KONG了。 KONG的使用基本和NGINX如出一辙。默认的工作目录是/usr/local/kong,默认的配置文件是/etc/kong/kong.yml。如果需要工作在1024以下的端口,则要求root权限。 KONG的启动命令很简单,就是kong start 但是启动之前需要编辑配置文件。 需要关注的主要是几个点: 发送匿名统计报告,(默认打开,一定要关闭) 后端数据库的配置,(默认Cassandra,建议Postgres) 内存缓存大小配置,(默认128M,建议1G) 出口端口(默认HTTP 8000,HTTPS 8443,建议直接改成80和443) 管理端口(默认8001,我改成8888比较吉利) 配置好之后,启动KONG即可。 配置 KONG的配置完全通过REST API完成,Admin端口在配置文件中可以配置,下面全部使用http://localhost:8888/作为Admin EndPoint 使用什么语言的HTTP库都可以完成这个工作,这里直接使用Linux自带的curl。 注册API 第一步,我们需要先注册我们的API 假设我们想把上面那个API挂载到/test路径下, 即用户访问http://localhost/test/ 时,会返回GET / 用户访问http://localhost/test/test 时,会返回GET /test 执行 # 查看当前已经注册的API curl -X GET http://localhost:8888/apis/ # 注册一个新的API curl -i -X POST http://localhost:8888/apis/ -d "name=testapi" -d "request_path=/test" -d "upstream_url=http://localhost:8812/" -d "strip_request_path=true" 这里注册URL的几个参数意思是,这个API注册的名字叫testapi。它被挂载在网关的/test路径下,上游转发到http://localhost:8812去处理,转发的时候把前面的/test前缀给去掉。 {"upstream_url":"http:\/\/localhost:8812\/","request_path":"\/test","id":"a302c28c-eb8a-4e53-bac2-9acb68695f3b","created_at":1468379310000,"preserve_host":false,"strip_request_path":true,"name":"testapi"} 返回结果表明API创建的结果。一般会返回一个API的ID。 这就表明API注册完成,我们可以测试一下: curl -i -X GET http://localhost/test curl -i -X POST https://localhost/test/test --insecure 如同我们预期的一样返回正确的结果,说明API已经成功注册。 当然,仅仅把API注册了开放出去,其实也就是把多个API集成到一个EndPoint,实际意义并不大,下面我们来试一试高级一点的功能。 注册插件 KONG自带插件目前可以分为以下几类: 身份认证,安全,流量控制,分析监控,格式转换,日志。KONG插件列表 有的API完全开放,不需要任何认证,有的API会涉及敏感数据,权限控制需要非常严格。有的API完全不在乎调用频次或者日志,有的则反过来。 值得高兴的是,KONG的插件独立作用于每一个API,不同的API可以使用完全不同的插件。提供了相当灵活的配置策略。 现在我们首先给这个测试API加上基本的权限验证。 curl -X POST http://localhost:8888/apis/testapi/plugins -d "name=key-auth" # 这个时候再去调用这个API就会返回401 Unauthorized curl -i -X GET http://localhost/test 添加用户 朴素的API可能压根没有严格的用户概念,端口大开,随便哪个阿猫阿狗都能进来扫一扫看一看。这可不行。 KONG有一个consumer的概念,consumer是全局共用的。 比如某个API启用了key-auth,那么没有身份的访问者就无法调用这个API了。 需要首先创建一个Consumer,然后在key-auth插件中为这个consumer生成一个key。 然后就可以使用这个key来透过权限验证访问API了。 同理,如果另外一个API也开通了key-auth插件,那么这个consumer也是可以通过key-auth验证访问这个API的,如果要控制这种情况,就需要ACL插件。 (认证与权限乃是两个不同的事物) 首先我们创建一个名为test_user的consumer # 获取所有Consumer列表 curl -X GET http://localhost:8888/consumers/ # 创建一个新的Consumer curl -X POST http://localhost:8888/consumers/ -d "username=testuser" 然后,我们在key-auth插件中为它创建一个key curl -X POST http://localhost:8888/consumers/testuser/key-auth -d "key=testkey" 这次加上apikey首部进行验证,成功调用 curl -i -X GET http://localhost/test --header "apikey: testkey" 删除用户 curl -i -X DELETE http://localhost:8888/consumers/testuser 删除插件 # 查询API拥有的插件ID curl -i -X GET http://localhost:8888/apis/testapi/plugins/ # 根据插件ID删除插件。 curl -i -X DELETE http://localhost:8888/apis/testapi/plugins/e49a2b2e-4c36-4c6a-bd1a-b5b065e63bb8 以上是基本的API,Consumer,Plugin的基本介绍。 ACL插件 # 注册overseas_index API curl -X POST http://localhost:8888/apis/ -d "name=overseas_index" -d "upstream_url=http://10.182.20.128:8848/" -d "request_path=/stat/overseas" -d "strip_request_path=true" # 为海外版API注册key-auth插件 curl -X POST http://localhost:8888/apis/overseas_index/plugins --data "name=key-auth" # 创建overseas_user 用户 curl -X POST http://localhost:8888/consumers/ -d "username=overseas_test" # 为overseas_test创建权限验证 curl -X POST http://localhost:8888/consumers/overseas_test/key-auth -d "" # 尝试查询海外版指标接口 curl -X POST https://10.182.20.127/stat/overseas/realtime -d "app_id=53ace32656240b11c2071b1a" -d "date=2015-07-22" -H "apikey: 5e3b2a7a735744b39aeea9ebc2de1f01" --insecure # 给测试API curl -X POST https://10.182.20.128/test -d "app_id=53ace32656240b11c2071b1a" -d "date=2015-07-22" -H "apikey: 5e3b2a7a735744b39aeea9ebc2de1f01" --insecure 设置ACL插件 # 给另外一个testapi加上权限验证插件,现在任何通过认证的用户都可以调用此API # 这不是我们所希望的,所以需要加入ACL控制 curl -X POST http://localhost:8888/apis/testapi/plugins --data "name=key-auth" # 为用户overseas_test创建ACL群组:overseas_user curl -X POST http://localhost:8888/consumers/overseas_test/acls \ --data "group=overseas_user" # 为用户overseas_test创建ACL群组:overseas_user curl -X POST http://localhost:8888/consumers/overseas_test/acls \ --data "group=overseas_user"
心血来潮,简单测试一下各种语言写的API Server的性能。 前言 我已经用过很多Web框架了。Python-httplib, Python-Flask,Python-Tornado,Node-http, Node-Express,Node-koa,Node-restify, Go-http。最近在做OpenAPI,用了一个开源组件Kong,后来觉得这玩意虽然设计的不错但是碍手碍脚,有一些功能还是需要深入底层去自己研究实现。后来发现Kong是基于OpenResty实现的,而OpenResty则是Nginx的一个“Bundle”,打好了很多方便的包,性能很不错的样子。正好籍由此次机会,测试一下各语言写的裸API性能。 所有Server端使用HelloWorld Server,即发送”Hello, World”字符串作为Body。 测试Client一并使用ab -kc10 -n50000进行。 测试环境Server与Client位于同一级房的两台相邻物理机。规格为: CPU: Intel(R) Xeon(R) CPU E5-2430 0 @ 2.20GHz 24核, 内存100G。 只是简单测试一下。 测试Server用例 Node.js单进程 var http = require('http'); var server = http.createServer( (req, res) => { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end("Hello, World"); }); server.listen(8080); Node.js Cluster(24) const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { for (var i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); }); } else { http.createServer((req, res) => { res.writeHead(200); res.end("Hello, World"); }).listen(8080); } Python-Tornado import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def get(self): self.write("Hello, world") def make_app(): return tornado.web.Application([ (r"/", MainHandler), ]) if __name__ == "__main__": app = make_app() app.listen(8080) tornado.ioloop.IOLoop.current().start() Go-Http package main import ( "io" "net/http" ) func main() { http.HandleFunc("/", sayhello) http.ListenAndServe(":8080", nil) } func sayhello(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "hello world") } OpenResty(Nginx+lua) worker_processes 1; error_log logs/error.log; events { worker_connections 1024; } http { server { listen 8080; location / { default_type text/html; content_by_lua ' ngx.say("<p>hello, world</p>") '; } } } 结果 对各种语言框架的最简EchoServer实现进行不同并发度的测试。结果如下: c = 1 lang rps tpr (ms) node 1x 2451.25 0.408 node 24x 1119.81 0.893 Py-Tornado 1301.68 0.768 Go-Http 7108.64 0.141 Nginx-lua 1x 7385.98 0.135 Nginx-lua 24x 7368.34 0.136 c = 10 lang rps tpr (ms) node 1x 3944.75 2.535 node 24x 5645.11 1.771 Py-Tornado 1318.85 7.582 Go-Http 70085.24 0.143 Nginx-lua 1x 24753.79 0.404 Nginx-lua 24x 24824.98 0.403 c = 100 lang rps tpr (ms) node 1x 4042.27 24.739 node 24x 5816.23 17.193 Py-Tornado 1283.43 78.261 Go-Http 77451.38 1.373 Nginx-lua 1x 25001.29 4.080 Nginx-lua 24x 70333.04 1.619 结论: OpenResty(Nginx+Lua) 与 Go语言属于性能第一梯队。Node属于第二梯队,Python垫底……。 Go是特么的禽兽啊…… OpenResty也不错……。
Abstract 字符编码,在计算机导论中经常作为开门的前几个话题来讲,然而很多CS教材对这个话题基本都是走马观花地几页带过。导致了许多人对如此重要且基本的概念认识模糊不清。直到在实际编程中,尤其是遇到多语言、国际化的问题,被虐的死去活来之后才痛下决心去重新钻研。诸如此类极其基础却又容易被人忽视的的知识点还有:大小端表示,浮点数细节,正则表达式,日期时间处理等。本文是系列的第一篇,旨在阐明字符编码这个大坑中许多纠缠不清的概念。 基本概念 现代编码模型自底向上分为五个层次: 抽象字符表 ACR (Abstract Character Repertoire) 编码字符集 CCS (Coded Character Set) 字符编码表 CEF (Character Encoding Form) 字符编码方案 CES (Character Encoding Schema) 传输编码语法 TES (Transfer Encoding Syntax) 现代编码模型-WikiUnicode术语表 抽象字符集 ACR 抽象字符集是现代编码模型的最底层,它是一个集合,通过枚举指明了所属的所有抽象字符。但是要了解抽象字符集是什么,我们首先需要了解什么是字符与抽象字符 字符 (character, char) 字符是指字母、数字、标点、表意文字(如汉字)、符号、或者其他文本形式的书写“原子”。例: a,啊,あ, α,Д等,都是抽象的字符。 抽象字符 (Abstract Character) 抽象字符就是抽象的字符。像a这样的字符是有形的,但在计算机中,有许多的字符是空白的,甚至是不可打印的。比如ASCII字符集中的NULL,就是一个抽象字符。注意\x00,\000,NULL,0 这些写法都只是这个抽象字符的某种表现形式,而不是这个抽象字符本身。 抽象字符集 ACR (Abstract Character Repertoire) 抽象字符集顾名思义,指的是抽象字符的集合。已经有了很多标准的字符集定义: Character Sets比如US-ASCII, UCS(Unicode), GBK这些我们耳熟能详的名字,都是(或者至少是)抽象字符集。 US-ASCII定义了128个抽象字符的集合。GBK挑选了两万多个中日韩汉字和其他一些字符组成字符集,而UCS则尝试去容纳一切的抽象字符。它们都是抽象字符集。抽象字符 英文字母A同时属于US-ASCII, UCS, GBK这三个字符集。抽象字符 中文文字蛤不属于US-ASCII,属于GBK字符集,也属于UCS字符集。抽象文字 Emoji 不属于US-ASCII与GBK字符集,但属于UCS字符集。 集合的一个重要特性,就是无序性。集合中的元素都是无序的,所以抽象字符集中的字符都是无序的。 抽象字符集与python中的set的概念类似:例如:我可以自己定义一个字符的集合,叫这个集合为haha字符集。haha_acr = { 'a', '吼', 'あ', ' α', 'Д' } 大家觉得抽象字符集这个名字太啰嗦,所以有时候直接叫它字符集。 最后需要注意一点的是,抽象字符集也是有开放与封闭之分的。ASCII抽象字符集定义了128个抽象字符,再也不会增加。这是一个封闭字符集。Unicode尝试收纳所有的字符,一直在不断地扩张之中。最近(2016.06)Unicode 9.0.0已经收纳了128,237个字符,并且未来仍然会继续增长,这是一个开放的字符集。 编码字符集 CCS (Coded Character Set) Coded Character Set. A character set in which each character is assigned a numeric code point. Frequently abbreviated as character set, charset, or code set; the acronym CCS is also used. 编码字符集是现代编码体系的第二层。编码字符集是一个每个所属字符都分配了码位的抽象字符集。编码字符集(CCS)也经常简单叫做字符集(Character Set)。这样的叫法经常会将抽象字符集ACR与编码字符集CCS搞混。不过大多时候人们也不在乎这种事情。 抽象字符集是抽象字符的集合,而集合是无序的。无序的抽象字符集并没有什么卵用,因为我们只能判断某个字符是否属于某个字符集,却无法方便地引用,指称这个集合中的某个特定元素。以下两个表述指称了同一个字符,但哪一种更方便呢?ASCII(抽象)字符集中的那个代表什么都没有的通常表示为NULL的抽象字符ASCII(编码)字符集中的0号字符为了更好的描述,操作字符,我们可以为抽象字符集中的每个字符关联一个数字编号,这个数字编号称之为码位(Code Point)。 通常根据习惯,我们为字符分配的码位通常都是非负整数,习惯上用十六进制表示。且一个编码字符集中字符与码位的映射是一一映射。 举个例子,为haha抽象字符集进行编码,就可以得到haha编码字符集。haha_ccs = { 'a' : 0x0, '吼':0x1 , 'あ':0x2 , ' α':0x3 , 'Д':0x4 }字符吼与码位0x1关联,这时候,在haha编码字符集中,吼就不再是一个单纯的抽象字符了,而是一个编码字符(Coded Chacter),且拥有码位 0x1。 如果说抽象字符集是一个Set,那么编码字符集就可以类比为一个Dict。CCS = { k:i for i, k in enumerate(ACR)}它的key是字符,而value则是码位。至于码位具体是怎样分配的,这个规律就不好说了。比如为什么我想给haha_ccs的吼字符分配码位0x1而不是0x23333呢?因为这样能续一秒,反映了CCS设计者的主观趣味。 编码字符集有许许多多,但最出名的应该就是US-ASCII和UCS了。ASCII因为太有名了,所以就不说了。 统一字符集 UCS (Universal Character Set) 最常见的编码字符集就是统一字符集 UCSUCS. Acronym for Universal Character Set, which is specified by International Standard ISO/IEC 10646, which is equivalent in repertoire to the Unicode Standard. UCS就是统一字符集,就是由 ISO/IEC 10646所定义的编码字符集。通常说的“Unicode字符集”指的就是它。不过需要辨明的一点是,“Unicode”这个词本身指的是一系列用于计算机表示所有语言字符的标准。 基本上所有能在其他字符集中遇到的符号,都可以在UCS中找到,而一些新的不属于任何传统字符集的字符,例如Emoji,也会收录于UCS中。这也是UCS地位超然的原因。 举个例子,UCS中码位为0x4E00~0x9FFF的码位,就用于表示“中日韩统一表意文字” 大家喜闻乐见的Emoji表情则位于更高的码位,例如“哭笑”在UCS中的码位就是0x1F602。(如果这个站点不支持Emoji,你就看不到这个字符了,上面那个是图片…) >>> ''.decode('utf-8') u'\U0001f602' 关于CCS,这些介绍大抵足够了。不过还有一个细节需要注意。按照目前最新Unicode 9.0.0的标准,UCS理论上收录了128,237个字符,也就是0x1F4ED个。不过如果进行一些尝试会发现,实际能用的最大的码位点在0x1F6D0 ,也就是128,720,竟然超过了收录的字符数,这又是为什么呢? 码位是非负整数没错,但这不代表它一定是连续分配的。出现这种情况只有一个原因,那就是UCS的码位分配不是连续的,中间有一段空洞,即存在一段码位,没有分配对应的字符。 实际上,UCS实际分配的码位是 0x0000~0x0xD7FF 与 0xE000~0x10FFFF 这两段。中间0xD800~0xDFFF这2048个码位留作它用,并不对应实际的字符。如果直接尝试去输出这个码位段的'字符',结果会告诉你这是个非法字符。例如在python2中尝试打印码位0xDDDD的字符: >>> print u'\UDDDD' File "<stdin>", line 1 SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 0-5: truncated \UXXXXXXXX escape 0x0000~0xD7FF | 0xE000~0x10FFFF 称为Unicode标量值(Unicode scala value)0xD800~0xDBFF 称为High-surrogate0xDC00~0xDFFF 称为Low-surrogateUnicode标量值就是实际存在对应字符的码位。为什么中间一端的码位会留空,则是为了方便下一个层次的字符编码表CEF的UTF-16而处理的。 其他编码字符集 除了ASCII与UCS,世界上还有许许多多的字符集。在US-ASCII诞生与Unicode诞生之间,很多英语之外的字符无法在计算机中表示。大家八仙过海各显神通,定义了许许多多其他的字符集。例如GBK字符集,以及其近似实现 Code Page 936。这些字符集中的字符,最后都汇入了Unicode中。 字符编码表 CEF (Character Encoding Form) Unicode Encoding Form. A character encoding form that assigns each Unicode scalar value to a unique code unit sequence. The Unicode Standard defines three Unicode encoding forms: UTF-8, UTF-16, and UTF-32 字符编码表是现代编码模型的第三层。现在我们拥有一个编码字符集了,Let's say: UCS。这个字符集中的每个字符都有一个非负整数码位与之一一对应。看上去很好,既然计算机可以存储整数,而现在字符已经能表示为整数,我们是不是可以说,用计算机存储字符的问题已经得到了解决呢? 慢!还有一个问题没有解决。 在讲抽象字符集ACR的时候曾经提起,UCS是一个开放字符集,未来可能有更多的符号加入到这个字符集中来。也就是说UCS需要的码位,理论上是无限的。但计算机整形能表示的整数范围是有限的。譬如,一个字节的无符号单字节整形(unsigned char, uint8)能够表示的码位只有0~0xFF,共256个;一个无符号短整形(unsigned short, uint16)的可用码位只有0~0xFFFF,共65536个;而一个标准整形(unsigned int, uint32)能表示的码位只有0~0xFFFFFFFF,共4294967296个。 虽然就目前来看,UCS收录的符号总共也就十多万个,用一个uint可以表示几十亿个字符呢。但谁知道哪天制定Unicode标准的同志们不会玩心大发造几十亿个Emoji加入UCS中。所以说到底,一对有限与无限的矛盾,必须通过一种方式进行调和。这个解决方案,就是字符编码表(Character Encoding Form)。 字符编码表将码位(Code Point)映射为码元序列(Code Unit Sequences)。对于Unicode而言,字符编码表将Unicode标量值(Unicode scalar value)一一映射为码元序列(Code Unit Sequences)。 码元 Code unit: The minimal bit combination that can represent a unit of encoded text for processing or interchange. 码元是能用于处理或交换编码文本的最小比特组合。通常计算机处理字符的码元为一字节,即8bit。同时因为计算机中char其实是一种整形,而整形的计算往往以计算机的字长作为一个基础单元,通常来讲,也就是4字节。 Unicode定义了三种不同的CEF,分别采用了1字节,2字节,4字节的码元,正好对应了计算机中最常见的三种整形长度:在Unicode中,指定了三种标准的字符编码表,UTF-8, UTF-16, UTF-32。分别将Unicode标量值映射为比特数为8、16、32的码元的序列。UTF-8的码元为uint8, UTF-16的码元为uint16, UTF-32的码元为uint32。当然也有一些非标准的CEF,如UCS-2,UCS-4,在此不多介绍。 需要注意一点的是,CEF将码位映射为码元序列。这个映射必须是一一映射(双射)。因为当使用CEF进行编码(Encode)时,是将码位映射为码元序列。而当使用CEF进行解码(Decode)时,是将码元序列还原为码位。为了保证两个过程都不出现歧义,必须保证CEF是一个双射。 知道了字符编码表CEF是什么还不够,我们还需要知道它是怎么做的。即:如何将一个无限大的整数,一一映射为指定字宽的码元序列。 这个问题可以通过变长编码来解决:无论是UTF-8还是UTF-16,本质思想都是通过预留标记位来指示码元序列的长度,从而实现变长编码。 各个CEF的细节我建议参看维基百科UTF-8 UTF-16UTF-32 写的相当清楚,我就没必要在此再写一遍了。更深入学习方式就是直接阅读Unicode 9.0.0 Standard 举个例子: ## 字符编码方案 CES (Character Encoding Schema) Unicode encoding scheme: A specified byte serialization for a Unicode encoding form, including the specification of the handling of a byte order mark (BOM), if allowed. 字符编码方案是现代编码模型的第四层。简单说,字符编码方案 CES 等于 字符编码表CEF 加上字节序列化的方案。 通过字符编码表CEF,我们已经可以将字符转为码元序列。无论是哪种UTF-X的码元,都可以找到计算机中与之对应的整形存放。那么现在我们能说存储处理交换字符这个问题解决了吗?还不行。假设一个字符按照UTF16拆成了若干个码元组成的码元序列,因为每个码元都是一个unsigned short,实际上是两个字节。因此将码元序列化为字节序列的时候,就会遇到一些问题。大小端序问题:每个码元究竟是高位字节在前还是低位字节在前呢?字节序标记问题:另一个程序如何知道当文本是什么端序的呢?这些都是CEF需要操心的问题。 对于网络交换和本地处理,大小端序各有优劣。这个问题不属于本文范畴。字节序标记BOM (Byte Order Mark),则是放置于编码字节序列开始处的一段特殊字节序列,用于表示文本序列的大小端序。 对于这两个问题的不同答案,在3种CEF:UTF-8,UTF-16,UTF-32上。Unicode实际上定义了 7种 字符编码方案CES: UTF-8 UTF-16LE UTF-16BE UTF-16 UTF-32LE UTF-32BE UTF-32其中UTF-8因为已经采用字节作为码元了,所以实际上不存在字节序的问题。其他两种CES嘛,都有一个大端版本一个小端版本,还有一个随机应变大小端带BOM的版本。 下面给一个Python编码的小例子,将Emoji:'哭笑' 转换为各种CES。 这里也出现一个问题,历史上字符编码方案(Character Encoding Schema)曾经就是指UTF(Unicode Transformation Formats)。所以UTF-X到底是属于字符编码方案CES还是属于字符编码表CEF是一个模棱两可的问题。UTF-X可以同时指代字符编码表CEF或者字符编码方案CES。UTF-8问题还好,因为UTF-8的字节序列化方案太朴素了,以至于CES和CEF都没什么区别。但其他两种:UTF-16,UTF-32,就比较棘手了。当我们说UTF-16时,既可以指代UTF-16字符编码表,又可以指代UTF-16字符编码方案。所以当有人说“这个字符串是UTF-16编码的”时,鬼知道他到底说的到底是一个(UTF-16 encoding form的)码元序列还是(UTF-16 encoding schema 的)字节流。 简单的说,字符编码表CEF和字符编码方案CES区别如下:c ∈ CCS ---CEF--> Code Unit Sequencec ∈ CCS ---CES--> Byte Sequence字符编码表CEF将码位映射为码元序列,而字符编码方案CES将码位序列化为字节流。 我们通常所说的动词编码(Encode)就是指使用CES,将CCS中字符组成的字符串转变为字节序列。而解码(Decode)就是反过来,将 编码字节序列 通过CES的一一映射还原为CCS中字符的序列。 除了Unicode标准定义的七中CES,还有两种CES: UCS-2,UCS-4 。严格来说,UCS-2和UCS-4属于字符编码表CEF的层次,不过鉴于其朴素的序列化方案,也可以理解为CES。这两种CES的特点是采用定长编码,比如UCS-2直接把码位序列化为unsigned short。之前一直很流行,但当UCS中字符越来越多,超过65536个之后,UCS-2就GG了。至于UCS-4,基本和UTF-32差不多。虽说有生之年基本不可能看到UCS大小超出四字节的表示范围,但每个字符统一用4字节来存储这件事本身就很蠢了……。 当然除了UCS,其他字符集,例如US-ASCII,GBK,也会有自己的字符编码方案,只不过我们很少听说,一个很重要的原因是,这些字符集的编码方案太简单了,以至于CCS,CEF,CES三层直接合一了。例如US-ASCII的CES,因为ASCII就128个字符,只要直接把其码位转换成(char),就完成了编码。如此简单的编码,直接让CCS,CEF,CES三层合一。很多其他的字符集也与之类似。 传输编码语法(Transfer Encoding Syntax) 传输编码语法是现代编码模型的最顶层通过CES,我们已经可以将一个字符表示为一个字节序列。但是有时候,字节序列表示还不够。比如在HTTP协议中,在URL里,一些字符是不允许出现的。这时候就需要再次对字节流进行编码。 著名的Base64编码,就是把字节流映射成了一个由64个安全字符组成字符集所表示的字符流。从而使字节流能够安全地在Web中传输。不过这一块的内容已经离我们讨论的主题太远了。 知乎链接:https://www.zhihu.com/question/31833164/answer/115069547
DBLink可以调用远端函数。FDW可以下推聚合函数,其他函数不行。
不过可以在远端定义好使用那些函数的视图,来解决部分问题。