Otsuhachi’s diary

プログラミングのことや覚書などその他いろいろ

当ブログの内容を使用したり参考にした場合に生じた問題の責任は負いかねます。

Pythonで文字列をモールス信号に変換する -実装編-

2021-10-13 追記

前回に引き続き文字列をモールス信号に変換するライブラリを作ってみる(実装編)。

細かく分けていって最後に全部まとめたコードを紹介するので、内容だけ見たい場合はここをクリック

コードはWindows用のものなので別OSで使いたい場合は一部コードを差し替える。
具体的には、以下の2点を変更すればよい。

  1. winsoundをインポートする文を消去する。
  2. winsoud.Beepの代わりに周波数とミリ秒を指定して音を鳴らすBeep関数を用意する。

目次

  1. インポート
  2. 変換テーブルの作成
  3. クラス定義
  4. 完成コード
  5. 使い方
  6. インストール

インポート

目次に戻る

必要なライブラリをインポートしていく。

使用するのは、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

  1. 基礎
  2. play
  3. parse_morse

基礎

目次に戻る
クラス定義の目次に戻る

まずはクラス属性と__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__()の処理

  1. 短点, 長点, 文字間を表す文字が重複していないか確認を行う
  2. short, long, sep, frequency, minimum_lengthを属性に代入する4
  3. textから連続する空白を取り除き、text.upper()を属性に代入する
  4. __morse_code属性にtextのモールス表現を代入する

__str__()self.textを返すようにして、元の文を取得できるようにする。
morse_codeプロパティはself.textのモールス信号表現を返す。

__add__()の処理

  1. otherに影響しない引数を辞書kwargsに登録しておく
  2. otherMorseCodeインスタンスstr型text属性を持つかどうか
    • kwargs['text']selfothertext属性を足したものを代入する
    • kwargs['text']selftext属性とotherを足したものを代入する5
  3. 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とそのインデックスifor

  1. charが空白かどうか
    • t: 単語なので、sep_word待機してcontinue
    • f: i0ではないか
      • sep_char待機する
  2. charのモールス表現dotとそのインデックスjfor
    1. j0ではないか
      • sep_dot待機する
    2. 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)

引数はtextcodeになっている点以外は__init__()と同じである。

注意点としてはcodeで使用する文字とshort, long, sepが対応している必要がある。
short='s'としている場合にcode中の短点を'.'で表すことはできない。

for文以降の処理

後述するcを復元した文字を入れるリストtextを用意しておく。

code.split(sep)の1文字cfor
cshort, longからなる文字列。

  1. cが空白かどうか
    • 直前に復元した文字が空白かどうか
      • t: continue
      • f: textに空白を追加してcontinue
  2. 0または1のみからなるリストpatternを用意する
  3. cの1点pfor
    1. plongなら
      • t: pattern1を追加
      • f: pshortなら
        • t:pattern0を追加
        • f: 例外発生
  4. textM2C_TABLE[tuple(pattern)]を追加する

for文を抜けたら

  1. text''で連結する。
  2. 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

使い方はで示した通り。


  1. 今回はアルファベットのみ対応していれば十分だったので、実装していない。

  2. もし、和英を設定可能なMorseCodeクラスを作成したければ、和に対応した正規表現も別途設定する必要がある。

  3. エディタのエラーを気にしないなら# type: ignoreはなくてもよい。

  4. この時点でotsuvalidatorのバリデータが引数の値を検証している。

  5. この時点で例外が発生する可能性がある。

  6. このクラスではminimum_length属性が短点の長さ

  7. BT=, AR+と同じモールス表現になる。