1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[無料][2024年版] LINE Messaging API v3 + Python(Flask) でボットを作る [その7 - クラス化編]

Last updated at Posted at 2024-01-07

コードの冗長性

前回の記事で、モードごとの検索結果取得関数を定義しました。具体的には、get_official_youtubeget_lyrics の2つで、どちらも同じテーブルデータから、曲名をキーにして異なるコラムのデータを引っ張ってくるというものです。返信メッセージの整形方法などに多少の違いはあれど、コードの大半は共通しています。

botfuncs.py
#####  MODE : GET OFFICIAL YOUTUBE  #####
def get_official_youtube(text:str) -> list:
	songtitles= search_song_title(text)
	if len(songtitles) == 0:  ## no candidate
		reply = 'NOT FOUND:\nPlease try again with different words.'
		messages = [TextMessage(text=reply)]
	elif len(songtitles) == 1:  ## only one candidate -> return URL
		reply = 'https://www.youtube.com/watch?v=' + DATA.loc[songtitles[0], 'official_youtube']
		messages = [TextMessage(text=reply)]
	else:
		quickreply = QuickReply(items=[])  ## instantiation 
		for song in songtitles:
			if len(song) > 20:  ## max characters : 20
				item = QuickReplyItem(action=MessageAction(label=song[:19]+'', text=song))
			else:
				item = QuickReplyItem(action=MessageAction(label=song, text=song))
			quickreply.items.append(item)
		messages = [TextMessage(text='candidate songs:', quickReply=quickreply)]
	return messages

#####  MODE : GET LYRICS  #####
def get_lyrics(text:str) -> list:
	songtitles= search_song_title(text)
	if len(songtitles) == 0:  ## no candidate
		reply = 'NOT FOUND:\nPlease try again with different words.'
		messages = [TextMessage(text=reply)]
	elif len(songtitles) == 1:  ## only one candidate -> return lyrics
		reply = DATA.loc[songtitles[0], 'lyrics'].replace('<br>', '\n')  ## original data uses <br> instead of \n
		reply = f'{songtitles[0]} :\n\n' + reply  ## add song title as header
		messages = [TextMessage(text=reply)]
	else:
		quickreply = QuickReply(items=[])  ## instantiation 
		for song in songtitles:
			if len(song) > 20:  ## max characters : 20
				item = QuickReplyItem(action=MessageAction(label=song[:19]+'', text=f'lyrics {song}'))  ## add prefix for next reply 
			else:
				item = QuickReplyItem(action=MessageAction(label=song, text=f'lyrics {song}'))
			quickreply.items.append(item)
		messages = [TextMessage(text='candidate songs:', quickReply=quickreply)]
	return messages

こういった冗長性を排除するために、関数は共通にしてキーワード変数などで指定して引っ張ってくるコラムや処理を変えるという方法もあります。ただし、それだと新しいモードを追加するたびに if...elif... を増やす必要もありますし、処理の修正の柔軟性には欠けます。そこで、今回はクラスを用いてみようと思います。「曲名から何かしらの情報を引っ張ってくる」ということで、GetBySongTitle とでも名付けます。

クラスの設計

クラスを定義する際には「抽象化」こそが重要です。一見全然違うようなことをやっているように見えても、抽象的には同じ概念ならばそれは一つにまとめられます。

今回の場合、

  • 検索文字列を受けとって、そこから曲名を探す
  • ヒットしないならエラーメッセージを返す
  • 1曲に特定できたなら、その曲の情報(YouTube動画 or 歌詞)を返す
  • 2曲以上候補があるなら、クイックリプライにして選択させる

という処理は全く同じです。異なるのは、

  1. データテーブルの参照するコラム
  2. YouTube動画の場合、取得したIDの前にURLを付加する
  3. 歌詞検索の場合、取得した歌詞の上に曲名をヘッダとして付ける
  4. 歌詞検索の場合、返信用のメッセージアクションのテキストにモード選択 prefix である lyrics を付ける。YouTube動画なら何も付けない。

