0
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?

alt属性だけで状態判定していませんか? 同一alt・異なる状態をsrcの命名規則で解決した話

0
Posted at

環境

項目 バージョン / 内容
ブラウザ Google Chrome 120(Manifest V3)
実行環境 Chrome拡張のcontent script(ES2022)
検証スクリプト Node.js 20.10 + cheerio 1.0
対象ページ ASPの案件一覧(サーバーサイドレンダリング、要ログイン)
取得方法 fetch したHTMLを DOMParser でパース

何が起きたか

ASPの案件一覧を自動収集するChrome拡張を作っていました。ページ全体を DOMParser(ブラウザ内蔵のHTML解析API)で読み込み、案件ごとの「リスティング可否」を判定して一覧化する仕組みです。

判定ロジックは、当初こう書いていました。

// NG: alt属性だけで状態を判定していた頃のコード
const doc = new DOMParser().parseFromString(html, 'text/html');

const rows = [...doc.querySelectorAll('.casino-list__item')];

const items = rows.map((row) => {
  const icon = row.querySelector('.status-icon');
  return {
    name: row.querySelector('.casino-list__name')?.textContent.trim(),
    listing: icon?.alt === 'リスティングOK' ? 'ok' : 'ng',
  };
});

一見問題なさそうです。ところが実際に出力された一覧は、「不可」の案件まで全部 ok になっていました。

原因は単純で、「リスティングOK」というアイコン画像が、実は 許可・条件付き・不可の3状態すべてで同じ alt を持っていた からです。alt が同じなら当然すべて ok に倒れます。テストを書いていなかったので、気づいたのは出力CSVを目視したときでした。

なぜaltは状態判定に使えないのか

alt属性は「画像が表示できないときに代わりに読ませるテキスト」であって、状態コードではありません。3状態を同じ文言で表現していても、アクセシビリティ的にはむしろ自然な設計です(人間にはアイコンの色や形で区別がつくため)。つまり altは人間向けのラベル、状態は別の場所に埋まっている と考えたほうが安全です。

解決策: srcのファイル名を第2のキーにする

DevToolsでimg要素を全件見ていくと、画像ファイル名が状態ごとに分かれていることが分かりました。

src のファイル名 alt 実際の状態
icon06.png リスティングOK 許可
icon07.png リスティングOK 条件付き
icon06_n.png リスティングOK 不可

icon06 / icon07 / 末尾 _n という命名規則です。altだけを見ていたら絶対に区別できません。

そこで、判定を「alt → src」の順ではなく、srcを主キー・altをフォールバック に変更しました。

// OK: srcのファイル名を主キーにした判定
const STATE_BY_FILE = {
  icon06: 'ok',
  icon07: 'conditional',
  icon06_n: 'ng',
};

function normalizeState(img) {
  if (!img) return 'unknown';

  // srcset や遅延読み込みにも備えて候補URLを全部集める
  const raw = img.getAttribute('src')
    || img.getAttribute('data-src')
    || img.getAttribute('srcset')?.split(',')[0]?.trim()
    || '';

  // クエリ文字列・ハッシュを落とし、拡張子を除いてベース名を取る
  const base = raw
    .split(/[?#]/)[0]
    .split('/')
    .pop()
    .replace(/\.[a-z0-9]+$/i, '')
    .toLowerCase();

  if (STATE_BY_FILE[base]) return STATE_BY_FILE[base];

  // 命名規則から外れたら alt にフォールバック(誤判定より unknown を優先)
  const alt = (img.getAttribute('alt') || '').trim();
  if (alt === 'リスティングOK') return 'unknown'; // 3状態を区別できないので確定させない
  return 'ng';
}

ポイントは、altで確定できなかったときに ok へ倒さない ことです。unknown を返して後段で弾く設計にしないと、今回と同じ事故が再発します。

鵜呑みにしない: 保存HTML全件との突き合わせ

srcの命名規則も、いつ差し替えられるか分かりません。そこで採用前に、保存したHTML全件を使って検証しました。

// verify.mjs — 保存済みHTMLと突き合わせる検証スクリプト
import fs from 'node:fs';
import * as cheerio from 'cheerio';

const files = fs.readdirSync('./snapshots').filter((f) => f.endsWith('.html'));

let checked = 0;
const mismatches = [];

for (const file of files) {
  const $ = cheerio.load(fs.readFileSync(`./snapshots/${file}`, 'utf8'));

  $('.casino-list__item').each((_, el) => {
    const name = $(el).find('.casino-list__name').text().trim();
    const src = $(el).find('.status-icon').attr('src') || '';
    const base = src.split('/').pop().replace(/\.[a-z0-9]+$/i, '');

    // 本文中の「出稿NG」表記を正解データとして使う
    const bodyText = $(el).text();
    const expected = bodyText.includes('出稿NG') ? 'ng' : null;

    if (expected) {
      checked++;
      if (base !== 'icon06_n') {
        mismatches.push({ file, name, src, expected });
      }
    }
  });
}

console.log(`検証件数: ${checked} / 不一致: ${mismatches.length}`);
console.table(mismatches);

結果は 不一致0件。これで初めて「srcの命名規則で判定する」を採用しました。逆に言えば、この突き合わせをやっていなければ、icon06 が icon06_n にリネームされた日に静かに壊れていたはずです。

ハマりポイント

  • srcset と遅延読み込み: 高DPI用に srcset が併記され、src にはプレースホルダーが入っていることがあります。src → data-src → srcset の順でフォールバックを用意しましょう。
  • クエリ文字列: icon06.png?v=2 のようなキャッシュバスターが付きます。split(/[?#]/)[0] を忘れると辞書引きが全滅します。
  • 相対URLの多様さ: /img/icon06.png、../img/icon06.png など。ファイル名だけ取り出せば吸収できます。
  • 大文字小文字と連番の飛び: ICON06.PNG のような表記ゆれに備えて toLowerCase()。連番は icon06 が欠番になる可能性もあるため、辞書にない値は unknown に落とすのが安全です。
  • 拡張機能側の制約: Manifest V3ではリモートコードの実行(eval や new Function)がCSPで禁止されます。DOMParser はブラウザAPIなので問題ありませんが、パース処理を外部スクリプトから動的注入する設計は避けましょう。
  • アイコン差し替えリスク: 画像ファイル名はデザイン変更で一番壊れやすい箇所です。検証スクリプトをCIに置き、スナップショットを定期取得して unknown の増加をアラートにしておくと安心です。

判定方法の比較

判定方法 3状態の区別 耐障害性 実装コスト 向いているケース
alt属性のみ ×(今回の事故) 低 低 状態が1種類しかない画面
srcのファイル名 ○ 中(リネームで壊れる) 低 命名規則が安定しているサイト
本文テキスト ○ 中〜高(文言変更で壊れる) 中 「出稿NG」等の明示表記がある場合
src + 本文の突き合わせ ○ 高 中 今回の採用案。検証も同時にできる

導入時の手順(チェックリスト)

  1. 対象ページのHTMLを数ページ分保存する(状態が異なる案件を含めること)
  2. img 要素を全件抽出し、alt × src の組み合わせを集計する
  3. 出現頻度表を作り、状態ごとにファイル名が分かれていないか確認する
  4. 本文中の明示表記(「出稿NG」など)を正解データとして突き合わせる
  5. normalizeState() のユニットテストを書く(icon06 / icon07 / icon06_n / 未知の値)
  6. スナップショット取得を定期実行し、unknown の件数を監視する

FAQ

Q. altとsrc、判定の初手はどちらにしますか?
A. srcを先に見ます。altは人間向けラベルで状態と1対1になる保証がなく、実際に3状態が同じaltでした。srcで確定できない場合のみaltにフォールバックし、それでも曖昧なら unknown にして弾くのが安全です。

Q. unknown が増えたらどうする?
A. 画像のリネームかDOM構造の変更を疑い、保存HTMLで差分を確認します。判定ロジックを直すのではなく、まず原因を特定する順序を守ると二次被害を防げます。

Q. 画像認識やOCRで判定するのはあり?
A. 最終手段としてはありですが、運用コストが高く、判定根拠の説明もしづらくなります。まずは命名規則・本文テキスト・class名といった構造化された情報を探すべきです。

Q. 同じaltに複数状態が割り当てられているのは仕様違反では?
A. 多くの場合違反ではありません。altは画像の代替テキストであり、色や形で区別できる人間には同一文言で十分だからです。スクレイピング側が「altは状態コード」という前提を置いていたのが誤りでした。

まとめ

  • alt属性は状態判定の主キーにしない。人間向けラベルと状態コードは別物
  • 同じaltで複数状態があるなら、srcのファイル名など別の構造化情報を探す
  • 判定を決める前に、保存HTML全件と本文表記を突き合わせてずれ0件を確認する
  • 判定不能時は握りつぶさず unknown を返し、監視でリネームに気づけるようにする

似た画面を触るときは、altで判定する前にsrcの命名規則を先に見る。これが今回いちばん効いた教訓でした。


この記事を書いた人

BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。

GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。

👉 BENTEN Web Works — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中

0
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
0
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?