HTML 문서를 관리 가능한 청크로 분할하는 것은 자연어 처리, 검색 인덱싱 등 다양한 텍스트 처리 작업에 필수적입니다. 이 가이드에서는 HTML 콘텐츠를 효과적으로 분할할 수 있도록 LangChain에서 제공하는 세 가지 서로 다른 text splitter를 살펴봅니다: 각 splitter는 고유한 기능과 사용 사례를 갖고 있습니다. 이 가이드는 이들 간의 차이점, 특정 상황에서 하나를 다른 것보다 선택해야 하는 이유, 그리고 효과적으로 사용하는 방법을 이해하는 데 도움을 줄 것입니다.
pip install -qU langchain-text-splitters

Splitter 개요

HTMLHeaderTextSplitter

문서의 heading 기반 계층 구조를 보존하고 싶을 때 유용합니다.
설명: header 태그(예: <h1>, <h2>, <h3> 등)를 기준으로 HTML 텍스트를 분할하고, 각 청크에 관련된 header 메타데이터를 추가합니다. 기능:
  • HTML element 수준에서 텍스트를 분할합니다.
  • 문서 구조에 인코딩된 풍부한 컨텍스트 정보를 보존합니다.
  • 요소별로 청크를 반환하거나 동일한 메타데이터를 가진 요소들을 결합할 수 있습니다.

HTMLSectionSplitter

<section>, <div> 또는 사용자 정의된 큰 섹션 단위로 HTML 문서를 분할하고 싶을 때 유용합니다.
설명: HTMLHeaderTextSplitter와 유사하지만, 지정된 태그를 기준으로 HTML을 섹션 단위로 분할하는 데 중점을 둡니다. 기능:
  • 섹션을 감지하고 분할하기 위해 XSLT 변환을 사용합니다.
  • 내부적으로 큰 섹션에는 RecursiveCharacterTextSplitter를 사용합니다.
  • 섹션 판별을 위해 폰트 크기를 고려합니다.

HTMLSemanticPreservingSplitter

구조화된 요소를 청크 간에 분할하지 않도록 하여 문맥적 관련성을 보존해야 할 때 이상적입니다.
설명: 테이블, 리스트 및 기타 HTML 구성 요소와 같은 중요한 요소의 시맨틱 구조를 보존하면서 HTML 콘텐츠를 관리 가능한 청크로 분할합니다. 기능:
  • 테이블, 리스트 및 기타 지정된 HTML 요소를 보존합니다.
  • 특정 HTML 태그에 대해 커스텀 핸들러를 허용합니다.
  • 문서의 시맨틱 의미가 유지되도록 보장합니다.
  • 내장 정규화 및 불용어 제거

적합한 Splitter 선택하기

  • HTMLHeaderTextSplitter를 사용할 때: HTML 문서를 header 계층 구조에 따라 분할하고 header에 대한 메타데이터를 유지해야 할 때.
  • HTMLSectionSplitter를 사용할 때: 사용자 정의 태그나 폰트 크기를 기반으로 문서를 더 크고 일반적인 섹션으로 분할해야 할 때.
  • HTMLSemanticPreservingSplitter를 사용할 때: 테이블과 리스트 같은 시맨틱 요소를 보존하면서, 이들이 분할되지 않고 문맥이 유지되도록 HTML 문서를 청크로 나눠야 할 때.
기능HTMLHeaderTextSplitterHTMLSectionSplitterHTMLSemanticPreservingSplitter
헤더 기반 분할
시맨틱 요소 보존(테이블, 리스트)아니오아니오
헤더 메타데이터 추가
HTML 태그용 커스텀 핸들러아니오아니오
미디어(이미지, 비디오) 보존아니오아니오
폰트 크기 고려아니오아니오
XSLT 변환 사용아니오아니오

예제 HTML 문서

