20210128のPythonに関する記事は30件です。

[Python][moto] moto でモック起動中に requests がエラーになる場合の解決策

概要

moto でモック起動中に requests を使うとエラーが発生する。

  • moto のバージョン
$ pip show moto | grep Version
Version: 1.3.16
  • サンプルコード
import requests
from moto import mock_s3

with mock_s3():
    res = requests.get("https://example.com")
    print(res.status_code)
  • サンプルコード実行時のエラー
$ python test.py 
Traceback (most recent call last):
  File "test.py", line 5, in <module>
    requests.get("https://example.com")
  File "/home/sidearrow/workspace/py-sandbox/.venv/lib/python3.8/site-packages/requests/api.py", line 76, in get
    return request('get', url, params=params, **kwargs)
  File "/home/sidearrow/workspace/py-sandbox/.venv/lib/python3.8/site-packages/requests/api.py", line 61, in request
    return session.request(method=method, url=url, **kwargs)
  File "/home/sidearrow/workspace/py-sandbox/.venv/lib/python3.8/site-packages/requests/sessions.py", line 542, in request
    resp = self.send(prep, **send_kwargs)
  File "/home/sidearrow/workspace/py-sandbox/.venv/lib/python3.8/site-packages/requests/sessions.py", line 655, in send
    r = adapter.send(request, **kwargs)
  File "/home/sidearrow/workspace/py-sandbox/.venv/lib/python3.8/site-packages/responses.py", line 733, in unbound_on_send
    return self._on_request(adapter, request, *a, **kwargs)
  File "/home/sidearrow/workspace/py-sandbox/.venv/lib/python3.8/site-packages/responses.py", line 680, in _on_request
    match, match_failed_reasons = self._find_match(request)
TypeError: cannot unpack non-iterable CallbackResponse object

解決法

  • moto のバージョンを 1.3.16.dev122 以降にアップグレードする。
$ pip install moto==1.3.16.dev122

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Visual Studio CodeとPython

概要

WindowsでのPythonの環境構築、Visual Studio Code(以下VSCode)での拡張機能の説明(解析、Notebook、コード整形、デバッグなど)について説明します。

? かごもく #40 開発環境の紹介 - connpass発表用の資料です。


Python環境構築

Python環境構築では、インタプリタ(python)バージョン切替、仮想環境(インタプリタバージョンとパッケージのセット)切替、そしてパッケージ管理に何を選定するかを決める必要があります。


Python環境構築のパターン

Windowsではおおむね以下のパターンとなるかと思います。

  1. Anaconda
  2. Python + venv
  3. Python + WSL2(Ubuntu) + pyenv + venv

? pyenv+venv以外に、pipenv、poetryなどもあります。


Anaconda

  • Windows上にAnacondaをインストール
  • インタプリタ切り替え、仮想環境切り替え、パッケージ管理をまとめて行うことができる
  • 機械学習に関連したライブラリが最適化されている(速度が速い)
  • 公式リポジトリの利用規約は要確認

Python + venv

  • Windows上にPython Windows版をインストール
  • Windows版のPythonはインタプリタ切替のランチャ(py.exe)が標準添付
  • venv等で仮想環境を構築し、pipでパッケージ管理を行う
  • Windowsでは動作しないライブラリもある(例:uvloop)

Python + venv コード例

py -3.6 -m venv .venv
& .\.venv\Scripts\Activate.ps1
python --version
pip install -r requiremtns.txt 

Python + WSL2(Ubuntu) + pyenv + venv

手順

  • Windows上にWSL2でUbuntuをインストールし、Ubuntu上にPython環境を構築
  • pyenvをインストールしてインタプリタのインストールと切り替え
  • venv等で仮想環境を構築し、pipでパッケージ管理を行う
  • wsl2の機能でUbuntu上のファイルをWindowsエクスプローラーで操作できる

Python + WSL2(Ubuntu) + pyenv + venv コード例

pyenv shell 3.6.9
python -m venv .venv
source .venv\bin\activate
python --version
pip install -r requiremtns.txt 

拡張機能

基本的には以下の拡張機能でおおむね問題ないかと思います。

  • Python(ms-python.python)
  • Pylance(ms-python.vscode-pylance)
  • Visual Studio Intelicode(visualstudioexptteam.vscodeintellicode)
  • Jupyter(ms-toolsai.jupyter)
    • Pylanceと同時にインストールのようでした。
  • GitLens(eamodio.gitlens)

主な機能


仮想環境の切り替え

https://code.visualstudio.com/docs/python/environments

プロジェクトフォルダ直下に仮想環境フォルダを作成することで、.pyファイルを開くと仮想環境が自動認識して切り替わります。

? 仮想環境のフォルダ名には.venvが良く使われます。
? 画面の左下で確認できます。

image.png


Jupyter Notebooksのサポート (Jupyter Notebook UI)

https://code.visualstudio.com/docs/python/jupyter-support

Jupyter NotebooksをVSCode上で行うことができます。

  • VSCode上でJupyter NotebookのUIを実行できる
  • 以下のコマンドでJupyter Notebookを作成できる >Create New Blank Jupyter Notebook image.png

実行サンプル

image.png


