Closed1

LangChainでMarkdownのパース

kun432kun432

LLamaIndexでMarkdownのパースは色々工夫していた。

https://zenn.dev/kun432/scraps/c7798221de529e

同じことをLangChainでもやりたいなーと思って、いくつかDocumentLoadersを試してみたけど、いい感じのものがないので、少し書いてみた。

まず、Wikipediaの記事をMarkdown化。

from pathlib import Path
import requests
import re

def replace_heading(match):
    level = len(match.group(1))
    return '#' * level + ' ' + match.group(2).strip()

# Wikipediaからのデータ読み込み
wiki_titles = ["オグリキャップ"]
for title in wiki_titles:
    response = requests.get(
        "https://ja.wikipedia.org/w/api.php",
        params={
            "action": "query",
            "format": "json",
            "titles": title,
            "prop": "extracts",
            # 'exintro': True,
            "explaintext": True,
        },
    ).json()
    page = next(iter(response["query"]["pages"].values()))
    wiki_text = f"# {title}\n\n## 概要\n\n"
    wiki_text += page["extract"]

    wiki_text = re.sub(r"(=+)([^=]+)\1", replace_heading, wiki_text)
    wiki_text = re.sub(r"\t+", "", wiki_text)
    wiki_text = re.sub(r"\n{3,}", "\n\n", wiki_text)
    data_path = Path("data")
    if not data_path.exists():
        Path.mkdir(data_path)

    # markdown(.md)ファイルとして出力
    with open(data_path / f"{title}.md", "w") as fp:
        fp.write(wiki_text)

で、自分の場合はMarkdownHeaderTextSplitterを使って、軽くセクション単位で分割して、メタデータを少しいじったり、不要なセクションを削除したり、セクション内のテキスト量が大きすぎる場合に一定サイズで分割したりってのを追加した。

!pip install -qU langchain-text-splitters
import glob
import os
from langchain_text_splitters import MarkdownHeaderTextSplitter
from langchain_core.documents import Document

def text_split(text, max_length=400):
    """

    """
    chunks = re.split(r'(?<=[。!?\n])', text)
    chunks = [s for s in chunks if s.strip()]
    temp_chunk = ""
    final_chunks = []

    for chunk in chunks:
        if len(temp_chunk + chunk) <= max_length:
            temp_chunk += chunk
        else:
            final_chunks.append(temp_chunk)
            temp_chunk = chunk

    if temp_chunk:
        final_chunks.append(temp_chunk)

    return final_chunks

sections_for_delete = ["競走成績", "外部リンク", "参考文献"]

headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
    ("####", "Header 4"),
    ("#####", "Header 5"),
    ("######", "Header 6"),
]

files = glob.glob('data/*.md')
splits = []

for file in files:
    with open(file) as f:
        md = f.read()

        markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
        docs_split = markdown_splitter.split_text(md)

        docs_for_delete = []
        for idx, d in enumerate(docs_split):
            metadatas = []
            header_keys = []

            d.metadata["source"] = file

            for m in d.metadata:
                if m.startswith("Header"):
                    metadatas.append(d.metadata[m])
                    header_keys.append(m)
                    # 削除対象のセクションを含むドキュメントを後で削除するためにそのインデックス登録しておく
                    if d.metadata[m] in sections_for_delete:
                        docs_for_delete.append(idx)

            # セクションの階層を結合、パンくずリストとしてセクション情報に追加
            if len(metadatas) > 0:
                d.metadata["section"] = metadata_str = " > ".join(metadatas)
                for k in header_keys:
                    if k.startswith("Header"):
                        del d.metadata[k]

        # 削除対象セクションの削除
        docs = [item for i, item in enumerate(docs_split) if i not in docs_for_delete]

        for d in docs:
            chunks = text_split(d.page_content, 500)
            if len(chunks) == 1:
                splits.append(d)
            else:
                for idx, chunk in enumerate(chunks, start=1):
                    metadata = d.metadata.copy()
                    metadata["section"] += f"({idx})"
                    splits.append(Document(page_content=chunk, metadata=metadata))

こういう感じになる。

for i in splits:
    print(i.metadata)
    print(i.page_content[:60] + "...")
    print("====")
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 概要'}
オグリキャップ(欧字名:Oguri Cap、1985年3月27日 - 2010年7月3日)は、日本の競走馬、種牡馬。
1...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > デビューまで > 誕生に至る経緯'}
オグリキャップの母・ホワイトナルビーは競走馬時代に馬主の小栗孝一が所有し、笠松競馬場の調教師鷲見昌勇が管理した。ホワイト...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > デビューまで > 誕生・生い立ち > 稲葉牧場時代'}
オグリキャップは1985年3月27日の深夜に誕生した。誕生時には右前脚が大きく外向しており、出生直後はなかなか自力で立ち...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > デビューまで > 誕生・生い立ち > 美山育成牧場時代'}
1986年の10月、ハツラツは岐阜県山県郡美山町(現:山県市)にあった美山育成牧場に移り、3か月間馴致を施された。当時の...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 笠松競馬時代 > 競走内容(1)'}
1987年1月28日に笠松競馬場の鷲見昌勇厩舎に入厩。登録馬名は「オグリキヤツプ」。ダート800mで行われた能力試験を5...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 笠松競馬時代 > 競走内容(2)'}
敗れたのはいずれもダート800mのレースで、短距離戦では大きな不利に繋がるとされる出遅れをした。一方オグリキャップに勝っ...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 笠松競馬時代 > 競走内容(3)'}
なお、マーチトウショウは1989年に中央競馬へ移籍したが勝利を挙げられず、その後、笠松へ戻って1戦に出走した後高知競馬へ...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 佐橋五十雄への売却と中央競馬への移籍(1)'}
1988年1月、馬主の小栗はオグリキャップを2000万円でない佐橋五十雄に売却し、佐橋は中央競馬への移籍を決定した。オグ...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 佐橋五十雄への売却と中央競馬への移籍(2)'}
また、佐橋はオグリキャップが中央競馬のレースで優勝した際にはウイナーズサークルでの記念撮影に招待し、種牡馬となった場合に...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 中央競馬時代 > 4歳(1988年)'}
中央競馬移籍後のオグリキャップは栗東トレーニングセンターの調教師瀬戸口勉の厩舎で管理されることが決まり、1月28日に鷲見...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 中央競馬時代 > 4歳(1988年) > 競走内容(1)'}
オグリキャップの中央移籍後の初戦にはペガサスステークスが選ばれ、鞍上は佐橋の希望により河内洋に決まった。地方での快進撃は...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 中央競馬時代 > 4歳(1988年) > 競走内容(2)'}
オグリキャップは初代馬主の小栗孝一が中央で走らせるつもりがなかったことでクラシック登録をしていなかったため、前哨戦である...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 中央競馬時代 > 4歳(1988年) > 競走内容(3)'}
このレースでのオグリキャップの走破タイムは同レースのレースレコードを記録し、このタイムは前月に同じ東京芝1600mで行わ...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 中央競馬時代 > 4歳(1988年) > 競走内容(4)'}
毎日王冠までは避暑 を行わず、栗東トレーニングセンターで調整を行い、8月下旬から本格的な調教を開始。9月末に東京競馬場に...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 中央競馬時代 > 4歳(1988年) > 競走内容(5)'}
レースでは馬群のやや後方につけて追い込みを図り、出走馬中最も速い上がりを記録したものの、2番手を先行し直線で先頭になった...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 中央競馬時代 > 4歳(1988年) > 競走内容(6)'}
レース後、次走の有馬記念で挽回を果たしたいと考えた佐橋は、瀬戸口を通じてこの時点で有馬記念での騎乗馬が決まっていなかった...
====
{'source': 'data/オグリキャップ.md', 'section': 'オグリキャップ > 競走馬時代 > 中央競馬時代 > 4歳(1988年) > 競走内容(7)'}
翌1989年1月10日には、1988年度のJRA賞最優秀4歳牡馬に選出された。タマモクロスを担当した調教助手の井高淳一と...
(snip)
このスクラップは19日前にクローズされました