それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

アカウント・リカバリで電話番号・生年月日はやめてほしい

アカウントのパスワード復活で生年月日はやめてほしい

個人情報をクレクレするのは良いけれど。生年月日を使うのはやめてほしい。

f:id:takuya_1st:20170404035852p:plain:w300

生年月日を漏らされるとめんどくさい

個人情報が漏れたときに、生年月日が流出すると本当にめんどくさいことになる。

クレジットカード番号なら変更可能だが、名前、住所、生年月日、職場は変更することが困難だ。

変更が困難な情報を預けるのは本当にめんどくさい。

どんなにセキュリティを確保されても漏れるときは漏れる、個人情報が必要な場面はそうそうないでしょう。

しかも、名前、住所、生年月日、職場は公開情報。

これらの公開情報とアカウント情報を紐付けられるのがめんどくさい。

この会社が漏らしたら紐付いた情報がドンドン連鎖的に照合されていく。。。

だから本当の生年月日・名前など書くはずがなく。

公開情報で認証するのは本当に危険。

アカウント乗っ取りのリスクを考慮するなら、公開情報で認証するなんてナンセンスだと思いませんか。

この会社のIDの場合

  • メールアドレス
  • 生年月日
  • 電話番号
  • Pontaカード番号

で認証をしています。すべて公開情報です。まいったな。

かりにメールアカウントが乗っ取られていたらそれは此の会社の責任ではないだろう。だからメールとアカウント復活用の予備メアドだけで十分なんですよ。それでも足りないなら、ユーザーが希望してSMSによる2要素を追加することも出来るってのがお互い楽なのに。なんでこういうエセキュリティが増えちゃうんだろうか。

え?→「アドレスや任意の文字列を付加したようなメールアドレスについては、同一と判断します」

何のためにメアドを変えられるか理解してない。

ポンタにログイン使用して気づいた。

尚、Gmailau one netのメールサービス、Livedoorメール、HotmailYahoo!メールなどで1つのメールアドレスとして扱われる、複数のメールアドレスや任意の文字列を付加したようなメールアドレスについては、同一と判断します。 さらに詳しく

馬鹿なの?

Gmailエイリアスのメアドを同一に扱ってしまうと、「そのメアド」が登録されているかわかってしまう。

つまり、ユーザーIDが漏れてしまう。

どういうことか?

example@gmail.com
example+nospan@gmail.com
exam.ple@gmail.com

これらを「別の」メアドとして使えること、それ自体に意味があります。スパムよけではなく、「ログインIDの隠蔽」という意味があります。

ログインIDとして同じにしてしまうと、上記のメアドはすべてコレに丸められて

example@gmail.com

このオリジナルのメアドでログインが可能になってしまいます。

更に恐ろしいことに、そのメアドが登録されているかどうかの情報が漏洩します。

これはセキュリティ的には由々しき事態です。

エイリアスを同一視=セキュリティレベルの低下

サイトごとにメアドとパスワードを変えられません。

メアドとパスワードの両方をサイトごとに変更することでセキュリティを確保できているはずが、全く意味がなくなります。

Gmailエイリアスを同一視してしまうとセキュリティレベルの著しい低下を招きます。

なぜ、ログインにエイリアス使うのとセキュリティレベルが向上するのでしょうか

なぜか?

メアドは「公開情報」だからです。

メールアドレスのような公開情報でログインを可能にしてしまうと、<パスワード>の強度だけが純粋にセキュリティの強度です。

そのため、メールのエイリアスを使えることで、セキュリティを強化することが出来ます。つまり公開情報でのログインを不可にすることが出来る。

メールエイリアスを許可することでログインIDを不可視にすることが可能になっています。つまり攻撃を受けにくくなります。

また、別のサイトが「おもらし」してしまったとしても自サイトのログインのブルート・フォース攻撃には使われることはありませんし、もちろん同一IDではないでログインを拒否することが出来ます。

これが、メールエイリアスを使う最大の理由でしょう。現代の自衛策です。

ログインIDにメアドと電話番号は危険

たとえば、ドコモショップではドコモIDをドコモのキャリアメールにするように指示されます。まっぴらごめんです。

たとえば、au では au id が電話番号です。恐ろしいことです。

au id の場合は、2段階認証をオフにすることが出来ますが、乗っ取り事例を紹介されてトコトン警告されます。大きなお世話です。電話番号でログイン可能にしてるからそんな事例が起きたとしか考えられない。ランダムな文字列をパスワードだけでなくIDにも採用すれば危険性は少し減少する。

