Python 實(shí)現(xiàn)視頻爬取下載及斷點(diǎn)續(xù)傳優(yōu)化、異步下載

轉(zhuǎn)載自公眾號(hào):FightingCoder

一般情況下我們使用爬蟲更多的應(yīng)該是爬數(shù)據(jù)或者圖片吧,今天在這里和大家分享一下關(guān)于使用爬蟲技術(shù)來進(jìn)行視頻下載的方法,不僅可以方便的下載一些體積小的視頻,針對(duì)大容量的視頻下載同樣試用。

image

先上個(gè)??

requests模塊的iter_content方法

這里我們使用的是python的requests模塊作為例子,需要獲取文本的時(shí)候我們會(huì)使用response.text獲取文本信息,使用response.content獲取字節(jié)流,比如下載圖片保存到一個(gè)文件,而對(duì)于大個(gè)的文件我們就要采取分塊讀取的方式了,

requests.get方法的stream

第一步,我們需要設(shè)置requests.get的stream參數(shù)為True。
默認(rèn)情況下是stream的值為false,它會(huì)立即開始下載文件并存放到內(nèi)存當(dāng)中,倘若文件過大就會(huì)導(dǎo)致內(nèi)存不足的情況.
當(dāng)把get函數(shù)的stream參數(shù)設(shè)置成True時(shí),它不會(huì)立即開始下載,當(dāng)你使用iter_content或iter_lines遍歷內(nèi)容或訪問內(nèi)容屬性時(shí)才開始下載。需要注意一點(diǎn):文件沒有下載之前,它也需要保持連接。

iter_content:一塊一塊的遍歷要下載的內(nèi)容
iter_lines:一行一行的遍歷要下載的內(nèi)容

使用上面兩個(gè)函數(shù)下載大文件可以防止占用過多的內(nèi)存,因?yàn)槊看沃幌螺d小部分?jǐn)?shù)據(jù)。
示例代碼:

r = requests.get(url_file, stream=True)
f = open("file_path", "wb")
for chunk in r.iter_content(chunk_size=512):
     if chunk:
        f.write(chunk)

上面的代碼表示請求了url_file,這個(gè)url_file是一個(gè)大文件,所以開啟了stream模式,然后通過迭代r對(duì)象的iter_content方法,同時(shí)指定chunk_size=512(即每次讀取512個(gè)字節(jié))來進(jìn)行讀取。但是如果僅僅是迭代是不行,如果下載中途出現(xiàn)問題我們之前的努力就白費(fèi)了,所以我們需要做到一個(gè)斷點(diǎn)續(xù)傳的功能。

斷點(diǎn)續(xù)傳

所謂斷點(diǎn)續(xù)傳,也就是要從文件已經(jīng)下載的地方開始繼續(xù)下載。在以前版本的 HTTP 協(xié)議是不支持?jǐn)帱c(diǎn)的,HTTP/1.1 開始就支持了。一般斷點(diǎn)下載時(shí)會(huì)用到 header請求頭的Range字段,這也是現(xiàn)在眾多號(hào)稱多線程下載工具(如 FlashGet、迅雷等)實(shí)現(xiàn)多線程下載的核心所在。

image

如何在代碼中實(shí)現(xiàn)用呢,來接著往下看

HTTP請求頭Range

