2013年1月

我们知道 Go 中的 Struct 可以像其它语言中的类一样,包含数据,同样的,它也可以通过定义方法来保存属性它自己的行为,将一个方法绑定、关联到一个 Struct 或者任何一个其它的类型中是非常简单的,它就像普通的函数定义一样,唯一的不同是你需要为其指定一个类型(type)。

下面这个示例是一个普通的函数定义,它将返回一个类型为 int 的值:

func myFunc() int {
    // code
}

上面定义的函数将不接收任何参数,返回一个 int 类型的值,如果我们要将其绑定或者说关联到一个Struct上面的话,只需要做很少的修改即可:

type myType struct {}
func (m myType) myFunc() int {
    // code
}

让我们现在来扩展一下前面一篇文章《Go 中的 Struct——无类实现面向对象编程》中的那个 Rectangle 示例, 我们为其添加一个名为 Area 的方法,用来计算长方形的面积:

package main
import "fmt"
type Rectangle struct {
    width, length int
}
func (r Rectangle) Area() int {
    return r.width * r.length
}
func main() {
    r := Rectangle{4,3}
    fmt.Println("Rectangle is:", r)
    fmt.Println("Rectangle area is:", r.Area())
}

输出为:

Rectangle is: {4 3}
Rectangle area is: 12

许多面向对象的程序开发语言都有一个类似 this 或者 self 的概念,用它来代表并访问当前的实例,Go没有这样的概念,如果要将某一个函数或者方法绑定到一个Struct中,直接将一个Struct的实例作为参数传递给该方法或者函数,在上面的示例中,r 就是这个传递给方法 Area 的 Rectangle实例,它会在每一次调用 r.Area() 方法时,指向 r 自身。

同样的,你还可以传递Struct的引用,但是这与直接传递实例没有差别,因为Go会自动为你进行转换:

package main
import "fmt"
type Rectangle struct {
    length, width int
}
func (r Rectangle) AreaByValue() int {
    return r.length * r.width
}
func (r *Rectangle) AreaByReference() int {
    return r.length * r.width
}
func main() {
    r := Rectangle{4,3}
    fmt.Println("Rectangle is: ", r)
    fmt.Println("Rectangle area is: ", r.AreaByValue())
    fmt.Println("Rectangle area is: ", r.AreaByReference())
    fmt.Println("Rectangle area is: ", (&r).AreaByValue())
    fmt.Println("Rectangle area is: ", (&r).AreaByReference())
}

输出为:

Rectangle is:  {4 3}
Rectangle area is:  12
Rectangle area is:  12
Rectangle area is:  12
Rectangle area is:  12

在上面的代码中,我们定义了两人个很相似的方法,一个传递实例,一个传递实例的引用,我们同时又使用了实例分别调用它们,再使用了实例的地址调用它们,可以看到,四种方法的结果都是一样的。

下面我们再“绑定”一个名为 Perimeter 的方法来设置长方形的周长:

package main
import "fmt"
type Rectangle struct {
    width, length int
}
func (r Rectangle) Area() int {
    return r.width * r.length
}
func (r Rectangle) Perimeter() int {
    return 2 * (r.length + r.width)
}
func main() {
    r := Rectangle{4,3}
    fmt.Println("Rectangle is:", r)
    fmt.Println("Rectangle area is:", r.Area())
    fmt.Println("Rectangle perimeter is:", r.Perimeter())
}

输出结果为:

Rectangle is: {4 3}
Rectangle area is: 12
Rectangle perimeter is: 14

现在你可能会想到了,既然为自定义的Struct添加方法绑定,扩展它的行为这么简单,那是否我也可以很容易扩展以有的Struct的方法呢?看下面的示例:

package main
import "fmt"
import "time"
func (t time.Time) first3Chars() string {
    return t.Weekday().String()[0:3]
}
func main() {
    t := time.Time{}
    fmt.Println("First 3 Chars is: ", t.first3Chars())
}

输出为:

./structsmethods4.go:4: cannot define new methods on non-local type time.Time
./structsmethods4.go:9: t.first3Chars undefined (type time.Time has no field or method first3Chars)

你只能将方法或者函数绑定到相同的包内的Struct中,如果你真的需要扩展其它包,那再想想还有什么办法?其实很简单,那就是使用Go Struct中的匿名字段

package main
import (
    "fmt"
    "time"
)
type myTime struct {
    time.Time // 匿名字段
}
func (t myTime) first3Chars() string {
    return t.Weekday().String()[0:3]
}
func main() {
    m := myTime{}
    fmt.Println("Full Weekday:", m.Weekday().String())
    fmt.Println("First 3 chars:", m.first3Chars())
}

输出结果:

Full Weekday: Monday
First 3 chars: Mon

匿名字段中的方法

在《Go Struct中的匿名字段》这篇文章里面我讲到过了对匿名字段的方法,当 time.Time 成为了 myTime 的一个匿名字段之后,我们就能通过 myTime 访问 Time 所的方法,我们同样可以使用 myTime.String() 将自己转换成为一个字符串,让我们再来做看一看前面的示例。

在下面这个示例中,我们返回到Kitchen与House的问题上面来,我们知道了如何访问匿名字段的数据,那么也应该知道如何访问匿名字段的方法了,他们是一样的:

package main
import "fmt"
type Kitchen struct {
    numOfForks int
    numOfKnives int
}
func (k Kitchen) totalForksAndKnives() int {
    return k.numOfForks + k.numOfKnives
}
type House struct {
    Kitchen
}
func main() {
    h := House{Kitchen{4,4}}
    fmt.Println("Sum of forks and knives in house:", h.totalForksAndKnives())
}

输出结果为:

Sum of forks and knives in house: 8

Go 允许你定义含有无变量名的字段的Struct,这些无名称的字段称之为匿名字段,让我们通过一些示例来了解它们到底是什么,以及它们为什么很有用。

在下面的示例中,我们定义一个名为 Kitchen 的Struct,它仅仅只包含一个名为 numOfPlates 的 int类型的字段用来保存盘子的个数,我还定义了另一个Struct名为 House ,它包含了一个 Kitchen 的实例,但是我没有为其指定字段名,所以,它是一个匿名字段。

package main
import "fmt"
type Kitchen struct {
    numOfPlates int
}
type House struct {
    Kitchen // 匿名字段
    numOfRooms int
}
func main() {
    h := House{Kitchen{10}, 3} // 初始化需要使用Struct名
    fmt.Println("房屋 h 有", h.numOfRooms, "间房") // numOfRooms 是 House 的一个字段
    fmt.Println("房屋 h 有", h.numOfPlates, "个盘子") // numOfPlates 是Kitchen提供的字段,而Kitchen又是House的一个匿名字段,所以这里可以访问到它
    fmt.Println("这间房屋的厨房有:", h.Kitchen) // 我们可以通过Struct的名称访问整个匿名Struct的内容
}

输出结果为:

房屋 h 有 3 间房
房屋 h 有 10 个盘子
这间房屋的厨房有: {10}

首先我们需要注意到的事情是,当我们定义了 Kitchen 为House的一个匿名字段之后,我们将可以直接通过Kitchen中的字段名称访问Kitchen中字段的值,这同样也“成了”House的字段,但是如果像在Java这样的语言中,我们做一下对比,它需要像下面这样的去实现同样的功能:

public class Kitchen {
    public int numOfPlates;
}

public class House {
    public Kitchen kitchen;
}

public static void main(String[] args) {
    House h = new House();
    h.kitchen.numOfPlates = 10; // 引用自字段的子字段
}

其次我们需要知道,匿名字段的完整的实例数据是可以被访问到的,需要通过它的Struct名称,在本示例中,通过 h.Kitchen 来访问获得,所以,如果你想打印出 Kitchen 中的 Plate 数量的话中,可以这样:

fmt.Println(h.Kitchen.numofPlates)

最后,要注意到,初始化的时候,匿名字段的Struct名称是不允许省略的,它必须像这样:h := House{Kitchen{10}, 3}*,而下面这样的写法都是错误的:*h := House{{10}, 3} 或者 h := House{10,3}。

匿名字段发生命名冲突

很有可能两个不同的Struct具有同样名称的字段,而一个Struct又是另一个Struct的匿名字段,这就产生的命名冲突的问题,当这个问题出现的时候,外层的Struct中的字段可以像以前一样直接被访问到,但是其匿名字段就需要通过Struct名称访问了。

在下面的示例中,我们的 House 与 Kitchen 同时具有 numOfLamps 字段,但是House是外层的struct,它的 numOfLamps* 字段将隐藏掉Kitchen的 *numOfLamps*,如果你还是需要访问Kitchen的numOfLamps值,那么需要通过引用它的Struct名:*h.Kitchen.numOfLamps :

package main
import "fmt"
type Kitchen struct {
    numOfLamps int
}
type House struct {
    Kitchen
    numOfLamps int
}
func main() {
    h := House{Kitchen{2}, 10} // Kitchen 有2个Lamps,House有10个
    fmt.Println("House有", h.numOfLamps, "个Lamps")
    fmt.Println("Kitchen有", h.Kitchen.numOfLamps, "个Lamps")
}

输出为:

House有 10 个Lamps
Kitchen有 2 个Lamps

所以从上面可以看到,对于不同层级的相同名称的字段,Go制定了相应的规则,但是对于同一层级的相同名称的字段,则需要我们自己去解决,在下面的示例中,Kitchen与Bedroom都具有 numOfLamps 字段,并且它们又都是House的匿名字段,现在,如果我们引用 House.numOfLamps,Go编译器就会不知道你到底是要使用哪一个 numOfLamps值:

package main
import "fmt"
type Kitchen struct {
    numOfLamps int;
}
type Bedroom struct {
    numOfLamps int;
}
type House struct {
    Kitchen
    Bedroom
}
func main() {
    h := House{Kitchen{2}, Bedroom{3}} // Kitchen 2, Bedroom 3
    fmt.Println("Ambiguous number of lamps:", h.numOfLamps) // 这会出错
}

输出为:

./anonymousfields3.go:15: ambiguous selector h.numOfLamps

因为Go不知道你到底是要 Bedroom 的还是 Kitchen 的 numOfLamps,所以,它直接就提供值了,结果就是返回 h.numOfLamps 有歧义。

解决这个问题的办法是通过匿名字段的Struct名称访问其值:

fmt.Println("House中Lamps的数量为:", h.Kitchen.numOfLamps + h.Bedroom.numOfLamps)

输出为:

House中Lamps的数量为:5

星期五, 12/28/2012 - 16:22 — matrixstack

Go是非面向对象的,而是面向过程的,但是在当今程序开发的世界里,使用最为广泛的还是面向对象编程,这或许会让你感觉Go作为一个新生的语言最应该有的一面却没有被支持,但是其实它只是用了自己的方法,在本文里,我将介绍在Go里面,如何实现面向对象(Go提供了面向对象所提供的一切,或许提供得更多)。

如果你一直都是一个“类”用户,那么转到Go的“面向对象”上来,可能还需要一些观念上的转变,这或许很难,但是一旦你转变过来了,你就会发现,Go的方式有多么的强大了。

在了解Go的面向对象之前,一定要在心里面知道:Go是没有类的,这是你进入Go的面向对象世界的第一步,也是最重要的一步;另一个你知道知道的是:Go不是面向对象的,只是他的实现类似于面向对象,我们来看一个简单的示例,它能说明一切:

Java代码:

class House {
    public String getHouseName() { // 方法定义在类里面
        // 实现
    }
}

Go 代码:

type House struct { }
func (h House) GetHouseName() string {} //方法被定义在 Struct 外,但是却作用于 House

现在让我们来学习一下如何创建 Struct 以及如何通过它实现“面向对象”,一个 Struct 通过关键字 type 与 struct 定义:

type my_struct_name struct { }
type Rectangle struct { }
type Vehicle struct { }
type VehicleCar struct { }

上面的所有定义都是合法的,但是下面这样是不合法的:

type Hash# struct { } // 名称不允许包含特殊字符
type 0Struct struct { } // 名称不允许以数字开始

当然了,Struct 还能存储其它数据,所以,在这方面它类似于 Class,可以用来定义真实世界中存在的实体:

type mystruct struct {
    i int
    j int
    s string
}

type Rectangle struct {
    length, width int //你可以在一行中通过逗号分割的方式创建多个同类型的项目
    area float64
}

下面这个示例展示了一个 Struct 的实际应用:

package main
import "fmt"
// 定义 Rectangle 结构
type Rectangle struct {
    length, width int
}
func main() {
    r := Rectangle{}
    fmt.Println("默认的长方形是:", r) // 打印出默认的 Rectangle 的归零值
}

输出为:

默认的长方形是: {0 0}

这里有一个重要的需要注意的事情是:struct中的变量的值都是归零的,也就是说,int 类型的值将为 0,string 类型的值将为空字符串等等,所以,每一个你自己定义的Struct 也都是有一个属于其自己的归堆值的。下面这个示例中,我将展示另一种方式初始化 Struct——在创建它的时候就指定初始值:

package main
import "fmt"
type Rectangle struct {
    length, width int
    name string
}
func main() {
    r1 := Rectangle{2, 1, "First Rectangle"}
    fmt.Println("第一个长方形 r1 为:", r1)
    r2 := Rectangle{width: 3, name: "Second Rectangle", length: 4}
    fmt.Println("第二个长方形 r2 为:", r2)
    pr := new (Rectangle) // 获取一个指向一个新的长方形的指针
    (*pr).width = 6 // 设置 pr 的宽为 6
    pr.length = 8 // 与上面是同样的效果,看这里是没有 -> 符号的
    pr.name = "指针长方形"
    fmt.Println("指针长方形是:", pr)
}

输出为:

第一个长方形 r1 为: {2 1 First Rectangle}
第二个长方形 r2 为: {4 3 Second Rectangle}
指针长方形是: &{8 6 指针长方形}

这里需要注意的事情是:

  • 你可以只将值按 struct 定义时的顺序赋给它的成员,但是需要放在大括号里面,并且每一个值需要使用逗号分开;
  • 如果你给定值的时候还提供了名称,那么就不受顺序的限制;
  • 你可以通过 new 获取到一个新创建的 struct 的指针;
  • 这样的一个指针可以不需要 *操作符;
  • Go默认提供根据Struct的值转换成为字符打印的功能

封装以及 Struct 及其变量的访问

其它的编程语言使用一些特别的关键字(比如 public、private、package‘protected等)定义类中不同的属性与变量的可访问性,当我在使用Go之前,我认为所有的这些都是言存在的,但是之后,我发现,原来实现变量与属性的访问控制可以如此简单:

type notExported struct { // 以小写字母开头命名的struct只能在同一个包内调用,我们称这为不导出
}
type Exported { // 大写开头则可导出,也就是任何包都可以访问
    notExportedVariable int // 小写字母开头不允许包外访问
    ExportedVariable int // 大写字母开头则允许包外访问
    s string // 不导出
    S string // 导出
}

所以,你应该明白,在Go里面命名都应该是统一规定的,它要求你使用驼峰式的命名,是否使用大小写开头则要看是否需要导出。

for 声明是 Go 中唯一的一个循环声明,普通的用法如下:

for "initialization statements"; "bool expression that has to evaluate to true"; "statements run prior to every loop except the first" {
    // Code to be executed if the boolean expression evaluates to true
}

翻译过来为:

for "初始化声明"; "条件判断,为真是循环才执行"; "除第一外每一次循环都会执行的声明" {
    // 循环主体
}

上面的三个部分中,任何一个部分都是可以不写的,但是即使不写,那个属于它的结束符的分号却还是必须要写,除非三个声明都没有的时候,所有的分号都可以一并省略,请看下面的示例:

package main
import "fmt"
func main() {
    // 初始化i为0;每一次循环之后检查i是否小于5;让i加1
    for i := 0; i < 5; i++ {
        fmt.Println("i现在的值为:", i)
    }
}

执行输出结果为:

cox@Cox:~/workspace/go/src/gotutorial$ go run forloop1.go
i现在的值为: 0
i现在的值为: 1
i现在的值为: 2
i现在的值为: 3
i现在的值为: 4

从上面的结果可以看到,我们在定义 i 的初始值为0之后,在循环主体第一次执行前,i++是没有运行的,所以我们来能取得它的初始值 0。下面这些循环体你都可以在自己的示例中去运行:

