ラベル Python の投稿を表示しています。 すべての投稿を表示
ラベル Python の投稿を表示しています。 すべての投稿を表示

2023年1月30日月曜日

Raspberry Piに2つのBluetoothスピーカーを接続してPythonで再生する

Raspberry Piに2つのBluetoothスピーカーを接続して、Pythonでその2つのスピーカーのうち1台を指定して音声ファイルを再生してみる。


環境

Raspberry Pi B+とRaspberry Pi OS。

$ lsb_release -dr
Description:    Raspbian GNU/Linux 11 (bullseye)
Release:        11
$ python3 -V
Python 3.9.2


2つのBluetoothスピーカーをRaspberry Piに接続する

今回使用するのはRaspberry Pi B+で、本体にBluetoothの機能がないので、USB接続のBluetoothアダプタ(ドングル)を2つ用意して接続する。Bluetoothアダプタが認識されているか確認しておく。

$ lsusb
Bus 001 Device 004: ID xxxx:xxxx Cambridge Silicon Radio, Ltd Bluetooth Dongle (HCI mode)
Bus 001 Device 005: ID xxxx:xxxx Cambridge Silicon Radio, Ltd Bluetooth Dongle (HCI mode)
...


必要なモジュールのインストールと設定

Raspberry PiにBluetoothスピーカーをつなげると同様に、サウンドサーバとそのbluetoothモジュールをインストールし、使用するユーザ(pi)をbluetoothグループに追加しておく。

以下コマンドでモジュールのインストール。

$ sudo apt install pulseaudio pulseaudio-module-bluetooth

ユーザーをbluetoothグループに追加。

$ sudo usermod -a -G bluetooth pi
$ sudo reboot


2つのBluetoothスピーカーを接続

2つのBluetoothアダプタそれぞれにBluetoothスピーカーを接続する。基本的な接続方法はRaspberry PiにBluetoothスピーカーをつなげると同じだが、Bluetoothアダプタが2つ以上ある場合は、接続設定する前に、Bluetoothアダプタを選択しておく。bluetoothctlのlistコマンドでBluetoothアダプタの一覧が表示される(defaultが現在選択されているアダプタ)ので、selectコマンドで設定するアダプタを選択する。

$ bluetoothctl
[bluetooth]# list
Controller 11:11:11:11:11:11 raspberrypi #2 [default]
Controller 22:22:22:22:22:22 raspberrypi

以下のようにアドレスを指定して選択。

[bluetooth]# select 22:22:22:22:22:22
Controller 22:22:22:22:22:22 raspberrypi [default]

アダプタを選択したら、スピーカーを接続する。devicesコマンドで接続可能なBluetoothデバイスの一覧が表示されるので、接続するBluetoothスピーカーが一覧にあるのを確認する。

[bluetooth]# power on
[bluetooth]# scan on
[bluetooth]# devices

アドレスを指定してBluetoothスピーカーに接続する。

[bluetooth]# pair XX:XX:XX:XX:XX:XX
[bluetooth]# connect XX:XX:XX:XX:XX:XX
[bluetooth]# quit

selectコマンドでアダプタを切り替えて、もうひとつのBluetoothスピーカーも同様に接続する。


PythonでBluetoothスピーカーから音声ファイルを再生する

Pythonで音声ファイルを再生するには音声ファイルを再生するPythonコードをcron実行するのように、VLCで音声ファイルを再生する。まずは、PythonでVLCを制御するモジュールをインストール。

$ pip3 install python-vlc
$ pip3 show python-vlc
Name: python-vlc
Version: 3.0.16120
...

今回は2つのBluetoothスピーカーのうちひとつを選択して、音声ファイルを再生する。やり方としては、python-vlcでスピーカー名とそのアドレスを取得できるので、それをもとに再生するスピーカーを選択する。スピーカー名とそのアドレスは以下のように音声出力デバイス情報から取得できる。

import vlc

player = vlc.MediaPlayer()
mod = player.audio_output_device_enum()
while mod:
    mod = mod.contents
    address = mod.device.decode('utf-8').split('.')[1]
    print('--------------------')
    print('device =', mod.device)
    print('address =', address)
    print('name =', mod.description.decode('utf-8'))
    mod = mod.next

上記コードを実行すると以下のような結果が得られる。ひとつ目の内部オーディオはRaspberry Pi本体のオーディオ出力。今回は同じモデルのBluetoothスピーカーを2台使用しているので、スピーカー名は同じになる。このスピーカー名とアドレスで音声ファイルを再生するスピーカーを選択する。

device = b'alsa_output.platform-bcm2835_audio.analog-stereo'
address = platform-bcm2835_audio
name = 内部オーディオ Analog Stereo
--------------------
device = b'bluez_sink.AA_AA_AA_AA_AA_AA.a2dp_sink'
address = AA_AA_AA_AA_AA_AA
name = Speaker
--------------------
device = b'bluez_sink.BB_BB_BB_BB_BB_BB.a2dp_sink'
address = BB_BB_BB_BB_BB_BB
name = Speaker

以下のPythonスクリプトで、接続された2つのBluetoothスピーカーのうち1台を指定して音声ファイルを再生する。このスクリプトと同じディレクトリに再生する音声ファイル(ここではtest.mp3)を配置しておく。

import os
import sys
from time import sleep

import vlc

# スピーカー名とアドレスの設定
SPEAKER1 = {'name': 'Speaker', 'address': 'XX_XX_XX_XX_XX_XX'}
SPEAKER2 = {'name': 'Speaker', 'address': 'YY_YY_YY_YY_YY_YY'}
SPEAKERS = [SPEAKER1, SPEAKER2]

# 再生する音声ファイル
# スクリプトと同じディレクトリに配置する
SOUND_FILE = 'test.mp3'


def get_output_device(player, speaker):
    """指定のスピーカーを取得する"""
    speaker_name = speaker['name']
    speaker_address = speaker['address']
    mod = player.audio_output_device_enum()
    while mod:
        mod = mod.contents
        address = mod.device.decode('utf-8').split('.')[1]
        if speaker_name == mod.description.decode('utf-8') and speaker_address == address:
            return mod.device
        mod = mod.next

    return None


def play(speaker, volume):
    """指定したスピーカーで音声ファイルを再生"""
    script_dir = os.path.dirname(os.path.realpath(__file__))
    media_path = os.path.join(script_dir, SOUND_FILE)
    player = vlc.MediaPlayer(media_path)
    device = get_output_device(player, speaker)
    if device:
        player.audio_output_device_set(None, device)
        player.audio_set_volume(volume) # ボリューム設定
        player.play()

        # 再生が終わるまで実行を継続する
        sleep(10)


def main():
    # 引数としてスピーカーID(0か1)を指定する(デフォルトは0)
    speaker_id = 0
    if len(sys.argv) >= 2 and sys.argv[1] in ('0', '1'):
        speaker_id = int(sys.argv[1])

    play(SPEAKERS[speaker_id], volume=100)

if __name__ == '__main__':
    main()

2つ目のスピーカーから再生する場合は次のように実行する。

$ python3 sample.py 1


2023年1月16日月曜日

InstagramグラフAPIとPythonで指定ハッシュタグ付きの最新投稿を取得する

InstagramグラフAPIを使うとInstagramの投稿情報などを取得できる。ただ、APIの使用に必要なアクセストークンの取得にやや手間がかかる。Pythonで指定したハッシュタグ付きの最新投稿を取得してみたので、アクセストークンの取得も含めて手順をまとめておく。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.5 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


APIアクセストークンの取得

InstagramグラフAPIを使うため、FacebookグラフAPIのアクセストークンを取得する。FacebookとInstagramのアカウントはあるとする。Instagramアカウントはプロアカウントに切り替えておく。


FacebookページとInstagramプロアカウントの紐づけ

アクセストークン取得前に、FacebookページとInstagramプロアカウントを紐づけておく。Facebookページがなければ、Facebookのメニューの[ページ]を開いて、[新しいページを作成]から作成できる。

InstagramプロアカウントのFacebookページへの紐づけは、Facebookページの画面の[Instagram投稿を宣伝]>[アカウントをリンク]で行う。


Facebook for Developersでアプリの作成

次に、Facebook for Developersアカウントを作成する。Meta  for Developersにアクセスして、アカウントを作成する。アカウントを作成したら、マイアプリの画面でアプリを作成する。今回は以下の設定で作成。

アプリタイプ:ビジネス
アプリ名:テストアプリ(任意名)
アプリの連絡先メールアドレス:登録メールアドレス(デフォルト)
ビジネスマネージャーアカウントをお持ちですか?:ビジネスマネージャアカウントが設定されていません(デフォルト)

アプリを作成したら、[アプリに製品を追加]からInstagramグラフAPIの[設定]ボタンを押す。これで、メニューの商品欄にInstagramグラフAPIが追加される。ここまでやったら、画面右下の[変更を保存]ボタンを押す。


permissonの追加

Facebook for Developersのトップメニューの[ツール]>[グラフAPIエクスプローラー]を開く。[Metaアプリ]で作成したアプリを選択し、[ユーザーまたはページ]で[ページアクセストークン]からFacebookページを選択してから、permissionを追加する。▼をクリックし、さらにOtherを開くとInstagramのpermissionが表示される。今回は以下5つを追加。

  • instagram_basic
  • instagram_content_publish
  • instagram_manage_comments
  • instagram_manage_insights
  • instagram_manage_messages



permissionを追加し、[Generate Access Token]をクリックすると、Facebookでログインのポップアップ画面が表示される。この画面ではFacebookユーザーとして続行をクリック。

次に表示される画面では、今回は[現在のページのみにオプトイン]を選択。Facebookページを選択してから[続行]ボタンを押す。

続く画面では、今回は[現在のInstagramアカウントのみにオプトイン]を選択。Instagramプロアカウントを選択してから[続行]ボタンを押す。

アプリが要求するアクセス許可の確認画面が表示されるので、[続行]で先に進む。次の画面で[OK]を押すと、アクセストークンが表示される。


無期限アクセストークンの取得

ここまでで取得できたアクセストークンには期限があるので、無期限のアクセストークンを取得する。[ツール]>[グラフAPIエクスプローラー]でグラフAPIエクスプローラを開いてて、[Metaアプリ]で対称のアプリを選択し、アクセストークン左のiアイコンをクリックするとアクセストークン情報が表示される。


画面右下の[アクセストークンツールで開く]をクリックして、アクセストークンツールを開き、[アクセストークンを延長]をクリックすると、期限のないアクセストークンが表示される。


[デバッグ]ボタンを押すと無期限のアクセストークン情報が表示される。有効期限が「受け取らない」になっていればOK。



InstagramビジネスアカウントID の取得

アクセストークンのほかにAPI使用に必要なInstagramビジネスアカウントIDを取得する。Facebook for Developersのトップメニューの[ツール]>[グラフAPIエクスプローラー]を開く。クエリに「me?fields=accounts{instagram_business_account}」を指定して、[送信]ボタンを押して表示されるレスポンスのinstagram_business_accountがInstagramビジネスアカウントID。



指定ハッシュタグ付きの最新投稿を取得

無期限アクセストークンとInstagram ビジネスアカウントIDが取得できたので、指定したハッシュタグが付いた最新の投稿をAPIで取得してみる。

はじめに、APIのエンドポイントアクセスに使うRequestsをインストールしておく。

$ pip3 install requests

指定ハッシュタグ付きの最新投稿を取得する手順としては、IG Hashtag SearchでハッシュタグIDを取得して、そのIDをもとにIGハッシュタグ付きの最近のメディアで最新投稿を検索する。以下コードでは、ハッシュタグ「スイーツ」が付いた最新の投稿を取得する。

from pprint import pprint

import requests

API_VER = 'v15.0'
INSTA_ACCOUNT_ID = 'xxxxxxxxxx' # Instagram ビジネスアカウントID
INSTA_ACCESS_TOKEN = 'xxxxxxxxxxx' # 無期限アクセストークン


