
1、图片从哪里来
2、图片瀑布流的实现

我这边采取的是爬取百度的图片,主要使用golang来编写爬虫。那就让我们开始吧。
主要思路:
在百度图片搜索一个关键词,然后根据鼠标滚轮不停地向下滑动,就会不停地有新的图片地址加载下来。如果我们能有一个工具,能够模拟鼠标滚轮向下滑动,并且监听这段时间浏览器去访问图片地址的请求的话,我们就能采取同样的方式去请求图片地址,然后保存到本地文件夹。
刚好chromedp就是这样杰出的框架,它就像使用Selenium一样,可以模拟浏览器操作,同时也能监听网络请求,只需要电脑上安装了chrome浏览器,不需要安装chromedriver就可以,可以说非常方便。
一、分析百度图片搜索一个关键词的请求

二、使用chromedp框架编写代码
这里就不写太多的文字了,主要的代码思想我会在代码注释里面阐述。
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=©right=&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路径下面。
这里使用的是vue框架和vue-waterfall-easy框架,其中vue-waterfall-easy是github上面别人写好的一个组件,我只是拿来用一下,这里表示感谢,关于具体如何使用可以参考仓库里面的readme文档,我就不细讲如何来从零到有搭建整个项目了,对了,特别注意下,这里的vue版本只能使用2,3我试了下好像报错,所以我是从3版本直接改为2版本,因为我前端技能就是菜鸡水平,整个前端项目的目录结构如下:

下面就贴一下几个比较重要的文件:
App.vue
<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
import Vue from "vue";
import App from "./App.vue";
new Vue({
render: (h) => h(App),
}).$mount("#app");
vue.config.js
// 后端服务器地址,提供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设置和后端进行交互了。
主要功能是提供返回20张图片地址的接口,一张图片可以被访问,必须拥有一个静态地址,所以我们需要首先把下载的图片上传到服务器,然后将图片文件夹映射成一个静态地址,才可以确保这些图片能够通过链接的方式访问到。
直接放代码吧,具体的细节可以参考代码里面的注释:
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上面使用交叉编译的方式就可以实现,具体命令如下:
SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build -o api main.go 执行命令之后就可以看到生成了一个api的可执行文件,上传到服务器之后,赋予可执行权限:
chmod +x api 执行命令:
nohup ./api & 即可。
因为使用的是centos机器,所以我直接使用yum进行安装:
yum install nginx -y 安装完成之后,就可以使用:
service nginx start 启动Nginx。
一般情况下访问服务器的80端口出现Welcome to Nginx一般代表nginx安装成功。
安装完成,我们需要根据实际的位置配置下nginx配置文件,该文件的路径为/etc/nginx/nginx.conf,具体nginx.conf的内容如下:
# 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才能生效:
service nginx restart 值得注意的是前端项目打包的dist文件夹我这里是/home/mzitu-site/dist,自己可以根据实际情况填写。

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

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