Python3 + MongoEngine + Pillow でTwitter の画像をMongoDBに保存する
Webから拾ってきた画像、たとえば Twitter に投稿された画像などをMondoDBに保存したいと考えました。言語は Python を使っているとすると MongoEngine というライブラリが“よさそう”な候補として挙がってくることになります。ところが、ネットを検索しても MongoEngine の情報が多くないようです。「画像を保存する方法」を検索しても、なかなか適切な情報がありません。そこで、ネットの情報と自分で試行錯誤した結果を方法を記しておきます。環境は Ubuntu 14.04, Python 3.4, MongoDB 3.0.3 です。
MongoEngine とは?
MongoEngine は NoSQLである MongoDB を扱うための Python用 のライブラリです。いわゆる ORM(Object-Relational Mapper)、つまり DB のデータをオブジェクトとして扱うためのものと似ています。MongoDB はリレーショナル・データベースではなく NoSQL に属するドキュメント・データベースの一種ですので、厳密には ODM (Object-Document Mapper)になります。
Python + MongoDB というと PyMongo や Django に備わっている ORM がスタンダードとして使われており、情報量も多いようです。しかし、ネットで見る評価では、MongoEngine も使いやすいという印象を受けます。MongoEngine は内部で PyMongo を使用しつつ、より直感的にデータを扱うためのライブラリになります。
MongoEngine の導入方法は、他のパッケージ同様に pip で行います。検索するとすぐに見つかると思いますので、ここでは割愛します。MongoDB の導入方法も同様に割愛します。
MongoEngine の使い方
MongoEngine を導入すると、mongoengine.onnect を使用してすぐに MongoDB に接続することができるようになります。データベース名を「twitter_data」とすると、以下のようになります。
from mongoengine import connect connect('twitter_data', host='localhost',port=27017)
この「twitter_data」に tweet というコレクションを保存することにします。 tweet コレクションは、MongoEngine では Document クラスを継承した Tweet クラスとして定義します。とりあえずは、ツイートのIDとユーザーのアカウント名、表示名だけ保存するように定義します。
from mongoengine.document import Document from mongoengine import fields class Tweet(Document): twid = fields.IntField(unique=True) account = fields.StringField() name = fields.StringField()
この Tweet クラスのインスタンスを適当に作成し、save メソッドを実行することでデータとしてMongoDBに保存します。実際には、Twitter API で取得した内容などを使用してインスタンスを作成するイメージです。
tw = Tweet() tw.twid = 663314203642454016 tw.account = "felis_catus_" tw.name = "たけまる" tw.save()
以下のように、ドキュメントが保存されていることがわかります(画面は PyCharm)。
MongoDB に保存されたデータを読み込むのは、Tweet.object.all() を利用して実行することができます。
for tw in Tweet.objects.all(): # 何かの処理
さらに詳しいことは、公式ドキュメントや、
MongoEngine User Documentation — MongoEngine 0.24.0 documentation
こちらの記事などを参考にしてください。
MongoDBに画像を保存するには
ここからが本記事の本番です。
ここまでの内容は、ネット検索ですぐに調べられると思いますし、特別に難しいことはありません。
ですが、たとえば、ある URL から取得した画像を MongoDB に保存するにはどうすればいいのでしょうか。
MongoEngine のAPIリファレンスを見ると、ImageField というものがあります。これを使えば、MongoDBに画像を保存することができそうです。
3. API Reference — MongoEngine 0.24.0 documentation
というわけで、おもむろに Tweet クラスに image というプロパティを追加してみます。
from mongoengine.document import Document from mongoengine import fields class Tweet(Document): twid = fields.IntField(unique=True) account = fields.StringField() name = fields.StringField() image = fields.ImageField()
そして、imageプロパティに画像を追加してみましょう(まだ良くわからないので適当に画像の URL を指定しています)。
tw = Tweet() tw.twid = 663314203642454016 tw.account = "felis_catus_" tw.name = "たけまる" tw.image = "/path/to/image.png" tw.save()
これは、(当然ながら)実行してもエラーとなってしまいます。
mongoengine.fields.ImproperlyConfigured: PIL library was not found
StackOverflow で調べると、Pillow を導入しろ と言われているのがわかります。Pillow というのは、Python の画像処理ライブラリ PIL の派生版で、Python 3 の場合はこれを使うことになります。
Pillow — Pillow (PIL Fork) 9.0.1 documentation
なるほど、と納得しつつ、早速
$ pip install pillow
を実行しようとしたら、途中でエラーが出てきて失敗しました。どうやら、libjpeg-dev というライブラリが入っていないことが原因だったようです。
あらためて libjpeg-dev と Pillow を導入します。
$ sudo apt-get install libjpeg-dev $ pip install pillow
これで無事に Pillow が使えるようになりました。
さて、この Pillow で URL から画像を取得して、MongoDB に保存できればいいわけです。
ところが、意外とここが苦労するポイントでした。というのも、Python 2 のときに使われていたライブラリや手順が Python 3 では微妙に異なり、新旧の情報が入り混じって存在している状況です。
https://librabuch.jp/2013/05/python_pillow_pil/librabuch.jp
結局、上記のサイトなどを参考にしつつ、自分で色々試してみたところ、次の方法でうまく行くことがわかりました。
from io import BytesIO import requests tw = Tweet() tw.twid = 663314203642454016 tw.account = "felis_catus_" tw.name = "たけまる" response = requests.get("path/to/image.png") img = BytesIO(response.content) tw.image.put(tmg) tw.save()
ポイントは、
- Requests モジュールを使う
- Pillow(PIL)を使う(といいつつ使ってない)
- StringIO(Python2) や io.StringIOではなく、io.BytesIO を使う
- PILモジュールの Image.open で取得した画像オブジェクトではなく ByteIOのオブジェクトそのものをMongoEngineに渡す
- tw.image に代入するのではなく、tw.image.put の引数として img を渡す
といったところです。
追記
ちなみに、ネット記事の記載通りに Pillow の Image.open を使ってやると、何故か ValidationError
が発生する。これを調べてもなかなか良い情報に出会わない。
response = requests.get("path/to/image.png")
img = Image.open(BytesIO(response.content))
Traceback (most recent call last): File "***.py", line 25, in <module> tw.image.put(img) File "***", line 1490, in put raise ValidationError('Invalid image: %s' % e) mongoengine.errors.ValidationError: Invalid image: read
同じ現象で悩んでいる人はいるもよう。
このへんがヒントになるかもしれない。Python 2 と Python 3 の互換性関連のような気がするなあ……。
Managed Cloud, Cloud Migration and Adoption – Carbon60
MondoDBから画像を取得するには
保存しただけでは中途半端ですので、保存した画像を取得する方法です。こちらは素直に記述すれば大丈夫でした。
from PIL import Image from mongoengine import connect connect('twitter_data', host='localhost',port=27017) for tw in Tweet.objects.all(): img = Image.open(tw.image) img.save(str(tw.twid) + ".png")
これを実行すると、カレントディレクトリにMongoDBから取得した画像が保存されます。
Twitterから取得した画像を保存する
最後に、いままでの流れを応用して Twitter の検索結果からツイートを取得し、画像をMongoDBに保存する ことをやってみたのが以下のコードです。
from mongoengine import connect class MongoDB: @classmethod def connect(cls): connect('twitter_data', host='localhost',port=27017, tz_aware=True) from mongoengine.document import Document from mongoengine import fields class Tweet(Document): twid = fields.IntField(unique=True) account = fields.StringField() name = fields.StringField() created = fields.DateTimeField() image = fields.ImageField() from requests_oauthlib import OAuth1Session import json class Timeline: @classmethod def get_session(cls): return OAuth1Session("...", "...", "...", "...") @classmethod def search(cls, params): session = cls.get_session() response = session.get("https://api.twitter.com/1.1/search/tweets.json", params = params) if response.status_code == 200: data = json.loads(response.text) return data["statuses"] return None from io import BytesIO import requests def upload(): params = { "q" : "JavaScript+を+追いかける私がいた+exclude:retweets+filter:images", #画像があるものだけ "count" : 1, "result_type" : "recent" } MongoDB.connect() for tl in Timeline.search(params): if Tweet.objects(twid=tl["id"]).count() < 1: tw = Tweet() tw.twid = tl["id"] tw.account = tl["user"]["screen_name"] tw.name = tl["user"]["name"] tw.created = tl["created_at"] response = requests.get(tl["entities"]["media"][0]["media_url_https"]) img = BytesIO(response.content) tw.image.put(img) tw.save() from PIL import Image def download(): MongoDB.connect() for tw in Tweet.objects.all(): img = Image.open(tw.image) img.save(str(tw.twid) + ".png")
♪ いつからだろう JavaScript を 追いかける私がいた
— たけまる (@felis_catus_) 2015年11月8日
どうかお願い置いてかないで 使ってよ私の このスキルを pic.twitter.com/XQUFucZ8H1
↓
画像が MongoDB に保存される
↓
MongoDB から画像を取得