2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

APIなし、Shift_JIS、DOM解析。PlaywrightでASP管理画面を「完全自動化」するまでの血と汗の記録

2
Posted at

APIなし、Shift_JIS、DOM解析。PlaywrightでASP管理画面を「完全自動化」するまでの血と汗の記録

記事のイメージ画像

はじめに

ASP(アフィリエイト・サービス・プロバイダ)の管理画面。それは自動化エンジニアにとっての「魔境」です。
APIが未整備なだけでなく、CSV生成に時間がかかったり、ファイル名の取得にDOMの正規表現解析が必要だったりと、一筋縄ではいきません。

この記事では、NestJS + Playwright を使い、そんな魔境をどうやって攻略したか、実際に運用しているコードのロジックをベースに解説します。

結論:何をやったか

  1. Playwrightのor()を活用し、リンクとボタンが混在する不安定なUIでも確実にCSV生成を実行。
  2. Intl.DateTimeFormat を駆使し、タイムゾーンに依存しない「JSTの昨日」を厳密に特定。
  3. $$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👍をお願いします!
質問があればコメント欄までお気軽にどうぞ!🚀

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?