range是請求資源的部分內(nèi)容(不包括響應(yīng)頭的大?。瑔挝皇莃yte,即字節(jié),從0開始.
如果服務(wù)器能夠正常響應(yīng)的話,服務(wù)器會(huì)返回 206 Partial Content 的狀態(tài)碼及說明.
如果不能處理這種Range的話,就會(huì)返回整個(gè)資源以及響應(yīng)狀態(tài)碼為 200 OK .(這個(gè)要注意,要分段下載時(shí),要先判斷這個(gè))

Range請求頭格式

Range: bytes=start-end

Range頭域

Range頭域可以請求實(shí)體的一個(gè)或者多個(gè)子范圍。例如,
表示頭500個(gè)字節(jié):bytes=0-499
表示第二個(gè)500字節(jié):bytes=500-999
表示最后500個(gè)字節(jié):bytes=-500
表示500字節(jié)以后的范圍:bytes=500-
第一個(gè)和最后一個(gè)字節(jié):bytes=0-0,-1
同時(shí)指定幾個(gè)范圍:bytes=500-600,601-999
例如

Range: bytes=10- :第10個(gè)字節(jié)及最后個(gè)字節(jié)的數(shù)據(jù)
Range: bytes=40-100 :第40個(gè)字節(jié)到第100個(gè)字節(jié)之間的數(shù)據(jù).

注意,這個(gè)表示[start,end],即是包含請求頭的start及end字節(jié)的,所以,下一個(gè)請求,應(yīng)該是上一個(gè)請求的[end+1, nextEnd]

下載實(shí)例

下面我們通過具體的代碼去進(jìn)一步了解一些細(xì)節(jié)。

import requests
import tqdm
 def download_from_url(url, dst):
    response = requests.get(url, stream=True) #(1)
    file_size = int(response.headers['content-length']) #(2)
    if os.path.exists(dst):
        first_byte = os.path.getsize(dst) #(3)
    else:
        first_byte = 0
    if first_byte >= file_size: #(4)
        return file_size
    header = {"Range": f"bytes={first_byte}-{file_size}"} 
    pbar = tqdm(
        total=file_size, initial=first_byte,
        unit='B', unit_scale=True, desc=dst)
    req = requests.get(url, headers=header, stream=True) #(5)
    with(open(dst, 'ab')) as f:
        for chunk in req.iter_content(chunk_size=1024): #(6)
            if chunk:
                f.write(chunk)
                pbar.update(1024)
    pbar.close()
    return file_size

下面我們開始解讀標(biāo)有注釋的代碼:
tqdm是一個(gè)可以顯示進(jìn)度條的包,具體的用法可以參考官網(wǎng)文檔:https://pypi.org/project/tqdm/
(1)設(shè)置stream=True參數(shù)讀取大文件。
(2)通過header的content-length屬性可以獲取文件的總?cè)萘俊?br> (3)獲取本地已經(jīng)下載的部分文件的容量,方便繼續(xù)下載,當(dāng)然需要判斷文件是否存在,如果不存在就從頭開始下載。
(4)本地已下載文件的總?cè)萘亢途W(wǎng)絡(luò)文件的實(shí)際容量進(jìn)行比較,如果大于或者等于則表示已經(jīng)下載完成,否則繼續(xù)。
(5)開始請求視頻文件了
(6)循環(huán)讀取每次讀取一個(gè)1024個(gè)字節(jié),當(dāng)然你也可以設(shè)置512個(gè)字節(jié)

效果演示

首先調(diào)用上面的方法并傳入?yún)?shù)。

url = "http://v11-tt.ixigua.com/7da2b219bc734de0f0d04706a9629b61/5c77ed4b/video/m/220d4f4e99b7bfd49efb110892d892bea9011612eb3100006b7bebf69d81/?rc=am12NDw4dGlqajMzNzYzM0ApQHRAbzU6Ojw8MzQzMzU4NTUzNDVvQGgzdSlAZjN1KWRzcmd5a3VyZ3lybHh3Zjc2QHFubHBfZDJrbV8tLTYxL3NzLW8jbyMxLTEtLzEtLjMvLTUvNi06I28jOmEtcSM6YHZpXGJmK2BeYmYrXnFsOiMzLl4%3D"
download_from_url(url, "夏目友人帳第一集.mp4")

在命令行中運(yùn)行代碼之后看到效果如下

image

如果在pycharm直接運(yùn)行的話是下面的效果

image

