マンガサイト
漫画サイト。 それは漫画がブラウザで読める web サイトのことである。
漫画サイトには大きく分けて二種類ある。一方が違法サイトで、もう一方が合法サイトである。 仕組みは表面上はどちらもよく似ている。内部的には違うが、目的が違うので手段が違うと言っていいだろうが、大まかには同じようなものだ。
ここでは、違法サイトについて調べてみたい。
まず、あるひとつの違法サイトでのコンテンツの総数を数えてみたい。 現段階で、数え上げたのは四万四千のタイトル数だった。これは全てがコミックの1巻分ではなくて、第何話や、週刊誌の第何号というものを含めての数で、プラスこの数の20%分くらいがまだ隠れ層になっていて表出していない。
これはどう云うことかというと、いずれ説明するが、今回とった手法が関係しているが、例えば 100 回ランダムに違法サイトのページアドレスを出して、データベースに記録するということを繰り返して、次回はデータベースに記録されているもの以外をデータベースに記録していくようにしたとして、100 回のうち 20 回ほどが、記録されてないアドレスになってきた場合、続けていくと100 回のうち 15 、10 、とだんだんと記録されていないアドレスがランダムでは出にくくなっていくことになる。 このことをここで、隠れ層と云った。 あくまでも、違法サイトのコンテンツページのアドレスを前もって知っているわけではなく、ページにあるマンガ画像が表示されない方法で、ダウンロードせずにコンテンツが埋め込まれている URL を収集して、漫画のタイトルや著者情報を分析していく。
マンガサイトを調査 実践。( Ruby )
有効な URL と 有効ではない URL を調査し、データベースに記録しよう。
Ruby 言語を使って https リクエストして調査してみましょう。 有効なURLとは漫画の画像があるページのことになります。すなわち、マンガのサイトにある全てのコンテンツの数を全部数えてみようということになります。 対象は漫画Bankです。
Python 言語の場合はこっちにあります。
ポリシーについてはこちら
- レポート この日本語の翻訳ニュースの配信の約1時間後、漫画bankはサイトを閉鎖。 本稿以降のプログラムは対象のサイトが消失。
curl -is "https://mangabank.org/watch/?tour=/vol" | grep location
ターミナルコマンドを実行してリダイレクトされるアドレスを表示するには上記のようになりま。
require 'net/http' require 'uri' require 'sqlite3' SQL =<<EOS create table url ( id INTEGER PRIMARY KEY, url text ); EOS ## ---------------------------------- # _INVALID DB ===> DB1 db1 = SQLite3::Database.open("mb_urls_INVALID.db") tb1 = db1.execute("SELECT COUNT(*) FROM sqlite_master WHERE TYPE='table' AND NAME='url';") if tb1[0][0] == 0 then db1.execute(SQL) last_id1 = 0 else last1 = db1.execute("SELECT id FROM url ORDER BY id DESC LIMIT 1;") puts "invalid" pp last1[0][0] puts " ---------------------------------- " last_id1 = last1[0][0] end # ---------------------------------- # _VALID DB ===> DB db = SQLite3::Database.open("mb_urls_VALID.db") tb = db.execute("SELECT COUNT(*) FROM sqlite_master WHERE TYPE='table' AND NAME='url';") if tb[0][0] == 0 then db.execute(SQL) last_id = 0 else last = db.execute("SELECT id FROM url ORDER BY id DESC LIMIT 1;") puts "valid" pp last[0][0] puts " ---------------------------------- " last_id = last[0][0] end count = last_id count1 = last_id1 # ---------------------------------- target_url ="https://mangabank.org/watch/?tour=/vol/" #uri = URI.parse(target_url) # extract redirect URL def get_redirect_url(uri) redirect = Net::HTTP.get_response(uri)['location'] return redirect end # ---------------------------------- 50000.times do |repeat| uri = URI.parse(target_url) redirect_url = get_redirect_url(uri) puts redirect_url address = redirect_url s_number = address.split('/')[-1] puts s_number number = s_number.to_i def dbr(address,number,db1,count1,db,count,second) gate = false again = false 11.times do |x| puts " ---------------------------------- " puts x ###### <1 check invalid db if x == 0 then puts " ---------------------------------- " flag1 = nil flag1 = db1.execute("SELECT id FROM url WHERE url=\"#{address}\" ;") if !flag1.empty? then puts "flag 1:INVALID DB id=#{flag1[0][0]} #{address}" db1.execute("UPDATE url SET url='null' WHERE id=#{flag1[0][0]} ;") puts "is VALID / remove from INVALID DB" gate = true end if second != true then number = number - 10 address = "https://mangabank.org/#{number}/" end end ###### 1> if x != 0 then number = number + 1 address = "https://mangabank.org/#{number}/" puts " ---------------------------------- " puts address ###### <2 flag2 = nil flag2 = db1.execute("SELECT id FROM url WHERE url=\"#{address}\" ;") if !flag2.empty? then puts "flag 2:INVALID DB id=#{flag2[0][0]} #{address}" puts "#{number} + INVALID" if gate != true then next end end ###### 2> puts " ---------------------------------- " uri = URI.parse(address) res = Net::HTTP.get_response(uri) puts "#{address} responce code: #{res.code}" if res.code != '200' then count1 += 1 puts "insert INVALID DB id: #{count1} -> #{address}" db1.execute("INSERT INTO url (id,url) VALUES (?,?)",count1,address ) # db1.commit if second == true then break end next elsif !flag2.empty? and res.code == '200' then puts "flag 2:INVALID DB id=#{flag2[0][0]} + #{address}" db1.execute("UPDATE url SET url='null' WHERE id=#{flag2[0][0]} ;") puts "is VALID / remove from INVALID DB" end end flag = nil flag = db.execute("select id from url where url=\"#{address}\" ;") if !flag.empty? then puts "flag 0: id=#{flag[0][0]} #{address}" puts "already exsist in DB" puts break else if x == 0 then uri = URI.parse(address) res = Net::HTTP.get_response(uri) puts "#{address} responce code: #{res.code}" end if res.code == '200' then count += 1 puts "insert VALID DB id: #{count} -> #{address}" db.execute("INSERT INTO url (id,url) VALUES (?,?)",count,address ) # db.commit again = true next elsif res.code != '200' then count1 += 1 puts "insert NOT-VALID-DB id: #{count1} -> #{address}" db1.execute("INSERT INTO url (id,url) VALUES (?,?)",count1,address ) # db1.commit end end end return count1,count,address,again end second = false count1,count,address,again = dbr(address,number,db1,count1,db,count,second) puts limit = 5 while again == true do puts " ---------------------------------- " puts " " + limit.to_s number = number + 1 address = "https://mangabank.org/#{number}/" puts address uri = URI.parse(address) res = Net::HTTP.get_response(uri) puts "#{address} responce code: #{res.code}" if res.code == '200' then second = true count1,count,address,again = dbr(address,number,db1,count1,db,count,second) limit = 5 else limit -= 1 puts " Retry #{limit}" ###### <3 flag1 = nil flag1 = db1.execute("SELECT id FROM url WHERE url=\"#{address}\" ;") if !flag1.empty? then puts "flag 3: id=#{flag1[0][0]} #{address}" puts "#{number} INVALID" if limit < 1 then again = false end next end ###### 3> count1 += 1 puts "insert INVALID DB id: #{count1} -> #{address}" db1.execute("INSERT INTO url (id,url) VALUES (?,?)",count1,address ) ## db1.commit if limit < 1 then again = false end end end end db.close db1.close
2つのデータベースをマージするためのもの。Ruby
##---- Lua ##local sqlite3 = require("lsqlite3") ## ##--[[local db1 = sqlite3.open('lua_mb_urls_not.db') -- Lua ##local db2 = sqlite3.open('mb_urls_not.db') -- other ## ##local newdb = sqlite3.open('mb_url_notadditive.db') -- for working space ##local newdb1 = sqlite3.open('lua_mb_url_not1.db') -- additive & order by url ##]] ##---- Lua ####----> Ruby > ##db1 = SQLite3::Database.open "lua_mb_urls_not.db" # Lua ##db2 = SQLite3::Database.open "mb_urls_not.db" # made from python code ##newdb = SQLite3::Database.open "mb_url_notadditive.db" # additive work space ##newdb1 = SQLite3::Database.open "mb_url_not1.db" # additive & order by url ####----< Ruby < ##---- Lua ##local db1 = sqlite3.open('lua_mb_urls_new.db') -- Lua ##local db2 = sqlite3.open('mb_urls_new.db') -- other ## ##local newdb = sqlite3.open('mb_url_additive.db') -- for working space ##local newdb1 = sqlite3.open('lua_mb_url_new1.db') ## ##newdb:exec[[ ## CREATE TABLE IF NOT EXIST url (id INTEGER PRIMARY KEY, url text); ##]] ## ##newdb1:exec[[ ## CREATE TABLE IF NOT EXIST url (id INTEGER PRIMARY KEY, url text); ##]] ##---- Lua ##----> Ruby > require 'sqlite3' db1 = SQLite3::Database.open "lua_mb_urls_new.db" # Lua db2 = SQLite3::Database.open "mb_urls_new.db" # made from python code newdb = SQLite3::Database.open "mb_url_additive.db" # additive work space newdb1 = SQLite3::Database.open "mb_url_new1.db" # additive & order by url SQL =<<EOS create table IF NOT EXISTS url( id INTEGER PRIMARY KEY, url text ); EOS newdb.execute(SQL) newdb1.execute(SQL) ##----< Ruby < ##---- Lua ##local smt1 = "SELECT id FROM url ORDER BY id DESC LIMIT 1 ;" ## ##local last1 = 0 ##local last2 = 0 ## ##for id in db1:urows(smt1) do ## last1 = id ##end ## ##for id in db2:urows(smt1) do ## last2 = id ##end ## ##print("db1 has "..last1.." URLs") ##print("db2 has "..last2.." URLs") ##---- Lua ##----> Ruby > last1 = db1.execute("select id from url order by id desc limit 1") last2 = db2.execute("select id from url order by id desc limit 1") k = last1[0].pop # offset l = last2[0].pop puts "db1 has #{k} URLs" puts "db2 has #{l} URLs" i = 0 newdb_last = newdb.execute("select id from url order by id desc limit 1") if !newdb_last.empty? then newcount = newdb_last[0].pop # offset else newcount = 0 end ##----< Ruby < ##---- Lua ##local newcount = 0 ##for last_id in newdb:urows(smt1) do ## newcount = last_id ##end ## ##local i = 0 ## ##for url_data in db2:urows("SELECT url FROM url ;") do ##--[[ ##for url_data in db2:urows("SELECT url FROM url ORDER BY url ;") do ##]] ## i = i + 1 ## print("copy from db2 ") ## print(i) ## print("total "..last2) ## ## local existflag = 0 ## ## for exist in newdb:urows("SELECT id FROM url WHERE url=\""..url_data.."\" ;") do ## existflag = exist ## print(url_data.." is aleady exist in DB / skip") ## end ## ## if existflag == 0 then -- record ## newcount = newcount + 1 ## print() ## print("newdb : "..newcount.." "..url_data) ## local stmt2 = newdb:prepare[[ INSERT INTO url VALUES (:id, :url) ]] ## stmt2:bind_names{ id = newcount, url = url_data } ## stmt2:step() ## stmt2:reset() ## stmt2:finalize() ## end ##end ## ##db2:close() ## ##i = 0 -- reset ## ##for url_data in db1:urows("SELECT url FROM url ;") do ##--[[ ##for url_data in db1:urows("SELECT url FROM url ORDER BY url ;") do ##]] ## i = i + 1 ## print("copy from db1 ") ## print(i) ## print("total "..last1) ## ## local existflag = 0 ## ## for exist in newdb:urows("SELECT id FROM url WHERE url=\""..url_data.."\" ;") do ## existflag = exist ## print(url_data.." is aleady exist in DB / skip") ## end ## ## if existflag == 0 then -- record ## newcount = newcount + 1 ## print() ## print("newdb : "..newcount.." "..url_data) ## local stmt2 = newdb:prepare[[ INSERT INTO url VALUES (:id, :url) ]] ## stmt2:bind_names{ id = newcount, url = url_data } ## stmt2:step() ## stmt2:reset() ## stmt2:finalize() ## end ##end ## ##db1:close() ##---- Lua ##----> Ruby > l.times do |index| i += 1 res = db2.execute("select url from url where id='#{i}' ;") if res.empty? then puts "no data / skip" next end x = res[0][0] # url puts "copy from db2" p i,l res2 = newdb.execute("select url from url where url='#{x}' ;") if !res2.empty? then p x puts 'already exist in db / skip' next end puts newcount += 1 url = x #url.gsub!(/\'/,"\'\'") puts "#{newcount} #{url}" newdb.execute("insert into url (id, url) values( '#{newcount}','#{url}') ;") end db2.close i = 0 k.times do |index| i += 1 res = db1.execute("select url from url where id='#{i}' ;") if res.empty? then puts "no data / skip" next end x = res[0][0] # url f = newdb.execute("select url from url where url='#{x}' ;") puts "copy from db1" p i,k if !f.empty? then p res[0] puts 'already exist in db / skip' next else newcount += 1 url = x #url.gsub!(/\'/,"\'\'") puts "#{newcount} #{url}" newdb.execute("insert into url (id, url ) values( '#{newcount}','#{url}') ;") end end db1.close ##----< Ruby < ##---- Lua ##print("new DB last id : "..newcount) ##---- Lua ##----> Ruby > puts "new DB last id:#{newcount}" ##----< Ruby < ##---- Lua ##local newdb1_last = 0 ##for last_id in newdb1:urows(smt1) do ## newdb1_last = last_id ##end ## ##local counter = newdb1_last -- first time ==> 0 ##---- Lua ##----> Ruby > newdb1_last = newdb1.execute("select id from url order by id desc limit 1") if !newdb1_last.empty? then counter = newdb1_last[0].pop # offset else counter = 0 end ##----< Ruby < ##---- Lua ##i = 0 -- reset ##---- Lua ##----> Ruby > i = 0 ##----< Ruby < ##---- Lua ##for url_data in newdb:urows("SELECT url FROM url ORDER BY url ;") do ## i = i + 1 ## print("copy from newdb ") ## print(i) ## print("total "..newcount) ## ## local existflag = 0 ## ## for exist in newdb1:urows("SELECT id FROM url WHERE url=\""..url_data.."\" ;") do ## existflag = exist ## print(url_data.." is aleady exist in DB / skip") ## end ## ## if existflag == 0 then -- record ## counter = counter + 1 ## print() ## print("newdb1 : "..counter.." "..url_data) ## local stmt2 = newdb1:prepare[[ INSERT INTO url VALUES (:id, :url) ]] ## stmt2:bind_names{ id = counter, url = url_data } ## stmt2:step() ## stmt2:reset() ## stmt2:finalize() ## end ##end ## ##--[[ ##newdb1:close() ##newdb:close() ##]] -- end -- ##---- Lua ##----> Ruby > newdb.execute("select url from url order by url ;") do |row| i += 1 url = row[0] #url.gsub!(/\'/,"\'\'") f = newdb1.execute("select url from url where url='#{url}' ;") puts "copy from newdb" p i,newcount if !f.empty? then p url puts 'already exist in db / skip' next else counter = counter + 1 puts "#{counter} #{url}" newdb1.execute("insert into url (id, url ) values( '#{counter}','#{url}') ;") end end newdb1.close newdb.close ##----< Ruby <
漫画BANK マンガサイトを調査 実践。( python )
Lisp 風だと
hy 1.0a3 using CPython(default) 3.9.7 on Linux => (import requests) => (setv res (requests.get "https://mangabank.org/watch/?tour=/vol/" :allow_redirects False)) => (type res) <class 'requests.models.Response'> => (get res.headers "location")
pip install pycurl
import pycurl c = pycurl.Curl() #c.setopt(c.URL, 'https://mangabank.org/watch/?tour=/vol/') ## Follow redirect. #c.setopt(c.FOLLOWLOCATION, 1) # True: 1 / False : 0 #for x in range(1): # c.perform() # #print(c.getinfo(c.REDIRECT_URL)) # print(c.getinfo(c.EFFECTIVE_URL)) # #c.close() #import requests import sqlite3 SQL = """ create table url( id int primary key, url text ); """ ## first time only db = sqlite3.connect('mb_urls_not.db') db.execute(SQL) db.close() db = sqlite3.connect('mb_urls_new.db') db.execute(SQL) db.close() con1 = sqlite3.connect('mb_urls_not.db') cur1 = con1.cursor() con = sqlite3.connect('mb_urls_new.db') cur = con.cursor() last_id1 = cur1.execute("select id from url order by id desc limit 1;") index_num1 = last_id1.fetchone() if index_num1 != None: count1 = int(*index_num1) else: count1 = 0 last_id = cur.execute("select id from url order by id desc limit 1;") index_num = last_id.fetchone() if index_num != None: count = int(*index_num) else: count = 0 from urllib.parse import urlparse import re c.setopt(c.WRITEFUNCTION, lambda bytes: len(bytes)) for i in range(10000): # Follow redirect. c.setopt(c.FOLLOWLOCATION, 0) # True: 1 / False : 0 c.setopt(c.URL, 'https://mangabank.org/watch/?tour=/vol/') c.perform() #res = c.getinfo(c.EFFECTIVE_URL) res = c.getinfo(c.REDIRECT_URL) address = res number = re.sub(r'\/','',urlparse(res).path) print() print(" " + number) ## number = int(number) - 10 ## address = 'https://mangabank.org/'+str(number)+'/' def dbr(c,address,number,cur1,count1,cur,count,second): gate = False for x in range(11): print(x,end=' ') again = False ###### <1 check invalid db if x == 0: flag1 = None flag1 = cur1.execute("select id from url where url=? ;",[address]) flagcheck = flag1.fetchone() if flagcheck != None: print("flag 1: id=",*flagcheck,address) cur1.execute("update url set url=? where id=? ;",["null",*flagcheck]) #cur1.commit() print(address,"is VALID / remove from NOT-VALID") gate = True if second != True: number = int(number) - 10 address = 'https://mangabank.org/'+str(number)+'/' ###### 1> if x != 0: number = int(number) + 1 address = 'https://mangabank.org/'+str(number)+'/' # print(address) ###### <2 flag2 = None flag2 = cur1.execute("select id from url where url=? ;",[address]) flagcheck = flag2.fetchone() if flagcheck != None: print() print("flag 2: id= ",*flagcheck,address) print(number,"NOT-VALID") if gate != True: continue ###### 2> c.setopt(c.URL,address) c.setopt(c.FOLLOWLOCATION, 0) # True: 1 / False : 0 c.perform() print(address,"responce code: ", c.getinfo(c.HTTP_CODE)) if c.getinfo(c.HTTP_CODE) != 200: count1 += 1 print("insert NOT-VALID-DB id: ",count1," -> ",address) cur1.execute("insert into url (id,url) values(?,?) ;",[count1,address]) con1.commit() if second == True: break continue elif c.getinfo(c.HTTP_CODE) == 200: if flagcheck != None: print() print("flag 2-1: id=",*flagcheck,address) cur1.execute("update url set url=? where id=? ;",["null",*flagcheck]) #cur1.commit() print(address,"is VALID / remove from NOT-VALID") flag = None flag = cur.execute("select id from url where url=? ;",[address]) flagcheck = flag.fetchone() if flagcheck != None: print() print("flag 0: id= ",*flagcheck,address) print("already exsist in DB") print() break else: if x == 0 : c.setopt(c.URL,address) c.setopt(c.FOLLOWLOCATION, 0) # True: 1 / False : 0 c.perform() print(address,"responce code: ", c.getinfo(c.HTTP_CODE)) if c.getinfo(c.HTTP_CODE) == 200: count += 1 print("insert id: ",count," -> ",address) cur.execute("insert into url (id,url) values(?,?) ;",[count,address]) con.commit() again = True continue #break elif c.getinfo(c.HTTP_CODE) != 200: count1 += 1 print("insert NOT-VALID-DB id: ",count1," -> ",address) cur1.execute("insert into url (id,url) values(?,?) ;",[count1,address]) con1.commit() return count1,count,address,again second = False count1,count,address,again = dbr(c,address,number,cur1,count1,cur,count,second) print() limit = 5 while again == True: print(" ", str(limit)) number = re.sub(r'\/','',urlparse(address).path) number = int(number) + 1 address = 'https://mangabank.org/'+str(number)+'/' c.setopt(c.URL,address) c.setopt(c.FOLLOWLOCATION, 0) # True: 1 / False : 0 c.perform() print(address,"responce code: ", c.getinfo(c.HTTP_CODE)) if c.getinfo(c.HTTP_CODE) == 200: second = True count1,count,address,again = dbr(c,address,number,cur1,count1,cur,count,second) limit = 5 else: limit -= 1 print(" retry ", str(limit)) ###### <3 flag1 = None flag1 = cur1.execute("select id from url where url=? ;",[address]) flagcheck = flag1.fetchone() if flagcheck != None: print("flag 3: id=",*flagcheck,address) print( number,"NOT-VALID") if limit < 1: again = False continue ###### 3> count1 += 1 print("insert NOT-VALID-DB id: ",count1," -> ",address) cur1.execute("insert into url (id,url) values(?,?) ;",[count1,address]) con1.commit() if limit < 1: again = False c.close() con1.close() con.close()
漫画Bank マンガサイトを調査 実践。
有効な URL と 有効ではない URL を調査し、データベースに記録しよう。
Lua 言語で cURL のライブラリを使って https リクエストして調査してみましょう。
Python 言語の場合はこっちにあります。
curl -is "https://mangabank.org/watch/?tour=/vol" | grep location
これは、上記のターミナルコマンドを実行してリダイレクトされるアドレスを記録していくプログラムです。 ただ、一度噛みつくと深く掘り返すようになっています。
local sqlite3 = require("lsqlite3") local curl = require('cURL') local db = sqlite3.open('lua_mb_urls_new.db') local db1 = sqlite3.open('lua_mb_urls_not.db') db:exec[[ CREATE TABLE url (id INTEGER PRIMARY KEY, url); ]] db1:exec[[ CREATE TABLE url (id INTEGER PRIMARY KEY, url); ]] --[[ lsqlite3 usage ]]-- --for row in db:nrows("SELECT * FROM url") do -- print(row.id, row.url) --end --for row in db:rows("SELECT * FROM url") do -- print(row[1], row[2]) --end --for id,url in db:urows("SELECT * FROM url") do -- print(id, url) --end local id_count = 0 for bigloop=1,1000 do local easy = curl.easy{ url = "https://mangabank.org/watch/?tour=/vol", --followlocation = true, -- true: 1 followlocation = 0, -- false: 0 maxredirs = 2, [curl.OPT_VERBOSE] = 0, } local buffer = {} easy:setopt_writefunction(table.insert, buffer) ----------------------------------------------------------------- --[[ hettp request ]]-- easy:perform() local res_code = easy:getinfo_response_code() local res_location = easy:getinfo_redirect_url() -- 'URL'... redirect to print() --print("code:",res_code) -- always 301 not 200 print("redirect URL:",res_location) local address_code = res_location:match("%d+") -- "[0-9]+" string local address_number = tonumber(address_code) --[[ example: 'https://mangabank.org/2457593/' --]] local smt1 = "SELECT id,url FROM url WHERE url=".."\""..res_location.."\"" --print(smt1) local exist = false ---- VALID DB for id,url in db:urows(smt1) do -- print(id,url) exist = true end local gate = false local remove = false if exist == true then print(res_location.." is aleady exist in VALID-DB") else-- exist == false gate = true ---- INVALID DB for id,url in db1:urows(smt1) do remove = true print(res_location.." is VALID.remove from INVALID-DB") local null_smt1 = "UPDATE url SET url='null' WHERE id="..id..";" assert(db1:exec(null_smt1)) end local id_count = 0 for id in db:urows("SELECT id FROM url ORDER BY id DESC LIMIT 1;") do id_count = id end id_count = id_count + 1 print(res_location.." record in VALID-DB id:"..tostring(id_count)) local stmt2 = db:prepare[[ INSERT INTO url VALUES (:id, :url) ]] stmt2:bind_names{ id = id_count, url = res_location } stmt2:step() stmt2:reset() stmt2:finalize() end ----------------------------------------------------------------- if gate == true then address_number = address_number - 11 local record = false for i=1,20 do print("VALID DB DATA size :"..id_count) address_number = address_number + 1 local next_url = "https://mangabank.org/"..tostring(address_number).."/" print() print("test:",next_url) smt1 = "SELECT id,url FROM url WHERE url=".."\""..next_url.."\"" --print(smt1) local exist = false ---- VALID DB == db for id,url in db:urows(smt1) do -- print(id,url) exist = true end if exist == true then print(next_url.." is aleady exist in VALID-DB") else-- exist == false -- INVALID DB == db1 for id,url in db1:urows(smt1) do exist = true if remove ~= true then print(next_url.." is aleady exist in INVALID-DB") goto continue -- go to next loop end end -- http request if nexe_url == res_location then res_code = 200 goto recording -- skip http request end easy:setopt(curl.OPT_URL,next_url) easy:perform() res_code = easy:getinfo_response_code() ::recording:: if res_code == 200 then record = true --local id_count = 0 for id in db:urows("SELECT id FROM url ORDER BY id DESC LIMIT 1;") do id_count = id end id_count = id_count + 1 print(res_code,next_url.." record in VALID-DB id:"..tostring(id_count)) print() local stmt2 = db:prepare[[ INSERT INTO url VALUES (:id, :url) ]] stmt2:bind_names{ id = id_count, url = next_url } stmt2:step() stmt2:reset() stmt2:finalize() else print(res_code,next_url.." is INVALID") print() record = false ---- INVALID-DB if remove == true then for id,url in db1:urows(smt1) do print(next_url.." is aleady exist in INVALID-DB") goto continue -- go to next loop end end local id_count1 = 0 for id in db1:urows("SELECT id FROM url ORDER BY id DESC LIMIT 1;") do id_count1 = id end id_count1 = id_count1 + 1 print(res_code,next_url.." record in INVALID-DB id:"..tostring(id_count1)) print() local stmt2 = db1:prepare[[ INSERT INTO url VALUES (:id, :url) ]] stmt2:bind_names{ id = id_count1, url = next_url } stmt2:step() stmt2:reset() stmt2:finalize() end end ::continue:: end while record == true do for x = 1,6 do address_number = address_number + 1 local next_url = "https://mangabank.org/"..tostring(address_number).."/" print() print("test:",next_url) smt1 = "SELECT id,url FROM url WHERE url=".."\""..next_url.."\"" --print(smt1) local exist = false ---- VALID DB == db for id,url in db:urows(smt1) do -- print(id,url) exist = true end if exist == true then print(next_url.." is aleady exist in VALID-DB") else-- exist == false -- INVALID DB == db1 for id,url in db1:urows(smt1) do exist = true print(next_url.." is aleady exist in INVALID-DB") goto continue2 -- go to next loop end -- http request easy:setopt(curl.OPT_URL,next_url) easy:perform() res_code = easy:getinfo_response_code() if res_code == 200 then record = true --local id_count = 0 for id in db:urows("SELECT id FROM url ORDER BY id DESC LIMIT 1;") do id_count = id end id_count = id_count + 1 print(res_code,next_url.." record in VALID-DB id:"..tostring(id_count)) print() local stmt2 = db:prepare[[ INSERT INTO url VALUES (:id, :url) ]] stmt2:bind_names{ id = id_count, url = next_url } stmt2:step() stmt2:reset() stmt2:finalize() ---- INVALID-DB if remove == true then for id,url in db1:urows(smt1) do print(next_url.." is exist in INVALID-DB but now VAILD.") local null_smt1 = "UPDATE url SET url='null' WHERE id="..id..";" assert(db1:exec(null_smt1)) goto continue2 -- go to next loop end end else print(res_code,next_url.." is INVALID") print() record = false ---- INVALID-DB if remove == true then for id,url in db1:urows(smt1) do print(next_url.." is aleady exist in INVALID-DB") goto continue2 -- go to next loop end end --local id_count1 = 0 for id in db1:urows("SELECT id FROM url ORDER BY id DESC LIMIT 1;") do id_count1 = id end id_count1 = id_count1 + 1 print(res_code,next_url.." record in INVALID-DB id:"..tostring(id_count1)) print() local stmt2 = db1:prepare[[ INSERT INTO url VALUES (:id, :url) ]] stmt2:bind_names{ id = id_count1, url = next_url } stmt2:step() stmt2:reset() stmt2:finalize() end end ::continue2:: end end end easy:close() end --[[big loop end]]-- db1:close() db:close()
Lua 言語で軽いのと、わりと速いのですが数万件の https リクエストを処理するには時間がかかります。
複数のプロセスでリクエストして、あとから結果をマージすると速く多くのデータができます。 例えば、同じようなプログラムを他の言語(でなくてもいいですが)で書いて、同じような別のデータベースに記録するようにして、2つの結果のデータベースの中身を比べながら、どちらにも存在するデータ、どちらかにしかないデータをまた別のデータベースに記録していくと重複なくデータが抽出できます。 Ruby , python , Lua はよくにている言語なので、書き比べてみてはどうでしょうか。 マンガサイトを調査 実践。( python ) - 黒猫クックブック ちょっと毛色の違う Go 言語で書いてみると、ずいぶんと速かったので、http リクエストの実験として色々とやってみるのもいいかもしれません。
これは、釣り( fishing )です。 さびき釣りのようなイメージです。
リクエストのリトライまでの間隔が短ければ、より速いので cURL を使い、さらに、リダイレクト先までフォローせずに、リクエストの戻ってきたヘッダーの中の location を読み取りリダイレクト先の URL を得ます。なるべくリクエストにかかる時間のオーバーヘッドを減らすということで速く釣りあげます。
2つのデータベースをマージするためのもの。Lua ( / Ruby )
local sqlite3 = require("lsqlite3") --[[local db1 = sqlite3.open('lua_mb_urls_not.db') -- Lua local db2 = sqlite3.open('mb_urls_not.db') -- other local newdb = sqlite3.open('mb_url_notadditive.db') -- for working space local newdb1 = sqlite3.open('lua_mb_url_not1.db') -- additive & order by url ]] ----> Ruby > --db1 = SQLite3::Database.open "lua_mb_urls_not.db" # Lua --db2 = SQLite3::Database.open "mb_urls_not.db" # made from python code --newdb = SQLite3::Database.open "mb_url_notadditive.db" # additive work space --newdb1 = SQLite3::Database.open "mb_url_not1.db" # additive & order by url ----< Ruby < local db1 = sqlite3.open('lua_mb_urls_new.db') -- Lua local db2 = sqlite3.open('mb_urls_new.db') -- other local newdb = sqlite3.open('mb_url_additive.db') -- for working space local newdb1 = sqlite3.open('lua_mb_url_new1.db') newdb:exec[[ CREATE TABLE IF NOT EXIST url (id INTEGER PRIMARY KEY, url text); ]] newdb1:exec[[ CREATE TABLE IF NOT EXIST url (id INTEGER PRIMARY KEY, url text); ]] ----> Ruby > --require 'sqlite3' --db1 = SQLite3::Database.open "lua_mb_urls_new.db" # Lua --db2 = SQLite3::Database.open "mb_urls_new.db" # made from python code --newdb = SQLite3::Database.open "mb_url_additive.db" # additive work space --newdb1 = SQLite3::Database.open "mb_url_new1.db" # additive & order by url -- --SQL =<<EOS --create table IF NOT EXISTS url( -- id INTEGER PRIMARY KEY, -- url text -- ); --EOS -- --newdb.execute(SQL) --newdb1.execute(SQL) -- ----< Ruby < local smt1 = "SELECT id FROM url ORDER BY id DESC LIMIT 1 ;" local last1 = 0 local last2 = 0 for id in db1:urows(smt1) do last1 = id end for id in db2:urows(smt1) do last2 = id end print("db1 has "..last1.." URLs") print("db2 has "..last2.." URLs") ----> Ruby > --last1 = db1.execute("select id from url order by id desc limit 1") --last2 = db2.execute("select id from url order by id desc limit 1") -- --k = last1[0].pop # offset --l = last2[0].pop -- --puts "db1 has #{k} URLs" --puts "db2 has #{l} URLs" -- --i = 0 -- --newdb_last = newdb.execute("select id from url order by id desc limit 1") --if !newdb_last.empty? then -- newcount = newdb_last[0].pop # offset --else -- newcount = 0 --end ----< Ruby < local newcount = 0 for last_id in newdb:urows(smt1) do newcount = last_id end local i = 0 for url_data in db2:urows("SELECT url FROM url ;") do --[[ for url_data in db2:urows("SELECT url FROM url ORDER BY url ;") do ]] i = i + 1 print("copy from db2 ") print(i) print("total "..last2) local existflag = 0 for exist in newdb:urows("SELECT id FROM url WHERE url=\""..url_data.."\" ;") do existflag = exist print(url_data.." is aleady exist in DB / skip") end if existflag == 0 then -- record newcount = newcount + 1 print() print("newdb : "..newcount.." "..url_data) local stmt2 = newdb:prepare[[ INSERT INTO url VALUES (:id, :url) ]] stmt2:bind_names{ id = newcount, url = url_data } stmt2:step() stmt2:reset() stmt2:finalize() end end db2:close() i = 0 -- reset for url_data in db1:urows("SELECT url FROM url ;") do --[[ for url_data in db1:urows("SELECT url FROM url ORDER BY url ;") do ]] i = i + 1 print("copy from db1 ") print(i) print("total "..last1) local existflag = 0 for exist in newdb:urows("SELECT id FROM url WHERE url=\""..url_data.."\" ;") do existflag = exist print(url_data.." is aleady exist in DB / skip") end if existflag == 0 then -- record newcount = newcount + 1 print() print("newdb : "..newcount.." "..url_data) local stmt2 = newdb:prepare[[ INSERT INTO url VALUES (:id, :url) ]] stmt2:bind_names{ id = newcount, url = url_data } stmt2:step() stmt2:reset() stmt2:finalize() end end db1:close() ----> Ruby > --l.times do |index| -- i += 1 -- res = db2.execute("select url from url where id='#{i}' ;") -- if res.empty? then -- puts "no data / skip" -- next -- end -- x = res[0][0] # url -- puts "copy from db2" -- p i,l -- -- res2 = newdb.execute("select url from url where url='#{x}' ;") -- if !res2.empty? then -- p x -- puts 'already exist in db / skip' -- next -- end -- puts -- newcount += 1 -- url = x -- #url.gsub!(/\'/,"\'\'") -- puts "#{newcount} #{url}" -- newdb.execute("insert into url (id, url) values( '#{newcount}','#{url}') ;") --end -- --db2.close -- --i = 0 --k.times do |index| -- i += 1 -- res = db1.execute("select url from url where id='#{i}' ;") -- if res.empty? then -- puts "no data / skip" -- next -- end -- x = res[0][0] # url -- f = newdb.execute("select url from url where url='#{x}' ;") -- puts "copy from db1" -- p i,k -- -- if !f.empty? then -- p res[0] -- puts 'already exist in db / skip' -- next -- else -- newcount += 1 -- url = x -- #url.gsub!(/\'/,"\'\'") -- puts "#{newcount} #{url}" -- newdb.execute("insert into url (id, url ) values( '#{newcount}','#{url}') ;") -- end --end -- --db1.close ----< Ruby < print("new DB last id : "..newcount) ----> Ruby > --puts "new DB last id:#{newcount}" ----< Ruby < local newdb1_last = 0 for last_id in newdb1:urows(smt1) do newdb1_last = last_id end local counter = newdb1_last -- first time ==> 0 ----> Ruby > --newdb1_last = newdb1.execute("select id from url order by id desc limit 1") --if !newdb1_last.empty? then -- counter = newdb1_last[0].pop # offset --else -- counter = 0 --end ----< Ruby < i = 0 -- reset ----> Ruby > --i = 0 ----< Ruby < for url_data in newdb:urows("SELECT url FROM url ORDER BY url ;") do i = i + 1 print("copy from newdb ") print(i) print("total "..newcount) local existflag = 0 for exist in newdb1:urows("SELECT id FROM url WHERE url=\""..url_data.."\" ;") do existflag = exist print(url_data.." is aleady exist in DB / skip") end if existflag == 0 then -- record counter = counter + 1 print() print("newdb1 : "..counter.." "..url_data) local stmt2 = newdb1:prepare[[ INSERT INTO url VALUES (:id, :url) ]] stmt2:bind_names{ id = counter, url = url_data } stmt2:step() stmt2:reset() stmt2:finalize() end end --[[ newdb1:close() newdb:close() ]] -- end -- ----> Ruby > --newdb.execute("select url from url order by url ;") do |row| -- i += 1 -- url = row[0] -- #url.gsub!(/\'/,"\'\'") -- f = newdb1.execute("select url from url where url='#{url}' ;") -- puts "copy from newdb" -- p i,newcount -- -- if !f.empty? then -- p url -- puts 'already exist in db / skip' -- next -- else -- counter = counter + 1 -- puts "#{counter} #{url}" -- newdb1.execute("insert into url (id, url ) values( '#{counter}','#{url}') ;") -- end --end -- --newdb1.close --newdb.close ----< Ruby <
漫画サイトのたのしみ方
つかれたる牛のよだれはたらたらと千万年も尽きざるごとし
序
漫画サイト。 それは漫画がブラウザで読める web サイトのことである。
漫画サイトには大きく分けて二種類ある。一方が違法サイトで、もう一方が合法サイトである。 仕組みは表面上はどちらもよく似ている。内部的には違うが、目的が違うので手段が違うと言っていいだろうが、大まかには同じようなものだ。
ここでは、違法サイトについて調べてみたい。
まず、あるひとつの違法サイトでのコンテンツの総数を数えてみたい。 現段階で、数え上げたのは 51717 のタイトル数だった。これは全てがコミックの 1 巻分ではなくて、第何話や、週刊誌の第何号というものを含めての数で、プラスこの数の 20% 分くらいがまだ隠れ層になっていて表出していない。そしてコンテンツは日々、運営者によって更新されるので、時間とともに数は増えていく。
隠れ層 . . . これはどう云うことかというと、今回とった手法が関係しているが、例えば 100 回ランダムに違法サイトのページアドレスを出して、データベースに記録するということを繰り返して、次回はデータベースに記録されているもの以外をデータベースに記録していくようにしたとして、100 回のうち 20 回ほどが、記録されてないアドレスになってきた場合、続けていくと100 回のうち 15 、10 、5 とだんだんと記録されていないアドレスがランダムでは出にくくなっていくことになる。 このことをここで、隠れ層と云った。隠れ層とは云わないのかもしれない、全部ではないということ。隠れ層を見つけ出すという目的ではなく1、ある時点の表出しているコンテンツのメタ情報について考察していく。
前提
あくまでも、違法サイトのコンテンツページのアドレスを前もって知っているわけではなく、ページにあるマンガ画像が表示されない方法で、ダウンロードせずにコンテンツが埋め込まれている URL を収集して、漫画のタイトルや著者情報を分析していくというパズルであり、漫画読みたいということではない。 スタンスとしては漫画家個人に寄りそう必要なデータを明瞭にオープンに出したい。このスタンスにおいては漫画家に不利益になることには言及しない。出版社、および漫画家の権利保守のための作業軽減に役立つことも提供できる可能性はあり。
漫画読みたい気持ちもある場合は、まずは合法マンガサイトを掘り進んでコードと構造に潜り込んでみるといい。
どの漫画家が作品を何点公開しているのかなど並べて見るプログラムを考えて書いてみるなどすると、いろいろサイトの事情も見えてくる。
code ⌨
require 'open-uri' require 'nokogiri' require 'time' address = 'https://www.mangaz.com/authors/list' html = URI.open(address) doc = Nokogiri::HTML(html) mangaka ={} count = 1 (16..794).each do |num| a_tag = doc.xpath('//a')[num] if a_tag.inner_text == 'ページの上部に戻る' next end count += 1 author_profile_page = 'https://mangaz.com' + a_tag[:href].to_s author_name = a_tag.inner_text mangaka.store(author_name,author_profile_page) end total_titles = 0 manga = [] count = 0 mangaka.each do |key,value| html = URI.open(value) doc = Nokogiri::HTML(html) h4 = doc.css('h4') total_titles += h4.size a = doc.xpath('//h4/a') a.each do |atag| item_name = atag.inner_text item_url = atag['href'] manga[count] = {'title' => item_name, 'book_profile_page' => item_url, 'author' => key, 'author_profile_page' => value } count += 1 end sleep 1 end temp = '' r18 ='' count = 0 manga.each_with_index do |data,index| if temp == data['author'] then count += 1 else puts count = 1 end if data['book_profile_page'].match("\/r18.") r18 = ' -R18- ' else r18 ='' end print index + 1, " #{r18} ",data['author']," #{count} " ,data['title'] puts temp = data['author'] sleep 0.2 end--- author, title, tag
temp = '' r18 ='' count = 0 manga.each_with_index do |data,index| if temp == data['author'] then count += 1 else puts count = 1 end if data['book_profile_page'].match("\/r18.") r18 = ' -R18- ' else r18 ='' end print index + 1, " #{r18} ",data['author']," #{count} " ,data['title'] puts value = data['book_profile_page'] html = URI.open(value) doc = Nokogiri::HTML(html) atag = doc.css('.tag > a') puts atag.each do |tag| print ' -tag- ', tag.inner_text puts end puts temp = data['author'] sleep 0.2 end--- csv
require 'csv' require 'httparty' #require 'open-uri' require 'nokogiri' require 'time' manga.each do |hash| CSV.open('mangaz-list.csv','a+') do |data| if hash['book_profile_page'].match("\/r18.") r18 = '-R18-' else r18 ='nonrestrictive' end value = hash['book_profile_page'] #html = URI.open(value) html = HTTParty.get(value) doc = Nokogiri::HTML(html.body) doc.remove_namespaces! atag = doc.css('.tag > a') #doc = nil tags = '' atag.each do |tag| tags += tag.inner_text + ';' end data << [hash['author'].to_s,hash['author_profile_page'].to_s, hash['title'].to_s,hash['book_profile_page'].to_s,r18,tags] puts hash['title'] end doc = nil end--- sqlite3
require 'sqlite3' require 'csv' SQL=<<EOS create table tbl_manga_z ( id integer, author text, author_profile_page text, title text, book_profile_page text, r18 text, tags text ); EOS db = SQLite3::Database.new("mangaz_list.db") db.execute(SQL) count = 0 CSV.foreach('mangaz-list.csv') do |line| count += 1 author = line[0] author.gsub!(/\'/,"\'\'") author_profile_page = line[1] author_profile_page.gsub!(/\'/,"\'\'") title = line[2] title.gsub!(/\'/,"\'\'") book_profile_page = line[3] book_profile_page.gsub!(/\'/,"\'\'") r18 = line[4] #r18.gsub!(/\'/,"\'\'") tags = line[5] tags.gsub!(/\'/,"\'\'") id = count db.execute("insert into tbl_manga_z (id,author,author_profile_page,title,book_profile_page,r18,tags) values('#{id}', '#{author}','#{author_profile_page}','#{title}','#{book_profile_page}','#{r18}','#{tags}')") end db.close
マンガサイト観測 1
違法と思われる漫画サイトの中でシンプルな構造のオンラインリーディング型のサイト . . . マンガ Thank をターゲットに観測す。(注)マンガ Thank というサイトは存在せず、こころの中に存在すべし。
違法と思われるマンガ Thank(仮称)の HTML の構造をよく確認する
違法と思われるコンテンツである漫画のスキャンデータ、もしくは電子書籍マンガからのコピーである画像ファイルは、概ね cloudflare 社2のキャッシュのイメージファイルが <img>
で直接指定されている。
また、lazy loading3 で読み込まれている。
仕組みとしてはページに別のサーバーにある WordPress で管理された画像群の画像ファイル(ここが cloudflare のキャッシュになっているということで、それは当該のファイルの URL を逆引きしてドメインからドメイン所有者をわりだすと cloudflare になっているということ。いずれ図説したい。)をページ内に lazy loading で読み込んで表示させていることが確認できる。
Cloudflare, Inc.(クラウドフレア)とは https://ja.wikipedia.org/wiki/Cloudflare
シンプルなので理解しやすいはずである。 つまり、オンラインリーディング型のサイト、マンガ Thank(仮称)のメインのコンテンツは画像だけなので、その画像がページ内にたとえば 100 点収まっているだけというシンプルな構造で、付加的にアップデート日時がタグで囲まれて配置されている。これはマンガ Thank(仮称)のシステム管理者用にあるのだと考えられる。 アップデート日時の情報は、マンガ Thank(仮称)のマンガコンテンツを表示するページのソースコード表示で確認できるが、ブラウザでレンダリングされた表示では見えないようにはなっている。
ひとつめのデータベーステーブル [tbl_manga]
このマンガ Thank(仮称)ページの構造から考えて、以下のようなデータベースを作ることにする。
[tbl_manga]
id INTEGER PRIMARY KEY, title text, url text, updated_datetime datetime, author text, book_title text
カラム title
の値は、ページにあるタイトルを抽出したもの。
カラム url
の値は、コンテンツの埋め込まれたページの URL で、ページに埋め込まれた画像ファイルの URL ではない。画像の URL はこのステップでは収集しない。
画像ファイルがすべて cloudflare 管轄のドメインのものを使用されているのか確認したい段階で URL を収集することになるのかもしれないが、それ自体は非常に簡単にできる。ここでは割愛。
仕組み上、この url
の値はユニークである。つまり重複はない。title
が重複することがあっても url
は重複しない。なぜかは考えればすぐにわかるはずだ。 updated_datetime
は、ページソースコードにあるアップデートした日時を収める。ただし、これが正確なのかは不明なので、後にプロファイルするための参考にするために抽出する。 その他は、ターゲットのページから抽出するのではなく、抽出されたデータをもとに生成する。
Scraping data from the site 🍄
urls = generated require 'sqlite3' require 'csv' require 'time' require 'date' SQL =<<EOS create table tbl_manga ( id INTEGER PRIMARY KEY, title text, url text, updated_datetime datetime, author text, book_title text ); EOS db = SQLite3::Database.open "mangathank_new.db" db.execute(SQL) db.close 100.times do |loop| db = SQLite3::Database.open "mangathank_new.db" 4.times do |loop| driver.get(urls) #sleep 1 flag = [] URL = driver.current_url flag = db.execute("select id from tbl_manga where url='#{URL}'") #p flag if flag.any? then next else CSV.open('mangathank_TEMP.csv','a+') do |data| title = driver.find_element(:class_name, 'entry-title') updated = driver.find_element(:class_name,'updated') updated_datetime = updated.attribute('datetime') title.text.to_s.rstrip! pp title.text data << [URL,title.text,updated_datetime] end end end if File.exist?("mangathank_TEMP.csv") then CSV.foreach('mangathank_TEMP.csv') do |line| count += 1 url = line[0] url.to_s.gsub!(/\'/,"\'\'") title = line[1] author_name = title.slice(/(?<=\[).*?(?=\])/) updated_datetime = line[2] updated_datetime.to_s.gsub!(/\'/,"\'\'") id = count title.to_s.gsub!(/\'/,"\'\'") #puts title #puts author_name db.execute("insert into tbl_manga (id, title, url, updated_datetime ) values('#{id}', '#{title}','#{url}','#{updated_datetime}')") if author_name != nil then # author_name.gsub!(/\'/,"\'\'") pp author_name end book_title = title.slice(/((?<=\]).+?$)/) if book_title then book_title.gsub!(/((?=第).*(巻|卷))/,'') book_title.gsub!(/((?=第).*話)/,'') book_title.gsub!(/(.(?<=\()文庫版(?=\)).)/,'') book_title.gsub!(/(.(?<=\[)文庫版(?=\]).)/,'') book_title.gsub!(/文庫版/,'') book_title.gsub!(/(.(?<=\()完(?=\)).)/,'') book_title.gsub!(/(.(?<=【).*(?=】).)/,'') book_title.gsub!(/(.(?<=\[).+?(?=\]).)/,'') book_title.gsub!(/\'/,"\'\'") book_title.lstrip! book_title.rstrip! else book_title = title end pp book_title db.execute("update tbl_manga set author = '#{author_name.to_s}', book_title = '#{book_title.to_s}' where id = '#{id}' ;") end db.close system("rm -r mangathank_TEMP.csv") else next end db.close end
目標 ゴール設定は、オンラインリーディング型のサイトであるマンガ Thank(仮称)にコンテンツとして使用されている漫画について、すべて権利者を割り出して、並べて見ること。
たとえば、コミックスであれば、たいていのもは出版社が発行しているので、その出版社名、書籍データを、違法にアップロードされたものからできるだけ正確に逆引きして表示するまでがひとまずの目標とする。曖昧なメタデータから、正確なメタデータへ変換するということ。
書籍データは ISBN4 をともなったものが多いが、全てではないのと、電子書籍は書籍という枠には入れられていないために ISBN での管理を外れていても例外とは云えない。 おおかたの書籍検索システムは ISBN コードを主軸としているので、ISBN を使って書籍データを問い合わせる仕組みになっているが、これでは要件に適していないので、本のタイトルから書籍データを問い合わせることが可能なシステムの API を使い、これを実現する。
マンガサイト観測 2
マンガサイトにある 51717 タイトルの出版社情報を並べてみる。 マンガ 51717 タイトル list pdf
違法と思われるオンラインリーディング型サイトのマンガ Thank (仮称) から現時点で抽出された 51717 のタイトル情報を国会図書館 NDL (National Diet Library) サーチに問い合わせて書籍データを得る。
Rf. API仕様の概要 « 国立国会図書館サーチについて https://iss.ndl.go.jp/information/api/riyou/
マンガサイト観測 1 で解説したように ISBN ではなく、サイト内で独自に割り振られたタイトルと著者名らしき文字列の情報から、タイトル名と著者名を抜き出して、NDL ( National Diet Library ) サーチの API を使いデータ照会できるように、仕様に合わせたクエリをつくり HTTPS でリクエストする。
つまり、 ISBN はわからないので、タイトルから ISBN をわりだすということが可能な API を使うということになる。 このような場合、国会図書館サーチか amazon の API かの選択になるが、今回は制限のほぼなさそうな国会図書館サーチを選ぶ。国立国会図書館サーチの使用法は仕様公開ページをよく読んでもどこか説明が足りないので、実際に使えるサンプルを探して試行錯誤する方がよいと思う。
マンガ Thank (仮称) のコンテンツのタイトルは独自につけられている為、というより何者かによってコンテンツ(スキャンされたマンガの画像のこと)がアップロードされた時点でメタ情報が入力されているので、そのメタ情報入力の際に明らかにタイトルの英単語のつづりを間違っているなどの場合がある。
これを間違ったそのままでクエリにして、 NDL サーチから正しい情報が引き出せないケースがあるが、現時点では、それは修正せずに間違っていようがタイトルからタイトルらしきものを文字列抽出し、著書名らしきものを文字列抽出してクエリに組み込むプログラムを作った。つまり照会結果が無い場合、なんらかのメタデータにパターンや平均的でない特徴が見られることが期待できる。
NDL search
この照会結果を新たなデータベースに書き込んでいくが、データベースのテーブルは以下のようになっている。
なぜデータベースを使うのか? コンテンツの数量が多いからである。 50000 を超えて、さらにあと 10% 前後はまだデータ未取得で、さらには日々増えているので、一気に全データを取得 . . . とは考えずに継続的に改良を加えながらデータをとっていく。
ふたつめのデータベーステーブル [tbl_ bookdata]
[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
マンガサイト観察 1 で用意した [tbl_manga] とは別に分けている。これは、 [tbl_manga] から読みだしたデータを使って、 NDL サーチにクエリをリクエストして得た情報を [tbl_bookdata] に書き込むということになる。
このデータベースのテーブル [tbl_bookdata] に書き込まれたものから、 id
, seriestitle
, publisher
, url
を抽出したものはこうなる。
id
, seriestitle
, publisher
, url
"1","null","null","null" "2","null","null","null" "3","null","null","null" "4","null","null","null" "5","null","Sony Music Labels","https://iss.ndl.go.jp/books/R100000002-I027014370-00" "49738","null","Sony Music Labels","https://iss.ndl.go.jp/books/R100000002-I027014370-00" "6","null","Sony Music Labels","https://iss.ndl.go.jp/books/R100000002-I027014370-00" "7","null","null","null" "8","null","null","null" "9","null","null","null" "10","null","null","null" "11","null","null","null" "12","null","アスキー・メディアワークス,KADOKAWA","https://iss.ndl.go.jp/books/R100000002-I024687562-00" "13","null","アスキー・メディアワークス,KADOKAWA","https://iss.ndl.go.jp/books/R100000002-I024687572-00" "14","null","null","null" "15","null","null","null" "16","null","null","null" "17","null","null","null" "18","null","null","null" "19","null","null","null" "20","null","null","null" "21","null","null","null" "22","null","平凡社","https://iss.ndl.go.jp/books/R100000002-I027189887-00" "23","null","平凡社","https://iss.ndl.go.jp/books/R100000002-I028029779-00" "24","null","平凡社","https://iss.ndl.go.jp/books/R100000002-I000011141069-00" "25","null","平凡社","https://iss.ndl.go.jp/books/R100000002-I023371158-00" "26","null","平凡社","https://iss.ndl.go.jp/books/R100000002-I024193406-00" "27","null","平凡社","https://iss.ndl.go.jp/books/R100000002-I025336987-00" "28","null","null","null" "29","角川コミックス・エース ; KCA500-1","KADOKAWA","https://iss.ndl.go.jp/books/R100000002-I026685661-00" "30","角川コミックス・エース ; KCA500-2","KADOKAWA","https://iss.ndl.go.jp/books/R100000002-I027116764-00" "31","YOUNG ANIMAL COMICS","白泉社","https://iss.ndl.go.jp/books/R100000002-I030414985-00" "32","YOUNG ANIMAL COMICS","白泉社","https://iss.ndl.go.jp/books/R100000002-I030704315-00" "33","YOUNG ANIMAL COMICS","白泉社","https://iss.ndl.go.jp/books/R100000002-I031233553-00" . . . . . .
コンテンツのタイトルのみで著者の情報がミッシングしている場合は、国会図書館 NDL サーチでは必ずしも正しい照会結果になるとは限らない。 例としては、上の囲みで"Sony Music Labels"となっている 3 行は、明らかに正しくない結果だが、照会結果がゼロではなく、見当違いのものにクエリがマッチしたということになる。
上の囲みので id
, seriestitle
, publisher
, url
という並びで 1 行になっている。 id
はカウントアップされいく整数で、このデータベースでは 51717 行あるので 1~51717 まである。データベーステーブル [tbl_bookdata] で ex_id
という整数のコラムを用意したが、ここへは [tbl_manga] の id
の値が入る。ex_id
も 1~51717 まである。ということは、[tbl_bookdata] と [tbl_manga] を内部結合に使うこともできる。
seriestitle
, publisher
については NDL サーチの結果のデータとして用意されているものだ。
seriestitle
が連載誌名で publisher
がその出版社名に当てはまる。これらはない場合もあるので、その場合は値は空になる。 [tbl_bookdata] においての url
は、国会図書館サーチの結果の web ページの URL が値として入る。
[tbl_manga] においての url
とは異なっていて、[tbl_manga] の url
はマンガ Thank (仮称) のそれぞれのコンテンツの URL が収まっているので関連はあるが別のものを指している。 [tbl_bookdata] の url
はマンガ Thank (仮称) のコンテンツはこの書誌であるという補足になっている関係になる。
また、国会図書館 NDL サーチでは、ことば(キーワード)の揺れにたいして特に寛容というわけでもない(が、アクセスの回数や頻度については明確な制限が提示されていないけれども、たいへん寛容である5)ので、ことばがマッチせずに探し出せないケースが多々ある。 独自に付けられた識別のことばが含まれたまま NDL サーチにクエリが送られた場合、マッチせずに結果が無い状態になり null
で置き換えられる。
なるべくキーワードが NDL のデータベース上のものと一致するように、予め NDL 内での書籍データを確認して(何度かテストして失敗したもののクエリ内容と、手動で検索して発見される書籍データをよく見比べて)、違法漫画サイトで付与されているコンテンツタイトルを正規表現で NDL 内でのデータの収まり方に寄せて照会のリクエストのクエリを組むようにする。
例) コンテンツのタイトルのパターン
コンテンツのタイトルから正規表現を使って、必要のない文字列を除去し、タイトルと著者に分け、 NDL サーチにリクエストするクエリに組み込まれる。 "のらくろ 漫画集 1" "田河水泡"
のらくろ漫画集 (講談社): 1975|書誌詳細|国立国会図書館サーチ
例) コンテンツのタイトルのパターン
[長屋憲 × 佐藤秀峰] ブラックジャックによろしく 第01巻
コンテンツのタイトルから正規表現を使って、著者の候補を分けて、NDL サーチにリクエストするクエリをつくる。 "ブラックジャックによろしく 1" "佐藤秀峰" "長屋憲"
ブラックジャックによろしく 1 佐藤秀峰 - 国立国会図書館サーチ
ブラックジャックによろしく 1 長屋憲 - 国立国会図書館サーチ
詳細はこちら [NDL search (Ruby)]
出版社データ
以上のことを踏まえて、必ずしも正確ではない、コンテンツにたいして著作権を保持している可能性のある出版社を列挙す。
select distinct(publisher) from tbl_bookdata group by mangathank_title ;
出版社データ 🍞
"null" "Sony Music Labels" "アスキー・メディアワークス,KADOKAWA" "平凡社" "KADOKAWA" "白泉社" "徳間書店" "Cygames,講談社" "集英社" "マッグガーデン" "角川書店,角川グループパブリッシング" "講談社" "少年画報社" "角川書店" "ヒーローズ,小学館クリエイティブ" "アスキー・メディアワークス,角川グループパブリッシング" "光文社" "メディアワークス,角川書店" "スクウェア・エニックス" "角川書店(発売),バンダイビジュアル (販売)" "EGMONT MANGA & ANIME" "エイベックス・エンタテインメント,エイベックス・マーケティング" "新書館" "バンダイビジュアル" "オーバーラップ" "秋田書店" "一迅社" "ホビージャパン" "キルタイムコミュニケーション" "マーベラスAQL,ポニーキャニオン" "一迅社,講談社" "アース・スターエンターテイメント,泰文堂" "アース・スターエンターテイメント" "Kadokawa" "KADOKAWA" "角川書店,KADOKAWA" "ハーレクイン" "日本文芸社" "星海社,講談社" "リイド社" "集英社クリエイティブ,集英社" "芳文社" "双葉社" "小学館" "バンダイナムコアーツ" "幻冬舎コミックス,幻冬舎(発売)" "TBS,ポニーキャニオン" "秋水社,大都社" "ジェネオン・ユニバーサル・エンターテイメント" "角川書店,角川グループホールディングス" "ブシロード,KADOKAWA" "富士見書房,角川グループパブリッシング" "久保書店" "マイクロマガジン社" "朝日ソノラマ" "朝日新聞社,朝日新聞出版" "朝日新聞出版" "創美社,集英社" "幻冬舎コミックス,幻冬舎" "アスキー・メディアワークス,Kadokawa" "TYPE-MOON,Kadokawa" "TYPE-MOON,KADOKAWA" "アイプロダクション,祥伝社" "ぶんか社" "TOブックス" "メディアファクトリー" "SBクリエイティブ" "角川グループパブリッシング" "アニプレックス" "PHP研究所" "イースト・プレス" "竹書房" "一二三書房" "コミックス,講談社 (共同刊行・発売)" "宙出版" "アスキー・メディアワークス,角川グループパブリッシング(発売)" "アスキー・メディアワークス,角川グループパブリッシング (発売)" "新潮社" "アルファポリス,星雲社" "エイベックス・ピクチャーズ" "小学館,ジェネオン・ユニバーサル・エンターテイメント" "Tonkam" "ジーオーティー" "Jパブリッシング" "ラポート" "中央公論社" "祥伝社" "ワニブックス" "アスキー・メディアワークス,角川グループホールディングス" "キングレコード" "ノース・スターズ・ピクチャーズ,徳間書店" "リブレ" "スーパー・ビジョン,ポリドール映像販売" "ノース・スターズ・ピクチャーズ,竹書房" "マガジンハウス" "フジテレビ映像企画部,ポニーキャニオン" "ジェネオンエンタテインメント" "主婦の友社" "NBCユニバーサル・エンターテイメント" "サード・ライン・ネクスト,星雲社 (発売)" "ぺんぎん書房" "宝島社" "マーベラスエンターテイメント,ポニーキャニオン" "みなみ出版,星雲社" "ホーム社" "青磁ビブロス" "ジャイブ" "学習研究社" "コロムビアミュージックエンタテインメント" "ビブロス" "ハーパーコリンズ・ジャパン" "アルファポリス,星雲社 (発売)" "SG企画" "ワーナー・ブラザース・ホームエンターテイメント" "ハピネット" "主婦と生活社" "ホーム社,集英社(発売)" "ホーム社,集英社" "学研プラス" "講談社,コミックス" "虫プロ商事" "TBS,日本コロムビア" "フォーラムエイトパブリッシング,フォーラムエイト (発売)" "文禄堂" "愛媛県教育会" "労働教育センター" "NHN comico,双葉社" "スターツ出版" "エンターブレイン,角川グループパブリッシング" "ラジオ大阪" "ポニーキャニオン" "GRINP" "Kodansha,ポニーキャニオン" "実業之日本社" "南海出版公司" "秋田書店,白泉社" "白泉社,集英社 (発売)" "あおば出版" "中央公論新社" "フロンティアワークス" "小池書院" "大都社" "小学館,メディアファクトリー" "東映ビデオ" "太田出版" "東宝" "フロンティアワークス,KADOKAWA" "ジュリアンパブリッシング" "星海社,講談社 (発売)" "ブライト出版" "オークラ出版" "誠文堂新光社" "角川書店,角川グループパブリッシング (発売)" "富士見書房,角川グループホールディングス" "フレックスコミックス,ソフトバンククリエイティブ" "SBクリエイティブ" "バップ" "G-NOVELS,誠文堂新光社" "NHN Comico,双葉社" "LINE,日販アイ・ピー・エス" "LINE Digital Frontier,日販アイ・ピー・エス" "サンリオ" "" "ネクストF,ジャイブ" "三交社" "自称清純派" "フレックスコミックス" "ポッポ焼き屋" "彗星社,星雲社" "HSU出版会,幸福の科学出版" "松竹" "フジテレビ,東宝" "エンターブレイン,角川グループホールディングス" "リブレ出版" "フレックスコミックス,ほるぷ出版" "飛鳥新社" "LDH pictures,バップ" "Avex Pictures" "ソフトバンククリエイティブ" "「インベスターZ」製作委員会,バップ" "大和書房" "湖南美术出版社" "冬水社" "エンターブレイン,KADOKAWA" "インデックス・コミュニケーションズ" "モール・オブ・ティーヴィー" "幻冬舎" "テレビ東京,ポニーキャニオン" "新紀元社" "コアマガジン" "サンタスティック・エンタテイメント" "NBCユニバーサル・エンターテイメント,エイベックス・ピクチャーズ" "Viz Media" "フジテレビジョン,ポニーキャニオン" "ワニマガジン社" "朝日新聞社" "オレンジページ" "文藝春秋" "コミックス,講談社" "富士見書房,KADOKAWA" "M'sワールド,GPミュージアムソフト" "山と溪谷社" "「嬢王3~Special Edition~」製作委員会,東宝" "エンターブレイン" "林檎プロモーション" "[八木戸マト]" "「Claymore」製作委員会,エイベックス・マーケティング" "トゥーマックス,エイベックス" "トゥーマックス,エイベックス・ディストリビューション" "トゥーマックス,avex distribution" "エンターブレイン,角川グループパブリッシング (発売)" "ソニー・マガジンズ" "ロングランドジェイ,ジーウォーク" "ハーレクイン・エンタープライズ日本支社" "早川書房" "スタジオDNA" "エニックス" "KADOKAWAメディアファクトリー" "エイベックス・マーケティング" "河出書房新社" "ワンツーマガジン社" "コアミックス" "小学館クリエイティブ,小学館" "メディアワークス,主婦の友社" "メディアワークス" "松竹映像商品部" "東映ビデオ,東映" "ベストフィールド" "日本評論社" "Ariola Japan" "リンダパブリッシャーズ,徳間書店" "ひばり書房" "ワーナー・ブラザースホームエンターテイメント" "OKAWA-Verlag" "OKAWA-VERLAGS GMBH" "สยามอินเตอร์คอมิกส์" "東芝エンタテインメント,ジェネオンエンタテインメント" "偕成社" "マーベラスエンターテインメント,松竹ビデオ事業室" "Nozomi entertainment : Right Stuf" "云南人民出版社" "민음사" "Gantz Partners,松竹ビデオ事業室" "日本放送出版協会" "スタジオ・シップ" "近代映画社" "小学館,コロムビアミュージックエンタテインメント" "宝塚クリエイティブアーツ" "AKS" "台灣東販" "ヒーローズ,小学館クリエイティブ (発売)" "創美社" "スコラ" "テレビ朝日,ポニーキャニオン" "マーベラスエンターテイメント,メディアファクトリー" "ネクストF,ジャイブ (発売)" "ギャガ" "フリュー,エイベックス・ピクチャーズ" "ABCライツビジネス,ポニーキャニオン" "[集英社]" "Bbmfマガジン" "青泉社" "潮出版社" "白泉社,集英社" "筑摩書房" "フジテレビジョン" "幻冬舎コミックス,幻冬舎 (発売)" "ビズコミュニケーションズジャパン" "メディアワークス,角川グループパブリッシング" "一賽舎" "フロンティアワークス,NBCユニバーサル・エンターテイメント" "一賽舎,スタジオDNA" "講談社コミッククリエイト,講談社" "ブッキング" "ブシロードメディア,KADOKAWA" "Carlsen" "茜新社" "メディアワークス,角川書店,角川グループパブリッシング" "ラクセント,フロンティアワークス" "小学館クリエイティブ,小学館 (発売)" "[斎創@さいそう。]" "マーベラスエンターテイメント,エイベックス・マーケティング・コミュニケーションズ" "ビクターエンタテインメント" "大陸書房" "モーターマガジン社" "扶桑社" "時鐘舎,北國新聞社" "Gzブレイン,KADOKAWA" "KADOKAWA Game Linkage,KADOKAWA" "KADOKAWA Game Linkage,KADOKAWA (発売)" "JICC出版局" "エイベックス,Avex Distribution" "バードランドミュージックエンタテインメント (発売),アドニス・スクウェア (販売)" "フロンティアワークス,KADOKAWAメディアファクトリー" "富士見書房" "「新米姉妹のふたりごはん」製作委員会,ポニーキャニオン" "「怨み屋本舗reboot」製作委員会,東宝" "デジタルサイト,ジェネオンエンタテインメント" "ハーレクイン,洋販" "東芝エンタテインメント,ポニーキャニオン" "小学館,ジェネオンエンタテインメント" "ユニバーサル・ピクチャーズ・ジャパン,ジェネオンエンタテインメント" "ミューズ・プランニング,エイベックス・マーケティング" "集英社クリエイティブ" "関西テレビ放送,ポニーキャニオン" "ひかりのくに" "東映アニメーション,ジェネオン・エンタテインメント" "ネルケプランニング" "東映アニメーション,東映" "日本コロムビア" "二見書房" "講談社 : 講談社コミッククリエイト" "学習研究社,少年画報社" "Tokyopop" "ベストセラーズ" "岩崎書店" "小学館,エイベックス・マーケティング・コミュニケーションズ" "小学館,エイベックス・マーケティング" "笠倉出版社" "外道高校野球部,東宝" "マーベラスエンターテイメント,キングレコード" "ハーヴェスト出版,星雲社" "プランタン出版,フランス書院" "宙出版,主婦と生活社" "エスピーオー" "白泉社,ジェネオンエンタテインメント" "Glénat" "Planet Manga : Panini Comics" "Pika édition" "小学館,ポニーキャニオン" "フジテレビジョン,よしもとミュージック" "青林堂" "富士見書房,角川書店" "ジェネオン・エンタテインメント" "GDH" "NHKソフトウェア,ジェネオンエンタテインメント" "若木書房" "エンジェル出版" "松文館" "テレビ東京,バップ" "comico,双葉社" "日本映像,フルメディア" "VERTICAL" "宙出版,主婦の友社" "サード・ライン・ネクスト,星雲社" "インテルフィン" "ポプラ社" "秋水社,双葉社" "シンエイ動画,バンダイビジュアル" "楽楽出版" "メディエイション,廣済堂出版" "金の星社" "ランティス,キングレコード" "メイド様!プロジェクト,ジェネオン・ユニバーサル・エンターテイメント" "藤子不二雄ファンサークルネオ・ユートピア" "講談社,バンダイビジュアル" "「新宿セブン」製作委員会,東宝" "大垣書店" "DREAMUSIC PUBLISHING,KING RECORDS" "講談社インターナショナル" "Funimation Entertainmment" "アスキー,アスペクト" "アスキー" "テレビ朝日,ジェネオンエンタテインメント" "テレビ東京 (製作),創通映像 (製作),シンエイ動画 (製作),バンダイビジュアル (発売)" "クロスメディア・パブリッシング,インプレス" "主婦の友インフォス,主婦の友社" "ミリオン出版,大洋図書" "SQUARE ENIX" "姉妹社" "ランティス,バンダイビジュアル" "VIZ Media,LLC" "長春出版社" "ゴマブックス" "三栄書房" "Bbmfマガジン,グリーンアロー出版社" "テレビ東京,エイベックス・ピクチャーズ" "東京漫画社" "シンエイ動画" "コスミック出版" "キングレコード,ポニーキャニオン" "小学館,エイベックス・ディストリビューション" "GDH,ビクターエンタテインメント" "国書刊行会" "NHKエンタープライズ" "小学館クリエイティブ" "フェアベル"
著者データ
マンガ家の方のチェック用に CVS で Author (著者)と Publisher (出版社)のデータのみ確認できるファイル。
author_publisher.csv
https://we.tl/t-HbrbAUUZFB?src=dnlwe.tl
https://we.tl/t-TYe3kJsh6qwe.tl
これはつまり、ここに名前があれば、あなたの著作が、おそらく無断で掲載されているので、あなたは当事者ですからテイクダウンする対策をこうじてくださいね、ということで候。
マンガサイト観測 3
マンガ Thank(仮称)のなかにあるテキストデータで、コンテンツの内容を表現したタイトル( title )をいくつか抽出して、その文字列で google 検索してみると、マンガ Thank(仮称)以外のページがヒットする場合があることに気がつくだろう。
当初考えていたのは、マンガ Thank(仮称)というサイトで、表示されているコンテンツ(つまりマンガのスキャンデータ)は、サイトの運営者がスキャンして、それをアップロードしているものと考えていた。
NDL サーチにヒットしない文字列のケースを観察して、正規表現のパズルを解き続けていると、なぜ、入力するメタデータに一定の命名規則がきちんと適用されないのかという疑問がわいた。その理由はいくつか考えられる。クオリティに明らかなばらつきが見られることから、少なくともメタデータを入力している作業者は複数で、最低限の文字処理の知識、データベースで運用する前提の知識のコモンセンスは徹底されてないことは確かである。
だが、マンガ Thank (仮称)で使っているタイトルの文字列のまま、他のサイトでも使われているということ、また、そのサイト mangaΠ (仮称) というのが、オンラインリーディング型ではなくて、ダウンロード型・・・つまり、マンガの画像データを zip, rar など圧縮してまとめてダウンロードさせる配布サイトで、それもどうやらマンガ Thank(仮称)よりも保有コンテンツ数が多いような雰囲気(印象であって不確かなもの)があるので、これはもしかして、ダウンロードサイトからコンテンツをダウンロードし、そのファイルを解凍し、それをマンガ Thank(仮称)のコンテンツとして使用しているのではないか?という予測に至った。
これはコンテンツのアップロードの日時を比較し、ファイルの内容を比較すれば、流れのつじつまはひとつ確認できるのであろうけれども、未確認である。
この仮説が示すのは、エコサイクルが形成されているということ。 つまり、マンガ Thank(仮称)の運営側には、マンガをスキャンしてアップロードする作業スタッフを抱えていないのかもしれないということだ。
そのステップは、他にあって、オンラインリーディング型のサイトマンガ Thank (仮称)とダウンロード型サイト mangaΠ (仮称) は、双方の運営は、まったくの無関係でいながら結果的に分業している場合もありうる。それは、わからないことだが。
マンガ Thank(仮称)に限ったことで、問題は、このコンテンツである画像ファイルは cloudflare のドメインにあることだ。
cloudflare のドメインにある画像ファイルを直接読み込むようにしているため、cloudflare にコンテンツ配信のキャッシュファイルを個別に配信停止するように求めなければ、じつはこれは、画像ファイルの URL さえ記述すればどのウェブサイトであろうと、コンテンツを公開できてしまうということに候。
仮にマンガ Thank(仮称)がドメインごと消えた場合でも、再度画像をアップロードすることなしに新たに同じようにコンテンツが配信される可能性はあるし、全く新たに似たようなサイトが始動することもありえる。
つづく
-
それもパズルなり。https://kuroca.hatenablog.com/entry/20211009/1633774217↩
-
空中分解…海賊版サイト対策検討会はなぜ迷走したか https://www.yomiuri.co.jp/fukayomi/20181017-OYT8T50059/2/↩
-
A lightweight but powerful delayed content, image and background lazy-loading plugin for jQuery & Zepto http://jquery.eisbehr.de/lazy/example_basic-usage↩
-
日本図書コード管理センター https://isbn.jpo.or.jp/index.php/fixabout/fixabout_3/↩
-
※アクセス数の上限につきましては、サービスへの影響等を含めて総合的に判断されるため、具体的な数値の目安をお示しすることができません。恐れ入りますが、APIをご利用いただく際は、多重アクセスが生じないようご対応をお願いいたします。https://iss.ndl.go.jp/information/api/↩