住基ネット統一文字からUCSへの変換表を作成した話

はじめに

マイナンバーカードに内蔵されているICチップには、氏名や住所などの個人情報が格納されています。これらの文字情報は住民基本台帳ネットワークシステムを元に構成されていますが、格納形式として使用されているのは Unicode ではなく、JIS X 0208(第一・第二水準)およびJIS X 0212(補助漢字)に基づいた文字コード体系となっています。1

住民基本台帳のデータには、異体字・外字・俗字など、JIS規格に含まれない文字が数多く含まれており、これらは住基ネット統一文字(J+コード)として別途定義されています。住基文字は全国の自治体で統一的に運用されており、文字種としては約2万字に及びます。しかし、ICチップに格納される際には、コンピュータで確実に表示・処理できるように、約1万3千字(JIS X 0208 / 0212 のみ)に制限されています。

この制限により、ICチップに格納できない住基文字が登場する場合には、JIS内に含まれる似た字形の「代替文字」が選定されて登録されます。たとえば、住民基本台帳上で「髙」が使われていても、ICチップ上では「高」で代用されるといったケースが該当します。

本来であればこの仕組みにより、ICチップ上の文字はすべてJIS X 0208 / JIS X 0212の範囲に収まるはずです。ところが実際にマイナンバーカードから読み出した文字列の中には漢字のはずなのにJIS X 0208 / JIS X 0212の範囲外の文字(ハングルなど)が含まれているケースがありました。調査したところ、住基ネット統一文字上では正しく漢字として定義されているものの、JIS X 0208 / JIS X 0212の範囲外のためにUTF-8で解釈するとハングルになることがあることが分かりました。

住基ネット統一文字とUCSのギャップ

このように、ICチップ上の文字がJIS X 0208 / JIS X 0212の範囲外として登録された結果、住基ネット統一文字(J+コード)をそのままUCS(Unicode)のコードポイント(U+コード)に読み替えてしまうと、意図しない文字に化けてしまうケースが発生します。

例えば、本来は氏名や住所に使われる漢字であるにもかかわらず、読み替えた後の値がUCSのハングル(全く別の文字領域)を指してしまう、といった現象が実際に発生します。

住基コードは当初、UCSの拡張として設計されましたが、その後のUCSの規格変更に追随できなかったため、現在ではUCSベースのシステムでそのまま扱うことができない符号体系となっています。

実際、住基文字全体21,170字のうち、UCSと一致するのは15,379字にとどまり、残りの5,791字はUCSとは一致しません2。この中には追加漢字や俗字などが含まれており、例えば住基コード J+B2FD は本来「八の松」という漢字を意味しますが、単純に U+B2FD と解釈するとUCSではハングル「닽」となってしまいます。

変換表を作成する理由

マイナンバーカードのICチップから取得した文字情報をそのままシステムに取り込むと、上記のようなズレによって以下のような問題が生じます。

  • 氏名や住所に実在しないハングルや記号が紛れ込む
  • 同一人物や同一住所であっても、文字コードの違いにより正しく突合できない
  • ユーザーに表示される情報が誤ってしまい、正しい情報が確認できない

これを防ぐためには、「住基コード→正しいUCSコードポイント」への対応表を事前に定義しておき、読み取ったデータを正しい文字に変換できるようにする必要があります。

変換表をどう作るか

変換表を作成するにあたり活用したのは、文字情報技術促進協議会が公開しているMJ文字情報一覧表です。この一覧表には、住基ネット統一文字コード(J+コード)と対応する UCS(U+コード)が整理されており、基本的にはここから住基文字がどのUCSに対応するのかを取得できます。3

ただし、MJ文字情報一覧表は住基ネット統一文字(21,170字)のうち主に漢字19,563字を対象としており、未収録の1,607字前後については対応関係が公開されていません。

今回の変換表は MJ文字情報一覧表に掲載されている範囲のみを対象としました。なお、これら未収録文字の多くは「非漢字」「互換非漢字」「変体仮名」に属しており、実運用ではほとんど登場しないため、影響は限定的です。

変換表生成の実装

ここまでの方針をもとに、実際に変換表を生成するためのGoコードを実装しました。

MJ文字情報一覧表(csv化したもの)を入力とし、「J+コードをそのままU+に直読みするとハングルや記号になるケースだけを補正」する変換表を出力します。

以下がその実装です。

package main

import (
    "encoding/csv"
    "fmt"
    "os"
    "strconv"
    "strings"
)

// UCSコードをUTF-8文字に変換する関数
func ucsToUTF8(ucs string) string {
    if ucs == "" || !strings.HasPrefix(ucs, "U+") {
        return ""
    }

    code, err := strconv.ParseInt(ucs[2:], 16, 32)
    if err != nil {
        return ""
    }

    return string(rune(code))
}

// 住基ネット統一文字コードとUCSの変換表を作成する関数
func createCharacterConversionTable() error {
    // https://moji.or.jp/mojikiban/mjlist/ からダウンロードしたExcelファイルをcsvに変換したもの
    // MJ文字情報一覧表 Ver.006.02
    file, err := os.Open("mji.00602.csv")
    if err != nil {
        return fmt.Errorf("mji.00602.csvを開けませんでした: %w", err)
    }
    defer file.Close()

    reader := csv.NewReader(file)
    records, err := reader.ReadAll()
    if err != nil {
        return fmt.Errorf("CSVの読み込みに失敗しました: %w", err)
    }

    outFile, err := os.Create("output/juki_to_ucs_conversion_table.csv")
    if err != nil {
        return fmt.Errorf("出力ファイルの作成に失敗しました: %w", err)
    }
    defer outFile.Close()

    writer := csv.NewWriter(outFile)
    defer writer.Flush()

    if err := writer.Write([]string{"住基ネット統一文字", "UCS"}); err != nil {
        return fmt.Errorf("ヘッダーの書き込みに失敗しました: %w", err)
    }

    // 変換対象文字の個数のカウント
    conversionCount := 0

    // 住基ネット統一文字コードの重複チェック用マップ
    jukiCodeMap := make(map[string]string)

    for i, record := range records {
        if i == 0 { // ヘッダーをスキップ
            continue
        }

        correspondingUCS := record[3] // 対応するUCS列
        jukiCode := record[8]         // 住基ネット統一文字コード列

        // 住基ネットコードがJ+で始まるかチェック
        // 住基ネットコードが空白の場合はスキップされる
        if !strings.HasPrefix(jukiCode, "J+") {
            continue
        }

        correspondingUcsUTF8 := ucsToUTF8(correspondingUCS)
        jukiUTF8 := ucsToUTF8("U+" + jukiCode[2:])

        runes := []rune(jukiUTF8)
        if len(runes) != 1 {
            return fmt.Errorf("住基ネット統一文字コードが1文字ではありません: %s (文字数: %d)", jukiUTF8, len(runes))
        }

        ucsUTF8 := correspondingUcsUTF8
        if jukiUTF8 != ucsUTF8 {
            if existingUCS, exists := jukiCodeMap[jukiUTF8]; exists {
                return fmt.Errorf("住基ネット統一文字コードの重複があります: %s (対応するUCS: %s と %s)",
                    jukiUTF8, existingUCS, ucsUTF8)
            }
            jukiCodeMap[jukiUTF8] = ucsUTF8

            if err := writer.Write([]string{jukiUTF8, ucsUTF8}); err != nil {
                return fmt.Errorf("データの書き込みに失敗しました: %w", err)
            }
            conversionCount++
        }
    }

    fmt.Printf("変換表に%d個の文字マッピングを追加しました\n", conversionCount)
    return nil
}

func main() {
    fmt.Println("変換表の作成を開始します...")

    if err := createCharacterConversionTable(); err != nil {
        fmt.Printf("変換表の作成に失敗しました: %v\n", err)
        return
    }

    fmt.Println("変換表の作成が完了しました")
    fmt.Println("結果は output/juki_to_ucs_conversion_table.csv に保存されました")
}

実行すると、次のようなログが出力されます。

変換表の作成を開始します...
変換表に5563個の文字マッピングを追加しました
変換表の作成が完了しました
結果は output/juki_to_ucs_conversion_table.csv に保存されました

出力結果のサンプル

実際に生成されたoutput/juki_to_ucs_conversion_table.csv は5,563件のマッピングを含んでいます。以下はその一部です。

住基ネット統一文字,UCS
괝,々
삳,㐪
묍,㐮
...
낯,㡌
낼,㡢
넊,㢴
...

※ハングルや記号に化けているように見える左側の文字(住基ネット統一文字)が、右側の正しいUCSコードポイントに変換されているのが分かります。

検証

変換表 juki_to_ucs_conversion_table.csv の信頼性を確かめるため、以下の確認を行いました。

  1. 住基ネット統一文字列(左側)にJIS X 0208 / JIS X 0212の文字が含まれていないこと
    • JIS X 0208 / JIS X 0212の範囲内文字はUCSにも同じ字形が定義されているため変換する必要はありません。
    • CyberLibrarian 公開のJIS X 0208 コード表およびJIS X 0212 コード表をhtmlファイルとして保存し、Goプログラムで文字を抽出してcsv化しました。
    • 得られた文字数はJIS X 0208が6,879字、JIS X 0212が6067字で、公開仕様と一致しています。
    • これらを突合した結果、出力された変換表の住基列には JIS の文字は一切含まれていませんでした。
  2. 住基ネット統一文字列(左側)の重複がないこと
    • 同じ住基コードが複数行に現れることはなく、1つの住基文字に対して1つの変換先が割り当てられていることを確認しました。
    • これにより、変換が適切に定義されている(曖昧なマッピングが存在しない)ことを保証しています。

JIS文字一覧のcsv化に用いたプログラム

検証に用いたJIS文字のcsv化は、以下のGoプログラムで行いました。

package main

import (
    "encoding/csv"
    "fmt"
    "os"
    "strings"

    "golang.org/x/net/html"
)

func main() {
    // https://www.asahi-net.or.jp/~ax2s-kmtn/ref/jisx0208.html の HTML を保存
    // https://www.asahi-net.or.jp/~ax2s-kmtn/ref/jisx0212/index.html の HTML を保存
    input0208File := "JIS X 0208コード表 - CyberLibrarian.html"
    input0212File := "JIS X 0212コード表(全コード) - CyberLibrarian.html"
    output0208File := "output/jisx0208_chars.csv"
    output0212File := "output/jisx0212_chars.csv"

    file0208, err := os.Open(input0208File)
    if err != nil {
        fmt.Println("ファイルを開けません:", err)
        return
    }
    defer file0208.Close()

    file0212, err := os.Open(input0212File)
    if err != nil {
        fmt.Println("ファイルを開けません:", err)
        return
    }
    defer file0212.Close()

    doc0208, err := html.Parse(file0208)
    if err != nil {
        fmt.Println("HTML の解析に失敗しました:", err)
        return
    }

    doc0212, err := html.Parse(file0212)
    if err != nil {
        fmt.Println("HTML の解析に失敗しました:", err)
        return
    }

    chars0208 := extractChars(doc0208)
    chars0212 := extractChars(doc0212)

    err = writeCSV(output0208File, chars0208)
    if err != nil {
        fmt.Println("CSV の書き出しに失敗しました:", err)
        return
    }

    err = writeCSV(output0212File, chars0212)
    if err != nil {
        fmt.Println("CSV の書き出しに失敗しました:", err)
        return
    }

    fmt.Println("JIS X 0208 の文字を", output0208File, "に保存しました!")
    fmt.Println("JIS X 0212 の文字を", output0212File, "に保存しました!")
}

func extractChars(n *html.Node) []string {
    var chars []string
    if n.Type == html.ElementNode && (n.Data == "td" || n.Data == "th") {
        if len(n.Attr) > 0 {
            for _, attr := range n.Attr {
                if attr.Key == "class" && strings.Contains(attr.Val, "cha") {
                    if n.FirstChild != nil {
                        chars = append(chars, n.FirstChild.Data)
                    }
                }
            }
        }
    }
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        chars = append(chars, extractChars(c)...)
    }
    return chars
}

func writeCSV(filename string, data []string) error {
    file, err := os.Create(filename)
    if err != nil {
        return fmt.Errorf("ファイルの作成に失敗しました: %w", err)
    }
    defer file.Close()

    writer := csv.NewWriter(file)
    defer writer.Flush()

    for _, char := range data {
        if char != "" {
            if err := writer.Write([]string{char}); err != nil {
                return fmt.Errorf("データの書き込みに失敗しました: %w", err)
            }
        }
    }
    return nil
}

まとめ

本記事では、住基ネット統一文字(J+コード)とUCS(U+コード)の間で発生するズレを補正するための変換表を作成しました。

  • 実際のマイナンバーカードから読み出した文字列には、一部の漢字がJIS X 0208 / JIS X 0212の範囲外のまま登録され、UTF-8で解釈するとハングルに化けるケースが存在した。
  • この問題を防ぐには、MJ文字情報一覧表を参照して正しい対応関係を定義することが必要。
  • Goで変換表を生成し、5,563件の補正マッピングを得た。
  • 検証の結果、変換表にはJIS文字が含まれず、また住基コード側に重複がないことを確認できた。

これにより、マイナンバーカードのICチップから読み取った住基文字を正しいUCSに変換し、氏名や住所の文字化けや突合不一致といった問題を防ぐことができます。


  1. https://www.jpki.go.jp/faq/word.html 「代替文字」で記載があります。
  2. https://www.jstage.jst.go.jp/article/johokanri/55/11/55_826/_html/-char/ja
  3. 「MJ文字情報一覧表」は、独立行政法人 情報処理推進機構(IPA)が作成した成果物を、文字情報技術促進協議会が CC BY-SA 2.1 JP ライセンスのもとで公開しているものです。