Closed8

node-fetch + Cheerioでプロキシを刺しながらスクレイピングする【Node.js】

ピン留めされたアイテム
masa5714masa5714

【はじめに】安価で高品質な有料のプロキシリストをお探しの方へ

WebShare を強くおすすめします。
僕もスクレイピングの際に絶対に利用してるプロキシリストです。

IP認証機能も備わっていたり、APIが提供されていたり大変便利です。

その上かなり安価です。
様々なプロキシを検討してきましたが、WebShare を超える低価格は現時点で見つかっていません。間違いなく最強です。


masa5714masa5714

必要なものをインストールしよう

  • node-fetch
  • https-proxy-agent
  • Cheerio
npm install node-fetch@2 https-proxy-agent cheerio
masa5714masa5714

再利用可能なフェッチ関数を用意する

毎度いろいろと記述するのが面倒くさいので、自作でクライアントを作成してフェッチしやすい状態にしておきたいと思います。

  • proxy.ts
  • fetch.ts

をつくりたいと思います。

リストからプロキシをランダムで1つ取得する関数

utils/functions/proxy.ts
import fs from "fs/promises";
import path from "path";

export const getRandomTheProxy = async () => {
  const proxy = await proxyList();
  const proxyArray = proxy.trim().split(/\n/);
  return proxyArray[Math.floor(Math.random() * proxyArray.length)];
};

const proxyList = (): Promise<string> => {
  const fileProxyListPath = path.resolve(__dirname, "./../proxy/proxy-list.txt");
  return new Promise(async (resolve) => {
    const result = await fs.readFile(fileProxyListPath, "utf-8");
    resolve(result);
  });
};

node-fetchのクライアント

utils/functions/fetch.ts
import fetch, { Response } from "node-fetch";
import { HttpsProxyAgent } from "https-proxy-agent";
import { getRandomTheProxy } from "@utils/functions/proxy";
import * as cheerio from "cheerio";

