ikeikeikeike's unk blog.

http://github-awards.com/users/ikeikeikeike

Railsでelasticsearchとsearchkick(retire)とacts-as-taggable-onでラクをしてfacetsした話

タグを付けてfacetsするところまで。コードは全く書きません。

便利でいいですね。

gem "searchkick"  # full text search
gem 'acts-as-taggable-on'  # For tags

インストール後migration実行

$ rake acts_as_taggable_on_engine:install:migrations
$ rake db:migrate

モデル定義

class Product < ActiveRecord::Base
  searchkick  # For full text search powerd by elasticsearch.
  acts_as_taggable  # Alias for acts_as_taggable_on :tags

  # for searchkick reindex data.
  #
  def search_data
    {
      hoge: hoge,
      homu: homu,
      home: home,
      tags: tag_list  # gen by acts_as_taggable_on.
    }
  end

end

Productにデータをテキトーに入れて, elasticsearchのindexを生成し直します(elasticsearchは事前に立ち上げておいてください)

Product.reindex

facetsしてみます

4.0.2@2.1.0 (main)> Product.search('*', limit: 0, fields: [:tags], facets: {tags: {limit: 5}}).facets
  Search (45.5ms)  {"query":{"match_all":{}},"size":0,"from":0,"facets":{"tags":{"terms":{"field":"tags","size":155}}},"fields":[]}
=> {"tags"=>
  {"_type"=>"terms",
   "missing"=>0,
   "total"=>3550,
   "other"=>2577,
   "terms"=>
    [{"term"=>"アメリカ", "count"=>269},
     {"term"=>"イギリス", "count"=>215},
     {"term"=>"日本", "count"=>182},
     {"term"=>"インド", "count"=>180},
     {"term"=>"ブラジル", "count"=>127}]}}

簡単すぎわらえない

試しに ansible(アンシボー) したら簡単だった

Playbookは全て自作したのですがそれでも初学から1日で終わった。なるほど覚えること少なくてハッヤイ

(同じことやるのに大体ですがChefだと3日かかった記憶があります)

これだけ作るのが早いくて、これくらいのレベルのplaybookだと汎用性考えなくても良いかもしれないと思いましたー。

良かったこと

  • 一応会社でPythonistaしてますが。Ansibleにはそんなの必要じゃなかった

面倒くさいなって思ったこと

  • when, failed_when: 関数とかイレサシテクレヨ-、registerするのメンドクサイヨ-

作ったもの

実行ファイルはこんな感じにしています。 (hosts, ssh_config等は設定済み)

---

- name: 何か色々インストールして設定ファイル書き換えるよー

  vars_files:
    - vars/main.yml

  hosts: vagrant

  sudo: yes

  roles:
    - common
    - monit
    - varnish
    - nginx
    - postgresql
    - elasticsearch
    - addusers
    - rbenv

以前作ったもの

参考

ansibleに関するikeikeikeikeのはてなブックマーク

追記

ファイルの上書きをやめて lineinfile を使うようにした

use lineinfile module · 333b288 · ikeikeikeike/ansible-playbooks · GitHub

Codeigniterで保存されているパスワードを復号化してDjangoに移行してみよう

いきなりですが

こちらのサイトによると最近流行りのPHPフレームワークTOP2は Laravel, Phalcon なんだとか

http://www.sitepoint.com/best-php-frameworks-2014/

多分昨今のPHP界隈は Codeigniter から Laravel, Phalcon あたりに旧システムを移植する話がどんどん増えてきてる頃合い(良く分かってないです)

せっかく移行するなら言語を変えたいと思ってる人もいてもいいはずですよね。

んでCodeigniterのパスワードは復号化できるのでPythonで復号化処理を書いちゃえばみなさんの重い腰もあがりやすいはず (Djangoは復号化出来ない, パスワードチェックはhash値の比較だったかな)

もくじ

  • Codeigniter復号化処理
  • Django inspectdb Command
  • South パッケージ
  • Django Custom User

いろいろ端折っているのでimportエラー他はそれなりに出ますよ

Codeigniter復号化処理

まず下記のような復号化処理を定義しておきます

# -*- coding: utf-8 -*-
import base64
import logging
import hashlib
import itertools

import mcrypt  #  XXX: pip install python-mcrypt


logger = logging.getLogger(__name__)


class DecodeError(Exception):
    """ CrypterDecode error """


