Downloader in Python
Oct 17 2020
Ho scritto questo codice in Python e per me funziona bene, ma so che il codice non è ottimizzato e deve essere fatto molto refactoring. Quindi ho bisogno di una revisione su come questo codice può essere migliorato. Ho iniziato a scrivere questo quando la libreria WGET non funzionava per me e volevo uno script leggero per i miei altri progetti. Sto anche pensando di sostituire la libreria delle richieste con aiohttp. Essendo un principiante in questo, attendo con impazienza le tue recensioni.
Grazie.
Requisiti : - tqdm , richieste
Implementazione: - downloader.py
import requests
import os
from uuid import uuid4
from urllib.parse import urlparse, unquote
import re
from datetime import datetime
from requests.exceptions import HTTPError, ReadTimeout,InvalidSchema
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from tqdm import tqdm
class Rget:
def __init__(self, url, dest=os.getcwd(), filename=None, progress_bar=True, headers=None):
self.url = url
self.dest = self.check_if_dir_exist(dest)
self.filename = filename
self.progress_bar = progress_bar
# self.headers = self.fetch_headers(headers)
def check_if_dir_exist(self, dest):
"""
Function to check whether the directory exist.
If Directory is not present it creates one and returns the path.
"""
if not os.path.exists(dest):
os.makedirs(dest)
return dest
def detect_filename(self, url, response):
"""
Function to autodetect file name from url and content disposition
headers.
"""
if not self.filename == None:
self.filename = self.get_valid_filename(self.filename)
else:
if 'filename' in response.headers.get('Content-Disposition'):
filename = response.headers.get('Content-Disposition') \
.split('filename=')[1].split(';')[0].replace('"', '')
else:
filename = os.path.basename(urlparse(unquote(response.url))[2])
self.filename = self.get_valid_filename(filename)
def get_valid_filename(self, filename):
"""
Return the given string converted to a string that can be used for a clean
filename. Remove leading and trailing spaces; convert other spaces to
underscores; and remove anything that is not an alphanumeric, dash,
underscore, or dot.
https://github.com/django/django/blob/master/django/utils/text.py
"""
s = str(filename).strip()
separator = ' '
return re.sub(r'(?u)[^-\w.]', separator, s)
def fix_existing_filename(self, filename, dest):
"""
Function that checks whether the file is already downloaded(exists)
If already downloaded adds a prefix of current timestamp and returns
the filename along with proper extension
"""
name, ext = filename.rsplit('.', 1)
time = datetime.now().strftime('%m-%d-%Y_%I.%M.%S%p')
name = name+'_'+time
return name+'.'+ext
def requests_retry_session(self,
retries=3,
backoff_factor=0.3,
status_forcelist=(500, 502, 504),
session=None,
):
"""
A high level function that I certainly didnot write
and I don't remember where I copied it from so if somebody knows whose code
this is then inform me.
What it bascially does is it automatically retries the request be it
HEAD, POST, GET, DELETE for 3 times(defalut) can be changed.
"""
session = session or requests.Session()
retry = Retry(
total=retries,
read=retries,
connect=retries,
backoff_factor=backoff_factor,
status_forcelist=status_forcelist,
)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)
session.mount('https://', adapter)
return session
def download(self):
"""
Function to download file into a temporary file and rename
it to user provided filename or autodetected filename.
"""
try:
with self.requests_retry_session().get(self.url, stream=True, timeout=3) as response:
response.raise_for_status()
self.detect_filename(self.url, response)
self.file_size = int(response.headers['Content-Length'].strip())
with open(os.path.join(self.dest, 'rget_'+str(uuid4())+'.tmp'), 'wb+') as temp:
with tqdm(
total = self.file_size,
initial=0,
unit='B',
desc=self.filename,
ascii=True,
unit_scale=True,
unit_divisor=1024,
) as progressBar:
for chunk in response.iter_content(chunk_size=8192):
temp.write(chunk)
progressBar.update(len(chunk))
if os.path.exists(os.path.join(self.dest, self.filename)):
self.filename = self.fix_existing_filename(self.filename, self.dest)
os.rename(temp.name, os.path.join(self.dest, self.filename))
return self.filename
#* A bit of Exception handling to showoff ;)
except ReadTimeout:
return('Maximum Retries reached, Check your internet connection and try again')
except:
return 'Please check the url and try again'
Utilizzo: -
# importing Rget class from downloader.py
from downloader import Rget
url = 'https://drive.google.com/u/0/uc?id=18dn4ha9Lyb1MqjYEjtRAEA5uEKxjPkwD&export=download'
# Optional parameters like destination and fileName can also be provided
file = Rget(url = url)
# printing the fileName once the file gets downloaded
# since download funtion returns the filename
print(file.download())
Risposte
19 Ocab19 Oct 18 2020 at 11:04
Innanzitutto, un paio di cose di stile / linting:
- Stai importando HTTPError e InvalidSchema da requests.exceptions ma non li stai utilizzando.
- Sii coerente riguardo al tuo rientro. 4 spazi è il numero consigliato da PEP8 e va bene se non vuoi seguirlo, ma cerca di non mescolare 2 e 4 rientri di spazio nello stesso progetto, come stai facendo all'interno
requests_retry_session() - Prova a utilizzare la formattazione delle stringhe invece di concatenare con
+. Questo ti risparmia la fatica di convertire manualmente i valori instr(come fai con l'uuid indownload()) ed è anche più facile da leggere. Dai un'occhiata alle stringhe f se sei su Python 3.6+ (cosa che dovresti):https://realpython.com/python-f-strings/ - Non confrontare
Nonecon==. Usare laisparola chiave è il modo più idiomatico di farlo. La prima rigadetect_filename()può essere riscritta comeif self.filename is not None. Vedere:https://stackoverflow.com/questions/14247373/python-none-comparison-should-i-use-is-or - Come regola generale, il codice commentato è qualcosa di cui non abbiamo bisogno, quindi potremmo anche eliminarlo completamente. Se hai bisogno di quella linea indietro, puoi sempre ottenerla dalla tua cronologia di git. Perché stai usando git, giusto? DESTRA??
Cose minori e nitide:
- L'ultimo pezzo di
download()usa un nudo tranne, che di solito è una cattiva idea perché cattura alcune eccezioni che probabilmente non vuoi catturare. Vedere:https://stackoverflow.com/questions/54948548/what-is-wrong-with-using-a-bare-except - La tua docstring per
fix_existing_filename()dice che controlla se il nome del file esiste già, ma in realtà non lo fa. - In
download(), non è necessario aprire il file in lettura-scrittura se non si intende effettivamente leggere da esso. L'impostazione della modalità di apertura suwbrende più chiaro per il lettore che intendi scrivere solo su quel file. - In
check_if_dir_existnon hai bisognoifdell'istruzione, perché puoi passareexist_ok=Trueaos.makedirse questo creerà automaticamente la directory solo se non esiste. In effetti, mi sbarazzerei completamente di questo metodo perché puoi fare tutto in una riga. - Invece di generare tu stesso un nome di file temporaneo, dai un'occhiata al
tempfilemodulo nella libreria standard. Non solo risolve lo stesso problema che hai risoltouuid4, ma è anche un po 'più chiaro per il lettore che stai generando un file temporaneo. Vedere:https://docs.python.org/3/library/tempfile.html#examples requests_retry_session()richiede unsessionargomento per consentire il riutilizzo di un esistenterequests.Session(), ma a) non usi mai quell'argomento eb) non ha molto senso. Come lettore, mi aspetto che una funzione come questa crei ogni volta una nuova sessione. Se la riconfigurazione di una sessione esistente fa parte dell'ambito di quella funzione, allora dovrebbe indicarlo in qualche modo nel nome.- Anche per quanto riguarda le sessioni, è buona norma installare un hook su di esso in modo che chiami automaticamente
raise_for_status()dopo ogni richiesta. In questo modo non devi ricordarti di farlo manualmente dopo ogni chiamata. La sintassi può sembrare un po 'strana ma ne vale sicuramente la pena:https://stackoverflow.com/questions/45470226/requests-always-call-raise-for-status - L'utilizzo di
detect_filename()è un po 'strano. Mi aspetto che un metodo come questo restituisca il nome del file invece di aggiornare l'filenameattributo e non restituire nulla.
Cose più grandi:
- Evita di effettuare chiamate nelle impostazioni predefinite delle funzioni, come fai in
__init__. La chiamata viene eseguita solo una volta al momento della definizione del metodo e memorizzata lì per sempre. Mentre, in questo caso, il tuocwdè sempre lo stesso perché non stai cambiando la tua directory corrente da nessun'altra parte, è un antipattern fare cose del genere in Python. Sembra strano e potresti anche ottenere risultati inaspettati se aggiungi unchdirda qualche parte, perché il risultato originalegetcwd()sarà ancora quello predefinito della funzione. Invece, dovresti cambiare ildestaNonenella definizione del metodo e quindi aggiungere unif dest is None: dest = os.getcwd()al suo interno. - Dai un'occhiata al
pathlibmodulo nella libreria standard. Può aiutarti a semplificare la maggior parte delle operazioni di gestione dei file che coinvolgonooseos.pathchiamate. È anche più robusto perché è indipendente dalla piattaforma. Vedere:https://docs.python.org/3/library/pathlib.html - Alcuni dei metodi della classe non sono affatto correlati alla classe.
get_valid_filename,fix_existing_filenameErequests_retry_sessionnon usare maiself, in modo da non fare un sacco di senso per loro di essere all'interno della classe. Invece dovresti estrarre quei metodi e renderli funzioni. Se vuoi davvero che siano in una classe, usali@staticmethodin modo che sia chiaro che non interagiscono con la classe oi suoi attributi, ma consiglierei la prima opzione. - Sarebbe una buona idea memorizzare il tuo
requests.Sessioncome attributo, in modo da non doverlo ricreare ogni volta che lo invochidownload(). Il punto centrale di una sessione è poterla riutilizzare per trarne vantaggio salvando i cookie e mantenendo aperte le connessioni. - In
download(), impostifile_sizecome nuovo attributo, ma non ha molto senso. Hai bisogno che sia un attributo? È una proprietà del tuo oggetto? Avrai mai bisogno di usarlo al di fuori del metodo attuale? Se la risposta a tutte queste domande è "no", mantienila invece come variabile locale.
Belle cose:
- Buona separazione della logica tra più metodi ben definiti.
- Docstring informativi, le persone tendono a saltarli abbastanza spesso.
- tqdm! È una libreria fantastica e puoi trarne il massimo specificando correttamente cose come unità e ridimensionamento.
- Una certa gestione delle eccezioni è decisamente meglio di niente. Non è assolutamente uno spettacolo, ma qualcosa di importante da tenere a mente :)
- Nel complesso è un buon codice! Non scoraggiarti dal numero di commenti qui. L'hai inviato a questa comunità, quindi sono stato pignolo di proposito, ma questo codice è migliore della maggior parte di quello che leggo al lavoro ogni giorno :)