export const fetchClient = async () => {
  const proxyHost = await getRandomTheProxy();
  const proxyURL = `http://${proxyHost}`;

  const client = new HttpsProxyAgent(proxyURL);

  async function getRequest(url: string): Promise<{ response: Response; $: cheerio.CheerioAPI } | undefined> {
    try {
      const response = await fetch(url, { agent: client });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const body = await response.text();
      const $ = cheerio.load(body);
      return { response, $ };
    } catch (error) {
      console.error(error);
      return undefined;
    }
  }

  async function postRequest(
    url: string,
    {
      headers = {},
      parameters = {},
    }: {
      headers?: {};
      parameters?: {};
    }
  ): Promise<{ response: Response; $: cheerio.CheerioAPI } | undefined> {
    try {
      const response = await fetch(url, {
        method: "POST",
        agent: client,
        headers: headers,
        body: new URLSearchParams(parameters),
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const body = await response.text();
      const $ = cheerio.load(body);
      return { response, $ };
    } catch (error) {
      console.error(error);
      return undefined;
    }
  }

  return {
    client,
    getRequest,
    postRequest
  };
};

const response = await fetch(url, { agent: client }); という記述がありますが、この { agent: client } 部分によってプロキシが有効化されます。

masa5714masa5714

スクレイピングしてみよう

これで準備は整ったので実際にスクレイピングしてみます。

index.ts
import { fetchClient } from "@utils/functions/fetch";

const targetURL = "https://www.reirieofficial.com/";

(async () => {
  try {
    const { getRequest } = await fetchClient();
    const clientResponse = await getRequest(targetURL);

    if (!clientResponse) throw new Error("GETリクエストで問題が発生しました。");

    const { response, $ } = clientResponse;

    const newsTitleElement = $("div[id^=comp-] div[data-mesh-id^=comp] h2");
    const newsTitleText = newsTitleElement.text();

    console.log(newsTitleText);
  } catch (error) {
    console.log(error);
  }
})();

たったこれだけでプロキシ刺しながらスクレイピングが開始できるようになりました!

ポイントは

const { getRequest } = await fetchClient(); でフェッチのクライアントを作成し、
const clientResponse = await getRequest(targetURL); でGETリクエストを行い、
const { response, $ } = clientResponse; でCheerioが解析してくれたHTMLを $ として受け取る

の3点だけです。

$ は Cheerioの機能がそのまま使えます。

上記の例ではNEWSという文言を取得しています。

※ ちなみに要素取得をする際は const newsTitleElement = $("div[id^=comp-] div[data-mesh-id^=comp] h2"); のように元サイトがビルドされる度に変わりそうな箇所を予測して、スクレイピングに影響しないような指定を見つけるように意識するようにしましょう。

masa5714masa5714

リストなどの繰り返し要素の値を取得したい

スクレイピングするなら必須ですね。

import { fetchClient } from "@utils/functions/fetch";

const targetURL = "https://www.reirieofficial.com/";

(async () => {
  const results: {
    title: string;
    url: string;
  }[] = [];

  try {
    const { getRequest } = await fetchClient();
    const clientResponse = await getRequest(targetURL);

    if (!clientResponse) throw new Error("GETリクエストで問題が発生しました。");

    const { response, $ } = clientResponse;

    const newsArticleElements = $("div[id^=pro-gallery-container-comp-] article");

    newsArticleElements.each((index, newsArticle) => {
      const newsArticleElement = $(newsArticle);
      const newsArticleTitle = newsArticleElement.find("a p").text();
      const newsArticleURL = newsArticleElement.find("a").attr("href");

      results.push({
        title: newsArticleTitle ? newsArticleTitle : "",
        url: newsArticleURL ? newsArticleURL : "",
      });
    });
  } catch (error) {
    console.log(error);
  }

  console.log(results);
})();

.each でループ処理をぶん回せます。

▼実行結果

masa5714masa5714

Postmanでリクエストが通るパターンを事前に見つけよう!

いきなり node-fetch でリクエストしてしまってもいいのですが、
場合によってはheaderやCookie情報の有無で正常なリクエストを判別してるケースがあります。(CloudFrontを利用のサイトでこの動きを確認した。)

この場合、node-fetchで何も考えずにリクエストしてしまうと、そのIPアドレスを無駄死にさせてしまいます。これは勿体ないです。

事前に Postman を使って欠けてもいいデータを選別し確実にリクエストが通るように検証しましょう。(一度リクエストが通ったIPなら数十回は適当なリクエストしてもブロックされることは滅多にありません。仮にブロックされたとしたら相当スクレイピング対策に力を注いでるサイトだと思います。)

また、余裕があれば事前処理として、相手サイトのホストに向けてプロキシの有効性をチェックするような関数を書いておくと良いでしょう。(プロキシリストを一つずつリクエストしてブロックされてないかをチェックするような処理。)

masa5714masa5714

POSTメソッドで何をリクエストで投げればいいか分からない方へ

スクレイピングは必ずしもGETリクエストで行うものではありません。
場合によってはPOSTメソッドで受け取ったデータに応じてビューが切り替わることがあります。

しかし、どのデータをPOSTすればデータが切り替わるか分からないでしょう。

そんなときも Postman を利用しましょう。

まずは Chrome開発者ツールの Network から対象のデータリソースを選択し、Payloadタブの From data欄から何が送信されているかを確認します。その中から処理に関係していそうなものに目星をつけましょう。(こればかりは地味に探すしかありません。)

それっぽいものを見つけたらPostmanで下記のようにやってみましょう。
POSTメソッドにし、Bodyタブでform-dataでキーと値を入れて、Sendをクリックします。

下部に結果が表示されますから、それが望んだ結果になっていれば成功です!
違う場合は他のform-dataを探してみるか、CookieやSessionが影響していないかを検証しましょう。(この作業が一番 "やってる感" あって楽しいです。)

このスクラップは2ヶ月前にクローズされました