エイリアスはスパム避けだけではない。

何のためにメアドが複数作れるか全く理解出来てないんじゃなかろうか

どこかのサイトがおもらしして、メアド同一で乗っ取られたら、責任取ってくれるんでしょうかね。

エイリアスのメアドに変更が絶対に不可能です。

ドット入れると強制的にメアドを変えられません。この会社は本当にGmailエイリアスが嫌いなようです。

f:id:takuya_1st:20170404034612p:plain

公開情報で認証するのやばいよね。近づかないほうがいいです。この会社も

あ、、絵合わせセキュリティですか。

この会社、マジで近づかないほうが良さそうですね。

f:id:takuya_1st:20170404034005p:plain:w200

メールエイリアスが意味を失いつつある。

複数登録が多くなると、重複登録を許してしまうとか、本人確認がめんどくさいとか、会社の個人情報の収集の目的や、キャンペーン重複登録をさせたくないなどの一方的な理由でセキュリティレベルを下げられるのは全く嬉しくない状況が起きている。

仕方ないので、メールエイリアスicloud.com を使いましょう!

au id や livedoor メールを チェックするのに @googlemail.com @me.com を調べないのは本当にアホですね。ザルすぎて頭オカシイ

Pontaカードやめよう

個人情報の乞食されるのでも、多少はメリットあるから使ってましたが、今日から一切使わないことにします。

ローソンは頻繁に使うので、ポンタやめてdpoint にします。

f:id:takuya_1st:20170404042409j:plain:w300

libtrash でゴミ箱を扱う→obsolete → trash-cli

debian の wheezy あたりではもうobsoleteパッケージになってレポジトリから削除されてるっぽい

obsolete

http://usami-k.seesaa.net/article/2644852.html

現在はコッチ

https://github.com/andreafrancia/trash-cli/

takuya@:~$ apt install  trash-cli
alias rm='trash-put'

2017-05-14 修正

trash-rm は ゴミ箱の中から指定ファイルを消すものでした、勘違いです。

SpeakerDeck のスライドを取得してローカルで見る

SpeakerDeck をオフラインで見たい

Speaker Deck で良さげなスライドをEvernoteに溜め込みたいなと思った。

ブックマークしてても検索出てこないし。どうしようかな~っておもって。とりあえずダウンロードしてみることにした。

URLをスクレイピングするのに asyncio使おうとしたけど、あんまり早くならないし、 コードは煩雑だし。muliprocessは良く出来てるけど、それするくらいなら、 xargs でマルチプロセス作ったほうがずっとスッキリしてて楽だった。

speaker-deck.py

gist.github.com

neobundle から dein に乗り換えた

neobundle のメンテが面倒になったので

ここらで、マルっと dein に乗り換えようと思って dein に乗り換えました。

特徴

dein 乗換てよかったこと。

速い

起動速いんですね。

toml ファイルがいい

toml ファイルに設定がまとまるので、gitによる差分管理が楽になった。

インストールと初期設定

vim のインストール

macOS X

brew install vim --with-lua

debian

sudo apt install vim-nox
sudo update-alternatives --config editor

dein の準備

mkdir ~/.vim 
cd ~/.vim
curl -LJO  https://raw.githubusercontent.com/Shougo/dein.vim/master/bin/installer.sh > installer.sh
bash installer.sh ~/.cache/dein

.vimrc から dein の起動

" dein の設定とインストール
" $> cd .vimrc
" $> bash installer.sh  ~/.cache/dein
"
if isdirectory( expand('~/.cache/dein')  )
    if &compatible
        set nocompatible               " Be iMproved
    endif
    set runtimepath+=~/.cache/dein/repos/github.com/Shougo/dein.vim

   " Required:
    if dein#load_state('~/.cache/dein')
        call dein#begin('~/.cache/dein')

       " Let dein manage dein
       " Required:
        call dein#add('~/.cache/dein/repos/github.com/Shougo/dein.vim')

       " Add or remove your plugins here:
                call dein#load_toml(expand('~/.vim/dein.plugins.toml'),       {'lazy': 0} ) "  main 
        call dein#load_toml(expand('~/.vim/dein.plugins.colors.toml'),{'lazy': 0} ) " colorscheme
        call dein#load_toml(expand('~/.vim/dein.plugins-lazy.toml'),  {'lazy': 1} ) " others for lazy 

       " You can specify revision/branch/tag.
        call dein#add('Shougo/vimshell', { 'rev': '3787e5' })

       " Required:
        call dein#end()
        call dein#save_state()
    endif

   " Required:
    filetype plugin indent on
    syntax enable

   " If you want to install not installed plugins on startup.
    if dein#check_install()
      call dein#install()
    endif
