迫真保存部・webscrapingの裏技!

こちらの記事で紹介したニコニコ静画一括保存ソフト、「Niconico Seiga Hozon」のコード紹介です。

言語はpythonです。

import requests
import bs4
import tkinter
from tkinter import messagebox
from tkinter import filedialog
import os,sys
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from time import sleep
import filetype
options = Options()
base_url = "https://seiga.nicovideo.jp/user/illust/"
headers = {
       "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0",
       }
tbl = str.maketrans('¥/:*?"<>|','_________')
root = tkinter.Tk()
root.title(u"Niconico Seiga Hozon")
root.geometry("450x250")
Static4 = tkinter.Label(text=u"投稿者名")
Static4.place(x=250,y=10)
Static5 = tkinter.Label(text=u"投稿枚数")
Static5.place(x=250,y=65)
Static6 = tkinter.Label(text=u"(未取得)")
Static6.place(x=250,y=85)
Static1 = tkinter.Label(text=u'投稿者の静画ID')
Static1.place(x=5,y=5)
EditBox = tkinter.Entry()
EditBox.place(x=7,y=25)
Static9 = tkinter.Label(text=u"メールアドレスまたは電話番号")
Static9.place(x=5,y=95)
EditBox3 = tkinter.Entry()
EditBox3.place(x=7,y=115) 
Static10 = tkinter.Label(text=u"パスワード")
Static10.place(x=5,y=140)
Editbox4 = tkinter.Entry()
Editbox4.place(x=7,y=160)
EditBox5 = tkinter.Entry(width=30)
EditBox5.place(x=250,y=30)
EditBox5.insert(tkinter.END,"(未取得)")
Static11 = tkinter.Label(text=u"取得位置")
Static11.place(x=250,y=120)
entry2 = StringVar()#始めのページ
EditBox6 = tkinter.Entry(width=2,textvariable=entry2)
EditBox6.place(x=255,y=140)
Static12 = tkinter.Label(text=u"頁から")
Static12.place(x=270,y=140)
entry3 = StringVar()#終わりのページ
EditBox7 = tkinter.Entry(width=2,textvariable=entry3)
EditBox7.place(x=315,y=140)
Static13 = tkinter.Label(text=u"頁まで")
Static13.place(x=330,y=140)

def userinfo_get(event):
   
   artist_int = EditBox.get()
   artist = str(artist_int)
   req = requests.get(base_url + artist,headers=headers)
   data = bs4.BeautifulSoup(req.content,'html.parser')
   user_data = data.find(class_='user_thum')
   side = data.find(class_='refine_list')
   if user_data is None:  
       EditBox5.delete(0, tkinter.END)
       EditBox5.insert(tkinter.END,"(情報を取得できませんでした)")
       Static6["text"] = '(情報を取得出来ませんでした)'
       
   else:
       get_user = user_data.find('img')
       user_img = get_user.attrs['src']
       get_user_img = requests.get(user_img)
       n = data.find(class_="nickname")
       nickname = n.decode_contents(formatter="html") 
       NICK = nickname.translate(tbl)
       p = side.find(class_="count")
       p_num = p.decode_contents(formatter="html")
       p_int = int(p_num)
       last_p = (p_int - 1) // 40 + 1
       EditBox5.delete(0, tkinter.END)
       EditBox5.insert(tkinter.END,NICK)
       EditBox6.delete(0, tkinter.END)
       EditBox6.insert(tkinter.END,1)
       EditBox7.delete(0, tkinter.END)
       EditBox7.insert(tkinter.END,last_p)
       Static6["text"] = p_num + " 枚"

Button1 = tkinter.Button(text=u'更新')
Button1.bind("<Button-1>", userinfo_get)
Button1.place(x=140,y=21)
def delete_box():
   EditBox.delete(0, tkinter.END)
   EditBox5.delete(0, tkinter.END)
