- 投稿日:2022-01-11T22:27:10+09:00
小ネタ/GitHub Actions の Python 環境で MeCab + NEologd を使う
こちら↓の Q&A サイトで関連する質問の抽出を実装する際に、GitHub Actions(Python)で MeCab + NEologd を使ったので、メモ的に残しておきます。 ワクチン接種証明書アプリ FAQ 参考: DockerでPython公式イメージ + MeCab + NEologd辞書(Takayoshi_Makabe さん) ポイント GitHub Actions で使用する.ymlファイルを記述する際、 cdが使えないので代わりにworking-directoryを指定する /etc/mecabrcに上書きしようとすると権限エラーが出るので、別の場所に書き出しておいたmecabrcを/usr/local/etc/の下にコピーする の 2 点、気を付けるだけです。 .ymlファイルの例 Ubuntu のイメージを使う例です。 data-creator.yml # This is a workflow example with Actions name: mecab neologd example env: TZ: Asia/Tokyo on: schedule: - cron: '00 */3 * * *' workflow_dispatch: jobs: process_data: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: ref: main path: production - name: Setup python uses: actions/setup-python@v2 with: python-version: "3.9" - name: Install packages run: sudo apt-get install -y mecab libmecab-dev mecab-ipadic-utf8 - name: Get MeCab dictionary run: git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git - name: Setup MeCab dictionary run: ./bin/install-mecab-ipadic-neologd -n -y working-directory: ./mecab-ipadic-neologd - name: Setup MeCab configure run: echo dicdir = `mecab-config --dicdir`"/mecab-ipadic-neologd" > mecabrc && sudo cp mecabrc /usr/local/etc/ - name: Install dependencies run: pip install mecab-python3 ※この後ろに具体的な処理を書きます。 実際の使用例 こちら↓で使っています。 GitHub - code-for-hamamatsu / vaccinecert-faq なお、MeCab の処理に関連するコードだけ抜き出したリポジトリ(本体に組み込む前のテストで使ったもの)はこちら↓です。 GitHub - hmatsu47 / vaccinecert-qa-similarity-test
- 投稿日:2022-01-11T21:28:33+09:00
VOICEVOX EngineのCPU/GPU速度比較
はじめに ヒホさんのVOICEVOX Engineを利用してあれこれ遊んでいます。VOICEVOXはこちら。 自分は普段mac miniを使っているのでdocker CPU版を使用しています。古いWindows 10機もあるので今回速度比較を行ってみました。「最近のPCじゃないのか...」と思われた方、すみません。 コード コード中の(音声合成を行う文章)は、下記の青空文庫・若草物語の最初の部分「作者について」を使用しました。文面で言えばこの「若草物語」(原名リツル、ウィメン)は〜一生でありました。までなのですがそのまま載せたらあかんやろな?と思ったのでこういう形にしています。実行するときには同サイトからコピペしてみてください(もちろん、適当な20行くらいの文面ならなんでもいいです)。 VOICEVOX Engineは長い文章を入力するとエラーになるので、「。」で区切って行ごとに処理します。(現在の仕様では、エラーの場合は21byteのデータが返ってきます) test.py from os import replace import subprocess import time import re data=""" (音声合成を行う文章) """ data = data.replace('\n', '') tmp = re.split('[。]', data) start_time = time.time() i=0 for t in tmp: if t == '': continue f = open('text.txt', 'w', encoding='UTF-8') f.write(t) f.close() s = 'curl -s -X POST "localhost:50021/audio_query?speaker=3" --get --data-urlencode text@text.txt > query.json' r = subprocess.run(s, shell=True) s = 'curl -s -H "Content-Type: application/json" -X POST -d @query.json "localhost:50021/synthesis?speaker=3" > audio' + str(i) + '.wav' r = subprocess.run(s, shell=True) i=i+1 tat_time = time.time() - start_time print ("実行時間:{0}".format(tat_time) + "[秒]") (↑当初、localhostのところに実験後のIPアドレスを書いたままにしていましたので修正) OS CPU VIDEO mac mini 2018 Monterey Core i5 3GHz, 6c 内蔵Chipset Windows10 Pro (21H2) Core i7 4770K, 3.5GHz, 4c GeForce GTX 1060 (6GB) WSL2 (Ubuntu 20.04) 同上 同上 ・実行はいずれもVSCodeからF5 ・製品版VOICEVOXはver.0.9.3 ・VOICEVOX Engine docker版は2022年1月3日頃にDLしたもの ・Windows/WSL2ではWindows版dockerプログラムを使用 ・Windows10はMSのInsider Previewとかではなく普通のもの。WSL2のネットワーク安定化のために高速ブート機能はオフ 結果 速い順に並べると WSL2 (docker GPU) > Win10 (docker GPU, IP指定) > Win10 (製品0.9.3 GPU) > Win10 (docker GPU, localhost) >> CPU勢 という感じで、僅差ですがWSL2が最速でした。 環境 動作種別 実行時間(単位:秒) WSL2 docker GPU 51.7, 11.7, 11.5 Win10 docker GPU, IP指定 53.5, 13.3 Win10 製品版0.9.3, GPU 55.5, 21.4, 19.9 Win10 docker GPU, localhost 63.8, 22.1, 22.1 WSL2 docker CPU 87.2 Win10 docker CPU, IP指定 94.7 Win10 docker CPU, localhost 97.6 Win10 製品版0.9.3, CPU 169.2 mac mini docker CPU 174.9 (↑並びが一部間違っていたので修正) 補足・考察 1) GPU版の速度について 表中、実行時間でGPU版は複数回数分を記載していますが、GPU版は「初回と2回目以降で大幅に速度が違う」ことを示すためです(CPU版は毎回ほぼ同じ時間)。 このテストを行う前に、簡単な一行の文章を10回繰り返してWAVを作るプログラムで試していたのですがどうも「6回目くらい?から加速する」「文章を変更したらまた元に戻る」「dockerコンテナを起動・再起動するとまた元に戻る」という特性があると気づきました(「元に戻る」とは加速していない元の速度くらいに戻る、という意味です)。 つまりGPUが初期化されたり、初めての文字(のある程度の塊?)に遭遇したらそのたびに「加速されていないような速度に戻る」のだと思います。 なので「普通の文章がずらっと並んでいる場合に速度メリットが得られるのか?」がめっっっっちゃ気になったので今回のテストを実施したわけです。結果として170秒くらいが50秒くらいになった、つまり「CPUに対してGPUだとざっくり3倍程度の高速化が期待できる」というのが自分の結論です。 (↑もともと使っているmacに対してGTX1060機なら3倍速い、という意味であり、docker CPU/GPUの差は1.6~1.8倍ってところですかね) 2) localhostについて VOICEVOX Engineのdocker版は以下のように実行するよう公式README.mdに書かれています。 CPU docker pull hiroshiba/voicevox_engine:cpu-ubuntu20.04-latest docker run --rm -it -p '127.0.0.1:50021:50021' hiroshiba/voicevox_engine:cpu-ubuntu20.04-latest GPU docker pull hiroshiba/voicevox_engine:nvidia-ubuntu20.04-latest docker run --rm --gpus all -p '127.0.0.1:50021:50021' hiroshiba/voicevox_engine:nvidia-ubuntu20.04-latest また、製品版も実行するとlocalhost:50021で待ち受け状態になります。 つまり基本的にVOICEVOX Engineはlocalhostへのアクセスで実行する前提だと思います。 …なんですが、たぶん自分がWSL2の調整の関係でネットワーク関係をあれこれいらんことをしたからだと思いますが、今回のテスト結果のようにWindowsでlocalhost経由でアクセスすると妙に遅い状態になっています(初日は普通の速度が出ていた(ここで、WSL2がらみでなんかいろいろした)。翌日同じテストをすると2倍以上遅かった。今回のテスト結果は翌々日のもので、2日目より少し早くなっている)。 こりゃ困ったと思ったのですが、以下のように127.0.0.1ではなくWindows機のIPアドレスを指定して起動すると本来の速度に戻りました。 docker run --rm --gpus all -p '192.168.xxx.xxx:50021:50021' hiroshiba/voicevox_engine:nvidia-ubuntu20.04-latest つまりIPアドレスを指定してdockerコンテナを起動した方が速い(というか遅くならない)という結果でした。 一般的にWSL2からlocalhost通信は遮断されるのですがdockerコンテナに対しては問題なくlocalhostでアクセスできます。ちなみにWSL2の場合はlocalhostもIP指定も速度は同じでした。 おまけ VOICEVOX Engineを使ってマストドン自動読み上げプログラムを作ったのですが、現在のWSL2/Ubuntuでは音を鳴らせない(エラーになる)というのが盲点でした(笑)。ただしWSL2からWindowsアプリをしれっと呼び出せるのでなんとかなりました。 Pythonのplaysound、macではなんともないんですが「Windowsではver.1.3.0だとなんやらエラーがでてまともに使えないので1.2.2にすればいい」的な記事があったのですがやはりダメですね。普通に再生するだけなら1.2.2で大丈夫かもしれませんが、音声ファイルを次々に書き換える用途が考慮されていないようです(再生が終わってもファイルをつかんだままらしく、WAVファイルを更新できない)。諦めてwinsoundを使っています。macだとなんともないんですがね... tensorflow-gpu==2.5.0で適当なサンプルを実行するとWSL2の方がWindows10より7〜8%遅かったのですが、今回のように用途によってはWSL2の方が速いこともあるのだな、という発見がありました。 今回のまとめ
- 投稿日:2022-01-11T20:59:34+09:00
Pythonのenumでネストした (枝分かれした) 状態を管理
概要 以前「LINE公式垢でボットとチャット両立させてみた!」という記事を書いた際は、下記のようにenum.Enumを用いて、現在の会話の状態を管理していました。 class Type(Enum): BN_CREATE = auto() BN_CREATE_TRACK1 = auto() BN_CREATE_TRACK2 = auto() BN_CREATE_TRACK3 = auto() BN_CREATE_TRACK5 = auto() ... 本来、BN_CREATE_TRACK*はBN_CREATEから枝分かれした状態を表現したいのですが、それが出来ず、並列的に状態が管理されてしまっています。このネストした状態をPythonの enum.IntFlag ( enum.Flag )を用いて、表現したという記事です。 ネストした(枝分かれした)状態のイメージはこちら (フローチャート上部からメッセージを送っていくが、途中で分岐が起こり、どの状態かを保存しておく必要がありました。) レポジトリはこちら! ネストした状態を並列に表現することの問題 例えば、枝分かれした先の状態も含めて同じ処理をしたい時に、下記のように該当する状態を全て記述する必要がありました。 if ss_type in (Type.BN_CREATE, Type.BN_CREATE_TRACK1, Type.BN_CREATE_TRACK2, Type.BN_CREATE_TRACK3, Type.BN_CREATE_TRACK5): ... enum.IntFlag ( enum.Flag )を用いた解決法 enum.IntFlag ( enum.Flag ) はビット演算子 (&, |, ^, ~) を用いることができる列挙型です。 enum.IntFlag はint型が使えるところでは使える enum.Flag です。下記のような挙動をします。 from enum import Flag, auto class Color(Flag): RED = auto() GREEN = auto() BLUE = auto() PURPLE = RED | BLUE WHITE = RED | GREEN | BLUE def print_color(color): if color == Color.RED: print('Color is red') elif color == Color.GREEN: print('Color is green') elif color == Color.BLUE: print('Color is blue') elif color == Color.PURPLE: print('Color is purple') elif color == Color.WHITE: print('Color is white') else: print('not defined') if __name__ == '__main__': print_color(Color.BLUE) # Color is blue print_color(Color.PURPLE) # Color is purple print_color(Color.RED | Color.BLUE) # Color is purple print_color(Color.RED | Color.GREEN) # not defined print_color(Color.WHITE) # Color is white print_color(Color.RED | Color.GREEN | Color.BLUE) # Color is white (enum 超まとめ python3.10 より) 概要で記述した部分のコードを下記のように変更し、ヘルパ関数も追加しました。(クラス名も変わっていますが、その部分はお気になさらずに) # models/status_type.py class StatusType(IntFlag): _BN_CREATE_TRACK1 = auto() _BN_CREATE_TRACK2 = auto() _BN_CREATE_TRACK3 = auto() _BN_CREATE_TRACK5 = auto() BN_CREATE = auto() BN_CREATE_TRACK1 = BN_CREATE | _BN_CREATE_TRACK1 BN_CREATE_TRACK2 = BN_CREATE | _BN_CREATE_TRACK2 BN_CREATE_TRACK3 = BN_CREATE | _BN_CREATE_TRACK3 BN_CREATE_TRACK5 = BN_CREATE | _BN_CREATE_TRACK5 ... def is_included(parent: StatusType, child: StatusType) -> bool: return parent & child == parent _ が最初に入っていないものを enum.Enum を使っていた際と同じように用います。しかし、このヘルパ関数のおかげで、下記のように記述するだけでネストしたものも含めたい時の記述が簡潔になります。 if is_included(StatusType.BN_CREATE, ss_type): ... 少し雑な気もしますが、このようにネストした状態を列挙型を用いて表現することが出来ました。 何か他に良い方法がありましたら、是非とも教えてください! 参考サイト enum 超まとめ python3.10
- 投稿日:2022-01-11T18:59:26+09:00
BERTの事前学習 Next sentence prediction の実装
from transformers import BertForNextSentencePrediction nsp_bert = BertForNextSentencePrediction.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking') nsp_bert.eval() prompt = '私の家族は5人家族です。' next_sentence = '家族は、父、母、兄、私、妹です。' input_tensor = bert_tokenizer(prompt, next_sentence, return_tensors='pt') print(input_tensor) {'input_ids': tensor([[ 2, 1325, 5, 2283, 9, 76, 53, 2283, 2992, 8, 3, 2283, 9, 6, 800, 6, 968, 6, 1456, 6, 1325, 6, 4522, 2992, 8, 3]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])} list(bert_tokenizer.get_vocab().items())[:5] [('[PAD]', 0), ('[UNK]', 1), ('[CLS]', 2), ('[SEP]', 3), ('[MASK]', 4)] output = nsp_bert(**input_tensor) print(output) NextSentencePredictorOutput(loss=None, logits=tensor([[14.3159, 2.9107]], grad_fn=), hidden_states=None, attentions=None) torch.argmax(output.logits) tensor(0) うまく予測出来ました。 ここまでが簡単な Next sentence prediction の実装です。
- 投稿日:2022-01-11T18:53:38+09:00
Sublime Text Scripts API TIPS 個人用メモ
選択範囲が無い場合、行を選択する。 for region in self.view.sel(): if region.empty(): region = self.view.line(region) region 選択範囲を示すregionは、開始地点と終了地点の数字の入ったオブジェクト region = sublime.Region(0, 10)##Region自作。0文字目から10文字目 print(region) ##regionの開始地点と終了地点の数字取得 reg1 = str(region.begin()) reg2 = str(region.end()) 自作誤字脱字選択肢表示機能 いちいちlinterとかインストールするのとか面倒くさいので自分で作りたいとき。 i=0 ##選択範囲を数字で取得 reg1 = str(region.begin()) reg2 = str(region.end()) ##置き換え文字列 replace = "テスト" ##置き換え文字列用minihtml links = '<a href=\'subl:testtest {"reg1":%s,"reg2":%s,"rep":"%s"}\'>te2d</a>' % (reg1,reg2,replace) # links = '<a href=\'subl:testtest {"reg1":0}\'>tesdt2</a>' ##検索文字列 for region in view.find_all(' '): i =i+1 ##viewへ範囲表示と選択肢部分構築。同じ文字列を建築しても、それぞれ別々でキー設定しないといけないので、forループで構築する。 view.add_regions('HighlightTest'+str(i), [region], "region.redish", "dot", sublime.DRAW_NO_FILL+sublime.DRAW_STIPPLED_UNDERLINE+sublime.DRAW_NO_OUTLINE, [links] )
- 投稿日:2022-01-11T18:53:38+09:00
Sublime Text 自作Plugin Scripts API TIPS 個人用メモ
選択範囲が無い場合、行を取得する。 for region in self.view.sel(): if region.empty(): region = self.view.line(region) region 選択範囲を示すregionは、開始地点と終了地点の数字の入ったオブジェクト region = sublime.Region(0, 10)##Region自作。0文字目から10文字目 print(region) ##regionの開始地点と終了地点の数字取得 reg1 = str(region.begin()) reg2 = str(region.end()) 自作誤字脱字選択肢表示機能 いちいちlinterとかインストールするのとか面倒くさいので自分で作りたいとき。 i=0 ##選択範囲を数字で取得 reg1 = str(region.begin()) reg2 = str(region.end()) ##置き換え文字列 replace = "テスト" ##置き換え文字列用minihtml links = '<a href=\'subl:testtest {"reg1":%s,"reg2":%s,"rep":"%s"}\'>te2d</a>' % (reg1,reg2,replace) # links = '<a href=\'subl:testtest {"reg1":0}\'>tesdt2</a>' ##検索文字列 for region in view.find_all(' '): i =i+1 ##viewへ範囲表示と選択肢部分構築。同じ文字列を建築しても、それぞれ別々でキー設定しないといけないので、forループで構築する。 view.add_regions('HighlightTest'+str(i), [region], "region.redish", "dot", sublime.DRAW_NO_FILL+sublime.DRAW_STIPPLED_UNDERLINE+sublime.DRAW_NO_OUTLINE, [links] )
- 投稿日:2022-01-11T18:49:22+09:00
BERTの事前学習 Mask language modelの実装
環境の準備 !pip install -q transformers==4.9.0 !pip install -q fugashi !pip install -q ipadic # 必要なモジュールのインストール import torch import transformers BERT モデルと Tokenizer の準備 from transformers import BertJapaneseTokenizer, BertForMaskedLM 今回は東北大学で開発されたモデルである cl-tohoku/bert-base-japanese-whole-word-masking を使用します。 # 分かち書きをするtokenizer bert_tokenizer = BertJapaneseTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking') lm_bert = BertForMaskedLM.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking') BERTの入力の準備 BertJapaneseTokenizer を使用して BERT の入力を用意します。 今回は、「彼は[MASK]として働いている。」という文章の [MASK] を予測させてみます。 # 分かち書きの実行 text = '彼は * として働いている。' tokenized_text = bert_tokenizer(text) print(tokenized_text) MLM を実装するために予測したい単語(今回は *)を [MASK] トークンに置換します。 masked_idx = 2 # 予測させる単語をマスクする tokenized_text[masked_idx] = '[MASK]' print(tokenized_text) [MASK] した単語を含んだ文字列を convert_tokens_to_ids() を使用して id に変換します。 indexed_tokens = bert_tokenizer.convert_tokens_to_ids(tokenized_text) print(indexed_tokens) BERT モデルに使用できるように id 化した文字列を Tensor 型に変換します。 tokens_tensor = torch.tensor([indexed_tokens]) print(tokens_tensor) tensor([[1, 1, 1, 1]]) Mask language model の実行 BertForMaskedLM を使用して Mask language model を実行します。 モデルの構造を確認してみましょう。 .eval() で推論モードに切り替えることができ、こちらのコードを実行するとモデル構造が出力されます。 lm_bert.eval() モデルが用意できたのでMLMの推論を行います。 outputs = lm_bert(tokens_tensor) type(outputs) len(outputs[0][0][0]) 32000 今回の[MASK]した単語の予測値を取得します。 pred = outputs[0][0, masked_idx] pred tensor([-4.7888, 20.6928, -3.8209, ..., -8.5616, -4.3609, -0.3407], grad_fn=) この予測値に .topk() を用いることで予測値の内上位 ◯ に当てはまる予測値の値を取得することが出来ます。 pred.topk(2) torch.return_types.topk(values=tensor([20.6928, 9.1267], grad_fn=), indices=tensor([ 1, 466])) [MASK] した単語を上位 5 を出力する関数を定義します。 def predict_mlm(model, input, masked_idx): with torch.no_grad(): outputs = model(input) # 予測結果の上位5件を抽出 predictions = outputs[0][0, masked_idx].topk(5) for i, idx in enumerate(predictions.indices): index = idx.item() print(index) token = bert_tokenizer.convert_ids_to_tokens([index])[0] print(f'順位:{i + 1}\n単語:{token}') print('----') predict_mlm(lm_bert, tokens_tensor, masked_idx) 1 順位:1 単語:[UNK] 466 順位:2 単語:学校 20770 順位:3 単語:物事 959 順位:4 単語:諸 2633 順位:5 単語:男子
- 投稿日:2022-01-11T18:35:20+09:00
M1 Mac で古いバージョンの numpy をビルドする
環境 M1 Macbook Pro (2021) OS: MacOS X Monterey Python バージョン: 3.9.9 問題 M1 Mac に古いバージョンの numpy をインストールしようとしたところ,事前ビルド済みのバイナリが存在しないためにローカルでビルドが走ったが,エラーメッセージが出てビルドが失敗した. 詳細 python で pip install を行うと,コンパイルが必要なパッケージに関しては,使用している OS とバージョンに応じて事前にビルドされたバイナリがダウンロードされ,高速にパッケージをインストールできる仕組みになっている. しかし,事前ビルド済みのバイナリがない場合は,ローカルでビルドが行われる. 今回インストールしたい numpy==1.19.3 がリリースされたのは2020年10月のことで,M1 Mac がリリースされた2020年11月より前なので,当時存在しなかった OS に対して事前ビルド済みのバイナリは作られていない. 今回は以下のようにコマンドで numpy をインストールしようとしたところ, C のコンパイルでエラーが発生し,ビルドが失敗した. コマンド $ pip install numpy==1.19.3 エラーメッセージ <中略> building 'numpy.core._multiarray_umath' extension compiling C sources C compiler: clang -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include -I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include creating build/temp.macosx-12.1-arm64-3.9/numpy/core/src/multiarray creating build/temp.macosx-12.1-arm64-3.9/numpy/core/src/umath creating build/temp.macosx-12.1-arm64-3.9/build/src.macosx-12.1-arm64-3.9/numpy/core/src/umath creating build/temp.macosx-12.1-arm64-3.9/build/src.macosx-12.1-arm64-3.9/numpy/core/src/common creating build/temp.macosx-12.1-arm64-3.9/private creating build/temp.macosx-12.1-arm64-3.9/private/var creating build/temp.macosx-12.1-arm64-3.9/private/var/folders creating build/temp.macosx-12.1-arm64-3.9/private/var/folders/h8 creating build/temp.macosx-12.1-arm64-3.9/private/var/folders/h8/yzgn6yw92b3dx8g990_w73800000gp creating build/temp.macosx-12.1-arm64-3.9/private/var/folders/h8/yzgn6yw92b3dx8g990_w73800000gp/T creating build/temp.macosx-12.1-arm64-3.9/private/var/folders/h8/yzgn6yw92b3dx8g990_w73800000gp/T/pip-install-trn_r2lp creating build/temp.macosx-12.1-arm64-3.9/private/var/folders/h8/yzgn6yw92b3dx8g990_w73800000gp/T/pip-install-trn_r2lp/numpy_7552e917d2674e7f8beac383fc2b50df creating build/temp.macosx-12.1-arm64-3.9/private/var/folders/h8/yzgn6yw92b3dx8g990_w73800000gp/T/pip-install-trn_r2lp/numpy_7552e917d2674e7f8beac383fc2b50df/numpy creating build/temp.macosx-12.1-arm64-3.9/private/var/folders/h8/yzgn6yw92b3dx8g990_w73800000gp/T/pip-install-trn_r2lp/numpy_7552e917d2674e7f8beac383fc2b50df/numpy/_build_utils creating build/temp.macosx-12.1-arm64-3.9/private/var/folders/h8/yzgn6yw92b3dx8g990_w73800000gp/T/pip-install-trn_r2lp/numpy_7552e917d2674e7f8beac383fc2b50df/numpy/_build_utils/src compile options: '-DNPY_INTERNAL_BUILD=1 -DHAVE_NPY_CONFIG_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE=1 -D_LARGEFILE64_SOURCE=1 -DNO_ATLAS_INFO=3 -DHAVE_CBLAS -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/src/umath -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/src/npymath -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/src/common -Inumpy/core/include -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/include/numpy -Inumpy/core/src/common -Inumpy/core/src -Inumpy/core -Inumpy/core/src/npymath -Inumpy/core/src/multiarray -Inumpy/core/src/umath -Inumpy/core/src/npysort -I/Users/myuser/myrepo/venv/include -I/Users/myuser/.pyenv/versions/3.9.9/include/python3.9 -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/src/common -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/src/npymath -c' extra options: '-faltivec -I/System/Library/Frameworks/vecLib.framework/Headers' clang: numpy/core/src/multiarray/alloc.c clang: numpy/core/src/multiarray/array_assign_scalar.c clang: numpy/core/src/multiarray/common.c clang: numpy/core/src/multiarray/conversion_utils.c clang: numpy/core/src/multiarray/datetime_strings.c clang: numpy/core/src/multiarray/buffer.c clang: numpy/core/src/multiarray/descriptor.c clang: build/src.macosx-12.1-arm64-3.9/numpy/core/src/multiarray/einsum.c clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: build/src.macosx-12.1-arm64-3.9/numpy/core/src/multiarray/lowlevel_strided_loops.c clang: numpy/core/src/multiarray/hashdescr.c clang: numpy/core/src/multiarray/multiarraymodule.c clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: numpy/core/src/multiarray/nditer_constr.c clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: numpy/core/src/multiarray/refcount.c clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: numpy/core/src/multiarray/scalarapi.c clang: numpy/core/src/multiarray/temp_elide.c clang: numpy/core/src/multiarray/vdot.c clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: numpy/core/src/umath/ufunc_object.c clang: build/src.macosx-12.1-arm64-3.9/numpy/core/src/umath/loops.c clang: numpy/core/src/umath/ufunc_type_resolution.c clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: build/src.macosx-12.1-arm64-3.9/numpy/core/src/npymath/ieee754.c clang: numpy/core/src/common/array_assign.c clang: numpy/core/src/common/ucsnarrow.c clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: build/src.macosx-12.1-arm64-3.9/numpy/core/src/common/npy_cpu_features.c clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: /private/var/folders/h8/yzgn6yw92b3dx8g990_w73800000gp/T/pip-install-trn_r2lp/numpy_7552e917d2674e7f8beac383fc2b50df/numpy/_build_utils/src/apple_sgemv_fix.c clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly clang: error: the clang compiler does not support 'faltivec', please use -maltivec and include altivec.h explicitly error: Command "clang -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include -I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include -DNPY_INTERNAL_BUILD=1 -DHAVE_NPY_CONFIG_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE=1 -D_LARGEFILE64_SOURCE=1 -DNO_ATLAS_INFO=3 -DHAVE_CBLAS -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/src/umath -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/src/npymath -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/src/common -Inumpy/core/include -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/include/numpy -Inumpy/core/src/common -Inumpy/core/src -Inumpy/core -Inumpy/core/src/npymath -Inumpy/core/src/multiarray -Inumpy/core/src/umath -Inumpy/core/src/npysort -I/Users/myuser/myrepo/venv/include -I/Users/myuser/.pyenv/versions/3.9.9/include/python3.9 -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/src/common -Ibuild/src.macosx-12.1-arm64-3.9/numpy/core/src/npymath -c numpy/core/src/multiarray/array_assign_scalar.c -o build/temp.macosx-12.1-arm64-3.9/numpy/core/src/multiarray/array_assign_scalar.o -MMD -MF build/temp.macosx-12.1-arm64-3.9/numpy/core/src/multiarray/array_assign_scalar.o.d -faltivec -I/System/Library/Frameworks/vecLib.framework/Headers" failed with exit status 1 ---------------------------------------- ERROR: Failed building wheel for numpy Failed to build numpy ERROR: Could not build wheels for numpy, which is required to install pyproject.toml-based projects 解決方法 OpenBLAS というパッケージをインストールし,インストール先のパスを環境変数に指定して pip install を行う. $ brew install openblas $ OPENBLAS="$(brew --prefix openblas)" pip install numpy==1.19.3 無事にビルドできた. 参考文献
- 投稿日:2022-01-11T18:20:48+09:00
Python: 画像データからサムネイルを作成する
import imghdr from io import BytesIO from PIL import Image def create_square_thumb( binary_of_fullimage: bytes, thumb_size: tuple, # サムネイルのサイズ(x, y)を指定する。 例:(100, 100) thumb_quality: int, # サムネイルの品質を指定する。↓をご参照 ) -> bytes: """ 画像データのbytesから、指定したサイズと品質でサムネイルを作ってbytesで返す。 [thumb_quality: int] フィルタ ダウンスケーリング品質 アップスケーリング品質 パフォーマンス PIL.Image.NEAREST ⭐⭐⭐⭐⭐ PIL.Image.BOX ⭐ ⭐⭐⭐⭐ PIL.Image.BILINEAR ⭐ ⭐ ⭐⭐⭐ PIL.Image.HAMMING ⭐⭐ ⭐⭐⭐ PIL.Image.BICUBIC ⭐⭐⭐ ⭐⭐⭐ ⭐⭐ PIL.Image.LANCZOS ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐ """ # 画像データかどうか及び画像の場合その情報 image_type = imghdr.what(None, h=binary_of_fullimage) if image_type is None: return None # 画像データじゃない with Image.open(BytesIO(binary_of_fullimage)) as im: # 画像の中央をできるだけ大きいサイズで切り抜く image_square_cropped = get_square_cropped_pillow_image(im.copy()) # 切り抜きを、指定したサイズと品質でリサイズする image_square_cropped.thumbnail(thumb_size, thumb_quality) dummy_thumb: BytesIO = BytesIO() # サムネイルを一時保存するための入れ物 # 一時的に保存(スマホ写真データなど、exif情報がある場合にも対応) image_square_cropped.save( dummy_thumb, format=image_type ) if "exif" not in im.info else image_square_cropped.save( dummy_thumb, exif=im.info["exif"], format=image_type ) # サムネイルのバイナリデータを返す return dummy_thumb.getbuffer().tobytes() def get_square_cropped_pillow_image(im: Image) -> Image: """ 画像の縦・横 短い方に合わせて、画像の中央を正方形に切り抜いて返す。 """ square_size = min(im.size) width, height = im.size box = None if width > height: top = 0 bottom = square_size left = (width - square_size) / 2 right = left + square_size box = (left, top, right, bottom) else: left = 0 right = square_size top = (height - square_size) / 2 bottom = top + square_size box = (left, top, right, bottom) return im.crop(box) サムネイルを長方形にしたい場合は改造の必要がありんす
- 投稿日:2022-01-11T17:12:50+09:00
scikit-learnやlightgbmなどのONNX形式モデルがブラウザで動作しないとき
やりたいこと Pythonで訓練したモデルをONNX形式でブラウザ上(JavaScript)で推論させたい。 使用ライブラリ Python scikit-learn/lightgbm onnxmltools JavaScript onnxruntime-web 問題 JSのサンプルコード通りに書いてもsession.run()部分で4123988のようなエラーが出て動かない。 解決法 PythonでモデルをONNX形式に変換する際、zipmapオプションを無効にしないとブラウザでは動かないらしい。 例えば、LightGBMモデルだとこんな感じ。 import onnxmltools onnx_model = onnxmltools.convert_lightgbm(model, initial_types=initial_types, zipmap=False) 参考
- 投稿日:2022-01-11T16:12:07+09:00
Flask でフォームを保持する方法
Flask でフォームを作っていて、 送信ボタンを押したときにフォームの中身が全部消えるのが嫌だったので、 対策方法をメモっておきます。 できること Flaskでフォームを保持する方法がわかる 辞書でフォームのデータを引っ張っておく request.formに辞書の形でフォームのデータが内包されているため、 これを次のレンダー時に引っ張ってくるようにしましょう。 from Flask import request @app.route('/', methods=["GET", "POST"]) def index(): if request.method == 'POST': # POSTの場合 return render_template('index.html', form_data=request.form) else: # GETの場合 return render_template('index.html') セッションに保管しておいて、次来たときでも呼び出せるようにしておくのもいいですね。 from Flask import request,session @app.route('/', methods=["GET", "POST"]) def index(): for request.form in key: session[key] if request.method == 'POST': return render_template('index.html', form_data=session) else: return render_template('index.html') HTML側で細工しておく 力技ですが、HTMLに{% if %}とかを入れて対処しましょう。 {% if form_data %} <input type={{type}} class="form-control" id={{id}} name={{id}} value="{{session[id]}}"> {% else %} <input type={{type}} class="form-control" id={{id}} name={{id}}> {% endif%} フォームが一つとかだけならいいんですが、同じようなフォームを複数扱う場合に大変なので、 マクロ機能を使うと大変便利です。 え?違う内容のフォームがたくさんある?…力技でどうにかしましょう {% macro form(id) -%} {% if form_data %} <input type={{type}} class="form-control" id={{id}} name={{id}} value="{{session[id]}}"> {% else %} <input type={{type}} class="form-control" id={{id}} name={{id}}> {% endif%} {%- endmacro %}
- 投稿日:2022-01-11T15:54:51+09:00
【Project Euler】Problem 22: 名前のスコア
本記事はProjectEulerの「100番以下の問題の説明は記載可能」という規定に基づいて回答のヒントが書かれていますので、自分である程度考えてみてから読まれることをお勧めします。 問題 22. 名前のスコア 原文 Problem 22: Names scores 問題の要約:文字の値をアルファベットの順番、単語の値を文字の値の和としたときに添付のnames.txtの5000個の名前の値の合計を求めよ Google Colabでちょっと厄介なのがファイルの読み込み。Windowsでは以下の手順で行います。 1.ファイルをダウンロードして適当なフォルダに置く(例えば、¥ダウンロード) 2. 以下のプログラムを実行して「ファイル選択」ボタンを押すと、ダイアログボックスが出てくるのでダウンロードしたファイルを選択 3. 同じ名前でファイルがアップロードされる # show upload dialog from google.colab import files #[ファイル選択] #uploaded = files.upload()p022_names.txt #p022_names.txt(text/plain) - 46447 bytes, last modified: 2020/4/2 - 100% done #Saving p022_names.txt to p022_names (1).txt ここからファイルを読み、リストに入れて、ソートします。 f = open("p022_names.txt") string = f.read() f.close() names = string.replace('"','').split(',') names.sort() print(len(names), names[:10]) print(names[938-1]) #5163 ['AARON', 'ABBEY', 'ABBIE', 'ABBY', 'ABDUL', 'ABE', 'ABEL', 'ABIGAIL', 'ABRAHAM', 'ABRAM'] #COLIN 名前が5163個、最初の10個の名前と、938番目が"COLIN"であることを確認しました。 さて問題に戻っての文字と単語の値を返す関数charcost, wordcostを作って、wordcost("COLIN")が53になっていることを確認します。 def charcost(c): return ord(c)-ord('A')+1 # return A->1, B->2,,, def wordcost(s): return sum(map(charcost,s)) print( wordcost("COLIN")) #53 後は読み込んだ名前すべてのwordcostと位置(enumerateで取得)を掛け合わせて、合計を取れば完成です。 print(f"Answer: {sum([wordcost(n)*i for i, n in enumerate(names,1)])}") (開発環境:Google Colab)
- 投稿日:2022-01-11T15:54:24+09:00
【Project Euler】Problem 21: 友愛数
本記事はProjectEulerの「100番以下の問題の説明は記載可能」という規定に基づいて回答のヒントが書かれていますので、自分である程度考えてみてから読まれることをお勧めします。 問題 21. 友愛数 原文 Problem 21: Amicable numbers 問題の要約:10000未満の友愛数の合計を求めよ 友愛数(Wikipedia)に詳しい説明がありますが、「自分自身を除いた約数の和が、互いに他方と等しくなるような数をいう」とのこと。まず「自分自身を除いた約数(proper divisors)の和」を求める関数sumpdivを作ります。220と284が友愛数になっていることを確認。 from sympy import divisors # sum of the proper divisors def sumpdiv(n): return sum(sympy.divisors(n))-n print(sumpdiv(220),sumpdiv(284)) #284 220 これをもとに10000未満の友愛数を探します。 spdlist = [0] # store sum of the proper divisors of n ans = 0 for n in range(1,10000): spd = sumpdiv(n) spdlist.append(spd) if spd < n and spdlist[spd] == n: # check if amicable number print(n, spd) ans += n+spd print(f"Answer: {ans}")
- 投稿日:2022-01-11T15:52:20+09:00
【Project Euler】Problem 20: 階乗の数字和
本記事はProjectEulerの「100番以下の問題の説明は記載可能」という規定に基づいて回答のヒントが書かれていますので、自分である程度考えてみてから読まれることをお勧めします。 問題 20. 階乗の数字和 原文 Problem 20: Factorial digit sum 問題の要約:100!の数字和を求めよ これも簡単で数字和は「Problem 16: 2のべき乗の数字和」で作った関数digitsumを使います。 import math print(f"Answer : {digitsum(math.factorial(100))}") (開発環境:Google Colab)
- 投稿日:2022-01-11T15:17:07+09:00
PythonプログラムからTwitter APIを利用して大量のTweetを取得する
はじめに 過去の記事(※1)で自然言語処理をする機会が増えてますとお伝えしてました。 その対象としては、Twitterも含まれていて、Tweetの取得を試みた時期がありました。 この2~3か月くらい、筆者は仕事でデータ収集(BeautifulSoupやSeleniumを使ったWebスクレイピング)と自然言語処理(MeCabを使った形態素解析とgensim使った単語の分散表現と感情分析)をする機会が増えてます。 もともと趣味で、プログラムからTweet(投稿)したり、他者のTweetを数件取得したりは経験していました。 が、大量データが必要になったので、既存の有志ブログに掲載のCodeを参考にしながら紹介します。(掲載のCodeが素晴らしく、大変助かりました。) TwitterAPI でツイートを大量に取得。サーバー側エラーも考慮(pythonで) ※対象は「大量にダウンロード」の節です。 本稿では、元Codeに対して手を加えた部分を簡単に説明し、最後にCode全体を再掲してゆく。 本稿で紹介すること 元Codeに対する変更点 完成版のCode全体 本稿で紹介しないこと Twitter APIの利用手続きおよび仕様 Use Cases, Tutorials, & Documentation Twitter API Documentation | Docs 2021年度版 Twitter API利用申請の例文からAPIキーの取得まで詳しく解説 Pythonライブラリ(requests_oauthlib、requests、codecs、etc.)の全般 元Codeに対する変更点 大きく、3つです。 1. HTTPリクエストの発行 2. Waningメッセージの抑止 3. Tweet詳細情報の保存 1. HTTPリクエストの発行 ProxyやらUser-Agentやらを設定するため、慣れ親しんだrequestsを使うように変更しました。 以下の事項をrequests.get()のOptionパラメータで指定 timeoutの設定 SSL証明書チェックのSkip(無効化) Header(User-Agent)の設定 Proxyの設定 ★Proxy環境化で実行する場合は必須 特に、4点目の答えにたどり着くまでに筆者は少し悩みましたw 変更前(≒元Code) #---------------- # ツイート取得 #---------------- cnt = 0 unavailableCnt = 0 while True: res = self.session.get(url, params = params) 変更後 PROXIES = { 'http' :'http://${ProxyサーバのIPアドレス}:${ProxyサーバのPort番号}/', 'https':'http://${ProxyサーバのIPアドレス}:${ProxyサーバのPort番号}/' } HEADERS = { 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36' } ~ (中略) ~ #---------------- # ツイート取得 #---------------- cnt = 0 unavailableCnt = 0 while True: #res = self.session.get(url, params = params) res = requests.get(url, auth=self.auth, timeout=(10.0), verify=False, headers=HEADERS, proxies=PROXIES, params=params) 2. Waningメッセージの抑止 「1. HTTPリクエストの発行」の対応だけではWaningメッセージが大量に出力されて結果ログが見づらくなるため、Code冒頭でWarningメッセージを抑止する設定を入れました。 Advanced Usage — requests-docs-ja 1.0.4 documentation 本稿では通信相手を安全と判断して扱いますが、読者の所属組織(プログラム実行環境)のセキュリティルールを鑑みて適宜対応を願います。 # SSL証明書の検証をSkipし、以下のメッセージを抑止(無効化)する # InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings import urllib3 from urllib3.exceptions import InsecureRequestWarning urllib3.disable_warnings(InsecureRequestWarning) 3. Tweet詳細情報の保存 リツイート数やいいね数まで取得し、コンソール表示ではなくファイル出力するように変更しました。 収集対象数の上限は、とりあえず500で指定 sys.maxsizeで指定した場合は、Twitter APIの時間あたりのリクエスト上限数を鑑みながら大量にダウンロードする動き 変更前(≒元Code) if __name__ == '__main__': # キーワードで取得 getter = TweetsGetter.bySearch(u'渋谷') # ユーザーを指定して取得 (screen_name) #getter = TweetsGetter.byUser('AbeShinzo') cnt = 0 for tweet in getter.collect(total = 3000): cnt += 1 print ('------ %d' % cnt) print ('{} {} {}'.format(tweet['id'], tweet['created_at'], '@'+tweet['user']['screen_name'])) print (tweet['text']) 変更後 if __name__ == '__main__': # キーワードで取得 getter = TweetsGetter.bySearch(u'新型コロナウイルス感染症') f = codecs.open('tweet_bySearch.log', 'w', 'utf-8') cnt = 0 #for tweet in getter.collect(total=sys.maxsize): for tweet in getter.collect(total=500): cnt += 1 #print ('------ %d' % cnt) #print ('{} {} {}'.format(tweet['id'], tweet['created_at'], '@'+tweet['user']['screen_name'])) #print (tweet['text']) f.write('"{}","{}","{}","{}","{}","{}"\n'.format(tweet['id'], tweet['created_at'].strip(), '@'+tweet['user']['screen_name'].strip(), tweet['retweet_count'], tweet['favorite_count'], tweet['text'].strip().replace('\n', ''))) f.close() 完成版のCode全体 以下、完成版のCode全体です。 Twitter APIのConsumer Keyや社内Proxyサーバ、User-Agentなどは読者で各位設定ください。 # SSL証明書の検証をSkipし、以下のメッセージを抑止(無効化)する # InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings import urllib3 from urllib3.exceptions import InsecureRequestWarning urllib3.disable_warnings(InsecureRequestWarning) from requests_oauthlib import OAuth1Session, OAuth1 import requests import json import datetime, time, sys from abc import ABCMeta, abstractmethod import codecs CK = '${YOUR Consumer Key}' # Consumer Key CS = '${YOUR Consumer Secret}' # Consumer Secret AT = '${YOUR Access Token}' # Access Token AS = '${YOUR Access Token Secert}' # Access Token Secert PROXIES = { 'http' :'http://${ProxyサーバのIPアドレス}:${ProxyサーバのPort番号}/', # Proxy for HTTP 'https':'http://${ProxyサーバのIPアドレス}:${ProxyサーバのPort番号}/' # Proxy for HTTPS } HEADERS = { 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36' } class TweetsGetter(object): __metaclass__ = ABCMeta def __init__(self): #self.session = OAuth1Session(CK, CS, AT, AS) self.auth = OAuth1(CK, CS, AT, AS) @abstractmethod def specifyUrlAndParams(self, keyword): ''' 呼出し先 URL、パラメータを返す ''' @abstractmethod def pickupTweet(self, res_text, includeRetweet): ''' res_text からツイートを取り出し、配列にセットして返却 ''' @abstractmethod def getLimitContext(self, res_text): ''' 回数制限の情報を取得 (起動時) ''' def collect(self, total = -1, onlyText = False, includeRetweet = False): ''' ツイート取得を開始する ''' #---------------- # 回数制限を確認 #---------------- self.checkLimit() #---------------- # URL、パラメータ #---------------- url, params = self.specifyUrlAndParams() params['include_rts'] = str(includeRetweet).lower() # include_rts は statuses/user_timeline のパラメータ。search/tweets には無効 #---------------- # ツイート取得 #---------------- cnt = 0 unavailableCnt = 0 while True: #res = self.session.get(url, params = params) res = requests.get(url, auth=self.auth, timeout=(10.0), verify=False, headers=HEADERS, proxies=PROXIES, params=params) if res.status_code == 503: # 503 : Service Unavailable if unavailableCnt > 10: raise Exception('Twitter API error %d' % res.status_code) unavailableCnt += 1 print ('Service Unavailable 503') self.waitUntilReset(time.mktime(datetime.datetime.now().timetuple()) + 30) continue unavailableCnt = 0 if res.status_code != 200: raise Exception('Twitter API error %d' % res.status_code) tweets = self.pickupTweet(json.loads(res.text)) if len(tweets) == 0: # len(tweets) != params['count'] としたいが # count は最大値らしいので判定に使えない。 # ⇒ "== 0" にする # https://dev.twitter.com/discussions/7513 break for tweet in tweets: if (('retweeted_status' in tweet) and (includeRetweet is False)): pass else: if onlyText is True: yield tweet['text'] else: yield tweet cnt += 1 if cnt % 100 == 0: print ('%d件 ' % cnt) if total > 0 and cnt >= total: return params['max_id'] = tweet['id'] - 1 # ヘッダ確認 (回数制限) # X-Rate-Limit-Remaining が入ってないことが稀にあるのでチェック if ('X-Rate-Limit-Remaining' in res.headers and 'X-Rate-Limit-Reset' in res.headers): if (int(res.headers['X-Rate-Limit-Remaining']) == 0): self.waitUntilReset(int(res.headers['X-Rate-Limit-Reset'])) self.checkLimit() else: print ('not found - X-Rate-Limit-Remaining or X-Rate-Limit-Reset') self.checkLimit() def checkLimit(self): ''' 回数制限を問合せ、アクセス可能になるまで wait する ''' unavailableCnt = 0 while True: url = "https://api.twitter.com/1.1/application/rate_limit_status.json" #res = self.session.get(url) res = requests.get(url, auth=self.auth, timeout=(10.0), verify=False, headers=HEADERS, proxies=PROXIES) if res.status_code == 503: # 503 : Service Unavailable if unavailableCnt > 10: raise Exception('Twitter API error %d' % res.status_code) unavailableCnt += 1 print ('Service Unavailable 503') self.waitUntilReset(time.mktime(datetime.datetime.now().timetuple()) + 30) continue unavailableCnt = 0 if res.status_code != 200: raise Exception('Twitter API error %d' % res.status_code) remaining, reset = self.getLimitContext(json.loads(res.text)) if (remaining == 0): self.waitUntilReset(reset) else: break def waitUntilReset(self, reset): ''' reset 時刻まで sleep ''' seconds = reset - time.mktime(datetime.datetime.now().timetuple()) seconds = max(seconds, 0) print ('\n =====================') print (' == waiting %d sec ==' % seconds) print (' =====================') sys.stdout.flush() time.sleep(seconds + 10) # 念のため + 10 秒 @staticmethod def bySearch(keyword): return TweetsGetterBySearch(keyword) @staticmethod def byUser(screen_name): return TweetsGetterByUser(screen_name) class TweetsGetterBySearch(TweetsGetter): ''' キーワードでツイートを検索 ''' def __init__(self, keyword): super(TweetsGetterBySearch, self).__init__() self.keyword = keyword def specifyUrlAndParams(self): ''' 呼出し先 URL、パラメータを返す ''' url = 'https://api.twitter.com/1.1/search/tweets.json' params = {'q':self.keyword, 'count':100} return url, params def pickupTweet(self, res_text): ''' res_text からツイートを取り出し、配列にセットして返却 ''' results = [] for tweet in res_text['statuses']: results.append(tweet) return results def getLimitContext(self, res_text): ''' 回数制限の情報を取得 (起動時) ''' remaining = res_text['resources']['search']['/search/tweets']['remaining'] reset = res_text['resources']['search']['/search/tweets']['reset'] return int(remaining), int(reset) class TweetsGetterByUser(TweetsGetter): ''' ユーザーを指定してツイートを取得 ''' def __init__(self, screen_name): super(TweetsGetterByUser, self).__init__() self.screen_name = screen_name def specifyUrlAndParams(self): ''' 呼出し先 URL、パラメータを返す ''' url = 'https://api.twitter.com/1.1/statuses/user_timeline.json' params = {'screen_name':self.screen_name, 'count':200} return url, params def pickupTweet(self, res_text): ''' res_text からツイートを取り出し、配列にセットして返却 ''' results = [] for tweet in res_text: results.append(tweet) return results def getLimitContext(self, res_text): ''' 回数制限の情報を取得 (起動時) ''' remaining = res_text['resources']['statuses']['/statuses/user_timeline']['remaining'] reset = res_text['resources']['statuses']['/statuses/user_timeline']['reset'] return int(remaining), int(reset) if __name__ == '__main__': # キーワードで取得 getter = TweetsGetter.bySearch(u'新型コロナウイルス感染症') f = codecs.open('tweet_bySearch.log', 'w', 'utf-8') cnt = 0 #for tweet in getter.collect(total=sys.maxsize): for tweet in getter.collect(total=500): cnt += 1 #print ('------ %d' % cnt) #print ('{} {} {}'.format(tweet['id'], tweet['created_at'], '@'+tweet['user']['screen_name'])) #print (tweet['text']) f.write('"{}","{}","{}","{}","{}","{}"\n'.format(tweet['id'], tweet['created_at'].strip(), '@'+tweet['user']['screen_name'].strip(), tweet['retweet_count'], tweet['favorite_count'], tweet['text'].strip().replace('\n', ''))) f.close() # ユーザーを指定して取得 (screen_name) getter = TweetsGetter.byUser('Qiita') f = codecs.open('tweet_byUser.log', 'w', 'utf-8') cnt = 0 #for tweet in getter.collect(total=sys.maxsize): for tweet in getter.collect(total=500): cnt += 1 #print ('------ %d' % cnt) #print ('{} {} {}'.format(tweet['id'], tweet['created_at'], '@'+tweet['user']['screen_name'])) #print (tweet['text']) f.write('"{}","{}","{}","{}","{}","{}"\n'.format(tweet['id'], tweet['created_at'].strip(), '@'+tweet['user']['screen_name'].strip(), tweet['retweet_count'], tweet['favorite_count'], tweet['text'].strip().replace('\n', ''))) f.close() 出力ファイルを開くと、それぞれ以下のようになっていました。(本稿執筆、2022/01/11時点) tweet_bySearch.log "1480732674314899458","Tue Jan 11 02:45:54 +0000 2022","@nobusinsan","0","0","新型コロナウイルス 感染症専門医なる人たちは素人にも納得のいく説明をhttps://t.co/5a1cbldiod https://t.co/iitIImMWWd" "1480732637777956866","Tue Jan 11 02:45:46 +0000 2022","@Redpanda_hkd","0","0","[千歳市] 新型コロナウイルス感染症に係る注意喚起 (北海道 千歳市) https://t.co/lIFK2GPRip" "1480732523420590080","Tue Jan 11 02:45:18 +0000 2022","@DrugstoreCon","0","0","新型コロナウイルス感染症相談コールセンターの入電状況について|沖縄県・9時前後は特に繋がりにくくなっておりますので、入電数の少ない時間帯に電話していただくか、少し時間を空けておかけ直しいただきますよう、ご協力ください… https://t.co/byjbCjYQus" "1480732192372555776","Tue Jan 11 02:43:59 +0000 2022","@HKT48cheerweb","0","1","池袋駅前にて新型コロナウイルス感染症モニタリングPCR検査を都が無料で行う準備をテントを建てて行ってました。 https://t.co/lpKqV0LfLc" "1480731387145900035","Tue Jan 11 02:40:48 +0000 2022","@yoshiki7111","0","0","本日(1/10)、飯田保健所管内において、48名の新型コロナウイルス感染症の陽性者が確認されたとの公表がありました。感染された方、ご家族をはじめ関係者の ...リンク:https://t.co/BWvBTn8ylhタ グ:#ホームページ" tweet_byUser.log "1480720264652800001","Tue Jan 11 01:56:36 +0000 2022","@Qiita","0","0","○○LGTM達成や○○Contributions達成のお知らせツイートは @qiita_milestone でおこなっていますので是非フォローをお願いします! https://t.co/pkDG0n229U" "1480700066403074049","Tue Jan 11 00:36:20 +0000 2022","@Qiita","1","3","QiitaAzure記事投稿キャンペーン11月テーマ「マイクロソフト認定資格を取得する際の学習方法や経験談、おすすめ学習リソースなどを紹介しよう!」11月の月間Contributorの発表です。キャンペーンに参加したきっ… https://t.co/7sMoJ6DdYL" "1479329476119269376","Fri Jan 07 05:50:06 +0000 2022","@Qiita","0","2","?リリースノート訪問済みリンクのスタイルの追加と、検索結果ページにおけるautofocusを外しました。https://t.co/G2XOVqfzmN" "1479271101985603590","Fri Jan 07 01:58:08 +0000 2022","@Qiita","0","0","○○LGTM達成や○○Contributions達成のお知らせツイートは @qiita_milestone でおこなっていますので是非フォローをお願いします! https://t.co/4WcHuWxSw0" "1478957891084500992","Thu Jan 06 05:13:33 +0000 2022","@Qiita","0","0","○○LGTM達成や○○Contributions達成のお知らせツイートは @qiita_milestone でおこなっていますので是非フォローをお願いします! https://t.co/EsOgA9UJOV" まとめ PythonプログラムからTwitter APIを利用して、大量のTweetを取得することができました。 これを遊休マシンに仕込んでおけば、大量のTweetを溜め込むことができるようになるかな、と。
- 投稿日:2022-01-11T12:45:16+09:00
KLab サーバーサイドキャンプに参加しました〜
要するに pythonでサーバーサイドをやってみたい人におすすめ サーバーサイドの入門本を読んでから行くのがbetter 教材の音ゲーのクオリティーもすごい 自己紹介 暇人窟 @fzk_hrkw_kuee 電気電子のひと 学部3回 マイコンとか低レイヤが好き (ちゃんとやってるとは言っていない) 概要 pythonで音ゲーの通信対戦のバックエンドを作る fastAPIでやる pydanticも使用 SQLalchemyで生のSQL文を書く(ORMは使わない) githubのcodespacesを使用 →ローカルに何もなくてもできる! モチベーション バックエンドの基礎の本は読んだことはあったのですが、実践的な経験を得るため参加しました。 他の受講生は(専門外だが)多少バックエンドに触れたことがある人がほとんど、といった印象でした。 バックエンドの目的や構成などの事前知識が最低限無いとやや厳しいキャンプだったと思います。 化学系の人、ロボット系の人などさまざまなバックグラウンドの人が参加されていました。 学部3,4回生がボリュームゾーンでした。 形態 google meetで講義 slackで連絡・質問・雑談など 雰囲気が堅くなく、居心地は良かったです。 こちらのスケジュールにもかなり柔軟に対応して下さったので感謝(事前連絡はしましょう) (参加できなかったのですが)座談会もありました。 技術から就活までいろいろな質問が出ていて、想定よりも盛り上がっていたようです。 「学生とメンターだけでなく学生どうしのつながりも」と運営側はヨコのつながりも推奨していました。 実際後半では作ったサーバーで対戦する人もいて、受講生どうしの会話もそれなりにあったと思います。 実装 枠組みはだいたいできてる だいたいPOSTリクエスト クエリパラメータは使わずjsonを投げる ユーザー登録・設定に関する/user APIと 対戦に関する/room APIがある userの方はだいたい組み上がっており、チュートリアル的実装 roomの方は仕様を渡されあとは自分で設計 流れ 初日: とにかくSQLに触れる 途中からipythonでSQLalchemyにも触る 2日目: FastAPI,pydanticの説明 実装するAPIの仕様をもらい、あとはひたすら実装 以降は実装する上でのポイントを解説 年末年始 ブランク 3日目: テーブルの構成、複合主キー 4日目: トランザクションとロック ルームの人数制限をどう扱うか 5日目: 実装、成果発表、懇親会 感想 講義など 質問がしやすい雰囲気だったのが何より助かりました。 DMの質問も許容してもらえるなど、運営側がとても努力してくれていました。 対応も迅速で、ストレスなく開発が進められました。 また、受講生全体の進度に合わせて構成を適宜調整してくれました。毎回講義資料が進度に合わせて改訂され、講義構成が変わることもありました。 既に分かっているところにむやみに時間を割くことがなく、講義のスピード感はかなり良かったです(人によってはやや速く感じるかも)。 課題に関して codespacesのおかげで環境構築に手間がかからない、というのはかなり助かりました。コンテナバンザイ! また、ブラウザからAPIを叩ける Swagger UIはかなり重宝しました。 日程に対して課題はやや難易度が高く見受けられました (受講生全員がサーバを正常に動作させられた訳ではない)。 APIの仕様にあうサーバを設計する段階から始める、自由度がある課題なので、不慣れな人はやや取っつきにくく感じそうだなと思いました(一方で慣れてる人だと3日目に動作までこぎ着けた例も)。 難しく感じた人にはもう少し指針(実装する関数の入出力を指定するなど)があってもいいかなとおもいました。 ゲームに関して 音ゲーは本当にアプリとして見かけても何の違和感もないクオリティーでした。 初日からオフラインモードで遊べたので結構楽しめましたね。 まさか初日からほぼ全ての曲でフルコンを取る猛者がいるとは思いませんでしたが、、 なんでも音ゲー目当てにキャンプに参加されたそうです。すごい… その他 インターン前日に、なんと実装のお供にと大量のお菓子を送って頂きました!めちゃくちゃ嬉しい!! また、最終日の懇親会用にお酒とおつまみを(こちらも大量!)頂戴しました!おつまみのチョイスがこれまた良い! ノベルティの他に、修了証書まで頂きました。 続いて、修了証の贈呈の一枚です。皆さん、5日間本当におつかれさまでした!(ちなみに、修了証の右下のハンコは、サーバサイドキャンプ専用のオリジナルデザインだったりします…!)#KLabServerSideCamp#サーバサイドキャンプ pic.twitter.com/mazXOGUjkb— Kの27乗 (@oktillion27) January 7, 2022 貰えると嬉しいものですね。達成感もひとしおです。 KLabさんには技術者を大切にする風土があるようです。ゲームエンジニア等の道に進むならばかなり魅力的な企業だと思います! まとめ バックエンドを少しだけ触った人が次に挑む内容として◎ やや自由度が高い (必要な人には)もう少し指針があっても良いかも 雰囲気が非常に良い、質問への対応も速くて親切 KLabさん、ありがとうございました!!
- 投稿日:2022-01-11T12:21:09+09:00
Djangoで郵便番号の入力から住所を反映させる
郵便番号の入力から住所を自動で反映させる フォームから住所を書く際に郵便番号を入力するだけで自動で住所が入力されたら楽なのでその流れをDjangoのコードで書けるようにメモ。 ※既にアプリは出来てるものとして、その部分は割愛します。 ルーティング設定(urls.py) urls.py from django.urls import path,include from .import views from django.views.generic import TemplateView app_name = 'アプリ名' urlpatterns = [ path('', views.IndexView.as_view(),name="index"), path('inquiry/', views.InquiryView.as_view(),name="inquiry"), ] 後にInquiry classを作る予定でこのpath設定。 ビューの編集(views.py) views.py from .forms import InquiryForm #forms.pyをインポート from django.urls import reverse_lazy #処理が終わった後に画面に飛ばす処理 class InquiryView(generic.FormView): template_name = "inquiry.html" form_class = InquiryForm success_url = reverse_lazy('アプリ名:inquiry') フォームの設定(forms.py) forms.py import os from django import forms from django.db.models import fields #(省略) Zipcode = { 'zip_code': forms.TextInput( attrs={'class': 'p-postal-code','placeholder': '記入例:1060022'}, ), } class InquiryForm(forms.Form):#クラス名は必要に応じて変える # 郵便番号からcityまでを自動入力 zipcode = forms.RegexField(label='郵便番号(ハイフンなし)', regex=r'^[0-9]+$', max_length=7, widget=forms.TextInput(attrs={'onKeyUp' : "AjaxZip3.zip2addr(this,'','state','city')"}), ) state = forms.CharField(label='都道府県',max_length=6) city = forms.CharField(label='市区町村',max_length=10) address_1 = forms.CharField(label='番地',max_length=10) address_2 = forms.CharField(label='建物名・部屋番号',max_length=10) def __init__(self,*args,**kwargs):#入力フォーム表示用 self.fields['zipcode'].widget.attrs['class']='form-control col-11' self.fields['zipcode'].widget.attrs['placeholder']='例:1600022 ' self.fields['state'].widget.attrs['class']='form-control col-11' self.fields['state'].widget.attrs['placeholder']='都道府県をここに入力してください' self.fields['city'].widget.attrs['class']='form-control col-11' self.fields['city'].widget.attrs['placeholder']='市区町村をここに入力してください' self.fields['address_1'].widget.attrs['class']='form-control col-11' self.fields['address_1'].widget.attrs['placeholder']='番地をここに入力してください' self.fields['address_2'].widget.attrs['class']='form-control col-11' self.fields['address_2'].widget.attrs['placeholder']='建物名・部屋番号をここに入力してください' いい感じに動いてもらうためのJavaScript 今回はAjaxzip3を使ってるので、そこから引用してます。 ※今回はbase.htmlにべた書きしましたが、他におすすめがあれば教えてください。 base.html <script src="https://ajaxzip3.github.io/ajaxzip3.js" charset="UTF-8"></script>
- 投稿日:2022-01-11T12:15:05+09:00
郵便番号の入力から住所を自動で反映させる【Django】
郵便番号の入力から住所を自動で反映させる フォームから住所を書く際に郵便番号を入力するだけで自動で住所が入力されたら楽なのでその流れをDjangoのコードで書けるようにメモ。 ※既にアプリは出来てるものとして、その部分は割愛します。 ルーティング設定(urls.py) urls.py from django.urls import path,include from .import views from django.views.generic import TemplateView app_name = 'アプリ名' urlpatterns = [ path('', views.IndexView.as_view(),name="index"), path('inquiry/', views.InquiryView.as_view(),name="inquiry"), ] 後にInquiry classを作る予定でこのpath設定。 ビューの編集(views.py) views.py from .forms import InquiryForm #forms.pyをインポート from django.urls import reverse_lazy #処理が終わった後に画面に飛ばす処理 class InquiryView(generic.FormView): template_name = "inquiry.html" form_class = InquiryForm success_url = reverse_lazy('アプリ名:inquiry') フォームの設定(forms.py) forms.py import os from django import forms from django.db.models import fields #(省略) Zipcode = { 'zip_code': forms.TextInput( attrs={'class': 'p-postal-code','placeholder': '記入例:1060022'}, ), } class InquiryForm(forms.Form):#クラス名は必要に応じて変える # 郵便番号からcityまでを自動入力 zipcode = forms.RegexField(label='郵便番号(ハイフンなし)', regex=r'^[0-9]+$', max_length=7, widget=forms.TextInput(attrs={'onKeyUp' : "AjaxZip3.zip2addr(this,'','state','city')"}), ) state = forms.CharField(label='都道府県',max_length=6) city = forms.CharField(label='市区町村',max_length=10) address_1 = forms.CharField(label='番地',max_length=10) address_2 = forms.CharField(label='建物名・部屋番号',max_length=10) def __init__(self,*args,**kwargs):#入力フォーム表示用 self.fields['zipcode'].widget.attrs['class']='form-control col-11' self.fields['zipcode'].widget.attrs['placeholder']='例:1600022 ' self.fields['state'].widget.attrs['class']='form-control col-11' self.fields['state'].widget.attrs['placeholder']='都道府県をここに入力してください' self.fields['city'].widget.attrs['class']='form-control col-11' self.fields['city'].widget.attrs['placeholder']='市区町村をここに入力してください' self.fields['address_1'].widget.attrs['class']='form-control col-11' self.fields['address_1'].widget.attrs['placeholder']='番地をここに入力してください' self.fields['address_2'].widget.attrs['class']='form-control col-11' self.fields['address_2'].widget.attrs['placeholder']='建物名・部屋番号をここに入力してください' いい感じに動いてもらうためのJavaScript 今回はAjaxzip3を使ってるので、そこから引用してます。 ※今回はbase.htmlにべた書きしましたが、他におすすめがあれば教えてください。 base.html <script src="https://ajaxzip3.github.io/ajaxzip3.js" charset="UTF-8"></script> AjaxzipのGitHubはこちら
- 投稿日:2022-01-11T11:48:45+09:00
初心者挑戦 まずirisでデータ分類やってみた
はじめに 今回成果物を提出にするにあたりscikit-learnライブラリにあるモデルの評価用データセットIrisを使いロジスティック回帰を用いて分類してみます。 実行環境 ・Python ver3 ・Google Colaboratory 使用するデータセットIrisについて irisデータとは150個のアヤメ(花の一種)のサンプルの「がく片の長さ」「がく片の幅」「花びらの長さ」「花びらの幅」の4つの特徴量(単位はcm)と、3種の品種(0~2)が格納されています。今回は、データの可視化のために特徴量を「がくの長さ」「花びらの長さ」の2つを使用します。 実際にデータをダウンロードし一部を表示してみます。 from sklearn import datasets iris = datasets.load_iris() print(iris) from sklearn import datasetsでsklearnライブラリをインポートし iris = datasets.load_iris() print(iris) でirisデータを打ち出してみます。 4つの特徴データが入っていることがわかります。 使用する分類モデル【ロジスティック回帰】 今回使用するロジスティック回帰は、線形分離可能なデータの境界線を学習によって見つけて、データの分類を行うモデルです。特徴としては、境界線が直線になります。そのため、二項分類などクラスの少ないデータに使われます。 欠点としては、以下があげられます。 ・データが線形分離可能でないと分類ができない点 ・⾼次元の疎なデータ(0が多いデータ)には適さない点 ・訓練データから学習した境界線がデータの近くを通るため、汎化能力が低い点 今回は3品種、2データと種類の少ないデータになりますのでデータの分布状況を俯瞰、把握するのには適したモデルといえます。実際にデータの分布・分類を行ったグラフが以下のもです。 実際にコードを書く 下記のように計算ライブラリ(numpy)、データを可視化するためライブラリ(matlotlib)そして人工知能ライブラリ(sklearn)をインポートします。 import numpy as np import matplotlib import matplotlib.pyplot as plt from sklearn import datasets from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.datasets import make_classification 次にirisのデータを取得し、得たデータから「がくの長さ」「花びらの長さ」の特徴量を変数Xに格納します。また変数Yにはirisのクラスラベルを格納します。 iris = datasets.load_iris() X = iris.data[:, [0, 2]] y = iris.target モデルで学習さるため格納したデータを学習用70%、学習後モデルテスト用30%に分けます。 # trainデータ、testデータの分割 train_X, test_X, train_y, test_y = train_test_split( X, y, test_size=0.3, random_state=42) 学習モデルロジスティック回帰を定義します。 model = LogisticRegression() モデルにデータを学習させます。 model.fit(train_X, train_y) 出来たモデルにテスト用データで分類予測させ出力してみます。 y_pred = model.predict(test_X) print(y_pred) テスト結果 [1 0 2 1 1 0 1 2 1 1 2 0 0 0 0 1 2 1 1 2 0 2 0 2 2 2 2 2 0 0 0 0 1 0 0 2 1 0 0 0 2 1 1 0 0] 描画のための散布図を用意しデータを散布します。 plt.scatter(X[:, 0], X[:, 1], c=y, marker=".", cmap=matplotlib.cm.get_cmap(name="cool"), alpha=1.0) x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1 x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1 xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, 0.02), np.arange(x2_min, x2_max, 0.02)) Z = model.predict(np.array([xx1.ravel(), xx2.ravel()]).T).reshape(xx1.shape) plt.contourf(xx1, xx2, Z, alpha=0.4, cmap=matplotlib.cm.get_cmap(name="Wistia")) X,Y軸の範囲を設定し、メインタイトル、各軸のタイトルをつけグリッド線を引きます。 plt.xlim(xx1.min(), xx1.max()) plt.ylim(xx2.min(), xx2.max()) plt.title("classification data using LogisticRegression") plt.xlabel("Sepal length") plt.ylabel("Petal length") plt.grid(True) そして最後に散布図を表示ます。 plt.show() 以上で終了となります。
- 投稿日:2022-01-11T10:24:41+09:00
S3バケット内の情報取得スクリプト
経緯 S3バケット内のオブジェクト(ファイル)について、名前・最終更新日・サイズを取得してCSVファイルに出力したい。 Pythonでスクリプトを書いていく。不慣れなため無駄が多いかと思うが、ご容赦を。 参考:AWS Lambda Python S3でフォルダ以下のファイル一覧を取得する スクリプト作成 以下の構成において、xxx配下のファイル一覧(名前、最終更新日、サイズ)を取得する。 また、オブジェクトは名前(ooooo)で絞って出力する。 s3://aaa.backup/xxx/ooooo aaa.backup:バケット xxx:プリフィックス ooooo**:オブジェクト 出力するCSVの中身は以下のようにしたい。 見出し: Name,Data,Size データ: oooooabcd,[JST]2022/01/10 12:05:10,20.5 KB オブジェクトキーの出力 参考:Boto 3 で Amazon S3 上の key を取得する方法、実装例、注意点 Boto3でS3 Buckets上の情報を取得するときは、list_objects() を使用する。 今回は対象が多いため、プリフィックスとオブジェクト名の一部を指定して条件を絞る。 最終更新日をJSTに変更 S3の最終更新日時(content['LastModified'])はUTCで保存されている。今回はJSTで出力したいため、変換を行う。 参考:PythonでUTCからJSTへの時刻の変換 この方法で行うとS3で保持している最終更新日の最後の6字「+00:00」が邪魔になり、UTCのまま出力されてしまう。 そのため、文字列へ変換して該当文字を削除することでJSTで出力することができる。 datetimeを使用してJSTへの変換を指定 文字列にし、「+00:00」を省く(後方から6文字削除) JSTへの時刻の変換 import datetime def utc_to_jst(timestamp_utc): datetime_utc = datetime.datetime.strptime(timestamp_utc + "+0000", "%Y-%m-%d %H:%M:%S%z") datetime_jst = datetime_utc.astimezone(datetime.timezone(datetime.timedelta(hours=+9))) timestamp_jst = datetime.datetime.strftime(datetime_jst, '%Y-%m-%d %H:%M:%S') return timestamp_jst for page in page_iterator: for content in page['Contents']: date = content['LastModified'] Last = str(date) Last = Last[:-6] print(Last) print("[JST]" + utc_to_jst(Last)) (結果) 2022/01/10 12:05:10 +00:00 [JST]2022/01/10 12:05:10 オブジェクト名からプリフィックスの表示を除く そのままオブジェクトキー(content['Key'])を出力すると、プリフィックスを含んだものが出力される。 見にくいため、プリフィックス名を指定してオブジェクト名のみを出力する。 参考:完全一致する文字列を削除: replace() name = content['Key'] print(name) print(name.replace('xxx/','')) (結果) xxx/oooooabcd oooooabcd サイズをKB表示にする サイズ(content['Size'])をそのまま出力すると、バイトのため大きなサイズの際は見にくい。 また、単位もつけたいため、以下のように記述。 1024で割ってKBへ変換>小数点以下第2位を四捨五入>データ型を文字列に変換>単位をつける n = content['Size']/1024 Size = round(n,1) Size = str(Size) + "KB" print(content['Size']) print(Size) (結果) 22837 22.3KB 1000件以上のデータ出力に対応 Python で Amazon S3 バケットの全オブジェクト名を取得するジェネレータ 今回はV2不使用のため不要だったが、使用する際は考慮が要るかも。 CSV出力 Pythonでcsvファイルにデータを書き込みをする基本中の基本 Excelでも使用したいので文字コードを shift-jis で保存 ヘッダ行をつける(Name,Date,Size) ※実際のスクリプトでは1度開いてヘッダをつけ、再度開き直してオブジェクトキーなどの入力を行う ファイル名がオブジェクト名になるよう変数を設定 import csv wwith open(Object + '_JST.csv','a',newline='',encoding = "shift-jis") as f: writer = csv.writer(f) writer.writerow(["Name","Date","Size"]) with open(Object + '_JST.csv','a',newline='',encoding = "shift-jis") as f: writer = csv.writer(f) writer.writerow([content['Key'].replace('xxx/',''),Last,Size]) スクリプト 以下、完成したスクリプト import boto3 import datetime import csv MY_REGION = 'MY_REGION'; MY_BUCKET = 'MY_BUCKET'; TARGET_PATH = 'TARGET_PATH/'; Object = 'Object'; client = boto3.client('s3', region_name=MY_REGION) paginator = client.get_paginator('list_objects') # フィルタリング設定 operation_parameters = { 'Bucket': MY_BUCKET, 'Prefix': TARGET_PATH + Object } page_iterator = paginator.paginate(**operation_parameters) # UTCからJSTへの時刻の変換 def utc_to_jst(timestamp_utc): datetime_utc = datetime.datetime.strptime(timestamp_utc + "+0000", "%Y-%m-%d %H:%M:%S%z") datetime_jst = datetime_utc.astimezone(datetime.timezone(datetime.timedelta(hours=+9))) timestamp_jst = datetime.datetime.strftime(datetime_jst, '%Y-%m-%d %H:%M:%S') return timestamp_jst # 出力CSVを作成し、ヘッダをつける with open(Object + '_JST.csv','a',newline='',encoding = "shift-jis") as f: writer = csv.writer(f) writer.writerow(["Name","Date","Size"]) for page in page_iterator: for content in page['Contents']: # 最終更新日をUTCからJSTへ変更 Last = content['LastModified'] Last = str(Last) Last = Last[:-6] Last = utc_to_jst(Last) # サイズをキロバイトへ変更 n = content['Size']/1024 Size = round(n,1) Size = str(Size) + "KB" with open(Object + '_JST.csv','a',newline='',encoding = "shift-jis") as f: writer = csv.writer(f) # CSVへ情報出力 writer.writerow([content['Key'].replace('xxx/',''),Last,Size]) (出力結果) Name,Data,Size oooooabcd,[JST]2022/01/10 12:05:10,20.5KB oooooefgh,[JST]2022/01/11 10:03:18,13.2KB …
- 投稿日:2022-01-11T09:41:50+09:00
Pythonで指定の都道府県のコロナ感染者数をチャート出力する。
はじめに オミクロン株が流行ってきたのでコロナの感染状況を 参考にするためにチャート出力してみました。 環境 Windows 11 python-3.7.7-embed-amd64 いろいろ遊んでしまうので組み込みのpythonを使って活動内容を制限しています。 データはNHKのコロナ感染者数の都道府県別データを使用しています。 やり方 参考にしたpythonプログラムを修正します。 組み込みのPythonでgui(画面)のチャート出力ができないので 画像に保存してpaintで確認します。 プログラムの最後にplt.savefigを実行して画像ファイルに保存します。 プログラムコード #https://tomokichi.blog/%E3%80%90python%E3%80%91csv%E3%81%8B%E3%82%89%E3%82%B0%E3%83%A9%E3%83%95%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B/ import pandas as pd import matplotlib.pyplot as plt import japanize_matplotlib import matplotlib.dates as mdates #-------------------------------------------------------------------------- data = pd.read_csv('C:\\Users\\User\\Downloads\\nhk_news_covid19_prefectures_daily_data.csv', encoding='utf8') #print(data) x = data['日付'] a = data['都道府県名'] y = data['各地の感染者数_1日ごとの発表数'] datax = [] datay = [] for xi, ai, yi in zip(x, a, y): #print(xi) #print(ai) #print(yi) if ai == '東京都': datax.append(xi) datay.append(yi) #print(xi) #print(yi) #なぜか日付が1970からになる #https://qiita.com/damyarou/items/19f19658b618fd05b3b6 ss = [] ss=pd.to_datetime(datax, format='%Y/%m/%d') #plt.plot(datax,datay) plt.plot(ss,datay) plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y/%m/%d')) #plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%m/%d')) plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=6)) plt.savefig('C:\\Users\\User\\Documents\\Python\\outcovid19.png', dpi=300, orientation='portrait', transparent=False) 入力するのが嫌な方はこちらから https://github.com/kidenkadenyorozuya/readcsvfile 出力結果 参考
- 投稿日:2022-01-11T07:25:02+09:00
[AWS] OpenSearch Service(Elasticsearch)で商品検索APIを作る
1.はじめに みなさんOpenSearch(Elasticsearch)使ってますか? OpenSearchはオープンソースの検索エンジンで、全文検索や商品検索、さらにはデータ分析まで、検索用途なら何でも幅広く活用することができます。今回はそのOpenSearchを使って、ECサイトで使うような商品検索APIを作る方法を紹介してみたいと思います。 2.OpenSearchとElasticsearchの関係 OpenSearchという言葉自体を聞き慣れない人もいるかもですが、これはElasticsearch(ES)とほぼ同じものです。 ESは元々Apache License 2.0に準拠したオープンソースソフトウェア(OSS)で、AWSも以前はこのOSS版のESをサービスとして提供していました。 しかし2021年1月、開発元のElasitic社はESのライセンスを変更し、OSSではない独自ライセンスにしてしまいました。これによりAWSはElasticsearchをサービスとして提供できなくなるため、最終のOSSバージョンであるES7.10をフォークしてOpenSearchというOSSの検索エンジンを新たに作成、2020年7月に公開しました。OpenSearchの開発にはAWSだけでなく色々な企業が参画しており、Apache License 2.0の純粋なOSSとなってます。 こういった経緯があるので、現時点ではOpenSearchの中身はESとほぼ同じです。今後は別物として進化していくので少しずつ機能に差異が出てくる可能性はありますが、個人的には特殊なライセンス体系となってしまい色々縛りがあるESよりも、純粋なOSSとして提供されているOpenSearchの方がよいかなと思っています。 3.今回のAPI作成にあたり前提となる情報 まずは前提となる情報(システム構成など)を記載しておきます。 3-1.システム構成 API基盤にAPI Gateway+Lambdaを使用し、ここからOpenSearchに対して検索を行う形になります。簡単な構成図は以下の通りです。 ちなみに今回はAPIの構築にサーバレス向けフレームワークのChaliceを使います。言語はPythonです。Chaliceって何よ?という方向けに、本記事の末尾に参考のリンクを貼ってますので興味があればご覧ください。 3-2.前提とするデータ 今回検索対象とするデータは以下の通りです。商品の属性値(ブランド、性別、カテゴリ、価格、・・・etc)で絞り込みを行い、結果をJSONで返却するイメージになります。 商品コード 商品名 ブランド 性別 カテゴリ 価格(円) 色 サイズ A001 無地スキニーパンツ ブランドA メンズ パンツ 2000 ホワイト S A002 無地スキニーパンツ ブランドA メンズ パンツ 2000 ホワイト M A003 無地スキニーパンツ ブランドA メンズ パンツ 2000 ホワイト L A004 無地スキニーパンツ ブランドA メンズ パンツ 2000 ブラック S A005 無地スキニーパンツ ブランドA メンズ パンツ 2000 ブラック M A006 無地スキニーパンツ ブランドA メンズ パンツ 2000 ブラック L B001 デニムパンツ ブランドB レディース パンツ 2500 ブラウン S B002 デニムパンツ ブランドB レディース パンツ 2500 ブラウン M B003 デニムパンツ ブランドB レディース パンツ 2500 ブラウン L B004 デニムパンツ ブランドB レディース パンツ 2500 ホワイト S B005 デニムパンツ ブランドB レディース パンツ 2500 ホワイト M B006 デニムパンツ ブランドB レディース パンツ 2500 ホワイト L C001 長袖Tシャツ ブランドC メンズ Tシャツ 1500 ホワイト S C002 長袖Tシャツ ブランドC メンズ Tシャツ 1500 ホワイト M C003 長袖Tシャツ ブランドC メンズ Tシャツ 1500 ホワイト L C004 長袖Tシャツ ブランドC キッズ Tシャツ 1000 グレー S C005 長袖Tシャツ ブランドC キッズ Tシャツ 1000 グレー M C006 長袖Tシャツ ブランドC キッズ Tシャツ 1000 グレー L 4.商品検索APIの構築手順 前置きが長くなりましたが、ここからは実際の構築手順を書いていきます。早速やってみましょう。 4-1.OpenSearchクラスタの作成 まずはOpenSearchのクラスタを作成します。 AWSコンソール上でOpenSearchの画面を開き、右側上部にある「Create domain」ボタンを押下します。 続く画面でOpenSearchの名前やインスタンスタイプ、ストレージ容量等を指定します。今回はテスト用なので専用マスタノードは指定していません。この場合はデータノードがマスタノードを兼ねる形になり、検索パフォーマンスにいくらか影響が出る可能性があるので、本番用途であれば別途専用マスタノードを立てることを検討ください。 続いてアクセス権とセキュリティの設定を行います。OpenSearchの配置先はVPCまたはパブリック領域のどちらかを選べます。VPCだとセキュリティ的に固いのですが、ネットワークの設定が若干煩雑なので今回はパブリックに配置することにしました。その代わりに特定IPアドレスのみアクセスできるように制限を加えています。自分の開発用マシンなど、OpenSearchへアクセスする必要があるPCやネットワークのIPを指定してください。 続いてタグの追加画面が表示されます。必要に応じて設定しましょう。 最後に確認画面上で「確認」を押すとクラスタが作成されます。ステータスが「読み込み中」から「アクティブ」に変われば作成完了です。 4-2.OpenSearchのインデックス作成 続いて検索に使用するインデックスの作成を行います。 (1)Kibana(OpenSearch Dashboards)へのアクセス KibanaはElasticsearchに同梱されているGUIの分析ツールです。直感的な操作で様々なグラフを生成できるためデータ分析などに活用できますが、これ以外にもElasticsearchに対して各種クエリ(データ検索や登録など)を実行したりできるので何かと便利です。 OpenSearchではKibanaから名前が変わって「OpenSearch Dashboards」になりましたが、今回はクラスタ作成時にElasticsearch 7.10を選択したので画面上でも「Kibana」という表現になります。若干ややこしいですがご了承ください。 Kinabaへアクセスするには以下ドメインの詳細画面を開き、「Kibana URL」のリンクをクリックします。 するとKibanaが開きます。画面左のバーガーメニューから「Dev Tools」を選択します。 以下の通り、Dev Toolsのコンソールが開きます。左側のエリアにOpenSearch用のクエリを入力して実行すれば、結果が右側のエリアに表示されます。以降の手順でOpenSearchのAPIを叩くときはこの画面を使用することを前提にしています。 (2)インデックス作成 OpenSearchのインデックスは、通常のデータベース(RDB)でいうところのテーブルにあたります。 前段に記載したサンプルデータ格納用のインデックスを作成してみましょう。クエリは以下の通りです。 ※kibana上で実行 PUT /test-products 今回はインデックス名をtest-productsにしてみました。この時点では定義も何もない空のインデックスができるだけなので、続く手順でデータ構造を定義していきます。 (3)Mapping作成 Mappingとは、インデックス内のフィールド等の構成を予め事前に定義したものです。RDBでいうところの、Create Table時に指定する中身(カラム名やデータ型等)に相当します。 OpenSearchには自動マッピング機能が備わっており、何もしなくてもデータ登録時に自動でMappingが生成されますが、その内容は最初に登録されるデータに依存します。場合によっては想定外の型が勝手に指定されてしまう恐れもあるため、基本的にはMappingは自分で作成した方が望ましいです。(この辺はESも同じ) 今回データ用のマッピング生成クエリは以下の通りです。 ※kibana上で実行 PUT /test-products/_mapping { "properties" : { "productCode" : {"type" : "keyword"}, "productName" : {"type" : "keyword"}, "brand" : {"type" : "keyword"}, "gender" : {"type" : "keyword"}, "category" : {"type" : "keyword"}, "price" : {"type" : "integer"}, "color" : {"type" : "keyword"}, "size" : {"type" : "keyword"} } } データ構造がネストしていないのでシンプルですね。今回は基本的に全文検索は不要なので、文字列型は全てkeyword、数値型はintegerにしました。 (4)インデックスへのデータ登録 続いてインデックスへのデータ登録を行います。今回は複数件を一括で登録したいのでBulk APIを使用します。 クエリは以下の通りで、データの内容は前段で紹介したものと同じです。インデックスのID(RDBのテーブルでいうところの主キーみたいなもの)は商品コードと同一としました。 ※kibana上で実行 POST /test-products/_doc/_bulk { "index" : {"_id" : "A001" } } {"productCode": "A001","productName": "無地スキニーパンツ","brand": "ブランドA","gender": "メンズ","category": "パンツ","price": "2000","color": "ホワイト","size": "S"} { "index" : {"_id" : "A002" } } {"productCode": "A002","productName": "無地スキニーパンツ","brand": "ブランドA","gender": "メンズ","category": "パンツ","price": "2000","color": "ホワイト","size": "M"} { "index" : {"_id" : "A003" } } {"productCode": "A003","productName": "無地スキニーパンツ","brand": "ブランドA","gender": "メンズ","category": "パンツ","price": "2000","color": "ホワイト","size": "L"} { "index" : {"_id" : "A004" } } {"productCode": "A004","productName": "無地スキニーパンツ","brand": "ブランドA","gender": "メンズ","category": "パンツ","price": "2000","color": "ブラック","size": "S"} { "index" : {"_id" : "A005" } } {"productCode": "A005","productName": "無地スキニーパンツ","brand": "ブランドA","gender": "メンズ","category": "パンツ","price": "2000","color": "ブラック","size": "M"} { "index" : {"_id" : "A006" } } {"productCode": "A006","productName": "無地スキニーパンツ","brand": "ブランドA","gender": "メンズ","category": "パンツ","price": "2000","color": "ブラック","size": "L"} { "index" : {"_id" : "B001" } } {"productCode": "B001","productName": "デニムパンツ","brand": "ブランドB","gender": "レディース","category": "パンツ","price": "2500","color": "ブラウン","size": "S"} { "index" : {"_id" : "B002" } } {"productCode": "B002","productName": "デニムパンツ","brand": "ブランドB","gender": "レディース","category": "パンツ","price": "2500","color": "ブラウン","size": "M"} { "index" : {"_id" : "B003" } } {"productCode": "B003","productName": "デニムパンツ","brand": "ブランドB","gender": "レディース","category": "パンツ","price": "2500","color": "ブラウン","size": "L"} { "index" : {"_id" : "B004" } } {"productCode": "B004","productName": "デニムパンツ","brand": "ブランドB","gender": "レディース","category": "パンツ","price": "2500","color": "ホワイト","size": "S"} { "index" : {"_id" : "B005" } } {"productCode": "B005","productName": "デニムパンツ","brand": "ブランドB","gender": "レディース","category": "パンツ","price": "2500","color": "ホワイト","size": "M"} { "index" : {"_id" : "B006" } } {"productCode": "B006","productName": "デニムパンツ","brand": "ブランドB","gender": "レディース","category": "パンツ","price": "2500","color": "ホワイト","size": "L"} { "index" : {"_id" : "C001" } } {"productCode": "C001","productName": "長袖Tシャツ","brand": "ブランドC","gender": "メンズ","category": "Tシャツ","price": "1500","color": "ホワイト","size": "S"} { "index" : {"_id" : "C002" } } {"productCode": "C002","productName": "長袖Tシャツ","brand": "ブランドC","gender": "メンズ","category": "Tシャツ","price": "1500","color": "ホワイト","size": "M"} { "index" : {"_id" : "C003" } } {"productCode": "C003","productName": "長袖Tシャツ","brand": "ブランドC","gender": "メンズ","category": "Tシャツ","price": "1500","color": "ホワイト","size": "L"} { "index" : {"_id" : "C004" } } {"productCode": "C004","productName": "長袖Tシャツ","brand": "ブランドC","gender": "キッズ","category": "Tシャツ","price": "1000","color": "グレー","size": "S"} { "index" : {"_id" : "C005" } } {"productCode": "C005","productName": "長袖Tシャツ","brand": "ブランドC","gender": "キッズ","category": "Tシャツ","price": "1000","color": "グレー","size": "M"} { "index" : {"_id" : "C006" } } {"productCode": "C006","productName": "長袖Tシャツ","brand": "ブランドC","gender": "キッズ","category": "Tシャツ","price": "1000","color": "グレー","size": "L"} 4-3.API作成 続いてChaliceを使った検索用APIの作成方法を解説します。 (1)ライブラリのインストールとプロジェクト作成 まずは開発環境のマシンにChaliceやOpenSearchクライアントなど、必要なライブラリをインストールします。 ※開発マシン上で実行 $ pip install chalice $ pip install opensearch-py $ pip install requests $ pip install requests-aws4auth 続いて以下のコマンドでChaliceの新規プロジェクトを作成します。 ※開発マシン上で実行 $ chalice new-project product_search_sample そうすると以下の構成でディレクトリとファイルが生成されます。 product_search_sample/ ├── app.py ├── .chalice │ └── config.json └── requirements.txt (2)検索API作成 続いて検索API用のコードを書いていきます。 今回使用するOpenSearch用のクエリはこんな感じです。_searchエンドポイントを使用しており、ブランドや性別などの属性値を指定することで条件に一致する商品の一覧が返ってくるようになっています。 GET /test-products/_search { "from" : 0, "size": 50, "track_total_hits" : true, "sort" : [ {"productName" : {"order" : "asc"}} ], "query" : { "bool" : { "must" : [ { "terms" : { "brand" : ["ブランドA", "ブランドB", "ブランドC"] } }, { "terms" : { "gender" : ["メンズ"] } }, { "terms" : { "category" : ["Tシャツ"] } }, { "terms" : { "color" : ["ホワイト"] } }, { "terms" : { "size" : ["L"] } }, { "range" : { "price" : { "gte" : 0, "lt" : 3000 } } } ] } } } クエリ内で指定している各条件の意味は以下の通りです。 項目 意味 from 指定した数字(番号)以降の検索結果を取得する。ページネーションなどで使用。 size 検索結果として取得する件数を指定する。50の場合は上位50件だけ取得。 track_total_hits trueに設定すると、1万件以上のデータを検索対象にするときに正確なcountが取れる。 sort ソート順を指定する。 query この中に検索条件を指定する。 bool, must 複数の検索条件を連結する際に使用。mustの場合はAND条件になる。 terms keyword型のフィールドに対する検索に使用。条件に完全一致するデータを抽出する。条件は配列で複数指定可能。 range 数値型のフィールドに対する検索に使用。範囲指定できる。gteは"以上"、ltは"未満"の扱いになる。 以下、最終的なChaliceのコードを記載していきます。まずディレクトリ構成はこのような形です。新たにcustom-policy_dev.jsonを追加しています。 product_search_sample/ ├── app.py ※コード修正 ├── .chalice │ ├── config.json ※コード修正 │ └── custom-policy_dev.json ※ファイル追加 └── requirements.txt ※コード修正 app.pyはこんな感じです。リクエストボディからJSON形式で検索条件を受け取り、OpenSearch用のクエリを生成して実行、結果を整形して返却します。今回はざっくりしたサンプルコードなので、モジュール分割などはせず全ての処理をapp.pyに押し込んでいます。またデータチェックやエラー制御などは端折ってますのでご了承ください。 app.py import json import traceback import boto3 from opensearchpy import OpenSearch, RequestsHttpConnection from requests_aws4auth import AWS4Auth from chalice import Chalice from chalice import Response app = Chalice(app_name='product_search_sample') # CloudFrontの制約で、GETではリクエストボディが使えないためPOSTにする @app.route('/productSearch', methods=['POST'], cors=True) def index(): try: # 検索条件取得 searchCondition = getSearchCondition() # 検索クエリの組み立て query = createQuery(searchCondition) # OpenSearchへの接続と検索 searchResultsFromOs = executeQuery(query) # 返却用JSONの生成 responseData = createResponseData(searchResultsFromOs) # API応答値の返却 return Response( body = json.dumps(responseData, ensure_ascii=False), headers = {'Content-Type': 'application/json'}, status_code = 200 ) except Exception as e: # スタックトレース出力とエラー応答 print(traceback.format_exc()) responseData = {"message" : "内部エラーが発生しました"} return Response( body = json.dumps(responseData, ensure_ascii=False), headers = {'Content-Type': 'application/json'}, status_code = 500 ) def getSearchCondition(): ''' リクエストボディから検索条件を抽出する ※入力チェックなどは特に行ってないので、必要に応じて実装 ''' body = app.current_request.json_body # 念のためリクエストボディの内容を組み換え searchCondition = dict() if body.get("brand"): searchCondition["brand"] = body["brand"] if body.get("gender"): searchCondition["gender"] = body["gender"] if body.get("category"): searchCondition["category"] = body["category"] if body.get("price"): searchCondition["price"] = body["price"] if body.get("color"): searchCondition["color"] = body["color"] if body.get("size"): searchCondition["size"] = body["size"] return searchCondition def createQuery(searchCondition): ''' OpenSearchに対して投げるクエリを生成する ''' # ベースとなるクエリ query = { "from" : 0, "size": 50, "track_total_hits" : True, "sort" : [ {"productName" : {"order" : "asc"}} ], "query" : { "bool" : { "must" : [] } } } # 検索条件が存在する場合、MUST句(AND)に検索条件を詰め込む if searchCondition: for key in searchCondition.keys(): searchParameKey = key searchParamValue = searchCondition.get(key) if key == "price": # 検索条件がpriceの場合は数値での条件を指定 query["query"]["bool"]["must"].append( { "range" : { searchParameKey : { "gte" : searchParamValue[0], "lt" : searchParamValue[1] } } } ) else: # price以外は文字列検索 query["query"]["bool"]["must"].append( { "terms" : {searchParameKey : searchParamValue} } ) return query def executeQuery(query): ''' OpennSearchへ接続し、検索クエリを投げて結果を返す ''' # 接続文字列 host = 'xxxxxx.ap-northeast-1.es.amazonaws.com' port = 443 region = 'ap-northeast-1' service = 'es' credentials = boto3.Session().get_credentials() awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token) indexName = 'test-products' try: # ES接続とクエリ実行 osClient = OpenSearch( hosts = [{'host':host, 'port': port}], http_auth = awsauth, use_ssl = True, verify_certs = True, connection_class = RequestsHttpConnection ) searchResultsFromOs = osClient.search(index=indexName, body=query) return searchResultsFromOs except Exception as e: # スタックトレース出力 print(traceback.format_exc()) raise e def createResponseData(searchResultsFromOs): ''' OpenSearchの検索結果を受け取り、APIの応答値を生成する ''' # 最終的な応答値の雛形を定義 responsData = { "status" : "success", "totalCount" : 0, "results" : [], } # 検索結果が存在しない場合は早々に処理終了 totalCount = searchResultsFromOs["hits"]["total"]["value"] if totalCount == 0: return responsData # 応答値の生成 responsData["totalCount"] = totalCount for result in searchResultsFromOs["hits"]["hits"]: source = result["_source"] responsData["results"].append( { "productCode" : source["productCode"], "productName" : source["productName"], "brand" : source["brand"], "gender" : source["gender"], "category" : source["category"], "price" : source["price"], "color" : source["color"], "size" : source["size"] } ) return responsData config.jsonはこんな感じ。autogen_policyとiam_policy_fileを追加して、Lambadに割り当てるロールを自分で指定できるようにしています。ロールに割り当てる権限はcustom-policy_dev.jsonで定義します。 .chalice/config.json { "version": "2.0", "app_name": "product_search_sample", "stages": { "dev": { "api_gateway_stage": "api", "autogen_policy": false, ※追加 "iam_policy_file": "custom-policy_dev.json" ※追加 } } } custom-policy_dev.jsonはこんな感じ。CloudWatch LogsとOpenSearch(Elasticsearch)へのアクセス権限を追加しています。今回はサンプルなのでOpenSearchの権限を広めにとっていますが、セキュリティが甘くなるのも微妙なので必要に応じて調整が必要です。 .chalice/custom-policy_dev.json { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowAccessCloudWatchLogs", "Effect": "Allow", "Action": [ "logs:CreateLogStream", "logs:CreateLogGroup", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:*:*" }, { "Sid": "AllowElasticsearch", "Effect": "Allow", "Action": [ "es:*" ], "Resource": "*" } ] } requirements.txtはこんな感じ。Lambda実行時に必要になるライブラリを定義しています。 requirements.txt requests==2.26.0 requests-aws4auth==1.1.1 opensearch-py==1.0.0 (3)APIのデプロイと実行 コードができたら「chalice deploy」でデプロイしましょう。以下の通り、コマンド一発で必要なLambda関数とAPI Gateway定義が全て作成されます。 ※開発マシン上で実行 $ chalice deploy Creating deployment package. Creating IAM role: product_search_sample-dev Creating lambda function: product_search_sample-dev Creating Rest API Resources deployed: - Lambda ARN: arn:aws:lambda:ap-northeast-1:xxxxxxxxx:function:product_search_sample-dev - Rest API URL: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/ ではcurlコマンドで今回デプロイしたAPIを叩いてみましょう。まずは商品カテゴリが「Tシャツ」の商品一覧を取得してみます。クエリはこちら。 ※開発マシン上で実行 $ curl -H "Content-Type: application/json" \ -d '{"category" : ["Tシャツ"]}' \ -XPOST https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/productSearch 結果はこうなりました。ちゃんと取れましたね。 { "status": "success", "totalCount": 6, "results": [ { "productCode": "C001", "productName": "長袖Tシャツ", "brand": "ブランドC", "gender": "メンズ", "category": "Tシャツ", "price": "1500", "color": "ホワイト", "size": "S" }, { "productCode": "C006", "productName": "長袖Tシャツ", "brand": "ブランドC", "gender": "キッズ", "category": "Tシャツ", "price": "1000", "color": "グレー", "size": "L" }, ~~~(以下略)~~~ さらにその他の属性(ブランド、性別、価格)で試してみましょう。 ※開発マシン上で実行 $ curl -H "Content-Type: application/json" \ -d '{"brand": ["ブランドA", "ブランドB", "ブランドC"], "gender" : ["メンズ"], "price" : [0, 3000]}' \ -XPOST https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/productSearch 結果はこちら。ちゃんと取れてますね。 { "status": "success", "totalCount": 9, "results": [ { "productCode": "A001", "productName": "無地スキニーパンツ", "brand": "ブランドA", "gender": "メンズ", "category": "パンツ", "price": "2000", "color": "ホワイト", "size": "S" }, { "productCode": "A002", "productName": "無地スキニーパンツ", "brand": "ブランドA", "gender": "メンズ", "category": "パンツ", "price": "2000", "color": "ホワイト", "size": "M" }, ~~~(以下略)~~~ 5.さいごに 今回はAWSのOpenSearch Serviceを使って簡単な商品検索APIを作ってみました。これだけだとRDB使った方が楽じゃんと思われるかもしれませんが、OpenSearchは大量データを取り扱った時にその真価を発揮します。商品点数が100万、1,000万と増えていっても安定した性能を出せるのがOpenSearchのいいところで、実際に使ってみた感じ、非力なマシンを使ってもRDBに比べて性能は出やすいように思えます。 さらにAggregation等の機能を使えば検索結果に対する集計や分析なんかもできるので、RDBだと処理が重くて諦めてしまいがちな高度な検索機能も実現できたりします。この辺りについてはまた別の記事に書けたらいいなと思っています。 この記事が誰かのお役に立てると幸いです。 おまけ:Chaliceについて ChaliceはAWSが開発したサーバレスアプリケーション向けのフレームワークで、Python向けのライブラリとして提供されています。API Gateway+LambdaによるオーソドックスなREST APIを始め、色々なサーバレスアプリを爆速で作ることができます。開発効率は他のフレームワーク(SAM、Amplify等々)と比較しても圧倒的です。(個人的感想ですが) 詳しい解説については以下の記事を参照ください。
- 投稿日:2022-01-11T00:33:25+09:00
webカメラを利用した感情分析をしてみた
どうも、学生エンジニアのirohasです。 今回はOpenCVとkerasを用いて、感情分析をしてみました。 今回のは共有というよりか、備忘録ならびに自己満記事ですので、説明聞きたい!という方はブラウザバック推奨です... 手順としては、モデルを作成→作成したモデルを使ってディープラーニング→webカメラで実行という形です。 ディープラーニングにおけるモデル作成などの説明はインターネット上や書籍などで詳しく解説されているものが多いのでこの記事では割愛させていただきます。 この記事ではディレクトリ構成と、起動する際のコードを貼り付けようと思います。 C:. ├─detector ├─models ├─__pycache__ ├─cnn.py ├─emotion_detector.py ├─load.py ├─make_model.py └─emotion_detector.py ディレクトリ構成はこんな感じです。 続いて、動かすコードの中身です。 emotion_detector.py from keras.preprocessing.image import img_to_array import imutils import cv2 from keras.models import load_model import numpy as np # モデルと分類器の読み込み detection = 'detector/haarcascade_frontalface_default.xml' emotion = 'models/_mini_XCEPTION.102-0.66.hdf5' #モデルの読み込み face_detection = cv2.CascadeClassifier(detection) emotion_classifier = load_model(emotion, compile=False) EMOTIONS = ["angry" ,"disgust","scared", "happy", "sad", "surprised", "neutral"] #ビデオの開始 cv2.namedWindow('Camera') cam = cv2.VideoCapture(0) while True: frame = cam.read()[1] frame = imutils.resize(frame,width=300) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_detection.detectMultiScale(gray,scaleFactor=1.1,minNeighbors=5,minSize=(30,30),flags=cv2.CASCADE_SCALE_IMAGE) canvas = np.zeros((250, 300, 3), dtype="uint8") frameClone = frame.copy() if len(faces) > 0: faces = sorted(faces, reverse=True, key=lambda x: (x[2] - x[0]) * (x[3] - x[1]))[0] (fX, fY, fW, fH) = faces #グレースケール画像から顔のROIを抽出、28x28ピクセル固定にリサイズして準備。 # ROIをCNN経由で分類する roi = gray[fY:fY + fH, fX:fX + fW] roi = cv2.resize(roi, (64, 64)) roi = roi.astype("float") / 255.0 roi = img_to_array(roi) roi = np.expand_dims(roi, axis=0) preds = emotion_classifier.predict(roi)[0] emotion_probability = np.max(preds) label = EMOTIONS[preds.argmax()] else: continue for (i, (emotion, prob)) in enumerate(zip(EMOTIONS, preds)): #ラベルテキストを構築 text = "{}: {:.2f}%".format(emotion, prob * 100) w = int(prob * 300) cv2.rectangle(canvas, (7, (i * 35) + 5), (w, (i * 35) + 35), (0, 0, 255), -1) cv2.putText(canvas, text, (10, (i * 35) + 23), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 2) cv2.putText(frameClone, label, (fX, fY - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 0, 255), 2) cv2.rectangle(frameClone, (fX, fY), (fX + fW, fY + fH), (0, 0, 255), 2) cv2.imshow('camera', frameClone) cv2.imshow("Emotion Now", canvas) if cv2.waitKey(1) & 0xFF == ord('q'): break camera.release() cv2.destroyAllWindows() ■実行結果 こんな感じで上手くいきました。 個人的にはFERとか他のライブラリでも作成してみたいと思ったのと、二画面ではなく一画面でリアルタイム感情分析ができたらなと思っています。 ディープラーニングはまだまだ奥深いなと感じたので、これからも引き続き勉強していきます。
- 投稿日:2022-01-11T00:25:24+09:00
デッキ画像から駒名称を抜き出す【テンプレートマッチング編】
はじめに こんにちは。逆転オセロニアのYouTubeチャンネル「まこちゃんねる」の中の人です。 本稿では、デッキ画像から駒名称を抜き出すことを目標にします。 抜き出す方法はいくつか存在すると思いますが、今回はテンプレートマッチングを使った方法を利用してみます。 モチベーション リスナーさん「デッキに入ってる駒名称が分からない!教えて!」 まこちゃん(うーん...毎回手動で書くの面倒くさいな...ある程度自動化したさ) テンプレートマッチングとは? 入力画像(今回であればデッキ画像)からテンプレート画像に類似する箇所を探索する手法です。類似度と座標を取得することができます。ただし、回転や拡縮に弱く処理も遅いのが欠点です。 環境 macOS JupyterLab Python3.6 OpenCV 実装の流れ テンプレート画像作成処理の実装 入力画像から駒画像を16分割してテンプレート画像を作成する 16分割したテンプレート画像の手動アノテーションをする テンプレートマッチング処理より駒名称を取得する実装 テンプレートマッチング処理を行う 入力画像の駒画像を16分割して中心点を取得する テンプレート画像と一致した箇所に矩形描画する 駒名称を出力する テンプレート画像作成処理の実装 前述した通り、駒名称を抜き出すには駒のテンプレート画像が必要です。 オセロニアの公式攻略サイトには全駒の顔画像と名前がセットで公開されているので、これを利用できれば手間が省けるのですが、テンプレートマッチングは拡縮に弱いです。今回は入力画像がスマホのスクショなので、当然サイズも違ってきてマッチングできません。環境で使われている駒種には限りがあるので、今回は入力画像であるデッキ画像から駒のテンプレート画像を作成することにしました。 入力画像から駒画像を16分割してテンプレート画像を作成する deck.png(デッキのスクショ画像)は撮影する端末とゲーム内のデッキ描画の仕様が変わらなければ、毎回同じ座標から駒画像を抜き出すことができるはずです。抜き出した各画像に連番を振り保存します。 import cv2 import matplotlib.pyplot as plt %matplotlib inline # デッキ画像の読み込み img = cv2.imread("deck.png") plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) # デッキ画像からテンプレート画像となる領域リストを取得する関数 def crop_roi_list(img, x_start=80, y_start=750, w=240, h=220): roi_list = [] for column in range(4): for row in range(4): x1 = x_start + w * row x2 = x1 + w y1 = y_start + h * column y2 = y1 + h roi_list.append(img[y1 : y2, x1 : x2]) return roi_list # テンプレート画像の作成 roi_list = crop_roi_list(img) for idx, roi in enumerate(roi_list, start=1): filename = f"{idx:0=2}.png" cv2.imwrite(f"template/{filename}", roi) 16分割したテンプレート画像の手動アノテーションをする 出力された16枚の画像を気合いで駒名にリネームする。 テンプレートマッチング処理より駒名称を取得する実装 テンプレート画像を網羅的にチェックすることで、入力画像に含まれる駒名称を全て抜き出します。今回は類似度が0.7を超えた場合は、入力画像にテンプレート画像が含まれていると判断することにします。また、入力画像を事前に16分割し各駒の中心点を取得しておきます。これは、同じ中心点を含むテンプレート画像が複数枚マッチした時に、最も高い類似度の画像を採用するためです。分かりやすいように、一致した箇所に矩形とその駒名称も描画します。 テンプレートマッチング処理を行う 入力画像と作成したテンプレート画像を網羅的にテンプレートマッチング処理を行い、類似度が閾値を超える画像のみ取得する。 import cv2 import numpy as np import glob from PIL import ImageFont, ImageDraw, Image from itertools import groupby # 入力画像の読み込み img = cv2.imread("input.png") # グレースケール変換 img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) # 駒名称と座標 piece_list = [] # テンプレート画像を網羅的に走査する files = glob.glob("template/*.png") for file in files: # テンプレート画像の読み込み template = cv2.imread(file) # グレースケール変換 template_gray = cv2.cvtColor(template, cv2.COLOR_RGB2GRAY) # テンプレート画像の高さ・幅 h, w = template_gray.shape # テンプレートマッチング match = cv2.matchTemplate(img_gray, template_gray, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(match) # 閾値を超えていたら出力する if max_val > 0.7: piece_name = file.split("/")[1].rstrip(".png") piece_list.append({ "name": piece_name, "max_val": max_val, "x1": max_loc[0], "x2": max_loc[0] + w, "y1": max_loc[1], "y2": max_loc[1] + h}) 入力画像の駒画像を16分割して中心点を取得する テンプレート画像作成時と同じ範囲で16分割し、中心点を取得しておく。中心点を含む複数枚の画像が閾値を超えた場合、最も類似度が高い画像を採用するために利用する。 # 中点を取得する x_start, y_start, w, h = 80, 750, 240, 220 piece_center_loc_list = [] for column in range(4): for row in range(4): x1 = x_start + w * row x2 = x1 + w y1 = y_start + h * column y2 = y1 + h cx = int((x1+x2)/2) cy = int((y1+y2)/2) piece_center_loc_list.append({"cx": cx, "cy": cy}) # 属する中心点を調べる for piece in piece_list: for idx, piece_center_loc in enumerate(piece_center_loc_list): x1, x2, y1, y2 = piece["x1"], piece["x2"], piece["y1"], piece["y2"] cx, cy = piece_center_loc["cx"], piece_center_loc["cy"] if x1 < cx < x2 and y1 < cy < y2: piece["idx"] = idx テンプレート画像と一致した箇所に矩形描画する テンプレート画像と一致した箇所に矩形を描画し、駒名称と類似度を表示しておくことで、出力された駒名称が正しいかどうかを一目で把握できるようにしておく。 # 駒名称リスト piece_name_list = [] # フォントを読み込み font = ImageFont.truetype("/System/Library/Fonts/ヒラギノ角ゴシック W9.ttc", 16) # 矩形と駒名称を描画する piece_list.sort(key=lambda item: item['idx']) for key, piece_group in groupby(piece_list, key=lambda x: x['idx']): # 最も類似度が高い駒を採用する piece = sorted(piece_group, key=lambda x: x['max_val'], reverse=True)[0] x1, x2, y1, y2 = piece["x1"], piece["x2"], piece["y1"], piece["y2"] name = piece["name"] score = piece["max_val"] piece_name_list.append(name) # 矩形を描画する cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 200), 2) # 類似度と駒名称を描画する img_pil = Image.fromarray(img) draw = ImageDraw.Draw(img_pil) draw.text((x1 + 4, y2 - 42), f"{round(score, 3)}", font = font, fill = (255,255,255), stroke_width=2, stroke_fill='black') draw.text((x1 + 4, y2 - 22), f"{name}", font = font, fill = (255,255,255), stroke_width=2, stroke_fill='black') img = np.array(img_pil) cv2.imwrite("output.png", img) 駒名称を出力する 最後は駒名称を出力して目的を達成する。出力されたテキストをコピペしてYouTubeのコメント欄にえいや!と貼り付けて終了! # 駒名称一覧を出力する print("\n".join(piece_name_list)) [高みへ挑む者]イリオット [吸殺の妖魔]ヴェルグレーデ [ゆるダンス]ブランジェッタ [駆ける山のニンフ]ハル おわりに 今回の実装は、私の端末、オセロニアのデッキ仕様が変わったら全てが終わります。 なので、次の目標としてはデッキ画像やテンプレート画像が変わっても対応できるような実装も考えていきたいですね。環境で使われている駒はそこまで多くないので、解像度はそのままで利用しましたが、速度が気になるようだったら加工も考えて行こうかなと思います。 参考
- 投稿日:2022-01-11T00:22:32+09:00
PythonプラグラムからOpenLDAPに接続&ユーザ情報を取得する
はじめに これまでJupyterHub/JupyterLabを利用した分析環境をVirtualBoxもしくはWSL2で整備してきたが、共通して認証にはOpenLDAPを使ってきた。 自宅PCでは自ずと筆者のみの利用ではあるが、会社(組織)で利用するならばユーザ管理をする必要がある。 どれだけのユーザがいて、いつパスワード変更をしたか、etc. というわけで、OpenLDAPに登録したユーザの情報を取得するプログラムを書いてみようと思う。 なぜプログラムか?かというと、OpenLDAP操作用のWebUIにはFusionDirectoryやphpLDAPadminがあるものの、全ユーザの情報を一覧で取得するのはちょっと面倒と思った次第でして。 そこで、本稿では、「PythonプラグラムからOpenLDAPに接続&ユーザ情報を取得する方法」を順に紹介してゆく。 本稿で紹介すること OpenLDAPの起動 PythonプログラムでOpenLDAPへの接続 PythonプログラムでOpenLDAPからユーザ情報の取得 本稿で紹介しないこと WSL2のインストール Ubuntuのインストール(From Microsoft Store) ※Ubuntu 18.04.5 LTSを使用 Dockerのインストール ※Docker Community Edition 20.10.8を使用 Pythonライブラリ(LDAP3)の全般 OpenLDAP操作用のWebUI(FusionDirectoryおよびphpLDAPadmin)の全般 過去の記事を参照されたし。 ※1: Windows10のPCに分析環境(VirtualBox/Vagrant+Kubernetes+JupyterHub/JupyterLab)を作ってみた ※2: Windows10のPCに分析環境(VirtualBox/Vagrant+Docker+JupyterHub/JupyterLab)を作ってみた ※3: GPU搭載WindowsのWSL2でNGCカタログのコンテナイメージを使う ※4: SeleniumとMeCabを使えるJupyterLabコンテナイメージを作る ステップ紹介 大きく、3ステップです。 1. OpenLDAPの起動 2. PythonプログラムでOpenLDAPへの接続 3. PythonプログラムでOpenLDAPからユーザ情報の取得 事前準備 以下、筆者の環境です。 $ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=18.04 DISTRIB_CODENAME=bionic DISTRIB_DESCRIPTION="Ubuntu 18.04.5 LTS" $ docker version Client: Docker Engine - Community Version: 20.10.8 API version: 1.41 Go version: go1.16.6 Git commit: 3967b7d Built: Fri Jul 30 19:54:08 2021 OS/Arch: linux/amd64 Context: default Experimental: true Server: Docker Engine - Community Engine: Version: 20.10.8 API version: 1.41 (minimum version 1.12) Go version: go1.16.6 Git commit: 75249d8 Built: Fri Jul 30 19:52:16 2021 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.4.9 GitCommit: e25210fe30a0a703442421b0f60afac609f950a3 nvidia: Version: 1.0.1 GitCommit: v1.0.1-0-g4144b63 docker-init: Version: 0.19.0 GitCommit: de40ad0 本稿ではhrektts/fusiondirectory-openldap:1.1.9-1.2-1を取得&起動し、ユーザ管理に利用するものとします。 また、過去の記事(※4)でビルドしたjupyter/minimal-notebook:9e8682c9ea54-MeCab-Slumを起動し、Pythonプログラムの実行に利用するものとします。 $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE jupyterhub/jupyterhub 1.0-gpu e7dd44e871bd 3 days ago 420MB jupyter/minimal-notebook 9e8682c9ea54-MeCab-Slum a2645c7160be 4 days ago 3.92GB jupyter/minimal-notebook 9e8682c9ea54-MeCab d0e1a0e79bc8 4 days ago 3.55GB postgres 9.6 131ab388dac1 4 months ago 200MB nvcr.io/nvidia/pytorch 20.11-py3-gpu dd53c8b61c4b 5 months ago 15.3GB nvcr.io/nvidia/tensorflow 20.11-tf2-py3-gpu f6aa8098fc48 5 months ago 12.3GB adminer 4.8.1 4727f36d62d9 5 months ago 90MB nvcr.io/nvidia/tensorflow 21.07-tf2-py3 887093b5693e 5 months ago 11.1GB nvcr.io/nvidia/pytorch 21.07-py3 7beec3ff8d35 5 months ago 15GB nvcr.io/nvidia/tensorflow 20.11-tf2-py3 98a7952f7f9c 14 months ago 11.6GB nvcr.io/nvidia/pytorch 20.11-py3 ae35b2b3cad1 14 months ago 13.2GB jupyterhub/jupyterhub 1.0 c399e04fda3c 2 years ago 283MB osixia/phpldapadmin 0.9.0 78148b61fdb5 2 years ago 302MB jupyter/minimal-notebook 2343e33dec46 c3bbd3471e39 3 years ago 2.72GB jupyter/minimal-notebook 9e8682c9ea54 400c44c4a7a7 3 years ago 2.79GB hrektts/fusiondirectory-openldap 1.1.9-1.2-1 7f2e4370509d 4 years ago 226MB hrektts/fusiondirectory 0.2.0 b56f1086a08d 4 years ago 345MB 1. OpenLDAPの起動 本ステップは、WSL上で実行 以下のリンクに記載された手順を見本とし、起動します。 hrektts/fusiondirectory-openldap hrektts/fusiondirectory osixia/phpldapadmin 以下、環境設定(環境変数の定義)を含む起動コマンドです。 $ docker run --name ldap -p 389:389 \ -e LDAP_ORGANISATION="OpenLDAP for ldapauthenticator.LDAPAuthenticator" \ -e LDAP_DOMAIN="hoge.com" \ -e LDAP_ADMIN_PASSWORD="ldapadminpwd" \ -e FD_ADMIN_PASSWORD="fdadminpwd" \ -d hrektts/fusiondirectory-openldap:1.1.9-1.2-1 ちなみに、FusionDirectoryの起動コマンドはこちら。 Webブラウザで http://127.0.0.1/fd/ にアクセスし、Usernameに「fd-admin」、Passwordに上述の「FD_ADMIN_PASSWORD」に指定した文字列でログインできればOKです。 本稿では、予めFusionDirectoryを操作し、「jhub」OUを作成&3ユーザを作成するものとします。 $ docker run --name fd -p 80:80 --link ldap:ldap \ -d hrektts/fusiondirectory:0.2.0 ちなみに、phpLDAPadminの起動コマンドはこちら。 Webブラウザで http://127.0.0.1:8080 にアクセスし、Login DNに「cn=admin,dc=hoge,dc=com」、Passwordに上述の「LDAP_ADMIN_PASSWORD」に指定した文字列でログインできればOKです。 $ docker run --name phpldapadmin \ --hostname phpldapadmin-service \ --link ldap:ldap-host \ --env PHPLDAPADMIN_LDAP_HOSTS=ldap-host \ --restart=always \ --publish=8443:443 \ --publish=8080:80 \ --env PHPLDAPADMIN_HTTPS=false \ --detach osixia/phpldapadmin:0.9.0 2. PythonプログラムでOpenLDAPへの接続 本ステップは、JupyterLab上で実行 以下、PythonとPIPのVer情報です。 $ python -V Python 3.6.6 $ python -m pip --version pip 21.2.2 from /opt/conda/lib/python3.6/site-packages/pip (python 3.6) 以下、必要なPythonライブラリのインストールコマンドです。(PIPが古くなってますね、、、気になる方はUpgradeをば。) $ python -m pip install ldap3 Requirement already satisfied: ldap3 in /opt/conda/lib/python3.6/site-packages (2.7) Requirement already satisfied: pyasn1>=0.1.8 in /opt/conda/lib/python3.6/site-packages (from ldap3) (0.4.8) WARNING: You are using pip version 21.2.2; however, version 21.3.1 is available. You should consider upgrading via the '/opt/conda/bin/python -m pip install --upgrade pip' command. 以下、OpenLDAPへの接続Code例です。 OpenLDAPのIPアドレス、ポート番号、SSL接続有無は、読者の環境に合わせて書き換えてください。 from ldap3 import Server, Connection, ALL, NTLM, SUBTREE, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES server = Server('${OpenLDAPのIPアドレス}', get_info=ALL, port=389, use_ssl=False) conn = Connection(server, user='cn=admin,dc=hoge,dc=com', password='ldapadminpwd', auto_bind=True) 接続確立後、接続に指定したuserのDN文字列が表示できればOKです。 conn.extend.standard.who_am_i() # 'dn:cn=admin,dc=hoge,dc=com' 3. PythonプログラムでOpenLDAPからユーザ情報の取得 本ステップは、JupyterLab上で実行 以下、OpenLDAPからユーザ情報の取得Code例です。 OpenLDAPの階層(OU)は、読者の環境に合わせて書き換えてください。 LDAPクエリを実行し、Trueが表示できればOKです。 conn.search('ou=people,ou=jhub,dc=hoge,dc=com', '(objectclass=person)', attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES], paged_size=100, search_scope=SUBTREE) # True 検索結果をPrintしてみます。 for entry in sorted(conn.entries): print(entry.entry_dn) print(entry.entry_raw_attribute) 今回は、「jhub」OUの配下の全ユーザの情報が出力されます。 uid=admin,ou=people,ou=jhub,dc=hoge,dc=com <bound method EntryBase.entry_raw_attribute of DN: uid=admin,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.304321 cn: admin jupyterhub createTimestamp: 2022-01-10 12:43:49+00:00 creatorsName: cn=admin,dc=hoge,dc=com entryCSN: 20220110124409.081861Z#000000#000#000000 entryUUID: b44f782e-065e-103c-9bc3-61c9f2a130f0 givenName: admin hasSubordinates: False modifiersName: cn=admin,dc=hoge,dc=com modifyTimestamp: 2022-01-10 12:44:09+00:00 objectClass: inetOrgPerson organizationalPerson person sn: jupyterhub structuralObjectClass: inetOrgPerson subschemaSubentry: cn=Subschema uid: admin userPassword: b'{SSHA}cnvbA3tVbuxbCZYyafChFw6qV3ZLlfv/' > uid=user1,ou=people,ou=jhub,dc=hoge,dc=com <bound method EntryBase.entry_raw_attribute of DN: uid=user1,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.303493 cn: user1 jupyterhub createTimestamp: 2022-01-10 12:41:50+00:00 creatorsName: cn=admin,dc=hoge,dc=com entryCSN: 20220110124150.915437Z#000000#000#000000 entryUUID: 6d5d2ba0-065e-103c-9bbf-61c9f2a130f0 givenName: user1 hasSubordinates: False modifiersName: cn=admin,dc=hoge,dc=com modifyTimestamp: 2022-01-10 12:41:50+00:00 objectClass: inetOrgPerson organizationalPerson person sn: jupyterhub structuralObjectClass: inetOrgPerson subschemaSubentry: cn=Subschema uid: user1 userPassword: b'{SSHA}VAHOD9cryu8XfEcX/Q99/k2Tvoqg8E/B' > uid=user2,ou=people,ou=jhub,dc=hoge,dc=com <bound method EntryBase.entry_raw_attribute of DN: uid=user2,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.303761 cn: user2 jupyterhub createTimestamp: 2022-01-10 12:42:22+00:00 creatorsName: cn=admin,dc=hoge,dc=com entryCSN: 20220110124222.193645Z#000000#000#000000 entryUUID: 8001d8a0-065e-103c-9bc0-61c9f2a130f0 givenName: user2 hasSubordinates: False modifiersName: cn=admin,dc=hoge,dc=com modifyTimestamp: 2022-01-10 12:42:22+00:00 objectClass: inetOrgPerson organizationalPerson person sn: jupyterhub structuralObjectClass: inetOrgPerson subschemaSubentry: cn=Subschema uid: user2 userPassword: b'{SSHA}JTT6fLPXEsdfOLgPNI4WSBIKUjJ6uEL+' > uid=user3,ou=people,ou=jhub,dc=hoge,dc=com <bound method EntryBase.entry_raw_attribute of DN: uid=user3,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.304017 cn: user3 jupyterhub createTimestamp: 2022-01-10 12:42:40+00:00 creatorsName: cn=admin,dc=hoge,dc=com entryCSN: 20220110124240.888741Z#000000#000#000000 entryUUID: 8b267dd0-065e-103c-9bc1-61c9f2a130f0 givenName: user3 hasSubordinates: False modifiersName: cn=admin,dc=hoge,dc=com modifyTimestamp: 2022-01-10 12:42:40+00:00 objectClass: inetOrgPerson organizationalPerson person sn: jupyterhub structuralObjectClass: inetOrgPerson subschemaSubentry: cn=Subschema uid: user3 userPassword: b'{SSHA}qCTxg7K1DaelVbKfkL/1twiQEPCcCpxD' > もう一息! 以下、取得済みのユーザ情報のCSVファイル出力Code例です。 3列構成("uid":ユーザ名、"createTimestamp":登録日時、"modifyTimestamp":更新日時≒最終パスワード変更日時)です。 import codecs f = codecs.open('users.csv', 'w', 'utf-8') f.write('"uid","createTimestamp","modifyTimestamp"\n') for entry in sorted(conn.entries): f.write('%s,%s,%s\n' %(str(entry['uid']), str(entry['createTimestamp']), str(entry['modifyTimestamp']))) f.close() パスワード変更をすると、"modifyTimestamp"の値が更新されます。 users.csv "uid","createTimestamp","modifyTimestamp" admin,2022-01-10 12:43:49+00:00,2022-01-10 14:38:53+00:00 user1,2022-01-10 12:41:50+00:00,2022-01-10 12:41:50+00:00 user2,2022-01-10 12:42:22+00:00,2022-01-10 12:42:22+00:00 user3,2022-01-10 12:42:40+00:00,2022-01-10 12:42:40+00:00 JupyterLabのCSTTableのビューだと、以下のような見た目になります。 まとめ とりあえずは、PythonプラグラムからOpenLDAPに接続&ユーザ情報を取得することができました。 OpenLDAPに保存されている他の属性値も出力すれば、ユーザ情報の一覧化には重宝しそうな印象です。
- 投稿日:2022-01-11T00:22:32+09:00
PythonプログラムからOpenLDAPに接続&ユーザ情報を取得する
はじめに これまでJupyterHub/JupyterLabを利用した分析環境をVirtualBoxもしくはWSL2で整備してきたが、共通して認証にはOpenLDAPを使ってきた。 自宅PCでは自ずと筆者のみの利用ではあるが、会社(組織)で利用するならばユーザ管理をする必要がある。 どれだけのユーザがいて、いつパスワード変更をしたか、etc. というわけで、OpenLDAPに登録したユーザの情報を取得するプログラムを書いてみようと思う。 なぜプログラムか?かというと、OpenLDAP操作用のWebUIにはFusionDirectoryやphpLDAPadminがあるものの、全ユーザの情報を一覧で取得するのはちょっと面倒と思った次第でして。 そこで、本稿では、「PythonプログラムからOpenLDAPに接続&ユーザ情報を取得する方法」を順に紹介してゆく。 本稿で紹介すること OpenLDAPの起動 PythonプログラムでOpenLDAPへの接続 PythonプログラムでOpenLDAPからユーザ情報の取得 本稿で紹介しないこと WSL2のインストール Ubuntuのインストール(From Microsoft Store) ※Ubuntu 18.04.5 LTSを使用 Dockerのインストール ※Docker Community Edition 20.10.8を使用 Pythonライブラリ(LDAP3)の全般 OpenLDAP操作用のWebUI(FusionDirectoryおよびphpLDAPadmin)の全般 過去の記事を参照されたし。 ※1: Windows10のPCに分析環境(VirtualBox/Vagrant+Kubernetes+JupyterHub/JupyterLab)を作ってみた ※2: Windows10のPCに分析環境(VirtualBox/Vagrant+Docker+JupyterHub/JupyterLab)を作ってみた ※3: GPU搭載WindowsのWSL2でNGCカタログのコンテナイメージを使う ※4: SeleniumとMeCabを使えるJupyterLabコンテナイメージを作る ステップ紹介 大きく、3ステップです。 1. OpenLDAPの起動 2. PythonプログラムでOpenLDAPへの接続 3. PythonプログラムでOpenLDAPからユーザ情報の取得 事前準備 以下、筆者の環境です。 $ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=18.04 DISTRIB_CODENAME=bionic DISTRIB_DESCRIPTION="Ubuntu 18.04.5 LTS" $ docker version Client: Docker Engine - Community Version: 20.10.8 API version: 1.41 Go version: go1.16.6 Git commit: 3967b7d Built: Fri Jul 30 19:54:08 2021 OS/Arch: linux/amd64 Context: default Experimental: true Server: Docker Engine - Community Engine: Version: 20.10.8 API version: 1.41 (minimum version 1.12) Go version: go1.16.6 Git commit: 75249d8 Built: Fri Jul 30 19:52:16 2021 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.4.9 GitCommit: e25210fe30a0a703442421b0f60afac609f950a3 nvidia: Version: 1.0.1 GitCommit: v1.0.1-0-g4144b63 docker-init: Version: 0.19.0 GitCommit: de40ad0 本稿ではhrektts/fusiondirectory-openldap:1.1.9-1.2-1を取得&起動し、ユーザ管理に利用するものとします。 また、過去の記事(※4)でビルドしたjupyter/minimal-notebook:9e8682c9ea54-MeCab-Slumを起動し、Pythonプログラムの実行に利用するものとします。 $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE jupyterhub/jupyterhub 1.0-gpu e7dd44e871bd 3 days ago 420MB jupyter/minimal-notebook 9e8682c9ea54-MeCab-Slum a2645c7160be 4 days ago 3.92GB jupyter/minimal-notebook 9e8682c9ea54-MeCab d0e1a0e79bc8 4 days ago 3.55GB postgres 9.6 131ab388dac1 4 months ago 200MB nvcr.io/nvidia/pytorch 20.11-py3-gpu dd53c8b61c4b 5 months ago 15.3GB nvcr.io/nvidia/tensorflow 20.11-tf2-py3-gpu f6aa8098fc48 5 months ago 12.3GB adminer 4.8.1 4727f36d62d9 5 months ago 90MB nvcr.io/nvidia/tensorflow 21.07-tf2-py3 887093b5693e 5 months ago 11.1GB nvcr.io/nvidia/pytorch 21.07-py3 7beec3ff8d35 5 months ago 15GB nvcr.io/nvidia/tensorflow 20.11-tf2-py3 98a7952f7f9c 14 months ago 11.6GB nvcr.io/nvidia/pytorch 20.11-py3 ae35b2b3cad1 14 months ago 13.2GB jupyterhub/jupyterhub 1.0 c399e04fda3c 2 years ago 283MB osixia/phpldapadmin 0.9.0 78148b61fdb5 2 years ago 302MB jupyter/minimal-notebook 2343e33dec46 c3bbd3471e39 3 years ago 2.72GB jupyter/minimal-notebook 9e8682c9ea54 400c44c4a7a7 3 years ago 2.79GB hrektts/fusiondirectory-openldap 1.1.9-1.2-1 7f2e4370509d 4 years ago 226MB hrektts/fusiondirectory 0.2.0 b56f1086a08d 4 years ago 345MB 1. OpenLDAPの起動 本ステップは、WSL上で実行 以下のリンクに記載された手順を見本とし、起動します。 hrektts/fusiondirectory-openldap hrektts/fusiondirectory osixia/phpldapadmin 以下、環境設定(環境変数の定義)を含む起動コマンドです。 $ docker run --name ldap -p 389:389 \ -e LDAP_ORGANISATION="OpenLDAP for ldapauthenticator.LDAPAuthenticator" \ -e LDAP_DOMAIN="hoge.com" \ -e LDAP_ADMIN_PASSWORD="ldapadminpwd" \ -e FD_ADMIN_PASSWORD="fdadminpwd" \ -d hrektts/fusiondirectory-openldap:1.1.9-1.2-1 ちなみに、FusionDirectoryの起動コマンドはこちら。 Webブラウザで http://127.0.0.1/fd/ にアクセスし、Usernameに「fd-admin」、Passwordに上述の「FD_ADMIN_PASSWORD」に指定した文字列でログインできればOKです。 本稿では、予めFusionDirectoryを操作し、「jhub」OUを作成&3ユーザを作成するものとします。 $ docker run --name fd -p 80:80 --link ldap:ldap \ -d hrektts/fusiondirectory:0.2.0 ちなみに、phpLDAPadminの起動コマンドはこちら。 Webブラウザで http://127.0.0.1:8080 にアクセスし、Login DNに「cn=admin,dc=hoge,dc=com」、Passwordに上述の「LDAP_ADMIN_PASSWORD」に指定した文字列でログインできればOKです。 $ docker run --name phpldapadmin \ --hostname phpldapadmin-service \ --link ldap:ldap-host \ --env PHPLDAPADMIN_LDAP_HOSTS=ldap-host \ --restart=always \ --publish=8443:443 \ --publish=8080:80 \ --env PHPLDAPADMIN_HTTPS=false \ --detach osixia/phpldapadmin:0.9.0 2. PythonプログラムでOpenLDAPへの接続 本ステップは、JupyterLab上で実行 以下、PythonとPIPのVer情報です。 $ python -V Python 3.6.6 $ python -m pip --version pip 21.2.2 from /opt/conda/lib/python3.6/site-packages/pip (python 3.6) 以下、必要なPythonライブラリのインストールコマンドです。(PIPが古くなってますね、、、気になる方はUpgradeをば。) $ python -m pip install ldap3 Requirement already satisfied: ldap3 in /opt/conda/lib/python3.6/site-packages (2.7) Requirement already satisfied: pyasn1>=0.1.8 in /opt/conda/lib/python3.6/site-packages (from ldap3) (0.4.8) WARNING: You are using pip version 21.2.2; however, version 21.3.1 is available. You should consider upgrading via the '/opt/conda/bin/python -m pip install --upgrade pip' command. 以下、OpenLDAPへの接続Code例です。 OpenLDAPのIPアドレス、ポート番号、SSL接続有無は、読者の環境に合わせて書き換えてください。 from ldap3 import Server, Connection, ALL, NTLM, SUBTREE, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES server = Server('${OpenLDAPのIPアドレス}', get_info=ALL, port=389, use_ssl=False) conn = Connection(server, user='cn=admin,dc=hoge,dc=com', password='ldapadminpwd', auto_bind=True) 接続確立後、接続に指定したuserのDN文字列が表示できればOKです。 conn.extend.standard.who_am_i() # 'dn:cn=admin,dc=hoge,dc=com' 3. PythonプログラムでOpenLDAPからユーザ情報の取得 本ステップは、JupyterLab上で実行 以下、OpenLDAPからユーザ情報の取得Code例です。 OpenLDAPの階層(OU)は、読者の環境に合わせて書き換えてください。 LDAPクエリを実行し、Trueが表示できればOKです。 conn.search('ou=people,ou=jhub,dc=hoge,dc=com', '(objectclass=person)', attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES], paged_size=100, search_scope=SUBTREE) # True 検索結果をPrintしてみます。 for entry in sorted(conn.entries): print(entry.entry_dn) print(entry.entry_raw_attribute) 今回は、「jhub」OUの配下の全ユーザの情報が出力されます。 uid=admin,ou=people,ou=jhub,dc=hoge,dc=com <bound method EntryBase.entry_raw_attribute of DN: uid=admin,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.304321 cn: admin jupyterhub createTimestamp: 2022-01-10 12:43:49+00:00 creatorsName: cn=admin,dc=hoge,dc=com entryCSN: 20220110124409.081861Z#000000#000#000000 entryUUID: b44f782e-065e-103c-9bc3-61c9f2a130f0 givenName: admin hasSubordinates: False modifiersName: cn=admin,dc=hoge,dc=com modifyTimestamp: 2022-01-10 12:44:09+00:00 objectClass: inetOrgPerson organizationalPerson person sn: jupyterhub structuralObjectClass: inetOrgPerson subschemaSubentry: cn=Subschema uid: admin userPassword: b'{SSHA}cnvbA3tVbuxbCZYyafChFw6qV3ZLlfv/' > uid=user1,ou=people,ou=jhub,dc=hoge,dc=com <bound method EntryBase.entry_raw_attribute of DN: uid=user1,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.303493 cn: user1 jupyterhub createTimestamp: 2022-01-10 12:41:50+00:00 creatorsName: cn=admin,dc=hoge,dc=com entryCSN: 20220110124150.915437Z#000000#000#000000 entryUUID: 6d5d2ba0-065e-103c-9bbf-61c9f2a130f0 givenName: user1 hasSubordinates: False modifiersName: cn=admin,dc=hoge,dc=com modifyTimestamp: 2022-01-10 12:41:50+00:00 objectClass: inetOrgPerson organizationalPerson person sn: jupyterhub structuralObjectClass: inetOrgPerson subschemaSubentry: cn=Subschema uid: user1 userPassword: b'{SSHA}VAHOD9cryu8XfEcX/Q99/k2Tvoqg8E/B' > uid=user2,ou=people,ou=jhub,dc=hoge,dc=com <bound method EntryBase.entry_raw_attribute of DN: uid=user2,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.303761 cn: user2 jupyterhub createTimestamp: 2022-01-10 12:42:22+00:00 creatorsName: cn=admin,dc=hoge,dc=com entryCSN: 20220110124222.193645Z#000000#000#000000 entryUUID: 8001d8a0-065e-103c-9bc0-61c9f2a130f0 givenName: user2 hasSubordinates: False modifiersName: cn=admin,dc=hoge,dc=com modifyTimestamp: 2022-01-10 12:42:22+00:00 objectClass: inetOrgPerson organizationalPerson person sn: jupyterhub structuralObjectClass: inetOrgPerson subschemaSubentry: cn=Subschema uid: user2 userPassword: b'{SSHA}JTT6fLPXEsdfOLgPNI4WSBIKUjJ6uEL+' > uid=user3,ou=people,ou=jhub,dc=hoge,dc=com <bound method EntryBase.entry_raw_attribute of DN: uid=user3,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.304017 cn: user3 jupyterhub createTimestamp: 2022-01-10 12:42:40+00:00 creatorsName: cn=admin,dc=hoge,dc=com entryCSN: 20220110124240.888741Z#000000#000#000000 entryUUID: 8b267dd0-065e-103c-9bc1-61c9f2a130f0 givenName: user3 hasSubordinates: False modifiersName: cn=admin,dc=hoge,dc=com modifyTimestamp: 2022-01-10 12:42:40+00:00 objectClass: inetOrgPerson organizationalPerson person sn: jupyterhub structuralObjectClass: inetOrgPerson subschemaSubentry: cn=Subschema uid: user3 userPassword: b'{SSHA}qCTxg7K1DaelVbKfkL/1twiQEPCcCpxD' > もう一息! 以下、取得済みのユーザ情報のCSVファイル出力Code例です。 3列構成("uid":ユーザ名、"createTimestamp":登録日時、"modifyTimestamp":更新日時≒最終パスワード変更日時)です。 import codecs f = codecs.open('users.csv', 'w', 'utf-8') f.write('"uid","createTimestamp","modifyTimestamp"\n') for entry in sorted(conn.entries): f.write('%s,%s,%s\n' %(str(entry['uid']), str(entry['createTimestamp']), str(entry['modifyTimestamp']))) f.close() パスワード変更をすると、"modifyTimestamp"の値が更新されます。 users.csv "uid","createTimestamp","modifyTimestamp" admin,2022-01-10 12:43:49+00:00,2022-01-10 14:38:53+00:00 user1,2022-01-10 12:41:50+00:00,2022-01-10 12:41:50+00:00 user2,2022-01-10 12:42:22+00:00,2022-01-10 12:42:22+00:00 user3,2022-01-10 12:42:40+00:00,2022-01-10 12:42:40+00:00 JupyterLabのCSTTableのビューだと、以下のような見た目になります。 まとめ とりあえずは、PythonプログラムからOpenLDAPに接続&ユーザ情報を取得することができました。 OpenLDAPに保存されている他の属性値も出力すれば、ユーザ情報の一覧化には重宝しそうな印象です。