2023-01-14に更新

グレートクソアルゴリズム | くだらない youtube アルゴリズムを使用して youtube ビデオに関する情報を収集するくだらない Ruby プログラム

読了目安:22分

translate

クソアルゴリズム

クソアルゴリズムにたいして、クソアルゴリズムを利用して、必要なものだけを選別していくものを書いてみる。

え、突然なに ? と思うかもしれないが、やっぱり怒ってるんだな。
これじゃないロボってわかるかなぁ。

欲しかったロボはこれじゃない!世界中から子供たちの悲痛な叫びが聞こえる情操教育玩具。グッドデザイン賞受賞の伝説的玩具。
コレジャナイロボ(The Original Model)
https://www.assiston.co.jp/1595

わかんないと思うな。

コレジャナイ ユーチューブ

何を怒っているか整理すると、Youtube 検索結果について。

簡単に言うと、探したいものがあって、探してるときに、あなたの探してるものと関係ありそうなもの教えてあげる的に頼んでもない集合知をほいって添えられて、それを断るすべがないということについて。

探しているものがはっきりしていて、その言葉で検索かけているときに、ほいっ、あなたの前回見た動画から他の人が見たのこれだから、こんなの面白いみたいよ、どぞー !! って検索結果に混ぜられるの意味あると思うのか ?? まともに考えて。それ、おすすめ映画をレコメンドするアルゴリズムだよね。それ、バカでしかないからやめてほしいんだ。

それ、バカでしかないからやめてほしいんだ youtube . . .

例えば、ransomware というキーワードで検索したとして、それと前回たまたま見た何かの動画とは全く関係ない趣向で、今検索してるのに、じゃあこれもって一言も ransomware のことなんて発言しない youtuber のたくさん視聴された関連動画を検索結果に混ぜてくるのって、「機械学習してるからー」てことを人間が配慮してあげないとしたら、意味不明のバカでしかない。

意味不明のバカでしかない . . .

意味不明のバカな結果を出すアルゴリズムを権威的に出してくるって、意味不明なバカレベルである。だから、やめて、と思うだけなんだな。
他人の行動も、過去の自分のトレンドも全く関係がない TPO が読めないアルゴリズムって、ただの邪魔だ、ということ。
そんなことは当たり前過ぎるのに、なぜか当然のように諦めさせらるとっても不毛なシステムだ。
これがなんでもかんでも Collaborative filtering 。

この配慮のない他人の行動をどんなときにも当てはめようとしてくる様式をクソアルゴリズムと呼ばずにはおれない。

でも、クソとかバカとかいうのも、どうにもならないわかりきったことで、単に Google が正しくキュレーションされたものより、てっとりばやく消費される季節ネタのようなバズを見えるとこに置いた方が広告の流入になるという方針なだけで、そういった正攻法はかつて創業者によって「情報の精度が落ちる要因」とされているので、クソなことをわかってやっていて、かつて 2000 年代に蔓延したアホみたいなインデックス型のサーチエンジン並みのクオリティを実現するアルゴリズムを新参のカウンターとして、知的に駆逐した彼ら google 自身が「今」作っているということ。

The Age of PageRank is Over
09 Nov, 2022
Vladimir Prelovac
CEO, Kagi Inc.
https://blog.kagi.com/age-pagerank-over

もちろん、そんな 20 年以上レイドバックしたテクは 22 年以上前の板フロート掲示板王子によってチートされている。「クソをクソだと見抜けない人が使っている」ということが、クソの臭い嗅ぎ王子には見透かされたと言っていい。たぶん、世界中同じような状況じゃないかと思う。だって、結局古いんだもん。

というところまででクソアルゴリズムを悪く言うのはここまでにして、じゃあ、どうすればいいの ?
自分の決めたキーワードとの関連はどうやって判断するのか ? を考えてみる。

キーワードと youtube 動画の相関は、タイトルにキーワードが含まれるか ? だけで判断するということにする。

