8 文字列とテキスト編集

テキストは、プログラムで処理する最も一般的なデータ形式の一つです。+演算子で2つの文字列値を結合する方法はすでに説明しました。しかし、文字列値から部分文字列を抽出する、スペースを追加したり除去したりする、アルファベットを小文字または大文字に変換する、文字列が望む形式であるかチェックするなど、もっとたくさんのことができます。テキストのコピーアンドペーストに使われるクリップボードにアクセスするPythonのコードさえ書くことができます。

本章では、こうしたテキスト処理を説明します。それから、テキストを箇条書きにするという退屈な作業を自動化するプロジェクトに取り組みます。

文字列の操作

Pythonのコードで、文字列を書き、表示し、アクセスする方法を見ていきましょう。

文字列リテラル

文字列値はプログラムのメモリに保存されますが、コード中にそのまま書かれている文字列値を文字列リテラルと呼びます。Pythonのコードでは文字列リテラルを直接書けます。シングルクォートの中に文字列値のテキストを書きます。しかし、文字列中にクォートを使う場合はどうすればいいのでしょうか。'That is Alice's cat.'と入力してもうまくいきません。PythonはAliceのあとで文字列が終わると解釈し、残りの部分(s cat.')を不正なPythonのコードだとみなすからです。幸い、文字列リテラルを書く複数の方法があります。文字列という用語は、実行中のプログラムの文脈では文字列値を指し、Pythonのソースコードを入力する文脈では文字列リテラルを指します。

ダブルクォート

文字列リテラルは、シングルクォートで囲んでもダブルクォートで囲んでも構いません。ダブルクォートで囲むと文字列の中にシングルクォートを含められます。以下の式を対話型シェルに入力してみてください。

>>> spam = "That is Alice's cat."

文字列をダブルクォートで囲んでいますから、シングルクォートは文字列の一部であって文字列が終わる印ではないとわかります。しかし、文字列中にシングルクォートとダブルクォートの両方を使いたい場合があります。そのような場合にはエスケープ文字を使う必要があります。

エスケープ文字

エスケープ文字を使うと、通常は文字列リテラルに含めることのできない文字を文字列に含められるようになります。エスケープ文字はバックスラッシュ(\)に続けて文字列に含めたい文字を書きます(訳注:日本語環境では、バックスラッシュが、半角バックスラッシュのように見えることもあれば、半角円マークのようにに見えることもあります)。例えば、\'はシングルクォートのエスケープ文字で、\nは改行文字のエスケープ文字です。(エスケープ文字は2文字で構成されていますが、1文字だと考えるのが一般的です。)シングルクォートで囲まれた文字列中でこのエスケープ文字を使えます。対話型シェルで以下の内容を入力してエスケープ文字の働きを確認してみましょう。

>>> spam = 'Say hi to Bob\'s mother.'

Bob\'sのシングルクォートにはバックスラッシュがあるので、文字列値の終わりを意味するシングルクォートではないとわかります。\'と\"のエスケープ文字を使えば、文字列中にシングルクォートやダブルクォートを含められます。

表8-1は使用可能なエスケープ文字の一覧です。

表 8-1:エスケープ文字

エスケープ文字

表示

\'

シングルクォート

\"

ダブルクォート

\t

タブ

\n

改行

\\

バックスラッシュ

対話型シェルに以下の内容を入力して、これらのエスケープ文字を使う練習をしてみましょう。

>>> print("Hello there!\nHow are you?\nI\'m doing fine.")
Hello there!
How are you?
I'm doing fine.

\はエスケープ文字の開始を意味するので、文字列中にバックスラッシュそのものを使いたい場合は、エスケープ文字\\を使います。

raw文字列

文字列リテラルのクォートの前にrを置けばraw文字列リテラルを作成できます。raw文字列を利用すると、エスケープ文字をすべて無視するようになるので、バックスラッシュを含む文字列値を入力しやすくなります。対話型シェルで次のように入力してみてください。

>>> print(r'The file is in C:\Users\Alice\Desktop')
The file is in C:\Users\Alice\Desktop

これはraw文字列ですから、バックスラッシュが文字列の一部であるとみなされ、エスケープ文字の開始だと解釈されることはありません。

>>> print('Hello...\n\n...world!')  # raw文字列ではない
Hello...

...world!
>>> print(r'Hello...\n\n...world!')  # raw文字列
Hello...\n\n...world!

raw文字列は、r'C:\Users\Al\Desktop'のようなWindowsのファイルパスや正規表現(次章で取り上げます)の文字列など、バックスラッシュを多く含む文字列で便利です。

複数行文字列

\nエスケープ文字を使えば文字列中に改行を挿入することができますが、複数行文字列を使ったほうが簡単でしょう。Pythonの複数行文字列は、三重引用符(シングルクォートでもダブルクォートでも構いません)で囲みます。三重引用符に囲まれた部分では、クォートもタブも改行も文字列の一部だとみなされます。三重引用符の中では、インデントでブロックを表現するPythonのルールが適用されません。

例えば、新しいファイルエディタを開き、次のコードを入力してください。

print('''Dear Alice,

Can you feed Eve's cat this weekend?

Sincerely,
Bob''')

このプログラムをfeedcat.pyという名前で保存して実行してください。出力は次のようになります。

Dear Alice,

Can you feed Eve's cat this weekend?

Sincerely,
Bob

