Vue.jsやNuxt.jsの開発用サーバのポート番号を変更する

Vue.jsやNuxt.jsの開発用サーバのポート番号を変更するには package.json に記述すればよいようです。

Vue.js

package.json

変更前(デフォルト)

  "scripts": {
    "serve": "vue-cli-service serve",
    // ...
  },

↓ 変更後

  "scripts": {
    "serve": "vue-cli-service serve --port 8080",
    // ...
  },

開発用サーバの起動

$ npm run serve

Nuxt.js

package.json

変更前(デフォルト)

  "scripts": {
    "dev": "nuxt",
    // ...
  },

↓ 変更後

  "scripts": {
    "dev": "nuxt --port 8080",
    // ...
  },

開発用サーバの起動

$ npm run dev

2023/04/11追記

設定ファイルを書き換えずに一時的に変更したいならば、以下のように環境変数でも指定できます。

$ PORT=8080 npm run dev

Nuxt - Configuration

Pythonの自作パッケージにリソースファイルを含めるには

Pythonソースコード以外にテキストや画像など任意のファイルをパッケージに含めて、Pythonコードから参照する方法のメモです。

手順の概要

  1. setup.pyinclude_package_data = True と記載
  2. パッケージに含めたいファイル名を MANIFEST.in に記載
  3. Pythonコードからは pkgutil.get_data("sample", "data.txt") で参照

ソースコード

$ find . -type f
./requirements.txt
./sample/data.txt
./sample/__init__.py
./sample/main.py
./setup.py
./MANIFEST.in

requirements.txt

空ファイル

sample/data.txt

適当なリソースファイル

Hello

sample/__init__.py

空ファイル

sample/main.py

リソースファイルを参照するPythonコードのサンプル

import pkgutil

def main():
    print(pkgutil.get_data("sample", "data.txt"))
    # bytes型でファイルの中身を取得できる

setup.py

include_package_data = True が必要です。

from setuptools import setup, find_packages

with open('requirements.txt') as requirements_file:
    install_requirements = requirements_file.read().splitlines()

setup(
    name        = "sample",
    version     = "0.0.0",
    description = "A sample tool",
    author      = "suzuki-navi",
    include_package_data = True, # これが必要!
    packages    = find_packages(),
    install_requires = install_requirements,
    entry_points = {
        "console_scripts": [
            "sample = sample.main:main",
        ]
    },
)

MANIFEST.in

リソースファイルとしてパッケージに含めたいファイルを書きます。

include requirements.txt
include sample/*.txt

recursive-includeディレクトリの中を再帰的に探すこともできるようです。

Including files in source distributions with MANIFEST.in — Python Packaging User Guide

動かしてみる

その場で実行

$ pip install -e .

$ sample
b'Hello\n'

パッケージ作成

$ python setup.py sdist

tar.gzファイルに sample-0.0.0/sample/data.txt が含まれます。

$ tar tf dist/sample-0.0.0.tar.gz
sample-0.0.0/
sample-0.0.0/MANIFEST.in
sample-0.0.0/PKG-INFO
sample-0.0.0/requirements.txt
sample-0.0.0/sample/
sample-0.0.0/sample/__init__.py
sample-0.0.0/sample/data.txt
sample-0.0.0/sample/main.py
sample-0.0.0/sample.egg-info/
sample-0.0.0/sample.egg-info/PKG-INFO
sample-0.0.0/sample.egg-info/SOURCES.txt
sample-0.0.0/sample.egg-info/dependency_links.txt
sample-0.0.0/sample.egg-info/entry_points.txt
sample-0.0.0/sample.egg-info/top_level.txt
sample-0.0.0/setup.cfg
sample-0.0.0/setup.py

インストールして実行

$ pip install dist/sample-0.0.0.tar.gz
$ sample
b'Hello\n'

インストール先に sample/data.txt が含まれていることがわかります。

$ ls ~/.local/lib/python3.8/site-packages/sample/
data.txt  __init__.py  main.py  __pycache__

S3バケットのオブジェクト数をawscliで確認

S3バケットにあるオブジェクトの数をawscliで確認する方法です。

2つ方法を書きます。1つ目は単純にlsコマンドで数えています。2つ目はCloudWatchのMetricsで、バージョニングが有効化されているバケットの場合は過去バージョンのオブジェクトも含まれます。

aws s3 lsコマンド

aws s3 lsコマンドに --recursive --sum を付けると最後にオブジェクト数とデータサイズが表示されます。

$ aws s3 ls --recursive --sum BUCKET_NAME
...
Total Objects: 16849
   Total Size: 41267890

ずらっと表示されるのが嫌なら tail コマンドを後ろに付けます。

$ aws s3 ls --recursive --sum BUCKET_NAME | tail -n2
Total Objects: 16849
   Total Size: 41267890

データサイズがいらないのであれば、wcでもよいです。

$ aws s3 ls --recursive BUCKET_NAME | wc -l
16849

いずれもオブジェクトの一覧を出力して数を数えているだけです。オブジェクト数が膨大な場合は、数えるのに時間がかかりますし、API呼び出し回数も膨大になるので、お金もかかります。

昔似たことを書いてました。

aws cloudwatch get-metric-dataコマンド

CloudWatchのMetricsであれば、オブジェクト数が膨大でも問題ありませんが、現在ではなく、最新でも前日のたぶん朝9時ごろ時点での数しかわからないです。それに、コマンドがとても長いです。実行する際には以下のコマンド例から BUCKET_NAME の部分を探して置き換えてください。

$ aws cloudwatch get-metric-data --metric-data-queries '[{"Id":"objectCount","MetricStat":{"Metric":{"Namespace":"AWS/S3","MetricName":"NumberOfObjects","Dimensions":[{"Name":"StorageType","Value":"AllStorageTypes"},{"Name":"BucketName","Value":"BUCKET_NAME"}]},"Period":86400,"Stat":"Maximum"}}]' --start-time $(date -u +%Y-%m-%dT00:00:00Z -d "1 day ago") --end-time $(date -u +%Y-%m-%dT00:00:00Z) | jq '.MetricDataResults[0] | {Timestamp: .Timestamps[0], Value: .Values[0]}'

以下のような出力を得られます。

{
    "MetricDataResults": [
        {
            "Id": "objectCount",
            "Label": "NumberOfObjects",
            "Timestamps": [
                "2021-02-13T00:00:00Z"
            ],
            "Values": [
                16849.0
            ],
            "StatusCode": "Complete"
        }
    ],
    "Messages": []
}

スクリプト化したほうがいいかも。

profile=default
bucket_name=BUCKET_NAME

aws --profile $profile cloudwatch get-metric-data --metric-data-queries '[{
  "Id":"objectCount",
  "MetricStat":{
    "Metric":{
      "Namespace":"AWS/S3",
      "MetricName":"NumberOfObjects",
      "Dimensions":[
        {"Name":"StorageType","Value":"AllStorageTypes"},
        {"Name":"BucketName","Value":"'$bucket_name'"}
      ]
    },
    "Period":86400,
    "Stat":"Maximum"
  }
}]' \
    --start-time $(date -u +%Y-%m-%dT00:00:00Z -d "1 day ago") \
    --end-time $(date -u +%Y-%m-%dT00:00:00Z) |
    jq '.MetricDataResults[0] | {Timestamp: .Timestamps[0], Value: .Values[0]}'

最後にjqを付けることで、出力を少し簡潔にしています。

{
    "Timestamp": "2021-02-13T00:00:00Z",
    "Value": 16849
}

S3バケットのバージョニングが有効化されている場合、CloudWatchで得られるオブジェクト数は過去バージョンも含めた数になります。

参考

バージョン情報

$ aws --version
aws-cli/1.18.209 Python/3.8.5 Linux/5.4.0-1036-gcp botocore/1.19.49

大きなファイルをS3にawscliでMultipart Upload

S3にPUTするときの最大サイズは5GBだそうです。これを超えるサイズをアップロードする場合にはMultipart Uploadが必要です。

aws s3 cpコマンドでは大きいファイルをアップロードする際には自動でMultipart Uploadになりますが、Multipart Uploadの処理の中身を理解するために、aws s3apiコマンドで手動で動かしてみました。

手順概要

  1. aws s3api create-multipart-uploadコマンドでMultipart Upload開始を宣言し、UploadIdを取得
  2. aws s3api upload-partコマンドで分割したファイルをアップロード。分割した数だけこのコマンドを実行
    • UploadIdは全部同じものを指定
    • 1から始まる整数をパーツの番号として指定
    • パートごとにETagが返却されるので、それを記録しておく
  3. aws s3api complete-multipart-uploadコマンドでUploadIdとETagのリストを渡すことで完了

手順詳細

手順をBashスクリプトにしたものを載せておきます。

set -Ceu
set -o pipefail

# アップロード先を指定するパラメータ
profile=default
bucket=SAMPLE_BUCKET
key=movies/sample.mov

# アップロードするローカルにあるサンプルファイル
localfile=sample.mov

# 分割したファイルを保存する一時ディレクトリ
mkdir -p parts

# part 1つあたりのサイズ
partsize=6000000
# AWSの仕様によりpart 1つあたりのサイズは5MB以上が必要です

# ファイルサイズから分割数を計算
part_count=$(( ($(ls -l sample.mov | awk '{print $5}') + $partsize - 1) / $partsize))

################################
# 手順1
################################

# UploadId を取得
upload_id=$(aws --profile $profile s3api create-multipart-upload --bucket $bucket --key $key --query UploadId --output text)

################################
# 手順2
################################

# 分割したファイルを順番に aws s3api upload-part コマンドによりアップロード。
# 手順3で必要なJSONファイルも同時に作成します。

echo '{"Parts":[' >| parts.json

for i in $(seq $part_count); do # 1から分割数までをループ
    echo $i

    # アップロードするファイルの$i番目のpartを抜き出す
    ((cat $localfile | tail -c+$(( ( $i - 1) * $partsize + 1 ))) || true) | head -c $partsize >| parts/$i
    # headがあるとその前のcatやtailは異常終了してしまいますが、
    # 冒頭で set -e しているので、スクリプトが中断しないように true を書いています。
    # シンプルに書くと$iが2ならば次のようなコマンドです。
    # cat $localfile | tail -c+6000001 | head -c 12000000 > parts/2

    echo -n '{"ETag":' >> parts.json

    # part 1つをアップロード
    # ETagが出力されるので、そのままJSONに書き出す
    aws --profile $profile s3api upload-part --bucket $bucket --key $key --part-number $i --body parts/$i --upload-id $upload_id --query ETag --output text >> parts.json

    # JSONに書き出すPartNumberは1から始まる連番
    echo -n ', "PartNumber":'$i'}' >> parts.json
    if [[ $i < $part_count ]]; then
        echo ',' >> parts.json
    else
        echo >> parts.json
    fi
done

echo ']}' >> parts.json

# parts.json には以下のようなJSONが書き出されます
# {"Parts":[
#     {"ETag":"03c58c6387cd642d23657231feb1044f", "PartNumber":1},
#     {"ETag":"d18f4e61324478f2b47f907e2b1367b3", "PartNumber":2},
#     {"ETag":"52e280adbaa9afcfd30d071255a5b452", "PartNumber":3},
#     {"ETag":"144e843c6ad29b05f5faedaf464a3a9a", "PartNumber":4},
#     {"ETag":"2251deec7a0d3bad59df68114b18d27d", "PartNumber":5}
# ]}

################################
# 手順3
################################

# ETagのリストをJSONで渡して完了
# これをするまでは aws s3 ls コマンドで見てもアップロード中のファイルは見えない
aws --profile $profile s3api complete-multipart-upload --bucket $bucket --key $key --upload-id $upload_id --multipart-upload file://parts.json

# aws s3api complete-multipart-uploa コマンドは以下のようなレスポンスをします
# {
#     "VersionId": "Z4epmUvPw5tDhUd5ctQ6hqtbJRABcND8",
#     "Location": "https://SAMPLE_BUCKET.s3.ap-northeast-1.amazonaws.com/movies%2Fsample.mov",
#     "Bucket": "SAMPLE_BUCKET",
#     "Key": "movies/sample.mov",
#     "ETag": "\"af82619f75aff5484a77ab040f516057-2\""
# }

メモ

分割サイズ

上記スクリプトでは6000000バイト(6MB弱)ずつに分割しています。分割サイズが5MBを下回ると次のようなエラーになってしまいます。

An error occurred (EntityTooSmall) when calling the CompleteMultipartUpload operation: Your proposed upload is smaller than the minimum allowed size

最後のパートだけはサイズが小さくても大丈夫です。どうしようもないですからね。

参考 UploadPart - Amazon Simple Storage Service

list-parts コマンド

手順2のあと以下のようなコマンドを実行すると

aws --profile $profile s3api list-parts --bucket $bucket --key $key --upload-id $upload_id

このようなJSONが出力されます。

{
    "Parts": [
        {
            "PartNumber": 1,
            "LastModified": "2021-02-02T06:05:03.000Z",
            "ETag": "\"5ec31e0a715293b7512d178890908310\"",
            "Size": 6000000
        },
        {
            "PartNumber": 2,
            "LastModified": "2021-02-02T06:05:04.000Z",
            "ETag": "\"9b41c50e2d76cc7959f07cffbafaedd6\"",
            "Size": 6000000
        },
        ...
    ],
    "Initiator": {
        ...
    },
    "Owner": {
        ...
    },
    "StorageClass": "STANDARD"
}

手抜きして、これをそのまま手順3のaws s3api complete-multipart-uploadコマンドに渡してしまおうかと思ったのですが、aws s3api complete-multipart-uploadコマンドはETagPartNumberのみが必要で、それ以外の要素がJSONに含まれるとエラーになってしまいました。

それにaws s3api list-partsコマンドのレスポンスにあるETagはなぜか値自体にダブルクオーテーションが含まれていました。

Pythonのboto3で試すと

同じことをPythonのboto3で試したら、aws s3api list-partsコマンドと同じく、ETagの値自体にダブルクオーテーションが含まれており、手順3に相当する箇所でもダブルクオーテーションを含めたままで動作しました。違和感・・・

PillowをAWS Lambda + Python + Serverless Frameworkで動かす

画像を処理できるPillowをAWS Lambda + Python + Serverless Frameworkの環境で動かしてみました。

AWS Lambda + Python + Serverless FrameworkにPythonのパッケージをインストールする方法は前回の記事に書きました。

AWS Lambda + Python + Serverless FrameworkのLayerにpipインストール

これに従うだけです。

requirements.txt はこの1行。

Pillow

serverless.yml は次のとおり。

service: sample

frameworkVersion: '2'

provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
  region: ap-northeast-1

functions:
  hello:
    handler: handler.hello
    events:
      - httpApi: "*"
    layers:
      - { Ref: PythonRequirementsLambdaLayer }

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    layer: true

handler.pyは次のとおり。画像ファイルをPillowで生成してそれをHTTPレスポンスします。Lambdaからのレスポンス時はBASE64エンコードが必要です。API Gatewayがそれをデコードしたうえでブラウザにレスポンスしてくれます。

import base64
import io

import PIL
import PIL.Image
import PIL.ImageDraw

def hello(event, context):
    print("Hello, Pillow!")
    print(PIL.__version__)
    # これはCloudWatch Logsに書き出される

    image = PIL.Image.new('RGB', (100, 100))
    draw = PIL.ImageDraw.Draw(image)

    draw.rectangle((20, 20, 80, 80)) # 質素な四角形を作成

    # バイナリイメージを作成
    output = io.BytesIO()
    image.save(output, format = "JPEG")
    responseBody = output.getvalue()

    # BASE64エンコード
    responseBody = base64.b64encode(responseBody).decode("utf-8")

    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "image/jpeg",
        },
        "body": responseBody,
        "isBase64Encoded": True,
    }

デプロイ。

$ serverless deploy -v

ローカルに手動でpipインストールしたりせず、全部Serverless Frameworkがいろいろやってくれます。

作成されたAPI Gatewayにブラウザでアクセスすると、次のような画像が表示されます。上記Pythonコードにある draw.rectangle((20, 20, 80, 80)) の絵です。

f:id:suzuki-navi:20210202163612p:plain

CloudWatch Logsには以下が書き出されていました。

Hello, Pillow!
8.1.0

AWS Lambda + Python + Serverless FrameworkのLayerにpipインストール

AWS LambdaのLayerにPythonのライブラリをインストールする方法です。Serverless Frameworkを使用している場合です。

少し前にも似た記事を書いたのですが、そのときはLayerに乗せられずLambda本体にライブラリのイメージが含まれてしまっていました。

前回:Serverless FrameworkとAWS Lambda with Pythonの環境にpipインストール

serverless.yml に以下のように記述すればLayerにできましたので、メモしておきます。

コメントを書いた2か所が必要です。

service: sample

frameworkVersion: '2'

provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
  region: ap-northeast-1

functions:
  hello:
    handler: handler.hello
    layers:
      # LayerとFunctionの紐づけの設定。
      # この書き方で serverless-python-requirements が
      # 作成してくれるLayerと紐づけてくれる。
      - { Ref: PythonRequirementsLambdaLayer }

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    # この設定でpipライブラリをLayerに保存してくれる。
    # これだけではFunctionとLayerの紐づけがなされないので、
    # 上記functionsの節にもlayersの記述が必要
    layer: true

バージョン情報

  • serverless@2.18.0
  • serverless-python-requirements@5.1.0

OpenCVからPillowに画像データを変換

PythonOpenCVから画像を扱うPillowというライブラリに渡してみます。

OpenCVからPillowにはNumpy配列を介して渡せるのですが、3色の順番がOpenCVはBGRで、PillowがRGBという違いがあるようで、変換が必要でした。

import cv2
import PIL.Image

def buildVideoCaptures(videoPath, outputPath):
    cap = cv2.VideoCapture(videoPath)
    if not cap.isOpened(): return

    _, img = cap.read()
    # imgは読み込んだフレームのNumpy配列でのピクセル情報(BGR)
    # imgのshapeは (高さ, 横幅, 3)

    # BGRをRGBに変換
    # これをしないと色が変になる
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

    # Numpy配列からPillowのImageオブジェクトに変換
    img = PIL.Image.fromarray(img)

    # 画像ファイルで書き出す
    img.save(outputPath, format = "JPEG")

buildVideoCaptures("./sample.mp4", "./thumbnail.jpg")

バージョン情報

$ pipenv install opencv-python
$ pipenv install pillow

$ pipenv run pip list
Package       Version
------------- --------
numpy         1.19.5
opencv-python 4.5.1.48
Pillow        8.1.0
pip           20.3.1
setuptools    51.0.0
wheel         0.36.1

関連

PillowとOpenCVについて最近の記事