Trình tải xuống bằng Python

Oct 17 2020

Tôi đã viết mã này bằng python và nó hoạt động tốt đối với tôi nhưng tôi biết rằng mã không được tối ưu hóa và cần phải cấu trúc lại rất nhiều. Vì vậy, tôi cần xem lại cách mã này có thể được cải thiện. Tôi bắt đầu viết bài này khi thư viện WGET không hoạt động với tôi và tôi muốn có một script nhẹ cho các dự án khác của mình. Tôi cũng đang nghĩ đến việc thay thế thư viện yêu cầu bằng aiohttp . Là người mới bắt đầu trong lĩnh vực này, tôi mong nhận được đánh giá của bạn.

Cảm ơn bạn.

Yêu cầu : - tqdm , yêu cầu

Triển khai: - 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'

Sử dụng:-

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

Trả lời

19 Ocab19 Oct 18 2020 at 11:04

Đầu tiên, một vài kiểu / cách vẽ:

  1. Bạn đang nhập HTTPError và InvalidSchema từ các request.exceptions nhưng không sử dụng chúng.
  2. Hãy nhất quán về thụt lề của bạn. 4 dấu cách là con số được khuyến nghị bởi PEP8 và không sao nếu bạn không muốn làm theo điều đó, nhưng cố gắng không kết hợp thụt lề 2 và 4 dấu cách trong cùng một dự án, giống như bạn đang thực hiện bên trongrequests_retry_session()
  3. Cố gắng sử dụng định dạng chuỗi thay vì nối với +. Điều này giúp bạn không gặp phải rắc rối khi chuyển đổi thủ công các giá trị thành str(giống như bạn làm với uuid in download()) và nó cũng dễ đọc hơn. Hãy xem chuỗi f nếu bạn đang sử dụng Python 3.6+ (mà bạn nên làm):https://realpython.com/python-f-strings/
  4. Không so sánh Nonevới ==. Sử dụng istừ khóa là cách sử dụng thành ngữ hơn. Dòng đầu tiên trong detect_filename()có thể được viết lại thành if self.filename is not None. Xem:https://stackoverflow.com/questions/14247373/python-none-comparison-should-i-use-is-or
  5. Theo nguyên tắc chung, mã nhận xét là thứ chúng tôi không cần, vì vậy bạn cũng có thể xóa nó hoàn toàn. Nếu bạn cần dòng đó trở lại, bạn luôn có thể lấy nó từ lịch sử git của mình. Bởi vì bạn đang sử dụng git, phải không? ĐÚNG??

Những điều nhỏ nhặt, khó hiểu:

  1. Phần cuối cùng download()sử dụng một ngoại trừ trần, thường là một ý tưởng tồi vì nó bắt một số ngoại lệ mà bạn có thể không muốn mắc phải. Xem:https://stackoverflow.com/questions/54948548/what-is-wrong-with-using-a-bare-except
  2. Docstring của bạn cho fix_existing_filename()biết nó sẽ kiểm tra xem tên tệp đã tồn tại chưa, nhưng nó không thực sự làm điều đó.
  3. Trong đó download(), bạn không cần phải mở tệp ở dạng đọc-ghi nếu bạn không có ý định thực sự đọc từ nó. Đặt chế độ mở của bạn để wbgiúp người đọc hiểu rõ hơn rằng bạn chỉ định ghi vào tệp đó.
  4. Trong check_if_dir_existbạn không cần ifcâu lệnh, bởi vì bạn có thể chuyển exist_ok=Trueđến os.makedirsvà điều đó sẽ tự động tạo thư mục chỉ khi nó không tồn tại. Trên thực tế, tôi sẽ loại bỏ hoàn toàn phương pháp này vì bạn chỉ có thể làm mọi thứ trong một dòng.
  5. Thay vì tự tạo tên tệp tạm thời, hãy xem tempfilemô-đun trong thư viện chuẩn. Nó không chỉ giải quyết được vấn đề tương tự như bạn đã làm uuid4mà còn rõ ràng hơn một chút cho người đọc rằng bạn đang tạo một tệp tạm thời. Xem:https://docs.python.org/3/library/tempfile.html#examples
  6. requests_retry_session()lấy một sessionđối số để cho phép sử dụng lại một đối số hiện có requests.Session(), nhưng a) bạn không bao giờ sử dụng đối số đó và b) nó không có nhiều ý nghĩa. Là một độc giả, tôi mong đợi một chức năng như thế này để tạo một phiên mới mỗi lần. Nếu cấu hình lại một phiên hiện có là một phần trong phạm vi của chức năng đó, thì nó sẽ chỉ ra rằng trong tên bằng cách nào đó.
  7. Ngoài ra về các phiên, bạn nên cài đặt hook trên đó để nó tự động gọi raise_for_status()sau mỗi yêu cầu. Bằng cách đó, bạn không cần phải nhớ làm điều đó theo cách thủ công sau mỗi lần gọi. Cú pháp có thể hơi kỳ lạ nhưng nó chắc chắn đáng giá:https://stackoverflow.com/questions/45470226/requests-always-call-raise-for-status
  8. Cách sử dụng của detect_filename()là một chút kỳ lạ. Tôi mong đợi một phương thức như vậy để trả lại tên tệp thay vì cập nhật filenamethuộc tính và không trả lại bất kỳ thứ gì.

