Python'da İndirici

Oct 17 2020

Bu kodu python'da yazdım ve benim için gayet iyi çalışıyor, ancak kodun optimize edilmediğini ve birçok yeniden düzenleme yapılması gerektiğini biliyorum. Bu nedenle, bu kodun nasıl geliştirilebileceğini incelemem gerekiyor. Bunu WGET kitaplığı benim için çalışmadığında yazmaya başladım ve diğer projelerim için hafif bir betik istedim. Ayrıca istek kitaplığını aiohttp ile değiştirmeyi düşünüyorum. Bunda acemi olmak, incelemelerinizi dört gözle bekliyorum.

Teşekkür ederim.

Gereksinimler : - tqdm , istekler

Uygulama: - 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'

Kullanım: -

# 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())

Yanıtlar

19 Ocab19 Oct 18 2020 at 11:04

İlk olarak, birkaç stil / tüy bırakan şey:

  1. HTTPError ve InvalidSchema'yı request.exceptions'dan içe aktarıyorsunuz, ancak kullanmıyorsunuz.
  2. Girintiniz konusunda tutarlı olun. PEP8 tarafından önerilen sayı 4 boşluktur ve bunu takip etmek istemiyorsanız sorun değil, ancak içeride yaptığınız gibi aynı projede 2 ve 4 boşluk girintisini karıştırmamaya çalışın.requests_retry_session()
  3. Birleştirme yerine dize biçimlendirmesini kullanmayı deneyin +. Bu sizi değerleri manuel olarak dönüştürme zahmetinden kurtarır str(uuid ile yaptığınız gibi download()) ve okunması da daha kolaydır. Python 3.6+ kullanıyorsanız (yapmanız gereken) f dizelerine bir göz atın:https://realpython.com/python-f-strings/
  4. Karşılaştırmak etmeyin Noneile ==. isAnahtar kelimeyi kullanmak, bunu yapmanın daha deyimsel yoludur. İlk satır detect_filename()olarak yeniden yazılabilir if self.filename is not None. Görmek:https://stackoverflow.com/questions/14247373/python-none-comparison-should-i-use-is-or
  5. Genel bir kural olarak, yorumlanmış kod ihtiyacımız olmayan bir şeydir, bu yüzden onu tamamen silebiliriz. Bu satıra ihtiyacınız olursa her zaman git geçmişinizden alabilirsiniz. Git kullandığınız için, değil mi? SAĞ??

Küçük, titiz şeyler:

  1. Son kısmı download(), bir dışını kullanır, ki bu genellikle kötü bir fikirdir, çünkü muhtemelen yakalamak istemeyeceğiniz bazı istisnaları yakalar. Görmek:https://stackoverflow.com/questions/54948548/what-is-wrong-with-using-a-bare-except
  2. Doküman diziniz fix_existing_filename(), dosya adının zaten var olup olmadığını kontrol ettiğini söylüyor, ancak aslında bunu yapmıyor.
  3. İçinde download(), gerçekten okumayı düşünmüyorsanız, dosyayı okuma-yazma olarak açmanıza gerek yoktur. Açık modunuzu sadece wbo dosyaya yazmayı düşündüğünüz okuyucu için daha anlaşılır kılar.
  4. Gelen check_if_dir_existİhtiyacınız olmayan ifgeçilmesinin mümkün, çünkü açıklama exist_ok=Trueiçin os.makedirsve var olmayan, yalnızca otomatik olarak dizin oluşturacaktır. Aslında, bu yöntemden tamamen kurtulurum çünkü her şeyi tek bir satırda yapabilirsiniz.
  5. Kendiniz geçici bir dosya adı oluşturmak yerine tempfile, standart kitaplıktaki modüle bir göz atın . Yalnızca yaptığınız sorunu çözmekle kalmaz, aynı uuid4zamanda geçici bir dosya oluşturduğunuz okuyucu için biraz daha nettir. Görmek:https://docs.python.org/3/library/tempfile.html#examples
  6. requests_retry_session()var sessionolanı yeniden kullanmaya izin vermek için bir argüman alır requests.Session(), ancak a) bu argümanı asla kullanmazsınız ve b) bu ​​pek bir anlam ifade etmez. Bir okuyucu olarak, böyle bir işlevin her seferinde yeni bir oturum oluşturmasını beklerdim. Mevcut bir oturumun yeniden yapılandırılması bu işlevin kapsamının bir parçasıysa, bunu adında bir şekilde belirtmesi gerekir.
  7. Ayrıca oturumlarla ilgili olarak, raise_for_status()her istekten sonra otomatik olarak arayacak şekilde üzerine bir kanca takmak iyi bir uygulamadır . Bu şekilde, her çağrıdan sonra bunu manuel olarak yapmayı hatırlamanıza gerek kalmaz. Sözdizimi biraz tuhaf görünebilir ama kesinlikle buna değer:https://stackoverflow.com/questions/45470226/requests-always-call-raise-for-status
  8. Kullanımı detect_filename()biraz tuhaf. Böyle bir yöntemin filenameözniteliği güncellemek ve hiçbir şey geri vermemek yerine dosya adını döndürmesini beklerdim .