# ハッシュタグ「スイーツ」のIDを取得
query = 'スイーツ'
ig_hashtag_search_url = f'https://graph.facebook.com/{API_VER}/ig_hashtag_search?user_id={INSTA_ACCOUNT_ID}&q={query}&access_token={INSTA_ACCESS_TOKEN}'
response = requests.get(ig_hashtag_search_url)
res = response.json()['data']
hashtag_id = res[0]['id']
print('hashtag_id=', hashtag_id)

# ハッシュタグ「スイーツ」がついた最新投稿を取得
recent_media_url = f'https://graph.facebook.com/{hashtag_id}/recent_media?user_id={INSTA_ACCOUNT_ID}&fields=id,media_type,media_url,comments_count,like_count,caption,permalink,timestamp&access_token={INSTA_ACCESS_TOKEN}'
response = requests.get(recent_media_url)
res = response.json()['data']
pprint(res)

上記コードを実行すると、以下のような結果が得られる。

hashtag_id= 17843787853011507
[{'caption': 'xxxxx\n'
             '#xxxxx\n'
             '#xxxxx',
  'comments_count': 0,
  'id': '11111111111111',
  'like_count': 0,
  'media_type': 'IMAGE',
  'media_url': ''https://xxxxx',
  'permalink': 'https://www.instagram.com/p/xxxxx/',
  'timestamp': '2023-01-14T07:30:42+0000'},
 {'caption': 'xxxxxxxxxxxxx\n'
             '\n'
             '#xxxxxxxxxxxxxxxxxx',
  'comments_count': 0,
  ...


2022年12月30日金曜日

PythonとTwitter API v2でリアルタイムにツイートを取得する

Twitter API v1.1のリアルタイムのツイート収集が使えなくなったので、API v2に移行した。PythonライブラリのTweepyとTwitter API v2でリアルタイムにツイート収集する方法をまとめておく。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.5 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ pip3 show tweepy
Name: tweepy
Version: 4.12.1
...


Twitter API のBearer Tokenの取得

Twitter API v2でリアルタイムにツイートを収集するために、Twitterの開発者サイトでBearer Tokenを取得しておく。開発者サイトのProjects & Apps > Overviewの画面で、対象アプリの[Keys and tokens](鍵のアイコン)を開く。この画面でBearer Tokenを生成できる。


Tweepyでリアルタイムツイート取得

TweepyでTwitter API v2を使ってリアルタイムにツイートを収集するには、StreamingClientクラスを使う。v1.1のとき(Tweepy4でリアルタイムツイートを取得する)と同様に、このクラスを継承したクラスを作成する。以下のコードでは、Twitter API v2のsampleメソッドを使って、リアルタイムに1%のツイートを出力する。

import tweepy

# Twitter APIの認証情報
# Twitterの開発者向けページで取得したBearer Tokenを使う
BEARER_TOKEN = 'Bearer Token'

class TwStream(tweepy.StreamingClient):
    def on_tweet(self, tweet):
        print('-------------------------')
        print(tweet.data)

def main():
    twstream = TwStream(BEARER_TOKEN)
    twstream.sample()

if __name__ == '__main__':
    main()

ただ、このコードで取得できるのはツイートのIDやテキストのみで、取得できるデータはv1.1のときと違ってかなり限定されている。v2では、取得する情報を指定する必要がある。


APIで取得対象のデータを指定する

Twitter API v2では、ExpansionsFieldsを指定することで、さまざまなデータを取得できる。以下のコードでは、ExpansionsとTweetフィールド、Userフィールドを指定。

import tweepy

# Twitter APIの認証情報
# Twitterの開発者向けページで取得したBearer Tokenを使う
BEARER_TOKEN = 'Bearer Token'

class TwStream(tweepy.StreamingClient):
    def on_tweet(self, tweet):
        print('-------------------------')
        print(tweet.data)

def main():
    twstream = TwStream(BEARER_TOKEN)
    # 取得するExpansions
    expansions = ['author_id', 'referenced_tweets.id', 'attachments.media_keys']
    # 取得するTweetフィールド
    tweet_fields = ['author_id', 'created_at', 'attachments', 'lang', 'public_metrics']
    # 取得するUserフィールド
    user_fields = ['created_at', 'description', 'location', 'verified']
    twstream.sample(expansions=expansions, tweet_fields=tweet_fields, user_fields=user_fields)

if __name__ == '__main__':
    main()

上記のように取得データを指定できるが、v1.1のように言語の指定ができない。このままだと、英語やさまざまな言語のツイートが混ざって取得される。filterメソッドを使うと言語の指定ができるが、filterメソッドでは取得できるツイート数に制限があるので、大量のツイートを収集したい場合には使えない。日本語で絞りたい場合は次のようなコードになる。取得したTweetオブジェクトのlangフィールドを参照して、日本語(ja)のみに限定する。

import tweepy

# Twitter APIの認証情報
# Twitterの開発者向けページで取得したBearer Tokenを使う
BEARER_TOKEN = 'Bearer Token'

class TwStream(tweepy.StreamingClient):
    def on_tweet(self, tweet):
        if tweet.lang == 'ja':
            print('-------------------------')
            print(tweet.data)

def main():
    twstream = TwStream(BEARER_TOKEN)
    # 取得するExpansions
    expansions = ['author_id', 'referenced_tweets.id', 'attachments.media_keys']
    # 取得するTweetフィールド
    tweet_fields = ['author_id', 'created_at', 'attachments', 'lang', 'public_metrics']
    # 取得するUserフィールド
    user_fields = ['created_at', 'description', 'location', 'verified']
    twstream.sample(expansions=expansions, tweet_fields=tweet_fields, user_fields=user_fields)

if __name__ == '__main__':
    main()



2022年11月27日日曜日

Raspberry Piで動作するSelenium+最新PythonのDockerコンテナを作成する

Raspberry Pi上のDockerでSeleniumを使う場合、Doker Hubにはselenium/standalone-chromeをforkしてRaspberry PiのCPUアーキテクチャであるARMに対応したseleniarm/standalone-chromiumがある。ただ、selenium/standalone-chromeのイメージにインストールされているPythonのバージョンは3.8と少し古い。そこで、今回はSeleniumと最新バージョンのPythonがインストールされたDockerコンテナを作成して、Raspberry Piで起動してみる。


環境

Raspberry Pi OS(64bit)。Raspberry Pi OS(64bit)にDockerとComposeをインストールする方法はRaspberry Pi OS 64bitでDockerを使うを参照。

$ lsb_release -dr
Description:    Raspbian GNU/Linux 11 (bullseye)
Release:        11
$ docker --version
Docker version 20.10.21, build baeda1f
$ docker-compose --version
docker-compose version 1.29.2, build unknown


Dockerfileの作成

Seleniumと最新Pythonが使えるDockerイメージを作成するためのDockerfileを用意する。ベースとなるDockerイメージとしてDocker Hubにある公式のPyrthonイメージの3.11-bullseyeを使う。Seleniumを使うにはブラウザとドライバが必要だが、今回はFirefoxとそのドライバであるgeckodriverをインストールする。Firefoxで日本語を表示できるように、日本語フォントの googlefonts / noto-cjkもインストールする。

FROM python:3.11-bullseye

WORKDIR /app

# Firefoxと日本語フォントインストール
RUN apt update \
    && apt -y install --no-install-recommends \
    # 日本語フォントインストール
    # 日本語フォントをインストールしておかないとFirefoxの表示が文字化けする
    fonts-noto-cjk \
    firefox-esr \
    && apt clean

# SeleniumのPythonバインディングインストール
RUN pip install selenium --no-cache-dir

# geckodriverインストール
RUN curl -LO https://github.com/mozilla/geckodriver/releases/download/v0.32.0/geckodriver-v0.32.0-linux-aarch64.tar.gz \
  && tar xzvf geckodriver-v0.32.0-linux-aarch64.tar.gz \
  && mv geckodriver /usr/bin/ \
  && rm geckodriver-v0.32.0-linux-aarch64.tar.gz


docker-composeファイルとSeleniumを使うサンプルスクリプトの用意

Dockerfileのほかに、docker-compose.ymlとSeleniumを使うPythonサンプルスクリプトを用意する。

version: '3'
services:
  selenium:
    build:
      context: .
      dockerfile: ./Dockerfile
    tty: true
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - ./app:/app


以下のPythonスクリプトでは、Firefoxを起動してGoogleで「Raspberry Pi」を検索し、結果のスクリーンショットを保存する。

import os

from selenium.webdriver import Firefox, FirefoxOptions
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions

url = 'https://www.google.com'

# ブラウザーをヘッドレスで起動
options = FirefoxOptions()
options.add_argument('-headless')
service = Service(
    executable_path='/usr/bin/geckodriver', log_path=os.devnull
)
with Firefox(service=service, options=options) as driver:
    driver.get(url)

    # 画面表示を最大5秒まで待つ
    driver.implicitly_wait(5)
    # 検索ボックスのエレメントを取得(inputタグのname属性はq)
    elem = driver.find_element(By.NAME, 'q')
    # 検索ボックス内のテキストをクリア
    elem.clear()
    # 検索ワードを入力
    elem.send_keys('Raspberry Pi')
    # 検索ボタンがクリックできるようになるまで最大10秒待つ
    wait = WebDriverWait(driver, 10)
    input = wait.until(expected_conditions.element_to_be_clickable((By.XPATH, "//input[@value='Google 検索']")))

    if input.get_attribute('type') == 'submit':
        input.click()
    else:
        print('Failed to find submit button.')

    # 検索結果がなければ「No results found.」と表示する
    assert 'No results found.' not in driver.page_source

    # スクリーンショットを保存
    driver.save_screenshot('firefox_search.png')


ファイル配置は以下のようになる。

|-- Dockerfile
|-- app
| `-- sample.py
`-- docker-compose.yml


コンテナ上でSeleniumを使う

ファイルの準備ができたら、Dockerイメージを作成してSeleniumをPythonで使ってみる。まずはイメージをビルドする。

$ docker-compose build

続いて、コンテナ上で、作成したPythonスクリプトを実行する。

$ docker-compose run selenium python sample.py

スクリプトを実行すると、以下のようなfirefox_search.pngというFirefoxのスクリーンショットファイルが保存される。


2022年11月11日金曜日

音声ファイルを再生するPythonコードをcron実行する

Raspberry Piで音声ファイルを再生するPythonコードを作成したが、それをcron実行しても音声が再生されなかった。調べたところ、環境変数を設定する必要があった。cronでPythonスクリプトを実行して音声を再生させる手順をまとめておく。


環境

Raspberry Pi OS。

$ lsb_release -dr
Description:    Raspbian GNU/Linux 11 (bullseye)
Release:        11
$ python3 -V
Python 3.9.2


Pythonで音声ファイルを再生する

まずは音声ファイルをPythonで再生してみる。ここではPythonからVLCを使って再生する。そのために、PythonでVLCを制御するモジュールをインストールする。

$ pip3 install python-vlc

インストールされたバージョン。

$ pip3 show python-vlc
Name: python-vlc
Version: 3.0.16120
...

以下のコードでスクリプトファイルと同じディレクトリにあるtest.mp3を再生できる。

import os
import time
import vlc

script_dir = os.path.dirname(os.path.realpath(__file__))
media_path = os.path.join(script_dir, 'test.mp3')
p = vlc.MediaPlayer(media_path)
p.play()

# 再生が終わるまで実行を継続する
time.sleep(10)


cronでPythonコードを実行する

作成したスクリプトファイルtest.pyを~/appディレクトリ配下に配置して、cronで実行してみる。

$ crontab -e

ここでは以下のようにcronに設定を追加する。動作確認のために1分ごとに実行させて、ログファイル(test.log)を出力する。

* * * * *  python3 ~/app/test.py > ~/app/test.log 2>&1

ただし、上記のようにcronで実行させても音声ファイルを再生できない。以下のエラーメッセージが出力される。

vlcpulse audio output error: PulseAudio server connection failure: Connection refused


cronで音声ファイルを再生する

cronで音声ファイルを再生できるようにするには、サウンドサーバであるpulseaudioが使用するディレクトリを環境変数PULSE_RUNTIME_PATHに設定する。今回は以下のようなシェルスクリプトのファイル(test.sh)をPythonスクリプトと同じディレクトリに作成し、cronで実行する。

#!/bin/sh

export PULSE_RUNTIME_PATH="/run/user/$(id -u)/pulse"
python3 ~/app/test.py

作成したスクリプトファイルに実行権限を付与しておく。

$ chmod u+x test.sh

このスクリプトファイルをcronで実行すると音声ファイルを再生できる。

$ crontab -e 

動作確認のために以下のような設定をcronに追加する。こうすると1分ごとにPythonスクリプトが実行されて、音声ファイルが再生される。

* * * * *  ~/app/test.sh


2022年10月24日月曜日

SQLAlchemyでテーブルレコードを逐次取得してメモリ使用量を減らす

SQLAlchemyでデータベースのテーブルからデータを取得するときに、テーブルレコードの件数が多くなるほど、Pythonプロセスのメモリ使用量が増える。場合によっては、システムのメモリ不足に陥ることもあり得る。SQLAlchemyでは、レコードを逐次取得することでメモリ使用量を減らすことができるので、その方法をまとめておく。


SQLAlchemyのstream_resultsオプションについて

SQLAlchemyはデフォルトでclient side cursorsで動作し、取得したデータをすべてメモリに保持する。一方、server side cursorsで動作した場合は、クライアント側で必要とされる分だけ逐次保持される。つまり、server side cursorsを使うとメモリ使用量を抑えることができる。Working with Engines and ConnectionsのUsing Server Side Cursors (a.k.a. stream results)に説明があり、以下はその抜粋。

A client side cursor here means that the database driver fully fetches all rows from a result set into memory before returning from a statement execution. Drivers such as those of PostgreSQL and MySQL/MariaDB generally use client side cursors by default. A server side cursor, by contrast, indicates that result rows remain pending within the database server’s state as result rows are consumed by the client. 

SQLAlchemyでは、execution_optionsメソッドでstream_results=Trueとすることで、server side cursorsで動作させられる。


環境

WSL2(Ubuntu20.04)とデータベースはMariaDB 10.5。

$ lsb_release -dr
Description:    Ubuntu 20.04.5 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ mariadbd --version
mariadbd  Ver 10.5.17-MariaDB-1:10.5.17+maria~ubu2004 for debian-linux-gnu on x86_64 (mariadb.org binary distribution)

SQLAlchemyのほかに、DBAPIとしてmysqlclient、メモリ使用量を確認するためにメモリプロファイラーのmemory_profilerをインストールする。

$ pip3 install mysqlclient sqlalchemy memory_profiler

インストールされたバージョン。

$ pip show mysqlclient | grep Version
Version: 2.1.1

$ pip show sqlalchemy | grep Version
Version: 1.4.41

$ pip show memory_profiler | grep Version
Version: 0.60.0


WSL2のDNS設定変更

後述のテストデータベースの準備でgit cloneをするが、そのときに「Could not resolve host: github.com」というメッセージが表示されてcloneできない場合は、UbuntuのDNS設定を変更する(WSL2 DNS stops working #4285)。cloneに問題がない場合は、この項は不要なので、次のテストデータベースの準備へ進む。

WSL2 Ubuntuの/etc/wsl.confに以下記述を追加する。

$ sudo vi /etc/wsl.conf
+ [network]
+ generateResolvConf = false

Ubuntuのターミナルを終了して、一度WSLを停止する。

PS > wsl --shutdown

Ubuntuを起動して、/etc/resolv.confに以下を追記する。

$ sudo vi /etc/resolv.con
+ nameserver 8.8.8.8


テストデータベースの準備

SQLAlchemyでメモリ使用量を確認するためには、それなりのレコード件数のあるテーブルが必要になる。このテスト用データベースとして、Other MySQL Documentationにあるemployee dataを使う。これはGitHubのtest_dbで公開されている。

まずはこのリポジトリをcloneする。

$ git clone https://github.com/datacharmer/test_db.git

このリポジトリにはemployeesというデータベースがあり、以下のようにデータベースをインポートできる。

$ cd test_db
$ mysql -u username -p < employees.sql

インポートが完了したら、テーブルを確認。

$ mysql -u username -p -D employees

MariaDB [employees]> show tables;
+----------------------+
| Tables_in_employees  |
+----------------------+
| current_dept_emp     |
| departments          |
| dept_emp             |
| dept_emp_latest_date |
| dept_manager         |
| employees            |
| salaries             |
| titles               |
+----------------------+


メモリ使用量の比較

stream_results=Trueを設定した場合としない場合でメモリ使用量を比較してみる。以下のようなstream_sample1.pyとdatabase.pyを用意する。以下のコードでは、salariesテーブルから全件を取得して、salaryカラムの平均を求める。stream_results=Trueを設定する場合はfetch_strem関数を、設定しない場合はfetch関数を使用する。

import statistics

from sqlalchemy import MetaData, Table, select
from memory_profiler import profile

from database import db


def _get_table_object(tablename):
    # テーブル名からテーブルオブジェクトを取得
    meta_data = MetaData(bind=db.engine)
    meta_data.reflect(only=[tablename])
    return meta_data.tables[tablename]


@profile
def fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execute(stmt)
    return statistics.fmean([result['salary'] for result in results])


@profile
def stream_fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execution_options(stream_results=True).execute(stmt)
    return statistics.fmean([result['salary'] for result in results])


@profile
def main():
    tablename = 'salaries'
    salary = fetch(tablename)
    # salary = stream_fetch(tablename)
    print(f'salary={salary}')


if __name__ == '__main__':
    main()
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session


class Database:
    def __init__(self):
        db_user = 'username' # DBユーザー名
        db_passwd = 'password' # DB接続パスワード
        db_name = 'employees' # DB名
        self.engine = create_engine(
            f'mysql+mysqldb://{db_user}:{db_passwd}@localhost:3306/{db_name}?charset=utf8mb4'
        )

        self.session = scoped_session(
            sessionmaker(autocommit=False, autoflush=True, bind=self.engine)
        )


db = Database()

以下はfetch関数(stream_results=True設定なし)を使用した場合の結果。SELECT文を実行したところで600MBほどメモリを使用している。

$ time python3 sample_stream1.py
Filename: sample_stream1.py

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    88     35.6 MiB     35.6 MiB           1   @profile
    89                                         def fetch(tablename):
    90     36.7 MiB      1.2 MiB           1       con = db.engine.connect()
    91     37.1 MiB      0.3 MiB           1       table_obj = _get_table_object(tablename)
    92     37.1 MiB      0.0 MiB           1       stmt = select(table_obj)
    93    634.7 MiB    597.6 MiB           1       results = con.execute(stmt)
    94    659.2 MiB     24.5 MiB     2844050       return statistics.fmean([result['salary'] for result in results])


salary=63810.744836143705
Filename: sample_stream1.py

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
   106     35.6 MiB     35.6 MiB           1   @profile
   107                                         def main():
   108     35.6 MiB      0.0 MiB           1       tablename = 'salaries'
   109     63.3 MiB     27.7 MiB           1       salary = fetch(tablename)
   110                                             # salary = stream_fetch(tablename)
   111     63.3 MiB      0.0 MiB           1       print(f'salary={salary}')



real    3m23.536s
user    2m58.915s
sys     0m23.535s

以下はstream_fetch関数(stream_results=True設定)を使用した場合。SELECT文を実行したところではメモリを消費しておらず、メモリ消費のピークはsalaryカラムの平均を求める箇所で約150MB。stream_results=Trueを設定しない場合に比べて、メモリ使用のピークは4分の1程度となっている。topコマンドで確認した限り、MariaDBサーバーのプロセスが使用するメモリ使用量はどちらでも変わらないので、stream_results=Trueを設定することで、Pythonプロセスで減ったメモリ使用量がそのままシステムのメモリ使用量の減少になる。ちなみに、実行速度とのトレードオフになるが、salaryの平均値を求める際にリストでなくジェネレーター式を使うとさらにメモリ消費を抑えられる。

$ time python3 sample_stream1.py
Filename: sample_stream1.py

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    96     35.5 MiB     35.5 MiB           1   @profile
    97                                         def stream_fetch(tablename):
    98     36.7 MiB      1.2 MiB           1       con = db.engine.connect()
    99     37.0 MiB      0.3 MiB           1       table_obj = _get_table_object(tablename)
   100     37.0 MiB      0.0 MiB           1       stmt = select(table_obj)
   101     37.0 MiB      0.0 MiB           1       results = con.execution_options(stream_results=True).execute(stmt)
   102    147.1 MiB      1.6 MiB     2844050       return statistics.fmean([result['salary'] for result in results])


salary=63810.744836143705
Filename: sample_stream1.py

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
   105     35.5 MiB     35.5 MiB           1   @profile
   106                                         def main():
   107     35.5 MiB      0.0 MiB           1       tablename = 'salaries'
   108                                             # salary = fetch(tablename)
   109     38.2 MiB      2.7 MiB           1       salary = stream_fetch(tablename)
   110     38.2 MiB      0.0 MiB           1       print(f'salary={salary}')



real    3m0.012s
user    2m35.694s
sys     0m23.953s


server side cursorsの注意点

server side cursorsで動作させる場合は注意が必要な点がある。server side cursorsの場合、同じ接続で複数の操作を実行できない。例えば、以下のコードではsalariesテーブルから全件取得したあとにsalariesテーブルからemp_no=10001のレコードを削除しようとしているが、「Commands out of sync; you can't run this command now」というエラーになる。stream_results=Trueを設定していない場合はエラーにならない。

import statistics

from sqlalchemy import MetaData, Table, select, delete
from memory_profiler import profile

from database import db


def _get_table_object(tablename):
    # テーブル名からテーブルオブジェクトを取得
    meta_data = MetaData(bind=db.engine)
    meta_data.reflect(only=[tablename])
    return meta_data.tables[tablename]


@profile
def stream_fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execution_options(stream_results=True).execute(stmt)

    # 以下はエラーになる
    # stream_results=Trueとした同じ接続でテーブル操作を行おうとすると以下エラーになる
    # sqlalchemy.exc.ProgrammingError: (MySQLdb.ProgrammingError) (2014, "Commands out of sync; you can't run this command now")
    stmt = delete(table_obj).where(table_obj.c.emp_no == 10001)
    con.execute(stmt)

    return statistics.fmean([result['salary'] for result in results])


@profile
def main():
    tablename = 'salaries'
    salary = stream_fetch(tablename)
    print(f'salary={salary}')


if __name__ == '__main__':
    main()

複数処理を実行したい場合は、以下のようにデータベース接続を新たに作成する。

import statistics

from sqlalchemy import MetaData, Table, select, delete
from memory_profiler import profile

from database import db


def _get_table_object(tablename):
    # テーブル名からテーブルオブジェクトを取得
    meta_data = MetaData(bind=db.engine)
    meta_data.reflect(only=[tablename])
    return meta_data.tables[tablename]


@profile
def stream_fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execution_options(stream_results=True).execute(stmt)

    # 以下はエラーにならない
    con2 = db.engine.connect()
    stmt2 = delete(table_obj).where(table_obj.c.emp_no == 10001)
    con2.execute(stmt2)

    return statistics.fmean([result['salary'] for result in results])


@profile
def main():
    tablename = 'salaries'
    salary = stream_fetch(tablename)
    print(f'salary={salary}')


if __name__ == '__main__':
    main()

上記コードではstream_results=Trueを設定してSELECTで取得したあとにDELETEしているが、resultsはDELETE前のSELECT時点のデータとなる。


yeild_perオプションについて

stream_results=Trueだけを指定した場合、はじめは少ないバッファーサイズから徐々にmax_row_buffer(デフォルト1000)を上限として増やしていく。それに対して、バッファーサイズを一定にしたい場合は、yeild_perを使う(Fetching Large Result Sets with Yield Per)。yeild_perを使うと、stream_results=Trueの場合と同様にserver side cursorsで動作する。

以下コードは、stream_sample1.pyのstream_fetch関数をyeild_perを使ってバッファサイズを一定(1000)にするように書き換えたもの。

@profile
def stream_fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execution_options(yield_per=1000).execute(stmt)
    return statistics.fmean([result['salary'] for result in results])

stream_results=Trueを指定して上記コードと同じ結果となるコードを書くことができる。ただし、以下のようにオプション指定が増えたりするので、yeild_perを使うほうが簡単。

@profile
def stream_fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execution_options(stream_results=True, max_row_buffer=1000).execute(stmt)
    return statistics.fmean([result['salary'] for result in results.yield_per(1000)])


2022年9月26日月曜日

Twitter Streaming APIのfilterは日本語に対応していない

Twitter Streaming APIではリアルタイムにツイートを取得できる。sampleとfilterの2種類の機能があるが、filterではキーワードを指定することで、このキーワードを含むツイートを取得することができる。と思っていたのだが、実際にfilterを使ってツイートを収集してみると、期待通りのツイートが取得できていない。

実際にキーワードを指定して収集してみる。以下コードはTwitter Streaming APIで省略されていないツイートテキストを取得するのコードを変更して、filterでキーワード「スイーツ」を指定したもの。これにより、「スイーツ」を含むツイートが取得できることを期待している。

import tweepy

# Twitter APIの認証情報
# Twitterの開発者向けのページで取得したキーとトークンを使う
CONSUMER_KEY = 'Consumer Key'
CONSUMER_SECRET = 'Consumer Secret'
ACCESS_TOKEN = 'Access Token'
ACCESS_TOKEN_SECRET = 'Access Token Secret'

class TwStream(tweepy.Stream):
    def _get_text(self, status):
        # 拡張ツイートの場合
        if hasattr(status, 'extended_tweet'):
            return status.extended_tweet['full_text']
        else:
            return status.text

    def on_status(self, status):
        # status(APIで取得できるデータ)を受け取ったときにon_statusが実行される
        print('-------------------------')
        # リツイートの場合は「RT」+ メンション分だけ文字数オーバーするので省略されることがある
        if hasattr(status, 'retweeted_status'):
            text = self._get_text(status.retweeted_status)
        else:
            text = self._get_text(status)
        # スクリーンネームとツイートを表示
        print(f'@{status.author.screen_name} {text}')

def main():
    twstream = TwStream(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)

    # キーワード「スイーツ」を指定してツイートを取得
    twstream.filter(track=['スイーツ'], languages=['ja'])

if __name__ == '__main__':
    main()

実際に取得できたツイートすべてに「スイーツ」が含まれるが、よくよくみるとハッシュタグ「#スイーツ」や「スイーツ」のあとに空白があるツイートのみしか取得できていない。Twitterで「スイーツ」で検索した結果と比べてみても、それが確認できる。

Twitter APIの仕様を確認すると、Standard streaming API request parametersには以下の一文がある。

Non-space separated languages, such as CJK are currently unsupported.

つまり、filterでは日本語などの単語がスペースで区切られていない言語は未サポートということ。キーワードとして「スイーツ」を指定した場合、スイーツの直後にスペースがあるハッシュタグは取得できるが、ツイートテキスト内に「スイーツ」があっても取得対象にならない。過去のツイートを取得できるSearch APIでは、キーワード「スイーツ」を指定して「スイーツ」を含むツイートを取得できるが、Streaming APIでは同様のことはできないようだ。

Stream APIのfilterで日本語を使う場合は、特定のハッシュタグを含むツイートを取得する場合など、限定的な使い方になりそう。


2022年9月18日日曜日

Twitter Streaming APIで省略されていないツイートテキストを取得する

Twitter Streaming APIでツイートテキストを取得するときに、省略されていないテキスト全文を取得する方法をまとめておく。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.5 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


省略されていないテキストを取得する方法

Twitter Streaming APIでツイートテキストを収集する場合、レスポンスデータのtextフィールドに、最大140文字までのテキストが格納されている。

ただし、2017年に半角文字の言語では280文字までに文字数制限が拡張されたために、ツイートテキストが140文字を超える場合がある。ツイートテキストが140文字を超える場合、textフィールドの最後が三点リーダー(…)となり、テキストの一部が省略されてしまう。

拡張された文字数のツイートテキストを取得するには、extended_tweetフィールドを使う。このextended_tweetフィールドはツイートテキストの文字数が140文字を超えた場合に存在し、このフィールドのfull_textフィールドにツイートテキストが格納されている。

ただこれでもまだ省略されてしまう場合がある。それはリツイートの場合で、ツイートテキストにRTとメンションが付与されるので、元のツイートが文字数制限いっぱいだと、ツイートテキストは一部省略されてしまう。

リツイートの場合、レスポンスデータのextended_tweetフィールドに元ツイートの情報があるので、そのツイートテキストを取得することで、省略されていないテキストを取得することができる。


省略されていないテキストを取得するコード

上記をふまえて、PythonでTwitter Streaming APIのレスポンスから省略されていないツイートテキスト全文を取得するコードを作成する。Twitter Streaming APIのデータ取得には、PythonライブラリのTweepyを使う。TweepyでStreaming APIを使う方法はTweepy4でリアルタイムツイートを取得するを参照。以下はStreaming APIでランダムサンプリングを取得して、スクリーンネームとツイートテキストを表示するコード。

import tweepy

# Twitter APIの認証情報
# Twitterの開発者向けのページで取得したキーとトークンを使う
CONSUMER_KEY = 'Consumer Key'
CONSUMER_SECRET = 'Consumer Secret'
ACCESS_TOKEN = 'Access Token'
ACCESS_TOKEN_SECRET = 'Access Token Secret'

class TwStream(tweepy.Stream):
    def _get_text(self, status):
        # 拡張ツイートの場合
        if hasattr(status, 'extended_tweet'):
            return status.extended_tweet['full_text']
        else:
            return status.text

    def on_status(self, status):
        # status(APIで取得できるデータ)を受け取ったときにon_statusが実行される
        print('-------------------------')
        # リツイートの場合は「RT」+ メンション分だけ文字数オーバーするので省略されることがある
        if hasattr(status, 'retweeted_status'):
            text = self._get_text(status.retweeted_status)
        else:
            text = self._get_text(status)
        # スクリーンネームとツイートを表示
        print(f'@{status.author.screen_name} {text}')

def main():
    twstream = TwStream(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)

    # 日本語ツイートに限定してランダムサンプリングを取得
    twstream.sample(languages=['ja'])

if __name__ == '__main__':
    main()


2022年8月30日火曜日

Dockerコンテナ上のPythonアプリをホストのcronで実行する

Dockerコンテナ上のPythonアプリをホストのcronで実行することがあったので、その方法をまとめておく。


環境


Raspberry Pi OS 64bit with desktop。Docker環境はRaspberry Pi OS 64bitでDockerを使うで作成したもの。
$ lsb_release -dr
Description:    Debian GNU/Linux 11 (bullseye)
Release:        11
$ docker --version
Docker version 20.10.17, build 100c701
$ docker-compose --version
docker-compose version 1.29.2, build unknown


Dockerコンテナの準備

まずはDockerコンテナを準備する。イメージはDocke公式Pythonイメージを使う。docker-compose.yml、Dockerfileの他にapp.pyを用意する。app.pyは「Hello Test」と表示するだけのPythonアプリで、これをコンテナ上で実行する。

version: "3"
services:
  testpy:
    build:
      context: .
      dockerfile: Dockerfile
FROM python:3.10-bullseye

WORKDIR /usr/src/app

COPY . .

CMD [ "python", "./app.py" ]
print('Hello Test')


コンテナ上のアプリを実行する

ファイルの準備ができたら、とりあえずcronを使わずにコンテナ上のアプリを実行してみる。まずはイメージをビルドする。

$ docker-compose build

そしてコンテナ上のアプリを実行してみる。

$ docker-compose run testpy
Creating testpy_testpy_run ... done
Hello Test


ホストでcron設定をする

Dockerコンテナ上のアプリが実行できることを確認できたので、ホストのcronでコンテナを起動する。

まずはコンテナを起動するスクリプトファイル作成する。cronではこのスクリプトファイルを実行する。docker-compose.ymlなどのファイルは~/docker/testpyにあるとする。cronで実行するときはTオプションをつけておかないとエラーになる。

#!/bin/sh

cd ~/docker/testpy
/usr/local/bin/docker-compose run -T testpy

作成したスクリプトに実行権限を付与しておく。

$ chmod u+x testpy.sh

cronの設定

$ crontab -e

ここでは実行結果を後で確認できるように、以下のように出力結果をログに保存しておく。

* * * * * ~/docker/testpy/testpy.sh > ~/testpy.log 2>&1

cronでスクリプトが実行できたらログファイルを確認する。cronでDockerコンテナ上のPythonアプリが実行できた。

$ cat ~/testpy.log
Creating testpy_testpy_run ...
Creating testpy_testpy_run ... done
Hello Test


2022年6月21日火曜日

DockerでDjango+PostgreSQL環境を作成する

Docker ComposeでDjango+PostgreSQLの環境を作成したので、その手順をまとめておく。Djangoのバージョンは3.2。


環境


Docker Desktop(Windows10 Pro)。
PS > docker --version
Docker version 20.10.16, build aa7e414
PS > docker-compose --version
docker-compose version 1.29.2, build 5becea4c


ファイルの準備

Dockerコンテナ作成に必要なファイルを準備する。準備するのは以下5つのファイル。


Dockerfile_web

Django用のDockerイメージを作成するDockerfile。Python公式イメージを使用。

FROM python:3.10

# 標準出力、標準エラーストリームのバッファリングを行わない
# Djangoのログをリアルタイムで出力するようにする
ENV PYTHONUNBUFFERED 1 

# ロケール設定の準備
RUN apt update \
 && apt -y install --no-install-recommends \
    locales \
 && sed -i -e 's/# ja_JP.UTF-8 UTF-8/ja_JP.UTF-8 UTF-8/' /etc/locale.gen \
 && locale-gen

# タイムゾーン、ロケール設定
ENV TZ="Asia/Tokyo" \
    LANG="ja_JP.UTF-8"

RUN mkdir /app
WORKDIR /app

ADD requirements.txt /app/
RUN pip install -r requirements.txt


requirements.txt

DockerイメージにインストールするPythonライブラリ。DjangoとPostgreSQL接続用のDBアダプタをインストール。

Django>=3.2,<4.0
psycopg2


Dockerfile_db

PostgreSQL用のDockerイメージを作成するDockerfile。PostgreSQL公式イメージを使用。

FROM postgres:14.3

# ロケール設定に必要
RUN localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8

# タイムゾーン、ロケール設定
ENV TZ="Asia/Tokyo" \
    LANG="ja_JP.UTF-8"


docker-compose.yml

Composeファイル。PostgreSQLのデータベース領域は、後で作成するボリュームを指定する。

version: '3'

services:
  db:
    build:
     context: .
     dockerfile: Dockerfile_db
    ports: 
        - "5432"
    environment:
      POSTGRES_DB:
      POSTGRES_USER:
      POSTGRES_PASSWORD:
    volumes:
      - postgres_db:/var/lib/postgresql/data
  web:
    build:
     context: .
     dockerfile: Dockerfile_web
    command: python3 /app/manage.py runserver 0.0.0.0:8000
    volumes:
      - .\app:/app
    ports:
      - "8000:8000"
    depends_on:
      - db

volumes:
  postgres_db:
    external: true


.env

環境変数ファイル。

# プロジェクト名
COMPOSE_PROJECT_NAME=django

# PostgreSQLの設定
POSTGRES_DB=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres


Djangoプロジェクトの作成

ファイルの準備ができたら、まずはDjangoプロジェクト作成してmanage.pyを作成する。ここではsampleというプロジェクトを作成。

PS> docker-compose run web django-admin.py startproject sample .

このコマンドを実行すると、ホストにappフォルダが作成され、その配下にsampleフォルダとmanage.pyが作成される。

続いて、Djangoのデータベース接続設定を変更しておく。必要なら言語とタイムゾーン設定も変えておく。

...
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'postgres',
        'USER': 'postgres',
        'PASSWORD': 'postgres',
        'HOST': 'db',
        'PORT': 5432,
    }
}
...
LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'


コンテナの起動

コンテナを起動する前に、PostgreSQL用のボリュームを作成する。Windowsホストのフォルダをマウントする方法もあるが、PostgreSQLコンテナでWindowsホストの領域をマウントすると以下エラーになる(Mounting data volume for Postgres in docker for Windows doesn't work)。

data directory "/var/lib/postgresql/data" has invalid permissions

そこで、Dockerでボリュームを作成して、そのボリュームをPostgreSQLのデータベース領域とする。

PS> docker volume create --name postgres_db
postgres_db

次にコンテナを起動。

PS> docker-compose up -d
Creating network "django_default" with the default driver
Creating django_db_1 ... done
Creating django_web_1 ... done
PS> docker-compose ps
    Name                  Command               State            Ports
-------------------------------------------------------------------------------
django_db_1    docker-entrypoint.sh postgres    Up      0.0.0.0:59332->5432/tcp
django_web_1   python3 /app/manage.py run ...   Up      0.0.0.0:8000->8000/tcp


コンテナを確認

各コンテナを確認してみる。まずはDjangoコンテナ。以下のように日付表示がJSTで、日本語表示になっていれば、タイムゾーンとロケールの設定が反映されている。

PS> docker-compose exec web date
2022年  6月 18日 土曜日 20:53:19 JST

次にPostgreSQLコンテナ。webコンテナと同様にタイムゾーンとロケールを確認。

PS> docker-compose exec db date
2022年  6月 18日 土曜日 20:53:19 JST 

続いてDjangoの確認。Djangoが起動していれば、ホストのブラウザでhttp://localhost:8000にアクセスすると以下の画面が表示される。

PostgreSQlへの接続確認もする。以下コマンドで接続できればOK。

PS> docker-compose exec db psql -U postgres 

最後に、Djangoのマイグレーションを実行してみる。

PS>  docker-compose run web python3 manage.py migrate
Creating django_web_run ... done
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK


2022年6月14日火曜日

Pythonで海面水温衛星データを可視化する

G-Portalという地球観測衛星データ提供システムがあり、登録するとさまざまな衛星データを取得できる。今回は気候変動観測衛星「しきさい」(GCOM-C)の海面水温プロダクト(SST) を取得して可視化してみる。


環境

WSL2(Ubuntu20.04)とJupyter。

$ lsb_release -dr
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ jupyter --version
jupyter core     : 4.7.1
jupyter-notebook : 6.2.0
qtconsole        : 5.0.2
ipython          : 7.20.0
ipykernel        : 5.4.3
jupyter client   : 6.1.12
jupyter lab      : 3.0.9
nbconvert        : 6.0.7
ipywidgets       : 7.6.3
nbformat         : 5.1.2
traitlets        : 5.0.5


G-Portalの登録

衛星データをダウンロードするために必要なユーザ登録をしておく。ユーザ登録画面から、アカウント名やメールアドレスなどを登録する。


衛星データのダウンロード

G-Portalトップページのメニューからログインにアクセスして、登録したユーザーアカウントとパスワードでログインする。[衛星からの検索]をクリックして、ダウンロードするデータを選択する画面に遷移する。


1.絞り込み

ダウンロードする衛星データの種類を指定する。

[1.絞り込み]タブの[衛星、センサから選ぶ]を選択する。そして、[GCOM-C/SGLI] > [LEVEL2] > [海洋圏] > [L2-海面水温]のチェックボックスをチェックする。


2.期間指定

取得するデータを検索する期間を指定する。

[2.期間指定]タブで期間を指定する。


3.範囲指定

取得するデータの範囲を指定する。

[3.範囲指定]タブを開いて、右側に表示されている地図で指定するか、緯度・経度を数値で指定する。


検索するデータの指定ができたら[検索する]ボタンを押す。対象データの一覧が表示されるので、使用するデータをダウンロードする。ダウンロードしたファイル名はGC1SG1_L2SG_SSTDQ_3001.h5とする。

ダウンロードしたファイルはHDF形式で、可視化する際にはGeoTIFFが必要になる。データ一覧の[加工]ボタンを押すと、GeoTIFFへの加工要求ができる。加工には少し時間がかかるので、今回は後述するツールでGeoTIFFに変換する。


衛星データをGeoTIFFに変換する

ダウンロードしたHDF形式の衛星データをGeoTIFFに変換する。

まずはG-Portalから変換ツールをダウンロードする。トップページから[ツール・ドキュメント]画面へ遷移し、[GCOM-C]を選択。SGLI 地図投影・GeoTIFF出力ツール(Ver.1.1)をダウンロードする。ここではLinux版を使う。

ファイル名をsgli_geo_map_linuxとしてダウンロードし、先にダウンロードした衛星データを、WSL2上の同じディレクトリに配置する。

変換する前に、ダウンロードした衛星データ内のデータセット名を確認しておく。

! pip3 install h5py

import h5py

def print_dataset_name(name, obj):
    if isinstance(obj, h5py.Dataset):
        print(name)

with h5py.File('./GC1SG1_L2SG_SSTDQ_3001.h5', mode='r') as f:
    f.visititems(print_dataset_name)


上記コードを実行すると、以下のようにHDFファイル内のデータセット一覧が表示される。

Geometry_data/Latitude
Geometry_data/Longitude
Geometry_data/Obs_time
Geometry_data/Sensor_zenith
Geometry_data/Solar_zenith
Image_data/Cloud_probability
Image_data/Line_tai93
Image_data/QA_flag
Image_data/SST

上記データセットのうち、今回使用する海面水温プロダクト(SST)はImage_data/SST。変換時にはこのデータセットを指定する。

以下コマンドでHDF形式からGeoTIFFに変換する。はじめにダウンロードしたツールに実行権限を付与しておく。変換されたGeoTIFFのファイル名はGC1SG1_L2SG_SSTDQ_3001_SST.tif となる。

$ chmod u+x sgli_geo_map_linux
$ ./sgli_geo_map_linux GC1SG1_L2SG_SSTDQ_3001.h5 -d Image_data/SST


SSTデータの値を摂氏(℃)に変換

SSTの値は一般的に使われる温度の単位(摂氏)ではないので、摂氏に変換する必要がある。しきさい(GCOM-C)のページにSSTデータの仕様とその変換方法が記載されている。トップページから[データを使う]>[標準プロダクト&アルゴリズム]を開き、Ver.3の海面水温を選択する。開いた画面のAttribute情報にあるSSTを開く。

この仕様によると、有効な値は0~65531で、65532~65535は陸地や雲、その他のエラーなどで水温が取得できなかった場合。SSTの値を摂氏(℃)に変換する式は以下の通り。DNがSSTの値で、Slopeは0.0012、Offsetは-10となっている。

SST[degree]=DN*Slope+Offset


海面水温データを可視化する

ダウンロードした海面水温衛星データを、Jupyterで可視化してみる。地理空間データ用のPythonライブラリであるrasterioEarthPyを使う。

! pip3 install rasterio # ラスター形式の地理空間データを扱うためのPythonライブラリ
! pip3 install earthpy # ラスターおよびベクター形式空間データを扱うためのPythonライブラリ

import rasterio
from earthpy.mask import mask_pixels
from earthpy.plot  import plot_bands

file_path = './GC1SG1_202206100147B05610_L2SG_SSTDQ_3001_SST.tif'
sst = rasterio.open(file_path)

# バンド番号(1からはじまる)を指定してデータ読み込み
# https://rasterio.readthedocs.io/en/latest/topics/reading.html
sst_array = sst.read(1)
print(sst_array.shape)

# SSTの非有効値をマスクする
# https://earthpy.readthedocs.io/en/latest/api/earthpy.mask.html
mask_values = [65532, 65533, 65534, 65535] # マスクする値
sst_masked = mask_pixels(sst_array, sst_array, vals=mask_values)

# SSTの値を摂氏(℃)に変換
slope = 0.0012
offset = -10.0
sst_deg = sst_masked * slope + offset
sst_deg_min = sst_deg.min() # 最低水温
sst_deg_max = sst_deg.max() # 最高水温

# 海面水温を可視化
plot_bands(sst_deg, vmin=sst_deg_min, vmax=sst_deg_max, cmap='rainbow')

以下のような画像が表示される。陸地や雲などで有効値がない箇所は白色になっている。


2022年5月29日日曜日

Spotify再生回数トップ50の曲を分類して可視化する

Spotify APIで再生回数トップ50の曲データを取得するでは、Spotify APIで再生回数トツプ50の曲一覧を取得した。Spotify APIでは、曲名やアーティスト名などの情報のほかに、曲の特徴を取得できる。曲の特徴とは、ダンスに適した曲か、エネルギッシュさなどといったもので、これらの情報を利用して曲の分類と可視化をしてみる。


環境

WSL2(Ubuntu20.04)とJupyter。

$ lsb_release -dr
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ jupyter --version
jupyter core     : 4.7.1
jupyter-notebook : 6.2.0
qtconsole        : 5.0.2
ipython          : 7.20.0
ipykernel        : 5.4.3
jupyter client   : 6.1.12
jupyter lab      : 3.0.9
nbconvert        : 6.0.7
ipywidgets       : 7.6.3
nbformat         : 5.1.2
traitlets        : 5.0.5

Spotify APIで再生回数トップ50の曲データを取得するの手順でspotipyをインストールしておく。

さらに、曲を特徴でクラスタリングして可視化するためのPythonライブラリをインストールする。

$ pip3 install scikit-learn pandas matplotlib seaborn


spotipyで曲の特徴を取得

APIで取得できる特徴の説明はGet Track's Audio Featuresにある。今回はacousticness(アコースティック度)、danceability(ダンスに適した曲か)、energy(エネルギッシュさ)、instrumentalness(ボーカルがない度合い)、liveness(ライブ感)、loudness(曲全体のボリューム)、speechiness(話し言葉の度合い)、valence(曲のポジティブ度)を使用する。

今回もSpotify APIで再生回数トップ50の曲データを取得すると同様にspotipyを使う。曲の特徴を取得するには、spotifyのaudio_featuresメソッドに引数としてトラックIDのリストを渡す。取得した特徴はPandasのDataFrameにしておく。
import pandas as pd
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials

# language='ja'としないとアーティスト名が英語表記になる
spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials(), language='ja')

def get_tracks(playlist_id):
    results = spotify.playlist_items(
        playlist_id,
        fields=None,
        limit=50,
        offset=0,
        market='JP',
        additional_types=('track', 'episode'),
    )
    items = results['items']

    tracks = []
    for item in items:
        data = {}
        track = item['track']
        data['track_id'] = track['id']
        data['track_name'] = track['name']
        data['artists'] = [ar['name'] for ar in track['artists']]
        tracks.append(data)

    return tracks

def get_track_features(track_ids):
    results = spotify.audio_features(track_ids)
    # idと分類に使用する特徴
    features = ['id', 'danceability', 'energy', 'loudness', 'speechiness', 'acousticness', 'instrumentalness', 'liveness', 'valence', 'tempo']
    track_features = []
    for re in results:
        track_features.append(
            {key: value for key, value in re.items() if key in features}
        )
    return track_features

# プレイリストの曲一覧取得
# トップ50 日本
# URLの最後の22文字がプレイリストID
playlist_id = '37i9dQZEVXbKXQ4mDTEBXq'
tracks = get_tracks(playlist_id)
track_info = {tr['track_id']: f'{tr["track_name"]} - {"|".join(tr["artists"])}' for tr in tracks}

# 曲の特徴を取得
track_ids = [tr['track_id'] for tr in tracks]
track_features = get_track_features(track_ids)
df_feature = pd.DataFrame([tf for tf in track_features])
df_feature.set_index('id', inplace=True)
df_feature.info()

次のようにDataFrameには50曲分の特徴が格納される。
Index: 50 entries, 4IfrM44LofE9bSs6TDZS49 to 3oQaOjaIYPsnJbGNzXcIID
Data columns (total 9 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   danceability      50 non-null     float64
 1   energy            50 non-null     float64
 2   loudness          50 non-null     float64
 3   speechiness       50 non-null     float64
 4   acousticness      50 non-null     float64
 5   instrumentalness  50 non-null     float64
 6   liveness          50 non-null     float64
 7   valence           50 non-null     float64
 8   tempo             50 non-null     float64


クラスタリングによる分類と可視化

まずはK平均法でクラスタリングする。クラスター数は今回はとりあえず5とする。そして、その結果をもとにクラスターごとの曲を表示する。

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

# language='ja'としないとアーティスト名が英語表記になる
spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials(), language='ja')

def get_tracks(playlist_id):
    results = spotify.playlist_items(
        playlist_id,
        fields=None,
        limit=50,
        offset=0,
        market='JP',
        additional_types=('track', 'episode'),
    )
    items = results['items']

    tracks = []
    for item in items:
        data = {}
        track = item['track']
        data['track_id'] = track['id']
        data['track_name'] = track['name']
        data['artists'] = [ar['name'] for ar in track['artists']]
        tracks.append(data)

    return tracks

def get_track_features(track_ids):
    results = spotify.audio_features(track_ids)
    # idと分類に使用する特徴
    features = ['id', 'danceability', 'energy', 'loudness', 'speechiness', 'acousticness', 'instrumentalness', 'liveness', 'valence', 'tempo']
    track_features = []
    for re in results:
        track_features.append(
            {key: value for key, value in re.items() if key in features}
        )
    return track_features

def kcluster(df):
    X = StandardScaler().fit_transform(df)
    # とりあえずクラスター数5
    n_clusters = 5
    clusterer = KMeans(init='k-means++', n_clusters=n_clusters, random_state=0)
    kmeans = clusterer.fit(X)
    return kmeans.labels_

# プレイリストの曲一覧取得
# トップ50 日本
# URLの最後の22文字がプレイリストID
playlist_id = '37i9dQZEVXbKXQ4mDTEBXq'
tracks = get_tracks(playlist_id)
track_info = {tr['track_id']: f'{tr["track_name"]} - {"|".join(tr["artists"])}' for tr in tracks}

# 曲の特徴を取得
track_ids = [tr['track_id'] for tr in tracks]
track_features = get_track_features(track_ids)
df_feature = pd.DataFrame([tf for tf in track_features])
df_feature.set_index('id', inplace=True)
df_feature.info()

# K平均法でクラスタリング
df_feature['cluster'] = kcluster(df_feature.values)

# クラスターごとの曲名を表示
with pd.option_context('display.max_rows', None, 'display.max_columns', None):
    for cluster, rows in df_feature.groupby('cluster'):
        print(f'\ncluster {cluster}:')
        for id in rows.index:
            print(track_info[id])

結果は以下の通り。

cluster 0:
ミックスナッツ - Official髭男dism
喜劇 - 星野 源
Habit - SEKAI NO OWARI
FEARLESS - LE SSERAFIM
Dynamite - BTS
Butter - BTS
群青 - YOASOBI
一途 - King Gnu
常緑 - 大橋ちっぽけ
怪物 - YOASOBI
WA DA DA - Kep1er
The Feels - TWICE
Bye-Good-Bye - BE:FIRST
ELEVEN - IVE

cluster 1:
ベテルギウス - 優里
なんでもないよ、 - マカロニえんぴつ
水平線 - back number
残響散歌 - Aimer
STAY (with Justin Bieber) - The Kid LAROI|ジャスティン・ビーバー
Cry Baby - Official髭男dism
猫 - DISH//
I LOVE... - Official髭男dism
おもかげ (produced by Vaundy) - milet|Aimer|幾田りら

cluster 2:
M八七 - 米津玄師
ドライフラワー - 優里
カメレオン - King Gnu
シャッター - 優里
勿忘 - Awesome City Club
逆夢 - King Gnu
点描の唄 - Mrs. GREEN APPLE|井上苑子
魔法の絨毯 - 川崎 鷹也
YOKAZE - 変態紳士クラブ
ハート - あいみょん
Stand by me, Stand by you. - 平井 大

cluster 3:
W / X / Y - Tani Yuuki
シンデレラボーイ - Saucy Dog
CITRUS - Da-iCE
恋風邪にのせて - Vaundy
きらり - 藤井 風
LOVE DIVE - IVE
Permission to Dance - BTS
Mela! - 緑黄色社会
夜に駆ける - YOASOBI
三原色 - YOASOBI
115万キロのフィルム - Official髭男dism
ヨワネハキ - MAISONdes|和ぬか|asmi
Pretender - Official髭男dism
踊 - Ado
BOY - King Gnu

cluster 4:
踊り子 - Vaundy

さらに、特徴ごとにクラスター別の箱ひげ図を作成する。上記コードに以下を追記。

# 特徴別の箱ひげ図を作成
fig, ax =plt.subplots(len(df_feature.columns) - 1, 1, figsize=(12, 40), facecolor=(1, 1, 1))
for i, col_name in enumerate(df_feature):
    if col_name != 'cluster':
        sns.boxplot(x=df_feature[col_name], y=df_feature['cluster'], width=0.5, orient='h', ax=ax[i])
        sns.swarmplot(x=df_feature[col_name], y=df_feature['cluster'], color="r", orient='h', dodge=True, ax=ax[i])
plt.tight_layout()
plt.show()

結果は以下のような箱ひげ図が作成される。クラスター4に1曲だけ属する「踊り子」は、ほかの曲に比べてacousticness(アコースティック度)とinstrumentalness(ボーカルがない度合い)が高いことがわかる。



2022年5月8日日曜日

Spotify APIで再生回数トップ50の曲データを取得する

音楽ストリーミングサービスのSpotifyにはAPIが用意されていて、曲に関するさまざまなデータを取得できる。今回は、Spotify APIのPythonラッパーであるspotipyを利用して、プレイリストの曲一覧を取得してみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


Spotify APIのCLIENT IDとCLIENT SECRETの取得

Spotify APIを使用するには、Spotifyの開発者サイトでアプリを作成してCLIENT IDとCLIENT SECRETを取得する必要がある。

アプリを作成するには、Spotify for DevelopersのDASHBOARDにログインして、「CREATE AN APP」をクリックする。アプリ名(App name)とアプリの説明(App description)を記入するだけで作成できる。アプリを作成すると、DASHBOARDでCLIENT IDとCLIENT SECRETが確認できるようになる。


spotipyのインストール

Spotify APIによるデータ取得には、Spotify APIのPythonラッパーであるspotipyを使う。pipでインストールできる。

$ pip3 install spotipy
$ pip3 show spotipy
Name: spotipy
Version: 2.19.0
...

インストールしたら、環境変数SPOTIPY_CLIENT_IDとSPOTIPY_CLIENT_SECRETにそれぞれCLIENT IDとCLIENT SECRETを設定しておく。

export SPOTIPY_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxx
export SPOTIPY_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx


spotipyでプレイリストの曲一覧を取得

spotipyでプレイリスト「トップ50 日本」の曲一覧を取得してみる。spotipyにはプレイリストの曲情報を取得するためのplaylist_itemsメソッドがあり、引数としてプレイリストIDが必要になる。プレイリストIDは、プレイリストURLの最後の22桁。「トップ50 日本」のプレイリストIDは「37i9dQZEVXbKXQ4mDTEBXq」。

playlist_itmesメソッドで取得できるのは曲名、アーティスト名、曲が含まれるアルバム情報などいろいろある。ここでは曲名とアーティスト名を取得して、一覧表示してみる。

import spotipy
from spotipy.oauth2 import SpotifyClientCredentials

# language='ja'としないとアーティスト名が英語表記になる
spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials(), language='ja')

