[GO练手项目]教你如何实现简单的图片瀑布流,快来瞧一瞧!
菜牛冲鸭
2021年09月18日 21:03

要实现简单的图片瀑布流,需要搞清楚两个问题:

1、图片从哪里来

2、图片瀑布流的实现

1. 图片来源

我这边采取的是爬取百度的图片,主要使用golang来编写爬虫。那就让我们开始吧。

主要思路:

在百度图片搜索一个关键词,然后根据鼠标滚轮不停地向下滑动,就会不停地有新的图片地址加载下来。如果我们能有一个工具,能够模拟鼠标滚轮向下滑动,并且监听这段时间浏览器去访问图片地址的请求的话,我们就能采取同样的方式去请求图片地址,然后保存到本地文件夹。

刚好chromedp就是这样杰出的框架,它就像使用Selenium一样,可以模拟浏览器操作,同时也能监听网络请求,只需要电脑上安装了chrome浏览器,不需要安装chromedriver就可以,可以说非常方便。

一、分析百度图片搜索一个关键词的请求

二、使用chromedp框架编写代码

这里就不写太多的文字了,主要的代码思想我会在代码注释里面阐述。

代码块
JavaScript
自动换行
复制代码
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/chromedp/cdproto/input"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path"
	"strconv"
	"strings"
	"time"

	"github.com/chromedp/cdproto/network"
	"github.com/chromedp/chromedp"
)

var cnt = 0

func main() {
	// 定义打开谷歌浏览器的一个临时数据文件夹
	dir, err := ioutil.TempDir("", "chromedp-example")
	if err != nil {
		panic(err)
	}
	// defer函数代表当整个程序执行完之后会执行os.RemoveAll(dir),其实就是把这个临时数据文件夹删除
	defer os.RemoveAll(dir)

	// 配置一下等会程序运行打开的浏览器的一些参数
	opts := append(chromedp.DefaultExecAllocatorOptions[:],
		// 禁止GPU
		chromedp.DisableGPU,
		// 禁用默认的浏览器检查
		chromedp.NoDefaultBrowserCheck,
		// 一般我们调试的时候肯定是将这个值置为false,这样你就能看到程序在运行时打开浏览器,如果需要部署
		// 到服务器,你希望无头打开浏览器,就得把这个值置为true
		chromedp.Flag("headless", false),
		// 忽略证书错误
		chromedp.Flag("ignore-certificate-errors", true),
		// 使用刚才创建的临时数据文件夹
		chromedp.UserDataDir(dir),
	)

	allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
	// 最后执行完之后肯定会关闭这个上下文
	defer cancel()

	// 使用log.Printf打印日志
	taskCtx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf))
	defer cancel()

	// 检查浏览器进程是否启动
	if err := chromedp.Run(taskCtx); err != nil {
		panic(err)
	}

	// 监听网络事件
	listenForNetworkEvent(taskCtx)
	// actions就代表后面打开浏览器要执行的一系列操作
	var actions []chromedp.Action

	actions = append(actions,network.Enable())
	// 指定要访问的地址
	actions = append(actions,chromedp.Navigate(`https://image.baidu.com/search/index?tn=baiduimage&ipn=r&ct=201326592&cl=2&lm=-1&st=-1&fm=result&fr=&sf=1&fmq=1631628760308_R&pv=&ic=&nc=1&z=&hd=&latest=&copyright=&se=1&showtab=0&fb=0&width=&height=&face=0&istype=2&ie=utf-8&sid=&word=%E7%BE%8E%E5%A5%B3%E5%A4%B4%E5%83%8F`))

	// 模拟滚轮滚动50次,触发新的图片加载
	for i:= 0; i < 20; i++{
		actions = append(actions,chromedp.Sleep(1*time.Second))
		actions = append(actions,chromedp.ActionFunc(func(ctx context.Context) error {
			time.Sleep(1*time.Second)
			// 在页面的(200,200)坐标的位置
			p := input.DispatchMouseEvent(input.MouseWheel, 200, 200)
			p = p.WithDeltaX(0)
			// 滚轮向下滚动1000单位
			p = p.WithDeltaY(float64(1000))
			err = p.Do(ctx)
			return err
		}))
	}

	//执行这一列的操作
	chromedp.Run(taskCtx,
		actions...
	)

}

//监听网络事件
func listenForNetworkEvent(ctx context.Context) {
	chromedp.ListenTarget(ctx, func(ev interface{}) {
		switch ev := ev.(type) {
		// 是一个响应收到的事件
		case *network.EventResponseReceived:
			resp := ev.Response
			if len(resp.Headers) != 0 {
				//将这个resp转成json
				response, _ := resp.MarshalJSON()
				var res = &UrlResponse{}
				json.Unmarshal(response, &res)
				// 我们只关心是图片地址的url
				if strings.Contains(res.Url,".jpg") || strings.Contains(res.Url, "f=JPEG"){
					cnt++
					// 去对每个图片地址下载图片
					downloadImage(res.Url,"美女头像",cnt)
				}
			}
		}
	})
}