インタラクティブウインドウ (#%%)

https://code.visualstudio.com/docs/python/jupyter-support-py

誤解を恐れずに言うと、Jupyter Notebooksのような動きを.pyファイルで行うことができます。つまり逐次動作を確認しながら、.pyファイルを作成することができる機能となります。

  • .pyファイルに、#%%を打ち込むことで、PythonファイルをNotebookのようにセルを作って逐次実行することができる。
  • "Shift + Enter "でセルが実行され、結果が表示される

実行サンプル

image.png


文法チェック(リンター)

https://code.visualstudio.com/docs/python/linting

文法チェック、使用していない変数の確認などを行うことができます。

Python: Select Linterを実行して、文法チェックに使用するリンターを選択します。とりあえずは既定のPylintでよいと思われます。

? VSCodeのPythonは、最も多くのPython開発者に対してわかりやすい一連の文法チェックを使用するように既定で構成されていています。

選択したリンターがインストールされていない場合には、リンターが自動でプロジェクトの仮想環境にインストールされます。

実行サンプル

image.png


コード整形(フォーマッター)

https://code.visualstudio.com/docs/python/editing#_formatting

pipでblackをインストールし、FormatのProviderとして設定します。

整形の規約としては、autopep8, black, yapf3種類あります。
blackが一番厳しい規約のため仕事として行うならばblackが望ましいと思われます。(誰でも同じような書式としてフォーマットされる。)

手順

  1. blackをpipでインストールする

    pip install black
    
  2. 設定で有効にする

    image.png

? "ALT + SHIFT + F"で整形できます。


デバッグ

https://code.visualstudio.com/docs/python/debugging

アクティビティバーで実行ビューを選択し、構成の初期化とlaunch.jsonファイルの作成を行い、実行ビューから実行することでデバッグできます。

Django, Flusk, 単体.pyファイルの実行などの構成ファイル(launch.json)が用意されています。

image.png

手順(単体.pyファイル)

? 引数が必要な場合などはlaunch.jsonファイルのカスタマイズが必要です。

  1. デバッグする.pyファイルを開く

    image.png

  2. アクティビティバーで実行ビューを選択し「実行とデバッグ」を実行

    image.png

  3. 「Python File」を選択

    image.png

  4. デバッグできる

    ブレークポイントで止まった時の例
    image.png

    捕捉されなかった例外(Uncaught Exception)が発生したとき
    image.png
    ? tornadoフレームワークでは、tornadoが先に例外を補足するためかブレークしませんでした。

launch.jsonファイル例

現在のプロジェクトディレクトリ(${workspaceFolder}) でmain.pyファイルを起動するために設定された Python インタプリタを使用します。args[]配列にコマンドライン引数を追加したり、env[] 配列を追加して環境変数を追加することもできます。

{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",  
    "configurations": [  
        {  
            "name": "Python: tornado",  
            "type": "python",  
            "request": "launch",  
            "program": "${workspaceFolder}/main.py",  
            "args": [],  
            "console": "integratedTerminal"  
        }  
    ]  
}

テスト

https://code.visualstudio.com/docs/python/testing

Python組み込みのunittest、またはpytestを使用したテストをサポートしています。

テストが検出されると、アクティビティバーのテストエクスプローラーアイコンも表示されます。テストエクスプローラーで、テストを視覚化、移動、および実行することができます。

手順

  1. F1キーからコマンドでPython: Discover Testsを実行

    image.png

  2. フレームワーク有効確認のダイアログが表示されるので選択

    image.png

  3. 使用するフレームワークを選択(基本的にはunittestでOK)

    image.png

  4. テストコードが保存されたフォルダを選択

    image.png

  5. テストコードファイルのファイル名パターンを指定

    image.png

  6. アクティビティバーのテストエクスプローラーにテストが表示される

    image.png

  7. すべての単体テストを実行

    image.png

  8. 単体テストの出力を表示

    image.png

    start
    test_unittest.Test_TestIncrementDecrement.test_decrement
    test_unittest.Test_TestIncrementDecrement.test_increment
    test_decrement (test_unittest.Test_TestIncrementDecrement) ... FAIL
    NoneType: None
    test_increment (test_unittest.Test_TestIncrementDecrement) ... ok
    
    ======================================================================
    FAIL: test_decrement (test_unittest.Test_TestIncrementDecrement)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "c:\Users\fukumori\source\repos\pythontest\test_unittest.py", line 10, in test_decrement
        self.assertEqual(inc_dec.decrement(3), 4)
    AssertionError: 2 != 4
    
    ----------------------------------------------------------------------
    Ran 2 tests in 0.000s
    
    FAILED (failures=1)
    
  9. 「Deubg Test」からデバッグを行う

    image.png


リファクタリング

https://code.visualstudio.com/docs/python/editing#_refactoring

PyCharmに比べると種類は少ないですがリファクタリングがサポートされています。

  • 変数の抽出
  • メソッドの抽出
  • インポートの並べ替え

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モンハン玄人向けのdiscord botを作ってみた

はじめに

見る専だったQiitaですが、先輩に作ったプログラムとか上げとくといいよと言われたので、記録しておこうかと思います。  

しょうもないDiscord botですがもしよかったら見ていってください。

目次

  1. 前提知識と作るきっかけ
  2. botの機能
  3. 実際の動作の様子
  4. コード
  5. 感想

1. 前提知識と作るきっかけ

モンハンとは簡潔に言うと狩りをするゲームである。今までゲームはあまりやってこなかったが、コロナ過で友達に誘われて狩りに手を出してしまった私はあっという間にHRをカンストした。

だが、ここで幸せな狩り生活を送っていた私にじわじわとある問題が迫ってくる。
恐らくどのゲームにも言えることだが、やり込みすぎるとゲーム内でやることが無くなってくるのである。

友人と遊んでも何を狩りに行こうか悩んだり、何の武器を使おうか悩んだり、、、、、

ん、、、?

ランダムで武器とかモンスターとかスタイル1とか返してくれるbot作ればいいのでは!?

そういうわけで14種類の武器、93匹の大型モンスター、6種類のスタイルを特定のコマンドを入力するとランダムでひとつ返してくれる botを作成した。

2. 実際の動作の様子

3.jpg

やったね!
これであまり使ったことない武器やスタイルも堪能できるぞ~!!:relaxed:

4.jpg

エリアルヘビィボウガン…???????????????

3. botの機能

当botには以下の機能を持たせた。

発言内容 ランダムで1つ返すものの内容
「モンスター」 全ての大型モンスターのうちの1匹
「武器」 全ての武器種のうちのひとつ
「スタイル」 全てのスタイルのうちのひとつ
「武器とスタイル」 武器とスタイルを同時に返す
「やばいクエスト」 個人の主観で選んだ難しいクエストのうちのひとつ
発言内容 ランダムじゃない内容とその他
「にゃーん」 「にゃーん」
「アプデ」 最近更新した内容を記載
文中に「レポート」 レポートお疲れ様☕️
文中に「したくない」「やりたくない」 労いの言葉: ランダムで3つ用意
文中に「つかれた」 「お疲れ様!!!」

あと他にも何か入れた気がするが割愛する。

4. コード

作成したコードを以下に記載する。

:random_bot.py
# MHXX bot 作成
import discord
import random as rm

token = 'hogehoge-hogohogo-hogegegege'

client = discord.Client()

# 武器種配列
w = ['大剣', '片手剣', '双剣', '太刀', 'ハンマー', '狩猟笛', 'ランス',
     'ガンランス', 'スラッシュアックス', 'チャージアックス', '操虫棍',
     '弓', 'ライトボウガン', 'ヘビィボウガン']

# スタイル配列
s = ['ギルド', 'ストライカー', 'エリアル', 'ブシドー', 'ブレイブ', 'レンキン']

# モンスター配列
m = ['イャンガルルガ', '隻眼イャンガルルガ', 'イャンクック', 'ゲリョス', 'ドスイーオス',
     'ドスギアノス', 'ドスゲネポス', 'ドスマッカォ', 'ドスランポス', 'ホロロホルル',
     '朧隠ホロロホルル', 'アルバトリオン', 'アマツマガツチ', 'オオナズチ', 'オストガロア',
     'キリン', 'クシャルダオラ', 'シャガルマガラ', 'テオ・テスカトル', 'バルファルク',
     'ラオシャンロン', 'ミラバルカン', 'ミラボレアス', 'ミラルーツ', 'アカムトルム',

     'ウカムルバス', 'グラビモス', 'セルレギオス', 'ティガレックス', '荒鉤爪ティガレックス',
     'ナルガクルガ', '白疾風ナルガクルガ', 'バサルモス', 'フルフル', 'ベリオロス',
     'ライゼクス', '青電主ライゼクス', 'リオレイア', 'リオレイア希少種', '紫毒姫リオレイア',
     'リオレウス', 'リオレウス希少種', '黒炎王リオレウス', 'ディアブロス', '鏖魔ディアブロス',
     'アオアシラ', '紅兜アオアシラ', 'ウルクスス', '大雪主ウルクスス', 'ガムート',

     '銀嶺ガムート', 'ケチャワチャ', 'ドスファンゴ', 'ドドブランゴ', 'ババコンガ',
     'ラージャン', '激昂したラージャン', 'ラングロトラ', 'アトラル・カ', 'アルセルタス',
     'ゲネル・セルタス', 'ジンオウガ', '金雷公ジンオウガ', 'ネルスキュラ', 'ザボアザギル',
     'テツカブラ', '岩穿テツカブラ', 'ガララアジャラ', 'イビルジョー', '怒り喰らうイビルジョー',
     'ウラガンキン', '宝纏ウラガンキン', 'ディノバルド', '燼滅刃ディノバルド', 'ドボルベルク',

     'ブラキディオス', '猛り爆ぜるブラキディオス', 'ボルボロス', 'ショウグンギザミ', '鎧裂ショウグンギザミ',
     'ダイミョウザザミ', '矛砕ダイミョウザザミ', 'アグナコトル', 'タマミツネ', '天眼タマミツネ',
     'ハプルボッカ', 'ラギアクルス', 'ロアルドロス', 'ヴォルガノス', 'ガノトトス',
     'ドスガレオス', 'ゴア・マガラ', '混沌に呻くゴア・マガラ']

# クエスト配列
q = ['「MHヒストリーⅠ」 (イベントクエスト 6/7)', '「MHヒストリーⅡ」 (イベントクエスト 6/7)', '「ドリフターズ・孤島の漂流者 1/7」',
     '「牙狼・闇に堕ちし呀」 (イベントクエスト 2/7)', '「夜空照らすは偽りの月陽」 (イベントクエスト 3/7)', '「飢え渇き生態を蹂躙す」 (イベントクエスト 3/7)',
     '「哭き呻くもの」 (イベントクエスト 4/7)', '「モンハン部・熱血昇段試験」 (イベントクエスト 2/7)', '「絶望の淵の溶岩島」 (イベントクエスト 4/7)',
     '「怒髪天を貫き何処へ往く」 (イベントクエスト 4/7)', '「天彗龍より姉御が怖いぜぃ!」 (イベントクエスト 4/7)', '「千夜一夜の太古の閣」 (イベントクエスト 4/7)',
     '「心を照らす畏怖の光」 (イベントクエスト 4/7)', '「ナイショの霞龍」 (イベントクエスト 4/7)', '「地底火山の炎の王」 (イベントクエスト 4/7)',

     '「奈落からの招待状」 (イベントクエスト 5/7)', '「覇竜との聖戦さ…」 (イベントクエスト 5/7)', '「古の白き神」 (イベントクエスト 5/7)',
     '「老山龍、侵攻中!」 (イベントクエスト 5/7)', '「煌黒はとこしえに」 (イベントクエスト 5/7)', '「黒き伝説との対峙」 (イベントクエスト 5/7)',
     '「伝説との戦い」 (イベントクエスト 5/7)', '「滅びの伝説に挑みし者」 (イベントクエスト 5/7)', '「USJ・霊峰に吹き荒れる嵐」 (イベントクエスト 2/7)',
     '「金と銀がもたらす悲哀」 (G4 8/10)', '「絆の証! 闘技大会の大決戦」 (G4 7/10)', '「狩人達の究道」 (G4 7/10)',
     '「絆の証? 雪山の獰猛大決戦」 (G4 7/10)', '「風薫る密林」 (G4 7/10)', '「狩魂よ砂中に眠れ」 (G4 6/10)',

     '「戦慄の遺群嶺」 (G4 6/10)', '「炎戈竜は地底で吠える」 (G4 5/10)', '「感動の生まれる瞬間」 (G4 3/10)',
     '「戈と槌は相容れず」 (G4 3/7)', '「【特殊許可】鏖魔狩猟依頼G4」', '「【特殊許可】鏖魔狩猟依頼G5」',
     '「【超特殊許可】鏖魔狩猟依頼」', '「【特殊許可】紅兜狩猟依頼G5」', '「【特殊許可】大雪主狩猟依頼G5」',
     '「【特殊許可】紫毒姫狩猟依頼G5」', '「【特殊許可】白疾風狩猟依頼G5」', '「【特殊許可】宝纏狩猟依頼G5」',
     '「【特殊許可】隻眼狩猟依頼G5」', '「【特殊許可】黒炎王狩猟依頼G5」', '「【特殊許可】金雷公狩猟依頼G5」',

     '「【特殊許可】荒鉤爪猟依頼G5」', '「【特殊許可】燼滅刃狩猟依頼G5」', '「【特殊許可】朧隠狩猟依頼G5」',
     '「【特殊許可】鎧裂狩猟依頼G5」', '「【特殊許可】天眼狩猟依頼G5」', '「【特殊許可】静電主狩猟依頼G5」',
     '「【特殊許可】銀嶺狩猟依頼G5」', '「【超特殊許可】紅兜狩猟依頼」', '「【超特殊許可】大雪主狩猟依頼」',
     '「【超特殊許可】矛砕狩猟依頼」', '「【超特殊許可】紫毒姫狩猟依頼」', '「【超特殊許可】岩穿狩猟依頼」',
     '「【超特殊許可】白疾風狩猟依頼」', '「【超特殊許可】宝纏狩猟依頼」', '「【超特殊許可】隻眼狩猟依頼」',

     '「【超特殊許可】黒炎王狩猟依頼」', '「【超特殊許可】金雷公狩猟依頼」', '「【超特殊許可】荒鉤爪狩猟依頼」',
     '「【超特殊許可】矛砕狩猟依頼」', '「【超特殊許可】紫毒姫狩猟依頼」', '「【超特殊許可】岩穿狩猟依頼」',
     '「【超特殊許可】燼滅刃狩猟依頼」', '「【超特殊許可】朧隠狩猟依頼」', '「【超特殊許可】鎧裂狩猟依頼」',
     '「【超特殊許可】天眼狩猟依頼」', '「【超特殊許可】静電主狩猟依頼」', '「【超特殊許可】銀嶺狩猟依頼」', ]

# クエスト配列2
q2 = ['「MHヒストリーⅠ」 (イベントクエスト 6/7)', '「MHヒストリーⅡ」 (イベントクエスト 6/7)', '「ドリフターズ・孤島の漂流者 1/7」',
      '「牙狼・闇に堕ちし呀」 (イベントクエスト 2/7)', '「夜空照らすは偽りの月陽」 (イベントクエスト 3/7)', '「飢え渇き生態を蹂躙す」 (イベントクエスト 3/7)',
      '「哭き呻くもの」 (イベントクエスト 4/7)', '「モンハン部・熱血昇段試験」 (イベントクエスト 2/7)', '「絶望の淵の溶岩島」 (イベントクエスト 4/7)',
      '「怒髪天を貫き何処へ往く」 (イベントクエスト 4/7)', '「天彗龍より姉御が怖いぜぃ!」 (イベントクエスト 4/7)',
      '「心を照らす畏怖の光」 (イベントクエスト 4/7)', '「ナイショの霞龍」 (イベントクエスト 4/7)', '「地底火山の炎の王」 (イベントクエスト 4/7)',

      '「金と銀がもたらす悲哀」 (G4 8/10)', '「絆の証! 闘技大会の大決戦」 (G4 7/10)', '「狩人達の究道」 (G4 7/10)',
      '「絆の証? 雪山の獰猛大決戦」 (G4 7/10)', '「風薫る密林」 (G4 7/10)', '「狩魂よ砂中に眠れ」 (G4 6/10)',

      '「戦慄の遺群嶺」 (G4 6/10)', '「炎戈竜は地底で吠える」 (G4 5/10)', '「感動の生まれる瞬間」 (G4 3/10)',
      '「戈と槌は相容れず」 (G4 3/7)', '「【特殊許可】鏖魔狩猟依頼G4」', '「【特殊許可】鏖魔狩猟依頼G5」',
      '「【超特殊許可】鏖魔狩猟依頼」', '「【特殊許可】紅兜狩猟依頼G5」', '「【特殊許可】大雪主狩猟依頼G5」',
      '「【特殊許可】紫毒姫狩猟依頼G5」', '「【特殊許可】白疾風狩猟依頼G5」', '「【特殊許可】宝纏狩猟依頼G5」',
      '「【特殊許可】隻眼狩猟依頼G5」', '「【特殊許可】黒炎王狩猟依頼G5」', '「【特殊許可】金雷公狩猟依頼G5」',

      '「【特殊許可】荒鉤爪猟依頼G5」', '「【特殊許可】燼滅刃狩猟依頼G5」', '「【特殊許可】朧隠狩猟依頼G5」',
      '「【特殊許可】鎧裂狩猟依頼G5」', '「【特殊許可】天眼狩猟依頼G5」', '「【特殊許可】静電主狩猟依頼G5」',
      '「【特殊許可】銀嶺狩猟依頼G5」', '「【超特殊許可】紅兜狩猟依頼」', '「【超特殊許可】大雪主狩猟依頼」',
      '「【超特殊許可】矛砕狩猟依頼」', '「【超特殊許可】紫毒姫狩猟依頼」', '「【超特殊許可】岩穿狩猟依頼」',
      '「【超特殊許可】白疾風狩猟依頼」', '「【超特殊許可】宝纏狩猟依頼」', '「【超特殊許可】隻眼狩猟依頼」',

      '「【超特殊許可】黒炎王狩猟依頼」', '「【超特殊許可】金雷公狩猟依頼」', '「【超特殊許可】荒鉤爪狩猟依頼」',
      '「【超特殊許可】矛砕狩猟依頼」', '「【超特殊許可】紫毒姫狩猟依頼」', '「【超特殊許可】岩穿狩猟依頼」',
      '「【超特殊許可】燼滅刃狩猟依頼」', '「【超特殊許可】朧隠狩猟依頼」', '「【超特殊許可】鎧裂狩猟依頼」',
      '「【超特殊許可】天眼狩猟依頼」', '「【超特殊許可】静電主狩猟依頼」', '「【超特殊許可】銀嶺狩猟依頼」', ]

q3 = ['閃きへの1ピースを求めて(G2)', '洞窟に潜む影(G2)', '氷河竜・ベリオロス!(G3)', '氷海の素晴らしい氷(G3)', '氷河竜が大発明のカギ?(G3)',
      '氷海の恐怖体験(G3)', '絆の証?雪山の獰猛大決戦(G4)', 'フルフル討伐(闘技大会)']

@client.event
async def on_ready():
    print('ログインしました!')


@client.event
async def on_message(message):
    # 他のbotの発言は無視する
    if message.author.bot:
        return

    # "武器"と入力されたらランダムで 武器 を返す
    if message.content == '武器':
        await message.channel.send('__' + str(w[rm.randint(0, 13)]) + '__')

    # "スタイル"と入力されたらランダムで 武器 を返す
    if message.content == 'スタイル':
        await message.channel.send('__' + str(s[rm.randint(0, 5)]) + '__')

    # "モンスター"と入力されたらランダムで モンスター を返す
    if message.content == 'モンスター':
        await message.channel.send('__' + str(m[rm.randint(0, 92)]) + '__')

    # "やばいクエスト"と入力されたらランダムで クエスト名 を返す
    if message.content == 'やばいクエスト2':
        await message.channel.send('> ' + str(q[rm.randint(0, 71)]))

    # "やばいクエスト!"と入力されたらランダムで クエスト名(ボス級の古龍・禁忌モンス以外) を返す
    if message.content == 'やばいクエスト':
        await message.channel.send('> ' + str(q2[rm.randint(0, 61)]))

    # エロいクエスト
    if message.content == 'えろいクエスト':
        await message.channel.send('> ' + str(q3[rm.randint(0, 7)]))

    # "武器とスタイル"と入力されたらランダムで 武器とスタイル を返す
    if message.content == '武器とスタイル':
        await message.channel.send('__' + str(s[rm.randint(0, 5)]) + ' ' + str(w[rm.randint(0, 13)]) + '__')

    # "にゃーん"と入力されたら にゃーん を返す
    if message.content == 'にゃーん':
        await message.channel.send('にゃーん')

    if 'やりたくない' in message.content or 'したくない' in message.content:
        msg = message.author.mention + 'ならできるよ!いつもえらいよ、がんばろ(;_;)/~~~'
        msg1 = message.author.mention + 'がいつも頑張ってるの知ってるよ!もう少しだけがんばろう(>_<)'
        msg2 = 'そんなにつらいのに頑張ってて偉いね!終わったらたくさん遊ぼう(*ノωノ)'

        msgall = [msg, msg1, msg2]

        await message.channel.send(str(msgall[rm.randint(0, 2)]))

    if message.content.startswith('レポート'):
        await message.channel.send('レポートお疲れ様☕')

    if 'つかれた' in message.content:
        await message.channel.send('お疲れ様!!!')

    # "隠し機能"と入力したら隠し機能を説明する.
    if message.content == '隠し機能':
        await message.channel.send('**--------------隠し機能--------------**\n'
                                   '「にゃーん」     :「にゃーん」\n'
                                   '文頭に「レポート」  :「レポートお疲れ様☕」\n'
                                   '文中に「やりたくない」: なぐさめ(3パターン)\n'
                                   '文中に「つかれた」  :「お疲れ様!!!」\n'
                                   '「えろいクエスト」  : G級のフルフルが出るクエストor闘技大会\n')

    # "説明"と入力したらbotの説明をする
    if message.content == '説明':
        await message.channel.send('こんにちは!**MHXX_RANDOM** botです!\n'
                                   '様々なものをランダムに返すbotです。\n'
                                   '\n'
                                   '「武器」      :「武器」(No にゃんたー)、\n'
                                   '「スタイル」    :「スタイル」、\n'
                                   '「モンスター」   :「モンスター」(No 小型モンスター)、\n'
                                   '「やばいクエスト2」 : やばそうなクエスト(ボス級古龍と禁忌モンスターあり)\n'
                                   '「やばいクエスト」: やばそうなクエスト(No ボス級古龍と禁忌モンスター)\n'
                                   '「武器とスタイル」 :「スタイル」&「武器」\n'
                                   '\n'
                                   '「アプデ」     :最近の更新情報\n')

    # "アプデ"と入力したら最近更新したbotの変更内容(アプデ)を表示する
    if message.content == 'アプデ':
        await message.channel.send('**--------------アップデート--------------**\n'
                                   '12/20\n'
                                   '・やばいクエストで禁忌ボスが出ないようにしました。出す場合はやばいクエスト2でさせます。'
                                   '12/14\n'
                                   '・細かい調整(**文字の太さ**と__アンダーライン__)'
                                   '・「隠し機能」を追加しました。\n'
                                   '・「やばいクエスト!」を追加しました。(詳細は説明を参照されたい)\n'
                                   '・「武器とスタイル」と入力すると武器とスタイルをまとめて返してくれるようになりました。\n'
                                   '\n'
                                   '12/08\n'
                                   '・アプデ内容を「アプデ」入力で表示できるようになりました。\n'
                                   '・G級の高難易度クエスト(個人の感想)を「やばいクエスト」入力でランダムで表示できるようになりました。\n')


client.run(token)



正直csvファイルとか使えばよかったんだけど、最初は武器とスタイルしか入れない予定だったからコード上に書いて、そっから機能追加してったらこうなりました。いや、途中で変えろよ。

くそコードですがQiitaのためにかえるのがめんどくさかったです。動いてるのでOK、、、

ただ、if文はどうしたらもっとスタイリッシュにできるんだろう、、、
それはめんどくさい以前に分からなかったので、こうするといいよ!っていう書き方があったら是非教えてほしいです!

5. 感想

友人とこれであそんだらばちくそ盛り上がった。マジで楽しい。おすすめ。
pipのダウンロードとか初めてだったから少してこずった。わかりやすく解説してくださっている方がたくさんいて助かった。
好きなことをもっと便利に楽しくすることが簡単にできるなんていい時代ですね。


  1. Monster Hunter XX固有のゲーム要素、[ギルド・ストライカー・ブシドー・エリアル・ブレイブ・レンキン]があり、プレイヤーはひとつのスタイルを選択出来る。その名の通りそれぞれ戦いのスタイルが違って楽しい。詳しくは→Monster Hunter XX公式サイト 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python3 の venv モジュールはどのように仮想化を実現しているのかを調べてみた

Python3では以下のコマンドで仮想環境の構築とアクティベートを行います。
(参考: https://docs.python.org/ja/3.9/library/venv.html#module-venv)

  • 仮想環境の構築
    • python3 -m venv /path/to/new/virtual/environment
  • アクティベート
    • $ source <venv>/bin/activate

Python3.8・mac環境でvenvを実行した際は以下の環境が作られました。

$ tree . -L 5
.
└── venv
    ├── bin
    │   ├── Activate.ps1
    │   ├── activate
    │   ├── activate.csh
    │   ├── activate.fish
    │   ├── easy_install
    │   ├── easy_install-3.8
    │   ├── pip
    │   ├── pip3
    │   ├── pip3.8
    │   ├── python -> python3
    │   └── python3 -> /Library/Frameworks/Python.framework/Versions/3.8/bin/python3
    ├── include
    ├── lib
    │   └── python3.8
    │       └── site-packages
    │           ├── __pycache__
    │           ├── easy_install.py
    │           ├── pip
    │           ├── pip-20.2.1.dist-info
    │           ├── pkg_resources
    │           ├── setuptools
    │           └── setuptools-49.2.1.dist-info
    └── pyvenv.cfg

activate の shell を読んでみる

activate の中で仮想環境を実現していると思われたので覗いてみました。
以下が activate の中身です。

venv_highlight.png

赤枠部分に以下の特徴が見て取れました。

  • 1. VIRTUAL_ENV の環境変数を設定
    • VIRTUAL_ENV=<path to .venv>
    • ※ には私のvevnを構築した際のフルパスが入っていました。
  • 2. コマンドの検索先を$VIRTUAL_ENV/bin:$PATHを最優先にする
    • PATH="$VIRTUAL_ENV/bin:$PATH"
  • 3. 定義してあるPYTHONHOMEの無効化

2.によって Pyhon や pip などのコマンドの向き先を仮想環境内のパスに切り替えているのがわかります。
3.によって PYTHONHOMEを無効化することで、設定されていたモジュールの検索先パスを無効化していることが分かります。

ただこれだけでは仮想化の実現に機能が足りません。
モジュールの検索先パスを仮想環境に切り替えるには何をしているのでしょうか。

モジュールの検索先パスの切り替えの実現方法

Python3はモジュールを検索するパスのリストを持っています。
そのリストが sys.pathです。
(※ 実際は sys.path を参照に到る前にもっと工程があります。 参考:https://docs.python.org/ja/3/reference/import.html#searching)

ところで、sys.path にパスが設定されるのはどこの工程でしょうか。
その疑問を解決してくれている記事を見つけました。
http://hagifoo.hatenablog.com/entry/2013/07/29/132740

しかし、Python2.7の頃の話のようなので、
Python3.8のsite.pyドキュメントを見に行くことにしました。

site.py のドキュメントを読んでみる

https://docs.python.org/ja/3/library/site.html

以下のような説明があります。

site.main() 関数の処理は、前部と後部からなる最大で四つまでのディレクトリを構築するところから始まります。 前部では sys.prefix と sys.exec_prefix を使用します; 空の前部は使われません。 後部では、1つ目は空文字列を使い、2つ目は lib/site-packages (Windows) または lib/pythonX.Y/site-packages (Unix と Macintosh) を使います。 前部-後部の異なる組み合わせごとに、それが存在しているディレクトリを参照しているかどうかを調べ、存在している場合は sys.path へ追加します。 そして、新しく追加されたパスからパス設定ファイルを検索します。

よく分からないので、部分的に見てみましょう。

前部では sys.prefix と sys.exec_prefix を使用します; 空の前部は使われません。

上記よく分からない....

後部では、1つ目は空文字列を使い、2つ目は lib/site-packages (Windows) または lib/pythonX.Y/site-packages (Unix と Macintosh) を使います。

お、 lib/pythonX.Y/site-packages が出てきた!

前部-後部の異なる組み合わせごとに、それが存在しているディレクトリを参照しているかどうかを調べ、存在している場合は sys.path へ追加します。

sys.path へ追加って書いてある!
Python3.8 でもlib/pythonX.Y/site-packagessys.path に追加をしてくれているのは、Lib/site.py のようですね。

ちなみにこのsite.pyのページに、
venvがsystemのsite-packagesを隠蔽することが書かれていました。

"pyvenv.cfg" という名前のファイルが上で挙げたディレクトリの 1 つに存在していた場合、 sys.executable, sys.prefix, sys.exec_prefix にはそのディレクトリが設定され、 site-packages もチェックします (sys.base_prefix と sys.base_exec_prefix は常にインストールされているPythonの "実際の" プレフィックスです)。 (ブートストラップの設定ファイルである) "pyvenv.cfg" で、キー "include-system-site-packages" に "true" (大文字小文字は区別しない) 以外が設定されている場合は、 site-packages を探しにシステムレベルのプレフィックスも見に行きません; そうでない場合は見に行きます。

以上が調べてみた結果でございます。

appendix

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonでhistcounts/histcounts2

Matlabで個人的によく使うhistcoutsとその2変数版のhistcounts2のやりかたメモ。

ヒストグラムをプロットするやり方

”python ヒストグラム” とかでググると
matplotlibのhistやhist2dの情報が見つかって、その返り値からcountをとってくる方法が色々と出てくる。

たとえばこんな感じ。

import numpy as np
import matplotlib.pyplot as plt

x = np.random.normal(loc=0, scale=1, size=1000)
y = np.random.normal(loc=0, scale=2, size=1000)

xEdge = np.arange(-3, 3, 0.1)
yEdge = np.arange(-6, 6, 0.2)

fig = plt.figure()
ax = fig.add_subplot(111)
(cnt, xBin, yBin, img) = ax.hist2d(x, y, bins=[xEdge, yEdge])

これはこれで良いのだけれど(ちなみにax.hist2dの4つめの返り値が何なのかは知らない)
いちいちプロットするのがまどろっこしい…と思っていたのだが、普通にnumpyにあった。

1変数の場合

import numpy as np

x=np.random.normal(loc=0, scale=1, size=1000)
xEdge=np.arange(-3, 3, 0.1)
(cnt, xBin) = np.histogram(x, bins=xEdge)

2変数の場合

import numpy as np

x = np.random.normal(loc=0, scale=1, size=1000)
y = np.random.normal(loc=0, scale=2, size=1000)
xEdge = np.arange(-3,3,0.1)
yEdge = np.arange(-6,6,0.2)
(cnt, xBin, yBin) = np.histogram2d(x, y, bins=[xEdge,yEdge])

これで簡単にcntをz-scoreにしてプロットとかできて捗る。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

絶対パスと相対パス

pythonを使用してExcelファイルの操作を勉強しています。
本日の気づき(復習)は、絶対パスと相対パスに関してです。

複数ブックのセルを取得して一覧を作る

先ずは前提として
デスクトップ上にtestフォルダーを作成し、その中に
スタッフのチェックシートを纏めて保存したbooksフォルダーと
以下のようなプログラムを作ってみました。

User/Desktop/test/read_books.py
from pathlib import Path

from openpyxl import load_workbook, Workbook

wb_new = Workbook()
ws_new = wb_new.active
ws_new.title = '社員一覧表'

ws_new['B2'] = '部署名'
ws_new['C2'] = '氏名'

path = Path('./books')
for i, file in enumerate(path.glob('*.xlsx')):
    wb = load_workbook(file, read_only=True)
    ws = wb['チェックシート']

    row_no = i + 3
    ws_new[f'B{row_no}'] = ws['C2'].value
    ws_new[f'C{row_no}'] = ws['C3'].value

wb_new.save('社員一覧表.xlsx')

こちらで問題なく社員一覧表は作成出来たのですが
ふと、booksフォルダーは、デスクトップ上で保管したいなと思い至り
booksフォルダーをデスクトップ上へ移動させます。
後は記述を変更するだけです。が、どこを変えるのかな??

相対パスを使用する

答えは簡単でした。相対パスを使用します。

User/Desktop/test/read_books.py
# 記述を変更
path = Path('../books')

こちらで問題なく作成できました。
思いがけずスクールの復習もでき
言語が変わっても記述方法が変わるだけで、意外と考え方は変わらないことも再発見できました。
Pythonの標準ライブラリであるpathlibを使うと相対パスを絶対パスに変換のできるみたいですし
色々と勉強が楽しみです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

git レポジトリにあるパッケージを pip install (setup.py-style, pyproject.toml)

setup.py-style

setuptools(デフォルト)を使う場合、以下の形式で install ができます。

pip install "git+protocol://git.example.com/MyProject.git#egg=<project name>&subdirectory=<setup.pyがある階層までのパス>"

パラメータ の egg に関して

egg= is used by pip in its dependency logic to identify the project prior to pip downloading and analyzing the metadata.

pypaのドキュメントの上記説明だと具体的にどのように、何を指定すれば良いか分かりませんでしたが、以下の記事が役立ちました。

egg=(パッケージ名)は setup.py の中にある関数 setup(name=○○○, ...) の引数 name に渡される名称です。

https://qiita.com/tshimura/items/64603dfb8f0d6eb35992#pip-install-%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9

パラメータ の subdirectory に関して

setup.pyがプロジェクトのルートにない場合に使います。

subdirectory を追加して、プロジェクトルートからのsetup.pyがある階層までのパスを指定する必要があります。

The value of the “subdirectory” component should be a path starting from the root of the project to where setup.py is located.

pyproject.toml にビルドツールを指定している場合 (PEP 517)

pyproject.toml にビルドツールを指定して install する場合のドキュメントを見つけられませんでした。しかし、suffix に egg を指定しない記法で install ができました。pipはpyproject.tomlを確認した際は指定のビルドツールを使ってくれるみたいです。

if a project specifies a different build system using a pyproject.toml file, as per PEP 517, pip will use that instead.

by https://pip.pypa.io/en/stable/reference/pip/

If a user wants to explicitly request PEP 517 handling even though a project doesn’t have a pyproject.toml file, this can be done using the --use-pep517 command line option.

by https://pip.pypa.io/en/stable/reference/pip/

pyproject.toml がプロジェクトルートにある場合

install に成功した自作サンプル↓

pip install git+https://github.com/nnashiki/wordcloud-cli

なお、build ツールには poetryを指定しています。

pyproject.toml がプロジェクトルートにない場合

install に成功した自作サンプル↓

pip install git+https://git@github.com/nnashiki/wordcloud_cli_training#subdirectory=wordcloud-cli-for-japanese

なお、build ツールには poetryを指定しています。

参考にしたドキュメント類

pep517 について知りたい場合のリンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

IceCream使い方まとめ

はじめに

下記記事にてIceCreamというライブラリを知り、最近使用しています。

デバッグ時はprintではなく、Icecreamを使うと便利
https://qiita.com/purun/items/c7aca300b970344214cf

IceCreamの機能についての日本語記事が見当たらないので、簡単にまとめました。

内容については基本的には公式READMEを参考にしていますが、
手元での実行結果とドキュメントで挙動が異なるものは実行結果を正として記載しています。
動作確認versionは2.0.0です。

インストール

$ pip install icecream

基本機能

引数ありで実行

ic()に引数を渡すと、渡した引数とその値を出力してくれます。

from icecream import ic

def foo(i):
    return i + 333

ic(foo(123))
ic| foo(123): 456

下記の場合も同様。

d = {'key': {1: 'one'}}
ic(d['key'][1])

class klass():
    attr = 'yep'
ic(klass.attr)
ic| d['key'][1]: 'one'
ic| klass.attr: 'yep'

値を複数渡すこともできます

ic(foo(123), d['key'][1], klass.attr)
ic| foo(123): 456, d['key'][1]: 'one', klass.attr: 'yep'

なお、Python3.8以降であれば、f文字列で同様の出力ができるようです。

print(f"{foo(123)=}")
foo(123)=456

ic()を使用した場合と比べ記述が煩雑になりますが、わざわざIceCreamをインストールせずともこちらで十分な場合もあるでしょう。

引数なしで実行

ic()を引数なしで実行した場合は、実行箇所のファイル名、行数、関数名、実行時刻(UTC)を出力してくれます。
これを利用して、処理実行の有無や処理順のチェックを行うことができます。

from icecream import ic

def foo():
    ic()
    first()

    if expression:
        ic()
        second()
    else:
        ic()
        third()

出力:

ic| main.py:4 in foo() at 12:49:18.489
ic| main.py:11 in foo() at 12:49:18.491

これは、下記のようにprint()に自前の引数を渡してデバッグするよりスマートです。

def foo():
    print(0)
    first()

    if expression:
        print(1)
        second()
    else:
        print(2)
        third()

なお、ic()に引数を渡す場合でも、設定によってこれらのコンテキスト情報を出力することができます。(後述)

その他

戻り値

ic() は受け取った引数をそのまま返すため、既存のコードに簡単に挿入できます。

from icecream import ic
a = 6
def half(i):
    return i / 2
b = half(ic(a))
ic| a: 6

文字列を返す

コンソールへの出力を行うかわりに、ic.format() を使用することで文字列を返します。

s = 'sup'
out = ic.format(s)
print(out)
s: 'sup'

出力の無効化/有効化

ic.disable(), ic.enable() を呼ぶことで出力の無効化/有効化を行います。

ic(1)

ic.disable()
ic(2)

ic.enable()
ic(3)

出力

ic| 1: 1
ic| 3: 3

ic.disable() することで、出力が止められます。
なお、disableで止まるのは標準エラーへの出力のみで、ic(),ic.format()からの戻り値はenable時と同様に返却されます。

カスタム出力設定

ic.configureOutput()メソッドを使用して、出力値のカスタマイズができます。

prefix

出力のprefix。デフォルトは"ic|"。

ic.configureOutput(prefix='hello -> ')
ic('world')
hello -> 'world'

関数を渡すこともできます。

def unixTimestamp():
    return '%i |> ' % int(time.time())

ic.configureOutput(prefix=unixTimestamp)
ic('world')
1519185860 |> 'world': 'world'

outputFunction

文字列の出力処理にカスタム関数を渡すことができます。

デフォルトの関数 では、pygmentsによるシンタックスハイライトを行ってstderrへ出力しています。
以下の例では、logging.warning()を行うカスタム関数を渡しています。

def warn(s):
    logging.warning(s)

ic.configureOutput(outputFunction=warn)
ic('eep')
WARNING:root:ic| 'eep': 'eep'

上記の例を見るとわかりますが、ic| 'eep': 'eep'という生成済み文字列を受け取り、
その出力を行う関数を定義できます。

argToStringFunction

値を文字列に変換する関数を定義できます。
デフォルトでは、値はpprint.pformat()で整形して出力されます。

特定の型のオブジェクトに対して出力形式を定義したい場合なんかに便利そうです。

import pprint
from icecream import ic

class Person:
    def __init__(self, name):
        self.name = name

p=Person(name="taro")
ic(p)

def toString(obj):
    if isinstance(obj, Person):
        return obj.name
    return pprint.pformat(obj)

ic.configureOutput(argToStringFunction=toString)
ic(p)

出力:

ic| p: <__main__.Person object at 0x10795fd60>
ic| p: taro

includeContext

引数を渡して実行する時の、ファイル名・行数・親関数名を出力するオプションです。
includeContextをTrueにすることで引数を渡した際もこのコンテクスト情報を出力できます。
引数なしでの実行には影響しません。

from icecream import ic

ic(0)

ic.configureOutput(includeContext=True)
ic(1)

ic.configureOutput(includeContext=False)
ic(2)
ic| arg: 0
ic| main.py:4 in foo()- arg: 1
ic| arg: 2

引数がない時の出力と異なり、時刻情報は出力されません。

from icecream import ic

ic.configureOutput(includeContext=True)
ic()
ic(0)
ic| main.py:5 in <module> at 12:56:38.755
ic| main.py:6 in <module>- 0

※2.0.0時点で設定できないこと

  • 文字列フォーマットのカスタマイズ
    • <prefix> <context> - <arg>: <val> という出力形式は変更できない
  • 引数なし実行時の時刻出力
  • 出力時刻のタイムゾーン変更

ちなみに

Node.js, PHP, Go 等の他言語向けにも同様のライブラリがあるっぽい。
https://github.com/gruns/icecream#icecream-in-other-languages

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

協調フィルタリング

はじめに

今回は協調フィルタリング(アイテムベース/ユーザーベース)に関してまとめました。レコメンドシステムの種類に関しては、こちらの記事を参考にしていただければと思います。

協調フィルタリングとは

協調フィルタリングは、複数のユーザーの評価を元に推薦するアイテムを決定する手法です。ここでいう評価には、明示的な評価(5starなどユーザーがつける評価)と、暗黙的な評価(ユーザーの行動履歴から導かれる評価)があります。

協調フィルタリングにはアイテムベースとユーザーベースという2種類があり、それぞれ以下のような推薦の仕方をします。

アイテムベース: ユーザーが過去に好んだアイテムと類似度が高いアイテムを推薦
ユーザーベース: 嗜好の似ているユーザーが好むアイテムを推薦

協調フィルタリングの特徴

  • ユーザーの嗜好(行動履歴や評価)が反映されるので、コンテンツベースの推薦手法よりもパーソナライズされる
  • 評価値行列さえあればドメイン知識がなくても推薦できる
  • 新しい出会いの可能性がある(セレンディピティ
  • ユーザー数の多いシステムでないと有用な結果が得られない
  • 評価値行列を元に推薦を行うので、新規ユーザーや新商品へのレコメンドが上手く行えない(コールドスタート問題

アイテムベースの協調フィルタリング

まずは以下の例を使ってアイテムベースの協調フィルタリングをみてみます。

オランダ スイス アメリカ 韓国
対象者 5 0 0 0
user1 4 4 0 0
user2 1 0 0 0
user3 0 2 3 2
user4 4 3 1 0
user5 1 0 4 5

6人のユーザーが旅行した4都市を、1〜5で評価しているとします(0は未評価とします)。
対象者はオランダのみ評価しており、残りの3つのうち次に対象者に推薦する旅行先を決めるというシチュエーションとします。推薦する旅行先は、他の旅行者1〜5の評価データを用いて決定します。

具体的には、評価データから各都市とオランダの類似度を求め、オランダに近い国を対象者に推薦します(対象者はオランダに最高評価5をつけているため)。この時類似度を求める方法はいくつかあるのですが、今回はコサイン類似度を用います。

$$\cos({x}, {y}) = \frac{{x} \cdot {y}}{|{x}| |{y}|}$$
これがコサイン類似度の計算式なので、試しにオランダとスイスの類似度を計算してみます。

$x = (4, 1, 0, 4, 1)$と$y = (4, 0, 2, 3, 0)$より、
$x \cdot y = 4 \cdot 4 + 4 \cdot 3 = 28 $
$|x| = \sqrt{16 + 1 + 16 + 1} = \sqrt{34}$
$|y| = \sqrt{16 + 4 + 9} = \sqrt{29}$

$$\cos({x}, {y}) = \frac{28}{{\sqrt{34}} {\sqrt{29}}} = 0.8917$$

したがって、オランダとスイスの類似度は0.89です。
他の国に関しても計算してみます。

コード
import numpy as np

def cos_sim(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

NL = [4,1,0,4,1]
CH = [4,0,2,3,0]
US = [0,0,3,1,4]
KR = [0,0,2,0,5]

print("オランダとオランダの類似度:" , cos_sim(NL, NL) )
print("オランダとスイスの類似度:", cos_sim(NL, CH))
print("オランダとアメリカの類似度:", cos_sim(NL, US))
print("オランダと韓国の類似度:", cos_sim(NL, KR))
実行結果
オランダとオランダの類似度: 1.0
オランダとスイスの類似度: 0.8917016574178748
オランダとアメリカの類似度: 0.269069117598525
オランダと韓国の類似度: 0.1592324388246205

オランダ自身の類似度は1になります。
計算結果からオランダとスイスの類似度が高いことが分かったので、対象者に次の旅行先に推薦するならスイスが良さそうだということが分かります。

ユーザーベースの協調フィルタリング

続いてユーザーベースの協調フィルタリングに関してみていきます。

itemA itemB itemC itemD itemE
対象者 5 3 4 4 ?
user1 3 1 2 3 3
user2 4 3 4 3 5
user3 3 3 1 5 4
user4 1 5 5 2 1

アイテムベースの協調フィルタリングではアイテムの類似度を計算しましたが、今度はユーザーの類似度を計算します。コサイン関数ではなく、ピアソン相関係数を使って類似度を求めます。

$$
r = \frac{\sum{(x-\bar{x})(y-\bar{y})}}{\sqrt{\sum(x-\bar{x})^2}\sqrt{(y-\bar{y})^2}}
$$

相関係数の計算方法はいくつかありますが、今回はscipyのpearsonrを使って計算しました。
この記事にピアソン相関係数の色んな求め方が書かれていたので参考までに。
https://www.st-hakky-blog.com/entry/2018/01/30/004659

コード
from scipy.stats import pearsonr

target = [5,3,4,4]
user1 = [3,1,2,3]
user2 = [4,3,4,3]
user3 = [3,3,1,5]
user4 = [1,5,5,2]

print("targetとuser1の相関係数:", pearsonr(target, user1))
print("targetとuser2の相関係数:", pearsonr(target, user2))
print("targetとuser3の相関係数:", pearsonr(target, user3))
print("targetとuser4の相関係数:", pearsonr(target, user4))
実行結果
targetとuser1の相関係数: (0.8528028654224415, 0.14719713457755845)
targetとuser2の相関係数: (0.7071067811865475, 0.29289321881345254)
targetとuser3の相関係数: (0.0, 1.0)
targetとuser4の相関係数: (-0.7921180343813393, 0.20788196561866068)

出力されたデータの右側の値は帰無仮説(無相関)とし設定した場合のp値です。今回は単純に左側のピアソン相関係数を見ていただければと思います。対象者はuser1とuser2と相関があると言えます。

ここで、相関のあるuser1とuser2のitemEに対する評価を用いて加重平均を出し、対象者のitemEへの評価を予測します。

コード
print((0.85*3+0.70*5)/(0.85+0.70))
実行結果
3.9032258064516134

したがって、対象者のitemE対する評価は3.90だと予測できます。
この評価値予測から推薦するのかしないのか、、、という流れになります。

最後に

今回は簡単な例を用いて協調フィルタリングに関してまとめました。レコメンドって勉強すればするほど奥が深いなと感じています。何を目的としてレコメンドを行っているか、KPIを何で置いてるか、扱う商材やユーザーによって手法や閾値も変わってくると思います。次回は実際に企業で使われているユースケース等も参考にまとめてみたいです。

参照

レコメンドアルゴリズムの基礎と「B-dash」におけるシステム構成の紹介
https://www.slideshare.net/takemikami/bdash

【Python】ピアソンの相関係数をいろいろな方法で計算する方法まとめ(SciPy / Numpy / Pandas)
https://www.st-hakky-blog.com/entry/2018/01/30/004659

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

協調フィルタリングとは

はじめに

今回は協調フィルタリング(アイテムベース/ユーザーベース)に関してまとめました。レコメンドシステムの種類に関しては、こちらの記事を参考にしていただければと思います。

協調フィルタリングとは

協調フィルタリングは、複数のユーザーの評価を元に推薦するアイテムを決定する手法です。ここでいう評価には、明示的な評価(5starなどユーザーがつける評価)と、暗黙的な評価(ユーザーの行動履歴から導かれる評価)があります。

協調フィルタリングにはアイテムベースとユーザーベースという2種類があり、それぞれ以下のような推薦の仕方をします。

アイテムベース: ユーザーが過去に好んだアイテムと類似度が高いアイテムを推薦
ユーザーベース: 嗜好の似ているユーザーが好むアイテムを推薦

協調フィルタリングの特徴

  • ユーザーの嗜好(行動履歴や評価)が反映されるので、コンテンツベースの推薦手法よりもパーソナライズされる
  • 評価値行列さえあればドメイン知識がなくても推薦できる
  • 新しい出会いの可能性がある(セレンディピティ
  • ユーザー数の多いシステムでないと有用な結果が得られない
  • 評価値行列を元に推薦を行うので、新規ユーザーや新商品へのレコメンドが上手く行えない(コールドスタート問題

アイテムベースの協調フィルタリング

まずは以下の例を使ってアイテムベースの協調フィルタリングをみてみます。

オランダ スイス アメリカ 韓国
対象者 5 0 0 0
user1 4 4 0 0
user2 1 0 0 0
user3 0 2 3 2
user4 4 3 1 0
user5 1 0 4 5

6人のユーザーが旅行した4都市を、1〜5で評価しているとします(0は未評価とします)。
対象者はオランダのみ評価しており、残りの3つのうち次に対象者に推薦する旅行先を決めるというシチュエーションとします。推薦する旅行先は、他の旅行者1〜5の評価データを用いて決定します。

具体的には、評価データから各都市とオランダの類似度を求め、オランダに近い国を対象者に推薦します(対象者はオランダに最高評価5をつけているため)。この時類似度を求める方法はいくつかあるのですが、今回はコサイン類似度を用います。

$$\cos({x}, {y}) = \frac{{x} \cdot {y}}{|{x}| |{y}|}$$
これがコサイン類似度の計算式なので、試しにオランダとスイスの類似度を計算してみます。

$x = (4, 1, 0, 4, 1)$と$y = (4, 0, 2, 3, 0)$より、
$x \cdot y = 4 \cdot 4 + 4 \cdot 3 = 28 $
$|x| = \sqrt{16 + 1 + 16 + 1} = \sqrt{34}$
$|y| = \sqrt{16 + 4 + 9} = \sqrt{29}$

$$\cos({x}, {y}) = \frac{28}{{\sqrt{34}} {\sqrt{29}}} = 0.8917$$

したがって、オランダとスイスの類似度は0.89です。
他の国に関しても計算してみます。

コード
import numpy as np

def cos_sim(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

NL = [4,1,0,4,1]
CH = [4,0,2,3,0]
US = [0,0,3,1,4]
KR = [0,0,2,0,5]

print("オランダとオランダの類似度:" , cos_sim(NL, NL) )
print("オランダとスイスの類似度:", cos_sim(NL, CH))
print("オランダとアメリカの類似度:", cos_sim(NL, US))
print("オランダと韓国の類似度:", cos_sim(NL, KR))
実行結果
オランダとオランダの類似度: 1.0
オランダとスイスの類似度: 0.8917016574178748
オランダとアメリカの類似度: 0.269069117598525
オランダと韓国の類似度: 0.1592324388246205

オランダ自身の類似度は1になります。
計算結果からオランダとスイスの類似度が高いことが分かったので、対象者に次の旅行先に推薦するならスイスが良さそうだということが分かります。

ユーザーベースの協調フィルタリング

続いてユーザーベースの協調フィルタリングに関してみていきます。

itemA itemB itemC itemD itemE
対象者 5 3 4 4 ?
user1 3 1 2 3 3
user2 4 3 4 3 5
user3 3 3 1 5 4
user4 1 5 5 2 1

アイテムベースの協調フィルタリングではアイテムの類似度を計算しましたが、今度はユーザーの類似度を計算します。コサイン関数ではなく、ピアソン相関係数を使って類似度を求めます。

$$
r = \frac{\sum{(x-\bar{x})(y-\bar{y})}}{\sqrt{\sum(x-\bar{x})^2}\sqrt{(y-\bar{y})^2}}
$$

相関係数の計算方法はいくつかありますが、今回はscipyのpearsonrを使って計算しました。
この記事にピアソン相関係数の色んな求め方が書かれていたので参考までに。
https://www.st-hakky-blog.com/entry/2018/01/30/004659

コード
from scipy.stats import pearsonr

target = [5,3,4,4]
user1 = [3,1,2,3]
user2 = [4,3,4,3]
user3 = [3,3,1,5]
user4 = [1,5,5,2]

print("targetとuser1の相関係数:", pearsonr(target, user1))
print("targetとuser2の相関係数:", pearsonr(target, user2))
print("targetとuser3の相関係数:", pearsonr(target, user3))
print("targetとuser4の相関係数:", pearsonr(target, user4))
実行結果
targetとuser1の相関係数: (0.8528028654224415, 0.14719713457755845)
targetとuser2の相関係数: (0.7071067811865475, 0.29289321881345254)
targetとuser3の相関係数: (0.0, 1.0)
targetとuser4の相関係数: (-0.7921180343813393, 0.20788196561866068)

出力されたデータの右側の値は帰無仮説(無相関)とし設定した場合のp値です。今回は単純に左側のピアソン相関係数を見ていただければと思います。対象者はuser1とuser2と相関があると言えます。

ここで、相関のあるuser1とuser2のitemEに対する評価を用いて加重平均を出し、対象者のitemEへの評価を予測します。

コード
print((0.85*3+0.70*5)/(0.85+0.70))
実行結果
3.9032258064516134

したがって、対象者のitemE対する評価は3.90だと予測できます。
この評価値予測から推薦するのかしないのか、、、という流れになります。

最後に

今回は簡単な例を用いて協調フィルタリングに関してまとめました。レコメンドって勉強すればするほど奥が深いなと感じています。何を目的としてレコメンドを行っているか、KPIを何で置いてるか、扱う商材やユーザーによって手法や閾値も変わってくると思います。次回は実際に企業で使われているユースケース等も参考にまとめてみたいです。

参照

レコメンドアルゴリズムの基礎と「B-dash」におけるシステム構成の紹介
https://www.slideshare.net/takemikami/bdash

【Python】ピアソンの相関係数をいろいろな方法で計算する方法まとめ(SciPy / Numpy / Pandas)
https://www.st-hakky-blog.com/entry/2018/01/30/004659

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Raspberry PiでWi-Fiログを収集し、入退室管理を自動化するシステムを作ってみる。

研究室入退室管理システムについて

新型コロナウイルスの感染拡大の影響によって、僕の所属している研究室では滞在していた時間をGoogleフォームで送信しなければなりません。
入力項目は、「氏名・学籍番号・時刻」なのですが、学籍番号と時刻の入力がめちゃくちゃ手間です。
学籍番号は11桁の数字、時刻は2020-10-19 13:00~20:00のように入力しなければなりません。
しかも、この報告みんな忘れがちで先生からお怒りのメールが届いたりしました。

そこで、研究室に滞在していた時間を自動で記録し、「氏名・学籍番号・時刻」を事前入力したGoogleフォームのURLをLINEに送信してくれるシステムを開発してみました。これでもう怒られることはありません。

研究室に入室した場合、その日の21時になるとこんな感じで送られてきます。

googleform送信.png

「本当に楽。便利。」って一人で感動してました。

システム全体像

システムの全体像は以下のようになっています。
RAS全体像.png

  • パケット監視ノード
    • Raspberry Pi 3B+にBUFFALO製のWi-Fiアダプタを接続した構成になっています。
    • パケットキャプチャを行い、パケット解析によって誰が何時に滞在していたかを取得します。
  • サーバ
    • MacでMySQLを動作させて、ユーザや入退室情報の管理を行っています。

サーバの機能

サーバには、

  • ユーザ情報や各ユーザの入退室情報を管理する
  • 21時に各ユーザのスマートフォンへ通知する

といった機能を実装しています。
MySQLによってデータを管理し、LINE Notifyを利用して通知を行っています。

データの管理

データの管理では、ユーザの情報を管理するテーブルと入退室の記録を管理するテーブルを利用しています。
(データベースの設計についても詳しくないので、詳しい方教えてください。。。)

userテーブル

ユーザの情報を管理するテーブル(user)は以下のようになっています。

id student_id name full_name token mac_addr
1 23456789111 yamada 山田花太郎 fasdijfoasjJFioasdj aa:aa:aa:aa:aa:aa
  • id : 自動的に連番で振られるユニークなID
  • student_id : 学籍番号 (Googleフォームに埋め込む)
  • name : ユーザ名
  • full_name : フルネーム (Googleフォームに埋め込む)
  • token : LINE Notifyのトークン
  • mac_addr : スマートフォンのMACアドレス

なかなかクリティカルな情報を保持しているのがuserテーブルです。
研究でブロックチェーン使っているので、今後はブロックチェーンにデータ記録したいなあなんて思ってます。

accessテーブル

各ユーザの入退室記録を管理するテーブル(access)は以下のようになっています。

id date entry_time exit_time
1 2020-11-09 2020-11-09 13:33:01 2020-11-09 20:53:02
  • id : userテーブルのIDと連携する
  • student_id : 学籍番号 
  • entry_time : 入室時間
  • exit_time : 退室時間

なぜ、dateが必要なのか忘れました。これがaccessテーブルです。

MySQLの操作(Python)

Pythonのクラスとしてデータベースに対する操作をまとめました。
いくつか例を載せておきます。

src/pi_src/db_func.py
class db_func:

    def __init__(self):

        # コネクションの作成
        self.conn = mydb.connect()

        # コネクションが切れた時に再接続してくれるよう設定
        self.conn.ping(reconnect=True)

        # 接続できているかどうか確認
        try:
            print(self.conn.is_connected())
        except mydb.Error as err:
            print("Something went wrong: {}".format(err))

    def create_user(self, student_id, user_name, full_name, token, mac_addr):
        try:
            cur = self.conn.cursor()
            # クエリを作成
            query = """
                INSERT INTO user (student_id, name, full_name, token, mac_addr) 
                VALUES ('{0}','{1}','{2}','{3}', '{4}');
                """.format(student_id, user_name, full_name,token, mac_addr)

            print("")
            print("ユーザの作成")
            print("")

            # 実行&コミット
            cur.execute(query)
            self.conn.commit()

        except mydb.Error as err:
            self.conn.rollback()
            return False

    def get_access_info(self, field_name, wh_field, value):
        # DB操作用にカーソルを作成
        cur = self.conn.cursor()

        query = "SELECT {0} FROM access where {1} = '{2}';".format(
            field_name, wh_field, value
        )

        cur.execute(query)
        get_value = cur.fetchall()

        # 取得した値を返す
        return get_value

このような感じで必要なクエリをたくさん定義したクラスを用意してデータベースの操作を行っています。

パケット監視ノードの機能

パケットキャプチャできるようにする

Wi-Fiアダプタを使用したパケットキャプチャについての詳細はこちらを参照してください。

パケットキャプチャの詳細

パケットキャプチャはtcpdumpを使用して行います。
パケットの取得についてですが、そのままキャプチャしてしまうと膨大なパケット量になるため、
今回は必要なパケットのみを取得するために以下のようなフィルタリングを行っています。

実際に使用しているtcpdumpコマンド
sudo tcpdump -w ~/pcap/wlan-%F-%T.pcap -G 180 -i wlan0 type data subtype null -s 42

-w : 書き込みファイル名を指定 (%F=2020-01-15 %T=10:14:39 のようなフォーマットになる)
-G : ファイルを何秒で分割するかを指定
-i : どのネットワークインタフェースを対象指定
type : パケットのタイプを指定
subtype : パケットのサブタイプを指定
-s : 取得バイト数の指定

パケットの計測実験を行ったところ、スマートフォンがWi-Fiに接続すると継続的に送信するパケットがあることが判明したため、
今回はそのようなパケットを取得するためのコマンドを採用しました。
(プローブリクエストとかキャプチャしたらもっと効率いいのかな?詳しい方教えてください。。。)

上記コマンドを実行すると、3分毎にパケットキャプチャファイル(pcapファイル)が生成されます。
スクリーンショット 2021-01-28 19.44.32.png

パケットの解析

上記手順でパケットの取得は行えました。解析に移りたいと思います。
解析といっても大したことはしておらず、

  • キャプチャしたパケットから送信元/送信先MACアドレスを取り出す
  • userテーブルに記録されているMACアドレスと一致するかどうか検索
  • 一致した場合、そのユーザとパケットの到着時間を取得

といった流れを実装しました。
scapyというPythonで書かれたパケット解析のライブラリを使用しています。WireSharkより詳細な解析が可能なのが特徴です。
以下、解析部分のソースコードです。

src/pi_src/wlan_pcap.py
from scapy.all import *
class wlan_pcap:
    def __init__(self):
        # データベースへの接続
        self.db = db_func.db_func()

    def pcap_reader(self, filename, output_file):
        # MACアドレスをキーとした辞書を作成する
        access_data = {}

        # MACアドレスのリストを取得
        column_name = "mac_addr"
        table_name = "user"
        addr_list = self.get_addr_list(column_name, table_name)

        try:
            for packet in PcapReader(filename):
                # 802.11レイヤーのパケットのみを処理
                if packet.haslayer(Dot11) and packet.type==2: # タイプ2 = Data Frames
                    # flagの特定部分を抽出
                    DS_flag = packet.FCfield & 0x3
                    # DS(Distribution System)に向かうかどうかを判定
                    toDS = DS_flag & 0x01 != 0
                    fromDS = DS_flag & 0x2 != 0

                    if toDS and not fromDS:
                        # MACアドレスがuserテーブルに登録されているかどうか
                        if packet.addr2 in addr_list:
                            # 送信者(=スマホ)のMACアドレスを格納
                            TA = packet.addr2
                            # パケットの到着時間を取得
                            packet_time = dt.datetime.fromtimestamp(packet.time, dt.timezone(dt.timedelta(hours=9)))

                            access_data[TA] = packet_time                    
                else:
                    pass

     def get_addr_list(self, column_name, table_name):
        # userテーブルから登録されているMACアドレスを取得する
        addr_list = self.db.select(column_name, table_name)
        return addr_list

入退室記録機能

パケットをキャプチャ/解析する機能が実装できたので、それを利用した入退室記録機能の説明に移ります。

入退室記録については、「ユーザのID・日付・入室/退室時刻」を引数としてaccess_manage()を呼び出すことで更新を行います。
以下に関数の中身を載せています。この関数では、src/pi_src/db_func.pyに実装しているaccess_info_manage()を呼び出すことで、accessテーブルを更新しています。何を言っているのか分かりにくいかと思いますが、

  • access_manage.pyのaccess_manage() : 「入室記録を更新する or 退室記録を更新する」を判定してaccess_info_manage()を呼び出す
  • db_func.pyのaccess_info_manage() : MySQLに接続し、accessテーブルを実際に更新する

といった役割分担です。

src/pi_src/access_manage.pyのaccess_manage()
    def access_manage(self, match_user, time_flag=False):
        # クエリに含める情報の設定
        field_name = "*"
        table_name = "access"
        wh_field = "id"
        user_id = match_user["id"]

        # 過去の入退室記録があるかどうか検索
        access_info = self.db.get_where(field_name, table_name, wh_field ,user_id)

        # 入退室時間(=パケット到着時間)を取り出す
        access_time = match_user["time"]

        # 入退室に関する記録がない場合
        if len(access_info) == 0:
            # 初めての入室記録を生成
            self.db.access_info_manage(user_id, today, access_time ,True)
            print("")
            print("入退室記録なし")

            return True

        # 入室記録はあるが日付が変わっている場合
        elif str(date) != str(today):
            # 入室記録の生成
            self.db.access_info_manage(user_id, today, access_time ,True)
            print("")
            print("今日はまだ入室してないと思います。")

            return True

        # 入退室記録があり、退出時間を更新する場合
        else:
            # 退室記録を生成/更新
            self.db.access_info_manage(user_id, today, access_time)
            print("")
            print("退出記録更新")

            return False
src/pi_src/db_func.pyのaccess_info_manage()
def access_info_manage(self, user_id, date, access_time, entry_flag=False):
    cur = self.conn.cursor()

    # flagが「入室」の場合
    if entry_flag:
        # クエリの作成
        query = """
                INSERT INTO access (id, date, entry_time) 
                VALUES ({0}, '{1}', '{2}');
                """.format(user_id, date, access_time)
    else:
        query = """
                UPDATE access SET exit_time = '{0}' 
                WHERE date = '{1}' and id = {2} ;
                """.format(access_time, date, user_id)

    # 実行&コミット
    cur.execute(query)
    self.conn.commit()

入退室記録の更新

上記まで、パケットをキャプチャ/解析し、accessテーブルに記録する機能を実装できました。
それぞれの機能は独立しています。
残りの作業としては、独立した機能をいい感じの流れで呼び出して入退室記録を定期的に更新するものを作る必要があります。

tcpdumpはファイル分割しているので、バックグラウンドで実行し続けておきます。

sudo tcpdump -w ~/pcap/wlan-%F-%T.pcap -G 180 -i wlan0 type data subtype null -s 42 &

以下のような流れで定期更新を行います。

  • 解析対象のpcapファイルを取得
  • パケット解析処理を呼び出す
  • 入退室記録を更新する
  • 解析対象のpcapファイルを取得
src/pi_src/pcap_analyzer.py
class pcap_analyzer:
    def __init__(self):
        # パケット解析ライブラリ
        self.wp = wlan_pcap.wlan_pcap()

        # データベースへの接続
        self.db = db_func.db_func()

        # アクセス管理するライブラリ
        self.am = access_manage.access()

    def main(self):
        # 処理対象のpcapファイルを取得する
        path = "/home/pi/pcap"
        file_list = self.get_pcap(path)
        pcap_file = os.path.join(path, file_list[0])

        # 入退室管理を更新する
        self.access_data = self.wp.pcap_reader(pcap_file, "test")
        if self.access_data != -1:
            self.access_update()
        else:
            print("pcap読み込み時に何らかのエラー発生")

        # 処理対象のpcapファイルを削除する
        self.wp.delete_file(pcap_file)


if __name__ == "__main__":
    pa = pcap_analyzer()
    pa.main()

これを定期的に実行すれば、いい感じに更新できます。
そこで、crontabを利用して3分毎にpcap_analyzer.pyを実行します。

crontab -e

# エディタが開くので、最後の行に以下を追加
*/3 * * * * cd [pcap_analyzer.pyがあるディレクトリ] ; [使用しているpythonのパス] pcap_analyzer.py


## 例 ##
*/3 * * * * cd /home/pi/RAS_src ; /home/pi/python-venv/RAS/bin/python /home/pi/RAS_src/pcap_analyzer.py

これで3分毎に入退室記録を更新するプログラムが実行されるようになりました。

補足ですが、今回はtcpdumpによるパケットキャプチャファイル生成の仕様を利用しています。
tcpdumpでファイルの分割を行っているため、ファイル数が2になった際に、1つ目のファイルへのデータ書き込みが終わったと判断できます。
そのため、ファイル数が2になったら1つ目のファイルを取得してくる関数( get_pcap() )を用意しました。

src/pi_src/pcap_analyzer.pyのget_pcap()
def get_pcap(self, path):
    # tcpdumpの仕様を利用したファイル取得
    file_list = self.wp.get_file(path)
    print(file_list)
    if len(file_list) >= 2:
        return file_list
    else:
        print("既定のpcapfile数を下回ってます。")
        sys.exit(1)

通知機能

最後に、通知機能です。
やっていることは単純で、21時になると

  1. accessテーブルから今日の入室記録があるユーザを検索
  2. 入室記録がある場合、そのユーザへ通知するために必要な情報をuserテーブルから取得
  3. Googleフォームへ事前入力するためにデータを整形してURLを作成
  4. LINE Notifyを使用して各ユーザへURLを送信する

といった機能を持つプログラムを実行しています。
全部載せると長くなるので特徴的な部分だけ以下に載せます。

1,2の処理を終えた後、以下の関数が実行されます。

src/pi_src/notification.py
def form_info_create(self):
    self.form_info = []

    # 入室記録があるユーザ分ループで処理
    for (user, access) in zip(self.user_info, self.access_info):
        # 必要なデータを取得して整形する
        date = str(access[1]) + "%20"
        entry_time = str(access[2])[11:16]
        exit_time = str(access[3])[11:16]

        access_time = date + entry_time + "~" +exit_time
        print(access_time)

        # Googleフォームへ値を埋め込んで送信するために必要なデータを定義
        form_dic = {
                "full_name" : user[1],
                "student_id": user[2],
                "token" : user[3],
                "access_time" : access_time
        }

        self.form_info.append(form_dic)
        print(self.form_info)

    # LINE Notifyを利用する
    self.line.line_push(self.form_info)

最後の行のline_push()はsrc/pi_src/line_func.pyに定義されています。
以下がline_push()です。

src/pi_src/line_func.py
class line_func:
    def __init__(self):
        # LINE通知APIのエンドポイント
        self.line_notify_api = 'https://notify-api.line.me/api/notify'

        # GoogleフォームのURL
        self.url = "https://docs.google.com/forms~~~~~~~~"
        # 入力欄の識別子
        self.entry = {
            "name": 208832475,
            "id": 913668226,
            "time": 1243542068
        }

    def line_push(self, user_list):
        # ユーザごとのURLを保持
        url_list = []

        for user_dic in user_list:
            # ユーザ情報の取得
            full_name = user_dic["full_name"]
            student_id = user_dic["student_id"]
            access_time = user_dic["access_time"]
            token = user_dic["token"]

            # GoogleフォームのURLを生成
            name = "entry.{0}={1}&".format(self.entry["name"], full_name)
            id = "entry.{0}={1}&".format(self.entry["id"], student_id)
            time = "entry.{0}={1}".format(self.entry["time"], access_time)

            url = self.url + name + id + time

            # 各ユーザへ送信する
            headers = {'Authorization': f'Bearer {token}'}
            data = {'message': f'{url}'}
            status = requests.post(self.line_notify_api, headers = headers, data = data)

            print(status)

これで通知を行えるようになったので、先ほどと同じようにcrontabに登録します。
今回は毎日夜21時に実行したいので、以下のようにします。

crontab -e

# エディタが開くので、最後の行に以下を追加
0 21 * * * cd [notification.pyがあるディレクトリ] ; [使用しているpythonのパス] notification.py

これで、入退室記録が21時に届くようになりました。

最後に

入退室管理システムを実装してみました。メルカリさんがWi-Fiログで勤怠管理をしていて、それをヒントに今回はパケットキャプチャベースのものを試作しました。
説明不足・説明が下手な部分があるかと思いますが、ご勘弁ください。指摘等あればコメントください。
ソースコードは公開しています。
https://github.com/is0356xi/Room_Access_System

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PytorchとTensorFlowのコーディングを比較

はじめに

ディープラーニングのモデルを実装するとき、大体の方がPytorchかTensorFlowを使われるかと思います。
私はPytorch派で、時々画像分類とかして遊んでいます。

お好きな方を使えば良いと思うのですが、初学者の方やTensorFlowをいつも使っている人と話すときによく聞くことがあります。

「Pytorch よくわからん」

学習のロジックを具体的に実装出来るPytorchは好きなのですが、TensorFlowをほとんど使ったことがないのでどれくらい違いがあるか把握してませんでした。

ですので、PytorchとTensorFlowを使ったコーディングってどれだけ違うのか確認しました。

動作環境

  • Google colaboratoryで実装、動作確認をしています。
  • 記事投稿時点でのPytorch, tensorflowのバージョン
    • Pytorch:1.7.0+cu101
    • TensorFlow:2.4.0
    • それぞれ以下のコードを実行して確認できます
#Pytorch
import torch
print(torch.__version__)

# TensorFlow
import tensorflow as tf
print(tf.__version__)

コーディングを比較

PytorchとTensorFlowを使ったコーディングを比較するために、同じ問題(MNISTの画像分類問題)を同じやり方(モデル、誤差関数、最適化、、)で実装しました。
また、私自身のコーディングの癖などが出ないように以下で公開されているチュートリアルを参考にしました。

そのままのコードだと、畳み込み層でのパラメータの値や全結合層の数が微妙に違うのでTensorFlowのチュートリアル側に合わせてPytorchのコードを修正しました。

やること

みんな大好きMNIST手書き文字画像分類を行います。
ちょっと細かいかもしれませんが、学習処理だけではなく初めから比べていきます。

パッケージのインポート

  • TensorFlow
import tensorflow as tf
from tensorflow import keras
from keras import datasets
from keras import layers
from keras import models
  • Pytorch
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
from   torchvision import transforms

TensorFlowでは6個、Pytorchでは7個importしました。
「.」で繋げればTensorFlowは「tensorflow」から、Pytorchは「torch」から大体の機能を参照できるので上記のimport数の差に意味はありません。

Tensorflowはkerasが組み込まれていて、kerasからのインポートが多いですね。
layersは結合層や畳み込み層の機能(DenseとかConv2Dとか)を表現したモジュール、
modelsはモデルを表現したモジュールといった感じでしょうか。
Tensorflow + kerasで使われるケースが多いっぽいです。

Pytorchは主にtorchとtorchvisionにモジュールが分かれています
主要なものはtorchをインポートすれば使えると思います。
torchvisionはComputer Vision系のモデルやデータセットが使えるパッケージです。
tensorflowと比べると、torch.optimやtorch.nn.functionalが違うところかと思います。

MNISTデータ読み込み

まずはMNISTデータセットを取得します。

  • TensorFlow
# データセット取得
(train_images, train_labels), (test_images, test_labels) = datasets.mnist.load_data()
# 28X28 の行列へ変換
train_images = train_images.reshape((60000, 28, 28, 1))
test_images = test_images.reshape((10000, 28, 28, 1))
# 値が[0 - 1]の値になるように変換
train_images, test_images = train_images / 255.0, test_images / 255.0
  • pytorch
# データセット取得
train_dataset = torchvision.datasets.MNIST(root="./data", train=True, download=True, transform = transforms.ToTensor())
# データローダ定義
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, shuffle=True)

tensorflow、pytorchどちらもdatasetsモジュールで取得できますね。

tensorflowはtrain、testデータセットをまとめて取得できます。
取得時点の形は(データ数、高さ、幅)なので、
Conv2Dに入力するデータのため(データ数、高さ、幅、チャネル数)にreshapeします。

pytorchはtrain, testは別々で取得です。
テストデータの取得はtorchvision.datasets.MNISTメソッドのtrainパラメータをFALSEに設定します。
pytorchではDataLoaderを定義するところが異なる点ですね。
「batch_size」で1バッチに含めるデータ数を指定できます。
「shuffle」でデータをシャッフルするかを指定できます。
  trainはTrueを指定します。
  test用のデータセットではFalseを指定して再現性があるようにします。

Model定義

いよいよMNISTの画像分類モデル定義部分です。
とても簡単に構成を説明すると、
畳み込み層×3 + プーリング層×2 + 全結合層 + 出力層、となります

  • tensorflow
# model
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1) ))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.Flatten())
model.add(layers.Dense(64, activation="relu"))
model.add(layers.Dense(10, activation="softmax"))

tensorflow + kerasでは、models.Sequential()でインスタンスを作成して、そこにレイヤを追加していく作りです。
レイヤは、Conv2Dが畳み込み層、MaxPooling2Dがプーリング層、Denseが全結合層です。
Flatten()は行列(H × W × CH)のデータを1次元データに整列します
活性化はReLU関数を設定しているようですが、Conv2Dのactivationプロパティとして組み込まれてますね。
model.add(layers.Conv2D(... ))で畳み込み1層分の入出力データのシェイプ、活性化をまとめて追加しています。
それが2層目以降もプーリング層、全結合層と追加される。さらにFlatten()も追加します。
定義は1層毎に追加して各層の実行処理はmodelに任せる感じの実装ですね。

  • Pytorch

Pytorchはまずモデルのクラス定義からです。

class Net(nn.Module):
  def __init__(self):
    super(Net, self).__init__()
    self.conv1 = nn.Conv2d(1, 32, (3, 3))
    self.conv2 = nn.Conv2d(32, 64, (3, 3))
    self.conv3 = nn.Conv2d(64, 64, (3, 3))
    self.fc1 = nn.Linear(576, 64)
    self.fc2 = nn.Linear(64, 10)

  def forward(self, x):
    x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
    x = F.max_pool2d(F.relu(self.conv2(x)), (2, 2))
    x = x.view(-1, self.num_flat_features(x))
    x = F.relu(self.fc1(x))
    x = self.fc2(x)
    return x

  def num_flat_features(self, x):
    size = x.size()[1:]  # all dimensions except the batch dimension
    num_features = 1
    for s in size:
      num_features *= s
    return num_features

net = Net()

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)

torch.nn.Moduleを継承したモデル定義のクラスを作成し、各層の定義や順伝播の処理を作成していきます。この辺りが大きく違いますね。
Define by Run方式のPytorchとDefine and Run方式のTensorflowの違いとも言えるでしょうか。

クラスを定義した後は、Net()のところインスタンス化、誤差関数と最適化関数を定義します。
誤差関数にCrossEntropyLoss(交差エントロピー誤差)、最適化関数にAdamを使います。
Pytorchだとこの辺が個別に提供されてて、自分で選んで使うことになります。
後述しますが、tensorflow + kerasだと、誤差関数と最適化に何使うかはcompile()メソッドで指定してmodelに組み込まれます。
コンポーネントを自分で組み合わせて使うPytorch、なんでもmodelに集約して実行処理はお任せするtensorflow + kerasというイメージですね。

では、各メソッドの定義内容を確認します。

  • コンストラクタ
    • 各層のインスタンスをメンバ変数へ設定します。
      この段階では各層の定義だけなので各層がどんな順で重なっているかはまだ分かりません。
def __init__(self):
  super(Net, self).__init__()
  self.conv1 = nn.Conv2d(1, 32, (3, 3))  #畳み込み層1
  self.conv2 = nn.Conv2d(32, 64, (3, 3)) #畳み込み層2
  self.conv3 = nn.Conv2d(64, 64, (3, 3)) #畳み込み層3 
  self.fc1 = nn.Linear(576, 64) #全結合
  self.fc2 = nn.Linear(64, 10)   #出力層
  • forwardメソッド
    • 順伝播の処理を定義してあります。
    • self.conv1, 2, 3はコンストラクタで定義した畳み込み層のインスタンスが入ったメンバ
    • Fから活性化関数が使えます。
    • ここで、各層の繋がりが定義されます
def forward(self, x):
  x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
  x = F.max_pool2d(F.relu(self.conv2(x)), (2, 2))
  x = F.relu(self.conv3(x))
  x = x.view(-1, self.num_flat_features(x))
  x = F.relu(self.fc1(x))
  x = self.fc2(x)
  return x

tensorflow + kerasではmodelに追加しておけば後はFWにお任せでしたが、Pytorchでは上記のように実装します。
この辺がめんどくさいと感じるか、流れがわかって良いと感じるかは分かれるかなと思います。
あとは、Pytorchの方がデバッグしやすいかなーと思います。自分でバグ混入させることも多いですが、、、

学習処理

  • tensorflow
model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5)

compile、model.fitメソッドで学習処理を実行してくれて簡単ですね。
誤差計算とか、順伝播処理実行とか基本的なことはtensorflowがやってくれます。
使う側は入力データの用意やエポック数などのパラメータを設定するだけ。

  • Pytorch
for epoch in range(5):
  print(f"Epoch {epoch+1}/5")
  for idx, data in enumerate(train_loader, 0):
    inputs, labels = data
    optimizer.zero_grad() # 重み初期化
    output = net(inputs)  # 順伝播
    loss = criterion(output, labels) # 誤差計算
    loss.backward() # 誤差伝播
    optimizer.step()

Pytorchは色々実装しないといけません。
これまで書いてきたように、Pytorchでは各機能がコンポーネントで提供されている状態なので、それらを繋げた一連の学習ループは実装しないといけません。
tensorflowだとfit()メソッドでやってくれてる部分です。
ただ、書籍などで学んだ学習の流れ(データ入力→順伝播→誤差計測→誤差逆伝播)をコードに反映できるので個人的にはこっちの方が好きです。
fitでよくわからんことあるけど学習できる、より、バグコード書くこともあるけど学習の流れを理解して実装出来るという差がありますね。
どちらも一長一短なところはあります。

まとめ

簡単にまとめると、細かく実装するPytorchとなんとなくでも学習が出来るTensorflowという感じでしたね。
個人的にはやはりPytorchが好みですが、状況に応じてお好きな方を選べばよいと思います。
あと、久しぶりにTensorflow + kerasの実装を読みましたが、とても楽ですね。
compile → fitの流れには惹かれるものがあり、ちょっと使ってみようかな。。なんて気も出てきました。

Pytorch、Tensorflow + kerasを使った実装を比べてみましたがいかがでしたでしょうか。
何か参考になることがあればうれしいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

googletrans with google colab

Click here for the article that I used as a reference. The reference code is almost the same.

Googletrans: Free and Unlimited Google translate API for Python

Install the python package googletrans with google colab and split from English txt to English and Japanese txt (and others).

Subtitle files (.srt and .sbv) etc. to google translate and see the translated one, the time code part and counter index are randomly changed to kanji, and the colon (:) is full-width.
It is necessary to perform the process of escaping the translation of such a part and returning it to the text after translating the text.
If you can use the translation function in the procedure of sending back with API, the processing can be programmed on this side, so even if you have never done a text processing program, it is quite so with regular expressions that you have heard, so it seems. I was wondering. I was heavy, but it seems to be quite so, so I will leave a note here as well, with the feeling that I will put out my right hand to a person who is feeling heavy as well, "I think I can do this a little, I do not know."

this QR is URL of this page:

qr-code (1).png

The image is that when you run googletrans on google colab, you can upload the text and download the translated version.

like this. It runs on a google cloud computer, so you don't need a python runtime environment at hand.
https://youtu.be/tEJDsapYFr8
If you want to use local python instead of google colab, please refer to the page linked at the bottom of this article.

About google colab

The package to install has the tkk fix patch (probably uninvestigated) applied, 4.0.0-rc1 did not result in an error.
In version 3.0.0 installed bypip install googletrans

code = unicode (self.RE_TKK.search(r.text).group (1)).replace ('var','')
AttributeError:'NoneType' object has no attribute'group'

Will result in the error. (As of 2021.1.27.)
This problem will often be a problem with Emacs's googletranlete program. Since it will be a chase that it will be corrected according to the token specification change of the service of google translate, this is the method that is temporarily used now, so there will always be changes in the future, so the link at the end Please check the Issue with. There will be updates to the problem at that point and tips such as solutions by volunteers.

If you specify the version on google colab and install the modified googletrans, it's OK. If you have an unversioned package installed, uninstall it, then
Install googletrans with google colab.

google-colab_googletrans.ipynb
pip install googletrans == 4.0.0-rc1

Install and

ipynb (ipython)

google-colab_googletrans.ipynb
from google.colab import files
from googletrans import Translator
import sys

uploaded = files.upload()

filename = ''
for fn in uploaded.keys():
  print('User uploaded file "{name}" with length {length} bytes'.format(
      name=fn, length=len(uploaded[fn])))
  filename = fn

#args= sys.argv
args= [('translate.py'),filename,'>','translated-jp.txt']
if len(args) < 2:
    print('python3 translate.py textfile.txt > output_textfile.txt')
else:
    print('open '+args[1])
    f = open(args[1])
    lines = f.readlines()
    f.close()

    translator = Translator()
    for line in lines:
        translated = translator.translate(line, dest="ja");
        print(line) # Original
        print(translated.text) # translated
        print()
    print('EOF')
    files.download(filename)

github gist (private):
google-colab_googletrans.py

However, there was a problem when I tried it, and after using it for several hours and verifying it, when there was a blank line in the text to be translated, it became IndexErorr: list index out of range.

In other words, the text to be translated is

00:00:00.320,00:00:06.320
welcome all you super amazing hardware addicts
i am so excited to share this project with you

00:00:06.880,00:00:11.920
after we got that letter in from the listener
talking about how they put lineage os

00:00:11.920, 00:00:17.840
on their fire hd tablet i just had to do
it and the kids have loved this change

In such a case, you will get an error if you stumble on the blank line on the 4th line.

0:00:00.320,0:00:06.320

00000.320,00006.320

welcome all you super amazing hardware addicts 

超素晴らしいハードウェア中毒者を歓迎します

i am so excited to share this project with you

このプロジェクトをあなたと共有できることをとてもうれしく思います

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-23-e8018cddf127> in <module>()
     23     translator = Translator()
     24     for line in lines:
---> 25         translated = translator.translate(line, dest="ja");
     26         print(line) # Original
     27         print(translated.text) # Japanese

1 frames
/usr/local/lib/python3.6/dist-packages/googletrans/client.py in <lambda>(part)
    220         # not sure
    221         should_spacing = parsed[1][0][0][3]
--> 222         translated_parts = list(map(lambda part: TranslatedPart(part[0], part[1] if len(part) >= 2 else []), parsed[1][0][0][5]))
    223         translated = (' ' if should_spacing else '').join(map(lambda part: part.text, translated_parts))
    224 

IndexError: list index out of range

But if you fill in the blank lines and then upload

00:00:00.320,00:00:06.320
welcome all you super amazing hardware addicts
i am so excited to share this project with you
00:00:06.880,00:00:11.920
after we got that letter in from the listener
talking about how they put lineage os
00:00:11.920,00:00:17.840
on their fire hd tablet i just had to do
it and the kids have loved this change

It's a simple problem that doesn't cause an error, so I think it will be improved soon.

(Addition) Improvement. (2021-01-28)

Since the process of removing line breaks '\n' and whitespace' ' is not a problem of googletrans at all, it has been improved so that the list passed to googletrans does not include line breaks and whitespace.

ipynb (ipython)

google-colab_googletrans.ipynb
pip install googletrans == 4.0.0-rc1

translate.ipynb

google-colab_googletrans.ipynb
from google.colab import files
from googletrans import Translator
import sys

uploaded = files.upload()

filename = ''
for fn in uploaded.keys():
  print('User uploaded file "{name}" with length {length} bytes'.format(
      name=fn, length=len(uploaded[fn])))
  filename = fn

#args= sys.argv
args= [('translate.py'),filename]
if len(args) < 2:
    print('python3 translate.py textfile.txt output_textfile.txt')
else:
    print('open '+args[1])
    with open(args[1]) as f:
      line = f.readlines() 
    f.close()

    #line_list = []

    line[:] = [l.rstrip('\n') for l in line]
    #for l in line:
    #  line_list.append(l.strip())

    line[:] = [a for a in line if a != '']

    ##print(line)

    translator = Translator()
    f = open(filename, 'w')
    for l in line:
      translated = translator.translate(l, dest="ja");
      print(l) # Original
      f.writelines(l)
      f.write('\n')
      print(translated.text) # dest lang
      f.writelines(translated.text)
      f.write('\n')
      print()
      f.write('\n')
    print('EOF')
    f.close()

    files.download(filename)

github gist google-colab_googletrans.py

Cf. how to remove newline character from a list in python

Cf. list comprehension:
https://realpython.com/lessons/writing-your-first-list-comprehension/

https://youtu.be/WehZC2g-FdU

English to French
https://youtu.be/WlZbQnKOCMk

All of lang list

list
LANGUAGES = {
    'af': 'afrikaans',
    'sq': 'albanian',
    'am': 'amharic',
    'ar': 'arabic',
    'hy': 'armenian',
    'az': 'azerbaijani',
    'eu': 'basque',
    'be': 'belarusian',
    'bn': 'bengali',
    'bs': 'bosnian',
    'bg': 'bulgarian',
    'ca': 'catalan',
    'ceb': 'cebuano',
    'ny': 'chichewa',
    'zh-cn': 'chinese (simplified)',
    'zh-tw': 'chinese (traditional)',
    'co': 'corsican',
    'hr': 'croatian',
    'cs': 'czech',
    'da': 'danish',
    'nl': 'dutch',
    'en': 'english',
    'eo': 'esperanto',
    'et': 'estonian',
    'tl': 'filipino',
    'fi': 'finnish',
    'fr': 'french',
    'fy': 'frisian',
    'gl': 'galician',
    'ka': 'georgian',
    'de': 'german',
    'el': 'greek',
    'gu': 'gujarati',
    'ht': 'haitian creole',
    'ha': 'hausa',
    'haw': 'hawaiian',
    'iw': 'hebrew',
    'he': 'hebrew',
    'hi': 'hindi',
    'hmn': 'hmong',
    'hu': 'hungarian',
    'is': 'icelandic',
    'ig': 'igbo',
    'id': 'indonesian',
    'ga': 'irish',
    'it': 'italian',
    'ja': 'japanese',
    'jw': 'javanese',
    'kn': 'kannada',
    'kk': 'kazakh',
    'km': 'khmer',
    'ko': 'korean',
    'ku': 'kurdish (kurmanji)',
    'ky': 'kyrgyz',
    'lo': 'lao',
    'la': 'latin',
    'lv': 'latvian',
    'lt': 'lithuanian',
    'lb': 'luxembourgish',
    'mk': 'macedonian',
    'mg': 'malagasy',
    'ms': 'malay',
    'ml': 'malayalam',
    'mt': 'maltese',
    'mi': 'maori',
    'mr': 'marathi',
    'mn': 'mongolian',
    'my': 'myanmar (burmese)',
    'ne': 'nepali',
    'no': 'norwegian',
    'or': 'odia',
    'ps': 'pashto',
    'fa': 'persian',
    'pl': 'polish',
    'pt': 'portuguese',
    'pa': 'punjabi',
    'ro': 'romanian',
    'ru': 'russian',
    'sm': 'samoan',
    'gd': 'scots gaelic',
    'sr': 'serbian',
    'st': 'sesotho',
    'sn': 'shona',
    'sd': 'sindhi',
    'si': 'sinhala',
    'sk': 'slovak',
    'sl': 'slovenian',
    'so': 'somali',
    'es': 'spanish',
    'su': 'sundanese',
    'sw': 'swahili',
    'sv': 'swedish',
    'tg': 'tajik',
    'ta': 'tamil',
    'te': 'telugu',
    'th': 'thai',
    'tr': 'turkish',
    'uk': 'ukrainian',
    'ur': 'urdu',
    'ug': 'uyghur',
    'uz': 'uzbek',
    'vi': 'vietnamese',
    'cy': 'welsh',
    'xh': 'xhosa',
    'yi': 'yiddish',
    'yo': 'yoruba',
    'zu': 'zulu',

Reference for tkk error:
stackoverflow.com "googletrans stopped working with error nonetype object has no attribute group"

py-googletrans/issues/234

googletrans with local python

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メタクラスを用いてサブクラスの妥当性検証をする

はじめに

Twitterで一時期流行していた 100 Days Of Code なるものを先日知りました。本記事は、初学者である私が100日の学習を通してどの程度成長できるか記録を残すこと、アウトプットすることを目的とします。誤っている点、読みにくい点多々あると思います。ご指摘いただけると幸いです!

今回学習する教材

  • Effective Python

    • 8章構成
    • 本章216ページ
  • 第4章:メタクラスと属性

サブクラスをメタクラスで検証する

メタクラスとは

メタクラスはクラスより上の概念をぼんやりと意味する言葉であり、単純化すると、Python の class 文に割り込んで、クラスが定義されるたびに特別な振る舞いを与えるものです。
また、Python3 と Python2 で構文が違うようです。今回扱うのは Python3 です。

メタクラスは type からの継承で定義されます。
デフォルトでは、 __new__メソッドで関連するclass文の内容を受け取ります。

class Meta(type):
    def __new__(meta, name, bases, class_dict):
        print((meta, name, bases, class_dict))
        return type.__new__(meta, name, bases, class_dict)

メタクラスを指定するには、metaclass=メタクラス名 とします。

class MyClass(object, metaclass=Meta):
    one = 1

ex = MyClass()

メタクラスは、クラス名、それが継承している親クラス、class本体で定義されているすべてのクラス属性にアクセスできます。

ex = MyClass()
# (<class '__main__.Meta'>, 'MyClass', (<class 'object'>,), {'__module__': '__main__', '__qualname__': 'MyClass', 'one': 1})
print(one)
# 1
print(ex.__qualname__)
# __main__

サブクラスを検証

クラスが正しく定義されているかを検証することがメタクラスの使用法の一つです。
メタクラスは、新たなサブクラスが定義されるたびに妥当性検証(validation)コードを実行することで、正しく定義されているか確認することが可能です。
collections.abcなどによるクラスの妥当性検証は、基本的には__init__で実行されますが、メタクラスの場合は、それより前にエラーを発見できます。
例として、遠足のおやつの値段が300円以内か検証するコードを書きます。

class ValidateSnack(type):
    def __new__(meta, name, bases, class_dict):
        # 抽象Polygonクラスは妥当性検証しない
        if bases != (object,):
            if class_dict['sides'] >= 300:
                raise ValueError('おやつは300円まで!')
        return type.__new__(meta, name, bases, class_dict)

class Snack(object, metaclass=ValidateSnack):
    price = None # サブクラスで規定される


class Umaibo(Snack):
    price = 10

class Butamen(Snack):
    price = 70

class Cake(Snack):
    price = 400

上記のコードを実行すると、300円以上の駄菓子を定義しようとするタイミングで、妥当性検証コードがclass文本体の直後でエラーを発生させます。

Traceback (most recent call last):
  File "33_code.py", line 32, in <module>
    class Cake(Snack):
  File "33_code.py", line 19, in __new__
    raise ValueError('おやつは300円まで!')
ValueError: おやつは300円まで!

他にも、以下のようなことをしたいときにメタクラスを用いるようです。

  • プログラムで型を自動登録する
  • クラスが定義された後で、プロパティを修正したり、注釈を加える
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

__getattr__, ___getattribute__, __setattr__について

はじめに

Twitterで一時期流行していた 100 Days Of Code なるものを先日知りました。本記事は、初学者である私が100日の学習を通してどの程度成長できるか記録を残すこと、アウトプットすることを目的とします。誤っている点、読みにくい点多々あると思います。ご指摘いただけると幸いです!

今回学習する教材

  • Effective Python

    • 8章構成
    • 本章216ページ
  • 第4章:メタクラスと属性

遅延属性には __getattr__, __geatattribute__, __setattr__ を使う

__getattr__メソッド

通常、オブジェクトのインスタンス辞書に属性が見つからない場合は、AttributeError になります。

class Sample(object):
    self.exists = 5

sample = Sample()
print(sample.exists)
print(sample.foo)

実行結果

5
AttributeError: 'Sample' object has no attribute 'foo'

__getattr__は、オブジェクトのインスタンス辞書に属性が見つからないときに呼び出されるメソッドです。
例を見てみます。

class SampleGetattr(object):
    def __init__(self):
        self.exists = 5

    def __getattr__(self, name):
        print('__getattr__メソッドが呼び出されました。')

data = SampleGetattr()
print(data.exists)
data.foo

実行結果

5
# __getattr__メソッドが呼び出されました。

このように、存在しない属性にアクセスしようとすると呼び出されます。
__getattr__メソッドは、遅延アクセス(実際にオブジェクトが必要になったときに初期化すること)したい場合に便利です。

class LazyGetattr(object):
    def __init__(self):
        self.exists = 5

    def __getattr__(self, name):
        print('__getattr__メソッドが呼び出されました。')
        value = 12
        setattr(self, name ,value)    # インスタンス辞書に追加

data = LazyGetattr()
print('Before', data.__dict__)
data.foo
print('After', data.__dict__)
data.foo

実行結果

Before {'exists': 5}
__getattr__メソッドが呼び出されました。
After {'exists': 5, 'foo': 12}

1回目にfooが呼び出されたときにインスタンス辞書に追加し、2回目に呼び出されたときは、既に辞書にあるので`getattr' は呼び出されません。

__getattribute__メソッド

__getattribute__メソッドは、インスタンス辞書にアクセスしようとした属性が存在してもしなくても呼び出されます。

class SampleGetAttribute(object):
    def __init__(self):
        self.exists = 5

    def __getattribute__(self, name):
        print('__getattribute__メソッドが呼び出されました')

data = SampleGetAttribute()
data.exists
data.foo
data.foo
#__getattribute__メソッドが呼び出されました
#__getattribute__メソッドが呼び出されました
#__getattribute__メソッドが呼び出されました

__setattr__メソッド

__setattr__メソッドは属性がインスタンスで代入されるたびに呼び出されます。

class SampleSetattr(object):
    def __init__(self):
        self.exists = 5   
        self.x = 2        

    def __setattr__(self, name, value):
        print('__setattr__メソッドが呼び出されました')
        super().__setattr__(name, value)

data = SampleSetattr()
data.x = 3  
data.y = 4 
__setattr__メソッドが呼び出されました   # self.exists = 5 の初期化時に呼び出された
__setattr__メソッドが呼び出されました   # self.x = 2 の初期化時に呼び出された
__setattr__メソッドが呼び出されました   # data.x = 3 で x に代入されたので呼び出された
__setattr__メソッドが呼び出されました   # 新たな属性に代入されたので呼び出された

__getattribute____setattr__の問題点

これらのメソッドは、オブジェクトのあらゆるアクセスで呼び出されてしまうため、無限ループの原因になります。回避するためには、インスタンス辞書から値を取り出す__getattribute__メソッドでは、super.__getattiribute__を使い、オブジェクトの属性を変更する__setattr__メソッドでは、super.__setattr__を使う必要があります。
親クラスのメソッドを使うことで無限ループに陥らずに済みます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データサイエンスって?

目次

1.はじめに
2.データサイエンスとは
3.データサイエンティストとは
4.必要なスキル
5.参考文献

1. はじめに

皆さん、こんにちは!まさや(@masa__ya8)です!
今回は2020.12.01に大学でデータサイエンスサークル(非公認)(@plot_niigata)を設立し、新歓に向けて記事を書きました。記事に関して間違っている点などありましたら、コメントなどで指摘していただけると嬉しいです!

2. データサイエンスとは

データサイエンスとは、

「データを活用して課題を解決すること」

です。

インターネットの普及やIT・科学技術の発達などにより、ビックデータと呼ばれる膨大なデータも効率よく扱えるようになったことで、近年その注目が高まっています。

データサイエンスの一連のプロセスとしては、

問題設定

データ収集

データ分析

モデル構築・精度検証精度向上の取り組み

課題解決

となります。

ビジネスはもちろん、医療や教育など様々な場面でデータサイエンスは多くの価値を生み出しています。

3. データサイエンティストとは

データサイエンティストとは、データサイエンスを職業として扱う人のことです。
日本では不足しており、需要が高く魅力的な職業と言われています。また、様々なスキルが求められ、統計学やAIに関するスキルが必要不可欠です。

「2.データサイエンスとは」で一連のプロセスを記載しましたが、実際に例を使って説明します。また、この例は「Python実践データ分析100本ノック」の第3,4,5章の内容を使用しています。

「スポーツジムのデータ」

問題設定・・・退会してしまうのを防ぎたい!退会してしまう人はなぜ退会するのか?

データ収集・・・ジム利用履歴や会員データなどを使用する

データ分析・・・値がないデータ(欠損値)などを処理した後、顧客データの基礎集計や顧客行動の各種統計量を把握し、グラフや表にする(可視化)

モデル構築・精度検証精度向上の取り組み・・・決定木というアルゴリズムを用いて、退会予測モデルを作成する。精度向上の取り組みとして、ここではモデルのパラメーターをいじっている(木構造の深さを浅くしている)

課題解決・・・構築したモデルから、どのデータが重要かがわかる。また、データを入力するだけでどの顧客が退会するのかが予測できる


楽天こちら
amazonこちら

4. 必要なスキル

以下は、一般社団法人データサイエンティスト協会が公開している情報をもとに作成した3つのスキルセットです。
スライド1.jpeg

データサイエンス

  • データを最適な形式で集計、可視化

  • 分析結果を正しく読み取る力

  • 機械学習や統計モデルの知識・スキル

など

データエンジニアリング

  • 機械学習などの高度なアルゴリズムの開発・実装

  • データ加工

  • プログラミング(Python, R, SQLなど)

など

ビジネス力

  • 課題抽出・企画提案

  • プレゼンテーション

  • コミュニケーション

など

下記の情報をもとに作成しました。詳しくはこちらをご参照ください。
AI drops -未来につながるAIメディア-
一般社団法人データサイエンティスト協会 スキルチェックリスト(PDF)

5. 参考文献

この記事は以下の情報を参考にして執筆しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kerasで作成した機械学習モデルをcoremltoolsで変換する際のメモ

Kerasベースの機械学習モデル(画像認識用)をiOS上で動かす手法の一つとして、coremltoolsでCoreMLベースに変換する方法があります。
この手続き、言うは易しなんですが、公式ドキュメントとかを注意深く読んでいないと色々ハマりまくるので、気づいた注意点を共有したいと思います。

主な注意点

  • バージョン環境・構築(各FWのバージョンを注意深く合わせましょう。)
  • 変換関数の選択と引数(変換はできるけど動かない・・・などの事態に対する対策です。)

詳しくは以下で説明します。

その1:バージョン・環境

※超重要です。互換性がないバージョンが組み合わさると謎のエラーに苦しめられることになります。実行する前に各バージョン番号をよく確認し、入れ替え作業をしましょう。
※現時点でcoremltools4.X系がリリースされているようです。こちらの確認は追ってやりたいと思っています。本記事では3.X系で試行します。

  • Python v3.6.9 →現時点ではTensorflowがPython3.7以降をサポートしていないようです。
  • Tensorflow v1.14 →coremltools3.4ではTensorfrow2.X系はうまく動きませんでした。(サポートはされているようですが・・・現状は1.X系を強くお勧めです。)
  • Keras v2.3.1
  • coremltools v3.4
  • Google Colab →実行環境。これはローカルのJupyter Notebookなど、何でもいいと思います。

ちなみに変換後のモデルを動作させている環境は以下の通りです。
- iOS14.2
- Xcode 12.0

環境構築

Google Colab上でバージョンを入れ替えます。Google Colabでは"!"を先頭につけることで、コマンド実行できます。
ローカルでやる方は、普通にコマンドライン・ターミナル等で"!"を除去してpipしましょう。

!pip install tensorflow==1.14
!pip install keras==2.3.1
!pip install coremltools==3.4

また、Kerasはtf.kerasとkerasの2通りのimportが存在します。この2つは別物と考えるべきもののようです。
今回はimport kerasのパターンを使用します。
tf.kerasをimportしている場合は、後述のconvertメソッドの書き方が変わるはずです。

import keras

その2:変換関数の選択と引数

いよいよモデル変換処理です。予めKerasで構築・学習済みモデルを変数として用意してください。
(以下の例では、target_model変数が該当します。)

import coremltools

mlmodel = coremltools.converters.keras.convert(target_model,
                                               input_names='image',
                                               image_input_names='image', #重要!
                                               output_names='prediction',
                                               image_scale=1/255) #重要!

ここでの注意点を以下に列挙します。

coremltools.converters.keras.convert関数を使用する

coremltools.convertersモジュールには、kerasの他にtensorflowその他各機械学習FWに対応するモジュールが含まれています。
適切なモジュール・関数を選択しないと、意図通りには動きません。
ここでは、Kerasのモデルなのでcoremltools.converters.keras.convertを選択します。

引数 image_input_namesの指定

この引数を指定することで、CoreML変換後モデルのデフォルトの入力形式が画像になります。
この引数が未指定だと、入力がMultiArrayになり取り扱いが難しくなります。
(指定する値、ここでは'image'はなんでも大丈夫だと思います。)

引数 image_scaleの指定

回帰モデルを作成されている方で、出力値がやたらと大きな数値なって謎・・・という方、これが原因の可能性があります。
画像識別モデルを作成する場合は、画素を標準化するのが一般的かと思いますが、
学習段階でその処理を行なっている場合、coremltoolsで変換する場合も同じスケーリングを指定する必要があります。
これを指定しておかないと、SwiftのVisionフレームワーク等を使用する場合に255スケールで画像入力されてしまうようです。

最後に

以上、タイトル通りメモ書き程度ですが、誰かの役に立てば幸いです。
もし誤りや変な記載があればご指摘いただければと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

私のanaconda重すぎ…?だったのでPipenv使う【Windows10 Python環境構築】

Anacondaの仮想環境起動めんどくさい

初心者向け記事です。
今までWindowsではインストールするだけで簡単だったのでAnaconda使ってたのですが、重すぎでした。
Windows PCが先日お亡くなり?になったタイミングでPipenvで環境構築するようにしました。

Pipenvにするといいこと

雑に特徴を説明。

  • Anacondaより軽い。すぐ起動する
  • ダウンロード用のpipと環境という意味のenvくっついてPipenv。つまりライブラリの管理と仮想環境構築できるってこと
  • ファイル渡せば他の人、PCでもすぐ環境作れる
  • スクリプト便利に使える

Pythonをダウンロード

安定版のstableをDLしておくといいと思います。

https://www.python.org/downloads/windows/

image.png

記事書いたときにはver 3.9出てますが、3.8入れます。
新しいの入れると動かないライブラリとかあるので、新しければいいというわけではない。

image.png

Add Python バージョン Path ってところにチェック入れておくと便利

image.png

一応これ押しておく。

windowsボタンの横の検索窓のところに
cmdって入れるとあの黒い画面出るのでpythonと入力してPython 3.8.7みたいなのがでればOK。exit()で抜けて、pipと入力したら

Usage:
  pip <command> [options]

みたいなのがダーと出てきたらOK

一応Update

pipを最新にしときます。管理者として実行しないとエラーになるはずなので
先ほどと同じようにcmdと打って出てきた表示を
右クリック→管理者として実行をしてください

C:\WINDOWS\system32> pip install --upgrade pip

あとはいらないので閉じてください。

そして、また普通のコマンドプロンプト開く。
※ 最初に開いたコマンドプロンプト開きっぱなしの人は閉じてもう一度起動させる。

pipenv インストール

Pipenvをインストールします

pip install pipenv

インストール終わったら、

pip env

と入力して

Usage: pipenv [OPTIONS] COMMAND [ARGS]...

Options:

みたいなのがいっぱいでたらOK

1. 簡単に使い方説明

Django(WEBアプリのフレームワーク)で初期画面表示させるまでテストとしてやってみます。

プロジェクト保存するフォルダ(今回はdjango)作ってそのフォルダ内でshift押しながら右クリックしてpowershellで起動すると楽に開けます。

pipenv --python 3

こうすればすでに入っている私の場合はpython 3.8で仮想環境が作れます。
pipfileがつくられて、構成が記述されてます。

pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]

[dev-packages]

[requires]
python_version = "3.8"

3.8が入ってますね。

もし、複数のバージョンをすでに入れた場合は

pipenv --python 3.9

みたいに指定することが可能です。

2. インストールする

pipenvで何かを入れるときはpipenv install 〇〇です
django入れてみます。

pipenv install django

バージョンを指定したい場合

詳細に指定したい場合はこんな感じで。

pipenv install django==2.2.16

リポジトリから取得する場合

pipenv install git+https://github.com/<ユーザ>/<リポジトリ>.git@<リビジョン>#egg=<パッケージ名>

3. 仮想環境に入る

仮想環境に入ります。普段はここからやればOK

pipenv shell

あとは普通に開発できます。仮想環境から抜けるときは、

exit

とりあえず環境構築するだけならここで完了です。

実践: Djangoを試してみる

WEBアプリのフレームワーク使ってテストしてみます。
pipenv shellした後に

django-admin startproject testapp .
python manage.py runserver

http://127.0.0.1:8000/

image.png

はい、OK

4. Pipfile,Pipfile.lockで、他の人、PCの環境を一瞬で構築

すでに誰かが環境構築していて、gitとかから取得した場合などに使う。

Pipfileから環境構築する場合

これ使うとPipfile.lockが更新されます。

pipenv install

Pipfile.lockから構築

詳細なバージョンなども合わせて環境構築する場合はpipfile.lock使うこともできます。

pipenv sync

5. Scriptで開発を便利に

スクリプト使うことができます。
たとえば、djangoのサーバー起動するのにpython manage.py runserverとか面倒だし、コマンド覚える必要もあります。

なので

Pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
django = "*"

[dev-packages]

[scripts]
start = "python manage.py runserver"
test = "python manage.py test"

[requires]
python_version = "3.8"

スクリプトを起動

$ pipenv run start

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
January 28, 2021 - 17:57:09
Django version 3.1.5, using settings 'testapp.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

ちゃんとサーバー起動しました。こんな感じで簡単に使えます。

もちろんtestも

$ pipenv run test
System check identified no issues (0 silenced).

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

補足1

開発環境だけでつかうようなものを入れるとき

pipenv install --dev flake8

コードきれい規約の通りキレイにかけてるか見てくれるflake8ですが、本番環境ではいらないのでこんな感じで開発環境のみで使うように入れることも可能。

Pipfile
[dev-packages]
flake8 = "*"

こんな記述が追加されてます。スクリプトに追記してテストしてみます。

[scripts]
start = "python manage.py runserver"
test = "python manage.py test"
lint = "flake8 --show-source ."
$ pipenv run lint
.\testapp\settings.py:89:80: E501 line too long (91 > 79 characters)
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
                                                                               ^
.\testapp\settings.py:92:80: E501 line too long (81 > 79 characters)
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
                                                                               ^
.\testapp\settings.py:95:80: E501 line too long (82 > 79 characters)
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
                                                                               ^
.\testapp\settings.py:98:80: E501 line too long (83 > 79 characters)
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',

Django、一行長すぎってひっかかってるぞ。

まあ、こんな感じで使えます。

開発環境のライブラリも入れる場合

pipenv install --dev

とすれば、今回の場合

[dev-packages]
flake8 = "*"

も入ることになります。
同じようにPipfile.lockからの場合

pipenv sync --dev

補足2 Pathについて

詰まるとしたらPathの登録だと思います。PythonとかpipとかうってもうまくいかないのはPathが通っていないからです。
「ここににゅうりょくして検索」とか書いてあるところにシステム環境変数の編集って入力して環境変数ってボタン押す。

Pathってところ選んで、編集押す。

C:\Users\ここにユーザー名\AppData\Local\Programs\Python\Python38\Scripts\
C:\\Users\ここにユーザー名\AppData\Local\Programs\Python\Python38\

みたいなのがなかったら新規で追加してください。Python38というのはver 3.8系を今回入れたのでPython38 になってます。
3.9系入れたらPython39です。

複数のバージョン入っていた場合上に書いてあるものが実行されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python を活用しながら Web サイトのデータ収集を効率化(1)【環境構築編】

Web スクレイピングで Python の勉強がもっと楽しくなる!

みなさん、こんにちは。
株式会社キカガクの機械学習講師 二ノ宮です。
突然ですが、初めて Python を学習をしているこんなことを感じませんか?

if 文や for 文など基本的な文法は学んだが、実際にどうやって活用していけばいいのかわからない」

語学の勉強で考えるとわかりやすいですが、基礎的な文法と実際に学んだことを活用していくシーンが紐付いていないと、学んでもどこに応用できるのかがわからず、使える知識になっていきません。
このような状況ですと、身につけた知識で具体的にできることのイメージがわかない→プログラミング学習が楽しくない→勉強しなくなるという悪循環にハマってしまいます。

そこで、 Web スクレイピングという手法で Web サイトのデータを効率的に取得する実践的な Python の活用法を複数回に分けて紹介していきます。

今回の記事の最後では、実際に特定のサイトのソースコードを取得してくるところまで扱うので、お楽しみにしていてください。

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f313035363233372f38636365383738352d336166332d333336342d663631322d3866663636613336383431342e676966.gif
※ サイトからソースコードを取得しているデモ

本記事は、以下のような読者を対象にしています。

・Python 文法を一通り学んでみたが、その知識をどこに活用していくかイメージがつかない方
・他のプログラミング言語をやってみたことがあり、Python も学んでみたいと思っている方
・スクレイピングをやってみたい方

いずれかに該当する方が、実際に手を動かしながら本連載を読み終わっているころには、Python を使ったプログラミングが楽しい!と感じる状態になっているはずです。

※ 基礎的な Python の文法を学んでから取り組みたいという方は、後日公開されます「Python 基礎文法編」の記事をご参照しながら、本記事を読んでいただければ幸いです。

目次

1.「Web スクレイピング」を使った Web データ収集の仕組み
2. 全体の流れ
3. 事前準備
4. 環境構築
5. まとめ
6. 参考

1. 「Web スクレイピング」を使った Web データ収集の仕組み

タイトルにいきなり「スクレイピング」という言葉がでてきましたが、警戒する必要はありません。

簡単に言うと、スクレイピングとは、任意の Web サイトからデータを自動的に取得していくことを表します。
スクリーンショット 2021-01-29 11.17.58.png

スクレイピングを行うことで例えば、次のことができるようになります。

  • 世界中のトレンドを可視化するために、ニュースサイトから注目ランキング上位のニュースのデータを取得
  • 株価予測をするために、株価を公開している Web サイトから株価情報を取得
  • 特定の芸能人の画像を集めるために、SNS にログインし全ての画像を自動で取得

また、機械学習のシステムを構築する際も、最初にモデルをつくるためのデータが必要です。
確かに手動で Web 上のデータをコピー & ペーストとしていくことで、情報を取得していくことができますが、非常に手間と時間がかかってしまいます。
そこで、スクレイピングを使うことで、Web データの取得における手間を効率化することが可能になります。

このように Python だけではなく、今後、機械学習を使ってデータの予測をしてみたい方は、これを機に Web 上でのデータを収集するスクレイピング力を身に着けていただければと思います。

※補足:スクレイピング以外の手法として、Web API を利用することで Web サービスが保有しているデータを外部から利用することも可能です。

注意事項

なお、スクレイピングをする際は、必ず Web サイトからデータを取得してよいかどうかの確認をしてください。
詳しくはこちらをご参照ください。
Web スクレイピングの注意事項一覧

2. 全体の流れ

それでは、さっそくコーディングをしていく前に全体像を把握しておきましょう。

スクレイピングをするためには、Selenium というブラウザの操作を自動化するパッケージを使います。
Selenium を使うことで、入力ボックスにチェックをいれることや、特定のサイトにアクセスが可能になるため、定期的に特定の Web ページから画像やテキストデータといった情報を取得する用途で使われる場合があります。

この Selenium を含めたパッケージを使ってスクレイピングをするには、大きく分けて以下の 4 つのステップがあります。
スクリーンショット 2021-01-29 10.29.39.png

1. 環境構築

Selenium を使用するために必要な環境を準備します。

2. 場所の選択

「どこのサイト」の「どの部分」を抽出するのかを選択します。
データを取得する範囲を絞ることは後のデータの整形という部分の工程が楽になるというメリットがあります。

3. データの抽出

どの Web サイトのどこのデータをスクレイピングをするのかを決めたら、実際にデータを取得します。

4. データの整形

最後に取得したデータを扱えるように、データをきれいに整えていきます。
機械学習用の場合、データセットに活用できるように Pandas や NumPy といったパッケージを使用することが多いです。

まずは、ステップ 1 の環境構築の解説していきます。

3. 事前準備

使用していくものは以下の二点です。

  • Google Chrome(デスクトップアプリ)
  • Google Colaboratory(Google のサービス)

実行環境としては、 ブラウザから Python を記述、実行できる Google Colaboratory(以下、Colab)を使用します。
Colab は Python の環境構築をする必要がなく、最初から数百の Python パッケージがインストール済となっている非常に便利なツールです。

まずは、以下のサイトにアクセスして、Google アカウントでログインしてください。
▶参考: Google Colaboratory
すると、このようなページが開かれます。
スクリーンショット 2021-01-28 14.58.29.png

次に画面の左上にある「ファイル」というボタンから、「ノートブックの新規作成」をクリックして、今回作業するためのノートブックを作成してください。
ノートブックとは実際にスクレイピングのコーディングをしていく場所のことを指します。
image.png

すると、新しいノートブックが開きます。
画面左上に表示されている名前をクリックすれば、ノートブックの名前の変更が可能になります。

title.gif

ここまででコーディングをするための準備ができました。

詳しい Colab の使用方法は以下の記事をご参照ください。
▶参考:Google Colaboratory なら Python ですぐに学べる

4. 環境構築

では、さっそくスクレイピング用のパッケージ Selenium をインストールしていきましょう。
Seleniumでは、WebDriver (ChromeDriver) を仲介してブラウザを操作します。
つまり Selenium を使うには WebDriver のインストールが不可欠です。

SeleniumとWebDriverのインストール
!apt-get update
!apt install chromium-chromedriver
!cp /usr/lib/chromium-browser/chromedriver/usr/bin
!pip install selenium

すると、このような結果が出力されるかと思います。
スクリーンショット 2021-01-28 16.50.05.png

次に webdriver と詳細設定の Options をインポートしていきます。
Optionsselenium の詳細設定をするものくらいの理解で大丈夫です。

webdriverとopitonsのインポート
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

では、インポートをした Options を使って selenium の詳細設定をしていきます。
Headless モードを始めとした記述内容は、次回の記事で解説いたします。

Headlessモードに設定
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
browser = webdriver.Chrome('chromedriver',options=options)

最後に本当に環境構築ができたかどうかをソースコードを取得することで確認してみましょう。
今回は Web スクレイピング用に独自に用意したサイトを使ってスクレイピングをしてみます。
image.png

Web スクレイピング入門
※ 本サイトは、サイトの所有者に事前に許可を得てアクセスしておりますので、皆様も安心してご使用いただけます。
ただし、繰り返しアクセスをしてしまうとサイトに負荷がかかってしまうため、本記事の内容に沿って挙動を確かめる目的のみでのご使用をお願い致します。

そこで、browser.get で、指定した URL に遷移し、その URL のソースコードを print で表示させてみましょう。

ソースコードを取得
browser.get("https://scraping-for-beginner.herokuapp.com/?fbclid=IwAR1__GW643UFEJmc1486KoGZfPJHhrRN-ybnWw8YTCznyQm2aS4myUR3kI8")
print(browser.page_source) 

すると、このようにソースコードが取得できるはずです。
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f313035363233372f38636365383738352d336166332d333336342d663631322d3866663636613336383431342e676966 (1).gif

以上になります。
お疲れさまでした!

5. まとめ

このように、簡単に短時間でスクレイピングをするための環境構築が可能になりました。
今回は、記述するコード量が多くなかったですが、次回の記事ではより本格的に Python を使いながらデータの収集をやっていきましょう!

6. 参考

退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DjangoとClean Architecture

目次

  1. 概要
  2. 前置き
  3. はじめに
  4. DDDについて
  5. LayeredArchitectureについて
  6. OnionArchitectureについて
  7. OnionArchitectureの応用
  8. CleanArchitectureについて
  9. CleanArchitectureの応用
  10. 実装テクニック・ノウハウ
  11. 用語集

概要

複雑・肥大化するwebアプリケーションコードの寿命を伸ばす&筋の良い作りにするために、代表的なアーキテクチャの概念をまとめてみる。
特に、業務でも使うことの多いDjangoをターゲットとして、CleanArchitectureを使った場合の構成を模索してみた備忘録。


前置き

  • ソフトウェアの寿命って?
    • アップデートについていけなくなる。保守・開発ができなくなる。などが原因で使われなくなった時
  • 寿命の短いソフトウェアって?
    • 複雑怪奇なコードで、追加開発・修正のコスト大 → 誰も保守・開発できなくなり廃棄
    • 特定の人しか知らない構造・コードになっていて、後から追うのが辛い → 作り直した方が早いので廃棄
  • 寿命を伸ばすためには?
    • 仕様変更や技術のアップデートに耐えられる柔軟性を持たせる
    • デファクトスタンダードなアーキテクチャに則って、同じ文脈で保守・開発できる絶対数を増やす

はじめに

はじめに、導入としてDDDの概念をチョーざっくり説明する。
次に、アーキテクチャとして、LayeredArchitectureの説明を行う。
また、LayeredArchitectureをベースに、OnionArchitectureの概念と、用語・構造を説明する。
OnionArchitectureの応用として、既存のMVC型WebアプリケーションフレームワークとOnionArchitectureを組み合わせた場合を説明する。
そして、CleanArchitectureの概念と、用語・構造を説明する。
CleanArchitectureの応用として、既存のMVC型WebアプリケーションフレームワークとCleanArchitectureを組み合わせた場合を説明する。

補足
実装テクニック・ノウハウと、用語集も記述している。


DDDについて

ドメイン駆動設計(Domain-Driven Design)とは

EricEvansのドメイン駆動に関するdocumentは、ここを参照いただければと思うので、詳細は割愛。

ざっくり自分の理解をまとめると、
ドメインモデリングのノウハウやベストプラクティスを集大成した、1つの設計 思想・哲学。TDDなどのような開発プロセスとは違う。

  • 「ドメイン」とは何か?
    • ソフトウェア化する対象のことで 「A sphere of knowledge, influence, or activity.」 と定義している。つまり、ソフトウェア化する対象それぞれが持つ業務内容や知識のこと。
  • 「ドメインモデリング」とは何か?
    • ソフトウェア化する対象の定義・設計
  • 「ドメインモデリング」の価値とは何か?
    • ドメインモデルが適しているかという観点で開発が回るので、ソフトウェアのコア機能を、コンパクト かつ 短いサイクルでの改善ができる
    • ドメイン知識が、所定のモジュールに集約される(= 凝集 )ので、コードが散らばらずに保守・開発がしやすくなる

つまり、
ソフトウェア化したい現実の対象を、「ドメイン」としてソフトウェアの世界に落とし込むという思想


LayeredArchitectureについて

LayeredArchitectureとは?

DDDにて紹介されたドメインを隔離するためのアプリケーションアーキテクチャ
LayeredArch00.png
原則として、上から下に向けて依存関係になっていて、逆方向への依存はNG

LayeredArchitecture構成要素

  • UI/Presentation
    • ユーザや別システムへの情報表示
    • ユーザや別システムからのコマンド受付
    • 画面構成やデザイン変更による仕様変更が頻繁に発生し得る不安定な層でもある
  • Application
    • UIから呼び出されるアプリケーションのロジックが定義される
    • アプリケーションとしてのドメイン知識はここには詰め込まないで、UIからデータを受け取ってパースしたり、UI向けのデータ加工を行う層
  • Domain
    • Applicationから呼び出されるモジュール。
    • ソフトウェアとしてのドメイン知識をモジュールとして提供する層。
  • Infrastructure
    • DBや外部サービス(File Access, Access, ORM, etc...)にアクセスして永続化を担当する層。
    • 一般的な技術要素が詰め込まれている
    • 技術要素に引きづられるので、ここも頻繁に変動が起こり得る不安定な層。

LayeredArchitectureの特徴

  1. Domainで、ドメインロジックを提供することで、ドメイン知識を所定のモジュールに押し込めている。
  2. Applicationで、アプリケーション固有のデータ加工や、ドメインロジックの制御を提供することで、ドメインに影響を与えずにロジックを組み立てることが可能になる。
  3. 基本的に下の階層のモジュール呼び出しのみ可能としている。
  4. コードの再テストや修正による影響は、依存先である 上位の層 のみとなる。

LayeredArchitectureの弱点

LayeredArch01.png

  • Infrastructure層が上位層から依存されていることが問題。
    • Infrastructureのモジュールの修正や置き換えが発生した場合、それを参照しているDomain、さらにはApplication/UIまで影響が波及していく。つまり、Domain層より上位の層の修正対応のほか、テストの追加・修正から再実行まで行う必要が出てくる。
    • 例えば、MySQLモジュールのバージョンアップや、MySQLからCloud Saas Serviceへの移行による影響が、ドメインロジックまで波及する。(ドメインロジックに波及するということはアプリケーション・UIもテストが必要になる)ドメインロジックやアプリケーションロジックが膨大になればなるほど、この影響は辛いものとなる。。。

OnionArchitectureについて

参考

OnionArchitectureとは?

LayeredArchitectureをベースに、「テスト」や「インフラ(DB)」等の外部サービスと技術要素との結合を疎にして、より「ドメイン」の実装に着目したアーキテクチャ。
OnionArch00.png

OnionArchitecture構成要素

  • UserInterface/Presentation
    • ユーザや別システムへの情報表示
    • ユーザや別システムからのコマンド受付
    • 画面構成やデザイン変更による仕様変更が頻繁に発生し得る不安定な層でもある
  • Infrastructure
    • DBや外部サービス(File Access, Access, ORM, etc...)にアクセスして永続化を担当する層。
    • 技術要素に引きづられるので、ここも頻繁に変動が起こり得る不安定な層。
  • Tests
    • テストコードモジュール。
    • UIの変更に伴い、テスト項目も変動する不安定な層。
  • ApplicationService
    • UIから呼び出される(=エンドポイント)が定義される。
    • ソフトウェアとしてのドメイン知識はここには詰め込まないで、UIからデータを受け取ってパースしたり、UI向けのデータ加工を行う層。
  • DomainService
    • ドメインロジックを提供する層。
    • 依存性逆転の原則DIに則って、Infrastructureから参照されるRepository(=Interfaceクラス)を用意する層。
  • DomainModel
    • ドメイン知識に関連した状態と振る舞いを持つオブジェクト(EntityValueObjectに代表されるもの)を配置する層。
    • 他の層への依存関係を持たない。

OnionArchitectureの特徴

  1. LayeredArchitectureの弱点であった、 Infrastructureへの Domainの依存を、 依存性逆転の原則DIにて解消している
    つまり、以下図のように、InfraStructure層がDomainService層に依存するようになっていることがわかる。
    OnionArch01.png

  2. Applicationロジック/Domainロジックが、外的要因(UI・インフラ)への依存関係をなくしたことにより、外的要因の影響を受けにくい(=疎結合)になった。つまり、外的要因の修正・変更があっても、ドメインモデルやServiceロジックの修正・テストは不要になった(= 最も重要なドメインに関する機能を担保できるようになる。

OnionArchitectureの応用

Djangoとの融合

OnionArch02.png
Djangoのアーキテクチャに対して、OnionArchitectureを適用した簡易例を上記図で表している。

  • UI(PresentationLayer)
    • MVCにおけるControllerがここに存在。
    • 責務としては、入力値のvalidation程度。
    • エンドポイントの提供。
    • ApplicationService/DomainService/DomainModelの呼び出し・組み立てを行う。
    • Controllerのhelperが存在。
    • Controllerの共通処理を提供。
  • Infrastructuire
    • MVCにおけるModelがここに存在。
    • ORMを使ったモデルが定義され、DBへのアクセス・制御を行っている。
    • Domainロジックで扱うデータ型でwrapされたRepositoryオブジェクトの具象クラスが存在する。
    • Modelもここでwrapして、Repositoryとしてドメインロジック内で扱えるようにする。
    • IOを直接制御するロジックもここでwrapして、Repositoryとしてドメインロジック内で扱えるようにする。
  • ApplicationService
    • アプリケーション向けのデータ加工やDomainService/DomainModelの呼び出しを行う
    • アプリケーション固有の表示方法やvalidationなどのロジックはここに押し込める。
  • DomainService
    • ドメインロジックの組み立てを行う。
    • Infrastructureで具象化されるRepositoryのInterfaceクラスを定義する。
  • DomainModel

OnionArchitectureでのDjangoエンドポイント処理の流れ

  1. UIから飛んできたリクエストは、ApplicationServiceに飛ばされる。
  2. ApplicatinServiceでUIからデータを受け取り、DomainServiceで処理できるデータ形式に加工。
  3. DomainServiceでApplicatinServiceからデータを受け取る。
    1. 受け取ったデータを使ってDomainModelを呼び出し、ドメインロジックを組み立てる。
    2. RepositoryとDIで渡されたRepositoryの具象クラスを使って、データの永続化処理を実行
  4. DomainModelでDomainServiceから要求されたドメインモデルを返す。
  5. ApplicationServiceでDomainServiceから返り値を受け取る。受け取ったデータは、UIで表示できるデータ形式に加工。
  6. UIでApplicationServiceからの返り値を受け取り、UIに反映させる。

CleanArchitectureについて

参考

CleanArchitectureとは?

OnionArchitectureなどのDDDベースで発案されたアーキテクチャの包括的な概念・構成を定義したもの。
さらに、具体的なクラス構成案や、依存性逆転のためのオブジェクト構成まで言及している。


上記構成図はOnionArchitecture同様、外から内に向かって依存していることを表している。逆はNG。
外側のオブジェクトを参照せざる得ない場合は、依存性逆転の原則DIを使って実現させるテクニックを使う。

CleanArchitecture構成要素

  • Frameworks & Drivers(=External Interfaces)
    • 概要
      • 技術的要素(DBやメールシステムなど)やUIそのものが存在し、外的要因で修正・変更が走りやすい層。
    • DB
      • DBそのものや、DBを取り扱うミドルウェア・フレームワークのコードが存在する層。
    • UI
      • フロントのUIコードが存在する層。
  • Interface Adapters
    • 概要
      • 入力、永続化、表示を担当するオブジェクトが所属
      • External Interfacesとの接点
    • Gateways
      • データの永続化処理を担当する層
    • Controller
      • UseCasesに伝えるためのデータ加工を担当する層
    • Presenter
      • データの表示のためのデータ加工を担当する層
      • UIからのエンドポイントとしても機能する
  • Application Business Rules(=UseCases)

    • 概要
      • UIから呼び出される(=エンドポイント)が定義される。
      • ソフトウェアとしてのドメイン知識はここには詰め込まないで、UIからデータを受け取ってパースしたり、UI向けのデータ加工を行う層。
      • Entitiesを組み合わせて制御する。
    • Interactor
      • ContorllerやPresenterはInterfaceで用意される。これらの具象クラスを担当する。
  • Enterprise Business Rules(=Entities)

    • 概要
      • DDDにおけるEntityとは意味合いが異なるので注意!
      • ドメイン知識に関連した状態と振る舞いを持つオブジェクト(EntityValueObjectに代表されるもの)を配置する層。
      • OnionArchitectureにおけるDomainModelだけでなく、DomainServiceも含まれ、ドメインルールを包括的に提供する層である。
      • 他の層への依存関係を持たない。

CleanArchitectureの具体的なモジュール構成とその成り立ち


上記はCleanArchitectureの具体的なモジュール構成を表した図である。
このモジュール構成の成り立ちを追いかけて、構成のイメージを掴む。

  1. OnionArchitectureより、以下のモジュールを構築する
    CleanArch00.png

  2. Infrastructureを分割する
    CleanArch01.png
    実際のDBやIO操作を担う、ORMやライブラリをInfrastructureとして改めて定義する。
    また、Infrastructureモジュールを参照して、DomainService向けにデータの永続化ロジックの具象クラスを提供するモジュールを、DataAccessとして定義する。

    そして、DataAccessが参照するデータ永続化に関するInterfaceクラスを提供するモジュールを、DataAccessInterfaceとして定義する。

    ここで現れたモジュールの責務は以下の通り

    • Infrastructure
      • 実際のDBやIO操作を担う。ORMを利用したModelもここに存在。
    • DataAccess
      • Infrastructureの機能をラップして、ドメインロジックで扱う事のできるモジュールとして絵提供する。
    • DataAccessInterface
      • InfrastructureのInterfaceクラス。依存性逆転の原則より発生。
  3. ApplicationServiceを分割する
    CleanArch02.png
    UIは多くの変更が発生し得る層であり、変更の影響として、ApplicationServiceを修正・追加したり、出し分けたりすることが多々起こり得る。その際にも、柔軟にApplicationServiceの拡張と修正によるUIへの影響低減を実現させたい。

    そこで、開放閉鎖の原則を用いて、UI側の変更に対して、ApplicationService側が柔軟に拡張可能な仕組みを取り入れる。

    つまり、ApplicationServiceの実行ロジックをInterfaceで提供することで、UIからは追加したいInterfaceの具象オブジェクトを渡して使いたいロジックを追加・選択できる仕組みである。
    また、UIも、ApplicationInterfaceを介してアプリケーションロジックを組み立てる責務を担った、Controllerモジュールを切り分けて用意している。
    ここで現れたモジュールの責務は以下の通り

    • Controller
      • UIから受け取ったデータをApplicationServiceで利用できるデータ形式に加工する。
    • IApplicationService
      • UIからApplicationServiceを呼び出す際に、開放閉鎖の原則より、修正の影響を受けないようために用意されるInterfaceクラス。
  4. DomainServiceを責任分割する
    CleanArch03.png
    DomainServiceには、ドメインモデルに対する操作と、DataAccessを用いてデータの永続化を行う機能が存在している。
    そこで、ドメインモデルとドメインロジックを提供する Entities と、EntitiesとDataAccessを呼び出して組み合わせる、 UseCaseInteractor に分割させる。
    ここで現れたモジュールの責務は以下の通り

    • Entities
      • ドメインモデルおよびドメインロジックを提供する。
      • DDDにおけるEntityおよびValueObjectもここに存在。
  5. UIを責任分割する

    CleanArch04.png
    UIには、viewとしてリクエストを受け取り、レスポンスを返す機能と、Controllerから帰ってきたデータを加工して表示に渡す機能が混在している。
    そこで、UIもControllerから受け取ったデータを表示ように加工するPresenterと、表示(リクエスト・レスポンス処理)を行うViewに分割させる。
    ここで現れたモジュールの責務は以下の通り

    • Presenter
      • Viewsで表示させるデータに加工する。
    • View
      • エンドポイントの提供を行い、request/responseの処理を行う。
      • 表示処理
  6. PresenterとControllerの依存とApplicationServiceからの依存を分離する


    Controllerは、InputBoundaryを呼び、結果をPresenterを呼んで加工するという2つの責務が発生しているので、その責務を分断させる。
    IOutputBoundaryは表示のためのデータの加工ロジックのInterfaceクラス。Presenterは、IOutputBoundaryの具象クラスとなる。
    また、View以外を起点としてViewに向かってデータが流れる場合にも、ApplicationServiceからPresenterへの依存関係を結ばせないために、依存性逆転の原則に従って、ApplicationServiceからはIOutputBoundaryというInterfaceクラスに依存するだけになっている。
    ここで現れたモジュールの責務は以下の通り

    • IOutputBoundary
      • PresenterのInterfaceクラス。

上記より、CleanArchitectureで提案されているモジュール構造が、既存のLayerdArchitectureをベースに、SOLIDの原則に従って変形していくとほぼ一致する形になることがわかる。

CleanArchitectureの特徴

  1. 外側から内側に向けて依存方向を向けることを原則としている。
  2. DDDのEntityとは異なり、EntitiesではDomainServiceとDomainModelを含む、ドメインロジックを提供する責務がある。
  3. OnionArchitectureと同様、Infrastructureの要素(=InterfaceAdapterのGateways)は、Repositoryの具象クラスとなる。RepositoryのInterfaceクラスは、UseCasesおよびEntitiesで提要される。
  4. UIとApplicationServiceとの関係について、開放閉鎖の原則依存性逆転の原則をベースに、入力データの加工を担当するInputBoundaryと出力データ向けの加工を担当するOutputBoundaryというInterfaceクラスが存在するようになった。

CleanArchitectureの応用

Djangoとの融合

CleanArch06.png
Djangoに対して、CleanArchitectureを適用した簡易例を上記図で表している。

  • InterfaceAdapter (UI)
    • View
    • MVCにおけるControllerがここに存在。
      • 責務としては、入力値のvalidation程度。
      • エンドポイントの提供。
      • データの加工やロジックの呼び出しはController/Presenterに任せる。
    • Controller
    • Viewで受け取ったデータをUseCasesで扱えるデータに加工する。
    • Presenter
    • Viewに返すデータに加工する。
    • OutputBoundryの具象クラス。
  • InterfaceAdapter (Gateways)
    • Infrastructure
    • MVCにおけるModelがここに存在。
    • ORMを使ったモデルが定義され、DBへのアクセス・制御を行っている。
    • DBへのアクセス・制御ロジックのライブラリがここに存在する。
    • DataAccess
    • Infrastructureで扱うデータを、ドメインロジックで扱う型としてラップする。
  • UseCases
    • InputBoundry
    • アプリケーション独自のロジックを扱う上で、Viewからデータが流れてくるロジックのInterfaceクラス。
    • OutputBoundry
    • アプリケーション独自のロジックを扱う上で、Viewにデータを返すロジックのInterfaceクラス。
    • UseCaesInteractor
    • InputBoundryの具象クラス。
    • アプリケーション独自のロジックを提供する。
  • Entities
    • DataAccessInterface
    • DataAccessモジュールで扱うクラスのInterfaceクラス。
    • Entities
    • ドメインロジックおよびドメインモデルを提供する。
    • DDDのEntityやValueObjectを提供する。

実装テクニック・ノウハウ

依存性逆転の原則

抽象に依存せよ

依存する・されるの関係は、依存される側に変更があると、依存している側にも影響を及ぼす。(=importしているモジュールに変更があると、そのモジュールを利用しているコードも修正・テストが必要になる)

DomainがInfrastructureに依存している構成



上記例は、Domain層のItemクラスとInfrastructure層のItemRepositoryクラス間の依存関係を表している。具体的なクラス実装例は以下の通り。

// Itemオブジェクトを表すDomein層のクラス
#include<iostream>
#include<string>
#include<ItemRepository> // ItemRepositoryへの依存がある状態
using namespace std;
public class Item{
    private int itemID=0;
    private string itemName="";
    public Item(int id, string name){
        itemID = id;
        itemName = name;
    }    
    public void SaveItem(){
        ItemRepository repo = new ItemRepository(); // ItemRepositoryを参照してインスタンス化
        repo.save(this.itemID, this.itemName);
    }
}
// ItemRepositoryとして、DBを介して永続化を行うInfrastructure層のクラス
#include<iostream>
#include<string>
using namespace std;
public class ItemRepository{
    public void save(int id, string name){
        sql::mysql::MySQL_Driver *driver = sql::mysql::get_mysql_driver_instance();
        // MySQLのInsert処理
        // ........
    }
}

具体例より、ItemクラスではItemRepositoryを参照しており、ItemRepositoryの実装に変更・修正があった場合はItemクラスも修正・テストが必要になる。
そこで、InterfaceClassを介して、依存の方向を逆向きに変えるテクニックが依存性逆転の原則である。

依存性を逆転さた構成(DIなし)



上記例は、Domain層の中にInterfaceクラスとしてItemRepositoryクラスを用意し、Interfaceの中に実装クラスとしてItemRepositoryImplementクラスを用意した上でのクラス間依存関係を表している。具体的なクラス実装例は以下の通り

#include<iostream>
#include<string>
#include<IItemRepository> // IItemRepositoryへの依存がある状態
#include<ItemRepositoryImplement> // ItemRepositoryImplementへの依存がある状態
using namespace std;
public class Item{
    private int itemID=0;
    private string itemName="";
    public Item(int id, string name){
        itemID = id;
        itemName = name;
    }    
    public void SaveItem(){
        IItemRepository repo = new ItemRepositoryImplement(); // IItemRepository/ItemRepositoryImplementを参照してインスタンス化
        repo.save(this.itemID, this.itemName);
    }
}
// RepositoryのInterfaceを表すDomain層のクラス
#include<iostream>
#include<string>
using namespace std;
public class IItemRepository{
    // saveメソッドの純粋仮想関数
    public virtual void save(int id, string name) = 0;
}
// Repositoryの具象クラスを表すInfrastructure層のクラス
#include<iostream>
#include<string>
#include<IItemRepository>
using namespace std;
public class ItemRepositoryImplement: public IItemRepository{
    public void save(int id, string name){ // 純粋仮想関数の具象化
        sql::mysql::MySQL_Driver *driver = sql::mysql::get_mysql_driver_instance();
        // MySQLのInsert処理
        // ........
    }
}

具体例より、依然としてItemクラスの中でItemRepositoryImplementクラスを判別して呼び出しており、参照関係が消えていないことがわかる。
そこで、DIを利用して、Itemクラスの呼び出し元から、Interfaceクラスと具象クラスの組み合わせを渡して、Itemクラスの中で具象クラスを参照しないテクニックを使う必要がある。

依存性を逆転さた構成(DIあり)


上記例は、DIを使った場合のクラス間の依存関係を表している。具体的なクラス実装例は以下の通り。

// Itemクラスを呼び出す上位モジュール
#include<iostream>
#include<string>
#include<Hypodermic/Hypodermic.h> //DIコンテナのためのライブラリ
#include<Item> // Itemへの依存がある状態
#include<IItemRepository> // IItemRepositoryへの依存がある状態
#include<ItemRepositoryImplement> // ItemRepositoryImplementへの依存がある状態
using namespace std;
public void Main(){
    // DIコンテナの定義
    Hypodermic::ContainerBuilder builder;
    builder.registerType< ItemRepositoryImplement >()
        .as< IItemRepository >();
    m_container = builder.build();
    // Itemのインスタンス化
    repo = m_container->resolve< IItemRepository >()
    item = new Item(1, "my item", repo) // repoを渡す
    item.SaveItem(); // infrastructure層のロジックが実行される
}
// Itemオブジェクトを表すDomein層のクラス
#include<iostream>
#include<string>
#include<IItemRepository> // IItemRepositoryへの依存がある状態
using namespace std;
public class Item{
    private int itemID=0;
    private string itemName="";
    IItemRepository itemRepo;
    public Item(int id, string name, IItemRepository repo){
        itemID = id;
        itemName = name;
        itemRepo = repo
    }    
    public void SaveItem(){
        itemRepo.save(this.itemID, this.itemName);
    }
}
// RepositoryのInterfaceを表すDomain層のクラス
#include<iostream>
#include<string>
using namespace std;
public class IItemRepository{
    // saveメソッドの純粋仮想関数
    public virtual void save(int id, string name) = 0;
}
// Repositoryの具象クラスを表すInfrastructure層のクラス
#include<iostream>
#include<string>
#include<IItemRepository>
using namespace std;
public class ItemRepositoryImplement: public IItemRepository{
    public void save(int id, string name){ // 純粋仮想関数の具象化
        sql::mysql::MySQL_Driver *driver = sql::mysql::get_mysql_driver_instance();
        // MySQLのInsert処理
        // ........
    }
}

具体例より、Interfaceクラスと具象クラスの対比表を作ってItemクラスの呼び出し時に渡していることがわかる(DIコンテナ)。
Itemクラスでは、Interfaceクラス型を使って処理を定義可能であるため、具象クラスが何かを知らなくても良い。つまり、Infrastructure層への参照がなくなったことがわかる。

DI(Dependency Injection)

参考

説明

あるコンポーネントAが利用している別のコンポーネントBを外部から注入することにより、コンポーネントAがコンポーネントBの実装との依存関係を排除するデザインパターン。
あるモジュールA内でモジュールBを参照しないように、Aの呼び出し時にBのインスタンスを渡す(注入)ということ。
この時、受け取るインスタンスの型は、Interfaceクラスを利用することで、Aの中でBのメソッドにアクセスする際も、具象クラスの実装内容に影響を受けることがなくなる
以下は、DIを利用しない例。

// メインプログラム
include<FooClient>
class Program{
    public void Main(){
        FooClient client = new FooClient();
        client.Execute();
    }
}
// FooComponentを利用する機能
include<FooComponent>
class FooClient{
    public FooClient(){
    }
    public void Execute(){
        FooComponent component = new FooComponent();
        component.Execute();
    }
}
// 機能Fooを提供するコンポーネント
class FooComponent{
    public void Execute(){
        // ...なにか処理を行う
    }
}

上記例では、FooComponentに修正があった際にFooClientの修正、再ビルド、再テストが必要になる。
そこで、FooComponentのインスタンスをFooClientに渡すことで、FooComponentの中身を知らなくても良いようにする。
以下がその例。

// メインプログラム
include<FooClient>
include<FooComponent>
class Program{
    public void Main(){
        FooClient client = new FooClient(new FooComponent());
        client.Execute();
    }
}
// FooComponentを利用する機能
include<IComponent> // Interface以外の参照がなくなった!
class FooClient{
    IComponent component;
    public FooClient(IComponent component){
        this.component = component;
    }
    public void Execute(){
        component.Execute();
    }
}
// ComponentのInterface
class IComponent{
    public virtual void Execute() = 0;
}
// 機能Fooを提供するコンポーネント
include<IComponent>
class FooComponent: public IComponent{
    public void Execute(){
        // ...なにか処理を行う
    }
}

上記例では、FooComponentに修正があった場合でも、FooClientではFooComponentに依存していないため、修正・テストが不要になる(Interface自体に変更がない限り)
このように、インスタンスを実際に渡して依存性を排除するテクニックがDIである。

開放閉鎖の原則

拡張に対して開かれている(Open)
あるモジュールが拡張可能である場合、そのモジュールは拡張に対して開かれている(Open)と言う。
修正に対して閉じている(Closed)
あるモジュールが修正に対してソースコードに影響を受けない場合、そのモジュールは修正に対して閉じている(Closed)と言う。
つまり、 コードを修正せずに追加によって拡張可能にするテクニック である。
以下例を参考にまずい箇所を説明する。

// 呼び出し元オブジェクト
#include<iostream>
#include<string>
#include<Engine>
using namespace std;
class Car{
    public void RunEngine(){
        // Engineのインスタンス化
        Engine engine = new Engine();
        // ガソリンエンジンの起動
        engine.runGasEngine();
        // ハイブリッドエンジンの起動
        engine.runHybridEngine();
        // 他のエンジンがあればここに延々と続いていく。。。
    }
}
// 呼び出し先オブジェクト
#include<iostream>
#include<string>
using namespace std;
class Engine{
    public void runGasEngine(){
        // ガソリンエンジン起動処理
    }
    public void runHybridEngine(){
        // ハイブリッドエンジン起動処理
    }
    // 他のエンジンが追加されたらここを修正
}

例では、参照元のCarオブジェクトでエンジンを追加してEngineロジックを実行したい場合、参照先のEngineクラスとそのテストコードの修正が必要となる。
つまり、Engineクラスは修正に対して閉じていないということになる。
そこで、上記例を開放閉鎖の法則に従って修正したコードを以下に示す。

// 呼び出し元オブジェクト
#include<iostream>
#include<string>
#include<EngineRun>
#include<GasEngine>
#include<HybridEngine>
using namespace std;
class Car{
    public void RunEngine(){
        RunEngine runEngine = new RunEngine();
        // ガソリンエンジンの起動
        runEngine.run(new GasEngine());
        // ハイブリッドエンジンの起動
        runEngine.run(new runHybridEngine())
    }
}
// 呼び出し先のロジック実行オブジェクト
#include<iostream>
#include<string>
#include<IEngine>
using namespace std;
class RunEngine{
    public void run(IEngine engine){
        engine.run();
    }
}
// 実行ロジックのInterfaceクラス
#include<iostream>
#include<string>
using namespace std;
class IEngine{
    // エンジン実行の純粋仮想関数
    public virtual void run() = 0;
}
// 実行ロジックの具象クラス
#include<iostream>
#include<string>
#include<IEngine>
class GasEngine: public IEngine{
    public void run(){
        // ガソリンエンジンの起動処理
    }
}

#include<iostream>
#include<string>
#include<IEngine>
class HybridEngine: public IEngine{
    public void run(){
        // ハイブリッドエンジンの起動処理
    }
}

開放閉鎖の原則を実現するためには、はじめに、出し分けるロジック(GasEngine/HybridEngineクラス)のInterfaceクラスを用意し、出し分けたいロジックは具象クラスとして実装する。
また、Interfaceを介して、ロジックを実際に実行するクラス(RunEngineクラス)を実装する。
呼び出し元(Carクラス)からは、Interfaceクラスの具象クラスをインスタンス化し、RunEngineクラスに渡している。

結果、新しいEngineクラス(例えばHydroEngineクラス)が追加されたとしても、Interfaceを介しているだけなので、呼び出し先のRunEngine/GasEngine/HybridEngineクラスは修正が不要となる。
変わりに、 Interfaceを継承した具象クラス(HydroEngineクラス)を追加し、呼び出し先に渡すだけでロジックの修正が済む。
これが開放閉鎖の原則の一例である。


用語解説

  • 依存
    • 要素Aを実装する際に、要素Bを読み込まなければ実現できないような場合(≒import/require/include/etc..)、要素Aは要素Bに依存している。依存の方向は、「要素A → 要素B」
  • ドメインロジック
    • ドメイン知識を提供するためのモジュール・クラス・メソッド。
  • Entity
    • Entityは識別子(=ID)によって同一性を示す。
    • オブジェクト自体のフィールドの変更を許容する。
    • つまり、オブジェクト一つ一つが意味を持ち、一意性を持つ。そして、そのオブジェクトが持つ情報が書き換わることを許容し、書き換わったとしても同じオブジェクトであることを表している。
  • ValueObject
    • ValueObjectは保持する情報が同一であれば同一とみなす。
    • オブジェクトが保持するフィールドが変わることを許容しない。
    • つまり、オブジェクト一つ一つが不変なデータの塊であり、それらフィールドが同一であれば、複数のオブジェクトが存在しても全て同じオブジェクトとみなす事ができる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Excelを自動化できる!Pythonのモジュール「OpenPyXL」で効率化してみた

Excelを自動化できる!Pythonのモジュール「OpenPyXL」で効率化してみた

https://aizine.ai/openpyxl-0126/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonで学ぶアルゴリズム 第24弾:ベルマン・フォード法

#Pythonで学ぶアルゴリズム< ベルマン・フォード法 >

はじめに

基本的なアルゴリズムをPythonで実装し,アルゴリズムの理解を深める.
その第24弾としてベルマン・フォード法を扱う.

ベルマン・フォード

今回から,最適経路に関するアルゴリズムを扱っていくが,その1つ目としてベルマン・フォード法を扱う.
いま,次に示すような経路において,最短経路となるときの距離(コスト)を求めたい.
image.png
丸で囲まれたところは目的地であり,それらを結ぶ矢印が道とする.矢印の近くにある数値はその道(矢印)を通る時の距離(コスト)である.また矢印には始点と終点があり,逆走はしないものとする.この経路に対して,ベルマン・フォード法を適用していく.

ベルマン・フォード法とは,始点地におけるコストと道でのコストを足し合わせた結果,終点地のコストよりも小さければ,その終点地のコストを更新していくというものである.いま示した経路において,最短経路を見つけていく様子をひとつひとつ以下に示す.なお,各目的地における初期値はスタート地点以外∞としている.
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png

このようにして各目的地における最短距離(最小コスト)が求められる.以下の実装では,その各目的地における最短距離を出力するものとする.また,∞の表現について,Pythonではfloat('inf')とすることで,表現できるようだが,ほかの言語では,9999999のように十分大きな数値として行えばよいらしい.単純に初期値は更新されるものとして大きな値にしておくということに変わりはない.

実装

先ほどの説明をもとに,実装した.以下にソースコードとそのときの出力を示す.

ソースコード
bellman_ford.py
"""
2021/01/28
@Yuya Shimizu

ベルマン・フォード法
"""

def bellman_ford(Map, num_node):
    """ 経路の表現
            [始点, 終点, 辺の値]
            A, B, C, D, ... → 0, 1, 2, ...とする """
    node = [float('inf') for i in range(num_node)]  #スタート地点以外の値は∞で初期化
    node[0] = 0     #スタートの始点は0で初期化

    Continue = True #終了条件を表す変数 Falseとなれば終了

    while Continue:
        Continue = False

        #経路の要素を各変数に格納することで,視覚的に見やすくする
        for factor in Map:
            start = factor[0]   #始点
            goal  = factor[1]   #終点
            cost  = factor[2]   #コスト

            #更新条件
            if node[start] + cost < node[goal]:
                node[goal] = node[start] + cost     #更新
                Continue = True

    return node

if __name__ == '__main__':
    MAP = [[0, 1, 4], [0, 2, 3], [1, 2, 1], [1, 3, 1],
               [1, 4, 5], [2, 5, 2], [4, 6, 2], [5, 4, 1], [5, 6, 4]]

    #今の目的地の数は7つ(0~6: A~G)
    opt_node = bellman_ford(MAP, 7)
    print(opt_node)
出力
[0, 4, 3, 5, 6, 5, 8]

はじめに図で示した通り,[A, B, C, D, E, F, G] = [0, 4, 3, 5, 6, 5, 8]というように各目的地における最短距離を出力できていることが分かる.

感想

ベルマン・フォード法は今まで知らなかった.また,Pythonに∞表現があることも知らなかった.最短距離問題は距離をコストとすることで,ほかにも応用できるし,そもそも問題が面白かった.前回までの並べ替えとは少し雰囲気が変わり,また士気が高まり,次回からの学習も楽しみである.

参考文献

Pythonで始めるアルゴリズム入門 伝統的なアルゴリズムで学ぶ定石と計算量
                         増井 敏克 著  翔泳社

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

とりあえずcudaが使いたい人のためのQiita

Dockerを使いましょう

Docker知らないからってこのページを閉じようとしたあなた。多分想像以上に環境構築簡単なのでご安心を。Dockerさえ入ってしまえばLinuxのOSやversionが関係ないのがDockerの強みです。

背景

私事ながら、Geforce RTX3090を手に入れたのですが、今までのcudaの導入方法だと一向に認識されず、1週間溶かしまして絶望していたのですが、Dockerを使えば一瞬でしたので同じように悩んでる人に向けてそれを共有したいと思いました。

環境

  • Ubuntu 18.04(GPUが接続されているLinuxならDockerさえ入ればなんでもいい)

やり方

1. Dockerのインストール

たくさんの記事があるので割愛。
https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04-ja
などを参照。

2. 使いたいDocker imageを探す

Docker hubで使いたいDockerイメージを探します。色々ありますがとりあえずcudaが使いたい人はnvidia/cudaの最新のものにしましょう。base, runtime, develとありますが、特にこだわりがなければdevelを選択するのが良いでしょう。(develにしかないコマンドがある)

base: 事前ビルドされたCUDAアプリケーションを展開するための最小構成のイメージ。CUDA Toolkit 9.0以降のみ。
runtime: CUDA toolkitから全ての共有ライブラリを追加したCUDA実行用のイメージ。
devel: 共有ライブラリだけでなくコンパイラーチェーン、デバッグ用ツール、スタティックライブラリなどが追加されたCUDA開発用のイメージ。
https://www.atmarkit.co.jp/ait/articles/1804/18/news141_3.html

以下で用いるDockerイメージはnvidia/cuda:11.2.0-runtime-ubuntu20.04とします。
(ちなみにコロンの前のnvidia/cudaREPOSITORY, コロンの後の11.2.0-runtime-ubuntu20.04TAGといいます)

3. Docker pull

個人的にはGitでいうgit cloneという理解をしています。使いたいDockerイメージをローカルに作成します。今回はnvidia/cuda:11.2.0-runtime-ubuntu20.04を用いるので、

docker pull nvidia/cuda::11.2.0-runtime-ubuntu20.04

となります。docker pullは時間がかかるので気長に待ちましょう。

4. コンテナを生成、起動

dockerイメージのコピーであるコンテナを生成し、その中に入っちゃいます。

docker run -it nvidia/cuda::11.2.0-runtime-ubuntu20.04

すると、謎の場所に連れて行かれると思います

[root@4ae03cb199f4 /]#

これで起動成功です。ここはcudaの入ったubuntu20.04の世界になっています。

あとはこの中でいつも通り環境を構築していきましょう。(僕はanacondaを入れました)もし何か失敗してしまってもコンテナを消してまた1から作ればOKです。ホストOSが汚染されることはありません。

Dockerの使い方について

ちなみに上の方法をそのままやったところでろホストOSのディレクトリは覗けませんし、ipアドレスが繋がってないのでサーバーを立てることもできません。ここからどうカスタマイズしていくかは使う人にかかってきます。ぜひ色々と調べてみてください。(私も絶賛Docker勉強中であります)
例えばanacondaを使うなら、-vで接続するディレクトリを決めて-pでポート番号を指定したりするとコンテナ内の環境でホストのファイルの操作ができるようになります。

参考

https://www.amazon.co.jp/Docker実践ガイド-第2版-impress-top-gear/dp/4295005525

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonにおけるQtCore.QAbstractItemModel.matchを調べた

免責事項

本記事の内容を基にして不具合が発生した場合これの責任は一切持ちません。
本記事はPyside2の記事やら素のQtやらいろんなところからついばんで生まれたので無駄等々いろんな不具合の恐れがあります。つよつよマンはぜひこの糞ザコを正しい情報で殴ってください。理解出来たら反映します。

環境

Python 3.7.4
PyQt5 5.15.0

なぜやろうとした

リストの検索を、前方一致以外で、条件当てはまるものを全て表示させたかった。

どうやったらできた

サンプルコードをどうぞ。
このコードをPyQt5ように書き直したのを基にしている。

# -*- coding: utf-8 -*-

import sys
import os.path

from PyQt5 import QtCore, QtGui, QtWidgets


class UISample(QtWidgets.QDialog):

    def __init__(self, parent=None):
        super(UISample, self).__init__(parent)
        # カスタムUIを作成

        # 1.まずレイアウトひな形を決定?
        layout = QtWidgets.QVBoxLayout()

        # 2.リストの描画部分を用意
        self.listView = QtWidgets.QListView()

        # 3.レイアウトにリストの描画部分を追加
        layout.addWidget(self.listView)

        # 4.リスト項目追加入力欄を用意
        self.lineEdit = QtWidgets.QLineEdit()

        # 5.レイアウトにリスト項目追加入力欄を追加
        layout.addWidget(self.lineEdit)

        # 6.リスト項目検索入力欄の注釈ラベルを用意
        label = QtWidgets.QLabel('↓に入力された文字の列を選択')

        # 7.レイアウトに6で用意したリスト項目検索入力欄の注釈ラベルを追加
        layout.addWidget(label)

        # 8.リスト項目検索入力欄を用意
        self.lineEditB = QtWidgets.QLineEdit()

        # 9.レイアウトにリスト項目検索入力欄を追加
        layout.addWidget(self.lineEditB)

        # 10.レイアウトを用意?
        self.setLayout(layout)

        # ここまでUI作成

        # Model(リストの実体)作成
        self.model = QtCore.QStringListModel()
        self.model.setStringList(['aaa', 'bbb', 'ccc'])
        self.listView.setModel(self.model)


        # Signal-Slot作成
        self.lineEdit.returnPressed.connect(self.addList)
        self.lineEditB.textChanged.connect(self.matchSelect)

    def matchSelect(self):
        # Listから指定の文字の行を探して、見つかったら選択
        txt = self.lineEditB.text()

        if not txt:
            return

        stIndex = self.model.index(0, 0)
        searchIndex = self.model.match(
            stIndex,
            QtCore.Qt.DisplayRole,
            hits = -1,
            flags = (
                QtCore.Qt.MatchContains|
                QtCore.Qt.MatchCaseSensitive|
                QtCore.Qt.MatchWildcard
            )
        )

        print([n.data() for n in searchIndex])


    def addList(self):
        txt = self.lineEdit.text()
        strList = self.model.stringList()
        strList.append(txt)
        self.model.setStringList(strList)
        self.lineEdit.clear()


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    a = UISample()
    a.show()
    sys.exit(app.exec_())

解説

本コードの中核は、以下の部分である

stIndex = self.model.index(0, 0)
    searchIndex = self.model.match(
        stIndex,
        QtCore.Qt.DisplayRole,
        txt,
        hits = -1,
        flags = (
            QtCore.Qt.MatchContains|
            QtCore.Qt.MatchCaseSensitive|
            QtCore.Qt.MatchWildcard
        )

self.model.match以下の変数がmacthをいじろうとした初学者が見慣れた3つでなく、2つ増えていることが分かるだろう。

hits

Pyside2公式ドキュメントのQtCore.QAbstractItemModel.match曰く、hits

If you want to search for all matching items, use hits = -1.
邦訳:すべての一致する項目を検索したい場合は, hits = -1 を利用します.

とのことである。そのため―1を指定することによって本懐(一致するものすべて出す)を遂げられるわけである。
なお、デフォルトは1のようだ。

flags:意味と値

次に、flagsは何を意味するのだろうか。
先ほどの曰く、

The way the search is performed is defined by the flags given.
邦訳:検索の実行方法は、指定されたフラグによって定義されます。

とのことだ。指定すべき値はPyside2公式ドキュメントの.PySide2.QtCore.Qt.MatchFlagに記載されている。
原文はリンクに参照していただくとして翻訳を掲載する。
米印で所感も載せる。

変数 情報
QtCore.Qt.MatchCaseSensitive 大文字と小文字を区別して検索
QtCore.Qt.MatchContains 検索語が項目のどこかに一致する
QtCore.Qt.MatchStartsWith 検索語が項目の先頭に一致する
QtCore.Qt.MatchEndsWith 検索語が項目の最後に一致する
QtCore.Qt.MatchExactly QVariantベースのマッチングを実行
※事実上、文字以外のものを文字列データとして以外で探す方法?
QtCore.Qt.MatchFixedString 文字列ベースのマッチングを実行。
文字列ベースの比較は、
MatchCaseSensitive フラグが指定されていない限り、
大文字小文字を区別しない。
QtCore.Qt.MatchRegExp 正規表現を検索語として使用し、
文字列ベースのマッチングを実行。
QtCore.Qt.MatchWildcard 検索語としてワイルドカード*付きの文字列を使用して、
文字列ベースのマッチングを実行。
QtCore.Qt.MatchRecursive 階層全体を検索
QtCore.Qt.MatchWrap 回り込み検索を実行し、
検索がモデル内の最後の項目に到達したときに、
最初の項目から再び開始し、
すべての項目が検査されるまで継続。
※途中からのスキャンに有用

この中から相反するもの以外のいずれか1つか複数を選択し、引数とする。

優先関係の例は以下の通り。

A B 優先関係 考察
MatchContains MatchWildcard MatchWildcard Wildcardの方が機能が広いから?
MatchStartsWith MatchWildcard MatchStartsWith 分からん。
MatchEndsWith MatchWildcard MatchEndsWith 分からん。
MatchStartsWith MatchEndsWith MatchStartsWith 後ろから入力はできないからか?

2個以上の条件を重ねる場合は二個づつ解決していけばよい。
たとえば、A、B、Cとあるばあい、A、Bの関係を考え、残った方とCで考える。
MatchStartsWith、MatchEndsWith、MatchWildcardで考えるならば、まず、
MatchStartsWith、MatchEndsWithを考える。表を見るとMatchStartsWithが勝つとわかる。
次に、勝ち残ったMatchStartsWithとMatchWildcardを考える。表を見るとMatchStartsWithが勝つとわかる。
こうして、MatchStartsWithが優勝し、条件として実働する。

まだ検証してないが、
もしかしたら一緒に指定するとエラーになるものもあるかもしれないし、
Pyside2に合ってPyQt5にない条件もあるかもしれない。

frags:実際に値を指定する

例えば、ただ、検索語が項目のどこかに一致するものを選択したい場合、
flags = QtCore.Qt.MatchContainsとする。
複数の条件を指定する場合は|で区切る。始端終端に|がある事は許可されないので、条件の間にのみに|を挿入すること。
(Pythonのリストみたいに全行にカンマを入れたりはできないという事だ。)


flags = QtCore.Qt.MatchContains|QtCore.Qt.MatchCaseSensitive

一行に書き並べるとそれはそれは長くなるので、サンプルコードよろしくカッコに囲んだうえで改行するとよい。
なお、|は前後どちらでもよいが、先ほどの注意、「始端終端には|はダメ」を守ろう。

flags = (
    QtCore.Qt.MatchContains|
    QtCore.Qt.MatchCaseSensitive|
    QtCore.Qt.MatchWildcard
)

この場合は、ある行の条件を無効化といったことができる。
開発中挙動の調節に役立つだろう。もしくはこのような記事を作るときに。

例:Qt.MatchCaseSensitiveを無効化

flags = (
    QtCore.Qt.MatchContains|
    #QtCore.Qt.MatchCaseSensitive|
    QtCore.Qt.MatchWildcard
)

ただし、「始端終端には|はダメ」は変わらず守らなくてはならない。

終わりに

以上でやり方の説明を終わる。
免責のとおり、責任は持てないがソースは示したので何かトラブルがあった時はそっちをさかのぼってみてほしい。
改善案があれば寄せてほしい。この記事があなたのコーディングの一助になればうれしい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonのQt(PyQt5)におけるQtCore.QAbstractItemModel.matchの調査まとめ~サンプルコードを添えて~

免責事項

本記事の内容を基にして不具合が発生した場合これの責任は一切持ちません。
本記事はPyside2の公式ドキュメントを参考に英語貧者が頑張って翻訳しながらコーディングしたことのまとめなため無駄等々いろんな不具合の恐れがあります。つよつよマンはぜひこの糞ザコを正しい情報で殴ってください。理解出来たら反映します。

環境

Python 3.7.4
PyQt5 5.15.0

なぜやろうとした

リストの検索を、前方一致以外で、条件当てはまるものを全て表示させたかった。

どうやったらできた

サンプルコードをどうぞ。
このコードをPyQt5用に書き直したのを基にしている。

# -*- coding: utf-8 -*-

import sys
import os.path

from PyQt5 import QtCore, QtGui, QtWidgets


class UISample(QtWidgets.QDialog):

    def __init__(self, parent=None):
        super(UISample, self).__init__(parent)
        # カスタムUIを作成

        # 1.まずレイアウトひな形を決定?
        layout = QtWidgets.QVBoxLayout()

        # 2.リストの描画部分を用意
        self.listView = QtWidgets.QListView()

        # 3.レイアウトにリストの描画部分を追加
        layout.addWidget(self.listView)

        # 4.リスト項目追加入力欄を用意
        self.lineEdit = QtWidgets.QLineEdit()

        # 5.レイアウトにリスト項目追加入力欄を追加
        layout.addWidget(self.lineEdit)

        # 6.リスト項目検索入力欄の注釈ラベルを用意
        label = QtWidgets.QLabel('↓に入力された文字の列を選択')

        # 7.レイアウトに6で用意したリスト項目検索入力欄の注釈ラベルを追加
        layout.addWidget(label)

        # 8.リスト項目検索入力欄を用意
        self.lineEditB = QtWidgets.QLineEdit()

        # 9.レイアウトにリスト項目検索入力欄を追加
        layout.addWidget(self.lineEditB)

        # 10.レイアウトを用意?
        self.setLayout(layout)

        # ここまでUI作成

        # Model(リストの実体)作成
        self.model = QtCore.QStringListModel()
        self.model.setStringList(['aaa', 'bbb', 'ccc'])
        self.listView.setModel(self.model)


        # Signal-Slot作成
        self.lineEdit.returnPressed.connect(self.addList)
        self.lineEditB.textChanged.connect(self.matchSelect)

    def matchSelect(self):
        # Listから指定の文字の行を探して、見つかったら選択
        txt = self.lineEditB.text()

        if not txt:
            return

        stIndex = self.model.index(0, 0)
        searchIndex = self.model.match(
            stIndex,
            QtCore.Qt.DisplayRole,
            hits = -1,
            flags = (
                QtCore.Qt.MatchContains|
                QtCore.Qt.MatchCaseSensitive|
                QtCore.Qt.MatchWildcard
            )
        )

        print([n.data() for n in searchIndex])


    def addList(self):
        txt = self.lineEdit.text()
        strList = self.model.stringList()
        strList.append(txt)
        self.model.setStringList(strList)
        self.lineEdit.clear()


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    a = UISample()
    a.show()
    sys.exit(app.exec_())

解説

本コードの中核は、以下の部分である

stIndex = self.model.index(0, 0)
    searchIndex = self.model.match(
        stIndex,
        QtCore.Qt.DisplayRole,
        txt,
        hits = -1,
        flags = (
            QtCore.Qt.MatchContains|
            QtCore.Qt.MatchCaseSensitive|
            QtCore.Qt.MatchWildcard
        )

self.model.match以下の変数が、
macthをいじろうとした初学者にとって見慣れた3つでなく、2つ増えていることが分かるだろう。
この増えた2つについて本記事ではなるべく丁寧に解説する。

hits

Pyside2公式ドキュメントのQtCore.QAbstractItemModel.match曰く、hits

If you want to search for all matching items, use hits = -1.
邦訳:すべての一致する項目を検索したい場合は, hits = -1 を利用します.

とのことである。そのため―1を指定することによって本懐(一致するものすべて出す)を遂げられるわけである。
なお、デフォルトは1のようだ。

flags:意味と値

次に、flagsは何を意味するのだろうか。
先ほどの曰く、

The way the search is performed is defined by the flags given.
邦訳:検索の実行方法は、指定されたフラグによって定義されます。

とのことだ。指定すべき値はPyside2公式ドキュメントの.PySide2.QtCore.Qt.MatchFlagに記載されている。
原文はリンクに参照していただくとして翻訳を掲載する。
米印で所感も載せる。

変数 情報
QtCore.Qt.MatchCaseSensitive 大文字と小文字を区別して検索
QtCore.Qt.MatchContains 検索語が項目のどこかに一致する
QtCore.Qt.MatchStartsWith 検索語が項目の先頭に一致する
QtCore.Qt.MatchEndsWith 検索語が項目の最後に一致する
QtCore.Qt.MatchExactly QVariantベースのマッチングを実行
※事実上、文字以外のものを文字列データとして以外で探す方法?
QtCore.Qt.MatchFixedString 文字列ベースのマッチングを実行。
文字列ベースの比較は、
MatchCaseSensitive フラグが指定されていない限り、
大文字小文字を区別しない。
QtCore.Qt.MatchRegExp 正規表現を検索語として使用し、
文字列ベースのマッチングを実行。
QtCore.Qt.MatchWildcard 検索語としてワイルドカード*付きの文字列を使用して、
文字列ベースのマッチングを実行。
QtCore.Qt.MatchRecursive 階層全体を検索
QtCore.Qt.MatchWrap 回り込み検索を実行し、
検索がモデル内の最後の項目に到達したときに、
最初の項目から再び開始し、
すべての項目が検査されるまで継続。
※途中からのスキャンに有用

この中から相反するもの以外のいずれか1つか複数を選択し、引数とする。

優先関係の例は以下の通り。

A B 優先関係 考察
MatchContains MatchWildcard MatchWildcard Wildcardの方が機能が広いから?
MatchStartsWith MatchWildcard MatchStartsWith 分からん。
MatchEndsWith MatchWildcard MatchEndsWith 分からん。
MatchStartsWith MatchEndsWith MatchStartsWith 後ろから入力はできないからか?

2個以上の条件を重ねる場合は二個づつ解決していけばよい。
たとえば、A、B、Cとあるばあい、A、Bの関係を考え、残った方とCで考える。
MatchStartsWith、MatchEndsWith、MatchWildcardで考えるならば、まず、
MatchStartsWith、MatchEndsWithを考える。表を見るとMatchStartsWithが勝つとわかる。
次に、勝ち残ったMatchStartsWithとMatchWildcardを考える。表を見るとMatchStartsWithが勝つとわかる。
こうして、MatchStartsWithが優勝し、条件として実働する。

まだ検証してないが、
もしかしたら一緒に指定するとエラーになるものもあるかもしれないし、
Pyside2に合ってPyQt5にない条件もあるかもしれない。

frags:実際に値を指定する

例えば、ただ、検索語が項目のどこかに一致するものを選択したい場合、
flags = QtCore.Qt.MatchContainsとする。
複数の条件を指定する場合は|で区切る。始端終端に|がある事は許可されないので、条件の間にのみに|を挿入すること。
(Pythonのリストみたいに全行にカンマを入れたりはできないという事だ。)


flags = QtCore.Qt.MatchContains|QtCore.Qt.MatchCaseSensitive

一行に書き並べるとそれはそれは長くなるので、サンプルコードよろしくカッコに囲んだうえで改行するとよい。
なお、|は前後どちらでもよいが、先ほどの注意、「始端終端には|はダメ」を守ろう。

flags = (
    QtCore.Qt.MatchContains|
    QtCore.Qt.MatchCaseSensitive|
    QtCore.Qt.MatchWildcard
)

この場合は、ある行の条件を無効化といったことができる。
開発中挙動の調節に役立つだろう。もしくはこのような記事を作るときに。

例:Qt.MatchCaseSensitiveを無効化

flags = (
    QtCore.Qt.MatchContains|
    #QtCore.Qt.MatchCaseSensitive|
    QtCore.Qt.MatchWildcard
)

ただし、「始端終端には|はダメ」は変わらず守らなくてはならない。

終わりに

以上でやり方の説明を終わる。
免責のとおり、責任は持てないがソースは示したので何かトラブルがあった時はそっちをさかのぼってみてほしい。
改善案があれば寄せてほしい。この記事があなたのコーディングの一助になればうれしい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Opencvについて④

Opencv3系についてのメモ

基本的には公式ドキュメントを確認しながらの物。

メモについて
Opencvについて①
Opencvについて②
Opencvについて③

環境・使用画像は前回と同様。


①2値化(単純な閾値処理)

一言でいうと白黒画像
『0』か『1』下で画像を表す。

opencv.ipynb
import cv2
import numpy as np

cap_dir = niku.png
img = cv2.imread(cap_dir,0)
threshold = 100
ret,th1 = cv2.threshold(img,threshold,255,cv2.THRESH_BINARY)
ret,th2 = cv2.threshold(img,threshold,255,cv2.THRESH_BINARY_INV)
ret,th3 = cv2.threshold(img,threshold,255,cv2.THRESH_TRUNC)
ret,th4 = cv2.threshold(img,threshold,255,cv2.THRESH_TOZERO)
ret,th5 = cv2.threshold(img,threshold,255,cv2.THRESH_TOZERO_INV)

cv2.imshow("THRESH_BINARY",th1)
cv2.imshow("THRESH_BINARY_INV",th2)
cv2.imshow("THRESH_TRUNC",th3)
cv2.imshow("THRESH_TOZERO",th4)
cv2.imshow("THRESH_TOZERO_INV",th5)

cv2.waitKey(0)
cv2.destroyAllWindows()

cv2.threshold(第1引数,第2引数,第3引数,第4引数)

第1引数:入力画像(グレースケール画像)
第2引数:しきい値
第3引数:最大値(入力画像の数値が0~255なので255でよいかと思う)
第4引数:しきい値処理フラグ

cv2.thresholdは2つの値を返すので
ret,th1 = cv2.threshold(img,threshold,255,cv2.THRESH_BINARY)となっており
th1側に処理された後の画像(2値画像)が格納されます。

しきい値を100にしている場合は以下の様な画像になる。

スクリーンショット (365).png


しきい値を150にしている場合は以下の様な画像になる。

スクリーンショット (366).png

上規の画像は分かりにくいと思うのでここに公式の2値化画像があるので確認して下さい。

輪郭検出を行うのであれば必須と思われます。

ちなみにカラー画像でも試してみましたが処理はできますが多分使えないと思います。


②大津の2値化

opencv.ipynb
img = cv2.imread(cap_dir,0)
ret2,img_o = cv2.threshold(img,0,255,cv2.THRESH_OTSU)

cv2.imshow("niku",img)
cv2.imshow("niku_othu",img_o)
cv2.waitKey(0)
cv2.destroyAllWindows()

cv2.threshold(第1引数,第2引数,第3引数,第4引数)

第1引数:入力画像(グレースケール画像)
第2引数:しきい値
第3引数:最大値(入力画像の数値が0~255なので255でよいかと思う)
第4引数:しきい値処理フラグ(ここではcv2.THRESH_OTSU)

しきい値に関しては0にする。との公式表記があります。

正直どういった状況で大津の技法を使用するのかよくわかりません。
これに関しては某e-learningで触った程度です。

実際に使ってみた感じで行くとthreshold(閾値)が自動変化するので
背景色が単色で且つ、粒子等の輪郭検出を行うには楽でした。

ただ、上記は確認画像が安定している事が前提なので
安定していないとかなりおかしな輪郭検出となりました。

なのでTHRESH_BINARYをデフォルトと思って
処理したい画像に応じて変更いていくのが吉と感じました。

スクリーンショット (369).png


③2値化+トラックバー

opencv.ipynb
def onTrackbar(position):
    global threshold
    threshold = position

cv2.namedWindow("img")
threshold = 100
cv2.createTrackbar("threshold","img",threshold,255,onTrackbar)

while True:
    ret,img_th = cv2.threshold(img,threshold,255,cv2.THRESH_BINARY)
    cv2.imshow("img",img_th)
    if cv2.waitKey(10) == 27:
        break
cv2.destroyAllWindows()

cv2.createTrackbar(第1引数、第2引数,第3引数,第4引数,第5引数)
第1引数:トラックバー名称
第2引数:第1引数で指定したトラックバーが表示されているウィンドウの名前
第3引数:トラックバーのデフォルト値
第4引数:トラックバーの取りうる最大値
第5引数:トラックバーの位置が変わる度に呼び出されるコールバック関数

第5引数に関してはdef関数

前述のcv2.nameWindow("img")の記載がないとトラックバーは表示されないので注意

スクリーンショット (413).png
スクリーンショット (410).png
スクリーンショット (411).png

トラックバーにてthreshold値を変化させて、画像の変化が見られて楽しいです。
2値化の引数を変えても楽しめます。


④まとめ

多分2値化をある程度使えないと、輪郭検出はしんどいと思いました。
公式ドキュメントは神

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jupyter NotebookでTab補完が出来ない。。。原因はJedi(ジェダイ)のフォースが暗黒面に。。。(嘘)

事の経緯

筆者はPythonで開発を行う時にPipenvで環境の切り分けを毎回行っています。最初は面倒くさいと思っていましたが、慣れるととても便利で気に入っています。先日Jupyter Notebookを使える環境を作って喜々としてJupyterを起動してNotebookを作成しました。ところが、怪しい挙動に見舞われました。ライブラリをインポート使用とimpoと入力してTabキーを打鍵してもTab補完が出来ません。。。

jupyter

原因の究明

最初はPC内の処理が重くてなかなk補完が出ないのかと思っていましたが、待てど暮らせど出て来ません。Jupyterを再起動しようとターミナルに戻ると以下の様なエラーが表示されていました。

実際のエラー表示
[I 14:14:31.852 NotebookApp] Jupyter Notebook 6.2.0 is running at:
[I 14:14:31.852 NotebookApp] http://localhost:8888/?token=c5ac034a93761e5e38d0b6043e3f51aa61ede0947a935dbb
[I 14:14:31.852 NotebookApp]  or http://127.0.0.1:8888/?token=c5ac034a93761e5e38d0b6043e3f51aa61ede0947a935dbb
[I 14:14:31.852 NotebookApp] サーバを停止し全てのカーネルをシャットダウンするには Control-C を使って下さい(確認をスキップするには2回)[C 14:14:31.889 NotebookApp] 

    To access the notebook, open this file in a browser:
        file:///Users/user/Library/Jupyter/runtime/nbserver-21142-open.html
    Or copy and paste one of these URLs:
        http://localhost:8888/?token=c5ac034a93761e5e38d0b6043e3f51aa61ede0947a935dbb
     or http://127.0.0.1:8888/?token=c5ac034a93761e5e38d0b6043e3f51aa61ede0947a935dbb
[I 14:14:38.996 NotebookApp] Creating new notebook in 
[I 14:14:40.717 NotebookApp] Kernel started: 42a9eb0c-a578-49c8-980d-c71bf41f9273, name: python3
[IPKernelApp] ERROR | Exception in message handler:
Traceback (most recent call last):
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/ipykernel/kernelbase.py", line 265, in dispatch_shell
    yield gen.maybe_future(handler(stream, idents, msg))
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/tornado/gen.py", line 762, in run
    value = future.result()
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/tornado/gen.py", line 234, in wrapper
    yielded = ctx_run(next, result)
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/ipykernel/kernelbase.py", line 580, in complete_request
    matches = yield gen.maybe_future(self.do_complete(code, cursor_pos))
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/ipykernel/ipkernel.py", line 356, in do_complete
    return self._experimental_do_complete(code, cursor_pos)
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/ipykernel/ipkernel.py", line 381, in _experimental_do_complete
    completions = list(_rectify_completions(code, raw_completions))
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/IPython/core/completer.py", line 484, in rectify_completions
    completions = list(completions)
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/IPython/core/completer.py", line 1818, in completions
    for c in self._completions(text, offset, _timeout=self.jedi_compute_type_timeout/1000):
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/IPython/core/completer.py", line 1861, in _completions
    matched_text, matches, matches_origin, jedi_matches = self._complete(
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/IPython/core/completer.py", line 2029, in _complete
    completions = self._jedi_matches(
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/IPython/core/completer.py", line 1373, in _jedi_matches
    interpreter = jedi.Interpreter(
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/jedi/api/__init__.py", line 725, in __init__
    super().__init__(code, environment=environment,
TypeError: __init__() got an unexpected keyword argument 'column'
[I 14:16:40.863 NotebookApp] Saving file at /Untitled.ipynb

ネットで色々と調べて見たところGitHub上で議論されていました。議論によるとNotebookで補完を担っているJedi(ジェダイ)の最新版(Ver.0.18.0)に問題(バグ)が有る様です。Jedi Ver.0.17.2を利用すれば現在の所バグは発生しない様です。

実際にJedi Ver.0.17.2をインストールしてみる

実際にJedi Ver.0.17.2をインストールして再度Jupyter Notebookを起動してみます。筆者の環境ではPipenvを用いているのでpipenv install jedi==バージョン番号でインストール出来ました。pipなど別のコマンドを用いてる場合はそのコマンドに合わせたインストールコマンドを実行します。

pipenv install jedi==0.17.2

Jupyter Notebookの再起動

Jupyter Notebookを再起動します。そしてTab補完を試しました。今度は正しく補完されました。エラーも出ていません。

jupyter restart

[I 14:29:00.768 NotebookApp] Jupyter Notebook 6.2.0 is running at:
[I 14:29:00.768 NotebookApp] http://localhost:8888/?token=1060ea919a5036907778b675b60638d9ec208ebee7a9103d
[I 14:29:00.768 NotebookApp]  or http://127.0.0.1:8888/?token=1060ea919a5036907778b675b60638d9ec208ebee7a9103d
[I 14:29:00.768 NotebookApp] サーバを停止し全てのカーネルをシャットダウンするには Control-C を使って下さい(確認をスキップするには2回)[C 14:29:00.790 NotebookApp] 

    To access the notebook, open this file in a browser:
        file:///Users/user/Library/Jupyter/runtime/nbserver-22055-open.html
    Or copy and paste one of these URLs:
        http://localhost:8888/?token=1060ea919a5036907778b675b60638d9ec208ebee7a9103d
     or http://127.0.0.1:8888/?token=1060ea919a5036907778b675b60638d9ec208ebee7a9103d
[W 14:29:05.876 NotebookApp] Notebook Untitled.ipynb is not trusted
[I 14:29:06.199 NotebookApp] Kernel started: aaeb58b6-3e67-47ac-90bb-958888bb6793, name: python3

これで解決しました。タイトルはJedi(ジェダイ)ライブラリに起因したバグだったのでスターウォーズネタに絡めた単なる洒落です。。。決して以下の人物とは関係ありません。。。

Darth Vader
画像はWikipediaより

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jupyter NotebookでTab補完が出来ない。。。原因はJediのフォースが暗黒面に。。。(嘘)

事の経緯

筆者はPythonで開発を行う時にPipenvで環境の切り分けを毎回行っています。最初は面倒くさいと思っていましたが、慣れるととても便利で気に入っています。先日Jupyter Notebookを使える環境を作って喜々としてJupyterを起動してNotebookを作成しました。ところが、怪しい挙動に見舞われました。ライブラリをインポート使用とimpoと入力してTabキーを打鍵してもTab補完が出来ません。。。

jupyter

原因の究明

最初はPC内の処理が重くてなかなk補完が出ないのかと思っていましたが、待てど暮らせど出て来ません。Jupyterを再起動しようとターミナルに戻ると以下の様なエラーが表示されていました。

実際のエラー表示
[I 14:14:31.852 NotebookApp] Jupyter Notebook 6.2.0 is running at:
[I 14:14:31.852 NotebookApp] http://localhost:8888/?token=c5ac034a93761e5e38d0b6043e3f51aa61ede0947a935dbb
[I 14:14:31.852 NotebookApp]  or http://127.0.0.1:8888/?token=c5ac034a93761e5e38d0b6043e3f51aa61ede0947a935dbb
[I 14:14:31.852 NotebookApp] サーバを停止し全てのカーネルをシャットダウンするには Control-C を使って下さい(確認をスキップするには2回)[C 14:14:31.889 NotebookApp] 

    To access the notebook, open this file in a browser:
        file:///Users/user/Library/Jupyter/runtime/nbserver-21142-open.html
    Or copy and paste one of these URLs:
        http://localhost:8888/?token=c5ac034a93761e5e38d0b6043e3f51aa61ede0947a935dbb
     or http://127.0.0.1:8888/?token=c5ac034a93761e5e38d0b6043e3f51aa61ede0947a935dbb
[I 14:14:38.996 NotebookApp] Creating new notebook in 
[I 14:14:40.717 NotebookApp] Kernel started: 42a9eb0c-a578-49c8-980d-c71bf41f9273, name: python3
[IPKernelApp] ERROR | Exception in message handler:
Traceback (most recent call last):
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/ipykernel/kernelbase.py", line 265, in dispatch_shell
    yield gen.maybe_future(handler(stream, idents, msg))
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/tornado/gen.py", line 762, in run
    value = future.result()
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/tornado/gen.py", line 234, in wrapper
    yielded = ctx_run(next, result)
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/ipykernel/kernelbase.py", line 580, in complete_request
    matches = yield gen.maybe_future(self.do_complete(code, cursor_pos))
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/ipykernel/ipkernel.py", line 356, in do_complete
    return self._experimental_do_complete(code, cursor_pos)
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/ipykernel/ipkernel.py", line 381, in _experimental_do_complete
    completions = list(_rectify_completions(code, raw_completions))
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/IPython/core/completer.py", line 484, in rectify_completions
    completions = list(completions)
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/IPython/core/completer.py", line 1818, in completions
    for c in self._completions(text, offset, _timeout=self.jedi_compute_type_timeout/1000):
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/IPython/core/completer.py", line 1861, in _completions
    matched_text, matches, matches_origin, jedi_matches = self._complete(
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/IPython/core/completer.py", line 2029, in _complete
    completions = self._jedi_matches(
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/IPython/core/completer.py", line 1373, in _jedi_matches
    interpreter = jedi.Interpreter(
  File "/Users/user/.local/share/virtualenvs/test-MRRjzm7v/lib/python3.8/site-packages/jedi/api/__init__.py", line 725, in __init__
    super().__init__(code, environment=environment,
TypeError: __init__() got an unexpected keyword argument 'column'
[I 14:16:40.863 NotebookApp] Saving file at /Untitled.ipynb

ネットで色々と調べて見たところGitHub上で議論されていました。議論によるとNotebookで補完を担っているJedi(ジェダイ)の最新版(Ver.0.18.0)に問題(バグ)が有る様です。Jedi Ver.0.17.2を利用すれば現在の所バグは発生しない様です。

実際にJedi Ver.0.17.2をインストールしてみる

実際にJedi Ver.0.17.2をインストールして再度Jupyter Notebookを起動してみます。筆者の環境ではPipenvを用いているのでpipenv install jedi==バージョン番号でインストール出来ました。pipなど別のコマンドを用いてる場合はそのコマンドに合わせたインストールコマンドを実行します。

pipenv install jedi==0.17.2

Jupyter Notebookの再起動

Jupyter Notebookを再起動します。そしてTab補完を試しました。今度は正しく補完されました。エラーも出ていません。

jupyter restart

[I 14:29:00.768 NotebookApp] Jupyter Notebook 6.2.0 is running at:
[I 14:29:00.768 NotebookApp] http://localhost:8888/?token=1060ea919a5036907778b675b60638d9ec208ebee7a9103d
[I 14:29:00.768 NotebookApp]  or http://127.0.0.1:8888/?token=1060ea919a5036907778b675b60638d9ec208ebee7a9103d
[I 14:29:00.768 NotebookApp] サーバを停止し全てのカーネルをシャットダウンするには Control-C を使って下さい(確認をスキップするには2回)[C 14:29:00.790 NotebookApp] 

    To access the notebook, open this file in a browser:
        file:///Users/user/Library/Jupyter/runtime/nbserver-22055-open.html
    Or copy and paste one of these URLs:
        http://localhost:8888/?token=1060ea919a5036907778b675b60638d9ec208ebee7a9103d
     or http://127.0.0.1:8888/?token=1060ea919a5036907778b675b60638d9ec208ebee7a9103d
[W 14:29:05.876 NotebookApp] Notebook Untitled.ipynb is not trusted
[I 14:29:06.199 NotebookApp] Kernel started: aaeb58b6-3e67-47ac-90bb-958888bb6793, name: python3

これで解決しました。タイトルはJedi(ジェダイ)ライブラリに起因したバグだったのでスターウォーズネタに絡めた単なる洒落です。。。決して以下の人物とは関係ありません。。。

Darth Vader
画像はWikipediaより

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonとRCONでMinecraft自動湧き潰し

はじめに

きっかけ

というものを知ったので試したくなった。

できたもの

torching_s.gif

環境

  • Docker version 19.03.12, build 48a66213fe
  • docker-compose version 1.26.2, build eefe0d31
  • spigot_server-1.16.5
  • Python 3.8.3

mcrconの導入

$ python -m pip install mcrcon

マイクラサーバー

docker-compose.yml
version: "3"

services:
  mc:
    image: itzg/minecraft-server
    ports:
      - "25565:25565"
      - "127.0.0.1:25575:25575"
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - ./data:/data
    environment:
      EULA: "TRUE"
      TYPE: "SPIGOT"
      MAX_MEMORY: "4G"
      ENABLE_RCON: "true"
      RCON_PASSWORD: "testing"
      RCON_PORT: 25575
    tty: true
    stdin_open: true
    restart: always

湧き潰し自動化

やりたかったこと

  • プレイヤーが左手に松明を持って草ブロックの上にいるときだけ実行
  • プレイヤーの動きに合わせてプレイヤーの周囲にだけ松明を設置
  • 草ブロックの上にだけ松明を設置
  • 6マスおきに松明を設置(引数で変更可能)
  • 一度に松明2本分先までを湧き潰し(引数で変更可能)
  • 段差があっても上下1段までは地面を探索して松明を設置(引数で変更可能)

完成形

torching.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import re
import time
from mcrcon import MCRcon


def get_settings():
    """
    Get info for connecting rcon from the env.

    Returns
    -------
    (host, passwd, port) : (str, str, int)
    """

    host = os.getenv("RCON_HOST")
    if host is None:
        host = "localhost"

    passwd = os.getenv("RCON_PASSWORD")
    if passwd is None:
        passwd = "testing"

    port = os.getenv("RCON_PORT")
    if port is None:
        port = "25575"

    return host, passwd, int(port)


class Torching(object):

    def __init__(self, mcr, mod=6, dn=2, dy_max=1):
        """
        Parmeters
        ---------
        mcr : connected MCRcon

        mod : int, optional (default=6)
            何マスおきに松明を設置するか

        dn : int, optional (default=2)
            一度にplayerの周囲何本分先まで松明を設置するか

        dy_max : int, optional (default=1)
            段差があるときに上下どこまで追加で判定するか

        Note
        ----
        1人のplayerだけがloginしている状況を想定
        """
        self.mcr = mcr
        self.mod = mod
        self.dn = dn
        self.dy_max = dy_max

        # '{player} has the following entity data: "minecraft:{data}"'
        # から{data}を抜き出す正規表現
        self.entity = re.compile(r'(?<=minecraft:).*(?=")')

        # '{player} has the following entity data: [{x}d, {y}d, {z}d]'
        # から{x}d, {y}d, {z}dを抜き出す正規表現
        self.position = re.compile(r"(?<=\[).*(?=\])")

    def _run(self, cmd, debug=False):
        """
        Run the minecraft command.

        Parameters
        ----------
        cmd : str

        debug : bool, optional (default=False)
            If True, print the cmd and the response.
        """
        res = self.mcr.command(cmd)
        if debug:
            print(f"cmd: {cmd}")
            print(f"response: {res}")
        return res

    def what_in_the_left(self):
        """
        Return what the player have in the left hand.

        Returns
        -------
        block_name : str
        """

        res = self._run("data get entity @p Inventory[{Slot:-106b}].id")
        if res == "No entity was found":
            raise SystemExit(res)
        if res.startswith("Found no elements"):
            # empty
            return ""
        block_name = self.entity.search(res).group(0)
        return block_name

    def get_world_name(self):
        """
        Return the current world name in which the player is.

        Returns
        -------
        wn : str
        """

        res = self._run("data get entity @p Dimension")
        if res == "No entity was found":
            raise SystemExit(res)
        wn = self.entity.search(res).group(0)
        return wn

    def get_current_position(self):
        """
        Return the current player position.

        Returns
        -------
        (xp, yp, zp) : (float, float, float)
        """

        res = self._run("data get entity @p Pos")
        if res == 'No entity was found':
            raise SystemExit(res)
        pos = self.position.search(res).group(0)

        # "{x}d, {y}d, {z}d"からdを取り除いて,で分割
        pos_array = pos.replace("d", "").split(",")

        # float <- str
        xp, yp, zp = [float(s.strip()) for s in pos_array]
        return xp, yp, zp

    def on_the_ground(self, wn, x, y, z):
        """
        Check whether (x, y, z) is on the ground.

        Parameters
        ----------
        wn : int
            world name

        x : int

        y : int

        z : int

        Returns
        -------
        out : bool
        """
        # 1個下が草ブロックなら
        res = self._run(f"execute in minecraft:{wn} if block {x} {y-1} {z} minecraft:grass_block")
        if res == 'Test passed':
            return True
        else:
            return False

    def _set_torch(self, wn, x, y, z):
        """
        Set torch if (x, y, z) is air and (x, y-1, z) is grass block.

        Parameters
        ----------
        wn : str

        x : int

        y : int

        z : int

        Returns
        -------
        out : int
            0 - success
            1 - (x, y, z) is not air, you should try on upper.
           -1 - (x, y, z) is air but not on the ground, you should try on lower.
        """

        res = self._run(f"execute in minecraft:{wn} if block {x} {y} {z} minecraft:torch")
        if res == 'Test passed':
            # already exists
            return 0

        # 草が生えていたら十分条件
        res = self._run(f"execute in minecraft:{wn} if block {x} {y} {z} minecraft:grass run setblock {x} {y} {z} minecraft:torch")
        if res.startswith("Changed the block"):
            return 0

        res = self._run(f"execute in minecraft:{wn} if block {x} {y} {z} minecraft:air")
        if res == 'Test passed':

            # 空気ブロックかつ一個下が草ブロックなら
            res = self._run(f"execute in minecraft:{wn} if block {x} {y-1} {z} minecraft:grass_block run setblock {x} {y} {z} minecraft:torch")
            if res.startswith("Changed the block"):
                return 0

            else:
                # 空気ブロックだが松明を置けなかった場合
                return -1

        else:
            # 空気ブロックじゃない場合
            return 1

    def search_ground_and_set(self, wn, x, y, z):
        """
        (x, y, z)に松明を設置
        設置できなかった場合は{self.dy_max}の範囲で上下方向に地面を探索して設置

        Parameters
        ----------
        x : int

        y : int

        z : int
        """

        res = self._set_torch(wn, x, y, z)
        if res == 0:
            # success
            return

        # try to find the ground and set torch
        #   bit = 1  : upward
        #   bit = -1 : downward
        bit = res
        for dy in range(self.dy_max):
            res = self._set_torch(wn, x, y+(bit)*(dy+1), z)
            if res == 0:
                return

    def _exec(self, xp, yp, zp):
        """
        playerが草ブロックの上にいるとき、{self.mod}の倍数の位置にだけ松明を設置
        (ただし、建築の妨害をしないためにpalyerに一番近い場所には設置しない)

        Parammeters
        -----------
        xp : float
            player position 0

        yp : float
            player position 1

        zp : float
            player position 2
        """

        # get the current world name
        wn = self.get_world_name()

        # int <- float
        x, y, z = [int(p) for p in [xp, yp, zp]]
        if not self.on_the_ground(wn, x, y, z):
            # the player is not on the ground, skip
            return

        # get the nearest index
        xi = round(xp/self.mod)
        zj = round(zp/self.mod)

        # walk around the player
        for i in range(-self.dn, self.dn+1):
            for j in range(-self.dn, self.dn+1):
                if i == 0 and j == 0:
                    # skip the nearest one
                    continue

                # back to int
                x = (xi + i)*self.mod
                z = (zj + j)*self.mod

                self.search_ground_and_set(wn, x, y, z)

    def main(self, dt=1.0):
        """
        playerが左手に松明を持っているときにだけ実行

        Parameters
        ----------
        dt : float, optional (default=1.0)
            interval in [sec]
            (小さくしすぎると高負荷)
        """

        # initialize
        xp, yp, zp = 0, 0, 0

        # infinite loop
        while True:

            # take interval
            time.sleep(dt)

            # skip if torch is not in the left
            block_name = self.what_in_the_left()
            if not block_name == "torch":
                continue

            # store the previous value
            xp_old, yp_old, zp_old = xp, yp, zp

            # reload
            xp, yp, zp = self.get_current_position()
            if xp == xp_old and yp == yp_old and zp == zp_old:
                # the player is not moving, skip
                continue

            # playerの周囲に松明を設置
            self._exec(xp, yp, zp)


if __name__ == "__main__":

    with MCRcon(*get_settings()) as mcr:

        torching = Torching(mcr)
        torching.main()

使い方

  • マイクラにログインしてからDockerを動かしているhostで実行(localhostのport 25575に接続)
  • RCONの設定を変更している場合は環境変数から読み込む
    $ RCON_PASSWORD="" RCON_PORT="" python torching.py
  • 無限ループを利用しているので終了するときはマイクラからログアウトするかCtrl+Cで

コマンドの簡単な解説

現在プレイヤーが左手に持っているブロック名を取得

data get entity @p Inventory[{Slot:-106b}].id
('{player} has the following entity data: "minecraft:{block_name}"'の形で返ってくる)

現在プレイヤーがいるワールド名を取得

data get entity @p Dimension
('{player} has the following entity data: "minecraft:{world_name}"'の形で返ってくる)

プレイヤーの現在位置を取得

data get entity @p Pos
('{player} has the following entity data: [{x}d, {y}d, {z}d]'の形で返ってくる)

(x, y, z)座標が特定のブロックかどうか確認

execute in minecraft:{world_name} if block {x} {y} {z} minecraft:{block_name}
(IDが一致すれば"Test passed"が、一致しなければ"Test failed"が返ってくる)

(x, y, z)座標が特定のブロックだった場合に(x', y', z')に特定のブロックを設置

execute in minecraft:{world_name} if block {x} {y} {z} minecraft:{block_name} run setblock {x'} {y'} {z'} minecraft:{block_name'}
(成功すれば'Changed the block at {x'} {y'} {z'}'が返ってくる)

おわりに

  • 動作は確認していますが保証はしません
  • 複数人がログインしている場合の挙動は確認していません(友達がいない)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KeyError: "There is no item named '[Content_Types].xml' in the archive" というエラーが出たら。

開こうとしているファイルが損傷している可能性がある。
以上。

暇なときに記事としての体裁を整える。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む