endif

toml ファイルを書く

ぱぱっと移動させたのがこんな感じ。メモ程度にする。

[[plugins]]
repo = 'Shougo/dein.vim'

[[plugins]]
repo = 'cespare/vim-toml'
on_ft = 'toml'

[[plugins]]
repo =  'itchyny/lightline.vim'
[[plugins]]
repo = 'Shougo/neosnippet.vim'

[[plugins]]
repo = 'Shougo/neosnippet-snippets'

[[plugins]]
repo = 'Shougo/neocomplete'
hook_add = '  let g:neocomplete#enable_at_startup = 1'

[[plugins]]
repo = 'tomtom/tcomment_vim'
[[plugins]]
repo = 'Shougo/vimfiler'

[[plugins]]
repo = 'scrooloose/nerdtree'

[[plugins]]
## vim のコマンド入力でEmacsのように option で1単語ずつ移動できるように
repo = 'houtsnip/vim-emacscommandline'

[[plugins]]
repo = 'xolox/vim-misc'
[[plugins]]
repo = 'xolox/vim-colorscheme-switcher'
depends = ['misc.vim']

[[plugins]]
# カーソル位置のコンテキストに合わせてftを切り替える
repo = 'osyo-manga/vim-precious'
depends = ['context_filetype.vim']

[[plugins]]
repo = 'Shougo/context_filetype.vim'
[[plugins]]
repo = 'Shougo/neco-syntax'

#[[plugins]]
### python
repo = 'davidhalter/jedi-vim'
on_ft = 'python'

[[plugins]]
## C言語用
repo =  'Rip-Rip/clang_complete'
on_ft = ["c", "cpp"]
hook_add = '''
let g:clang_library_path="/usr/local/opt/llvm/lib"
'''

[[plugins]]
repo = 'pangloss/vim-javascript'
on_ft = ['js','javascript']

[[plugins]]
repo = 'tpope/vim-endwise'
on_ft = ['ruby']
[[plugins]]
repo = 'plasticboy/vim-markdown'
on_ft = ['md']

vim-precious 強い

toml 中にvim script 書いたら、ハイライトを切り替えてくれる vim-precious。これHTMLでも使えたりするので強い。

参考資料

https://qiita.com/delphinus/items/00ff2c0ba972c6e41542

python の subprocess 起動で、起動コマンドのstdin に書き込む

起動して入力をわたしたい。

cmd1= "cat"
p = subprocess.Popen(cmd1.strip().split(" "), stdin=subprocess.PIPE)
  p.stdin.write("Hello World\n".encode('utf8'))

コマンドを起動するときに、 stdin に PIPEを指定する。実行中の python と サブプロセスのSTDINを繋いでやる。

これで p.write が呼べる

複数パイプしたいとき

複数パイプの起動をしたいときは、こちらのエントリに書きました。

python でコマンド実行。サブプロセスの終了待ち・強制終了・親プロセスと一緒に殺す。 - それマグで!

正規表現の名前付きマッチで、scanf から卒業する

正規表現の名前付きなマッチを覚えました。

>> ret = '直通特急 阪神梅田行 18:29 発 3番のりば'.
match(/(?<type>.+) (?<dest>.+)(?<dep_time>.+)/)
=> #<MatchData "直通特急 阪神梅田行 18:29 発 " type:"直通特急" dest:"阪神梅田" dep_time:"18:29 ">
>> puts ret[:type]
=> "直通特急"

名前付きマッチの後方参照はとても楽しい!!便利!!これで scanf みたいなレガシーとバイバイできそう

php でも動いた

<?php

$str='直通特急 阪神梅田行 18:29 発 3番のりば';
$regex= "/(?<type>.+) (?<dest>.+)行 (?<dep_time>.+)発 /";
preg_match(  $regex, $str , $matches );

var_dump($matches);

実行結果

takuya@orm$ php test.php
array(7) {
  [0]=>
  string(39) "直通特急 阪神梅田行 18:29 発 "
  ["type"]=>
  string(12) "直通特急"
  [1]=>
  string(12) "直通特急"
  ["dest"]=>
  string(12) "阪神梅田"
  [2]=>
  string(12) "阪神梅田"
  ["dep_time"]=>
  string(6) "18:29 "
  [3]=>
  string(6) "18:29 "
}

bash でも出来ないか?

できなかった。