class Codeigniter(object):

    def __init__(self, bit='rijndael-256', mode='cbc'):
        # AES:Rijndael 256bit, CBC modeを使用
        self.crypter = mcrypt.MCRYPT(bit, mode)

    def encode(self):
        raise NotImplementedError("Not implemented error: Codeigniter.encode")

    def decode(self, string, key='my-secret-key'):
        """ Codeigniterで保存されているパスワードの復号化 """
        data = ''
        hashkey = hashlib.md5(key).hexdigest()

        try:
            ## Codeigniter独自(っぽい)の復号化前処理
            # data_str: 元のバイト列を返す
            # key_str:  hash文字数が超えたらcycleして初めからhash文字を返す
            iters = zip(base64.b64decode(string), itertools.cycle(hashlib.sha1(hashkey).hexdigest()))
            for data_str, key_str in iters:
                temp = ord(data_str) - ord(key_str)
                if temp < 0:
                    temp += 256
                data += chr(temp)
        except TypeError as err:
            logger.exception(err)
            raise DecodeError(str(err))

        ## 以下mcrypt使用時のお約束
        init_vect = data[0:self.crypter.get_iv_size()]
        data = data[self.crypter.get_iv_size():]

        try:
            self.crypter.init(hashkey.ljust(24, '\0'), init_vect)
        except ValueError as err:
            logger.exception(err)
            raise DecodeError(str(err))

        return self.crypter.decrypt(data)[0:32].rstrip('\0')
実行方法
>>> codeigniter = Codeigniter()
>>> codeigniter.decode(encrypted_password)
'output raw password'
Django inspectdb Command

次に app の作成, Database から Django のモデル定義を生成するコマンドを実行

http://docs.djangoproject.jp/en/latest/ref/django-admin.html#inspectdb

$ python manage.py startapp egg
$ python manage.py inspectdb > egg/models.py

生成後のディレクトリ構成は下記のようになっている

$ tree
.
├── development.db
├── egg
│   ├── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── ham
│   ├── __init__.py
│   ├── __init__.pyc
│   ├── settings.py
│   ├── settings.pyc
│   ├── urls.py
│   └── wsgi.py
└── manage.py
South パッケージ

migration ツールの South を実行して Codeigniter のDatabase定義を修正する準備

$ python manage.py schemamigration [django_app] --initial

これで、定義は終了したのでmodelsを修正すれば、south がDatabaseの改修をしてくれます。

$ python manage.py schemamigration [django_app] --auto
$ python manage.py migrate

Django Custom User

最後に先ほど定義した復号化処理を Django user model へ差し込めば移行処理が完了

# -*- coding: utf-8 -*-
from django.db import models
from django.utils import timezone
from django.contrib.auth import hashers
from django.utils.encoding import python_2_unicode_compatible

from . import crypter


@python_2_unicode_compatible
class BaseUser(models.Model):
    """ 各UserModelの親クラス

    passwordのアルゴリズムを旧フレームワークから移植している
    """
    encrypted_password = models.CharField(u'パスワード', max_length=128L)
    last_login = models.DateTimeField(u'ログイン日時', default=timezone.now)

    is_active = True

    REQUIRED_FIELDS = []

    class Meta:
        abstract = True

    def get_username(self):
        """ AccountのID識別子を返却 """
        return getattr(self, self.USERNAME_FIELD)

    def __str__(self):
        return self.get_username()

    def natural_key(self):
        return (self.get_username(), )

    def is_anonymous(self):
        return False

    def is_authenticated(self):
        return True

    def set_password(self, raw_password):
        """ パスワードを設定する """
        self.encrypted_password = hashers.make_password(raw_password)

    def check_password(self, raw_password):
        """ raw passwordのチェック

        旧passwordが正しい場合は即Django暗号化方式のパスワードへ更新する
        """

        def setter(raw_password):
            """ password更新 """
            self.set_password(raw_password)
            self.save(update_fields=["encrypted_password"])

        if self.has_usable_password():
            # Django password
            return hashers.check_password(raw_password, self.encrypted_password, setter)
        else:
            # Old framework password
            try:
                result = crypter.codeigniter.decode(self.encrypted_password) == raw_password
            except crypter.DecodeError:
                return False

            if result:
                setter(raw_password)

            return result

    def set_unusable_password(self):
        """ 有効なhash値にならないパスワードをセット """
        self.encrypted_password = hashers.make_password(None)

    def has_usable_password(self):
        """ 有効なパスワードか """
        return hashers.is_password_usable(self.encrypted_password)

    def get_full_name(self):
        raise NotImplementedError()

    def get_short_name(self):
        raise NotImplementedError()

    def _get_encrypted_password(self):
        return self.encrypted_password

    def _set_encrypted_password(self, value):
        self.encrypted_password = value

    password = property(_get_encrypted_password, _set_encrypted_password)
実行方法

テキトーにrunserverしてログインしてみてください

$ python manage.py runserver

うん、これでDjangoに移行できるかな?

稚拙なことしか書いてないですが、是非トライしてみてください。

Pythonの例外情報(Traceback)をLogに記録するには Logger.exception が便利って話

