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?

公開フィードの画像と代替テキストを検査する方法:Node.jsとparse5の最小例

0
Posted at

この記事はPogeeの編集企画として、生成AIを用いて作成しています。HTML、画像のパス、説明文は独立した架空の教材です。Pogeeの実画面、内部実装、会話ログは使っていません。掲載コードはローカルで実行確認しています。

画像が並ぶフィードでは、見た目だけでなく、画像が伝える内容をテキストでも受け取れるかを確認したい場面があります。最初の整理として、HTMLにあるimg要素を列挙し、alt属性が「ない」「空または空白だけ」「文字がある」のどれかを分けてみます。

これはアクセシビリティ適合を判定するツールではありません。装飾画像なら空のaltが適切な場合があり、文字があっても画像の意味を伝えているとは限りません。機械は確認候補を抽出し、画像の役割と文脈は人が確かめる、という役割分担にします。

HTMLを正規表現ではなくパーサーで読む

今回はNode.jsとHTMLパーサーのparse5を使います。属性名の大小文字、改行、文字参照、コメントやscript内の文字列をHTMLとして扱うためです。ブラウザーは起動せず、自分で用意したローカルHTMLファイルだけを読みます。外部サイトへのアクセスや画像の取得は行いません。

自作HTMLの画像をparse5で調べ、altの欠落・空・非空を人の確認へ渡す流れ

図1:自作の架空HTMLをparse5で読み、altの欠落・空文字・非空文字を分けて人の確認へ渡す流れ。生成AIの支援で作成した説明図で、Pogeeの製品画面や検査結果ではありません。

環境と、自作のfeed.html

実行環境はNode.js v25.2.1、parse5 8.0.0です。空の作業ディレクトリで次を実行します。--save-exactで直接依存を固定し、生成されたpackage-lock.jsonも保存します。同じ依存解決を再現するときは、そのファイルとpackage.jsonを使ってnpm ciを実行します。

npm init -y
npm install --save-exact --ignore-scripts parse5@8.0.0

次をfeed.htmlとして置きます。station.pngなどは属性検査用の架空パスです。画像ファイルの配布や読み込みはなく、altの内容が実画像と合うかをテストした結果でもありません。lineはこのファイル内でimgの開始タグが始まる行です。

<!doctype html>
<html lang="ja">
<meta charset="utf-8">
<title>架空の公開フィード</title>
<main>
  <article>
    <h1>雨の日の駅</h1>
    <img src="station.png" alt="雨の駅で赤い傘を持つ人物">
    <img src="separator.png" alt="">
    <img src="note.png">
    <img srcset="scene-small.png 480w, scene-large.png 960w" alt=" ">
  </article>
</main>
</html>

1枚目は非空の説明、2枚目は装飾用を想定した空文字、3枚目はalt欠落、4枚目は空白だけです。空文字と空白だけは同じ確認グループにまとめますが、元の値は出力に残します。4枚目のsrcsetも文字列として記録するだけで、候補URLの構文や選択結果は検証しません。

check-image-alt.mjs:確認候補をJSON Linesで出す

次を同じディレクトリのcheck-image-alt.mjsに置きます。.mjsなのでES Modulesとして実行できます。parseのsourceCodeLocationInfoを有効にし、HTML名前空間のimgだけを処理します。

import { readFile } from "node:fs/promises";
import { parse } from "parse5";

function* elements(node) {
  if (node.tagName) yield node;
  for (const child of node.childNodes ?? []) yield* elements(child);
}

try {
  if (process.argv.length !== 3) {
    throw new Error("usage: node check-image-alt.mjs feed.html");
  }
  const html = await readFile(process.argv[2], "utf8");
  const document = parse(html, { sourceCodeLocationInfo: true });
  const rows = [];
  for (const node of elements(document)) {
    if (node.tagName !== "img" ||
        node.namespaceURI !== "http://www.w3.org/1999/xhtml") continue;
    const attrs = new Map(node.attrs.map(({ name, value }) => [name, value]));
    const alt = attrs.get("alt");
    const status = alt === undefined ? "MISSING_ALT" :
      alt.trim() === "" ? "EMPTY_ALT_REVIEW" : "TEXT_ALT_REVIEW";
    rows.push({
      image: rows.length + 1,
      line: node.sourceCodeLocation?.startLine ?? null,
      status,
      alt: alt ?? null,
      src: attrs.get("src") ?? null,
      srcset: attrs.get("srcset") ?? null
    });
  }
  const missing = rows.filter(row => row.status === "MISSING_ALT").length;
  const empty = rows.filter(row => row.status === "EMPTY_ALT_REVIEW").length;
  const text = rows.filter(row => row.status === "TEXT_ALT_REVIEW").length;
  console.log(JSON.stringify({ images: rows.length, missing, empty, text }));
  for (const row of rows) console.log(JSON.stringify(row));
  process.exitCode = missing > 0 ? 1 : 0;
} catch (error) {
  console.error(JSON.stringify({ error: error.message }));
  process.exitCode = 2;
}

