[n番煎じ] 言語処理100本ノック 2015 第1章 with Python

# はじめに
NLP関係の研究室に配属されてだいぶ時間が経ち，「[100本ノック](http://www.cl.ecei.tohoku.ac.jp/nlp100/)いつかやらないとな...」と思いながらどれだけの時間が経ったことでしょう...笑

今月はめちゃくちゃ時間に余裕がありそうなので，本読んだりプログラム書いたりして基礎的な部分の復習をできればと思っています．
そのついでにアウトプットもおこうという感じです．
(既に先人が何人もいるのであれですが...時間の許す限り記事更新も同時進行で頑張りたいと思います．)

もし何かためになるアドバイス等があれば優しいコメントを残して頂けると幸いですm(_ _)m

コードだけは[コチラ](https://github.com/3000manJPY/NLP100Knocks)へ．

# 第1章: 準備運動
>テキストや文字列を扱う題材に取り組みながら，プログラミング言語のやや高度なトピックを復習します．
>キーワード：文字列, ユニコード, リスト型, 辞書型, 集合型, イテレータ, スライス, 乱数

# 問題00 文字列の逆順
> 文字列"stressed"の文字を逆に（末尾から先頭に向かって）並べた文字列を得よ．

```python
#!/usr/local/bin python3
# -*- coding: utf-8 -*-

def reverse_str(string: str) -> str:
    """
    受け取った文字列を逆順にして返す

    Parameter
    ----------
    string: str
        逆順にしたい文字列

    Return
    ----------
    逆順になった文字列
    """
    return string[::-1]

def main():
    in_str = 'stressed'

    rev_str = reverse_str(in_str)
  
    print('文字列(入力): {}'.format(in_str))
    print('文字列(逆順): {}'.format(rev_str))

if __name__ == '__main__':
    main()
```

```console
$ python3 00.py 
文字列(入力): stressed
文字列(逆順): desserts
```

# 問題01 「パタトクカシーー」
> 「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ．

```python
#!/usr/local/bin python3
# -*- coding: utf-8 -*-

def extract_odd_chars(string: str) -> str:
    """
    奇数番目の文字だけを抽出して返す関数

    Parameter
    ----------
    string: str
        抽出対象の文字列

    Return
    ----------
    抽出された文字列
    """
    return string[::2]

def main():
    in_str = 'パタトクカシーー'

    extracted_str = extract_odd_chars(in_str)
    
    print('文字列(入力): {}'.format(in_str))
    print('文字列(抽出): {}'.format(extracted_str))

if __name__ == '__main__':
    main()
```

```console
$ python3 01.py 
文字列(入力): パタトクカシーー
文字列(抽出): パトカー
```

# 問題02 「パトカー」＋「タクシー」＝「パタトクカシーー」
> 「パトカー」＋「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ．

```python
#!/usr/local/bin python3
# -*- coding: utf-8 -*-

def joint_alt_str(string1: str, string2: str) -> str:
    """
    入力された2つの文字列を交互に結合して1つの文字列にする関数

    Parameters
    ----------
    string1, string2: str
        結合したい文字列

    Return
    ----------
    join_str: str
        結合後の文字列
    """
    join_str = ''
    
    for c1, c2 in zip(string1, string2):
        join_str += c1 + c2

    return join_str

def main():
    str1 = 'パトカー'
    str2 = 'タクシー'

    join_str = joint_alt_str(str1, str2)

    print('文字列(入力): {}，{}'.format(str1, str2))
    print('文字列(結合): {}'.format(join_str))

if __name__ == '__main__':
    main()
```

```console
$ python3 02.py 
文字列(入力): パトカー，タクシー
文字列(結合): パタトクカシーー
```

# 問題03 円周率
> "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し，各単語の（アルファベットの）文字数を先頭から出現順に並べたリストを作成せよ．

```python
#!/usr/local/bin python3
# -*- coding: utf-8 -*-

def get_char_num_list(string: str) -> list:
    """
    与えられたテキストの各単語の文字数をリスト化して返す関数

    Parameter
    ----------
    string: str
        カウントしたい文字列

    Return
    ----------
    単語数を順番に格納したリスト
    """
    split_str = string.split(' ')

    return [len(word) - word.count(',') - word.count('.') for word in split_str]

def main():
    in_str = 'Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.'

    cnt_list = get_char_num_list(in_str)

    print('文字列(入力): {}'.format(in_str))
    print('文字数リスト: {}'.format(cnt_list))

if __name__ == '__main__':
    main()
```

```console
$ python3 03.py 
文字列(入力): Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.
文字数リスト: [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]
```

# 問題04 元素記号
> "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し，1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字，それ以外の単語は先頭に2文字を取り出し，取り出した文字列から単語の位置（先頭から何番目の単語か）への連想配列（辞書型もしくはマップ型）を作成せよ．

```python
#!/usr/local/bin python3
# -*- coding: utf-8 -*-

def get_initials(string: str, uni_list: list) -> list:
    """
    与えられたテキストの頭文字を指定文字数分取得し，mapping付きで返す関数
    (idx_listで指定されたものは1文字，それ以外は全て2文字)

    Parameters
    ----------
    string: str
        ターゲットの文字列
    idx_list: list
        一文字だけ取り出す単語のインデックス
    
    Return
    ----------
    initial_list: dict
        頭文字と単語番号を保有する辞書
    """
    split_str = string.split(' ')
    idx_list = [val - 1 for val in uni_list]
    initial_list = {}

    for i, word in enumerate(split_str):
        if i in idx_list:
            num_of_char = 1
        else:
            num_of_char = 2

        initial_list[word[0:num_of_char]] = i + 1

    return initial_list

def main():
    in_str = 'Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.'
    uni_list = [1, 5, 6, 7, 8, 9, 15, 16, 19]

    initial_list = get_initials(in_str, uni_list)

    print('文字列(入力): {}'.format(in_str))
    print('インデックスのリスト: {}'.format(uni_list))
    print('頭文字リスト: {}'.format(initial_list))

if __name__ == '__main__':
    main()
```

```console
$ python3 04.py 
文字列(入力): Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.
インデックスのリスト: [1, 5, 6, 7, 8, 9, 15, 16, 19]
頭文字リスト: {'H': 1, 'He': 2, 'Li': 3, 'Be': 4, 'B': 5, 'C': 6, 'N': 7, 'O': 8, 'F': 9, 'Ne': 10, 'Na': 11, 'Mi': 12, 'Al': 13, 'Si': 14, 'P': 15, 'S': 16, 'Cl': 17, 'Ar': 18, 'K': 19, 'Ca': 20}
```

# 問題05 n-gram
> 与えられたシーケンス（文字列やリストなど）からn-gramを作る関数を作成せよ．この関数を用い，"I am an NLPer"という文から単語bi-gram，文字bi-gramを得よ．

```python
#!/usr/local/bin python3
# -*- coding: utf-8 -*-

def get_char_ngram(string: str, n: int) -> list:
    """
    与えられたテキストの文字n-gramを返す関数

    Parameters
    ----------
    string: str
        ターゲットの文字列
    n: int
        n-gramのn

    Return
    ----------
    文字n-gramのリスト
    """
    return [string[i:i+n] for i in range(len(string)-n+1)]

def get_word_ngram(string: str, n: int) -> list:
    """
    与えられたテキストの単語n-gramを返す関数

    Parameters
    ----------
    string: str
        ターゲットの文字列
    n: int
        n-gramのn

    Return
    ----------
    単語n-gramのリスト
    """
    split_str = string.split(' ')

    return [' '.join(split_str[i:i+n]) for i in range(len(split_str)-n+1)]

def main():
    in_str = 'I am an NLPer'

    word_bigram = get_word_ngram(in_str, 2)
    char_bigram = get_char_ngram(in_str, 2)

    print('文字列(入力): {}'.format(in_str))
    print('単語バイグラム: {}'.format(word_bigram))
    print('文字バイグラム: {}'.format(char_bigram))

if __name__ == '__main__':
    main()
```

```console
$ python3 05.py 
文字列(入力): I am an NLPer
単語バイグラム: ['I am', 'am an', 'an NLPer']
文字バイグラム: ['I ', ' a', 'am', 'm ', ' a', 'an', 'n ', ' N', 'NL', 'LP', 'Pe', 'er']
```

(見やすいように単語n-gramも結合してますが，処理的にはリストで返したほうが扱いやすいですね．)

# 問題06 集合
> "paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を，それぞれ, XとYとして求め，XとYの和集合，積集合，差集合を求めよ．さらに，'se'というbi-gramがXおよびYに含まれるかどうかを調べよ．

```python
#!/usr/local/bin python3
# -*- coding: utf-8 -*-

def get_char_ngram(string: str, n: int) -> list:
    """
    与えられたテキストの文字n-gramを返す関数

    Parameters
    ----------
    string: str
        ターゲットの文字列
    n: int
        n-gramのn

    Return
    ----------
    文字n-gramのリスト
    """
    return [string[i:i+n] for i in range(len(string)-n+1)]

def get_difference_list(list1: list, list2: list) -> list:
    """
    2つのリストの差集合を返すリスト

    Parameters
    ----------
    list1, list2: list
        list型の集合(ここでは文字bigram)

    Return
    ----------
    list1とlist2の差集合
    """
    return list(set(list1) - set(list2))

def get_intersection_list(list1: list, list2: list) -> list:
    """
    2つのリストの積集合を返すリスト

    Parameters
    ----------
    list1, list2: list
        list型の集合(ここでは文字bigram)

    Return
    ----------
    list1とlist2の積集合
    """
    return list(set(list1) & set(list2))

def get_union_list(list1: list, list2: list) -> list:
    """
    2つのリストの和集合を返すリスト

    Parameters
    ----------
    list1, list2: list
        list型の集合(ここでは文字bigram)

    Return
    ----------
    list1とlist2の和集合
    """
    return list(set(list1) | set(list2))

def is_element(element: str, target_list: list) -> bool:
    """
    ある文字列が文字列リストの要素か否かを返す関数

    Parameters
    ----------
    target_list: list
        ターゲットの文字列リスト
    element: str
        検索文字列

    Return
    ----------
    存在すればTrue，それ以外はFalse
    """
    return True if element in target_list else False

def main():
    str1 = 'paraparaparadise'
    str2 = 'paragraph'

    str1_char_bigram = get_char_ngram(str1, 2)
    str2_char_bigram = get_char_ngram(str2, 2)
    
    bigram_union = get_union_list(str1_char_bigram, str2_char_bigram)
    bigram_intersection = get_intersection_list(str1_char_bigram, str2_char_bigram)
    bigram_difference = get_difference_list(str1_char_bigram, str2_char_bigram)

    print('文字列(入力): {}，{}'.format(str1, str2))
    print('str1文字バイグラム: {}'.format(str1_char_bigram))
    print('str2文字バイグラム: {}'.format(str2_char_bigram))
    print('和集合: {}'.format(bigram_union))
    print('積集合: {}'.format(bigram_intersection))
    print('差集合: {}'.format(bigram_difference))
    print('str1に\'se\'？: {}'.format(is_element('se', str1_char_bigram)))
    print('str2に\'se\'？: {}'.format(is_element('se', str2_char_bigram)))

if __name__ == '__main__':
    main()
```

```console
$ python3 06.py 
文字列(入力): paraparaparadise，paragraph
str1文字バイグラム: ['pa', 'ar', 'ra', 'ap', 'pa', 'ar', 'ra', 'ap', 'pa', 'ar', 'ra', 'ad', 'di', 'is', 'se']
str2文字バイグラム: ['pa', 'ar', 'ra', 'ag', 'gr', 'ra', 'ap', 'ph']
和集合: ['gr', 'pa', 'is', 'ap', 'di', 'ag', 'se', 'ra', 'ad', 'ph', 'ar']
積集合: ['ap', 'ra', 'pa', 'ar']
差集合: ['se', 'is', 'di', 'ad']
str1に'se'？: True
str2に'se'？: False
```

# 問題07 テンプレートによる文生成
> 引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ．さらに，x=12, y="気温", z=22.4として，実行結果を確認せよ．

```python
#!/usr/local/bin python3
# -*- coding: utf-8 -*-

def get_text(x: int, y: str, z: float) -> str:
    """
    x,y,zを受け取って「x時のyはz」という文字列を返す関数

    Parameters
    ----------
    x: int
        時間(？)
    y: str
        zの単位(？)
    z: float
        yの具体的数値(？)

    Return
    ----------
    「x時のyはz」という文字列
    """
    return str(x) + '時の' + y + 'は' + str(z)

def main():
    x = 12
    y = '気温'
    z = 22.4

    text = get_text(x, y, z)
  
    print('x = {}, y = {}, z = {}'.format(x, y, z))
    print('取得文字列: {}'.format(text))

if __name__ == '__main__':
    main()
```

```console
$ python3 07.py 
x = 12, y = 気温, z = 22.4
取得文字列: 12時の気温は22.4
```

(returnのところでもなぜ`format()`を使わなかったのだと記事にまとめながら思ったり．)

# 問題08 暗号文
> 与えられた文字列の各文字を，以下の仕様で変換する関数cipherを実装せよ．
> ・英小文字ならば(219 - 文字コード)の文字に置換
> ・その他の文字はそのまま出力
> この関数を用い，英語のメッセージを暗号化・復号化せよ

```python
#!/usr/local/bin python3
# -*- coding: utf-8 -*-

def cipher(string: str) -> str:
    """
    文字列を受け取り，英語小文字のみを暗号/復号化する関数

    Parameter
    ----------
    string: str
        暗号/復号化対象の文字列

    Return
    ----------
    暗号/復号後の文字列
    """
    return ''.join(chr(219 - ord(c)) if c.islower() else c for c in string)

def main():
    string = "I couldn't imagine how my program would work when I was writing it."

    enc_string = cipher(string)
    dec_string = cipher(enc_string)
  
    print('入力文字列: {}'.format(string))
    print('文字列(暗号化後): {}'.format(enc_string))
    print('文字列(復号化後): {}'.format(dec_string))

if __name__ == '__main__':
    main()
```

```console
$ python3 08.py 
入力文字列: I couldn't imagine how my program would work when I was writing it.
文字列(暗号化後): I xlfowm'g rnztrmv sld nb kiltizn dlfow dlip dsvm I dzh dirgrmt rg.
文字列(復号化後): I couldn't imagine how my program would work when I was writing it.
```

# 問題09 Typoglycemia
> スペースで区切られた単語列に対して，各単語の先頭と末尾の文字は残し，それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ．ただし，長さが４以下の単語は並び替えないこととする．適当な英語の文（例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."）を与え，その実行結果を確認せよ．

```python
#!/usr/local/bin python3
# -*- coding: utf-8 -*-

import random

def get_typoglycemia_text(string: str, delimiter: str) -> str:
    """
    文字列を受け取りタイポグリセミアなテキストを生成する関数．
    (4文字以内の単語は並び替えない)

    Parameters
    ----------
    string: str
        ターゲットの文字列
    delimiter: str
        デリミタ文字

    Return
    ----------
    変換後の文字列
    """
    split_str = string.split(delimiter)

    return ' '.join([word[0] + ''.join(random.sample(word[1:-1], len(word[1:-1]))) + word[-1] if len(word) > 4 else word for word in split_str])

def main():
    string = 'I couldn\'t believe that I could actually understand what I was reading : the phenomenal power of the human mind .'
    delimiter = ' '

    conv_string = get_typoglycemia_text(string, delimiter)
  
    print('入力文字列: {}'.format(string))
    print('文字列(変換後): {}'.format(conv_string))

if __name__ == '__main__':
    main()
```

```console
$ python3 09.py 
入力文字列: I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .
文字列(変換後): I cuoldn't belveie that I cuold alauctly unestdanrd what I was randieg : the peanohemnl peowr of the human mind .
```

(よくよく考えたらreturnするときデリミタ文字でjointしないと意味ないやんと気付いたり)