LoginSignup
17
12

More than 5 years have passed since last update.

AtCoderツールを自作した話

Last updated at Posted at 2017-03-06

はじめに

このところAtCoderの過去問をじわじわと解いている私です。
最近OnlineJudgeHelperというツールの存在を知り「これ使えばすごく捗るじゃん?」と衝撃を受けたわけですが、
「いや、これくらいのツールは自分で作れないと!」という変な意地もあって、
結局オレオレAtCoderツールを自作してしまいましたよ、という話です。

テストの自動実行

テストの自動実行だけはわりと昔からやっていましたが、仕掛けは至ってシンプルなものです。

  • 入力データ例を "data/A_001.in" といったファイル名で置いておく
  • 出力データ例を "data/A_001.exp" といったファイル名で置いておく
  • プログラムを実行して、出力結果を "data/A_001.out" といったファイル名で保存
  • "data/A_001.out" と "data/A_001.exp" の中身を文字列比較
  • 上記をテストケースの番号順に一括実行
testing.rb
def test_auto(prog)
  1.upto(999) do |i|
    break unless test(prog, i)
  end
end

def test(prog, n)
  base, ext = prog.split('.')
  q = base.split('_')[0]
  cs = format('%s_%03d', q, n)
  infile = "data/#{cs}.in"
  outfile = "data/#{cs}.out"
  expfile = "data/#{cs}.exp"

  return false unless File.exist?(infile)
  return false unless File.exist?(expfile)

  cmd =
    case ext
    # 拡張子(使用言語)によって起動コマンドを変える
    end

  ec = system("#{cmd} < #{infile} > #{outfile}")

  input = File.read(infile)
  result = File.read(outfile)
  expected = File.read(expfile)

  puts "==== #{cs} ===="
  puts '-- input --'
  print input
  puts '-- expected --'
  print expected
  puts '-- result --'
  print result
  if !ec
    puts '!!!!! RE !!!!!'
  elsif result != expected
    puts '!!!!! WA !!!!!'
  else
    puts '<< OK >>'
  end
  true
end

ちょっとうれしいポイントとしては、
例えばD問題で最初に書いた”D.cxx”が通らなかった!となって別の方法を試してみる時に、
別バージョンをアンスコ付きの"D_v2.cxx"といったファイル名にしておくと、
どちらのプログラムも同じテストデータ(data/D_00X.in)でテストできるようになっています。

 テストデータのダウンロード

しばらくはテストデータを問題ページから1つ1つ手でコピーしていましたが、
ある時、「スクレイピングで自動化できるんじゃね?」と気がつきました。

scraping.rb
require 'mechanize'

def _fetch_all
  url = @url + 'assignments'
  puts "fetch from #{url} ..."
  sleep 0.1
  page = @agent.get(url)
  ('A'..'Z').each do |q|
    link = page.link_with(text: q)
    next unless link
    _fetch(q, link.href)
  end
end

def _fetch(q, url)
  puts "fetch from #{url} ..."
  sleep 0.1
  page = @agent.get(url)
  fmt = smp = desc = ''
  page.search("//div[contains(@class,'part')]").each do |section|
    h3 = section.search('h3')
    next if h3.empty?
    title = h3[0].content
    pre = section.search('pre')
    next if pre.empty?
    data = pre[0].content.lstrip.gsub("\r\n", "\n")
    case title
    when /^入力$/
      desc = section.content
      fmt = data
    when /^入力例\s*(\d+)$/
      _out_data(q, $1, 'in', data)
      smp = data if data.size > smp.size
    when /^出力例\s*(\d+)$/
      _out_data(q, $1, 'exp', data)
    end
  end
  _out_src(q, 'rb', src_gen_ruby(fmt, smp))
  _out_src(q, 'cxx', src_gen_c(desc, fmt, smp))
end

問題によってページの構成にバリエーションがあるため、
使っていてうまく取れないケースが出てきたらその都度修正するということを繰り返していましたが、
最近は安定感が出てきました。

ログイン機能

過去問ばかり解いていたのでしばらく気が付きませんでしたが、
あるときコンテスト中にテストデータを落とそうとしたら動かなくて、
コンテスト中はこういうのは禁止なのかな?と思っていたら、
どうやらスクレイピングツールからAtCoderにログインしておけば
コンテスト中でもダウンロードできるらしいということがわかり、
ログインしてからダウンロードする機能をつけました。
この部分は自力で作れなくて、他所のAtCoderツールを大いに参考にさせて頂きました。

ソース提出機能

ログインのスクリプトができると、それを少し改造するだけでソースの提出もできるようになりました。
実は提出フォームから提出しても大した手間じゃないと思っていたので、
あまり自動化する必要性を感じていなかったんですが、
できてしまうとそれなりに便利ですね。
ソースの拡張子から言語を自動判別できるので、
言語の選択ミスで余計なペナルティをくらうことがなくなるのもいいです。
オレオレツールなので、言語はRubyとC++しか対応してないです。

