前言 本文使用網路上的短網址服務 來實作一個並行的爬蟲。
此服務的短網址有兩個特性:
有效期限
由非會員所生成的短網址只有 14 天的有效期限。
這個特性導致了一種可能性,比方說 /a
短網址所儲存的圖片為一隻貓,過了 14 天後,由於 a
這個代碼被釋放了,下一個使用者上傳了一隻狗的圖片,剛好又被分配到了 a
這個代碼,於是 /a
這個短網址所儲存的圖片就變成了狗。
使用者應該注意這個特性,在必要時註冊會員,以避免短網址很快被取代。
編碼
生成的短網址由其網域名稱和一個 Base52 的編碼所組成。所謂 Base52 是由 ASCII 字元 a-z 和 A-Z 所組成的表示方法。
由於其有效期限的特性,導致這個服務在使用量不大的情況下,所生成的短網址代碼最多就只有 3 碼。
這個特性也導致了一種可能性,由於代碼是 3 位 Base52 的字元,所以其所有組合為 52 的 3 次方,即 140,608 種。一般人可以隨意輸入代碼就存取到其他短網址。
當然,這個服務有提供密碼功能,使用者應該善用密碼。
實作 首先建立一個 Letters()
函式,在 Base52 的情況下,這個函式會生成一個元素由 a 到 Z 所組成的陣列,這個陣列總共會有 52 個元素。
1 2 3 4 5 6 7 8 9 10 func Letters (base int ) []string { letters := make ([]string , base) for i := 0 ; i < base/2 ; i++ { letters[i], letters[i+base/2 ] = string ('a' +i), string ('A' +i) } return letters }
再建立一個 Code()
函式,實現進位系統。在 Base52 的情況下,可以用數字來取得字元。比如輸入 1 可以得到 a、輸入 2 可以得到 b,如果輸入 53 則可以得到 aa,以此類推。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func Code (num int , base int ) string { code := "" letters := Letters(base) for num > 0 { num-- code = letters[num%base] + code num /= base } return code }
建立一個 generateCodes()
函式,建立一個元素由所有組合的代碼所組成的陣列,並且將元素的順序打亂。在 Base52 的情況下,這個函式會輸出包括從 a 到 ZZZ 所有元素的陣列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func generateCodes (nums int ) []string { codes := make ([]string , nums) for i := 0 ; i < nums; i++ { codes[i] = helper.Code(i, base) } rand.Seed(time.Now().UnixNano()) rand.Shuffle(len (codes), func (i, j int ) { codes[i], codes[j] = codes[j], codes[i] }) return codes }
建立主程式,以發送請求並下載圖片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 func Handle () { codes := generateCodes(amount) codeChan := make (chan string ) imageChan := make (chan Image) go func () { for { for _, code := range codes { codeChan <- code } } }() for i := 0 ; i < concurrency; i++ { go func () { for code := range codeChan { image := fetchImage(code) go func () { defer helper.Measure(time.Now(), "fetch" ) imageChan <- image }() time.Sleep(time.Duration(86400 *concurrency/amount) * time.Second) } }() } for image := range imageChan { if len (image.FileInfos) > 0 { image.download() } } }
宣告一些會頻繁使用到的常數:
1 2 3 4 5 6 const ( baseURL string = "https://risu.io/" base int = 52 amount int = base * base * base concurrency int = 10 )
宣告取得的圖片結構體:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Image struct { Code string FileInfos []FileInfo `json:"file_infos"` } type FileInfo struct { Filename string `json:"filename"` ContentType string `json:"content_type"` ByteSize string `json:"byte_size"` FilePath string `json:"file_path"` CreatedAt string `json:"created_at"` }
建立一個 setCode()
方法,用來設置圖片結構體的代碼:
1 2 3 func (image *Image) setCode(code string ) { image.Code = code }
建立一個 download()
方法,用來下載圖片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func (image *Image) download() error { defer helper.Measure(time.Now(), "download" ) date, err := time.Parse("2006-01-02 15:04:05" , image.FileInfos[0 ].CreatedAt) if err != nil { log.Panicln(err) } name := fmt.Sprintf("storage/%s_%s.jpg" , date.Format("20060102150405" ), image.Code) url := image.FileInfos[0 ].FilePath return storeImage(name, url) }
建立一個 storeImage()
函式,用來發送請求,並將圖片儲存到本地。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func storeImage (path string , url string ) error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() file, err := os.Create(path) if err != nil { return err } defer file.Close() _, err = io.Copy(file, resp.Body) return err }
建立一個 fetchImage()
函式,用來發送請求取得圖片資訊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 func fetchImage (code string ) Image { var image Image client := &http.Client{ Timeout: time.Duration(10 * time.Second), Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true , }, }, } req, err := http.NewRequest("GET" , baseURL+code, nil ) if err != nil { return image } resp, err := client.Do(req) if err != nil { return image } defer resp.Body.Close() doc, err := html.Parse(resp.Body) if err != nil { return image } node := getNode(doc) if err = json.Unmarshal([]byte (node), &image); err != nil { return image } image.setCode(code) return image }
建立一個 getNode()
方法,用來解析 HTML,並取得圖片資訊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func getNode (n *html.Node) string { node := "" var f func (*html.Node) f = func (n *html.Node) { if n.Type == html.ElementNode && n.Data == "page-image" { for _, a := range n.Attr { node = a.Val } } for c := n.FirstChild; c != nil ; c = c.NextSibling { f(c) } } f(n) return node }
結論 這個短網址服務有一些地方可以改善:
代碼數量
使用極短的代碼來實現短網址,容易遭人任意存取,產生意想不到的風險。因此在做短網址服務時,可以考慮使用 Base64,或者至少要有 5 至 6 位的代碼。
警告標語
為了保護使用者,應該明確提醒使用者應該加上密碼,以避免一些含有個人資料的圖片遭他人存取。
程式碼