Base64 ASCII و بایناری و utf-8

ASCII یک استاندارد کدگذاری داده هاست که توی ارتباطات کاربرد داره. این استانداره 128 کاراکتر داره که 95 عدد اون نوشتنی هستند. جدول زیر نشون دهنده کاراکترهای این استاندارد هستند.

بایناری که همان دودویی هست نقش مهمی توی کامپیوتر بازی میکنه خیلی از فایلها محتوای دودویی دارن مثل عکس ویدیو و pdf و هر فایل دیگه ای. علاوه بر همه اینها ممکنه توی یک برنامه یک آبجکت یا داده ساختار هم باشه که طبیعتا این هم دودویی هست. شامل 0 و 1

نکته مهم اینه که خیلی از پروتکل های ارتباطی قابلیت ارسال محتوای دودویی رو ندارن مثل HTTP یا SMTP یعنی اگر یک ایمیل داشته باشید نمیتونید باهاش فایل بایناری بفرستید. ولی ما همه تست کردیم و شده چراکه اون اپلیکیشن ایمیل اول اون فایل دودویی رو به یک استاندارد دیگه تبدیل میکنه و میفرسته و در مقصد هم اون رو دوباره به دودویی تبدیل میکنه و تحویل گیرنده میده. چراکه پروتکل های گفته شده بر اساس تکست کار میکنن محتوایی که قابل پرینت شدن باشن نه دودویی. بنابراین اگر از طریق این پروتکل ها یا توی JSON بخوایم یک محتوای دودویی بفرستیم اول باید به تکست encode بشه بعد توی مقسد از تکست به بایناری decode بشه.

استانداردی که برای این کار استفاده میشه Base64 هست. Base64 مخصوص تبدیل کردن محتوای دودویی به ASCII های قابل چاپ هست. کاراکترهای موجود در Base64 شامل A تا Z به اضافه a تا z به اضافه 0 تا 9 به اضافه + / = هستند. یعنی از 000000 تا 111111 رو شامل میشه بنابراین هر بایت به یک کاراکتر تبدیل میشه و میتونیم روی وب یا پروتکل های دیگه که بر اساس تکست کار میکنن بفرستیم.

آیا فرستادن فایل های بایناری روی وب و تبدیل به Base64 کار خوبیه؟ کاربردهایش چیه؟

1.اتچ کردن فایل بایناری توی ایمیل

2.ارسال فایل بایناری روی HTTP یا SMTP با JSON یا XML

3.ذخیره کردن آبجکتهای کوچیک بایناری توی فایل متنی

4.امبد کردن فایل های بایناری مثل عکس توی HTMLیا CSS

استفاده از Base64 حدود 33 درصد افزایش حجم خواهد داشت بنابراین استفاده از این برای فایل بزرگ توصیه نمیشه. برای فایلهای حجیم باید فایل مستقیم بدون encodeشدن از سرور دانلود شه چرا که تبدیل حجم زیاد خودش از هر نظر هزینه بر هست و فایل حجیم هم نیازی نیست که توی HTML بخوایم امبد کنیم و بهتره لینک دانلود بدیم یا استریم کنیم.

حالا ما میخوایم یک فایل pdf توی بروزر نشون بدیم یک راه حل اینه که فایل رو مستقیم لینک بدیم توی فرانت اند از pdf viewer خود بروزر استفاده کنیم یا iframe چون این دوتا میتونن فایل pdf که بایناری هست رو نشون بدن.

به دلایلی من تصمیم گرفتم که pdf رو هر صفحه شو به یک عکس تبدیل کنم و توی فرانت نشون بدم آیا میتونم این کار رو بکنم. HTTP میتونه فایل بایناری بفرسته ولی توی HTML نمیتونم فایل بایناری امبد کنم اصلا base64 برای همین اختراع شده که تبدیلش کنم به base64 که توی html قرار بگیره.

حالا میتونم توی بک اند تبدلیش کنم به base64 و بفرستم سمت فرانت و فرانت مستقیم بزاره توی html یا میتونم pdf رو به صورت بایناری بفرستم فرانت و اونجا base64 اش کنم و توی فرانت بزارم توی html. ترجیه میدم توی بک اند این کار رو بکنم.

توی کد بالا یک صفحه از pdf رو میگیره و از بایناری تبدیلش میکنه به Base64 که برای امبد شدن توی HTML خوبه و میفرسته و ابته قبل از فرستادن تبدیل میکنه به utf-8 و این دیگه چیه؟

مساله اینجاست که تابع bs64encode فایل بایناری رو به کاراکترهای Base64 تبدیل میکنه ولی هنوز هم تایپش بایناریه یعنی مقدارش کاراکتر base64 ولی وقتی میزنیم print(type(… میبینیم که بایناری هست نوعش مثل b’VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHN0cmluZw==’

خوب این مشکلش چیه توی پایتون مشکلی پیش نمیاد چون ظاهرا پایتون میخات بفرسته تبدلیش میکنه به str ولی برای اینکه explicit باشیم باید به utf-8 اونو دیکد کنیم که دیگه تایپش هم str بشه. در ضمن بروزر های مدرن هم این تبدیل رو خودشون ظاهرا هوشمندانه انجام میدن.

ببینید ما اول بایناری رو تبدیل کردیم به base64 و همینطور که توی جدول ها معلومه base64 خودش زیرمجموعه ASCII هست و از طرفی خود ASCII زیرمتجموعه UTF-8 هست بنابراین توی این کیس مشکل خاصی هم پیش نمیاد. ولی اگر تبدیل برعکس باشه ممکنه مشکل ایجاد بشه.

حالا که همه چی اوکی هست چرا نهایتا باید تبدیل کنیم به utf-8 وقتی که خود بروزر و پایتون می فهمند که string هستن.

1.باید Explicit باشیم. شاید در آینده مشکلی پیش بیاد بهتره مستقیم خودمون تبدیل کنیم به utf-8 که تبدیل به string بشه

2.رشته string و utf-8 استاندارد وب هست. وقتی با وب کار میکنیم همیشه تبدیل کنیم به utf-8

3.شاید یک اپلیکیشن یا بروزر صراحتا به string بودن داده گیر بده پس استاندارد وب رو رعایت کنیم

پینوشت خود utf-8 چیه؟

utf-8 استاندارد کد کردن محتوا و بیشترین انطباق رو با unicode داره یعنی خیالمون راحته که اکثر زبانها و کاراکتر های چاپی و غیرچاپی رو شامل میشه. توسط بیشتر از 95 درصد مرورگرها پشتیبانی میشه و Unix هم کاملا باهاش کار میکنه. پس از این به بعد خواستیم داده ای انتقال بدیم بهتره تبدیل کنیم به utf-8.

اگر بایناری بود اول تبدیل کنیم به base64 که از بایناری به محتوای متنی تبدیل بشه بعد تبدیلش کنیم به utf-8 که توی بهترین استاندارد و در type یک رشته string باهاش کار کنیم.

جلوگیری از لاگین همزمان با یک نام کاربری

اگر بخایم یک نام کاربری نتونه همزمان توی دو تا سیستم لاگین کنه باید چیکار کنیم؟ باید یک Middleware بنویسیم که اگر سشن کاربر با سشن ذخیره شده قبلی برابر نبود اون رو پاک کنه و جدید رو بنویسه.

حالا این میدلویر رو باید اضافه کنیم

اگر اون سشنی که پاک شده بعدش بیاد لاگین بزنه به ارور میخوره بنابراین باید این تابع رو یه تغییراتی بدیم

اینکه این کد به درستی کار میکنه کافیه؟

آیا اینکه کد زیر به درستی کار میکنه کافیه؟

تابع بالا به درستی کار میکنه اما با تابع زیر جایگزینش کردم:

توی کد اول از دو فرم استفاده شده اما صرفا جهت رندر شدن توی HTML و از هیچ کدوم از قابلیت های دیگه فرم استفاده نشده. توی فرم هر صحت دیتای هر فیلد بر اساس دیتابیس بررسی میشه و صحت سنجی میشه. هر ارور مرتبطی با کل فرم یا هر کدوم از فیلدها در صورت وجود به کاربر نشون داده میشه. تمامی validation های لازم بعد از صدا زدن تابع is_valid انجام میشه. تمامی این موارد توی کد اول انجام نشده و در کل از فرم جنگو اصلا استفاده نشده. بخاطر همین کد دوم قابل اعتمادتره و قطعا باگهای کمتری دیده خواهد داشت. از طرفی واقعا کد دوم خواناتر و زیباتر هست.

چطور برای دو مدل که با هم ارتباط دارند Form و View بنویسیم

دو فرم که با مدل های آنها با هم ارتباط دارند رو میشه توی یک فرم به شکل زیر ساخت:

view باید به شکل زیر باشه

پر شدن فیلد اتوماتیک توی فرم

توی یک فرم یک فیلد داریم بنام full_name که بر اساس first_name و last_name توی view باید ساخته بشه. برای اینکه بشه توی فرم و view ازش استفاده کرد توی fields مربوط به فرم اونو آوردیم.

چون این فیلد اتوماتیک ساخته میشه نمیخایم توی وب نشون داده بشه بنابراین expert_form.fields['full_name'].widget = forms.HiddenInput() ولی مشکل اینجاست که قبل از اینکه فرم post بشه ارور میگیریم که این فیلد باید پر بشه از طرفی Hidden هست.

راه حل این شد که باید توی __init__ فرم این رو از حالت required خارج کنیم.

بهترین روش استفاده از فرم ها توی جنگو

فرم در جنگو قابلیت های زیادی داره و من توی پروژه های مختلف هر بار چالش هایی داشتم که به مرور متوجه شدم که این قابلیت ها خیلی میتونه کمک کنه.

1.فرض کنید که توی یک فرم از یک مدل ما میخایم که یک فیلد رو نشون کاربر نده و اون رو پنهان کنه برای اون کار باید از HiddenInput استفاده کرد link_form.fields['provider'].widget = forms.HiddenInput()

2.فرض کنید به همون فیلد فرم میخایم یه مقدار اولیه بدیم که دیگه نیاز نباشه کاربر فیلدی رو پر کنه. link_form.initial = {'provider': svc}

3.هر فیلد توی یک فرم یک queryset داره که متونید اون رو تغییر بدید و مثلا یه فیلدش رو کم کنید یا sort کنید. self.fields['provider'].queryset.order_by('name')

4.یکی از نکات مشکلسازی که وجود داره. اگر میخاید فیلدهای یک فرم رو توی view تغییراتی بهش بدید (از اون چیزی که کاربر وارد میکنه) باید اون فیلد رو save کنید و اون instance ساخته شده رو تغییر بدید و بعدش commit کنید. link_instance = link_form.save(commit=False)

5.اگر بخایم مقدار وارد شده توسط کاربر رو بگیریم باید از cleaned_data استفاده کنیم. link_form.cleaned_data['provider'].name

6.توی خود کلاس Form میتونید برای هر فیلد محدودیت هایی ایجاد کنید و در صورت تخطی از اون محدودیت ها ValidationError برگردونید.

Form و View ای که تمامی این موارد رو توش داره رو در کدهای زیر میبینید. هر بار نیاز به زدن چنین فرمهایی بودیم بد نیست یه سری به اینا بزنیم.

و فرم مربوطه

Signal و Middleware توی پروسه لاگین (جلوگیری از چندین لاگین همزمان با یک نام کاربری)

توی یه پروژه خواستیم یک نام کاربری دو تا session نداشته باشه یعنی یه کاربر توی دوتا مرورگر یا دو تا سیستم لاگین نکنه. برای این کار از Middleware و Signal توی پروژه استفاده کردم.

توی Middleware.py توی app مربط کد زیر کد زیر رو زدم:

حالا اینو توی MIDDLEWARE توی SETTING اضافه میکنیم.

Signal زیر رو اضافه میکنم که اگر کسی logtout کرد session اش پاک شه که بشه جای دیگه لاگین کنن:

خروجی کد مصاحبه

خروجی کد زیر چیه؟

طبق حدثم:

ولی else چی میگه

خروجی:

یعنی اگه Exception اتفاق نیوفته else هم اتفاق میوفته. در هر حالتی finally اتفاق میوفته

سوال مصاحبه ارثبری

امروز توی یه مصاحبه سوال زیر مطرح شد و گفتن که خروجی برنامه چیه و من بعدا که تستش کردم سورپرایز شدم.

خروجی کد از قرار زیره:

بله سورپرایز شدیم. حالا برای اینکه بفهمیم چرا اینطور شد کد زیر رو اجرا میکنیم:

و خروجی کد:

خوب مشخص شد مساله چیه. توی ارثبری کلاس ها توی پایتون حتی اگه هیچی هم تغییر نکنه کلاس جدید به جای جدیدی از حافظه اشاره میکنه. در واقع کلاس پدر کپی میشه توی جای جدیدی از حافظه و کلاس فرزند یه pointer به جای جدیده. و در زمان اجرا کلاس هر تغییری هم بکنه به جای جدیدی نمیره که بنابراین آدرس حافظه کلاس در هیچ شرایطی تغییر نمیکنه.

اما متغیری که توی کلاس تعریف شده داستانش فرق میکنه. توی کلاس A پدر متغیر val به یه آدرس توی حافظه اشاره میکنه. کلاس B و C که فرزند هستن به همون آدرس حافظه اشاره مکنه. حالا وقتی B.val رو تغییر میدیم (از اونجایی که integer توی پایتون immutable هست) اون خونه حافظه تغییر نمیکنه بلکه یک خونه جدید با مقدار جدید ساخته میشه و B.val به اون اشاره میکنه. حالا باید خونه قبلی حافظه رو پاک کنه اما چون 2 تا pointer دیگه بهش وصله اونو پاک نمیکنه (Garbage collector وقتی اون خونه حافظه رو پاک میکنه که هیچ pointer ای به اون اشاره نکنه)

حالا داستان دوم. با این منطق اگر A.val رو تغییر بدیم باید C.val تغییر نکنه چون A.val به آدرس جدید اشاره میکنه ولی خانه قبلی پاک نمیشه چون هنوز C.val به خانه قبلی اشاره میکنه. اما مساله دیگه ای هم هست این جا از ارثبری استفاده کردیم و C فرزند A هست پس حالا که A.val به خانه جدیدی از حافظه اشاره میکنه این pointer رو توی فرزندانش هم عوض میکنه و بازم C.val و A.val به یک خانه جدید یکسان اشاره میکنن. ولی B.val دیگه این اتفاق براش نمیوفته چون از قبل به خونه دیگه ای اشاره میکنه (تغییر کرده) اگر پایتون این رو جوری دیگه پیاده سازی میکرد قطعا به مشکل میخوردیم.

واقعا سوال باحالی بود.