え、突然なに ? と思うかもしれないが、やっぱり怒ってるんだな。
これじゃないロボってわかるかなぁ。
欲しかったロボはこれじゃない!世界中から子供たちの悲痛な叫びが聞こえる情操教育玩具。グッドデザイン賞受賞の伝説的玩具。
コレジャナイロボ(The Original Model)
https://www.assiston.co.jp/1595
わかんないと思うな。
何を怒っているか整理すると、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 動画のタイトルが web ページのデータ上のどこにあるのかを割り出すことが必要。
キーワードで youtube 検索するには、
https://www.youtube.com/results?search_query=ransomware
で、get する。そうすると、検索結果を表示するリダイレクトが youtube ページで行われる。
この行為を Ruby コードで書くと、
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 番目になったかも ?
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
https://rentry.co/u89yc
https://rentry.co/t94yo
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"="●●●●●●●●●●●"
というところから抜き出していくコードになっている。
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 で取り出せばうまいこといくと思われる。ただ、今回はレギュラーエクスプレッションズの処理にした。
videoid_list.uniq!
videotitle_list.pop # trash
videotitle_list.uniq!
strings の値、それぞれの配列の値、並び方は、とにかくよーく確かめてね。
確かめないと、以後の行程で全く意味ないからね。
id_list = []
title_list = []
videoid_list.each_with_index {|content,ind|
if videotitle_list[ind] != nil
# puts "-"*20
# puts "#{ind} https://www.youtube.com./watch?v=#{content[1..-2]}"
# 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 "-"*20
# puts "no title found"
# puts "#{ind} https://www.youtube.com./watch?v=#{content[1..-2]}"
end
}
videoid_list.clear
videotitle_list.clear
検索した結果のタイトルにキーワードにした ransomware
が含まれていればリストに追加するし、キーワードが含まれていない場合は無関係という判断でリストから外します。
残すリストをそれぞれの配列 id_list = []
title_list = []
に追加していく。
id_list = id_list.zip(title_list)
zip してひとまとめにしておく。
こうなってるかな。
id_list.each_with_index do |list,ind|
puts "#{ind}: videoId => #{list[0]}"
puts "#{ind}: title => #{list[1]}"
end
じゃあ、まず、この第一回目の検索の結果を使って、10 スレッドづつ https get するようにしてテスト。
#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
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 : 01 ~ code : 10 までを全部まとめて、さらに SQLite3 データベースに保存していくようにするとこうなる。
ここまでで、ようやく半分。Step 1 として、これを補完する Step 2。
これで、全部の半分。
並べて見ると
# 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 "-"*20
# puts "#{ind} https://www.youtube.com./watch?v=#{content[1..-2]}"
# 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 "-"*20
# puts "no title found"
# puts "#{ind} https://www.youtube.com./watch?v=#{content[1..-2]}"
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""
# puts"-"*30
# puts $1
# puts $~
# puts"-"*30
tempstrings = $'
while tempstrings.match(/\"title\"=\>\{\"accessibility\"=\>\{\"accessibilityData\"=\>\{\"label\"=\>"(.*?)\"\}\},/) do
if $0 == nil
break
end
# puts" _"*20
# puts""
# puts $1
tempstrings = $'
videotitle_list2.push($1)
match_videoid = /\"commandMetadata\"=\>\{\"webCommandMetadata\"=\>\{\"url\"=\>\"\/watch\?v=(.{11}\"),/ =~ $'
if match_videoid != nil
# puts ("\"" + $1)
videoid_list2.push("\"" + $1)
end
end
else
# puts "-"*30
# puts y x
# puts "can't find the title"
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 "-"*30
# puts "error"
# puts ind,content
# puts "https://www.youtube.com./watch?v=#{content[1..-2]}"
# puts "-"*30
next
end
mmmm = videotitle_list2[ind].match(/#{words}/i)
if mmmm == nil
# puts "-"*30
# puts ind,content
# puts videotitle_list2[ind]
# puts "skip"
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 "#{ind}: #{list[0]}"
# puts "#{ind}: #{list[1]}"
# 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
# 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""
# puts"-"*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
から始まって分と秒で表されています。
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント