山pの楽しいお勉強生活

勉強の成果を垂れ流していきます

Pythonからs3fsを使用してS3を操作した際にハマった件について

はじめに

PythonsからS3をいじる際に、これまではaws cliを直接叩いていたのですが、s3fsを使用するとexistsやlsみたいなわかりやすい名称で使えるというのをどこかで知ったので使ってみた。

同名でS3をマウントするもの( https://github.com/s3fs-fuse/s3fs-fuse )もあるようですが、本記事でのs3fs( https://github.com/dask/s3fs )とは別物です。 で、キャッシュのせいかファイルを置いた直後にexistsするとFalseだったりと整合性がとれなくてハマった話です。

s3fsとは

リポジトリTOPには下記のように記載されています。

S3FS builds on boto3 to provide a convenient Python filesystem interface for S3.

Google翻訳すると↓

S3FSはboto3をベースにしており、S3用の便利なPythonファイルシステムインタフェースを提供します。

「はじめに」にも書きましたが、同名でS3をマウントするものもあるようですが、別物です。 むしろ、GitHubの☆を見てもマウントする奴が3000以上、本題の方が130位なので、どちらかというとこの記事に書くライブラリの方が無名。

使い方

インストールして(詳細

pip install s3fs

以下のように書けばOK

import s3fs
fs = s3fs.S3FileSystem(anon=True)
list = fs.ls('my-bucket')

ドキュメントがしっかりしているので、以下に気をつければ?簡単に使えて良いです。 が、気をつけるなんて現実的ではないので個人的にはあまりオススメしません。

ハマったことと対応方法

内容としては、ファイル配置後に上位のフォルダを見た際にFileNotFoundErrorになるという事。
で、キャッシュが悪さしているので、キャッシュを更新すれば正しく動作します。 (キャッシュ更新するにはlsのrefreshオプションをTrueにする)

具体的には下記のコード参照。 (実際に試したい場合には↓↓に全コード書いたので、そちらをオススメ。)

import s3fs

fs = s3fs.S3FileSystem(key=key, secret=secret)
# 特定のPATHの下にフォルダを挟んでファイルを配置
output_path = '{}/hoge/{}'.format(s3_path, 'test.txt')
fs.put('./test.txt', output_path)

# ファイルを配置したフォルダではなく上位のフォルダでlsするとFileNotFoundError
try:
    fs.ls(s3_path) # FileNotFoundError
except FileNotFoundError:
    print('ls : FileNotFoundError')
# existsでも同様に取得できない
if not fs.exists(s3_path):
    print('exists : False')
# 上位のフォルダから再帰的に削除しようとしてもFileNotFoundError
try:
    fs.rm(s3_path, recursive=True) # FileNotFoundError
except FileNotFoundError:
    print('rm : FileNotFoundError')

# lsにrefreshオプションがあったので付与すると取得できる
try:
    fs.ls(s3_path, refresh=True) # refresh=Trueをつける
    print('ls : refreshしたら例外なし')
except FileNotFoundError:
    pass

# 1度refreshするとexistsも問題なし
if fs.exists(s3_path):
    print('exists : refresh後はTrue')

# 同様にrefresh後はlsにrefreshつけなくても例外なし    
try:
    fs.ls(s3_path)
    print('ls : FileNotFoundErrorは発生しない')
except FileNotFoundError:
    pass

考察とか

全コード

import s3fs

key = 'xxxxxxxxxx'
secret = 'yyyyyyyyyyyyyyy'
s3_path = 'my-bucket/aaa/bbb'

# 前準備
try:
    fs.ls(s3_path, refresh=True)
    fs.rm(s3_path, recursive=True)
except FileNotFoundError:
    pass
try:
    fs.ls(s3_path)
except FileNotFoundError:
    pass
try:
    fs.ls('{}/{}'.format(s3_path, 'hoge'))
except FileNotFoundError:
    pass

# 特定のPATHの下にフォルダを挟んでファイルを配置
output_path = '{}/hoge/{}'.format(s3_path, 'test.txt')
fs.put('./test.txt', output_path)

# ファイルが置かれていることを確認
try:
    fs.ls(output_path)
    print('ls : FileNotFoundErrorは発生しない')
except FileNotFoundError:
    pass
if fs.exists(output_path):
    print('exists : True')

# ここから本番
# ファイルを配置したフォルダではなく上位のフォルダでlsするとFileNotFoundError
try:
    fs.ls(s3_path) # FileNotFoundError
except FileNotFoundError:
    print('ls : FileNotFoundError')
# existsでも同様に取得できない
if not fs.exists(s3_path):
    print('exists : False')
# 上位のフォルダから再帰的に削除しようとしてもFileNotFoundError
try:
    fs.rm(s3_path, recursive=True) # FileNotFoundError
except FileNotFoundError:
    print('rm : FileNotFoundError')

# lsにrefreshオプションがあったので付与すると取得できる
try:
    fs.ls(s3_path, refresh=True) # refresh=Trueをつける
    print('ls : refreshしたら例外なし')
except FileNotFoundError:
    pass

# 1度refreshするとexistsも問題なし
if fs.exists(s3_path):
    print('exists : refresh後はTrue')

# 同様にrefresh後はlsにrefreshつけなくても例外なし    
try:
    fs.ls(s3_path)
    print('ls : FileNotFoundErrorは発生しない')
except FileNotFoundError:
    pass

参考URL

PowerShellでJava実行する際にシステムプロパティを設定する際の注意点

概要

f:id:yamap_55:20181108195116p:plain f:id:yamap_55:20181108195251p:plain

  • 正しく実行する際には java "-Dsystem.language=ja" Sample のようにダブルクォーテーションで囲う必要がある。 f:id:yamap_55:20181108195415p:plain

  • PowerShellは「-」の後に「.」が来ると区切りと判定するらしい

経緯とか

  • 日本語版Stack Overflowで質問発見
  • ↓のJavaコマンドを投げても正しく動かないらしい。
    • java -cp libtensorflow-1.11.0.jar;. -Djava.library.path=.\jni HelloTensorFlow
  • エラーメッセージを見るとクラスが正しく認識でされてないっぽいので、コマンドのどこかにスペース入ってるんじゃね?っと思ったら違うらしい。
    • エラーメッセージ : エラー: メイン・クラス.library.path=.\jniが見つからなかったかロードできませんでした
  • やり取りしていたら、PowerShell上ではオプションが正しく設定されないという事が発覚。
  • 調べてみたらPowerShellの仕様っぽい。

echoの例

  • 正常系1
PS C:\work\20181108> echo a b
a
b
  • 正常系2
PS C:\work\20181108> echo a.b
a.b
  • 想定外1
PS C:\work\20181108> echo -a.b
-a
.b
  • 想定外2

本題ではないですが、「,」を入れてみたら分割されました。

PS C:\work\20181108\j> echo a,b
a
b

参考URL

S3で静的ファイルのホスティングしてIP制御

経緯

静的なHTMLをお客さんに見せる事になり、ローカルPCで見せてもいいけど、どうせならどこかにホスティングして置いといた方がいいよね。って事でS3に置いたらIP制御でハマったのでメモ。

※ハマったというか、エンドポイントではなくて直接ファイルにアクセスしてたのでうまくいかなかっただけ。。。

手順

  1. S3にバケット作る
  2. バケットのプロパティ → Static website hosting → 「このバケットを使用してウェブサイトをホストする」 → 保存
    • エンドポイントのURLをメモ
    • http://bucket-name.s3-website-ap-northeast-1.amazonaws.com
  3. 2で設定したインデックスドキュメント(デフォルトはindex.html)を置く
  4. エンドポイントにブラウザでアクセスして、index.htmlが表示されることを確認。
  5. バケット → アクセス権限 → バケットポリシー
    • 以下のように記載(うまくいかない場合は、ポリシージェネレーターで作成)

バケットポリシーエディタ

{
    "Version": "xxxxxxxxxxxxx",
    "Id": "xxxxxxxxxxxx",
    "Statement": [
        {
            "Sid": "xxxxxxxxxxxxx",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": "arn:aws:s3:::bucket-name/*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": [
                        "198.51.100.0",
                        "192.0.2.0"
                    ]
                }
            }
        }
    ]
}
  • 各「xxxx」的な所と、Resourceの「バケット名」、aws:SourceIpの「IP」は適当な値に変更する。

確認

  • 自分のIPのみを設定して表示されることを確認。
  • 他のIP(スマホからなど)で表示されないことを確認。
  • ↑と同じく、直接ファイルのリンクにアクセスして、表示される、されない事を確認。

VSCodeで静的コード解析ツールPylintを使用する

はじめに

PythonではPEP8というコード規約が一般的に使用されているようです。(PEP 8 -- Style Guide for Python Code

この規約に準拠したツールがいくつかあり、これ!っというものは特にないという印象です。
で、代表的なものとして、同名のpep8があるのでややこしい。
ちなみにツールの方のpep8は名前を変更してpycodestyleとなっていますので注意。(pep8は1.7.1が最終更新のようです。)
詳細はQiitaにまとまっている記事がありましたのでこちらを参照してください。(pep8 が pycodestyle に変わった話

他にもflake8や今回紹介するPylintが競合としてあるようです。
で、今回はPylintを使用することになっていたので、こちらをVSCodeで使用する方法について調べたメモです。

手順

  • VSCodePythonを入れる際にはまず使用するであろう拡張機能ms-python.pythonを入れる
  • 設定を確認
    • "python.linting.enabled"true になっていること
    • "python.linting.pylintEnabled"true になっていること
    • "python.linting.lintOnSave" はお好みで
    • "python.linting.pylintPath" このPATHでコマンドが実行されるので、PATHが通ってない場合には修正が必要です。
  • Pylintの設定ファイルを作成
    • デフォルトだと警告が出てくれない事、厳しすぎる規約を変更したい事から設定ファイルを作成します。
    • 下記のコマンドでデフォルトの設定ファイルが作成されます。
    • pylint --generate-rcfile > .pylintrc
      • 【注意】私の環境だとUTF-16で出力されたのでUTF-8に変換の必要がありました。
    • 設定ファイルの場所は公式ドキュメントを参照。
  • 指摘を確認
    • 問題ビューに指摘が表示されます。(macだとshift + command + m)
    • 明らかにおかしいコードで表示されない場合には、設定ファイルを一旦renameして指摘されるか確認しましょう。
  • 設定ファイルを調整
  • システム全体で警告を無効化する場合は設定ファイルを修正。
    • あるコードのみ例外的に無視したい場合にはコメントとして↓のように無視したいコードを入力すれば無視してくれます。
      • # pylint: disable=C0330
      • あまり調べていないのですがファイル全体で無効化されるようなので注意。
    • 好みですが、私は以下を修正しました。
    • max-line-lengthを120
      • デフォルトの100だと厳しいと感じました。
    • good-namesにf,vを追加
      • スコープに関係なく、1から2文字の変数を許してくれません。
      • 実際のプロジェクトでは分析でよく使用される変数名を追加しています。

参考URL

オブジェクトの配列をPandasのDataFrameに変換する

背景とか

オブジェクト(エンティティとか、JavaBeansとか、DTOとか呼ばれる属性(フィールド)しか持たない奴)の配列をPandasのDataFrameに変更したメモです。
簡単にできそうだったけどできなかったので結局dictionaryに変換して突っ込みました。
PythonにもPandasにも詳しくないのですが、こういうケースはよくあると思ったのですが、あまりない??

コード

import pandas as pd

class Hoge:
    c1 = ''
    c2 = ''
    c3 = ''

    def __init__(self, *args):
        self.c1 = args[0]
        self.c2 = args[1]
        self.c3 = args[2]

hoge1 = Hoge('c1_1','c2_1','c3_1')
hoge2 = Hoge('c1_2','c2_2','c3_2')
hoge3 = Hoge('c1_3','c2_3','c3_3')

ar = [hoge1, hoge2, hoge3]

# 最初こんなのでできるかなと思ってました。
df1 = pd.DataFrame(ar)
print(df1) # 1列3行でHogeオブジェクトが設定される(当たり前)

# 最終的にこうなりました。
df2 = pd.DataFrame(list(map(lambda hoge: vars(hoge), ar)))
print(df2)

SQLAlchemyでAUTO_INCREMENTされた値を取得したい

結論

  • addした後にflushすると、addしたオブジェクトに設定されている。
    • 【未確認】exuecuteで実行する場合には返り値から取得できる?(参考

環境

  • MySQL 5.7(Cloud SQL
  • Python 3.6.6
  • PyMySQL 0.8.1
  • SQLAlchemy 1.2.0

コード

from sqlalchemy import Column
from sqlalchemy.dialects.mysql import INTEGER,TEXT
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class tablea(Base):
    """テーブルの定義"""
    __tablename__ = 'tablea'

    id = Column(INTEGER, primary_key=True)
    id2 = Column(INTEGER, primary_key=True, autoincrement=True)
    name = Column(TEXT)

if __name__ == "__main__":
    # MySQLに接続。
    url = 'mysql+pymysql://user:password@hostname/dbname?charset=utf8'
    engine = create_engine(url, echo=True)

    engine.execute('DROP TABLE IF EXISTS {}'.format("tablea"))

    # テーブル作成
    Base.metadata.create_all(engine)

    # セッションの作成
    Session = sessionmaker(bind=engine)
    session = Session()

    tablea1 = tablea(id=100, name='太郎')
    tablea2 = tablea(id=200, name='次郎')
    session.add(tablea1)
    session.add(tablea2)

    # addしただけではDBに反映されない
    # autoincrementされるid2についても値はNone
    print('add後のid2の値')
    print(tablea1.id2) # None
    print(tablea2.id2) # None
    session.flush()
    print('flush後のid2の値')
    print(tablea1.id2) # 1
    print(tablea2.id2) # 2

    # 全件出力
    print("全件出力")
    for row in session.query(tablea).all():
        print(row.id, row.id2, row.name)

#    session.commit() # commitしなければrollbackされる

SQLAlchemyでAttributeError

経緯とか

  • 調査依頼を受けて調べてみた結果を記載。
  • どこかの誰かは助かるかも的な奴。

調査依頼の内容

  • SQLAlchemyでselectしようとしたら変な例外で落ちた。
  • SQLを実行する前に落ちている。
  • よくわからないオブジェクトでよくわからない例外が発生している。

概要

  • 変なオブジェクトを挿入しようとしたので例外が発生。
  • なんで変なオブジェクト挿入しようとしたのかというと、autoincrementされた値を取得したいため。

詳細

  • insertしようとして、intのカラムにオブジェクトを設定する。
  • オブジェクトを設定した際には型チェックしてくれないのでそのまま設定される。
  • addしただけではDBに挿入されず、次のフラッシュでデータベースに反映されるため、例外は発生しない。
  • selectを行う際にフラッシュの処理が走る。
  • そこでaddを実際にinsertを行うための前処理?が走る。
  • 設定された値の文字コードを変換する処理が走る。
  • 「設定された値 = オブジェクト」なので、変換できない。
  • 例外発生。

環境

  • Windows10
  • MySQL 5.7(Cloud SQL
  • Python 3.6.6
  • PyMySQL 0.8.1
  • SQLAlchemy 1.2.0

例外

Traceback (most recent call last):
  File "test.py", line 55, in <module>
    for row in session.query(tablea).all():
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\orm\query.py", line 2726, in all
    return list(self)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\orm\query.py", line 2877, in __iter__
    self.session._autoflush()
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\orm\session.py", line 1428, in _autoflush
    self.flush()
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\orm\session.py", line 2237, in flush
    self._flush(objects)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\orm\session.py", line 2363, in _flush
    transaction.rollback(_capture_exception=True)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\util\langhelpers.py", line 66, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\util\compat.py", line 187, in reraise
    raise value
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\orm\session.py", line 2327, in _flush
    flush_context.execute()
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 391, in execute
    rec.execute(self)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 556, in execute
    uow
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\orm\persistence.py", line 181, in save_obj
    mapper, table, insert)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\orm\persistence.py", line 866, in _emit_insert_statements
    execute(statement, params)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\engine\base.py", line 948, in execute
    return meth(self, multiparams, params)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\sql\elements.py", line 269, in _execute_on_connection
    return connection._execute_clauseelement(self, multiparams, params)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\engine\base.py", line 1060, in _execute_clauseelement
    compiled_sql, distilled_params
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\engine\base.py", line 1200, in _execute_context
    context)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\engine\base.py", line 1416, in _handle_dbapi_exception
    util.reraise(*exc_info)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\util\compat.py", line 187, in reraise
    raise value
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\engine\base.py", line 1193, in _execute_context
    context)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\sqlalchemy\engine\default.py", line 507, in do_execute
    cursor.execute(statement, parameters)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\pymysql\cursors.py", line 168, in execute
    query = self.mogrify(query, args)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\pymysql\cursors.py", line 147, in mogrify
    query = query % self._escape_args(args, conn)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\pymysql\cursors.py", line 127, in _escape_args
    return dict((key, conn.literal(val)) for (key, val) in args.items())
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\pymysql\cursors.py", line 127, in <genexpr>
    return dict((key, conn.literal(val)) for (key, val) in args.items())
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\pymysql\connections.py", line 846, in literal
    return self.escape(obj, self.encoders)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\pymysql\connections.py", line 839, in escape
    return converters.escape_item(obj, self.charset, mapping=mapping)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\pymysql\converters.py", line 27, in escape_item
    val = encoder(val, mapping)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\pymysql\converters.py", line 118, in escape_unicode
    return u"'%s'" % _escape_unicode(value)
  File "C:\Users\username\AppData\Local\Programs\Python\Python36\lib\site-packages\pymysql\converters.py", line 73, in _escape_unicode
    return value.translate(_escape_table)
AttributeError: 'ResultProxy' object has no attribute 'translate'

再現コード

from sqlalchemy import Column
from sqlalchemy.dialects.mysql import INTEGER,TEXT
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class tablea(Base):
    """テーブルの定義"""
    __tablename__ = 'tablea'

    id = Column(INTEGER, primary_key=True, autoincrement=True)
    name = Column(TEXT)

class tableb(Base):
    """テーブルの定義"""
    __tablename__ = 'tableb'

    id = Column(INTEGER, primary_key=True, autoincrement=True)
    tablea_id = Column(INTEGER)

if __name__ == "__main__":
    # MySQLに接続。
    url = 'mysql+pymysql://user:password@hostname/dbname?charset=utf8'
    engine = create_engine(url, echo=True)

    engine.execute('DROP TABLE IF EXISTS {}'.format("tablea"))
    engine.execute('DROP TABLE IF EXISTS {}'.format("tableb"))

    # テーブル作成
    Base.metadata.create_all(engine)

    # セッションの作成
    Session = sessionmaker(bind=engine)
    session = Session()

    # autoincrementのidカラムには何も指定せずに挿入
    tablea1 = tablea(name='太郎')
    session.add(tablea1)

    # autoincrementされたidを取得するために、MySQLのlast_insert_idを使用する
    lastindex = session.execute("select last_insert_id() as id")

    # 取得したIDを別テーブルに挿入
    # ※取得したオブジェクトをそのまま設定しているが、設定時、add時共にエラーとならない
    tablea2 = tableb(tablea_id=lastindex)
#    tablea2 = tableb(tablea_id=10)
    session.add(tablea2)

    # 全件出力
    print("全件出力")
    # 全然関係ないselect投げる際に例外発生
    for row in session.query(tablea).all():
        print(row.id, row.name)

#    session.commit() # commitしなければrollbackされる

学んだこととか

  • 型が恋しい。
    • intのカラムにオブジェクト突っ込んだ時に例外として欲しい。
  • session.query()はflushするが、session.execute()はflushしない?
    • 上記のコードを正しく動かすために、「session.execute("select last_insert_id() as id").fetchone().id」とすると動作はするが、意図した値にならない。
      • 0が返ってくる。(本当は1が返ってきて欲しい)
    • もし、このコードのまま使うのであればflush()してからlast_inset_id()を実行する必要がある。
    • ただ、flush()すると、挿入時に使用したオブジェクトに値を設定してくれるので、わざわざlast_insertで取得する必要はない。
  • スタックトレースをよく見る。
    • SQLAlchemyを動作させるところから1時間位?かかってしまったがよくスタックトレースみたら、「ResultProxy」をエンコードしようとして落ちてる事、flush時に発生している事がわかるので、どこかで「ResultProxy」を値に突っ込んでいる事が原因とわかったはず。
    • あるあるだが、よく見ること大事。