PyYAMLで改行のある文字列を出力する

# 概要

- Pythonを使ってYAMLファイルを出力する際に、改行を含む文字列の出力にちょっとしたコツが必要だったので、メモとして残しておきます。(YAMLの知識が足りていなかったという噂もありますが。。)

# PyYAML

- PythonでYAMLを扱うということで、[PyYAML](https://github.com/yaml/pyyaml)を利用します。

# やりたいこと

- このような改行を含む文字列のデータがあった際に

```python
 {
      'aa': 'bbbb\ncccc\ndddd',
      'bb': 'eeee'
 }
```

- こんな感じで改行コードでちゃんと改行してブロックスタイルで出力したい

```yaml
aa: |
  bbbb
  cccc
  dddd
bb: eeee
```

# そのままやってみる

- とりあえず、そのままyaml.dumpしてみます
    - ※実際に必要だったのはファイル出力する処理でしたが、わかりやすくprintする処理にしています

```python
import yaml

def main():

    test_dict = {
        'aa': 'bbbb\ncccc\ndddd',
        'bb': 'eeee'
    }
    print(
        yaml.dump(test_dict,
                  allow_unicode=True,
                  encoding='utf-8',
                  default_flow_style=False).decode()
    )


if __name__ == '__main__':
    main()
```

- なにか違う。。

```yaml
aa: 'bbbb

  cccc

  dddd'
bb: eeee
```

## representerの登録

- 調べてみたところ[このあたり](https://stackoverflow.com/questions/8640959/how-can-i-control-what-scalar-form-pyyaml-uses-for-my-data)に同じような内容があったので、修正して対応してみました。
    - 修正したコードがこちらになります。

```python
import yaml

def represent_str(dumper, instance):
    if "\n" in instance:
        return dumper.represent_scalar('tag:yaml.org,2002:str',
                                       instance,
                                       style='|')
    else:
        return dumper.represent_scalar('tag:yaml.org,2002:str',
                                       instance)

def main():

    test_dict = {
        'aa': 'bbbb\ncccc\ndddd',
        'bb': 'eeee'
    }
    yaml.add_representer(str, represent_str)
    print(
        yaml.dump(test_dict,
                  allow_unicode=True,
                  encoding='utf-8',
                  default_flow_style=False).decode()
    )


if __name__ == '__main__':
    main()
```

- add_representerメソッドで出力する際のスタイルを定義してあげています
    - 文字列に改行コードを含む場合のみ `style='|'` を指定するようにします。
- 実行結果がこちらです

```yaml

aa: |-
  bbbb
  cccc
  dddd
bb: eeee
```
- いい感じになりました！

## うまくいかなケースがある。。

- 上記の実装で出力していたところ、いくつか想定どおりに出力されないケースがありました。
    - 調べていったところ、、 `文字列の改行の前にスペースがある場合` にうまく出力されない事がわかりました。
    - このようなデータの場合

```python
    test_dict = {
        'aa': 'bbbb\ncccc \ndddd',
        'bb': 'eeee'
    }
```
- 改行してくれません。。

```yaml
aa: "bbbb\ncccc \ndddd"
bb: eeee
```

## 調べてみました

- 気になったのでPyYAMLのコードを確認してみました。
- [analyze_scalar](https://github.com/yaml/pyyaml/blob/master/lib3/yaml/emitter.py#L626) メソッド内でデータのタイプを判定しているのですが、 `スペースの次に改行コードがある場合` 、space_breakという変数がTrueになります。
    - すると[このあたり](https://github.com/yaml/pyyaml/blob/master/lib3/yaml/emitter.py#L761)のフラグがすべてFalseにセットされます。

```python
        if space_break or special_characters:
            allow_flow_plain = allow_block_plain =  \
            allow_single_quoted = allow_block = False
``` 

- 実際にyamlを出力する際にはこちらの[choose_scalar_style](https://github.com/yaml/pyyaml/blob/master/lib3/yaml/emitter.py#L494)メソッド内でスタイルを判定して出力するようになっています。
- 本来は、先程実装したとおり、 `add_representer` メソッドで指定したスタイルで出力されるはずなのですが、先程のフラグのうち、 `allow_block`がFalseになっていると指定したスタイル(ここでは`|`)で出力されないようになっています。

```python
        if self.event.style and self.event.style in '|>':
            if (not self.flow_level and not self.simple_key_context
                    and self.analysis.allow_block):
                return self.event.style
```

- 先程のフラグをセットしているanalyze_scalarのところのコメントにはこのような記載があります。
    - スペースに続く改行がある場合にはブロックスタイルでの出力がされないということのようです。

```
Spaces followed by breaks, as well as special character
 are only allowed for double quoted scalars.
```

- さらにanalyze_scalarを見てみると同様に`allow_block`がFalseセットされている[箇所](https://github.com/yaml/pyyaml/blob/master/lib3/yaml/emitter.py#L751)があります。
    - `文字列の末尾にスペースがある場合` にallow_blockがFalseセットされるようです。
    - コメントにはこのような記載があり、同様にプロックスタイルでの出力がされないということのようです。
        - ` We do not permit trailing spaces for block scalars.`

- まとめると、文字列の中に、 `スペースに続く改行がある場合` 、 `文字列の末尾にスペースがある場合` には改行コードで文字列が改行されたブロックスタイルでの出力がされないようです。

## 対応

- ということで、最終的にこちらのような実装で対応をしました。
    - 単純にrepresentメソッド内で先程の該当する文字列を置換しています。

```python
import yaml
import re


def represent_str(dumper, instance):
    if "\n" in instance:
        instance = re.sub(' +\n| +$', '\n', instance)
        return dumper.represent_scalar('tag:yaml.org,2002:str',
                                       instance,
                                       style='|')
    else:
        return dumper.represent_scalar('tag:yaml.org,2002:str',
                                       instance)


def main():

    test_dict = {
        'aa': 'bbbb\ncccc \ndddd',
        'bb': 'eeee'
    }
    yaml.add_representer(str, represent_str)
    print(
        yaml.dump(test_dict,
                  allow_unicode=True,
                  encoding='utf-8',
                  default_flow_style=False).decode()
    )


if __name__ == '__main__':
    main()
```

- スペースのあとに改行があるケースでも想定どおりの出力となりました。

```yaml
aa: |-
  bbbb
  cccc
  dddd
bb: eeee
```
## YAMLは奥が深い。。

- きちんと調べていませんが、YAMLの仕様にのっとった処理になっているということのようです。
    - 普段あまり気にしていなかったのですが、YAMLの仕様は奥が深いようです。。

## 参考サイト

- https://yaml-multiline.info/
- https://yaml.org/spec/1.2/spec.html
- https://qiita.com/jerrywdlee/items/d5d31c10617ec7342d56