LoginSignup
20
14

More than 5 years have passed since last update.

utf8 のファイルの読み込み

Posted at

あくまで、お勉強的なメモですし。迷った軌跡をそのまま書いてあるので、ちょっと冗長です。

きっかけ

UTF-8 のファイルを読み込んで wchar_t で文字列を得たい!っていう、すっげー単純な要求だったのですが。

std::wifstream stream;
stream.imbue(std::locale(std::locale::empty(), new std::codecvt_utf8<wchar_t>()));

stream.open(L"utf8test.dat");   //  25MBytes

decltype(stream)::char_type buf[1024 * 64];
while (stream.read(buf, _countof(buf)).gcount() > 0) {
}

このコードを実行したらローカルHDD上のファイルを読み終わるまでに三分もかかってしまって使い物にならなかったのです。(VS2015 c++ デバッグ実行)

追っていくと、codecvt_utf8 が 1 バイトごとに操作しているところにぶつかって、あーこれはおそいわー となる何かだったのです。

どげんかせんといかん。

そもそも普通に、コード変換なんかせずに読み込んだら普通にいつもの速度出るんですよ。

std::ifstream stream;   //  wifstream → ifstream
//stream.imbue(std::locale(std::locale::empty(), new std::codecvt_utf8<wchar_t>()));

stream.open("utf8test.dat");    //  25MBytes

decltype(stream)::char_type buf[1024 * 64];
while (stream.read(buf, _countof(buf)).gcount() > 0) {
}

これだと 200㍉秒。まぁ当たり前。
というわけで普通に Windows の MultiByteToWideChar を使ったコードにしましょうねって言う。

utf8 のファイルの読み込み

いつもだったら、普通にファイルを読み込みんで、バッファを MultiByteToWideChar して~で済むんですが。

//読み込みサイズ
std::streamsize read = stream.read(buf, _countof(buf)).gcount();

int size = MultiByteToWideChar(CP_UTF8, 0, buf, read, nullptr, 0);
std::wstring dest(size + 1);
MultiByteToWideChar(CP_UTF8, 0, buf, read, &dest[0], size + 1);

こんな感じ。まぁ定石。(FileMap 使えという意見もあろうけど、今回は使わない。)
文字コードが UTF-8 なので、1 文字に対するバイト数が 1-6 バイトとまちまちなので、今回は使えない。ぬぅ残念。

MultiByteToWideCharの第二引数を 0 にして呼び出すと、一見普通に変換してくれるんだけど、最後の文字を見ると文字化けしていることがある。
この場合、MultiByteToWideCharの第二引数を MB_ERR_INVALID_CHARS にしてやると、MultiByteToWideChar は失敗して GetLastError が 1113 を返してくる。

最後の文字が途中で途切れちゃってるのは普通に想像つくと思うんですが。

じゃ、最後の文字どれよって話なんですが、ここで詰まったのです。
MultiByteToWideChar には、これを知る方法が無い。変換できた最後の文字の次の文字へのポインタとか取れたらよかったんですがそんな引数無いんですよね。

一文字削って、WideCharToMultiByte してバイト数をカウントとかしたくないし。
かといって、1 バイトずつ減らしていって 変換して、成功した最初のところ!とかやりたくないし。

ここで、Wikipedia の UTF-8 をみたら、UTF-8 は先頭バイトと後続バイトが簡単に見分けがつくんですね。
そして、先頭バイトを見ればその文字のバイト長まで簡単にわかる。良かった良かった。

というわけで、変換処理はこんなかんじに。

std::wstring read(std::istream *pStream) {
    char buf[64 << 10];
    const int bufExtra = 8;
    const int bufPrimal = _countof(buf) - bufExtra;

    auto read = pStream->read(buf, bufPrimal).gcount();
    if (read == 0) {
        return std::string();
    }
    assert(read < 1 + bufPrimal);

    //次の文字へのポインタ
    char *plast;
    if (read < bufPrimal) {
        //  ファイル終端部分
        plast = buf + read;
    }
    else {
        //  先頭バイトを探す。
        char * plead = buf + (bufPrimal - 1);

        while ((*plead & 0xC0) == 0x80 && buf + bufPrimal - 6 < plead) {
            plead -= 1;
        }

        //  文字のバイト数
        int bytes =
            ((*plead & 0xFE) == 0xFC) ? 6 :
            ((*plead & 0xFC) == 0xF8) ? 5 :
            ((*plead & 0xF8) == 0xF0) ? 4 :
            ((*plead & 0xF0) == 0xE0) ? 3 :
            ((*plead & 0xE0) == 0xC0) ? 2 :
            ((*plead & 0x80) == 0x00) ? 1 :
            0;

        //  次の文字へのポインタ取得
        plast = plead + bytes;
        assert(plast < &buf[_countof(buf)]);

        if (buf + bufPrimal < plast) {
            //文字がバッファから、はみ出てる。
            //  plast まで 読み足す (bufExtra領域)
            read = pStream->read(buf + bufPrimal, plast - (buf + bufPrimal)).gcount();
            if (read < plast - (buf + bufPrimal)) {
                //  途切れた
                throw;
            }
        }
        else if (buf + bufPrimal == plast) {
            //ぴったり収まってる。
            //  何もしない
        }
        else {
            //  はみ出もしていなければ、ぴったり収まってるわけでもない。
            //  なんかおかしい。
            throw;
        }
    }


    //変換バイト数
    int sizeConvert = plast - buf;

    //変換.
    int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, buf, sizeConvert, nullptr, 0);
    std::wstring dest(size + 1);
    MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, buf, sizeConvert, &dest[0], size + 1);

    return dest;
}

int main() {
    std::ifstream stream;
    stream.open(L"utf8test.dat");   //  25MBytes

    while (read(&stream).length()) {
    }
}

std::wstreambuf 化

standard じゃん?かっこいいじゃん?(馬鹿)

struct utf8_streambuf : std::wstreambuf {
    using _Traits = traits_type;

    std::istream *pStream;
    std::wstring dest;

    utf8_streambuf(std::istream *pStream) : pStream(pStream) {
        dest = read(pStream);
        setg(&dest[0], &dest[0], &dest[dest.length()]);
    }

    // get a character from stream, but don't point past it
    virtual int_type underflow() {
        if (gptr() != 0 && gptr() < egptr()) {
            return _Traits::to_int_type(*gptr());   // return buffered
        }

        dest = read(pStream);
        setg(&dest[0], &dest[0], &dest[dest.length()]);

        return _Traits::to_int_type(*dest.begin());
    }

    // put a character back to stream
    virtual int_type pbackfail(int_type c = _Traits::eof()) {
        if (c != _Traits::eof()) {
            if (gptr() != 0 && eback() == gptr()) {
                dest.insert(dest.begin(), c);
                setg(&dest[0], &dest[0], &dest[dest.length()]);
            }
            else if (gptr() != 0 && eback() != gptr() && _Traits::eq(c, gptr()[-1]) == false) {
                *_Gndec() = c;
            }
        }
        return c;
    }
};


std::ifstream u8stream(L"utf8test.dat");    //25mb
utf8_streambuf u8buf(&u8stream);

std::wistream stream(&u8buf);

decltype(stream)::char_type buf[1024 * 64];
while (stream.read(buf, _countof(buf)).gcount() > 0) {
}

20
14
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
14