// 没有判断声明,循环将永远不停的执行
for i := 0; ; i++ {
    fmt.Println("i现在的值为:", i)
}
// 因为 i 的值不会变化,而i的初始值小于3,所以,它将永远小于3,那么循环加永远运行
for i := 0; i < 3; {
    fmt.Println("i现在的值为", i)
}
// 虽然我们没有在 *for* 中声明初始值以及变量的变量方式,但是我们把初始化移到for循环外面,然后把初始变量的变化声明移动到循环里面也是可以的,下面代码的输出将为:
// s的值为:
// s的值为: a
// s的值为: aa
// s的值为: aaa
// s的值为: aaaa
s := ""
for ; s != "aaaaa"; {
    fmt.Println("s的值为:", s)
    s = s + "a"
}

在 Go 的 for 循环里面,你可以同时初始化多个变量,多个条件判断或者同时更新多个值,请看下面的示例:

package main
import "fmt"
func main() {
    for i, j, s := 0,5,"a"; i < 3 && j < 100 && s != "aaaaa"; i, j, s = i+1, j+1, s + "a" {
        fmt.Printf("i=%d, j=%d, s=%sn", i, j, s)
    }
}

输出:

i=0, j=5, s=a
i=1, j=6, s=aa
i=2, j=7, s=aaa

第三次循环之后,虽然 i < 100 与 s != "aaaaa" 都成立,但是 i < 3 已经不成立,所以循终止。

break 关键字

break 的作用是让循环在它所出现的地方终止,然后执行该循环后面的下一行代码或指令,如下示例:

package main
import "fmt"
func main() {
    i := 0
    for {
        if i >= 3 { break }
        fmt.Println("当前i的值为:", i)
        i++
    }
    fmt.Println("for后的第一个声明")
}

输出为:

当前i的值为: 0
当前i的值为: 1
当前i的值为: 2
for后的第一个声明

continue 关键字

continue 中止当前循环,然后回到循环的起始进行下一步循环,如下示例:

package main
import "fmt"
func main() {
    for i := 0; i < 11; i++ {
        if i%2 == 0 {
            continue //我不输入偶数
        }
        fmt.Println("当前i的值为:", i)
    }
}

输出为:

当前i的值为: 1
当前i的值为: 3
当前i的值为: 5
当前i的值为: 7
当前i的值为: 9

range 关键字

range 关键字让你可以遍历数据/Map,为了便于理解,你可以把它理解为“每一个XX的索引”,当你使用它对数组或者片段遍历时,每一次返回它们的索引,对于MAP遍历时,每一次返回它的键,如下示例:

package main
import "fmt"
func main() {
    // 对于数组,range 返回数据的索引
    a := [...]string{"a", "b", "c", "d"}
    for i := range a {
        fmt.Println("数组的第", i+1, "个值为:", a[i])
    }

    // 对于 map,返回每一个键
    capitals := map[string]string {"France":"Paris", "Italy":"Rome", "China":"Beijing" }
    for key := range capitals {
        fmt.Println("Capital of", key, "is", capitals[key])
    }
}

输出为:

数组的第 1 个值为: a
数组的第 2 个值为: b
数组的第 3 个值为: c
数组的第 4 个值为: d
Capital of Italy is Rome
Capital of France is Paris
Capital of China is Beijing

同时,range还可以同时返回两人个值,第一个值为索引或者键,第二个值为数组或者Map的值本身,比如上面的两人个循环我们可以改写成为下面这样也可以得到同样一输出:

package main
import "fmt"
func main() {
    // 对于数组,range 返回数据的索引
    a := [...]string{"a", "b", "c", "d"}
    for i,v := range a {
        fmt.Println("数组的第", i+1, "个值为:", v)
    }

    // 对于 map,返回每一个键
    capitals := map[string]string {"France":"Paris", "Italy":"Rome", "China":"Beijing" }
    for key,value := range capitals {
        fmt.Println("Capital of", key, "is", value)
    }
}

程序需要根据不同的条件进行不同的处理, 我们程序不可能只按着某一个顺序一路执行下去,比如我们经常会遇到的这样一种情况:如果某一个文件存在,则将新内容更新至已有文件,如果文件不存在,则先创建文件,这个时候我们就可以使用if - else 控制结构了,在Go中,该控制结构如下:

if some_boolean_expression {
    // 如果 some_boolean_expression 为 true,则会执行这里
} else if alternate_boolean_expression {
    // 如果 some_boolean_expression 为 false 并且 alternate_boolean_expression为 true,则执行这里
} else {
    // 如果上面的所有判断都为 false ,才能执行这里
}

注意事项 :

  • 在上面的结构说明,some_boolean_expression 以及 alternate_boolean_expression 的计算结果只能是 true 或者 false 里面的一个,不能为其它的值;
  • 对于判断声明,括号不是必须的,你可以为其添加一个括号,但是这是没必要的;
  • 与其它语言不同的是,Go要求左花括号必须与 if 或者 else 处于同一行内,就会报错,详情看写 GO 程序中一些常见的语法和其它错误
  • int 类型的数据不能作为判断声明的结果被 if-else使用。

下面这些是条件判断的比较操作符:

  • == : 等于
  • != : 不等于
  • < : 小于
  • <= : 小于或等于
  • > : 大于
  • >= : 大于或等于
  • && : 和(并且/与)
  • || : 或

下面是一个最很简单的示例:

package main
import "fmt"
func main() {
    if true {
        fmt.Println("这里会执行")
    }
    if false {
        fmt.Println("这里不会执行")
    }
}

输出很很简单,就是打印出“这里会执行”这五个字符,下面这个示例则使用了一些判断操作符:

package main
import "fmt"
func main() {
    a, b := 4, 5
    if a < b {
        fmt.Println("a < b")
    } else if a > b {
        fmt.Println("a > b")
    } else {
        fmt.Println("a = b")
    }
}

输入结果为 a < b,对于有其它语言编程经验的人来说,下面这些错误可能会经常出现:

// 非布尔值 5
if 5 {}
// 非布尔值 s
var s string
if s {}
// 非布尔值 e
var e os.Error
if e {}
// if 声明与左大括号不在一行内
if true
{}
// else 与 右大括号不在一行
if true {
}
else {
}

本文是 「Golang.org」的原文档翻译,出于本人有限的英语水平,如有不当,还请指正。

Go 是一个全新的语言,当然,它也从很多以存在多年的优秀语言中吸取了很多优秀的特性,但是因为其自身的特殊性,使得 Go 程序与其它相关的语言所开发的出来的程序有很大的不同,直接将C++或者Java程序翻译为Go程序并不会让你得到一个满意的结果——Java程序永远都只是Java程序,它成不了Go程序。另一方面,从Go的角度去想一个问题,你会得到一个可以成功解决它但是和其它语言完全不一样的方式。换句话说,想写出漂亮的Go程序,你必需要了解它的特性和风格,甚至是已有的约定,比如命名规范、代码格式化结构等,了解这些,可以让你的程序更简单地其它人看懂。

本文档给出了一些能让你写出更加干净的符合Go语言习惯的代码,但是建议你在阅读本文之前先阅读一下Language Specification、Tour of Go以及「如何写Go程序(How to Write Go Code)」.

示例

Go Package Sources 并不只包含了它的核心库,同时它还包含了很多使用该语言写的示例程序,如果你有关于如何使用Go解决问题或者在Go应该如何实现的相关问题,这些示例代码或话会给你答案,它还能告诉你,你应该将自己的想法以怎样的一种方式转换成Go程序。

格式化

格式化应该是最具有争议但是也是最重要的一个Go语言的特性,每一个开发者可以有属性自己的书写格式,但是所有人都使用同一种格式看起来肯定是最好的,这使得我们不再需要花费太多时间放在了解别人的书写风格上。

在Go中,不同于其它语言,它让计算机程序来管理着代码的格式化,gofmt 命令(也可以使用 go fmt)可以读取Go代码文件,并将它们以标准格式对其进行格式化,包括它们的缩进、空格等,请看下面这个示例:

type T struct {
    name string // name of the object
    value int // its value
}

gofmt 会将其格式化为:

type T struct {
    name    string  // name of the object
    value   int     // its value
}

所有Go标准库中的代码都已经通过gofmt进行了格式化。

比如下面这些Go中最常见的会使用到的格式化约定:

  • 缩进:Go使用制表符(Tabs)进行缩进,而不是很多语言中都推荐的空格,gofmt默认会使用制表符进行缩进,你应该仅仅在需要空格的地使用空格。
  • 行长度:Go没有行代度限制,所以,你完全不用担心这个问题,但是如果你感觉某一个行实在是太长了,那你可以将其截断,然后使用一些制表符让它看起来和它原本应该属于的那一行开始于同一个地方。
  • 括号:Go只有很少的一些地方需要括号,结构控制(比如if, for, switch等的条件判断)都是不需要括号的,操作符的优先级层次更短更清晰,所以,x << 1 + y < 6,就可以很明白的知道这是什么了。

注释

Go同时对G风格的块注释 /* ......*/ 和 C++风格的行注释 // .....提供支持,行注释是标准的,而块注释一般只建议使用的对包的注释上或者使用在需要禁用(但不是删除)一大段代码的功能上。

godoc程序(同时也是文档服务器)会从Go代码中提供包的说明文档,Comments that appear before top-level declarations, with no intervening newlines, are extracted alog with the declaration to serve as explanatory text for the item. The nature and style of these comments determines the quality of the documentation godoc produces.

任何一个包都应该包含一个包注释——一个用来说明包功能以及使用方法等信息的块注释,对于被分割为多个文件的包,包注释只需要出现在某中的任何一个文件中即可,godoc会将它作为所有同一个包的说明文档。The package comment should introduce the package and provide information relevant to the package as a whole. It will appear first on the godocpage and should set up the detailed documentation that follows.

/*
    Package regexp implements a simple library for
    regular expressions.

    The syntax of the regular expressions accepted is:

    regecxp:
        concatenation { '|' concatenation }
    concatenation:
        { closure }
    closure:
        term [ '*' | '+' | '?' ]
    term:
        '^'
        '$'
        '.'
        character
        '[' [ '^' ] character-ranges ']'
        '(' regexp ')'
*/
package regexp

如果某个包是一个十分简单的包,那么这个包的注释可以很简单:

// Package path implements utility routines for
// manipulating slash-separated filename paths.

注释不需要进行特殊的格式化,文档创建工作不能确定使用的是否是固定宽度的字体,所以,你不需要使用空格去对注释进行格式化——godoc像gofmt一样,它会帮助你完成这些格式化。注释应该仅仅保是纯文本的字符串,所以,HTML或者其它的标记方式(比如 THIS)都是不应该被使用的,godoc只会照字面的意思去解决,所以,请尽可能让你的注释内容就是你想表达的内容的文本——使用正确的拼写,合理的分段,长句进行合理的截段等。

在包内,任何一个仅跟着一个声明的注释都被当作该声明的注释文档,任何一个可导出的(大写开头的命名)的声明都应该具有一个说明注释,而这个注释文档最好也有遵循一定的约定,它的最第一句话应该是一个以该声明名称开始的用来概括该声明作用的句子。

// Compile parses a regular expression and returns, if successful, a Regexp
// object that can be used to match against text.
func Compile(str string) (regexp *Regexp, err error) {...}

Go 的声明语法允许分组声明,这使得一个单一的文档注释可以说明一组相关的常量或者变量,整个声明可以像下面这样来做(这是懒人做法啊):

// Error codes returned by failures to parse an expression.
var (
    ErrInternal     = errors.New("regexp: internal error")
    ErrUnmatchedLpar= errors.New("regexp: unmatched '('")
    ErrUnmatchedRpar= errors.New("regexp: unmatched ')'")
    ...
)

即使是私有名称,分组也是可以指明它们之间的关系的,比如下面这一组私有变量由一个互斥锁保护:

var (
    countLock   sync.Mutex
    inputCount  unit32
    outputCount unit32
    errorCount  unit32
)

名称

与其它语言一样,名称在Go中也是十分重要的,在某些环境下,一个名称甚至还需要其特有的语义——对于一个实例,它的方法或变量是否能被外部调用和访问取决于那个方法或变量的名称是否以大写字母开始,PublicItem这样的就是可以被外部访问的,而 privateItem则只能放在家里自己使用,也就是说:大写开始表示可以被外部访问,也就是可以被导出,而小写开头则只能是私有的。

包名称

当一个包被导入之后,包名称就成为一个访问该包资源的入口,比如,当我们:

import "bytes"

之后,我们就可以请求 bytes.Buffer了,一个简洁、明了令人回味的包名称可以帮助他人在使用该包的过程中更加方便快乐。我们约定包名称应该是一个“仅仅小写字母的单个字汇字符串”,我们不必要使用下划线或者驼峰式的写出一个完整的长的名称,你要想想,假如你自己在使用别人的某一个包时,需要像fooBarFooBar.FooBar这样请求它的资源,你会是一种什么样的感觉,我们只需要简单的foo.Bar即可。你不必要担心你所选择包名可能与其它已有的包名相同,因为Go允许在导入一个包时为其指定一个本地使用的别名。

另一个约定是“包名是该访问该包所有资源的基础入口”,比如 src/pkg/encoding/base64 包应该以encoding/base64导入,它的名称为 base64,而不是 encoding_base64或者encodingBase64。

包导入工具将使用名称来引用它的内容(import . 只在某些特殊用途的时候使用,如非必要,请尽可能的不去使用),所以,包中可导出的内容就可以以最直接方式访问到,对于实例,如bufio包中的Reader,就可以直接使用 bufio.Reader访问到,而不是 BufReader或者其它的名称,因为用户看前者会更直观。由于导入的实体总是以其完整的位置访问,所以 bufio.Reader不会与 io.Reader 混淆。相似的,用来创建新的 ring.Ring实例的构建函数,通常我们可能会将其命名为 NewRing,但是,因为 Ring 是 ring 导出的唯一一个类型,所以,我们也只能创建ring.Ring 这一种类型的数据,所以,直接将该构建函数取名为 New即可。

另一个*简化名称*的示例是 once.Do,我们不将其命名为 once.DoOrWaitUntilDown,长的名称在绝大多数时候并不会让我们更容易理解其所指代的意思,要让使用者更方便的调用也让他们更好的了解其使用方法或者意思,那好的办法是选择简洁的名称,并为其写上足够说明其意思和使用方法的注释。

Getters

Go 不会自动的提供 getters 和 setters 函数,但是这种事情你完全可以自己去完成,但是你也应该在适当的时候这么做,但是,你不需要把 Get 写进你所定义的方法名称中去,比如你现在有一个字段 owner(注意这是小写字母开头的,所以不导出,外部也部无法直接调用),它的 getter 名称应该称之为 Owner而不是GetOwner,它成为了获取 owner 字段数据的钩子,而 setter 函数则应该称之为 SetOwner:

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}

接口名称

Go 约定接口名称为方法名称后面加上 er:Reader、Writer、Formatter等,Go中有许多这样名称的标准接口,也有很多方法去实现了这些接口,它们这些方法的名称都是像 Read、Write、Close、Flush、String等这样的,为了避免造成混乱,在你自己的程序中尽可能的不要去使用上面这些标准库中提供了的方法或者接口名称。

相反的,如果你的程序实现了一个与某一个已经被另一个大家所熟知的程序实现过的接口,那么你最好是使用该程序所使用的方法名称,一个将你的数据转换为字符的方法,不要叫作ToString,而应该直接叫作 String。

多词汇名称

最后,Go约定使用“驼峰式”的命名,而不使用下划线的方式,比如MixedCaps或者mixedCaps而不是mixed_caps。

分号

与 C 一样,Go使用分号“;”来终止一条语句,而与C不一样的是这些分号一般是不会出现在的源代码中的,Go使用一种简单的规则在其扫描代码的过程中合适的插入这些分号,所以,你在写代码的时候完全不需要将其明显的写入进源代码中。

Go所约定的规则是这样的:如果一个新行的最后面是一个类型识别码(一般都是 int 或者 float64这样的),一个常量(比如数字或者字符串)或者下面这些关键字或者符号中的任何一个:

break continue fallthrough return ++ -- ) }

Go 会自动在其后面加了分号,上面所说的这些可以简单的描述为:“如果一个新行前面的Token可以终止一个声明,则在该Token后插入分号”。

结束大括号前的分号也是可以省略的,比如:

go func() { for { dst <- <-src } }()

就不需要分号,Go程序只需要在诸如循环、分隔初始值、条件或者延续元素的情况,才需要明显地使用分号,当然,你可以将多行地声明写进同一行中,这个时候每一个声明后就需要使用分号来区分。

有一点你需要特别注意的,你不应该把控制结构(if、for、switch或者select)的左大括号写在下一行,如果你这么做了,那么Go会在这些声明后面加上一个分号,这肯定不是你想看到的。

if i < f() {
    g()
}

而下面这样就是错误的:

if i < f()  // wrong!
{
    go ()   // wrong!
}

控制结构