極たまに下記のようなコードを見ます。例外情報が欲しいのは分かりますがlogging.Loggerクラスには専用のメソッドがあります。

import traceback

try:
    dosomething()
except (KeyError, ValueError):
    logger.error(traceback.format_exc())

Logエラーレベルが ERROR のメッセージで例外情報を残したいなら下記にすべきです。

try:
    dosomething()
except (KeyError, ValueError) as err:
    logger.exception('Error dosomething: %s', err)

ドキュメント

Macの濁点問題を解決するPython unicodedataモジュール

日本語のURLを使いたいんです

最近Scrapyで採取したゴミMongoDB※1(笑) に入れておき、その後TinkererでブログにしてS3へアップロードする一連のスクリプトを書いて、ゴミサイトを何個も作りまくってるんですけど

Tinkererで生成したtag list, tag cloudにUnicode文字も含めるカスタマイズをしたのですが、その後頻繁にS3で404が発生する現象が起きていたので調べていたのです、そしたらなにやらUTF-8関連のきな臭い問題のようでした

UTF-8-MAC

使用している環境がMacなものでUTF-8-MAC問題が発生していました。 (この問題についてはこちらを参照)

説明は省きますが修正するにはUTF-8-MACからUTF-8に変換すれば良いみたいです。普通この際iconvを使うのが一般的思いますが今回は一連のゴミ採集スクリプトの言語がPythonなのでPythonでやりたいと思います

unicodedataモジュール

unicodedataモジュールに変換処理のめんどうをみてもらうことにする※2

例えばゴミって単語をUTF-8-MACからUTF-8へ変換する場合

import unicodedata
unicodedata.normalize('NFC', u'ゴミ')

この処理を施した文字列をboto.s3に直接渡すことで問題なくS3上でゴミタグを表示できた