takuya@orm$ regex='(?<name>.+)';  [[ "aaaaaaa aaaaaaaaaa" =~ $regex ]];echo "${BASH_REMATCH[0]}"
takuya@orm$

SQLiteでの alter colmunの代替案

SQLite にはいくつかの機能がない。

たとえば、次の通り。

  • alter table rename columnがない
  • alter table drop column がない

SQLiteでの alter colmunの代替案

  1. いったん別のテーブルにデータを退避する
  2. drop table でテーブルを消す
  3. create table でテーブルを作り直す
  4. select / into で退避データを投入する

いったん別のテーブルにデータを退避する。

方法は2つある。一つは、テーブルを別名にする。もう一つは、create table as select でテーブルをコピーする

ALTER TABLE my_table RENAME TO my_table;
create table my_temp as select * from movie_info;

create table select from でコピーすると、SQLiteのデフォルトなTextメインのカラムになるので細かいConstrainsや型情報やdefault / unique が消える可能性がある。

drop table で消す

drop table my_table;

create table で作り直す

create table my_table  (  .... ) ;

insert select する。

insert into my_table select * from my_temp;

Alter table と column まわりはちょっとハマりますね

ffmpeg で mkv の字幕ファイルをmp4 にする

字幕も含めてコピーしたいな

mkvのストリームに字幕が含まれてて、これを維持したまま、mp4 に変換したいな。

h264 / aac はそのままでいいんだけど、字幕はそのまま copy で動かなかったので調査した。

"ffmpeg -i '#{src}.mkv' -y -map 0:0 -map 0:1 -map 0:2  -c:v copy -c:a:1 copy -c:s mov_text  '#{dst}.mp4'"

逆は

"ffmpeg -i '#{src}.mp4' -y -map 0:0 -map 0:1 -map 0:2  -c:v copy -c:a:1 copy -c:s srt  '#{dst}.mkv'"

mkv は srt で mp4 は mov_text らしい。

ストリームに何が含まれるかは ffprobe json

takuya-1st.hatenablog.jp

参考資料

Creating multiple outputs – FFmpeg

https://trac.ffmpeg.org/wiki/Map#Example1

http://stackoverflow.com/questions/8672809/use-ffmpeg-to-add-text-subtitles

画像がカラーか、白黒かを判別する

カラー画像かモノクロか判別する。

takuya@:$ convert sample.cl.jpg -colorspace HSB -separate -delete 0 -fx "u*v" -blur 2x2 -threshold 30% -format '%[fx:mean]\n' info:
0.693993
takuya@:$ convert sample.bw.jpg -colorspace HSB -separate -delete 0 -fx "u*v" -blur 2x2 -threshold 30% -format '%[fx:mean]\n' info:
0

スキャンしたデータがカラー(表紙)か本文(頁)かを判断したいので調べた。0に近くなれば白黒

彩度をみて -threashold で許容範囲を決めて、 -format で数字にしてる

コマンドはこんな感じ。

convert sample.cl.jpg \
  -colorspace HSB \ 
  -separate -delete 0 \
  -fx "u*v" -blur 2x2 \ 
  -threshold 30% \
  -format '%[fx:mean]\n' info:

参考資料

http://q.hatena.ne.jp/1340883203

プレイリストのファイル形式を色々試してみる。

プレイリスト扱えると便利

WEBサイトに動画や音声をたくさんおいていると、再生が面倒なのでプレイリストを作って対応したい。どのアプリが、どのプレイリストに対応しているか、そもそもプレイリストのファイル形式はどのようなものがあるのだろうか。

プレイリストファイルの形式。

ブラウザやOS標準になっているなど、すぐに使えるプレイリストのファイル形式には次のようなものがある。

幾つかあるけれどVLC をメインに考えることにした。

フォーマット ファイルの中身 対応アプリなど
XSPF XML VLC
ASX XML WMP / VLC
m3u/m3u8 TXT Safari/Android/iTunes
PLS TXT vlc

此の他にも類似物として podcast.xml がある。

また、プレイリストは入れ子に出来るようになっている。

XSPF の例

拡張子は xspf 。 Contet-Typeは application/xspf+xml

<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<trackList>
<track>
<location>http://example.com/sample.mp4</location>
<title>自分のムービー</title>
</track>
</trackList>
</playlist>

ASX の例

拡張子は asx 。Content-Type は video/x-ms-asf

<asx version=”3.0”> 
<entry> 
<title> 自分のムービー </title> 
<ref href=http://example.com/sample.mp4” /> 
</entry> 
</asx>