Button4 = tkinter.Button(text=u"クリア",command=delete_box)
Button4.place(x=180,y=21)
Static2 = tkinter.Label(text=u'保存先')
Static2.place(x=5,y=50)
entry1 = StringVar()
EditBox2 = tkinter.Entry(textvariable=entry1)
EditBox2.place(x=5,y=70)

def dirdialog_clicked():
   iDir = os.path.abspath(os.path.dirname(__file__))
   iDirPath = filedialog.askdirectory(initialdir = iDir)
   entry1.set(iDirPath)
Button = tkinter.Button(text=u'参照',command=dirdialog_clicked)
Button.place(x=140,y=66)

def img_save():
   
   start_p = EditBox6.get()
   end_p = EditBox7.get()
   if not start_p.isdecimal() or not end_p.isdecimal():
       messagebox.showinfo("エラー", "取得位置は整数で指定してください")
   else:
       start_p_int = int(start_p)
       end_p_int = int(end_p)
       filepath = EditBox2.get()
       verification = os.path.exists(filepath)
   
       if verification is False:
           dirdialog_clicked()
           filepath = EditBox2.get()
           verification = os.path.exists(filepath)
       if verification is True:
           artist = str(EditBox.get())
           req = requests.get(base_url + artist,headers=headers)
           data = bs4.BeautifulSoup(req.content,'html.parser')
  
           ill_data = data.find(class_='item_list autopagerize_page_element')
           if ill_data == None:
               messagebox.showinfo("失敗", "データの取得に失敗しました\n投稿者の静画IDを確認してください")
           else:
               adress = EditBox3.get()
               password = Editbox4.get()
               browser = webdriver.Chrome('chromedriver.exe')
               browser.get('https://account.nicovideo.jp/login')
               elem_mailtel = browser.find_element_by_id('input__mailtel')
               elem_mailtel.send_keys(adress)
               elem_password = browser.find_element_by_id('input__password')
               elem_password.send_keys(password)
               elem_login_btn = browser.find_element_by_id('login__submit')
               elem_login_btn.click()
               sleep(3)
         
               if len(browser.find_elements_by_id('CommonHeader')) > 0 :
                   side = data.find(class_='refine_list')
                   p = side.find(class_="count")
                   p_num = p.decode_contents(formatter="html")
                   p_num = int(p_num)
                   last_p = (p_num - 1) // 40 + 2
                   for p_n in range(start_p_int,end_p_int + 1):
                       p_n = str(p_n)
                       req = requests.get(base_url + artist + "?page=" + p_n,headers=headers)
                       data = bs4.BeautifulSoup(req.content,'html.parser')
                       ill_data = data.find(class_='item_list autopagerize_page_element')
                       getData = ill_data.find_all('img')
                       for image in getData:
                           img_url = image.attrs['src']
                           a = image.attrs['alt']
                           art_name = a.translate(tbl)
                           imgname1 = img_url.split("/")[-1]
                           img_id = imgname1.rsplit("q")[0]
                           NICK = EditBox5.get()
       
                       
                           browser.get('http://seiga.nicovideo.jp/image/source/' + img_id)
                           img_url = browser.current_url
                           high_img = img_url.replace('/o','/priv')
                           if NICK == "":
                               filename = art_name + "_" + "im" + img_id
                               getImg = requests.get(high_img)
                               file = open(filepath + "/" + filename,'wb')
                               file.write(getImg.content)
                               file.close()
 
                               kind = filetype.guess(filepath + "/" + filename)
                               if os.path.exists(filepath + "/" + filename + "." + kind.extension):
                                   os.remove(filepath + "/" + filename + "." + kind.extension)
                               os.rename(filepath + "/" + filename,filepath + "/" + filename + "." + kind.extension)
                           else:
                               if NICK == "only im":
                                   filename = "im" + img_id
                                   getImg = requests.get(high_img)
                                   file = open(filepath + "/" + filename,'wb')
                                   file.write(getImg.content)
                                   file.close()
                                   kind = filetype.guess(filepath + "/" + filename)
                                   if os.path.exists(filepath + "/" + filename + "." + kind.extension):
                                        os.remove(filepath + "/" + filename + "." + kind.extension)
                                   os.rename(filepath + "/" + filename,filepath + "/" + filename + "." + kind.extension)
                               else:
                                   filename = NICK + "_" + art_name + "_" + "im" + img_id
                                   getImg = requests.get(high_img)
                                   file = open(filepath + "/" + filename,'wb')
                                   file.write(getImg.content)
                                   file.close()
                                   kind = filetype.guess(filepath + "/" + filename)
                                   if os.path.exists(filepath + "/" + filename + "." + kind.extension):
                                        os.remove(filepath + "/" + filename + "." + kind.extension)
                                   os.rename(filepath + "/" + filename,filepath + "/" + filename + "." + kind.extension)
                           sleep(1)
                   browser.quit()
           
               else:
                   browser.quit()
                   messagebox.showinfo("失敗", "ログインに失敗しました\nメールアドレスまたは電話番号、パスワードを確認してください")
           
Button2 = tkinter.Button(text=u'保存する',width=25,height=2,command=img_save)
Button2.place(x=250,y=190)
root.mainloop()


長スギィ!

半分以上GUIを表示するためのコードなので、画像を保存する部分だけを抜き出します。


base_url = "https://seiga.nicovideo.jp/user/illust/"
headers = {
       "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0",
       }
tbl = str.maketrans('¥/:*?"<>|','_________')

def img_save():
   
   start_p = EditBox6.get()
   end_p = EditBox7.get()
   if not start_p.isdecimal() or not end_p.isdecimal():
       messagebox.showinfo("エラー", "取得位置は整数で指定してください")
   else:
       start_p_int = int(start_p)
       end_p_int = int(end_p)
       filepath = EditBox2.get()
       verification = os.path.exists(filepath)
   
       if verification is False:
           dirdialog_clicked()
           filepath = EditBox2.get()
           verification = os.path.exists(filepath)
       if verification is True:
           artist = str(EditBox.get())
           req = requests.get(base_url + artist,headers=headers)
           data = bs4.BeautifulSoup(req.content,'html.parser')
  
           ill_data = data.find(class_='item_list autopagerize_page_element')
           if ill_data == None:
               messagebox.showinfo("失敗", "データの取得に失敗しました\n投稿者の静画IDを確認してください")
           else:
               adress = EditBox3.get()
               password = Editbox4.get()
               browser = webdriver.Chrome('chromedriver.exe')
               browser.get('https://account.nicovideo.jp/login')
               elem_mailtel = browser.find_element_by_id('input__mailtel')
               elem_mailtel.send_keys(adress)
               elem_password = browser.find_element_by_id('input__password')
               elem_password.send_keys(password)
               elem_login_btn = browser.find_element_by_id('login__submit')
               elem_login_btn.click()
               sleep(3)
         
               if len(browser.find_elements_by_id('CommonHeader')) > 0 :
                   side = data.find(class_='refine_list')
                   p = side.find(class_="count")
                   p_num = p.decode_contents(formatter="html")
                   p_num = int(p_num)
                   last_p = (p_num - 1) // 40 + 2
                   for p_n in range(start_p_int,end_p_int + 1):
                       p_n = str(p_n)
                       req = requests.get(base_url + artist + "?page=" + p_n,headers=headers)
                       data = bs4.BeautifulSoup(req.content,'html.parser')
                       ill_data = data.find(class_='item_list autopagerize_page_element')
                       getData = ill_data.find_all('img')
                       for image in getData:
                           img_url = image.attrs['src']
                           a = image.attrs['alt']
                           art_name = a.translate(tbl)
                           imgname1 = img_url.split("/")[-1]
                           img_id = imgname1.rsplit("q")[0]
                           NICK = EditBox5.get()
       
                       
                           browser.get('http://seiga.nicovideo.jp/image/source/' + img_id)
                           img_url = browser.current_url
                           high_img = img_url.replace('/o','/priv')
                           if NICK == "":
                               filename = art_name + "_" + "im" + img_id
                               getImg = requests.get(high_img)
                               file = open(filepath + "/" + filename,'wb')
                               file.write(getImg.content)
                               file.close()
 
                               kind = filetype.guess(filepath + "/" + filename)
                               if os.path.exists(filepath + "/" + filename + "." + kind.extension):
                                   os.remove(filepath + "/" + filename + "." + kind.extension)
                               os.rename(filepath + "/" + filename,filepath + "/" + filename + "." + kind.extension)
                           else:
                               if NICK == "only im":
                                   filename = "im" + img_id
                                   getImg = requests.get(high_img)
                                   file = open(filepath + "/" + filename,'wb')
                                   file.write(getImg.content)
                                   file.close()
                                   kind = filetype.guess(filepath + "/" + filename)
                                   if os.path.exists(filepath + "/" + filename + "." + kind.extension):
                                        os.remove(filepath + "/" + filename + "." + kind.extension)
                                   os.rename(filepath + "/" + filename,filepath + "/" + filename + "." + kind.extension)
                               else:
                                   filename = NICK + "_" + art_name + "_" + "im" + img_id
                                   getImg = requests.get(high_img)
                                   file = open(filepath + "/" + filename,'wb')
                                   file.write(getImg.content)
                                   file.close()
                                   kind = filetype.guess(filepath + "/" + filename)
                                   if os.path.exists(filepath + "/" + filename + "." + kind.extension):
                                        os.remove(filepath + "/" + filename + "." + kind.extension)
                                   os.rename(filepath + "/" + filename,filepath + "/" + filename + "." + kind.extension)
                           sleep(1)
                   browser.quit()
           
               else:
                   browser.quit()
                   messagebox.showinfo("失敗", "ログインに失敗しました\nメールアドレスまたは電話番号、パスワードを確認してください")

まだ長くて分かりにくいですね。これが初めてのプログラミングだったので、かなり冗長になっている部分もあるかと思います。

まず、このソフトで行っていることを簡単に説明すると、

1.seleniumを用いてニコ動にログイン
2.beautifulsoupでニコ静のユーザーページから画像URLを取得し、im以下の数字を抜き取る
3.「http://seiga.nicovideo.jp/image/source/ + im以下の数字」へ移動、大きい画像が表示されるURLを取得
4.名前を付けて画像を保存

※2~4を繰り返す

こんな感じになっています。順に解説。


1.seleniumを用いてニコ動にログイン

 #テキストボックスからメアドとパスワードを得る
 adress = EditBox3.get()
 password = Editbox4.get()
               
 #seleniumでChromeを開き、ログインページに移動
 browser = webdriver.Chrome('chromedriver.exe')
 browser.get('https://account.nicovideo.jp/login')
 
 #ログイン情報を入力
 elem_mailtel = browser.find_element_by_id('input__mailtel')
 elem_mailtel.send_keys(adress)
 elem_password = browser.find_element_by_id('input__password')
 elem_password.send_keys(password)
 
 #ログインボタンを押す
 elem_login_btn = browser.find_element_by_id('login__submit')
 elem_login_btn.click()
               

「メアド、パスワードを入力しログインボタンを押す」という作業をseleniumを用いて行っています。

参考にした動画
https://youtu.be/f8FXUUQ4uRA

本当にこの動画の通りに記述しただけなので詳しい解説はこちらを参考にしてください。

最初はseleniumを使わずに、

#ログインページのURLから、BeautifulSoupオブジェクト作成
url = "https://account.nicovideo.jp/login"
session = requests.session()
response = session.get(url)
bs = BeautifulSoup(response.text, 'html.parser')

#クッキーとトークンを取得
authenticity = bs.find(attrs={'name':'auth_id'}).get('value')
cookie = response.cookies

#ログイン情報
mail = "メールアドレス"
password = "パスワード"
login_info ={
  'auth_id': authenticity,
   'mail_tel':mail,
   'password':password
   }

#ログイン情報を送信
res = session.post(my_url, data=login_info) 

こんな感じでログインしようと思っていました。seleniumを使わない分、容量削減になると考えたからです。

しかし上手くいきません。この方法でログインするためにはトークン?とかいうものをサーバーへ送る必要があるみたいなんですが、どうやって取得すればいいのかよくわかりませんでした。

参考にしたサイト
https://kusoimox.hatenablog.jp/entry/2019/09/23/090000
https://gb-j.com/column/post-1911/


2.beautifulsoupでニコ静のユーザーページから画像URLを取得し、im以下の数字を抜き取る

base_url = "https://seiga.nicovideo.jp/user/illust/"
artist = EditBox.get() #投稿者の静画ID

#ニコ静の投稿者ページのHTMLを取得
req = requests.get(base_url + artist)
data = bs4.BeautifulSoup(req.content,'html.parser')

#絵の画像ソースのURLを取得
ill_data = data.find(class_='item_list autopagerize_page_element')
getData = ill_data.find('img')
img_url = getData.attrs['src']

#画像ソースのURLから数字を抜き取る
imgname1 = img_url.split("/")[-1]
img_id = imgname1.rsplit("q")[0]

ここで取得しているのはニコニコ静画のそれぞれの絵のURLである
https://seiga.nicovideo.jp/seiga/im(半角数字列)
のうちの、(半角数字列)の部分です。

スクリーンショット (489)

上の画像の「NEDIYU」のURLは
https://seiga.nicovideo.jp/seiga/im10823178
です。

投稿者ページの絵のサムネイルの画像ソースにその数字は含まれています。右側の青い部分
https://lohas.nicoseiga.jp/thumb/10823178qz?1633092347
のthumbとqz?の間ですね。

そこでsplit()を2回使用してその数字を抜き出しています。切り出し方にはもっと賢い方法があるのかもしれませんが、見つからなかったので2行かけて記述。

ちなみに「qz?」を「i?」に変えるだけで中程度のサイズの画像を取得できます。
https://lohas.nicoseiga.jp/thumb/10823178i?1633092347


3.「http://seiga.nicovideo.jp/image/source/ + im以下の数字」へ移動、大きい画像が表示されるURLを取得

#大きい画像が表示されるページへ遷移
browser.get('http://seiga.nicovideo.jp/image/source/' + img_id)

#遷移したページのURLを取得
img_url = browser.current_url

#取得したURLの「o」を「priv」に変更し、画像ソースのURLにする
high_img = img_url.replace('/o','/priv')

ニコ動にログインした状態で
http://seiga.nicovideo.jp/image/source/(2で取得した数字列)
へ遷移することで大きい画像を表示することができます。

大きい画像へアクセスすることでURLが
https://lohas.nicoseiga.jp/o/(複雑な半角英数字列)
に変化します。この(複雑な半角英数字列)はページにアクセスした際に生成されるようなので、直接この英数字列を取得することはできません。seleniumなどを用いて一度大きい画像のページを表示する必要があります。

※実際にニコ動にログインした状態で以下のURLにアクセスしてみるとURLが変化することがわかると思います。
https://seiga.nicovideo.jp/image/source/10823178

参考にしたサイトhttps://qiita.com/mataneko_boy/items/b99f2b3d3e43f56a28cd

はじめはここで得られる
https://lohas.nicoseiga.jp/o/(半角英数字列)
からrequests.get()で画像を取得しようと思ったのですが、うまくいきません。

そこで、このページのHTMLを見てみると

スクリーンショット (24)

ページ自体のURLは
https://lohas.nicoseiga.jp/o/(半角英数字列)
なのですが、画像ソースのURLは
https://lohas.nicoseiga.jp/priv/(半角英数字列)
でした。この二つのURLの(半角数字列)は全く同じです。つまり、oをprivへ置換することで画像ソースのURLを得ることができるのです。


4.名前を付けて画像を保存

base_url = "https://seiga.nicovideo.jp/user/illust/"
tbl = str.maketrans('¥/:*?"<>|','_________')

