در جلسه قبل برایتان توضیح خلاصه ای از صورت این تمرین دادم. بیایید کمی بیشتر با این تمرین آشنا شویم. البته در صورتی که کاملا متوجه تمرین شده اید می توانید مستقیما به بخش پاسخ مراجعه کنید. اگر یادتان باشد در جلسه قبل توضیح دادم که پوشه ای از ۴ یا ۵ تصویر را آماده کرده ام و تمام این تصاویر با فرمت jpg می باشند. همانطور که می دانید برای اجرای فایل های پایتون از درون ترمینال باید دستور زیر را در ترمینال اجرا کنیم:
python3 file.py
من می خواهم شما فایل file.py را طوری بنویسید که بتوانیم از طریق ترمینال دستور زیر را اجرا کنیم:
python3 file.py pics/ new/
در این دستور دو پارامتر را داریم: دستور pics که نام پوشه حاوی تصاویر ما است (علامت / در انتهای آن نشان دهنده همین موضوع است اما اگر از ویندوز استفاده می کنید به جای آن \ قرار دهید) سپس یک new را داریم که نام پوشه جدیدی است که حاوی تصاویر تبدیل شده خواهد بود. در حال حاضر این پوشه وجود ندارد بنابراین باید آن را درون کدهایمان بسازیم. برای انجام این کار به سه ماژول sys و os و PIL نیاز خواهید داشت. پیشنهاد می کنم که روی آن خوب فکر کرده و سعی کنید خودتان آن را جواب بدهید.
برای حل این مسئله و در قدم اول باید آرگومان های پاس داده شده را از ترمینال بگیرید (new و pics). من در این دوره به طور مستقیم روش انجام این کار را توضیح نداده ام چرا که می خواهم شما هنر جست و جو در گوگل را یاد بگیرید. در مرحله دوم باید وجود پوشه ای به نام new را بررسی کنیم و در صورتی که وجود نداشته باشد آن را بسازیم. در نهایت با حلقه ها در پوشه pics گردش کرده و تک تک تصاویر درون آن را به png تبدیل می کنیم.
برای دریافت آرگومان های موجود در ترمینال باید از sys.argv استفاده کنیم. این دستور در پایتون به ما اجازه دریافت آرگومان ها از ترمینال را می دهد:
import os import sys from PIL import Image, ImageFilter image_folder = sys.argv[1] output_folder = sys.argv[2] print(image_folder, output_folder)
argv لیستی (آرایه ای) از دستورات ترمینال را به ما می دهد و دستور ترمینال ما نیز بدین شکل است:
python3 main.py pics/ new/
آرگومان اول main.py (نام فایل من) است، آرگومان دوم pics و آرگومان سوم نیز new می باشد. ما می دانیم که ایندکس لیست ها از صفر شروع می شود بنابراین برای دریافت آرگومان دوم و سوم باید از اعداد ۱ و ۲ استفاده کنیم. من برای اطمینان این مقادیر را print کرده ام. حالا دستور زیر را اجرا می کنیم:
python3 main.py pics/ new/
نتیجه اجرای آن به شکل زیر خواهد بود:
pics/ new/
با انجام این کار مطمئن می شویم که آرگومان ها را به درستی دریافت کرده ایم. حالا باید به مرحله دوم برویم که بررسی وجود پوشه new می باشد. برای انجام این کار از ماژول os استفاده می کنیم (البته شما می توانید از ماژول pathlib نیز استفاده کنید):
import os import sys from PIL import Image, ImageFilter image_folder = sys.argv[1] output_folder = sys.argv[2] print(os.path.exists(output_folder))
برای بررسی وجود یک پوشه می توانیم از os.path استفاده کرده و متد exists را روی آن صدا بزنیم. من نتیجه آن را چاپ کرده ام تا مشخص شود که چه چیزی برای ما برمی گرداند. حالا دوباره دستور زیر را در ترمینال اجرا می کنیم:
python3 main.py pics/ new/
با اجرای این دستور مقدار False را دریافت می کنید. با این حساب متوجه می شویم که پوشه ای به نام new در محل اسکریپت ما (فایل main.py) وجود ندارد. بر این اساس می توانیم کد خود را با یک شرط ساده if پیاده سازی کنیم:
import os import sys from PIL import Image, ImageFilter image_folder = sys.argv[1] output_folder = sys.argv[2] if not os.path.exists(output_folder): os.makedirs(output_folder)
کد بالا می گوید در صورتی که output_folder وجود نداشته باشد (دستور not شرط وجود را برعکس کرده و به عدم وجود تبدیل می کند) آن را برایمان می سازد. برای ساخت پوشه جدید از متد makedirs در ماژول os استفاده می شود.
در نهایت نوبت به نوشتن حلقه for می رسد:
import os import sys from PIL import Image, ImageFilter image_folder = sys.argv[1] output_folder = sys.argv[2] if not os.path.exists(output_folder): os.makedirs(output_folder) for filename in os.listdir(image_folder): img = Image.open(f'{image_folder}{filename}') clean_name = os.path.splitext(filename)[0] img.save(f'{output_folder}{clean_name}.png', 'png') print('all done!')
ما در ابتدا از متد listdir استفاده کرده ایم تا لیستی از فایل های موجود در پوشه خود (image_folder) را داشته باشیم. با انجام این کار می توانیم در این فایل ها گردش کنیم و در هر گردش نام یکی از فایل ها را می گیریم. در مرحله بعدی با استفاده از متد open تصویر مورد نظر خودمان را باز کرده ایم. آدرس تصویر ما یک آدرس نسبی (نسبت به فایل main.py) است بنابراین ابتدا نام پوشه را دریافت می کنیم (در این مثال قرار است new باشد) و سپس نام فایل را به آن می چسبانیم. image_folder یا نام پوشه در انتهای خود علامت / را دارد و همچنین filename پسوند فایل را نیز دارد بنابراین نیازی به اضافه کردن چیزی در این قسمت نیست. در مرحله بعدی باید نام فایل بدون پسوند را دریافت کنیم (دیگر نمی خواهیم jpg. در انتهای فایل باشد) بنابراین از متد splitext روی path استفاده کرده ایم که کارش تقسیم نام فایل است. این متد یک tuple را به ما می دهد که دو عضو دارد: عضو اول نام فایل بدون پسوند و عضو دوم، پسوند فایل است بنابراین ما عضو اول (ایندکس صفر) را دریافت کرده ایم. در نهایت باید آدرس پوشه مقصد (output_folder) را به آن داده و نام بدون پسوند فایل (clean_name) را نیز به آن بدهیم و در انتها پسوند png را نیز اضافه کنیم. آرگومان دوم نیز نوع فایل خروجی را مشخص می کند که طبیعتا باید png باشد. حالا فایل را ذخیره کرده و دستور زیر را در ترمینال اجرا نمایید:
python3 main.py pics/ new/
با اجرای این کد تمام تصاویر ما به png تبدیل می شوند!
حالا که با پردازش تصویر در پایتون آشنا شده ایم بهتر است در مورد پردازش ویدیو صحبت کنیم چرا که هر دو در عمل یک کار را انجام می دهند؛ ویدیو ها مجموعه ای از تصاویر هستند که به آن ها فریم می گوییم بنابراین ویرایش ویدیو آنچنان تفاوتی با ویرایش تصاویر ندارد. زمانی که کار پردازش تصویر ما ساده باشد می توانیم از کتابخانه هایی مانند pillow استفاده کنیم اما در صورتی بخواهیم کار حرفه ای تری انجام بدهیم باید به سراغ کتابخانه هایی مانند OpenCV برویم. OpenCV یک کتابخانه بسیار قدرتمند است تا حدی که از آن به همراه یادگیری ماشینی استفاده می کنند تا ماشین های خودران را برنامه نویسی کنند بنابراین ما نمی توانیم آن را در یک دوره ساده مانند پایتون بررسی کنیم. وظیفه من آشنا کردن شما با این تکنولوژی است و از اینجا به بعد بر عهده شما است تا به دنبال آن بروید یا آن را رها کنید.
ما تا به حال با تصاویر کار کرده ایم و حالا نوبت به فایل های PDF می رسد. مثل همیشه باید یک یا چند فایل PDF را برای خودتان آماده کنید تا شروع به کدنویسی کنیم. من سه فایل PDF تصادفی را برای شروع کار انتخاب کرده ام و آن ها را در پوشه ای به نام pdf در کنار فایل main.py قرار داده ام. حتما شما هم می دانید که برای شروع کار به یک کتابخانه نیاز داریم که با فایل های PDF کار کند.
بسیاری از برنامه نویسان تازه کار تصور می کنند که استفاده از کتابخانه ها مخصوص افرادی است که زبان برنامه نویسی را به خوبی یاد نگرفته اند و می خواهند از مسئولیت نوشتن کدهای آن فرار کنند اما اصلا اینطور نیست. نقش شما به عنوان یک توسعه دهنده مانند نقش مکانیک است. هر مکانیک ابزار خودش را دارد (آچار، چکش و غیره) بنابراین شما هم ابزار خودتان را دارید (کتابخانه ها، فریم ورک ها و غیره). هیچ فرد عاقلی از مکانیک انتظار ندارد که ابتدا خودش آچار را بسازد و سپس از آچار برای سر هم کردن موتور استفاده کند! چرا؟ به دلیل اینکه فرآیند ساخت آچار و شروع کار از صفر یک فرآیند بسیار طولانی است که به زمان بسیار زیادی نیاز دارد. شما نمی توانید فقط برای کار با PDF ها یک یا دو ماه زمان بگذارید!
پکیجی که برای این پروژه انتخاب کرده ایم پکیجی به نام PyPDF2 است. بنابراین می توانیم آن را با دستور زیر نصب کنیم:
pip install PyPDF2
پس از نصب این پکیج باید به فایل main.py رفته و این پکیج را وارد آن کنیم:
import PyPDF2 with open('./pdf/dummy.pdf', 'r') as file: print(file)
من یکی از فایل های خودم به نام dummy.pdf را با with open و در حالت خواندن (r) باز کرده و در نهایت file را print کرده ام. با اجرای این کد نتیجه زیر را دریافت می کنیم:
<_io.TextIOWrapper name='./pdf/dummy.pdf' mode='r' encoding='UTF-8'>
یعنی یک شیء گرفته ایم که همان PDF ما می باشد. حالا که می دانیم فایل ما به درستی کار می کند باید به جای توابع پیش ساخته پایتون از pypdf2 استفاده کنیم. فرض کنید بخواهیم تعداد صفحات فایل PDF خود را به دست بیاوریم. برای این کار می توانیم یک reader (خواننده) ایجاد کرده و سپس از خصوصیت numPages استفاده کنیم:
import PyPDF2 with open('./pdf/dummy.pdf', 'r') as file: reader = PyPDF2.PdfFileReader(file) print(reader.numPages)
متد pdfFileReader یک reader یا خواننده به ما می هد که فایل PDF را گرفته و آن را می خواند. در نهایت من numPages را روی آن صدا زده ام تا تعداد صفحات PDF را دریافت کنیم. با اجرای کد بالا خطای زیر را می گیریم:
PdfReadWarning: PdfFileReader stream/file object is not in binary mode. It may not be read correctly. [pdf.py:1079] Traceback (most recent call last): File "/mnt/Development/Roxo Academy/Python/training4/main.py", line 4, in <module> reader = PyPDF2.PdfFileReader(file) File "/home/amir/.local/lib/python3.8/site-packages/PyPDF2/pdf.py", line 1084, in __init__ self.read(stream) File "/home/amir/.local/lib/python3.8/site-packages/PyPDF2/pdf.py", line 1689, in read stream.seek(-1, 2) io.UnsupportedOperation: can't do nonzero end-relative seeks
خطای بالا به ما می گوید که فایل PDF در قالب باینری خوانده نشده است. یعنی چه؟ کتابخانه pypdf2 فایل های PDF را در قالب باینری خوانده و پردازش می کند بنابراین ما نیز باید فایل را در قالب باینری باز کنیم:
import PyPDF2 with open('./pdf/dummy.pdf', 'rb') as file: reader = PyPDF2.PdfFileReader(file) print(reader.numPages)
با اضافه کردن حرف b به حالت خواندن فایل به پایتون گفته ایم که این فایل را در حالت باینری باز کند. حالا می توانیم دوباره کد بالا را اجرا کنیم. این بار عدد ۱ به عنوان نتیجه برایمان برگردانده می شود. چرا؟ به دلیل اینکه فایل من فقط ۱ صفحه دارد. متد بعدی ما getPage است که صفحه خاصی از PDF را برای ما دریافت می کند. استفاده از آن نیز بسیار ساده است:
import PyPDF2 with open('./pdf/twopage.pdf', 'rb') as file: reader = PyPDF2.PdfFileReader(file) print(reader.getPage(0))
من این بار فایل PDF دیگری به نام twopage.pdf را باز کرده ام چرا که دو صفحه دارد. حالا متد getPage را از reader آن صدا زده ام و عدد صفر (صفحه اول) را به آن پاس داده ام. با اجرای این کد نتیجه ای مانند نتیجه زیر را دریافت می کنیم:
{'/Type': '/Page', '/Parent': IndirectObject(3, 0), '/Resources': {'/Font': {'/F1': IndirectObject(9, 0)}, '/ProcSet': IndirectObject(8, 0)}, '/MediaBox': [0, 0, 612, 792], '/Contents': IndirectObject(5, 0)}
این یک شیء خاص است که توسط pypdf2 ساخته می شود و حاوی انواع و اقسام اطلاعات راجع به فایل ما است. مثلا اگر بخواهیم متن pdf را دریافت کنیم باید به شکل زیر عمل کنیم:
import PyPDF2 with open('./pdf/twopage.pdf', 'rb') as file: reader = PyPDF2.PdfFileReader(file) print(reader.getPage(0).extractText())
متد extractText وظیفه استخراج متن از این فایل pdf را دارد بنابراین با ارجای آن متن درون فایل twopage.pdf را دریافت می کنیم:
A Simple PDF File This is a small demonstration .pdf file - just for use in the Virtual Mechanics tutorials. More text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. Boring, zzzzz. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. And more text. Even more. Continued on page 2 ...
متنی که در این بخش می بینید همان متن فایل PDF من است. طبیعتا فایل PDF شما هر چیزی باشد، متن همان فایل برگردانده خواهد شد.
متد بعدی ما برای چرخاندن صفحات PDF است. کتابخانه pypdf2 دو متد دیگر به نام های rotateClockwise (چرخش در جهت عقربه های ساعت) و rotateCounterClockwise (چرخش در خلاف جهت عقربه های ساعت) دارد که به ما در این زمینه کمک می کنند. مثال:
import PyPDF2 with open('./pdf/twopage.pdf', 'rb') as file: reader = PyPDF2.PdfFileReader(file) print(reader.getPage(0).rotateClockwise(90))
من در کد بالا گفته ام که صفحه اول PDF ما (شمارش صفحه از صفر شروع می شود) باید ۹۰ درجه در جهت عقربه های ساعت بچرخد. به نظر شما با اجرای این کد چه اتفاقی می افتد؟ با اجرای این کد نتیجه زیر را دریافت می کنیم:
{'/Type': '/Page', '/Parent': IndirectObject(3, 0), '/Resources': {'/Font': {'/F1': IndirectObject(9, 0)}, '/ProcSet': IndirectObject(8, 0)}, '/MediaBox': [0, 0, 612, 792], '/Contents': IndirectObject(5, 0), '/Rotate': 90}
باز هم یک شیء PyPDF2 دریافت کرده ایم. چرا؟ به دلیل اینکه صفحه مورد نظر ما چرخیده است اما فعلا روی مموری قرار دارد و روی فایل اصلی (روی هارد دیسک) ذخیره نشده است. با این حساب اگر فایل PDF خود را باز کنید می بینید که هیچ تغییری ایجاد نشده است. برای حل این مشکل باید صفحه ای که در مموری چرخیده است را در هارد دیسک ذخیره کنیم. چطور؟
import PyPDF2 with open('./pdf/twopage.pdf', 'rb') as file: reader = PyPDF2.PdfFileReader(file) writer = PyPDF2.PdfFileWriter() page = reader.getPage(0) page.rotateClockwise(90) writer.addPage(page) with open('./pdf/tilted.pdf', 'wb') as new_file: writer.write(new_file)
ما در ابتدا یک reader (خواننده - فایل را می خواند) و یک writer (نویسنده - درون فایل می نویسد) را ساخته ایم. در مرحله بعدی صفحه اول (شماره صفر) را از آن دریافت کرده ایم و آن را در متغیر page ذخیره کرده ایم. یعنی چه؟ یعنی این صفحه در حال حاضر در مموری ذخیره شده است. حالا می توانیم با استفاده از متد addPage یک صفحه جدید اضافه کنیم و page را به آن پاس بدهیم. در مرحله بعدی با open دوباره فایلی به نام tilted.pdf را باز می کنیم. طبیعتا چنین فایلی هنوز وجود ندارد و ما می خواهیم آن را بسازیم. اگر یادتان باشد گفتم که کتابخانه PyPDF2 با فایل ها در حالت باینری کار می کند بنابراین باید حالت باز کردن فایل را wb قرار بدهیم. در نهایت از writer استفاده می کنیم تا با متد write آن، فایل جدید را روی هارد دیسک ذخیره کنیم. با اجرای این کد نتیجه ای را در کنسول نمی گیریم اما اگر به فایل های خودتان نگاه کنید، فایل tilted.pdf را پیدا خواهید کرد که صفحه اول twopage است که ۹۰ درجه چرخیده است.
با اینکه کدهای نوشته شده در بخش قبلی به درک بهتر ما از PyPDF2 کمک کرده اند اما از نظر عملی، مواقع زیادی وجود ندارند که در آنها نیاز به چرخاندن فایل PDF داشته باشیم به همین دلیل در این قسمت می خواهیم یک تمرین خاص را حل کنیم. این تمرین از نظر استفاده، شباهت زیادی به تمرین تبدیلگر تصاویر ما دارد. شما باید کدها را طوری بنویسید که اسکریپت بتواند چنین کدی را در ترمینال اجرا کند:
python main.py dummy.pdf twopage.pdf tilted.pdf
من در این قسمت نام سه فایل PDF را پاس داده ام و انتظار دارم اسکریپت ما هر سه فایل را در یک فایل PDF ادغام کند. طبیعتا ممکن است که کاربر یک فایل، دو فایل یا حتی ۱۰۰ فایل را به ما پاس بدهد. اسکریپت ما باید در تمامی این شرایط کار کند و بتواند هر تعداد فایلی را دریافت کند. برای انجام این کار از یک ترفند ساده استفاده می کنم:
import PyPDF2 import sys inputs = sys.argv[1:]
اگر یادتان باشد در جلسات مربوط به مبانی اولیه پایتون، این روش خاص برای انتخاب ایندکس را برایتان توضیح داده بودم. با این روش می توانیم از ایندکس ۱ تا آخرین ایندکس را انتخاب کرده و درون input بگذاریم. در مرحله بعدی تابعی می نویسیم که لیستی از فایل های PDF ما را دریافت کرده و سپس بین آن ها گردش می کند:
import PyPDF2 import sys inputs = sys.argv[1:] def pdf_combiner(pdf_list): for pdf in pdf_list: print(pdf) pdf_combiner(inputs)
من می خواهم ابتدا منطق اولیه کد را تست کنیم بنابراین بین لیست PDF ها گردش کرده و فعلا فقط نام آن ها را پرینت کرده ام. لیست PDF ها طبیعتا همان inputs است بنابراین در هنگام فراخوانی تابع pdf_combiner آن را پاس داده ام. حالا فایل خودتان را ذخیره کرده و دستور زیر را در ترمینال اجرا کنید:
python3 main.py ./pdf/dummy.pdf ./pdf/twopage.pdf ./pdf/tilted.pdf
از آنجایی که فایل های PDF من درون پوشه ای به نام pdf هستند باید آدرس نسبی را با ذکر پوشه بدهم (آدرس نسبت به فایل main.py). با اجرای این کد نتیجه زیر را می گیریم:
./pdf/dummy.pdf ./pdf/twopage.pdf ./pdf/tilted.pdf
بنابراین همه چیز به خوبی کار می کند. برای اینکه کار ما شلوغ نشود من فایل های PDF خود را از پوشه pdf خارج کرده و در کنار main.py قرار می دهم تا بتوانیم نامشان را بدون ذکر پوشه pdf بیاوریم. در مرحله بعدی باید به جای چاپ نام این فایل ها از یک merger (ادغام گر) استفاده کنیم که به صورت پیش فرض در کتابخانه PyPDF2 موجود است:
import PyPDF2 import sys inputs = sys.argv[1:] def pdf_combiner(pdf_list): merger = PyPDF2.PdfFileMerger() for pdf in pdf_list: merger.append(pdf) merger.write('combined.pdf') pdf_combiner(inputs)
من با صدا زدن متد PdfFileMerger یک merger یا ادغام گر را دریافت می کنم. حالا می توانم با استفاده از آن در حلقه for تک تک این فایل های PDF را به هم ضمیمه کنم (append به معنی «ضمیمه» می باشد و کارش هم همین است). در نهایت خارج از حلقه for باید writer را صدا بزنیم که مسئولیت نوشتن pdf نهایی را دارد. توجه داشته باشید که تا قبل از writer همه چیز در مموری (RAM سیستم) انجام می شود و هنوز چیزی در هارد دیسک وجود ندارد بنابراین تا زمانی که writer را صدا نزنید هیچ اتفاقی نمی افتد. من نام combined.pdf را برای فایل نهایی انتخاب کرده ام اما شما می توانید نام دیگری را انتخاب نمایید.
در مرحله بعدی و پس از ذخیره کد بالا، می توانیم کد زیر را در ترمینال اجرا کنیم:
python3 main.py ./dummy.pdf ./twopage.pdf ./tilted.pdf
طبیعتا با اجرای این کد نتیجه ای نمی گیریم اما اگر به پوشه پروژه نگاه کنید، یک فایل به نام combined.pdf را مشاهده خواهید کرد که تمام pdf های ما را در خود دارد.
من یک فایل واترمارک نمونه را برای شما قرار می دهم. برای انجام این پروژه به این فایل نیاز پیدا خواهید کرد:
آیا می دانید واترمارک چیست؟ واترمارک همان تصاویر و نقوش کمرنگی هستند که به پس زمینه متون در فایل های PDF را روی تصاویر اضافه می شوند و نشان دهنده تعلق آن فایل به شرکت یا فرد خاصی هستند. ما می خواهیم در این تمرین اسکریپتی بنویسیم که بتواند این واترمارک را به تمام صفحات یک فایل PDF اضافه کند. من کدهای آماده را برایتان قرار داده و سپس آن را خط به خط بررسی می کنیم:
import PyPDF2 template = PyPDF2.PdfFileReader(open('combined.pdf', 'rb')) watermark = PyPDF2.PdfFileReader(open('wtr.pdf', 'rb')) output = PyPDF2.PdfFileWriter() for i in range(template.getNumPages()): page = template.getPage(i) page.mergePage(watermark.getPage(0)) output.addPage(page) with open('watermarked_output.pdf', 'wb') as file: output.write(file)
من در ابتدا دو فایل pdf مورد نظرم را باز کرده ام و در reader های خودشان قرار داده ام (template و watermark) سپس یک writer را نیز ساخته ام تا با آن چیزی را بنویسیم. ما نمی دانیم که فایل اصلی (template) چند صفحه خواهد داشت بنابراین از یک حلقه for استفاده می کنیم تا بین صفحات آن گردش کنیم. چطور؟ با تابع range که قبلا با آن آشنا شده ایم و پاس دادن تعداد صفحات (numPages) به آن. حالا هر صفحه را گرفته و با تابع mergePage آن ها را در هر ادغام می کنیم. وظیفه mergePage ادغام صفحات با یکدیگر است به طوری که باید یک صفحه را داشته باشید (page) و سپس mergePage را روی آن صدا زده و صفحه دیگری را به آن بدهید. در نهایت با استفاده از with open می توانیم pdf نهایی را ایجاد کنیم. اگر کدهای بالا را اجرا کنید یک PDF جدید دریافت خواهید کرد.
امیدوارم از این قسمت لذت برده باشید. در جلسه بعدی به سراغ ارسال ایمیل و حتی ساخت یک پروژه برای بررسی قدرت پسورد های مختلف را راه اندازی می کنیم.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.