やったもん勝ち

主にプログラミングのこと。生産性向上の某とかも。

AWS lambdaのpythonで並列実行する方法

普通の並列実行は、こちらを参考にして実装できました。

qiita.com

lambdaで実行するとなると若干修正しました。

import time
from multiprocessing import Process, Pipe

hoge_list = [1, 2, 3, 4]


def wait_and_print(i, *args, **kwargs):
    print(f'start {i}')
    for j in range(i):
        print(f'{i}: {j}')
        time.sleep(1)


def lambda_handler(event, context):
    processes = []
    parent_connections = []
    for hoge in hoge_list:
        parent_conn, child_conn = Pipe()
        parent_connections.append(parent_conn)
        process = Process(target=wait_and_print, args=(hoge, child_conn,))
        processes.append(process)

    for process in processes:
        process.start()

    for process in processes:
        process.join()

iphone, ipadでkindleの本をオーディオブックにする方法

最近本を読むように心がけていますが、電車移動などのスキマ時間を使って時間を作るようにしていますが、徒歩での移動中などの時間はまだまだ有効活用できていない感がありました。

近ごろまではpodcastやvoicyを聞きながら歩いていたり、部屋の掃除をしていたりしましたが、本当だったらオーディオブックを読みたいと思っていました。

しかしオーディオブックを公式に探すとなると、出版されているものがかなり少ないなということがわかり一時期は諦めていましたが、最近iphoneでも「読み上げ機能」なるものを使えばオーディオブック化できることを知り、活用中です。
ただし、機械的に読み上げるだけなのでイントネーションなどは英語での読み上げ機能などに比べかなり精度が悪く平坦な音を組み合わせているだけです。
加えて、技術書はほとんどまともに読み上げることができません。
ビジネス書や自己啓発本なんかが適していますね。

設定は1分で終わります。

iphone の設定アプリより、「一般」を選択

f:id:benzenetarou:20190520025922j:plain

「一般」より、「アクセシビリティ」を選択

f:id:benzenetarou:20190520025925j:plain

アクセシビリティ」より、「スピーチ」を選択

f:id:benzenetarou:20190520025929j:plain

画面の読み上げをオンにする

f:id:benzenetarou:20190520025933j:plain

kindleアプリで、「画面上部から下に2本指でスワイプ」

f:id:benzenetarou:20190520025936j:plain

これで読み上げてくれます。

ipadでも同様の方法でできました。

これで読書がちょっと捗るようになりました! 代わりに机の前でガッツリ時間を取れるときには技術書を読み込んでいきたいと思います。

よく使うpython3のスニペット集(随時更新)

pythonスニペット

unixtime⇔datetimeの変換

import datetime

# datetimeのnowをつくる
now = datetime.datetime.now()
# => datetime.datetime(2018, 6, 20, 12, 37, 50, 642687)

# datetiemをunixtimeに変換
unixtime = int(now.strftime('%s'))
# => 1529465870

# unixtimeをdatetimeに変換
datetime.datetime.fromtimestamp(unixtime)
# => datetime.datetime(2018, 6, 20, 12, 37, 50)```

datetime⇔strの変換

import datetime

# datetime型にする
date_str = '2018/2/1 12:30'
date_dt = datetime.datetime.strptime(date_str, '%Y/%m/%d %H:%M')
print(date_dt)

# str型にする
dt_now = datetime.datetime.now()
print(dt_now.strftime('%Y-%m-%d %H:%M:%S'))
# 2018-02-02 18:31:13

boto3でデフォルト以外のprofileを使う

import boto3
from boto3.session import Session

profile = 'hoge'
session = Session(profile_name=profile)

配列のループをindexと一緒に

teams = ["Packers", "49ers", "Ravens", "Patriots"]
for index, team in enumerate(teams):
    print index, team
 
>>> 0 Packers
>>> 1 49ers
>>> 2 Ravens
>>> 3 Patriots

2つの配列を同時にループ

nfc = ["Packers", "49ers"]
afc = ["Ravens", "Patriots"]
 