# プレイリスト「トップ50 日本」のプレイリストID
# SpotifyのプレイリストURLの最後の22文字がプレイリストID
playlist_id = '37i9dQZEVXbKXQ4mDTEBXq'

results = spotify.playlist_items(
    playlist_id,
    fields=None,
    limit=50,
    offset=0,
    market='JP',
    additional_types=('track', 'episode'),
)
items = results['items']

tracks = []
for item in items:
    data = {}
    track = item['track']
    data['name'] = track['name']
    data['artists'] = [ar['name'] for ar in track['artists']]
    tracks.append(data)

for i, tr in enumerate(tracks):
    print(i + 1, tr['name'], '-', '|'.join(tr['artists']))

上記コードを実行すると、以下のようにトップ50の曲一覧が取得できる。

1 W / X / Y - Tani Yuuki
2 シンデレラボーイ - Saucy Dog
3 ミックスナッツ - Official髭男dism
4 ベテルギウス - 優里
5 なんでもないよ、 - マカロニえんぴつ
6 カメレオン - King Gnu
7 水平線 - back number
8 喜劇 - 星野 源
9 ドライフラワー - 優里
10 きらり - 藤井 風
11 残響散歌 - Aimer
12 CITRUS - Da-iCE
13 LOVE DIVE - IVE
14 シャッター - 優里
15 Butter - BTS
16 Dynamite - BTS
17 群青 - YOASOBI
18 恋風邪にのせて - Vaundy
19 勿忘 - Awesome City Club
20 逆夢 - King Gnu
21 Permission to Dance - BTS
22 一途 - King Gnu
23 STAY (with Justin Bieber) - The Kid LAROI|ジャスティン・ビーバー
24 夜に駆ける - YOASOBI
25 点描の唄 - Mrs. GREEN APPLE|井上苑子
...
47 まつり - 藤井 風
48 もう少しだけ - YOASOBI
49 虹 - 菅田 将暉
50 FEARLESS - LE SSERAFIM


