🥝

ページの中から lazy load の画像 URL を抽出する。

2021/07/06に公開

tags: Ruby Nokogiri lazyload

本記事は、「マンガサイトにつひての色々な事情 03 」 からのつづきになっています。

https://qiita.com/dauuricus/items/563cbcc9776f66cb672e

ここまでのこと

「マンガサイトにつひての色々な事情 02 」において設定した

目標
ゴール設定は、オンラインリーディング型のサイトであるマンガ Thank(仮称)にコンテンツとして使用されている漫画について、すべて権利者を割り出して、並べて見ること。

については、「マンガサイトにつひての色々な事情 03 」までで到達しました。
=====> ブクログ web本棚サービス 色々な事情 (色々な事情,マンガサイトにつひての)
https://booklog.jp/profiletag/色々な事情

この記事の内容

ここでは、画像ファイルがすべて cloudflare 管轄のドメインのものを使用されているのか確認したい・・・というステップになるでしょう。
あるマンガサイトに使われている特定のマンガコンテンツについては、全て網羅的な属性 ... 書誌情報を得ました。この記事では、さらにマンガコンテンツの画像のデータについてを扱います。

「マンガサイトにつひての色々な事情 02 」 の中の「違法と思われるマンガ Thank(仮称)の html の構造をよく確認する」で簡単に説明した箇所を引用します。

違法と思われるコンテンツである漫画のスキャンデータ、もしくは電子書籍マンガからのコピーである画像ファイルは、概ね cloudflare 社[1]のキャッシュのイメージファイルが <img> で直接指定されている。

また、lazy loading で読み込まれている。
仕組みとしてはページに別のサーバーにある WordPress で管理された画像群の画像ファイル(ここが cloudflare のキャッシュになっているということで、それは当該のファイルの URL を逆引きしてドメインからドメイン所有者をわりだすと cloudflare になっているということ。いずれ図説したい。)をページ内に lazy loading で読み込んで表示させていることが確認できる。
シンプルなので理解しやすいはずである。

画像ファイルの置いてあるドメインについては、こちらからどうぞ。

lazy load の箇所

example.html
<figure class="gallery-item">
            <div class="gallery-icon portrait">
                <img width="750" height="1181" src="src="https://ssl.クラウドフレアだよ.store/wp-content/plugins/a3-lazy-load/assets/images/lazy_placeholder.gif" data-lazy-type="image" data-src="https://ssl.クラウドフレアだよ.store/wp-content/uploads/2019/09/16/aaaaaa-750x1181.jpg" class="attachment-large size-large lazy-loaded" alt="" loading="lazy"><noscript><img width="750" height="1181" src="https://ssl.クラウドフレアだよ.store/wp-content/uploads/2019/09/16/aaaaaa-750x1181.jpg" class="attachment-large size-large" alt="" loading="lazy" /></noscript>
            </div></figure>


<div class="gallery-icon portrait"> の中の <img>をクローズアップする。

<img width="750" height="1181" src="https://ssl.クラウドフレアだよ.store/wp-content/plugins/a3-lazy-load/assets/images/lazy_placeholder.gif" data-lazy-type="image" data-src="https://ssl.クラウドフレアだよ.store/wp-content/uploads/2019/09/16/aaaaaa-750x1181.jpg" class="lazy-hidden attachment-large size-large" alt="" loading="lazy">

このような <img> が、不特定多数 ( 50000*300 回程度頻出する可能性) あるような気がする・・・という気分で、ここからプリローダー画像ではない画像の URL ( attribute data-src の値。)だけを抽出することを考える。

Rf. a modern vanilla JavaScript version of the original Lazy Load plugin

https://github.com/tuupola/lazyload

シンプルなサンプル

require 'nokogiri'
require 'open-uri'
require 'csv'

URL = 任意のアドレスまたは database から読み込む

doc = Nokogiri::HTML(URI.open(URL))

title = doc.css('title').inner_text
figure = doc.css('figure')
#puts figure
#puts figure.inner_html

#img_tag = figure.css("img[src$='.jpg']")
#arr = []
#img_tag.each do |line|
#    arr.push(line.attr('src'))
#end
#arr.each do |data|
#    CSV.open("list.csv","a+") do |f|
#        f << [data]
#    end
#end

