golang 开荒 - 3 - 用原生GO写一个分布式爬虫-单任务版

    技术2026-08-06  15

    目标: 获取并打印所有城市第一页用户的详细信息

    下载用于处理字符集的官方包,把GBK国民转为UTF-8

    go get -g -v golang.org/x/text

    这样在 $gopath/src/text/ 下面就会有对应的编码, 在代码中用如下语法可以把GBK等其他编码转为UTF-8等

    找到有用的信息

    总体逻辑

    城市列表解析器 》 单个城市解析器 》 单个用户解析器

    css 选择器

    golang 有第三方库支持css选择器

    $("#cityList>dd>a')

    xpath

    正则 - 解析器

    // [^>] : 非 > 的字符 re := regexp.MustCompile(`<a href="(http://www.zhenai.com/zhenghun/[0-9a-zA-Z]+)"[^>]*>([^<]+)</a>`) // -1 表示找到所有的匹配regexp 的字串 matches := re.FindAllSubmatch(contents,-1) /** 城市列表解析器 */ func printCity(contents []byte) { // [^>] : 非 > 的字符 re := regexp.MustCompile(`<a href="(http://www.zhenai.com/zhenghun/[0-9a-zA-Z]+)"[^>]*>([^<]+)</a>`) // -1 表示找到所有的匹配regexp 的字串 matches := re.FindAllSubmatch(contents, -1) //for _, m := range matches { // for _, submatch := range m{ // // 这里 %s 后面只用空格, 不用换行 // //fmt.Printf("%s ", submatch) // fmt.Printf("%s \n", submatch) // // } // fmt.Println() //} // 显示 : City: 淄博, URL: http://www.zhenai.com/zhenghun/zibo for _, m := range matches{ fmt.Printf("City: %s, URL: %s \n",m[2],m[1]) } fmt.Printf("matches total num: %d \n", len(matches)) }

    单任务版爬虫架构

    request : 数据交互的桥梁的通道, 本项目就是没爬过的 url fetcher : 从网络上获取html内容的模板(对应utf-8), engine : 从队列里面取任务, 然后用fetcher 去拿html 数据, 拿到数据后把文本给解析器,解析器返回解析后的数据和新的任务(如发现页面里还有其他页面的链接),再加入 任务队列 paser : 解析器,接收 fetcher 取到的 html 代码,开始爬,同时拿到关联的 url, 再加到任务队列里面

    Engine

    package engine import ( "log" "mods/pachong/fetcher" ) // ...Request 表示接受多个 Request func Run(seeds ...Request){ // 把seeds中的每一个request 传到 requests中 var requests []Request for _, r := range seeds{ requests = append(requests, r) } // 遍历requests for len(requests) > 0{ r := requests[0] requests = requests[1:] log.Printf("fetching %s", r.Url) // 用fetcher 取到html代码 body,err := fetcher.Fetch(r.Url) // 这里报错需要忽略,不然爬虫会终止 if err != nil{ log.Printf("Fetcher: error "+ "fetching url %s: %v",r.Url,err) } // 用解析器 进行页面的解析,得到解析结果 parseResult parseResult := r.ParserFunc(body) // 把解析结果中的 request 拿出来,加入队列 // 注意这里的写法,三个点,就不用传下标了 parseResult.Requests... requests = append(requests, parseResult.Requests...) // 把解析结果中的items 取出来 (这里方便演示,就打印出来) for _, item := range parseResult.Items{ log.Printf("got item %v", item) } } }

    Fetcher

    package fetcher import ( "bufio" "fmt" "golang.org/x/net/html/charset" "golang.org/x/text/encoding" _ "golang.org/x/text/encoding/simplifiedchinese" unicode2 "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" "io/ioutil" "log" "net/http" "strings" ) func determineEncoding(r *bufio.Reader)encoding.Encoding{ bytes, err := r.Peek(1024) if err!= nil{ // 如果 Peek 不出来,就返回一个默认的 Encoding log.Printf("fetcher error : %v",err) return unicode2.UTF8 } e,_,_ := charset.DetermineEncoding(bytes, "") return e } func Fetch(url string)([]byte ,error) { // 自动发现网页编码,猜文件的编码 // charset.DetermineEncoding() //Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36 /* 会返回 403 */ //resp, err := http.Get(url) //if err != nil { // return nil, err //} /* 会返回202 */ //client := &http.Client{} //req, err := http.NewRequest("GET", url, nil) //if err != nil { // panic(err) // return nil, err //} //req.Header.Set("User-Agent", // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36") // //resp, err := client.Do(req) //if err != nil { // panic(err) //} /* 更新cookie */ client := &http.Client{} newUrl := strings.Replace(url, "http://", "https://", 1) req, err := http.NewRequest("GET", newUrl, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36") // 这里要自己去网页里面拿cookie cookieTmp := "xxxx...." req.Header.Add("cookie", cookieTmp) resp, err := client.Do(req) if err != nil { log.Fatalln(err) } // close the resp sooner or later defer resp.Body.Close() // 判断头部信息 if resp.StatusCode != http.StatusOK { return nil, // 生成error fmt.Errorf( "wrong status code: %d",resp.StatusCode) } bodyReader := bufio.NewReader(resp.Body) e := determineEncoding(bodyReader) utf8Reader :=transform.NewReader(bodyReader,e.NewDecoder()) // 这里不取header, 只取body return ioutil.ReadAll(utf8Reader) }

    打印网页爬取到的具体数据

    httputil.DumpResponse(resp *http.Response, body bool)

    解析器 Parse, 分三个函数

    ParseCitylist: 首页城市列表 package parser import ( "mods/pachong/engine" "regexp" ) const cityListRe = `<a href="(http://www.zhenai.com/zhenghun/[0-9a-zA-Z]+)"[^>]*>([^<]+)</a>` func ParseCityList( contents []byte) engine.ParseResult{ // [^>] : 非 > 的字符 re := regexp.MustCompile(cityListRe) // -1 表示找到所有的匹配regexp 的字串 matches := re.FindAllSubmatch(contents, -1) result := engine.ParseResult{} // 这里的每个URL 要生成一个新的 Request for _, m := range matches{ // 这里的城市名返回 result.Items = append(result.Items,string(m[2])) // 构造 Request 结构中的 ParseResult 结构 result.Requests = append(result.Requests, engine.Request{ Url: string(m[1]), // 这里的parser 就是对城市页面下面的 用户列表的parser ParserFunc: ParseCity, }) } return result } ParseCity: 城市页面下面的人 package parser import ( "mods/pachong/engine" "regexp" ) const cityRe = `<a href="(http://album.zhenai.com/u/[0-9]+)" [^>]*>([^<]+)</a>` func ParseCity(contents []byte) engine.ParseResult{ re := regexp.MustCompile(cityRe) matches := re.FindAllSubmatch(contents,-1) result := engine.ParseResult{} for _, m := range matches{ name := string(m[2]) result.Items = append(result.Items, "User "+string(m[2])) result.Requests = append( result.Requests, engine.Request{ Url: string(m[1]), // 这里要保留所有的name, 所以用闭包 和 return ParserFunc: func(bytes []byte) engine.ParseResult { return ParseProfile(bytes,name) }, }) } return result } ParseProfile: 个人页面信息 package parser import ( "github.com/bitly/go-simplejson" "log" "mods/pachong/engine" "mods/pachong/model" "regexp" "strconv" "strings" ) // json串 //<script>window.__INITIAL_STATE__={"objectInfo":{"age":50, //"avatarPhotoID":335980841,"avatarPraiseCount":0,"avatarPraised" var re = regexp.MustCompile(`<script>window.__INITIAL_STATE__=(.+);\(function`) func ParseProfile(contents []byte, name string) engine.ParseResult { match := re.FindSubmatch(contents) result := engine.ParseResult{} if len(match) >= 2 { json := match[1] profile := parseJson(json) profile.Name = name // 放解析的数据插入 Items result.Items = append(result.Items, profile) } return result } // 解析json func parseJson(json []byte) model.Profile { res, err := simplejson.NewJson(json) if err != nil { log.Println("parsing json error...") } // 判断是否是数组 infos, err := res.Get("objectInfo").Get("basicInfo").Array() var profile model.Profile for k, v := range infos { /* "basicInfo":[ "未婚", "25岁", "魔羯座(12.22-01.19)", "152cm", "42kg", "工作地:阿坝茂县", "月收入:3-5千", "医生", "大专" ], */ if e, ok := v.(string); ok { switch k { case 0: profile.Marriage = e case 1: profile.Age, err = strconv.Atoi(e) if err != nil { } case 2: profile.Xingzuo = e case 3: profile.Height, err = strconv.Atoi(e) if err != nil { } case 4: profile.Weight, err = strconv.Atoi(e) if err != nil { } case 6: profile.Income = e case 7: profile.Occupation = e case 8: profile.Education = e } } } infos2, err := res.Get("objectInfo").Get("detailInfo").Array() /* "detailInfo": ["汉族", "籍贯:江苏宿迁", "体型:富线条美", "不吸烟", "不喝酒", "租房", "未买车", "没有小孩", "是否想要孩子:想要孩子", "何时结婚:认同闪婚"], 汉族籍贯:安徽合肥体型:运动员型稍微抽一点烟社交场合会喝酒已购房已买车有孩子且住在一起是否想要孩子:视情况而定何时结婚:一年内 */ for _, v := range infos2 { // 从中取几个值 if e, ok := v.(string); ok { if strings.Contains(e, "族") { profile.Hukou = e } else if strings.Contains(e, "房") { profile.House = e } else if strings.Contains(e, "车") { profile.Car = e } } } gender, err := res.Get("objectInfo").Get("genderString").String() profile.Gender = gender return profile }

    main.go

    从调用主页面开始爬

    engine.Run(engine.Request{ Url:"http://www.zhenai.com/zhenghun", ParserFunc : parser.ParseCityList, })

    status code:403 错误

    开始用的这个,到了用户页面就会报403

    resp, err := http.Get(url) if err != nil { return nil, err }

    后来改这个,不报403, 报202了

    client := &http.Client{} req, err := http.NewRequest("GET", url, nil) if err != nil { panic(err) return nil, err } req.Header.Set("User-Agent", `xxxx`)//浏览器中User-agent resp, err := client.Do(req) if err != nil { panic(err) }

    原因是cookie限制,做了反爬虫机制,如下方法配合手动访问,然后添加cookie

    client := &http.Client{} newUrl := strings.Replace(url, "http://", "https://", 1) req, err := http.NewRequest("GET", newUrl, nil) if err != nil { panic(err) return nil, err } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36") User-agent cookie1 := "xxx" req.Header.Add("cookie", cookie1) resp, err := client.Do(req) if err != nil { log.Fatalln(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("wrong status code: %d", resp.StatusCode) }

    问题 cannot use e (type string) as type int in assignment:

    strconv.Atoi()
    Processed: 0.009, SQL: 9