HTML の <ruby> に思うこと

ニュースサイトから記事本文を抜き出して利用しようという作業をやってみて、<ruby> 要素がほかの要素に比べてかなり異質なものあることに気がつきました。

ニュースの本文は

...<p><span class="colorC"><ruby>気象庁<rt>きしょうちょう</rt></ruby></span>は「もっと<a href="javascript:void(0)" class="dicWin" id="id-0000"><ruby><span class="under">二酸化炭素</span><rt>にさんかたんそ</rt></ruby></a>を<ruby>出<rt>だ</rt></ruby>さないようにしなければなりません」と<ruby>話<rt>はな</rt></ruby>しています。</p>...

のようになっています。BeautifulSoup の .text で単純にタグを削除するだけでは

気象庁きしょうちょうは「もっと二酸化炭素にさんかたんそを出ださないようにしなければなりません」と話はなしています。

と、ルビが本文に混じってしまい、まともな文になりません。事前に rt 要素を削除しておく必要があります。

実際に行った作業は「HTML からタグを除去して本文を取り出す」なのですが、単純に行って得られるものは上記のとおりで、それを「本文」とは呼べないと思うのです。

マークアップ言語

はじめに、

気象庁は「もっと二酸化炭素を出さないようにしなければなりません」と話しています。

という“内容”そのものがあります。これに“意味”をつけるために、たとえば「気象庁」は固有名詞という意味をつけるためにこれを <span> で囲み、それからこの文ひとつでひとつの段落という意味をつけるためにこれを <p> で囲みます。これがマークアップであり、そのためのタグであるはずです。

次の段階で、これをどう表示するか、たとえば固有名詞は文字を赤色にし、段落はそのはじめに改行する、というものはさらに分離してスタイルシートで記述することになっています。

逆向きに考えてみます。ブラウザで表示されるもののソースから表現のスタイルを取り除き、“意味”づけのタグを取り除いたら、残るのは“内容”そのものでなければならないはずです。

ところが今回の例では上述のように、余計なものが残ったままになります。

ルビは“内容”か

私は「ふりがなは内容そのものではなく、それに付け加えているもの」と考えますので、「余計なもの」と書きました。たとえば、ここの言葉は重要と思って下線を付ける、固有名詞ははっきり分かるように文字の色を他と違える、というのと同じで、内容に対して何らか助けのために付け加えているものだと思います。ですから、そういったものを剥ぎ取ったときには残っていてはならないと考えます。

そう考えたときに、現在の仕様の <rt> はかなり異質です。下線や色付けがタグ内の属性 (attribute) とそれに紐付けられるスタイルで表現されるように、ふりがなもタグではなく属性で示されるのが適当だと思います。

仮に <span>ruby という属性が定義されているとして

...<p><span class="colorC" ruby="きしょうちょう">気象庁</span>は「...」と<span ruby="はな">話</span>しています。</p>...

と書くのがもっとも自然のように感じます。

既存の仕様である <a> を逆に考えてみましょう。<a> にとって属性 href は、それがなければほとんど意味がないくらい重要なものです。

もっと<a href="example.html>二酸化炭素</a>を出さないようにしなければなりません

だからと言ってもしそれがタグになっていて

もっと<a>二酸化炭素<href>example.html</href></a>を出さないようにしなければなりません

と書かれるものだとしたら、その違和感がわかろうというものです。

<rt> のあり方

前節に書いたように、ルビは属性で指定されるのが自然だと思います。ではどうすればいいのか。

<ruby>関連要素って駄目駄目」に、この問題の歴史 (なんと不幸な!) から対案まで解説されていました。この記事をぜひじっくり読んでいただきたい。

この記事の「脱法ルビ」が、当面の対応策です。仕様に反せず、仕様内でできる策です。現状ここまでできるのですから、ここに示されているスタイルをレンダリングエンジン側で装備し、属性名を data-ruby ではなく ruby とできるよう仕様のほうが改訂されたら、当分は現在の <ruby>, <rt> との共存期間があったとしても、もうほとんど解決ではないですか。

……と思ったら、このスタイル、すなわち display: ruby-text を実装しているのは Gecko (Firefox) だけで、Webkit (Safari) も Blink (Chrome) も未対応でした (2018年6月現在)。ひどい状況です。

ルビを実際に使う人やデザイナのほとんどは、ほんの少し首をかしげるくらいはあっても、仕様に従って使うだけで、その仕様がどうあるべきかまで問い返したりはしません。そもそも世界のうちでごく限られた言語だけでしか知られておらず、その中でも使う人はごく少数ですから、問題にする人も滅多にいなければ対応もぜんぜん進まないのでしょう。ほんとうに不幸です。

ニュースのスクレイピングでタイピング練習

ひらがな数文字を打ち返すだけのタイピング練習は案の定すぐに飽きてしまったので、何か別のネタを考えなくてはならなくなりました。飽きないためには膨大かまたは頻繁に更新される元データがあればいい、青空文庫かな、でも小学生に向いているものがどれほどあるかしらん、頻繁に更新されるといえばニュース、でもこれまた小学生向きではなさそう……と思ったら実にぴったりのものがありました。NHK NENS WEB EASY です。ひとつの記事で50字ほどの文が10ほど。意味もわかりやすくて量もちょうどいい。かなり手間をかけて作られているようです。