다음 HTML 문서를 예제로 사용해 보겠습니다:
html_string = """
<!DOCTYPE html>
  <html lang='en'>
  <head>
    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>Fancy Example HTML Page</title>
  </head>
  <body>
    <h1>Main Title</h1>
    <p>This is an introductory paragraph with some basic content.</p>

    <h2>Section 1: Introduction</h2>
    <p>This section introduces the topic. Below is a list:</p>
    <ul>
      <li>First item</li>
      <li>Second item</li>
      <li>Third item with <strong>bold text</strong> and <a href='#'>a link</a></li>
    </ul>

    <h3>Subsection 1.1: Details</h3>
    <p>This subsection provides additional details. Here's a table:</p>
    <table border='1'>
      <thead>
        <tr>
          <th>Header 1</th>
          <th>Header 2</th>
          <th>Header 3</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Row 1, Cell 1</td>
          <td>Row 1, Cell 2</td>
          <td>Row 1, Cell 3</td>
        </tr>
        <tr>
          <td>Row 2, Cell 1</td>
          <td>Row 2, Cell 2</td>
          <td>Row 2, Cell 3</td>
        </tr>
      </tbody>
    </table>

    <h2>Section 2: Media Content</h2>
    <p>This section contains an image and a video:</p>
      <img src='example_image_link.mp4' alt='Example Image'>
      <video controls width='250' src='example_video_link.mp4' type='video/mp4'>
      Your browser does not support the video tag.
    </video>

    <h2>Section 3: Code Example</h2>
    <p>This section contains a code block:</p>
    <pre><code data-lang="html">
    &lt;div&gt;
      &lt;p&gt;This is a paragraph inside a div.&lt;/p&gt;
    &lt;/div&gt;
    </code></pre>

    <h2>Conclusion</h2>
    <p>This is the conclusion of the document.</p>
  </body>
  </html>
"""

HTMLHeaderTextSplitter 사용하기

HTMLHeaderTextSplitter는 HTML element 수준에서 텍스트를 분할하고, 임의의 청크에 대해 “관련”된 각 header에 대한 메타데이터를 추가하는 “structure-aware” text splitter입니다. 요소별로 청크를 반환하거나 동일한 메타데이터를 가진 요소들을 결합할 수 있으며, (a) 관련 텍스트를 시맨틱하게(대략) 함께 묶고 (b) 문서 구조에 인코딩된 풍부한 컨텍스트 정보를 보존하는 것을 목표로 합니다. 청크 생성 파이프라인의 일부로 다른 text splitter와 함께 사용할 수 있습니다. 이는 markdown 파일을 위한 MarkdownHeaderTextSplitter와 유사합니다. 분할할 header를 지정하려면 아래와 같이 HTMLHeaderTextSplitter를 인스턴스화할 때 headers_to_split_on을 지정하세요.
from langchain_text_splitters import HTMLHeaderTextSplitter

headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
    ("h3", "Header 3"),
]

html_splitter = HTMLHeaderTextSplitter(headers_to_split_on)
html_header_splits = html_splitter.split_text(html_string)
html_header_splits
[Document(metadata={'Header 1': 'Main Title'}, page_content='This is an introductory paragraph with some basic content.'),
 Document(metadata={'Header 1': 'Main Title', 'Header 2': 'Section 1: Introduction'}, page_content='This section introduces the topic. Below is a list:  \nFirst item Second item Third item with bold text and a link'),
 Document(metadata={'Header 1': 'Main Title', 'Header 2': 'Section 1: Introduction', 'Header 3': 'Subsection 1.1: Details'}, page_content="This subsection provides additional details. Here's a table:"),
 Document(metadata={'Header 1': 'Main Title', 'Header 2': 'Section 2: Media Content'}, page_content='This section contains an image and a video:'),
 Document(metadata={'Header 1': 'Main Title', 'Header 2': 'Section 3: Code Example'}, page_content='This section contains a code block:'),
 Document(metadata={'Header 1': 'Main Title', 'Header 2': 'Conclusion'}, page_content='This is the conclusion of the document.')]
각 요소를 해당 header와 함께 반환하려면 HTMLHeaderTextSplitter를 인스턴스화할 때 return_each_element=True를 지정하세요:
html_splitter = HTMLHeaderTextSplitter(
    headers_to_split_on,
    return_each_element=True,
)
html_header_splits_elements = html_splitter.split_text(html_string)
위와 비교하면, 요소가 header별로 집계되는 경우는 다음과 같습니다:
for element in html_header_splits[:2]:
    print(element)
page_content='This is an introductory paragraph with some basic content.' metadata={'Header 1': 'Main Title'}
page_content='This section introduces the topic. Below is a list:
First item Second item Third item with bold text and a link' metadata={'Header 1': 'Main Title', 'Header 2': 'Section 1: Introduction'}
이제 각 요소는 개별적인 Document로 반환됩니다:
for element in html_header_splits_elements[:3]:
    print(element)
