用 golang 可以很方便的爬图(http 下载图片,存储为 jpg 格式)。
一、http client
http client 有如下最佳实践:
- 尽量用 default http client:默认的 http client 设置了很多合适的参数(如超时、连接池),也便于在各请求间复用(如先将获取到的 token 存储到 client 内,后续所有请求都可用此 token)
- 要设置 timeout:防止因网络 or 下游 server 的问题,而盲等,导致自己阻塞,降低拥堵体验
- 谨慎处理 redirects:若 server 不存在某资源时可能会用 3xx redirect 到新目的地。然而若 client 未合适处理,可能导致 loop redirect。因此需要如下做法:
- 校验 redirect url
- 限制 redirect 次数
- reuse connections:可用连接池实现,可减少新建连接的开销、降低延迟、增加流量。
- 默认会打开 Keep-Alive,除非需求是在一个连接上的数据请求密度不高则可手动关闭。详见实验
二、反向代理
2.1 http
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
// NewProxy 拿到 remoteURL 后,创建一个反向代理
func NewProxy(remoteURL string) (*httputil.ReverseProxy, error) {
remote, err := url.Parse(remoteURL)
if err != nil {
return nil, err
}
proxy := httputil.NewSingleHostReverseProxy(remote)
proxy.Director = func(req *http.Request) {
//req.Header = ctx.Request.Header
req.Host = remote.Host
req.URL.Scheme = remote.Scheme
req.URL.Host = remote.Host
req.URL.Path = "/metrics"
}
return proxy, nil
}
// ProxyRequestHandler 使用 proxy 处理请求
func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { proxy.ServeHTTP(w, r) }
}
func main() {
proxy, err := NewProxy("http://127.0.0.1:7000/metrics") // 初始化反向代理并传入真正后端服务的地址
if err != nil {
panic(err)
}
http.HandleFunc("/api/proxy/metrics", ProxyRequestHandler(proxy)) // 使用 proxy 处理所有请求到你的服务
log.Fatal(http.ListenAndServe(":8080", nil))
}
2.2 gin
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"net/http/httputil"
"net/url"
)
// NewProxy 拿到 remoteURL 后,创建一个反向代理
func NewProxy(remoteURL string) (*httputil.ReverseProxy, error) {
remote, err := url.Parse(remoteURL)
if err != nil {
return nil, err
}
proxy := httputil.NewSingleHostReverseProxy(remote)
proxy.Director = func(req *http.Request) {
//req.Header = ctx.Request.Header
req.Host = remote.Host
req.URL.Scheme = remote.Scheme
req.URL.Host = remote.Host
req.URL.Path = "/metrics"
}
return proxy, nil
}
func proxy(c *gin.Context) {
proxy, err := NewProxy("http://127.0.0.1:7000/metrics") // 初始化反向代理并传入真正后端服务的地址
if err != nil {
panic(err)
}
proxy.ServeHTTP(c.Writer, c.Request)
}
func main() {
r := gin.Default()
r.GET("/api/proxy/metrics", proxy)
r.Run(":8080")
}
三、爬取并存储 jpg
package clients
import (
"fmt"
"github.com/sirupsen/logrus"
"io"
"net/http"
"os"
"strings"
"time"
)
var httpCli = &http.Client{
Timeout: 10 * time.Second,
}
type spiderCli struct {
httpCli *http.Client
txtFile *os.File
}
func Spider() *spiderCli {
txtFile, err := os.OpenFile(configs.TxtFileName(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
logrus.Fatal(err)
}
return &spiderCli{
httpCli: httpCli,
txtFile: txtFile,
}
}
func (c *spiderCli) Download(url string) ([]byte, error) {
res, err := c.httpCli.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("got invalid status code: %v", res.StatusCode)
}
return io.ReadAll(res.Body)
}
// Save img.jpg and txt
func (c *spiderCli) Save(fid int64, bytes []byte, plateText, plateColor, plateType string) error {
// img
fImgName := fmt.Sprintf("%v.jpg", fid)
fImg, err := os.Create(fImgName)
if err != nil {
return err
}
defer fImg.Close()
writtenImg, err := fImg.Write(bytes)
if err != nil {
return err
}
if writtenImg == 0 {
os.Remove(fImgName)
}
// txt
line := strings.Join([]string{fImgName, plateText, plateColor, plateType}, ",")
writtenTxt, err := c.txtFile.WriteString(line + "\n")
_ = writtenTxt
return err
}
func (c *spiderCli) Close() {
c.txtFile.Close()
}