Golang实现多线程下载

DJ沸羊羊 · · 3794 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

前段时间写了个小爬虫,从国外某网站上下载视频,初期使用的是单线下载,后面发现访问服务端的资源数过多,会被反爬机制限制,还有一个问题就是单线下载境外网站内容,效率比较低,下载速度很慢,后面修修补补改了改,改为多线访问同个资源,顺利解决反爬机制,也提升了下载效率.

多线程下载必须服务端支持

   1.判断服务端是否支持多线下载:
     使用 HEAD 方法请求资源,然后查看服务端返回数据
image.png
  2.查看返回数据头部是否存在 `Accept-Ranges →bytes`
    如果有,那么就支持多线程下载,没有的话基本上可以洗洗睡了.

Golang 实现环节

`

最近写了个小爬虫,从国外某网站上下载视频,初期使用的是单线下载,后面发现访问服务端的资源数过多,会被反爬机制限制,还有一个问题就是单线下载境外网站内容性能比较差,下载速度很慢,后面修修补补改了改,改为多线访问同个资源,顺利解决反爬机制,也提升了下载效率.

多线程下载必须服务端支持

   1.判断服务端是否支持多线下载:
     使用 HEAD 方法请求资源,然后查看服务端返回数据
image.png
  2.查看返回数据头部是否存在 `Accept-Ranges →bytes`
    如果有,那么就支持多线程下载,没有的话基本上可以洗洗睡了.

Golang 实现环节

判断是否支持多线下载

image.png

多线下载任务分配

image.png

执行下载

image.png

完整代码

package download

import (
    "github.com/labstack/gommon/log"
    "io/ioutil"
    "net/http"
    "os"
    "strconv"
    "strings"
    "sync"
    "sync/atomic"
    "time"
)

var client = http.Client{Timeout: time.Second * 180}

var threadGroup = sync.WaitGroup{}

var packageSize int64

func init() {
    //每个线程下载文件的大小
    packageSize = 1048576 * 4
}

func Download(url, cachePath string, scheduleCallback func(schedule float64)) string {

    var localFileSize int64
    var file *os.File
    if info, e := os.Stat(cachePath); e != nil {
        if os.IsNotExist(e) {
            if createFile, err := os.Create(cachePath); err == nil {
                file = createFile
            } else {
                panic(err)
            }
        } else {
            panic(e)
        }
    } else {
        localFileSize = info.Size()
    }
    //HEAD 方法请求服务端是否支持多线程下载,并获取文件大小
    if request, e := http.NewRequest("HEAD", url, nil); e == nil {
        if response, i := client.Do(request); i == nil {
            defer response.Body.Close()
            //得到文件大小
            ContentLength := response.ContentLength
            if localFileSize == ContentLength {
                log.Warn("file exist~")
                return cachePath
            } else {
                //判断是否支持多线下载
                if strings.Compare(response.Header.Get("Accept-Ranges"), "bytes") == 0 {
                    //支持 走下载流程
                    if dispSliceDownload(file, ContentLength, url, scheduleCallback) == 0 {
                        return cachePath
                    } else {
                        return ""
                    }
                } else {
                    panic("nonsupport ~")
                }
            }
        } else {
            panic(i)
        }
    } else {
        panic(e)
    }
    return ""
}

func dispSliceDownload(file *os.File, ContentLength int64, url string, scheduleCallback func(schedule float64)) int {
    defer file.Close()
    //文件总大小除以 每个线程下载的大小
    i := ContentLength / packageSize
    //保证文件下载完整
    if ContentLength%packageSize > 0 {
        i += 1
    }
    //下载总进度
    var schedule int64
    //分配下载线程
    for count := 0; count < int(i); count++ {
        //计算每个线程下载的区间,起始位置
        var start int64
        var end int64
        start = int64(int64(count) * packageSize)
        end = start + packageSize
        if int64(end) > ContentLength {
            end = end - (end - ContentLength)
        }
        //构建请求
        if req, e := http.NewRequest("GET", url, nil); e == nil {
            req.Header.Set(
                "Range",
                "bytes="+strconv.FormatInt(start, 10)+"-"+strconv.FormatInt(end, 10))
            //
            threadGroup.Add(1)
            go sliceDownload(req, file, &schedule, &ContentLength, scheduleCallback, start)
        } else {
            panic(e)
        }

    }
    //等待所有线程完成下载
    threadGroup.Wait()
    return 0
}

func sliceDownload(request *http.Request, file *os.File, schedule *int64, ContentLength *int64, scheduleCallback func(schedule float64),
    start int64) {
    defer threadGroup.Done()
    if response, e := client.Do(request); e == nil && response.StatusCode == 206 {
        defer response.Body.Close()
        if bytes, i := ioutil.ReadAll(response.Body); i == nil {
            i2 := len(bytes)
            //从我们计算好的起点写入文件
            file.WriteAt(bytes, start)
            atomic.AddInt64(schedule, int64(i2))
            val := atomic.LoadInt64(schedule)
            num := float64(val*1.0) / float64(*ContentLength) * 100
            scheduleCallback(float64(num))
        } else {
            panic(e)
        }
    } else {
        panic(e)
    }
}

因为硬盘空间有限,爬取到180GB的时候,就结束了爬取.

image.png
个人联系方式:
作者QQ:853151446
作者邮箱:853151446@qq.com
有问题或者可以一起探讨的,请联系我,或者留言.

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:DJ沸羊羊

查看原文:Golang实现多线程下载

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:701969077

3794 次点击  
加入收藏 微博
1 回复  |  直到 2020-09-27 14:16:05
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传