日本語の改行位置を自動的に認識してくれるBudouXというライブラリがリリースされたのを知りました。

Shuhei IitsukaさんはTwitterを使っています 「日本語改行問題を解消するための新しいライブラリ、BudouX をリリースしました。 JavaScript / Python で動きます。機械学習モデルを含めても小さい(15KB くらい)ので、クライアントサイドでも動きます。荒削りなところもありますが、使ってみていただけると嬉しいです。 https://t.co/3xsKESmQfn」 / Twitter

このBlogのMarkdown Parser として使用しているPython-Markdown に処理が組み込みができると記事が読みやすくなりそうなので、実施できるか試してみました。 実施した内容を記載します。

ライブラリのインストール

Python-Markdownと、BudouXをpipでインストールします。

!pip install markdown
!pip install budoux

BudouX の動作確認

インストール後、まずBudouXの動作を確認します。

import budoux
parser = budoux.load_default_japanese_parser()
print(parser.parse('今日は晴れています。'))
print(parser.translate_html_string('今日は晴れています。'))

['今日は', '晴れています。']
<span style="word-break: keep-all; overflow-wrap: break-word;">今日は<wbr>晴れています。。</span>

テキストに対して形態素解析だけ実施する。parseいうメソッドと、テキストに対して、形態素解析し、htmlとして修飾するtranslate_html_stringいうメソッドが提供されています。どちらも問題なく動作しました。

Python-Markdown の拡張ポイントについて

Python-Markdown には、extention という拡張機能を登録できます。
これは拡張記法を登録する機能で、独自記法の登録ができます。
今回実施したいのは、通常の文章に対してのBudouXの処理を差し込みとなるため、extentionは使用できなさそうでした。

Python-Markdownには、処理フェーズごとにprocessorの登録ができBudouXの処理はこのprocessorで実施するとうまくいきそうです。
処理フェーズごとにprocessorは以下のように分かれています。

  • 前処理を行う preprocessor。
  • ブロックレベルの処理を行い、ElementTreeにする blockprocessor。
  • ElementTreeに対して処理を行う treeprocessor。
  • treeprocessorから呼び出される inlineprocessor。
  • 後処理を行う postprocessor。

今回拡張が必要だったのでは、blockprocessor で、ヘッダーとパラグラフの文字列にBudoXの処理での編集を行えれば良さそうでした。
BudouXHashHeaderProcessor と、BudouXParagraphProcessor を作成、デフォルトのblockprocessor ではなく作成したprocessorに置き換えるようにしました。 作成したコードは以下の通りです。

import markdown
import re
import xml.etree.ElementTree as etree
from markdown.blockprocessors import  BlockProcessor, BlockParser,EmptyBlockProcessor,ListIndentProcessor,CodeBlockProcessor,HashHeaderProcessor,SetextHeaderProcessor,HRProcessor,OListProcessor,UListProcessor,BlockQuoteProcessor, ReferenceProcessor, ParagraphProcessor

# BudouXのインポート、parserの生成    
import budoux
parser = budoux.load_default_japanese_parser()

import logging
logger = logging.getLogger("TEST")

# BudouX を呼びだしを行うProcessorクラス その1 (これはヘッダーに対して処理を行う)
class BudouXHashHeaderProcessor(BlockProcessor):
    """ Process Hash Headers. """

    # Detect a header at start of any line in block
    RE = re.compile(r'(?:^|\n)(?P<level>#{1,6})(?P<header>(?:\\.|[^\\])*?)#*(?:\n|$)')

    def test(self, parent, block):
        return bool(self.RE.search(block))

    def run(self, parent, blocks):
        block = blocks.pop(0)
        m = self.RE.search(block)
        if m:
            before = block[:m.start()]  # All lines before header
            after = block[m.end():]     # All lines after header
            if before:
                # As the header was not the first line of the block and the
                # lines before the header must be parsed first,
                # recursively parse this lines as a block.
                self.parser.parseBlocks(parent, [before])
            # Create header using named groups from RE
            h = etree.SubElement(parent, 'h%d' % len(m.group('level')))
            h.text = m.group('header').strip()
            # BudouXでテキスト加工
            h.text = parser.translate_html_string(h.text)
            if after:
                # Insert remaining lines as first block for future parsing.
                blocks.insert(0, after)
        else:  # pragma: no cover
            # This should never happen, but just in case...
            logger.warn("We've got a problem header: %r" % block)


