3 grudnia 2024
Istnieje wiele przyczyn, z powodu których nie tworzymy optymalnego kodu. Pierwsza, jaka przychodzi mi na myśl to krótkie terminy niepozwalające rozwinąć skrzydeł i dopracować szczegółów. Kolejną może być fakt, że klient nie zawsze oczekuje i potrzebuje kodu wykonującego się w ułamku sekundy. W sytuacji, gdy nasz algorytm będzie wykorzystywany stosunkowo rzadko bardziej opłacalne może okazać się czekanie kilku(nastu) sekund niż ponoszenie większych kosztów. Ostatnią przyczyną, o jakiej chciałbym wspomnieć są nasze umiejętności. Kto nigdy nie poczuł zażenowania analizując własny kod sprzed kilku miesięcy, a tym bardziej lat niech pierwszy rzuci pendrivem.
Do wyboru mamy dwie opcje. Pierwsza z nich to usprawnienie istniejącego algorytmu, natomiast druga – zazwyczaj prostsza i dająca lepsze efekty – to napisanie funkcji od nowa. W poniższym przykładzie zaprezentuję obydwa podejścia. Drugie podejście bywa szczególnie owocne po latach, ponieważ w czasie, który minął od stworzenia kodu mogły pojawić się nowe, przydatne biblioteki.
Zadanie postawione przez klienta brzmi następująco: „Przygotujcie mi funkcję, która wczyta plik PDF, następnie podzieli go na pojedyncze strony i doda do istniejącego raportu jako załącznik.”. Podczas testowania rozwiązań wykorzystałem 13-stronnicowy plik PDF dostarczony przez klienta.
Na początku nasz algorytm wygląda następująco:
def pdf_upload (filename, data):
images = []
created_jpgs = False
with Image(filename=filename, resolution=300) as img:
images = []
if len(img.sequence) > 1:
for x in img.sequence:
path = '{0}-{1.index}.jpg'.format(data.full_path.replace('.jpg', ''), x)
convert_image(images, x, path)
else:
path = '{0}-{1}.jpg'.format(data.full_path.replace('.jpg', ''), 0)
convert_image(images, img, path)
return images
def convert_image(images, img, path):
"""Postprocessing photo."""
images.append(path)
img_page = Image(image=img)
img_page.compression_quality = 20
img_page.resize(2000, 2820) # zachowuje proporcje formatu A
img_page.alpha_channel = 'remove'
img_page.save(filename=path)
Pseudokod:
- wczytaj PDF i „zeskanuj go” z DPI 300 - sprawdź czy liczba stron jest większa niż 1 - określ ścieżkę do pliku i przekonwertuj każdą stronę przy pomocy funkcji ‘convert_image’ - jeśli nie użyj innej nazwy, a następnie przekonwertuj stronę
Czas wykonywania tego algorytmu wynosi aż 163s, dla DPI 200 maleje do 17.2s, ale w przypadku niektórych PDFów jakość może okazać się niewystarczająca.
def pdf_upload_fast_and_furious(filename, data):
images = []
pdf = PdfFileReader(filename)
for page in range(pdf.getNumPages()):
pdf_writer = PdfFileWriter()
pdf_writer.addPage(pdf.getPage(page))
output = f'{filename.replace(".pdf", "")}-{page}.pdf'
with open(output, 'wb') as output_pdf:
pdf_writer.write(output_pdf)
with Image(filename=output, resolution=300) as img:
path = f'{data.full_path.replace(".jpg", "")}-{page}.jpg'
images.append(path)
convert_image(images, img, path)
os.remove(output)
return images
Pseudokod:
- dla każdej strony w dokumencie wykonaj: - wczytaj stronę do pamięci - zapisz stronę jako PDF - wczytaj stronę „skanując” ją z DPI 300 - określ ścieżkę do pliku i przekonwertuj go znaną funkcją ‘convert_image’ - usuń stronę w PDF
Tak zapisany kod wykonuje się 24.5s, co oznacza 6.8x szybszy algorytm niż na początku.
def pdf_upload_2_fast_2_furious(filename, data):
"""Upload drawing in high quality."""
with open(filename, 'rb') as filehandle:
pdf = PdfFileReader(filehandle)
pages = pdf.getNumPages()
with tempfile.TemporaryDirectory() as path:
images_from_path = convert_from_path(
filename,
output_folder=path,
last_page=pages,
dpi= 300,
first_page=1,
thread_count=1,
)
images = []
for index, page in enumerate(images_from_path):
path = f'{data.full_path.replace(".jpg", "")}-{index}.jpg'
page.save(path, 'JPEG')
images.append(path)
return images
Pseudokod:
- wczytaj plik do pamięci - pobierz liczbę stron - użyj funkcji ‘convert_from_path’ , aby podzielić plik na pojedyncze JPG - zapisz pojedyncze JPG jako pliki
Tym razem algorytm wykonuje się w zaledwie 9.2 sekundy, ale istnieje możliwość przyspieszenia go poprzez zwiększenie liczby wątków biorących udział w operacji. Po wyborze dwóch wątków czas wykonania to 7.4s natomiast przy czterech 7.7s. Oznacza to, że przy wykorzystaniu jednego procesora logicznego nasz algorytm przyspieszył prawie 18-krotnie.