ASXにはASFの類似物があり、そちらはテキストファイルでPLSに近い

WindowsメディアのフォーマットはiPhoneで使えないので、今回は調査対象から外します。

m3u / m3u8

拡張子 .m3u, .m3u8 。 Content-Type application/x-mpegURL

#EXTM3U 
#EXTINF:-1,自分のムービー
http://example.com/sample.mp4

メタデータは次の行に書くのがポイント

#EXTINF:time_duration,ムービータイトル - シリーズ名

m3u8 の中に記載するのは m3u8か TS(content-type video/MP2T)が要求される。

時間は -1 にすると指定をキャンセルできる(あとで登場します)

PLS

拡張子 pls。 Content-type はaudio/x-scpls

[playlist]
File0=http://example.com/sample.mp4
Length0=1499
File1=http://example.com/sample.mp4
Length1=1499
NumberOfEntries=2
Version=2

中身は ini ファイルと同じフォーマットだと思う

今回は iOS Safari / iTunes / VLC について

VLC は Win/Mac/iOS でほぼ動作は変わらずなんでも再生できた。

また、iTunesは m3u8 については、ポッドキャストとして取り込んでくれた。

app xspf m3u8 pls
VLC
iOS/Safari
Mac/Safari
iTunes

m3u8 の中身にTSを入れると、HLSのストリーミングになる。Safariはこのストリーミングとして読み込むので、m3u8の中身にTS以外をいれるのは厳しい。

AppleTV は iOSSafariと同じになる。AppleTV / iOSはm3u8のmp4未対応みたいだね

m3u8 の中身にアレコレ入れてみる

m3u の中身に、mp4 を入れた結果

app mp4(264/aac) mpegts(264/aac)
VLC
iOS/Safari
Mac/Safari
iTunes

此の時にチェックに使ったものは次の通り。

#EXTM3U
#EXTINF:-1,自分のムービー-sample_0
https://example.com/my-movie.ts
#EXTM3U
#EXTINF:60,自分のムービー-sample_0
https://example.com/my-movie.ts

再生時間には、INT秒以外を入れると動かなかった。正確に時刻を測定したら逆に動かなかったという。

再生時間には-1を入れておくのが無難なようです。

ちなみに、h264/aacの mpegts を mp4(h264/aac)から変換するには次のようにした。

ffmpeg -i input.mp4 -acodec copy -vcodec copy -bsf:v h264_mp4toannexb -t 60 -f mpegts out

ブラウザにキャッシュを許すためには

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8

# 略...

#EXT-X-ENDLIST

EXT-X-TARGETDURATIONは、各エントリを全部まとめて此のファイルが再生時間が何秒あるか

MEDIA-SEQUENCE はどこから再生を始めるかをエントリ配列の最初から

AppleTV でも再生できる。

AppleTV でruby airplay を試したら、ちゃんと再生できた。

itunes に突っ込んだとき

itunes でもm3u8動作チェックはできる*1

ただしポッドキャストとして扱われるのに、ポッドキャストではなくミュージック一覧にでてきた

itunes に追加する方法

ストリームを開く

f:id:takuya_1st:20170330022721p:plain:w200

URLを入力する

f:id:takuya_1st:20170330022726p:plain:w200

プレイリストに出てくる

f:id:takuya_1st:20170330022730p:plain:w200

結論

m3u8 は mp4 を突っ込んでも iOSで動く。mpegtsに変換するのも時間はほとんどかからない。

また、HLSにあるような細切れになTSにする必要はなかった。

ハードディスクにたまったムービーファイルを AppleTV に配信して連続再生ができるようになった。

youtubeのファイルと組み合わせれば急にCMが入って目がさめること無く連続再生ができるね。

youtubeのプレイリストがm3u8 で取り出せばいいのになぁ。

追記

iOSは m3u8 の URL に GET のパラメータを入れても無視するようでした。

なので直接ファイルを指定しないと駄目なようです。GET引数でリダイレクト仕様としたけど駄目だった。

参考資料

https://en.wikipedia.org/wiki/PLS_(file_format)

https://en.wikipedia.org/wiki/Advanced_Stream_Redirector

*1:たぶんおなじAVFoundationだから?

pythonでHTMLをパースしてXpathする

python でも xpath したい。

libxml でパースするには、lxml を使うと楽

pip install lxml

使い方。

lxml.html を使うと確実に、パースする事ができる。parse は IO を取るのでStringIOを使うことになる。

lxml.html.parse(StringIO(page.html))

サンプル

import lxml.html
from io import StringIO, BytesIO

def parse( node , idx ) :
    name  = node.xpath(".//*[@class='desc-title']//text()")[0]
    url  = node.xpath(".//a/@href")[0]
    img_url  = node.xpath(".//img/@src")[0]
    price = node.xpath('.//div[contains(./@class, "desc-price")]//strong/text()')[0].replace(',','')
    identify_url = page.url + '#' + str(i+1)
    html = lxml.etree.tostring( node ,  pretty_print=True,method='xml', encoding='utf-8').decode('utf-8')
    item = Item(link=url,title=name,price=price,html=html, identify_url=identify_url,img_url=img_url  )
    return session.add(item)


if __name__ == '__main__' : 

    doc = lxml.html.parse(StringIO(page.html))
    ret = doc.xpath("//li[@class='list-item']")

    for i, node in enumerate(ret) : 
      parse( node,i)

jq コマンドで json を minify する

jq を使って minify することが出来る

cat composer.json | jq -c . 

または

jq -c . < sample.json

任意のJSONJSONのフィルタを変えて小さくすることが出来る。

サンプル

composer.json を小さくする
{
  "repositories" : [
    {
      "type" :"git",
      "url" : "https://github.com/takuya/simple-app.git"
    }
  ],

  "require":{
    "takuya/simple-app": "dev-master"
  }
}

出来た。

 jq -c .  < composer.json
{"repositories":[{"type":"git","url":"https://github.com/takuya/simple-app.git"}],"require":{"takuya/simple-app":"dev-master"}}

pythonのORM:SQLAlchemy の基本的な使い方

SQLAlchemy を使ってみる。

目次

インストール

pip install sqlalchemy

テーブルの作成

テーブルを作成するときは、ORMを使うと便利。

Baseを継承して 、カラムをメンバに定義したうえで、 Base.metadata.create_all(engine) を呼び出す。

from sqlalchemy import * 
from sqlalchemy.orm import *


Base = declarative_base()
class Page(Base):
  ##テーブル名
  __tablename__ = 'pages'
  ## カラム名
  id   = Column(Integer, primary_key=True)
  url  = Column(Text, unique=True, nullable=False)
  html = Column(Text , nullable=True)
  http_status = Column(Integer, default=0, nullable=True )
  status = Column(Integer, default=0, nullable=True )
  ## メソッドもここに書く
  def is_not_found : 
       return  self.http_status == 404;

engine = create_engine('sqlite:///./urls.db', echo=True)
Base.metadata.create_all(engine)

手順のおさらい

  1. 必要なパッケージをimport
  2. Base=declarative_base()を使う
  3. RDBMSに接続する
  4. create_all する

RDBMS への接続

engine = create_engine('sqlite:///./urls.db', echo=True)

echo=True にすると接続と実行ログが出るので重宝するので、初回実行は必ずTrueにしておく。

テーブル定義のクラス。

class Page(Base):
  __tablename__ = 'pages'

  id   = Column(Integer, primary_key=True)
  url  = Column(Text, unique=True, nullable=False)
  html = Column(Text , nullable=True)
  http_status = Column(Integer, default=0, nullable=True )
  status = Column(Integer, default=0, nullable=True )

メンバ変数で入れていく。

テーブル名を __tablename__ で代入する。

カラムには、Column( TYPE, *options ) で指定する。

TYPEには sqlalchemy.Integer sqlalchemy.String 等がある。

Base.metadata.create_all() は複数回呼び出しても大丈夫。なかったら作るだけ。既存のテーブルを消したり、再定義しない。

INSERT/UPDATE をする。

データの投入はクラスを使うと便利。

ORMを使うためクラスのインスタンス変数をメインにいじっていく。

また、接続はすべてSession単位で扱われる。

SessionはBegin / Commit を抽象化したものだと思えばいい。

INSERT INTO をするには Page( url=i ) で クラスインスタンスを作成し、session.add(page) でレコードを追加する。

そして、最後にsession.commit() で コミットする。

INSERT の例

Session = sessionmaker(bind=engine)
session = Session()

url = 'https://web.wm.auone.jp/lx/regular/?p=2'

page = Page( url=i  )

ret = session.add(page)

session.flush()
session.commit()

UPDATE について

UPDATE 句については、クラスのメンバ変数を更新し、最後にCommitする。

page = session.query(Page).filter( Page.html == (None) ).first()

page.html =  fetch( page.url )
page.http_status = 200
page.status = 1
session.commit()

クラスインスタンスを作成する代わりに、Fiter( SELECT ) でオブジェクトとしてレコードを取り出してから、メンバ変数を更新し、最後に COMMITする。

