名もなき未知

エンジニアリングとか、日常とかそういうのをまとめる場所。アクセス解析のためGAを利用、Googleに情報を送信しています。商品紹介のためAmazonアフィリエイトを利用、Amazonに情報を送信しています。記事に関しては私が書いていない引用文を除いて自由にご利用ください。

WSL2 + Marp で pdf を何とか出力できるようにする

下記はメモ

前提の課題として、 Marp で pdf を出そうとしたのだが、こんな感じにエラーが出る。 html を

❯ pnpx marp test-slide/slide.md --pdf
[  INFO ] Converting 1 markdown...
[ ERROR ] Failed converting Markdown. (connect ECONNREFUSED 127.0.0.1:54872)

何のことやらという感じであるが、 marp-team/marp-cli: A CLI interface for Marp and Marpit based converters を見ると、pdf, pptx を出すために Chrome Google なりそれに類するブラウザが必要なようです。

で、ブラウザを表示するためには WSL2 上から chrome が起動できる必要があるが、これに関しては以前、「Xサーバーアプリが必要なので、VcXsrv を入れてみる。」あたりで設定している。今回もこう言った設定が必要となる。

namonakimichi.hatenablog.com

Google ChromeCUI ベースで入れる方法としては下記の方法を参考にした(他の記事も読んだが、追加するキーも方法も一緒だったので、まあ良いことにした)(ぱっと Google のサイトから探せなかったので、妥協した)。

qiita.com

このセットアップを行って VcXsrv を起動した状態で、 google-chrome を terminal からたたくと、Chrome が起動する。

最後に、下記の issue を参考に CHROME_PATH を指定し、起動する。

github.com

ちなみに絶対的なパス指定でなくても、 which google-chrome とかで行けそうな気がしたので、試したらできた。

❯ CHROME_PATH=$(which google-chrome-stable) pnpx marp test-slide/slide.md -o output.pdf
[  INFO ] Converting 1 markdown...
[  INFO ] test-slide/slide.md => output.pdf

というわけでOK。

ところで

なんかもはや Docker でやったほうが早かったのではないか、という説がある。

ABC195に参加しました

2年ぶりくらいにコンテストに参加しました。まあ気が向いたのと、たまたま時間が空いたので(ただ、しばらく今後も土日のこの時間予定が入ってて参加できなさそう)、参加してみました。

当日は4完でしたが、できたものを考えるともっと早く解けないといけないかなという感じですね…。

Fは無理そうでしたが、時間があればEは惜しいところまで行けたんじゃないかなと思います(解けるとは言っていない)

A問題

A - Health M Death

剰余を取ります。提出は Scala

import java.util.Scanner

object Main extends App {
  val sc = new Scanner(System.in)

  val m = sc.nextInt
  val h = sc.nextInt
  println(if(h % m == 0) "Yes" else "No")

}

B問題

B - Many Oranges

答え見たら分析方法が違ったのであれですが…。

みかんの重さで表現しうるのはN個選んだ時に AN ~ BN になるので、その範囲にないときは問答無用で UNSATISFIABLE にしました( AN~BN にしうる範囲では、起点を AN として、適当にミカンの重さを1gづつ調整できれば多分表現しうると考えたので)。

それ以外の場合は、まあ最小個数を math.floor(w.toDouble / a).toInt 、最大個数を math.ceil(w.toDouble / b).toInt として雑に出しました。

本当にこれでいいのかは不安だったので、かなり心配でした。想定解は N の全探索みたいですね。

提出は Scala

import java.util.Scanner

object Main extends App {
  val sc = new Scanner(System.in)

  val a = sc.nextInt
  val b = sc.nextInt
  val w = sc.nextInt * 1000

  def ok(a: Int, b: Int, w: Int): Boolean = {
    val d = b - a
    val t = w / a
    if(a * t <= w && w <= (a + d) * t) true
    else false
  }

  if(ok(a, b, w)) {
    val f = math.floor(w.toDouble / a).toInt
    val s = math.ceil(w.toDouble / b).toInt
    println(s"${s} ${f}")
  } else {
    println("UNSATISFIABLE")
  }
}

これ嘘解放じゃないか?大丈夫かな。

C問題

C - Comma