for teama, teamb in zip(nfc, afc):
    print teama + " vs. " + teamb
 
>>> Packers vs. Ravens
>>> 49ers vs. Patriots

正規表現

match

import re

pattern = r"ca"
text = "caabsacasca"
matchOB = re.match(pattern , text)
if matchOB:
    print matchOB.group()  # 'ca'

search

pattern = r"ca"
text = "caabsacasca"
matchOB = re.search(pattern , text)

if matchOB:
    print(matchOB)   # <_sre.SRE_Match object; span=(0, 2), match='ca'>
    print(matchOB.group()) # マッチした文字列を返す # ca
    print(matchOB.start()) # マッチの開始位置を返す # 0
    print(matchOB.end())  # マッチの終了位置を返す # 2
    print(matchOB.span())  # マッチの位置(start, end)を含むタプルを返す # (0, 2)

ロギング

import logging
from logging import getLogger, StreamHandler, Formatter

# loggerオブジェクトの宣言
logger = getLogger("Log")
# loggerのログレベル設定(ハンドラに渡すエラーメッセージのレベル)
logger.setLevel(logging.DEBUG)
# handlerの生成
stream_handler = StreamHandler()
# handlerのログレベル設定(ハンドラが出力するエラーメッセージのレベル)
stream_handler.setLevel(logging.DEBUG)
# ログ出力フォーマット設定
handler_format = Formatter('[%(levelname)s]\t%(asctime)s : %(message)s')
stream_handler.setFormatter(handler_format)
# loggerにhandlerをセット
logger.addHandler(stream_handler)

# ログ出力
logger.error("ERROR")
logger.warning("WARN")
logger.info("INFO")
logger.debug("DEBUG")

ファイル操作

ファイル読み込み

file_name = 'hoge.csv'
with open(file_name) as f:
    for line in f:
        cols = line.split(",")
        print(cols[0].strip())
  • strip()しないと、最後のカラムに改行も含まれてしまう。

ファイル書き込み

file_name = 'dest.txt'
with open(file_name, mode='w') as f:
    f.write(s)

AWS Lambdaでdatetimeを使うときにコンテナが再利用される罠

lambdaのpythondatetimeを使おうとしてコードを書いて、いざ動かしてみると、どうもちょっとおかしい。
明らかにおかしいというよりは時折おかしくなることがある。
これが逆に厄介でした。
原因は、lambdaの実行コンテナのコンテキストは再利用されることがあるためでした。

こちらの公式ドキュメントに詳しく書いてあります。

docs.aws.amazon.com

たとえば、Lambda 関数がデータベース接続を確立する場合、連続した呼び出しでは接続を再確立する代わりに元の接続が使用されます。接続を作成する前に接続が存在するかどうかを確認するロジックをコードに追加することをお勧めします。

DB接続なんかの情報も再利用されるとのことで、いつかつまづきそうな気がするので書いておく。
datetimeも、先頭の方で定義しておくだけだと、ずっとそれが使い回されるようです。

じゃあ、どうすればいいか。
こちらの記事のコメントにありました。

qiita.com

lambda_handler関数の中で逐一定義してあげればいいのだそうです。

本当かどうか、検証コード書いて試してみます。

import json
from datetime import datetime

dt_global = datetime.now()

def lambda_handler(event, context):
    dt_inside = datetime.now()
    dt_inside_str = dt_inside.strftime('%Y-%m-%d %H:%M:%S')
    dt_global_str = dt_global.strftime('%Y-%m-%d %H:%M:%S')

    # こちらは毎実行ごとに取得される
    print(f"dt_inside: {dt_inside_str}")

    # こちらはコンテナが再利用される限り変更されない
    print(f"dt_global: {dt_global_str}")
    
    return ''

確かに、テスト実行を連打してみると、dt_globalの方は更新されていないのに、dt_insideは毎回更新されています。

これで1ヶ月くらいずっと正常に動いてたと思っていたlambdaがおじゃんになりました〜

AWS SSMセッションマネージャーでvimのインサートモードから抜けられない問題

会社でAWS SSMを使って開発をしてたのが初めてだったのですが、罠にハマりました。

