🚅

Yahoo!鉄道運行情報をscrapeしてMastodonに投稿するBot

2023/05/25に公開

Yahoo!鉄道運行情報を Mastodon Bot でつぶやかせていました。

運行情報 九州【非公式】 のような感じで、日々の鉄道運行情報を私の Mastodon インスタンスでつぶやかせていました。

Nokogiri で必要な情報を取り出してくる

各エリアごとのページを開くと、一番上に事故や災害などによる運行情報が出ているものが一覧になっているので取り出してきます。

Nokogiri で doc.css(".elmTblLstLine.trouble a") を呼び出すと、上記の行がリストで取り出されてくるので、表示行のリンク先をhrefで取り出して詳細ページのURLにアクセスして、運行情報の詳細を取り出してました。

# hrefのリンク文字列が相対リンクになっているので、ドメイン名を補っている
yahoo_uri = trouble_line.attr('href').gsub(/^/,'https://transit.yahoo.co.jp')

# Nokogiri でページにアクセスして中身を取り出している
line = Nokogiri::HTML.parse(URI.open(yahoo_uri))
name = line.css("h1.title").text
date = Time.strptime(line.css(".trouble p span").text, '(%m月%d日 %H時%M分')
text = line.css(".trouble p").text

Mastodon API で投稿する

取り出してきた運行情報が直近のものかどうかを判定した後に、Mastodon Statuses API を呼びだして実際に Bot の投稿として POST してました。

ソースコード例

#!/usr/bin/env ruby

require 'time'
require 'net/http'
require 'uri'
require 'open-uri'
require 'nokogiri'
require 'json'

robot_config = [
    {
        :name => "trainhokkaido", # MastodonのBotのアカウント名
        :bearer_hash => "ベアラートークン(Mastodonのアプリ登録で得られるハッシュ値)",
        :yahoo_uri => "https://transit.yahoo.co.jp/traininfo/area/2/"
    },
    {
        :name => "traintouhoku",
        :bearer_hash => "ベアラートークン(Mastodonのアプリ登録で得られるハッシュ値)",
        :yahoo_uri => "https://transit.yahoo.co.jp/traininfo/area/3/"
    },
    {
        :name => "trainkanto",
        :bearer_hash => "ベアラートークン(Mastodonのアプリ登録で得られるハッシュ値)",
        :yahoo_uri => "https://transit.yahoo.co.jp/traininfo/area/4/"
    },
    {
        :name => "trainchubu",
        :bearer_hash => "ベアラートークン(Mastodonのアプリ登録で得られるハッシュ値)",
        :yahoo_uri => "https://transit.yahoo.co.jp/traininfo/area/5/"
    },
    {
        :name => "trainkinki",
        :bearer_hash => "ベアラートークン(Mastodonのアプリ登録で得られるハッシュ値)",
        :yahoo_uri => "https://transit.yahoo.co.jp/traininfo/area/6/"
    },
    {
        :name => "trainchugoku",
        :bearer_hash => "ベアラートークン(Mastodonのアプリ登録で得られるハッシュ値)",
        :yahoo_uri => "https://transit.yahoo.co.jp/traininfo/area/8/"
    },
    {
        :name => "trainshikoku",
        :bearer_hash => "ベアラートークン(Mastodonのアプリ登録で得られるハッシュ値)",
        :yahoo_uri => "https://transit.yahoo.co.jp/traininfo/area/9/"
    },
    {
        :name => "trainkyushu",
        :bearer_hash => "ベアラートークン(Mastodonのアプリ登録で得られるハッシュ値)",
        :yahoo_uri => "https://transit.yahoo.co.jp/traininfo/area/7/"
    },
]

robot_config.each do |bot|
  doc = Nokogiri::HTML.parse(URI.open(bot[:yahoo_uri]))
  if doc.css(".elmTblLstLine.trouble a").length == 0 then
    puts "#{Time.now} #{bot[:name]} 事故・遅延情報はありません"
  else
    doc.css(".elmTblLstLine.trouble a").each do |trouble_line|
      yahoo_uri = trouble_line.attr('href').gsub(/^/,'https://transit.yahoo.co.jp')
      line = Nokogiri::HTML.parse(URI.open(yahoo_uri))
      name = line.css("h1.title").text
      date = Time.strptime(line.css(".trouble p span").text, '(%m月%d日 %H時%M分')
      text = line.css(".trouble p").text
      puts "#{Time.now} #{bot[:name]} #{name} #{text} #{yahoo_uri}"

      if Time.now - 15*60 < date then
        uri = URI.parse('https://mastodon.chotto.moe/api/v1/timelines/home')
        req = Net::HTTP::Get.new(uri)
        req['Authorization'] = 'Bearer ' + bot[:bearer_hash]
        res = Net::HTTP.start(uri.hostname, uri.port, {use_ssl: true}) do |http|
          http.request(req)
        end

        timeline = JSON.parse(res.body, {symbolize_names: true})
        if timeline.none?{ |t| t[:content].gsub(/<[^>]*>/, "") =~ /#{text}/ } then
          uri = URI.parse('https://mastodon.chotto.moe/api/v1/statuses')
          req = Net::HTTP::Post.new(uri)
          req['Authorization'] = 'Bearer ' + bot[:bearer_hash]
          req.body = "status=##{name} #{text} #traindelay #{yahoo_uri}"
          res = Net::HTTP.start(uri.hostname, uri.port, {use_ssl: true}) do |http|
            http.request(req)
          end
          puts "#{Time.now} #{bot[:name]} toot: code=#{res.code} body=#{res.body}"
        end
      end
    end
  end
  
  doc.css(".icnNormal").each do |normal_line|
    yahoo_uri = normal_line.parent.parent.css('a').attr('href').text.gsub(/^/,'https://transit.yahoo.co.jp')
    line = Nokogiri::HTML.parse(URI.open(yahoo_uri))
    name = line.css("h1.title").text
    date = Time.strptime(line.css(".normal p span").text, '(%m月%d日 %H時%M分')
    text = line.css(".normal p").text
    puts "#{Time.now} #{bot[:name]} #{name} #{text} #{yahoo_uri}"

    if Time.now - 15*60 < date then
      uri = URI.parse('https://mastodon.chotto.moe/api/v1/timelines/home')
      req = Net::HTTP::Get.new(uri)
      req['Authorization'] = 'Bearer ' + bot[:bearer_hash]
      res = Net::HTTP.start(uri.hostname, uri.port, {use_ssl: true}) do |http|
        http.request(req)
      end

      timeline = JSON.parse(res.body, {symbolize_names: true})
      if timeline.none?{ |t| t[:content].gsub(/<[^>]*>/, "") =~ /#{text}/ } then
        uri = URI.parse('https://mastodon.chotto.moe/api/v1/statuses')
        req = Net::HTTP::Post.new(uri)
        req['Authorization'] = 'Bearer ' + bot[:bearer_hash]
        req.body = "status=##{name} #{text} #traindelay #{yahoo_uri}"
        res = Net::HTTP.start(uri.hostname, uri.port, {use_ssl: true}) do |http|
          http.request(req)
        end
        puts "#{Time.now} #{bot[:name]} toot: code=#{res.code} body=#{res.body}"
      end
    end
  end
end

cron で巡回させる

6年ぐらいずっとこのプログラムを10分おきに動かしてました。
Yahoo!JAPANさん、ありがとうございました。
(先ほど、このBotは全て停止したので、この記事をもって供養とします。)

Discussion