轉載自公衆號:FightingCoder
一般情況下我們使用爬蟲更多的應該是爬數據或者圖片吧,今天在這裏和大家分享一下關於使用爬蟲技術來進行視頻下載的方法,不僅可以方便的下載一些體積小的視頻,針對大容量的視頻下載同樣試用。
先上個🌰
requests模塊的iter_content方法
這裏我們使用的是python的requests模塊作爲例子,需要獲取文本的時候我們會使用response.text獲取文本信息,使用response.content獲取字節流,比如下載圖片保存到一個文件,而對於大個的文件我們就要採取分塊讀取的方式了,
requests.get方法的stream
第一步,我們需要設置requests.get的stream參數爲True。
默認情況下是stream的值爲false,它會立即開始下載文件並存放到內存當中,倘若文件過大就會導致內存不足的情況.
當把get函數的stream參數設置成True時,它不會立即開始下載,當你使用iter_content或iter_lines遍歷內容或訪問內容屬性時纔開始下載。需要注意一點:文件沒有下載之前,它也需要保持連接。
iter_content:一塊一塊的遍歷要下載的內容
iter_lines:一行一行的遍歷要下載的內容
使用上面兩個函數下載大文件可以防止佔用過多的內存,因爲每次只下載小部分數據。
示例代碼:
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個字節)來進行讀取。但是如果僅僅是迭代是不行,如果下載中途出現問題我們之前的努力就白費了,所以我們需要做到一個斷點續傳的功能。
斷點續傳
所謂斷點續傳,也就是要從文件已經下載的地方開始繼續下載。在以前版本的 HTTP 協議是不支持斷點的,HTTP/1.1 開始就支持了。一般斷點下載時會用到 header請求頭的Range字段,這也是現在衆多號稱多線程下載工具(如 FlashGet、迅雷等)實現多線程下載的核心所在。
如何在代碼中實現用呢,來接着往下看
HTTP請求頭Range
range是請求資源的部分內容(不包括響應頭的大小),單位是byte,即字節,從0開始.
如果服務器能夠正常響應的話,服務器會返回 206 Partial Content 的狀態碼及說明.
如果不能處理這種Range的話,就會返回整個資源以及響應狀態碼爲 200 OK .(這個要注意,要分段下載時,要先判斷這個)
Range請求頭格式
Range: bytes=start-end
Range頭域
Range頭域可以請求實體的一個或者多個子範圍。例如,
表示頭500個字節:bytes=0-499
表示第二個500字節:bytes=500-999
表示最後500個字節:bytes=-500
表示500字節以後的範圍:bytes=500-
第一個和最後一個字節:bytes=0-0,-1
同時指定幾個範圍:bytes=500-600,601-999
例如
Range: bytes=10- :第10個字節及最後個字節的數據
Range: bytes=40-100 :第40個字節到第100個字節之間的數據.
注意,這個表示[start,end],即是包含請求頭的start及end字節的,所以,下一個請求,應該是上一個請求的[end+1, nextEnd]
下載實例
下面我們通過具體的代碼去進一步瞭解一些細節。
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是一個可以顯示進度條的包,具體的用法可以參考官網文檔:https://pypi.org/project/tqdm/
(1)設置stream=True參數讀取大文件。
(2)通過header的content-length屬性可以獲取文件的總容量。
(3)獲取本地已經下載的部分文件的容量,方便繼續下載,當然需要判斷文件是否存在,如果不存在就從頭開始下載。
(4)本地已下載文件的總容量和網絡文件的實際容量進行比較,如果大於或者等於則表示已經下載完成,否則繼續。
(5)開始請求視頻文件了
(6)循環讀取每次讀取一個1024個字節,當然你也可以設置512個字節
效果演示
首先調用上面的方法並傳入參數。
url = "http://v11-tt.ixigua.com/7da2b219bc734de0f0d04706a9629b61/5c77ed4b/video/m/220d4f4e99b7bfd49efb110892d892bea9011612eb3100006b7bebf69d81/?rc=am12NDw4dGlqajMzNzYzM0ApQHRAbzU6Ojw8MzQzMzU4NTUzNDVvQGgzdSlAZjN1KWRzcmd5a3VyZ3lybHh3Zjc2QHFubHBfZDJrbV8tLTYxL3NzLW8jbyMxLTEtLzEtLjMvLTUvNi06I28jOmEtcSM6YHZpXGJmK2BeYmYrXnFsOiMzLl4%3D"
download_from_url(url, "夏目友人帳第一集.mp4")
在命令行中運行代碼之後看到效果如下
如果在pycharm直接運行的話是下面的效果
完全不一樣的效果,個人感覺還是在pycharm裏看着舒服,後面併發的時候看着也方便。
好了下面我們就打開我們的文件看看結果如何:
可以發現這個視頻被成功的下載下來,怎麼樣激不動激不動啊。
對於單文件的下載我們就完成,但是對於夏目友人帳這個動漫來說不只有一集,如果我們下載一個系列的話,我們就得使用併發了,這裏我使用aiohttp把上面的代碼改成併發的版本。
使用aiohttp進行併發下載
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)
上面的代碼功能和我們的同步代碼一樣的,不同的是這裏是異步的。
併發下載演示
我們首先要拿到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,方便演示效果,好了下面我們就來看效果。
可以發現開始併發的下載了。
完整代碼如下:
# -*- 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")