Go 的控制结构与 C 十分类似,但是有一个重要的不同在于Go没有 do 或者 while 循环,仅仅使用 for 来做这些基本工作,switch 有更多的灵活性以及if和switch可以接受一个初始值声明,它还有一个新的控制结构 select,语法也略有不同:没有括号,主体必须分隔。

if

在Go中一个简单的if语句看起来像下面这样的:

if x > 0 {
    return y
}

强制性的要求 if 声明必须被分隔为多行,由于是在它的主体中包含有如 return或者break等控制语句的情况更为适用。

由于 if 和 switch 支持初始化声明,所以,我们可以在其条件判断前为该条 if 语句初始化一个本地变量:

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

在Go标准库中,如果一个 if 语句不接会导致程序继续执行其下一条语句时,我们可以不需要使用 else ,这通常的都是在if主体中存在 return、break、continue或者goto等等这些能控制代码结构的关键字。

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.close()
    return err
}
codeUsing(f)

在上面的代码中,我们首先打开一个文件,如果没有错误存在,程序会继续往下执行,如果再没有错误,则会执行 codeUsing(f)函数,但是如果有错误出现,那么在if执行之后就会跳出整个程序。

声明重载(Redeclaration)

在上面的示例中,你应该看到,我们的一个操作符 := 将 os.Open 返回的值同时声明给了两人个变量:f和err:

f, err := os.Open(name)

接着:

d, err := f.Stat()

看到,err 在前面声明之后,我们接着又重新声明了它,这表明,在第一个声明中,创建了err并为其赋了值,而在后面一次声明中,仅仅只是为其赋了新值。

在一个为变量 v 赋值的 := 的声明中,那么:

  • 如果在该声明同样的作用域内该声明之前已经存在了对该变量的声明,那么本声明直接为其赋值
  • 没有出现上面的情况,本声明将先创建该变量之后再为其赋值

for

Go的for循环有点像C的for循环——但是是不一样的,它有三种不同的格式:

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

更短的声明让我们更容易给循环一个索引变量:

sum := 0
for i := o; i < 10; i+ {
    sum += 1
}

如果你在遍历一个数组、片(Slice)、字符串或者Map,或者从一个 channel 中读取数据,range 可以被用来控制该循环:

for key, value := range oldMap {
    newMap[key] = value
}

如果你仅仅只需要循环过程中 range 产生的第一个值(range 产生两人个值 key, value),那么直接丢掉第二值即可:

for key := range m {
    if expired(k) {
        delete(m, key)
    }
}

但是如果你仅仅只想要其产生的第二个值,那么需要使用一个“空定义(blank identifier)”作为占位符,它是一个下划线:“_”:

sum := 0
for _, value := range array {
    sum += value
}

这是因为,如果你没有那个占位符,那么 range 还是会首先把它产生的第一个值赋值给 value,而有了那个点位符之后,range将key赋值给,之后会直接把该值丢掉,然后range就赋值其第二值 value给value了。

对于字符串,range能做更多的事情,breaking out individual Unicode characters by parsing the UTF-8. Erroneous encodings consume one byte and produce the replacement rune U+FFFD. The loop

func main() {
        for pos, char := range "编程开发" {
                fmt.Printf(" aracter %c starts at byte position %dn", char, pos)
        }
}

将会打印出:

aracter 编 starts at byte position 0
aracter 程 starts at byte position 3
aracter 开 starts at byte position 6
aracter 发 starts at byte position 9

最后,Go没有逗号操作符,而 ++ 与 -- 是声明而不是表达式,如果你想在一个for循环中使用多个变量,你需要使用 Parallel Assignment。

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

switch

