2015年3月4日水曜日

テキストファイル読み込みのライブラリ紹介

先日、「ファイル閲覧機能のライブラリ化(本編1)」で、読者の方から質問がありました。投稿には含まれてない、ファイルを読む処理に関してです。plistファイルにはテキスト形式でないものも含まれていますが、どのように除外しているかという質問内容でした。

その場での回答ですが、テキスト形式でないplistファイルは除外しておらず、その場合だけテキスト形式に変換してから、呼び出し側に返しているという内容を書きました。また、具体的な処理は書ききれないので、新たな投稿として近日中に書きますと約束しました。というわけで、この投稿にて紹介します。

 

ファイル閲覧機能から呼ばれているのは、Documentsフォルダ内のテキストファイルを読み込む汎用的な処理で、ファイル関係の独自ライブラリに含まれています。いろいろな拡張子のファイルを読み込み、ファイル内容をStringとして返します。ファイルの保存場所は決まっていますから、呼び出し側が渡すのは、ファイル名だけです。

ファイル名を受け取った読み込み処理では、Documentsフォルダのパスを得てから、ファイル名を加えたパスを生成します。そのパスを使って、テキストファイルとして読み込み、String変数に入れるのがメインの処理です。ファイルの読み込みには、NSStringクラスのcontentsOfFileを使っています。

具体的な処理内容は、次のようなSwiftコードになります。

// 指定されたテキストファイルを読み込む(Documentsディレクトリ内から)
func readFileDocsF(rFName:String) -> String? {
    // パスの生成
    let iPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
    let iPathFile: String = iPath.stringByAppendingPathComponent(rFName)
    // 一部の拡張子を特別に処理
    let iSuffix: String = (rFName as NSString).pathExtension
    if (iSuffix == "plist") {
        let iStr: String? = readPlistDocF(iPathFile)
        return iStr
    }
    // 残りの拡張子は普通に読み込む
    let iData = NSString(contentsOfFile:iPathFile, encoding:NSUTF8StringEncoding, error:nil) as String?
    return iData
}

見て分かるように、一部の拡張子(ここではplistのみ)だけ、別な処理を呼ぶ形になっています。ここに別な拡張子を加えることで、別な拡張子でも特別な処理に迂回することが可能です。

 

続いて、上記のソースコードから呼ばれる、拡張子がplistの場合の読み込み処理です。

plistの拡張子を持つファイルには、3つの形式があるようです。テキスト形式のXMLファイル、バイナリ形式のファイル、NeXT時代のファイルの3つです。このうち、NeXT時代のファイルは今は使われていないので、前の2つの形式だけに対応しました。

具体的な処理内容は、次のSwiftコードになります。

// plistファイルを読んでテキスト化する関数
private func readPlistDocF(rPathFile:String) -> String? {
    // バイナリーとして読み込み、最初の6文字を取り出す
    let iNSData: NSData? = NSData(contentsOfFile:rPathFile, options:.DataReadingMappedIfSafe, error:nil)
    if (iNSData == nil) { println("ERROR:PlistRead:NSData=nil"); return "" }
    let iCnt: Int = 6
    var aryByte: [UInt8] = [UInt8](count:iCnt, repeatedValue:0x0)
    iNSData!.getBytes(&aryByte, length:aryByte.count)
    var iStr: String? = NSString(bytes:aryByte, length:aryByte.count, encoding: NSUTF8StringEncoding) as String?
    // 取り出した6文字を判定して、その結果に適した方法で、ファイル全体をテキスト形式に変換する
    if (iStr == "<?xml ") {
        let iData = NSString(contentsOfFile:rPathFile, encoding:NSUTF8StringEncoding, error:nil) as String?
        return iData
    }
    if (iStr == "bplist") {
        let STR_ADD_HEADER: String = "■ 以下(2行目以降)は、ファイル内容を変換(description)した結果 ■\n"
        if let iNSDic: NSDictionary = NSDictionary(contentsOfFile:rPathFile) {
            return STR_ADD_HEADER + iNSDic.description
        }
        if let iNSArray: NSArray = NSArray(contentsOfFile:rPathFile) {
            return STR_ADD_HEADER + iNSArray.description
        }
        // 上記以外は表示不可に
        return "(plist:ファイルの内容は、表示できません)"
    }
    // どちらでもないときはエラーとして扱う
    println("ERROR:PlistRead:NSData=???")
    return ""
}

