madokaのブログ

勉強したことのoutput先として使ってます。内容はpythonがらみが多いかもです。

PHPの勉強まとめ

環境構築

  • PHPのversion管理をする..env的なのはあるが、brewで入れられないので少し面倒。。
  • phpbrewとphpenvというものが見つかった。
  • 適当に2つ調べてみるとphpenvのinstallでつまずいたときの対処法を書いた記事が多く出てきたため、phpbrew を採用した。

わたしの環境

  • macOS Mojave(10.14.5)
  • homebrew 2.2.1

    インストール方法

    phpbrewのインストール

    こちらからphpbrewのソースをダウンロード (わたしは2019/12/19時点での最新版 phpbrew 1.24.1 をダウンロードしました。)

$ curl -L -O https://github.com/phpbrew/phpbrew/raw/master/phpbrew

ダウンロードしたものを実行できるようにし、環境パスの通っているところに移動します。

$ chmod +x phpbrew
$ sudo mv phpbrew /usr/local/bin/phpbrew
$ phpbrew init

.zshrc(.bashrc)に下記を追加します。

source $HOME/.phpbrew/bashrc

ソースの読み込みます。

source ~/.zshrc

phpbrewの依存パッケージをダウンロード(phpbrew のgit にRequirementのページがあるのでそれを参照してください。)

$ brew install curl pcre mhash libtool icu4c gettext jpeg openssl libxml2 mcrypt gmp libevent

phpのインストール

とりあえず、実行してみてエラーが出たものを片っ端から+zlib=$(brew --prefix zlib)な感じで追加していけばそのうちinstallできました。

$ phpbrew install 5.6.40 +openssl=$(brew --prefix openssl) +zlib=$(brew --prefix zlib)

使うphpのversionを指定します。

$ phpbrew use 5.6.40

参考にしたページ * https://qiita.com/wisteria3221/items/4aa5ed1a9d106c3f7990 * https://qiita.com/da-sugi/items/60b3c7a6ba1634f3341c

PHPの起動する環境を作る

とりあえず、ApacheMysqlが必要らしい。

MAMPとは

macOSまたはWindows上で動作するWebデベロッパー向けのツール。ApacheMySQLPHPを一式管理してくれるものになります。アプリ内のhtdocsにphpのファイルを置いて起動ボタンを押せばlocalhostで出力が見れるので、学び始めに使うにはすごいちょうど良かったと思います。

ダウンロードした url →
https://pc-karuma.net/mamp-install-mac/

phpのバージョンが古くて選択できないとき

MAMPのところからphpのファイルを新しいのを適当に_とかつけて認識できないようにします。 すると再起動後選べるようになります。

PHPの仕組み

PHP<?phpから始まり、?>で終わります。 ファイルを解析してこのタグを探します。これに囲まれていないところはPHPパーサに無視されるので、あらゆるドキュメント内に埋め込むことができます。 * phpのみのファイルは>をつけない方が良いそう。空白や改行に弱く、終了タグ後にそれらがあるとおかしくなったりするそうです。

構文について

  • <?=<?php echoを意味します 。<?=の記法は5.4.0から使えるようになりました。
  • コメントアウト //#でできます。部分的コメントアウト/* */コメントアウトしたものはsourceとして画面には送られてきません。
  • 一つのタグ内で分は完結しなければならないわけではありません。むしろ一度書いた変数はそのファイル内ではいくらでも呼び出せます。
  • 文の終わりには;をつけましょう。
<?php
    echo "hello";
?>

このように。

変数

変数の宣言方法は

<?php
    $var = TRUE;
?>

と書きます。

この変数を呼び出すときは、同様に

$var

と書きます。 変数名は大文字小文字を区別します。使える文字は0-9,a-zA-Z,_となっています。ただし数字始まりはNGです。

参照の代入については&をつけることでできるようになる。注意点としては、名前のある変数のみが参照による代入ができるという点です。

    <?php
    $foo = 'Bob';              // 値'Bob'を$fooに代入する。
    $bar = &$foo;              // $fooを$barにより参照
    $hoge = $foo;              // $fooを$barの値を参照
    $bar = "My name is $bar";  // $barを変更...
    echo $bar;
    echo $foo;                 // $fooも変更される。
    echo $hoge;                // 変更されていない。
    ?>

スコープ

  • ローカルスコープ : メソッドやクラスを書いたときのその中身のこと。それらの中では、グローバルスコープのものは通常通り参照することはできません。
  • グローバルスコープ : ファイル直のところ。メソッドやクラス内ではないところ。このスコープはファイル内で他のファイルを読み込んだ場合、グローバルの領域は共通であり、相互にグルーバル変数を読み取ルことができます。

ローカルスコープでグローバル変数を使いたいときは以下のように書くことで使えるようになります。 使いたい場合は、global $b;と表記することで、使えるようになります。書き換えも同様にできます。スーパーグローバルである$GLOBALSを使用することでもこれは可能です。

    <?php
    $a = 1;
    function test1()
    {
        echo $a;
        $a = 2;
    }


    function test2()
    {
        $a = 2;
        echo $a;
    }

    function test3()
    {
        global $a;
        echo $a;
        $a = 2;
    }

    $b = 3;
    function test4()
    {
        echo $GLOBALS['b'];
        $GLOBALS['b'] = 4;
    }


    test1(); // 何も出ない
    echo $a; // 1
    test2(); // 2
    echo $a; // 1
    test3(); // 1
    echo $a; // 2
    test4(); // 3
    echo $b; // 4
    ?>

静的変数

