node-fetch + Cheerioでプロキシを刺しながらスクレイピングする【Node.js】
ts-nodeで実行する際に問題が起きたので、
node-fetchはversion 2を指定してインストールしています。
▼ 参考:
必要なものをインストールしよう
- node-fetch
- https-proxy-agent
- Cheerio
npm install node-fetch@2 https-proxy-agent cheerio
再利用可能なフェッチ関数を用意する
毎度いろいろと記述するのが面倒くさいので、自作でクライアントを作成してフェッチしやすい状態にしておきたいと思います。
- proxy.ts
- fetch.ts
をつくりたいと思います。
リストからプロキシをランダムで1つ取得する関数
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のクライアント
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 }
部分によってプロキシが有効化されます。
スクレイピングしてみよう
これで準備は整ったので実際にスクレイピングしてみます。
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");
のように元サイトがビルドされる度に変わりそうな箇所を予測して、スクレイピングに影響しないような指定を見つけるように意識するようにしましょう。
リストなどの繰り返し要素の値を取得したい
スクレイピングするなら必須ですね。
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
でループ処理をぶん回せます。
▼実行結果
Postmanでリクエストが通るパターンを事前に見つけよう!
いきなり node-fetch でリクエストしてしまってもいいのですが、
場合によってはheaderやCookie情報の有無で正常なリクエストを判別してるケースがあります。(CloudFrontを利用のサイトでこの動きを確認した。)
この場合、node-fetchで何も考えずにリクエストしてしまうと、そのIPアドレスを無駄死にさせてしまいます。これは勿体ないです。
事前に Postman を使って欠けてもいいデータを選別し確実にリクエストが通るように検証しましょう。(一度リクエストが通ったIPなら数十回は適当なリクエストしてもブロックされることは滅多にありません。仮にブロックされたとしたら相当スクレイピング対策に力を注いでるサイトだと思います。)
また、余裕があれば事前処理として、相手サイトのホストに向けてプロキシの有効性をチェックするような関数を書いておくと良いでしょう。(プロキシリストを一つずつリクエストしてブロックされてないかをチェックするような処理。)
POSTメソッドで何をリクエストで投げればいいか分からない方へ
スクレイピングは必ずしもGETリクエストで行うものではありません。
場合によってはPOSTメソッドで受け取ったデータに応じてビューが切り替わることがあります。
しかし、どのデータをPOSTすればデータが切り替わるか分からないでしょう。
そんなときも Postman を利用しましょう。
まずは Chrome開発者ツールの Network から対象のデータリソースを選択し、Payloadタブの From data欄から何が送信されているかを確認します。その中から処理に関係していそうなものに目星をつけましょう。(こればかりは地味に探すしかありません。)
それっぽいものを見つけたらPostmanで下記のようにやってみましょう。
POSTメソッドにし、Bodyタブでform-dataでキーと値を入れて、Sendをクリックします。
下部に結果が表示されますから、それが望んだ結果になっていれば成功です!
違う場合は他のform-dataを探してみるか、CookieやSessionが影響していないかを検証しましょう。(この作業が一番 "やってる感" あって楽しいです。)