蛇ノ目の記

技術のことも。そうでないことも。

herokuのProcfileに書くgunicornのコマンドにハマった話

herokuで動かすWebアプリがどんなプロセスを使うかを定義するProcfile

Procfileはドキュメントルートに置き、例えばFlask + gunicornを使うときには web: gunicorn app:appとする。古事記にもそう書いてある(heroku公式ドキュメントやいろいろなブログ)

heroku公式ドキュメント: Deploying Python Applications with Gunicorn | Heroku Dev Center

その教えに従ってProcfileを作成したが一向に動かない。

Failed to find application object 'app' in 'myapp'

と表示されるばかり。

ディレクトリ構成はこんな感じ。

flask_app
├ Procfile
├ requirements.txt
├ runtime.txt
└ myapp
  ├ myapp.py
  ├ static
  └ templates

一度myappディレクトリに移動する必要があるのでは、と考えて

web: gunicorn --chdir myapp myapp:app

と書き換えたところアプリが正常に動いた。

作ったWebアプリの話はまた後日。

追記

アプリのディレクトリを分けずに以下のようなディレクトリ構成にすることで、Procfileにディレクトリ移動を書かなくてよくなる。アプリのディレクトリを分けているとローカルでの実行やパッケージングで不便になる、と@shimizukawaからアドバイスをもらった。ありがとうございます。

flask_app
├ Procfile
├ requirements.txt
├ runtime.txt
├ myapp.py
├ static
├ templates

また、herokuの環境変数PYTHONPATHをPYTHONPATH=myappsと設定すると、アプリのディレクトリを分けていてもProcfile実行時のディレクトリ移動が必要なくなる。と@shimizukawaからアドバイスをもらった。ありがとうございます。

ユニットテストで躓いたところ - mock.patch()

単純なスクリプトユニットテストを書いていて躓くことがあったので、解決法をメモ。

例えばこんな、おみくじをするだけの簡単なスクリプトがあるとする。

import random


fortunes = {
    1: '凶',
    2: '吉',
    3: '大吉'
}

number = random.choice([1, 2, 3])

fortune = fortunes[number]

print(fortune)

変数numberに1〜3までのランダムな値が入って、対応する運勢が出力される 。random.choice()の部分をパッチして、期待通りの運勢が出力されることをテストする。

import unittest

from importlib import import_module
from unittest.mock import patch


class FortuneTestCase(unittest.TestCase):

    @patch('random.choice', lambda x: 1)
    def test_bad(self):
        module = import_module('fortune')
        expect = ('凶')
        self.assertEqual(module.fortune, expect)

    @patch('random.choice', lambda x: 3)
    def test_good(self):
        module = import_module('fortune')
        expect = ('大吉')
        self.assertEqual(module.fortune, expect)

さて、このテストを実行するとどうなるか。