含まれていたら、関連動画としてリストに追加するし、含まれていなければそれ以上関係性を考慮しない。これだけのストレートなルールを設定する。
なので、youtube 動画のタイトルが web ページのデータ上のどこにあるのかを割り出すことが必要

キーワードで youtube 検索するには、

https://www.youtube.com/results?search_query=ransomware

で、get する。そうすると、検索結果を表示するリダイレクトが youtube ページで行われる。

この行為を Ruby コードで書くと、

code : 01

require 'uri'
require 'net/http'

words = "ransomware"
keywords = URI.encode_www_form(search_query: words)
target = 'https://www.youtube.com/results?' << keywords
resp_0 = Net::HTTP.get_response(URI.parse(target))

ページのなかの、<script> のうちの1つに検索結果の情報が詰まっている。

<script> というタグはいくつもあって、その 34 番目が検索結果の JSON に該当するよ。
44 番目になったかも ?

code : 02

require 'nokogiri'

doc = Nokogiri::HTML.parse(resp_0.body, nil,'utf-8')
script_tag = doc.css('script')

json_str = ""
script_tag.each_with_index {|element,i|
  if i == 33 then
    json_str = element.to_s[58..-11]
    #<script nonce="fX_rKtuwcvo7T-wFeZz4CQ">var ytInitialData =
  end
}
doc = nil

34 番目<script> に入っているものを取り出すと、 JSON データ構造としては余計なスクリプトが含まれているので、後に JSON としてパースするのに邪魔になるので、element.to_s[58..-11] というように、ノードからテキストにして、インデックスを使ってスライスして、JSON データとして扱える strings にします。この youtube レクチャーを参考にしましたよ。

Python Web Scraping: JSON in SCRIPT tags : John Watson Rooney

Python code:
image
image

https://rentry.co/u89yc
https://rentry.co/t94yo

code : 03

require 'json'

begin
    script33 = JSON.parse(json_str)
rescue => e
    puts e
#    return nil
end

JSON を parse するとは hash にするということなので、key と value のひたすら折り重なる廻廊となる、key も value(s) もあらかじめ知ってたら、スムーズだけれども、この JSON のデータ構造は知らない、知りたくないという場合は、いったん JSON をテキストファイルにして、vim エディターでよく見る。必要なら編集してキーワードサーチして、じっと見る。必要なら何時間も、何日間も見る。
大体わかったら、JSON らしくないレギュラーエクスプレッションズで処理できる。
取り出したいのは videoId と title 。

videoId は、url 上ではこうなっている。
https://www.youtube.com/watch?v= + videoId
なので基本的な url から videoId を取り出すのは、watch?v= 以降の 11 文字を切り出したらいい。

rf.
stackoverflow.com : How do I get the youtube video-id from a URL

これが基本事項だけども、以下は JSON データから "videoId"="●●●●●●●●●●●" というところから抜き出していくコードになっている。

code : 04

script33 = JSON.parse(json_str) # JSON to Hash

videoid_list = [] # temp work space for youtube 'video-id' s
videotitle_list = [] # temp work space for youtube 'video title' s