数え上げです。その数字以下の時は必ずコンマの数がこれだけあることが保証される、みたいな形になるので数え上げていきます。

解放では 1,000以上、1,000,000 以上みたいな形でうまく集計していましたが、僕は雑に 1,000 で割る、 1,000.000 で割る、を繰り返して一定の桁以上であれば全部買うと、そうでないならばそこまでの値をカウントする感じで書きました。

想定解がキレイなので、これは思いつきたかったな。提出はScala

import java.util.Scanner

object Main extends App {
  val sc = new Scanner(System.in)

  var n = sc.nextLong
  val t = Array.range(1, 6).map(i => Array(i, math.pow(10, 3 * i).toLong)).reverse
  val l = math.pow(10, 3).toLong
  var ans: Long = 0
  for((e, ii) <- t.zipWithIndex) {
    val i = e(0).toInt
    val v = e(1)
    val a: Long = n / v
    if(a >= l) {
      ans += (t(ii - 1)(1) - v) * i
    } else if(a > 0) {
      ans += (n - v + 1) * i
    }
  }
  println(ans)
}

D問題

D - Shipping Center

あらかじめ入れるものをソートしておいて、使える箱のリストを毎回生成して、貪欲に入れる形でやっていました。

配列あれこれするのがめんどくさかったので提出は Python。配列の長さが短めなので、雑に作り直しても間に合うと思ってやったら案の定大丈夫でした。あんまりきれいなコードではない。

from copy import deepcopy

def run():
    n, m, q = map(int, input().split())
    wv = [[0 for i in range(2)] for i in range(n)]
    for i in range(n):
        w, v = map(int, input().split())
        wv[i] = [w, v]
    wv.sort(key=lambda t: (t[0], -t[1]))
    x = [int(i) for i in input().split()]
    for i in range(q):
        r, l = map(int, input().split())
        qx = x[:r - 1] + x[l:]
        if(len(qx) == 0):
            print(0)
        else:
            qx.sort()
            qw = deepcopy(wv)
            ans = 0
            for j in qx:
                tans, ti = -1, -1
                for k in range(len(qw)):
                    if qw[k][0] > j:
                        break
                    if qw[k][1] > tans:
                        tans, ti = qw[k][1], k
                if tans >= 0:
                    ans += tans
                    qw.pop(ti)
                if len(qw) == 0:
                    break
            print(ans)

if __name__ == "__main__":
    run()

E問題

E - Lucky 7 Battle

ここからは復習。これは遷移をいい感じにDPに入れていって、最終結果から推定する問題でしたね。

後ろから見るのは思いついたんですが、時間切れだったのと、DPに保存すべき値がうまく思いつきませんでした。

なので解説見ながら書いた感じです。公式の解説読んで。提出は scala

import java.util.Scanner

object Main extends App {
  val sc = new Scanner(System.in)

  val n = sc.nextInt
  val s = sc.next
  val x = sc.next

  var dp = Array.fill(n + 1, 7)(false)
  dp(n)(0) = true
  for(i <- (0 until n).reverse) {
    val si = s(i) - '0'
    val xi = x(i)
    for(j <- 0 until 7) {
      val f1 = (10 * j) % 7
      val f2 = (10 * j + si) % 7
      dp(i)(j) = xi match {
        case 'A' => dp(i + 1)(f1) && dp(i + 1)(f2)
        case 'T' => dp(i + 1)(f1) || dp(i + 1)(f2)
      }
    }
  }
  println(if(dp(0)(0)) "Takahashi" else "Aoki")
}

F問題

F - Coprime Present

BitDPらしいですね。素数の範囲がせいぜい72までに限定されるので、それをいい感じに数え上げられればよさそうです。

なお、数え上げパートが全く思いつかなかったのでユーザー解説のほうを参考に書いていきました。ありがとうございます。

Coprime Present [パナソニックプログラミングコンテスト(AtCoder Beginner Contest 195) F] - はまやんはまやんはまやん

Scalaで書いたのですが、はまりポイントがあり

  • どうやら Array の引数として渡せるのは Int 型が想定らしい、なぜか ( 1 << 20 ) を変数に入れてから Array に突っ込むと動く(これ想定されている挙動か?怪しい気がするが)
  • DPを int[n + 1][1 << 20] の形で定義するとメモリが足りなくなり、REとなる(MLEではないらしい)。今回はせいぜい、一つ前のものを見る程度だったので、変数を2つ使って表現し、更新する形にした。

