さんだすメモ

さメモ

技術ブログでは、ない・・・

Twitterを使い始めた[Ruby]

はじめに

追記: 今は使えない方法です。

Rubyを使って検索しました。明らかに最善じゃないところがありますが、メモってことでお願いします。
扱いやすさのためClass Tsitterを作ります。名前は適当です。

  • initialize(username, password)
  • get_rate_limit_status(resource_list)
  • search_tweet(search_word, count:40)

initialize(username, password)について

インスタンス生成の際にログインしてapiを使える状態にします。

インスタンス変数を初期化

@hash_cookie = {'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0'}
@hash_header = {}

Cookie記録用とリクエストヘッダに使うためのハッシュです。

/loginを見てログインフォームを確認する

# host = 'twitter.com', port = 443
https_twitter = Net::HTTP.new('twitter.com', 443)
https_twitter.use_ssl = true
https_twitter.verify_mode = OpenSSL::SSL::VERIFY_PEER

# リダイレクト先を設定してloginにアクセス
path = '/login'
query = 'hide_message=true&redirect_after_login=https%3A%2F%2Ftweetdeck.twitter.com%2F%3Fvia_twitter_login%3Dtrue'
response_login = https_twitter.start { |https|
  https.get("#{path}?#{query}")
}

# cookieを登録
self.register_cookie(response_login)

# form欄にあるauthenticity_tokenを取得
response_login.body.force_encoding('utf-8').match(/<input type="hidden" value="([^"]*)" name="authenticity_token">/)
authenticity_token = $1

フォームは次のような形になっていて、この中にpostすべき情報が書かれています。

<form action="https://twitter.com/sessions" class="LoginForm js-front-signin" method="post"
  data-component="dialog"
  data-element="login"
>
...
</form>

/sessionsにpost

# formに入力してpost
path = '/sessions'
response_sessions = https_twitter.start { |https|
  request = Net::HTTP::Post.new(path, @hash_header)
  request.set_form_data(
    {
      'session[username_or_email]' => username,
      'session[password]' => password,
      'remember_me' => '0',
      'return_to_ssl' => 'true',
      'redirect_after_login' => 'https://tweetdeck.twitter.com/?via_twitter_login=true',
      'authenticity_token' => authenticity_token
    }
  )
  https.request(request)
}

# cookieを登録
self.register_cookie(response_sessions)

tweetdeckに戻る

# host = 'tweetdeck.twitter.com', port = 443
https_tweetdeck = Net::HTTP.new('tweetdeck.twitter.com', 443)
https_tweetdeck.use_ssl = true
https_tweetdeck.verify_mode = OpenSSL::SSL::VERIFY_PEER

path = '/'
query = 'via_twitter_login=true'
response_tweetdeck = https_tweetdeck.start { |https|
  request = Net::HTTP::Get.new("#{path}?#{query}", @hash_header)
  https.request(request)
}

bundle...jsからbearer_tokenを取得

# host = 'ton.twimg.com', port = 443
https_ton = Net::HTTP.new('ton.twimg.com', 443)
https_ton.use_ssl = true
https_ton.verify_mode = OpenSSL::SSL::VERIFY_PEER

# urlを取得
url_bundle = response_tweetdeck.body.force_encoding('utf-8').match(/<script src=([^<>]*)><\/script>\z/)[1]

uri_bundle = URI.split(url_bundle)
path = uri_bundle[5]
response_bundle = https_ton.start { |https|
  https.get(path)
}

# bearer_tokenを取得
bearer_token = response_bundle.body.force_encoding('utf-8').match(/bearer_token:"([^"]*)"/)[1]

どうも固定らしいのでいちいち調べる必要はないですね。

ヘッダーを編集する

@hash_header['Authorization'] = "Bearer #{bearer_token}"
@hash_header['X-Csrf-Token'] = @hash_cookie['ct0'].split('=')[1]
@hash_header['X-Twitter-Auth-Type'] = 'OAuth2Session'
@hash_header['X-Twitter-Client-Version'] = 'Twitter-TweetDeck-blackbird-chrome/4.0.190220122730 web/'

