カカドゥ開発者ブログ

株式会社カカドゥのエンジニアブログです。Python, Django, SEO成分が多めです。

エンジニア必見!SEOのための構造化データ記述にこれからはJSON-LDがおすすめ

f:id:masutaro:20150925072949p:plain

こんにちは!カカドゥの増田です。

SEO対策をしているウェブサービスのエンジニアなら、SEO対策としてパンくずや、レビュー(口コミ)の評点に、MicrodataやRDFaを使って構造化データ(メタデータ)をつける作業を依頼されたことのある人は、少なくないんじゃないでしょうか。

構造化データを記述する方法としては、従来からMicrodataやRDFaなどが使われていましたが、2014年頃からJSON-LDというのも使えるようになっています。

僕自身が最近、新規サイトの構築にあたって初めてJSON-LDを使い、JSON-LDは従来の方法のデメリットを解決するとても素晴らしい規格だと思ったのですが、使えるようになって1年経過した現在でも、知っている人がまだそれほどいない気がするので、啓蒙とレコメンドとしてこのエントリを書こうと思った次第です。

従来のMicrodataやRDFaのデメリット

MicrodataやRDFaを使って、マークアップ内にメタデータを組み込む従来の方法には以下のようなデメリットがあったと思います。

  • HTML/CSSの観点からすると、記述されたメタデータは不要なものでHTMLの可読性を損ねる
  • HTMLにメタデータが混在して可読性が低いため、記述を間違えやすい
  • エンジニアが良かれと思ってマークアップの人に頼らずHTMLを編集したら、本番サイトのレイアウトが崩れていた・・
  • マークアップ担当がうっかりメタデータ消しちゃって数ヶ月経過していた・・

JSON-LDを使うことで"コンテンツ""メタデータ"を分離できるため、上記のデメリットが解消されます。

実装例(PHP)

以下にPHPでの実装例を記載します。実際はフレームワークを使ってテンプレートに出力するでしょうから、まんまこのとおりにはならないと思いますが、いかに簡単な記述で、構造化データを付与することができるか感じていただけるかと思います。

プログラムコード

パンくず(Breadcrumbs)の構造化データを作成する場合の実装例です。

<script type="application/ld+json">
<?php
$items = [];
// 以下の$id_namesの値は、実際はプログラムで自動生成する
$id_names = [
    [ "@id" => "http://example.com/", "name" => "ホーム" ],
    [ "@id" => "http://example.com/category/1", "name" => "グルメ" ],
    [ "@id" => "http://example.com/article/32", "name" => "おいしいカレーの作り方" ]
];

foreach($id_names as $key => $value) {
    array_push($items, [
        "@type" => "ListItem",
        "position" => $key+1,
        "item" => $value
    ]);
}

$json_ld = [
    "@context" => "http://schema.org",
    "@type"    => "BreadcrumbList",
    "itemListElement" => $items
];
print(json_encode($json_ld));
?>
</script>

※本例では$id_namesの値をハードコードで書いてますが、実際はプログラムで作って渡してやることになるでしょう

出力結果

以下の内容がheadかbodyの任意の位置に記述されればOKです。