staticをつけることで、ローカルスコープ内でクラスプロパティと同等に扱えるようになります。

<?php
function test()
{
    static $a = 0;
    echo $a;
    $a++;
}
test(); // 0
test(); // 1
test(); // 2
?>

グローバル変数と静的変数のリファレンス

ただし、リファレンス変数の修正子としてstatic、globalは実装されているため、これらに&をつけた参照を渡すと想定通りには動かないので、つけないようにしましょう。 試した感じ、globalの方は普通に代入できているようでした。 (version 5.6.40 )

<?php
    function test_global_ref() {
        global $obj;
        $obj = &new stdclass;
    }

    function test_global_noref() {
        global $obj;
        $obj = new stdclass;
    }

    test_global_ref();
    var_dump($obj);
    test_global_noref();
    var_dump($obj);


    function get_instance_ref()
    {
        static $obj;

        echo 'Static object: ';
        var_dump($obj);
        if (!isset($obj)) {
            // Assign a reference to the static variable
            $obj = &new stdclass;
        }
        $obj->property++;
        return $obj;
    }

    function get_instance_noref()
    {
        static $obj;

        echo 'Static object: ';
        var_dump($obj);
        if (!isset($obj)) {
            // Assign the object to the static variable
            $obj = new stdclass;
        }
        $obj->property++;
        return $obj;
    }

    $obj1 = get_instance_ref();
    $still_obj1 = get_instance_ref();
    $obj2 = get_instance_noref();
    $still_obj2 = get_instance_noref();

    ?>

可変変数

変数名を変数として持つことができます。以下のように、変数名を足したり、リストで持てたりできるので便利かもしれないですね。

<?php
class foo {
    var $bar = 'I am bar.';
    var $arr = array('I am A.', 'I am B.', 'I am C.');
    var $r   = 'I am r.';
}

$foo = new foo();
$bar = 'bar';
$baz = array('foo', 'bar', 'baz', 'quux');
echo $foo->$bar . "\n";
echo $foo->{$baz[1]} . "\n";

$start = 'b';
$end   = 'ar';
echo $foo->{$start . $end} . "\n";

$arr = 'arr';
echo $foo->{$arr[1]} . "\n";

?>

外部からの変数

htmlフォームからデータにアクセスする方法は二種類だけです。

<?php
echo $_POST['username'];
echo $_REQUEST['username'];
?>

bool

  • bool値の表記は、TUREまたはFALSEです。

また、下記のものは全てFALSEとみなされます。 * integer の 0 および -0 (ゼロ) * float の 0.0 および -0.0 (ゼロ) * 空の文字列、 および文字列の "0" * 要素の数がゼロである 配列 * 特別な値 NULL (値がセットされていない変数を含む) * 空のタグから作成された SimpleXML オブジェクト

cast は (bool) $varなどでもできますが、もっと簡単な方法があります。

<?php
    !!$var;
?>

と書くことです。!が一つであると、$varをboolにキャストした結果のNOTになってしまいますが、二つつけることで戻すということをしています。

整数

integerおよびfloatの上限はプラットフォーム依存です。 integerからfloatに変換するときにfloatの限界値を超えている場合は、undefinedとなる。 また、未知の端数を integer にキャストすると、予期しない結果となることがあります。

<?php
   echo (int) ( (0.1+0.7) * 10 ); // 7が出力されます!
?>

怖い。。。 実はこれPCの弱いところで7.999999...という数値になっていることが原因。お手軽な回避方法は以下。一回文字列に変換することです!?!?

<?php
   echo (int)(string) ((0.1 + 0.7) * 10); // 8
?>

文字列

文字列を指定する最も簡単な方法は、'で括ることです。この引用符をリテラルとして指定する場合は、バックスラッシュでエスケープする必要があります。また、バックスラッシュをリテラルとしてしているには、バックスラッシュを二つ書きます。 ただこの'で囲うときは改行の\nなどは認識できずそのまま出てきてしまいます。

このような特殊な文字列を認識させたい場合は"を使いましょう。ただしこちらも認識できる表記は限られています。

ヒアドキュメント

文字列を区切る方法としてヒアドキュメント構文<<<があります。この後に適当なIDを指定し、改行した後からの文字列を認識します。文字列の終端には、次行に先ほどのIDを書きます。 非常に重要なことですが、終端 ID がある行には、セミコロン (;) 以外の他の文字が含まれていてはならないことに注意しましょう。

<?php
$bar = <<<EOT
bar,
EOT;
?>
文字の組み込み

単純に変数をそのまま出力する場合は、そのまま変数を書き込めば組み込めますが、複雑になる場合は{}で囲うことで組み込めるようになります。 変数名の終わりを正しく認識してもらいたい場合は、${var}というように囲みましょう。

    <?php
    $great = 'fantastic';

    // うまく動作しません。出力: This is { fantastic}
    echo "This is { $great}";

    // うまく動作します。出力: This is fantastic
    echo "This is {$great}";
    ?>
文字列から数値への変換
<?php
$foo = "10.1times " + 1.0;
echo $foo; // 11.1
?>

配列

phpでは配列はarray()または[]と書くことで作れます。 また配列とはいうものの、pythonでいう辞書、javaではMapと呼ばれているものと同様にkeyを設定することもできる。

<?php
$array = array(
    "foo" => "bar",
    "bar" => "foo",
    100   => -100,
    -100  => 100,
);
var_dump($array);
/*
array(4) {
   ["foo"]=>
   string(3) "bar"
   ["bar"]=>
   string(3) "foo"
   [100]=>
   int(-100)
   [-100]=>
   int(100)
 }

*/
?>