page_content='This is an introductory paragraph with some basic content.' metadata={'Header 1': 'Main Title'}
page_content='This section introduces the topic. Below is a list:' metadata={'Header 1': 'Main Title', 'Header 2': 'Section 1: Introduction'}
page_content='First item Second item Third item with bold text and a link' metadata={'Header 1': 'Main Title', 'Header 2': 'Section 1: Introduction'}

URL 또는 HTML 파일에서 분할하는 방법:

URL에서 직접 읽으려면 URL 문자열을 split_text_from_url 메서드에 전달하세요. 마찬가지로, 로컬 HTML 파일은 split_text_from_file 메서드에 전달할 수 있습니다.
url = "https://plato.stanford.edu/entries/goedel/"

headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
    ("h3", "Header 3"),
    ("h4", "Header 4"),
]

html_splitter = HTMLHeaderTextSplitter(headers_to_split_on)

# for local file use html_splitter.split_text_from_file(<path_to_file>)
html_header_splits = html_splitter.split_text_from_url(url)

청크 크기를 제한하는 방법:

HTML header 기반으로 분할하는 HTMLHeaderTextSplitterRecursiveCharacterTextSplitter처럼 문자 길이에 기반하여 분할을 제한하는 다른 splitter와 조합할 수 있습니다. 이는 두 번째 splitter의 .split_documents 메서드를 사용하여 수행할 수 있습니다:
from langchain_text_splitters import RecursiveCharacterTextSplitter

chunk_size = 500
chunk_overlap = 30
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=chunk_size, chunk_overlap=chunk_overlap
)

# Split
splits = text_splitter.split_documents(html_header_splits)
splits[80:85]
[Document(metadata={'Header 1': 'Kurt Gödel', 'Header 2': '2. Gödel’s Mathematical Work', 'Header 3': '2.2 The Incompleteness Theorems', 'Header 4': '2.2.1 The First Incompleteness Theorem'}, page_content='We see that Gödel first tried to reduce the consistency problem for analysis to that of arithmetic. This seemed to require a truth definition for arithmetic, which in turn led to paradoxes, such as the Liar paradox (“This sentence is false”) and Berry’s paradox (“The least number not defined by an expression consisting of just fourteen English words”). Gödel then noticed that such paradoxes would not necessarily arise if truth were replaced by provability. But this means that arithmetic truth'),
 Document(metadata={'Header 1': 'Kurt Gödel', 'Header 2': '2. Gödel’s Mathematical Work', 'Header 3': '2.2 The Incompleteness Theorems', 'Header 4': '2.2.1 The First Incompleteness Theorem'}, page_content='means that arithmetic truth and arithmetic provability are not co-extensive — whence the First Incompleteness Theorem.'),
 Document(metadata={'Header 1': 'Kurt Gödel', 'Header 2': '2. Gödel’s Mathematical Work', 'Header 3': '2.2 The Incompleteness Theorems', 'Header 4': '2.2.1 The First Incompleteness Theorem'}, page_content='This account of Gödel’s discovery was told to Hao Wang very much after the fact; but in Gödel’s contemporary correspondence with Bernays and Zermelo, essentially the same description of his path to the theorems is given. (See Gödel 2003a and Gödel 2003b respectively.) From those accounts we see that the undefinability of truth in arithmetic, a result credited to Tarski, was likely obtained in some form by Gödel by 1931. But he neither publicized nor published the result; the biases logicians'),
 Document(metadata={'Header 1': 'Kurt Gödel', 'Header 2': '2. Gödel’s Mathematical Work', 'Header 3': '2.2 The Incompleteness Theorems', 'Header 4': '2.2.1 The First Incompleteness Theorem'}, page_content='result; the biases logicians had expressed at the time concerning the notion of truth, biases which came vehemently to the fore when Tarski announced his results on the undefinability of truth in formal systems 1935, may have served as a deterrent to Gödel’s publication of that theorem.'),
 Document(metadata={'Header 1': 'Kurt Gödel', 'Header 2': '2. Gödel’s Mathematical Work', 'Header 3': '2.2 The Incompleteness Theorems', 'Header 4': '2.2.2 The proof of the First Incompleteness Theorem'}, page_content='We now describe the proof of the two theorems, formulating Gödel’s results in Peano arithmetic. Gödel himself used a system related to that defined in Principia Mathematica, but containing Peano arithmetic. In our presentation of the First and Second Incompleteness Theorems we refer to Peano arithmetic as P, following Gödel’s notation.')]

제한 사항

HTML 문서마다 구조적 변이가 상당히 있을 수 있으며, HTMLHeaderTextSplitter가 임의의 청크에 모든 “관련” header를 연결하려고 시도하더라도 때로는 특정 header를 놓칠 수 있습니다. 예를 들어, 알고리즘은 header가 항상 관련 텍스트 “위”의 노드(즉, 앞선 형제, 조상, 그리고 그 조합)에 존재하는 정보 계층을 가정합니다. 다음 뉴스 기사(이 문서를 작성하는 시점 기준)에서는 문서가 최상위 헤드라인의 텍스트가 “h1”으로 태그되어 있지만, 우리가 기대하는 텍스트 요소들보다 구별되는(distinct) 서브트리에 위치해 있도록 구조화되어 있습니다. 따라서 “h1” 요소와 그에 관련된 텍스트가 청크 메타데이터에 나타나지 않음을 관찰할 수 있습니다(하지만 해당되는 경우 “h2”와 그 관련 텍스트는 보입니다):
url = "https://www.cnn.com/2023/09/25/weather/el-nino-winter-us-climate/index.html"

headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
]

html_splitter = HTMLHeaderTextSplitter(headers_to_split_on)
html_header_splits = html_splitter.split_text_from_url(url)
print(html_header_splits[1].page_content[:500])
No two El Niño winters are the same, but many have temperature and precipitation trends in common.
Average conditions during an El Niño winter across the continental US.
One of the major reasons is the position of the jet stream, which often shifts south during an El Niño winter. This shift typically brings wetter and cooler weather to the South while the North becomes drier and warmer, according to NOAA.
Because the jet stream is essentially a river of air that storms flow through, they c

HTMLSectionSplitter 사용하기

HTMLHeaderTextSplitter와 개념적으로 유사하게, HTMLSectionSplitter는 element 수준에서 텍스트를 분할하고 임의의 청크에 대해 “관련”된 각 header에 대한 메타데이터를 추가하는 “structure-aware” text splitter입니다. HTML을 섹션 단위로 분할할 수 있게 해줍니다. 요소별로 청크를 반환하거나 동일한 메타데이터를 가진 요소들을 결합할 수 있으며, (a) 관련 텍스트를 시맨틱하게(대략) 함께 묶고 (b) 문서 구조에 인코딩된 풍부한 컨텍스트 정보를 보존하는 것을 목표로 합니다. xslt_path를 사용해 HTML을 변환할 절대 경로를 제공하여, 지정된 태그를 기반으로 섹션을 감지할 수 있도록 합니다. 기본값은 data_connection/document_transformers 디렉터리의 converting_to_header.xslt 파일을 사용하는 것입니다. 이는 섹션 감지를 더 쉽게 할 수 있도록 HTML을 포맷/레이아웃으로 변환하기 위함입니다. 예를 들어, 폰트 크기에 기반한 span을 header 태그로 변환하여 섹션으로 감지할 수 있습니다.

HTML 문자열을 분할하는 방법:

from langchain_text_splitters import HTMLSectionSplitter

headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
]

html_splitter = HTMLSectionSplitter(headers_to_split_on)
html_header_splits = html_splitter.split_text(html_string)
html_header_splits
[Document(metadata={'Header 1': 'Main Title'}, page_content='Main Title \n This is an introductory paragraph with some basic content.'),
 Document(metadata={'Header 2': 'Section 1: Introduction'}, page_content="Section 1: Introduction \n This section introduces the topic. Below is a list: \n \n First item \n Second item \n Third item with  bold text  and  a link \n \n \n Subsection 1.1: Details \n This subsection provides additional details. Here's a table: \n \n \n \n Header 1 \n Header 2 \n Header 3 \n \n \n \n \n Row 1, Cell 1 \n Row 1, Cell 2 \n Row 1, Cell 3 \n \n \n Row 2, Cell 1 \n Row 2, Cell 2 \n Row 2, Cell 3"),
 Document(metadata={'Header 2': 'Section 2: Media Content'}, page_content='Section 2: Media Content \n This section contains an image and a video: \n \n \n      Your browser does not support the video tag.'),
 Document(metadata={'Header 2': 'Section 3: Code Example'}, page_content='Section 3: Code Example \n This section contains a code block: \n \n    <div>\n      <p>This is a paragraph inside a div.</p>\n    </div>'),
 Document(metadata={'Header 2': 'Conclusion'}, page_content='Conclusion \n This is the conclusion of the document.')]