さて、これをなんとか持ってきてタイピング練習の材料にしようと思ったのですが、何しろ本業でも何でもないので情報を集めるところからスタートでした。今回やっていることは実は「スクレイピング」という程のこともないのですが、そのとっかかりということで、せっかくなのでここに記録しておきます。

環境の準備

NHK NENS WEB EASY のページの肝心な部分は JavaScript で生成されているようで、Python で単純に requests.get(url) とやっても、ブラウザで見ている HTML ソースとは別のものしか得ることができません。そこでまず、ブラウザが実際に表示するページを取得できるようにします。

Debian パッケージ chromium-driver をインストールします。

sudo apt-get install chromium-driver

これを Python から使うためにライブラリ Selenium をインストールします。

pip install selenium

取得した HTML から必要な箇所を切り出すのには BeautifulSoup4 を使います[1]

pip install beautifulsoup4

でインストールします。

ニュースサイトの構造

ニュースサイトによくあることですが、各記事の URL は数字の羅列のような名前で、先頭ページではそれが日々更新されます。

ブラウザのデベロッパーツールで NHK NENS WEB EASY の先頭ページ https://www3.nhk.or.jp/news/easy/ の構造を見てみます。

<div class="top-news-list__pickup news-list-item" id="js-news-pickup">
  ...
  <h1 class="news-list-item__title is-pickup">
  <a href="./k10011463631000/k10011463631000.html"><em class="title"><ruby>日本<rt>にっぽん</rt></ruby>の<ruby>二酸化炭素<rt>にさんかたんそ</rt></ruby>の<ruby>濃度<rt>のうど</rt></ruby>が<ruby>今<rt>いま</rt></ruby>まででいちばん<ruby>高<rt>たか</rt></ruby>くなる</em><time class="time">6月5日 11時30分</time></a>
  </h1>
</div>

最初に大きく取り上げられている記事は <div id="js-news-pickup"> で、その中の <h1> の中の <a> から記事個別ページの URL が得られます。

その記事個別ページを同様にブラウザのデベロッパーツールで見てみると、記事本文は <div id="#js-article-body"> にあることがわかります。これを切り出してくればいい訳です。

スクレイピング

参考にした (というか、ほぼそのままコピーさせてもらった) コードは「Python Webスクレイピング テクニック集」の「JavaScriptによる描画に対応する」です。

ソース中のコメント「ブラウザを起動する」の箇所で、ブラウザのパスを指定する必要がありました。Debian のパッケージを使っている場合

driver = webdriver.Chrome(executable_path='/usr/bin/chromedriver', chrome_options=options)

です。

切り出し

1回めの

        # ブラウザでアクセスする
        siteurl = "https://www3.nhk.or.jp/news/easy/"
        driver.get(siteurl)

        ...

        # BeautifulSoupで扱えるようにパースします
        soup = BeautifulSoup(html, "html.parser")

        # id で特定の要素を切り出す
        href = soup.select_one("#js-news-pickup h1 a").get('href')

で記事個別ニュースの URL が得られるので、2回めは

        driver.get(newsurl)
        html = driver.page_source.encode('utf-8')
        soup = BeautifulSoup(html, "html.parser")
        # ルビを削除
        for s in soup(['rt']):
            s.decompose()

        ...

        # ニュースの本文
        text = soup.select_one("#js-article-body").text

で、記事本文を切り出します。

ルビを削除

ニュースの本文は

...<p><span class="colorC"><ruby>気象庁<rt>きしょうちょう</rt></ruby></span>は「もっと<a href="javascript:void(0)" class="dicWin" id="id-0000"><ruby><span class="under">二酸化炭素</span><rt>にさんかたんそ</rt></ruby></a>を<ruby>出<rt>だ</rt></ruby>さないようにしなければなりません」と<ruby>話<rt>はな</rt></ruby>しています。</p>...

のようになっています。BeautifulSoup の .text で単純にタグを削除するだけでは

気象庁きしょうちょうは「もっと二酸化炭素にさんかたんそを出ださないようにしなければなりません」と話はなしています。

と、ルビが本文に混じってしまい、まともな文になりません。事前に rt 要素を削除しておく必要があります。

この作業をやってみて、rt がほかとは異質なタグ(要素)であることを実感しました。これについてはまた別の記事に書こうと思います。

1文ずつに分解

記事を「。」で区切り、リストにします。「。」自身も含めたいので split が使えません。NHK のニュース記事で全体の最後に「。」がないことはまさかないだろうと仮定して、

        lines = re.findall(".*?。", text)

とします。あとは Errnot がこれを1文ずつ表示するようにするだけです。これでこの bot を相手に XMPP のチャットでオウム返しにタイピングの練習をすることができるようになりました。

それにしても、いろいろ寄せ集めるだけでこれだけできるのですから、便利な世の中になったものだとつくづく思いました。

  1. Python にはじめからある html.parser でもある程度できます。また Selenium にも同様の機能があるようです。