primitiveなものであればkeyとして書くことはできるものの、実際にkeyとなりうるのは文字列またはint型なので、float型で書くと丸められてしまったり、bool値であるとTRUEは1にFALSEは0になってしまいます。 また、文字列であっても"1" はintの1と認識されてしまいます。

<?php
$array = array(
    1    => "a",
    "1"  => "b",
    1.5  => "c",
    true => "d",
);
var_dump($array);
/*
array(1) {
 [1]=> string(1) "d" 
}
*/
?>

もちろんkeyを書かなくても表記できます。その場合は、0始まりで順番に数値が振られていきます。また、部分的に数値のkeyを指定した場合にはその数字から1加算されて続くこととなります。

<?php
$array = array(
         "a",
         "b",
    6 => "c",
         "d",
);
var_dump($array); 
/*
  array(4) {
      [0]=>
      string(1) "a"
      [1]=>
      string(1) "b"
      [6]=>
      string(1) "c"
      [7]=>
      string(1) "d"
    }
*/
?>

要素の追加、削除は以下のように行います。

<?php
$arr = array("a" => 1, "b" => 2);

$arr[] = 56;    // このスクリプトのこの位置に記述した場合、
                // $arr[13] = 56; と同じです
var_dump($arr);

$arr["x"] = 42; // キー"x"の新しい要素を配列に追加します
                
unset($arr[5]); // 配列から要素を削除します

unset($arr);    // 配列全体を削除します
?>

配列の中身を削除してもindexは以前の続きから振られます。

<?php
    // 簡単な配列を生成します。
    $array = array(1, 2, 3, 4, 5);
    print_r($array);

    // 全てのアイテムを削除しますが、配列自体は削除しないでおきます。
    foreach ($array as $i => $value) {
        unset($array[$i]);
    }
    print_r($array);

    // アイテムを追加します(新しい添え字は0ではなく
    // 5となることに注意)
    $array[] = 6;
    print_r($array);

    // 添え字を振りなおします。
    $array = array_values($array);
    $array[] = 7;
    print_r($array);

    ?>

注意事項: 文字列に引用符がないと、未知の定数として見なされます。未定義の定数は同じ名前の文字列に自動的に変換されるのでコードは動作します。

<?php
    $foo = array();
    $foo[bar] = 'enemy';
    print_r($foo); // Array ( [bar] => enemy ) 
    print_r($foo[bar]); // enemy
    ?>

オブジェクト

クラスの定義方法はjsなどとあまり変わらないようです。呼び出しが一風変っていて、$bar->do_foo();と呼び出すようです。javapythonなど多くの言語では$bar.do_foo();.でつなげているから違和感を感じます。

<?php
class foo
{
    function do_foo()
    {
        echo "foo を実行します。";
    }
}

$bar = new foo;
$bar->do_foo();
?>

コールバック

<?php 

// コールバック関数の例
function my_callback_function() {
    echo 'hello world!';
}

// コールバックメソッドの例
class MyClass {
    static function myCallbackMethod() {
        echo 'Hello World!';
    }
}

// タイプ 1: 単純なコールバック
call_user_func('my_callback_function'); 

// タイプ 2: 静的クラスメソッドのコール
call_user_func(array('MyClass', 'myCallbackMethod')); 

// タイプ 3: オブジェクトメソッドのコール
$obj = new MyClass();
call_user_func(array($obj, 'myCallbackMethod'));

// タイプ 4: 静的クラスメソッドのコール (PHP 5.2.3 以降)
call_user_func('MyClass::myCallbackMethod');

// タイプ 5: 相対指定による静的クラスメソッドのコール (PHP 5.3.0 以降)
class A {
    public static function who() {
        echo "A\n";
    }
}

class B extends A {
    public static function who() {
        echo "B\n";
    }
}

call_user_func(array('B', 'parent::who')); // A

// タイプ 6: __invoke を実装したオブジェクトを callable として用いる (PHP 5.3 以降)
class C {
    public function __invoke($name) {
        echo 'Hello ', $name, "\n";
    }
}

$c = new C();
call_user_func($c, 'PHP!');
?>

構文

定数

define()関数を使用することにより、定数を定義することができます。定数は一度定義されると変更、または未定義とすることはできません。また、5.3.0のバージョンからはconstが使えるようになりました。 constによる定数で指定できるものはスカラ一式となっています。arrayもできますが、defineで書くのはバージョン7以上。ただし、リソースを定数として指定はできますが、不具合が発生する可能性があるのでお勧めできません。 定数の名前を動的に得ることもでき、その場合はconstant()を使用することもできます。 ちなみに定数は$なしで書けます。

制御構文

if, for, foreach, while, do..while, switchなども同様に使えます。制御構文の本文は{}の中に書きます。 またphpは、if, while, for, foreach, switchに関する別の構文を提供しています。{:に、 }をそれぞれ endif;, endwhile;, endfor;, endforeach;, endswitch;にすることができます。

if

<?php
// {}で囲う、elseif, else if どちらでも可
if ($var) {
// 内容
}elseif ($var){

} else if ($var){
}else{
    
}
// 一行だけの場合は:でも書ける
if ($var):
    echo "s";
    echo "aa";
endif;
?>

for

<?php
for ($i=0 ; $i<10 ; $i++){
    echo $i;
}

$arr=array(1,2,3,4);
foreach($arr as $value) {
    echo $value;
}

?>

for文の中で値を変更することは基本的にできないが、以下のように&をつけるとできるようになります。ただし、使用後にはunset($arr)を忘れずにしましょう。でないと、最後の要素が書き換えられてしまいます。

    <?php
    $arr = array(1, 2, 3, 4);
    foreach ($arr as &$value) {
        $value = $value * 2;
    }
    // この時点で、$arr は array(2, 4, 6, 8) となります

    // unset($value) しなければ、$value は今でも最後の要素 ($arr[3]) を指したままです

    foreach ($arr as $key => $value) {
        // $arr[3] が、$arr の各要素で上書きされて...
        echo "{$key} => {$value} ";
        print_r($arr);
    }
    // ...つまり、二番目から最後までの値が最後の値にコピーされていきます

    // 出力
    // 0 => 2 Array ( [0] => 2, [1] => 4, [2] => 6, [3] => 2 )
    // 1 => 4 Array ( [0] => 2, [1] => 4, [2] => 6, [3] => 4 )
    // 2 => 6 Array ( [0] => 2, [1] => 4, [2] => 6, [3] => 6 )
    // 3 => 6 Array ( [0] => 2, [1] => 4, [2] => 6, [3] => 6 )
    ?>

配列の要素を読むときに&つけると、$valueの参照先をその配列の要素に変更します。ゆえに、このループが終わった後の$valueの参照先は、配列の最後の要素の参照先となります。 ここで、&をつけないで$valueをforeachで呼び出してしまうと、この配列の最後の要素の参照先に今見ようとしている要素の中身を入れてしまうため、配列の最後の要素が書き換えられてしまうのです。

コード規約

PSR-1 : http://www.infiniteloop.co.jp/docs/psr/psr-1-basic-coding-standard.html

PSR-2 : http://www.infiniteloop.co.jp/docs/psr/psr-2-coding-style-guide.html

  • <?php または <?= で始めましょう。
  • phpだけのファイルには?>は書きません。
  • indent は空白4文字
  • 全てのphpファイルは最後に空行を入れましょう。
  • 改行コードはLF
  • 文字コードUTF-8 (BOMなし)
  • 宣言的なものと副作用のある(echo,require,includeなど)文はファイルを分けましょう。

  • Class, Namespace : StudlyCase(PascalCase)

  • method, property : camelCase
  • const : UPPER_SNAKE_CASE

  • アクセス修飾子は必ずつけましょう。

  • abstract, finalは先頭に、staticはアクセス修飾子後につけましょう。
  • useはnamespaceの後に書きましょう。
  • class, method の開き括弧は改行。その他制御文は改行しません。
  • クロージャの開き括弧は改行しません。
  • 開き括弧、閉じ括弧の後に空白は入れません。
  • else if はelseifと一文字にしましょう。
  • namespace, use, classの間は空行を入れましょう。
  • extendsとimplementsはクラス名と同じ行に定義されなければなりません。
  • switch, caseにて、breakしない場合はコメントをしましょう。
  • php予約語は小文字で使用しなければなりません。true/false/null

自作git hooksをリポジトリを越えて共有する

自作のgit hooksを一つのリポジトリだけでなく他のものでも使いたくて、今まではそれぞれのリポジトリにコピーを持っていていました。ただこの方法だと、編集などが加わるとすべて直さなければいけないことや、というか毎度コピーめんどくさいのです。このような背景から自作のgit hooksテンプレート化しかつシンボリックリンク化することにしました。

作業方法

まずhomeにテンプレートとして使うディレクトリと、リンク先にするhooksのディレクトリを作成します。

$ mkdir ~/.git_template
$ mkdir ~/.git_hooks

ディレクトリの作成ができたら実際にテンプレートにしたいhooksのプログラムを.git_hooksに移動させます。 ここで、他の安全なところでgit initを行い (またはすでに.gitがあるところからでも可)、これによってできた.git/hooksの中身を.git_hooksに移動させておくと新しいhooksを作るときに楽かもです。

# .gitが存在するディレクトリにて
$ cp -r .git/hooks ~/.git_hooks

そして今回のターゲットである自作hooksを.git_hooksに移動させ、実行ができるように権限を変えておきます。

$ chmod 755 `作ったfilepath`

ここまでできたら~/.git_template下にhooksとして配置するようにシンボリックリンクを作成します。

$ ln -s ~/.git_hooks ~./git _template/hooks

シンボリックリンクができたら最後にテンプレートのディレクトリとして登録を行いましょう。

$ git config --global init.templatedir '~/.git_template'

このコマンドを実行すると.gitconfig

[init]
     templatedir = ~/.git_template

が追加されているはずです。

あとは新しく作ったリポジトリで試してみましょう。

まとめ

またひとつ環境改善ができて良かったです。ちょいちょいこういった環境改善をしていければいいですね。。!

多重継承において取得できるメソッドやプロパティはどれ?

pythonでは多重継承ができます。もし同じメソッド、プロパティを持つクラス2つを継承した場合、どちらが取得できるのでしょうか。

下記にこれを検証するプログラムを書きました。

クラス名 継承順番 初期化順番
C A, B A, B
D B, A A, B
E A, B B, A

このようにクラスを作り、継承の順番、初期化順番によって取得できるメソッド、プロパティがどのように変わるか検証します。

class A:
    def __init__(self):
        self.text = "a"

    def say_class_name(self):
        print("A")


class B:
    def __init__(self):
        self.text = "b"

    def say_class_name(self):
        print("B")


class C(A, B):
    def __init__(self):
        A.__init__(self)
        B.__init__(self)

    def print(self):
        self.say_class_name()
        print(self.text)


class D(B, A):
    def __init__(self):
        A.__init__(self)
        B.__init__(self)

    def print(self):
        self.say_class_name()
        print(self.text)


class E(A, B):
    def __init__(self):
        B.__init__(self)
        A.__init__(self)

    def print(self):
        self.say_class_name()
        print(self.text)


if __name__ == '__main__':
    C().print()
    print("----")
    D().print()
    print("----")
    E().print()

これを起動すると以下のように出力が得られます。

A
b
----
B
b
----
A
a

C, Dについて継承する順番を変えたところ、利用しているメソッドが変化しました。このことから優先されるメソッドは、継承の順番で先に書いたものであることがわかります。

C, Eについて初期化する順番を変えたところ、メソッドは変わらなかったがプロパティが最後に書かれたものが取得できました。このことからプロパティについては上書きという考え方が適切でしょう。


ちなみにinitを書かないとB -> A の順で初期化を行なっていることが下記より確認できました。

class C(A, B):
    def print(self):
        self.say_class_name()
        print(self.text)
        
if __name__ == '__main__':
    C().print()
A
a

まとめ

  • プロパティは初期化する順番によって変わり、後に初期化した方が優先されます。デフォルトでは継承した順番で先の方が後に初期化するようになっていそうです。
  • メソッドについては、継承した順番によって変わります。先に継承したものが優先されるようです。

Pythonをつかってドラゴンズの試合結果を取得する

ドラゴンズの試合状況をサクッと取得できるようにしたいということで、野球の試合状況について、試合の状況(1回表、試合前)や対戦相手、得点状況を取得するプログラムを組みました。

今回は下記のyahooスポーツから情報を取得することにしました。 baseball.yahoo.co.jp

必要なもの

  • python3.x
  • beautifulsoup4 (こちらは下記のコマンドで入れて下さい。)
$ pip install beautifulsoup4

BeatuifulSoupの使い方

from bs4 import BeautifulSoup


# web pageから情報を取ってくる
soup = BeautifulSoup(urlopen(url), "html.parser")

# fileから情報を取ってくる
with open("sample.html", "r") as f:
     soup = BeautifulSoup(f, "html.parser")

上記の処理にて対象のhtmlを操作する準備は整ったので、これを使って探索を行います。html.parserのところはparserを指定しています。lxmlの指定もできます。こちらの方が早いらしいです。

BeatuifulSoupのfind, find_all

soup.find('タグ名',id='id名',class_='クラス名',href='url',...)
# href='url'は属性の一例です

大体のものはこのfind、find_allで取って来れます。上記では引数を色々書きましたが、この中のどれかを引数にいれれば動きます。このfindで拾ってきたものについても、最初のBeatifulSoupで作ったもの同様にfind等を用いてその内部を取得することができます。


ここから下は実際にこのサイトのhtmlからどう情報を拾ってくるかの話になります。

yahoo スポーツ野球のurlについて

このsiteではhttps://baseball.yahoo.co.jp/npb/teams/team番号/scheduleというurlになっており、チーム番号を指定することで対象のチームに関する今月のスケジュールが見れるようになっています。

野球チームに関しては下記のようにナンバリングされてました。

{'巨人': 1, 
 'ヤクルト': 2,
 'ロッテ': 9,
 '日本ハム': 8,
 '阪神': 5,
 '中日': 4,
 '西武': 7,
 'オリックス': 11,
 '楽天': 376,
 '広島': 6,
 'ソフトバンク': 12,
 'DeNA': 3}

また、年月についての指定は、上記のurlにクエリをのせることで指定ができました。 https://baseball.yahoo.co.jp/npb/teams/team番号/schedule?date=201905 このように年月を西暦の4桁と月の2桁からなる6桁の数字をdateに入れるようになってました。

今回扱うhtmlについて

全体のhtmlについては長すぎるので、 スポーツナビ - プロ野球 -中日ドラゴンズ - 日程・結果 からDeveloperモードで見て下さい。Macだと option + command + iで出てきます。ここでは対象の日付の情報が書かれた要素だけ載せます。

<td class="gameday home active">
    <table>
        <tbody>
        <tr>
            <td class="game">
                <a href="https://baseball.yahoo.co.jp/npb/game/2019042703/" data-ylk="slk:result;pos:27"
                   data-rapid_p="46">
                    <em class="day">27</em>
                    <dl>
                        <dt>
                            vs.<span class="team pm T"><i>阪神</i></span>
                        </dt>
                        <dd class="yjMS">
                            <span class="score">
                            <strong>5</strong> - 4
                            </span>
                            <span class="status">
                            結果
                            </span>
                        </dd>
                    </dl>
                </a>
            </td>
        </tr>
        <tr>
            <td class="data"><p class="yjXS">ナゴヤドーム</p>
                <ul class="yjSt">
                    <li>勝:<a href="/npb/player/1300063/" data-ylk="slk:win_pit;pos:27" data-rapid_p="47">又吉</a></li>
                    <li>S:<a href="/npb/player/1700101/" data-ylk="slk:sav_pit;pos:27" data-rapid_p="48">鈴木博</a></li>
                </ul>
            </td>
        </tr>
        </tbody>
    </table>
</td>

対象の日付の情報が書かれた要素を取り出すために

<td>で一日ずつの内容が書かれているのでまずはtdの要素一覧を取得します。 そして今日の日付(27日)がかかれた要素<em>の内部のtextと一致するかどうかを見て、一致すればその要素が今日の要素であると判断します。なので、ここでは<td>をfind_allで全件取得をしそれぞれの要素の<em>をfindで取り出して、findの中身が対象の日付であるかどうかをcheckして取り出すようにしました。

