Pengunduh dengan Python
Oct 17 2020
Saya menulis kode ini dengan python dan berfungsi dengan baik untuk saya tetapi saya tahu bahwa kode tersebut tidak dioptimalkan dan banyak refactoring perlu dilakukan. Jadi saya perlu meninjau bagaimana kode ini dapat ditingkatkan. Saya mulai menulis ini ketika perpustakaan WGET tidak berfungsi untuk saya dan saya menginginkan skrip yang ringan untuk proyek saya yang lain. Saya juga berpikir untuk mengganti perpustakaan permintaan dengan aiohttp. Menjadi pemula dalam hal ini, saya menantikan ulasan Anda.
Terima kasih.
Persyaratan : - tqdm , permintaan
Implementasi: - 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'
Pemakaian:-
# 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())
Jawaban
19 Ocab19 Oct 18 2020 at 11:04
Pertama, beberapa hal gaya / linting:
- Anda mengimpor HTTPError dan InvalidSchema dari requests.exceptions tetapi tidak menggunakannya.
- Konsisten dengan lekukan Anda. 4 spasi adalah nomor yang direkomendasikan oleh PEP8 dan tidak masalah jika Anda tidak ingin mengikutinya, tetapi cobalah untuk tidak mencampur 2 dan 4 indentasi spasi dalam proyek yang sama, seperti yang Anda lakukan di dalam
requests_retry_session() - Coba gunakan pemformatan string alih-alih menggabungkan dengan
+. Ini menghemat masalah Anda dalam mengonversi nilai secara manual menjadistr(seperti yang Anda lakukan dengan uuid indownload()) dan juga lebih mudah untuk dibaca. Lihatlah f-string jika Anda menggunakan Python 3.6+ (yang seharusnya):https://realpython.com/python-f-strings/ - Jangan bandingkan
Nonedengan==. Menggunakaniskata kunci adalah cara yang lebih idiomatis untuk melakukannya. Baris pertamadetect_filename()bisa ditulis ulang sebagaiif self.filename is not None. Lihat:https://stackoverflow.com/questions/14247373/python-none-comparison-should-i-use-is-or - Sebagai aturan umum, kode yang diberi komentar adalah sesuatu yang tidak kami butuhkan, jadi sebaiknya kami menghapusnya sepenuhnya. Jika Anda membutuhkan baris itu kembali, Anda selalu bisa mendapatkannya dari riwayat git Anda. Karena Anda menggunakan git, bukan? BAIK??
Hal-hal kecil dan rewel:
- Bit terakhir dari
download()penggunaan telanjang kecuali, yang biasanya merupakan ide yang buruk karena menangkap beberapa pengecualian yang mungkin tidak ingin Anda tangkap. Lihat:https://stackoverflow.com/questions/54948548/what-is-wrong-with-using-a-bare-except - Docstring Anda untuk
fix_existing_filename()mengatakan itu memeriksa apakah nama file sudah ada, tetapi sebenarnya tidak melakukannya. - Di
download(), Anda tidak perlu membuka file sebagai baca-tulis jika Anda tidak bermaksud untuk benar-benar membacanya. Menyetel mode terbuka Anda agarwblebih jelas bagi pembaca bahwa Anda hanya bermaksud untuk menulis ke file itu. - Dalam
check_if_dir_existAnda tidak perluifpernyataan, karena Anda dapat melewatiexist_ok=Truekeos.makedirsdan yang secara otomatis akan membuat direktori hanya jika itu tidak ada. Sebenarnya, saya akan menyingkirkan metode ini sepenuhnya karena Anda bisa melakukan semuanya dalam satu baris. - Alih-alih membuat nama file sementara sendiri, lihat
tempfilemodul di pustaka standar. Tidak hanya menyelesaikan masalah yang sama dengan yang Anda lakukanuuid4, tetapi juga sedikit lebih jelas bagi pembaca bahwa Anda membuat file sementara. Lihat:https://docs.python.org/3/library/tempfile.html#examples requests_retry_session()mengambilsessionargumen untuk mengizinkan penggunaan kembali yang adarequests.Session(), tetapi a) Anda tidak pernah menggunakan argumen itu dan b) itu tidak masuk akal. Sebagai pembaca, saya mengharapkan fungsi seperti ini untuk membuat sesi baru setiap saat. Jika mengkonfigurasi ulang sesi yang ada adalah bagian dari ruang lingkup fungsi itu, maka itu harus menunjukkan bahwa dalam namanya entah bagaimana.- Juga tentang sesi, merupakan praktik yang baik untuk memasang hook di dalamnya sehingga secara otomatis memanggil
raise_for_status()setelah setiap permintaan. Dengan begitu, Anda tidak perlu mengingat untuk melakukannya secara manual setelah setiap pemanggilan. Sintaksnya mungkin terlihat agak aneh tetapi itu sangat berharga:https://stackoverflow.com/questions/45470226/requests-always-call-raise-for-status - Penggunaannya
detect_filename()agak aneh. Saya mengharapkan metode seperti itu untuk mengembalikan nama file alih-alih memperbaruifilenameatribut dan tidak mengembalikan apa pun.
Barang yang lebih besar:
- Hindari melakukan panggilan dalam fungsi default, seperti yang Anda lakukan di
__init__. Panggilan hanya dilakukan sekali pada waktu definisi metode dan disimpan di sana selamanya. Meskipun, dalam kasus ini, Andacwdselalu sama karena Anda tidak mengubah direktori Anda saat ini di tempat lain, itu antipattern untuk melakukan hal-hal seperti ini dengan Python. Kelihatannya aneh dan Anda juga mungkin mendapatkan hasil yang tidak diharapkan jika Anda menambahkan suatuchdirtempat, karena hasil asli kegetcwd()akan tetap menjadi fungsi default. Sebaliknya, Anda harus mengubahdestkeNonedalam definisi metode dan kemudian menambahkanif dest is None: dest = os.getcwd()di dalamnya. - Lihat
pathlibmodul di pustaka standar. Ini dapat membantu Anda menyederhanakan sebagian besar operasi manajemen file yang melibatkanosdanos.pathpanggilan. Ini juga lebih kuat karena platformnya independen. Lihat:https://docs.python.org/3/library/pathlib.html - Beberapa metode di kelas sebenarnya tidak terkait dengan kelas sama sekali.
get_valid_filename,fix_existing_filenamedanrequests_retry_sessiontidak pernah digunakanself, jadi tidak masuk akal bagi mereka untuk berada di dalam kelas. Sebaliknya, Anda harus mengekstrak metode tersebut dan membuatnya berfungsi. Jika Anda benar-benar ingin mereka berada di kelas, gunakan@staticmethodpada mereka sehingga jelas bahwa mereka tidak berinteraksi dengan kelas atau atributnya, tetapi saya akan merekomendasikan opsi pertama. - Sebaiknya simpan
requests.Sessionsebagai atribut, sehingga Anda tidak perlu membuatnya kembali setiap kali Anda memanggildownload(). Inti dari memiliki sesi adalah dapat menggunakannya kembali untuk memanfaatkannya menyimpan cookie dan menjaga koneksi tetap terbuka. - Dalam
download(), Anda menetapkanfile_sizesebagai atribut baru, tetapi itu tidak masuk akal. Apakah Anda membutuhkannya untuk menjadi atribut? Apakah itu milik objek Anda? Apakah Anda pernah perlu menggunakannya di luar metode saat ini? Jika jawaban untuk semua itu adalah "tidak", maka pertahankan sebagai variabel lokal.
Hal-hal bagus:
- Pemisahan yang baik dari logika Anda di berbagai metode yang terdefinisi dengan baik.
- Dokumen informatif, orang cenderung melewatkannya cukup sering.
- tqdm! Ini adalah perpustakaan yang luar biasa dan Anda memanfaatkannya dengan benar dengan menentukan hal-hal seperti unit dan penskalaan.
- Beberapa penanganan pengecualian pasti lebih baik daripada tidak sama sekali. Benar-benar bukan pamer, tapi sesuatu yang penting untuk diingat :)
- Secara keseluruhan itu kode yang bagus! Jangan berkecil hati dengan banyaknya komentar di sini. Anda memang mengirimkannya ke komunitas ini, jadi saya sengaja cerewet, tetapi kode ini lebih baik daripada kebanyakan yang saya baca di tempat kerja setiap hari :)
Kiat Pemilik Anjing yang Bermanfaat: Mengapa Penting untuk Membiarkan Anjing Anda Mengendus di Jalan
Taylor Sheridan Baru Menambahkan 1 Bintang 'Yellowstone' Favoritnya ke Pemeran 'Lawmen: Bass Reeves'