Python入門講座 [番外編]: AozoraReader¶

概要¶

青空文庫は著作権が消滅した作品や著者が許諾した作品を、電子書籍で公開し無料で提供しています。 この青空文庫に所蔵されている作品をMacのSpeechSynthesis機能を使って、読み上げてみようというのが、 このプログラムです。

音声で読み上げる際に複数の読みのある漢字を、SpeechSynthesis機能が読み上げてくれるかどうかがちょっと問題になります。 青空文庫では、(全ての作品ではありませんが)ルビ付きで公開されていますので、このルビを展開したテキストをSpeechSynthesisに渡すことで、この問題を回避しようというのが、このプログラムです。

青空文庫で公開されている作品は、テキスト版 および html版 が公開(作品によってはepub版も)が提供されています。 ルビのある作品では、いずれの形式でもルビ情報が追加されています。 ルビを使って読み上げ用のテキストを作成するには、 ルビのつく漢字をルビのテキストで置き換えることが必要です。 

青空文庫にあるルビ付きの作品を読み上げるためには、

  1. 青空文庫の作品群からweb経由でファイルの中身をダウンロードする。
  2. ダウンロードしたテキストから、ルビのついた文字をルビで置き換える。
  3. ルビ置き換えが完了したテキストを読み上げる

という三つのステップに分けることができます。それぞれのPythonプログラムを見ていきましょう。

ファイルのダウンロード¶

青空文庫から、読みたい作品の内容をPythonプログラムに読み込む方法を説明します。 まずは、青空文庫の総合インデックス から読みたい作品のURLを入手します。

「吾輩はねこである」であれば、https://www.aozora.gr.jp/cards/000148/files/789_14547.html 「銀河鉄道の夜」であれば、https://www.aozora.gr.jp/cards/001234/files/46340_24939.html

です。青空文庫の総合インデックスから読みたい作品の図書カードページに移動します。図書カードページの「ファイルのダウンロード」セクションの一覧表の「ファイル名(リンク)」の欄で必要なURLを入手します。マウスの右ボタンなどで表示される「リンクのアドレスをコピー」を使って、URLをクリップボードにコピーします。

このurlを使い、pythonプログラムの中でファイルをダウンロードするために, python3の標準モジュールの一つurllib.requestを利用します。urllib.request.urlopen を使って、ファイルの中身をpythonの変数 text に 書き込みます。

In [1]:
from urllib.request import urlopen
url="https://www.aozora.gr.jp/cards/000148/files/789_14547.html"
text=urlopen(url).read().decode('shift-jis')

ルビの展開¶

青空文庫では 通常テキスト版とHTML版が提供されています。今から見る様に、HTMLでは、簡単にルビの ついた文字をルビの中身で置き換える(ここではこの操作をルビの展開と呼びましょう)ことができます。 テキスト版でも同様のことは可能ですが、ここで説明する方法よりもう少し手の込んだプログラムが必要ですので、 この文書ではルビの展開はHTML版だけを対象とします。

HTML版では <ruby> タグを使ってルビを実現しています。 <ruby> タグは、

<ruby> 
  漢 <rp>(</rp><rt>かん</rt><rp>)</rp> 字 <rp>(</rp><rt>じ</rt><rp>)</rp>
</ruby>

や 

<ruby> 漢字 <rp>(</rp><rt>かんじ</rt><rp>)</rp>

というようにルビをつける文字と<rt> タグで指定されるルビとなるテキストから構成されています。

BeautifulSoup の利用法¶

HTML文書処理のためのPythonモジュール BeautifulSoupを使うことで、これを簡単に実現できます。

DOMツリーを表現する BeautifulSoup オブジェクトをまず作成します。 青空文庫のファイルは標準の文字コードとして、shift-jisを採用していますが、BeautifulSoupeはこれをユニコードに変換した上で、 HTMLの解析を行い、DOMツリーを作成します。

HTML文書を解析するモジュールとして、html.parser,lxml, html5libの三つが利用できます。html.parserはpython組み込みの構文解析モジュール(パーサー)です。lxml および html5lib はpythonとは別にインストールが必要ですが、lxmlは高速、html5lib はHTML文法に厳格な解析という特徴があり、目的に応じた使い分けができます。

In [2]:
from bs4 import BeautifulSoup
soup=BeautifulSoup(urlopen(url), features="html.parser") # features は lxmlも可能。 lxmlの方が少し実行時間は短い。
In [3]:
%%timeit 
soup=BeautifulSoup(text, features="html.parser") # features は lxmlも可能。
377 ms ± 2.77 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [4]:
%%timeit 
soup=BeautifulSoup(text, features="lxml") # features は lxmlも可能。 lxmlの方が少し実行時間は短い。
<magic-timeit>:1: XMLParsedAsHTMLWarning: It looks like you're parsing an XML document using an HTML parser. If this really is an HTML document (maybe it's XHTML?), you can ignore or filter this warning. If it's XML, you should know that using an XML parser will be more reliable. To parse this document as XML, make sure you have the lxml package installed, and pass the keyword argument `features="xml"` into the BeautifulSoup constructor.
201 ms ± 3.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [5]:
%%timeit 
soup=BeautifulSoup(text, features="html5lib") # features は lxmlも可能。 lxmlの方が少し実行時間は短い。
<magic-timeit>:1: XMLParsedAsHTMLWarning: It looks like you're parsing an XML document using an HTML parser. If this really is an HTML document (maybe it's XHTML?), you can ignore or filter this warning. If it's XML, you should know that using an XML parser will be more reliable. To parse this document as XML, make sure you have the lxml package installed, and pass the keyword argument `features="xml"` into the BeautifulSoup constructor.
1.47 s ± 9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

BeautifulSoup オブジェクトでは .find あるいは .find_all メソッドを使い、タグの種類やアトリビュートに基づいて、サブツリーあるいはそのリストを取り出せます。 青空文庫のHTML文書ではメインのテキストは<div class="main_text"> タグでマークアップされていますので、これを取り出します。 文書の題名(title)および著者(author)も同様に取り出します。 取り出したツリーのテキストの最初の98文字を確認のため印刷してみます。

In [6]:
main_text=soup.find('div', attrs={"class":"main_text"})
authors=soup.find_all(attrs={"class":"author"})
title  =soup.find(attrs={"class":"title"})
print(title.text,  [author.text for author in authors])
吾輩は猫である ['夏目漱石']

読み込んだHTML文書の元のエンコーディングを確認して見ます。

In [7]:
soup.original_encoding
Out[7]:
'shift_jis'

BeautifulSoupでタグ名"tag"を持つ最初の要素は.findメソッドを使わずとも、 BeautifulSoupオブジェクトに".tag"アトリビュートをつけることで取り出すことができます。

In [8]:
soup.find('ruby') == soup.ruby
Out[8]:
True
In [9]:
print(soup.ruby)
<ruby><rb>吾輩</rb><rp>(</rp><rt>わがはい</rt><rp>)</rp></ruby>

rubyの中には複数のrtタグが存在する可能性がありますので、全ての rtタグを .find_all を使って取り出します。

In [10]:
soup.ruby.find_all('rt')
Out[10]:
[<rt>わがはい</rt>]

ruby要素の中にある全てのrt要素の内容をつなげた文字列を作ります。

In [11]:
" ".join((rt.text for rt in soup.ruby.find_all('rt')))
Out[11]:
'わがはい'

全てのrubyタグについて、その内容を 含まれるrtタグの文字列を連結してできる文字列のオブジェクト(soup.new_string) で置き換え(.replaceWith)ます。

In [12]:
while main_text.ruby:
        rt=" ".join((rt.text for rt in main_text.ruby.find_all('rt')))
        main_text.ruby.replace_with(soup.new_string(rt));

補足説明¶

このコードはちょっと説明が必要かもしれません。先ほど述べた様に、main_text.rubyは DOMツリーmain_textの最初のruby要素を指しています。ループの中でこの要素はruby要素から 文字列オブジェクトに置き換えられます。 これによって、次のループの実行時には、main_text.ruby は次のruby要素を指していることになります。 この様にして、main_text中の全てのruby要素がルビのテキストを展開した文字列オブジェクトに置き換えられ、main_text.rubyがNoneとなるまでループが繰り返されます。

for ruby in main_text.find_all('ruby'):
  rt=" ".join((rt.text for rt in ruby.find_all('rt')))
  ruby.replace_with(soup.new_string(rt))

としても同じです。

テキストを読み上げる¶

テキストの読み上げには macspeechX を使います。 LinuxやWindowsではOpenJTalk, Windowsでも組み込みの音声合成ソフトウェアを利用可能です。

macspeechX¶

macspeechXはPyPIで公開されている拙作のモジュールです。macosxが提供するTTSライブラリ(SpeechSynthesis)を利用しています。 SpeakString 関数をつかい、システムの既定の声を使ったテキストの読み上げを行います。

In [13]:
from macspeechX import SpeakString
SpeakString("こんにちは。'mac-speech-X' にようこそ。")
Out[13]:
<macspeechX.macspeechX_AVF.SpeechChannel at 0x10ad62750>

SpeakString 関数は与えられた文字列を全て読み上げ様のデータ形式に変換したのち、読み上げを開始します。 main_textを行ごとに分けて、SpeakStringに渡すことで、最初の読み上げまでの時間を短縮します。

In [14]:
lines=main_text.text.split()
for t in lines[:2]:
      SpeakString(t)

まとめ¶

以上の役割を一つの関数にまとめました。 test(<青空文庫内の作品のHTMファイルへのURL>) を実行すると作品の読み上げを開始します。

In [15]:
#!python3
# -*- coding:utf-8 -*-
"""
青空文庫のルビ付き作品を、macspeechXを使って、読み上げる。
"""
import macspeechX
from macspeechX import SpeakString
from bs4 import BeautifulSoup
from urllib.request import urlopen
from io import BytesIO

farady_no_den_url="https://www.aozora.gr.jp/cards/001234/files/46340_24939.html"
wagahai_url="https://www.aozora.gr.jp/cards/000148/files/789_14547.html"
ginga_tetsudo_url="https://www.aozora.gr.jp/cards/000081/files/456_15050.html"

def test(url=ginga_tetsudo_url, voice=0):
    v=macspeechX.Voice(voice)
    soup=BeautifulSoup(urlopen(url), features="html.parser")
    main_text=soup.find("div", attrs={"class":"main_text"})
    authors=soup.find_all(attrs={"class":"author"})
    title  =soup.find(attrs={"class":"title"})
    # ルビー タグを展開して、テキストに埋め込む。
    while main_text.ruby:
        rt=" ".join((rt.text for rt in main_text.ruby.find_all('rt')))
        main_text.ruby.replace_with(soup.new_string(rt));
    lines=main_text.text.split()
    #
    SpeakString(title.text,v)
    for author in authors:
        SpeakString(author.text,v)
    for t in lines:
        SpeakString(t,v)
In [16]:
test("https://www.aozora.gr.jp/cards/000148/files/2672_6499.html",voice="Otoya(Enhanced)")
In [ ]: