概要
- Pythonを使ってYAMLファイルを出力する際に、改行を含む文字列の出力にちょっとしたコツが必要だったので、メモとして残しておきます。(YAMLの知識が足りていなかったという噂もありますが。。)
PyYAML
- PythonでYAMLを扱うということで、PyYAMLを利用します。
やりたいこと
- このような改行を含む文字列のデータがあった際に
{
'aa': 'bbbb\ncccc\ndddd',
'bb': 'eeee'
}
- こんな感じで改行コードでちゃんと改行してブロックスタイルで出力したい
aa: |
bbbb
cccc
dddd
bb: eeee
そのままやってみる
- とりあえず、そのままyaml.dumpしてみます
- ※実際に必要だったのはファイル出力する処理でしたが、わかりやすくprintする処理にしています
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()
- なにか違う。。
aa: 'bbbb
cccc
dddd'
bb: eeee
representerの登録
- 調べてみたところこのあたりに同じような内容があったので、修正して対応してみました。
- 修正したコードがこちらになります。
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='|'
を指定するようにします。
- 文字列に改行コードを含む場合のみ
- 実行結果がこちらです
aa: |-
bbbb
cccc
dddd
bb: eeee
- いい感じになりました!
うまくいかなケースがある。。
- 上記の実装で出力していたところ、いくつか想定どおりに出力されないケースがありました。
- 調べていったところ、、
文字列の改行の前にスペースがある場合
にうまく出力されない事がわかりました。 - このようなデータの場合
- 調べていったところ、、
test_dict = {
'aa': 'bbbb\ncccc \ndddd',
'bb': 'eeee'
}
- 改行してくれません。。
aa: "bbbb\ncccc \ndddd"
bb: eeee
調べてみました
- 気になったのでPyYAMLのコードを確認してみました。
-
analyze_scalar メソッド内でデータのタイプを判定しているのですが、
スペースの次に改行コードがある場合
、space_breakという変数がTrueになります。- するとこのあたりのフラグがすべてFalseにセットされます。
if space_break or special_characters:
allow_flow_plain = allow_block_plain = \
allow_single_quoted = allow_block = False
- 実際にyamlを出力する際にはこちらのchoose_scalar_styleメソッド内でスタイルを判定して出力するようになっています。
- 本来は、先程実装したとおり、
add_representer
メソッドで指定したスタイルで出力されるはずなのですが、先程のフラグのうち、allow_block
がFalseになっていると指定したスタイル(ここでは|
)で出力されないようになっています。
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セットされている箇所があります。-
文字列の末尾にスペースがある場合
にallow_blockがFalseセットされるようです。 - コメントにはこのような記載があり、同様にプロックスタイルでの出力がされないということのようです。
We do not permit trailing spaces for block scalars.
-
-
まとめると、文字列の中に、
スペースに続く改行がある場合
、文字列の末尾にスペースがある場合
には改行コードで文字列が改行されたブロックスタイルでの出力がされないようです。
対応
- ということで、最終的にこちらのような実装で対応をしました。
- 単純にrepresentメソッド内で先程の該当する文字列を置換しています。
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()
- スペースのあとに改行があるケースでも想定どおりの出力となりました。
aa: |-
bbbb
cccc
dddd
bb: eeee
YAMLは奥が深い。。
- きちんと調べていませんが、YAMLの仕様にのっとった処理になっているということのようです。
- 普段あまり気にしていなかったのですが、YAMLの仕様は奥が深いようです。。