type UrlResponse struct {
	Url string `json:"url"`
}
/**
根据图片的地址下载图片
 */
func downloadImage(imgUrl ,dir string, cnt int){
	defer func() {
		if r := recover(); r != nil {
			fmt.Printf("发生异常,地址忽略: %s", imgUrl)
		}
	}()
	//生成文件名
	fileName := path.Base(strconv.Itoa(cnt)+".jpg")

	// 设置请求地址和请求头参数
	imgReq, err := http.NewRequest("GET", imgUrl, nil)
	imgReq.Header.Add("Referer", "https://image.baidu.com/")
	imgReq.Header.Add("Accept-Encoding", "gzip,deflate,br")
	imgReq.Header.Add("Host", "image.baidu.com")
	imgReq.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36")

	client := &http.Client{}
	// 执行请求
	imgRes, err := client.Do(imgReq)

	if err != nil {
		log.Println("Get image error :", err)
		return
	}
	defer imgRes.Body.Close()
	if imgRes.ContentLength == 0{
		return
	}
	// 这种时候dir文件夹必须存在,不然会报错
	f, err := os.Create(dir + "/" + fileName )
	if err != nil {
		log.Println("Create image error:", err)
		return
	}
	// 拷贝二进制流数据,保存成本地图片
	io.Copy(f, imgRes.Body)
}
复制成功

三、使用go build main.go执行代码,得到想要的图片,如下图所示:

这样的话,我们的图片已就绪,进行下一步。这些图片后面会放到远程服务器的/home/pic路径下面。

2. 前端项目

这里使用的是vue框架和vue-waterfall-easy框架,其中vue-waterfall-easy是github上面别人写好的一个组件,我只是拿来用一下,这里表示感谢,关于具体如何使用可以参考仓库里面的readme文档,我就不细讲如何来从零到有搭建整个项目了,对了,特别注意下,这里的vue版本只能使用2,3我试了下好像报错,所以我是从3版本直接改为2版本,因为我前端技能就是菜鸡水平,整个前端项目的目录结构如下:

下面就贴一下几个比较重要的文件:

  • App.vue

代码块
JavaScript
自动换行
复制代码
<template>
  <div id="app">
    <vue-waterfall-easy
      :imgsArr="imgsArr"
      @scrollReachBottom="getData"
    ></vue-waterfall-easy>
  </div>
</template>

<script>
import vueWaterfallEasy from "vue-waterfall-easy";
import axios from "axios";
export default {
  components: {
    vueWaterfallEasy,
  },
  data() {
    return {
      imgsArr: [],
    };
  },

  created() {
    this.getData();
  },
  methods: {
    getData() {
      axios.get("/api/getImages").then((res) => {
        let data = res.data;
        if (data.code == 200) {
          let arr = [];
          data.data.forEach((element) => {
            arr.push({ src: element.img });
          });
          this.imgsArr = this.imgsArr.concat(arr);
        }
      });
    },
  },
};
</script>

<style lang="scss">
* {
  margin: 0;
  padding: 0;
}
a {
  color: #000;
  text-decoration: none;
  &:active {
    color: #000;
  }
}
html,
body,
#app {
  height: 100%;
}
#app {
  position: relative;
  #header {
    display: block;
    text-align: center;
    background: #000;
    color: #cccccc;
    line-height: 32px;
    font-size: 16px;
    font-weight: bold;
    letter-spacing: 2px;
    position: fixed;
    z-index: 999;
    width: 100%;
  }
  #content {
    position: absolute;
    top: 32px;
    bottom: 0;
    width: 100%;
  }
}
#app {
  overflow: auto;
  position: relative;
  .some-info {
    line-height: 1.6;
    text-align: center;
  }
}
</style>
复制成功

这里就是要注意/api/getImages是向后台请求获取图片地址的接口的。

  • main.js

代码块
JavaScript
自动换行
复制代码
import Vue from "vue";
import App from "./App.vue";

new Vue({
  render: (h) => h(App),
}).$mount("#app");
复制成功
  • vue.config.js

代码块
JavaScript
自动换行
复制代码
// 后端服务器地址,提供getImages接口
let url = "http://xxxx:9000";
module.exports = {
  devServer: {
    // 开发环境跨域情况的代理配置
    proxy: {
      // 【必要】访问自己搭建的后端服务器
      "/api": {
        target: url,
        secure: false,
        pathRewrite: {
          "^/api": "",
        },
      },
    },
    open: true,
    port: 8081,
  },
};
复制成功

以上就是前端项目比较重要的文件,其实就是用别人写好的组件,配置好滚动滑动的事件函数,去不停地请求后端服务器获取图片地址然后前端展示接口。

关于部署的话,其实很简单,就是在命令行执行npm run build就可以在当前文件夹下面看到一个dist文件夹,这个文件夹打包丢到服务器就可以通过nginx设置和后端进行交互了。

3. 后端项目

主要功能是提供返回20张图片地址的接口,一张图片可以被访问,必须拥有一个静态地址,所以我们需要首先把下载的图片上传到服务器,然后将图片文件夹映射成一个静态地址,才可以确保这些图片能够通过链接的方式访问到。

直接放代码吧,具体的细节可以参考代码里面的注释:

代码块
JavaScript
自动换行
复制代码
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"math/rand"
	"net/http"
	"strings"
)

type Response struct {
	Code int `json:"code"`
	Data [20]Image `json:"data"`
}

type Image struct {
	Img string `json:"img"`
}

// 遍历存放图片的文件夹/home/pic,获取到图片的静态地址
func GetRandomPic(url,pathname string) string {
	rd, err := ioutil.ReadDir(pathname)
	if err != nil {
		fmt.Println("遍历出错:", err)
		return ""
	}
	var length = len(rd)
	var fi = rd[rand.Intn(length)]
	if fi.IsDir() {
		return GetRandomPic(url,pathname + fi.Name() + "/")
	}
	return url+strings.Replace(pathname + fi.Name(),"/home/pic","",-1)
}

// 获取20张随机图片的地址
func getImages(w http.ResponseWriter,r *http.Request)  {
	//获取图片
	var res = &Response{Code: 200}
	var data [20]Image
	for i:= 0; i < 20; i++{
		var image = Image{
			Img: GetRandomPic("http://1.117.182.16:9000","/home/pic/"),
		}
		data[i] = image
	}
	res.Data = data
	// 设置响应头的content-type为application/json
	w.Header().Add("Content-Type","application/json")
	s, _ := json.Marshal(res)
	w.Write(s)
}

func main(){
	//设置/home/pic下面的图片为静态文件服务器下面的文件,直接通过服务器地址加上端口拼上文件路径就可以访问
	http.Handle("/", http.FileServer(http.Dir("/home/pic")))
	http.HandleFunc("/getImages",getImages)
	http.ListenAndServe(":9000",nil)
}
复制成功

因为要部署到服务器上面,服务器是centos环境,所以需要打包成linux下面的可执行文件,在windows上面使用交叉编译的方式就可以实现,具体命令如下:

代码块
JavaScript
自动换行
复制代码
SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build -o api main.go
复制成功

执行命令之后就可以看到生成了一个api的可执行文件,上传到服务器之后,赋予可执行权限:

代码块
JavaScript
自动换行
复制代码
chmod +x api
复制成功

执行命令:

代码块
JavaScript
自动换行
复制代码
nohup ./api &
复制成功

即可。

4. Nginx的安装和配置

因为使用的是centos机器,所以我直接使用yum进行安装:

代码块
JavaScript
自动换行
复制代码
yum install nginx -y
复制成功

安装完成之后,就可以使用:

代码块
JavaScript
自动换行
复制代码
service nginx start
复制成功

启动Nginx。

一般情况下访问服务器的80端口出现Welcome to Nginx一般代表nginx安装成功。

安装完成,我们需要根据实际的位置配置下nginx配置文件,该文件的路径为/etc/nginx/nginx.conf,具体nginx.conf的内容如下:

代码块
JavaScript
自动换行
复制代码
# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    server {
        listen       8000;
        listen       [::]:8000;
        server_name  _;

        location / {
           root /home/mzitu-site/dist;
           index index.html index.htm;
           try_files $uri $uri/ /index.html;
        }
        
        location /api/ {
           proxy_pass http://localhost:9000/;
        }
    }

# Settings for a TLS enabled server.
#
#    server {
#        listen       443 ssl http2;
#        listen       [::]:443 ssl http2;
#        server_name  _;
#        root         /usr/share/nginx/html;
#
#        ssl_certificate "/etc/pki/nginx/server.crt";
#        ssl_certificate_key "/etc/pki/nginx/private/server.key";
#        ssl_session_cache shared:SSL:1m;
#        ssl_session_timeout  10m;
#        ssl_ciphers HIGH:!aNULL:!MD5;
#        ssl_prefer_server_ciphers on;
#
#        # Load configuration files for the default server block.
#        include /etc/nginx/default.d/*.conf;
#
#        error_page 404 /404.html;
#            location = /40x.html {
#        }
#
#        error_page 500 502 503 504 /50x.html;
#            location = /50x.html {
#        }
#    }

}
复制成功

修改完配置文件,需要重启nginx才能生效:

代码块
JavaScript
自动换行
复制代码
service nginx restart
复制成功

值得注意的是前端项目打包的dist文件夹我这里是/home/mzitu-site/dist,自己可以根据实际情况填写。

5. 访问服务器nginx地址即可查看到图片瀑布流。

完结撒花,2021年9月18日晚。

怀着对golang的热爱,我必会不断地学习,产出更有意思的学习总结,希望走过路过的各位老板们能够三连一波,你们的支持和鼓励必是我不断向前的动力源泉。

眼睛睁不开了,想睡觉,呜呜!