出力は1行目が集計、その後が画像ごとのJSONです。JSON.stringifyを使うので、属性値に改行やタブがあっても1レコードの区切りを壊しません。altが存在しなければnull、空文字なら""を残すため、欠落と空を区別できます。srcとsrcsetは原文の属性値で、絶対URLへの解決も画像の到達確認もしていません。

終了コードは、この教材の確認ルールです。MISSING_ALTが1件以上なら1、なければ0、引数不足やファイル読み込みの失敗なら2です。1は確認が必要な候補を見つけたこと、0は欠落が見つからなかったことだけを意味します。HTML規格やWCAG全体の合否には置き換えられません。

node --version
node check-image-alt.mjs feed.html
v25.2.1
{"images":4,"missing":1,"empty":2,"text":1}
{"image":1,"line":8,"status":"TEXT_ALT_REVIEW","alt":"雨の駅で赤い傘を持つ人物","src":"station.png","srcset":null}
{"image":2,"line":9,"status":"EMPTY_ALT_REVIEW","alt":"","src":"separator.png","srcset":null}
{"image":3,"line":10,"status":"MISSING_ALT","alt":null,"src":"note.png","srcset":null}
{"image":4,"line":11,"status":"EMPTY_ALT_REVIEW","alt":" ","src":null,"srcset":"scene-small.png 480w, scene-large.png 960w"}

この実行の終了コードは1です。4枚のうち、alt欠落が1、空または空白だけが2、非空文字が1になりました。line 10の画像について、画像が持つ情報やリンク先などを確認してから修正します。欠落を消すために機械的にalt=""を足す方法では、必要な情報まで消しかねません。

15入力の実行確認と、結果の読み方

上の4枚を含む入力に加え、次の独立した合成入力を実際に実行しました。表の「件数」は実行した入力ファイルまたは引数条件の数です。公開サイトの検査件数、Pogeeの画像件数、ユーザー評価ではありません。

入力の種類 件数 確認した結果
本文の4枚のHTML 1 4枚、欠落1・空2・非空1、終了1
非空、空、空白、欠落 4 各1枚を区別。欠落だけ終了1
大文字の属性名と文字参照 1 ALTを読み、&amp;を&に復号
コメント・script・template 3 文字列や未展開template内のimgは数えない
srcsetのみ、改行した開始タグ 2 src=nullを保持。開始行も確認
画像のないHTML、空ファイル 2 images=0、終了0。検査対象がないことを表示
存在しないファイル、引数なし 2 標準エラーにJSON、終了2

たとえば画像のないHTMLでは、次の1行だけが出ました。終了コードは0ですが、「画像が適切」という結果ではありません。JavaScriptで画像を追加するページの取得元HTMLなど、そもそも期待するimgが入っていない可能性を先に確認します。

{"images":0,"missing":0,"empty":0,"text":0}

parse5はHTMLを解析するだけでJavaScriptを実行しません。この走査はchildNodesだけをたどるため、template.contentの未展開画像も対象外です。コメントやscript文字列中の疑似タグを画像と誤認しないことは、別入力で確認しています。HTMLが不正でもパーサーが補正する場合があるので、HTMLバリデーターとしても使いません。

人が確かめるのは、画像の役割とその場の意味

W3C WAIのalt決定木では、画像が文脈に意味を加えるか、装飾か、近くのテキストと重複するかなどによって書き方が変わります。情報を持つ写真なら、その場で必要な意味を短く伝えます。リンクや操作の一部なら、対象や機能も含めて考えます。図表の情報量が多ければ、短いaltだけに詰め込まず本文側の説明も検討します。

EMPTY_ALT_REVIEWは「空だから失敗」ではありません。装飾として空が適切か、情報を担う画像なのに説明が落ちていないかを確認するための印です。TEXT_ALT_REVIEWも「説明済みで成功」ではなく、ファイル名だけの記述、近くの文との重複、画像との食い違いがないかを見ます。

このコードはCSSの背景画像、SVGのimage要素、iframe内の別文書、シャドウDOM、JavaScript生成後のDOM、実際の画像表示、スクリーンリーダーの読み上げを確認していません。aria-labelやroleなどを含めたアクセシブルネームの計算も対象外です。権限のある実ページへ進むときは、表示後の状態や操作を別の工程で確認します。

公開フィードだからといって、第三者のページを一括取得する必要はありません。まず自作のテンプレートや許可されたHTMLから候補を抽出し、レビューで画像と本文を一緒に確かめる使い方が出発点になります。本稿の結果は、この架空教材と実行環境に限ります。

参考資料

以下の公式公開資料を2026年10月18日に確認しました。parse5のAPIはHTML文字列の解析、MDNとWAIはaltの用途と文脈の判断を参照しています。

parse5:parse()

MDN:<img> element

W3C WAI:An alt Decision Tree

制作元Pogeeの公開解説(本稿のHTML・検査対象とは別です)

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?