<script type="application/ld+json">
{"@context":"http:\/\/schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"http:\/\/example.com\/","name":"\u30db\u30fc\u30e0"}},{"@type":"ListItem","position":2,"item":{"@id":"http:\/\/example.com\/category\/1","name":"\u30b0\u30eb\u30e1"}},{"@type":"ListItem","position":3,"item":{"@id":"http:\/\/exampl.ecom\/article\/32","name":"\u304a\u3044\u3057\u3044\u30ab\u30ec\u30fc\u306e\u4f5c\u308a\u65b9"}}]}</script>

検証(バリデーション)

吐き出されたJSON-LDを念のためGoogleの構造化テストツールでチェックしましたが、下図のとおり問題ない結果でした。

f:id:masutaro:20150924174511p:plain

おわりに

いかがでしょうか?今回はパンくずを例にしましたが、他の種類の構造化データもすべてJSON-LDで記述することができます。HTMLの中で、コンテンツとメタデータを切り離して記述することができて大変便利なので、今後、構造化データを記述する機会にはぜひご利用いただきたいです。

JSON-LDがそれほどに普及しているように思えない原因は2つあると思っていて、ひとつは既存サイトでいま正常に動いているものをあえてわざわざ変更する必要がないからです。

もうひとつは、日常的にSEOの最新ニュース記事を読んでいる人(ディレクター)と、JSON-LDのメリットを享受する人(エンジニア)が異なっていて、本来届いて欲しい人に情報がまだ届ききっていないからじゃないかと思いました。なので、今後もエンジニアが知っておくと良さそうなSEO情報があれば記事化したいと思います!

この記事がいいなと思った方はフォローミー!(^_^)

DjangoでCelery+Redisのジョブキューを構築したときにハマったポイント

こんにちはカカドゥの増田です。

Djangoでジョブキューを使おうと思って、Celery(セロリ)+Redisで構築しようとしたら結構ハマったので記録しておきます。

環境

  バージョン
Ubuntu 15.04
Python 2.7.10
Celery 3.1.18
Redis 2.8.19

ファイルレイアウト

- project/
  - project/__init__.py
  - project/settings.py
  - project/urls.py
  - project/celery.py
- manage.py

発生した問題

  • アプリケーション側からは問題なくキューが入ったように見えるのに、Redisにキューが入らない
  • Redisを試す前にRabbitMQを試していたことが問題を複雑にしていた

原因

公式ドキュメントの説明に従って、project/__init__.pyを作成していなかったから。

from __future__ import absolute_import

# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app

解説

Djangoのproject/__init__.pyの中でfrom celery import appしなかった場合、fallbackの仕組みとしてBROKER_URLにamqp://guest:**@localhost:5672//を設定した状態として動作します。

そのあたりの実装は、以下のあたりを読むとわかります。

celery/_state.py at 43ef0321058f318310cb0abd994b82047a25751e · celery/celery · GitHub

def _get_current_app():
    if default_app is None:
        #: creates the global fallback app instance.
        from celery.app import Celery
        set_default_app(Celery(
            'default', fixups=[], set_as_current=False,
            loader=os.environ.get('CELERY_LOADER') or 'default',
        ))
    return _tls.current_app or default_app

celery/base.py at 43ef0321058f318310cb0abd994b82047a25751e · celery/celery · GitHub

    def connection(self, hostname=None, userid=None, password=None,
                   virtual_host=None, port=None, ssl=None,
                   connect_timeout=None, transport=None,
                   transport_options=None, heartbeat=None,
                   login_method=None, failover_strategy=None, **kwargs):
        conf = self.conf
        return self.amqp.Connection(
            hostname or conf.BROKER_URL,
            userid or conf.BROKER_USER,
            password or conf.BROKER_PASSWORD,
            virtual_host or conf.BROKER_VHOST,
            port or conf.BROKER_PORT,
            transport=transport or conf.BROKER_TRANSPORT,
            ssl=self.either('BROKER_USE_SSL', ssl),
            heartbeat=heartbeat,
            login_method=login_method or conf.BROKER_LOGIN_METHOD,
            failover_strategy=(
                failover_strategy or conf.BROKER_FAILOVER_STRATEGY
            ),
            transport_options=dict(
                conf.BROKER_TRANSPORT_OPTIONS, **transport_options or {}
            ),
            connect_timeout=self.either(
                'BROKER_CONNECTION_TIMEOUT', connect_timeout
            ),
        )

self.amqp.Connectionは内々でkombu.Connection()を呼び出しているのですが、Pythonインタプリタで引数を何も渡さずにConnectionを呼び出してやると、fallbackが働いたときの挙動がよく分かります。

>>> from kombu import Connection
>>> Connection()
<Connection: amqp://guest:**@localhost:5672// at 0x7f55f3b56810>

補足

今回僕の場合、Redisの前にRabbitMQを試していたため、RabbitMQデーモンが起動した状態になっており、それが災いして原因特定を困難にしました。

project/celery.pyを作らなかったためにfallback(縮退)モードとしてCeleryが動き、アプリケーションから入れたキューはRedisではなくRabbitMQに入ってしまったのでした。

RabbitMQを停止しておけば以下のようにキューを入れる段階でInternal Server Errorが出るのでもう少し調査が捗ったかもしれません。

f:id:masutaro:20150922092314p:plain

Supervisorで管理される子プロセスの環境変数について

こんにちはカカドゥの増田です。

Supervisorで管理される子プロセスの環境変数は、supervisordを起動したshellの環境変数が継承されるとのこと。

Subprocess Environment Subprocesses will inherit the environment of the shell used to start the supervisord program.

http://supervisord.org/subprocess.html#subprocess-environment

なので、/etc/profile.d配下にshellスクリプトを置いて本番と開発環境の値が変わるようにしている、うちの場合だとSupervisorだからと言って特別何かをする必要はなかった。

まぁ、環境変数が子プロセスに継承されるのは、通常のshell操作のときと同じなんですけど。

gunicornでgraceful restart(reload application)する方法

こんにちはカカドゥの増田です。

現在開発しているサービスは、Python + Django + Gunicornで開発しています。

これまでの経験では、Apache httpdにmod_perlやmod_phpで動かす運用をしていたため、業務レベルのサービスをスタンドアロンなアプリケーションサーバ単体で動かすのは実は初めてだったりします。

そこで、Gunicornでは、デプロイするときにApacheで言うところのgraceful restartをどうやったらできるのかなと思って調べたところ、

kill -HUP masterpid

してやればいいということが分かりました。日本語で説明している資料がなかったので記録している次第です。

公式ドキュメントには、以下のように説明されています。

How do I reload my application in Gunicorn?

You can gracefully reload by sending HUP signal to gunicorn:

$ kill -HUP masterpid

FAQ — Gunicorn 19.3.0 documentation

動作の仕組みとしては、gunicorn/arbiter.py at e0287108720b19ac520485349091df07d5c2a7d2 · benoitc/gunicorn · GitHubでコードを読むことができますが、マスタープロセスがHUPシグナルを受け取ると、新しくworkerを生成して、古いworkerをgracefully shutdownするとあります。

def handle_hup(self):
        """\
        HUP handling.
        - Reload configuration
        - Start the new worker processes with a new configuration
        - Gracefully shutdown the old worker processes
        """
        self.log.info("Hang up: %s", self.master_name)
        self.reload()

実際、kill -HUPをやってみるとworkerのpidが変わったことが確認できました。(もちろん、アプリケーションの動作が最新に切り替わったことも確認)

(env)root@host:~# pgrep gunicorn
14799  <-- master
14949  <-- worker
(env)root@host:~# kill -HUP 14799
(env)root@host:~# pgrep gunicorn
14799  <-- master
15012  <-- worker

これでサービスを落とすことなくデプロイする運用が見えました。一安心ですね。

DjangoのModel操作をしたときに内部で発行されたSQLログを確認する方法

こんにちは。カカドゥの増田です。

DjangoのORM(ORマッパー)はとても便利ですね。僕は以下の様な点でDjangoのORMがとても気に入ってます。

  • メソッド名が直感的(filter, exclude, annotate, aggregate...)
  • ドキュメントをよく読めば習得コストもそれほど高くない
  • ビジネスロジックの中に少ないコードでModel操作が書ける

そんな便利なORMですが、内部で実行されたSQLが効率的なものになっているか気になることがあります。

Djangoの場合は、以下のようにしてやれば内部で実行されたSQLを確認することができます。

from django.db import connection

print connection.queries

ログ処理の実装は以下のようになっており、実行されたSQLと、実行にかかった時間をミリ秒レベルで確認することができます。

self.db.queries_log.append({
    'sql': sql,
    'time': "%.3f" % duration,
})

# django.db.backends.utilsより抜粋

なお、connection.queriesは内的にはcollections.dequeによるリストのようデータ構造になっており、runserverのときは1リクエスト処理するときに実行されたSQLが、shellのときはshell起動中に実行されたSQLが、最大で9000件記録されています。永続化されてどこかに残っているわけではないです。

また、DjangoのModel操作は遅延評価されるため、views.pyの中でデバッグ文的に

print connection.queries

と書いてても、評価されていない(SQL実行されていない)DBアクセスは当然queriesに記録されていませんので、ご注意ください。(自分がそれでハマった)

手動ビルドしたPythonでDjangoのrunserverしたらNo module named _sqlite3というエラーが出た

こんにちはカカドゥの増田です。

手動でビルドしたPython(2.7.10)で、Djangoのrunserverをしようとしたとき、以下のようなエラーに遭遇しました。

raise ImproperlyConfigured("Error loading either pysqlite2 or sqlite3 modules (tried in that order): %s" % exc)
django.core.exceptions.ImproperlyConfigured: Error loading either pysqlite2 or sqlite3 modules (tried in that order): No module named _sqlite3

_sqlite3というモジュールが見つからなくて発生したらしい。

makeしたときのログを確認したところ_sqlite3がビルドできなかった旨のメッセージが出てました。

Python build finished, but the necessary bits to build these modules were not found:
_bsddb             _curses            _curses_panel   
_sqlite3           _tkinter           bsddb185        
bz2                dbm                dl              
gdbm               imageop            sunaudiodev     
To find the necessary bits, look in setup.py in detect_modules() for the module's name.

Ubuntuだとlibsqlite3-devというライブラリをインストールしてからビルドし直せば解決しました。

$ sudo apt-get install libsqlite3-dev

PythonをビルドするときSSL/TLSライブラリが入ってないとensurepipがfailする

こんにちはカカドゥの増田です。

Python2.7.9以上からpipが同梱されるようになっているそうで、configureするときに以下のようにしてやるとpipが最初から入ります。

$ ./configure --prefix=/usr/local --with-ensurepip=install

ですが、SSL/TLSのライブラリが入っていないと以下のようなエラーを吐いてインストールされません。

Ignoring ensurepip failure: pip 6.1.1 requires SSL/TLS

Ubuntuだと、libssl-devというライブラリをインストールしてからビルドすればOKです。

$ sudo apt-get install libssl-dev