あと isPrime をもっと楽に描く方法があったような気がするんですが(Javaのパッケージか何かにあったような)、探し出せなかったので雑に素数を出すコードを書いています。こういうのはライブラリみたいに使いまわせる形で用意しておいてもよいかも。

import java.util.Scanner

object Main extends App {
  val sc = new Scanner(System.in)

  def isPrime(a: Int): Boolean = {
    for(i <- 2 to math.sqrt(a).toInt) {
      if(a % i == 0) return false
    }
    true
  }

  val a, b = sc.nextLong
  val d = (b - a + 1).toInt

  val primes = (2 to 72).toArray.filter(isPrime)
  val l = primes.length
  val primesMap = (a to b).toArray.map(i => {
    var r = 0
    for(j <- 0 until l) {
      if(i % primes(j) == 0) {
        r |= (1 << j)
      }
    }
    r
  })
  var dp = Array.ofDim[Int](1 << l)
  dp(0) = 1
  for(i <- a to b) {
    var dp2 = Array.ofDim[Int](1 << l)
    for(j <- 0 until (1 << l)) {
      val t = (i - a).toInt
      if((j & primesMap(t)) == 0) dp2(j | primesMap(t)) += dp(j)
      dp2(j) += dp(j)
    }
    dp = dp2
  }
  println(dp.sum)
}

感想

めちゃくちゃ久々に参加しましたが、やっぱりやってて思ったのは

  • 集中力落ちてない?(D問題の細かいロジックのバグを直すのに時間がかかった)
  • 瞬発力も落ちてない? とりあえず試す力が弱い(特に B 問題は、最初にこれ解ける?と思って怪しかったので飛ばしてC問題行って、考え直してたのでもったいなかった)
  • タイピングが遅い(はい)

という感じなので、衰えを感じつつも、久々に出たのにレート変動なかったのは意外と元のコーディング能力は落ちてないのかなと思います。

逆に言えばまだまだ伸びる可能性は十分にあるので、今後も参加できるときは参加したい…(が、土日の21時は結構裏側に予定あったりして難しい)ですね。

(あとはどちらかといえば厳密解出すアルゴリズムのコンテストのほうが好きなのかもしれない、ISUCONはなんかすきだけど、マラソンはいまだに手を出そうという気になれていないという課題感を最近思いました)。

過去問ちょいちょい解いて水色には戻したいですね(希望的観測)。

GuzzleでHTTP通信してみる

PHPでモダンな感じで HTTP 通信するなら何?みたいな話をいつだったかしていました。

どうやら Guzzle が流行ってるらしい、とのことだったので、試しに Authorizationリクエストヘッダも含めた HTTP 通信をしてみることにしました。

なお、5年ぶりくらいに PHP を触ったので、連想配列を書くだけでも苦戦してました(他言語の書き方に慣れすぎたか…)。まあ思い出したけど。

やったこと

まず Docker で Guzzle を入れる

正直ここが一番大変だったといっても過言ではないです。

背景としては、特に普段から PHP を書いていないのでローカルに開発環境は不要と思い、実行環境だけあればよい気持ちになったためです。

Overview — Guzzle Documentation を見ている限りでは、 composer で入れるのが良いらしいですね。

ちょっと調べると composer も Docker Image があることを知りました。やったー。

というわけで下記の2つを使ってマルチステージビルドしました。

というわけで Dockerfile は下記になりました。

FROM php:8.0-fpm-buster

# composer はマルチステージビルドで持ってくる
# refer: https://qiita.com/yatsbashy/items/02bbbebbfe7e5a5976bc
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# composer need git
RUN apt-get update && apt-get install -y git

RUN composer require guzzlehttp/guzzle:^7.0
COPY . .
RUN chmod u+x entrypoint.sh

ENTRYPOINT ["/var/www/html/entrypoint.sh"]
# RUN ["php", "main.php"]

よくわかってないのですが、なぜか WORKDIR が /var/www/html/ になっているので(composerのせいか?) ENTRYPOINT はそこに合わせています(デバッグするときは ENTRYPOINT 外して、雑に exec -it {image_id} bash で入ってデバッグしていた)。