Daha büyük şeyler:

  1. Yaptığınız gibi, işlev varsayılanlarında arama yapmaktan kaçının __init__. Çağrı, yöntem tanımlama zamanında yalnızca bir kez gerçekleştirilir ve orada sonsuza kadar saklanır. Bu durumda, cwdmevcut dizininizi başka hiçbir yerde değiştirmediğiniz için her zaman aynı olsa da, Python'da böyle şeyler yapmak bir anti-modeldir. Garip görünüyor ve bir chdiryere bir yer eklerseniz beklenmedik sonuçlar da alabilirsiniz , çünkü orijinal sonuç getcwd()hala varsayılan işlev olacaktır. Bunun yerine, değiştirmek gerekir destiçin Noneyöntem tanımı ve daha sonra bir ekleme if dest is None: dest = os.getcwd()onun içinde.
  2. pathlibStandart kitaplıktaki modüle bir göz atın . Bu sizin dosya yönetimi kapsayan operasyonların en basitleştirmeye yardımcı olabilir osve os.pathçağrılar. Ayrıca platformdan bağımsız olduğu için daha sağlamdır. Görmek:https://docs.python.org/3/library/pathlib.html
  3. Sınıftaki yöntemlerden bazıları aslında sınıfla ilgili değildir. get_valid_filename, fix_existing_filenameVe requests_retry_sessionkullanmak asla selfonları sınıfa içinde olmak için bu yüzden çok anlamlı bir yapmaz. Bunun yerine, bu yöntemleri çıkarmalı ve onları işlev yapmalısınız. Gerçekten bir sınıfta @staticmethodolmalarını istiyorsanız, sınıfla veya sınıfın nitelikleriyle etkileşime girmemeleri için onları kullanın , ancak ilk seçeneği tavsiye ederim.
  4. Kendinizi requests.Sessionbir nitelik olarak saklamak iyi bir fikirdir , böylece her çağırdığınızda onu yeniden oluşturmak zorunda kalmazsınız download(). Bir oturuma sahip olmanın tüm amacı, çerezleri kaydetme ve bağlantıları açık tutma avantajından yararlanmak için onu yeniden kullanabilmektir.
  5. 'Da yeni bir öznitelik olarak download()ayarlarsınız file_size, ancak bu pek mantıklı gelmez. Bunun bir nitelik olmasına ihtiyacınız var mı? Nesnenizin bir özelliği mi? Mevcut yöntemin dışında hiç kullanmanız gerekecek mi? Bunların hepsinin cevabı "hayır" ise, onu bunun yerine yerel bir değişken olarak tutun.

Güzel şeyler:

  1. Mantığınızın birden çok iyi tanımlanmış yöntem arasında iyi ayrılması.
  2. Bilgilendirici dokümanlar, insanlar bunları oldukça sık atlama eğilimindedir.
  3. tqdm! Bu harika bir kitaplık ve birimler ve ölçekleme gibi şeyleri doğru şekilde belirleyerek en iyi şekilde yararlanabilirsiniz.
  4. Bazı istisnaların ele alınması kesinlikle hiç yoktan iyidir. Kesinlikle bir gösteriş değil ama akılda tutulması gereken önemli bir şey :)
  5. Genel olarak iyi bir kod! Buradaki yorum sayısından cesaretiniz kırılmasın. Bunu bu topluluğa gönderdin, bu yüzden bilerek titiz davrandım, ancak bu kod her gün işte okuduğum çoğu şeyden daha iyi :)