業務備忘録

備忘録です

バイナリの制御文字と'BOM'でハマった

2023-10-22 11:28:13 | 日記

最近部屋の片づけをしており、大量の本やCDを整理しています。
折角だから捨てる前に全部CDを取り込んでおこう…ということでアルバムを一つ一つasunderから取り込んでいるのですが、面倒な課題にぶつかってしまいました。

1. 曲・アルバム・作曲者の表記揺れが多すぎる問題

昔聞いていた曲には19世紀の古典的な曲が多く、再演・再奏された録音を収録している場合、アルバムが違うだけで「同じ曲なのにタイトルが違う」ということが頻発します。

J.Strauss Ⅱ Wo die Zitronen blühen

→ヨハンシュトラウス2世 シトロンの花咲くところ

→ヨハン・シュトラウスⅡ世 レモンの花咲くところ

→ヨハン・シュトラウス2世 シトロンの花咲く国

曲名が違うだけならそこまで神経質にならずともよいですが、作曲者名に揺れがあると「その作曲者の曲だけ聞きたい」という場合不便を極めます。

ということで、表記揺れを一括で直せるように楽曲ファイルのメタデータを編集できるようにしたいと思います。類似のMP3タグ編集ソフトで同等のことはできるのかもしれませんが、調べていません。どうせ勉強だし。

2. バイナリを読む

とりあえず、AACとかその他の音楽ファイルの形式は考えず、MP3のタグ(ID3V2)を編集することだけ考えます。
タグを編集する場合、バイナリ(0と1で構成された、テキスト以外のデータ)の読み書きを直接行うことになります。

ID3V2タグにはヘッダーも存在しますが、ここでは最初にタグの本体の中身を見ることにします。
バイナリエディタの左側のパネルの各セルには、1バイトの情報が表示されており、右側のパネルにはバイトに対応する文字がエンコードされて表示されています。

言わでものことかもしれませんが、1バイトはここでは16進表記されているので、'54'なら16 * 5 * 4で10進数の84となります。16進数をリテラル表記する際の'0x'という接頭辞をつけて表記するなら、0x54となります。
さて、上掲の画像の中で、エンコードされた文字列を見ていると、「TSSE」や「TRCK」、「TPE1」という大文字の文字列が目につきます。
この文字列からID3V2タグの「フレーム」と呼ばれる、楽曲のメタデータのより具体的な情報を格納する部分が開始します。「TSSE」などは各フレームの名称で、「TSSE」はエンコード設定に関する情報、「TRCK」はトラック番号、「TPE1」は主なるアーティストを指します。

今回は、「TRCK」について見ていきたいと思います。上掲の画像のうち、赤枠で囲った部分がフレーム名の「TRCK」を表す部分。
水色の枠で囲った部分が、フレームの本体サイズを表す部分で、今回は「00 00 00 05」なので、5バイトであることがわかります。本来はSyncsafe Integerという表記法に対応したサイズの計算が必要ですが、今回は割愛。
緑色の枠で囲った部分が、フラグを表す部分ですが、通常は使用されません。
そして、最後に、オレンジの枠で囲った部分が、フレームの本体で、ここにトラック番号が格納されています。サイズは先ほどフレームの本体サイズを表記していた箇所に従って5バイトとなります。

さて、フレームのサイズに従ってトラック番号を表す箇所を見てみると、「01 FF FE 31 00」と記されています。
31 = 0x31はUnicode(文字コードの標準規格)では'1'を指します。バイナリエディタで見ていた曲はアルバムの1曲目なので、正しそうですね。
ただ、0x31の前には、0x01,0xFF,0xFEというバイト列が並んでいます。0x01や0xFFや0xFEには対応する文字が無いようですが…。

3. 0x01,0xFFFEとは