ソース生成機能

たぶんOnlineJudgeHelperが話題になったときに、誰かが
「入力部分のソースの自動生成くらいできないと」
みたいな発言をしていたんだと思いますが、それを思い切り真に受けてしまい、
問題ページの入力データの説明から入力部分のソースを生成することにチャレンジしました。
RubyとC++の両方でソースを出したかったので、データフォーマットをパースした結果を
一旦共通の入力データ定義的な構造体に突っ込むようにしました。

src_gen.rb
InputDef = Struct.new(:type, :size, :fmt, :vars)

def parse_fmt(fmt)
  inpdefs = []
  fmt = fmt
        .gsub(/-1/, '') # N-1 -> N
        .gsub(/(-| )/, ' ') # a-b -> a b
        .gsub(/[_,\\\(\)\{\}]/, '')
        .split("\n")
  fmt << ''
  re = prev = nil
  fmt.each do |f|
    if re
      if f =~ re
        prev = f
        next
      end
      inpdef = inpdefs.last
      case inpdef.type
      when :matrix
        inpdef.size = prev[-2..-1].chars.to_a
      when :varray
        inpdef.size = prev =~ /(\d+)$/ ? $1 : prev[-1]
      end
      re = prev = nil
    end
    case f
    when /^([a-z]+).(\s+\1.)+\s+[\.|…]+\s+\1.$/i, /^([a-z]+)[01](\s+\1.)+$/i
      inpdefs << InputDef.new(:harray, f[-1], :number, $1)
    when /^([a-z]+)..(\s+\1..)+\s+[\.|…]+\s+\1..$/i
      inpdefs << InputDef.new(:matrix, nil, :number, $1)
      re = /(^#{$1}..(\s+#{$1}..)+\s+[\.|…]+\s+#{$1}..|:|:|\.+)$/
      prev = f
    when /^([a-z]+)..(\1..)+\s+[\.|…]+\s+\1..$/i
      inpdefs << InputDef.new(:matrix, nil, :char, $1)
      re = /(^#{$1}..(#{$1}..)+\s+[\.|…]+\s+#{$1}..|:|:|\.+)$/
      prev = f
    when /^([a-z]+)[01][01](\s+\1..)+$/i
      inpdefs << InputDef.new(:matrix, nil, :number, $1)
      re = /(^#{$1}..(\s+#{$1}..)+|:|:|\.+)$/
      prev = f
    when /^[a-z]+(\d)(\s+[a-z]+\1)*$/i
      vars = f.split.map { |v| v[0..-2] }
      inpdefs << InputDef.new(:varray, nil, :number, vars)
      pat = vars.map { |v| v + '.+' }.join('\s+')
      re = Regexp.new("^(#{pat}|:|:|\\.+)$")
      prev = f
    when /^[a-z]+(\s+[a-z]+)*$/i
      inpdefs << InputDef.new(:single, nil, :number, f.split)
    end
  end
  inpdefs
end

上記の処理で

  • 単独の変数
  • スペース区切りで横に並んだ単独の変数
  • 横方向の配列
  • 縦方向の配列
  • 行列

のフォーマットをパースしようとしていますが、まだ改良中です。

データ型はフォーマットの情報だけでは分からないので、一旦全て整数型として、
後で入力データ例と突き合わせてそれ以外の型を設定するようにしています。
とはいっても今のところ、整数じゃなさそうなものは全て文字列という扱いになっています。

使用イメージ

コンテスト名のフォルダを作って、その中に以下のファイルを置きます。

helper.rb
require '../../AtCoderFriends/helper.rb'

# start

# test_auto 'A.rb'
# submit 'A.rb'

# test_auto 'B.rb'
# submit 'B.rb'

# test_auto 'C.rb'
# submit 'C.rb'

# test_auto 'D.cxx'
# submit 'D.cxx'

Sublime Textの左側にソース、右側にhelper.rbを開いて、
適宜helper.rbのコメントを解除して動かすという使い方をしています。

ソース・テストデータ生成→コーディング→コンパイル→テスト→提出

の作業が全てSublime Textの中で完結できるので、自分としてはかなり快適です。
オレオレツールなのでCLIとかGUIの類は一切ありません。

ソース一式はこちら

追記(2018/02/24)

相対パスでソースをrequireするやり方だと
コンテストのフォルダが増えてきても階層化できなくて、だんだんつらくなってきたので
ソースをRubyGem化して、Sublime Textのプラグインから呼ぶ方式に作り変えました。
うまくいけばさらに快適になるはずですが、しばらくはバグ取りが必要そうです。。。

追記(2018/06/14)

Sublime Textプラグインの操作イメージはこんな感じです。
だいぶ快適になりました。

操作イメージ

17
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
12