Go 中的 switch 比C中的更加通用,它的表达式并不局限在常量或者数字中,它的 case 是从上到下进行匹配,直到找到一个为 true 的 case,如果你的 if-else-if-else-... 有点儿太多了,那么就使用 switch吧。

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
`   return 0

Go 中没有自动向下执行(Fall Through)机制,所以,你不能写成这个样子:

case i < 0:
case i > 10:
    doSomethingWithI(i)

但是Go可以将多个条件使用逗号分开,所以上面这样在其它语言里面常见的写法在Go你应该写成:

case i < 0, i > 10:
    doSomethingWithI(i)

Switch还可以被用来发现某一个接口变量的动态类型,像下面这样的去实现:

switch t := interfaceValue.(type) {
default:
    fmt.Printf("unexpected type %T", t)
case bool:
    fmt.Printf("boolean %tn", t)
case int:
    fmt.Printf("integer %dn", t)
case *bool:
    fmt.Printf("pointer to boolean %tn", *t)
case *int:
    fmt.Printf("pointer to integer %dn", *t)
}

函数(Functions)

多值返回 (Multiple return values)

Go函数和其它语言最不一样的特性就是它可以在一个函数中同时返回多个值,这个特性能改进很多在C里面要通过各种各样的特殊办法实现的功能,而这个特性你在本文档前面也已经见到过了:

f, err := os.Open(name)

同样的,在os包中还定义的很多比如 Write方法:

func (file *File) Write(b []byte) (n int, err error)(

就像它的文档所说的,该方法返回写入的总字节数,并且当 n != len(b) 的时候返回一个 non-nil 的 error,这种写法在Go很通用,你可以在专门的章节中看到更多的关于 error 处理的示例。

类似的这种方法省却了一个指针的使用,而且它看起来更加符合我们的普通常人的理解(需要返回什么东西就直接返回就是),下面这个示例演示了一个能同时返回两个值的函数,它先从一个字节数组(byte array)中找到一个数字,然后返回下一个为数字的元素的位置:

func nextInt(b [byte], i int) (int, int) {
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

然后你可以像下面这样的去调用它:

for i := 0; i < len(a); {
    x, i = nextInt(a, i)
    fmt.Println(x)
}

带名称的结果参数

Go 函数中的返回结果参数可以被赋予一个名称,它可以像其它的变量一样的在函数中使用,命名之后,当函数执行时,它会被赋予一个其所属的数据类型的0值,而当函数执行一个 return声明之后(不需要带任何参数的声明),该命名了的返回参数当前的值就会被返回。

Go 不强制你必须为返回参数命名,但是这样做可以使你的代码更短——它们被文档化了。如果我们将上面的函数进行命名,那么:

func nextInt(b []byte, pos int) ( value, nextPos int) { ... }

因为命名了的返回结果会在函数执行时被初始化了,并且和一个未经修饰的 return 绑定,所以它能使唤代码更清晰,下面这是一个 io.ReadFull的一个版本:

func ReadFull(r Reader, buf []byte) ( n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

延迟(Defer)

Go的defer声明一个定时运行的函数呼叫(deferred function),这样的函数会在函数终止前立马执行,这种方式在其它方法里面不太常见,但是它却非常有用,比如我们有一个函数要打开一个文件,但是不管这个函数是否执行成功,我们都应该在不再需要这个文件的时候关闭这个文件,这个时候我们就可以在成功打开文件之后,立马定时关闭该文件,当函数再往下执行时,不管是出现什么错误或者是成功执行,文件都会被正确关闭。

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.close() // f.close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err // f will be closed if we return here
        }
    }
    return string(result), nil // f will be closed if we return here
}

延迟请求一个函数的执行至少有两人个好处:首先,它能让你永远不会因为某些错误(这实在是太容易发生了)而关闭文件;其次,关闭与打开文件的代码可以被写在一起,这样后面的代码完全可以放开安排自己的代码结构了(肯定啊,不需要在每一个 return 前面关闭一次文件了,我在任何一个地方可以 return ,文件会自动的关闭)。

传递给被延迟请求的函数的参数是在 defer 执行是就计算出来的,而不是函数执行时才计算的,所以像下面这段代码运行的结果将是 10 0,而不是10 10:

func forDefer() {
    i := 0
    defer fmt.Printf("%d ", i)
    i = 10
    fmt.Printf("%d ", i)
}

同时它还遵循LIFO(Last Input First Output:先进后出)所以,当defer执行之后,延迟函数所调用的参数不管再怎么改变,都不会影响延迟函数本身的输出,所以像下面这段你代码,它最终执行后的结果将不是 0 1 2 3 4,而是4 3 2 1 0:

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

延迟函数执行还时常被用在跟踪函数本身的执行上,比如下面的示例:

func trace(s string)    { fmt.Println("entering: ", s) }
func untrace(s string)  { fmt.Println("leaving: ", s) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something you like as normal...
}

对于这个跟踪函数,我们还可以做得更简洁高效,还记得我们在前面了解到的关于延迟函数的参数的值是在defer执行时就被计算的吗?我们在这里来使用这一特性重新完成前面的那个函数运行跟踪函数:

func trace(s string) string{
    fmt.Println("entering: ", s)
    return s
}

func un(s string) {
    fmt.Println("leaving: ", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    def un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

上面代码的输出结果是:

entering: b
in b
entering: a
in a
leaving: a
leaving: b

对于习惯于以块为单位管理代码的其它语言的开发者来说,defer可以看起来很奇怪,但是它功能的功能及实用性是可以肯定的,而且是十分有趣的,它使得代码不在以块为单位而是以功能来进行组织管理的。

数据

通过 new 分配内存

Go内置了两人函数 make 和 new,它们做的事情是不一样的,而且绑定了不同的类型,这很容易让人混淆,但实际上它的规则是很简单的。让我们先来谈谈 new,它是一个用来分配内存的内置函数,但是它不像其它语言中的 new 函数,它不初始化内存, new(T) 只是创建一个没有任何数据的类型为T的实例,并且返回它的地址——*T类型的值,在Go的术语中,它返回一个新建的分配了0空间的类型为T的实例的指针。

由于new返回的内存是归零了的,所以它能让我们很方便的设计它的数据结构,因为归零的数据可以不需要更多的初始货即可直接使用,比如 bytes.Buffer的文档这么写着“Buffer的归零数据是一个空的可以随便人到中年来使用的缓冲”。相反的,sync.Mutex没有明确的构建或者初始化方法,转而它的归零值为一个未锁定的mutex。

The zero-value-is-useful property works transitively. Consider this type declaration.

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

SyncedBuffer类型的值在其分配了或者仅仅只是声明了之后都是可以仍的,下面这段代码不需要更多的安排即可以正确的工作:

p := new(SyncedBuffer)  // type *Synceduffer
var v SyncedBuffer  // type SyncedBuffer

构造函数与复合声明(Constructors and composite literals)

很多时候一个归零的值并不足以满足程序的业务需求,这时一个初始化构造函数就成了必须的东西了,比如下面这段从 os 衍生的函数:

func NewFile(fd ind, name string) *File{
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

上面这样的写法有点太过于复杂和麻烦了,使用初始化成员可以更简单的完成这个工作,每次只需要使用一个表达式即可创建一个新实例:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File(fd, name, nil, 0}
    return &f
}
注意:不像 C,返回本地变量的地址在Go中是完全可以的,当函数返回之后,本地变量地址所占用的存储空间同样的会生存下来,事实上,我们可以直接可以将上面函数中的最后两人行合并为一行:
return &File(fd, name, nil, 0}

上面这些初始化成员必须按其构造函数定义了的顺序赋值,这是很麻烦的一件事情,我们必须要一直记得它们是一个什么样的顺序,即使有100个,我们也必须记住这100个的顺序,好在我们可以使用一种更加合理且方便的方法来使用它,那就是为每一个初始化成员指定一个标签,其格式为field: value:

return &File(fd: fd, name: name}

如果初始化成员没有任何元素的话,那么就会创建一个归零的实例,这个时候,&File{}与new(File)是等价的。

复合声明同样可以被用来创建array、slice、map,通过指定适当的索引和map键来标识字段,在下面这个例子中,无论是Enone、Eio还是Einval初始化都能很好的工作,只要确保他们不同就好了。

a := [...]string    {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string       {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}

用 make 分配内存

回到内存分配。内奸函数 make(T, args) 与 new(T) 有着不同的功能。它只能创建 slice、map和channel,并且返回一个有初始值(非零)的T类型的实例,而不是 *T。从本质上讲,导致这三个类型有所不同的原因在于指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量这三项的描述符,在这些项目被初始化之前,slice为nil,对于slice、map和channel,make初始化了内部的数据结构,填充适应的值。

make 返回初始化后的(非零)值

例如,*make([]int, 10, 100)分配了100个整数的数组,然后用长度10和容易100创建了slice结构指向数组的前10个元素,区别是new([]int)返回指向新分配的内存的指针,而零值填充的slice鸨是指向nil的slice值。

下面这个例子展示这两人者之间的不同:

var p *[]int = new([]int)   // 分配slice结构内存 : *p = nil
                // 已经可用
var v []int = make([]int, 100)  // v 指向一个新分配的有 100 个整数的数组

var p *[]int = new([]int)   // 不必要的复杂的例子
*p = make([]int, 10, 100)

v := make([]int, 100)       // 更常见

务必赢得 make 仅适用于 map、slice和channel,并且返回的不是指针,应当用new来获得特定的指针。

本文是 「Golang.org」的原文档翻译,出于本人有限的英语水平,如有不当,还请指正。

说明

本文档演示了一个最简单的 Go 包的开发过程以及一些 Go 命令行工具的使用,以及Go是如何架构、安装与搜索包的。

代码组织

GOPATH 以及工作区

Go 语言设计的目标之一是让软件开发更简单,得到的结果是 Go命令不使用任何的 Makefiles或者其它的配置文件,取而代之的是直接使用程序的源代码来管理依赖关系及架构条件,这意味着,你程序的源代码与架构脚本总是同步的,因为它们就是同样的东西(注:如果使用 Makefiles之类的东西,那么,你源代码修改了,还需要再去修改Makefiles,而是在Go中,因为没有这些配置文件,所以,你修改源代码,同时直接就修改了这些配置)。

你唯一需要做的是设置一个 GOPATH 环境变量,GOPATH 的作用是告诉 Go 从哪里去搜索Go的执行脚本以及将你的Go安装到哪里.$GOPATH 是一个包括很多路径的列表,它的格式与系统的PATH变量格式一样,一个Unix系统中常见的GOPATH像这样的:

GOPATH=/home/user/ext:/home/user/mygo

(在Windows系统中,将“:”修改为“;”)

任何一个被定义至 $GOPATH$ 环境变量中的路径都指定着一个工作区的的位置(比如上例中的 /home/user/ext或者/home/user/mygo),一个工作区包含着Go源代码以及它们所依赖的包对象以及一些可执行的工具,它被规定为需要有下面这三个子目录:

  1. src : Go 源代码文件
  2. pkg : 以编译的 Go 包
  3. bin : 可执行命令

src 目录中的子目录都是独立的包,这些包中的所有源代码文件(.go,.co,.h以及.s等)都是子目录包的元素。

它构建一个import "widget"的程序时,Go构建工具会搜索 Go 根目录下的 src/pkg/widget 目录——如果没有找到,那么它会接着尝试去寻找同一工作区中的 src/widget 目录。

多个工作区可以给我们带来更我铁灵活性与方便,但是在本文档中,我们只涉及单个工作区,下面让我们先创建一个示例工作区,首先创建一个目录,我们用它来存放项目的源代码:

cox@CoxStation:~$ mkdir $HOME/example/src -p

下一步,设置 GOPATH 环境变量,你需要同时将 $GOPATH/bin 添加到 PATH 环境变量中,这样一来,你就不需要每次执行这些命令的时候输入完整的绝对路径了。

cox@CoxStation:~$ export GOPATH=$HOME/example
cox@CoxStation:~$ export PATH=$PATH:$HOME/example/bin

导入路径

Go 标准库或者包都可以使用最短的路径名导入,比如 fmt 或者 net/http 等,这是为了程序开发者的方便而设计的,但是对于你自己的项目,你需要认真的设计一个基本的导入路径,这需要考虑它它的可读性同时还需要考虑到它不应该和以有的或者将来会出现的其它的库相冲突。

最好的一个办法是总是以你所开发的项目的版本管理工具名称开始,比如,如果你的项目源代码库位置为 example.com 或者 code.google.com/p/example ,那么你应该让你的包路径从该 URL 地址开始,比如:example.com/foo/bar 或者 code.google.com/p/example/foo/bar ,如果你遵循这样的约定,那么 Go 会自动为你检测URL地址所指定的包,同时下载以及以你的导入路径安装至你的工作区中。

如果你没有像上面这样的发布路径,那么你至少应该使用一个唯一的前缀,比如 widget 以及 widget/foo/bar ,一个好的规则是使用你的项目名称或者公司名称作为前缀,这一般是不会被别人使用的,比如:example.com。

我们下面使用 example.com 作为基本的导入路径:

cox@CoxStation:~$ mkdir -p $GOPATH/src/example.com

包名称

任何一个 Go 源代码文件的第一个声明都应该是包名:

package NAME

NAME 就是你的包名称,它也是你导入该包时默认使用的名称(所有处于同一个包中的文件都必须共享同一个包名)。

Go 约定包名称是导入路径中的最后一个元素,一个以 crypto/rot12 导入的包的包名应该是 rot13,Go 不要求所有被导入到某一个程序中的包名都具有唯一性,但是要求其完整的导入路径必须是唯一的。

在 example 中创建一个新包,名为 newmath:

cox@CoxStation:~$ cd $GOPATH/src/example.com
cox@CoxStation:~/example/src/example.com$ mkdir newmath

之后创建一个名为 $GOPATH/src/example.com/newmath/sqrt.go 的文件,它的内容为:

// Package newmath is a trivial example package.
package newmath

// Sqrt returns an approximation to the square root of x.
func Sqrt(x float64) float64 {
    // This is a terrible implementation.
    // Real code should import "math" and use math.Sqrt.
    z := 0.0
    for i := 0; i < 1000; i++ {
        z -= (z*z - x) / (2 * x)
    }
    return z
}

该包将可以使用下面这个路径导入:

import "example.com/newmath"

构建以及安装

Go 命令包含了很多子命令,最核心的子命令就是install,运行go install importpath 将构建并安装它所依赖的所有包。

安装一个包 表示将构建之后的包文件或者可执行的文件写入到当前工作区的pkg或者bin目录中。

构建一个包

要构建并安装 newmath包,使用下面命令:

cox@CoxStation:~/example$ go install example.com/newmath

如果构建成功,那么该命令不会有任何输出。Go约定,如果没有指定导入路径,那么go使用当前目录,下面的这两行命令与上面那个是一样的效果:

$ cd $GOPATH/src/example.com/newmath
$ go install

构建之后的结果是go为你创建了一个目录树(假定我们现在使用的是Linux 64位系统——我就是这样的系统),它看起来像是这样的:

$GOPATH/
    pkg/
        linux_amd64/
            example.com/
                newmath.a
    src/
        example.com/
            newmath/
                sqrt.go

构建一个命令工具

Go 命令将包名为 main 的包视为可执行的,从而会将它安装到 $GOPATH/bin 目录中,创建一个名为 hello 的可执行命令,我们首先需要创建它的包,之后再创建包文件:

cox@CoxStation:~/example$ mkdir $GOPATH/src/example.com/hello
cox@CoxStation:~/example$ vi $GOPATH/src/example.com/hello/hello.go

文件内容为:

// Hello is a trivial example of a main package.
package main

import (
    "example.com/newmath"
    "fmt"
)

func main() {
    fmt.Printf("Hello, Golang. Sqrt(2) = %vn", newmath.Sqrt(2))
}

保存上面文件之后,我们运行下面这个命令安装它:

cox@CoxStation:~/example$ go install example.com/hello

之后你就可以像运行其它命令一样的运行我们的 hello 了:

cox@CoxStation:~/example$ $GOPATH/bin/hello
Hello, Golang. Sqrt(2) = 1.414213562373095

如果你将 $GOPATH/bin 目录添加到了全局环境变量 PATH 中的话,那么你还可以像下面这样的执行:

cox@CoxStation:~/example$ export PATH=$PATH:$HOME/example/bin/
cox@CoxStation:~/example$ hello
Hello, Golang. Sqrt(2) = 1.414213562373095

现在我们的示例程序的目录数看起来应该像下面这个样子的了:

$GOPATH/
    bin/
        hello
    pkg/
        linux_amd64/
            example.com/
                newmath.a
    src/
        example.com/
            hello/
                hello.go
            newmath/
                sqrt.go

测试

Go 内置了一个轻量级的测试框架,它由 go test 命令和 testing 包组成。

创建一个以 _test.go 结尾的文件,让它包含只有用来标记的参数 t *testing.T的名为TestFUNC* 的函数,那么测试框架就会执行每一个像上面这样的函数,其实函数名称中的 FUNC 修改成为你需要测试的函数名,如果该函数调用了失败函数,比如t.Error或者t.Fail,那么表示整个测试失败。

我们现在创建一个名为 $GOPATH/src/example.com/newmath/sqrt_test.go 的测试文件用来测试 Sqrt函数:

cox@CoxStation:~/example$ vi $GOPATH/src/example.com/newmath/sqrt_test.go

它的内容为:

package newmath

import "testing"

func TestSqrt(t *testing.T) {
    const in, out = 4, 2
    if x := Sqrt(in); x != out {
        t.Errorf("Sqrt(%v) = %v, want %v", in, x, out)
    }
}

现在运行测试:

cox@CoxStation:~/example$ go test example.com/newmath
ok      example.com/newmath 0.005s

远程包

一个导入路径可以告诉 go 如何从一个版本管理系统(比如Git或者Mercurial)中获取包代码,go命令使用这些属性自动从远程搜索包,比如我们的这个示例程序,它的远程地址是:code.google.com/p/go.example,如果你将该URL址址告诉go,那么go会自动从远程下载该包的源代码:

$ go get code.google.com/p/go.example/hello
$ $GOPATH/bin/hello
Hello, world. Sqrt(2) = 1.414213562373095

如果指定的远程包在本地没有复本,那么 go 会自动的为你下载它,如果已经存在,则直接跳过,运行了上面的命令之后,你的目录看起来应该像下面这样的了。

$GOPATH/
    bin/
        hello
    pkg/
        linux_amd64/
            code.google.com/p/go.example/
                newmath.a
            example/
                newmath.a
    src/
        code.google.com/p/go.example/
            hello/
                hello.go
            newmath/
                sqrt.go
                sqrt_test.go
        example/
            hello/
                hello.go
            newmath/
                sqrt.go
                sqrt_test.go

如果你不直接告诉 go 去下载安装某个包,而是在某一个你所写的包中导入它,比如:

import "code.google.com/p/go.example/newmath"

那么,go 一样的也会自动的为你做好这一切。

说明

  1. 本翻译文档中所涉及的代码和原始文档有些话差入。
  2. 你可以从这里下载到本文档中所创建的示例代码:http://dl.antusoft.com/examples/golang/go.example.tar.gz
  3. 官方示例项目地址为:http://code.google.com/p/go.example/

什么是Go?来自其网站的介绍:

Go 编程语言是一个使得程序员更加有效率的开源项目。Go 是有表 达力、简洁、清晰和有效率的。它的并行机制使其很容易编写多核 和网络应用,而新奇的类型系统允许构建有弹性的模块化程序。Go 编译到机器码非常快速,同时具有便利的垃圾回收和强大的运行 时反射。它是快速的、静态类型编译语言,但是感觉上是动态类型 的,解释型语言。Go 1 是 Go 语言的第一个稳定发布版本。

基本上任何一个语言最开始的学习的时候都离不开那个“Hello World!”程序,既然找不到其它的方法,那么,学习Go的时候还是也来一个Hello World吧。

package main

import "fmt"

/* 打印出 Hello World 的各种语言的版本 */

func main() {
    fmt.Printf("Hello, world; or 你好,世界n")
}

在上面的代码段中,首行是必须的,任何 Go 文件都必须以 package 开头,对于需要独立运行的执行文件,则必须是 package main;接着我们导入了 fmt 包,这就是说需要将 "fmt" 包加入 main,不是 main 的其它包都称之为库,其它许多语言中都有着类似的概念,比如Python中的 module。后面跟着的是一行注释。

最后面我们定义了一个名为 main() 的函数,当 Go程序在执行的时候,首先调用的函数就是 main.main(),这是从C语言中继承而来的,在这里我们使用关键字 func 定义了这个函数,这个函数不接受任何参数,也不返回任何类型的值,该函数将打印出 “Hello, world; or 你好,世界”字符串,你可以发现,在Go语言中,可以直接使用非ASCII字符,这是因为Go支持任何编码的字符串,你甚至可以使用非ASCII编码的字符作为变量或者函数名称。

将上面的代码保存为一个helloworld.go 文件,然后编辑它:

go build helloworld.go

这会在当前文件夹中生成一个同名的可执行文件,我现在使用的是Windows 7,所以,这个可执行文件的文件名为 helloworld.exe 。如果不想编译它即执行,可以使用 go run 命令:

go run helloworld.go

它会直接运行 helloworld 程序,而不是生成可执行文件后手工去执行。

直接进入正题,在任何一个地方创建一个名为 helloworld.go 的文件,然后立马执行它:

cox@Cox:~/workspace/go/src$ touch helloworld.go
cox@Cox:~/workspace/go/src$ go run helloworld.go
package :
helloworld.go:1:1: expected 'package', found 'EOF'

简单吧,看错误信息,你现在应该知道,没有绑定到某一个 package 的文件是不被 Go 认可的,所以,我们都需要加上这么一个,我们在前面的那个 helloworld.go 文件里面加上:

package main

之后再来执行:

cox@Cox:~/workspace/go/src$ vi helloworld.go
cox@Cox:~/workspace/go/src$ go run helloworld.go
# command-line-arguments
runtime.main: undefined: main.main

恩,Go程序需要从某一个位置开始执行,就像电脑有电源开关一样简单,在 Go 中,程序都是从一个名为 main 的函数开始执行,所以,再加上这个函数:

package main

func main() {}

再来执行它:

cox@Cox:~/workspace/go/src$ vi helloworld.go
cox@Cox:~/workspace/go/src$ go run helloworld.go

恩,没报任何错误了,那也就是说我们现在的程序已经正确运行,但是因为 main() 没有做任何事情,所以我们现在什么也看不到,我们现在让它来打印出 “Hello World! 您好!世界。” 这个字符串:

package main

func main() {
    Println("Hello World! 您好!世界。")
}

又出错了:

# command-line-arguments
./helloworld.go:4: undefined: Println

Println 未定义,恩,从我们上面的代码中确实看得到,从来没有它,突然就这么一下子就拿来用了,不成,就像你从来没有买过车,一辈子都没见过车,怎么可能走着走着就突然在开车了呢,我们来买一辆车:

package main

import "fmt"

func main() {
    fmt.Println("Hello World! 您好!世界。")
}

再来运行 helloworld.go:

cox@Cox:~/workspace/go/src$ go run helloworld.go
Hello World! 您好!世界。

成了,这个 fmt 就是我们买来的车,有了车我们才能开车,我们这里用了 fmt 里面的 Println 函数。

你学会了吗?

  • 任何一个 Go 文件都必须被关联到某一个包上
  • 任何一个 Go 程序都必须有一个开始点,就是那个 main 函数
  • 任何一个函数或者变量都必须被定义之后才能使用

Golang 是一个以类BSD许可发布的程序开发语言,官方提供有两个编译器 gc Go编译器和gccgo编译器,其中gccgo编译器是GNU C 编译器的一部分,而 gc 编译器比较成熟并且它进行了更多的测试,本文所使用的是 gc 编译器安装一个以二进制包发布的 Go 版本。

系统需求

gc 编译器可以在下面这些操作系统和架构上面运行,请确定您当前的系统满足这些需求,如果你所使用的系统不在当前列表中,那么你就应该试试 gcc 编译器安装,这有可能有适合您的系统。

操作系统名称 系统架构 说明
FreeBSD 7+ amd64,386 Debian GNU/kFreeBSD不被支持
Linux 2.6.23+(glibc) amd64,386,arm CentOS/RHEL 5.x不被支持,还无ARM版本的二进制包
Mac OS X 10.6/10.7 amd64,386 使用XCode的gcc(1)
Windows 2000+ amd64,386 使用mingw gcc(2),cygwin或msys不是必需的

1) 只有你想使用 cgo 时,gcc才是必须的;2) 你仅仅只需要安装命令行工具即可,如果你已经嬢了  Xcode 4.3+,那么你可以在组件面板中添加命令行工具

下载 Go Tools

访问 Go 项目下载页面:http://code.google.com/p/go/downloads/list,选择一个适合你的操作系统或者CPU架构的发行版,官方的发行版有FreeBSD,Linux,Mac OS X(Snow Leopard/Lion)以及Windows系统,同时支持 32位与64位CPU架构,如果没有适合你所使用的操作系统的二进制安装包,你可以需要从源代码安装或者使用gccgo而不是gc。

安装 Go Tools

Go的发行布将被安装至 /usr/local/go目录(Windows下为c:Go),但是你是可以修改该安装目录的,使用Go时,你需要指定 GOROOT 环境变量至你使用 Go 的目录,比如,如果你将 Go 安装至你的home目录,那么你需要在 $HOME/.profile文件中添加下面这两行:

export GOROOT=$HOME/go
export PATH=$PATH:$GOROOT/bin

Windows用户请设置环境变量,怎么设置我也不太明白,很少用那个东西……

FreeBSD, Linux以及Mac OS X压缩包

如果你是从一个老版本的Go进升级,那么你需要在安装之前先删除原先版本:

rm -r /usr/local/go

将你获得的压缩包文件解压至 /usr/local:

tar -C /usr/local -xvf go1.0.3.linux-amd64.tar.gz

包文件的名称可能和上面我所使用的不一样,因为我们的系统可能不一样,之后你可以将 /usr/local/go/bin目录添加至你的环境变更 PATH 中,比如添加至 /etc/profile或者$HOME/.profile。

Mac OS X 包安装工具

如果 Mac OS X 的包安装工具的话只需要接照安装工具的流程进行即可,默认会安装至 /usr/local/go 目录中。

Windows 下面自己研究

测试你的安装

要测试你的Go是否已经正确安装的最简单的办法就是写一个Go程序,比如,Hello Golang

package main

import "fmt"

func main() {
    fmt.Printf("Hello, Golang!n")
}

然后测试运行:

$ go run hello.go
Hello, Golang!

OK,这表示你安装成功了。

文本格式

Instiki 支持很多种不同的文本格式,每一个不同的 Web (一个Web可以简单的认为是一套系统上面可以建立多个站点中的一个)可以有自己不同的文本格式。

XHTML 友好的标记格式

对于任何一种使用 XHTML 友好标记格式的页面,系统都会将内容以 application/xhtml+xml 标记发送,这表示,你可以直接在页面插入 SVG 图片,如果你想了解更多关于 SVG 的知识,可以查看一下我的另一篇文章《可缩放矢量图形 - Scalable Vector Graphics (SVG)》,所有下面这些标记格式都是基于 Maruku以及它内置的增强版本的 Markdown 语法

  • Markdown+itex2MML这是Instiki默认使用的文本格式,它使用了 itex2MML ,并且允许你直接在文本中书法 itex 方程式 ,这些方程式会被 MathML 翻译为浏览器可识别的标签,而对于某些不支持MathML的浏览器,MathJax将被用来渲染 MathML。在某些数学公式使用很多页面中可能加载会十分的慢,但是 MathJax 是一个跨浏览器的工具。
  • Markdown+BlahTex/PNG这种文本格式会将你的数学公式编译为 PNG 图片,而不使用 MathML,安装会稍稍复杂一点,而且渲染的效果也没有 MathML的好,但是用户不需要使用支持 MathML 的浏览器即可查看。
  • Markdown如果你不需要数学公式的支持,那么选择这种方式是最好的,这样 $ 这个符号就没有什么特殊的含义了,在前面的两人种格式中,它被用来限定数学公式的范围。

HTML 友好的标记格式

如果你选择了这些格式里面的任何一种,那么内容将被标记为 text/html ,这表示你的网页里面不会有数学公式以及SVG图片,除非必要,我个人不建议你使用这些标记格式。

  • Textile使用 RedCloth 渲染 Textile 文本
  • RDoc支持 RDoc
  • Mixed与 Textile 类似,只是对于一个单独的分行,它不会被解释为 * <br /> *

分类

Instiki可以对所有页面进行归类,要给某个页面加入到一个分类中,只需要在页面中添加下面这一行即可:

category: food

你可以为一个页面添加多个分类,只需要在上面的那一行中写上多个分类即可,分类之间使用英文逗号分开:

category: food,restaurants

你可以将分类放在任何一个位置,但是我们约定将它放在文本的最下方,你还可以将一个页面归入某个类中但是在页面展示时不将其显示出来,只需要下面这样即可:

:category: S5-slideshow

上面这个示例将会让文档成为一个 S5 slideshow 页面.

文件上传

如果你选中了 Allow uploads… 的复选框,那么用户就可以上传文件至服务器,上传的方法是:

  1. 编辑某个需要插入图片的页面,在需要插入图片的地方插入下面这样的代码:
    [[mypic.jpg:pic]]

    或者:
    [[myfile.pdf:file]]

    注意 : 这里面填写的文件名称将是文件上传之后在服务器上面的名称,它并不需要与用户要地的文件名称一样。
  2. 保存页面,上面的代码会生成一个链接,并提示这里需要上传图片,点击上传链接即可打开文件上传页面。
  3. 在文件上传页面中,可以输入一个 Description :
+ 对于图片文件,它将是 &lt;image&gt; 标签的 *alt* 属性
+ 对于其它文件,它将作为提示文本,并且同样也是文件链接文本
  1. 当你上传完成之后,图片、视频、文件等会自动的出现在你的页面上。

如果你想为图片或者文件定义与 Description 不一样的文本,你可以使用下面这样的标记:

[[mypic.jpg|alt text:pic]]

或者:

[[myfile.pdf|link text:file]]

除了上面这种维基的格式,你同样还可以使用 Markdown 风格的标记方式:

![alt text](/mywiki/files/mypic.jpg)

或者:

[link text](/mywiki/files/myfile.pdf)

Wolfram CDF 文件

Wolfram Research 定义了一种文件格式名为 Camputable Document Format,这种文件需要浏览器插件的支持,如果你在文本中输入了像下面这样的标记:

[[Voronoi.cdf| 588 x 380 :cdf]]

那么会像 FlashPlayer一样,Instiki为会指向CDF文件的链接创建一个类似Flash获取插件的链接,它像下面这样的:

http://www.wolfram.com/cdf-player/

上面这个标记中,尺寸是可选的。

文件的管理

你可以在管理员界面中查看所有上传至维基的文件,并且可以直接删除他们,但是这里还有一个更快捷的方法,将:

[[myfile.pdf:file]]

修改为:

[[myfile.pdf:delete]]

保存页面之后,会生成一个:

*Delete myfile.pdf*

的链接,点击该链接,然后输入Web密码,即可删除该文件,这种方法可以被用来替换页面中已有的文件:

  1. 首先像上面这样删除该文件。
  2. 重新添加该页面,再加上下面这行标记:[[myfile.pdf:file]]
  3. 保存页面后作前面说过的上传文件的操作。

搜索

每一个页面顶部的搜索框除了支持普通搜索外,你还可以在这里面输入正则表达式,如果你懂正则,那么这个搜索工具将会帮助你更快的检索到你真正想要的内容,默认的,该搜索工具是大小写不敏感的,关于正则的有关知识,我这里不做任何的讲解,如果你想了解它,建议你从这里开始:http://www.regular-expressions.info/quickstart.html,下面只是几个示例:

  • (bzr|bazaar)
  • init[^w]
  • wikis?word

快捷访问键

绝大多数浏览器都支持通过键盘访问网页中的内容,我们称这为快捷键或者访问键,英文名称为AccessKeys,这些快捷键是由网页中的标签定义的,根据你所使用的操作系统或者浏览器的不同,可以通过下面这些方式访问快捷键:

  • Windows ⇒ Alt + AccessKey
  • Macintosh ⇒ Control + AccessKey
  • Opera 浏览器 ⇒ 先按 Shift+ESC ⇒ 然后按 AccessKey
  • GNOME 下的 FireFox ⇒ Alt + Shift + AccessKey

这里有一点需要注意,在Windows的Internet Explorer(IE)浏览器中,如果某个被设置了快捷链的链接指向另一个页面,那么当你击活该快捷键后,只会将焦点移动到该快捷键所定义的链接上,并不会自动的为你转向该链接所指向的页面,你需要再按一次回车键才能算作是点击。

在Instiki中,定义了下面这些快捷键:

  • Alt-E :编辑当前页面
  • Alt-U :查看当前版本
  • Alt-H :回到首页
  • Alt-A :查看所有页面列表
  • Alt-S :保存当前正在编辑的页面
  • Alt-C :显示所有隐藏的分类;退出当前页面的编辑模式
  • Alt-B :查看上一个版本
  • Alt-F :查看下一个版本
  • Alt-X :导出Wiki
  • Alt-W :编辑Web(当前站点设置)

结语

本文并未对 Instiki 的所有功能及使用方法都作了介绍,仅仅只是对其基本功能的一个说明,我会在更久的使用了它更了解它之后,对其它的一些功能做更深入的说明,比如它的 SVG-EDIT 编辑器(这是一个开源项目,你可以在你的项目中使用),数学公式以及我很喜欢的那个 S3 Slideshow等等。

最近一直在为 Golang Wiki 选择维基引擎,最开始选择的是 DokuWiki 与 MoinMoinWiki ,前者是因为它足够的简单,后者是因为个人对 Python 的喜爱,然后,在网上找寻的过程中知道了 Instiki Wiki Engine,基于Ruby,它让我眼前一亮的是内置的 SVG编辑器 以及 Slideshow功能 ,尤其是前者,太爽了,所以,现在也在自己的本地测试与试用这个系统了。

简单的特性介绍 - Introduction to Instiki

Instiki 提供了基于 itex2MML 的对数学公式的支持,但是要求文本格式为系统默认的 “Markdown + itex2MML” 格式。同时,在每一个维基页面底部都有一个名为 “TeX” 的链接,用户可以通过它将内容导出为 LaTex 格式,不过这个我还没有测试过导出来的文档是不是对中文也很友好。

最让我眼前一亮的就是 “SVG编辑器以及直接将SVG图片插入内容中“ 的功能,它使使得我们可以在编辑内容的时候直接打开一个SVG图片编辑器,完成图片编辑之后,可以选择保存至本地图片库或者远程图片库(这需要提供相关图片库的帐号),更简单的,是可以直接将编辑后的图片插入文本内容中,因为SVG可以使用纯文本描述,所以我们根据就不需要将它保存为文件,如果选择直接插入SVG图片代码,我们还是可以再修改它的,办法就是把整个SVG代码选中,创建SVG图片的按钮就会变成编辑已有图片,点击它就会载入选中的图片进行编辑。

另外一个很不错的功能(但是有可能我会用不着),就是对数据公式的支持(可以说是完美的支持),它并不将我们的公式转为图片,而是使用的MathJax库,以文本的方式展示公式,这个有机会使用得到的时候再好好的去研究一下下。

一个 Instiki 实例可以创建无数个维基站点(在 Instiki 中称之为 “Webs”),每一个站点又都可以被设置为密码保护(使用独立的密码),如果你愿意,还可以将一个密码保护的站点发布一份“只读(Read-Only)”版权,这样其它人可以读取它的内容但是无法对其做出任何修改。

安装与系统需求 - Install & Requirements

在我写这篇文章时,它要求Ruby版本为 1.8.6、1.8.7或者1.9.2,Rubygems版本为 1.3.6或者更高的版本。

Mac OS X - Leopard, Snow Leopard 以及 Lion

Ruby 1.8.7 以及 Rubygems 是跟随开发者工具套件(Developer Tools) 一起被安装的,该套件现在已经可以在 App Store 上面直接下载到,只需要你有一个 Apple ID 即可,在苹果电脑上面安装 Instiki 需要该套件的支持,如果你没有安装,需要先安装它,这根据你的网速可能需要一些时间,尤其是在国内网络情况并不太理想的前提下。

首先将 Rubygems 升级到最新版本:

sudo gem update --system
sudo gem update

如果你的 Rubygems 版本过老的话,可能上面的命令无法成功执行,这需要你手工升级它,详情请移步这里:http://rubygems.org/pages/download ,之后你就可以安装 Instiki 了,关于如何在 Tiger 上面的安装 Instiki ,请移步这里:http://golem.ph.utexas.edu/wiki/instiki/show/Installing+under+MacOSX+Tiger

Linux

在 Debian 或者 Ubuntu 上面安装Instiki,你可以执行下面这行命令安装一些需求包(我安装的时候因为被SVG编辑器完全吸引了,而忘记了安装说明,最后是自己一次一次出错才成功安装的,每出一次错补装一个需求包):

$ sudo apt-get install ruby ruby1.8-dev libopenssl-ruby rake rubygems libsqlite3-ruby1.8 ri1.8 libxslt-dev libxml2-dev libsqlite3-dev swig flex bison

如果你的机器上面都有这些了,那就没必须再安装了,我的电脑上面就是没有 libxslt-ruby 、libsqlite3-dev 以及 libxml2-dev 。

如果你使用的是 Febora 的话,则可以执行下面这行命令来完成需求包的安装:

$ yum install make ruby ruby-devel rubygems sqlite sqlite-devel swig flex bison

然后还需要执行下面这行命令来创建一个软链接:

$ ln -s /usr/lib/libsqlite3.so.0.8.6  /usr/lib/libsqlite3.so
注意 :如果你得到下面这样一个错误信息:

install_gem_spec_stubs': undefined method `loaded_specs' for Gem:Module (NoMethodError)

那表示你的 Rubygems 版本过低,你可以通过下面这两人行命令升级它:

sudo gem install rubygems-update
   sudo update_rubygems

CentOS 是一个 RedHat 家族的Linux发行版,所以,你可以直接像 Fedora 一样做,只是有已知的有两个不一样的错误可能会发成在CentOS上面:

  1. /dev/null is not world-writable ,你需要:
    chmod 777 /dev/null
  2. /dev/urandom is not a "special character device.” ,你需要:
    rm /dev/urandom
    mknod -m 644 /dev/urandom c 1 9
    chown root:root /dev/urandom

对于这上面这个解决办法,你可能需要写进自启动脚本中,因为我们现在所作的任何修改,在系统重新启动之后都会失效。

Windows 系列

饿……没什么想法,我从来不认为这些软件应该被安装在Windows 系统之上,所以,如果你真的有这样的需求,自己去找解决办法吧……

安装 Instiki - Install Instiki

从 http://rubyforge.org/frs/?group_id=186 下载 Instiki 的最新版并解压,如果你想试用最新的开发版本,则可以下载 http://golem.ph.utexas.edu/~distler/code/instiki-svn.tar.gz ,不过开发中的版本可能存在很多的Bug,但是Instiki一直使用的都是开发中的版本,所以,如果有Bug的话,应该也是能在第一时间得到解决的。

如果你选择开发版,你还可以使用 bzr :

bzr branch http://golem.ph.utexas.edu/~distler/code/instiki/svn/ location-of-Instiki/

或者使用 Git :

git clone http://github.com/parasew/instiki.git location-of-Instiki/

之后如果要更新开发版本,只需要使用 [bzr pull]golem.ph.utexas.edu/wiki/instiki/show/Bazaar 或者git pull 即可。

注意 :安装时请讲上面两人行命令中的 location-of-Instiki/ 修改成为你要安装它的目录,同时保证整个路径中没有空格。

运行 Bundler 并启动你的 Instiki 之旅 - Running Bundler and starting Instiki

如果上面的安装或者取得文档都没有错误发生,那么可以在你的 Instiki 主目录中运行下面这行命令了:

ruby bundle install --path vendor/bundle

这将下载并安装其它的一些需求包(sqlite3-ruby/itextomml等),最后:

./instiki --daemon

将启动 Instiki。

打开你的浏览器,并在地址栏中输入:http://localhost:2500 并设置你的第一个维基站点,这很简单,只需要填写维基的名称以及路径和管理它的密码即可。

维护 - Care and Feeding

你可以通过下面这行命令停止 Instiki

% kill pid-of-Instiki

然后 Instiki 还提供了很多的命令行工具用来维护与保养你的 Instiki。

运行:

./instiki

将使用 Mongrel(如果你安装了的话)或者 WEBrick 启动 Instiki,如果你已经安装了 Mongrel,但是却还是想通过 WEBrick 启动 Instiki的话,需要给上面的命令加上 webrick 参数:

./instiki webrick

但是这样的方式启动的 Instiki 将直接在前端运行,所以当你退出终端或者键入 Ctrl + C 之后就会立马停止,如果你想在后端运行,那么你需要像前面那样运行:

./instiki --daemon

或者:

./instiki webrick --daemon

下面还有一些其它的命令行标记:

  • -d, –daemon :以守护进程的方式运行 Instiki
  • -p, –port=Port :定义Instiki监听的端口,默认端口号为 2500
  • -b, –binding=1p :绑定 Instiki 到特定的地址,默认监听 0.0.0.0,即监听所有地址
  • -c, –config=file :使用一个特定的配置文件
  • -u, –debugger :启用Debug模式
  • -e, –environment=name :设定服务器运行的模式(test/development/production),默认为产品模式,即 production
  • -P, –path=/path :绑定一个URL路径给Instiki,默认为 /
  • -h, –help :显示帮助信息,并列出可用的命令

为 Instiki 优化的主机服务 - Dreamhost

Dreamhost 总是为这些开源软件进行各种各样的优化,同样的,在DreamHost上面安装并运行 Instiki 也是十分简单的一件事情,DreamHost 通过 mod_rails 对 Ruby-on-Rails* 提供支持,唯一不足是它只提供了老版本的 sqlite3。

  1. 在DreamHost上面创建一个新的域名虚拟主机(比如 wiki.matrixstack.com)
  2. 在 Specify your web directory 选项中选中 Ruby on Rails Passenger(mod_rails) 选项,并填写路径wiki.matrixstack.com/public

这会在你的家目录中创建一个名为 wiki.matrixstack.com 的目录,登陆 Shell 然后执行下面这些命令:

cd wiki.yourdomain.com
wget http://golem.ph.utexas.edu/~distler/code/instiki-svn.tar.gz
tar --strip-components 1 -xzf instiki-svn.tar.gz
sed '/sqlite3-ruby", :r/s//sqlite3-ruby", "~>1.2.0", :r/' Gemfile > Gemfile.tmp
mv Gemfile.tmp Gemfile
unset GEM_HOME GEM_PATH
ruby bundle

打开浏览器并访问 http://wiki.matrixstack.com对维基进行设置即可。

升级已有的 Instiki 安装

Instiki 的开发还处于活跃期,经常会有新功能被添加或者Bug被修补,所以你肯定时常想升级你现有的Instiki安装,如果你通过 Bazaar 安装,那么:

  1. 运行
    % bzr pull

    或者
    % git pull
  2. 进行所有需要的升级
    % ruby bundle update
    % ruby bundle exec rake upgrade_instiki
  3. 重新启动 Instiki
    % ps -ax | grep instiki
    % kill PID-of-Instiki
    % ./instiki --daemon

如果你是通过 tarball 安装的,那么:

  1. 备份所有上传的文件,它们被保存在 webs/Name-Of-Web/files/ 目录中
  2. 如果你使用默认的 SQLite3 数据库,则需要备份它,它位于 db/production.db.sqlite3 ,如果你使用的是MySQL,则需要备份 config/database.yml 文件
  3. 停止 Instiki
    % ps -ax | grep instiki
    % kill PID-of-Instiki
  4. 下载并解压最新版本
  5. 将第 1 与第 2 步备份的文件重新恢复到对应的目录中
  6. 运行
    % ruby bundle update
    % ruby bundle exec rake upgrade_instiki
  7. 重新启动 Instiki
    % ./instiki --daemon

完成 Instiki 的升级。

安全相关 - Security

软件再好也不可能达到完美的安全,要不然也不会有人站出来说:“永远不要相信用户的输入”了。

以非特权用户运行 Instiki

如果你是一个具有服务器特权的用户,那么我不建议你直接以该用户的身份运行 Instiki,你可以创建一个新的无特权的用户来运行:

  1. 创建一个无特权的没有 shell 访问权限的新用户,我这里就创建一个 instiki 用户
  2. 让该用户仅仅只能访问以下这些必须的文件:
    % sudo chown instiki public secret db db/production.db.sqlite3 config/environment.rb config/database.yml
    % sudo chown -R instiki log storage cache webs tmp
  3. 以 instiki 用户的身份运行 Instiki:
    % sudo -u instiki ./instiki --daemon
  4. 在这种情况下,你可以通过下面这种方式停止 Instiki
    % sudo -u instiki kill pid-of-Instiki

使用 MySQL 数据库替换默认的 SQLite

如果你使用 Sqlite 已经运行你的Instiki一段时间,现在计划转到 MySQL 数据库上去,那么你可以这样做:

  1. 确定你现在运行的Instiki版本大于 0.19,然后执行下面这行命令:
    % ruby bundle exec rake upgrade_instiki

    这会升级你的数据库Schema到最新版本,老版本的数据库Schame可能导致在SQLite3⇒MySQL的转换过程中数据的丢失。
  2. 在 Gemfile 文件的最后面添加下行这一行:
    gem "mysql"

    然后重新执行:
    % ruby bundle

    这将为你安装好 Mysql Gem
  3. 在你的系统环境变量中添加一个新的变量:
- 如果你使用的是 sh ( bash, zsh 等):

        % RAILS_ENV='production'

