mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-20 00:14:50 +00:00
Merge pull request #2372 from arc53/fast-ebook
feat: faster ebook parsing
This commit is contained in:
3 files changed
+31
-131
No files matched your search
@@ -19,25 +19,10 @@ class EpubParser(BaseParser):
|
|||||||
def parse_file(self, file: Path, errors: str = "ignore") -> str:
|
def parse_file(self, file: Path, errors: str = "ignore") -> str:
|
||||||
"""Parse file."""
|
"""Parse file."""
|
||||||
try:
|
try:
|
||||||
import ebooklib
|
from fast_ebook import epub
|
||||||
from ebooklib import epub
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ValueError("`EbookLib` is required to read Epub files.")
|
raise ValueError("`fast-ebook` is required to read Epub files.")
|
||||||
try:
|
|
||||||
import html2text
|
|
||||||
except ImportError:
|
|
||||||
raise ValueError("`html2text` is required to parse Epub files.")
|
|
||||||
|
|
||||||
text_list = []
|
book = epub.read_epub(file)
|
||||||
book = epub.read_epub(file, options={"ignore_ncx": True})
|
text = book.to_markdown()
|
||||||
|
|
||||||
# Iterate through all chapters.
|
|
||||||
for item in book.get_items():
|
|
||||||
# Chapters are typically located in epub documents items.
|
|
||||||
if item.get_type() == ebooklib.ITEM_DOCUMENT:
|
|
||||||
text_list.append(
|
|
||||||
html2text.html2text(item.get_content().decode("utf-8"))
|
|
||||||
)
|
|
||||||
|
|
||||||
text = "\n".join(text_list)
|
|
||||||
return text
|
return text
|
||||||
@@ -11,7 +11,7 @@ rapidocr>=1.4.0
|
|||||||
onnxruntime>=1.19.0
|
onnxruntime>=1.19.0
|
||||||
docx2txt==0.9
|
docx2txt==0.9
|
||||||
ddgs>=8.0.0
|
ddgs>=8.0.0
|
||||||
ebooklib==0.20
|
fast-ebook
|
||||||
elevenlabs==2.41.0
|
elevenlabs==2.41.0
|
||||||
Flask==3.1.3
|
Flask==3.1.3
|
||||||
faiss-cpu==1.13.2
|
faiss-cpu==1.13.2
|
||||||
@@ -23,7 +23,6 @@ google-auth-httplib2==0.3.1
|
|||||||
google-auth-oauthlib==1.3.1
|
google-auth-oauthlib==1.3.1
|
||||||
gTTS==2.5.4
|
gTTS==2.5.4
|
||||||
gunicorn==25.3.0
|
gunicorn==25.3.0
|
||||||
html2text==2025.4.15
|
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
jiter==0.13.0
|
jiter==0.13.0
|
||||||
jmespath==1.1.0
|
jmespath==1.1.0
|
||||||
|
|||||||
@@ -20,133 +20,49 @@ def test_epub_init_parser():
|
|||||||
assert parser.parser_config_set
|
assert parser.parser_config_set
|
||||||
|
|
||||||
|
|
||||||
def test_epub_parser_ebooklib_import_error(epub_parser):
|
def test_epub_parser_fast_ebook_import_error(epub_parser):
|
||||||
"""Test that ImportError is raised when ebooklib is not available."""
|
"""Test that ImportError is raised when fast-ebook is not available."""
|
||||||
with patch.dict(sys.modules, {"ebooklib": None}):
|
with patch.dict(sys.modules, {"fast_ebook": None}):
|
||||||
with pytest.raises(ValueError, match="`EbookLib` is required to read Epub files"):
|
with pytest.raises(ValueError, match="`fast-ebook` is required to read Epub files"):
|
||||||
epub_parser.parse_file(Path("test.epub"))
|
epub_parser.parse_file(Path("test.epub"))
|
||||||
|
|
||||||
|
|
||||||
def test_epub_parser_html2text_import_error(epub_parser):
|
|
||||||
"""Test that ImportError is raised when html2text is not available."""
|
|
||||||
fake_ebooklib = types.ModuleType("ebooklib")
|
|
||||||
fake_epub = types.ModuleType("ebooklib.epub")
|
|
||||||
fake_ebooklib.epub = fake_epub
|
|
||||||
|
|
||||||
with patch.dict(sys.modules, {"ebooklib": fake_ebooklib, "ebooklib.epub": fake_epub}):
|
|
||||||
with patch.dict(sys.modules, {"html2text": None}):
|
|
||||||
with pytest.raises(ValueError, match="`html2text` is required to parse Epub files"):
|
|
||||||
epub_parser.parse_file(Path("test.epub"))
|
|
||||||
|
|
||||||
|
|
||||||
def test_epub_parser_successful_parsing(epub_parser):
|
def test_epub_parser_successful_parsing(epub_parser):
|
||||||
"""Test successful parsing of an epub file."""
|
"""Test successful parsing of an epub file."""
|
||||||
|
fake_fast_ebook = types.ModuleType("fast_ebook")
|
||||||
|
fake_epub = types.ModuleType("fast_ebook.epub")
|
||||||
|
fake_fast_ebook.epub = fake_epub
|
||||||
|
|
||||||
fake_ebooklib = types.ModuleType("ebooklib")
|
|
||||||
fake_epub = types.ModuleType("ebooklib.epub")
|
|
||||||
fake_html2text = types.ModuleType("html2text")
|
|
||||||
|
|
||||||
# Mock ebooklib constants
|
|
||||||
fake_ebooklib.ITEM_DOCUMENT = "document"
|
|
||||||
fake_ebooklib.epub = fake_epub
|
|
||||||
|
|
||||||
mock_item1 = MagicMock()
|
|
||||||
mock_item1.get_type.return_value = "document"
|
|
||||||
mock_item1.get_content.return_value = b"<h1>Chapter 1</h1><p>Content 1</p>"
|
|
||||||
|
|
||||||
mock_item2 = MagicMock()
|
|
||||||
mock_item2.get_type.return_value = "document"
|
|
||||||
mock_item2.get_content.return_value = b"<h1>Chapter 2</h1><p>Content 2</p>"
|
|
||||||
|
|
||||||
mock_item3 = MagicMock()
|
|
||||||
mock_item3.get_type.return_value = "other" # Should be ignored
|
|
||||||
mock_item3.get_content.return_value = b"<p>Other content</p>"
|
|
||||||
|
|
||||||
mock_book = MagicMock()
|
mock_book = MagicMock()
|
||||||
mock_book.get_items.return_value = [mock_item1, mock_item2, mock_item3]
|
mock_book.to_markdown.return_value = "# Chapter 1\n\nContent 1\n\n# Chapter 2\n\nContent 2\n"
|
||||||
|
|
||||||
fake_epub.read_epub = MagicMock(return_value=mock_book)
|
fake_epub.read_epub = MagicMock(return_value=mock_book)
|
||||||
|
|
||||||
def mock_html2text_func(html_content):
|
|
||||||
if "Chapter 1" in html_content:
|
|
||||||
return "# Chapter 1\n\nContent 1\n"
|
|
||||||
elif "Chapter 2" in html_content:
|
|
||||||
return "# Chapter 2\n\nContent 2\n"
|
|
||||||
return "Other content\n"
|
|
||||||
|
|
||||||
fake_html2text.html2text = mock_html2text_func
|
|
||||||
|
|
||||||
with patch.dict(sys.modules, {
|
with patch.dict(sys.modules, {
|
||||||
"ebooklib": fake_ebooklib,
|
"fast_ebook": fake_fast_ebook,
|
||||||
"ebooklib.epub": fake_epub,
|
"fast_ebook.epub": fake_epub,
|
||||||
"html2text": fake_html2text
|
|
||||||
}):
|
}):
|
||||||
result = epub_parser.parse_file(Path("test.epub"))
|
result = epub_parser.parse_file(Path("test.epub"))
|
||||||
|
|
||||||
expected_result = "# Chapter 1\n\nContent 1\n\n# Chapter 2\n\nContent 2\n"
|
assert result == "# Chapter 1\n\nContent 1\n\n# Chapter 2\n\nContent 2\n"
|
||||||
assert result == expected_result
|
fake_epub.read_epub.assert_called_once_with(Path("test.epub"))
|
||||||
|
|
||||||
# Verify epub.read_epub was called with correct parameters
|
|
||||||
fake_epub.read_epub.assert_called_once_with(Path("test.epub"), options={"ignore_ncx": True})
|
|
||||||
|
|
||||||
|
|
||||||
def test_epub_parser_empty_book(epub_parser):
|
def test_epub_parser_empty_book(epub_parser):
|
||||||
"""Test parsing an epub file with no document items."""
|
"""Test parsing an epub file with no content."""
|
||||||
# Create mock modules
|
fake_fast_ebook = types.ModuleType("fast_ebook")
|
||||||
fake_ebooklib = types.ModuleType("ebooklib")
|
fake_epub = types.ModuleType("fast_ebook.epub")
|
||||||
fake_epub = types.ModuleType("ebooklib.epub")
|
fake_fast_ebook.epub = fake_epub
|
||||||
fake_html2text = types.ModuleType("html2text")
|
|
||||||
|
|
||||||
fake_ebooklib.ITEM_DOCUMENT = "document"
|
|
||||||
fake_ebooklib.epub = fake_epub
|
|
||||||
|
|
||||||
# Create mock book with no document items
|
|
||||||
mock_book = MagicMock()
|
mock_book = MagicMock()
|
||||||
mock_book.get_items.return_value = []
|
mock_book.to_markdown.return_value = ""
|
||||||
|
|
||||||
fake_epub.read_epub = MagicMock(return_value=mock_book)
|
fake_epub.read_epub = MagicMock(return_value=mock_book)
|
||||||
fake_html2text.html2text = MagicMock()
|
|
||||||
|
|
||||||
with patch.dict(sys.modules, {
|
with patch.dict(sys.modules, {
|
||||||
"ebooklib": fake_ebooklib,
|
"fast_ebook": fake_fast_ebook,
|
||||||
"ebooklib.epub": fake_epub,
|
"fast_ebook.epub": fake_epub,
|
||||||
"html2text": fake_html2text
|
|
||||||
}):
|
}):
|
||||||
result = epub_parser.parse_file(Path("empty.epub"))
|
result = epub_parser.parse_file(Path("empty.epub"))
|
||||||
|
|
||||||
assert result == ""
|
assert result == ""
|
||||||
|
|
||||||
fake_html2text.html2text.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
def test_epub_parser_non_document_items_ignored(epub_parser):
|
|
||||||
"""Test that non-document items are ignored during parsing."""
|
|
||||||
fake_ebooklib = types.ModuleType("ebooklib")
|
|
||||||
fake_epub = types.ModuleType("ebooklib.epub")
|
|
||||||
fake_html2text = types.ModuleType("html2text")
|
|
||||||
|
|
||||||
fake_ebooklib.ITEM_DOCUMENT = "document"
|
|
||||||
fake_ebooklib.epub = fake_epub
|
|
||||||
|
|
||||||
mock_doc_item = MagicMock()
|
|
||||||
mock_doc_item.get_type.return_value = "document"
|
|
||||||
mock_doc_item.get_content.return_value = b"<p>Document content</p>"
|
|
||||||
|
|
||||||
mock_other_item = MagicMock()
|
|
||||||
mock_other_item.get_type.return_value = "image" # Not a document
|
|
||||||
|
|
||||||
mock_book = MagicMock()
|
|
||||||
mock_book.get_items.return_value = [mock_other_item, mock_doc_item]
|
|
||||||
|
|
||||||
fake_epub.read_epub = MagicMock(return_value=mock_book)
|
|
||||||
fake_html2text.html2text = MagicMock(return_value="Document content\n")
|
|
||||||
|
|
||||||
with patch.dict(sys.modules, {
|
|
||||||
"ebooklib": fake_ebooklib,
|
|
||||||
"ebooklib.epub": fake_epub,
|
|
||||||
"html2text": fake_html2text
|
|
||||||
}):
|
|
||||||
result = epub_parser.parse_file(Path("test.epub"))
|
|
||||||
|
|
||||||
assert result == "Document content\n"
|
|
||||||
|
|
||||||
fake_html2text.html2text.assert_called_once_with("<p>Document content</p>")
|
|
||||||
Reference in new issue
Block a user