청크 크기를 제한하는 방법:

HTMLSectionSplitter는 청크 생성 파이프라인의 일부로 다른 text splitter와 함께 사용할 수 있습니다. 내부적으로, 섹션 크기가 청크 크기보다 큰 경우 RecursiveCharacterTextSplitter를 사용합니다. 또한 텍스트의 폰트 크기를 고려하여, 설정된 폰트 크기 임계값을 기준으로 섹션 여부를 판단합니다.
from langchain_text_splitters import RecursiveCharacterTextSplitter

headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
    ("h3", "Header 3"),
]

html_splitter = HTMLSectionSplitter(headers_to_split_on)

html_header_splits = html_splitter.split_text(html_string)

chunk_size = 50
chunk_overlap = 5
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=chunk_size, chunk_overlap=chunk_overlap
)

# Split
splits = text_splitter.split_documents(html_header_splits)
splits
[Document(metadata={'Header 1': 'Main Title'}, page_content='Main Title'),
 Document(metadata={'Header 1': 'Main Title'}, page_content='This is an introductory paragraph with some'),
 Document(metadata={'Header 1': 'Main Title'}, page_content='some basic content.'),
 Document(metadata={'Header 2': 'Section 1: Introduction'}, page_content='Section 1: Introduction'),
 Document(metadata={'Header 2': 'Section 1: Introduction'}, page_content='This section introduces the topic. Below is a'),
 Document(metadata={'Header 2': 'Section 1: Introduction'}, page_content='is a list:'),
 Document(metadata={'Header 2': 'Section 1: Introduction'}, page_content='First item \n Second item'),
 Document(metadata={'Header 2': 'Section 1: Introduction'}, page_content='Third item with  bold text  and  a link'),
 Document(metadata={'Header 3': 'Subsection 1.1: Details'}, page_content='Subsection 1.1: Details'),
 Document(metadata={'Header 3': 'Subsection 1.1: Details'}, page_content='This subsection provides additional details.'),
 Document(metadata={'Header 3': 'Subsection 1.1: Details'}, page_content="Here's a table:"),
 Document(metadata={'Header 3': 'Subsection 1.1: Details'}, page_content='Header 1 \n Header 2 \n Header 3'),
 Document(metadata={'Header 3': 'Subsection 1.1: Details'}, page_content='Row 1, Cell 1 \n Row 1, Cell 2'),
 Document(metadata={'Header 3': 'Subsection 1.1: Details'}, page_content='Row 1, Cell 3 \n \n \n Row 2, Cell 1'),
 Document(metadata={'Header 3': 'Subsection 1.1: Details'}, page_content='Row 2, Cell 2 \n Row 2, Cell 3'),
 Document(metadata={'Header 2': 'Section 2: Media Content'}, page_content='Section 2: Media Content'),
 Document(metadata={'Header 2': 'Section 2: Media Content'}, page_content='This section contains an image and a video:'),
 Document(metadata={'Header 2': 'Section 2: Media Content'}, page_content='Your browser does not support the video'),
 Document(metadata={'Header 2': 'Section 2: Media Content'}, page_content='tag.'),
 Document(metadata={'Header 2': 'Section 3: Code Example'}, page_content='Section 3: Code Example'),
 Document(metadata={'Header 2': 'Section 3: Code Example'}, page_content='This section contains a code block: \n \n    <div>'),
 Document(metadata={'Header 2': 'Section 3: Code Example'}, page_content='<p>This is a paragraph inside a div.</p>'),
 Document(metadata={'Header 2': 'Section 3: Code Example'}, page_content='</div>'),
 Document(metadata={'Header 2': 'Conclusion'}, page_content='Conclusion'),
 Document(metadata={'Header 2': 'Conclusion'}, page_content='This is the conclusion of the document.')]

HTMLSemanticPreservingSplitter 사용하기