X-Csrf-TokenCookiect0と同じ値である必要があります。
以上でapiを利用できるようになります。

接続する

# host = 'api.twitter.com', port = 443
@https_api = Net::HTTP.new('api.twitter.com', 443)
@https_api.use_ssl = true
@https_api.verify_mode = OpenSSL::SSL::VERIFY_PEER

get_rate_limit_status(resource_list)について

apiの制限までの残り回数やリセットされる時刻などを得られます。

def get_rate_limit_status(resource_list)
  path = "/1.1/application/rate_limit_status.json"
  query = "resources=#{resource_list.join(',')}"

  response_rate_limit_status = @@https_api.start { |https|
    request = Net::HTTP::Get.new("#{path}?#{query}", @hash_header)
    https.request(request)
  }

  return response_rate_limit_status
end

rate_limit_statusを取得するのにもapi制限があるので使いすぎないよう注意します。'rate_limit_context' => 'resources' => 'search' => '/search/universal'といった具合に辿り、'remaining''reset'などで欲しいパラメータを得られます。

search_tweet(search_word, count:40)について

path = '/1.1/search/universal.json'
query = URI.escape("q=#{search_word}&count=#{count}&modules=status&result_type=recent&pc=false&ui_lang=ja&cards_platform=Web-13&include_entities=1&include_user_entities=1&include_cards=1&send_error_codes=1&tweet_mode=extended&include_ext_alt_text=true&include_reply_count=true")

response_search = @https_api.start { |https|
  request = Net::HTTP::Get.new("#{path}?#{query}", @hash_header)
  https.request(request)
}

return response_search

検索クエリにsince_idmax_idを使うことで検索範囲をツイート単位で指定できます。

応答

JSON形式でハッシュが返ってきます。内容は大体次のような感じで取り出せます。

hash_search_result = JSON.parse(response_search.body.force_encoding('utf-8'))

# modulesに各ツイートの内容が配列で格納されている
modules = hash_search_result['modules'].each { |hash_module|

  full_text = hash_module['status']['data']['full_text']
  screen_name = hash_module['status']['data']['user']['screen_name']
  id_str = hash_module['status']['data']['id_str']
  
  url = "https://twitter.com/#{screen_name}/status/#{id_str}"
  
  # Wed Feb 20 04:09:32 +0000 2019
  # 曜日 月 日 時:分:秒 +0000 年
  hash_module['status']['data']['created_at'].match(/^(...) (...) (..) (..):(..):(..) (\+....) (....)$/)
  time = Time.gm($8, $2, $3, $4, $5, $6, 0).localtime('+09:00')

  # ######### #
  # 適当な処理 #
  # ######### #
}

api制限

レスポンスヘッダにapi制限についての情報も付いてきます。

# 残り回数
response_search_result.get_fields('x-rate-limit-remaining')

# 残り回数がリセットされる時刻
response_search_result.get_fields('x-rate-limit-reset')

エラーなど

2種類ほどエラーが出ました。独立して起こっていた気がします。

  • response_search_resultの内容(つまりhash_search_result)が{ "errors" => ...}のような感じでキーにmodulesがない
  • レスポンスヘッダに'x-rate-limit-remaining''x-rate-limit-reset'がない

最後に

search_tweetは毎回セッションを閉じる形になるので、keep-aliveな通信ではないみたいですね。今回は時間がなかったのでsearch_tweetをたくさん回してしまいました。また機会があれば、負荷をかけない形でやりたいです。

    ステーション

はじめに

補助的な作業が要らないので完成すれば簡単にできます。

手順

意味不明なドットは省略です。

とりあえず必要なところにアクセスしておく

host_vcms = "vcms...................."
port_vcms = 443
https_vcms = Net::HTTP.new(host_vcms, port_vcms)
https_vcms.use_ssl = true
https_vcms.verify_mode = OpenSSL::SSL::VERIFY_PEER

host_vms = "vms...................."
port_vms = 443
https_vms = Net::HTTP.new(host_vms, port_vms)
https_vms.use_ssl = true
https_vms.verify_mode = OpenSSL::SSL::VERIFY_PEER

最新回の情報を取り出す

GETリクエストを送るときにヘッダーにX-Reuested-Withを加えないと拒否されます。

path_program = "/api/.........../#{program_title}"
request_program = Net::HTTP::Get.new(path_program)
request_program['X-Requested-With'] = "XMLHttpRequest"
response_program = https_vcms.start { |https|
  https.request(request_program)
}

# ハッシュを表す文字列が渡される
# 中でnullが使われているので、とりあえずnull = nilとしておく
null = nil
hash_response_program = eval(response_program.body.force_encoding("utf-8"))

# title, video_idを取得
title = hash_response_program[:episode][:program_name] + "-" + hash_response_program[:episode][:name]
video_id = hash_response_program[:episode][:video][:id].to_s

video_idがわかった後

# play_checkにアクセスしてトークンを取得
# ヘッダーに`X-Reuested-With`を加えておく必要がある
path_play_check = "/............./play_check"
query_play_check = "video_id=#{video_id}"
request_play_check = Net::HTTP::Get.new("#{path_play_check}?#{query_play_check}")
request_play_check['X-Requested-With'] = "XMLHttpRequest"
response_play_check = https_vcms.start { |https|
  https.request(request_play_check)
}

# ハッシュを表す文字列が渡される
# null = nil
hash_response_play_check = eval(response_play_check.body)

# キー情報にアクセスするためのCookieを取得する
url_playlist = hash_response_play_check[:playlist_url]
uri_playlist = URI.split(url_playlist)
path_playlist = uri_playlist[5]
query_playlist = uri_playlist[7]
request_playlist = Net::HTTP::Get.new("#{path_playlist}?#{query_playlist}")
response_playlist = https_vms.start { |https|
  https.request(request_playlist)
}

# Domain, Pathはffmpegで必須
cookie_m3u8 = response_playlist['Set-Cookie'].match(/(_logica.*); path=\/; secure; HttpOnly/)[1] + ";Domain=vms....................;Path=/"
url_m3u8 = response_playlist.body.match(/http.*$/)[0]

system "ffmpeg -cookies #{cookie_m3u8} -i #{url_m3u8} -bsf:a aac_adtstoasc -movflags faststart -acodec copy C:/.../#{title}.m4a"

最後に

video_idは4桁の数字です。重労働ですが、色々調べたいならffmpegで先頭に-t 00:00:03などとすればよいのではないでしょうか(ラジオ)。オープニングは毎回同じなので、やり方は考えていませんがなにかしらを基準に判定させるのもアリですね。

  らぶ動画

はじめに

video_idに対応するものを1つ保存します。video_idは適当な名前です。補助的な作業が要らないので完成すれば簡単にできます。

手順

意味不明なドットは省略です。

ログインする

require 'net/https'
require 'openssl'

host_love = "https://.........com"
port_love = 443
https_love = Net::HTTP.new(host_love, port_love)
https_love.use_ssl = true
https_love.verify_mode = OpenSSL::SSL::VERIFY_PEER

# ログインする
response_love = https_love.start { |https|
  https_love.post('/user....?LoginForm', "plan_id=#{plan_id}&actmode=LoginDo&userno=#{userno}&password=#{password}")
}

# cookieを取得
header_love = {}
response_love.get_fields('Set-Cookie').each { |cookie|
  header_love['Cookie'] = cookie.split(';')[0]
}

動画の情報を見つける

# 動画のページにアクセスする
response_movie = https_love.get("/.../movie_#{"%02d" % video_id}.html", header_love)

# 存在しなければ飛ばす
if response_movie.code = "404"
  # 終了処理
end

# ページの本体
body_movie = response_movie.body.force_encoding('utf-8')

# タイトル
body_movie.match(/\<p\>(.*[^\s])(\s*)\|(\s*)([^\s].*)\<\/p\>/)
title = $1 + "_" + $4

HLSストリーミングだった場合

  • https://cc........../init?...
  • https://cc........../get_info?...

の2つを順番に調べます。