script33.each do |y,x|
  if y == 'contents'
    x.each do |yy,xx|
      match =  xx.to_s.match(/(\"videoId\"=\>.{13})/)
      if match != nil
        temp = $&[11..-1]
        videoid_list.push(temp)
        while $'.match(/(\"videoId\"=\>[^\[].{12})/) != nil do
          videoid_list.push($&[11..-1])
        end
      end
      match2 =  xx.to_s.match(/\"title\"=\>\{\"runs.+?\[\{\"text\"\=\>(.*?)\}\],/)
      if match2 != nil
        temp = $1
        videotitle_list.push(temp)
          while $'.match(/\"title\"=\>\{\"runs.+?\[\{\"text\"=\>(.*?)\}\],/) != nil do
            videotitle_list.push($1)
        end
      end
    end
  end
end

youtube でキーワード検索した結果の videoId と、 title ぽいものが、いくつづつかそれぞれ配列に入る。
videoIdについては以下のページの一番最初のほうで少し書いたので参考にしてほしい。

アルゴリズムはチートされる 注目と広告、アテンション エコノミー / attention economy
https://crieit.net/posts/c2b7c645c32fda0b2cffd3aea91d6a01

ここからは、データは重複していく可能性があることに気をつけていく。
title ぽいものは、いちばん最後に title じゃないものが配列にプッシュされているので取り除いておく。これは、Hash の処理で取り出さずにレギュラーエクスプレッションズの文字列処理で条件を書いてスキャンしたので起こったことなので、きっちり Hash で取り出せばうまいこといくと思われる。ただ、今回はレギュラーエクスプレッションズの処理にした。

code : 05

videoid_list.uniq!
videotitle_list.pop # trash
videotitle_list.uniq!

strings の値、それぞれの配列の値、並び方は、とにかくよーく確かめてね。
確かめないと、以後の行程で全く意味ないからね。

code : 06

id_list = []
title_list = []

videoid_list.each_with_index {|content,ind|
  if videotitle_list[ind] != nil
#    puts &quot;-&quot;*20
#    puts &quot;#{ind}  https://www.youtube.com./watch?v=#{content[1..-2]}&quot;
#    puts videotitle_list[ind]
    mmmm = videotitle_list[ind].match(/#{words}/i)
    if mmmm != nil
      id_list.push(content)
      title_list.push(videotitle_list[ind])
    end
  else
#    puts &quot;-&quot;*20
#    puts &quot;no title found&quot;
#    puts &quot;#{ind}  https://www.youtube.com./watch?v=#{content[1..-2]}&quot;

  end
}

videoid_list.clear
videotitle_list.clear

検索した結果のタイトルにキーワードにした ransomware が含まれていればリストに追加するし、キーワードが含まれていない場合は無関係という判断でリストから外します。
残すリストをそれぞれの配列 id_list = [] title_list = [] に追加していく。

code : 07

id_list = id_list.zip(title_list)

zip してひとまとめにしておく。

こうなってるかな。

code : 08

id_list.each_with_index do |list,ind|
  puts "#{ind}: videoId => #{list[0]}"
  puts "#{ind}: title => #{list[1]}"
end

じゃあ、まず、この第一回目の検索の結果を使って、10 スレッドづつ https get するようにしてテスト。

code : 09

#mute = Mutex.new
counter = 0
db_counter = 0

while id_list.size > 0 && counter < 20000 do
  counter += 1
  threads = []

  10.times do |k|
    if id_list.size > 0 
      threads << Thread.new do
        #mute.synchronize do

            tempwork = id_list.shift
            if tempwork == nil
              next
            end
            target = "https://www.youtube.com/watch?v=" << tempwork[0][1..-2]
            id_title_list = work(target,words)
            if id_title_list != nil
              id_list.concat(id_title_list)
            end

        #end
      end
    end
  end

  threads.each(&:join)
  id_list.uniq!

  puts id_list.size
end

code : 10

work 関数
https://rentry.co/dzyu8

work 関数は https get から始まる code : 01 ~ code : 04 とよく似ているが、今度は最初の https get で得たキーワード検索結果の JSON とは違っているので、<script> の順番も違い、41 番目<script> から JSON データをとってきている。
ということで当然 JSON のデータ構造も、キーワード検索結果の code : 04 ものとは別物なので、そこから videoId や title の値をスクレイプするレギュラーエクスプレッションズも新たなものになっている。

44 番目になったかも ?

code : 11

code : 01 ~ code : 10 までを全部まとめて、さらに SQLite3 データベースに保存していくようにするとこうなる。

https://rentry.co/359r5

ここまでで、ようやく半分。Step 1 として、これを補完する Step 2。

https://rentry.co/b4ugy

これで、全部の半分。
並べて見ると

Step1

# encoding: UTF-8
require 'net/http'
require 'uri'
require 'sqlite3'
require 'time'
require 'json'
require 'nokogiri'

SQL =<<EOS                                                                     
create table youtube (
    id INTEGER PRIMARY KEY,
    videoid text,
    chan_id text,
    publ_id text,
    title text
    );
EOS

system("mkdir" ,"youtube__")
db = SQLite3::Database.open("./youtube__/youtube.db")
db.execute(SQL)

words = "ransomware"
keywords = URI.encode_www_form(search_query: words)
target = 'https://www.youtube.com/results?' << keywords
resp_0 = Net::HTTP.get_response(URI.parse(target))

doc = Nokogiri::HTML.parse(resp_0.body, nil,'utf-8')
script_tag = doc.css('script')

json_str = ""
script_tag.each_with_index {|element,i|
  if i == 33 then
    json_str = element.to_s[58..-11]
   #<script nonce="fX_rKtuwcvo7T-wFeZz4CQ">var ytInitialData =
  end
}
doc = nil

script33 = JSON.parse(json_str)

videoid_list = []
videotitle_list = []
script33.each do |y,x|
  if y == 'contents'
    x.each do |yy,xx|
      match =  xx.to_s.match(/(\"videoId\"=\>.{13})/)
      if match != nil
        temp = $&[11..-1]
        videoid_list.push(temp)
        while $'.match(/(\"videoId\"=\>[^\[].{12})/) != nil do
          videoid_list.push($&[11..-1])
        end
      end
      match2 =  xx.to_s.match(/\"title\"=\>\{\"runs.+?\[\{\"text\"=\>(.*?)\}\],/)
      if match2 != nil
        temp = $1
        videotitle_list.push(temp)
          while $'.match(/\"title\"\=\>\{\"runs.+?\[\{\"text\"\=\>(.*?)\}\],/) != nil do
            videotitle_list.push($1)
        end
      end
    end
  end
end

videoid_list.uniq!
videotitle_list.pop # trash scan
videotitle_list.uniq!

id_list = []
title_list = []

videoid_list.each_with_index {|content,ind|
  if videotitle_list[ind] != nil
#    puts &quot;-&quot;*20
#    puts &quot;#{ind}  https://www.youtube.com./watch?v=#{content[1..-2]}&quot;
#    puts videotitle_list[ind]
    mmmm = videotitle_list[ind].match(/#{words}/i)
    if mmmm != nil
      id_list.push(content)
      title_list.push(videotitle_list[ind])
    end
  else
#    puts &quot;-&quot;*20
#    puts &quot;no title found&quot;
#    puts &quot;#{ind}  https://www.youtube.com./watch?v=#{content[1..-2]}&quot;

  end
}

videoid_list.clear
videotitle_list.clear

id_list = id_list.zip(title_list)

def work(target,words)
  begin
    resp_1 = Net::HTTP.get_response(URI.parse(target))
  rescue => e
    puts e
    sleep 1
    return nil
  end
  doc = Nokogiri::HTML.parse(resp_1.body, nil,'utf-8')
  script_tag = doc.css('script')

  json_str = ""
  script_tag.each_with_index {|element,i|
    if i == 40 then
      json_str = element.to_s[58..-11]
    end
  }
  script_tag = nil

  title_tag = doc.css('title')
  doc = nil
  mmmm = title_tag[0].to_s.match(/#{words}/i)
  if mmmm == nil
    return nil
  end

  title_tag = nil

  begin
    script40 = JSON.parse(json_str)
  rescue => e
    puts e
    return nil
  end

  videoid_list2 = []
  videotitle_list2 = []

  script40.each {|y,x|
    if y.to_s == "contents"
      match1 = x.to_s.match(/\{\"title\"=\>\{\"runs\"=\>\[\{\"text\"=\>\"(.*?)\"/)
      if match1 != nil
#        puts&quot;&quot;
#        puts&quot;-&quot;*30 
#        puts $1
#        puts $~
#        puts&quot;-&quot;*30 

        tempstrings = $'
        while tempstrings.match(/\"title\"=\>\{\"accessibility\"=\>\{\"accessibilityData\"=\>\{\"label\"=\>"(.*?)\"\}\},/) do
          if $0 == nil
            break
          end
#          puts&quot; _&quot;*20 
#          puts&quot;&quot;
#          puts $1
          tempstrings = $'
          videotitle_list2.push($1)
          match_videoid = /\"commandMetadata\"=\>\{\"webCommandMetadata\"=\>\{\"url\"=\>\"\/watch\?v=(.{11}\"),/ =~ $'
          if match_videoid != nil
#            puts (&quot;\&quot;&quot; + $1)
            videoid_list2.push("\"" + $1)
          end
        end
      else
#        puts &quot;-&quot;*30
#        puts y x
#        puts &quot;can't find the title&quot;
        next
      end
    end
  }

  videoid_list2.uniq!
  videotitle_list2.uniq!

  videoid_list3 = []
  videotitle_list3 = []

  videoid_list2.each_with_index {|content,ind|
    if videotitle_list2[ind] == nil
#      puts &quot;-&quot;*30
#      puts &quot;error&quot;
#      puts ind,content 
#      puts &quot;https://www.youtube.com./watch?v=#{content[1..-2]}&quot;
#      puts &quot;-&quot;*30
      next
    end
    mmmm = videotitle_list2[ind].match(/#{words}/i)
    if mmmm == nil
#      puts &quot;-&quot;*30
#      puts ind,content 
#      puts videotitle_list2[ind]
#      puts &quot;skip&quot;
      next
    end
    #puts "-"*30
    #puts ind,content 
    videoid_list3.push(content[0..-1])
    #puts "https://www.youtube.com./watch?v=#{content[1..-2]}"
    #puts videotitle_list2[ind]
    videotitle_list3.push(videotitle_list2[ind])
  }
  videoid_list2.clear
  videotitle_list2.clear
  ziped_list = videoid_list3.zip(videotitle_list3)
  videoid_list3.clear

#  ziped_list.each_with_index do |list,ind|
#    puts &quot;#{ind}: #{list[0]}&quot; 
#    puts &quot;#{ind}: #{list[1]}&quot; 
#  end

  return ziped_list
end

#youtube = Struct.new("Youtube", :videoid, :title, :date)
#youtube_data = youtube.new("video_id","title","date")

#mute = Mutex.new
counter = 0
db_counter = 0
while id_list.size > 0 && counter < 20000 do
  counter += 1
  threads = []

  10.times do |k|
    if id_list.size > 0 
      threads << Thread.new do
        #mute.synchronize do

            tempwork = id_list.shift
            if tempwork == nil
              next
            end
            target = "https://www.youtube.com/watch?v=" << tempwork[0][1..-2]
            id_title_list = work(target,words)
            if id_title_list != nil
              id_list.concat(id_title_list)
            end

        #end
      end
    end
  end
  threads.each(&:join)
  id_list.uniq!

  db.transaction do
    id_list.each_with_index {|data,num|
      if num == db_counter
        v_id = data[0]
        title = data[1].delete("\t\r\n")
        sth = db.prepare("insert into youtube (id,videoid,title) values(?,?,?)")
        sth.execute(db_counter,v_id,title)
        db_counter += 1
      end
    }
  end
  puts id_list.size
end

Step 2

# encoding: UTF-8
require 'net/http'
require 'uri'
require 'sqlite3'
require 'time'
require 'json'
require 'nokogiri'

# ./youtube__/youtube.db
#
#SQL =<<EOS
#create table youtube (
#    id INTEGER PRIMARY KEY AUTOINCREMENT,
#    videoid text,
#    chan_id text,
#    publ_id text,
#    title text
#    );
#EOS

db = SQLite3::Database.open("./youtube__/youtube.db")

def working(target,id_pack)
  puts target
  begin
    resp_0 = Net::HTTP.get_response(URI.parse(target))
  rescue =>e
    puts e.message
    sleep 1
    return nil
  end
  doc = Nokogiri::HTML.parse(resp_0.body, nil,'utf-8')
  #title_tag = doc.css('title')
  script_tag = doc.css('script')
  json_str = ""
  script_tag.each_with_index {|element,i|
    if i == 40 then
      json_str = element.to_s[58..-11]
      #<script nonce="fX_rKtuwcvo7T-wFeZz4CQ">var ytInitialData =
    end
  }
  doc = nil

  begin
    script40 = JSON.parse(json_str)
  rescue => e
    puts e
    return nil
  end
  ids = id_pack.new("videoid","publisheddate","channelid","title")

  script40.each {|y,x|
    if y.to_s == "contents"
      puts"-"*30 
      puts "URL: #{target}"
      ids.vi = target.sub("https://www.youtube.com/watch?v=","")
      match_date = x.to_s.match(/\"dateText\"=\>\{\"simpleText\"=\>\"(.{10})/)
      if match_date != nil
        ids.da = match_date[1]

        ch_id = $'.match(/\"browseId\"=\>\"(.*?)\",/) 
        if ch_id != nil
          #puts "channelId: #{$1}"
          ids.ch = $1
        end
      end
      match1 = x.to_s.match(/\{\"title\"=\>\{\"runs\"=\>\[\{\"text\"=\>\"(.*?)\"/)
      if match1 != nil
#        puts&quot;&quot;
#        puts&quot;-&quot;*30 
        #puts "title: #{$1}"
        ids.ti = $1
#        puts $~
      end
    end
  }

  #struct data
  return ids
end

threads = []
iiii = 0
mute = Mutex.new

id_pack = Struct.new("Id_pack",:vi,:da,:ch,:ti) 

lastid = db.execute("SELECT id FROM youtube order by id DESC limit 1")

db.execute("SELECT id,videoid FROM youtube").each do |videoid|
  row = videoid[1]
  num = videoid[0]
  puts "#{row} #{num}"

  str1 = row.gsub("\"","")
  if iiii < 10 && num < lastid[0][0]
    iiii += 1
    threads << Thread.new do
      target = 'https://www.youtube.com/watch?v=' << str1
      ids = working(target,id_pack)
      if ids != nil
        mute.synchronize do

          db.transaction do
            v_id = str1
            date = ids.da
            chid = ids.ch
            title = ids.ti
            sth = db.prepare("update youtube set chan_id=?, publ_id=? where id=?")
            sth.execute(chid,date,num)
          end

        end
      end
    end
  else 
    iiii = 0
    target = 'https://www.youtube.com/watch?v=' << str1
    ids = working(target,id_pack)
    if ids != nil
      mute.synchronize do

        db.transaction do
          v_id = str1
          date = ids.da
          chid = ids.ch
          title = ids.ti
          sth = db.prepare("update youtube set chan_id=?, publ_id=? where id=?")
          sth.execute(chid,date,num)
        end

      end

      threads.each(&:join)
    end
  end
end

exit

Step 1 で ransomware というキーワードで検索して、タイトルのなかに ransomware という言葉が含まれる youtube 動画の videoId , title の情報が youtube というデータベースのテーブルに保存されます。

Step 2 で タイトルのなかに ransomware という言葉が含まれる youtube 動画の youtube というデータベースのテーブルから読み出された youtubeId をもとに、channnel id , published された日付が youtube テーブルに追記されます。

youtube table

    id INTEGER PRIMARY KEY,
    videoid text,
    chan_id text,
    publ_id text,
    title text

こういうデータベースができあがるようになりました。
タイトルと年月日時で、年代の古いものから並べるなどに使えるデータです。
Step 1 , 2 は、ひとつにまとめることができますね。
ひとつにまとめて、さらに動画の長さのデータもあればいいと思います。

youtube での動画の長さは、

"duration": "PT4M13S"

というように埋め込まれているようです。PT から始まって分と秒で表されています。

https://rentry.co/dyxuo

ツイッターでシェア
みんなに共有、忘れないようにメモ

tomato

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント