Pythonで文字列をモールス信号に変換する -実装編-
2021-10-13 追記
前回に引き続き文字列をモールス信号に変換するライブラリを作ってみる(実装編)。
細かく分けていって最後に全部まとめたコードを紹介するので、内容だけ見たい場合はここをクリック
コードはWindows
用のものなので別OSで使いたい場合は一部コードを差し替える。
具体的には、以下の2点を変更すればよい。
winsound
をインポートする文を消去する。winsoud.Beep
の代わりに周波数とミリ秒を指定して音を鳴らすBeep
関数を用意する。
目次
インポート
必要なライブラリをインポートしていく。
使用するのは、time.sleep
, winsound.Beep
, otsuvalidator
, それからコーディングの補助でtyping.cast
をインポートしている。
otsuvalidator
はクラスの属性が適正かどうかを確認するライブラリなので、pip[env] install otsuvalidator
等でインストールするか、嫌なら適宜属性チェックを行うメソッドを実装して差し替えるとよい。
typing.cast
はバリデータを元の型として扱う、コーディングの補助目的なので、不要であればその部分を差し替えればよい。
Windows以外のOSを使用する場合は、周波数とミリ秒を指定して音を鳴らすBeep
関数を用意しておくこと。
以下がインポート部分のコード
import time from typing import cast from winsound import Beep from otsuvalidator import VInt, VRegex, VString
変換テーブルの作成
このライブラリのある意味本体ともいえる変換テーブルの作成をしていく。
<元の文字>:<0と1のみのタプル>
という形式の辞書。
それから、復元用のテーブルも作成する。
こちらは<0と1のみのタプル>:<元の文字>
という形式の辞書。
短点
を0
, 長点
を1
として扱う。
今回は一部記号とアルファベット、数字のテーブルを作ったが、アルファベットではなく平仮名やカタカナを使ったり、両方用意して後述のコードにmode
属性なりを追加して参照するテーブルを使い分ければ、和英を切り替えることができるようになる。1
以下が変換テーブル部分のコード
# この上にインポート部分 C2M_TABLE = { '.': (0, 1, 0, 1, 0, 1), ',': (1, 1, 0, 0, 1, 1), '?': (0, 0, 1, 1, 0, 0), '_': (0, 0, 1, 1, 0, 1), '+': (0, 1, 0, 1, 0), '-': (1, 0, 0, 0, 0, 1), '×': (1, 0, 0, 1), '^': (0, 0, 0, 0, 0, 0), '/': (1, 0, 0, 1, 0), '@': (0, 1, 1, 0, 1, 0), '(': (1, 0, 1, 1, 0), ')': (1, 0, 1, 1, 0, 1), '"': (0, 1, 0, 0, 1, 0), '\'': (0, 1, 1, 1, 1, 0), '=': (1, 0, 0, 0, 1), 'A': (0, 1), 'B': (1, 0, 0, 0), 'C': (1, 0, 1, 0), 'D': (1, 0, 0), 'E': (0, ), 'F': (0, 0, 1, 0), 'G': (1, 1, 0), 'H': (0, 0, 0, 0), 'I': (0, 0), 'J': (0, 1, 1, 1), 'K': (1, 0, 1), 'L': (0, 1, 0, 0), 'M': (1, 1), 'N': (1, 0), 'O': (1, 1, 1), 'P': (0, 1, 1, 0), 'Q': (1, 1, 0, 1), 'R': (0, 1, 0), 'S': (0, 0, 0), 'T': (1, ), 'U': (0, 0, 1), 'V': (0, 0, 0, 1), 'W': (0, 1, 1), 'X': (1, 0, 0, 1), 'Y': (1, 0, 1, 1), 'Z': (1, 1, 0, 0), '1': (0, 1, 1, 1, 1), '2': (0, 0, 1, 1, 1), '3': (0, 0, 0, 1, 1), '4': (0, 0, 0, 0, 1), '5': (0, 0, 0, 0, 0), '6': (1, 0, 0, 0, 0), '7': (1, 1, 0, 0, 0), '8': (1, 1, 1, 0, 0), '9': (1, 1, 1, 1, 0), '0': (1, 1, 1, 1, 1), } M2C_TABLE = {x[1]: x[0] for x in C2M_TABLE.items()}
算術記号×
とアルファベットX
は(1, 0, 0, 1)
と同じ値になってしまっている。
また、モールス信号から文字に変換する際のテーブルはC2M_TABLE
のキーと値を反転させたものになっている。
この場合、キーが重複した場合に値が上書きされてしまう。
そのため、記号よりも後にアルファベットの登録を行って×
ではなくX
と対応するようにしている。
クラス定義
モールス信号クラスを作成していく。
それから、今回モールス信号に変換可能な文字列を表す正規表現MORSE_CODE_REGEX
も定義しておく。2
基礎
まずはクラス属性と__init__()
, __str__()
, __add__()
メソッド。
それからmorse_code
プロパティを定義していく。
以下が該当部分のコード。
# この上にインポート部分 # この上に変換テーブル部分 MORSE_CODE_REGEX = '^[A-Z0-9 \\.,\\?_\\+\\-×\\^\\/@\\(\\)"\'=]*$' class MorseCode: """モールス信号クラスです。 文字列のモールス表現を取得したり、モールス信号音を再生することができます。 """ text: str = cast(str, VRegex('^[A-Z0-9 \\.,\\?_\\+\\-×\\^\\/@\\(\\)"\'=]*$', 1)) short: str = cast(str, VString(1, 1)) long: str = cast(str, VString(1, 1)) sep: str = cast(str, VString(1, 1)) frequency: int = cast(int, VInt(37, 32767)) minimum_length: int = cast(int, VInt(1)) def __init__(self, text: str, *, short: str = '.', long: str = '-', sep: str = ' ', frequency: int = 440, minimum_length: int = 100): """textを表現するモールス信号を生成します。 textに含むことができる文字は"A-Z0-9 .,?-@"です。 また、連続する空白は1つとして扱われます。 Args: text (str): 元となる文字列です。 short (str, optional): 短点に使用する文字です。 long (str, optional): 長点に使用する文字です。 sep (str, optional): 文字間の区切りに使用する文字です。 frequency (int, optional): 再生時の周波数Hzです。 minimum_length (int, optional): 再生時の短点の長さです。 Raises: ValueError: short, long, sepに重複する文字をあてることはできません。 """ if len({short, long, sep}) != 3: msg = 'short, long, sepはそれぞれ違う文字である必要があります。' raise ValueError(msg) self.short = short self.long = long self.sep = sep self.frequency = frequency self.minimum_length = minimum_length while ' ' in text: text = text.replace(' ', ' ') self.text = text.upper() ct = C2M_TABLE s = self.short l = self.long res = [] for c in self.text: if c == ' ': res.append(' ') else: res.append(''.join(map(lambda x: l if x else s, ct[c]))) self.__morse_code = self.sep.join(res) def __str__(self) -> str: return self.text def __add__(self, other) -> 'MorseCode': kwargs = { 'short': self.short, 'long': self.long, 'sep': self.sep, 'minimum_length': self.minimum_length, } if type(other) is MorseCode or type(getattr(other, 'text', None)) is str: kwargs['text'] = self.text + other.text else: kwargs['text'] = self.text + other return MorseCode(**kwargs) @property def morse_code(self) -> str: """モールス表現化した文字列を返します。 Returns: str: モールス表現です。 """ return self.__morse_code
属性はバリデータを本来の型として扱うためにcast
しているので、不要であれば以下のtext
属性の例を参考にして書き換える。
型ヒントがいらない場合
text = VRegex(MORSE_CODE_REGEX, 1)
cast
をインポートしたくない場合3
text: str = VRegex(MORSE_CODE_REGEX, 1) # type: ignore
otsuvalidator
をインストールしたくない場合にはproperty
でカプセル化して属性を保護したり、インスタンス生成時の引数が正しいかどうか判定する処理を追加しておく。
また、以降に紹介するコードも自身の選択に応じて微修正する。
__init__()
の処理
短点
,長点
,文字間
を表す文字が重複していないか確認を行うshort
,long
,sep
,frequency
,minimum_length
を属性に代入する4text
から連続する空白を取り除き、text.upper()
を属性に代入する__morse_code
属性にtext
のモールス表現を代入する
__str__()
はself.text
を返すようにして、元の文を取得できるようにする。
morse_code
プロパティはself.text
のモールス信号表現を返す。
__add__()
の処理
other
に影響しない引数を辞書kwargs
に登録しておくother
がMorseCode
インスタンスかstr型
のtext
属性を持つかどうかkwargs['text']
にself
とother
のtext
属性を足したものを代入するkwargs['text']
にself
のtext
属性とother
を足したものを代入する5
kwargs
をアンパックして、MorseCode
インスタンスを生成し、返す
play
続いてモールス信号を再生するplay
メソッドを定義していく。
以下が該当部分のコード。
# この上にインポート部分 # この上に変換テーブル部分 # この上にクラスの基礎 def play(self, repeat: int = 1, BT: bool = False, AR: bool = False): """モールス信号音を再生します。 Args: repeat (int, optional): 本文の繰り返し再生数です。 BT (bool, optional): 送信開始の合図を再生するかどうかです。 AR (bool, optional): 送信終了の合図を再生するかどうかです。 """ n = self.minimum_length ct = C2M_TABLE sep_dot = n / 1000 sep_char = sep_dot * 3 sep_word = sep_dot * 7 fq = self.frequency text = ' '.join([self.text for _ in range(repeat)]) if BT: text = '= ' + text if AR: text += ' +' for i, char in enumerate(text): if char == ' ': time.sleep(sep_word) continue elif i: time.sleep(sep_char) for j, dot in enumerate(ct[char]): if j: time.sleep(sep_dot) if dot: Beep(fq, n * 3) else: Beep(fq, n)
モールス信号では短点の長さが時間の基準となる。6
短点を1
としたときの長さの関係は以下の通り。
点 | 長さ |
---|---|
長点 | 3 |
点の間 | 1 |
文字の間 | 3 |
単語の間 | 7 |
モールス信号を送信開始する前と後に特定の信号BT
, AR
を、送信するらしいので、引数BT
, AR
でそれの有無を指定している。7
また、本文の再生回数を指定することができる。
minimum_length
はミリ秒(1/1000秒
)なので、time.sleep
する際には1/1000
する必要がある。
そこで、それぞれsep_dot
, sep_char
, sep_word
として代入しておく。
text
を再生回数、前後信号の有無等で整形したのち、for
文で処理を行っている。
音を鳴らす際のBeep
に関しては何度も書いている通りWindows
以外のOSは別途用意しておくこと。
for文内の処理
text
の1文字char
とそのインデックスi
のfor
文
char
が空白かどうか
sep_word
待機してcontinue
i
が0
ではないか
sep_char
待機するchar
のモールス表現dot
とそのインデックスj
のfor
文
j
が0
ではないか
sep_dot
待機するdot
が長点かどうか
n * 3
ミリ秒鳴らすn
ミリ秒鳴らす
クラス定義-parse_morse
モールス表現を復号化するparse_morse
メソッドを定義していく。
これはクラスメソッドなので、インスタンスを生成せずに使うことができる。
以下が該当部分のコード。
# この上にインポート部分 # この上に変換テーブル部分 # この上にクラスの基礎 # この上にplayメソッド @classmethod def parse_morse(cls, code: str, *, short: str = '.', long: str = '-', sep: str = ' ', frequency: int = 440, minimum_length: int = 100) -> 'MorseCode': if len({short, long, sep, ' '}) > 4: msg = '3種類以上の文字を使用することはできません。' raise ValueError(msg) if len({short, long, sep}) != 3: msg = 'short, long, sepはそれぞれ違う文字である必要があります。' raise ValueError(msg) char = code.split(sep) table = M2C_TABLE text = [] for c in char: if c == '': if text[-1] == ' ': continue text.append(' ') else: pattern = [] for p in c: if p == long: pattern.append(1) elif p == short: pattern.append(0) else: msg = f'"{code}"をモールス信号として解釈できませんでした。' raise ValueError(msg) text.append(table[tuple(pattern)]) text = ''.join(text) return MorseCode(text, short=short, long=long, sep=sep, frequency=frequency, minimum_length=minimum_length)
引数はtext
がcode
になっている点以外は__init__()
と同じである。
注意点としてはcode
で使用する文字とshort
, long
, sep
が対応している必要がある。
short='s'
としている場合にcode
中の短点を'.'
で表すことはできない。
後述するfor文以降の処理
c
を復元した文字を入れるリストtext
を用意しておく。code.split(sep)
の1文字c
のfor
文
c
はshort
, long
からなる文字列。
c
が空白かどうか
continue
text
に空白を追加してcontinue
0
または1
のみからなるリストpattern
を用意するc
の1点p
のfor
文
p
がlong
なら
pattern
に1
を追加p
がshort
なら
pattern
に0
を追加text
にM2C_TABLE[tuple(pattern)]
を追加するfor文
を抜けたら
text
を''
で連結する。text
やその他引数を使用してMorseCode
インスタンスを生成し、返す
完成コード
完成したコードは以下の通りとなる。
import time from typing import cast from winsound import Beep from otsuvalidator import VInt, VRegex, VString C2M_TABLE = { '.': (0, 1, 0, 1, 0, 1), ',': (1, 1, 0, 0, 1, 1), '?': (0, 0, 1, 1, 0, 0), '_': (0, 0, 1, 1, 0, 1), '+': (0, 1, 0, 1, 0), '-': (1, 0, 0, 0, 0, 1), '×': (1, 0, 0, 1), '^': (0, 0, 0, 0, 0, 0), '/': (1, 0, 0, 1, 0), '@': (0, 1, 1, 0, 1, 0), '(': (1, 0, 1, 1, 0), ')': (1, 0, 1, 1, 0, 1), '"': (0, 1, 0, 0, 1, 0), '\'': (0, 1, 1, 1, 1, 0), '=': (1, 0, 0, 0, 1), 'A': (0, 1), 'B': (1, 0, 0, 0), 'C': (1, 0, 1, 0), 'D': (1, 0, 0), 'E': (0, ), 'F': (0, 0, 1, 0), 'G': (1, 1, 0), 'H': (0, 0, 0, 0), 'I': (0, 0), 'J': (0, 1, 1, 1), 'K': (1, 0, 1), 'L': (0, 1, 0, 0), 'M': (1, 1), 'N': (1, 0), 'O': (1, 1, 1), 'P': (0, 1, 1, 0), 'Q': (1, 1, 0, 1), 'R': (0, 1, 0), 'S': (0, 0, 0), 'T': (1, ), 'U': (0, 0, 1), 'V': (0, 0, 0, 1), 'W': (0, 1, 1), 'X': (1, 0, 0, 1), 'Y': (1, 0, 1, 1), 'Z': (1, 1, 0, 0), '1': (0, 1, 1, 1, 1), '2': (0, 0, 1, 1, 1), '3': (0, 0, 0, 1, 1), '4': (0, 0, 0, 0, 1), '5': (0, 0, 0, 0, 0), '6': (1, 0, 0, 0, 0), '7': (1, 1, 0, 0, 0), '8': (1, 1, 1, 0, 0), '9': (1, 1, 1, 1, 0), '0': (1, 1, 1, 1, 1), } M2C_TABLE = {x[1]: x[0] for x in C2M_TABLE.items()} MORSE_CODE_REGEX = '^[A-Z0-9 \\.,\\?_\\+\\-×\\^\\/@\\(\\)"\'=]*$' class MorseCode: """モールス信号クラスです。 文字列のモールス表現を取得したり、モールス信号音を再生することができます。 """ text: str = cast(str, VRegex(MORSE_CODE_REGEX, 1)) short: str = cast(str, VString(1, 1)) long: str = cast(str, VString(1, 1)) sep: str = cast(str, VString(1, 1)) frequency: int = cast(int, VInt(37, 32767)) minimum_length: int = cast(int, VInt(1)) def __init__(self, text: str, *, short: str = '.', long: str = '-', sep: str = ' ', frequency: int = 440, minimum_length: int = 100): """textを表現するモールス信号を生成します。 textに含むことができる文字は"A-Z0-9 .,?-@"です。 また、連続する空白は1つとして扱われます。 Args: text (str): 元となる文字列です。 short (str, optional): 短点に使用する文字です。 long (str, optional): 長点に使用する文字です。 sep (str, optional): 文字間の区切りに使用する文字です。 frequency (int, optional): 再生時の周波数Hzです。 minimum_length (int, optional): 再生時の短点の長さです。 Raises: ValueError: short, long, sepに重複する文字をあてることはできません。 """ if len({short, long, sep}) != 3: msg = 'short, long, sepはそれぞれ違う文字である必要があります。' raise ValueError(msg) self.short = short self.long = long self.sep = sep self.frequency = frequency self.minimum_length = minimum_length while ' ' in text: text = text.replace(' ', ' ') self.text = text.upper() ct = C2M_TABLE s = self.short l = self.long res = [] for c in self.text: if c == ' ': res.append(' ') else: res.append(''.join(map(lambda x: l if x else s, ct[c]))) self.__morse_code = self.sep.join(res) def __str__(self) -> str: return self.text def __add__(self, other) -> 'MorseCode': kwargs = { 'short': self.short, 'long': self.long, 'sep': self.sep, 'minimum_length': self.minimum_length, } if type(other) is MorseCode or type(getattr(other, 'text', None)) is str: kwargs['text'] = self.text + other.text else: kwargs['text'] = self.text + other return MorseCode(**kwargs) @property def morse_code(self) -> str: """モールス表現化した文字列を返します。 Returns: str: モールス表現です。 """ return self.__morse_code def play(self, repeat: int = 1, BT: bool = False, AR: bool = False): """モールス信号音を再生します。 Args: repeat (int, optional): 本文の繰り返し再生数です。 BT (bool, optional): 送信開始の合図を再生するかどうかです。 AR (bool, optional): 送信終了の合図を再生するかどうかです。 """ n = self.minimum_length ct = C2M_TABLE sep_dot = n / 1000 sep_word = sep_dot * 7 sep_char = sep_dot * 3 fq = self.frequency text = ' '.join([self.text for _ in range(repeat)]) if BT: text = '= ' + text if AR: text += ' +' for i, char in enumerate(text): if char == ' ': time.sleep(sep_word) continue elif i: time.sleep(sep_char) for j, dot in enumerate(ct[char]): if j: time.sleep(sep_dot) if dot: Beep(fq, n * 3) else: Beep(fq, n) @classmethod def parse_morse(cls, code: str, *, short: str = '.', long: str = '-', sep: str = ' ', frequency: int = 440, minimum_length: int = 100) -> 'MorseCode': if len({short, long, sep, ' '}) > 4: msg = '3種類以上の文字を使用することはできません。' raise ValueError(msg) if len({short, long, sep}) != 3: msg = 'short, long, sepはそれぞれ違う文字である必要があります。' raise ValueError(msg) char = code.split(sep) table = M2C_TABLE text = [] for c in char: if c == '': if text[-1] == ' ': continue text.append(' ') continue pattern = [] for p in c: if p == long: pattern.append(1) elif p == short: pattern.append(0) else: msg = f'"{code}"をモールス信号として解釈できませんでした。' raise ValueError(msg) text.append(table[tuple(pattern)]) text = ''.join(text) return MorseCode(text, short=short, long=long, sep=sep, frequency=frequency, minimum_length=minimum_length)
使い方
上のコードをコピーして、別モジュールからインポートするも良し、同じファイルに書いて使用するも良し。
後日github
経由のインストールくらいはできるようにする……かも?
インストールできるようになった。
やりかたはここ。github
のリポジトリはここ。
from morse_code import MorseCode # 同じフォルダ内の`morse_code.py`にコピーした場合 def show_morse(mc: MorseCode): print(f'"{mc}" "{mc.morse_code}"') # モールスインスタンス生成 paris = MorseCode('paris') show_morse(paris) # モールス + モールス(文字指定) hello = MorseCode('hello') python = MorseCode('python', short='0', long='1') show_morse(hello + python) # モールス + モールス(文字指定) show_morse(python + hello) # モールス + 文字列 show_morse(hello + ' otsuhachi') # モールス復号化 m2c = MorseCode.parse_morse('.- .-. . -.-- --- ..- - .... . .-. . ..--..') show_morse(m2c) # 再生 m2c.play(BT=True, AR=True) # モールス + 整数 (例外発生) # show_morse(hello + 1) """ Traceback (most recent call last): File "~\sandbox.py", line 24, in <module> show_morse(hello + 1) File "~\morse_code.py", line 133, in __add__ kwargs['text'] = self.text + other TypeError: can only concatenate str (not "int") to str """
"PARIS" ".--. .- .-. .. ..." "HELLOPYTHON" ".... . .-.. .-.. --- .--. -.-- - .... --- -." "PYTHONHELLO" "0110 1011 1 0000 111 10 0000 0 0100 0100 111" "HELLO OTSUHACHI" ".... . .-.. .-.. --- --- - ... ..- .... .- -.-. .... .." "ARE YOU THERE?" ".- .-. . -.-- --- ..- - .... . .-. . ..--.."
無事、作成することができた。
コードを書くよりも記事にする方が疲れる。
インストール
以下のコマンドを実行することでインストールできる。
pip install git+https://github.com/Otsuhachi/OtsuMorseCode
使い方は上で示した通り。