HTMLSemanticPreservingSplitter는 테이블, 리스트 및 기타 HTML 구성 요소와 같은 중요한 요소의 시맨틱 구조를 보존하면서 HTML 콘텐츠를 관리 가능한 청크로 분할하도록 설계되었습니다. 이를 통해 테이블 헤더, 리스트 헤더 등의 문맥적 관련성을 잃게 만드는, 요소의 청크 간 분할을 방지합니다. 이 splitter는 본질적으로 문맥적으로 관련성 높은 청크를 만들도록 설계되었습니다. HTMLHeaderTextSplitter로 일반적인 재귀 분할을 수행하면, 테이블, 리스트 및 기타 구조화된 요소가 중간에서 분할되어 중요한 컨텍스트를 잃고 좋지 않은 청크를 만들 수 있습니다. HTMLSemanticPreservingSplitter는 특히 테이블과 리스트 같은 구조화된 요소를 포함한 HTML 콘텐츠를 분할할 때, 이 요소들을 온전히 보존하는 것이 중요할 때 필수적입니다. 또한 특정 HTML 태그에 대한 커스텀 핸들러를 정의할 수 있는 기능 덕분에 복잡한 HTML 문서를 처리하는 데 유연한 도구가 됩니다. 중요: max_chunk_size는 청크의 절대 최대 크기를 의미하지 않습니다. 보존 대상 콘텐츠가 청크에서 분리되어 있을 때 최대 크기를 계산하여 분할이 일어나지 않도록 한 후, 보존 데이터를 다시 청크에 추가하면 max_chunk_size를 초과할 수 있습니다. 이는 원본 문서의 구조를 유지하기 위해 매우 중요합니다.
참고:
  1. 코드 블록의 콘텐츠를 재포맷하기 위해 커스텀 핸들러를 정의했습니다.
  2. 특정 HTML 요소에 대한 deny 리스트를 정의하여, 전처리 단계에서 해당 요소와 그 내용을 제거했습니다.
  3. 요소 분할 방지 효과를 보여주기 위해 의도적으로 작은 청크 크기를 설정했습니다.
# BeautifulSoup is required to use the custom handlers
from bs4 import Tag
from langchain_text_splitters import HTMLSemanticPreservingSplitter

headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
]


def code_handler(element: Tag) -> str:
    data_lang = element.get("data-lang")
    code_format = f"<code:{data_lang}>{element.get_text()}</code>"

    return code_format


splitter = HTMLSemanticPreservingSplitter(
    headers_to_split_on=headers_to_split_on,
    separators=["\n\n", "\n", ". ", "! ", "? "],
    max_chunk_size=50,
    preserve_images=True,
    preserve_videos=True,
    elements_to_preserve=["table", "ul", "ol", "code"],
    denylist_tags=["script", "style", "head"],
    custom_handlers={"code": code_handler},
)

documents = splitter.split_text(html_string)
documents
[Document(metadata={'Header 1': 'Main Title'}, page_content='This is an introductory paragraph with some basic content.'),
 Document(metadata={'Header 2': 'Section 1: Introduction'}, page_content='This section introduces the topic'),
 Document(metadata={'Header 2': 'Section 1: Introduction'}, page_content='. Below is a list: First item Second item Third item with bold text and a link Subsection 1.1: Details This subsection provides additional details'),
 Document(metadata={'Header 2': 'Section 1: Introduction'}, page_content=". Here's a table: Header 1 Header 2 Header 3 Row 1, Cell 1 Row 1, Cell 2 Row 1, Cell 3 Row 2, Cell 1 Row 2, Cell 2 Row 2, Cell 3"),
 Document(metadata={'Header 2': 'Section 2: Media Content'}, page_content='This section contains an image and a video: ![image:example_image_link.mp4](example_image_link.mp4) ![video:example_video_link.mp4](example_video_link.mp4)'),
 Document(metadata={'Header 2': 'Section 3: Code Example'}, page_content='This section contains a code block: <code:html> <div> <p>This is a paragraph inside a div.</p> </div> </code>'),
 Document(metadata={'Header 2': 'Conclusion'}, page_content='This is the conclusion of the document.')]

테이블과 리스트 보존

이 예시에서는 HTMLSemanticPreservingSplitter가 HTML 문서 내의 테이블과 큰 리스트를 어떻게 보존하는지 보여줍니다. 청크 크기를 50자로 설정하여, 최대 정의된 청크 크기를 초과하더라도 이러한 요소가 분할되지 않도록 보장하는 방식을 설명합니다.
from langchain_text_splitters import HTMLSemanticPreservingSplitter