class BudouXParagraphProcessor(BlockProcessor):
    """ Process Paragraph blocks. """

    def test(self, parent, block):
        return True

    def run(self, parent, blocks):
        block = blocks.pop(0)
        if block.strip():
            # Not a blank block. Add to parent, otherwise throw it away.
            if self.parser.state.isstate('list'):
                # The parent is a tight-list.
                #
                # Check for any children. This will likely only happen in a
                # tight-list when a header isn't followed by a blank line.
                # For example:
                #
                #     * # Header
                #     Line 2 of list item - not part of header.
                sibling = self.lastChild(parent)
                if sibling is not None:
                    # Insetrt after sibling.
                    if sibling.tail:
                        # BudouXでテキスト加工 文字列が存在するので改行処理の前に実施
                        sibling.tail = parser.translate_html_string(sibling.tail)
                        sibling.tail = '{}\n{}'.format(sibling.tail, block)
                    else:
                        sibling.tail = '\n%s' % block
                        # BudouXでテキスト加工
                        sibling.tail = parser.translate_html_string(sibling.tail)
                else:
                    # Append to parent.text
                    if parent.text:
                        # BudouXでテキスト加工 文字列が存在するので改行処理の前に実施
                        parent.text = parser.translate_html_string(parent.text)
                        parent.text = '{}\n{}'.format(parent.text, block)

                    else:
                        parent.text = block.lstrip()
                        # BudouXでテキスト加工
                        parent.text = parser.translate_html_string(parent.text)

            else:
                # Create a regular paragraph
                p = etree.SubElement(parent, 'p')
                p.text = block.lstrip()
                # BudouXでテキスト加工
                p.text = parser.translate_html_string(p.text)


# blockprocessors の登録処理
def build_block_parser(md, **kwargs):
    """ Build the default block parser used by Markdown. """
    parser = BlockParser(md)
    parser.blockprocessors.register(EmptyBlockProcessor(parser), 'empty', 100)
    parser.blockprocessors.register(ListIndentProcessor(parser), 'indent', 90)
    parser.blockprocessors.register(CodeBlockProcessor(parser), 'code', 80)
    parser.blockprocessors.register(BudouXHashHeaderProcessor(parser), 'hashheader', 70)
    parser.blockprocessors.register(SetextHeaderProcessor(parser), 'setextheader', 60)
    parser.blockprocessors.register(HRProcessor(parser), 'hr', 50)
    parser.blockprocessors.register(OListProcessor(parser), 'olist', 40)
    parser.blockprocessors.register(UListProcessor(parser), 'ulist', 30)
    parser.blockprocessors.register(BlockQuoteProcessor(parser), 'quote', 20)
    parser.blockprocessors.register(ReferenceProcessor(parser), 'reference', 15)
    parser.blockprocessors.register(BudouXParagraphProcessor(parser), 'paragraph', 10)
    return parser

# Markdown > HTML変換
md = markdown.Markdown();
# デフォルトのparserを変更
md.parser = build_block_parser(md)
html = md.convert("""
------------

## 感想、思ったこと     

以下調べて思ったことになります。       

* New Relic、DataDog、 Mackerel が多い。個人的に観測していた肌感とは一致した。         
* Cloudwatch の印象が変わった。New Relic、DataDog等 と似たようなことはできそう。   
* Sentry系のツールはそれなりに利用実績がある。統合監視って訳ではないのだろうが、フロントエンド監視に特化していて、フロントエンド監視をしたいというユースケースが多いように思えた。   
* Prometheus、Grafanaの組み合わせは、オンプレミス環境で使われてそう。  
* Zabbixは枯れてはいるが、プラグインインストールでモダンなこともできそう。  

企業利用を軸で調べたことでツールへの先入観が消えました。         
機能の比較とともに、各社何故使っているのかを考えると、自社に適用していくときの判断材料になりそうに思います。      

---

## 参考     

以下、参考にした記事になります。    

* [Googleの「AMP優遇」がまもなく終了 - GIGAZINE](https://gigazine.net/news/20210519-google-amp-no-longer-preferential-treatment/)     

Django 1.x から Django 3.x へのアップデートになり、Mezzanine というか Django のプラグインでのエラーが大量に発生しました。      
なかなかエラーを解消できず、2-3時間サイト停止させてしまいました。。      
事前の検証はやっておいたほうが良いかなと思いました。     

""")

