このブログでMeCabを使った形態素解析をするとき、MeCabが出力する文字列を行単位に分割するときはいつもstr.split(“\n”)のような書き方をしていました。しかし、最近Pythonのstrオブジェクトが、splitlines() という行単位で区切る専用のメソッドを持っているのに気づいたのでその紹介です。といっても、splitlines()で改行コードで区切れるぜ、だけだと記事が一瞬で終わってしまうので、ついでにPythonのstr型が持っている3つの分割メソッド[split()/ rsplit()/ splitlines()]をそれぞれ紹介しようと思います。
ドキュメントはこちらのページにあります。
参考: 組み込み型 — Python 3.9.4 ドキュメント
まずはいつも使っている str.split() です。
呼び出しは、 str.split(sep=None, maxsplit=-1) のように定義されています。sepが区切り文字で、maxsplit が最大何箇所で分割るかの指定です。分割された結果は最大でmaxsplit+1個の要素になるので注意してください。 デフォルトの -1 の場合は、区切れる場所全部で区切ります。
# 文字列を, (カンマ) で区切る
print("a,b,c,d,e,f,g".split(","))
# ['a', 'b', 'c', 'd', 'e', 'f', 'g']
# スペースで区切る場合
print(" a b c d e f g ".split(" ")) # cとdの間にはスペース2個入れた
# ['', 'a', 'b', 'c', '', 'd', 'e', 'f', 'g', '']
# maxsplit = 3 を指定。 3箇所で区切られ、長さ4の配列になる
print("a,b,c,d,e,f,g".split(",", 3))
# ['a', 'b', 'c', 'd,e,f,g']
# maxsplit が十分大きい時は指定しないのと同じ
print("a,b,c,d,e,f,g".split(",", 100))
# ['a', 'b', 'c', 'd', 'e', 'f', 'g']
一つ目のセパレーター引数(sep)ですが、これを省略することができます。省略すると、半角スペースやタブ、改行などの空白文字で区切られます。そして、さらに特殊な挙動として、連続する空白文字をひとつのセパレーターとして見なすようになります。また、文字列の先頭や末尾に空白があっても、結果の最初や最後に空文字列は含まれません。それぞれ見ていきましょう。
text = """
最初に改行がある複数行のテキストです。
この行には 連続した半角スペースがあります。
タブも\t入れてみました。
この下に空白行があります。
"""
print(text.split())
# ['最初に改行がある複数行のテキストです。', 'この行には', '連続した半角スペースがあります。', 'タブも', '入れてみました。', 'この下に空白行があります。']
split(” “)だと、スペースが連続してたらり、先頭/末尾にスペースがあると結果の配列に空白文字列が含まれていましたが、それがないのがわかりますね。また、複数の種類の文字(半角スペースやタブや改行)で、全部区切ってくれるのも便利です。
続いて、str.rsplit() を見ていきましょう。 使い方は str.rsplit(sep=None, maxsplit=-1) であり、splitと同じです。 挙動もほぼ同じなのですが、splitとの違いは右から順番に分割していくことです。maxsplit を指定しないと、分割できるところは全部分割するので、splitとrsplitの結果は同じになります。それでは、maxsplit に少し小さめの数値を設定して違いを見てみましょう。
text = "a,b,c,d,e,f,g"
print(text.split(",", 2))
# ['a', 'b', 'c,d,e,f,g']
print(text.rsplit(",", 2))
# ['a,b,c,d,e', 'f', 'g']
雰囲気伝わったでしょうか。
URLを / で分解して、右からn番目の要素を取るなどの使い方ができそうですね。
うちのブログだと、右から2番目がカテゴリを表す文字列です。
# このブログの記事URLの例
url ="https://analytics-note.xyz/machine-learning/scikit-learn-n-gram/"
# 右から4分割した結果。
print(url.rsplit("/", 3))
# ['https://analytics-note.xyz', 'machine-learning', 'scikit-learn-n-gram', '']
# インデックス0 にhttpsからドメイン名まで入る
print(url.rsplit("/", 3)[0])
# https://analytics-note.xyz
# インデックス1がカテゴリ名
print(url.rsplit("/", 3)[1])
# machine-learning
そして、最後が str.splitlines()です。 使い方は str.splitlines([keepends]) であり、セパレーターも分割回数も指定できません。keepends は 分割結果に末尾の改行コードを残すかどうかで、Trueと判定される何かを入れておくと改行コードが残ります。あまり必要ないので、何も入れなくていいでしょう。
import MeCab
tagger = MeCab.Tagger()
result = tagger.parse("すもももももももものうち")
# MeCabの結果を表示しておく
print(result)
"""
すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも 名詞,一般,*,*,*,*,もも,モモ,モモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも 名詞,一般,*,*,*,*,もも,モモ,モモ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
うち 名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS
"""
# splitlines() で分割した配列
print(result.splitlines())
"""
[
'すもも\t名詞,一般,*,*,*,*,すもも,スモモ,スモモ',
'も\t助詞,係助詞,*,*,*,*,も,モ,モ',
'もも\t名詞,一般,*,*,*,*,もも,モモ,モモ',
'も\t助詞,係助詞,*,*,*,*,も,モ,モ',
'もも\t名詞,一般,*,*,*,*,もも,モモ,モモ',
'の\t助詞,連体化,*,*,*,*,の,ノ,ノ',
'うち\t名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ',
'EOS'
]
"""
splitlines() でサクッと行ごとに分離できていますね。いつも使っているsplit(“\n”)との違いも見ておきましょう。
print(result.split("\n"))
"""
[
'すもも\t名詞,一般,*,*,*,*,すもも,スモモ,スモモ',
'も\t助詞,係助詞,*,*,*,*,も,モ,モ',
'もも\t名詞,一般,*,*,*,*,もも,モモ,モモ',
'も\t助詞,係助詞,*,*,*,*,も,モ,モ',
'もも\t名詞,一般,*,*,*,*,もも,モモ,モモ',
'の\t助詞,連体化,*,*,*,*,の,ノ,ノ',
'うち\t名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ',
'EOS',
''
]
"""
一見同じ結果に見えますが、最後に”という空白文字列の要素が入ってます。不要な’EOS’部分を切り捨てるときに切り落とす長さがかわるのでこれは注意して使いましょう。
こうしてみると、 splitlines でも split でもどちらを使っても良さそうですが、splitlinesには明確なメリットがあります。ドキュメントを見ていただけると一覧表が載っているのですが、splitlinesはかなり多くの環境の様々な改行コードや垂直タブ、改ページなど多くの特殊文字で区切ってくれます。split(“\n”)だと、改行コードが違う別の環境ではちょっと動作が不安なので、splitlinesを使った方が汎用性の高いコードになると期待できます。
少し気になる挙動なのですが、「{改行コード}テキスト{改行コード}」という文字列を分解すると、先頭の{改行コード}では区切って1要素目に空白文字列を返してくるのに、末尾の{改行コード}は、その後ろに何もなければ{改行コード}を消して終わります。
print("""
先頭と末尾に改行コード
""".splitlines()
)
# ['', '先頭と末尾に改行コード']
なぜこのような挙動になるのか調べたのですが、結果的に、keepends を指定して動きを見ると理解できました。
"""
aaa
bbb
""".splitlines(True)
# ['\n', 'aaa\n', 'bbb\n']
上記のコードの通り、splitlinesは「改行コードの後ろ」で区切ってるんですね。そして、keependsがFalse(もしくは未指定)の場合は、この区切られた結果から末尾の改行コードを消しているようです。
"""
aaa
bbb
""".splitlines()
# ['', 'aaa', 'bbb']
split() (sep指定なし)は、連続する空白文字をまとめて一つのセパレーターとして扱っていましたが、splitlinesにはそのような機能はなく、普通に改行の数だけ区切って空白文字列だけの要素を返してきます。
print("""この下に連続した改行
この上に連続した改行""".splitlines()
)
# ['この下に連続した改行', '', '', '', 'この上に連続した改行']
今回の記事で紹介したメソッドたちは大変シンプルな機能なのですが、不注意に使うと思ってた結果と要素数やインデックス等がずれたりするので、慣れるまではよく確認しながら使いましょう。