Flask タグの一覧です(18 件)

Flaskでog:imageを動的に切り替える

FaceBookなどにブログをシェアした時にサムネイルとして使用されるog:imageを動的に切り替えたい
具体的には

  • エントリーに画像リンクが一つ貼られている場合はその画像URLを使用する
  • エントリーに画像リンクが複数貼られている場合は一番最初に貼られている画像URLを使用する
  • エントリーに画像リンクが一つもない場合、あらかじめ用意した画像URLを使用する
  • トップページの場合も同じくあらかじめ用意した画像URLを使用する

このブログはFlaskを使用した自作ブログなので、このような機能を追加する場合は自力でなんとかするしかなくて(まあそれが楽しいんだけど)他に困っている人がいるとも思えないけれども一応メモしておく

ブログの本文を取り出す

SQLAlchemyを使っているのでこのような感じで個別エントリーを取り出す

entry = Entry.query.filter_by(id = post_id).one()

ブログの本文からimgタグのsrcを取り出す

これはbs4を使う。find_allでなくfindを使うことによって目的のタグが見つかったらそこでパースをやめるようにした(処理時間の短縮)

def get_src_tag(e):
    """get first img src tag"""
    soup = BeautifulSoup(e.text, 'html.parser')
    img = soup.find('img')
    if img:
        return img['src']
    else:
        return None

view関数の作成

@mod.route('/entry/<int:post_id>/')
def show_entry(post_id):
    entry = Entry.query.filter_by(id = post_id).one()
    flash(u'%s' % entry.pub_date.strftime('%Y-%m-%dT%H:%M:%S+09:00'), 'alert-success')
    return render_template('show.html',
        entries=Entry.query.filter_by(id=post_id).all(), tags=show_tags(),\
                                      archives=count_archives(), src=get_src_tag(entry))

本件とは関係ないものも混じっているけれどだいたいこんな感じ
entry = Entry.query.filter_by(id = post_id).one() と src=get_src_tag(entry) のところが今回追加したところ

テンプレート(jinja2)の変更

とりあえずヘッダー部分だけ貼り付け

<html lang="ja">
    <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
        {% if request.endpoint == 'entry.show_entries' %}
        <title>aoshiman.org</title>
        <meta property="og:title" content="aoshiman.org" />
        <meta property="og:url" content="https://blog.aoshiman.org" />
        <meta property="og:image" content="https://static.flickr.com/3604/3281177662_70807f7065.jpg" />
        {% elif request.endpoint == 'entry.show_entry' %}
        {% for entry in entries-%}
        <title>{{entry.title|safe}} | aoshiman.org</title>
        <meta property="og:title" content={{entry.title|safe}} | aoshiman.org />
        <meta property="og:url" content="https://blog.aoshiman.org{{url_for('entry.show_entry', post_id=entry.id)}}" />
        {% if src != None %}
        <meta property="og:image" content={{src}} />
        {% else %}
        <meta property="og:image" content="https://static.flickr.com/3604/3281177662_70807f7065.jpg" />
        {% endif %}
        {%-endfor %}
        {% else %}
        <meta property="og:title" content="aoshiman.org" />
        <meta property="og:url" content="https://blog.aoshiman.org" />
        <meta property="og:image" content="https://static.flickr.com/3604/3281177662_70807f7065.jpg" />
        <title>aoshiman.org</title>
        {% endif %}
        <meta property="og:type" content="article" />
        <meta property="og:site_name" content="aoshiman.org" />
        <meta property="fb:app_id" content="1842519809295747" />
        <meta name="twitter:card" content="summarylargeimage" />
        <link rel="stylesheet" href="{{url_for('static', filename='bootstrap/css/bootstrap.min.css')}}" media="screen">
        <link rel="stylesheet" href="{{url_for('static', filename='css/style.css')}}">
        <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="alternate" title="RSS" href="https://blog.aoshiman.org/rss/" type="application/rss+xml">
        <link rel="shortcut icon" href="{{ url_for('static',filename='favicon.ico') }}" />
        <script type="text/javascript">
            var _gaq = _gaq || [];
            _gaq.push(['_setAccount', 'UA-*******-*']);
            _gaq.push(['_trackPageview']);

            (function() {
             var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
             ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
             var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
             })();
         </script>
    </head>

ちょっと長いんだけど、request.endpointで個別記事かそうでないかを判別。srcがNoneかそうでないかで画像URLを振り分けている


探検! Python Flask を読んだ