Eve'sの中にあるシングルクォートをエスケープする必要はありません。複数行文字列ではシングルクォートとダブルクォートをエスケープしてもしなくても構いません。

print("Dear Alice,\n\nCan you feed Eve's cat this weekend?\n\nSincerely,\nBob")

この三重引用符を使っていないprint()を呼び出すと先ほどと同じテキストを表示します。

複数行コメント

シャープ記号(#)以下の同じ行の部分はコメントになりますが、複数行のコメントを入れるには複数行文字列を使うことが多いです。

"""This is a test Python program.
Written by Al Sweigart al@inventwithpython.com

This program was designed for Python 3, not Python 2.
"""

def say_hello():
    """This function prints hello.
    It does not return anything."""
    print('Hello!')

この例の複数行文字列は完全に正しいPythonのコードです。

インデックスとスライス

文字列ではリストと同じようにインデックスとスライスを使えます。文字列'Hello, world!'は、各文字を要素とするリストだと考えられます。(通常の)インデックスと負の数のインデックスは以下のように対応します。


'   H  e   l   l   o  ,     w  o  r  l  d   !  '
    0  1   2   3   4  5  6  7  8  9 10  11 12
  -13 -12 -11 -10 -9 -8 -7 -6 -5 -4 -3 -2  -1

スペースとエクスクラメーションマーク(!)も文字列に含まれますから、'Hello, world!'は13文字の長さになります。インデックス0がHで、インデックス12が!です。

以下の式を対話型シェルに入力してみてください。

>>> greeting = 'Hello, world!'
>>> greeting[0]
'H'
>>> greeting [4]
'o'
>>> greeting[-1]
'!'
>>> greeting[0:5]
'Hello'
>>> greeting[:5]
'Hello'
>>> greeting[7:-1]
'world'
>>> greeting[7:]
'world!'

インデックスを指定すると、文字列中で指定した場所の文字を取得します。インデックスの範囲を指定すると、開始インデックスを含み終了インデックスを含みません。よって、greetingが'Hello, world!'だとすると、greeting[0:5]は'Hello'に評価されます。greeting[0:5]で取得できる部分文字列はgreeting[0]からgreeting[4]までの文字をすべて含みます。インデックス5のカンマやインデックス6のスペースは含みません。これはforループのrange(5)が5を含まないのと似ています。

文字列をスライスしても元の文字列は変化しないことに気をつけてください。スライスした部分を別の変数に格納することはできます。対話型シェルで次のように入力してみてください。

>>> greeting = 'Hello, world!'
>>> greeting_slice = greeting[0:5]
>>> greeting_slice
'Hello'
>>> greeting
'Hello, world!'

スライスして取得した部分文字列を別の変数に格納すると、元の文字列と部分文字列の両方にすぐ簡単にアクセスできます。

in演算子とnot in演算子

リスト値と同じように文字列値にin演算子とnot in演算子を使うことができます。2つの文字列の間にin演算子またはnot in演算子を置くと、それがブール値のTrueまたはFalseに評価されます。以下の式を対話型シェルに入力してみてください。

>>> 'Hello' in 'Hello, World'
True
>>> 'Hello' in 'Hello'
True
>>> 'HELLO' in 'Hello, World'
False
>>> '' in 'spam'
True
>>> 'cats' not in 'cats and dogs'
False

これらの式は左側の文字列(大文字と小文字を区別します)が右側の文字列中に含まれるかどうかを判定します。

f文字列

ある文字列中に別の文字列を埋め込むことはプログラミングでよくあります。これまでは、+演算子を使って文字列を結合してきました。

>>> name = 'Al'
>>> age = 4000
>>> 'Hello, my name is ' + name + '. I am ' + str(age) + ' years old.'
'Hello, my name is Al. I am 4000 years old.'
>>> 'In ten years I will be ' + str(age + 10)
'In ten years I will be 4010'

しかし、このように入力するのは面倒です。f文字列というもっとシンプルなアプローチがあります。文字列内に変数または式を埋め込めます。raw文字列のrと同じように、f文字列はfをクォートの前に置きます。以下の式を対話型シェルに入力してみてください。

>>> name = 'Al'
>>> age = 4000
>>> f'My name is {name}. I am {age} years old.'
'My name is Al. I am 4000 years old.'
>>> f'In ten years I will be {age + 10}'
'In ten years I will be 4010'

波かっこ({})で囲まれた部分は、str()関数に渡してから+演算子で前後の文字列と結合させたかのように解釈されます。f文字列で波かっこ自体を使いたければ、波かっこを二重にします。

>>> name = 'Zophie'
>>> f'{name}'
'Zophie'
>>> f'{{name}}'  # 波かっこ自体は波かっこを二重にする
'{name}'

f文字列はPythonで便利に使える機能ですが、バージョン3.6になってようやく導入されました。それよりも古いPythonのバージョンのコードでは、別のテクニックを使うことになります。

f文字列の代替手段:%sとformat()

バージョン3.6以前のPythonでは、文字列の中に別の文字列を埋め込むのにf文字列とは別の方法を使っていました。一つの方法は文字列補間で、別の文字列と置き換えるフォーマット指示子%sを文字列に入れる方法です。対話型シェルで次のように入力してみてください。

>>> name = 'Al'
>>> age = 4000
>>> 'My name is %s. I am %s years old.' % (name, age)
'My name is Al. I am 4000 years old.'
>>> 'In ten years I will be %s' % (age + 10)
'In ten years I will be 4010'

最初の%sが文字列のあとのかっこ内の最初の値で置き換えられ、2番目の%sが2番目の値で置き換えられます。文字列を1つや2つ埋め込むくらいならf文字列と同じように便利ですが、埋め込む箇所が増えるとf文字列のほうがずっと読みやすいです。

format()文字列メソッドを使って埋め込む方法もあります。f文字列と同じように、文字列を埋め込む箇所に波かっこを使います。以下の式を対話型シェルに入力してみてください。

>>> name = 'Al'
>>> age = 4000
>>> 'My name is {}. I am {} years old.'.format(name, age)
'My name is Al. I am 4000 years old.'

format()メソッドは%s文字列補間よりも高機能です。波かっこ内に整数値のインデックス(0から始まります)を入れて、format()が挿入する引数の順番を指定できます。複数の文字列を順不同で挿入するときに役立ちます。

>>> name = 'Al'
>>> age = 4000
>>> '{1} years ago, {0} was born and named {0}.'.format(name, age)
'4000 years ago, Al was born and named Al.'

これらの2つの代替手段よりもf文字列が好まれますが、既存のコードで目にする可能性がありますので、これらの方法も知っておいてください。

便利な文字列メソッド

文字列を分析したり文字列値を変形したりする文字列メソッドがいくつかあります。この節ではよく使うメソッドを説明します。(訳注:アルファベットが想定されているメソッドがありますので、日本語に適用する際には注意してください。)

大文字と小文字の変換

文字列メソッドのupper()とlower()は、元の文字をすべて大文字または小文字に変換した文字列を返します。アルファベット以外の文字は変換されずそのままです。対話型シェルで次のように入力してみてください。

>>> spam = 'Hello, world!'
>>> spam = spam.upper()
>>> spam
'HELLO, WORLD!'
>>> spam = spam.lower()
>>> spam
'hello, world!'

これらのメソッドは、元の文字列を変更するのではなく、新しい文字列値を返すことに注意してください。元の文字列を変更したければ、upper()またはlower()を呼び出した得た結果を元の文字列を格納していた変数の格納してください。変数spamに格納されている文字列を変更するときには、spam.upper()ではなくspam = spam.upper()と書かなければなりません。(変数eggsが10という値を格納しているときに、eggs + 3と書いてもeggsの値は変更されず、変更するためにはeggs = eggs + 3と書くのと同じです。)

upper()メソッドとlower()メソッドは、大文字と小文字を区別せずに比較したいときに便利です。例えば、'great'と'GREat'は等しくありませんが、以下のプログラムではユーザーがGreat、GREAT、grEATのどれを入力しても対応できるようにしてあります。ユーザーが入力した文字列を小文字に変換しているからです。

print('How are you?')
feeling = input()
if feeling.lower() == 'great':
    print('I feel great too.')
else:
    print('I hope the rest of your day is good.')

このプログラムを実行すると、質問を表示して、ユーザーがGREatのようなgreatに相当する内容を入力すれば、I feel great too.と出力します。大文字と小文字の違いなど、ユーザーの入力の表記ゆれや間違いに対応できるようにすれば、プログラムがうまく動かなくなる可能性を減らせます。

How are you?
GREat
I feel great too.

isupper()メソッドとislower()メソッドは、対象文字列に少なくとも1つのアルファベットが含まれていて、そのアルファベットがすべて大文字または小文字であればブール値Trueを返します。それ以外の場合はFalseを返します。以下のコードを対話型シェルに入力して、それぞれのメソッドの返り値を確認してください。

>>> spam = 'Hello, world!'
>>> spam.islower()
False
>>> spam.isupper()
False
>>> 'HELLO'.isupper()
True
>>> 'abc12345'.islower()
True
>>> '12345'.islower()
False
>>> '12345'.isupper()
False

文字列メソッドのupper()とlower()は文字列を返しますから、返された文字列値に対してさらに文字列メソッドを呼び出すことができます。

>>> 'Hello'.upper()
'HELLO'
>>> 'Hello'.upper().lower()
'hello'
>>> 'Hello'.upper().lower().upper()
'HELLO'
>>> 'HELLO'.lower()
'hello'
>>> 'HELLO'.lower().islower()
True

こうした式ではメソッド呼び出しが連鎖しているように見えます。

文字列を構成する文字のチェック

islower()とisupper()に加えて、isで始まる文字列メソッドがほかにもいくつかあります。これらのメソッドは文字列の特性に応じてブール値を返します。よく使うisX()文字列メソッドを紹介します。

isalpha() 文字列が文字だけで構成されていたらTrueを返します。

isalnum() 文字列が文字と数字だけで構成されていたらTrueを返します。

isdecimal() 文字列が数字だけで構成されていたらTrueを返します。

isspace() 文字列がスペース、タブ、改行だけで構成されていたらTrueを返します。

istitle() 文字列が大文字で始まりその他は小文字である単語だけで構成されていたらTrueを返します。

以下の式を対話型シェルに入力してみてください。

>>> 'hello'.isalpha()
True
>>> 'hello123'.isalpha()
False
>>> 'hello123'.isalnum()
True
>>> 'hello'.isalnum()
True
>>> '123'.isdecimal()
True
>>> '    '.isspace()
True
>>> 'This Is Title Case'.istitle()
True

isX()文字列メソッドはユーザーの入力を検証する必要があるときに役立ちます。例えば、以下のプログラムは、正しく入力されるまでユーザーに年齢とパスワードを繰り返し尋ねます。新しいファイルエディタウィンドウを開いて、以下のコードをvalidateInput.pyという名前で保存してください。

while True:
    print('Enter your age:')
    age = input()
    if age.isdecimal():
        break
    print('Please enter a number for your age.')

while True:
    print('Select a new password (letters and numbers only):')
    password = input()
    if password.isalnum():
        break
    print('Passwords can only have letters and numbers.')

最初のwhileループでは、ユーザーに年齢を尋ね、入力された値をageに格納しています。ageが正しい値(数字の値)であれば、最初のwhileループから抜け、パスワードを尋ねる2つ目のwhileループに進みます。正しい値でなければ、ユーザーに数字を入力する必要があることを伝えて、もう一度年齢を尋ねます。2つ目のwhileループでは、パスワードを尋ね、入力された値をpasswordに格納し、その値が文字と数字だけで構成されていたらループを抜けます。そうでなければ、その値を受け入れられないので、パスワードには文字と数字しか使えないことをユーザーに伝えて、もう一度パスワードを入力してもらいます。

このプログラムを実行すると、次のように出力されます。

Enter your age:
forty two
Please enter a number for your age.
Enter your age:
42
Select a new password (letters and numbers only):
secr3t!
Passwords can only have letters and numbers.
Select a new password (letters and numbers only):
secr3t

isdecimal()とisalnum()を変数について呼び出し、その変数に格納されている値が数字だけで構成されているか、文字と数字だけで構成されているかを検証しています。forty twoを拒否して42を受け入れていますし、secr3t!を拒否してsecr3tを受け入れています。

文字列の先頭または末尾をチェックする

startswith()メソッドとendswith()メソッドは、文字列値がメソッドに渡された文字列で始まっていればあるいは終わっていれば、Trueを返します。それ以外の場合はFalseを返します。以下の式を対話型シェルに入力してみてください。

>>> 'Hello, world!'.startswith('Hello')
True
>>> 'Hello, world!'.endswith('world!')
True
>>> 'abc123'.startswith('abcdef')
False
>>> 'abc123'.endswith('12')
False
>>> 'Hello, world!'.startswith('Hello, world!')
True
>>> 'Hello, world!'.endswith('Hello, world!')
True

これらのメソッドは等価演算子(==)の代わりに使えます。文字列全体ではなく、文字列の最初や最後だけをチェックすればよい場合に便利です。

文字列の連結と分割

join()メソッドは、文字列のリストがあり、その文字列を連結して一つの文字列値を作成したいときに使います。文字列についてjoin()メソッドを呼び出し、文字列のリストを渡します。そうすると渡されたリストをその文字列で連結したものが返されます。対話型シェルで次のように入力してみてください。

>>> ', '.join(['cats', 'rats', 'bats'])
'cats, rats, bats'
>>> ' '.join(['My', 'name', 'is', 'Simon'])
'My name is Simon'
>>> 'ABC'.join(['My', 'name', 'is', 'Simon'])
'MyABCnameABCisABCSimon'

join()を呼び出した文字列が、引数に渡されたリストの各文字列の間に挿入されます。例えば、', 'という文字列についてjoin(['cats', 'rats', 'bats'])を呼び出すと、'cats, rats, bats'という文字列が返されます。

文字列についてjoin()を呼び出し、リスト値を渡すということを覚えておいてください(間違えて逆にして呼び出してしまうことがよくあります)。split()メソッドは反対の動作をします。つまり、文字列値に対して呼び出すと、文字列のリストを返します。以下の式を対話型シェルに入力してみてください。

>>> 'My name is Simon'.split()
['My', 'name', 'is', 'Simon']

デフォルトでは、スペース、タブ、改行文字といった空白文字で文字列'My name is Simon'を分割します。これらの空白文字は返されるリスト中の文字列には含まれません。区切り文字をsplit()メソッドに渡して指定することができます。対話型シェルで次のように入力してみてください。

>>> 'MyABCnameABCisABCSimon'.split('ABC')
['My', 'name', 'is', 'Simon']
>>> 'My name is Simon'.split('m')
['My na', 'e is Si', 'on']

複数行文字列を改行文字で区切るためにsplit()をよく使います。対話型シェルで次のように入力してみてください。

>>> spam = '''Dear Alice,
... There is a milk bottle in the fridge
... that is labeled "Milk Experiment."
...
... Please do not drink it.
... Sincerely,
... Bob'''
...
>>> spam.split('\n')
['Dear Alice,', 'There is a milk bottle in the fridge',
'that is labeled "Milk Experiment."', '', 'Please do not drink it.',
'Sincerely,', 'Bob']

split()に引数'\n'を渡すと、spamに格納されている複数行文字列を改行文字で分割して、1行ずつ要素に入れたリストが返されます。

テキストの左寄せ、右寄せ、中央寄せ

文字列メソッドのrjust()とljust()は文字列を空白で詰めたものを返します。第一引数は出来上がりの文字列の長さです。以下の式を対話型シェルに入力してみてください。

>>> 'Hello'.rjust(10)
'     Hello'
>>> 'Hello'.rjust(20)
'              Hello'
>>> 'Hello, World'.rjust(20)
'         Hello, World'
>>> 'Hello'.ljust(10)
'Hello     '

'Hello'.rjust(10)は「文字列'Hello'を合計10文字になるように右寄せしてください」と読めます。'Hello'は5文字ですから、5個のスペースが左側に詰められ、'Hello'を右寄せした10文字の文字列になります。

rjust()とljust()のオプションの第二引数は、スペース以外の詰める文字を指定します。以下の式を対話型シェルに入力してみてください。

>>> 'Hello'.rjust(20, '*')
'***************Hello'
>>> 'Hello'.ljust(20, '-')
'Hello---------------'

center()文字列メソッドはljust()やrjust()と似ていますが、中央寄せにします。以下の式を対話型シェルに入力してみてください。

>>> 'Hello'.center(20)
'       Hello        '
>>> 'Hello'.center(20, '=')
'=======Hello========'

表示されたテキストは中央寄せになっています。

空白の削除

空白文字(スペース、タブ、改行文字)を、文字列の片側あるいは両側から切り落としたいときがあるでしょう。文字列メソッドstrip()は、文字列の始まりと終わりの空白を削除した新しい文字列を返します。lstrip()は左側(始まり)から、rstrip()は右側(終わり)から空白文字を取り除きます。以下の式を対話型シェルに入力してみてください。

>>> spam = '    Hello, World    '
>>> spam.strip()
'Hello, World'
>>> spam.lstrip()
'Hello, World    '
>>> spam.rstrip()
'    Hello, World'

オプションで取り除く文字を指定できます。以下の式を対話型シェルに入力してみてください。

>>> spam = 'SpamSpamBaconSpamEggsSpamSpam'
>>> spam.strip('ampS')
'BaconSpamEggs'

strip()に引数'ampS'を渡しているので、spamに格納されている文字列の両端からa、m、p、Sを切り落とします。strip()に渡す文字の順番は関係ありません。strip('ampS')とstrip('mapS')とstrip('Spam')は同じ動作になります。

文字の数値コード

コンピュータは情報をバイト(0と1の列)として保存します。つまり、テキストを数値に変換できなければならないということです。この要請から、すべてのテキスト文字はUnicodeコードポイントと呼ばれる数値と対応します。例えば、'A'の数値コードは65で、'4'の数値コードは52で、'!'の数値コードは33です。ord()関数を使うと指定した文字の数値コードを取得でき、chr()関数を使うと指定した数値コードの文字を取得できます。以下の式を対話型シェルに入力してみてください。

>>> ord('A')
65
>>> ord('4')
52
>>> ord('!')
33
>>> chr(65)
'A'

文字を順番に並べたり、文字の数値計算を行ったりする必要があるときに、これらの関数を使えます。

>>> ord('B')
66
>>> ord('A') < ord('B')
True
>>> chr(ord('A'))
'A'
>>> chr(ord('A') + 1)
'B'

Unicodeコードポイントはこれだけではありませんが、詳細に入ると本書の範囲を逸脱してしまいます。もっと知りたければ、https://nedbatchelder.com/text/unipain.html でNed Batchelderの2012 PyCon talk, “Pragmatic Unicode, or How Do I Stop the Pain?”を見るか読むかするのをおすすめします。

文字列をファイルに書き込んだりインターネットで送ったりするときには、テキストからバイトに変換します。これをエンコードと呼びます。Unicodeのエンコード基準がいくつかありますが、最もよく使われるのはUTF-8です。Unicodeのエンコーディングを選ぶ必要があったとしたら、'utf-8'を選んでおけばまず間違いないです。Tom Scottの“Characters, Symbols and the Unicode Miracle”という題名の動画(https://youtu.be/MijmeoH9LT4)でUTF-8について説明されています。

文字列のコピーアンドペースト

pyperclipモジュールには、コンピュータのクリップボードからテキストをコピーアンドペーストできるcopy()関数とpaste()関数があります。プログラムの出力をクリップボードに送れば、メールやワープロソフトなどに簡単に貼り付けられます。

pyperclipモジュールはPython本体には付属していません。Appendix Aのサードパーティのパッケージのインストール手順の指示に従ってインストールしてください。pyperclipをインストールしてから、対話型シェルで次のように入力してください。

>>> import pyperclip
>>> pyperclip.copy('Hello, world!')
>>> pyperclip.paste()
'Hello, world!'

プログラム外でクリップボードの内容を変更すると、paste()関数の結果が変わります。例えば、"For example, if I copied this sentence to the clipboard and then called paste(), it would look like this:"をクリップボードに貼り付けてからpaste()を呼び出すと、次のようになります。

>>> pyperclip.paste()
'For example, if I copied this sentence to the clipboard and then called
paste(), it would look like this:'

input()関数を呼び出して入力しなくても大量のテキストを受け取ることができるので、クリップボードは便利です。例えば、aLtErNaTiNgのように小文字と大文字を交互に繰り返すプログラムを書くとしましょう。小文字と大文字を交互に繰り返したいテキストをクリップボードにコピーして、このプログラムを実行してください。そうするとプログラムがクリップボードを読み取り、小文字と大文字を交互に繰り返したテキストをクリップボードに保存します。以下のコードをalternatingText.pyという名前で保存してください。

import pyperclip

text = pyperclip.paste()  # クリップボードからテキストを取得
alt_text = ''  # 小文字と大文字を交互に繰り返したテキストを格納
make_uppercase = False
for character in text:
    # 各文字を反復処理してalt_textに追加
    if make_uppercase:
        alt_text += character.upper()
    else:
        alt_text += character.lower()

    # make_uppercaseの値を反転
    make_uppercase = not make_uppercase
pyperclip.copy(alt_text)  # 結果をクリップボードに保存
print(alt_text)  # 結果を画面にも表示

クリップボードにテキスト(例えば"If you copy some text to the clipboard (for instance, this sentence) and run this program, the output and clipboard contents become this:")をコピーしてこのプログラムを実行すると、出力とクリップボードに保存されている内容は次のものになります。

iF YoU CoPy sOmE TeXt tO ThE ClIpBoArD (fOr iNsTaNcE, tHiS SeNtEnCe) AnD
 RuN ThIs pRoGrAm, ThE OuTpUt aNd cLiPbOaRd cOnTeNtS BeCoMe ThIs:

pyperclipモジュールを活用すればクリップボードを操作できるので、プログラムとテキストのやり取りを直接できるようになります。

プロジェクト2:ウィキマークアップに箇条書き記号を追加する

Wikipediaの記事を編集する際には、各項目の行頭にアスタリスクをつけると箇条書きリストを作成できます。箇条書きリストにしたい長いリストがあるとします。1行ずつ行頭にアスタリスクを入れていってもいいのですが、ちょっとしたPythonスクリプトを書けばその作業を自動化できます。

bulletPointAdder.pyスクリプトはクリップボードからテキストを取得して、アスタリスクとスペースを各行の行頭に入れ、その追加後のテキストをクリップボードに保存します。例えば、以下のテキスト(List of lists of listsというWikipediaの記事)をクリップボードにコピーしたとします。

Lists of animals
Lists of aquarium life
Lists of biologists by author abbreviation
Lists of cultivars

その状態でbulletPointAdder.pyプログラムを実行すると、クリップボードの内容は以下のようになります。

* Lists of animals
* Lists of aquarium life
* Lists of biologists by author abbreviation
* Lists of cultivars

このテキストをWikipediaの記事に貼り付けると箇条書きになります。

ステップ1:クリップボードのコピーアンドペースト

bulletPointAdder.pyプログラムは以下の内容を実行します。

  • クリップボードからテキストを取得する
  • そのテキストに対して何らかの処理を行う
  • 新しいテキストをクリップボードに保存する

テキストの処理は少しややこしいですが、コピーアンドペーストはごく単純です。pyperclip.copy()関数とpyperclip.paste() 関数を使うだけです。とりあえずその関数を呼び出す部分を書いてみましょう。次のコードをbulletPointAdder.pyという名前で保存してください。

import pyperclip
text = pyperclip.paste()

# TODO:行を分割してアスタリスクをつける

pyperclip.copy(text)

TODOコメントはプログラムの未完成部分を示します。次にその部分に取り組みます。

ステップ2:テキストを行ごとに分割する

pyperclip.paste()を呼び出すと、クリップボードに保存されているテキストがすべて一つの大きな文字列として返されます。“List of Lists of Lists”の例で言うと、textに格納される文字列は次のようになります。

'Lists of animals\nLists of aquarium life\nLists of biologists by author
abbreviation\nLists of cultivars'

この文字列中の改行文字\nにより、クリップボードから貼り付けたときに複数行で表示されます。この一つの文字列値には複数の「行」があるということです。各行の行頭にアスタリスクをつけたいです。

文字列の中から改行文字\nを一つずつ探してその直後にアスタリスクを加えるコードを書くこともできます。しかし、split()メソッドを使って元の文字列を一行ずつ分割した文字列のリストを取得し、リストの中に入っている各文字列の先頭にアスタリスクを加えるほうが簡単でしょう。

次のようにプログラムを編集してください。

import pyperclip
text = pyperclip.paste()

# 行を分割してアスタリスクをつける
lines = text.split('\n')
for i in range(len(lines)):  # リスト"lines"のすべてのインデックスを反復処理
    lines[i] = '* ' + lines[i] # リスト"lines"の各文字列にアスタリスクをつける

pyperclip.copy(text)

テキストを改行で分割してリストを取得します。そのリストをlinesという名前の変数に格納し、ループで各要素を反復処理します。行ごとに行頭にアスタリスクとスペースを加えます。そうすればlinesの中に入っている各行すべてにアスタリスクがつきます。

ステップ3:調整した各行を連結する

リストlinesにはアスタリスクで始まるように調整した行が入っています。しかし、pyperclip.copy()は文字列が入ったリストではなく、一つの文字列値を取ります。一つの文字列値を作るために、join()メソッドにlinesを渡してリスト中の文字列を連結した一つの文字列を取得します。

import pyperclip
text = pyperclip.paste()

# 行を分割してアスタリスクをつける
lines = text.split('\n')
for i in range(len(lines)):  # リスト"lines"のすべてのインデックスを反復処理
    lines[i] = '* ' + lines[i]  # リスト"lines"の各文字列にアスタリスクをつける
text = '\n'.join(lines)
pyperclip.copy(text)

このプログラムを実行すると、クリップボードのテキストを、各行にアスタリスクをつけたものに置き換えます。これでプログラムは完成ですから、クリップボードにテキストをコピーしてから実行してみてください。

この箇条書きを自動化する作業は必要ないとしても、行末のスペースを落とすとか、テキストを大文字または小文字に変換するといった、似たようなテキスト操作を自動化する必要を感じることはあるかもしれません。どのような作業であっても、クリップボードを通じてテキストのやり取りができます。

短いプログラム:ピッグ・ラテン(訳注:英語をラテン語風に変換する言葉遊び)

ピッグ・ラテンは英語をラテン語風に変換します。母音で始まる単語にはyayをつけます。子音(chやgrのような連結子音を含む)で始まる単語は、その子音(連結子音)を語末に移し、さらにayをつけます。

次のように出力するピッグ・ラテンプログラムを書いてみましょう。

Enter the English message to translate into pig latin:
My name is AL SWEIGART and I am 4,000 years old.
Ymay amenay isyay ALYAY EIGARTSWAY andyay Iyay amyay 4,000 yearsyay oldyay.

このプログラムでは、本章で紹介したメソッドを使って文字列を変換しています。次のコードをpigLat.pyという名前で保存してください。

# 英語からピッグ・ラテンに変換
print('Enter the English message to translate into pig latin:')
message = input()

VOWELS = ('a', 'e', 'i', 'o', 'u', 'y')

pig_latin = [] # ピッグ・ラテンに変換した単語のリスト
for word in message.split():
    # 語頭の非アルファベットを分離
    prefix_non_letters = ''
    while len(word) > 0 and not word[0].isalpha():
        prefix_non_letters += word[0]
        word = word[1:]
    if len(word) == 0:
        pig_latin.append(prefix_non_letters)
        continue

    # 語末の非アルファベットを分離
    suffix_non_letters = ''
    while not word[-1].isalpha():
        suffix_non_letters = word[-1] + suffix_non_letters
        word = word[:-1]

    # その語が全部大文字なのか先頭が大文字なのかを記憶
    was_upper = word.isupper()
    was_title = word.istitle()

    word = word.lower() # 変換するために単語を全部小文字に

    # 語頭の子音を分離
    prefix_consonants = ''
    while len(word) > 0 and not word[0] in VOWELS:
        prefix_consonants += word[0]
        word = word[1:]

    # ピッグ・ラテンの語尾を追加
    if prefix_consonants != '':
        word += prefix_consonants + 'ay'
    else:
        word += 'yay'

    # 全部大文字または先頭が大文字を復元
    if was_upper:
        word = word.upper()
    if was_title:
        word = word.title()

    # 語頭と語末に非アルファベットを復元
    pig_latin.append(prefix_non_letters + word + suffix_non_letters)

# すべての単語を一つの文字列に連結
print(' '.join(pig_latin))

このコードを最初から一行ずつ見ていきましょう。

# 英語からピッグ・ラテンに変換
print('Enter the English message to translate into pig latin:')
message = input()

VOWELS = ('a', 'e', 'i', 'o', 'u', 'y')

まず、ピッグ・ラテンに変換する英語のテキストをユーザーに入力してもらいます。また、小文字の母音(yを含む)を保持する定数をタプルで設定します。あとでこれを使います。

次に、英語をピッグ・ラテンに変換した語を格納する変数pigLatinを作成します。

pigLatin = [] # ピッグ・ラテンに変換した単語のリスト
for word in message.split():
    # 語頭の非アルファベットを分離
    prefixNonLetters = ''
    while len(word) > 0 and not word[0].isalpha():
        prefixNonLetters += word[0]
        word = word[1:]
    if len(word) == 0:
        pigLatin.append(prefixNonLetters)
        continue

単語ごとに変換する必要がありますから、message.split()を呼び出して単語のリストを得ます。文字列'My name is AL SWEIGART and I am 4,000 years old.'にsplit()を適用すると、['My', 'name', 'is', 'AL', 'SWEIGART', 'and', 'I', 'am', '4,000', 'years', 'old.']が返されます。

'old.'のような文字列を'old.yay'ではなく 'oldyay.'へと変換できるように、それぞれの単語の語頭と語末からアルファベット以外の部分を除去する必要があります。この非アルファベットをprefixNonLettersという名前の変数に保存しておきます。

    # 語末の非アルファベットを分離
    suffixNonLetters = ''
    while not word[-1].isalpha():
        suffixNonLetters += word[-1] + suffixNonLetters
        word = word[:-1]

単語の最初の文字についてisalpha()を呼び出すループで、語頭を除去するかどうかを判定します。語頭を除去する場合はprefixNonLettersと結合します。'4,000'のように単語全体が非アルファベットなら、単にpigLatinリストに追加して次の語に進みます。word文字列の語末の非アルファベットを保存しておく必要もあります。その部分のコードは先ほどのループに似ています。

次に、ピッグ・ラテンへの変換後に復元できるよう、その語が全部大文字なのか先頭が大文字なのかを記憶しておきます。

    # その語が全部大文字なのか先頭が大文字なのかを記憶
    wasUpper = word.isupper()
    wasTitle = word.istitle()

    word = word.lower() # 変換するために単語を全部小文字に

forループの残りのコードではwordを小文字で扱います。

sweigartをeigart-swayのように変換するために、wordの最初の子音を取り除く必要があります。

    # 語頭の子音を分離
    prefixConsonants = ''
    while len(word) > 0 and not word[0] in VOWELS:
        prefixConsonants += word[0]
        word = word[1:]

wordの語頭から非アルファベットを除去したループと似たようなループを使います。ただし今回はprefixConsonantsという名前の変数に子音を保存します。

その語が子音で始まれば、その子音をprefixConsonantsに格納し、 wordの終わりにその子音と文字列'ay'を結合します。それ以外の場合は、wordが母音で始まりますから、'yay'と結合するだけでよいです。

    # ピッグ・ラテンの語尾を追加
    if prefixConsonants != '':
        word += prefixConsonants + 'ay'
    else:
        word += 'yay'

wordにword = word.lower()で小文字を設定したことを思い出してください。wordがもともとはすべて大文字や先頭が大文字だったとしたら、wordを元の大文字に戻します。

    # 全部大文字または先頭が大文字を復元
    if wasUpper:
        word = word.upper()
    if wasTitle:
        word = word.title()

forループの最後で、元の語にあった非アルファベットをくっつけ、リストpigLatinに追加します。

    # 語頭と語末に非アルファベットを復元
    pigLatin.append(prefixNonLetters + word + suffixNonLetters)

# すべての単語を一つの文字列に連結
print(' '.join(pigLatin))

このループが終われば、join()メソッドでこの文字列のリストを一つの文字列へと結合します。そして、その一つの文字列をprint()に渡し、画面上にピッグ・ラテンを表示します。

まとめ

テキストは一般的なデータ形式であり、Pythonには文字列値に保存されたテキストを処理する便利な文字列メソッドがたくさんあります。インデックスやスライスに加えて、こうした文字列メソッドを多くのプログラムで使うことになるでしょう。

今書いているプログラムはそれほど洗練されているように見えません。画像や色がついているグラフィカルユーザーインターフェース(GUI)はありません。これまでのところは、print()でテキストを表示して、input()でユーザーがテキストを入力しました。しかし、クリップボードを経由すれば、大量のテキストを素早く入力できます。大量のテキストを操作するプログラムを書く際には、クリップボードを使うと便利です。こうしたテキストベースのプログラムにおしゃれなウィンドウやグラフィックスはありませんが、有用な作業を素早く終わらせられます。

大量のテキストを操作するもう一つの方法は、ハードドライブ上のファイルを直接読み書きすることです。その方法については第10章で説明します。

これでPythonプログラミングの基礎概念を学び終えました。本書の残りの部分で新しい概念に出会うでしょうが、作業を自動化できる便利なプログラムを書く準備はもう整っています。ここまでに学んだ基本概念から作られているちょっとしたPythonプログラム集を見たければ、私のThe Big Book of Small Python Projects (No Starch Press, 2021)という別の本をご覧ください。それぞれのプログラムのソースコードを手で写し、いろいろいじってみてどういう挙動になるのか確かめてみてください。プログラムの動作を理解したら、自分でゼロからプログラムを再作成してみてください。ソースコードを正確に再現する必要はありません。プログラムがどのように実行されるかよりも、プログラムが何を実行するかを意識してください。

Pythonでウェブページをダウンロードしたり、スプレッドシートを更新したり、ショートメールを送ったりする方法がまだわからないと思われるかもしれません。それらはPythonのモジュールを使って実現します。別のプログラマが書いてくれたモジュールの関数を使うと、そうした作業が簡単にできます。次章では、実際に役立つ業務自動化プログラムを書きます。

練習問題

  1. エスケープ文字とは何ですか?

  2. エスケープ文字\nと\tはそれぞれ何を表していますか?

  3. 文字列中にバックスラッシュ文字\を入れるにはどうすればよいですか?

  4. 文字列値"Howl's Moving Castle"有効な文字列です。Howl'sのシングルクォートがエスケープされていないのに問題が生じないのはなぜですか?

  5. 文字列中に\nを入れずに改行したい場合は、どうすればよいですか?

  6. 次の式はどのように評価されますか?

  • 'Hello, world!'[1]
  • 'Hello, world!'[0:5]
  • 'Hello, world!'[:5]
  • 'Hello, world!'[3:]

  7. 次の式はどのように評価されますか?

  • 'Hello'.upper()
  • 'Hello'.upper().isupper()
  • 'Hello'.upper().lower()

  8. 次の式はどのように評価されますか?

  • 'Remember, remember, the fifth of November.'.split()
  • '-'.join('There can be only one.'.split())

  9. 右寄せ、左寄せ、中央寄せができる文字列メソッドは何ですか?

10. 文字列の両端から空白文字を除去するにはどうすればよいですか?

練習プログラム:表形式の表示

練習のために、リストのリスト(入れ子のリスト)を取って各列を右寄せしてきれいに表形式で表示するprintTable()という名前の関数を書いてください。内側のリストに入っている文字列の個数は同じであると想定してください。例えば次のような値です。

tableData = [['apples', 'oranges', 'cherries', 'banana'],
             ['Alice', 'Bob', 'Carol', 'David'],
             ['dogs', 'cats', 'moose', 'goose']]

printTable()関数は次のように出力します。

   apples Alice  dogs
  oranges   Bob  cats
 cherries Carol moose
   banana David goose

ヒント:まず、列幅を決めるために、内側のリストの各列中で最も長い文字列を見つけなければなりません。各列の最大文字数(列幅)を整数のリストに格納します。そのために、printTable()関数は、tableDataの内側のリスト数(列数)だけ0を含むリストを作成する、 colWidths = [0] * len(tableData)という行で開始します。このようにすれば、colWidths[0]がtableData[0]の最大文字数(列幅)を保持し、colWidths[1]がtableData[1]の最大文字数(列幅)を保持するといった形で、それぞれの列について最大文字数(列幅)を保持できます。colWidthsリストから最大文字数(列幅)を取得して、その文字数を文字列メソッドrjust()に渡します。

第2部 業務自動化