Unicodeエンコーディング(UTF-16またはUTF-8)が使用されているデータ・ファイルには、ファイルの最初の数バイトにバイト順序マーク(BOM)が含まれている場合があります。キャラクタ・セットUTF-16が使用されているデータ・ファイルでは、ファイルの最初の2バイトの値{0xFE,0xFF}は、ファイルがビッグ・エンディアンのデータを含んでいることを示すBOMです。{0xFF,0xFE}という値は、ファイルにリトル・エンディアンのデータが含まれていることを示すBOMです。
https://docs.oracle.com/cd/E57425_01/121/SUTIL/GUID-CFEED713-D459-42F4-A777-7AAA654451AC.html

0xFF,0xFEが使用されている場合、続くデータはリトルエンディアン(=多バイトをメモリに格納する際の方式1つ。リトルエンディアンの場合下位バイトを先に格納する)ことを明示しています。このような記号をBOM(Byte Order Mark)と呼びます。
(ただし、UTF-8のBOMは0xEFBBBFで、これはUTF-8で書き込まれていることを明示するために使われるのであって、今見ているファイルがUTF-8で書き込まれているはずなのに0xFFEが書き込まれているのはなぜなのかはよくわかりません。)

せんずるに、0XFFFEは文字コードとしては定義されない値であり、今回はUTF-8のファイルとしてバイナリデータを扱うので、楽曲に関するメタデータとして考慮する必要はないということです。
ただし、読み込んだバイト列をエンコードする際に弾きだす必要はありますね。

0x01については、unicodeの制御文字のようで、意味としては'START OF HEADING'=ヘッダ開始を表す符号にすぎないようで、これも文字列として扱わないように弾く必要あり。そのほかunicodeには0x000~0x001Fまでの制御文字があるようです。

 


【Java】XMLからオブジェクトを生成する

2023-10-01 22:05:19 | 日記

書くことが思いつかず前の記事から2週間以上空いてしまいました。
前々回まで、StreamAPIの練習として適当に作成していた図書館の貸し出しをモデルにしたプロジェクトを書いていましたが、今回はStreamAPIとは別方向の改修を行いたいと思います。

1.前回までのコード

public class BookList {
        Map<String, Book> bookList = new HashMap<>();

    public void setBookList() {
        bookList.put("こころ", new Book("こころ", "夏目漱石", 200));
        bookList.put("斜陽",new Book("斜陽", "太宰治", 180));
        bookList.put("想像の共同体",new Book("想像の共同体", "アンダーソン", 350));
        bookList.put("近代文学論争",new Book("近代文学論争", "臼井吉見", 300));
        bookList.put("近代天皇像の形成",new Book("近代天皇像の形成", "安丸良夫", 400));
        bookList.put("天皇の肖像",new Book("天皇の肖像", "多木浩二", 190));
        bookList.put("ドストエフスキーの詩学",new Book("ドストエフスキーの詩学", "バフチン", 500));
        bookList.put("『キング』の時代",new Book("『キングの時代』", "佐藤卓己", 530));
       
    }

面倒なのでDBは使わない方針でしたが、そのため、書籍一覧を保持するBookListクラスのオブジェクトは以上のように初期化されていました。

上記のコードでbookListが保持しているBookクラスのオブジェクトには、貸出履歴や貸出回数を管理するメンバ変数が存在しますが、当然のことながら、JVM(Java Virtual Machine)が終了した時点でそれらの変数は解放されてしまいます。
そこで今回は、DBを使わずにXMLファイルを読み込むことでBookListの初期化を行いたいと思います。
XMLファイルを読み込む練習がしたいだけなので、DB使えよ…みたいな指摘はなしで。

2.XMLファイルとは

.xmlファイルとは、ファイル名の拡張子(末尾部分)が「.xml」のファイルで、XML(Extensible Markup Language)で記述されたテキストファイルのこと。ソフトウェアの設定ファイルなどによく見られる。

IT用語辞典 e-Words(https://e-words.jp/w/.xml%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.html)


xmlファイルでは、自分で任意のタグを用意することでデータを定義することができます。
例えば今回改修範囲であるBookクラス(書籍名・著者・ページ数などを保持するクラス)をXMLで表現すると以下のようになります。

<books>
    <book id="0001">
        <title>こころ</title>
        <author>夏目漱石</author>
        <year>1914</year>
        <lentCount>0</lentCount>
        <lastLent></lastLent>
        <pages>250</pages>
    </book>
    <book id="0002">
        <title></title>
        <author>夏目漱石</author>
        <year>1910</year>
        <lentCount>0</lentCount>
        <lastLent></lastLent>
        <pages>270</pages>
    </book>

3.JAXBの利用

さて、XML形式で外出しした書籍一覧情報ですが、これを実際のコード内で活用するには、

  1. XML形式の構文解析
  2. 毎にBookクラスのオブジェクトに変換

という手順を踏む必要があります。
こういった場合に使用できるAPIに、JAXB(Java Architecture XML Binding)があります。

JAXB(Java Architecture XML Binding)は、[…]XMLファイルを読み込むというよりも、「XMLファイルとJavaのオブジェクトを結び付ける」動作をします。JAXBを利用することで、XMLとJavaオブジェクトを相互変換できます。

Java本格入門p.262

JAXBはかつてはJavaの標準ライブラリ(Eclipseのパッケージエクスプローラ内のJRE システムライブラリーに格納されているパッケージ)であったが、現在は削除されているため、自力で導入する必要があります。導入については割愛。

【BookList.java】

   Books bookList = null;
 
   private Books makeBookList() {
       
        try(InputStream is = Files.newInputStream(Paths.get("./src/main/resources/xml/Book.xml"))){
            bookList = JAXB.unmarshal(is, Books.class);
           
        }catch(IOException e) {
            System.err.println(e);
        }
        return bookList;
    }

JAXBクラスのunmarshalメソッド(XMLからJavaオブジェクトへの変換を行うメソッド)を使用しています。引数にInputStreamを渡していますが、InputStreamの代わりにReaderクラスオブジェクトが指定されているオーバーロードもあるので、BufferedReaderでも大丈夫です。
第二引数にはBooksクラスのクラスリテラル(そのクラスを表すClassクラスのオブジェクト)が指定されます。
では、肝心のBooksクラスはどうなっているかというと、

【Books.java】

@XmlRootElement(name = "books")

public class Books {
    private List<Book> bookList;
   
    @XmlElement(name = "book")
    public List<Book> getBookList(){
        return bookList;
    }
   
    public void setBookList(List<Book> bookList) {
        this.bookList = bookList;
    }
   
}

@XmlRootElementは、オブジェクトをXML要素として表現するための注釈で、XMLのルートタグ(最上位の要素)と、ツリー構造で表現されるJavaのオブジェクトの、最上位のクラスを結び付けます。ここでは、Bookオブジェクトを格納するArrayListではなく、クラスに注釈を付与している点に注意しておきます。
@XmlElementも同様に、XML要素とオブジェクトを結び付ける注釈です。
このように、注釈型を用いるだけで、XML要素がJavaオブジェクトにマッピング(関連づけ・割り当て)されます。

4.実際に動かす

先掲のBookList.java内のbookListをwatch式で見てみます。
個人的には、@XmlRootElement注釈はBooksクラスに付与したのに、Booksクラスのメンバ変数であるbookListにバインドされているのが親切なような不気味なような…。

ともかく、XMLの各要素がBook型のリストに格納され、各BookオブジェクトはXMLタグで囲われた値を保持していることがわかります。ただし、

    <book id="0001">
        <title>こころ</title>
        <author>夏目漱石</author>
        <year>1914</year>
        <lentCount>0</lentCount>
        <lastLent></lastLent>
        <pages>250</pages>
    </book>

<book>タグの属性であるidは、Bookクラスのメンバ変数であるidにはバインドされていません。
タグ内の属性をバインドするには、@XmlAttribute注釈型を用います。

    @XmlAttribute(name = "id")
    public int getId() {
        return id;
    }

getterに@XmlAttributeを付与していることに注意します。メンバ変数に直接付与してもバインドされません。注意しましょう。

きちんとidもバインドされています。もっとも、XMLで"0001"だったidはint型の1として扱われ、0埋めではなくなっていますが…。