ブラウザからsshつなぐっていう感じの認識で、あまり深い理解はしていないのですが、とりあえずsshみたいに使えるもんだと思ってやってましたが、vimでハマりました。

vimでインサートモードになってから、ECSを押してもノーマルモードに戻れません。

詳しく説明すると、ECSを押した瞬間、ブラウザの中のssh画面の入力にフォーカスがあたっている部分から外れてしまう状態です。

vim中のECSとして認識されるというよりは、ブラウザに対してのECSとして認識されているようです。

これに1〜2時間位格闘した末、ブラウザから使うssmは諦めてターミナルから普通にsshすることにしました。

後日わかったのですがこれ、chromevimライクに操作できるようにする拡張機能"vimium"、これが原因でした。

chrome.google.com

これのせいでECSが謎のブラウザに対してのECSコマンドとして認識されてしまっていたのだっった。。。

これに限らず、同僚曰く「vimiumのプラグインはメリットよりも予期せぬ不具合によるデメリットの方が大きい」とのことだったので、もともとそんなフル活用できていなかったのでこれを機にやめてみたいと思います。

pythonのaws-sdkのboto3を使ってライブラリのコードをちゃんと読んでみる①

boto3とは?

pythonaws-sdkです。
他の言語のaws-sdkは大体aws-sdkみたいな名前で公開されていることが多いのですが、なぜかpythonだけboto3っていう名前です。

boto3.amazonaws.com

個人的にはこのドキュメントすごい読みやすくて好きなライブラリです。
awsapiの設計がちゃんとしててわかりやすいっていうイメージがあります。
あと自分が使い慣れているというものあって、このライブラリを使ってちゃんとコードを読めるようになっていこうと思います。(今までは雰囲気でやっていた。。。)(雰囲気でやっている人も多いはず。)

ドキュメントを読む

boto3.amazonaws.com

一番シンプルそうな、EC2のclientのdescribe_instances()メソッドを例にして見てみます。

まずはドキュメントを見てみます。

response = client.describe_instances(
    Filters=[
        {
            'Name': 'string',
            'Values': [
                'string',
            ]
        },
    ],
    InstanceIds=[
        'string',
    ],
    DryRun=True|False,
    MaxResults=123,
    NextToken='string'
)

この下のparamsの説明でREQUIREDなparamはないので、一旦シンプルに全取得してみます。

>>> import boto3
>>> client = boto3.client("ec2")
>>> response = client.describe_instances()
>>> print(response)

これでEC2の情報が取得できました。(ただし、aws configコマンドで事前にprofileなどは設定しているものとします。)

基本このように、ドキュメントがしっかりしているライブラリはそれ道理にやればうまくいきますね。
(AWSレベルになるとソースコード読む必要性に駆られることなさそう)

ソースコードを読む

github.com

まずは、この一覧の中のboto3っていうディレクトリ配下にソースコードがあるのかな?(これも雰囲気でしかわからないレベル)
f:id:benzenetarou:20190331184846p:plain

正直どこにあるのかよくわからない。
じゃあ実際にpythonから使うときはどうやって読み込まれるのか。
pipでinstallしたライブラリの保存場所とは。

Python Tips:ライブラリ・モジュールの場所を調べたい - Life with Python

これで調べられると。

>>> import boto3
>>> print(boto3.__file__)
/Users/my0shym/.anyenv/envs/pyenv/versions/3.6.0/lib/python3.6/site-packages/boto3/__init__.py

そういえば、anyenvとか使ってたな。なんのパッケージ管理システムを使ってたのかもはやわからなくなっていた。