- 如果你使用的是 csh (tcsh 等):

        % setenv RAILS_ENV production
  1. 导出你的文件至 dump/fixtures/.yml*
    % ruby bundle exec rake db:fixtures:export_all
  2. 编辑 config/database.yml ,并修改:
    production:
       adapter: sqlite3
       database: db/production.db.sqlite3

    为:
    production:
      adapter: mysql
      database: your_db_name
      username: your_db_username
      password: your_db_password
      host: 127.0.0.1
      port: 3306
  3. 创建一个新的MySQL数据库,数据库信息与你上面填写的一保持一致:
    % echo "create database your_db_name" | mysql -u your_db_username -p your_db_password
  4. 初始化你的数据库表并导入你的数据:
    % ruby bundle exec rake db:migrate
    % ruby bundle exec rake db:fixtures:import_all

大小写敏感问题

这里可能还有一个小问题需要你注意一下下,那就是在 SQLite3中,字符串是大小写敏感的,这也就是说你可以有一个页面的名称为 Foo ,而另一个页面的名称为 foo ,这使得 Instiki 会将它们分开对待,但是在 MySQL 下面,这个正好是相反的,字符串默认是大小写不敏感的。

这并没有哪一种方法可以说是绝对正确的,只们只是对待事物的方式不一样而已,如果你想在MySQl数据库中也对页面的名称大小写敏感,你可以单独的通过SQL命令修改受该问题影响的字段,根据你自己数据库的字符集编码选择下面命令中的任何一种即可:

ALTER TABLE `pages` MODIFY `name` varchar(255) COLLATE latin1_bin DEFAULT NULL;
ALTER TABLE `wiki_references` MODIFY `referenced_name` varchar(255) COLLATE latin1_bin NOT NULL DEFAULT '';
ALTER TABLE `wiki_files` MODIFY `file_name` varchar(255) COLLATE latin1_bin NOT NULL;

或者

ALTER TABLE `pages` MODIFY `name` varchar(255) COLLATE utf8_bin DEFAULT NULL;
ALTER TABLE `wiki_references` MODIFY `referenced_name` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT '';
ALTER TABLE `wiki_files` MODIFY `file_name` varchar(255) COLLATE utf8_bin NOT NULL;

这个问题我只在使用 Ubuntu 系统的时候出现过,因为很久没有使用其它系统了,所以也不知道是不是都是一样的,会遇到这种问题,这个问题发生的原因是因为系统默认没有开启 sudo 权限,解决办法很简单:

pantao@aliyun:~$ sudo cp /etc/apt/sources.list /etc/apt/sources.list_backup
[sudo] password for pantao:
pantao is not in the sudoers file.  This incident will be reported.
pantao@aliyun:~$ su
Password:
root@aliyun:/home/pantao# chmod u+w /etc/sudoers
root@aliyun:/home/pantao# vi /etc/sudoers
root@aliyun:/home/pantao# chmod u-w /etc/sudoers
root@aliyun:/home/pantao# exit
exitsudo ap
pantao@aliyun:~$ sudo cp /etc/apt/sources.list /etc/apt/sources.list_backup
[sudo] password for pantao:
pantao@aliyun:~$

在上面的代码中,最开始我是要备份软件源,但是因为我不在 sudoers 文件中,所以系统不允许我在 /etc/ 目录下面执行写操作,然后运行 su ,输入 root 用户密码之后切换到超级用户,然后:

  1. 修改 /etc/sudoers 文件权限,让所有者可写
  2. 打开该文件,并在 root ALL=(ALL:ALL) ALL 这一行下面添加:
    pantao  ALL=(ALL) ALL

    请将 pantap 修改成为你自己的用户名
  3. 按 ESC 键退出编辑状态,再输入 :wq 保存修改并退出编辑器
  4. 去掉该文件的写权限
  5. 退出 root 帐户
  6. 现在已经可以正常使用 sudo 了。

最终修改后的文件片段如下:

[...]
# Cmnd alias specification

# User privilege specification
root    ALL=(ALL:ALL) ALL
pantao  ALL=(ALL) ALL
# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) ALL
[...]

Beautiful Soup 是一个用来从HTML与XML文档中抓取数据的Python库,它提供了从HTML与XML文档中检索、抓取与修改数据的最傻瓜式的方法,能为我们的开发节省很多时间,本文翻译自 Beautiful Soup Documentation,但并非完全地逐字翻译,如果你想查阅原版英文文档,请点击前面的链接,本文绝大多数示例都是在Python的命令行工具中进行的。

功能演示与快速入门

下面这个摘录自《爱丽丝仙境梦游记》的故事段落是我们最开始要进行处理的HTML文档:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p><b>The Dormouse's story</b></p>

