👨‍💻

文字コードについて Part1 ~文字コードの歴史とUnicode編~

2024/05/17に公開

はじめに

はじめまして、株式会社バニッシュ・スタンダード(以下、VS)でエンジニアをやっているkzzzmです。
最近エンジニアとして入社させていただき、最年少エンジニアです!
そんな私がVSに入社して感じたことは、、、

  • 人とプロダクトが最高に面白い!!
  • 会社の掲げるPURPOSEやVALUEを全員が理解して、それを日々体現している!!

です。

まだまだキャリアの浅い私ですが、なかなかこんな会社無いんだろうなーと日々感じています。
入社して間もない私をすぐに仲間として迎え入れてくれたVSに対して私自身日々成長して恩返しをしていく所存です。

では本題に移ります。
まず今回文字コードについて記事を書こうと思った経緯からお話しします。
ある時、先輩エンジニアの方々と食事をしながら話をしている中で、文字コードの話題になりました。
その時私自身、文字コードについて表面的にしか理解出来ておらず、話にあまり参加出来なかったことが悔しかったです。
なので文字コードについてしっかりと勉強したいと思い、今回の題材としました。
1つの記事では文字コードについてまとめきれないと思ったので、シリーズ化していきたいと思います。
その第一弾です!

文字コードとは

文字コードは、文字をデジタルデータとして表現するためのシステムです。
これにより、コンピュータが文字情報を理解し、処理するための基礎が作られます。
コンピュータは基本的に0と1のみを理解するため、これらの数字を人間が使う文字へと変換するルールが必要です。
このルールの集まりを文字コードと呼びます。

文字コードには多くの種類がありますが、その設計は主に以下の目的を満たすようになっています:

  • 標準化
    世界中のシステム間で文字データを共有するためには、共通の理解が必要です。
    文字コードの標準化により、異なるシステムやプラットフォーム間でもテキストデータが正確に交換されることが可能になります。

  • 拡張性
    新しい文字や記号が定期的に追加されるため、文字コードはこれらの拡張を容易に受け入れることができる必要があります。例えば、絵文字のような新しい種類の文字が導入される場合、これをサポートするために文字コードは適応する必要があります。

  • 効率
    文字コードは、データストレージと処理効率を最適化するように設計されています。
    例えば、より一般的に使用される文字には短いコードを割り当てることで、データの保存空間を節約し、処理速度を向上させます。

文字コードの実用例

文字コードの実用例として、インターネット上でのテキスト送信があります。
Webページは世界中どこからでもアクセス可能で、多種多様な言語のテキストを含むことができます。
これを実現するために、HTMLやその他のWeb技術は特定の文字コード(主にUTF-8)を使用してテキストをエンコードします。
この共通のエンコーディングにより、異なる国や言語のユーザーが同じページを正確に閲覧できるようになります。

また、ソフトウェア開発においても、ソースコード内での文字エンコーディングの選択が重要です。開発者は、ソフトウェアが国際的な市場で使用される場合、多言語対応を可能にするためにUnicodeなどの広範な文字セットをサポートする文字コードを選択する必要があります。

文字コードの歴史的背景

初期の文字コード:テレタイプとASCII

  • テレタイプ
    19世紀末に登場したテレタイプは、遠隔地にメッセージを送信するための機械です。これにより、初めて人々は遠隔地へリアルタイムで文字情報を送ることが可能となりました。初期のテレタイプシステムであるBaudotコードは、5ビットのエンコーディングを使用しており、一連のキャラクターを電信線を通じて送信できました。

  • ASCII
    1960年代になると、アメリカ国立標準技術研究所(NIST)がASCII(アメリカ標準情報交換コード)を開発しました。ASCIIは128の文字セットを使用し、7ビットのエンコーディングを採用しています。これにより、英数字や基本的な記号を表すことができ、初めてデジタルコンピュータで広く利用されるようになりました。

ASCIIの限界は、英語の文字と基本的な記号しか表現できないことでした。
これに対応するために、さまざまな拡張ASCIIが登場しました。

拡張ASCII

元々の7ビットのASCII文字セットを拡張して8ビットを使用する一連の文字コードです。
これにより、128(2^7)の文字から256(2^8)の文字に表現可能な文字数が拡大されました。

