JEPA4Japan · チュートリアル

文字列とテキスト処理

1,888文字 5分で読めます #Python

Unicodeを意識しながら、文字列の切り出し、検索、正規化、整形を行います。

コース進捗 コース目次 24レッスン中 24件を公開中

文字列は小さなタイルの列

Pythonでは、"Python"、"こんにちは"、"👍🏽"のように引用符で囲んだテキストを文字列と呼びます。小さなタイルが一列に並んでいると考えてみましょう。Pythonは、その部品をUnicodeのコードポイントとして扱います。

  1. 受け取る 名前、メモ、タグ
  2. 整える 空白や表記をそろえる
  3. 探す 必要な部分を見つける
  4. 作り直す 元の文字列はそのまま
文字列は、調べて、新しいテキストに組み直せます。

位置は0から数えます。len()が数えるのはコードポイントです。バイト数でも、人が見て感じる文字数でもない場合があります。

word = "Python"

print(len(word))
print(word[0])
print(word[-1])
print(word[1:4])
6
P
n
yth

word[0]は最初、word[-1]は最後のタイルです。スライスは開始位置を含み、終了位置の直前で止まります。したがって[1:4]が取るのは位置1、2、3です。

指す、切る、分ける、つなぐ

1つのインデックスは、実在する位置を指す必要があります。word[99]ならIndexErrorです。一方、スライスはおおらかで、word[:99]はword全体を返します。空文字列には先頭も末尾もないため、インデックスを使う前に確認します。

name = "".strip()

if name:
    print(name[0])
else:
    print("先頭の文字はありません")

split()は文字列を分け、リストにします。join()は、選んだ区切りを使って文字列の部品をつなぎます。

messy = "  読む   書く\t遊ぶ  "
pieces = messy.split()

print(pieces)
print(" ".join(pieces))
['読む', '書く', '遊ぶ']
読む 書く 遊ぶ
  1. 分ける テキストが部品になる
  2. 並べる リストで順番に待つ
  3. つなぐ 再びテキストになる
" ".join(text.split())で、余分な空白を1つにまとめられます。

split(",")では、カンマだけが区切りです。カンマが2つ続けば、間に空の部品ができます。また、join()へ渡す要素はすべて文字列でなければなりません。

クリーナーは新しい文字列を返す

文字列は**変更不可能(immutable)**です。すでにある文字列のタイル1枚だけを入れ替えることはできません。次は、わざと失敗させる例です。

word = "python"
word[0] = "P"

これはTypeErrorになります。代わりに新しい文字列を作ります。

word = "python"
title = "P" + word[1:]

print(word)
print(title)
python
Python

文字列メソッドも同じです。strip()は前後の空白を取り、replace()は指定した並びを置き換え、casefold()はUnicodeを意識した強い検索用表記を作ります。どれも元を変更せず、新しい文字列を返します。

raw = "  Python makes TEXT  "
clean = raw.strip().replace("makes", "cleans")
searchable = clean.casefold()

print(clean)
print("python" in searchable)
print(searchable.count("text"))
print(searchable.find("cleans"))
Python cleans TEXT
True
1
7

inは「ある?」、count()は「重ならない一致が何個?」、find()は「最初の位置は?」に答えます。見つからないときのfind()は-1です。if text.find(term):は使いません。位置0は偽、-1は真として扱われ、意味が逆転するからです。

1つに見えても部品は複数かもしれない

見た目が同じ2つの文字列でも、コードポイントの並びが異なることがあります。標準ライブラリのunicodedataを使うと、正準等価な文字列を同じUnicode形式へそろえられます。

  1. タイル1枚 合成済みの文字
  2. タイル2枚 文字とアクセント
  3. NFC 等価な並びをそろえる
同じ見た目でも、同じコードポイントとは限りません。
import unicodedata

one = "café"
two = "cafe\u0301"

print(one == two)
print(len(one), len(two))
print(one == unicodedata.normalize("NFC", two))
print(len("👍🏽"))
False
4 5
True
2

絵文字は画面上では1つに見えますが、ここでは2コードポイントです。基本のインデックスは途中で分割できます。文字列の位置は、バイト位置でも、必ず安全な「見た目の1文字」の境界でもありません。

ミニ制作:学習メモクリーナー

note_cleaner.pyを作ります。標準ライブラリだけで動く完成プログラムです。Unicodeを正規化し、空白をまとめ、空でないキーワードを数えます。元の文字列は変更しません。

import unicodedata


def search_key(text):
    return unicodedata.normalize("NFC", text).strip().casefold()


def clean_note(text):
    normalized = unicodedata.normalize("NFC", text)
    return " ".join(normalized.split())


raw_note = "  CAFÉ   を練習し、cafe\u0301を復習する。  "
raw_keyword = " Café "

note = clean_note(raw_note)
keyword = search_key(raw_keyword)
matches = search_key(note).count(keyword)

print(f"元のメモ:<{raw_note}>")
print(f"整形後:<{note}>")
print(f"一致数:{matches}")
print(f"元の空白が残る:{raw_note.startswith('  ')}")

python3 note_cleaner.pyで実行し、次を確認します。

元のメモ:<  CAFÉ   を練習し、caféを復習する。  >
整形後:<CAFÉ を練習し、caféを復習する。>
一致数:2
元の空白が残る:True

境界の約束:キーワードは空にしません。text.count("")は文字ではなく隙間を数え、len(text) + 1になるため、学習語の集計には使えません。

3つの小さなミッション

  1. 安全な頭文字。 入力へstrip()を使い、空でない場合だけ最初と最後のコードポイントを表示します。
  2. 短いプレビュー。 text[:limit]を返すpreview(text, limit)を書き、0、3、文字列より大きな値で試します。
  3. 検索探偵。 位置0で一致する語と、存在しない語を試し、inまたはfind() != -1で正しく判定します。

第6章へ進む準備

  • len()、正負のインデックス、スライスを使える
  • 文字列が変更不可能だと説明できる
  • strip()、split()、join()、casefold()で文字列を整えられる
  • 問いに合わせてin、count()、find()を選べる
  • unicodedata.normalize()で等価なUnicode表現をそろえられる
  • コードポイント、見た目の記号、バイトが別物だと覚えている
  • メモクリーナーを実行し、4つの確認結果を再現できる

次章では、split()が作った部品をリストへ入れ、変更できるリストと形を固定するタプルを比べます。