body_movie.match(/E....\.P.....\.embedkey="([^"]*)"/)
embedkey = $1

host_cc = "cc.........."
port_cc = 443
https_cc = Net::HTTP.new(host_cc, port_cc)
https_cc.use_ssl = true
https_cc.verify_mode = OpenSSL::SSL::VERIFY_PEER

# https://cc........../init?...
path_init = "/init"
query_init = "embedkey=#{embedkey}"
response_init = https_cc.start { |https|
  https.get("#{path_init}?#{query_init}")
}

# ハッシュを表す文字列が渡される
# 中でnullが使われているので、とりあえずnull = nilとしておく
null = nil
hash_init = eval(response_init.body)

# https://cc........../get_info?...
# initから得た情報を使ってアクセスする
path_get_info = "/get_info"
array_query_get_info = Array.new()
[:videotype,:host,:id_vhost,:id_contents].each { |key|
  array_query_get_info.push("#{key}=#{hash_init[key]}")
}
query_get_info = array_query_get_info.join('&')
response_get_info = https_cc.start { |https|
  https.get("#{path_get_info}?#{query_get_info}")
}

# ハッシュを表す文字列が渡される
# null = nil
hash_get_info = eval(response_get_info.body)

# get_info内にいくつかindex.m3u8があるが、その中からbitrateが高いものを選ぶ
url_m3u8 = ""
max_bitrate = 0
hash_get_info[:qualities].each { |hash|
  if max_bitrate < hash[:bitrate].to_i
    max_bitrate = hash[:bitrate].to_i
    url_m3u8 = hash[:url]
  end
}

# ffmpegを使ってmp4で保存
system "ffmpeg -i #{url_m3u8} -flags +loop+global_header -bsf:a aac_adtstoasc -movflags faststart -c copy \"C:/.../#{title}.mp4\""

mp4だった場合

ソースのURLが貼ってある場合です。

body_movie.match(/\<source src="([^"]*)"/)
url_mp4 = $1
system "ffmpeg -i #{url_mp4} -flags +loop+global_header -movflags faststart -c copy \"C:/.../#{title}.mp4\""

最後に

video_idは2桁まで0埋めの数字です。ffmpegのコマンドについてはよくわかっていないんですが、それでも便利ですね。

ラジオ

有用かどうかはその人次第なので、合えば活用してみてください。響での全125回で合っていると思います。そこそこ大変でした。

1286 1332 1382 1427 1456 1529 1585 1633 1697 1739 1805 1851 1905 1945 2020 2073 2126 2190 2242 2304 2351 2408 2460 2518 2570 2628 2696 2753 2803 2865 2922 2964 3024 3086 3137 3194 3251 3300 3370 3402 3445 3497 3551 3597 3654 3703 3755 3787 3852 3893 3940 3961 3984 4041 4069 4122 4163 4211 4245 4307 4343 4397 4436 4497 4545 4594 4637 4699 4748 4804 4850 4898 4948 5007 5051 5112 5154 5219 5261 5266 5316 5383 5451 5507 5568 5625 5674 5729 5792 5854 5906 5958 6036 6061 6109 6172 6227 6280 6340 6384 6427 6480 6544 6588 6630 6686 6742 6787 6842 6899 6942 6981 7044 7079 7136 7181 7245 7740 7802 7879 8017 8095 8308 8502

画像の枠を切り取る[C]

はじめに

libpngについて参考にしたもの
スクショを編集する用途で作りましたが、上手にスクショすればいいだけだと気づいたので多分使いません。スクショをあとから切り取るのに使えるときがあるかも?

ソースコードおよび実行ファイル

Github.com

処理の流れ

  • 行・列を両端から調べていき、単色であるものをチェックする
  • 単色の行・列を除いた画像を出力する

行・列を両端から調べていき、単色であるものをチェックする

int rowIsSolidColor(int j, RAWDATA_t raw); // 行が単色ならFALSE
int columnIsSolidColor(int i, RAWDATA_t raw); // 列が単色ならFALSE