拡張ASCIIが多くの地域の言語ニーズに対応しようとしたものの、これらは各エンコーディングが異なる文字セットを持つため、国際的なコンテキストでは利用が限られます。
また、異なる拡張ASCII間での互換性がないため、グローバルな使用には不適合です。
これらの問題を解決するために、Unicodeが開発されました

Unicode

Unicodeが登場した1990年代初頭は、コンピュータ技術とグローバル化の進展が相まって、国際的な文字の取り扱いに新たな解決策が必要とされていました。それまでの文字エンコーディング方法では、各国の言語ごとに異なるシステムが存在し、それらの間での互換性が極めて限られていました。この問題を解決するため、Unicodeコンソーシアムは世界中のあらゆる文字を単一の文字セットに統一する野心的なプロジェクトを開始しました。

Unicodeの主な目的は、世界中のすべての文字システムをサポートし、ソフトウェア開発者が多言語対応のアプリケーションをより簡単に作成できるようにすることでした。この統一されたエンコーディングシステムにより、異なる言語間でのデータ交換が容易になり、グローバルなコミュニケーションが大幅に促進されることとなります。

Unicodeの導入は、デジタル通信の普及とともに急速に広がりました。これにより、世界のどこからでも、どのような言語のテキストも正確に送受信することが可能になり、インターネットの普及に不可欠な役割を果たしました。今日では、UnicodeはWebや多くのプログラミング言語の標準として広く受け入れられており、世界的なデジタルコミュニケーションの基盤を形成しています。

Unicodeエンコーディング形式

UTF-8

UTF-8はUnicodeのために特に設計された可変長の文字エンコーディングです。
UTF-8の主な特徴と利点は以下の通りです:

  • 可変長エンコーディング
    1バイトから4バイトまでの長さでUnicode文字をエンコードします。ASCII文字は1バイトでエンコードされ、より多くのビットが必要な文字(例えば多くの国際文字)は2バイト、3バイト、または4バイトを使用します。
  • 後方互換性
    UTF-8はASCIIと完全に互換性があります。つまり、従来のASCIIテキストファイルは、変更なしにUTF-8としても正しく解釈されます。
  • 省メモリ性と効率
    ラテン文字を主に使用するテキストでは、メモリの節約になります。また、ウェブで最も一般的に使用されるエンコーディングであり、HTMLや多くのプログラミング言語のデフォルトです。

UTF-16

UTF-16は、Unicodeの別の可変長エンコーディング形式で、以下の特徴があります:

  • 2バイトまたは4バイトの長さ
    基本多言語面(BMP)の文字は2バイトで表現され、補助的な文字(サロゲートペアを必要とする文字)は4バイトで表現されます。
  • 広範囲の言語のサポート
    アジア言語など、多くの文字を頻繁に使用する言語では、UTF-16が効率的な選択肢です。

UTF-32

UTF-32はUnicodeのすべての文字を一貫して4バイトで表現する固定長エンコーディングです。UTF-32の主な特徴は以下の通りです:

  • 固定長エンコーディングすべてのUnicode文字が同じサイズ(4バイト)でエンコードされるため、文字のインデックス付けやランダムアクセスが容易です。
  • メモリ消費が大きい
    UTF-32は他のエンコーディング方式に比べてデータサイズが大きくなるため、メモリの使用が多くなります。
  • 限定的な使用
    そのメモリ効率の悪さから、一部の特定のアプリケーションを除き、一般的な使用には推奨されません。

現代における文字コード

現在、Unicodeとそのエンコーディング形式(特にUTF-8)は、ほとんどのソフトウェアとインターネットプロトコルで標準として採用されています。これにより、多言語コンテンツの作成、配布、表示がはるかに容易になり、グローバルなコミュニケーションの障壁が大幅に低減しました。

このように文字コードは、単純な数字のセットから全世界の複雑な言語のニーズに対応するために進化してきました。その歴史は、技術の進化だけでなく、言語と文化の多様性をデジタル空間でどのように取り扱うかという問題にも直面しています。

Goによる文字エンコーディングとデコーディングの実装例

下記実装はGo言語を使用して文字列を異なるエンコーディング形式に変換し、再び元の文字列に戻す実装例です。具体的には、UTF-16とUTF-32という二つの異なるエンコーディング形式を扱います。

main.go
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"unicode/utf16"
)

func main() {
	// 元の文字列の配列はUTF-8
	texts := []string{"おはよう", "😃", "おはよう😃"}

	// UTF-16のエンコーディングとデコーディング
	fmt.Println("UTF-16 Encoded:")
	encodedUtf16, decodedText16 := encodeAndDecode(texts, 16)
	for i, encoded := range encodedUtf16 {
		fmt.Printf("%s: % x\n", texts[i], encoded)
	}
	fmt.Println("Decoded Texts:", decodedText16)

	fmt.Println("----------------------------------------")

	// UTF-32のエンコーディングとデコーディング
	fmt.Println("UTF-32 Encoded:")
	encodedUtf32, decodedText32 := encodeAndDecode(texts, 32)
	for i, encoded := range encodedUtf32 {
		fmt.Printf("%s: % x\n", texts[i], encoded)
	}
	fmt.Println("Decoded Texts:", decodedText32)
}

func encodeAndDecode(texts []string, bitSize int) ([][]byte, []string) {
	var encodedBytes [][]byte
	var decodedTexts []string

	for _, text := range texts {
		runes := []rune(text)
		var buffer *bytes.Buffer
		var encodedBytesPerText []byte
		var decodedRunes []rune

		switch bitSize {
		case 16:
			utf16CodePoints := utf16.Encode(runes)
			buffer = new(bytes.Buffer)
			for _, codePoint := range utf16CodePoints {
				binary.Write(buffer, binary.LittleEndian, codePoint)
			}
			encodedBytesPerText = buffer.Bytes()

			utf16CodePointsDecoded := make([]uint16, len(utf16CodePoints))
			binary.Read(bytes.NewReader(encodedBytesPerText), binary.LittleEndian, &utf16CodePointsDecoded)
			decodedRunes = utf16.Decode(utf16CodePointsDecoded)

		case 32:
			utf32CodePoints := make([]uint32, len(runes))
			for i, r := range runes {
				utf32CodePoints[i] = uint32(r)
			}
			buffer = new(bytes.Buffer)
			for _, codePoint := range utf32CodePoints {
				binary.Write(buffer, binary.LittleEndian, codePoint)
			}
			encodedBytesPerText = buffer.Bytes()

			utf32CodePointsDecoded := make([]uint32, len(utf32CodePoints))
			binary.Read(bytes.NewReader(encodedBytesPerText), binary.LittleEndian, &utf32CodePointsDecoded)
			for _, codePoint := range utf32CodePointsDecoded {
				decodedRunes = append(decodedRunes, rune(codePoint))
			}
		}

		encodedBytes = append(encodedBytes, encodedBytesPerText)
		decodedTexts = append(decodedTexts, string(decodedRunes))
	}

	return encodedBytes, decodedTexts
}

実行結果

UTF-16 Encoded:
おはよう: 4a 30 6f 30 88 30 46 30
😃: 3d d8 03 de
おはよう😃: 4a 30 6f 30 88 30 46 30 3d d8 03 de
Decoded Texts: [おはよう 😃 おはよう😃]
----------------------------------------
UTF-32 Encoded:
おはよう: 4a 30 00 00 6f 30 00 00 88 30 00 00 46 30 00 00
😃: 03 f6 01 00
おはよう😃: 4a 30 00 00 6f 30 00 00 88 30 00 00 46 30 00 00 03 f6 01 00
Decoded Texts: [おはよう 😃 おはよう😃]

最後に

今回は主に文字コードの導入部分や歴史について記述しました。
学習をしていくうちに知らなかったことがどんどん見つかって楽しかったです。
次回の記事では、文字エンコーディングの実用例や異なるエンコーディング間の変換方法、文字化けなどについて書いていこうと思います。

改めて文字コードについて学習するきっかけをくれた先輩エンジニアの方々に感謝します。

VSにご興味を持ってくださった方のご連絡を心よりお待ちしております。
https://v-standard.notion.site/a652acc985f34244bf3e7f1142a54799

参考文献

https://direct.gihyo.jp/view/item/000000001619

Discussion