html_string = """
<!DOCTYPE html>
<html>
    <body>
        <div>
            <h1>Section 1</h1>
            <p>This section contains an important table and list that should not be split across chunks.</p>
            <table>
                <tr>
                    <th>Item</th>
                    <th>Quantity</th>
                    <th>Price</th>
                </tr>
                <tr>
                    <td>Apples</td>
                    <td>10</td>
                    <td>$1.00</td>
                </tr>
                <tr>
                    <td>Oranges</td>
                    <td>5</td>
                    <td>$0.50</td>
                </tr>
                <tr>
                    <td>Bananas</td>
                    <td>50</td>
                    <td>$1.50</td>
                </tr>
            </table>
            <h2>Subsection 1.1</h2>
            <p>Additional text in subsection 1.1 that is separated from the table and list.</p>
            <p>Here is a detailed list:</p>
            <ul>
                <li>Item 1: Description of item 1, which is quite detailed and important.</li>
                <li>Item 2: Description of item 2, which also contains significant information.</li>
                <li>Item 3: Description of item 3, another item that we don't want to split across chunks.</li>
            </ul>
        </div>
    </body>
</html>
"""

headers_to_split_on = [("h1", "Header 1"), ("h2", "Header 2")]

splitter = HTMLSemanticPreservingSplitter(
    headers_to_split_on=headers_to_split_on,
    max_chunk_size=50,
    elements_to_preserve=["table", "ul"],
)

documents = splitter.split_text(html_string)
print(documents)
[Document(metadata={'Header 1': 'Section 1'}, page_content='This section contains an important table and list'), Document(metadata={'Header 1': 'Section 1'}, page_content='that should not be split across chunks.'), Document(metadata={'Header 1': 'Section 1'}, page_content='Item Quantity Price Apples 10 $1.00 Oranges 5 $0.50 Bananas 50 $1.50'), Document(metadata={'Header 2': 'Subsection 1.1'}, page_content='Additional text in subsection 1.1 that is'), Document(metadata={'Header 2': 'Subsection 1.1'}, page_content='separated from the table and list. Here is a'), Document(metadata={'Header 2': 'Subsection 1.1'}, page_content="detailed list: Item 1: Description of item 1, which is quite detailed and important. Item 2: Description of item 2, which also contains significant information. Item 3: Description of item 3, another item that we don't want to split across chunks.")]

설명

이 예시에서 HTMLSemanticPreservingSplitter는 전체 테이블과 순서 없는 리스트(<ul>)가 각자의 청크 내에서 보존되도록 합니다. 청크 크기가 50자로 설정되어 있더라도, splitter는 이러한 요소가 분할되어서는 안 된다는 것을 인식하고 그대로 유지합니다. 이는 데이터 테이블이나 리스트를 다룰 때 특히 중요합니다. 콘텐츠가 분할되면 문맥 손실이나 혼란을 초래할 수 있기 때문입니다. 결과 Document 객체는 이러한 요소의 전체 구조를 유지하여 정보의 문맥적 관련성을 보장합니다.

커스텀 핸들러 사용하기

HTMLSemanticPreservingSplitter는 특정 HTML 요소에 대한 커스텀 핸들러를 정의할 수 있습니다. 일부 플랫폼은 BeautifulSoup에서 기본적으로 파싱되지 않는 커스텀 HTML 태그를 사용합니다. 이 경우, 커스텀 핸들러를 사용하여 포맷팅 로직을 쉽게 추가할 수 있습니다. 이는 <iframe> 태그나 특정 ‘data-’ 요소처럼 특별한 처리가 필요한 요소에 유용할 수 있습니다. 이 예시에서는 iframe 태그를 Markdown 유사 링크로 변환하는 커스텀 핸들러를 만들어 보겠습니다.
def custom_iframe_extractor(iframe_tag):
    iframe_src = iframe_tag.get("src", "")
    return f"[iframe:{iframe_src}]({iframe_src})"


splitter = HTMLSemanticPreservingSplitter(
    headers_to_split_on=headers_to_split_on,
    max_chunk_size=50,
    separators=["\n\n", "\n", ". "],
    elements_to_preserve=["table", "ul", "ol"],
    custom_handlers={"iframe": custom_iframe_extractor},
)

html_string = """
<!DOCTYPE html>
<html>
    <body>
        <div>
            <h1>Section with Iframe</h1>
            <iframe src="https://example.com/embed"></iframe>
            <p>Some text after the iframe.</p>
            <ul>
                <li>Item 1: Description of item 1, which is quite detailed and important.</li>
                <li>Item 2: Description of item 2, which also contains significant information.</li>
                <li>Item 3: Description of item 3, another item that we don't want to split across chunks.</li>
            </ul>
        </div>
    </body>
</html>
"""