$ cd /Users/my0shym/.anyenv/envs/pyenv/versions/3.6.0/lib/python3.6/site-packages/boto3/
$ ls -l
total 72
-rw-r--r--   1 my0shym  staff   3338  9 12  2018 __init__.py
drwxr-xr-x   7 my0shym  staff    224  9 12  2018 __pycache__
-rw-r--r--   1 my0shym  staff   1490  9 12  2018 compat.py
drwxr-xr-x  12 my0shym  staff    384  9 12  2018 data
drwxr-xr-x  16 my0shym  staff    512  9 12  2018 docs
drwxr-xr-x   8 my0shym  staff    256  9 12  2018 dynamodb
drwxr-xr-x   6 my0shym  staff    192  9 12  2018 ec2
drwxr-xr-x   4 my0shym  staff    128  9 12  2018 examples
-rw-r--r--   1 my0shym  staff   3993  9 12  2018 exceptions.py
drwxr-xr-x  11 my0shym  staff    352  9 12  2018 resources
drwxr-xr-x   6 my0shym  staff    192  9 12  2018 s3
-rw-r--r--   1 my0shym  staff  19614  9 12  2018 session.py
-rw-r--r--   1 my0shym  staff   3095  9 12  2018 utils.py

これはboto3ディレクトリの下の構造と一緒だ。これでいいのだった。

ライブラリをimportするとまずinit.pyが読み込まれる。
init.pyは110行程度のコード。

# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import logging

from boto3.session import Session


__author__ = 'Amazon Web Services'
__version__ = '1.9.2'


# The default Boto3 session; autoloaded when needed.
DEFAULT_SESSION = None


def setup_default_session(**kwargs):
    """
    Set up a default session, passing through any parameters to the session
    constructor. There is no need to call this unless you wish to pass custom
    parameters, because a default session will be created for you.
    """
    global DEFAULT_SESSION
    DEFAULT_SESSION = Session(**kwargs)


def set_stream_logger(name='boto3', level=logging.DEBUG, format_string=None):
    """
    Add a stream handler for the given name and level to the logging module.
    By default, this logs all boto3 messages to ``stdout``.

        >>> import boto3
        >>> boto3.set_stream_logger('boto3.resources', logging.INFO)

    For debugging purposes a good choice is to set the stream logger to ``''``
    which is equivalent to saying "log everything".

    .. WARNING::
       Be aware that when logging anything from ``'botocore'`` the full wire
       trace will appear in your logs. If your payloads contain sensitive data
       this should not be used in production.

    :type name: string
    :param name: Log name
    :type level: int
    :param level: Logging level, e.g. ``logging.INFO``
    :type format_string: str
    :param format_string: Log message format
    """
    if format_string is None:
        format_string = "%(asctime)s %(name)s [%(levelname)s] %(message)s"

    logger = logging.getLogger(name)
    logger.setLevel(level)
    handler = logging.StreamHandler()
    handler.setLevel(level)
    formatter = logging.Formatter(format_string)
    handler.setFormatter(formatter)
    logger.addHandler(handler)


def _get_default_session():
    """
    Get the default session, creating one if needed.

    :rtype: :py:class:`~boto3.session.Session`
    :return: The default session
    """
    if DEFAULT_SESSION is None:
        setup_default_session()

    return DEFAULT_SESSION


def client(*args, **kwargs):
    """
    Create a low-level service client by name using the default session.

    See :py:meth:`boto3.session.Session.client`.
    """
    return _get_default_session().client(*args, **kwargs)


def resource(*args, **kwargs):
    """
    Create a resource service client by name using the default session.

    See :py:meth:`boto3.session.Session.resource`.
    """
    return _get_default_session().resource(*args, **kwargs)


# Set up logging to ``/dev/null`` like a library is supposed to.
# http://docs.python.org/3.3/howto/logging.html#configuring-logging-for-a-library
class NullHandler(logging.Handler):
    def emit(self, record):
        pass


logging.getLogger('boto3').addHandler(NullHandler())

client関連の部分だけ見てみます。

import logging

from boto3.session import Session


__author__ = 'Amazon Web Services'
__version__ = '1.9.2'


# The default Boto3 session; autoloaded when needed.
DEFAULT_SESSION = None

これが呼び出されます。

def client(*args, **kwargs):
    """
    Create a low-level service client by name using the default session.

    See :py:meth:`boto3.session.Session.client`.
    """
    return _get_default_session().client(*args, **kwargs)