Guzzle でリクエストをする

Qiita の API でも試しに叩いてみることにします。認証しているユーザーを返す API がある GET /authenticated_user のでそれをたたいてみることにします。

基本的には Client を作って、リクエストして、結果を受け取る形です。下記のページを参照に作っていきます。

出来上がったものがこちら。

<?php

require 'vendor/autoload.php';

$token = getenv('QIITA_ACCESS_TOKEN');

$client = new \GuzzleHttp\Client(
    [
        'base_uri' => 'https://qiita.com',
        'timeout' => 2.0,
    ]
);

$headers = [
    'Content-Type' => 'application/json;charset=utf-8',
    'Authorization' => 'Bearer '.$token,
];

$res = $client->request(
    'GET', '/api/v2/authenticated_user', [
        'headers' => $headers
    ]
);

echo $res->getStatusCode()."\n";
echo $res->getHeaderLine('content-type')."\n";
echo $res->getBody()."\n";

token は直打ちするとよろしくないので、環境変数からとる形にしています。

環境変数を渡しつつ実行する

最後に虚無みたいな entrypoint.sh を作っておくことにします。

#!/bin/bash

php main.php

まずはビルドします。

❯ docker build . -t php-practice
Sending build context to Docker daemon  4.608kB
Step 1/7 : FROM php:8.0-fpm-buster
 ---> 4ebd60322a2b
Step 2/7 : COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
 ---> Using cache
 ---> 49f3a96a6383
Step 3/7 : RUN apt-get update && apt-get install -y git
 ---> Using cache
 ---> 0bd8cb7267ee
Step 4/7 : RUN composer require guzzlehttp/guzzle:^7.0
 ---> Using cache
 ---> ea788036ebab
Step 5/7 : COPY . .
 ---> Using cache
 ---> a5fb0b2fa934
Step 6/7 : RUN chmod u+x entrypoint.sh
 ---> Using cache
 ---> 3db87772286c
Step 7/7 : ENTRYPOINT ["/var/www/html/entrypoint.sh"]
 ---> Using cache
 ---> ee0a466366fd
Successfully built ee0a466366fd
Successfully tagged php-practice:latest

で、うまくいったら run します。 ENTRYPOINT を指定してるし、 run したら消していいはずなので --rm つけてます。

トークンは Qiita のユーザーページから発行してください。

❯ docker run  -e QIITA_ACCESS_TOKEN=${PEKOPEKO} -t php-practice --rm
200
application/json; charset=utf-8
{"description":"えんじにゃー。ぷりぷりかしこま。Pythonしかできない。","facebook_id":"","followees_count":19,"followers_count":21,"github_login_name":"yumechi","id":"yumechi","items_count":9,"linkedin_id":"","location":"東京","name":"Senri Yumechi","organization":"","permanent_id":72473,"profile_image_url":"ほげほげ","team_only":false,"twitter_screen_name":"__yumechi","website_url":"https://namonakimichi.hatenablog.com/","image_monthly_upload_limit":104857600,"image_monthly_upload_remaining":104857600}

ちょっとやばそうなところは雑にマスクしましたが、無事に実行できました。やったね。

まとめ

やっぱり Docker ! マルチステージビルド ! はいいですね。ローカル環境が汚れにくいし、再現性もあっていい感じです。

環境変数も実行時に入れることによって、挙動を制御することもできるので、活用していきたいですね。

そして Guzzle ですが、ヘッダーの設定、リクエストの受付、そのあたり見ても他の言語の通信と大差ない感じで扱えて非常に楽でした。

cURL でリクエストする方法も知っていますが、 Guzzle にはどうやらまだ使いこなせていない機能でいいもんがたくさんありそうです。

PHP でなにかつくるかはわかりませんが、頭の片隅には入れておこうと思います。

追記

静的解析も少しやったんだった。この記事を参考にした。

PHPの静的解析いろいろ - Qiita

php -l <filename> はガバガバなので何も出なかった。

phpcs を試して多少おこられたので直しました( main.php はこれに従って直した)。

ほかについては試してませんが、 CI とか回すときにコードチェックもついでにできるようになるといいかもしれないですね (希望的観測)。

これで今度こそ終わり。