(env) $ python3 -m unittest tests.test
凶
.F
======================================================================
FAIL: test_good (tests.test.FortuneTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 1191, in patched
    return func(*args, **keywargs)
  File "/tests/test.py", line 21, in test_good
    self.assertEqual(module.fortune, expect)
AssertionError: '凶' != '大吉'
- 凶
+ 大吉


----------------------------------------------------------------------
Ran 2 tests in 0.029s

FAILED (failures=1)

test_goodメソッドのアサーションで例外が発生する。パッチした値が反映されていない。つまり初めに実行されたtest_badメソッドでパッチした値がそのまま使われている。

値がパッチされっぱなしなのではれば、モジュールを読み込み直してみる。importlib.reload()を使う。

Python3 公式ドキュメント importlib - reload

import unittest

from importlib import import_module, reload
from unittest.mock import patch


class FortuneTestCase(unittest.TestCase):

    @patch('random.choice', lambda x: 1)
    def test_bad(self):
        module = import_module('fortune')
        expect = ('凶')
        self.assertEqual(module.fortune, expect)

    @patch('random.choice', lambda x: 3)
    def test_good(self):
        module = import_module('fortune')
        reload(module)
        expect = ('大吉')
        self.assertEqual(module.fortune, expect)
(env) $ python3 -m unittest tests.test
凶
凶
.大吉
.
----------------------------------------------------------------------
Ran 2 tests in 0.025s

OK

無事テストが通った。テストメソッド実行ごとにパッチされると思っていただけにこの動きにだいぶ悩むこととなった。

公式ドキュメントに以下の注釈があるためtest_goodメソッドでだけreloadを使った。

注釈 :いろいろなテストが実行される順序は、文字列の組み込みの順序でテストメソッド名をソートすることで決まります。

https://docs.python.jp/3/library/unittest.html#organizing-test-code

BokehでグラフをJavaScriptとして出力してWebページに埋め込んでみた

グラフをWebページに埋め込んで、インタラクティブなWebアプリっぽくしたい。でもJavaScriptわからない。そんなときにBokehのembed.componentsを使おう。

Bokehのバージョンは0.13.0を使っている。

bokeh.pydata.org

componentsに描画するグラフのFigureオブジェクトを渡すと、JavaScriptを記述したscriptタグとグラフ部分のdivタグがタプルとして返ってくる。

scirpt, div= components(plot)

手始めにBokehのQuickstartにある折れ線グラフを表示してみる。

bokeh.pydata.org

from bokeh.embed import components
from bokeh.plotting import figure
from flask import Flask, render_template

app = Flask(__name__)


def get_line_graph():
    # prepare some data
    x = [1, 2, 3, 4, 5]
    y = [6, 7, 2, 4, 5]

    # create a new plot with a title and axis labels
    p = figure(title="simple line example", x_axis_label='x', y_axis_label='y')

    # add a line renderer with legend and line thickness
    p.line(x, y, legend="Temp.", line_width=2)
    print(type(p))

    return p

@app.route('/')
def index():
    plot = get_line_graph()
    script, div = components(plot)
    return render_template('index.html', script=script, div=div)
<!DOCTYPE html>
<html>
<head>
    <title>Flask Bokeh Sample</title>
    <link rel="stylesheet" href="https://cdn.pydata.org/bokeh/release/bokeh-0.13.0.min.css" type="text/css" />
    <link rel="stylesheet" href="https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.13.0.min.css" type="text/css" />
    <link rel="stylesheet" href="https://cdn.pydata.org/bokeh/release/bokeh-tables-0.13.0.min.css" type="text/css">

    <script type="text/javascript" src="https://cdn.pydata.org/bokeh/release/bokeh-0.13.0.min.js"></script>
    <script type="text/javascript" src="https://cdn.pydata.org/bokeh/release/bokeh-api-0.13.0.min.js"></script>
    <script type="text/javascript" src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.13.0.min.js"></script>
    <script type="text/javascript" src="https://cdn.pydata.org/bokeh/release/bokeh-tables-0.13.0.min.js"></script>
</head>

<body>
    <h1>Hello, Bokeh</h1>
    {{ script|safe }}
    {{ div|safe }}
</body>

</html>

Jinja2safeフィルタを使うことで、Flaskから渡した文字列をHTMLとして埋め込むことができる。テンプレートで気をつけておきたいのはCSSJavaScriptCDNから読み込むときのバージョン。Bokehの最新バージョンに合わせておこう。ここでは0.13.0としている。

このStackOverFlowの回答を参考にしたが、動かなかったので試しにバージョンを最新にしたら動いた。

stackoverflow.com

さて、上記のコードを実行すると以下のようなWebページになる。動かせるし、ズームも保存もできる。

f:id:Nao_Y:20180716104814p:plain

複数のグラフを埋め込むときはcomponents()メソッドにFigureオブジェクトが入ったタプルを渡す。すると、divタグがタプルとして返ってくる。

以下、折れ線グラフに加えて散布図を埋め込んだ例。

from bokeh.embed import components
from bokeh.plotting import figure
from flask import Flask, render_template
import numpy as np

app = Flask(__name__)


def get_line_graph():
    # prepare some data
    x = [1, 2, 3, 4, 5]
    y = [6, 7, 2, 4, 5]

    # create a new plot with a title and axis labels
    p = figure(title="simple line example", x_axis_label='x', y_axis_label='y')

    # add a line renderer with legend and line thickness
    p.line(x, y, legend="Temp.", line_width=2)
    print(type(p))

    return p

def get_scatter():
    # prepare some data
    N = 4000
    x = np.random.random(size=N) * 100
    y = np.random.random(size=N) * 100
    radii = np.random.random(size=N) * 1.5
    colors = [
        "#%02x%02x%02x" % (int(r), int(g), 150) for r, g in zip(50 + 2 * x, 30 + 2 * y)
    ]

    TOOLS = "crosshair,pan,wheel_zoom,box_zoom,reset,box_select,lasso_select"

    # create a new plot with the tools above, and explicit ranges
    p = figure(tools=TOOLS, x_range=(0, 100), y_range=(0, 100))

    # add a circle renderer with vectorized colors and sizes
    p.circle(x, y, radius=radii, fill_color=colors, fill_alpha=0.6, line_color=None)

    return p


@app.route('/')
def index():
    line = get_line_graph()
    scatter = get_scatter()
    plots = (line, scatter)
    script, div = components(plots)
    return render_template('index.html', script=script, div=div)
<!DOCTYPE html>
<html>
<head>
    <title>Flask Bokeh Sample</title>
    <link rel="stylesheet" href="https://cdn.pydata.org/bokeh/release/bokeh-0.13.0.min.css" type="text/css" />
    <link rel="stylesheet" href="https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.13.0.min.css" type="text/css" />
    <link rel="stylesheet" href="https://cdn.pydata.org/bokeh/release/bokeh-tables-0.13.0.min.css" type="text/css">

    <script type="text/javascript" src="https://cdn.pydata.org/bokeh/release/bokeh-0.13.0.min.js"></script>
    <script type="text/javascript" src="https://cdn.pydata.org/bokeh/release/bokeh-api-0.13.0.min.js"></script>
    <script type="text/javascript" src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-0.13.0.min.js"></script>
    <script type="text/javascript" src="https://cdn.pydata.org/bokeh/release/bokeh-tables-0.13.0.min.js"></script>
</head>

<body>
    <h1>Hello, Bokeh</h1>
    {{ script|safe }}
    {{ div.0|safe }}
    {{ div.1|safe }}
</body>

</html>

f:id:Nao_Y:20180716105326p:plain

以上、BokehのグラフをJavaScriptにしてWebページに埋め込む方法。

BokehのWidgetsを使うとダッシュボードめいたやつとか作れそう。

bokeh.pydata.org

PyCon APAC 2018 Day 2

前回の更新からだいぶ時間が経ってしまった。

この一ヶ月の間にPyQブログとgihyo,jpでPyCon APAC 2018記事が公開された。 あとstapy#36BPLL#22でPyCon APAC 2018行ってきたよ報告LTをした。stapyは帰国からの直行だったので抜群の鮮度だった思い出。

blog.pyq.jp

gihyo.jp

PyQブログ書いてるし、gihyo.jpに寄稿してるので実はブログ書いていたのでは。

では、PyCon APAC 2018 カンファレンス2日目に聴いたトークのまとめ。

Opening Keynote

Talk

Detecting offensive messages using Deep Learning: A micro-service based approach

by Alizishaan Khatri, Machine Learning Engineer at Pivotus Ventures

資料: alizishaan-khatri-detecting-offensive-messages-using-deep-learning.pdf - Google ドライブ

What are you doing to control abusive content on your platform? Can your current solution tell the difference between "fking awesome" and "fking loser"? Can it detect racist and sexist content? In this talk, you will learn how to build a deep learning based solution and deploy it as a micro-service.

訳:

自分(自社)のプラットフォーム上での攻撃的な言動に対してどのように対応しているでしょうか。今の解決策はf**king awesomeとf**king looserの違いを認識できますか?差別的な表現や性的な表現の検知は?このトークでは、深層学習ベースの解決策とマイクロサービスとしてデプロイする方法をお伝えします。

Pumping up Python modules using Rust

by Vigneshwer Dhinakaran, Mozilla TechSpeaker

資料: vigneshwer-dhinakaran-pumping-python-modules-using-rust.pdf - Google ドライブ

Learn to build high performance and memory safe Python extensions using Rust. Discover this and more tips to boost up your Python application.

訳:

パフォーマンスでメモリセーフなPython拡張をRustで作る方法や、Pythonアプリケーションの性能を上げるtipsをお伝えします。

How to understand user behaviour using Multiple Linear Regression?

by Sarthak Deshwal, Associate Software Engineer at Expedia Group

資料: sarthak-deshwal-how-to-understand-user-behavior-using-multiple-linear-regression.pdf - Google ドライブ

Studying the user behaviour on app/website is one of the most hot topics among product companies. Multiple linear regression exactly helps you to find out the most important factors about your user and deliver the most important features to them. We will discuss the process, and the pros and cons of it.

訳:

アプリケーションやWebサイト上のユーザの振る舞いを知ることは今、企業の間で最も熱いトピックになっています。重回帰分析はユーザについての要素を見つけたり、ユーザーに最も重要な機能を提供するのにとても役立ちます。この発表では重回帰分析を使うプロセスと、その長所と短所をお伝えします。

Elements of Programming Interviews in Python

by Tsung-Hsien Lee, Staff Software Engineer at Toyota Research Institute

資料: tsung-hsien-lee-elements-of-programming-interviews-in-python.pdf - Google ドライブ

I will educate the audience on how to solve algorithmic problems commonly asked at interviews, emphasizing the aspects of Python that are the most useful. I will also describe how hiring decisions are made behind-the-scenes, based on my professional experiences at Facebook, Google, and Uber.

訳:

コーディング面接でよく出題されるアルゴリズム問題に役立つhow toをPythonを用いてお伝えします。また、FacebookGoogle, Uberで採用に関わった経験をベースに、採用するエンジニアの決定について話します。

Learn Guitar Via Python Programming

by Rishabh Shah, Associate Tech. Architect at Systango Technologies

資料: 無し

Ever wanted to play your favourite song on a guitar quickly even when you don't know how to play a guitar? Our Python based MIDI to guitar tabs Transcriber can help you do that. You just need to find your song in MIDI format (with .mid as file extension), and let our Python Transcriber do its magic!

訳:

好きな曲をギターで簡単に弾けるようになりたいけれど、その方法がわからないということがありませんか?MIDIからタブ譜へ変換する私達のPythonライブラリは、その悩みに役立ちます。必要なのは、曲のMIDIデータを変換ライブラリに通すことだけです。

ギター下手マンとしてはだいぶ気になっていた発表だったけど、実際に聴くと今ひとつ面白さが伝わってこなかったのが残念。

そう思っている時期が私にもありました(´・ω・`)

Closing Keynote

Run your ICO using Python

by Abhishek Pandey, Senior Developer at Tilde Trading

仮想通貨についての基調講演。

PyCon APAC 2018 Day 1

【2018.7.2 追記】トークの発表資料への追加

シンガポールで開催されているPyCon APAC 2018にやってきた。

海外のカンファレンスは初めて、シンガポールも初上陸。

PyCon APAC ツアー

初海外カンファレンスなうえに海外経験が少ないのでPyCon JPが企画するAPACツアーに参加しての旅だった。Pythonエンジニア仲間と行くと心強いし楽しい。

PyCon JP Blog: PyCon APAC 2018 in シンガポール のツアー参加者募集

シンガポール上陸当日

上陸した5月31日の天気は悪かった。昼を食べ終わる頃には雨になり、あまり観光できずに宿に戻ることとなった。

夜は現地で働いているonodaさん、APACのスポンサーのHDEのメンバーとHolland Villageにあるイタリア料理店で夕飯。その後は近くにあった屋台街めいたところで二次会となった。いっつもビール飲んでるな。

一日に2度もタワービールと対面するとは。

Day 1

PyCon APAC 2018の会場はNUS(National University of Singapore)。

f:id:Nao_Y:20180601165901j:plain

School of Computingの2階に上がると受付。

f:id:Nao_Y:20180601170019j:plain

ここでTシャツをゲット。乾きやすい生地になっているのが東南アジア感ある。

スポンサーブースではいつもお世話になっているPyCharmのJetBrainsのブースでPyCharmステッカーとハンドスピナーをもらった。

次の日に貼った。

カンファレンス後はAPACツアー勢、onodaさんと共に宿近くのショッピングモールに入っているフードコートで夕飯。鶏肉が入ったアジアンなスープを食べた。

その後はビールをキメた。飲んでばっかりでは。

Opening Keynote

Day1の基調講演はKatharine Jarmul。ドイツでデータサイエンスのコンサルをやっているkjamistanの創業者。

機械学習システムにおけるプライバシーというテーマの講演。

f:id:Nao_Y:20180601170931j:plain

Google Assistantが学習した音声情報はどうやって収集されたのかという疑問をきっかけに、データを収集、訓練データとして使うときにエンジニアが注意しなければならないことに繋がった。

  • 倫理的なデータセット

    • ユーザからデータを収集するという契約は明示的にする( Zen of PythonのExplicit is better than Implicit.になぞらえて)
  • 収集するデータについてユーザとの合意を得る

  • 匿名化(Anonymazation)や仮名化(Pseudonymization)を施す

なんとか聴き取れた範囲はこのくらい。英語力の無さ……。

最後にオーディエンス全員で「I am a Data Guardian」と声を合わせたりした。

f:id:Nao_Y:20180601172735j:plain

GDPRが話題になっているタイミングに合った話だったと思う(小並感

Talk

聞いたトークのサマリーを訳してみる。

全体のスケジュールはConference Schedule | PyCon APAC 2018

Introduce Syntax and History of Python from 2.4 to 3.6

by Manabu Terada, Founder and CEO at CMS Communications Inc.

資料: manabu-terada-introduce-syntax-and-history-of-python-from-2.4-to-3.6.pdf - Google ドライブ

CMSコミュケーションズの寺田さん。一般社団法人PyCon JPの理事でもある。

今回のPyCon APACツアーを企画していただいた。多謝。

The speaker will introduce the new syntax and functions between Python 2.4 and Python 3.6 in this talk. I will also compare the old style to the new style. You will learn the best practices for Python coding and how to perform refactoring your old Python code. You can look at the evolution of Python.

訳:

Python2.4から3.6の間に登場した新しい文法と関数の紹介と旧スタイルと新スタイルの比較。Pythonの進化の道筋をたどることで、古いPythonのコードをリファクタリングする際のベストプラクティスが学べるだろう。

Teaching Computers ABCs: A Quick Intro to Natural Language Processing

by Lory Nunez, Data Scientist/Data Engineer at J.P. Morgan

資料: loryfel-nunez-teaching-computers-abc.pdf - Google ドライブ

Natural Language Processing (NLP) is a component of Artificial Intelligence. Knowledge of NLP can make unstructured text data add tremendous value to any application. We will go over basic NLP tasks, techniques and tools. We will end with an NLP app built from open source libraries.

訳:

自然言語処理はAIを構成する要素である。自然言語処理によって、構造化されていないテキストデータがさまざまアプリケーションに大きな価値を与えられるようになる。ここでは、基本的な自然言語処理のテクニック、ツール、ユースケース、そしてオープンソースライブラリを使って自然言語処理アプリケーションを作る方法について発表する。

サンプルアプリ(Wikipediaの文章を用いて人名同士の類似度を測るアプリ・ツイートをクラス分けして災害時のトリアージを行うアプリ)が紹介されたが、アクセスできなくなっている(´・ω・`)

Dockerizing Django

by Ernst Haagsman, Product Marketing Manager for PyCharm at JetBrains

資料: Release pycon_apac_2018 · ErnstHaagsman/ecs-talk · GitHub

Docker helps make sure that the Django application you develop is exactly the same as the Django application you eventually deploy. In this talk, you will learn how to containerize a Django application, and use docker-compose to connect your Django application to your entire stack.

訳:

Dockerは、開発したDjangoアプリケーションと最終的にデプロイしたアプリケーションが同一であることを確認する際に役立つ。Djangoアプリケーションのコンテナ化と、アプリケーションをdocker-composeで管理しているサービスに接続する方法を紹介する。

このリポジトリやブログの記事(Using Docker Compose on Windows in PyCharm | PyCharm Blog )が元になっているようだ。

github.com

Build a Data-Driven Web App That Everyone Can Use

by Galuh Sahid, Data Engineer at Midtrans

資料: galuh-sahidbuilding-a-data-driven-web-app-that-everyone-can-use.pdf - Google ドライブ

You're a data scientist with a machine learning model that you want to show everyone. Do you give your users your Python scripts and tell them to run "python mycoolmodel.py"? Is there a better alternative? How about a web app? The speaker will show you how Flask can be the best fit pun intended for this case.

訳:

データサイエンティストのあなたは自分が作った機械学習モデルをみんなに使ってもらいたいと考えている。そんなときに「mycoolmodel.pyと名付けたファイルをpythonコマンドで実行してください」とみんなに伝えていないだろうか。他にもっと良い方法があるのではないか?Webアプリにするのはどうだろう。この発表ではFlaskを用いて機械学習モデルをWebアプリ化するベストな方法を紹介する。

レベル感として今回のトークの中で最も面白いと感じられた。Flaskは少しチュートリアルをやればWebアプリが書けるし、機械学習モデルもKaggleのタイタニックのやつを解説している記事に沿えば作れそう。復習したいので動画公開が待ち遠しい。

Closing Keynote

The Python QuantsのCEO、Dr. Yves Hilpisch。

いわゆるFinPyの分野の話なのだけど、そっち方面の知識がゼロなのでついていくことができなかった……。


Day 1の話はこれにて終わり。Day 2に続く。

BeautifulSoup4でコメントタグの中身を取得する方法に腹落ちするまでの話

同人音楽即売会M3の出展サークルリストから、サークル名やキーワードを抜こうとしている。何に使うかは未定だけど。

サークルリスト 2018年春 | M3 - 音系・メディアミックス同人即売会 を見てわかるように スペース, サークル名, 概要 となっている。サークルに関する情報はこれだけではなくて、ソースを見てみるとコメントとしてサークル名(カナ)やジャンルコード、キーワードが記述されている。キーワードも取りたくなってしまったのでひたすらググることになったという話。なお、BeautifulSoupに関しての説明はだいぶ省く。

<!--  アナベル    A01 女性ヴォーカル   ジョセイヴォーカル オリジナル オリジナル ポップス    ポップス    -->

コメントアウトされた情報はキーワードの数や並びが統一されていないのだけどそれは置いておく。これが実データのつらみか。

コメントタグの中身を取る方法

さて、beautifulsoup comment と検索するとトップにstackoverflowがヒットする。

stackoverflow.com

曰く、find_allメソッドのキーワード引数stringに以下のような無名関数を渡してやれば無名関数を渡してやればコメントが取れる。

lambda text: isinstance(text, Comment)

これで無事コメントタグの中身が取得できてめでたしめでたし、となるけどイマイチ腹落ちしなかったので find_allメソッドを調べた。

BeautifulSoupと無名関数については以下を参考にしたりした。

www.tomordonez.com

find_allメソッド

find_allメソッドは引数で指定した条件に一致するタグの要素を返す。

ここではpタグを取得してみよう。

from bs4 import BeautifulSoup as bs

markup = """
<body>
<h1>First Heading</h1>
<p>This is paragraph.</p>
<p>spam ham eggs</p>
<!-- This is comment -->
<!-- foo bar baz -->
</body>
"""

soup = bs(markup, 'html.parser')
paras = soup.find_all('p')
for p in paras:
    print(p)

実行結果は以下。

<p>This is paragraph.</p>
<p>spam ham eggs</p>

変数pTagオブジェクトで、get_text()メソッドでタグに囲まれた文字列を取得できる。

This is paragraph.
spam ham eggs

となる。

コメントタグの中身を取る方法: 再訪

先の例に当てはめて、コメントタグの中身を取得してみる。

from bs4 import BeautifulSoup as bs
from bs4 import Comment

markup = """
<body>
<h1>First Heading</h1>
<p>This is paragraph.</p>
<p>spam ham eggs</p>
<!-- This is comment -->
<!-- foo bar baz -->
</body>
"""

soup = bs(markup, 'html.parser')
comments = soup.find_all(string=lambda text: isinstance(text, Comment))
for c in comments:
    print(c)
 This is comment 
 foo bar baz 

CommentはBeatifulSoup4でコメントを表すクラス。

https://github.com/waylan/beautifulsoup/blob/master/bs4/element.py#L746L749

つまりCommentオブジェクトであるということが条件になっている。

次にstring引数について調べてみる。

https://www.crummy.com/software/BeautifulSoup/bs4/doc/#the-string-argument

With string you can search for strings instead of tags. As with name and the keyword arguments, you can pass in a string, a regular expression, a list, a function, or the value True.

正規表現やリスト、関数を渡すことができるとある。無名関数を渡しているのはそのためだった。

変数textがどのような値なのか気になって仕方ないので、普通の関数にして確認してみる。

from bs4 import BeautifulSoup as bs
from bs4 import Comment

def is_comment(text):
    print(text, type(text)
    return isinstance(text, Comment)

markup = """
<body>
<h1>First Heading</h1>
<p>This is paragraph.</p>
<p>spam ham eggs</p>
<!-- This is comment -->
<!-- foo bar baz -->
</body>
"""

soup = bs(markup, 'html.parser')
comments = soup.find_all(string=is_comment)
 <class 'bs4.element.NavigableString'>
 <class 'bs4.element.NavigableString'>
First Heading <class 'bs4.element.NavigableString'>
 <class 'bs4.element.NavigableString'>
This is paragraph. <class 'bs4.element.NavigableString'>
 <class 'bs4.element.NavigableString'>
spam ham eggs <class 'bs4.element.NavigableString'>
 <class 'bs4.element.NavigableString'>
This is comment <class 'bs4.element.Comment'>
 <class 'bs4.element.NavigableString'>
foo bar baz <class 'bs4.element.Comment'>
 <class 'bs4.element.NavigableString'>
 <class 'bs4.element.NavigableString'>

見づらいうえに改行も認識されてる(´・ω・`)

h1タグやpタグはNavigableString オブジェクト、コメントタグはCommentオブジェクトということがわかった。

ついでにパースしたHTMLの各行に対して処理が行われていることも実感できた。

まとめると、

  • find_allメソッドのstring引数には関数を渡すことができる

  • パースしたHTMLの各行が、タグの場合はNavigableString、コメントであればCommentオブジェクトになる

  • string関数に各行がCommentオブジェクトであることを判定する関数を渡すことで、コメントだけを取得できる

ひとまず腹落ちしたので心置きなくHTMLからコメントが取得できるようになった。

数や順番が統一されていないサークル情報をどう使うかを考えないとならないけど、それは別の話になる。

リズと青い鳥 - 感想

連休中にリズと青い鳥を3回ほど観た。

liz-bluebird.com

タイトルこそ違うが本作は「響け!ユーフォニアム」シリーズのスピンオフ。登場人物の学年が一つ上がった後の物語。主人公はみぞれと希美。

監督は山田尚子。キャラクターデザインは西屋太志、音楽は牛尾憲輔という「聲の形」を作り上げた3人。

あらすじ

Filmarks - リズと青い鳥より引用

あの子は青い鳥。広い空を自由に飛びまわることがあの子にとっての幸せ。だけど、私はひとり置いていかれるのが怖くて、あの子を鳥籠に閉じ込め、何も気づいていないふりをした。 北宇治高等学校吹奏楽部でオーボエを担当する鎧塚みぞれと、フルートを担当する傘木希美。高校三年生、二人の最後のコンクール。その自由曲に選ばれた「リズと青い鳥」にはオーボエとフルートが掛け合うソロがあった。「なんだかこの曲、わたしたちみたい」 屈託もなくそう言ってソロを嬉しそうに吹く希美と、希美と過ごす日々に幸せを感じつつも終わりが近づくことを恐れるみぞれ。「親友」のはずの二人。しかし、オーボエとフルートのソロは上手くかみ合わず、距離を感じさせるものだった。

感想

みぞれと希美の関係性

これまでの「ユーフォ」はコンクールでの演奏シーン、府大会での演奏をめぐるオーディションやあすか先輩が吹奏楽を続ける理由など画にも物語にも動きがあり、まさしく吹奏楽部を描いた物語だった。これに対して本作は、みぞれと希美の関係性を落ち着いたトーンで描く「静」の物語になっている。

みぞれにとっての希美について印象に残っているのが、パンフレットでの対談で山田尚子監督が語っている「みぞれにとって希美の一言はいつも最終回」ということ。階段に腰掛けて、校門を眺めながら希美を待つという冒頭のシーンでのみぞれから「いつも最終回」という気持ちが伝わってくる。

パンフレット p.6

山田: そうですね。冒頭で見せた二人の関係性も、天真爛漫な希美と、希美が大好きで希美しか見えていないみぞれにとって、希美の一言がどれだけいつも「最終回」なのかが伝わればいいと思います。みぞれは「次がない」と思って毎日生きているので……。

1年生の頃に突然、希美が吹奏楽部から去ったようにいつまた離れていくかわからないという不安をいつもみぞれは抱えている。みぞれの希美に対する想いは、眩しい朝の青空を背景に青い鳥の羽を空にかざす希美を見上げるという冒頭のシーンから伝わってくる。開始数分でこれだけの情報量をほとんど台詞も無く描く山田尚子監督の力量たるや……。

一方、希美はみぞれをいつも後ろにいて自分を見てくれている存在と捉えているように思う。それは二人が歩く画から伝わってくる。いつも、みぞれは自分だけを見ていてくれていると思っているからこそ、プールに一緒に連れていきたい子がいるとみぞれから告げられたとき、みぞれと梨々花のオーボエが聴こえてきたときに表情を陰らせる。プールの話をするときに、希美が表情を陰らせたと同時に誰かが希美とみぞれの前を横切って一瞬、希美の表情がわからなくなり次の瞬間には何もなかったような顔をする、という演出は本当にはっとした。エモい。

みぞれと希美は好き合っているけど、互いに方向性が異なっているから羨むところも違ってくる。みぞれは、希美が後輩たちを仲良く喋っているのを寂しそうに見ている。それに対して希美は「同じ音大を目指す」といえば同じくらい上手いように見られると思う。終盤のあるシーンで二人の好きの方向性がはっきりと描かれる。この後、みぞれは希美が好かれたいと思っていることに気付くことができるのか。ここのシーンはクライマックスだけあって3度観て、3度とも涙が出そうになった。

パンフレット p.13

山田: みぞれは多分、 全ては理解してないかもしれないけれど、希美が言って欲しかった言葉が何だったのかを少しずつ理解していくのではないかと思います。

本作は「青い鳥が自分が青い鳥であることに、リズは自分がリズであることに気付く」物語と考えている。けれどみぞれと希美は、リズと青い鳥のように離れ離れになるわけではない。気付けたことで、きっとこれからの二人は少しずつ互いの在り方を見つめていけるのだと思う。初めて、希美が歩きながらみぞれを振り返ったラストシーンのように。

キャラクターデザイン

「ユーフォ」の池田晶子から「聲の形」でキャラクターデザインを担当した西屋太志に変更になった。「ユーフォ」と比べると線が細くて頭身が少し高いデザインで、いわゆる「萌え」の感じが薄まって繊細になっている。これによって「静」の雰囲気がより高まっている。池田晶子デザインだったらこの落ち着いた空気感はまた違ったものになったと思う。 余談ではあるけども、久美子がだいぶ可愛いと思ってる。これまでの主人公とは思えないくらい出番ないけど。

まとめ

Twitterではみぞれと希美の恋のような関係性(パンフレット pp.6~7で監督が愛よりも恋と語っている)を描いた百合的な話として褒めるツイートをしていた記憶があるけど、演出、脚本、キャラクターデザイン、音楽など全てが繊細でアニメーション作品としての完成度も本当に高い。まだ劇場で観れると思うので、全力でオススメしたい。本気で劇場で何度も観たい。無限に観たい。