from urllib.parse import urlencode
from bs4 import BeautifulSoup


def __get_date_elements(self, _date):
        tds = BeautifulSoup(urlopen(self.url), "html.parser").find_all("td")
        for td in tds:
            em = td.find("em")
            if em is not None:
                if em.text == _date.strftime("%d"):
                    return td

試合があるかどうか

tdのclassにgamedayとあれば試合がある日なようです。一方試合がない日はfillinとなっていました。

def __have_game(self, _date):
    element = self.__get_date_elements(_date)

    return element is not None and 'gameday' in element.get("class") \
            and not self.__get_status(_date) == GameStatus.CALLED_OFF

試合の進行状況

id=yjMSclassを持つ要素中の<span class="status">内に書いてありました。 試合前、中止、結果 の他に1回表、1回裏、、、が入るようです。

def __get_status(self, _date):
    element = self.__get_date_elements(_date)
    span = element.find("span", class_="status")
    if span:
       return span.text.strip()

対戦相手

webページから見ると球団名の記述がないように見えますが、ちょうど球団の画像がある部分に<span class="team">の部分に球団名が入っていたのでそこから取り出します。

def __get_battle_team(self, _date):
    element = self.__get_date_elements(_date)
    if element:
        span = element.find("span", class_="team")
        if span:
            return span.text

得点

得点はid=yjMS<span class=score>から取り出します。

def __get_scores(self, _date):
    pattern = "[0-9]+"

    element = self.__get_date_elements(_date)
    if element:
        span = element.find("span", class_="score")
        if span.text:
            score = re.findall(pattern, span.text)
            return int(score[0]), int(score[1])
    return 0, 0

ニュース

試合がある日にはその日の試合状況に応じたニュース記事がでてきます。あります。リンクがあればそれがそのニュース記事へリンクです。とりあえずaで取得します。

def __get_news_page(self, _date):
        element = self.__get_date_elements(_date)
        if element:
            a = element.find("a")
            if a:
                url = a.get("href")
                return url

このurlからsoupをつくって、ニュースの文章を取り出します。このidがyjSNLiveBattlereviewのところにあります。

def __get_news_text(self, _date):
        news_elem = BeautifulSoup(urlopen(self.__get_news_page(_date)), "html.parser")
        if news_elem:
            div = news_elem.find("div", id="yjSNLiveBattlereview")
            if div:
                return div.text

こんな感じで、それぞれの試合情報を取ってくることができます。 これらをまとめたものが下記になります。

import re
from urllib.parse import urlencode
from urllib.request import urlopen

from bs4 import BeautifulSoup


class BaseballNewsFactory:
    def __init__(self, ):
        self.url = "https://baseball.yahoo.co.jp/npb/teams/4/schedule"
        # self.__news_picker = news_picker

    def create_news(self, _date=None):
        _date = datetime.now() if _date is None else _date

        news = BaseballNews()
        news.my_team = "中日"
        news.date = _date
        news.status = self.__get_status(_date)

        if news.have_game():
            news.news_text = self.__get_news_text(_date)
            news.my_team_point, news.battle_team_point = self.__get_scores(_date)
            news.battle_team = self.__get_battle_team(_date)
        return news

    def __get_date_elements(self, _date):
        year_month = _date.strftime('%Y%m')
        elements = BeautifulSoup(urlopen("{}?{}".format(self.url, urlencode({"date": year_month}))))
        tds = elements.find_all("td")
        for td in tds:
            em = td.find("em")
            if em is not None:
                if em.text == _date.strftime("%d"):
                    return td

    def __get_status(self, _date):
        element = self.__get_date_elements(_date)
        if 'gameday' in element.get("class"):
            span = element.find("span", class_="status")
            if span:
                return span.text.strip()
        else:
            return "試合なし"

    def __get_scores(self, _date):
        pattern = "[0-9]+"

        element = self.__get_date_elements(_date)
        if element:
            span = element.find("span", class_="score")
            if span.text:
                score = re.findall(pattern, span.text)
                return int(score[0]), int(score[1])
        return 0, 0

    def __get_news_page(self, _date):
        element = self.__get_date_elements(_date)
        if element:
            a = element.find("a")
            if a:
                url = a.get("href")
                if url:
                    return BeautifulSoup(urlopen(url))

    def __get_news_text(self, _date):
        news_elem = self.__get_news_page(_date)
        if news_elem:
            div = news_elem.find("div", id="yjSNLiveBattlereview")
            if div:
                return div.text

    def __get_battle_team(self, _date):
        element = self.__get_date_elements(_date)
        if element:
            span = element.find("span", class_="team")
            if span:
                return span.text


from datetime import datetime


class BaseballNews(object):
    def __init__(self):
        self.__my_team = ""
        self.__battle_team = ""
        self.__my_team_point = 0
        self.__battle_team_point = 0
        self.__status = ""
        self.__news_text = ""
        self.__date = None

    @property
    def my_team(self):
        return self.__my_team

    @my_team.setter
    def my_team(self, value):
        self.__my_team = value

    @property
    def battle_team(self):
        return self.__battle_team

    @battle_team.setter
    def battle_team(self, value):
        self.__battle_team = value

    @property
    def my_team_point(self):
        return self.__my_team_point

    @my_team_point.setter
    def my_team_point(self, value):
        if isinstance(value, int):
            self.__my_team_point = value

    @property
    def battle_team_point(self):
        return self.__battle_team_point

    @battle_team_point.setter
    def battle_team_point(self, value):
        if isinstance(value, int):
            self.__battle_team_point = value

    @property
    def status(self):
        return self.__status

    @status.setter
    def status(self, value):
        if isinstance(value, str):
            self.__status = value

    @property
    def news_text(self):
        return self.__news_text

    @news_text.setter
    def news_text(self, value):
        if isinstance(value, str):
            self.__news_text = value

    @property
    def date(self):
        return self.__date

    @date.setter
    def date(self, value):
        if isinstance(value, datetime):
            self.__date = value

    def issue(self):
        return self.my_team_point > self.battle_team_point

    def have_game(self):
        return not (self.status == "試合なし"
                    or self.status == "中止")


if __name__ == '__main__':
    factory = BaseballNewsFactory()
    news = factory.create_news()
    print(news.date)
    print("{} vs {}".format(news.my_team, news.battle_team))
    print(news.status)
    print("{} - {}".format(news.my_team_point, news.battle_team_point))
    print(news.news_text)

これを実行すること、こんな出力が得られます。

2019-05-11 19:41:44.993285
中日 vs 阪神
結果
5 - 1

5月11日(土)阪神 vs. 中日 8回戦
中日は初回、高橋と阿部の連続適時打で3点を先制する。その後は8回表に、高橋の適時打で加点すると、9回には福田のソロが飛び出し、リードを広げた。投げては、先発・柳が8回5安打無失点の快投で今季3勝目。敗れた阪神は、打線が7安打1得点とつながりを欠いた。

勝ってる!!柳!!!

まとめ

今回のコードを使ってalexaちゃんでスキルを作ったりしました。ソースコードは下記になります。

github.com

対象の日付について書かれた要素を取り出せば一度の取得でいろいろ取り出せると思いましたが、一回読み出すとclassの情報が抜けてしまい取り出せませんでした。とりあえず毎度アクセスで対処しました。原因は不明です。。。

git rebaseが捗るオプション紹介

git rebaseでfixupやsquashをよくつかうわたしが出会って感動したオプションたちを紹介してこうと思います。

rebase.autostash

git rebase を行うとき、編集差分がある状態では実行できないため、stashして下さいてきなコメントが出てやり直しとなってしまうのが、なんとも面倒臭いと思うのです。

これを払拭するのがautostashです。

git rebase を実行した際に自動でgit stashを行い、rebaseの作業に移ります。そして、rebase作業が終わった際にはstashしたものを戻すgit stash popまでを自動でやってくれるというものです。ぜひ、おすすめしたい、、!

rebase.abbreviateCommands

git rebase -i で作業するとき、毎度出てくるこの画面。

pick 5f818fa edit file
pick cd4e4ab delete file

# Rebase 24029a2..cd4e4ab onto 24029a2 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.

わたしの場合、fixupをよく使いますが、これを行うためにはpickの文字を消してから、fを入力しなければなりません。これがちょっと面倒臭いと思うのです。

そこでこのabbreviateCommandsを使います。

これにより、pick が p となり編集がより楽になります。

pにカーソルを合わせたらrfを入力してpをfに変更して終了!とやるのが私流です。

p 5f818fa edit file
p cd4e4ab delete file

# Rebase 24029a2..cd4e4ab onto 24029a2 (2 commands)
・・・

rebase.autosquash

fixupやsquashをよく使う人におすすめなオプションです。 このオプションはコミットメッセージの文頭にfixup!またはsquash!がついたものコミットを対象にgit rebase -i画面においてあらかじめfixupやsquashにしてくれます。

$git rebase -i head~~ --autosquash

p 5f818fa edit file
f f540190 fixup! edit file

# Rebase 24029a2..f540190 onto 24029a2 (2 commands)
・・・

autosqaushのオプション追加によりこの画面におけるデフォルトのpick(p)ではなくfixup(f)の状態で画面がでてきたのがわかります。

ここまでautosqaushの説明をしてきましたが、コミットの際にわざわざfixup!とつけたコミットメッセージを書かなければ効果を発揮しないのが難点です。。。

ここでfixup!やsquash!を簡単につける方法があるのでぜひ紹介したいと思います。 git commit --fixup <コミットの指定>です。 コミットの指定についてはコミットidや、head~などが使えます。

$ git commit --fixup head
[master f540190] fixup! edit file
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git log --oneline
2285367 (HEAD -> master) fixup! edit file
ab0d701 edit file
24029a2 create file

デフォルトに設定しよう

このオプションたちはデフォルト設定にしておくととても便利。

$ git config --global rebase.autostash true
$ git config --global rebase.abbreviateCommands true
$ git config --global rebase.autosqaush true

これらを実行するとhome下の.gitconfigファイルの中身に下記が追加されます。

[rebase]
        autosqaush = true
        abbreviateCommands = true
        autostash = true

まとめ

gitはもどかしいと思うポイントは大体対策されてるんだなとrebaseのoptionたちから感じることができました。もどかしいと思ったらとりあえずオプションを探しにいく、これを続けてより効率の良い開発ができるようになりたいです。

もともとわたしはgit commit に関してはIDEIntelliJさんに頼ってるのですが、3番目に書いたgit commit --fixupに関してはサポートされてないなーと思ったら、今年でた2019.1で追加されたみたいです。うれしい。

slackアプリで送信したurlを展開する

f:id:xmadoka:20190227235654p:plain

slackにてurlを貼ってメッセージを送信すると自分のアカウントではurl展開されてページの概要のようなものが見れるのに、incoming webhooksから送られたurlは展開されないということに気がつきました。もちろんincoming webhooksでも展開して欲しいので調べてみました。