また、UPDATEは、条件にマッチするものを一括で更新することも有りえます。

session.query(Page).filter( Page.id < 100 ).update( {status=0} )

DELETEする。

DELETEにも2つの考え方があります。ひとつはオブジェクトをレコードをから消す。もう一つは検索条件にマッチするものを消す。

オブジェクトを取得して消す場合

page = session.query(Page).filter( Page.id == 1 ).one()

session.delete(page)
session.commit()

検索条件にマッチするものを消す場合。

session.query(Page).filter( Page.id == 1 ).delete()

INSERT・UPDATE・DELETE のキャンセル

セッションは begin 同等なので キャンセルできる。

session.rollback()

SELECT について。

テーブルからデータを取り出すには、SELECT句をWrapした query を使う。

またWhere句をWrapしたfilter / filter_by を使う。

pages = session.query(Page).all()
page = session.query(Page).first()

query した後に all ()first() を呼び出すことが必要。 これらのメソッドを実行することでPreared statement が実行される。

all () / first() とその仲間

SELECTをするためには、 query().filter().all() のように書くことが出来る。 all () の他には次のようなものがある。

レコードを返すメソッド一覧
  • all () 配列を返す。
  • first () クラス(またはNone)を返す。
  • count () 個数を返す
  • one 系
    • one() filterの結果が1つだけになることを期待する。
    • one_or_none() one と同じ
    • scalar() ORMマッピングしないときに使う。
one()について

one は考え方が、少し面倒ですね。

one() は filterの結果が1つだけになることを期待する。つまり、where で絞り込んだ結果が複数返ってきたら sqlalchemy.orm.exc.MultipleResultsFound を返してエラーになる。また、一つも見つからない場合は sqlalchemy.orm.exc.NoResultFound を返す。

one_or_none について

one_or_none は 結果が0件のときにonesqlalchemy.orm.exc.NoResultFound 例外を送出する代わりに None を返す。

scalar について

scalar()one() を内部的に実行する。

これは scalar 値を取り出すのに使う。たとえば、 select count(id)select id from tableを実行するときに使う。実際にはcount ()が あるしORM使うしあんまり使わないかも?

count について

count() は COUNT(*) を実行してくれる。

個数が少ないなら all() してlen()見ても良いのですが。count の方がいいね。

filter の書き方例

where句の代わりにFilter()を使ってデータを取り出す。

session.query(Page).filter( Page.id == 1 ).one()

これは、クラスに入れてある 変数 id が Colmun( ) を継承していて Column の演算子オーバーロードを使っているので クォートなしに書ける。詳しくは dir( Your_ORM_Class.colname ) などすればわかる。

この演算子オーバーロードのお陰で記述がクオートなしでスッキリする。こういうのはPHPRubyのORMにも真似してほしい所。

IS NULL / IS Not NULL

IS NULL は == None と解釈する。 IS Not NULL は != None と解釈する。

次のように書く

session.query(Page).filter( Page.html == None ) # is null
session.query(Page).filter( Page.html != None ) # is not null

演算子オーバーロードが嫌いな人は、次のように関数で書くことが出来る。

session.query(Page).filter( Page.html.is_(None) ) # is null
session.query(Page).filter( Page.html.isnot(None) ) # is not null
COLNAME = VALUE / COLNAME > VALUE

where 句の基本 COLNAME = VALUEは is null で見たように、 == VALUE で書く。

is not nullの書式を覚えてしまえば、他も全て同じに解釈できて便利です。

session.query(Page).filter( Page.id == 1 ) 
session.query(Page).filter( Page.id != 1 ) 
session.query(Page).filter( Page.id < 10  )
session.query(Page).filter( Page.id >= 10  )
session.query(Page).filter( Page.id == None )  # is NULL 

ちなみに、経験則ですが、ORMの使用法を見るとき、is not nullLIKE を見るといい、そこにORMの特徴が出るのでis not null / like を調べればだいたい使い方がわかる。

AND について

ANDは、条件を複数突っ込めば ANDになる。とてもかんたん。

session.query(Page).filter(  Page.id > 51 , Page.id < 100  )
LIKE 句について

LIKE は、直接文字列を突っ込みますね。% が Python の文字列フォーマット指定子なので上手に使ってください。

query.filter(Page.url.like('%search_string%'))

フォーマットするときは

search="search_string"
query.filter(Page.url.like( "%%%s%%" %   search )) # お世辞にも読み易いと言えない
直接SQLのWhere句を入れたい

Where句は複雑になるので、ORMに頼らずに直接指定したい場合。此の目的にはsqlalchemy.text が使える。

session.query(Item).filter( text(' id < 10 ' ) ).all()
session.query(Item).filter( text(' id > 10 and id < 12 ' ) ).all()

この場合はPrepareを経由せずに実行されるので、SQL Injection の可能性がある。そのためbindParam するには次のようにする。

プリペアドステートメントの使い方は、RDBMSによって多少異なるが、大抵はプレイスホルダを ?で書くか、 :param_name のようにコロンで書く

sqlalchemy.text() では :name は使えるが ? は使えないみたい

session.query(Item).filter( text(' id > :value1 and id < :value2' ) ).params(value1=10, value2=12).all()

これは動かなかった。( 追加調査が必要 かも

items  = session.query(Item).filter( text(' id = ? ' ) ).params(1)

SQL Injection を防ぐためには text() をで直接使うなというのではなく、ユーザーから入力される文字列に対して bind を実行するのである。

limit / offset

これは予想通りの記述。

session.query(Item).limit(10).offset(10)

ORDER BY / ORDER BY DESC

ORDER BY は かんたん。でも 降順はちょっとめんどくさい

ASC
session.query(Item).lorder_by( Item.id )
DESC
session.query(Item).order_by( Item.id.desc() )
session.query(Item).order_by( desc(Item.id) )
session.query(Item).order_by( " id desc " )

order_by_desc/ order_by_asc みたいな関数でも良いと思うんだけど、なかったので上記のように書く必要がある。

既存のテーブルを扱うクラスについて

既存のテーブルを扱うときは、必要なカラム分だけメンバ変数に取り出してもいい。全カラムを指定するのは流石に面倒だろうし。

from sqlalchemy import * 
from sqlalchemy.orm import *


Base = declarative_base()
class Item(IBase):
  __tablename__ = 'pages'

  id   = Column(Integer, primary_key=True)
  status = Column(Integer, default=0, nullable=True )

もちろんマッピングしてないカラムについてはORMから見えなくなる。

まとめ

ORMとして非常に書きやすいと思います。今回は sqlalchemy.ormがメインで、sqlalchemy.sql.expression については殆ど言及してないです。

expression を使ってもSQLを組み立てるより手軽でかなり使いやすいのでおすすめです。

たとえば、updateについては ORMを使わずに次のように書くことが出来ます。

公式ドキュメントからの引用

update(users).where(users.c.id==5).values(name='user #5')
users.update().where(users.c.id==5).values(name='user #5')

どんな使い方をしたとしてもSQL/ RDBMS を扱うには SQLAlchemy は期待にかなう優秀なツールですね。

参考資料

http://www.sqlalchemy.org/library.html#reference

http://stackoverflow.com/questions/4186062/sqlalchemy-order-by-descending

DBアクセス抽象化で、INSERT と UPDATE でコードを共通化させるハック。

困りごと: INSERT と UPDATE の共通化をしたい。

似たようなコードを何度も書くのはめんどくさい。テーブル定義が変わったときにもう鬱陶しい。

共通化したい・・・

function insert( name , date ){
    sql = "INSERT INTO table_name VALUES( #{name} , #{date} );"
    mysql->execute();
}
function update( name , date , id ){
    sql = "UPDATE table name_name=#{name}   modified=#{date}  ) where id = #{id} ;;"
    mysql->execute();
}

解決方法を見つけた。

いろいろな設計を見ててたら、次のようにすると解決した。

さきに、空っぽのレコードを作ってしまう。

function save( name , date , id=null){
    if ( ! id ) {
         sql = "INSERT INTO table name VALUES( );"
         mysql->execute(sql);
         id = mysql->last_insert_id();
     }
    sql = "UPDATE  table_name set   name=#{name}   modified=#{date}  where id = #{id} ;"
    mysql->execute(sql);
}

ものすごいバッドノウハウで臭いカンジがするんだけど。意外とコレでスッキリするんだよなぁ、

どうでもいい話

データ・ベースのアクセスってSQLのbuildingで四苦八苦したり、Prepareと相性が悪くて、なぜかStringを結合したりとかしちゃってるのを見たことがあってですね。

ORMを使えば良いんだろうけど、どのORMを採用するかで宗教戦争になりそうだったり、 SQLでこう書くのを、ORMでどう書くかと翻訳が2段階になって、時間コストが増えたりとかですね。

ORMは遅かったり最適化がうまくできん買ったりめんどくさいですよね