Golang实现多线程下载
2015 年 9 月 2 日
前段时间写了个小爬虫,从国外某网站上下载视频,初期使用的是单线下载,后面发现访问服务端的资源数过多,会被反爬机制限制,还有一个问题就是单线下载境外网站内容,效率比较低,下载速度很慢,后面修修补补改了改,改为多线访问同个资源,顺利解决反爬机制,也提升了下载效率.
多线程下载必须服务端支持
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 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 有问题或者可以一起探讨的,请联系我,或者留言.