#!/usr/bin/env ruby require 'nokogiri' require 'sqlite3' require 'time' require 'syslog' DB_FILE = "metrics.db" METRIC_MAP = { /Files received/ => "files_received", /Errors in receiving files/ => "errors", /Invalid Files/ => "invalid", /^Bulletins stored$/ => "stored", /Bulletins not stored/ => "not_stored" } # ---------------------------- # 時刻パース # ---------------------------- def parse_hour_from_header(text,ingested_at) hour = text.split(':').first.to_i now = Time.at(ingested_at).utc ts = Time.utc(now.year, now.month, now.day, hour) # if ts falls in the future, shift it one day before ts -= 86400 if ts > now ts.to_i end def extract_target_timestamp(table,ingested_at) th = table.at_css('tr th.th2') raise "時間ヘッダが見つかりません" unless th parse_hour_from_header(th.text.strip,ingested_at) end # ---------------------------- # center_id 作成 # ---------------------------- def make_center_id(text) part = text[/[A-Z]{4}, [\w\s']+,/] return nil unless part part .gsub(',', '') .gsub(/[ '\s]+/, '_') .downcase end def normalize_full_name(text) text.strip.gsub(/\s+/, ' ') end # ---------------------------- # table 探索(頑健版) # ---------------------------- def find_table_for_center(center_div, doc) # 方法1: ID対応 if center_div['id'] && center_div['id'] =~ /(\d+)$/ num = $1 table = doc.at_css("#table_div#{num} table") return table if table end # 方法2: 汎用 sibling 探索 center_div.xpath('following-sibling::*').each do |sib| table = sib.at_css('table') return table if table end nil end # ---------------------------- # DB 初期化 # ---------------------------- def init_db(db) db.execute <<~SQL CREATE TABLE IF NOT EXISTS centers ( center_id TEXT PRIMARY KEY, full_name TEXT ); SQL db.execute <<~SQL CREATE TABLE IF NOT EXISTS metrics ( ts INTEGER, center_id TEXT, metric TEXT, value INTEGER, ingested_at INTEGER, PRIMARY KEY (ts, center_id, metric) ); SQL end # ---------------------------- # stderr/syslog message # ---------------------------- def sys_warn msg Syslog.warn msg STDERR.puts msg end def sys_info msg Syslog.info msg STDERR.puts msg if STDERR.tty? end # ---------------------------- # メイン処理 # ---------------------------- def main2(db, html_file, ingested_at, wgettime) doc = Nokogiri::HTML(File.read(html_file)) insrows = 0 doc.css('div.centerName').each do |center_div| a = center_div.at_css('a') next unless a full_name = normalize_full_name(a.text) center_id = make_center_id(full_name) unless center_id sys_warn "center_id生成失敗: #{full_name}" next end # centers 登録 db.execute( "INSERT OR IGNORE INTO centers (center_id, full_name) VALUES (?, ?)", [center_id, full_name] ) # 対応する table を取得 table = find_table_for_center(center_div, doc) unless table sys_warn "table未発見: #{center_id}" next end # ts(このtableの対象時刻) begin ts = extract_target_timestamp(table,ingested_at) rescue => e sys_warn "時刻抽出失敗: #{center_id} #{e}" next end table.css('tr').each do |tr| th = tr.at_css('th') next unless th label = th.text.strip metric = nil METRIC_MAP.each do |pattern, name| if label.match(pattern) metric = name break end end next unless metric td = tr.css('td')[0] next unless td value = td.text.strip.to_i begin db.execute( "INSERT OR IGNORE INTO metrics (ts, center_id, metric, value, ingested_at) VALUES (?, ?, ?, ?, ?)", [ts, center_id, metric, value, ingested_at] ) insrows += db.changes rescue => e sys_warn "DB insert error: #{center_id} #{metric} #{e}" end end end sys_info("event=import html=#{html_file} inserted=#{insrows} wget=#{wgettime}") end def main(argv) ingested_at = Time.now.utc.to_i btime=etime=ingested_at.to_f facility= if STDERR.tty? then Syslog::LOG_USER else Syslog::LOG_NEWS end Syslog.open('iwg_import', Syslog::LOG_PID, facility) db = SQLite3::Database.new(DB_FILE) init_db(db) for arg in argv case arg when /^btime=/ then btime=$'.to_f when /^etime=/ then etime=$'.to_f ingested_at=etime.to_i else main2(db, arg, ingested_at, etime-btime) end end ensure Syslog.close db.close end # ---------------------------- # 実行 # ---------------------------- if ARGV.length < 1 puts "Usage: #{$0} input.html" exit 1 end main(ARGV)