完全不一樣的效果,個(gè)人感覺還是在pycharm里看著舒服,后面并發(fā)的時(shí)候看著也方便。
好了下面我們就打開我們的文件看看結(jié)果如何:

image

可以發(fā)現(xiàn)這個(gè)視頻被成功的下載下來,怎么樣激不動(dòng)激不動(dòng)啊。

image.gif

對(duì)于單文件的下載我們就完成,但是對(duì)于夏目友人帳這個(gè)動(dòng)漫來說不只有一集,如果我們下載一個(gè)系列的話,我們就得使用并發(fā)了,這里我使用aiohttp把上面的代碼改成并發(fā)的版本。

使用aiohttp進(jìn)行并發(fā)下載

import aiohttp
import asyncio
from tqdm import tqdm
async def fetch(session, url, dst, pbar=None, headers=None):
    if headers:
        async with session.get(url, headers=headers) as req:
            with(open(dst, 'ab')) as f:
                while True:
                    chunk = await req.content.read(1024)
                    if not chunk:
                        break
                    f.write(chunk)
                    pbar.update(1024)
            pbar.close()
    else:
        async with session.get(url) as req:
            return req


async def async_download_from_url(url, dst):
    '''異步'''
    async with aiohttp.connector.TCPConnector(limit=300, force_close=True, enable_cleanup_closed=True) as tc:
        async with aiohttp.ClientSession(connector=tc) as session:
            req = await fetch(session, url, dst)

            file_size = int(req.headers['content-length'])
            print(f"獲取視頻總長度:{file_size}")
            if os.path.exists(dst):
                first_byte = os.path.getsize(dst)
            else:
                first_byte = 0
            if first_byte >= file_size:
                return file_size
            header = {"Range": f"bytes={first_byte}-{file_size}"}
            pbar = tqdm(
                total=file_size, initial=first_byte,
                unit='B', unit_scale=True, desc=dst)
            await fetch(session, url, dst, pbar=pbar, headers=header)

上面的代碼功能和我們的同步代碼一樣的,不同的是這里是異步的。

image.gif

并發(fā)下載演示

我們首先要拿到MP4的鏈接,然后進(jìn)行下面的代碼即可

 task = [asyncio.ensure_future(async_download_from_url(url, f"{i}.mp4")) for i in range(1, 12)]
         loop = asyncio.get_event_loop()
         loop.run_until_complete(asyncio.wait(task))
         loop.close()

這里我同時(shí)下載了11次上面的那個(gè)視頻,命令為1-11,方便演示效果,好了下面我們就來看效果。

image

可以發(fā)現(xiàn)開始并發(fā)的下載了。
完整代碼如下:

# -*- coding: utf-8 -*-
# @Time : 2019/2/13 8:17 PM
# @Author : cxa
# @File : mp4downloders.py
# @Software: PyCharm
import requests
from tqdm import tqdm
import os
import aiohttp
import asyncio

try:
    import uvloop

    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
    pass


async def fetch(session, url, dst, pbar=None, headers=None):
    if headers:
        async with session.get(url, headers=headers) as req:
            with(open(dst, 'ab')) as f:
                while True:
                    chunk = await req.content.read(1024)
                    if not chunk:
                        break
                    f.write(chunk)
                    pbar.update(1024)
            pbar.close()
    else:
        async with session.get(url) as req:
            return req


async def async_download_from_url(url, dst):
    '''異步'''
    async with aiohttp.ClientSession() as session:
        req = await fetch(session, url, dst)

        file_size = int(req.headers['content-length'])
        print(f"獲取視頻總長度:{file_size}")
        if os.path.exists(dst):
            first_byte = os.path.getsize(dst)
        else:
            first_byte = 0
        if first_byte >= file_size:
            return file_size
        header = {"Range": f"bytes={first_byte}-{file_size}"}
        pbar = tqdm(
            total=file_size, initial=first_byte,
            unit='B', unit_scale=True, desc=dst)
        await fetch(session, url, dst, pbar=pbar, headers=header)