<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p>...</p>
"""

上面的代码并没有进行合理的缩进,那是因为我最开始想像大家展示一下 Beautiful Soup提供给我们的第一个有用的功能——美化代码结构:

>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup(html_doc)
>>> print(soup.prettify())
<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
 <body>
  <p>
   <b>
    The Dormouse's story
   </b>
  </p>
  <p>
   Once upon a time there were three little sisters; and their names were
   <a href="http://example.com/elsie" id="link1">
    Elsie
   </a>
   ,
   <a href="http://example.com/lacie" id="link2">
    Lacie
   </a>
   and
   <a href="http://example.com/tillie" id="link3">
    Tillie
   </a>
   ;
and they lived at the bottom of a well.
  </p>
  <p>
   ...
  </p>
 </body>
</html>

下面的这些示例则展示了如何通过Beautiful Soup获取HTML文档中的数据:

>>> soup.title
<title>The Dormouse's story</title>
>>> soup.title.name
'title'
>>> soup.title.string
u"The Dormouse's story"
>>> soup.title.parent.name
'head'
>>> soup.p
<p><b>The Dormouse's story</b></p>
>>> soup.p['class']
['title']
>>> soup.a
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.find(id='link3')
<a href="http://example.com/tillie" id="link3">Tillie</a>

还有一个我们用得最多的功能那就是从一个网页中获取所有网址,来看看Beautiful Soup做这个事情有多简单:

>>> for link in soup.find_all('a'): print(link.get('href'))
...
http://example.com/elsie
http://example.com/lacie
http://example.com/tillie

另一个用得最多的功能就是从某一个元素中获取文本内容:

>>> print(soup.get_text())
The Dormouse's storyThe Dormouse's story
Once upon a time there were three little sisters; and their names were
Elsie,
Lacie and
Tillie;
and they lived at the bottom of a well.
...
>>> print(soup.p.get_text())
The Dormouse's story

安装 Beautiful Soup

看了上面的这些功能演示是不是已经心动了,心动不如行动,先安装Beautiful Soup吧。

如果你使用的是Debian或者Ubuntu Linux,你可以使用系统的包管理工具直接安装:

$ apt-get install python-bs4

Beautiful Soup 4以在PyPi中发布了,所以,如果你不能使用系统的包管理工具安装它,那也可以使用Python的安装工具 easy_install 或者 pip 安装,在PyPi中的包名称为 beautifulsoup4,该包能同时工作于 Python 2.x 与 Python 3.x。

$ easy_install beautifulsoup4

或者

$ pip install beautifulsoup4
如果你使用的某一个软件是基于 Beautiful Soup 3 的,那么你可能需要了解一下怎么安装它,查阅:http://www.crummy.com/software/BeautifulSoup/bs3/documentation.html以获得更多的帮助信息,但是如果你是准备基于Beautiful Soup 创建新的应用,那么我强烈建议你使用 Beautiful Soup 4。

如果你的机器上面连 easy_install 或者 pip 工具都没有,那你还可以直接下载安装包使用包里的setup.py 安装,下载地址为:http://www.crummy.com/software/BeautifulSoup/download/4.x/,下载完成之后,将其解压至某个目录下,运行:

$ python setup.py install

如果还是安装失败,那要么你可以选择检查是哪里出了问题,再尝试安装,或者,最简单的办法,Beautiful Soup 4所使用的许可证允许你将它的所有代码直接包装进你自己的应用中,这样就不再需要安装它了。

Beautiful Soup 4是在 Python2.7与Python3.2上开发的,但是它应该是可以运行在其它版本中的。

安装之后的一些问题

Beautiful Soup包本身是 Python 2 的代码,当你安装至Python 3之后,它会自动转换成为Python 3代码,但是如果你不安装该包,代码则不会被转换,在Windows机器上会报出你安装了错误的版本。

  • 如果你得到 ImportError “No module named HTMLParser” ,则表示你在 Python 3中运行Python 2版本的代码。
  • 如果你得到 ImportError “No module named html.parser” ,则表示你在 Python 2中运行Python 3版本的代码。
  • 如果上面两个错误你都有,那最好的办法是删除所有现有安装,然后重新安装一次试试。
  • 如果你得到 SyntaxError “Invalid syntax” on the line ROOT_TAG_NAME = u'[document]' ,你需要转换Python 2的代码至Python 3代码,你可以通过安装脚本来转换: $ python3 setup.py install

或者使用Python 的 2to3 转换脚本进行:

$ 2to3-3.2 -w bs4

安装解析器

Beautiful Soup 支持Python标准库中的HTML的解析器,但是你同样还可以使用很多其它的第三方解析器,比如 lxml parser 。根据你的系统不同,你可以使用下面中的某一种方式安装 lxml:

$ apt-get install python-lxml
$ easy_install lxml
$ pip install lxml

如果你使用 Python 2,另一个纯Python的HTML解析器 html5lib parser 也是很不错的选择,它可以像浏览器一样的解析HTML文档,用下面的任何一种方法安装它:

$ apt-get install python-html5lib
$ easy_install html5lib
$ pip install html5lib

下表简单地说明了每一种解析器优点与缺点:

解析器 典型用法 优点 缺点
Python标准库 html.parser BeautifulSoup(markup, “html.parser”) 内置,不错的速度,宽容(2.7.3 与 3.2 ) 不是太宽容
lxml HTML parser BeautifulSoup(markup, “lxml”) 非常的快 , 宽容 依赖于外部C库
lxml XML parser BeautifulSoup(markup, [“lxml”, “xml”]) , BeautifulSoup(markup, “xml”) 非常的快,只支持正确的XML文档 依赖于第三方C库
html5lib BeautifulSoup(markup, html5lib) 宽容,像浏览器一样解析页面,操作HTML5文档 非常慢,依赖第三方库,仅支持 Python 2

如果可以的话,我建议你安装 lxml 库,如果你使用的是 2.7.3或者3.2.2以前的版本,建议你使用 lxml 或者 html5lib,早期版本的Python的HTML Parser并不是非常的好。

注意:不同的解析器会为其一份文档创建不同的 tree 。

创建 Soup

要解析一个文档,你可以将该文档的所有字符串或者一个打开的文件对象传送给BeautifulSoup构造器:

from bs4 import BeautifulSoup

soup = BeautifulSoup(open('index.html'))

soup = BeautifulSoup('<html>data</html>')

首先,文档会被转换为Unicode编码,HTML 元素也会被转换为Unicode字符串。

>>> BeautifulSoup("这是中文")
<html><body><p>这是中文</p></body></html>

Beautiful Soup 接着使用一个可用的解析器分析文档,如果不是特别指定的话,它会默认使用一个HTML解析器。

对象类型

Beautiful Soup 将HTML文档转换成为一个 Python 对象树,但是你只需要处理这下面这四种类型的对象即可。

标签 (Tag)

一个 Tag 对象对应于 XML 或 HTML 文档中的某一个标签:

>>> soup = BeautifulSoup('<b>Extremely bold</b>')
>>> tag = soup.b
>>> type(tag)
<class 'bs4.element.Tag'>

Tag对象有许多的方法与属性可供我们使用,这些会在本文下方专门的章节做介绍。

名称(Name)

任何一个Tag都有一个名称,通过 .name 属性访问:

>>> tag.name
'b'

如果你修改了它的名称,那么此次修改会体现到任何一个由Beautiful Soup 创建的HTML元素上:

>>> tag.name = 'blockquote'
>>> tag
<blockquote>Extremely bold</blockquote>

属性(Attributes)

一个Tag可以有不限数量的属性,如 <b class=“boldest”> 就有一个名为 class 的属性,它的值为boldest ,你可以像字典一样访问Tag的属性,比如:

>>> tag['class']
['boldest']

如果你想获取整个Tag的所有属性的字典,可以使用 .attrs

>>> tag.attrs
{'class': ['boldest']}

你可以添加、删除或者修改Tag的属性:

>>> tag['class'] = 'verybold'
>>> tag['id'] = 1
>>> tag
<blockquote id="1">Extremely bold</blockquote>

>>> del tag['class']
>>> del tag['id']
>>> tag
<blockquote>Extremely bold</blockquote>

多值的属性

HTML 4 定义了一些可以有多个值的属性,HTML 5又移除了一些,但是最后却又定义了更多的新的,最常见的可多值的属性是 class ,其它的还有比如 rel , rev , accept-charset , headers 以及accesskey 等,Beautiful Soup将多值以列表(list)的方式进行处理:

>>> css_soup = BeautifulSoup('<p>Strikeout</p>')
>>> css_soup.p['class']
['body', 'strikeout']
>>> css_soup = BeautifulSoup('<p>Strikeout</p>')
>>> css_soup.p['class']
['body']

如果一个属性,在文档中看似是一个多值的,但是却没有在任何一个HTML标准中被定义为多值属性,则 Beautiful Soup 会按标准执行,即将其按单个值进行处理:

>>> id_soup = BeautifulSoup('<p id="my id"></p>')
>>> id_soup.p['id']
'my id'

当Tag重新转为HTML代码时,多个值会被合并:

>>> rel_soup = BeautifulSoup('<p>Back to the <a rel="index">Home Page</a></p>')
>>> rel_soup.a['rel']
['index']
>>> rel_soup.a['rel'] = ['index', 'contents']
>>> print(rel_soup.p)
<p>Back to the <a rel="index contents">Home Page</a></p>

但是如果你使用的是 xml 解析器,那么不存在多值属性:

>>> xml_soup = BeautifulSoup('<p></p>', 'xml')
>>> xml_soup.p['class']
u'body strikeout'

可操纵字符串(NavigableString)

Beautiful Soup 将 Tag 中的字符串内容称之为 NavigableString :

>>> tag.string
u'Extremely bold'
>>> type(tag.string)
<class 'bs4.element.NavigableString'>

NavigableString 与 Unicode 字符类型,当然它还提供有很多特殊的功能,这些我也会在下面的章节中作详细介绍,你可以使用 unicode() 将其转换为 Unicode 字符:

>>> unicode_string = unicode(tag.string)
>>> unicode_string
u'Extremely bold'
>>> type(unicode_string)
<type 'unicode'>

你不能直接编辑字符,但是你可以使用一个新字符替换它:

>>> tag.string.replace_with("No langer bold")
u'Extremely bold'
>>> tag
<blockquote>No langer bold</blockquote>

BeautifulSoup 对象

BeautifulSoup 对象则是文档本身,很多时候你可以像Tag的那样操作它,但是因为它并不是一个真正的Tag,所以它没有名称,也没有属性,但是我们经常需要获取它的名称,所以,它有一个名称,为 [document]

>>> soup.name
u'[document]'

注释以及其它一些字符

Tag , NavigableString 以及 BeautifulSoup 提供了几乎所有我们在HTML或者XML文档中可见的内容,但是还有一些特殊的内容则没有被它们包括在里面,比如你最常需要考虑的是其注释:

>>> markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
>>> soup = BeautifulSoup(markup)
>>> comment = soup.b.string
>>> type(comment)
<class 'bs4.element.Comment'>

Comment 仅仅只是一个特殊的 NavigableString :

>>> comment
u'Hey, buddy. Want to buy a used parser?'

当一个 Comment 对象以 HTML 元素出现的时候,它有自己的特殊格式:

>>> print(soup.b.prettify())
<b>
 <!--Hey, buddy. Want to buy a used parser?-->
</b>

Beautiful Soup 同样定义了 XML 文档中会出现的各种各样的元素,比如 CData ,ProcessingInstruction , Declaration 以及 Doctype 等,像 Comment 一样,所有这些类都是NavigableString 的子类,但是都有它们自己的特性,下面这是一个 CData 示例:

>>> from bs4 import CData
>>> cdata = CData("A CDATA block")
>>> comment.replace_with(cdata)
u'Hey, buddy. Want to buy a used parser?'
>>> print(soup.b.prettify())
<b>
 <![CDATA[A CDATA block]]>
</b>

浏览树对象 (Navigating the tree)

“三姐妹”的HTML文档又来了:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p><b>The Dormouse's story</b></p>

<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p>...</p>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

我还是将使用这个文档来演示如何在树对象中游走。

往内在子元素中检索(Going Down)

Tag 可以包含字符串或者其它 Tag,这些被包裹的元素称为 Tag 的子元素,Beautiful Soup 提供了很多属性用来在 Tag 的子元素中进行导航或者遍历子元素。

注意:Beautiful Soup 字符串没有上面说的这些任何一种属性,因为字符串无法包含子元素。

使用 Tag 名称

最简单的检索树的方法是使用标签名称,如果你想获取 <head> 标签 ,只需要: soup.head :

>>> soup.head
<head><title>The Dormouse's story</title></head>
>>> soup.title
<title>The Dormouse's story</title>

你可以像上面这一样一直往后加添加标签的名称,比如我想获取 <body> 中的 <b> ,则可以使用 soup.body.b :

>>> soup.body.b
<b>The Dormouse's story</b>

使用标签浏览的局限在于我们总是只能得到第一个为该名称的子标签:

>>> soup.a
<a href="http://example.com/elsie" id="link1">Elsie</a>

如果你想得到所有某一个名称的标签,你需要使用搜索树的方法,比如 find_all() :

>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

.contents 与 .children

标签的所有子元素可以通过一个名为 .contents 的列表访问到:

>>> head_tag = soup.head
>>> head_tag
<head><title>The Dormouse's story</title></head>
>>> head_tag.contents
[<title>The Dormouse's story</title>]
>>> title_tag = head_tag.contents[0]
>>> title_tag
<title>The Dormouse's story</title>
>>> title_tag.contents
[u"The Dormouse's story"]

BeautifulSoup 对象只有一个子元素,即 <html> 标签:

>>> len(soup.contents)
1
>>> soup.contents[0].name
'html'

字符串没有任何 .contents ,因为它无法包裹其它数据:

>>> text = title_tag.contents[0]
>>> text.contents
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/bs4/element.py", line 667, in __getattr__
    self.__class__.__name__, attr))
AttributeError: 'NavigableString' object has no attribute 'contents'

除了通过列表 .contents 来访问子元素外,你还可能通过遍历访问它们,使用 .children :

>>> for child in title_tag.children: print(child)
...
The Dormouse's story

.descendants

descendants 是后代的意思,它不像 .contents 或者 .children 只能访问到父元素的下一级子元素,它能访问到父元素内的所有子元素,比如:

>>> head_tag.contents
[<title>The Dormouse's story</title>]

我们只得到了一个子元素,但是:

>>> for child in head_tag.descendants: print(child)
...
<title>The Dormouse's story</title>
The Dormouse's story

可以看到,当得到的子元素有其自己的子元素,那么也会一并的遍历到,这是因为 <head> 虽然只有一个子元素 <title> ,但是 <title> 也有一个子元素,这个子元素同样是 <head> 的后代:

>>> len(list(soup.children))
1
>>> len(list(soup.descendants))
23

.string

如果一个标签有且只有一个子元素,而该子元素是一个 NavigableString ,那么可以直接使用 .string访问到这个子元素:

>>> title_tag.string
u"The Dormouse's story"

如果一个元素有且仅有一个子元素,而该子元素同样有且仅有一个子元素并且该子元素是一个NavigableString ,那么该字符同样是最开始父元素的 .string :

>>> head_tag.contents
[<title>The Dormouse's story</title>]
>>> head_tag.string
u"The Dormouse's story"

但是如果一个元素有多个字符的话,因为我们无法通过一个属性来指定多个值,所以我们直接将该元素的 .string 设定为 None

>>> print(soup.html.string)
None

.strings 与 stripped_strings

像上面说过的,我们无法使用 .string 名称指定多个值,但是却可以使用 .strings 访问到,它是一个生成器(Generator):

>>> for string in soup.strings: print(repr(string))
...
u"The Dormouse's story"
u"The Dormouse's story"
u'n'
u'Once upon a time there were three little sisters; and their names weren'
u'Elsie'
u',n'
u'Lacie'
u' andn'
u'Tillie'
u';nand they lived at the bottom of a well.'
u'n'
u'...'

我在上面的示例中使用了 repr 函数是因为我想告诉你 .strings 会将任何字符串都囊括在内,这在很多时候是不需要的,所以还提供了另一个生成器 .stripped_strings :

>>> for string in soup.stripped_strings: print(repr(string))
...
u"The Dormouse's story"
u"The Dormouse's story"
u'Once upon a time there were three little sisters; and their names were'
u'Elsie'
u','
u'Lacie'
u'and'
u'Tillie'
u';nand they lived at the bottom of a well.'
u'...'

所有空字符串包括只有换行符等等都被去除掉了。

向外在父元素中检索(Going Up)

就像“族谱”一样,任何一个标签一定有它的父元素:

.parent

你可以访问通过 .parent 访问一个标签的父元素,在上面的示例HTML文档中, <head> <title> 的父元素:

>>> title_tag.parent
<head><title>The Dormouse's story</title></head>

顶层的元素的父元素就像Linux系统中的根目录 / 一样,就是 BeautifulSoup 对象本身:

>>> html_tag = soup.html
>>> type(html_tag.parent)
<class 'bs4.BeautifulSoup'>

而 BeautifulSoup 对象的父元素则被定义为 None :

>>> print(soup.parent)
None

.parents

就像 .descendants 可以遍历到一个元素的所有子元素一样,你还可以使用 .parents 遍历一个元素的所有父元素:

>>> link = soup.a
>>> link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> for parent in link.parents:
...     if parent is None:
...         print(parent)
...     else:
...         print(parent.name)
...
p
body
html
[document]

同等级兄弟元素中平行检索(Going Sideways)

考虑一下下面这个HTML文档:

>>> sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></b></a>")
>>> print(sibling_soup.prettify())
<html>
 <body>
  <a>
   <b>
    text1
   </b>
   <c>
    text2
   </c>
  </a>
 </body>
</html>

<b> 与 <c> 是具有同一个父元素的同一级标签,我们称这两个元素为兄弟元素,它们处于文档结构中的同一级,它们之间的这种关系同样可以被用来进行内容的检索。

.next_sibling 与 .previous_sibling

你可以使用 .next_sibling 与 .previous_sibling 检索下一个与上一个元素:

>>> sibling_soup.b.next_sibling
<c>text2</c>
>>> sibling_soup.c.previous_sibling
<b>text1</b>

因为 .next_sibling 与 .previous_sibling 都只是在同一级的兄弟元素中检索,所以,<b> 有 .next_sibling 但是因为它前面没有兄弟元素,所以它的 .previous_sibling 为 None,而 <c> 有 .previous_sibling 但是后面没有兄弟元素,所以它的 .next_sibling 为 None:

>>> print(sibling_soup.b.previous_sibling)
None
>>> print(sibling_soup.c.next_sibling)
None

“text1” 与 “text2” 却不是兄弟元素,因为他们没有同一个父元素:

>>> sibling_soup.b.string
u'text1'
>>> print(sibling_soup.b.string.next_sibling)
None

在真实的文档中, .next_sibling 与 .previous_sibling 得到的一般总是一个空白字符串,回到前面三姐妹的那个示例HTML文档中:

<a href="http://example.com/elsie" id="link1">Elsie</a>
<a href="http://example.com/lacie" id="link2">Lacie</a>
<a href="http://example.com/tillie" id="link3">Tillie</a>

你可以会认为第一个 <a> 的下一个元素是 Lacie,但是并不是这样的:

>>> link = soup.a
>>> link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> link.next_sibling
u',n'
>>> link.next_sibling.next_sibling
<a href="http://example.com/lacie" id="link2">Lacie</a>

这是因为空白也是一个子元素,空白字符串不代表,没有。

.next_siblings 与 .previous_siblings

你可以使用 .next_siblings 与 .previous_siblings 来遍历某一个元素所有前面的兄弟元素或者所有它后面的兄弟元素:

>>> for sibling in soup.a.next_siblings: print(repr(sibling))
...
u',n'
<a href="http://example.com/lacie" id="link2">Lacie</a>
u' andn'
<a href="http://example.com/tillie" id="link3">Tillie</a>
u';nand they lived at the bottom of a well.'
>>> for sibling in soup.find(id='link3').previous_siblings: print(repr(sibling))
...
u' andn'
<a href="http://example.com/lacie" id="link2">Lacie</a>
u',n'
<a href="http://example.com/elsie" id="link1">Elsie</a>
u'Once upon a time there were three little sisters; and their names weren'

根据文档解析器的解析流程进行检索(Going back and forth)

返回来看看最开始的那个 三姐妹 的示例:

<html><head><title>The Dormouse's story</title></head>
<p><b>The Dormouse's story</b></p>

HTML 解析器对上面这段HTML文档的解析可以以一个一个的事件来表示:

  1. 打开一个 <html> 标签
  2. 打开一个 <head> 标签
  3. 打开一个 <title> 标签
  4. 添加一个字符串 “The Dormouse's story”
  5. 关闭 <title> 标签
  6. 关闭 <head> 标签
  7. 打开一个 <p> 标签
  8. ……

BeautifulSoup 提供工具可以让你根据这种流程对文档进行检索

.next_element 与 .previous_element

.next_element 属性指向当前元素的下一个元素,看上去它与 .next_sibling 似乎有些一样,但是其实是完全不同的两码子事儿,比如在三姐妹的示例中, 最后一个 <a> 的 .next_sibling 是一个字符串:

>>> last_a = soup.find('a', id = 'link3')
>>> last_a
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_a.next_sibling
u';nand they lived at the bottom of a well.'

但是 .next_element 却不一样,根据上面所说的章节的说明, 它的值应该是 Tillie 这个词:

>>> last_a.next_element
u'Tillie'

简单来说,next_sibling 或者 previous_sibling 是根据标签或者元素在文档中的最终位置来定义的,但是 .next_element 与 previous_element 则是根据元素的解析流程来定义的。

.previous_element 与 .next_element 正好相反,它表示在当前元素前面被解析的元素:

>>> last_a.previous_element
u' andn'
>>> last_a.previous_element.next_element
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_a
<a href="http://example.com/tillie" id="link3">Tillie</a>

.next_elements 与 .previous_elements

到现在为止你应该找到规律了吧, .next_elements 与 .previous_elements 检索当前元素的所有后面被解析的以及前面被解析的元素:

>>> for element in last_a.next_elements: print(repr(element))
...
u'Tillie'
u';nand they lived at the bottom of a well.'
u'n'
<p>...</p>
u'...'

在树中搜索(Searching the tree)

Beautiful Soup 定义了许多用来搜索解析树的方法,但是所有这些方法都差不多,我将着重花时间来解释两人个使用频率最多的方法 find() 与 find_all() ,其它的方法基本上都是使用同样的参数定义,所以我将对其它的只作简单的介绍。

同样的,我们还是使用三姐妹的示例:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p><b>The Dormouse's story</b></p>

<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p>...</p>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

通过传递一个过滤器到某一个方法(比如 find_all() ),你可以获取到文档中的任何元素。

过滤器类型(Kinds of filters)

在对 find_all() 或者其它相类似的方法进行详细的说明前,我将先向你展示可以传递给这些方法的不同的过滤器类型,这些过滤器在整个搜索API中一次又一次的出现,你可以基于一个标签的名称,或者它的属性,或者一个文本字符串等等创建过滤器。

一个字符串

我简单的过滤器就是一个字符串,通过传递字符串到搜索方法中,Beautiful Soup将返回所有包含该字符的标签的元素:

>>> soup.find_all('b')
[<b>The Dormouse's story</b>]
>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

如果你传递的是一个 byte string, Beautiful Soup 会先将其编码为 UTF-8 字符串,所以你可以直接传递Unicode 字符串。

一个正则表达式

如果你传递一个正则表达式对象, Beautiful Soup 将使用它的 match() 方法对文档进行过滤,下面这段代码将搜索文档中所有以 b 开头的标签的元素:

>>> import re
>>> for tag in soup.find_all(re.compile('^b')): print(tag.name)
...
body
b

下面这段示例代码则搜索所有标签名称中包含 t 的元素:

>>> for tag in soup.find_all(re.compile('t')): print(tag.name)
...
html
title

一个列表

如果你传递的是一个列表,那么 Beautiful Soup 会返回“符合其中任何一个过滤器要求的”元素,下面这段代码将搜索到所有的 <b> 与 <a> 标签:

>>> soup.find_all(['a','b'])
[<b>The Dormouse's story</b>, <a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

True

True 将匹配文档中的所有标签,但是不包括字符串内容:

>>> for tag in soup.find_all(True): print(tag.name)
...
html
head
title
body
p
b
p
a
a
a
p

一个函数

如果上面的所有过滤器中没有一个能满足你的要求,那么你还可以提供自己的过滤函数,该函数需要接收一个标签作为参数,而且只允许接收这一个参数,它应该返回 True 或者 FalseTrue 表示匹配, False 表示不匹配。

下面这个示例函数将匹配所有具有 class 属性但是不没有 id 属性的元素:

>>> def has_class_but_no_id(tag):
...     return tag.has_key('class') and not tag.has_key('id')
...

我们使用这个函数对当前文档进行一些搜索过滤:

>>> soup.find_all(has_class_but_no_id)
[<p><b>The Dormouse's story</b></p>, <p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, <p>...</p>]
>>> for tag in soup.find_all(has_class_but_no_id): print(tag.name)
...
p
p
p

下面我们再定义一个示例函数,它将过滤出所有前后被字符串包裹的元素:

>>> from bs4 import NavigableString
>>> def surrounded_by_strings(tag):
...     return (isinstance(tag.next_element, NavigableString) and isinstance(tag.previous_element, NavigableString))
...
>>> for tag in soup.find_all(surrounded_by_strings): print tag.name
...
p
a
a
a
p

find_all()

函数签名: find_all(name, attrs, recursive, text, limit, kwargs)

find_all() 方法是根据标签的 “ 父-子 ” 关系对文档进行检索,并返回当前匹配过滤器的所有当前元素的子元素,在前面的一些示例中我已经使用过这个函数了,这里再列出一些更多该函数的示例:

>>> soup.find_all('title')
[<title>The Dormouse's story</title>]
>>> soup.find_all('p', 'title')
[<p><b>The Dormouse's story</b></p>]
>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.find_all(id='link2')
[<a href="http://example.com/lacie" id="link2">Lacie</a>]
>>> import re
>>> soup.find(text=re.compile('sisters'))
u'Once upon a time there were three little sisters; and their names weren'

上面这些示例中,有一些与我之前使用的是一样的,但是有一些却是刚刚才使用的新的,比如 text与 id 这两人个参数分别都表示什么?为什么 find_all('p','title') 返回一个 class 为 title 的 <p> 元素?下面让我们来详细的看看传递给 find_all() 方法的参数。

name 参数

传递一个 name 参数,代表你告诉 Beautiful Soup 你只需要名称为参数所指定的标签,文本字符串以及未匹配名称的标签都将直接忽略。

简单的用法示例:

>>> soup.find_all('title')
[<title>The Dormouse's story</title>]

关键字参数

如果一个函数未定义的关键字参数被传递给 find_all() 方法,那么该关键字的键将作为标签的属性进行搜索,而匹配规则是标签的该属性为该关键字参数的值,所以,当我们传递 id=“link2” 时,会匹配到 id 属性值为 link2 的元素:

>>> soup.find_all(id="link2")
[<a href="http://example.com/lacie" id="link2">Lacie</a>]

你可以使用一个字符串、正则表达式、列表、一个函数或者一个 True 值来过滤,比如下面的代码找到所有设定了 id 属性的元素:

>>> soup.find_all(id=True)
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

同样的,你还可以一次性通过多个属性对元素进行过滤:

>>> soup.find_all(href = re.compile('elsie'), id = 'link1' )
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

以 CSS 类为参数搜索

因为 class 是Python的一个关键字,所以我们无法以上面关键字参数的方式通过 class 属性过滤,但是我们有一个替代方案,那就是使用 class_ :

>>> soup.find_all('a', class_='sister')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

这个时候 class_ 就与前面的关键字参数无异,你同样可以使用一个字符、正则表达式等等的作为该参数的值:

>>> soup.find_all(class_=re.compile('itl'))
[<p><b>The Dormouse's story</b></p>]
>>> def has_six_characters(css_class):
...     return css_class is not None and len(css_class) == 6
...
>>> soup.find_all(class_=has_six_characters)
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
注意: class 是一个可多值的属性,当匹配任何其中一个值是,都会算成功匹配,如下示例代码:
>>> css_soup = BeautifulSoup('<p></p>')
>>> css_soup.find_all('p', class_='strikeout')
[<p></p>]
>>> css_soup.find_all('p', class_='body')
[<p></p>]

你也同样可以将完整的 class 值传递给 find_all() :

>>> css_soup.find_all('p', class_='body strikeout')
[<p></p>]

但是对于上例中,如果将两个类返过来写就不能匹配了:

>>> css_soup.find_all('p', class_='strikeout body')
[]

这是很不方便的一件事情,如果我们要通过 class 来搜索元素,这样的方法还需要 class 名称的顺序是统一的,但是我们可以写一个函数:

>>> css_soup = BeautifulSoup('<p></p><p>Another One</p>')
>>> css_soup.find_all('p', class_='body strikeout')
[<p></p>]
>>> css_soup.find_all('p', class_='strikeout body')
[<p>Another One</p>]

对于 class ,Beautiful Soup 还提供了一个快捷方式,那就是 attrs ,传递一个字符串给 attrs 将以 class 对元素进行过滤:

>>> soup.find_all('a', 'sister')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

你同样也可以传递一个正则表达式、函数或者 True,等参数,但是不允许使用字典类型的数据,这些都和使用 class_ 关键字的效果是一样的。

>>> soup.find_all('p', re.compile('itl'))
[<p><b>The Dormouse's story</b></p>]

如果传递的是一个字典的话,它不再仅仅只是当作 class 搜索了,而是以字典中的键为属性名称,值为属性值进行搜索:

>>> soup.find_all(href=re.compile('elsie'), id = 'link1')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> soup.find_all(attrs={'href': re.compile('elsie'), 'id': 'link1'})
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

使用 text 参数

除了对属性进行过滤外,还可以通过文本内容对元素进行过滤,使用的关键字就是 text ,你可以传递一个字符串、正则表达式、列表、函数或者一个 True 值,下面是一些简单的示例:

>>> soup.find_all(text='Elsie')
[u'Elsie']
>>> soup.find_all(text=['Tillie', 'Elsie', 'Lacie'])
[u'Elsie', u'Lacie', u'Tillie']
>>> soup.find_all(text=re.compile('Dormouse'))
[u"The Dormouse's story", u"The Dormouse's story"]
>>> def is_the_only_string_within_a_tag(s):
...     '''Return True if this string is the only child of its parent tag.'''
...     return (s == s.parent.string)
...
>>> soup.find_all(text=is_the_only_string_within_a_tag)
[u"The Dormouse's story", u"The Dormouse's story", u'Elsie', u'Lacie', u'Tillie', u'...']

虽然 text 是用来搜索字符串的,但是你可以通过绑定一个标签名称来搜索标签, Beautiful Soup 会搜索所有 .string 值匹配你所指定的字符串的元素:

>>> soup.find_all('a', text='Elsie')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

使用 limit 参数

limit 参数可以限定返回的结果集元素的数量,当一个文档十分大的时候,这个参数将十分有用,它所做的工作就像SQL中的 LIMIT 一样,在我们的示例文档中,有三个链接,但是我现在想将结果限定为两个:

>>> soup.find_all('a', limit=2)
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>]

使用 recursive 参数

通常的,当你从一个标签中呼叫 find_all() 方法时,它会搜索该函数的子元素,以及子元素的子元素……但是如果你将 recursive 参数设置为 False ,它将只匹配它自己的第一级子元素,而子元素的子元素则直接跳过:

>>> soup.html.find_all('title')
[<title>The Dormouse's story</title>]
>>> soup.html.find_all('title', recursive = False)
[]

下面这是我们示例的HTML代码片段:

<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
...

<title> 虽然是 <html> 标签的子元素,但是并不是它的直属子元素,所以我们将搜索不到。

Beautiful Soup 提供了很多基于树的搜索方法,这些方法基本上都使用与 find_all() 同样的参数,唯一不同的是 recursive 参数只能被使用在 find_all() 与 find() 这两人个方法中,其它的方法都不支持该参数。

像呼叫 find_all() 一样呼叫一个标签

因为 find_all() 是我们使用得多的 Beautiful Soup 搜索API,所以这里还提供了一个呼叫该方法的快捷方式,如果你将一个 Beautiful Soup 对象或者一个 Tag 对象当作一个函数来对待,那么你得到的结果将和使用该对象呼叫 find_all() 方法得到的结果一样:

>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

下面这两人个也是一样的:

>>> soup.title.find_all(text=True)
[u"The Dormouse's story"]
>>> soup.title(text=True)
[u"The Dormouse's story"]

find()

函数签名: find(name, attrs, recursive, text, kwargs)

find_all() 搜索呼叫它的对象的所有内容,但是有时候你仅仅只想搜索一个结果,如果你知道整个HTML文档只有一个 <body> 元素,那么搜索整个文档完全是一种浪费,除了每一次你都指定一个limit=1 这个参数外,你还可以使用一个更简单的方法: find() 。

>>> soup.find_all('title')
[<title>The Dormouse's story</title>]
>>> soup.find('title')
<title>The Dormouse's story</title>

这两者之间唯一的区别是 find_a;;() 返回一个结果集(不管结果集中有多少元素),而 find() 只返回匹配到的一个结果,如果未找到任何结果,那么 find_all() 会返回一个空的列表,但是 find() 则返回None。

>>> print(soup.find_all('nothing'))
[]
>>> print(soup.find('nothing'))
None

如果你还记得 soup.head.title 这种方式的检索的话,那么你应该发现,这两者之间得到的结果是一样的,这是因为它本身就是调用的 find() 接口:

>>> soup.head.title
<title>The Dormouse's story</title>
>>> soup.find('head').find('title')
<title>The Dormouse's story</title>

find_parents() 与 find_parent()

函数签名: find_parents(name, attrs, text, limit, kwargs)

函数签名: find_parent(name, attrs, text, kwargs)

在本文的前面部分已经有很大的篇幅来介绍 find_all() 与 find() 两人个函数了,下面还有十个函数是 Beautiful Soup 提供的搜索API,五个类似于 find_all() ,另外五个类似于 find() 。这些函数之间的唯一区别是他们搜索的范围不同。

首先让我们来看看 find_parents() 与 find_parent() :

>>> a_string.find_parents('a')
[<a href="http://example.com/lacie" id="link2">Lacie</a>]
>>> a_string.find_parents('p')
[<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;

and they lived at the bottom of a well.

] >>> len(a_string.find_parents('p')) 1 >>> print(a_string.find_parents('p', 'title')) []

 

因为 <p> 与 三个 <a> 中的某一个是 a_string 的父元素,所以我们都能搜索得到结果,但是因为 class 为 title 的 <p> 元素并不是a_string的父元素,所以我们这里得不到结果,而 find_parent() 则类似 find() 仅仅只返回一个结果。

>>> a_string.find_parent()
<a href="http://example.com/lacie" id="link2">Lacie</a>
>>> a_string.find_parent('p')
<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

你可能已经与我们前面说过的 .parent 与 .parents 属性联系上的,这两人个属性与我们刚才所了解的find_parent() 与 find_parents() 的关联是很紧密的,这两人个函数本质上就是使用这两个属性进行遍历。

find_next_siblings() 与 find_next_sibling()

函数签名: find_next_siblings(name, attrs, text, limit, kwargs)

函数签名: find_next_sibling(name, attrs, text, kwargs)

这两个函数对 .next_siblings 生成器进行遍历以搜索:

>>> first_link = soup.a
>>> first_link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> first_link.find_next_siblings('a')
[<a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> first_link.find_next_sibling('a')
<a href="http://example.com/lacie" id="link2">Lacie</a>

find_previous_siblings() 与 find_previous_sibling()

函数签名: find_previous_siblings(name, attrs, text, limit, kwargs)

函数签名: find_previous_sibling(name, attrs, text, kwargs)

这两个函数对 .previous_siblings 进行遍历以搜索:

>>> last_link = soup.find('a', id='link3')
>>> last_link
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_link.find_previous_siblings('a')
[<a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> last_link.find_previous_sibling('a')
<a href="http://example.com/lacie" id="link2">Lacie</a>

find_all_next() 与 find_next()

函数签名: find_all_next(name, attrs, text, limit, kwargs)

函数签名: find_next(name, attrs, text, kwargs)

这两个函数对 .next_elements 进行遍历以搜索:

>>> first_link = soup.a
>>> first_link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> first_link.find_all_next(text=True)
[u'Elsie', u',n', u'Lacie', u' andn', u'Tillie', u';nand they lived at the bottom of a well.', u'n', u'...']
>>> first_link.find_next('p')
<p>...</p>

find_all_previous() 与 find_previous()

函数签名: find_all_previous(name, attrs, text, limit, kwargs)

函数签名: find_previous(name, attrs, text, kwargs)

这两个函数对 .next_previous 进行遍历以搜索:

>>> last_link = soup.find('a', id='link3')
>>> last_link
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_link.find_all_previous('p')
[<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, <p><b>The Dormouse's story</b></p>]
>>> last_link.find_previous('title')
<title>The Dormouse's story</title>

CSS 选择器

CSS 选择器是一个十分强大的工具,Beautiful Soup 也提供了 CSS selector standard 定义的很多选择器,你仅仅只需要将选择器作为一个字符串传递给 .select() 即可:

你可以搜索标签:

>>> soup.select("title")
[<title>The Dormouse's story</title>]

或者标签中所有的子元素标签:

>>> soup.select('body a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

或者搜索某一个标签的下一层标签:

>>> soup.select('head > title')
[<title>The Dormouse's story</title>]
>>> soup.select('p > a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.select('body > a')
[]

通过 CSS class 搜索标签:

>>> soup.select('.sister')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

>>> soup.select('[class~=sister]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

或者通过ID搜索:

>>> soup.select('#link1')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> soup.select('a#link1')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

通过检测一个标签的属性是否存在搜索:

>>> soup.select('a[href]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

或者通过指定标签属性的值来搜索:

>>> soup.select('a[href="http://example.com/elsie"]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> soup.select('a[href^="http://example.com/"]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.select('a[href$="tillie"]')
[<a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.select('a[href*=".com/el"]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

匹配语言代码:

>>> multilingual_markup = """
...  <p lang="en">Hello</p>
...  <p lang="en-us">Howdy, y'all</p>
...  <p lang="en-gb">Pip-pip, old fruit</p>
...  <p lang="fr">Bonjour mes amis</p>
... """
>>> multilingual_soup = BeautifulSoup(multilingual_markup)
>>> multilingual_soup.select('p[lang|=en]')
[<p lang="en">Hello</p>, <p lang="en-us">Howdy, y'all</p>, <p lang="en-gb">Pip-pip, old fruit</p>]

该功能只要求用户有一定的CSS选择器知识即可使用。

修改树(Modifying the tree)

Beautiful Soup 的主要任务被设计为从现有文档中搜索与检索数据,但是你同样可以对文档的对象树进行修改并将其保存到一新的HTML或者XML文档中。

修改标签名称及属性

在本文的前面部分我就已经使用该功能做过示例,你可以重新为一个标签命名,修改一个标签的属性的值或者添加新属性以及删除已有数据等等:

>>> soup = BeautifulSoup('<b>Extremely bold</b>')
>>> tag = soup.b
>>> tag.name = 'blockquote'
>>> tag
<blockquote>Extremely bold</blockquote>
>>> tag['class'] = 'verybold'
>>> tag
<blockquote>Extremely bold</blockquote>
>>> tag['id'] = 1
>>> tag
<blockquote id="1">Extremely bold</blockquote>
>>> del tag['class']
>>> tag
<blockquote id="1">Extremely bold</blockquote>
>>> del tag['id']
>>> tag
<blockquote>Extremely bold</blockquote>

修改标签的 .string

如果你设置一个标签的 .string 属性,那么它的 contents 值将被修改为你设定的 .string 值:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.string = "New link text."
>>> tag
<a href="http://example.com/">New link text.</a>
注意:如果一个标签包含很多子标签,那么你使用这种方法修改 .string 值将会使用该值替代原来的所有内容,包括它的子标签等等。

追加- append()

你可以通过 Tag.append() 来添加标签的内容,它就像在Python List上使用 .append() 一样:

>>> soup = BeautifulSoup("<a>Foo</a>")
>>> soup.a.append("Bar")
>>> soup
<html><body><a>FooBar</a></body></html>
>>> soup.a.contents
[u'Foo', u'Bar']

BeautifulSoup.new_string() 与 .new_tag()

如果你需要向文档中添加字符串,你可以使用 append() 方法,或者你可以使用工厂方法BeautifulSoup.new_string() :

>>> soup = BeautifulSoup("<b></b>")
>>> tag = soup.b
>>> tag.append("Hello")
>>> new_string = soup.new_string(" there")
>>> tag.append(new_string)
>>> tag
<b>Hello there</b>
>>> tag.contents
[u'Hello', u' there']

如果你想插入一个完整的新的标签而不仅仅是往标签中插入内容,那最好的办法是使用BeautifulSoup.new_tag() 方法:

>>> soup = BeautifulSoup("<b></b>")
>>> original_tag = soup.b
>>> new_tag = soup.new_tag("a", href="http://www.example.com")
>>> original_tag.append(new_tag)
>>> original_tag
<b><a href="http://www.example.com"></a></b>
>>> new_tag.string = 'Link Text.'
>>> original_tag
<b><a href="http://www.example.com">Link Text.</a></b>

该方法仅仅只有第一个参数——标签名称是必须的。

插入- insert()

Tag.insert() 就像 Tag.append() 一样,不同在于它不仅仅只是将新标签添加到父元素 .contents 属性的最后,而可以将新标签添加到任何一个你所指定的位置,它就像 Python list中的 insert() 函数一样:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.insert(1, "but did not endorse ")
>>> tag
<a href="http://example.com/">I linked to but did not endorse <i>example.com</i></a>
>>> tag.contents
[u'I linked to ', u'but did not endorse ', <i>example.com</i>]

在前面或者后面插入 - insert_before() 与 insert_after()

insert_before() 方法会在标签或者字符串的前面立马插入新内容:

>>> soup = BeautifulSoup("<b>stop</b>")
>>> tag = soup.new_tag("i")
>>> tag.string = "Don't"
>>> soup.b.string.insert_before(tag)
>>> soup.b
<b><i>Don't</i>stop</b>

insert_after() 则是在标签或字符的后面立马插入新内容:

>>> soup.b.i.insert_after(soup.new_string(" ever "))
>>> soup.b
<b><i>Don't</i> ever stop</b>
>>> soup.b.contents
[<i>Don't</i>, u' ever ', u'stop']

清除- clear()

Tag.clear() 移除一个标签的所有内容:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.clear()
>>> tag
<a href="http://example.com/"></a>

extract()

PageElement.extract() 将一个标签从对象树中完全移除,并返回被移除的标签本身:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> i_tag = soup.i.extract()
>>> a_tag
<a href="http://example.com/">I linked to </a>
>>> i_tag
<i>example.com</i>
>>> print(i_tag.parent)
None

现在你得到了两人个根对象 BeautifulSoup 对象和另一个 extract 出来的对象,你可以继续对这个对象使用同样的方法:

>>> my_string = i_tag.string.extract()
>>> my_string
u'example.com'
>>> print(my_string.parent)
None
>>> i_tag
<i></i>

decompose()

decompose() 方法将一个标签从树中移除,然后 完全的删除它以及它的内容 。

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> soup.i.decompose()
>>> a_tag
<a href="http://example.com/">I linked to </a>

replace_with()

PageElement.replace_with() 删除一个标签或者一个字符串,然后使用一个你指定的标签或者字符串代替原来被删除的标签或者字符的位置:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> new_tag = soup.new_tag("b")
>>> new_tag.string = "example.net"
>>> a_tag.i.replace_with(new_tag)
<i>example.com</i>
>>> a_tag
<a href="http://example.com/">I linked to <b>example.net</b></a>

replace_with() 返回被移除的标签或者字符,这使得你可以在稍后重新将其加入文档或者插入到其它地方去。

wrap()

PageElement.wrap() 将你给定的元素使用你指定的标签包裹,它返回新的容器:

>>> soup = BeautifulSoup("<p>I wish I was bold.</p>")
>>> soup.p.string.wrap(soup.new_tag("b"))
<b>I wish I was bold.</b>
>>> soup.p.wrap(soup.new_tag("div"))
<div><p><b>I wish I was bold.</b></p></div>

unwrap()

Tag.unwrap() 是 wrap() 的反向操作,它使用标签中的内容来替换标签本身:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> a_tag.i.unwrap()
<i></i>
>>> a_tag
<a href="http://example.com/">I linked to example.com</a>

与 replace_with() 一样,unwrap() 返回替换前的原始标签。

输出(Output)

美化后输出

prettify() 方法将一个 Beautiful Soup 解析树格式化为一个美化的 Unicode 字符,每一个标签将独占一行,并进行合理的缩进:

    >>> str(soup)
    '<html><body><a href="http://example.com/">I linked to <i>example.com</i></a></body></html>'