探検! Python Flask
Robert Picard, 濱野 司(訳)
達人出版会
発行日: 2015-05-08
対応フォーマット: PDF, EPUB

昨日この本を見つけて、速効で買いました。フォーマットがePubだったので、mobiに変換してKindleに取り込んで読んでいます。なんか一年前に出てたみたいですね、全然知らんかった(;一_一)

このブログをFlaskを使って作り直したのが2011年9月なので、Flaskはかれこれ5年程触っていますが、未だに好きなフレームワークです。今まで日本語のFlask本は(多分)出ておらず、使用にあたっては、チュートリアルやったり、Flaskのリポジトリのexampleを弄ってみたり、トラブルはStackOverflowで解決図ったりしてのらりくらりなんとか使ってきましたが、やはり日本語のFlask解説本、あると便利ですね。

どんなことが書かれているかはページサンプルを見て頂ければと思いますが、個人的にはBlueprintsの解説がしっかりと書かれていたので嬉しかったです。プロダクトでも社内管理システムでもFlaskで作ろうとするならばBlueprints無いと大変だと思うので。

使ったことがないエクステンションが幾つかあったので時間があったら勉強がてら、このブログに採用してみたいなと思いました。


Frozen-Flask で build 後にHTTPサーバを実行させる

Frozen-Flaskで静的ファイルの生成を実施する場合、このようなスクリプトファイルを作って実行します

# freeze.py
from flask_frozen import Freezer
from app import app

freezer = Freezer(app)

if __name__ == '__main__':
    freezer.freeze()
実行するとbuildというフォルダが作成され、静的ファイル群はその中に生成されます。本番サーバにデプロイする場合は、build内のファイル郡をドキュメントルート等にアップロードします。
デプロイする前に生成された静的ファイルを確認する場合、今までは

$ cd build
$ python -m http.server
などとしていたのですが、freezer.run() を使えば、ビルド後にHTTPサーバ(実態はwerkzeugだと思う)が起動されることがドキュメントに書いてありました。

Frozen-Flask — Frozen-Flask 0.12 documentation

なので今は、引数なしfreeze.pyでビルド、 引数run付きで ビルド後にHTTPサーバ起動というようにしています。

import sys
from flask_frozen import Freezer
from app import app

freezer = Freezer(app)

if __name__ == '__main__':
    if len(sys.argv) > 1 and sys.argv[1] == "run":
        freezer.run(debug=True)
    else:
        freezer.freeze()

CMSやブログ的にFlaskを使っているならば、Frozen-Flaskはオススメです。また、静的サイトジェネレーター的に使う場合は下記サイトが参考になります。

Dead easy yet powerful static website generator with Flask | Code | Nicolas Perriault

Flaskアプリケーションに二段階認証機能を実装(通算2回目)

Flask製ブログツールをローカルではなく、パブリックなVM(Digitalocean)に立ててブログを書くことにしたので、念の為、二段階認証機能を実装しました。
実は、ブログに組み入れるのは2回目で、ちょうど2年前の11月に一度実装しています(その後面倒くさくなってやめた)。その時の学んだことは Shizuoka.py#3 で発表させて頂きました(なつかしい)。


当時は二段階認証の名前は聞いたことがあるけど実際にやったことがある人が少なく(Shizuoka.py#3の参加者でも使っている人が10人中1人(私)だった)、発表聴いてくれている方々が理解してくれているかどうか不安でした。それから2年経ち、多くのサービスで利用されるようになってきたので、今現在は実際に使うようになっている方も多いのではないかと思います。

pyotpを使う

pythonでtotpを扱うモジュールはいくつかありますが、私はpyotpを使用しています。シンプルなので気に入っています。 基本動作をIPyhtonで確認してみます

In [1]: import pyotp

In [2]: import time

In [3]: totp = pyotp.TOTP('JBSWY3DPEHPK3PXP')

In [4]: totp.now()
Out[4]: '211775'

In [5]: totp.verify(211775)
Out[5]: True

In [6]: time.sleep(30)

In [7]: totp.verify(211775)
Out[7]: False

因みに上記で記述されているシークレットキー(JBSWY3DPEHPK3PXP)はgoogle authenticatorのサンプルコードでも記述されていて、totpのサンプルで使われるのを時々見かけますが、これはBASE32文字列におけるHexspeak(deadbeef)になっています。その為、サンプルで使われているのかと思慮。

In [1]: import base64

In [2]: import binascii

In [3]: binascii.b2a_hex(base64.b32decode('JBSWY3DPEHPK3PXP'))
Out[3]: b'48656c6c6f21deadbeef'

Flaskアプリへ組み込む

Flaskアプリへ組み込む場合、べた書きしてもよいのですが、再利用できるように、pyotpを使用して受け取ったワンタイムパスワードが正しいかどうか判定するfunctionを作成します。こんな感じです。

from pyotp import TOTP
from datetime import datetime, timedelta

def tfa_auth(secret, otp):
    totp = TOTP(secret)
    now = datetime.now()
    past = now + timedelta(seconds=-30)
    try:
        return otp == totp.now() or otp == totp.at(past)
    except:
        return False

タイムギャップ処理(-30)は必ずつけるのですが、+30をどうするのか悩みます(最近のスマートフォンは必ず時刻同期されているだろうから必要ない?)。
そしてviews側のfunctionはこんなかんじです。この処理の前にユーザー認証処理があって、認証されたらこのfunctonに飛びます。

@app.route('/verify_tfa/', methods=['GET','POST'])
def verify_tfa():
    error = None
    if request.method == 'POST':
        if tfa_auth(app.config['TFA_SECRET'], str(request.form['token'])):
            session['logged_in'] = True
            flash(u'You were logged in', 'alert-success')
            return redirect(url_for('show_entries'))
        else:
            error = 'Invalid verification code'
    return render_template('verify_tfa.html', error = error)

二段階認証機能の実装は、QRコード生成等のステップが無ければ、結構単純なので一度やってみるのもいいと思います。


[jQuery] timeago.js を使って経過時間表示させる

Frozen-Flaskを使うようになり、今までサーバーサイドで表示させていた 経過時間表示が使い物にならなくなったので、クライアントサイドで表示させるように変更しました。具体的には timeago.js を使って実現しています。

timeago: a jQuery plugin

やり方ですが、まず app.py 側で該当エントリーの 日時(この場合は pub_date) を strftime を使ってISO8601に変換

@app.route('/entry/<int:post_id>/')
def show_entry(post_id):
    dt = Entry.query.filter_by(id = post_id).one()
    flash(u'%s' % dt.pub_date.strftime('%Y-%m-%dT%H:%M:%S+09:00'), 'alert-success')
    return render_template('show.html',
        entries = Entry.query.filter_by(id = post_id).all())

次に templateファイルl内 で flashメッセージを受け取る記述をします。その時に spanタグの class名をtimeagoとし、titleに日時(ISO8601)がセットされるようにします。spanタグはabbrでも可能ですが、後述するtimeago.js のパラメータの記述も合わせる必要があります。

{% elif request.endpoint == 'show_entry' %}
                <div class="row">
                    <div class="col-md-8">
                        {% for category, message in get_flashed_messages(with_categories=true) -%}
                        <div class="{{category}}"><span class=timeago title="{{message}}"></span></div>
                        {%- endfor %}
                        {{ self.body() }}
                    </div>
                    <div class="col-md-4">
                        <div id="sidebar">
                            {% block sidebar %}{% endblock %}
                        </div>
                    </div>
                </div>

最後に同じくtemplateファイルで timeago.js の設置と設定をします。

<script type="text/javascript" src="/static/js/jquery.timeago.js"></script>
        <script>
            // Japanese setting for Timeago
            $.timeago.settings.strings = {
                prefixAgo: "",
                prefixFromNow: "今から",
                suffixAgo: "",
                suffixFromNow: "後",
                seconds: "1 分未満",
                minute: "約 1 分",
                minutes: "%d 分",
                hour: "約 1 時間",
                hours: "約 %d 時間",
                day: "約 1 日",
                days: "約 %d 日",
                month: "約 1 月",
                months: "約 %d 月",
                year: "約 1 年",
                years: "約 %d 年",
                wordSeparator: ""
            };
        </script>
        <script>
            $(function() {
                $('span.timeago').timeago();
                $('span.timeago').before("この記事は投稿から");
                $('span.timeago').append("が経過しています");
            });
        </script>

私の場合、前後にメッセージを結合したかったので、before とappend で追加しています(このやり方でいいのか不安)。因みに日本語化はこちらを参考にしました。
jquery-timeago/locales at master · rmm5t/jquery-timeago

尚、今までのサーバーサイド側での方法はこちらのエントリーをご参照願います。
[Python][Flask]ブログエントリーの経過時間を表示させる | aoshiman.org


PAGE TOP