2022年4月22日金曜日

PythonとCartopyで地図データを可視化する

PythonでGIS用シェープファイルを可視化するでは、日本地図とフェリー航路のシェープファイルをPythonライブラリのGeoPandasで可視化した。日本地図のシェープファイルは国土地理院のホームページからダウンロードしたが、地理空間データ生成用のPythonパッケージであるCartopyを使うと、自分で地図のシェープファイルを用意する必要がない。CartopyにはNatural Earthのシェープファイルをロードするメソッドが用意されている。今回はCartopyを使って地図データを可視化してみる。


環境

WSL2(Ubuntu20.04)とJupyter。

$ lsb_release -dr
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


Pythonライブラリのインストール

必要なPythonライブラリをインストールする。Cartopyのインストールは少し手間がかかるので後述。

$ pip3 install matplotlib geopandas

インストールされたバージョン。

$ pip show matplotlib
Name: matplotlib
Version: 3.5.1
...
$ pip show geopandas
Name: geopandas
Version: 0.10.2
...


Cartopyのインストール

Cartopyはpipでインストールというわけにはいかず、少し手間がかかる。Cartopyをインストールする前に、proj、geos、pyshp、shapelyをインストールしておく必要がある。

まずはprojのインストール。projはaptコマンドでもインストールできるが、バージョンが古いのでソースからインストールする。ビルドの前に必要なパッケージをインストールする。

