Python爬蟲之xpath語法及案例使用 鋼鐵俠的知識庫 2022.08.15 我們在寫Python爬蟲時,經常需要對網頁提取信息,如果用傳統正則表達去寫會增加很多工作量,此時需要一種對數據解析的方法,也就是本章要介紹的Xpath表達式。 Xpath是什麼 XPath,全稱 XML Path La ...
背景
Golang語言本身未實現set,但是實現了map
golang的map是一種無序的鍵值對的集合,其中鍵是唯一的
而set是鍵的不重覆的集合,因此可以用map來實現set
Empty
由於map是key-value集合,如果使用map來實現set,則不需要關註value的具體類型和值
struct{}是具有零個元素的struct,struct{}的大小為0,不占用空間,因此十分適合作為value使用
type Empty struct{}
Int64HashSet
Golang是靜態強類型語言,對於int8、uint8、int64、uint64、 string基礎數據類型的set,均需要實現類似的代碼
定義
type Int8HashSet map[int8]Empty type UintHashSet map[uint8]Empty type Int64HashSet map[int64]Empty type Uint64HashSet map[uint64]Empty type Int64HashSet map[string]Empty
以int64為例,實現set的基本操作
初始化
func NewInt64HashSet(cap ...int) Int64HashSet { var set Int64HashSet if len(cap) == 0 { set = make(Int64HashSet) } else { set = make(Int64HashSet, cap[0]) } return set }
插入
func (set Int64HashSet) Insert(items ...int64) { for _, item := range items { set[item] = Empty{} } }
刪除
func (set Int64HashSet) Delete(items ...int64) { for _, item := range items { delete(set, item) } }
列表
func (set Int64HashSet) List() []int64 { list := make([]int64, 0, len(set)) for item := range set { list = append(list, item) } return list }
弊端
採用上面的方法實現,會充斥著大量重覆代碼,對於其它類型如int8,uint8,string等類型,需要單獨實現,儘管邏輯基本一致。
在Go 1.18版本之前,我們可以使用反射來避免這個問題,
使用反射在運行時推斷具體的類型,雖然有性能上的損耗,但是單次納秒級別的操作,基本可以忽略不計。
HashSet
interface{}是沒有方法的空介面,所有類型都實現了空介面
通過反射可以從interface獲取對象的值和類型
定義
type HashSet map[interface{}]Empty
初始化
func NewHashSet(cap ...int) HashSet { var set HashSet if len(cap) == 0 { set = make(HashSet) } else { set = make(HashSet, cap[0]) } return set }
插入
func (set HashSet) Insert(items ...interface{}) { for _, item := range items { set[item] = Empty{} } }
刪除
func (set HashSet) Delete(items ...interface{}) { for _, item := range items { delete(set, item) } }
列表
// 通過反射獲取到具體的類型 // 可以將int64替換為其它類型,如uint8, string等 func (set HashSet) ListInt64() []int64 { list := make([]int64, 0, len(set)) for item := range set { if val, ok := item.(int64); ok { list = append(list, val) } } return list } func (set HashSet) ListString() []string { list := make([]string, 0, len(set)) for item := range set { if val, ok := item.(string); ok { list = append(list, val) } } return list }
GenericHashSet
反射在編譯時缺少類型檢查,比如對於同一個set,先後插入int類型和string類型數據,在編譯和運行階段均不會報錯。
hash := NewHashSet(8) // 插入int類型 hash.Insert(111) // 插入string類型 hash.Insert("string")
使用反射在一定程度上避免了大量的重覆代碼,但是將set轉換為slice還是會存在重覆的相似邏輯的代碼
並且需要在運行時獲取/判斷對象的類型和值,存在一定的性能損耗
在Go 1.18版本提供了範型(Generics)的支持,
範型可以在編譯期間進行類型檢查和類型推斷,相對於反射機制而言,性能有所提升
定義
type GenericHashSet[T comparable] map[T]Empty
初始化
func NewGenericHashSet[T comparable](cap ...int) *GenericHashSet[T] { var set GenericHashSet[T] if len(cap) == 0 { set = make(GenericHashSet[T]) } else { set = make(GenericHashSet[T], cap[0]) } return &set }
插入
func (set *GenericHashSet[T]) Insert(items ...T) { for _, item := range items { (*set)[item] = Empty{} } }
刪除
func (set *GenericHashSet[T]) Delete(items ...T) { for _, item := range items { delete(*set, item) } }
列表
func (set *GenericHashSet[T]) List() []T { list := make([]T, 0, len(*set)) for item := range *set { list = append(list, item) } return list }
性能對比
插入操作測試代碼
func BenchmarkInt64HashSetInsert(b *testing.B) { intHashSet := NewInt64HashSet() rand.Seed(time.Now().UnixNano()) for i := 0; i < b.N; i++ { intHashSet.Insert(rand.Int63()) } } func BenchmarkGenericHashSetInsert(b *testing.B) { gHashSet := NewGenericHashSet[int64]() rand.Seed(time.Now().UnixNano()) for i := 0; i < b.N; i++ { gHashSet.Insert(rand.Int63()) } } func BenchmarkHashSetInsert(b *testing.B) { hashSet := NewHashSet() rand.Seed(time.Now().UnixNano()) for i := 0; i < b.N; i++ { hashSet.Insert(rand.Int63()) } }
插入操作測試結果
zbwdeAir:set zbw$ go test -bench="BenchmarkInt64HashSetInsert|BenchmarkGenericHashSetInsert|BenchmarkHashSetInsert" -benchmem goos: darwin goarch: arm64 pkg: set/set BenchmarkInt64HashSetInsert-8 10051916 119.2 ns/op 40 B/op 0 allocs/op BenchmarkGenericHashSetInsert-8 13957741 123.7 ns/op 57 B/op 0 allocs/op BenchmarkHashSetInsert-8 6526810 188.9 ns/op 63 B/op 1 allocs/op PASS ok set/set 4.897s
可以看出來,Int64HashSet性能最優,GenericHashSet次之,HashSet性能最差。
從實際使用角度看
對於Go < 1.18版本,使用HashSet即可。如果追求性能的極致,不介意大量重覆代碼,那還是使用Int64HashSet
對於單次操作的時間在ns級別,對於大部分業務場景,反射帶來的性能損耗基本可以忽略,性能的瓶頸並不在這裡。
對於Go >= 1.18版本,可以使用GenericHashSet
其它
如果需要實現有序set,則需要鏈表輔助實現
詳細代碼,見github
如果你覺得還可以,點一下Star