同じplist拡張子なのにファイル形式が複数あるので、まずはバイナリ形式として読み込みます。NSDataの形で読み込むと、バイナリ形式で読み込めます。読み込みが成功したら、先頭の6バイトをUTF8コードとして取り出します。

その6バイトの内容で、ファイル形式を判定しています。テキスト形式のXMLなら「<?xml 」に、バイナリ形式なら「bplist」になっているはずです。両方のファイル形式のフォーマットをネットで調べて確認しました。2つの形式のどちらでもない場合は、エラーメッセージを出し、空の文字列を返しています。テキスト形式のXMLと判定できたら、そのままの文字列を返します。

もう片方のバイナリ形式には、注意が必要です。iOSで扱うクラスのインスタンスを、シリアライズして保存されている場合がほとんどで、そのクラスも複数あります。それをどうやってテキスト形式に変換するのか、少し悩みました。

結果として、次のような形の処理にしました。NSDictionaryクラスのインスタンスとして、試しに読み込んでみます。もし該当するインスタンスであれば正常に読み込まれ、そうでなければnilとなります。読み込みが成功したときだけ、descriptionメソッドでテキスト形式に変換して返します。成功しなかった場合は、別なクラスで同様の処理を繰り返します。今のところ、NSDictionaryとNSArrayだけしか用意していません。

どのクラスのplistファイルなのか、もっとスマートな判定方法があれば良いのですが、いろいろ検索しても見付けられませんでした。

 

テキスト形式に変換して返す場合には、作り方というか、表示方法に注意が必要です。変換されたテキストを見る人に、「変換されていること」と「どのような変換をしたのか」を知らせる必要があります。何も知らせないで表示すると、表示されたままのテキスト形式で保存されたファイルだと、勘違いする可能性があるからです。

今回紹介した処理では、最初の1行にメッセージを表示する形にしました。プログラマーでない人にも、プログラマーにも理解できる形を意識したメッセージです。プログラマーでない人なら、よく分からないけど、何か変換した結果が2行目以降に表示されていると伝わります。さらにSwiftプログラマーなら、descriptionメソッドで変換したと分かるでしょう。Swiftを知らないプログラマーや、descriptionを知らないSwiftプログラマーでも、descriptionで検索して調べられるでしょう。

 

紹介した読み込み処理では、特定の拡張子(plist)で特別な読み方をしているものの、それ以外の拡張子をチェックしていません。そのまま読み込んだ場合、読み込むファイルがテキスト形式でないと、異常終了などが発生します。

拡張子を検査しない理由は、どんな拡張子でも読めるようにしておきたいからです。どの拡張子を読み込むべきか判断するのは、この読み込み処理を利用する側であるという考え方だからです。それは同時に、読み込めない拡張子は呼ばないとともに、逆に読み込めるなら未知の拡張子で使っても構わないという考え方でもあります。

今回の読み込み処理を改良するとしたら、plistのように特別な拡張子を選んで、テキスト形式に変換するための加工処理を加えることだけでしょう。加工する処理が複数になった場合は、バイナリ形式で読み込む処理を共通化して、利用しやすくする形になると思います。

 

ここで紹介した例では、テキスト形式以外のファイルも、テキスト形式に変換して表示するという発想が入っています。ライブラリを作る際には、このように利用範囲が広がる形にしておくと、いろいろと使えて便利です。複数のアプリで共通に使えるライブラリだからこそ、工夫したときの効果が大きいですから、自由な発想でライブラリの機能向上を目指したいものです。

 

(使用開発ツール:Xcode 6.1.1, SDK iOS 8.1)

2 件のコメント:

  1. 丁寧に有り難うございました。
    前に質問したkatohです。
    ソースまで見せてもらえて大変参考になりました。
    テキストじゃないのまで表示するって、目から鱗です。
    早速使わせていただきます。

    返信削除
    返信
    1. katohさん、お役に立てて幸いです。
      機能追加に配慮して作ってありますから好きなだけアレンジしてください。

      削除