$ sudo apt install cmake sqlite3 libsqlite3-dev 

続いてソースファイルをダウンロードしてビルドする。今回はバージョン8.2.1を使う。

$ curl -O https://download.osgeo.org/proj/proj-8.2.1.tar.gz
$ tar xzvf proj-8.2.1.tar.gz
$ cd proj-8.2.1
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
$ sudo cmake --build . --target install

次にgoesのインストール。

$ sudo apt install libgeos-dev
$ pip3 install geos

pyshpとshapelyのインストール。

$ pip3 install pyshp shapely

インストールした各ライブラリのバージョン。

$ pip3 show geos
Name: geos
Version: 0.2.3
...
$ pip3 show pyshp
Name: pyshp
Version: 2.2.0
...
$ pip3 show shapely
Name: Shapely
Version: 1.8.1.post1
...

最後にCartopyのインストール。

$ pip3 install cartopy

インストールされたバージョン

$ pip3 show cartopy
Name: Cartopy
Version: 0.20.2
...


CartopyとGeoPandasで地図とフェリー航路を可視化する

地図の他に、PythonでGIS用シェープファイルを可視化すると同様にフェリー航路も可視化する。フェリー航路のシェープファイルは、国土地理院のホームページにある地球地図日本の第2.2版ベクタの全レイヤを使う。ファイルをダウンロードして解凍しておく。フェリー航路のシェープファイルはferryl_jpn.shp。