という点です。(2)と(3)に関しては、「取得データに何らかの文字列を付加して加工する」という意味では全く同じです。(4)も、「何も付けない」というのは「空文字 '' を prefix として付ける」とみなすことができます。こう考えれば、

  • 情報を取得するコラム名
  • モード選択 prefix 名
  • テキストの加工方法

を指定すれば、同一の処理ができるということになります。最後の「テキストの加工方法」に関しては、関数で渡してあげる必要があります。

百聞は一見に如かず、ということで完成したコードを載せます。

botfuncs.py
class _GetBySongTitle:
	def __init__(self, col_name, mode_prefix='', decorate=lambda x: x):
		self.data = DATA[col_name] ## pd.Series : index - song title
		self.prefix = mode_prefix
		self.decorate = decorate  ## function to add sth to reply message

	def get(self, query:str) -> list:
		songtitles = search_song_title(query)
		if len(songtitles) == 0:  ## no candidate
			reply = 'NOT FOUND:\nPlease try again with different words.'
			messages = [TextMessage(text=reply)]
		elif len(songtitles) == 1:  ## only one candidate -> return URL
			reterieved = self.data[songtitles[0]]  ## retrieve data 
			reply = self.decorate(reterieved, songtitles[0])  
			messages = [TextMessage(text=reply)]
		else:
			quickreply = QuickReply(items=[])  ## instantiation 
			for song in songtitles:
				if len(song) > 20:  ## max characters : 20
					label = song[:19] + ''
				else:
					label = song
				item = QuickReplyItem(action=MessageAction(label=label, text=self.prefix+song))
				quickreply.items.append(item)
			messages = [TextMessage(text='candidate songs:', quickReply=quickreply)]
		return messages

_GetYoutube = _GetBySongTitle(
	col_name='official_youtube',
	mode_prefix='',
	decorate=lambda youtubeID, songtitle: 'https://www.youtube.com/watch?v=' + youtubeID)  ## add YouTube URL
get_official_youtube = _GetYoutube.get

_GetLyrics = _GetBySongTitle(
	col_name='lyrics',
	mode_prefix='lyrics ',
	decorate=lambda lyrics, songtitle: f'{songtitle} :\n\n' + lyrics)  ## add song title as header
get_lyrics = _GetLyrics.get

クラス _GetBySongTitle のコンストラクタにおいて、prefix 名と加工方法の関数を受け取り、格納しています。また、コラム名が確定しているので、その列だけを self.data としてシリーズ型として格納します。

メソッド get においては、元々のコードとほぼ同じ処理をしています。修正点は、曲名が一つに確定してデータを取得した後に、テキスト加工を行う self.decorate() を呼んでいるという点と、クイックリプライにおいて付加する文字を self.prefix に統一していることくらいです。

クラス定義後のインスタンス化の部分で、元コードのモード特有の処理を指定します。col_namemode_prefix は見たまんまです。decorate に関しては、無名関数 lambda を使うことでシンプルに記載しています。

最終的に、_GetBySongTitle クラスの get メソッドを元の関数名と同じになるように定義し直し、app.py にインポートさせます。これによって、app.py 側は一切修正することなく、同じ動作が保障されます。

アンダースコア _ の使用

ちなみになぜクラス名やインスタンス名にアンダースコア _ を付けているかというと、from botfuncs import * でインポートした時に余計なもの(必要な関数以外)をエディタ内で表示させないようにする、という便宜的な理由です。別に付けなければいけないわけではありません。

これに関しては他に良記事がたくさんあるので、そちらも参考にしてください。

次回予告

モードの追加は今回作ったクラスを使えば簡単に行えるようになったので、次回はテーマを変えて Flex Message を使ったメッセージ送信を扱おうと思います。

こちらの記事も良ければ参考にして下さい。

目次 : [無料][2024年版] LINE Messaging API v3 + Python(Flask) でボットを作る

GitHub レポジトリ
older_version 内に、各回時点での app.pyrequirements.txt があります。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?