1. مقدمة
في الواقع ، منذ أن بدأت في كتابة رمز Java ، واجهت عددًا لا يحصى من المشكلات المشوهة والترميز ، مثل الكود المشتعل الذي يحدث عند القراءة من ملف نصي إلى سلسلة ، رمز مشجعة يحدث عند الحصول على معلمات طلب HTTP في رمز servlet ، يتم تشغيله عند الاستعداد بواسطة JDBC ، وما إلى ذلك. هذه المشكلات شائعة للغاية. عندما تواجههم ، يمكنك حلها بنجاح من خلال البحث عنها ، لذلك ليس لديك فهم متعمق.
حتى قبل يومين ، تحدثت معي زميلي عن مشكلة ترميز ملف Java Source (يتم تحليل هذه المشكلة في المثال الأخير) ، وبدأت بهذه المشكلة وبدأت بمجموعة من المشكلات. ثم ناقشنا أثناء البحث في المعلومات. في وقت متأخر من الليل ، وجدنا أخيرًا فكرة رئيسية في مدونة ، وحل جميع الشكوك ، والجمل التي لم نفهمها من قبل يمكن شرحها بوضوح. لذلك ، قررت استخدام هذا المقال لتسجيل فهمي لبعض مشاكل الترميز ونتائج التجربة.
بعض المفاهيم التالية هي فهمي الخاصة بناءً على الظروف الفعلية. إذا كان هناك أي خطأ ، فيرجى التأكد من تصحيحها.
2. ملخص المفهوم
في الأيام الأولى ، لم يتم تطوير الإنترنت بعد ، واستخدمت أجهزة الكمبيوتر فقط لمعالجة بعض البيانات المحلية ، حيث صممت العديد من البلدان والمناطق مخططات ترميز للغات المحلية. يسمى هذا النوع من الترميز الإقليمي المرتبط بشكل جماعي بترميز ANSI (لأنها ملحقات لرموز ANSI-ASCII). ومع ذلك ، لم يناقشوا مسبقًا كيف يكونون متوافقين مع بعضهم البعض ، ولكن بدلاً من ذلك فعلوا ذلك ، مما وضع جذر ترميز النزاعات. على سبيل المثال ، فإن ترميز GB2312 المستخدم في الصراعات البرائحة مع ترميز Big5 المستخدم في تايوان. يمثل نفس البايتان أحرفًا مختلفة في مخططين الترميز. مع ظهور الإنترنت ، غالبًا ما يحتوي المستند على لغات متعددة ، ويواجه الكمبيوتر مشكلة عند عرضه لأنه لا يعرف أي من هاتين البايتين ينتميون إليه.
مثل هذه المشكلات شائعة في العالم ، لذا فإن الدعوة إلى إعادة تعريف مجموعة الأحرف الشائعة والرقم الموحد لجميع الشخصيات في العالم يرتفع.
ونتيجة لذلك ، ظهر رمز Unicode ، وهو ترقيم جميع الشخصيات في العالم بشكل موحد. نظرًا لأنه يمكن أن يحدد حرفًا بشكل فريد ، يجب تصميم الخط فقط لرمز Unicode. ومع ذلك ، فإن معيار Unicode يحدد مجموعة أحرف ، لكنه لا يحدد مخطط الترميز ، أي أنه يحدد فقط الأرقام التجريدية والأحرف المقابلة ، ولكنه لا يحدد كيفية تخزين سلسلة من أرقام Unicode. المطلب الحقيقي هو كيفية تخزين UTF-8 و UTF-16 و UTF-32 والحلول الأخرى. لذلك ، يمكن تحويل الترميزات مع بدايات UTF مباشرة من خلال العمليات الحسابية وقيم Unicode (نقاط الترميز ، نقاط الكود). كما يوحي الاسم ، فإن UTF-8 هو ترميز بطول 8 بت ، وهو ترميز متغير طول ، باستخدام 1 إلى 6 بايت لترميز حرف (لأنه مقيد بمدى Unicode ، فهو في الواقع 4 وحدات بايت على الأكثر) ؛ UTF-16 هو ترميز وحدة أساسية 16 بت ، وهو أيضًا ترميز متغير الطول ، إما بايتان أو 4 بايت ؛ UTF-32 هو طول ثابت ، وتخزين 4 بايت ثابت رقم Unicode.
في الواقع ، لقد كنت دائمًا سوء فهم بعض الشيء حول Unicode من قبل. في انطباعي ، لا يمكن أن يصل رمز Unicode إلا إلى 0xffff ، مما يعني أنه يمكن أن يمثل ما يصل إلى 2^16 حرفًا. بعد قراءة Wikipedia بعناية ، أدركت أن مخطط ترميز UCS-2 المبكر كان بالفعل مثل هذا. استخدم UCS-2 بشكل ثابت بايتان لترميز حرفًا ، بحيث يمكنه فقط تشفير الأحرف داخل نطاق BMP (المستوى الأساسي متعدد اللغات ، أي 0x0000-0xffff ، والذي يحتوي على الشخصيات الأكثر استخدامًا في العالم). من أجل تشفير الأحرف مع Unicode أكبر من 0xFFFF ، قام الأشخاص بتوسيع ترميز UCS-2 وإنشاء ترميز UTF-16 ، وهو طول متغير. في نطاق BMP ، يكون UTF-16 هو نفس UCS-2 ، بينما يستخدم UTF-16 خارج BMP 4 بايت للتخزين.
لتسهيل الوصف أدناه ، اسمحوا لي أن أشرح مفهوم وحدة الكود (CodeUnit). يسمى المكون الأساسي لترميز معين وحدة الكود. على سبيل المثال ، وحدة الكود في UTF-8 هي 1 بايت ، ووحدة التعليمات البرمجية في UTF-16 هي 2 بايت. من الصعب شرحها ، لكن من السهل فهمها.
لكي تكون متوافقة مع لغات مختلفة وأفضل منصة ، يحفظ Javastring رمز Unicode للأحرف. كان يستخدم لاستخدام مخطط ترميز UCS-2 لتخزين Unicode. في وقت لاحق ، وجد أن الشخصيات في نطاق BMP لم تكن كافية ، ولكن لاعتبارات استهلاك الذاكرة وتوافقها ، لم ترتفع إلى UCS-4 (أي UTF-32 ، ترميز 4 بايت ثابت) ، ولكن اعتمد UTF-16 المذكورة أعلاه. يمكن اعتبار نوع char وحدة الكود الخاصة به. هذه الممارسة تسبب بعض المتاعب. إذا كانت جميع الشخصيات ضمن نطاق BMP ، فهذا جيد. إذا كانت هناك أحرف خارج BMP ، فهي لم تعد وحدة رمز تتوافق مع حرف. تقوم طريقة الطول بإرجاع عدد وحدات التعليمات البرمجية ، وليس عدد الأحرف. تقوم طريقة Charat بشكل طبيعي بإرجاع وحدة التعليمات البرمجية بدلاً من الحرف ، والتي تصبح مزعجة عند العبور. على الرغم من توفير بعض طرق التشغيل الجديدة ، إلا أنها لا تزال غير مريحة ولا يمكن الوصول إليها بشكل عشوائي.
بالإضافة إلى ذلك ، وجدت أن Java لا تقوم بمعالجة حرفيات Unicode أكبر من 0xffff عند التجميع ، لذلك إذا لم تتمكن من كتابة حرف غير BMP ، لكنك تعرف رمز Unicode الخاص به ، فيجب عليك استخدام طريقة غبية نسبيًا للسماح بتخزينها في المتخلف يدويًا: قم بحساب UNICAND يدويًا من unicoding (أربعة بايت) للشخصية ، واستخدام أول اثنين من الفرق والمتنافسين على unists في آخر واحد. رمز العينة كما يلي.
public static void main (string [] args) {// string str = "" ؛ // نريد تعيين مثل هذا الحرف ، على افتراض أن طريقة الإدخال الخاصة بي لا يمكن كتابتها // ولكني أعلم أن أحادي unicode هو 0x1d11e // string str = "/u1d11e" ؛ // لن يتم التعرف على هذا // بحيث يمكن حسابه من خلال UTF-16 الترميز D834 DD1VERRING STR = "/UD834/UDD1E" ؛ // ثم كتابة system.out.println (str) ؛ // إخراج بنجاح ""} "}"} "}"} "}"} "}يمكن حفظ المفكرة التي تأتي مع Windows كترميز Unicode ، والذي يشير فعليًا إلى ترميز UTF-16. كما ذكر أعلاه ، فإن ترميزات الأحرف الرئيسية المستخدمة كلها ضمن نطاق BMP ، وضمن نطاق BMP ، فإن قيمة ترميز UTF-16 لكل حرف تساوي قيمة Unicode المقابلة ، والتي ربما هي السبب في أن Microsoft تطلق عليها Unicode. على سبيل المثال ، أدخلت "Good A" حرفين في Notepad ، ثم أنقذته كـ Unicode Big Endian (ذات أولوية عالية) ، وفتحت الملف مع WinHex. المحتوى كما هو موضح في الشكل أدناه. يسمى أول بايتات من الملف بايت بايت (علامة ترتيب البايت) ، (Fe FF) ترتيب إنديان كأولوية عالية ، ثم (59 7D) هو رمز Unicode "الجيد" ، و (00 61) هو "رمز Unicode".
مع رمز Unicode ، لا يمكن حل المشكلة على الفور ، لأنه أولاً وقبل كل شيء ، هناك كمية كبيرة من بيانات الترميز القياسية غير المرتبطة بالوحود في العالم ، ومن المستحيل علينا تجاهلها. ثانياً ، غالبًا ما يشغل ترميز Unicode مساحة أكبر من ترميز ANSI ، لذلك من منظور توفير الموارد ، لا يزال ترميز ANSI ضروريًا. لذلك ، من الضروري إنشاء آلية تحويل بحيث يمكن تحويل ترميز ANSI إلى Unicode للمعالجة الموحدة ، أو يمكن تحويل Unicode إلى ترميز ANSI لتلبية متطلبات النظام الأساسي.
طريقة التحويل سهلة نسبيا. بالنسبة لسلسلة UTF أو ISO-8859-1 ، يمكن تحويل الترميزات المتوافقة مباشرة من خلال قيم الحساب وقيم Unicode (في الواقع ، قد تكون أيضًا البحث عن الجدول). بالنسبة لترميز ANSI المتبقي من النظام ، لا يمكن القيام به إلا من خلال البحث عن الجدول. تستدعي Microsoft CodePage Table CodePage (صفحة الرمز) وتصنيفها وترقيمها عن طريق الترميز. على سبيل المثال ، لدينا CP936 المشتركة هي صفحة رمز GBK ، و CP65001 هي صفحة رمز UTF-8. الشكل التالي هو جدول رسم الخرائط GBK-> Unicode الموجود على موقع Microsoft الرسمي (غير مكتمل بصريًا). وبالمثل ، يجب أن يكون هناك جدول خرائط Unicode-> GBK عكسي.
مع صفحة رمز ، يمكنك بسهولة إجراء تحويلات تشفير مختلفة. على سبيل المثال ، التحويل من GBK إلى UTF-8 ، تحتاج فقط إلى تقسيم البيانات بالأحرف وفقًا لقواعد ترميز GBK ، واستخدام البيانات المشفرة لكل حرف للتحقق من صفحة رمز GBK ، والحصول على قيمة Unicode ، ثم استخدام Unicode للتحقق من صفحة رمز UTF-8 (أو حسابها مباشرة) ، ويمكنك الحصول على UTF -8 Encoding. الشيء نفسه ينطبق على العكس. ملاحظة: UTF-8 هو تطبيق قياسي لـ Unicode. تحتوي صفحة التعليمات البرمجية الخاصة به على جميع قيم Unicode ، لذلك لن يتم تحويل أي ترميز إلى UTF-8 ومن ثم لن يتم تحويله مرة أخرى. في هذه المرحلة ، يمكننا استنتاج أنه لإكمال أعمال تحويل الترميز ، فإن الشيء الأكثر أهمية هو التحويل بنجاح إلى Unicode ، لذا فإن اختيار مجموعة الأحرف (صفحة الرمز) هو المفتاح بشكل صحيح.
بعد فهم طبيعة مشكلة فقدان الترميز ، فهمت فجأة لماذا استخدم إطار JSP ISO-8859-1 لفك تشفير معلمات طلب HTTP ، مما أدى إلى حقيقة أنه كان علينا أن نكتب هذه العبارات عندما حصلنا على معلمات صينية:
Stringparam=newString(s.getBytes("iso-8859-1"),"UTF-8");
نظرًا لأن JSP Framework يتلقى دفق بايت ثنائي مشفر بواسطة المعلمة ، فإنه لا يعرف ما هو ترميزه (أو لا يهتم) ، ولا يعرف صفحة التعليمات البرمجية التي يجب التحقق منها إلى Unicode. ثم اختار حلًا لن يسبب الخسارة أبدًا. يفترض أن هذه هي البيانات المشفرة بواسطة ISO-8859-1 ، ثم يبحث في صفحة رمز ISO-8859-1 للحصول على تسلسل Unicode. نظرًا لأن ISO-8859-1 يتم ترميزها بواسطة Bytes ، وعلى عكس ASCII ، فإنه يشفر كل جزء من المساحة 0 ~ 255 ، بحيث يمكن العثور على أي بايت في صفحة الكود الخاصة به. إذا تم تحويله من Unicode إلى تيار البايت الأصلي ، فلن تكون هناك خسارة. وبهذه الطريقة ، للمبرمجين الأوروبيين والأمريكيين الذين لا يفكرون في لغات أخرى ، يمكنهم فك تشفير السلسلة مباشرة باستخدام إطار JSP. إذا كانوا يريدون أن يكونوا متوافقين مع لغات أخرى ، فإنهم بحاجة فقط إلى العودة إلى دفق البايت الأصلي وفك تشفيره مع صفحة الكود الفعلي.
لقد انتهيت من شرح المفاهيم ذات الصلة من Unicode وترميز الأحرف. بعد ذلك ، سأستخدم أمثلة Java لتجربة ذلك.
ثالثا. مثال تحليل
1. تحويل إلى مُنشئ سلسلة Unicode
تتمثل طريقة بناء السلسلة في تحويل مختلف البيانات المشفرة إلى تسلسل Unicode (مخزّن في ترميز UTF-16). يتم استخدام رمز الاختبار التالي لإظهار تطبيق طريقة بناء Javastring. تشارك الأحرف غير BMP في الأمثلة ، لذلك لا يتم استخدام طرق CodePointat.
اختبار الفئة العامة {public static void main (string [] args) يلقي ioException {// "hello" gbk byte byte [] gbkdata = {(byte) 0xc4 ، (byte) 0xe3 ، (byte) 0xba ، (byte) 0xc3} ؛ // {(byte) 0xa7 ، (byte) 0x41 ، (byte) 0xa6 ، (byte) 0x6e} ؛ // إنشاء سلسلة وفك تشفيرها إلى unicodestring strfromgbk = new string (gbkdata ، "gbk") showunicode (strfromgbk) ؛ showUnicode (strfrombig5) ؛} public static void showunicode (string str) {for (int i = 0 ؛ i <str.length () ؛نتائج التشغيل كما يلي
يمكن العثور على أنه نظرًا لأن رمز Unicode Masters String ، يجب تحويله إلى تشفيرات أخرى!
3. استخدام Unicode كجسر لتحقيق تحويل الترميز المتبادل
مع أساس الجزأين أعلاه ، من السهل جدًا تحقيق الترميز والتحويل المتبادل. تحتاج فقط إلى استخدامها معًا. أولاً ، يقوم Newstring بتحويل البيانات المشفرة الأصلية إلى تسلسل Unicode ، ثم اتصل بـ GetBytes للنقل إلى الترميز المحدد.
على سبيل المثال ، رمز تحويل GBK إلى Big5 بسيط للغاية على النحو التالي
يبرز الفراغ الأساسي العمومي (سلسلة [] args) UnsupportedEncodingException {// لنفترض أن هذه هي البيانات التي تقرأ من الملف في دفق البايت (ترميز GBK) بايت [] gbkdata = {byte) 0xc4 ، (byte) 0xe3 ، (byte) 0xba ، (byte) 0xc3} ؛ سلسلة (GBKDATA ، "GBK") ؛ // تحويل من Unicode إلى Big5 ترميز بايت [] Big5Data = TMP.GetBytes ("Big5") ؛ // العمليات الثانية ...}4. مشكلة فقدان الترميز
كما هو موضح أعلاه ، فإن السبب في أن إطار عمل JSP يستخدم حرف ISO-8859-1 تم تعيينه لفك تشفيره. استخدم أولاً مثالًا لمحاكاة عملية الاستعادة هذه ، كما يلي الرمز
اختبار الفئة العامة {public static void main (string [] args) يلقي UnduportedEncodingException {// JSP Framework يستقبل 6 بايت من البيانات البايت [] data = {(byte) 0xe4 ، (byte) 0xbd ، (byte) 0xa0 ، (byte) 0xe5 ، (byte) 0xa5 ، showbytes (data) ؛ // JSP Framework يفترض أنها ترميز ISO-8859-1 ، تقوم بإنشاء سلسلة كائن سلسلة TMP = سلسلة جديدة (بيانات ، "ISO-8859-1") نتيجة فك تشفير ISO: " + tmp) ؛ // لذا احصل أولاً على بايت 6 بايت من البيانات (ابحث بشكل عكسي في صفحة التعليمات البرمجية الخاصة بـ ISO-8859-1) بايت [] utfdata = tmp.getbytes (iso-8859-1") ؛ صفحة UTF-8 لإعادة بناء نتيجة سلسلة كائن السلسلة = سلسلة جديدة (UTFDATA ، "UTF-8") ؛ // طباعة مرة أخرى ، هذا صحيح! system.out.println ("utf-8 decoding نتيجة:" + result) ؛} showbytes public static showbytes (byte []) {for (byte b: data) system.out.printf (النتيجة الجري على النحو التالي. الإخراج الأول غير صحيح لأن قواعد فك التشفير غير صحيحة. لقد راجعت أيضًا صفحة التعليمات البرمجية بشكل غير صحيح وحصلت على Unicode الخاطئ. ثم وجدت أنه يمكن استعادة البيانات بشكل مثالي من خلال التحقق الخلفي الخاطئ لصفحة رمز ISO-8859-1.
هذه ليست النقطة. إذا كان المفتاح هو استبدال "الصين" بـ "الصين" ، فستكون التجميع ناجحة ، ونتيجة العملية كما هو موضح في الشكل أدناه. بالإضافة إلى ذلك ، يمكن العثور على أنه عندما يكون عدد الأحرف الصينية غريبًا ، يفشل التجميع وعندما يكون الرقم حتى ، يمر. لماذا هذا؟ دعنا نحللها بالتفصيل أدناه.
نظرًا لأن Javastring يستخدم Unicode داخليًا ، فسيقوم برنامج التحويل البرمجي بتقديم رمز الأسلاك الحرفية الخاصة بنا أثناء التجميع والتحويل من الملف المصدر الذي يشفر إلى Unicode (تقول Wikipedia إنه يستخدم ترميزًا مختلفًا قليلاً من UTF-8). عند التجميع ، لم نحدد المعلمة الترميز ، وبالتالي فإن المترجم سوف يفكرك في GBK افتراضيًا. إذا كانت لديك بعض المعرفة بـ UTF-8 و GBK ، فيجب أن تعلم أن الطابع الصيني يحتاج بشكل عام إلى 3 بايت لاستخدام ترميز UTF-8 ، بينما يحتاج GBK فقط إلى 2 بايت. يمكن أن يفسر هذا سبب تأثير تكافؤ رقم الحرف على النتيجة ، لأنه إذا كان هناك حرفين ، فإن ترميز UTF-8 يحتل 6 بايت ، ويمكن فك تشفير GBK إلى 3 أحرف. إذا كانت حرف واحد ، فسيكون هناك بايت لا يمكن الخففة ، وهو المكان الذي علامة الاستفهام في الشكل.
لكي تكون أكثر تحديداً ، فإن ترميز UTF-8 لكلمة "الصين" في الملف المصدر هو E4B8ADE59BBD. المترجم يفوقه في GBK. تبحث أزواج 3 بايت عن CP936 للحصول على 3 قيم Unicode ، والتي هي 6d93e15e6d57 على التوالي ، والتي تتوافق مع الأحرف الثلاثة الغريبة في الرسم البياني النتيجة. كما هو موضح في الشكل أدناه ، بعد التجميع ، يتم تخزين هذه الأحاديات الثلاثة بالفعل في ترميز UTF-8 في ملف .class. عند الجري ، يتم تخزين Unicode في JVM. ومع ذلك ، عندما يكون الإخراج النهائي هو الإخراج ، سيظل ترميزه وتمريره إلى المحطة. الترميز المتفق عليه هذه المرة هو الترميز الذي تم تعيينه بواسطة منطقة النظام ، لذلك إذا تم تغيير إعدادات الترميز الطرفية ، فستظل مشوهة. لا يحدد E15E هنا الأحرف المقابلة في معيار Unicode ، وبالتالي فإن الشاشة ستكون مختلفة تحت خطوط مختلفة على منصات مختلفة.
يمكن تخيل أنه إذا تم تخزين الملف المصدر في ترميز GBK ، ثم يخدع المترجم إلى قوله إنه UTF-8 ، فلا يمكن تجميعه بشكل أساسي وتمريره بغض النظر عن عدد الأحرف الصينية التي يتم إدخالها ، لأن ترميز UTF-8 منتظم للغاية ، ولن يتوافق بايتيها المدمجة بشكل عشوائي مع قواعد الترميز UTF-8.
بطبيعة الحال ، فإن الطريقة الأكثر مباشرة لتمكين المترجم من تحويل الترميز إلى Unicode بشكل صحيح هي إخبار المترجم بأمانة عن ترميز الملف المصدر.
4. ملخص
بعد هذه المجموعة والتجربة ، تعلمت الكثير من المفاهيم المتعلقة بالترميز وأصبحت على دراية بالعملية المحددة لتحويل الترميز. يمكن تعميم هذه الأفكار على لغات البرمجة المختلفة ، ومبادئ التنفيذ متشابهة. لذلك أعتقد أنني لن أكون جاهلًا بهذا النوع من المشكلات في المستقبل.
ما سبق هو كل محتوى هذه المقالة على أمثلة مفهوم الترميز مثل ANSI و Unicode و BMP و UTF ، وما إلى ذلك. آمل أن يكون ذلك مفيدًا للجميع. يمكن للأصدقاء المهتمين الاستمرار في الرجوع إلى الموضوعات الأخرى ذات الصلة على هذا الموقع. إذا كانت هناك أي أوجه قصور ، فيرجى ترك رسالة لإشارةها. شكرا لك يا أصدقائك لدعمكم لهذا الموقع!