地図とフェリー航路を可視化するコードは以下の通り。

import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import geopandas as gpd

# 解凍したフェリー航路のシェープファイルのパス
path_ferry =  "gm-jpn-all_u_2_2/ferryl_jpn.shp"
df_ferry = gpd.read_file(path_ferry)

plt.figure(figsize=(20, 16))

# 地図投影法を指定
ax = plt.axes(projection=ccrs.PlateCarree())

# Natural Earthの地図シェープファイル(解像度10m)を読み込む
ax.coastlines(resolution='10m')

# フェリー航路をプロット
df_ferry.plot(ax=ax, color='blue', linewidth=1.0, label='Ferry routes')

以下のように地図とフェリー航路が表示される。クリッピングされる地図の位置は、読みこんだシェープファイル(今回はフェリー航路)の位置により自動調整される。


北海道が途中で切れているので北海道全体がクリッピング範囲に入るようにしてみる。Cartopyにはset_extentというメソッドが用意されていて、任意の場所でクリッピングできるらしいのだが、実行しても処理が途中で落ちてしまう。ここでは回避策として、透明なポリゴンの境界を配置して、クリッピング位置が自動調整されたときに北海道が切り取られないようにする。

import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import geopandas as gpd
from shapely.geometry import Polygon

# 解凍したフェリー航路のシェープファイルのパス
path_ferry =  "gm-jpn-all_u_2_2/ferryl_jpn.shp"
df_ferry = gpd.read_file(path_ferry)

