MastodonのトゥートをAPIで取得したあとの処理用の簡易HTMLパーサーを書いてみた。


・目標

 APIでMastodonのトゥートを取得すると、トゥートの本文は

<p>APIのてすと3</p>
<p>ヌベヂョンヌゾジョンベルミッティスモゲロンボョwイヒーーイヒヒヒヒヒヒヒヒ</p>
<p>
    <a href="https://mell0w-5phere.net/jaded5phere" rel="nofollow noopener" target="_blank">
        <span class="invisible">https://</span>
        <span class="">mell0w-5phere.net/jaded5phere</span>
        <span class="invisible"></span>
    </a>
</p>

のようにHTMLになっている。Webで表示する分にはこのままでいいのだが、アプリを作りたいときは邪魔でしかないので、この中身を取り出せるパーサーを書こうということである。

・仕様

 今回出てくるタグは、<p>,<span>,<a>,<br />の4つのみである。処理が面倒な「閉じタグが不要なタグ」が今回は<br />しか出てこないので、こいつだけ例外的に名指しで置換する。
 残りは次のように処理していく。タグは入れ子になるので、再帰を使う。

  • ①開始タグを探す。見つからなければ、文字列全体を配列に追加してその配列を返す。
  • ②開始タグより前の文字列を配列に追加する。
  • ③終了タグを探す。
  • ④タグで囲まれた中身について処理を再帰的に実行する。返ってきた結果を、タグ名と属性(attribute)と一緒に配列に追加する。
  • ⑤終了タグより後の文字列について処理を繰り返し実行する。

わかりにくいのでソースコードを載せる。

def attrToDic(text):
    if text=="":return None
    attrs={}
    matches=re.findall(r'(\w+?)="(.*?)"',text)
    for m in matches:
        name=m[0]
        value=m[1]
        attrs[name]=value
    return attrs

def parse(text):
    elem=[] # 中身を格納する配列
    while True:
        # 1.開始タグの検索
        m=re.search(r"<(.+?)>",text) 
        if not m:break

        # 2.開始タグより前の文字列を格納
        bef=text[:m.start()]
        elem.append(bef) 

        taginfo=m.group(1)
        if taginfo=="br /": # brだけ改行文字で置換
            elem.append("\n")
            text=text[m.end():]
            continue

        # タグ名と属性を取得
        mi=re.match(r'^(\w+?)((?: .+?=\".*?\")*)$',taginfo) 
        if not mi: raise Exception
        tagname=mi.group(1)
        attr=attrToDic(mi.group(2))

        # 3.終了タグを検索
        me=re.search(r"</%s>"%tagname,text)
        if not me: raise Exception

        # 4.再帰的実行
        inner_elem=parse(text[m.end():me.start()])
        inner={
            "tagname":tagname,
            "content":inner_elem,
            "attr":attr
        }
        elem.append(inner)

        # 5.終了タグより後だけ残して繰り返し
        text=text[me.end():]

    # 1.開始タグが見つからなければ配列を返して終了
    elem.append(text) 
    return elem

・実践

 先頭で掲げたHTMLをこれで処理すると、結果はこうなる。

[
  {
    "tagname": "p", 
    "content": ["APIのてすと3"]
  }, 
  {
    "tagname": "p", 
    "content": ["ヌベヂョンヌゾジョンベルミッティスモゲロンボョwイヒーーイヒヒヒヒヒヒヒヒ"]
  }, 
  {
    "tagname": "p", 
    "content": [
      {
        "tagname": "a", 
        "content": [
          {
            "tagname": "span", 
            "content": ["https://"], 
            "attr": {"class": "invisible"}
          }, 
          {
            "tagname": "span", 
            "content": ["mell0w-5phere.net/jaded5phere"], 
            "attr": {"class": ""}
          }, 
          {
            "tagname": "span", 
            "content": [], 
            "attr": {"class": "invisible"}
          }
        ], 
        "attr": {
          "href": "https://mell0w-5phere.net/jaded5phere", 
          "rel": "nofollow noopener", 
          "target": "_blank"
        }
      }
    ]
  }
]

ちゃんとパース出来ていると思う。


 以上。閉じタグって大事だなと思った(こなみかん)。

【Python3】簡易HTMLパーサー【Mastodon API用】

投稿ナビゲーション


コメントを残す

メールアドレスが公開されることはありません。