Những thứ lớn hơn:

  1. Tránh thực hiện các cuộc gọi trong mặc định của hàm, giống như bạn làm trong __init__. Lệnh gọi chỉ được thực hiện một lần tại thời điểm xác định phương thức và được lưu trữ ở đó mãi mãi. Trong khi, trong trường hợp này, thư mục của bạn cwdluôn giống nhau vì bạn không thay đổi thư mục hiện tại của mình ở bất kỳ nơi nào khác, đó là phản vật chất để thực hiện những việc như thế này trong Python. Nó trông kỳ lạ và bạn cũng có thể nhận được kết quả không mong muốn nếu bạn thêm một chdirnơi nào đó, bởi vì kết quả ban đầu getcwd()sẽ vẫn là mặc định của hàm. Thay vào đó, bạn nên thay đổi destthành Nonetrong định nghĩa phương thức và sau đó thêm một if dest is None: dest = os.getcwd()bên trong nó.
  2. Hãy xem pathlibmô-đun trong thư viện chuẩn. Nó có thể giúp bạn đơn giản hóa hầu hết các hoạt động quản lý tệp liên quan đến osvà os.pathcác cuộc gọi. Nó cũng mạnh mẽ hơn vì nó độc lập với nền tảng. Xem:https://docs.python.org/3/library/pathlib.html
  3. Một số phương thức trong lớp không thực sự liên quan đến lớp. get_valid_filename, fix_existing_filenamevà requests_retry_sessionkhông bao giờ sử dụng self, vì vậy sẽ không có ý nghĩa gì đối với họ khi ở trong lớp. Thay vào đó, bạn nên giải nén các phương thức đó và làm cho chúng hoạt động. Nếu bạn thực sự muốn chúng ở trong một lớp, hãy sử dụng @staticmethodchúng để rõ ràng rằng chúng không tương tác với lớp hoặc các thuộc tính của nó, nhưng tôi khuyên bạn nên chọn tùy chọn đầu tiên.
  4. Sẽ là một ý tưởng hay nếu bạn lưu trữ requests.Sessiondưới dạng một thuộc tính để bạn không phải tạo lại nó mỗi khi bạn gọi download(). Toàn bộ điểm của việc có một phiên là có thể sử dụng lại nó để tận dụng lợi thế của việc lưu cookie và giữ cho các kết nối luôn mở.
  5. Trong download(), bạn đặt file_sizelàm thuộc tính mới, nhưng điều đó không có ý nghĩa gì. Bạn có cần đó là một thuộc tính không? Nó có phải là tài sản của đối tượng của bạn không? Bạn có bao giờ cần sử dụng nó bên ngoài phương thức hiện tại không? Nếu câu trả lời cho tất cả những điều đó là "không", thì hãy giữ nó làm biến cục bộ.

Những điều tốt đẹp:

  1. Phân tách tốt logic của bạn qua nhiều phương pháp được xác định rõ ràng.
  2. Các docstrings thông tin, mọi người có xu hướng bỏ qua chúng khá thường xuyên.
  3. tqdm! Đó là một thư viện tuyệt vời và bạn tận dụng tối đa nó bằng cách chỉ định đúng những thứ như đơn vị và tỷ lệ.
  4. Một số xử lý ngoại lệ chắc chắn tốt hơn là không có gì. Hoàn toàn không phải là sự phô trương, mà là một điều quan trọng cần ghi nhớ :)
  5. Nói chung là mã tốt! Đừng nản lòng với số lượng bình luận ở đây. Bạn đã gửi nó cho cộng đồng này, vì vậy tôi đã có chủ ý, nhưng mã này tốt hơn hầu hết những gì tôi đọc ở nơi làm việc hàng ngày :)