「青空文庫」をWordVBAで攻略する(2)

ルビを振るべき親文字の箇所を取得する

前回

akashi-keirin.hatenablog.com

紹介したのは、「青空文庫」からダウンロードしたテキストファイルから、ルビ情報(「《 》」で括られた文字列)を削除する、という対応だった。

しかし、いくら読みやすさのためといえども、ルビ情報全削除はあんまりである。

当然、次に目指すべきは、

ちゃんとルビを振ろうぜ

ということになる。

目次

こんなことをします

ルビの親文字に相当する部分をRangeオブジェクトとして取得するために必要なFunctionを作る。

通常であれば、これは人間にしか(完全には)なし得ない作業だが、幸い「青空文庫」では明確なルールに基づいてテキストデータを作ってくれているので、なんとかなる。

詳細は後述。

とにかく、たとえば、

f:id:akashi_keirin:20210131183928j:plain

この画像の中でいえば、

左の手で小次郎の鼻息《びそく》をそっと触れてみた。

の中から、

f:id:akashi_keirin:20210131183932j:plain

このように、親文字となるべき「鼻息」の部分をRangeオブジェクトとして取得するのである。

考え方

先述の通り、「青空文庫」では、次のようなルールでルビ情報をテキストデータ上で表現している。

  • 親文字の直後に「《 》」で括ってルビを示す
    【例】:小次郎の鼻息《びそく》→「鼻息」が親文字
  • ルビを振らない漢字と隣接しているときは、区切りの部分に「|」(全角バーティカルバー)が入っている
    【例】:柳生|石舟斎《せきしゅうさい》→「石舟斎」が親文字

実に明解。

つまり、「」の手前から遡り、非漢字にぶつかるか、「|」にぶつかるかしたら、その直後の位置までが親文字ということだ。

処理の手順は次の通り。すなわち、

  1. 《 》」で括られた部分のRangeオブジェクトを取得
  2. 取得したRangeオブジェクトを一旦開始方向に向けて潰し、その位置(*1)を取得する
  3. そこから1文字づつ遡って、漢字かどうか、または「|」かどうかを調べる
  4. 非漢字または「|」にぶつかったら、その直後の位置を取得する(*2)
  5. (*1)、(*2)で取得した位置を元にRangeオブジェクトを作成する

このような手順である。

ルビの親文字の箇所を取得する

指定した文字の位置を取得する

これは、前回も使用したgetNextPositionメソッドを使う。

コードを再掲する。

リスト1 標準モジュールFormatStrings
Private Function getNextPosition( _
             ByVal FindText As String) As Long
  Dim ret As Long
  ret = -1
  With Selection.Find
    .Text = FindText
    .Replacement.Text = ""
    .Wrap = wdFindStop
    .Format = False
    .Highlight = False
    .MatchCase = False
    .MatchFuzzy = False
    .MatchWholeWord = False
    .MatchByte = False
    .MatchAllWordForms = False
    .MatchSoundsLike = False
    .MatchWildcards = False
  End With
  Call Selection.Find.Execute
  If Not Selection.Find.Found Then GoTo ReturnValue
  ret = Selection.Range.Start
  Call Selection.Collapse(wdCollapseEnd)
ReturnValue:
  getNextPosition = ret
End Function

漢字かどうかを判定するFunction

これまた、ずいぶん前に書いたコードを再利用する。

スト2 標準モジュールFormatStrings
Private Function isKanji( _
             ByVal tgtChar As String) As Boolean
  isKanji = False
  Dim char As String * 1
  char = tgtChar
  If CInt(Asc(tgtChar)) > 0 Then Exit Function
  If CInt(Asc(char)) < CInt(&H889F) Then Exit Function
  isKanji = True
End Function

Privateメソッドで、1文字だけ渡して漢字かどうかを判定するだけの用途に使うので、ややこしい引数チェック等はなし。

親文字の箇所を取得するFunction

ここまでで準備はオッケー。後はコーディングあるのみ!

リスト3 標準モジュールFormatStrings
Private Function getRubiedCharPosition( _
             ByVal BasePos As Long, _
    Optional ByVal Delimiter As String = "|") As Long  '……(1)'
  Dim ret As Long  '……(2)'
  ret = 0
  Dim tgtDoc As Document
  Set tgtDoc = ActiveDocument
  Call tgtDoc.Range(BasePos, BasePos).Select  '……(3)'
  
  Dim tmp As String    '……(4)'
  Do
    tmp = tgtDoc.Range(BasePos - 1, BasePos).Text  '……(5)'
    If Not isKanji(tmp) Or tmp = Delimiter Then  '……(6)'
      ret = BasePos    '……(7)'
      Exit Do
    Else
      BasePos = BasePos - 1    '……(8)'
      If BasePos < 0 Then Exit Do
    End If
  Loop
  getRubiedCharPosition = ret
End Function

まず、(1)の

Private Function getRubiedCharPosition( _
             ByVal BasePos As Long, _
    Optional ByVal Delimiter As String = "|") As Long

で引数と返り値の設定。

引数BasePosは探索の開始位置。「青空文庫」でいえば、「」の直前の位置ということになる。

引数Delimiterは、親文字開始の目印となる文字。

青空文庫」の場合、「」という非漢字文字が区切りの役割を果たしているので、デフォルト値を「」にする意味はない。

とにかく、「」の直前の位置から遡り、非漢字文字にぶつかったらその直後の位置を整数値で返すので、返り値はLong型。

(2)の

Dim ret As Long
ret = 0

で返り値用変数と初期値を設定。

文書の先頭から親文字が始まる可能性があるので、初期値は0

先頭まで行っても非漢字文字にぶつからないということは、(「青空文庫」のボランティアスタッフがミスったのでない限り、)文書の先頭が親文字の開始位置だということだ。

探索を繰り返して、非漢字文字にぶつからないまま文書先頭に至ったときには「0」を返すようにするために、こうしておく。

(3)の

Call tgtDoc.Range(BasePos, BasePos).Select

で、探索開始位置にカーソルをセット。

(4)からの11行

Dim tmp As String
  Do
  tmp = tgtDoc.Range(BasePos - 1, BasePos).Text  '……(5)'
  If Not isKanji(tmp) Or tmp = Delimiter Then  '……(6)'
    ret = BasePos    '……(7)'
    Exit Do
  Else
    BasePos = BasePos - 1    '……(8)'
    If BasePos < 0 Then Exit Do
  End If
Loop

が探索過程。

まず、(5)の

tmp = tgtDoc.Range(BasePos - 1, BasePos).Text

で、先頭方向に1文字分の文字を取得。

(6)の

If Not isKanji(tmp) Or tmp = Delimiter Then

で、その文字が〝非漢字または区切り文字〟であるかどうかをを判定し、Trueならば、(7)の

ret = BasePos
Exit Do

で位置を返す。

非漢字文字にぶつかった時点で、BasePosの値は非漢字文字の直後、すなわち親文字の開始位置を表すので、これでよい。

(6)の判定結果がFalseだったら、(8)の

BasePos = BasePos - 1
If BasePos < 0 Then Exit Do

BasePosの値を1減らす。

また、1減らした段階でBasePosの値が負の数になっていたら、それ以上探索しても無駄なのでループを抜ける。(0が返ることになる。)

ルビの親文字の箇所を取得する

ここまでで準備はできた。

まさに、「時は来た、それだけだ!」状態である。

上掲getNextPositionで、「」の位置を取得すれば、それが〝ルビの親文字の箇所〟の終端となり、getRubiedCharPositionで、文書前方の直近の非漢字文字の直後の位置を取得すれば、それが〝ルビの親文字の箇所〟の始端となるのである!

つまり、たとえば、

f:id:akashi_keirin:20210131183935j:plain

このようにカーソルを置いて、次のコードを実行すれば、親文字の部分が選択されることになる。

リスト4 標準モジュール
Private Sub test00()
  Dim rng As Range
  Dim startPos As Long
  Dim endPos As Long
  endPos = getNextPosition("《")
  startPos = getRubiedCharPosition(endPos)
  Set rng = ActiveDocument.Range(startPos, endPos)
  Call rng.Select
End Sub

f:id:akashi_keirin:20210131183938j:plain

ほれ、この通り。

f:id:akashi_keirin:20210131183941j:plain

この状態で実行したら、

f:id:akashi_keirin:20210131183944j:plain

当然こうなる。

おわりに

あとは、親文字にルビを振り、「《 》」で括られた部分を削除するだけ。

ここまで来たら、あとは楽勝でしょう。