キャントストップ・エクスプレスで進める確率を計算してみる

ボードゲームアリーナでも遊べるボードゲーム、キャントストップ・エクスプレス(Can't Stop Express)の効率の良いプレイを目指し、ダイスの出目の確率を計算するプログラムを作成して検討してみた。

boardgamearena.com

キャントストップ・エクスプレス(Can't Stop Express)とは

大まかなゲームの流れは以下の通り。

  1. ダイスを5個振る
  2. 5つのダイスを「ダイス2つのペア2組」と「第5のダイス」に分ける
  3. 2.の組み合わせから1つを選びスコアパッドに記入する
  4. 記入ができなくなったら、そのプレイヤーはゲーム終了
  5. 全プレイヤーがゲーム終了した際に、スコアが最も高いプレイヤーが勝利

スコアパッドには「ダイス2つのペアの合計」と「第5のダイス」をそれぞれ記入していく。例えば上の画像の場面で、(第5のダイス, ペアの合計1, ペアの合計2) = (3, 8, 8) を選択した場合、スコアパッドには以下のように記録される。

これを繰り返していき、「第5のダイス」の8番目のマスが埋まったらそのプレイヤーはゲーム終了。点数計算はスコアパッドの各行に対して以下の要領で点数を出し、合計値がプレイヤーの得点となる。

  • 0個:0点
  • 1~4個:マイナス200点
  • 5個:0点
  • 6~10個:各数字に応じた得点(A列に記載の点数)×右側にチェックされた数(チェック数-5)

より詳細なルールは以下のページから。

ja.doc.boardgamearena.com

個人的に取っている戦略

確率計算の前に、個人的に取っている戦略を書いてみる。前提として考えることは以下の2つ。

  1. マイナス200点は得られるプラス点1マス分に比べて大きい
  2. ダイス2つの出目の合計は7に近いほど確率が高い

いずれも書いてみると当たり前のことではあるが、これらから以下の戦略をとる。

  1. 第5のダイスとして (1,2,6), (1,5,6) の組み合わせを選ぶ
  2. 「ダイス2つのペア」の合計として 5〜9 の範囲に収まるものを優先的に選ぶ
  3. スコアが伸ばせる場面であっても、マイナス点によるデメリットが大きいと考えられる場合は早めにゲーム終了する

1. 第5のダイスとして (1,2,6), (1,5,6) の組み合わせを選ぶ

1〜6が出る確率は等しいので、5つのダイスが振られた時点で「外れ値」となる可能性が高い1と6の目を除いておきたいという考えによる。なぜかというと、高めもしくは低めの出目のみを残した場合、より確率が低い合計値の列を選択する必要に迫られる可能性が高くなってしまうためである。そうすると、(2.とも関連してくるが)選択する合計値の列が分散する可能性が高くなり、結果としてマイナス点が残ってしまうことになってしまう。

そのため第5のダイスとしては、(1,2,6), (1,5,6) のいずれかの組み合わせを選ぶ。最悪の場合でも (1,2,3), (4,5,6) に留め、その場合は選択する出目を高めもしくは低めに少し寄せて対応する。

ただ、第5のダイスを除いた後のダイスの目が (小, 小, 大, 大) の4つであれば、(小, 大), (小, 大) の2組にしてしまえば7に近い合計値を作ることができ、1と6の目を除いた場合とそれほど変わらないのでは?という可能性は十分に考えられる。この辺りは後の確率計算で確かめてみたい。

2. 「ダイス2つのペア」の合計として 5〜9 に収まるものを優先的に選ぶ

第5のダイスの8番目のマスが埋まってしまうとゲーム終了となるので、基本的に最大ターン数は 7 x 3 + 1 = 22ターン*1となり、スコアパッドで埋められるマスは 22 x 2 = 44マスまでとなる。従って、9種類以上の合計値を選択してしまうと、すべてを0点に戻すためのマス数は 5 x 9 = 45マスとなることから、必ずどれかはマイナスが発生してしまうことが確定する。

また、プラス点を得るためには同じ合計値を6回以上選択する必要があるため、スコアを伸ばしていくためには極力選択する出目は絞るようにしたい。そのため、出る確率の高い5〜9の出目をまずは進めていき、必要に応じて他の出目も進めていくか、あるいは2や12などの低確率な出目を「捨て列」として割り切って処理するようにする。個人的な体感としては、合計値の種類は6〜7種類でゲーム終了まで行けると、マイナスのものがない理想的な点数の伸ばし方ができるように思われる。

3. スコアが伸ばせる場面であっても、マイナス点によるデメリットが大きいと考えられる場合は早めにゲーム終了する

終盤になると6, 7, 8あたりの合計値の列はマスが埋まってしまい、その列を選択しても点数が伸びなくなる場合がある。このような場面では、点数が伸びない場合が増えてくるにも関わらず、選択していない合計値を選ばざるを得ないことによるマイナス200点によるリスクが大きくなってしまう。そのため、他のプレイヤーの点数を見ながら早々にゲーム終了を選択するのが有効な場合がある。

個人的な体感としては、600点程度が確保できていればそのまま勝ち逃げできる場合が多いように思われる。当然、相手の点数が伸びているようであれば攻める必要があるが、そうでない場合は早めに切り上げるのも戦略の一つとして使える。

確率計算

上記の戦略として考えていることがどれぐらい有効かの確かめるため、確率を計算してみる。求めたい確率は以下の2つ。

  1. 第5のダイスとして N を選んだ際に、合計値が (X, Y) (X <= Y とする) となるダイス2つのペア2組が選べる確率
  2. 第5のダイスとして N を選んだ際に、合計値として Z を選べる確率

「選べる」確率としているのは、出目の組み合わせに対して選択肢が複数存在しているためである。

例えば、ダイスの出目が (1,2,3,4,6) だった場合に、第5のダイスとして 1 を選んだ場合を考えると、

  • 選べる (X, Y) → (5, 10), (6, 9), (7, 8)
  • 選べる Z → 5, 6, 7, 8, 9, 10

となり、第5のダイスとして 6 を選んだ場合を考えると、

  • 選べる (X, Y) → (3, 7), (4, 6), (5, 5)
  • 選べる Z → 3, 4, 5, 6, 7

となる。これらは選択肢によって独立した事象ではあるが、それぞれ「選べる」場合の数として数え上げているため、第5のダイスについて求めた確率をすべて足し合わせても100%になるわけではない。要約すると、「第5のダイスとして N を選んだ時に、この合計値を選択できる確率がどのぐらいか?」ということを見るために計算をしている。

適当なスクリプトを書いて確率を計算してみる。内容としては以下の通り*2

  1. ダイス d1, d2, d3, d4, d5 を振る。
  2. 1つを第5のダイス n、残りをダイス2つのペア2組に分け、合計値の小さい方を x、大きい方を y として、(n, x, y) の組み合わせを作る。
  3. 1.で出た目の組み合わせにおける全ての (n, x, y) の組み合わせを、set に入れて重複排除する。
  4. 重複排除したものを、(n, x, y) ごとに +1 する。これをダイス5つ出目の全パターン {6}^{5} = 7776 通り分行う。
  5. (n, x, y) が選べる組み合わせの数を、全体の組み合わせ数で割って確率を出す。「第5のダイスとして N を選んだ際」の確率のため、全体の組み合わせ数は N が選べる場合の数 {6}^{5} - {5}^{5} = 4651 で計算している。
dice = [1, 2, 3, 4, 5, 6]
count = {}

for n in range(1, 6+1):
  for x in range (2, 12+1):
    for y in range(x, 12+1):
      count[(n, x, y)] = 0

for d1 in dice:
  for d2 in dice:
    for d3 in dice:
      for d4 in dice:
        for d5 in dice:
          tmp = set()
          tmp.add((d1, min(d2+d3, d4+d5), max(d2+d3, d4+d5)))
          tmp.add((d1, min(d2+d4, d3+d5), max(d2+d4, d3+d5)))
          tmp.add((d1, min(d2+d5, d3+d4), max(d2+d5, d3+d4)))
          tmp.add((d2, min(d1+d3, d4+d5), max(d1+d3, d4+d5)))
          tmp.add((d2, min(d1+d4, d3+d5), max(d1+d4, d3+d5)))
          tmp.add((d2, min(d1+d5, d3+d4), max(d1+d5, d3+d4)))
          tmp.add((d3, min(d1+d2, d4+d5), max(d1+d2, d4+d5)))
          tmp.add((d3, min(d1+d4, d2+d5), max(d1+d4, d2+d5)))
          tmp.add((d3, min(d1+d5, d2+d4), max(d1+d5, d2+d4)))
          tmp.add((d4, min(d1+d2, d3+d5), max(d1+d2, d3+d5)))
          tmp.add((d4, min(d1+d3, d2+d5), max(d1+d3, d2+d5)))
          tmp.add((d4, min(d1+d5, d2+d3), max(d1+d5, d2+d3)))
          tmp.add((d5, min(d1+d2, d3+d4), max(d1+d2, d3+d4)))
          tmp.add((d5, min(d1+d3, d2+d4), max(d1+d3, d2+d4)))
          tmp.add((d5, min(d1+d4, d2+d3), max(d1+d4, d2+d3)))

          for s in tmp:
            count[s] += 1

for con in count:
  print(con, count[con], '{:.4f}%'.format(count[con]*100/((6**5)-(5**5))))

2.の合計値として Z を選べる確率については、上記スクリプトで出力した内容を Google Spreadsheet で集計する形で計算した*3

1. 第5のダイスとして N を選んだ際に、合計値が (X, Y) となるダイス2つのペア2組が選べる確率

第5のダイスごとに、合計値が (X, Y) となる確率を表にまとめてみた。同じ形式の大きめの表が並んでしまい見にくかったため折りたたんでおり、以下それぞれの表のタイトル部分をクリックすると表示される。

第5のダイスとして 1 を選んだ場合

X\Y 2 3 4 5 6 7 8 9 10 11 12
2 0.022% - - - - - - - - - -
3 0.108% 0.215% - - - - - - - - -
4 0.323% 0.645% 0.968% - - - - - - - -
5 0.538% 1.075% 2.150% 2.150% - - - - - - -
6 0.753% 1.720% 3.655% 4.730% 4.193% - - - - - -
7 0.968% 2.365% 4.730% 6.235% 8.600% 6.665% - - - - -
8 1.075% 2.580% 4.945% 6.665% 9.245% 10.750% 6.558% - - - -
9 0.860% 2.580% 4.515% 5.805% 7.525% 8.385% 8.170% 3.870% - - -
10 0.645% 1.935% 3.870% 5.160% 6.020% 6.880% 6.665% 4.300% 2.043% - -
11 0.430% 1.290% 2.580% 3.870% 4.515% 4.515% 3.870% 2.580% 1.720% 0.645% -
12 0.215% 0.645% 1.290% 1.935% 2.580% 2.795% 2.365% 1.720% 1.075% 0.430% 0.108%

第5のダイスとして 2 を選んだ場合

X\Y 2 3 4 5 6 7 8 9 10 11 12
2 0.108% - - - - - - - - - -
3 0.215% 0.215% - - - - - - - - -
4 0.645% 0.753% 1.097% - - - - - - - -
5 1.075% 1.075% 2.473% 2.150% - - - - - - -
6 1.720% 1.720% 3.763% 5.160% 4.193% - - - - - -
7 2.365% 2.365% 4.838% 6.235% 9.245% 6.665% - - - - -
8 2.580% 2.365% 4.623% 6.020% 7.525% 9.245% 4.193% - - - -
9 2.580% 2.580% 4.730% 5.805% 7.525% 8.385% 6.235% 3.870% - - -
10 1.935% 1.935% 4.515% 4.515% 6.235% 6.665% 5.375% 4.300% 2.043% - -
11 1.290% 1.290% 3.010% 3.870% 3.870% 4.515% 3.225% 2.580% 1.720% 0.645% -
12 0.645% 0.645% 1.505% 1.935% 2.580% 2.365% 2.150% 1.720% 1.075% 0.430% 0.108%

第5のダイスとして 3 を選んだ場合

X\Y 2 3 4 5 6 7 8 9 10 11 12
2 0.108% - - - - - - - - - -
3 0.430% 0.645% - - - - - - - - -
4 0.860% 1.075% 0.968% - - - - - - - -
5 1.075% 1.935% 2.580% 2.150% - - - - - - -
6 1.935% 3.010% 3.978% 5.053% 4.752% - - - - - -
7 2.365% 3.870% 4.730% 6.880% 9.353% 6.665% - - - - -
8 2.580% 3.870% 4.085% 6.020% 8.278% 9.030% 4.193% - - - -
9 1.935% 3.870% 3.655% 4.300% 5.698% 6.880% 5.375% 2.150% - - -
10 1.935% 3.870% 3.870% 4.515% 6.235% 6.880% 5.160% 3.010% 2.043% - -
11 1.290% 2.580% 2.580% 3.870% 4.300% 3.870% 3.225% 1.935% 1.720% 0.645% -
12 0.645% 1.290% 1.290% 1.935% 2.795% 2.365% 1.720% 1.505% 1.075% 0.430% 0.108%

第5のダイスとして 4 を選んだ場合

X\Y 2 3 4 5 6 7 8 9 10 11 12
2 0.108% - - - - - - - - - -
3 0.430% 0.645% - - - - - - - - -
4 1.075% 1.720% 2.043% - - - - - - - -
5 1.505% 1.935% 3.010% 2.150% - - - - - - -
6 1.720% 3.225% 5.160% 5.375% 4.193% - - - - - -
7 2.365% 3.870% 6.880% 6.880% 9.030% 6.665% - - - - -
8 2.795% 4.300% 6.235% 5.698% 8.278% 9.353% 4.752% - - - -
9 1.935% 3.870% 4.515% 4.300% 6.020% 6.880% 5.053% 2.150% - - -
10 1.290% 2.580% 3.870% 3.655% 4.085% 4.730% 3.978% 2.580% 0.968% - -
11 1.290% 2.580% 3.870% 3.870% 3.870% 3.870% 3.010% 1.935% 1.075% 0.645% -
12 0.645% 1.290% 1.935% 1.935% 2.580% 2.365% 1.935% 1.075% 0.860% 0.430% 0.108%

第5のダイスとして 5 を選んだ場合

X\Y 2 3 4 5 6 7 8 9 10 11 12
2 0.108% - - - - - - - - - -
3 0.430% 0.645% - - - - - - - - -
4 1.075% 1.720% 2.043% - - - - - - - -
5 1.720% 2.580% 4.300% 3.870% - - - - - - -
6 2.150% 3.225% 5.375% 6.235% 4.193% - - - - - -
7 2.365% 4.515% 6.665% 8.385% 9.245% 6.665% - - - - -
8 2.580% 3.870% 6.235% 7.525% 7.525% 9.245% 4.193% - - - -
9 1.935% 3.870% 4.515% 5.805% 6.020% 6.235% 5.160% 2.150% - - -
10 1.505% 3.010% 4.515% 4.730% 4.623% 4.838% 3.763% 2.473% 1.097% - -
11 0.645% 1.290% 1.935% 2.580% 2.365% 2.365% 1.720% 1.075% 0.753% 0.215% -
12 0.645% 1.290% 1.935% 2.580% 2.580% 2.365% 1.720% 1.075% 0.645% 0.215% 0.108%

第5のダイスとして 6 を選んだ場合

X\Y 2 3 4 5 6 7 8 9 10 11 12
2 0.108% - - - - - - - - - -
3 0.430% 0.645% - - - - - - - - -
4 1.075% 1.720% 2.043% - - - - - - - -
5 1.720% 2.580% 4.300% 3.870% - - - - - - -
6 2.365% 3.870% 6.665% 8.170% 6.558% - - - - - -
7 2.795% 4.515% 6.880% 8.385% 10.750% 6.665% - - - - -
8 2.580% 4.515% 6.020% 7.525% 9.245% 8.600% 4.193% - - - -
9 1.935% 3.870% 5.160% 5.805% 6.665% 6.235% 4.730% 2.150% - - -
10 1.290% 2.580% 3.870% 4.515% 4.945% 4.730% 3.655% 2.150% 0.968% - -
11 0.645% 1.290% 1.935% 2.580% 2.580% 2.365% 1.720% 1.075% 0.645% 0.215% -
12 0.215% 0.430% 0.645% 0.860% 1.075% 0.968% 0.753% 0.538% 0.323% 0.108% 0.022%

2. 第5のダイスとして N を選んだ際に、少なくとも合計値 Z が選べる確率

表にまとめると以下のようになる。

N\Z 2 3 4 5 6 7 8 9 10 11 12
1 5.93% 15.16% 29.67% 40.31% 53.54% 62.89% 62.89% 50.31% 40.31% 26.45% 15.16%
2 15.16% 15.16% 31.95% 40.31% 53.54% 62.89% 53.54% 50.31% 40.31% 26.45% 15.16%
3 15.16% 26.45% 29.67% 40.31% 55.39% 62.89% 53.54% 40.31% 40.31% 26.45% 15.16%
4 15.16% 26.45% 40.31% 40.31% 53.54% 62.89% 55.39% 40.31% 29.67% 26.45% 15.16%
5 15.16% 26.45% 40.31% 50.31% 53.54% 62.89% 53.54% 40.31% 31.95% 15.16% 15.16%
6 15.16% 26.45% 40.31% 50.31% 62.89% 62.89% 53.54% 40.31% 29.67% 15.16% 5.93%

縦軸に確率、横軸に合計値 Z をとってグラフにしてみると以下のようになる。7を中心とした線対称の綺麗なグラフになった。

考察

確率の表とグラフを見て、個人的に取っている戦略がどのぐらい有効かを検討してみる。

1. 第5のダイスとして N を選んだ際に、合計値が (X, Y) となるダイス2つのペア2組が選べる確率

どの第5のダイスでも X, Y が7に近いほど確率が高くなる、というのは想定の通りであったが、(7, 7) の組み合わせが最も確率が高くなるわけではなかった。最も選びやすい/選びにくい (X, Y) だけを抽出してみると、以下のようになる。

第5のダイス 選びやすい(X, Y) 確率 選びにくい(X, Y) 確率
1 (7, 8) 10.750% (2, 2) 0.022%
2 (6, 7), (7, 8) 9.245% (2, 2), (12, 12) 0.108%
3 (6, 7) 9.353% (2, 2), (12, 12) 0.108%
4 (7, 8) 9.353% (2, 2), (12, 12) 0.108%
5 (6, 7), (7, 8) 9.245% (2, 2), (12, 12) 0.108%
6 (6, 7) 10.750% (12, 12) 0.022%

第5のダイスにどれを選んだとしても、最も選びやすい (X, Y) は必ず7を含んでおり、もう1つは6または8であったことから、これら3つの列は伸ばしておいてもほぼ確実にマイナスになることはなさそうに思われる。

反対に、(2, 2), (12, 12) が選べる確率は 0.1% 程度と圧倒的に低く、これが選べる場合にはマイナスとなる列を1~2つ程度許容してでも 2 もしくは 12 で多くの加点を狙う、という戦略も選択肢に入ってくるのではないかと思う。

2. 第5のダイスとして N を選んだ際に、少なくとも合計値 Z が選べる確率

第5のダイスとして何を選んでも、7を選べる確率には差がなかった。これは少し意外ではあったが、個人的に取っている戦略の1.でも書いた通り、第5のダイスを除いた後に合計が7となる (小, 大) の出目が残っていれば良いためと考えれば自然なものかと思う。これより、序盤の目が極端に偏っていない限り、合計値7の列を進めておくのはマイナス回避のための保険として良さそうである。

また、合計値が7から1ずつ増減するにつれて、第5のダイスが小さい/大きい方から選べる確率が10%ぐらいずつ差が出てくることが見てとれる。マイナス点回避のために、確率の高い合計値5,6,8,9の選べる確率の高さを考慮すると、1と6を第5のダイスとして選んでおくのは戦略として有効と言えそうである。

1.でも少し触れたが、2 もしくは 12 を伸ばしたい場合は確率 15% 程度に賭ける必要があり、最大ターン数が基本的には22であることを考えると単純計算で約3.3マスしか伸ばせないことになる。このことから、2 もしくは 12 を伸ばすようなプレイで勝ちたい場合は、序盤である程度マス数を確保できている、かつ積極的に 2 もしくは 12 を選択していく必要があり、ある程度出目が偏っている場でないと勝ち切ることは難しいように思う。

まとめ

確率計算をしてみたが、失点回避をメインとしてプレイしたい場合には、7周辺の列に集中させる戦略はある程度間違っていなさそうであることが確認できたかと思う。ただし、(キャント・ストップの際と同じ結論にはなってしまうが)上記はあくまで確率上の話であるため、ゲーム中の出目が偏っていた場合や、勝つためにある程度リスクを取らないといけない場合も多い点には注意が必要である。その辺りの駆け引きのようなものがゲームとしての面白さになってくるように感じる。

日本語版はまだ出ていないようではあるが、ルールは上記の通りであることから英語版でもプレイする分に問題はなさそうであり、ボードゲームアリーナでも手軽に遊ぶことができる*4。この記事がプレイする際に何かしらの参考になれば幸いである。

*1:第5のダイスとして選んだ出目が、5つのダイスの出目に無かった場合はこれより延ばせるが、発生する確率は 1/32 のため2ゲームに1回ぐらいの発生率である

*2:組み合わせなどをうまく考えればパターン数を減らせそうではあるが、自分にとっての計算の分かりやすさ優先で全パターン数え上げる方式で実装した

*3:グラフ化したかったのでこのような方法を取ったのと、自分の python 力のなさによるもの。本来であればスクリプトで集計しやすいよう csv 出力などを工夫すべきかとは思う

*4:ただし、ゲームのテーブルを立てることができるのはボードゲームアリーナプレミアム会員のみ

Homebrew で Subversion をインストールする

以前の記事 で git-svn を Homebrew でインストールしたが、生の Subversion を触る機会が出てきたためこちらも Homebrew でインストールしてみる。

環境

  • macOS Monterey 12.6.3
  • Homebrew 4.2.2

インストール

brew install subversion

これだけ。brew install svn でも同じ実行結果が得られる。

実行確認

subversion ではなく svn コマンドで実行する。

% which svn
/usr/local/bin/svn

% svn --version
svn, version 1.14.3 (r1914484)
   compiled Dec 28 2023, 15:55:09 on x86_64-apple-darwin21.6.0

Copyright (C) 2023 The Apache Software Foundation.
This software consists of contributions made by many people;
see the NOTICE file for more information.
Subversion is open source software, see http://subversion.apache.org/

以下のリポジトリアクセス (RA) モジュールが利用できます:

* ra_svn : svn ネットワークプロトコルを使ってリポジトリにアクセスするモジュール。
  - Cyrus SASL 認証を併用
  - 'svn' スキームを操作します
* ra_local : ローカルディスク上のリポジトリにアクセスするモジュール。
  - 'file' スキームを操作します
* ra_serf : Module for accessing a repository via WebDAV protocol using serf.
  - using serf 1.3.10 (compiled with 1.3.10)
  - 'http' スキームを操作します
  - 'https' スキームを操作します

The following authentication credential caches are available:

* Mac OS X Keychain

コマンドが使えれば良いのであればここまででOK。

Google スプレッドシートで改行を含むセルをコピペしたときのダブルクオーテーションの対処法

Google スプレッドシートで、改行を含むセルをコピーして他のエディタなどにペーストしようとすると、ダブルクオーテーション( " )が文字列の前後に含まれてしまう。

ペースト後に普通に取り除いても良いのだが、複数セルをコピペしたい場合など最初から入っていない方が望ましい場合も多いので、対処できないかを調べてみた。

普通にコピペした場合

例として、スプレッドシートのA1セルには改行を入れた文字列、A2セルには改行を入れていない文字列を入力しておく。

これら2つのセルを選択してコピーし、エディタ(今回は VS Code を利用した)にペーストするとこのようになる。 改行を含む文字列のみ、前後にダブルクオーテーションがついてしまっている。

対処法

コピーしたセルを Google ドキュメントにペーストすることで対処が可能。 Microsoft Word や LibreOffice Writer など同様のワープロソフトでも同様の方法が使える。

単一セルを選択してコピペした場合、ペーストした際にダブルクオーテーションが入っていない文字列のみの状態になる。

複数セルの場合は、一度表形式で貼り付けた後、再度コピペすることで中身を取り出すことができる。 スプレッドシート側でコピーした後、Google ドキュメントでペーストする際に「リンクなしで貼り付け」を選択する。

この表を選択してコピーし、別の場所にペーストすると、表の中身のテキストがダブルクオーテーションなしで取り出すことができる。

多少手間がかかってしまうことと、セルの間の区切り文字が特に無くどこまでが1つのセルだったのかが分からないのは少々難点ではあるが、複数セルのダブルクオーテーションの取り除きの手間と比べると有用なのではないかと思われる。

おまけ

ここまで書いてきたが、セル内にダブルクオーテーションが含まれないことが保証されているのであれば、ペーストした後にエディタでダブルクオーテーションを全件検索→削除するのが一番速いかもしれない。。。

Mac で Python + ChromeDriver を動かすための準備

ChromeDriver は、Chrome ブラウザをプログラムで動かすためのドライバーであり、ブラウザ上でのテストや操作を自動化するために利用されるツール。さまざまな言語で利用できるが、今回は MacPython を使って実行する環境を整備したため、手順と詰まった点を記事にしておく。

環境

python は anyenv でインストールしたものを利用している。

% python --version
Python 3.11.3
% which python
/Users/gumfum/.anyenv/envs/pyenv/shims/python

% pip --version
pip 23.3.2 from /Users/gumfum/.anyenv/envs/pyenv/versions/3.11.3/lib/python3.11/site-packages/pip (python 3.11)
% which pip
/Users/gumfum/.anyenv/envs/pyenv/shims/pip

Selenium のインストール

pip でそのままインストールする。

 % pip install selenium
Collecting selenium
  Downloading selenium-4.17.2-py3-none-any.whl.metadata (6.9 kB)

( ...中略... )

Installing collected packages: selenium
Successfully installed selenium-4.17.2

ChromeDriver のインストール

chromedriver-binary-auto を pip でインストールする。

% pip install chromedriver-binary-auto 
Collecting chromedriver-binary-auto
  Downloading chromedriver-binary-auto-0.3.1.tar.gz (5.6 kB)

( ...中略... )

Installing collected packages: chromedriver-binary-auto
Successfully installed chromedriver-binary-auto-0.3.1

auto のつかないパッケージを指定した場合は chromedriver の最新版がインストールされるが、このバージョンが起動される Chrome のバージョンと一致していないとエラーの原因となってしまう。

% pip install chromedriver-binary
Collecting chromedriver-binary
  Downloading chromedriver-binary-123.0.6268.0.0.tar.gz (5.6 kB)

( ...中略... )

Installing collected packages: chromedriver-binary
Successfully installed chromedriver-binary-123.0.6268.0.0

Chrome のバージョンは「ヘルプ」>「Google Chrome について」で確認できる。

試しに、後で実行するサンプルコードを実行してみると、バージョンが異なっている旨のメッセージが表示される。

% python example.py
The chromedriver version (123.0.6268.0) detected in PATH at /Users/gumfum/.anyenv/envs/pyenv/versions/3.11.3/lib/python3.11/site-packages/chromedriver_binary/chromedriver might not be compatible with the detected chrome version (121.0.6167.85); currently, chromedriver 121.0.6167.85 is recommended for chrome 121.*, so it is advised to delete the driver in PATH and retry

これを回避するためには、先述の -auto がついたパッケージをインストールするか、chromedriver-binary のインストール時にバージョンを指定する必要がある。

% pip install chromedriver-binary==121.0.6167.85 

サンプルを実行する

chromedriver-binary のページに記載のある Example のコードをそのまま実行してみる。 pypi.org

Example 内に Adds chromedriver binary to path とコメントで記載があったが、上記インストール方法ではすでにパスが通っているようで問題なく実行できた。スクリプトが終了すると Chrome が終了し、表示が確認できる前に閉じられてしまったため、assert の代わりに sleep を入れて終了までに余裕を持たせている。

また、そのままスクリプトを終了させると webdriver のプロセスとブラウザーのプロセスが残る可能性があるため driver.quit() を最後に追加した*1

from selenium import webdriver
import chromedriver_binary  # Adds chromedriver binary to path
from time import sleep

driver = webdriver.Chrome()
driver.get("http://www.python.org")

sleep(10)
driver.quit()

実行すると Chrome が起動し、www.python.org のページが表示されること、少し待ってから Chrome が閉じられることを確認して環境構築はひとまず完了。

参考サイト

qiita.com

*1:同様の情報は多数見つかるのだが、webdriver のドキュメントでそのような記述を見つけられておらず引用を記載できず。処理内容を確認するためにはコードを読む必要があるかもしれない

Homebrew で Haskell Stack をインストールする

過去の記事で WindowsHaskell 環境を構築したが、Mac でも環境を構築してみた際の手順をまとめてみる。

gumfum.hatenablog.com

環境

  • macOS Monterey 12.6.3
  • Homebrew 4.2.2

インストール

brew install stack

これだけ。haskell-stack を指定しても同じものがインストールされる*1

brew install haskell-stack

stack コマンドのパスを確認してみると haskell-stack の名前でディレクトリが作成されていることがわかる。

% which stack
/usr/local/bin/stack

% ls -al /usr/local/bin/stack 
lrwxr-xr-x  1 gumfum  admin  40  1  5 14:24 /usr/local/bin/stack -> ../Cellar/haskell-stack/2.13.1/bin/stack

stack setup コマンドの実行が必要との情報もあったが、私が Homebrew 経由でインストールした場合には以下のようなメッセージが表示されたため、セットアップなどは必要なくそのまま使うことができそうである。
Haskell コンパイラ ghc や、インタプリタ ghci などを利用したい場合は、それぞれ stack を頭につけて実行する。

% stack setup
Stack will use a sandboxed GHC it installed. To use this GHC and packages outside of a project, consider using: stack ghc, stack ghci, stack runghc, or stack exec.

ghci を使う

stack ghci で対話環境を起動できる。メッセージがいくつか出てくるが、特に設定はしなくても使用できる。

% stack ghci

Note: No local targets specified, so a plain ghci will be started with no package hiding or package options.

( ... 中略 ... )
      
Prelude> putStrLn "Hello World!"
Hello World!
Prelude> sum [1..100]
5050

モジュールを import して使うこともできる。

Prelude> import Data.List
Prelude Data.List> nub [1,2,3,4,5,6,1,2,3,4]
[1,2,3,4,5,6]

終了する場合は :q もしくは :quit を実行するか、control + D を入力する。

Prelude> :q
Leaving GHCi.

サンプルプロジェクトを作る

stack new でサンプルプロジェクトが作成できる。

% stack new haskellTestProject

ディレクトリ構造は以下のようになっており、someFunc と表示されるだけのコードが作成される。テストコードも用意されているが中身は実装されていないようである。

% tree haskellTestProject
haskellTestProject
├── CHANGELOG.md
├── LICENSE
├── README.md
├── Setup.hs
├── app
│   └── Main.hs
├── haskellTestProject.cabal
├── package.yaml
├── src
│   └── Lib.hs
├── stack.yaml
└── test
    └── Spec.hs
% cat haskellTestProject/app/Main.hs 
module Main (main) where

import Lib

main :: IO ()
main = someFunc
% cat haskellTestProject/src/Lib.hs 
module Lib
    ( someFunc
    ) where

someFunc :: IO ()
someFunc = putStrLn "someFunc"
% cat haskellTestProject/test/Spec.hs 
main :: IO ()
main = putStrLn "Test suite not yet implemented"

このままビルドおよび実行することができる。実行時には プロジェクト名-exe を指定する*2。実行すると someFunc と出力されることが確認できた。

% cd haskellTestProject
% stack build
( ... 中略 ... )
Registering library for haskellTestProject-0.1.0.0.

% stack exec haskellTestProject-exe   
someFunc

環境構築としては一旦ここまで。

*1:どちらかというとhaskell-stackがパッケージの正式名称で、stackが略称として登録されている

*2:ビルド時の設定は haskellTestProject.cabal ファイルに記載があるようである

Mac で tree コマンドを使えるようにする

tree コマンドは、対象のディレクトリからのディレクトリ構造をツリー形式で表示するコマンド。Mac ではデフォルトで利用できるコマンドではないので、インストールして使えるようにする。

環境

  • macOS Monterey 12.6.3
  • Homebrew 4.2.2

インストール

Homebrew でインストールできる。

% brew install tree

念のためインストール先とバージョンを確認してみる。

% which tree
/usr/local/bin/tree

% ls -al /usr/local/bin/tree 
lrwxr-xr-x  1 gumfum  admin  31  1  3 18:07 /usr/local/bin/tree -> ../Cellar/tree/2.1.1_1/bin/tree

% tree --version
tree v2.1.1 © 1996 - 2023 by Steve Baker, Thomas Moore, Francesc Rocher, Florian Sesser, Kyosuke Tokoro

treeコマンドを使ってみる

tree <ディレクトリ> で、指定したディレクトリをルートとしたツリー構造を表示する。

% tree root_dir
root_dir
├── child_dir
│   ├── child_file_1
│   └── child_file_2
└── root_file

ディレクトリを指定しない場合は、現在のディレクトリをルートとして表示する。ルートは . として表示される。

% cd root_dir 
% tree
.
├── child_dir
│   ├── child_file_1
│   └── child_file_2
└── root_file

-f オプションを付けることで、各ディレクトリとファイルのルートディレクトリからのフルパスを表示できる。

% tree -f root_dir 
root_dir
├── root_dir/child_dir
│   ├── root_dir/child_dir/child_file_1
│   └── root_dir/child_dir/child_file_2
└── root_dir/root_file

-i オプションを付けることで、ツリー構造を表示せずにファイルの一覧のみを表示できる。

% tree -i root_dir 
root_dir
child_dir
child_file_1
child_file_2
root_file

% tree -f -i root_dir
root_dir
root_dir/child_dir
root_dir/child_dir/child_file_1
root_dir/child_dir/child_file_2
root_dir/root_file

他にも色々なオプションがあるが、--help で確認できる通りかなり多いためこの辺りで割愛。

PL/pgSQL で FOR 文を書いて動かしてみる

PostgreSQL では、PL/pgSQL という手続き言語を使うことができる。標準の SQL にはないループ文などを書くことができるため、試しに FOR 文を動かしてみる。

サンプルデータ

過去の記事でも利用している、都道府県コード(id)とローマ字表記(name)のテーブルで試してみる。

postgres=# SELECT * FROM prefecture ORDER BY id LIMIT 10;
 id |   name    
----+-----------
  1 | Hokkaido
  2 | Aomori
  3 | Iwate
  4 | Miyagi
  5 | Akita
  6 | Yamagata
  7 | Fukushima
  8 | Ibaraki
  9 | Tochigi
 10 | Gunma
(10 rows)

動かしてみたコード

例えば、サンプルデータのテーブルから id と name を SELECT し、1行ずつ出力するようなスクリプトは以下のようになる。

DO $$
DECLARE rec RECORD;
BEGIN
    FOR rec IN SELECT id, name FROM prefecture
    LOOP
        RAISE NOTICE '%, %', rec.id, rec.name;
    END LOOP;
END; $$ LANGUAGE plpgsql;

DECLARE rec RECORD はいわゆる変数宣言の部分で、こちらで宣言した変数 rec に SELECT 文の結果が入るようになる。

また、RAISE NOTICE はターミナルにメッセージを出力する部分で、'%' の部分は指定したパラメータで順に置き換わって出力される。

出力は以下のようになる。

NOTICE:  1, Hokkaido
NOTICE:  2, Aomori
NOTICE:  3, Iwate

( ... 中略 ... )

NOTICE:  45, Miyazaki
NOTICE:  46, Kagoshima
NOTICE:  47, Okinawa

他にも、[1-10] のようなリストを作成して id を指定するような FOR ループを回すこともできる。

DO $$ 
DECLARE rec RECORD;
BEGIN
    FOR counter IN 1..10
    LOOP
        SELECT * INTO rec FROM prefecture WHERE id = counter;
        RAISE NOTICE '%', rec.name;
    END LOOP;
END; $$ LANGUAGE plpgsql;

注意点として、SELECT 結果を RECORD に入れるなど他の場所で利用せずに実行しようとした場合はエラーとなり実行が失敗する。

postgres=# DO $$ 
BEGIN
    FOR counter IN 1..10
    LOOP
        SELECT * FROM prefecture WHERE id = counter;
    END LOOP;
END; $$ LANGUAGE plpgsql;

ERROR:  query has no destination for result data
HINT:  If you want to discard the results of a SELECT, use PERFORM instead.
CONTEXT:  PL/pgSQL function inline_code_block line 5 at SQL statement

SELECT だけしたい場合は PERFORM を利用する必要があるということではあるが、出力や他の箇所での利用もしないのであれば実行しても何も起きないのと同義なので、エラーが出た場合は処理内容を見直して修正するのが良さそうである。

参考サイト

PL/pgSQL では FOR 以外にも色々な制御文が使える。ドキュメントにもサンプルコードが載っているため参考になる。