figure.css('img').each do |tag|
    if tag.attribute('data-src')
        p tag.attribute('data-src').value
        image = tag.attribute('data-src').value

        CSV.open("image-list.csv","a+") do |f|
            f << [URL,title,image]
        end
    end
end;nil

2 つのテーブルから読み込んだデータで lazy load の画像 URL を抽出する。

2つのデータベーステーブルは、こうなっていました。

mangathank.db

[tbl_manga]

    id INTEGER PRIMARY KEY,
    title text,
    url text,
    updated_datetime datetime,
    author text,
    book_title text


Rf.「ひとつめのデータベーステーブル [tbl_manga] 」
https://zenn.dev/kurocat/articles/eb233dc31bb285

bookdata.db

[tbl_ bookdata]

    id INTEGER PRIMARY KEY,
    book_title text,
    url text,
    author text,
    creatortranscription text,
    volume text,
    seriestitle text,
    publisher text,
    isbn text,
    mangathank_title text,
    ex_id integer
[tbl_manga] から [tbl_bookdata] へ


[tbl_bookdata] book_titleauthor でクエリを作り国会図書館サーチ API へ問い合わせ、[tbl_bookdata] のそれぞれのカラムへ書籍情報データを書き込む。


Rf.「ふたつめのデータベーステーブル [tbl_ bookdata] 」
https://qiita.com/dauuricus/items/563cbcc9776f66cb672e

2 つのテーブルから読み込んだデータで lazy load の画像 URL を抽出して csv ファイルに書き込む。

サンプル。よく読んで、ご注意ください。

require 'nokogiri'
#require 'open-uri'
require 'httparty'
require 'sqlite3'
require 'csv'
require 'time'

db1 = SQLite3::Database.open "mangathank.db"
db2 = SQLite3::Database.open "bookdata.db"

ex_id = 0

(1..51717).each do |count_up|
    address = db1.execute("select url from tbl_manga where id = '#{count_up}'")
    ex_title = db1.execute("select title from tbl_manga where id = '#{count_up}'")
    isbn = db2.execute("select isbn from tbl_bookdata where ex_id = '#{count_up}'")
    if isbn[0]
        isbn = isbn[0].pop
    else
        isbn = '' 
    end
    book = ex_title[0].pop
    URL = address[0].pop
    ex_id = count_up
    puts ex_id ,puts

    response = HTTParty.get(URL)
    doc = Nokogiri::HTML(response.body)
    #doc = Nokogiri::HTML(open(URL))

    doc.remove_namespaces!
    title = doc.css('title').inner_text
    figure = doc.css('figure')
    #puts figure
    #puts figure.inner_html

    figure.css('img').each do |tag|
        if tag.attribute('data-src')
            #p tag.attribute('data-src').inner_html
            image = tag.attribute('data-src').value

            CSV.open("image-list.csv","a+") do |f|
                f << [book,ex_id,URL,title,isbn,image]
            end
        end
    end
    #sleep 0.01
end

db1.close
db2.close;nil

こういふリバースエンジニアリングのようなプログラミングは、ふいにどこかで止まる・・・ということを危惧して、ループが途中で止まってもカウンターの最後を確認できるようにしておく方が良い。

見たところ、やりたいことはわかるプログラムだが、51717 * 300 個の画像ファイル と仮定すると、かなりの数の行数になるので、きっと動かないだろう。
さらに、SQLite3 のテーブルに読み込むプログラムも込みだと数時間動いていたが、芳しくなかった。カラム数を考えてみた。数百万・・・たくさんになる(サイトで使用されているマンガの画像の数分あります)。

止まらないプログラムに修正

とてもたくさんの csv ファイルができることになる。

require 'nokogiri'
require 'open-uri'
#require 'httparty'
require 'sqlite3'
require 'csv'
require 'time'

db1 = SQLite3::Database.open("mangathank.db")
db2 = SQLite3::Database.open("bookdata.db")

ex_id = 0
num = 0
limit_num = 51717

