Scrapyで欅坂46とけやき坂46の各メンバーの画像収集する
はじめに
欅坂46の画像で誰が写っているのかを認識させるアプリをディープラーニングで作ろうと思いますので、今回は各メンバーの画像を収集します。
MicrosoftのBing Image SearchやGoogleの画像検索APIを使うことも考えましたが、Yahoo、Bing、Googleでの画像収集事情まとめを読んで「1メンバーあたり1000枚は欲しい」と思いましたので、Yahoo Japanの画像検索(簡易版)をスクレイピングすることに決めました。
このような場合、私は python + Requests + BeautifulSoup で実装してきましたが、勉強のため、以前から気になっていたScrapyを使うことにしました。
実装にあたってはこちらを参考にしました。
Python + Scrapyで画像を巡回取得する
完成品の実行方法
今回作った画像収集スクリプトは次のコマンドをターミナルに打って実行します。
$ scrapy crawl yahoo_image -a query=石森虹花 -a member_id=1
1回の実行につき1人のメンバーを画像検索にかけて画像を収集します。
queryにはメンバー名、member_idにはディープラーニングで学習させる際に使うラベルを指定します。
member_idの付け方は任意なので、もし「推しメンが1じゃないと嫌だ」という方は変えてもらって大丈夫です。
1回の実行でメンバー全員を処理することもできますが、上手く設計しないと、途中でバッチ処理がコケてしまった場合に、そこから再開させるためにはスクリプトを修正する必要が出てくる可能性があります。
今回採用した方法ならば、最悪でもシェルスクリプトでオプションの値を変えて並べるだけで済むので、再開しやすいです。
Scrapyをインストールする
~ $ pip install Scrapy
Projectを作成する
ターミナルに次のコマンドを打ってプロジェクトを作成します。
~ $ scrapy startproject image_scraper
すると、カレントディレクトリにimage_scraperディレクトリができています。
このディレクトリの構成は次のようになっているはずです。
~ $ tree image_scraper image_scraper/ ├── scrapy.cfg └── image_scraper ├── __init__.py ├── __pycache__ ├── items.py ├── middlewares.py ├── pipelines.py ├── settings.py └── spiders ├── __init__.py └── __pycache__ 4 directories, 7 files
spidersディレクトリには
Spiderを作成する
SpiderにはWebサイトをどのように探索するのかを定義します(下の引用*1を参考)。
Spiders are classes which define how a certain site (or a group of sites) will be scraped, including how to perform the crawl (i.e. follow links) and how to extract structured data from their pages (i.e. scraping items). In other words, Spiders are the place where you define the custom behaviour for crawling and parsing pages for a particular site (or, in some cases, a group of sites).
ここではPython + Scrapyで画像を巡回取得するの「Spiderを用意して実装する」を参考にして ~/image_scraper/spider/
に yahoo_image_spider.py
を作成します。
# -*- coding: utf-8 -*- import scrapy from image_scraper.items import ImageScraperItem from scrapy.spiders import CrawlSpider class YahooImageSpider(CrawlSpider): name = 'yahoo_image' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.image_id = 0 self.page_id = 0 self.image_num_per_page = 20 def start_requests(self): url = ('https://search.yahoo.co.jp/image/search?' 'p={}&ei=UTF-8&save=0'.format(self.query)) yield scrapy.Request(url, self.parse, dont_filter=True) def parse(self, response): self.page_id += 1 items = ImageScraperItem() items['member_id'] = self.member_id items['image_ids'] = [] items['image_urls'] = [] for src in response.css('#gridlist > div'): self.image_id += 1 url = src.css('div > p > a::attr(href)').extract_first() items['image_ids'].append(self.image_id) items['image_urls'].append(url) yield items _next_page = ('https://search.yahoo.co.jp/image/search?fr=top_ga1_sa&' 'p={query}&ei=UTF-8&b={start_image}') next_page = _next_page.format( query=self.query, start_image=self.page_id * self.image_num_per_page + 1 ) if next_page is not None: yield response.follow(next_page, callback=self.parse)
query_idとmember_idは上で紹介した実行時に与えられる値が入っています。
image_idは取得した画像に付けるIDでファイル名として使います。今回は取得した順に付けていきます。
page_idはスクレイプするWebページのIDで、スクレイプを停止するための条件判定で使います。これも取得した順につけていきます。
image_num_per_pageは1ページあたりの画像の枚数で20で設定しています。
これはYahoo Japanの画像検索(簡易版)では1ページに20枚の画像が表示されるためです。
クローラーを次ページへ巡回させる部分はScrapyのチュートリアル*2を参考にしました。
_next_page = ('https://search.yahoo.co.jp/image/search?fr=top_ga1_sa&' 'p={query}&ei=UTF-8&b={start_image}') next_page = _next_page.format( query=self.query, start_image=self.page_id * self.image_num_per_paホットバージョン Vol.152ge + 1 ) if next_page is not None: yield response.follow(next_page, callback=self.parse)
理由は、Python + Scrapyで画像を巡回取得するでは
rules = ( Rule(LinkExtractor(allow=( )), callback="parse_page", follow=True), )
で上手く行っていましたが、私の場合は途中でリンクが取得できずにクローラーが終了してしまったためです*3。
また、次のページのリンクを抽出するのにCSS Selectorを使っていません。
これはリンクを辿って行くと途中でCSSの構造が変わってしまい、意図通りにURLを取得できなかったためです。
そのため、URLのクエリストリングを更新するスタイルにしました。
ImageScraperItemを実装する
scrapy.Itemを継承したImageScraperItemは、Webページから取得したデータをScrapyの他の機能に渡すためのデータフォーマットを定義します。
item_scraper/items.py
を以下の内容で保存します。
# -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html import scrapy from scrapy.item import Field class ImageScraperItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() member_id = Field() image_ids = Field() image_urls = Field()
image_idsは、spiderの章でも紹介しましたが、画像のIDを格納します。
image_urlsはこれからダウンロードする画像のURLを格納するのに使います。
ImageScraperPipelineを実装する
Item Pipelineはspiderで取得したアイテムの処理を担当するパートです。
典型的な用途としては、HTMLのクレンジングやスクレイピングしたデータの整合性チェック、DBへのインサートを含むデータの保存があります。
今回はスクレイピングしたURLから画像をダウンロードしてローカルに保存するように実装していきます。
公式ドキュメントからの引用 Item Pipeline — Scrapy 1.5.1 documentation
After an item has been scraped by a spider, it is sent to the Item Pipeline which processes it through several components that are executed sequentially.
Each item pipeline component (sometimes referred as just “Item Pipeline”) is a Python class that implements a simple method. They receive an item and perform an action over it, also deciding if the item should continue through the pipeline or be dropped and no longer processed.
Typical uses of item pipelines are:
- cleansing HTML data
- validating scraped data (checking that the items contain certain fields)
- checking for duplicates (and dropping them)
- storing the scraped item in a database
image_scraper/pipelines.py
を以下の内容で保存します。
# -*- coding: utf-8 -*- # Define your item pipelines here # # Don't forget to add your pipeline to the ITEM_PIPELINES setting # See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html import pathlib import scrapy from scrapy.pipelines.images import ImagesPipeline from scrapy.utils.misc import md5sum class ImageScraperPipeline(ImagesPipeline): def get_media_requests(self, item, info): member_id = item['member_id'] for img_id, url in zip(item['image_ids'], item['image_urls']): meta = { 'member_id': member_id, 'image_id': img_id } yield scrapy.Request(url, meta=meta) def image_downloaded(self, response, request, info): checksum = None for path, image, buf in self.get_images(response, request, info): if checksum is None: buf.seek(0) checksum = md5sum(buf) width, height = image.size suffix = pathlib.Path(path).suffix path = '{member_id:03d}/{image_id:04d}{suffix}'.format( member_id=int(request.meta['member_id']), image_id=request.meta['image_id'], suffix=suffix ) self.store.persist_file( path=path, buf=buf, info=info, meta={'width': width, 'height': height} ) return checksum
プロジェクトを作成したままの状態ですと
def process_item(self, item, spider): pass
がありますが、これを残したままにしておくと画像がダウンロードされません*4ので、削除して下さい。
ダウンロードした画像は~/image_scraper/download/{メンバーID}/{画像ID}.{拡張子}
に保存されます。
例えば、石森虹化のメンバーIDを1として、取得順序が1番目のJPEG画像は~/image_scraper/download/001/0001.jpg
に保存されています。
画像保存場所にdownload
ディレクトリを指定している部分は次章のsettings.pyを変更することで行っています。
Scrapyの設定を変更する
pipelineの設定
プロジェクトを作成したままでもITEM_PIPELINES
は次のように有効になっているはずです。
ITEM_PIPELINES = { 'image_scraper.pipelines.ImageScraperPipeline': 1, }
この下に画像の保存先の設定としてIMAGES_STORE = './download'
を加えます。
これで画像は~/image_scraper/download
以下に保存されるようになります。
spiderの停止条件
次にspiderが規定以上のItem数になったら停止するように設定します。
Scrapyはデフォルト状態では並行処理する設定になっている*5ので、50と設定してもそれ以上の数の画像をダウンロードします。
今回の場合、メンバーにもよりますが、1人あたり平均して500枚程度の画像を取得できます。
EXTENSIONS = { 'scrapy.extensions.closespider.CloseSpider': 1, } CLOSESPIDER_ITEMCOUNT = 50
Bot判定を回避するための設定
以下の2つを設定します。
この設定は公式ドキュメントの Avoiding getting banned を参考にしました。
COOKIES_ENABLED = False
DOWNLOAD_DELAY = 3
全メンバー分をバッチ実行するシェルスクリプトを作成する
まず漢字とひらがなのメンバーリストmember_list.csv
を用意します。
$ head -n 5 member_list.csv member_id,member_name 1,石森虹花 2,今泉佑唯 3,上村莉菜 4,尾関梨香
このリストに記載されているメンバー全員についてscrapy
を実行するシェルスクリプトexecute_crawl.sh
を以下の内容で作成します。
実行時のログをあとで確認できるようにファイルとして保存しておくため、オプションに--logfile log/yahoo_image_${member_id}.log
と設定しています。
#!/bin/sh csv_file="member_list.csv" for line in `cat ${csv_file}` do member_id=`echo ${line} | cut -d ',' -f 1` member_name=`echo ${line} | cut -d ',' -f 2` echo "processing member_id ${member_id}, member_name ${member_name}" scrapy crawl yahoo_image -a query=$member_name -a member_id=$member_id --logfile log/yahoo_image_${member_id}.log done
実際に画像を収集する
先ほど作ったexecute_crawl.sh
を実行します。
~/image_scraper$ chmod 744 execute_crawl.sh ~/image_scraper$ ./execute_crawl.sh
私の環境では3時間ほどでクローリングが終了しました。
実際に画像が何枚ダウンロードできたのかを集計する
実際にダウンロードした画像が枚数だったのかを知るために次の4項目を集計してみます。
作成したスクリプトは次の通りです。
# -*- coding: utf-8 -*- import pathlib import pandas as pd member_list = pd.read_csv('member_list.csv') download_dir = pathlib.Path('./download') # --------------------------------------------- # Aggregating number of images at each member # --------------------------------------------- member_id = [] member_name = [] image_count = [] for _, (_id, _name) in member_list.iterrows(): _dir = download_dir / '{:03d}'.format(_id) image_path_list = [p for p in _dir.iterdir()] member_id.append(_id) member_name.append(_name) image_count.append(len(image_path_list)) download_image_count = pd.DataFrame( data={ 'member_id': member_id, 'member_name': member_name, 'image_count': image_count } ) column_order = ['member_id', 'member_name', 'image_count'] download_image_count = download_image_count[column_order] # --------------------------------------------- # Display summaries of downloaded images # --------------------------------------------- print('Number of downloaded images at each member:') print(download_image_count) print() number_of_members = member_list.shape[0] print('Number of members: {}'.format(number_of_members)) total_image_count = download_image_count.image_count.sum() print('Total number of images: {}'.format(total_image_count)) avg_image_count = download_image_count.image_count.mean() print('Average number of images: {}'.format(avg_image_count))
上を実行した結果は以下の通りです。
$ python ./summary_download.py Number of downloaded images at each member: member_id member_name image_count 0 1 石森虹花 484 1 2 今泉佑唯 481 2 3 上村莉菜 475 3 4 尾関梨香 523 4 5 織田奈那 451 5 6 小池美波 508 6 7 小林由依 497 7 8 齋藤冬優花 540 8 9 佐藤詩織 466 9 10 志田愛佳 476 10 11 菅井友香 449 11 12 鈴本美愉 512 12 13 長沢菜々香 478 13 14 土生瑞穂 464 14 15 原田葵 496 15 16 平手友梨奈 532 16 17 守屋茜 478 17 18 米谷奈々未 439 18 19 渡辺梨加 453 19 20 渡邉理佐 494 20 21 井口眞緒 402 21 22 潮紗理菜 450 22 23 柿崎芽実 436 23 24 影山優佳 444 24 25 加藤史帆 443 25 26 齊藤京子 465 26 27 佐々木久美 388 27 28 佐々木美玲 433 28 29 高瀬愛奈 437 29 30 高本彩花 446 30 31 長濱ねる 460 31 32 東村芽依 465 32 33 金村美玖 399 33 34 河田陽菜 359 34 35 小坂菜緒 340 35 36 富田鈴花 423 36 37 丹生明里 372 37 38 濱岸ひより 346 38 39 松田好花 379 39 40 宮田愛萌 371 40 41 渡邉美穂 445 Number of members: 41 Total number of images: 18399 Average number of images: 448.7560975609756
まとめ
スクレイピング・フレームワークのScrapyを使って欅坂46とけやき坂46のメンバーの画像を収集しました。
1メンバーあたり1000枚取得を目標にしていましたが、実際には450枚弱しか取得できませんでした。
CLOSESPIDER_ITEMCOUNT
の値を増やして実行してもいいのですが、目的はメンバーの顔を認識するアプリの作成なので、ひとまずこれで画像の収集を終わりにしたいと思います。
精度が出なかった場合はさらに画像をダウンロードすることを検討します。
*1:https://docs.scrapy.org/en/latest/topics/spiders.html
*2:https://docs.scrapy.org/en/latest/intro/tutorial.html#following-links
*3:目的に適した自前のLinkExtractorを定義することもできるようですが、今回は理解しやすかったチュートリアルの例を基に実装しました。
*4:これに気づくのに結構時間が掛かりました...(汗)
*5:https://stackoverflow.com/questions/44006672/scrapy-closespider-itemcount-setting-not-working-as-expected