def download_from_url(url, dst):
    '''同步'''
    response = requests.get(url, stream=True)
    file_size = int(response.headers['content-length'])
    if os.path.exists(dst):
        first_byte = os.path.getsize(dst)
    else:
        first_byte = 0
    if first_byte >= file_size:
        return file_size
    header = {"Range": f"bytes={first_byte}-{file_size}"}
    pbar = tqdm(
        total=file_size, initial=first_byte,
        unit='B', unit_scale=True, desc=dst)
    req = requests.get(url, headers=header, timeout=60, stream=True)
    with(open(dst, 'ab')) as f:
        for chunk in req.iter_content(chunk_size=1024):
            if chunk:
                f.write(chunk)
                pbar.update(1024)
    pbar.close()
    return file_size


if __name__ == '__main__':
    # 異步方式下載
    url = "http://v11-tt.ixigua.com/7da2b219bc734de0f0d04706a9629b61/5c77ed4b/video/m/220d4f4e99b7bfd49efb110892d892bea9011612eb3100006b7bebf69d81/?rc=am12NDw4dGlqajMzNzYzM0ApQHRAbzU6Ojw8MzQzMzU4NTUzNDVvQGgzdSlAZjN1KWRzcmd5a3VyZ3lybHh3Zjc2QHFubHBfZDJrbV8tLTYxL3NzLW8jbyMxLTEtLzEtLjMvLTUvNi06I28jOmEtcSM6YHZpXGJmK2BeYmYrXnFsOiMzLl4%3D"
    task = [asyncio.ensure_future(async_download_from_url(url, f"{i}.mp4")) for i in range(1, 12)]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(task))
    loop.close()
    # 注釋部分是同步方式下載。
    # url = "http://v11-tt.ixigua.com/7da2b219bc734de0f0d04706a9629b61/5c77ed4b/video/m/220d4f4e99b7bfd49efb110892d892bea9011612eb3100006b7bebf69d81/?rc=am12NDw4dGlqajMzNzYzM0ApQHRAbzU6Ojw8MzQzMzU4NTUzNDVvQGgzdSlAZjN1KWRzcmd5a3VyZ3lybHh3Zjc2QHFubHBfZDJrbV8tLTYxL3NzLW8jbyMxLTEtLzEtLjMvLTUvNi06I28jOmEtcSM6YHZpXGJmK2BeYmYrXnFsOiMzLl4%3D"
    #
    # download_from_url(url, "夏目友人帳第一集.mp4")
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 網(wǎng)絡(luò)下載是我們在項(xiàng)目中經(jīng)常要用到的功能,如果是小文件的下載,比如圖片和文字之類的,我們可以直接請求源地址,然后一次...
    liang_1閱讀 2,585評(píng)論 0 52
  • 小文件下載如果文件比較小,下載方式會(huì)比較多直接用NSData的+ (id)dataWithContentsOfUR...
    醉葉惜秋閱讀 952評(píng)論 0 0
  • API定義規(guī)范 本規(guī)范設(shè)計(jì)基于如下使用場景: 請求頻率不是非常高:如果產(chǎn)品的使用周期內(nèi)請求頻率非常高,建議使用雙通...
    有涯逐無涯閱讀 2,917評(píng)論 0 6
  • 本篇文章已授權(quán)微信公眾號(hào) dasu_Android(大蘇)獨(dú)家發(fā)布 這次想來講講斷點(diǎn)續(xù)傳,以前沒相關(guān)需求,所以一直...
    請叫我大蘇閱讀 4,604評(píng)論 1 19
  • 歡迎關(guān)注幼兒說,用簡書的媽咪,都是有品味的母親 最近,幼兒說小編在網(wǎng)絡(luò)上觀看了一個(gè)外國的視頻,看完后不禁感嘆,真的...
    幼兒說閱讀 471評(píng)論 0 0

友情鏈接更多精彩內(nèi)容