#投稿者ページのHTMLを取得
req = requests.get(base_url + artist)
data = bs4.BeautifulSoup(req.content,'html.parser')

#投稿者の名前を抜き出す
n = data.find(class_="nickname")
nickname = n.decode_contents(formatter="html") 
NICK = nickname.translate(tbl)

#それぞれの絵のタイトルを抜き出す
ill_data = data.find(class_='item_list autopagerize_page_element')
getData = ill_data.find('img')
a = getData.attrs['alt']
art_name = a.translate(tbl)

#名前を付けて保存
filename = NICK + "_" + art_name + "_" + "im" + img_id
getImg = requests.get(high_img)
file = open(filepath + "/" + filename,'wb')
file.write(getImg.content)
file.close()

#ファイル形式を判断し、名前を付け直す
kind = filetype.guess(filepath + "/" + filename)
if os.path.exists(filepath + "/" + filename + "." + kind.extension):
   os.remove(filepath + "/" + filename + "." + kind.extension)
os.rename(filepath + "/" + filename,filepath + "/" + filename + "." + kind.extension)

#1秒待つ
sleep(1)

結構長いですが、上半分は絵の作者、タイトルをHTMLタグや属性を目印に抜き出しているだけです。

参考にした動画
https://youtu.be/qIvkUCcOYyI

そして下半分、上で取得した情報をもとにファイルに名前を付け、ファイルパスを指定して、3で取得したURLから拡張子は付けずに画像を保存します。

そして保存したファイルの形式をfiletype.guessで読み取り、renameで名前を付け直します。

何故拡張子をつけないかというと、適当に.jpgとか.pngとか付けてしまうと、renameできなくなるからです。renameでは元の名前と変更後の名前が異なっている必要があります。

参考にしたサイト
https://techacademy.jp/magazine/36095

保存した後は、次の画像を保存するのですが、その前にある重要なことをしなくてはなりません。

それは「一秒待つ」ということです。

もし、1秒待たずに次の絵を保存すると、数十枚くらい保存したところで503エラーが発生します。

これはサーバーへのアクセスが集中したときに、負担を軽減するためにサーバー側でアクセスを一時制限している状態です。こうなると再びアクセスできるようになるまでしばらく待つ必要があります。

「しばらく」について具体的にどのくらいなのかはよく分かりませんが、少なくとも5分程度では回復しません。なので結果的に1枚保存するごとに1秒待つ方がスムーズに安定して保存することが出来るのです。

しかし、1秒待っても結局503エラーが発生することがあります。待ち時間を2秒、3秒にしてみたこともあるのですが、そうするとかえって安定しなくなります(ログイン状態が切れてしまうのか?)。僕が検証した限りでは1秒待つのが一番安定していました。この辺、よく分かる人はコメントして下さると有難いです。



以上です。

初めてプログラミングをしてみて思ったことは
「ググれば答えはわかるさ」
ということです。

やりたいことを検索すればそれについて書かれたページが必ずありました。それらを真似して、組み合わせてこのソフトはできています。感謝です。

なので、同じようなことをしたいと思った人がいたときに、少しでも助けになればと思いこれを書いています。もし内容が間違ってたらごめんなさい。







そういえば作り方については調べたけど、そもそも「ニコニコ静画から一括で画像保存するソフト」があるのかについては調べていませんでした。

あ り ま し た 。(しかも2つ)

しかも僕のソフトより高機能です。ログイン情報をfirefoxから読み取ることで、わざわざ入力しなくてもログインできるみたいだし、投稿者ごとだけじゃなくてタグごとに保存もできるし、ランキング上位を保存することもできるみたいだし、春画にも対応してるし、私いじけちゃうし…

スクリーンショット (25)

まあDLして比較みると明らかに僕のやつの方がシンプルですね!(ポジティブ)

タグ検索対応と春画対応は多分できると思います。要望があれば作るかも。

この記事が気に入ったらサポートをしてみませんか?