def _get_default_session():
    """
    Get the default session, creating one if needed.

    :rtype: :py:class:`~boto3.session.Session`
    :return: The default session
    """
    if DEFAULT_SESSION is None:
        setup_default_session()

    return DEFAULT_SESSION
def setup_default_session(**kwargs):
    """
    Set up a default session, passing through any parameters to the session
    constructor. There is no need to call this unless you wish to pass custom
    parameters, because a default session will be created for you.
    """
    global DEFAULT_SESSION
    DEFAULT_SESSION = Session(**kwargs)

ここまでででDEFAULT_SESSIONにSessionモジュールが読み込まれて、それに対してclient(*args, **kwargs)メソッド

    def client(self, service_name, region_name=None, api_version=None,
               use_ssl=True, verify=None, endpoint_url=None,
               aws_access_key_id=None, aws_secret_access_key=None,
               aws_session_token=None, config=None):

        return self._session.create_client(
            service_name, region_name=region_name, api_version=api_version,
            use_ssl=use_ssl, verify=verify, endpoint_url=endpoint_url,
            aws_access_key_id=aws_access_key_id,
            aws_secret_access_key=aws_secret_access_key,
            aws_session_token=aws_session_token, config=config)

self._session.create_clientは以下より定義されている。

    def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
                 aws_session_token=None, region_name=None,
                 botocore_session=None, profile_name=None):
        if botocore_session is not None:
            self._session = botocore_session
        else:
            # Create a new default session
            self._session = botocore.session.get_session()

つまりbotocore.session.get_session()から。

botocore.session.get_session()のbotocoreのソースコードはどこにあるのか?
調べようと思ったところでタイムアップになってしまった。
続きは次回、あるいは更新していこうと思います。

aws-cliを使ってs3にgz圧縮ファイルをアップロードするときの注意点

aws-cliを使ってローカルからファイルをアップロードしようとしたのですが、csv.gzなファイルをアップロードするときにちょっとハマってしまったのでメモとして残しておきます。

まずはcsvファイルを用意します。

$cat sample.csv
hoge,fuga
foo,bar

$ gzip sample.csv

$ ls
sample.csv.gz

gzファイルになりました。

これを普通に$ aws s3 cpコマンドを使ってs3にコピーしようとしてみます。

$ aws s3 cp sample.csv.gz s3://my-bucket/hoge_dir/

できました。

と、これをブラウザからダウンロードしてみると、ファイルはsample.csvという名前になっています。 お、これはgzをいい感じに解凍してくれているっぽいですね。

開いてみます。

Oops、だめだ。これcsvになっていない。 文字化けしてしまいました。

手動で同じことをやってみます。 すると、ダウンロードしても勝手に解凍されない。 sample.csv.gzファイルとしてダウンロードされています。 ローカルで解凍します。 csvファイルになったのでそれを開いてみます。

$cat sample.csv
hoge,fuga
foo,bar

ちゃんと開けていますね。

aws-cliでアップロードしたファイルをもう一度見てます。 ファイル名をcsvcsv.gzに戻してみます。 そしてこれを解凍し、開いてみます。 すると、正常に開けましたー

ということで、s3からダウンロードするときにgzを解凍していないのに解凍している風を出していたのが問題でした。

しかしs3はこんな余計なことをやってくれるんだろうか?

GUIからアップロードしたのと、CLIでコピーしたファイルの何が違うのか調べてみました。

すると、 プロパティ > メタデータ を見てみます。 正常に動作できるGUIでアップロードした方には、 Content-Type application/octed-stream とある。 一方で、挙動がおかしい方は Content-Type text/csv とある。

application/octed-streamとは?

https://wa3.i-3-i.info/word15821.html

まぁ「application/octet-stream」って単語が出てきたら「その他のファイル(ファイルの種類は気にするな)なんだな~」と、お考えください。

とのことで、よくわからないけど、これにすると大丈夫らしい。

ということでaws-cliでもやってみました。

$ aws s3 cp sample.csv.gz s3://my-bucket/hoge_dir/ --content-type --content-type application/octed-stream

とすると、無事正しい挙動になりました。