متن فارسی که در مسیر SSH به علامت سؤال تبدیل شد: ماجرای رکوردهای خراب
فرستادن متن فارسی از ویندوز به سرور با SSH، حروف را به ؟؟؟ تبدیل کرد و چند رکورد دیتابیس خراب شد. علت، کدپیج و راهحل ایمن را بخوانید.
برای اصلاح سریع یک متن فارسی روی سرور، فرمان را مستقیم از ویندوز فرستادیم. فرمان اجرا شد و خطایی نداد، اما متن ذخیرهشده فقط ردیفی از علامت سؤال بود. چند رکورد دیتابیس هم با همین مقدار خراب ذخیره شد. این یادداشت شرح همان اتفاق است: چه دیدیم، علت چه بود، و حالا متن فارسی را چطور به سرور میرسانیم.
ماجرا و علامتها
کار ساده به نظر میرسید: یک متن فارسی باید روی سرور اصلاح میشد. از سیستم ویندوزی خودمان با ابزار SSH وارد شدیم و فرمان را همراه با متن فارسی فرستادیم. اول متن را داخل آرگومان فرمان گذاشتیم و بعد به شکل heredoc هم امتحان کردیم.
در هر دو حالت نتیجه یکی بود. روی سرور هر حرف فارسی به «؟» تبدیل شده بود و هیچ خطایی هم دیده نشد. فرمان موفق تمام شد و مقدار خراب در دیتابیس نوشته شد. چند رکورد با همین متن خراب ذخیره شد و بعداً باید اصلاح میشد.
دو نکته از همان ابتدا مشخص بود. اول اینکه ایراد از دیتابیس یا سمت سرور شروع نشده بود: مقدار خراب از قبل در فرمان رسیده بود. دوم اینکه سیستم هیچ هشداری نداد. چیزی که خطا نمیدهد، معمولاً دیرتر و گرانتر پیدا میشود.
نکات کلیدی
- متن فارسی در آرگومان و در heredoc هر دو به ؟؟؟ تبدیل شد
- هیچ پیام خطایی دیده نشد و فرمان موفق گزارش شد
- چند رکورد دیتابیس با مقدار خراب ذخیره شد
ردگیری علت
اولین سوال این بود که مشکل از روش گذاشتن متن در فرمان است یا از خود مسیر انتقال. چون هم آرگومان و هم heredoc همین نتیجه را دادند، مشکل کوتوِیشن (quote) یا روش نوشتن فرمان نبود. همین مشاهده ساده، دایره را به مسیر انتقال محدود کرد.
بعد به سراغ سمت دیگر رفتیم: آیا سرور متن را اشتباه ذخیره میکند؟ ولی علامت سؤال در مقدار ذخیرهشده یک نشانه مهم بود. اگر سرور بایتهای درست را با کدگذاری اشتباه میخواند، معمولاً حروف عجیب و ناخوانا میدیدیم (چیزی شبیه به حروف لاتین با نشانههای اضافه). یک ردیف علامت سؤال یکدست، نشان میدهد اطلاعات حروف پیش از رسیدن به سرور از بین رفته است. این استدلال است، نه اندازهگیری روی بایتهای مسیر؛ ولی با رفتار ابزار ویندوزی که در بخش بعد میآید جور درمیآید.
در عمل، راهی که قطعیاش میکند این است که فایلی را که به سرور رسیده یا خروجی دریافتی را به شکل بایتی (hex) نگاه کنید. اگر بهجای بایتهای حروف فارسی فقط مقدار ۳f تکرار شده باشد، آسیب قبل از سرور رخ داده است.
نکات کلیدی
- شکست در هر دو روش آرگومان و heredoc یعنی مشکل از quoting نیست
- علامت سؤال یکدست یعنی اطلاعات حروف قبل از رسیدن به سرور از بین رفته
- برای اطمینان، بایتها را با hex ببینید، نه فقط متن نمایشدادهشده
علت اصلی: کدپیج کنسول بهجای UTF-8
ابزار SSH ویندوزی آرگومانها را با کدپیج کنسول میفرستاد، نه با UTF-8. کدپیج جدولی است که هر کد بایتی را به یک نویسه نگاشت میکند. مستندات مایکروسافت درباره Console Code Pages توضیح میدهد که هر کنسول کدپیج ورودی و خروجی جداگانه دارد، و متن UTF-8 فقط وقتی به APIهای خانواده A داده میشود که کدپیج روی 65001 (یعنی CP_UTF8) تنظیم شده باشد. همان مستند توصیه میکند برنامههای جدید از کدپیجها دور بمانند و از Unicode (خانواده W با UTF-16) استفاده کنند.
کدپیج پیشفرض یک سیستم ویندوزی معمولاً یک کدپیج قدیمی تکبایتی است که حروف فارسی در آن وجود ندارد. وقتی متن یونیکد باید به چنین کدپیجی تبدیل شود، نویسههایی که در آن جدول نیستند معمولاً با «?» جایگزین میشوند. این تبدیل در سمت فرستنده و قبل از ارسال انجام میشود. از آن لحظه به بعد، در جریان داده فقط بایت ۳f (همان «?» در ASCII) هست و هیچ ردی از حرف اصلی نمانده است.
دلیل اینکه سرور اعتراضی نکرد هم همین است. طبق RFC 3629، در UTF-8 نویسههای ASCII با یک بایت نوشته میشوند و نویسههای بالاتر با دو تا چهار بایت؛ حروف فارسی دو بایتیاند. علامت سؤال یک بایت ASCII کاملاً معتبر است، پس سرور هیچ بایت نامعتبری نمیدید که رد کند. دیتابیس هم فقط یک رشته معتبر را ذخیره کرد. نکته مهمتر: برخلاف خطای تبدیل در سمت دیتابیس (مثلاً مستندات PostgreSQL میگوید اگر نویسهای قابل تبدیل بین کدگذاریها نباشد، خطا گزارش میشود)، جایگزینی در سمت کلاینت بیصدا اتفاق میافتد، چون از دید سرور داده سالم است.
خلاصه اینکه زنجیره درست این بود: متن یونیکد روی ویندوز، تبدیل به کدپیج کنسول (با جایگزینی بیصدا)، ارسال از طریق SSH، و ذخیرهشدن یک رشته معتبر ولی بیمعنی. هر کدام از حلقهها جداگانه «درست کار میکرد».
نکات کلیدی
- کدپیج کنسول ویندوز با UTF-8 یکی نیست و آرگومانها از آن عبور میکنند
- نویسهای که در کدپیج نباشد به «?» (بایت ۳f) تبدیل میشود و اطلاعات برنمیگردد
- سرور فقط ASCII معتبر میبیند، پس خطایی نمیدهد
راهحل: فایل UTF-8 و انتقال فایل
تصمیمی که گرفتیم ساده است: متن فارسی هرگز از مسیر آرگومان یا heredoc ویندوزی عبور نمیکند. هر متن یا SQL فارسی را روی سیستم خودمان در یک فایل UTF-8 میسازیم، خود فایل را به سرور منتقل میکنیم و روی سرور از روی فایل اجرایش میکنیم. چون فایل با بایت منتقل میشود، هیچ تبدیل کدپیجی در راه نیست. جستجوی فارسی روی سرور هم همینطور است: عبارت جستجو را داخل فایل میگذاریم و از روی فایل میخوانیم، نه از روی آرگومان.
نمونه عمومی زیر، با مقادیر ساختگی، همین الگو را نشان میدهد. دو نکته مهم است: فایل باید UTF-8 باشد (ترجیحاً بدون BOM)، و پیش از اجرا بایتهای فایل روی سرور بررسی شود.
# روی ویندوز (PowerShell 7): ساخت فایل UTF-8 بدون BOM
Set-Content -Path fix.sql -Value $sql -Encoding utf8NoBOM
# انتقال فایل بهصورت بایتی
scp fix.sql deploy@example.com:/tmp/fix.sql
# بررسی بایتها روی سرور: حروف فارسی باید با d8 یا d9 شروع شوند
ssh deploy@example.com "head -c 64 /tmp/fix.sql | xxd"
# اجرا از روی فایل (دستور کلاینت دیتابیس را جایگزین کنید)
ssh deploy@example.com "your-db-client -f /tmp/fix.sql"نکات کلیدی
- هر متن یا SQL فارسی را در فایل UTF-8 بسازید و فایل را منتقل کنید
- جستجوی فارسی را هم از روی فایل انجام دهید
- قبل از اجرا، بایتهای فایل را روی سرور ببینید
درسها و چکلیست
اولین درس این است که مسیر انتقال متن، خودش بخشی از سیستم است و باید برای یونیکد تست شود. تا وقتی همه تستها ASCII بودند، این مسیر «کار میکرد». دومین درس این است که عملیات دادهای را اول روی یک رکورد امتحان کنیم. اگر همین فرمان اول روی یک رکورد اجرا شده بود، هزینه اشتباه یک ردیف بود، نه چند رکورد.
نکته سوم دربارهی خود مسئله است: وقتی خطایی نیست، موفقیت فرمان دلیلی بر درستی داده نیست. باید مقدار نوشتهشده را بعد از عملیات دوباره بخوانیم و با مقدار مورد انتظار مقایسه کنیم. اگر از ابتدا برای خطاهای رایج فارسی در رابط کاربری دقت داشتهاید (مثلاً در تایپوگرافی فارسی و RTL)، این همان دقت است ولی این بار در لایه انتقال داده.
در کار با سرورها و زیرساخت، این الگو را بخشی از روال کاری قرار دادهایم؛ جزئیات رویکردمان در خدمات سرور و زیرساخت و پروژه زیرساخت سرورها آمده است.
نکات کلیدی
- مسیر انتقال متن را با نمونه فارسی/یونیکد تست کنید، نه فقط ASCII
- هر عملیات دادهای را اول روی یک رکورد امتحان کنید
- بعد از نوشتن، مقدار را دوباره بخوانید و بایتها را بررسی کنید
- متن غیر ASCII را بهصورت فایل UTF-8 منتقل کنید، نه آرگومان یا heredoc
جمعبندی
علامت سؤال بهجای حروف فارسی یعنی اطلاعات قبل از رسیدن به سرور از دست رفته است، نه اینکه سرور آن را اشتباه خوانده باشد. از آن موقع، متن و SQL فارسی را فقط با فایل UTF-8 منتقل میکنیم و هر عملیات دادهای را اول روی یک رکورد امتحان میکنیم.
منابع
- Console Code Pages - Windows Console (Microsoft Learn)learn.microsoft.com
- RFC 3629: UTF-8, a transformation format of ISO 10646rfc-editor.org
- PostgreSQL 17 Documentation: Character Set Supportpostgresql.org