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ẽ:
- Bạn đang nhập HTTPError và InvalidSchema từ các request.exceptions nhưng không sử dụng chúng.
- 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 trong
requests_retry_session() - 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ànhstr(giống như bạn làm với uuid indownload()) 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/ - Không so sánh
Nonevới==. Sử dụngistừ khóa là cách sử dụng thành ngữ hơn. Dòng đầu tiên trongdetect_filename()có thể được viết lại thànhif self.filename is not None. Xem:https://stackoverflow.com/questions/14247373/python-none-comparison-should-i-use-is-or - 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:
- 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 - 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 đó. - 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 đó. - Trong
check_if_dir_existbạn không cầnifcâu lệnh, bởi vì bạn có thể chuyểnexist_ok=Trueđếnos.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. - 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àmuuid4mà 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 requests_retry_session()lấy mộtsessionđố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 đó.- 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 - 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ậtfilenamethuộc tính và không trả lại bất kỳ thứ gì.
Những thứ lớn hơn:
- 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ạncwdluô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ộtchdirnơi nào đó, bởi vì kết quả ban đầugetcwd()sẽ vẫn là mặc định của hàm. Thay vào đó, bạn nên thay đổidestthànhNonetrong định nghĩa phương thức và sau đó thêm mộtif dest is None: dest = os.getcwd()bên trong nó. - 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 đếnosvà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 - 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ụngself, 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. - 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ọidownload(). 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ở. - Trong
download(), bạn đặtfile_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:
- 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.
- Các docstrings thông tin, mọi người có xu hướng bỏ qua chúng khá thường xuyên.
- 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ệ.
- 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ớ :)
- 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 :)