# 標準出力
print(html)

    <hr />
    <h2><span style="word-break: keep-all; overflow-wrap: break-word;">感想、<wbr>思った<wbr>こと</span></h2>
    <p><span style="word-break: keep-all; overflow-wrap: break-word;">以下調べて<wbr>思った<wbr>ことになります。<wbr>       </span></p>
    <ul>
    <li><span style="word-break: keep-all; overflow-wrap: break-word;">New Relic、<wbr>DataDog、<wbr> Mackerel が<wbr>多い。<wbr>個人的に<wbr>観測していた<wbr>肌感とは<wbr>一致した。<wbr>         </span></li>
    <li><span style="word-break: keep-all; overflow-wrap: break-word;">Cloudwatch の<wbr>印象が<wbr>変わった。<wbr>New Relic、<wbr>DataDog等 と<wbr>似たようなことは<wbr>できそう。<wbr>   </span></li>
    <li><span style="word-break: keep-all; overflow-wrap: break-word;">Sentry系の<wbr>ツールは<wbr>それなりに<wbr>利用実績が<wbr>ある。<wbr>統合監視って<wbr>訳ではないのだろうが、<wbr>フロントエンド監視に<wbr>特化していて、<wbr>フロントエンド監視を<wbr>したいと<wbr>いう<wbr>ユースケースが<wbr>多いように<wbr>思えた。<wbr>   </span></li>
    <li><span style="word-break: keep-all; overflow-wrap: break-word;">Prometheus、<wbr>Grafanaの<wbr>組み合わせは、<wbr>オンプレミス環境で<wbr>使われてそう。<wbr>  </span></li>
    <li><span style="word-break: keep-all; overflow-wrap: break-word;">Zabbixは<wbr>枯れては<wbr>いるが、<wbr>プラグインインストールで<wbr>モダンな<wbr>こともできそう。<wbr>  </span></li>
    </ul>
    <p><span style="word-break: keep-all; overflow-wrap: break-word;">企業利用を<wbr>軸で<wbr>調べた<wbr>ことで<wbr>ツールへの<wbr>先入観が<wbr>消えました。<wbr>       <br />
    機能の<wbr>比較とともに、<wbr>各社何故使っているのかを<wbr>考えると、<wbr>自社に<wbr>適用していく<wbr>ときの<wbr>判断材料に<wbr>なりそうに<wbr>思います。<wbr>      </span></p>
    <hr />
    <h2><span style="word-break: keep-all; overflow-wrap: break-word;">参考</span></h2>
    <p><span style="word-break: keep-all; overflow-wrap: break-word;">以下、<wbr>参考に<wbr>した<wbr>記事に<wbr>なります。<wbr>    </span></p>
    <ul>
    <li><span style="word-break: keep-all; overflow-wrap: break-word;"><a href="https://gigazine.net/news/20210519-google-amp-no-longer-preferential-treatment/">Googleの<wbr>「AMP優遇」が<wbr>まも<wbr>なく<wbr>終了 - GIGAZINE</a>     </span></li>
    </ul>
    <p><span style="word-break: keep-all; overflow-wrap: break-word;">Django 1.x から<wbr> Django 3.x への<wbr>アップデートに<wbr>なり、<wbr>Mezzanine と<wbr>いうか Django の<wbr>プラグインでの<wbr>エラーが<wbr>大量に<wbr>発生しました。<wbr>    <br />
    なかなか<wbr>エラーを<wbr>解消できず、<wbr>2-3時間<wbr>サイト停止させてしまいました。。<wbr>    <br />
    事前の<wbr>検証は<wbr>やっておいた<wbr>ほうが<wbr>良いかなと<wbr>思いました。<wbr>     </span></p>

期待通り処理が行えていそうです。
実施にBlogに処理を組み込んで、複数のMarkdownを読み込ませて確認をしてみようと思います。

参考

記事作成時に参考にした文書へのリンクです。

以上です。

コメント