documents = splitter.split_text(html_string)
print(documents)
[Document(metadata={'Header 1': 'Section with Iframe'}, page_content='[iframe:https://example.com/embed](https://example.com/embed) Some text after the iframe'), Document(metadata={'Header 1': 'Section with Iframe'}, page_content=". Item 1: Description of item 1, which is quite detailed and important. Item 2: Description of item 2, which also contains significant information. Item 3: Description of item 3, another item that we don't want to split across chunks.")]

설명

이 예시에서는 iframe 태그를 Markdown 유사 링크로 변환하는 커스텀 핸들러를 정의했습니다. splitter가 HTML 콘텐츠를 처리할 때, 이 커스텀 핸들러를 사용하여 iframe 태그를 변환하는 동시에 테이블과 리스트 같은 다른 요소는 보존합니다. 결과 Document 객체는 여러분이 제공한 커스텀 로직에 따라 iframe이 처리되는 방식을 보여줍니다. 중요: 링크와 같은 항목을 보존할 때는 구분자에 .을 포함하지 않거나, 구분자를 비워 두지 않도록 주의해야 합니다. RecursiveCharacterTextSplitter는 마침표를 기준으로 분할하므로 링크가 절반으로 잘릴 수 있습니다. 대신 . 를 포함한 구분자 목록을 제공하세요.

커스텀 핸들러로 LLM을 사용해 이미지 분석하기

커스텀 핸들러를 사용하면 임의의 요소에 대한 기본 처리를 재정의할 수 있습니다. 좋은 예는 문서 내 이미지에 대한 시맨틱 분석을 청크 생성 흐름에 직접 삽입하는 것입니다. 함수가 태그를 발견할 때 호출되므로, <img> 태그를 재정의하고 preserve_images를 끄면 청크에 임베드하고 싶은 임의의 콘텐츠를 삽입할 수 있습니다.
"""This example assumes you have helper methods `load_image_from_url` and an LLM agent `llm` that can process image data."""

from langchain.agents import AgentExecutor

# This example needs to be replaced with your own agent
llm = AgentExecutor(...)


# This method is a placeholder for loading image data from a URL and is not implemented here
def load_image_from_url(image_url: str) -> bytes:
    # Assuming this method fetches the image data from the URL
    return b"image_data"


html_string = """
<!DOCTYPE html>
<html>
    <body>
        <div>
            <h1>Section with Image and Link</h1>
            <p>
                <img src="https://example.com/image.jpg" alt="An example image" />
                Some text after the image.
            </p>
            <ul>
                <li>Item 1: Description of item 1, which is quite detailed and important.</li>
                <li>Item 2: Description of item 2, which also contains significant information.</li>
                <li>Item 3: Description of item 3, another item that we don't want to split across chunks.</li>
            </ul>
        </div>
    </body>
</html>
"""


def custom_image_handler(img_tag) -> str:
    img_src = img_tag.get("src", "")
    img_alt = img_tag.get("alt", "No alt text provided")

    image_data = load_image_from_url(img_src)
    semantic_meaning = llm.invoke(image_data)

    markdown_text = f"[Image Alt Text: {img_alt} | Image Source: {img_src} | Image Semantic Meaning: {semantic_meaning}]"

    return markdown_text


splitter = HTMLSemanticPreservingSplitter(
    headers_to_split_on=headers_to_split_on,
    max_chunk_size=50,
    separators=["\n\n", "\n", ". "],
    elements_to_preserve=["ul"],
    preserve_images=False,
    custom_handlers={"img": custom_image_handler},
)

documents = splitter.split_text(html_string)

print(documents)
[Document(metadata={'Header 1': 'Section with Image and Link'}, page_content='[Image Alt Text: An example image | Image Source: https://example.com/image.jpg | Image Semantic Meaning: semantic-meaning] Some text after the image'),
Document(metadata={'Header 1': 'Section with Image and Link'}, page_content=". Item 1: Description of item 1, which is quite detailed and important. Item 2: Description of item 2, which also contains significant information. Item 3: Description of item 3, another item that we don't want to split across chunks.")]

설명:

HTML의 <img> 요소에서 특정 필드를 추출하도록 작성된 커스텀 핸들러를 통해, 해당 데이터를 agent로 추가 처리하고 결과를 청크에 직접 삽입할 수 있습니다. preserve_imagesFalse로 설정되어 있는지 확인하는 것이 중요합니다. 그렇지 않으면 <img> 필드에 대한 기본 처리가 수행됩니다.
Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.
I