1.step(limit_num,50) {|x|
    (x..(x + 50)).each do |count_up|
        if count_up > limit_num
            break
        end

        if (count_up % 100) == 0
            num += 1
        end

        address = db1.execute("select url from tbl_manga where id = '#{count_up}'")
        ex_title = db1.execute("select title from tbl_manga where id = '#{count_up}'")
        isbn = db2.execute("select isbn from tbl_bookdata where ex_id = '#{count_up}'")
        author = db2.execute("select author from tbl_bookdata where ex_id = '#{count_up}'")
        if isbn[0]
            isbn = isbn[0].pop
        else
            isbn = '' 
        end
        if author[0]
            author = author[0].pop
        else
            author = '' 
        end

        book = ex_title[0].pop
        URL = address[0].pop
        ex_id = count_up
        puts
        puts ex_id
        puts author
        puts book 
        puts

        #response = HTTParty.get(URL)
        #doc = Nokogiri::HTML(response.body)
        doc = Nokogiri::HTML(open(URL))

        doc.remove_namespaces!
        title = doc.css('title').inner_text
        figure = doc.css('figure')
        #puts figure
        #puts figure.inner_html

        figure.css('img').each do |tag|
            if tag.attribute('data-src')
                #p tag.attribute('data-src').inner_html
                image = tag.attribute('data-src').value

                CSV.open("image-list_"+"#{num}"+".csv","a+") do |f|
                    f << [author,book,ex_id,URL,title,isbn,image]
                end
            end
        end
        sleep 0.01
    end
}

db1.close
db2.close;nil

ドメイン

コンテンツページ内でローディングされる画像ファイルのあるドメインを調べるモジュール。
つまり、それぞれマンガ画像ファイルが、どこのドメインにあるか調べるため、画像ファイルまでのフルパスではなくて、ホスト名を抽出します。

たとえば、画像ファイルがこのように lazy load に指定されてあるとして、

<img width="750" height="1181" src="https://ssl.クラウドフレアだよ.store/wp-content/plugins/a3-lazy-load/assets/images/lazy_placeholder.gif" data-lazy-type="image" data-src="https://ssl.クラウドフレアだよ.store/wp-content/uploads/2019/09/16/aaaaaa-750x1181.jpg" class="lazy-hidden attachment-large size-large" alt="" loading="lazy">

画像までのフルパスは、data-srcの値ですから、

"https://ssl.クラウドフレアだよ.store/wp-content/uploads/2019/09/16/aaaaaa-750x1181.jpg"

です。

ここから、

"ssl.クラウドフレアだよ.store"

を抽出します。

get_hostdomain_name.rb
        temp_hostname = ''
        figure.css('img').each do |tag|
            if tag.attribute('data-src')
                #p tag.attribute('data-src').inner_html
                image = tag.attribute('data-src').value
            ######    ######
                uri = URI.parse(image)
                if temp_hostname != uri.host
		    temp_hostname = uri.host
                    CSV.open("imagefile-hostdomain.csv","a++") do |f|
                        f << [uri.host.to_s]
                        p uri.host
                    end
                end
            ######    ######
                CSV.open("image-list_"+"#{num}"+".csv","a+") do |f|
                    f << [author,book,ex_id,URL,title,isbn,image]
                end
            end
        end

ひとつ前のプログラムコードに上記のホスト名を抽出するモジュールを追加して、さらに、存在する分だけデータベースimagefile_domain.dbに書き込むプログラムにしてみます。
すると、いったいいくつのホストドメインがあるのかわかります。

digging_imagefile-hostdomain.rb

code
digging_imagefile-hostdomain.rb
require 'nokogiri'
require 'open-uri'
#require 'httparty'
require 'sqlite3'
require 'csv'
require 'time'

SQL =<<EOS
create table tbl_hostdomain (
    id INTEGER PRIMARY KEY,
    domain_name text
);
EOS

new_db = SQLite3::Database.open("imagefile_domain.db")
new_db.execute(SQL)

db1 = SQLite3::Database.open("mangathank.db")
db2 = SQLite3::Database.open("bookdata.db")

ex_id = 0
num = 0
limit_num = 51717
counter = 0

1.step(limit_num,50) {|x|
    (x..(x + 50)).each do |count_up|
        if count_up > limit_num
            break
        end

        if (count_up % 100) == 0
            num += 1
        end

        address = db1.execute("select url from tbl_manga where id = '#{count_up}'")
        ex_title = db1.execute("select title from tbl_manga where id = '#{count_up}'")
        isbn = db2.execute("select isbn from tbl_bookdata where ex_id = '#{count_up}'")
        author = db2.execute("select author from tbl_bookdata where ex_id = '#{count_up}'")
        if isbn[0]
            isbn = isbn[0].pop
        else
            isbn = '' 
        end
        if author[0]
            author = author[0].pop
        else
            author = '' 
        end

        book = ex_title[0].pop
        URL = address[0].pop
        ex_id = count_up
        puts
        puts ex_id
        #puts author
        #puts book 
        #puts

        #response = HTTParty.get(URL)
        #doc = Nokogiri::HTML(response.body)
        doc = Nokogiri::HTML(URI.open(URL))

        doc.remove_namespaces!
        title = doc.css('title').inner_text
        figure = doc.css('figure')
        #puts figure
        #puts figure.inner_html
        
        temp_hostname = ''
        figure.css('img').each do |tag|
            if tag.attribute('data-src')
                #p tag.attribute('data-src').inner_html
                image = tag.attribute('data-src').value
            ######    ######
                uri = URI.parse(image)
                if temp_hostname != uri.host
                    temp_hostname = uri.host
                    if counter == 0 then
                        counter = 1
                        new_db.execute("insert into tbl_hostdomain (id, domain_name) values('#{counter}','#{uri.host}');")
                    end
                    search_flag = new_db.execute("select id from tbl_hostdomain where domain_name ='#{uri.host}' ;")
                    if search_flag.any? then
                        p counter,uri.host
                        next
                    else
                        counter += 1
                        new_db.execute("insert into tbl_hostdomain (id, domain_name) values('#{counter}','#{uri.host}');")
                    
                        CSV.open("imagefile-hostdomain"+"#{num}"+".csv","a++") do |f|
                            f << [uri.host.to_s]
                        end
                    end 
                end
                next
                #この後はスキップされます
            ######    ######
                CSV.open("image-list_"+"#{num}"+".csv","a+") do |f|
                    f << [author,book,ex_id,URL,title,isbn,image]
                end
            end
        end
        sleep 0.02
    end
}

new_db.close
db1.close
db2.close;nil

25 個のドメインが抽出されました。

csv から sqlite3 へ

require 'sqlite3'
require 'csv'

SQL =<<EOS
create table tbl_image_list (
    id INTEGER PRIMARY KEY,
    author text,
    title text,
    ex_id integer,
    url text,
    isbn text,
    page_title text,
    image_url text
);
EOS

count = 0 
new_db = SQLite3::Database.open("image_list.db")
new_db.execute(SQL)

