เมื่อผมต้องคุยกับต่างดาว

เมื่อผมต้องคุยกับต่างดาว
01/04/22   |   6.6k

Photo by Pixabay: https://www.pexels.com/photo/alphabet-board-game-bundle-close-up-278888/

เรื่องนี้เริ่มขึ้นตอนเย็นวันหนึ่ง มีน้องโทรมาปรึกษาเรื่องการทดสอบระบบที่กำลังจะต้องหนีจาก Internet Explorer ที่กำลังจะหมดอายุกลางปีนี้ด้วยคำสั่งของ Microsoft หลังจากคุยเรื่องแรกจบ น้องแจ้งว่ามีอีกเรื่อง นั่นคือมีข้อมูลบางส่วนที่เกิดจากการแก้ไขระบบ ทำให้ระบบเข้ารหัสข้อมูลผิดอยู่จำนวนหนึ่ง ตัวอย่างหน้าตาของข้อมูลที่ประหลาดจะเป็นลักษณะนี้

"นาย..."

จากตอนแรกที่เดลี่เมื่อเช้าก็ได้ยินเกี่ยวกับเรื่องนี้แต่คิดว่าเป็นการเข้ารหัสเพื่อความปลอดภัย จึงคิดว่าน่าจะต้องแก้ด้วยการใช้ Key หรือ Library ที่ถูกต้อง แต่พอมาเห็นข้อมูลนี้จึงเดาว่า อ้อ... มันน่าจะเกี่ยวกับ Character Set ว่าแล้วก็ขอเล่าประวัติศาสตร์ของ Character Set กันสักหน่อย

ถ้าใครอยากข้ามส่วนนี้ก็จิ้มไป Section รองสุดท้ายก่อน References ได้เลยครับ

ASCII

ในยุคแรกเริ่มของคอมพิวเตอร์ ( ที่ไม่ได้ถึงกับเริ่มมากอะนะ เอาที่พอจะทราบมาบ้าง ) การเก็บข้อมูลแทนตัวอักษรเราใช้ 7-bit นั่นคือจะแทนตัวอักษรได้ 128 ตัว และส่วนใหญ่ในนั้นก็คือภาษาอังกฤษแบบสหรัฐอเมริกานั่นเอง ( ก็มันเกิดแถว ๆ นั้น ก็ต้องเป็นแบบนั้นแหละ ) เป็นที่มาของชื่อ ASCII ( American Standard Code for Information Interchange ) โดยรหัสตั้งแต่ 0x00 - 0x1F และ 0x7F เป็น Control Characters ( ที่เรารู้จักกันอย่างแพร่หลายก็น่าจะเป็น 0x0A - Line Feed, 0x0D - Carriage Return, 0x08 - Backspace ) ส่วนช่วง 0x20 - 0x7E จะเป็น Printable Characters ( Space, A-Z, a-z และ เครื่องหมายต่าง ๆ )

ASCII variations

เมื่อระบบคอมพิวเตอร์แพร่หลายมากขึ้น ความต้องการในการใช้ตัวอักษรท้องถิ่นอื่น ๆ ที่ไม่ใช่สหรัฐอเมริกาก็มีมากขึ้น ทำให้เกิดมาตรฐานเพิ่มเติม นั่นคือไหน ๆ ใน 1 Byte มันมี 8-bit นะ ( เมื่อคอมพิวเตอร์รองรับ 8-bit เป็นมาตรฐาน ประมาณช่วงทศวรรษ 1970 ) แต่ ASCII ไม่ได้ใช้ จึงมีการนำ bit สุดท้ายมารีดไถใช้ โดยนำตัวอักษรอีก 128 ตัวที่เหลือมาใช้งาน ตัวอย่างหนึ่งของตัวอักษรที่เพิ่มขึ้นมาก็คือพวกตัวอักษรที่มีเครื่องหมายประกอบบนล่าง เข้าใจว่าเรียกว่า accent เช่น à, é, ü, ... เป็นต้น

นอกเหนือจากตัวอักษรที่เป็น accent ยิ่งระบบคอมพิวเตอร์ไปยังประเทศได้มากขึ้น ความต้องการใช้ตัวอักษรของตัวเองก็มากขึ้นนั่นทำให้แต่ละประเทศนำ ASCII ( ที่เพิ่ม bit ที่  8 แล้ว ) มาแทนที่ด้วยตัวอักษรของตัวเอง และทำให้เกิด Character Set มากมายขึ้นบนโลก ตัวอย่างหนึ่งที่ประเทศไทยเคยใช้อยู่พักใหญ่ ๆ ก็คือ Windows-874 และ TIS-620

เช่น 0xE0 = สระ เอ, 0xB8 = ธ. ธง

Reference: https://th.wikipedia.org/wiki/TIS-620

Multi-byte Characters

แต่อย่างไรก็ตามการใช้ 1 byte ( 8-bit ) แทนตัวอักษร 1 ตัว ก็ไม่เพียงพอ เพราะในบางภาษาตัวอักษรเยอะกว่านั้น ยกตัวอย่างเช่นภาษาจีน... นับกันไม่หวาดไม่ไหวเลยทีเดียว จึงเกิดการใช้ข้อมูลเกินกว่า 1 byte แทนตัวอักษร 1 ตัวขึ้นมา เริ่มกันที่ Unicode คือการใช้รหัส 2 byte หรือมากกว่าในการแทนตัวอักษรทั้งโลก โดยทุกภาษาจะใช้รหัสมาตรฐานเดียวกัน หนึ่งใน Character Set ที่ใช้ Unicode ก็คือ UCS-2 ก็คือการใช้ข้อมูล 2 byte แทน 1 ตัวอักษร

เช่น U+00EO = à, U+00B8 = ¸

Reference: https://en.wikipedia.org/wiki/List_of_Unicode_characters#Latin-1_Supplement

เริ่มเห็นเค้าลางอะไรบางอย่างในโจทย์แล้วใช่มั้ยครับ...

UTF-8

ขอกล่าวถึง UTF-8 ที่เราใช้กันอย่างแพร่หลายในปัจจุบัน UTF-8 คือการแทนที่แต่ละตัวอักษรใน Unicode ด้วยจำนวน Byte ที่ไม่เท่ากัน เช่นรหัสตัวอักษรใน ASCII ดั้งเดิม ที่ไม่เกิน 7 bit เราจะใช้ 1 byte ส่วนนอกเหนือจากนั้นจะมีวิธีการเข้ารหัสของแต่ละช่วงของรหัส Unicode สิ่งที่ทำให้ UTF-8 น่าใช้ สำหรับผมแล้วก็คือมันประหยัดเนื้อที่มากกว่าการใช้ UCS-2 ตรง ๆ ครับ ในกรณีที่ผมตั้งสมมติฐานว่าข้อมูลส่วนใหญ่จะเป็นภาษาอังกฤษในรหัส ASCII ตั้งต้น ถ้าเราใช้ UCS-2 ในการเก็บ จะต้องใช้ 2 byte ต่อ 1 ตัวอักษร แต่ถ้าใช้ UTF-8 เราจะใช้แค่ 1 byte โดยที่ยังสามารถเก็บข้อมูลตัวอักษรภาษาอื่น ๆ ที่อยู่ใน Unicode ได้ใน String เดียวกันอีกด้วย ตัวอย่างการเทียบรหัส UCS-2 กับ UTF-8 ของภาษาไทย

ก. ไก่ = UCS-2 ( U+0E01 ) = UTF-8 ( 0xE0B881 )

ข. ไข่ = UCS-2 ( U+0E02 ) = UTF-8 ( 0xE0B882 )

สระ โอ = UCS-2 ( U+0E42 ) = UTF-8 ( 0xE0B982 )

มาเริ่มอ่านภาษาต่างดาวกันดีกว่า

โจทย์มีอยู่ว่า ข้อความหน้าตาแบบนี้ à¸™à¸²à¸¢

ซึ่งผมค่อนข้างคุ้นกับลักษณะการจัดกลุ่มได้ที่ละ 3 ตัวอักษร โดยตัวแรกกับตัวที่สองมักจะเป็นตัวเดียวกัน หรือตัวที่สองเปลี่ยนไปแค่ไม่กี่แบบ นัั่นคือ รหัสที่อยู่เบื้องหลังนี่น่าจะเป็น UTF-8 ภาษาไทยแหละ ลองมาดูกัน

กลุ่มแรก à¸™

กลุ่มที่สอง à¸²

กลุ่มที่สาม à¸¢

ผมเริ่มที่ ASCII ก่อน ลองดูว่า เจ้า à มีรหัส ASCII ที่เท่าไหร่ ปรากฎว่าเป็น 0x85 ใน Extended ASCII ซึ่งดูไม่เข้าเค้ากับภาษาไทยแบบ UTF-8

จึงลองเปลี่ยนไปดูตารางของ Unicode ( UCS-2 ) แทน ก็พบว่า ใช่เลย เจ้า  à มีรหัส UCS-2 ว่า U+00E0

และ ลูกน้ำหน้าตาประหลาด ( ¸ ) นั้น มีรหัส UCS-2 ว่า U+00B8

ดังนั้นถ้าเราเอาค่า UCS-2 กลุ่มละ 3 byte มาเรียงกันเป็น UTF-8 ก็น่าจะทำให้ได้ข้อมูลที่ถูกต้องนั่นเอง

ผมจึงเขียนโปรแกรมทดสอบสมมติฐานดังนี้

    
    echo "Source: $source\n";
    echo "iconv-bin2hex: ", $middle = bin2hex(iconv("utf8", "ucs-2", $source)), "\n";
    echo "middle: ";
    $packed = '';
    for($i = 0; $i < strlen($middle); $i += 4) {
        $char = substr($middle, $i, 2);
        echo $char, ' ';
        $packed .= pack('H*', $char);
    }
    echo "\n";

    echo "packed (bin2hex): ", bin2hex($packed), "\n";
    echo "packed (raw): ", $packed, "\n";

เนื่องจาก $source ที่มาจากฐานข้อมูลถูก Encode ไว้ด้วย UTF-8 ผมจึงต้องแปลงจาก UTF-8 ให้เป็น UCS-2 ก่อน (แล้วใช้ bin2hex เพื่อให้อ่านออกง่าย ๆ ) ซึ่งจะได้หน้าตาดังนี้

Source: นาย
iconv-bin2hex: e000b8009900e000b800b200e000b800a200

หลังจากนั้นก็ทำต่อโดยเอา byte ลำดับเลขคี่ 1, 3, 5, ... มาเรียงต่อกันเพื่อเป็น UTF-8 string ปลายทางนั่นคือจะได้

packed (bin2hex): e0b899e0b8b2e0b8a2
packed (raw): นาย

เป็นอันจบสำหรับการถอดรหัสต่างดาวในครั้งนี้

 

References

tags : programming



ติดตามข่าวสารและเรื่องราวดีๆ ทาง Email