plt.figure(figsize=(20, 16))

# 地図投影法を指定
ax = plt.axes(projection=ccrs.PlateCarree())

# Natural Earthの地図シェープファイル(解像度10m)を読み込む
ax.coastlines(resolution='10m')

# フェリー航路をプロット
df_ferry.plot(ax=ax, color='blue', linewidth=1.0, label='Ferry routes')

# 指定範囲で地図を切り取る
# set_extentがエラーになるので透明なポリゴンの境界で対処
# ax.set_extent([130, 150, 30, 40], crs=ccrs.PlateCarree())
polygon = Polygon([(130, 40), (150, 40), (150, 30), (130, 30), (130, 40)])
poly_gdf = gpd.GeoDataFrame([1], geometry=[polygon], crs=df_ferry.crs)
poly_gdf.boundary.plot(ax=ax, alpha=0)

以下のように北海道が途中で切れることなくプロットされた。


2022年4月14日木曜日

VS Code上のJupyterノートブックの配色を変える

VS CodeではPipenvとVS Codeの環境でJupyter Notebookを使うのように、Jupyterを使ってグラフなどを表示できる。Jupyter上で動作するpyLDAvisも使えるのだが、VS Codeをダークモードにしていると、以下画像のようにpyLDAvisの結果が見づらい。


そこで、VS Code上のJupyterノートブックの配色を変えてみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


必要なPythonライブラリなどのインストール

VS Code上でJupyterノートブックを作成して、pyLDAvisでLDAの結果を表示するために必要なPythonライブラリなどをインストールする。pyLDAvisとJupyterの他に、LDAモデル作成に使うscikit-learnもインストールする。

$ pip3 install scikit-learn jupyter pyldavis


VS Code上でpyLDAvisの表示

まずはVS Code上でJupyterノートブックを作成して、pyLDAvisでLDAの結果を表示する。PipenvとVS Codeの環境でJupyter Notebookを使うのように、拡張子ipynbのファイルを作成して、以下のコードを記述する。

import re

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
import pyLDAvis
import pyLDAvis.sklearn

pyLDAvis.enable_notebook()

categories = ['rec.sport.baseball', 'rec.sport.hockey', 'comp.sys.mac.hardware', 'comp.windows.x']
twenty_train = fetch_20newsgroups(subset='train', categories=categories, shuffle=True, random_state=42)

def remove_figures(text):
    """数字を除去"""
    text = text.lower()
    text = re.sub(r'\d+', '', text)
    return text

vectorizer = CountVectorizer(preprocessor=remove_figures)

# BOW
X = vectorizer.fit_transform(twenty_train.data)

lda = LatentDirichletAllocation(n_components=5, random_state=0)
lda.fit(X)

pyLDAvis.sklearn.prepare(lda, X, vectorizer)

実行すると以下のようにpyLDAvisでの表示ができているが、VS Codeがダークモードだと背景色と文字色がともに黒系の色なのとても見づらい。



VS Code上のJupyterノートブックの配色を変える

Jupyterの表示結果の配色を変えるには、VS Codeの設定を変更する。VS Codeの設定ファイルを開いて(Windowsの場合は%APPDATA%\Code\User\settings.jsonにある)、以下を追記する。

"workbench.colorCustomizations": {

    "notebook.outputContainerBackgroundColor": "#830af5",

},