int main(int argc, char *argv[]) {
  
  ...
  
  int *jList; // 上端・下端から続いている単色の行をFALSEにする
  jList = (int*)malloc(sizeof(int)*raw.height);
  for (j = 0; j < raw.height; j++) jList[j] = TRUE;
  for (j = 0; j < raw.height; j++) {
    if (rowIsSolidColor(j, raw)) jList[j] = FALSE;
    else break;
  }
  for (j = raw.height - 1; j >= 0; j--) {
    if (rowIsSolidColor(j, raw)) jList[j] = FALSE;
    else break;
  }
  
  int *iList; // 左端・右端から続いている単色の行をFALSEにする
  iList = (int*)malloc(sizeof(int)*raw.width);
  for (i = 0; i < raw.width; i++) iList[i] = TRUE;
  for (i = 0; i < raw.width; i++) {
    if (columnIsSolidColor(i, raw)) iList[i] = FALSE;
    else break;
  }
  for (i = raw.width - 1; i >= 0; i--) {
    if (columnIsSolidColor(i, raw)) iList[i] = FALSE;
    else break;
  }
  
  ...
  
}

単色の行・列を除いた画像を出力する

int deleteRowsColumns(int *jList, int *iList, RAWDATA_t *pRaw) {
  int i, j, c; // 元の画像の座標
  int ri, rj; // 新しい画像の座標
  
  // 新しい画像の各パラメータ
  unsigned char *data;
  unsigned int width;
  unsigned int height;
  unsigned int ch;
  
  height = 0;
  for (j = 0; j < pRaw->height; j++) if (jList[j]) height++;
  width = 0;
  for (i = 0; i < pRaw->width; i++) if (iList[i]) width++;
  ch = pRaw->ch;
  data = (unsigned char*)malloc(sizeof(unsigned char) * width * height * ch);
  
  rj = 0;  
  for (j = 0; j < pRaw->height; j++) {
    if (jList[j]) { // 単色が続いていた行をスキップ
      ri = 0;
      for (i = 0; i < pRaw->width; i++) {
        if (iList[i]) { // 単色が続いていた列をスキップ
          for (c = 0; c < pRaw->ch; c++) {
            data[ch * (ri + rj * width) + c] = pRaw->data[pRaw->ch * (i + j * pRaw->width) + c];
          }
          ri++;
        }
      }
      rj++;
    }
  }
  free(pRaw->data);
  
  // 新しい画像のパラメータに置き換える
  pRaw->data = data;
  pRaw->width = width;
  pRaw->height = height;
  return 0;
}

はてなブログをローカルで管理する[Ruby]

はじめに

ローカルで作業した方が便利なことが多いと思ったので作りました。

大まかな流れ

1つ目と3つ目については、Ruby ではてな OAuth のアクセストークンを取得するを参考にしました。

はてなにログインし、アプリを認証する

OAuth認証を使った連携では、アプリケーションとは別枠ではてなにログイン・認証しないといけません。今回のアプリは自作であり、信用できるので、認証作業もアプリにやらせます。

host_hatena = 'www.hatena.com'
port_hatena = 443
https_hatena = Net::HTTP.new(host_hatena, port_hatena)
https_hatena.use_ssl = true
https_hatena.verify_mode = OpenSSL::SSL::VERIFY_PEER

# ログインし、Cookieを取得
path_login = '/login'
query_login = "name=#{HATENA_ID}&password=#{PASSWORD}"
response_login = https_hatena.start { |https_login|
  https_hatena.post(path_login, query_login)
}
cookie_hatena = response_login['set-cookie']

# request_token.authorize_urlにアクセスして、フォームに入力する
uri_oauth = URI.split(request_token.authorize_url)
path_oauth = uri_oauth[5]
query_oauth = uri_oauth[7]
response_oauth = https_hatena.start { |https|
  request_oauth = Net::HTTP::Post.new(path_oauth, {'Cookie'=>cookie_hatena})
  request_oauth.set_form_data(
    {
      'rkm'=>'j7eJ2tohvlHmKr2D4lL4WA',
      'oauth_token' => URI.unescape(query_oauth.split('=')[1]),
      'name' => '許可する'
    }
  )
  https.request(request_oauth)
}

# レスポンスからoauth_verifierを抜き出す
body_response_oauth = response_oauth.body.force_encoding('utf-8')
oauth_verifier = body_response_oauth.match(/<div class=verifier><pre>([^<>]*)<\/pre><\/div>/)[1]

適当に調べて試行錯誤したところ、こんな感じになりました。
ちなみに、フォームは次のような形式でした。 送り先がhttps://www.hatena.com/oauth/authorizeであることと、送るべき内容がわかります。

<form action="/oauth/authorize" method="post">
  <input type="hidden" name="rkm" value="xxxxxxxxxxxxxxxxxx">         <!--固定されてました-->
  <input type="hidden" name="oauth_token" value="xxxxxxxxxxxxxxxxxx"> <!--毎回変わります-->
  <input class="oauth-btn btn-yes" type="submit" name="name" value="許可する" />
</form>

記事の管理

  • ブログエントリの一覧の取得
  • mdファイルを1つずつ調べ、投稿・編集する

APIについては、はてなブログAtomPubを参照しました。

ブログエントリの一覧の取得

記事の確認・編集に使うメンバURIは次のような形になっています。

"https://blog.hatena.ne.jp/#{HATENA_ID}/#{BLOG_ID}/atom/entry/#{entry_id}"

エントリ一覧のリクエストは次のように送りました。

access_token.request(:get, "https://blog.hatena.ne.jp/#{HATENA_ID}/#{BLOG_ID}/atom/entry?page=xxxxxxxx")

記事をインポートするときは、ここからentry_idやタイトルなどを抽出し、適切な形に保存します。一度に7件しかブログエントリを取得できないので、?page=xxxxxxを付け加えて、必要な情報を順次とりだします。

投稿・編集する

Markdown記法で書いているので、mdファイルを操作します。

mdファイルの管理のための書式

オプションにあたる部分は書式を自分で決めてコメントアウトなどで書くようにします。memberURIupdateddraftYNtitlecategoryなどがあれば十分だと思います。memberURIはリクエストを送るのに使用し、他はリクエストの中身に使用します。

投稿する

mdファイルからupdateddraftYNcategorytitle、本文の情報を抜き出します。そして、それらを反映したXML形式の文字列xml_entryを作り、投稿します。xml_entryは次のような内容になります。

xml_entry = "<?xml version=\"1.0\" encoding=\"utf-8\"?><entry xmlns=\"http://www.w3.org/2005/Atom\" xmlns:app=\"http://www.w3.org/2007/app\"><title>#{data_entry[:title].encode(xml: :text)}</title><author><name>#{HATENA_ID}</name></author><content type=\"text/x-markdown\">#{data_entry[:markdown].encode(xml: :text)}</content><updated>#{data_entry[:updated]}</updated>"
data_entry[:category].each { |category|
  xml_entry += "<category term=\"#{category.encode(xml: :text)}\"/>"
}
xml_entry += "<app:control><app:draft>#{data_entry[:draftYN]}</app:draft></app:control></entry>"

リクエストは次のように送りました。

response = access_token.post( "https://blog.hatena.ne.jp/#{HATENA_ID}/#{BLOG_ID}/atom/entry", xmlEntry, {'Content-Type'=>'application/xml'})

response.code"201"ならokです。
投稿したら、entry_idupdatedを取得してmdファイルに追加します。私はresponse['location']respose.bodyから抽出しました。

編集する

投稿と同じように、必要な情報を集めてリクエストを送ります。今回は次のように送りました。

response = access_token.put( "https://blog.hatena.ne.jp/#{HATENA_ID}/#{BLOG_ID}/atom/entry", xmlEntry, {'Content-Type'=>'application/xml'})

こちらもresponse.codeを確認しておくといいと思います。

最後に

大体のことを付け焼刃でやっているので、OAuth認証の作業、投稿・編集のリクエストの送り方は特に苦労しました。
投稿・編集をすべての記事でやると時間がかかるので、更新時にバックアップを取るような形にして、変化があれば更新というようにしました。