どうやらこの現象は仕様らしい

By default we unfurl all links in any messages posted by users. For messages posted via incoming webhooks, the chat.postMessage API method or chat.postEphemeral, we will unfurl links to media, but not other links.

ユーザーからの投稿にあるリンクはデフォルトでは全て展開する。chat.PostMessage もしくはchat.postEphemeralを使ったincoming webhooksのmediaへのリンクは展開しようとするが、他はしない。とのこと。

参照元: Unfurling links in messages | Slack

リンクを展開させる

slackにpostするときにtextに加え、unfurl_linkをtrueにして送るとリンクを展開するようになる。

{
    "text": "<https://api.slack.com>",
    "unfurl_links": true
}

これによりさきほどと同様のリンクが展開するように!

f:id:xmadoka:20190227235702p:plain

mediaについても試してみた

mediaはデフォルトで展開されるそうなので試してみた。(ここでいうmediaは動画や画像、ツイートなど)

{
    "text": "https://www.youtube.com/watch?v=ci7eOVfYBfA"
}

f:id:xmadoka:20190228222328p:plain 逆に消したいときは、unfurl_mediaをfalseにて送ることで、展開されなくなる。

{
    "text": "https://www.youtube.com/watch?v=ci7eOVfYBfA",
    "unfurl_media": false
}

まとめ

今回は展開するだけでしたが、調べてるうちにslackのメッセージにボタンつけることができたりなど、面白そうなことがいろいろできそうなことを知りました。今度機会があるときにでも遊んでみたいです。

「DNSをはじめよう」を読んでまとめてみた

mochikoAsTechさんの書いた「DNSをはじめよう」を読んで自分なりにまとめてみました。

ドメインの取得

ドメインの伝搬

レジストリ -> レジストラ -> リセラ

この関係性からレジストラからの登録が一番信頼できる。 この書籍に載ってた事業者種類とサイトとの対応づけは下記の表の通り。

事業者種類 事業者名一覧
レジストラ お名前.com、ゴンベイドメイン
リセラ 名付けてねっと、Yahoo!ドメインムームードメインVALUE DOMAIN

レジストリの役割

TLD(top level domain)* を管理すること。1つのTLDは1レジストリによって管理されている。 同じドメインが複数存在するとルーティングができなくなってしまうので、それを引き起こさないために一つのTLDは一つのレジストリに一元管理されている。

*: TLDとは「example.co.jp」、「yahoo.com」などの一番右側 jp、comを指す。

ドメイン取得のとき

  • 色々と買わせようとしてきたりするので根気強く
  • ドメインの自動更新は早めに切っておいた方がよい
  • Whois*の登録はしなければならない

*: Whoisとはドメインの所有者の氏名、連絡先などを公開しているサービス。 何らかのトラブルがあったときにドメイン所有者同士で解決してほしいためこういうサービスが存在する。(むしろそうしないとレジストリ側の処理数がやばいことになる)

DNS

フルリゾル

ドメインに紐づくIPアドレスを調べてくるもの。 「DNSキャッシュサーバ」、「フルサービスリゾルバ」とも呼ばれる。 TLDから順に辿ってIPアドレスを取得してくる。

個人的には電話番号を解釈するときの思考に似てるなと思った。

+81 03 xxxx xxxx とあったら、+81は日本 -> 日本の03は東京 ...みたいな。

TTL (time to live)

フルリゾルバにはキャッシュがある。ドメインに紐づくIPアドレスを一回取得したら、TTLとして登録されている時間分だけその情報を残しておく。そのため、ドメインに紐づくIPアドレスが変更してすぐにそれをすぐ自分が参照したいと思ったら、自分の利用しているフルリゾルバのTTLを一時的に短くするまたはクリアするなどの必要がある。

リソースレコード

リソースレコードのタイプ 値の意味
Aレコード ドメインに紐づくIPアドレス
NSレコード ドメインのゾーンを管理するネームサーバ
MXレコード ドメインに紐づくメール受信サーバ
TXT(SPF) このドメインのメール送信元サーバ
SOA ドメインのゾーンの管理情報
CNAME このドメインの別名でのリソースレコードの参照先

コマンドで調査

whoisというコマンドを使って、ドメインに紐づくwhoisの情報を取得することができる。

$ whois ドメイン名

【 whois 】コマンド――ドメイン情報を表示する:Linux基本コマンドTips(159) - @IT

digコマンドを使ってa,ns,mx,txt,soaなどの情報を指定して取得できる。指定しなくてもドメインに紐づくIPアドレスは取得できるが情報量多めになる。

$ dig ドメイン名 [クエリタイプ]
# クエリタイプにaやnsなどをいれる
$ dig ドメイン名 [クエリタイプ] +short
# +shortをいれるとドメインに紐づくIPアドレスの一覧だけが取得できる。

【 dig 】コマンド――ドメイン名からIPアドレスを調べる:Linux基本コマンドTips(158) - @IT

迷惑メール判定について

一つ目は必須だが、二つ目は受信サーバによってはこの条件があるという感じらしい。とりあえず上記2つを満たしていれば、いまのところ迷惑メール判定はされない。

まとめ

今回はこの本を読んでDNS周りで新しく得られた知識について書きました。ここにはまとめていませんが、実際のドメインの取得方法やDNS(Route53)への登録方法など、実践的な内容も細かに説明されているので、この本があれば自分のサイトをサクサク作れそうです。次はAWSをはじめようを読み進めていく予定です!