CSV.foreach('image-list_0.csv') do |line|
    count += 1
    author = line[0]
    author.to_s.gsub!(/\'/,"\'\'")
    title = line[1]
    title.to_s.gsub!(/\'/,"\'\'")
    ex_id = line[2]
    url = line[3]
    isbn = line[4]
    page_title = line[5]
    page_title.to_s.gsub!(/\'/,"\'\'")
    image_url = line[6]
    id = count
    new_db.execute("insert into tbl_image_list (id, author, title, ex_id, url, isbn, page_title, image_url) values('#{id}','#{author}','#{title}','#{ex_id}','#{url}','#{isbn}','#{page_title}','#{image_url}');")
end

new_db.close

imgurl_url_title.rb
require 'nokogiri'
require 'open-uri'
require 'sqlite3'
#require 'csv'
require 'time'

db1 = SQLite3::Database.open("mangathank_new.db")

SQL =<<EOS                                                                     
create table tbl_imgurl (
    id INTEGER PRIMARY KEY,
    ex_id integer,
    imgurl text,
    url text,
    title text,
    author text,
    book_title text
    );
EOS


last_id = 0
db1.execute("select id from tbl_manga order by id desc limit 1 ;") do |data|
    last_id = data[0]
end

ex_id = 0
id = 0
total = 0
limit_num = last_id

current = 0

db2 = SQLite3::Database.open("imgurl"+"#{current}"+".db")
db2.execute(SQL)

#(1..last_id).each do |count_up|
1.step(limit_num,51) {|x|

    (x..(x + 50)).each do |count_up|
        if count_up > limit_num
            break
        end

        if (count_up % 100) == 0
            current += 1
            db2.close
            id = 0
            db2 = SQLite3::Database.open("imgurl"+"#{current}"+".db")
            db2.execute(SQL)
        end
        
        p count_up,current

        address = db1.execute("select url from tbl_manga where id='#{count_up}' ;")
        title = db1.execute("select title from tbl_manga where id='#{count_up}' ;")
        book_title = db1.execute("select book_title from tbl_manga where id='#{count_up}' ;")
        author = db1.execute("select author from tbl_manga where id='#{count_up}' ;")

        pp title 

        if address[0]
            address = address[0].pop
        end
        if title[0]
            title = title[0].pop
        end
        if !book_title[0][0].empty?
            book_title = book_title[0].pop
        else
            book_title = title.slice(/((?<=\]).+?$)/)
    
            if book_title !=nil then
                num = book_title.to_s.slice(/((?<=第)\d+(?=巻|卷$))/)
                book_title.to_s.gsub!(/((?=第).*(巻|卷))/,'')
                book_title.to_s.gsub!(/((?=第).*話)/,'')
                book_title.to_s.gsub!(/(.(?<=\()文庫版(?=\)).)/,'')
                book_title.to_s.gsub!(/(.(?<=\[)文庫版(?=\]).)/,'')
                book_title.to_s.gsub!(/文庫版/,'')
                book_title.to_s.gsub!(/(.(?<=\()完(?=\)).)/,'')
                book_title.to_s.gsub!(/(.(?<=【).*(?=】).)/,'')
                book_title.to_s.gsub!(/(.(?<=\[).+?(?=\]).)/,'')
                book_title.to_s.gsub!(/\'/,"\'\'")
                book_title.lstrip!
                book_title.rstrip!
            else
                book_title = title
    
                num = book_title.to_s.slice(/((?<=第)\d+(?=巻|卷$))/)
                book_title.to_s.gsub!(/((?=第).*(巻|卷))/,'')
                book_title.to_s.gsub!(/((?=第).*話)/,'')
                book_title.to_s.gsub!(/(.(?<=\()文庫版(?=\)).)/,'')
                book_title.to_s.gsub!(/(.(?<=\[)文庫版(?=\]).)/,'')
                book_title.to_s.gsub!(/文庫版/,'')
                book_title.to_s.gsub!(/(.(?<=\()完(?=\)).)/,'')
                book_title.to_s.gsub!(/(.(?<=【).*(?=】).)/,'')
                book_title.to_s.gsub!(/(.(?<=\[).+?(?=\]).)/,'')
                book_title.to_s.gsub!(/\'/,"\'\'")
                book_title.strip!
            end
    
            if num != nil then
                num = num.to_i
                book_title += ' ' + num.to_s
            end
        end
        if !author[0][0].empty?
            author = author[0].pop
        else
            author = title.slice(/(?<=\[).*?(?=\])/)
        end
    
        ex_id = count_up
    
    #    puts
    #    puts ex_id
    #    puts title
        puts author
        puts book_title 
        puts
    
        url = address
        doc = Nokogiri::HTML(URI.open(url))
        doc.remove_namespaces!
    
        page_title = doc.css('title').inner_text
        #p page_title
    
        figure = doc.css('figure')
        #puts figure
        #puts figure.inner_html
        
        temp_amount = 0
        figure.css('img').each do |tag|
            if tag.attribute('data-src')
                #p tag.attribute('data-src').inner_html
                imgurl = tag.attribute('data-src').value
                id += 1
                total += 1
                temp_amount += 1
                # id  # amount of total image file
                db2.execute("insert into tbl_imgurl (id,ex_id,imgurl,url,title,author,book_title) values('#{id}','#{ex_id}','#{imgurl}','#{url}','#{title}','#{author}','#{book_title}') ;")
            end
        end
        print 'amount of images:', temp_amount
        puts
        puts
        #sleep 0.01
    end
}

db2.close
db1.close
print 'amount of total images:',total

関連記事

https://qiita.com/dauuricus/items/67e97c93a753149e4c1d

https://zenn.dev/kurocat/articles/eb233dc31bb285

https://qiita.com/dauuricus/items/563cbcc9776f66cb672e

https://dev.to/dauuricus/01-350e

脚注
  1. 空中分解…海賊版サイト対策検討会はなぜ迷走したか https://www.yomiuri.co.jp/fukayomi/20181017-OYT8T50059/2/ ↩︎

Discussion