Puppeteerを使ってDMMの同人ランキングから人気のあるジャンルを列挙してみる

E2Eテストの練習としてPuppeteerで実用的なコードを書いてみた。

やったこと

FANZA同人のランキング1〜100位の作品を取得し、全てのジャンルを計測して 人気ジャンル を出してみた。

www.dmm.co.jp

ソースコード

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)