(∩´∀`)∩ワーイ

脚注

※1 MongoDBScrapyは相性が悪い。 http://blog.scrapinghub.com/2013/05/13/mongo-bad-for-scraped-data/ (というか全般的なMongoDBへの評価ですね)

※2 unicodedataモジュールはUnicodeData.txtの対応表を元に変換処理をしてくれるようだ、Versionは5.2.0(UnicodeData File Format 5.2.0)。変換処理について知りたい人はこちらが参考になるかも

Southでカラムのdefault値を変更したいのけど--auto が効かない時

カラムのdefaultを変更したいのけど --auto オプションが効かないの

解1

Djangoの場合は必要なし, 実際にDBへは反映されていない

解2

マニュアルでschemamigration実行後、編集

$ python manage.py schemamigration table1 change_default_value_to_someone --add-field Egg.someone --add-field Spam.someone

その後 db.alter_column などで修正

Python Enum

Enumすら思い出せない忘れんぼさんなんで PythonEnum を弄ってみました

列挙型定義

from enum import Enum

class Colors(Enum):
    RED = '1'
    BLUE = 2
    GREEN = 'green'

print(Colors)  # <enum 'Colors'>
動的に
>>> Enum('Colors', (('RED', '1'), ('BLUE', 2), ('GREEN', 'green')))
<enum 'Colors'>
Enum型の一覧
>>> Colors.__members__
mappingproxy(OrderedDict([('RED', <Colors.RED: '1'>), ('BLUE', <Colors.BLUE: 2>), ('GREEN', <Colors.GREEN: 'green'>)]))
名前、値を得る
>>> Colors.RED
<Colors.RED: '1'>
>>> Colors['RED']
<Colors.RED: '1'>
>>> Colors.RED.name
'RED'
>>> Colors.RED.value
'1'
iterを当てるとgeneratorが返る
>>> iter(Colors)
<generator object <genexpr> at 0x108b12120>
>>> 
>>> (Colors.__members__[name] for name in Colors.__members__)
<generator object <genexpr> at 0x108b12438>
>>>
>>> for color in Colors:
...    print(repr(color))
...
<Colors.RED: 1>
<Colors.BLUE: 2>
<Colors.GREEN: 3>
定義後はClass, Attributeは削除できない
>>> del Colors.RED
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: RED
>>> 
>>> del Colors.RED.name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/python/lib/python3.4/enum.py", line 29, in __delete__
    raise AttributeError("can't delete attribute")
AttributeError: can`t delete attribute
>>>
>>> del Colors.RED.value
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/python/lib/python3.4/enum.py", line 29, in __delete__
    raise AttributeError("can't delete attribute")
AttributeError: can`t delete attribute
辞書のキーとして使う、Enumの比較

__hash__が定義されてたので辞書のキーでも使えた

>>> red1 = Colors.RED
>>> dct = {red1: 10}
>>> dct[red1]
10
>>> red2 = Colors.RED
>>> dct[red2]
10
>>> red1 == red2
True
Pickle化

__getnewargs__ がある, pickleable みたい

>>> from pickle import dumps, loads
>>> Colors.RED == loads(dumps(Colors.RED))
True

Enumの型を指定して扱う

Enumは定義時に型を指定すると列挙する値が、すべて指定した型になる

strとして列挙したい場合
from enum import Enum

class Colors(str, Enum):
    RED = '1'
    BLUE = 2
    GREEN = 'green'

print(repr(Colors.BLUE))  # <Colors.BLUE: '2'>
print(repr(Colors.BLUE.value))  # '2'
intとして列挙したい場合

IntEnumが用意されている

下記では 'green' がエラーになる

from enum import IntEnum

class Colors(IntEnum):
    RED = '1'
    BLUE = 2
    GREEN = 'green'  #  ValueError: invalid literal for int() with base 10: 'green'

isdigitな数値ならOKのようだ、isnumericはだめ(IVとか, 四とか)

from enum import Enum

class Colors(int, Enum):
    RED = '1'
    BLUE = 2
    GREEN = b'3'  
    YELLOW = '4'

print(repr(Colors.GREEN.value))  # 3
print(repr(Colors.YELLOW.value))  # 4
floatとして列挙したい場合
from enum import Enum

class Colors(float, Enum):
    RED = '1'
    BLUE = 2
    GREEN = b'3'
    YELLOW = '4'

print(repr(Colors.GREEN.value))  # 3.0
print(repr(Colors.YELLOW.value))  # 4.0
tupleとして列挙したい場合

もちろんtupleもある

from enum import Enum

class Colors(tuple, Enum):
    RED = (1, 'red color')
    BLUE = (2, 'blue color')
    GREEN = (3, 'green color')

print(repr(Colors.RED))  # <Colors.RED: (1, 'red color')>
print(repr(Colors.RED.value))  # (1, 'red color')
NamedInt
class Colors(NamedInt, Enum):
    RED = ('red color', 1)
    BLUE = ('blue color', 2)
    GREEN = ('green color', 3)

print(repr(Colors.RED))  # <Colors.RED: NamedInt('red color', 1)>
print(repr(Colors.RED.value))  # NamedInt('red color', 1)
OrderedEnum

Eum同士の比較が可能

class Colors(OrderedEnum, Enum):
    RED = 1
    BLUE = 2
    GREEN = 3

print(Colors.RED < Colors.BLUE)  # True
class Colors(Enum):
    RED = 1
    BLUE = 2
    GREEN = 3

print(Colors.RED < Colors.BLUE)  # TypeError: unorderable types: Colors() < Colors()
LabelledIntEnum
class LabelledIntEnum(int, Enum):
    def __new__(cls, *args):
        value, label = args
        obj = int.__new__(cls, value)
        obj.label = label
        obj._value_ = value
        return obj

class Colors(LabelledIntEnum):
    RED = (1, "red")
    BLUE = (2, "blue")
    GREEN = (3, "green")

print(repr(list(Colors)))  # [<Colors.RED: 1>, <Colors.BLUE: 2>, <Colors.GREEN: 3>]
print(repr(Colors(1)))  # <Colors.RED: 1>
print(repr(Colors.RED))  # <Colors.RED: 1>
print(repr(Colors.RED.value))  # 1
UniqueEnum

下のuniqueデコレーターと同じかな

列挙する値のユニークを保証する

uniqueデコレーターがありました。つかいませう

from enum import (
    Enum, 
    unique
)

@unique
class Colors(int, Enum):
    RED = '1'
    BLUE = 2
    GREEN = '3'
    YELLOW = '3'  # ValueError: duplicate values found in <enum 'Colors'>: YELLOW -> GREEN

自動採番

値がなければナンバリングされる

int
>>> Enum('Colors', ('RED', 'BLUE', 'GREEN', ), module=__name__).RED
<Colors.RED: 1>
>>> Enum('Colors', 'RED BLUE GREEN').RED
<Colors.RED: 1>
str
>>> StrEnum('Colors', 'RED BLUE GREEN').RED
<Colors.RED: '1'>
tuple
>>> TupleEnum('Colors', 'RED BLUE GREEN').RED
<Colors.RED: (1,)>
AutoNumberInAList
class AutoNumberInAList(Enum):
    def __new__(cls):
        value = [len(cls.__members__) + 1]
        obj = object.__new__(cls)
        obj._value_ = value
        return obj

class Colors(AutoNumberInAList):
    RED = ()
    BLUE = ()
    GREEN = ()

print(repr(Colors.RED))  # <Colors.RED: [1]>
print(repr(Colors.RED.value))  # [1]
AutoNumber

定義時に自動採番

auto_enum

定義時のmetaclassで自動採番

終わり

Enumには夢が詰まっているとのこと