轉(zhuǎn)載自公眾號:FightingCoder
一般情況下我們使用爬蟲更多的應(yīng)該是爬數(shù)據(jù)或者圖片吧,今天在這里和大家分享一下關(guān)于使用爬蟲技術(shù)來進行視頻下載的方法,不僅可以方便的下載一些體積小的視頻,針對大容量的視頻下載同樣試用滓技。
先上個??
requests模塊的iter_content方法
這里我們使用的是python的requests模塊作為例子,需要獲取文本的時候我們會使用response.text獲取文本信息,使用response.content獲取字節(jié)流,比如下載圖片保存到一個文件,而對于大個的文件我們就要采取分塊讀取的方式了,
requests.get方法的stream
第一步吟温,我們需要設(shè)置requests.get的stream參數(shù)為True侦啸。
默認情況下是stream的值為false,它會立即開始下載文件并存放到內(nèi)存當中谬返,倘若文件過大就會導(dǎo)致內(nèi)存不足的情況.
當把get函數(shù)的stream參數(shù)設(shè)置成True時之斯,它不會立即開始下載,當你使用iter_content或iter_lines遍歷內(nèi)容或訪問內(nèi)容屬性時才開始下載遣铝。需要注意一點:文件沒有下載之前佑刷,它也需要保持連接。
iter_content:一塊一塊的遍歷要下載的內(nèi)容
iter_lines:一行一行的遍歷要下載的內(nèi)容
使用上面兩個函數(shù)下載大文件可以防止占用過多的內(nèi)存酿炸,因為每次只下載小部分數(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,這個url_file是一個大文件,所以開啟了stream模式填硕,然后通過迭代r對象的iter_content方法麦萤,同時指定chunk_size=512(即每次讀取512個字節(jié))來進行讀取鹿鳖。但是如果僅僅是迭代是不行,如果下載中途出現(xiàn)問題我們之前的努力就白費了壮莹,所以我們需要做到一個斷點續(xù)傳的功能翅帜。
斷點續(xù)傳
所謂斷點續(xù)傳,也就是要從文件已經(jīng)下載的地方開始繼續(xù)下載命满。在以前版本的 HTTP 協(xié)議是不支持斷點的涝滴,HTTP/1.1 開始就支持了。一般斷點下載時會用到 header請求頭的Range字段胶台,這也是現(xiàn)在眾多號稱多線程下載工具(如 FlashGet歼疮、迅雷等)實現(xiàn)多線程下載的核心所在。
如何在代碼中實現(xiàn)用呢诈唬,來接著往下看
HTTP請求頭Range
range是請求資源的部分內(nèi)容(不包括響應(yīng)頭的大泻唷),單位是byte铸磅,即字節(jié)骤素,從0開始.
如果服務(wù)器能夠正常響應(yīng)的話,服務(wù)器會返回 206 Partial Content 的狀態(tài)碼及說明.
如果不能處理這種Range的話愚屁,就會返回整個資源以及響應(yīng)狀態(tài)碼為 200 OK .(這個要注意,要分段下載時痕檬,要先判斷這個)
Range請求頭格式
Range: bytes=start-end
Range頭域
Range頭域可以請求實體的一個或者多個子范圍霎槐。例如,
表示頭500個字節(jié):bytes=0-499
表示第二個500字節(jié):bytes=500-999
表示最后500個字節(jié):bytes=-500
表示500字節(jié)以后的范圍:bytes=500-
第一個和最后一個字節(jié):bytes=0-0,-1
同時指定幾個范圍:bytes=500-600,601-999
例如
Range: bytes=10- :第10個字節(jié)及最后個字節(jié)的數(shù)據(jù)
Range: bytes=40-100 :第40個字節(jié)到第100個字節(jié)之間的數(shù)據(jù).
注意梦谜,這個表示[start,end]丘跌,即是包含請求頭的start及end字節(jié)的,所以唁桩,下一個請求闭树,應(yīng)該是上一個請求的[end+1, nextEnd]
下載實例
下面我們通過具體的代碼去進一步了解一些細節(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
下面我們開始解讀標有注釋的代碼:
tqdm是一個可以顯示進度條的包荒澡,具體的用法可以參考官網(wǎng)文檔:https://pypi.org/project/tqdm/
(1)設(shè)置stream=True參數(shù)讀取大文件报辱。
(2)通過header的content-length屬性可以獲取文件的總?cè)萘俊?br>
(3)獲取本地已經(jīng)下載的部分文件的容量,方便繼續(xù)下載单山,當然需要判斷文件是否存在碍现,如果不存在就從頭開始下載。
(4)本地已下載文件的總?cè)萘亢途W(wǎng)絡(luò)文件的實際容量進行比較米奸,如果大于或者等于則表示已經(jīng)下載完成昼接,否則繼續(xù)。
(5)開始請求視頻文件了
(6)循環(huán)讀取每次讀取一個1024個字節(jié)悴晰,當然你也可以設(shè)置512個字節(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")
在命令行中運行代碼之后看到效果如下
如果在pycharm直接運行的話是下面的效果
完全不一樣的效果逐工,個人感覺還是在pycharm里看著舒服,后面并發(fā)的時候看著也方便。
好了下面我們就打開我們的文件看看結(jié)果如何:
可以發(fā)現(xiàn)這個視頻被成功的下載下來漂辐,怎么樣激不動激不動啊泪喊。
對于單文件的下載我們就完成,但是對于夏目友人帳這個動漫來說不只有一集,如果我們下載一個系列的話,我們就得使用并發(fā)了,這里我使用aiohttp把上面的代碼改成并發(fā)的版本。
使用aiohttp進行并發(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)
上面的代碼功能和我們的同步代碼一樣的者吁,不同的是這里是異步的窘俺。
并發(fā)下載演示
我們首先要拿到MP4的鏈接,然后進行下面的代碼即可
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()
這里我同時下載了11次上面的那個視頻,命令為1-11,方便演示效果,好了下面我們就來看效果。
可以發(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")