APIなし、Shift_JIS、DOM解析。PlaywrightでASP管理画面を「完全自動化」するまでの血と汗の記録
はじめに
ASP(アフィリエイト・サービス・プロバイダ)の管理画面。それは自動化エンジニアにとっての「魔境」です。
APIが未整備なだけでなく、CSV生成に時間がかかったり、ファイル名の取得にDOMの正規表現解析が必要だったりと、一筋縄ではいきません。
この記事では、NestJS + Playwright を使い、そんな魔境をどうやって攻略したか、実際に運用しているコードのロジックをベースに解説します。
結論:何をやったか
-
Playwrightの
or()を活用し、リンクとボタンが混在する不安定なUIでも確実にCSV生成を実行。 - Intl.DateTimeFormat を駆使し、タイムゾーンに依存しない「JSTの昨日」を厳密に特定。
-
$$evalによるDOM解析で、onclick属性の中に隠されたファイル名を正規表現でぶっこ抜く。
1. 「昨日」を確実に当てるための日付ハック
ASPの管理画面で「昨日のログ」をダウンロードする場合、サーバーのタイムゾーン設定に左右されない堅牢な日付計算が必須です。
// JSTの「今日 00:00」を基準に、厳密に「昨日」の文字列(YYYY年MM月DD日)を生成
const now = new Date();
const todayParts = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Tokyo",
year: "numeric", month: "2-digit", day: "2-digit",
}).formatToParts(now);
const yToday = todayParts.find((p) => p.type === "year")!.value;
const mToday = todayParts.find((p) => p.type === "month")!.value;
const dToday = todayParts.find((p) => p.type === "day")!.value;
const jstTodayMidnightUtc = new Date(`${yToday}-${mToday}-${dToday}T00:00:00+09:00`);
jstTodayMidnightUtc.setUTCDate(jstTodayMidnightUtc.getUTCDate() - 1);
// ASPの表示形式「2024年01月01日」のように合わせる
const yParts = new Intl.DateTimeFormat("ja-JP", {
timeZone: "Asia/Tokyo",
year: "numeric", month: "2-digit", day: "2-digit",
}).formatToParts(jstTodayMidnightUtc);
const expectedDateStrYesterday = `${yParts.find(p => p.type === 'year')!.value}年${yParts.find(p => p.type === 'month')!.value}月${yParts.find(p => p.type === 'day')!.value}日`;
ポイント: Intl.DateTimeFormat を使うことで、実行環境がどこであっても確実に「日本時間の昨日」を特定しています。
2. リンクかボタンか? 不安定なUIのねじ伏せ方
ASPのUIは時に気まぐれです。同じ「CSV生成」でも、リンク(<a>)だったりボタン(<button>)だったりします。
これを or() セレクタで突破します。
const csvGen = page
.getByRole("link", { name: "CSV生成" })
.first()
.or(page.getByRole("button", { name: "CSV生成" }).first());
await csvGen.waitFor({ state: "visible" });
await csvGen.click();
さらに、生成に時間がかかることを考慮し、あえて waitForTimeout(60000) を置く「待ちの美学」も、現場では時として正解になります。
3. DOMの中に隠れた「真のリンク」を暴く
ダウンロードリンクが動的に生成され、onclickの中にファイル名が埋め込まれている……そんな時でも $$eval なら一括で解析可能です。
const blocks = await page.$$eval('div[class^="csvInfoExport"]', (nodes) => {
return Array.from(nodes).slice(0, 5).map((el) => {
const text = (el.textContent || "").replace(/\s+/g, "");
const onclick = el.querySelector("a")?.getAttribute("onclick") || "";
// onclick="download('xxx.csv')" のような文字列からファイル名を抜き出す
let fileName = onclick.match(/"(.*?\.csv)"/)?.[1] || null;
return { text, fileName };
});
});
4. Shift_JIS CSVを安全に処理する
ダウンロードしたCSVは、ストリームではなく一旦 Buffer で読み込み、iconv-lite で変換するのが結局一番安定します。
private async processCsv(filePath: string): Promise<RawCatsData[]> {
const buffer = fs.readFileSync(filePath);
const utf8Data = iconv.decode(buffer, "Shift_JIS");
const records = parse(utf8Data, {
columns: true,
skip_empty_lines: true,
relax_column_count: true, // これが重要!
}) as RawCatsData[];
// 処理が終わったら一時ファイルを削除する「後片付け」も忘れずに
fs.unlinkSync(filePath);
return records;
}
まとめ:運用を止めないための「現実解」
綺麗なコードを書くことは重要ですが、「APIがない」「UIがレガシー」という極限状態では、Playwrightをいかに柔軟に、かつ泥臭く使いこなせるかが勝負になります。
- JSTの日付計算を徹底し、取得ミスを防ぐ。
- DOM解析($$eval)を駆使し、隠れた情報を抜く。
- CSVパース設定を緩めることで、些細な形式変更で落ちないようにする。
もし似たようなASP自動化で悩んでいる方がいれば、この「割り切り」のコードを参考にしてみてください。
役に立ったらLGTM👍をお願いします!
質問があればコメント欄までお気軽にどうぞ!🚀