ストリームから文字列を読み出す


概要
----

テキストファイルやネットワークなどストリームから文字列を取得する場合、読み出しのループのなかで文字列を加工したいことがあります。バッファはバイト単位なので、バッファの冒頭部および末尾の部分が完全な文字ではないことに対応することが課題です。

不正なバイト列を含む UTF-8 の文字列の数え方
-------------------------------------

C 言語ですが、こちらの[記事](http://qiita.com/masakielastic/items/432b774569b44f959b1c)をご参照ください。後続バイトの範囲を 0x80 から 0xBF としてチェックする方法は不完全なので、練習用途以外では避けるべきです ([参考記事](http://qiita.com/masakielastic/items/abf8e54344034f9b089c))。

文字列からストリームをつくる
-----------------------

ストリームを練習する場合、テキストファイルよりもメモリを使うほうが手軽です。`fopen` の引数に `php://memory` を指定します。

```php
// https://evertpot.com/222/
$str = 'あいうえおかきくけこ';

$stream = fopen('php://memory','r+');
fwrite($stream, $str);
rewind($stream);

echo stream_get_contents($stream);
```

SplTempFileObject を使う
------------------------

練習の段階ではテキストファイルを用意しなくてすむように SplFileObject の代わりに SplTempFileObject を使うことができます。

```php
$temp = new SplTempFileObject();
$temp->fwrite("あいうえお\n");
$temp->fwrite("かきくけこ\n");
$temp->rewind();

foreach ($temp as $line) {
    echo $line;
}
```

mb_substr を使う
---------------

不正なバイト列を考慮する `mb_substr` を使います。最初から最後の1つ前の文字列、および一番最後の文字をそれぞれ取り出します。一番最後の文字は文字を構成するバイト列の一部であることがありますので、次にバッファを読み込んだときに、バッファの先頭に連結します。

```php
function string_stream_gen($str, $size)
{
    $handle = fopen('php://memory','r+');
    fwrite($handle, $str);
    rewind($handle);
    $last = '';

    while (!feof($handle)) {
        $read = fread($handle, $size);
        $str = mb_substr($last.$read, 0, -1);
        $last = mb_substr($read, -1);

        if ('' === $str) {
            continue;
        }

        for ($i = 0, $len = mb_strlen($str); $i < $len; ++$i) {
            yield mb_substr($str, $i, 1);
        }
    }

    fclose($handle);
}

$gen = string_stream_gen('あいうえおかきくけこさしすせそ', 7);

foreach ($gen as $char) {
    echo $char, PHP_EOL;
}
```

IteratorAggregate を実装する
---------------------------

Generator と IteratorAggregate を組み合わせてみましょう。ストリームではなく通常の文字列から Iterator を実装する方法については[こちら](http://qiita.com/masakielastic/items/7b91dc89afcfdde7ada4)の記事をご参照ください。

```php
class StringStreamIterator implements IteratorAggregate
{
    private $value;
    private $size;

    public function __construct(string $value, int $size)
    {
        $this->value = $value;
        $this->size = $size;
    }

    public function getIterator()
    {
        $handle = fopen('php://memory','r+');
        fwrite($handle, $this->value);
        rewind($handle);

        $last = '';

        while (!feof($handle)) {
              $read = fread($handle, $this->size);
              $str = mb_substr($last.$read, 0, -1);
              $last = mb_substr($read, -1);

              if ('' === $str) {
                  continue;
              }

              for ($i = 0, $len = mb_strlen($str); $i < $len; ++$i) {
                  yield mb_substr($str, $i, 1);
              }
        }

        fclose($handle);
    }
}

$it = new StringStreamIterator('あいうえおかきくけこさしすせそ', 7);

foreach ($it as $char) {
    echo $char, PHP_EOL;
}
```

mb_strcut を使う
----------------

バイト単位で文字列を切り捨てする `mb_strcut` を使うことができますが、`mb_substr` よりも行数が多くなります。

```php
$buff = "あいうえお";
$buff_size = 7;

$str = mb_strcut($buff, 0, $buff_size);
$str_size = strlen($str);
$last = substr($buff, $str_size, $buff_size - $str_size);

var_dump(
    "あい" === $str,
    "\xe3" === $last,
    "う" === "\xe3\x81\x86"
);
```

grapheme_extract は不正なバイト列に対応していない
--------------------------------------------

`grapheme_extract` はバイト単位で文字列を取り出すことができますが、不正なバイト列が含まれると `false` を返すので、今回の用途にはあいません。

```php
var_dump(
    "あい" === grapheme_extract("あいうえお", 7, GRAPHEME_EXTR_MAXBYTES),
    false === grapheme_extract("あ\x80いうえお", 7, GRAPHEME_EXTR_MAXBYTES)
);
```