設定変更後、ノートブックの背景色が指定した色(#830af5)に変わる。


背景色以外の色も変更可能で、Notebook colorsに設定の一覧がある。


2022年3月21日月曜日

Pythonで新語に対応したmecab-unidic-NEologdを使う

PythonとMeCabでUniDicを使うでは、MeCabとUniDicで形態素解析して、IPA辞書との結果を比較してみた。UniDicにしてもIPA辞書にしても、よく使われる固有名詞には対応しているものの、世の中のさまざまな固有名詞には対応しきれない。ネット資源をもとにIPA辞書をカスタマイズしたmecab-ipadic-NEologdなら、最近登場した新語にも対応できる(mecab-ipadic-NEologdで形態素解析を新語に対応させる)。UniDicにも同様のmecab-unidic-NEologdがあるので、今回はmecab-unidic-NEologdをPythonで使ってみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ git version
git version 2.25.1


MeCabのインストール

IPA辞書やUniDicを使うだけならMeCabのPythonラッパーであるmecab-python3をpipでインストールすればよかったが、NEologdを使うためにはMeCabをインストールする。MeCabのインストールはRaspberry PiのPython3でMecabを使うを参照。


mecab-unidic-NEologdのインストール

gitでリポジトリをクローンして、インストールスクリプトを実行する。

$ git clone --depth 1 https://github.com/neologd/mecab-unidic-neologd.git
$ cd mecab-unidic-neologd
$ ./bin/install-mecab-unidic-neologd -n

インストールできたら、mecab-unidic-NEologdのインストール先ディレクトリを確認しておく。

$ echo `mecab-config --dicdir`"/mecab-unidic-neologd"
/usr/local/lib/mecab/dic/mecab-unidic-neologd


mecab-unidic-NEologdで形態素解析

準備ができたのでmecab-unidic-NEologdで形態素解析してみる。dオプションでmecab-unidic-NEologdのインストールディレクトリを指定する。

import MeCab

tagger = MeCab.Tagger('-d /usr/local/lib/mecab/dic/mecab-unidic-neologd')
text = '青いカザミドリがクルクル回る'
res = tagger.parse(text)
print(res)

上記コードを実行すると次のような結果になる。書字形出現形、発音形出現形、語彙素読み、語彙素、品詞(ハイフン区切り)、活用型、活用形が出力される。この出力項目のフォーマットは、/usr/local/lib/mecab/dic/mecab-unidic-neologd/dicrcで指定されている。

青い    アオイ  アオイ  青い    形容詞-一般     形容詞  連体形-一般

カザミ  カザミ  カザミ  Kazami  名詞-固有名詞-一般

ドリ    ドリ    トリ    鳥      名詞-普通名詞-一般

が      ガ      ガ      が      助詞-格助詞

クルクル        クルクル        クルクル        クルクル        名詞-固有名詞-一般

回る    マワル  マワル  回る    動詞-非自立可能 五段-ラ行       終止形-一般

EOS

以下はUniDicで同じ文字列を形態素解析した結果で、mecab-unidic-NEologdの結果とけっこう違う。またmecab-unidic-NEologdのデフォルトでは、UniDicに比べて取得できる項目が少なくなっている。

青い    形容詞,一般,,,形容詞,連体形-一般,アオイ,青い,青い,アオイ,青い,アオイ,和,"","","","","","",相,アオイ,アオイ,アオイ,アオイ,"2","C1","",73950814151361,269

カザミドリ      名詞,普通名詞,一般,,,

が      助詞,格助詞,,,,,ガ,が,が,ガ,が,ガ,和,"","","","","","",格助,ガ,ガ,ガ,ガ,"","動詞%F2@0,名詞%F1","",2168520431510016,7889

クルクル        副詞,,,,,,クルクル,くるくる,クルクル,クルクル,クルクル,クルクル,和,"","","","","","",相,クルクル,クルクル,クルクル,クルクル,"1","","",2892273994048000,10522

回る    動詞,非自立可能,,,五段-ラ行,連体形-一般,マワル,回る,回る,マワル,回る,マワル,和,"","","","","","",用,マワル,マワル,マワル,マワル,"0","C2","",9928873533907649,36121

EOS

次に、mecab-unidic-NEologdの効果がわかるように「カールおじさんインスタ映え」を形態素解析してみる。以下はmecab-unidic-NEologdを使用した結果。

カールおじさん  カールオジサン  カールオジサン  カールおじさん  名詞-固有名詞-一般

インスタ映え    インスタハエ    インスタハエ    インスタ映え    名詞-固有名詞-一般

EOS

固有名詞と「インスタ映え」とがちゃんと形態素解析できている。対して以下がUniDicの結果。固有名詞などが形態素として認識されていない。

カール  名詞,固有名詞,人名,一般,,,カール,カール-Karl,カール,カール,カール,カール,固,"","","","","","",人名,カール,カール,カール,カール,"1","","",2166596286161408,7882

おじ    名詞,普通名詞,一般,,,,オジ,伯父,おじ,オジ,おじ,オジ,和,"","","","","","",体,オジ,オジ,オジ,オジ,"0","C4","",1362303530443264,4956

さん    接尾辞,名詞的,一般,,,,サン,さん,さん,サン,さん,サン,和,"","","","","","",接尾体,サン,サン,サン,サン,"","C4","",3984363884782080,14495

インスタ        名詞,固有名詞,一般,,,,インスタ,インスタ,インスタ,インスタ,インスタ,インスタ,固,"","","","","","",固有名,インスタ,インスタ,インスタ,インスタ,"","","",92251508095197696,335609

映え    名詞,普通名詞,一般,,,,ハエ,映え,映え,バエ,映え,バエ,和,"ハ濁","濁音形","","","","",体,バエ,バエ,バエ,ハエ,"2","C3","",17388235261100544,63258

EOS


2022年3月9日水曜日

PythonとMeCabでUniDicを使う

PythonとMeCabで形態素解するときに利用できる辞書としては、IPA辞書やネットの資源を利用してIPA辞書をカスタマイズしたmecab-ipadic-NEologdがある。

Raspberry PiのPython3でMecabを使う

mecab-ipadic-NEologdで形態素解析を新語に対応させる

IPA辞書はしばらく更新されていないが、mecab-ipadic-NEologdは定期的に更新されている。ただmecab-ipadic-NEologdは場合によっては副作用がある。

mecab-ipadic-NEologdの辞書を修正する

そこで今回は、今も更新がされているUniDicを使って形態素解析をしてみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


必要なPythonモジュールとUniDicのインストール

まずはPythonでMeCabとUniDicを使用するため、MeCabのPythonラッパーであるmecab-python3とunidic-pyをインストールする。また、あとでUniDicとIPA辞書の比較をするので、IPA辞書もインストールする。

$ pip3 install mecab-python3 unidic ipadic
$ pip3 show mecab-python3
Name: mecab-python3
Version: 1.0.5
...
$ pip3 show unidic
Name: unidic
Version: 1.1.0
...
$ pip3 show ipadic
Name: ipadic
Version: 1.0.0
...

インストールできたらUniDicをダウンロードしておく。

$ python3 -m unidic download


MeCabとUniDicで形態素解析をする

環境が整ったので、UniDicで形態素解析をしてみる。

import MeCab
import unidic

tagger = MeCab.Tagger('-d ' + unidic.DICDIR)
text = '青いカザミドリがクルクル回る'
res = tagger.parse(text)
print(res)

上記コードを実行すると以下の結果が得られる。結果は分割された語ごとに、pos1からlemma_idまでがカンマ区切りになっている。結果の各項目についてはunidic-pyに説明がある。

また、「カザミドリ」は辞書に登録されていないのですべての項目はない。このフォーマットはunidic-pyがインストールされたディレクトリ配下のpython3.8/site-packages/unidic/dicdir/dicrcなどに記述されている。

青い    形容詞,一般,,,形容詞,連体形-一般,アオイ,青い,青い,アオイ,青い,アオイ,和,"","","","","","",相,アオイ,アオイ,アオイ,アオイ,"2","C1","",73950814151361,269

カザミドリ      名詞,普通名詞,一般,,,

が      助詞,格助詞,,,,,ガ,が,が,ガ,が,ガ,和,"","","","","","",格助,ガ,ガ,ガ,ガ,"","動詞%F2@0,名詞%F1","",2168520431510016,7889

クルクル        副詞,,,,,,クルクル,くるくる,クルクル,クルクル,クルクル,クルクル,和,"","","","","","",相,クルクル,クルクル,クルクル,クルクル,"1","","",2892273994048000,10522

回る    動詞,非自立可能,,,五段-ラ行,連体形-一般,マワル,回る,回る,マワル,回る,マワル,和,"","","","","","",用,マワル,マワル,マワル,マワル,"0","C2","",9928873533907649,36121

EOS

次に、書字形基本形だけを取り出してみる。

import MeCab
import unidic

tagger = MeCab.Tagger('-d ' + unidic.DICDIR)
text = '青いカザミドリがクルクル回る'
node = tagger.parseToNode(text)
tokens = []
while node:
    features = node.feature.split(',')
    if features[0] == 'BOS/EOS':
        # BOS/EOSをスキップ
        node = node.next
        continue
    if len(features) >= 11:
        # 書字形基本形
        base_form = features[10]
    else:
        # 書字形基本形がない場合は表層形を使う
        base_form = node.surface

    tokens.append(base_form)
    node = node.next

print(tokens)

結果は以下の通り。

['青い', 'カザミドリ', 'が', 'クルクル', '回る']

さらにUniDicを使うと語彙素も取得できる。語彙素とは異なる形態であるが同じ語であると考えられるものからなる語の集合。「おおきな蒼いふるどけい」を形態素解析すると以下の結果が得られる。品詞からはじまる項目の8番目が語彙素で、語彙素だけを取り出すと['大きな', '青い', 'フル', '時計']となる。「おおきな」が「大きな」、「蒼い」が「青い」などとなる。表記ゆれ対策に使えるかもしれない。

おおきな        連体詞,,,,,,オオキナ,大きな,おおきな,オーキナ,おおきな,オーキナ,和,"","","","","","",相,オオキナ,オオキナ,オオキナ,オオキナ,"1","","",1254551491584512,4564

蒼い    形容詞,一般,,,形容詞,連体形-一般,アオイ,青い,蒼い,アオイ,蒼い,アオイ,和,"","","","","","",相,アオイ,アオイ,アオイ,アオイ,"2","C1","",73950881260225,269

ふる    名詞,固有名詞,地名,一般,,,フル,フル,ふる,フル,ふる,フル,固,"","","","","","",地名,フル,フル,フル,フル,"1","","",56409902964417024,205218

どけい  名詞,普通名詞,一般,,,,トケイ,時計,どけい,ドケー,どけい,ドケー,漢,"ト濁","濁音形","","","","",体,ドケイ,ドケイ,ドケイ,トケイ,"0","C2","",7244141050348032,26354


IPA辞書との比較

UniDicとIPA辞書でどの程度違いがあるのか比較してみる。形態素解析するテキストは、Pythonで青空文庫の作品テキストからルビなどを取り除くの方法でルビなどを除去した青空文庫の『坊ちゃん』。botxhan.txtとして保存しておく。このテキストを形態素解析して、IPA辞書の原型とUnidicの書字形基本形の数をカウントする。以下は名詞のみをカウントして出現数上位20を比較するコード。

from collections import Counter

import MeCab
import unidic
import ipadic

def count(text, dic_arg, base_form_idx):
    tagger = MeCab.Tagger(dic_arg)
    node = tagger.parseToNode(text)
    tokens = []
    while node:
        feature_split = node.feature.split(',')
        pos1 = feature_split[0]

        if pos1 != '名詞':
            node = node.next
            continue

        if len(feature_split) >= base_form_idx+1:
            # 書字形基本形/原形
            base_form = feature_split[base_form_idx]
        else:
            # 書字形基本形/原形がない場合は表層形を使う
            base_form = node.surface

        tokens.append(base_form)
        node = node.next

    return Counter(tokens)

def main():
    # 『坊ちゃん』のテキスト読み込み
    with open('./botchan.txt', encoding='utf8') as f:
        text = f.read()

    unidic_count = count(text, '-d ' + unidic.DICDIR, base_form_idx=10)
    ipadic_count = count(text, ipadic.MECAB_ARGS, base_form_idx=6)

    for rank, (ucnt, icnt) in enumerate(zip(unidic_count.most_common(20), ipadic_count.most_common(20))):
        print(rank+1, ucnt, icnt)

if __name__ == '__main__':
    main()

結果は以下の通り。左側がUnidic、右側がIPA辞書を使った結果。

1 ('事', 292) ('おれ', 472)
2 ('もの', 220) ('の', 373)
3 ('赤', 175) ('事', 291)
4 ('シャツ', 170) ('*', 236)
5 ('山嵐', 154) ('もの', 218)
6 ('一', 153) ('ん', 216)
7 ('時', 115) ('人', 213)
8 ('方', 108) ('君', 184)
9 ('学校', 107) ('赤', 178)
10 ('人', 93) ('一', 173)
11 ('清', 89) ('よう', 173)
12 ('野', 88) ('シャツ', 170)
13 ('顔', 80) ('山嵐', 155)
14 ('気', 79) ('何', 146)
15 ('生徒', 77) ('二', 120)
16 ('今', 73) ('方', 115)
17 ('奴', 72) ('時', 108)
18 ('ところ', 72) ('それ', 100)
19 ('二', 71) ('これ', 99)
20 ('うち', 70) ('三', 92)

形容詞のみにした場合も比較してみる。

1 ('ない', 418) ('ない', 263)
2 ('いい', 121) ('いい', 114)
3 ('よい', 44) ('よい', 42)
4 ('わるい', 36) ('わるい', 36)
5 ('早い', 26) ('早い', 25)
6 ('悪るい', 23) ('長い', 17)
7 ('面白い', 17) ('面白い', 16)
8 ('長い', 17) ('強い', 15)
9 ('強い', 16) ('大きい', 11)
10 ('えらい', 11) ('小さい', 10)
11 ('小さい', 10) ('大人しい', 10)
12 ('大人しい', 10) ('えらい', 10)
13 ('旨い', 10) ('旨い', 10)
14 ('狭い', 10) ('狭い', 10)
15 ('大きい', 10) ('痛い', 9)
16 ('痛い', 9) ('蒼い', 9)
17 ('蒼い', 9) ('まずい', 9)
18 ('難有し', 9) ('嬉しい', 8)
19 ('嬉しい', 8) ('っぽい', 8)
20 ('むずかしい', 8) ('暗い', 8) 

この結果をみただけではUniDicとIPA辞書のどちらがいいのかの判断はできないが、それなりに違いはありそう。


2022年2月25日金曜日

Pythonで漢字やひらがなをローマ字やカタカナに変換する

PykakasiというPythonライブラリを使うと、漢字やひらがなをローマ字やカタカナに変換できる。Pykakasiをインストールして漢字やひらがなを変換するところまでやってみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


Pykakasiのインストール

変換ライブラリのPykakasiはpipでインストールできる。

$ pip3 install pykakasi
$ pip3 show pykakasi
Name: pykakasi
Version: 2.2.1
...


漢字やひらがなをローマ字やカタカナに変換する

Pykakasiがインストールできたら、漢字やひらがなを変換してみる。kakasiクラスのインスタンスを作成して、convertメソッドを使うだけで変換結果が得られる。

from pprint import pprint
import pykakasi

kks = pykakasi.kakasi()
text = "ふんわり名人サイコー"

# 漢字やひらがななどの文字種ごとに変換結果が得られる
result = kks.convert(text)
pprint(result, indent=2)

結果は以下のように、漢字やひらがななどの文字種ごとに変換結果が得られる。origが変換元の文字、hiraがひらがな、kanaがカタカナ、hepburnはヘボン式ローマ字、kunreiは訓令式ローマ字、passportはパスポート表記。

[ { 'hepburn': 'funwari',
    'hira': 'ふんわり',
    'kana': 'フンワリ',
    'kunrei': 'funwari',
    'orig': 'ふんわり',
    'passport': 'funwari'},
  { 'hepburn': 'meijin',
    'hira': 'めいじん',
    'kana': 'メイジン',
    'kunrei': 'meizin',
    'orig': '名人',
    'passport': 'meijin'},
  { 'hepburn': 'saikoo',
    'hira': 'さいこー',
    'kana': 'サイコー',
    'kunrei': 'saikoo',
    'orig': 'サイコー',
    'passport': 'saikoo'}]


2022年2月13日日曜日

PipenvとVS Codeの環境でJupyter Notebookを使う

Jupyter Notebook/Labはデータ分析には便利なのだが、コード編集は使い慣れたIDEなどを使いたい。VS Codeは拡張機能をインストールすることでJupyter Notebookを使えるようになるので、今回はPipenvで構築した環境のVS Code上でJupyter notebookを使えるようにしてみる。Pipenvはパッケージ管理と仮想環境の構築の両方ができるツール。


環境

WSL2(Ubuntu20.04)。VS CodeからRemote DevelopmentでWSL2に接続する。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


VS Codeの設定

今回はVS CodeでWSL2のUbuntu上に接続して環境を作成するので、Remote DevelopmentでWSL2にSSH接続する。方法はRemote DevelopmentでSSH接続先にVS Codeの開発環境を作成するを参照。

VS CodeでWSL2にSSH接続したらVS CodeにPython拡張機能をインストールする。Python拡張機能をインストールするとJupyter拡張機能もインストールされる。Ctrl+Shift+Xで拡張機能のメニューを表示し、検索ボックスで「python」で検索すると見つかる。


Pipenvで環境作成


まずは環境を作成するディレクトリ作成する。VS Code上でCtrl+Shift+@などでターミナルを開いてディレクトリ作成。ここではホームディレクトリ配下にtestenvというディレクトリを作成。
$ mkdir testenv
次にVS Codeから「Open Folder」で作成したディレクトリを開く。この状態でVS Codeのターミナルを開きPipenvをpipでインストール。
$ pip3 install pipenv
$ pip3 show pipenv
Name: pipenv
Version: 2021.11.23
...

続いてPipenvでPython3.8の仮想環境作成。
$ pipenv --python 3.8
Pipenvでjupyterと、Jupyter Notebookの動作確認用にMatplotlibをインストール。
$ pipenv install jupyter matplotlib

jupyterのインストールが完了したら、コマンドパレットを開いて「Python: Select Interpreter」を入力する。Pythonインタープリターの一覧が表示されるので、今回Pipenvで作成したenvtest-xxxxxを選択する。



Jupyterを使う


VS Code上で新規にNotebookを作成するにはコマンドパレットで「Jupyter: Create New Jupyter Notebook」と入力するか、VS CodeのExploreで拡張子ipynbのファイルを作成する。

新規作成したら右上の「Select Kernel」をクリックするとPythonインタープリターの一覧が表示されるので、その中から今回Pipenvで作成したenvtest-xxxxxを選択する。

ここまできたらMatplotlibでグラフを作成してVS Code上で表示してみる。コードを実行してipykernelをインストールするようメッセージが表示されたらインストールする。
%matplotlib inline
import math

import matplotlib.pyplot as plt

f = lambda x: math.exp(pow(x, 2) / 16)
X = [x/10 for x in range(-50, 51)]
y = [f(x) for x in X]
plt.plot(X, y, marker='o')
上記コードを実行すると以下のようなグラフがVS Code上に表示される。


ちなみに、VS Code上のNotebookの行数表示は、コード左側の余白を選択してCtrl-L。