とりあえず、Puppeteerを使ったE2Eテストをスラスラ書けるようにするため、DMMやとらのあな、メロンブックス辺りをスクレイピングして、人気の性癖を調べる実用的なコードでも。
— 神速 (@sinsoku_listy) 2019年6月10日
E2Eテストの練習としてPuppeteerで実用的なコードを書いてみた。
やったこと
FANZA同人のランキング1〜100位の作品を取得し、全てのジャンルを計測して 人気ジャンル を出してみた。
ソースコード
const puppeteer = require('puppeteer'); let browser; beforeEach(async () => browser = await puppeteer.launch()); afterEach(async () => await browser.close()); const timeout = 20 * 60 * 1000; // 20 minutes displayRanking = async (page) => { for(let i = 1; i < 5; i++) { await page.evaluate(_ => window.scrollBy(0, document.body.scrollHeight)); await page.waitFor(`.rank-rankListItem:nth-child(${20 * (i + 1)})`); } }; findTags = async (page, urls) => { const tagsArray = []; for(let i = 0; i < urls.length; i++) { console.log(`${i}: parsing... ${urls[i]}`); await page.goto(urls[i]); const tags = await page.$$eval('.genreTagList__item', divs => divs.map(div => div.innerText)); tagsArray.push(tags); } return tagsArray.flat(); }; test('doujin', async () => { const page = await browser.newPage(); await page.goto('https://www.dmm.co.jp/dc/doujin/-/ranking-all/=/sort=popular/term=monthly/'); const title = await page.$eval('.c_hdg_withSortTitle', el => el.innerText); expect(title).toMatch(/総合ランキング 月間/); await displayRanking(page); const itemsCount = await page.$$eval('.rank-rankListItem', items => items.length); expect(itemsCount).toBe(100); const urls = await page.$$eval('.rank-name a', links => links.map(link => link.href)); const tags = await findTags(page, urls); const ranks = {}; tags.forEach((tag) => { if (ranks[tag] === undefined) ranks[tag] = 0; ranks[tag] += 1; }); ranks['成人向け'] = 0; ranks['男性向け'] = 0; const topRanks = Object.entries(ranks).sort((a, b) => b[1] - a[1]).slice(0, 20); console.log(topRanks); }, timeout);
無限スクロール
最初は1〜20位までしか表示されていないので、スクロールしつつ waitFor
で作品の表示を待つようにする。
displayRanking = async (page) => { for(let i = 1; i < 5; i++) { await page.evaluate(_ => window.scrollBy(0, document.body.scrollHeight)); await page.waitFor(`.rank-rankListItem:nth-child(${20 * (i + 1)})`); } };
各ページのタグ取得
タブを生成して、並列に処理しようとしたら TimeoutError: Navigation Timeout Exceeded: 30000ms exceeded
が起きて、解決方法が分からなかった。
とりあえず、1ページずつ順番に処理する方法で対応。(時間かかるけど)
findTags = async (page, urls) => { const tagsArray = []; for(let i = 0; i < urls.length; i++) { console.log(`${i}: parsing... ${urls[i]}`); await page.goto(urls[i]); const tags = await page.$$eval('.genreTagList__item', divs => divs.map(div => div.innerText)); tagsArray.push(tags); } return tagsArray.flat(); };
ランキング計測
Rubyのgroup_byみたいなのが見当たらなかったので、それっぽくカウントした。
成人向けと男性向けは全ての作品に入っていたので除外してる。
const ranks = {}; tags.forEach((tag) => { if (ranks[tag] === undefined) ranks[tag] = 0; ranks[tag] += 1; }); ranks['成人向け'] = 0; ranks['男性向け'] = 0; const topRanks = Object.entries(ranks).sort((a, b) => b[1] - a[1]).slice(0, 20);
結果
こんなジャンルが人気あるみたいですよ。
console.log test/dmm.test.js:52 [ [ '中出し', 77 ], [ '巨乳', 63 ], [ 'フェラ', 56 ], [ '新作', 47 ], [ 'おっぱい', 44 ], [ '制服', 36 ], [ 'パイズリ', 35 ], [ '寝取り・寝取られ・NTR', 27 ], [ '準新作', 22 ], [ '処女', 20 ], [ 'アナル', 20 ], [ '人妻・主婦', 19 ], [ '野外・露出', 18 ], [ 'ラブラブ・あまあま', 16 ], [ '辱め', 14 ], [ 'ぶっかけ', 14 ], [ '近親相姦', 13 ], [ '和姦', 11 ], [ '3P・4P', 11 ], [ 'ハーレム', 11 ] ] PASS test/dmm.test.js (687.794s) ✓ doujin (686607ms)