refactor: Restructure project into core, services, ui, and utils directories, moving existing modules and adding new UI tabs.
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
"""
|
||||
Mô-đun Xuất - Hỗ trợ Word (DOCX), TXT, Markdown
|
||||
|
||||
Bản quyền © 2026 Công ty TNHH Công nghệ An ninh mạng Huyễn Thành Tân Cương (Công nghệ Huyễn Thành)
|
||||
Tác giả: Huyễn Thành
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import tempfile
|
||||
from typing import Tuple, Optional
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from locales.i18n import t
|
||||
|
||||
MODULE_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
EXPORT_DIR = os.path.join(MODULE_ROOT, "exports")
|
||||
os.makedirs(EXPORT_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def _sanitize_filename(name: str, max_len: int = 120) -> str:
|
||||
"""Làm sạch các ký tự không hợp lệ trong tên tệp và giới hạn độ dài"""
|
||||
if not name or not name.strip():
|
||||
name = "novel"
|
||||
safe = re.sub(r'[<>:"/\\|?*]', '_', name).strip()
|
||||
if len(safe) > max_len:
|
||||
safe = safe[:max_len]
|
||||
return safe
|
||||
|
||||
|
||||
def _extract_chapters_from_markdown(text: str) -> list:
|
||||
"""
|
||||
Trích xuất thông tin chương từ văn bản tiểu thuyết định dạng Markdown
|
||||
|
||||
Returns:
|
||||
[{"title": "...", "content": "..."}, ...]
|
||||
"""
|
||||
# Loại bỏ các thẻ HTML phụ trợ (details/summary/b/i) để parse Regex tiêu đề Markdown chính xác
|
||||
text = re.sub(r'</?(details|summary|b|i|br|u|strong|em)[^>]*>', '', text)
|
||||
|
||||
chapters = []
|
||||
current_chapter = None
|
||||
content_lines = []
|
||||
|
||||
# Phát hiện tiêu đề chương tổng quát hơn, hỗ trợ '#', '##', '###' và các cấp độ khác, hỗ trợ các biến thể tiếng Trung và không gian
|
||||
header_re = re.compile(r'^(?:# {1,6}\s*)?(Chương\s*\d+\s*[\s\S]*|Chương\s*\d+[\s\S]*|Chương\s*\d+\s*[::\s\--–]?.*)$', re.IGNORECASE)
|
||||
|
||||
for line in text.splitlines():
|
||||
if not line:
|
||||
# Dòng trống có tác dụng ngăn cách đoạn văn nhưng không kết thúc chương
|
||||
if current_chapter:
|
||||
content_lines.append('')
|
||||
continue
|
||||
|
||||
# Phát hiện tiêu đề chương
|
||||
if header_re.match(line.strip()):
|
||||
# Lưu chương trước
|
||||
if current_chapter:
|
||||
current_chapter['content'] = '\n'.join([l for l in content_lines]).strip()
|
||||
chapters.append(current_chapter)
|
||||
|
||||
# Trích xuất văn bản tiêu đề
|
||||
title_match = re.search(r'(第\s*\d+\s*章[\s\S]*)', line)
|
||||
title = title_match.group(1).strip() if title_match else line.strip()
|
||||
current_chapter = {'title': title, 'content': ''}
|
||||
content_lines = []
|
||||
continue
|
||||
|
||||
# Bỏ qua tiêu đề cấp tệp
|
||||
if line.strip().startswith('# '):
|
||||
continue
|
||||
|
||||
if current_chapter is None:
|
||||
# Nếu bạn chưa gặp tiêu đề chương, hãy đặt nội dung của chương đầu tiên
|
||||
current_chapter = {'title': t("exporter.first_chapter"), 'content': ''}
|
||||
content_lines = [line]
|
||||
else:
|
||||
content_lines.append(line)
|
||||
|
||||
# lưu chương cuối
|
||||
if current_chapter:
|
||||
current_chapter['content'] = '\n'.join([l for l in content_lines]).strip()
|
||||
chapters.append(current_chapter)
|
||||
|
||||
return chapters
|
||||
|
||||
|
||||
def export_to_txt(novel_text: str, title: str) -> Tuple[Optional[str], str]:
|
||||
"""
|
||||
Xuất ra định dạng TXT
|
||||
|
||||
Args:
|
||||
novel_text: Văn bản tiểu thuyết (định dạng Markdown)
|
||||
title: Tiêu đề tiểu thuyết
|
||||
|
||||
Returns:
|
||||
(Đường dẫn tệp, thông tin trạng thái)
|
||||
"""
|
||||
try:
|
||||
if not novel_text.strip():
|
||||
return None, t("exporter.no_content")
|
||||
|
||||
# Trích xuất chương
|
||||
chapters = _extract_chapters_from_markdown(novel_text)
|
||||
|
||||
if not chapters:
|
||||
return None, t("exporter.no_chapters")
|
||||
|
||||
# Tạo nội dung TXT
|
||||
txt_content = f"{title}\n\n"
|
||||
|
||||
for chapter in chapters:
|
||||
txt_content += f"{chapter['title']}\n\n"
|
||||
txt_content += f"{chapter['content']}\n\n"
|
||||
txt_content += "-" * 80 + "\n\n"
|
||||
|
||||
# Lưu tập tin (ghi nguyên tử)
|
||||
safe_title = _sanitize_filename(title)
|
||||
filename = f"{safe_title}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
|
||||
filepath = os.path.join(EXPORT_DIR, filename)
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile('w', encoding='utf-8', delete=False, dir=EXPORT_DIR) as tmp:
|
||||
tmp.write(txt_content)
|
||||
tmp_path = tmp.name
|
||||
os.replace(tmp_path, filepath)
|
||||
except Exception as e:
|
||||
logger.error(f"TXT write failed: {e}")
|
||||
return None, t("exporter.export_failed", error=str(e))
|
||||
|
||||
logger.info(f"TXT export success: {filename}")
|
||||
return filepath, t("exporter.export_success", filename=filename)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TXT export failed: {e}")
|
||||
return None, t("exporter.export_failed", error=str(e))
|
||||
|
||||
|
||||
def export_to_markdown(novel_text: str, title: str) -> Tuple[Optional[str], str]:
|
||||
"""
|
||||
Xuất ra định dạng Markdown
|
||||
|
||||
Args:
|
||||
novel_text: Văn bản tiểu thuyết (định dạng Markdown)
|
||||
title: Tiêu đề tiểu thuyết
|
||||
|
||||
Returns:
|
||||
(Đường dẫn tệp, thông tin trạng thái)
|
||||
"""
|
||||
try:
|
||||
if not novel_text.strip():
|
||||
return None, t("exporter.no_content")
|
||||
|
||||
# Thêm siêu dữ liệu
|
||||
md_content = f"# {title}\n\n"
|
||||
md_content += f"*{t('exporter.generated_at', datetime=datetime.now().strftime('%Y-%m-%d %H:%M:%S'))}*\n\n"
|
||||
md_content += "---\n\n"
|
||||
md_content += novel_text
|
||||
|
||||
# Lưu tập tin (ghi nguyên tử)
|
||||
safe_title = _sanitize_filename(title)
|
||||
filename = f"{safe_title}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
|
||||
filepath = os.path.join(EXPORT_DIR, filename)
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile('w', encoding='utf-8', delete=False, dir=EXPORT_DIR) as tmp:
|
||||
tmp.write(md_content)
|
||||
tmp_path = tmp.name
|
||||
os.replace(tmp_path, filepath)
|
||||
except Exception as e:
|
||||
logger.error(f"Markdown write failed: {e}")
|
||||
return None, t("exporter.export_failed", error=str(e))
|
||||
|
||||
logger.info(f"Markdown export success: {filename}")
|
||||
return filepath, t("exporter.export_success", filename=filename)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Markdown export failed: {e}")
|
||||
return None, t("exporter.export_failed", error=str(e))
|
||||
|
||||
|
||||
def export_to_docx(novel_text: str, title: str) -> Tuple[Optional[str], str]:
|
||||
"""
|
||||
Xuất ra định dạng Word (DOCX) - Dàn trang chuyên nghiệp
|
||||
|
||||
Args:
|
||||
novel_text: Văn bản tiểu thuyết (định dạng Markdown)
|
||||
title: Tiêu đề tiểu thuyết
|
||||
|
||||
Returns:
|
||||
(Đường dẫn tệp, thông tin trạng thái)
|
||||
"""
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml.ns import qn
|
||||
except ImportError:
|
||||
return None, t("exporter.missing_docx")
|
||||
|
||||
try:
|
||||
if not novel_text.strip():
|
||||
return None, t("exporter.no_content")
|
||||
|
||||
# Trích xuất chương
|
||||
chapters = _extract_chapters_from_markdown(novel_text)
|
||||
|
||||
if not chapters:
|
||||
return None, t("exporter.no_chapters")
|
||||
|
||||
doc = Document()
|
||||
|
||||
# Kiểu cấu hình
|
||||
style = doc.styles['Normal']
|
||||
font = style.font
|
||||
font.name = t("exporter.body_font")
|
||||
font.size = Pt(12)
|
||||
|
||||
# phông chữ tiếng trung
|
||||
rPr = style.element.get_or_add_rPr()
|
||||
rPr.find(qn('w:rFonts')).set(qn('w:eastAsia'), t("exporter.body_font"))
|
||||
|
||||
# định dạng đoạn văn
|
||||
style.paragraph_format.first_line_indent = Pt(24)
|
||||
style.paragraph_format.space_after = Pt(0)
|
||||
style.paragraph_format.line_spacing = 1.5
|
||||
|
||||
# Thêm tên sách
|
||||
title_para = doc.add_paragraph(title)
|
||||
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
title_run = title_para.runs[0]
|
||||
title_run.font.name = t("exporter.title_font")
|
||||
title_run.font.size = Pt(26)
|
||||
title_run.font.bold = True
|
||||
title_run.font.color.rgb = RGBColor(0, 0, 0)
|
||||
|
||||
# Cài đặt phông chữ tiếng Trung
|
||||
title_rPr = title_run._element.get_or_add_rPr()
|
||||
title_rPr.find(qn('w:rFonts')).set(qn('w:eastAsia'), t("exporter.title_font"))
|
||||
|
||||
# Thêm thông tin tác giả và ngày tháng
|
||||
info_para = doc.add_paragraph()
|
||||
info_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
info_run = info_para.add_run(t("exporter.generated_date", date=datetime.now().strftime('%Y-%m-%d')))
|
||||
info_run.font.size = Pt(10)
|
||||
|
||||
doc.add_paragraph() # Dòng trống
|
||||
|
||||
# Thêm chương
|
||||
for chapter in chapters:
|
||||
# Tiêu đề chương
|
||||
chapter_title_para = doc.add_paragraph(chapter['title'])
|
||||
chapter_title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
for run in chapter_title_para.runs:
|
||||
run.font.name = t('exporter.title_font')
|
||||
run.font.size = Pt(16)
|
||||
run.font.bold = True
|
||||
run_rPr = run._element.get_or_add_rPr()
|
||||
run_rPr.find(qn('w:rFonts')).set(qn('w:eastAsia'), t("exporter.title_font"))
|
||||
|
||||
doc.add_paragraph() # Dòng trống
|
||||
|
||||
# Nội dung chương - thêm theo đoạn
|
||||
paragraphs = chapter['content'].split('\n\n')
|
||||
for para_text in paragraphs:
|
||||
if para_text.strip():
|
||||
p = doc.add_paragraph(para_text.strip(), style='Normal')
|
||||
|
||||
doc.add_paragraph() # Dòng trống giữa các chương
|
||||
|
||||
# Lưu tập tin (ghi nguyên tử)
|
||||
safe_title = _sanitize_filename(title)
|
||||
filename = f"{safe_title}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
|
||||
filepath = os.path.join(EXPORT_DIR, filename)
|
||||
try:
|
||||
tmp_fd, tmp_path = tempfile.mkstemp(suffix='.docx', dir=EXPORT_DIR)
|
||||
os.close(tmp_fd)
|
||||
doc.save(tmp_path)
|
||||
os.replace(tmp_path, filepath)
|
||||
except Exception as e:
|
||||
logger.error(f"DOCX write failed: {e}")
|
||||
return None, t("exporter.export_failed", error=str(e))
|
||||
|
||||
logger.info(f"DOCX export success: {filename}")
|
||||
return filepath, t("exporter.export_success", filename=filename)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"DOCX export failed: {e}")
|
||||
return None, t("exporter.export_failed", error=str(e))
|
||||
|
||||
|
||||
def export_to_html(novel_text: str, title: str) -> Tuple[Optional[str], str]:
|
||||
"""
|
||||
Xuất ra định dạng HTML - Có thể đọc trên trình duyệt
|
||||
|
||||
Args:
|
||||
novel_text: Văn bản tiểu thuyết (định dạng Markdown)
|
||||
title: Tiêu đề tiểu thuyết
|
||||
|
||||
Returns:
|
||||
(Đường dẫn tệp, thông tin trạng thái)
|
||||
"""
|
||||
try:
|
||||
import markdown
|
||||
except ImportError:
|
||||
return None, t("exporter.missing_markdown")
|
||||
|
||||
try:
|
||||
if not novel_text.strip():
|
||||
return None, t("exporter.no_content")
|
||||
|
||||
# Chuyển đổi Markdown sang HTML
|
||||
html_content = markdown.markdown(novel_text)
|
||||
|
||||
# Được gói gọn dưới dạng tài liệu HTML hoàn chỉnh
|
||||
full_html = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'Arial', 'Times New Roman', serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.8;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}}
|
||||
h1 {{
|
||||
text-align: center;
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}}
|
||||
h2 {{
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
border-bottom: 2px solid #ddd;
|
||||
padding-bottom: 0.3em;
|
||||
}}
|
||||
p {{
|
||||
text-align: justify;
|
||||
text-indent: 2em;
|
||||
margin: 1em 0;
|
||||
}}
|
||||
.info {{
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{title}</h1>
|
||||
<p class="info">{t('exporter.generated_at', datetime=datetime.now().strftime('%Y-%m-%d %H:%M:%S'))}</p>
|
||||
<hr>
|
||||
{html_content}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
# Lưu tập tin (ghi nguyên tử)
|
||||
safe_title = _sanitize_filename(title)
|
||||
filename = f"{safe_title}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
|
||||
filepath = os.path.join(EXPORT_DIR, filename)
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile('w', encoding='utf-8', delete=False, dir=EXPORT_DIR) as tmp:
|
||||
tmp.write(full_html)
|
||||
tmp_path = tmp.name
|
||||
os.replace(tmp_path, filepath)
|
||||
except Exception as e:
|
||||
logger.error(f"HTML write failed: {e}")
|
||||
return None, t("exporter.export_failed", error=str(e))
|
||||
|
||||
logger.info(f"HTML export success: {filename}")
|
||||
return filepath, t("exporter.export_success", filename=filename)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"HTML export failed: {e}")
|
||||
return None, t("exporter.export_failed", error=str(e))
|
||||
|
||||
|
||||
def list_export_files() -> list:
|
||||
"""Liệt kê tất cả các tập tin xuất"""
|
||||
try:
|
||||
files = []
|
||||
for filename in os.listdir(EXPORT_DIR):
|
||||
filepath = os.path.join(EXPORT_DIR, filename)
|
||||
if os.path.isfile(filepath):
|
||||
file_size = os.path.getsize(filepath)
|
||||
file_time = datetime.fromtimestamp(os.path.getmtime(filepath))
|
||||
files.append({
|
||||
'name': filename,
|
||||
'path': filepath,
|
||||
'size': file_size,
|
||||
'time': file_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
})
|
||||
|
||||
return sorted(files, key=lambda x: x['time'], reverse=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"List export files failed: {e}")
|
||||
return []
|
||||
@@ -0,0 +1,779 @@
|
||||
"""
|
||||
Mô-đun phân tích tệp - Hỗ trợ txt/pdf/epub, có theo dõi tiến trình và xử lý lỗi, hỗ trợ mẫu chương tùy chỉnh
|
||||
|
||||
Bản quyền © 2026 Công ty TNHH Công nghệ An ninh mạng Huyễn Thành Tân Cương (Công nghệ Huyễn Thành)
|
||||
Tác giả: Huyễn Thành
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import tempfile
|
||||
from typing import Tuple, List, Optional, IO, Dict
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from locales.i18n import t
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# không thay đổi
|
||||
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
|
||||
MIN_PARAGRAPH_LENGTH = 20 # Độ dài đoạn văn tối thiểu
|
||||
|
||||
|
||||
# Mẫu chương mặc định
|
||||
CHAPTER_PATTERNS = {
|
||||
"default": [
|
||||
r'第\s*\d+\s*章[::\s]*.*',
|
||||
r'第\s*\d+\s*章',
|
||||
r'Chapter\s*\d+',
|
||||
],
|
||||
"compact": [
|
||||
r'^\d+\.',
|
||||
r'^\d+、',
|
||||
r'^\d+\s',
|
||||
],
|
||||
"brackets": [
|
||||
r'《第\d+章》',
|
||||
r'「第\d+章」',
|
||||
],
|
||||
"english": [
|
||||
r'Chapter\s+\d+[::\s]*.*',
|
||||
r'CHAPTER\s+\d+[::\s]*.*',
|
||||
r'Part\s+\d+',
|
||||
],
|
||||
"special": [
|
||||
r'【.*第\d+章.*】',
|
||||
r'≮.*第\d+章.*≯',
|
||||
r'◆.*第\d+章.*◆',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChapterInfo:
|
||||
"""Thông tin chương"""
|
||||
num: int
|
||||
title: str
|
||||
content: str
|
||||
start_pos: int = 0
|
||||
end_pos: int = 0
|
||||
|
||||
|
||||
class FileType(Enum):
|
||||
"""Loại tệp"""
|
||||
TXT = "txt"
|
||||
PDF = "pdf"
|
||||
EPUB = "epub"
|
||||
MD = "md"
|
||||
DOCX = "docx"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
def get_file_type(file_path: str) -> FileType:
|
||||
"""Nhận loại tập tin"""
|
||||
if not file_path:
|
||||
return FileType.UNKNOWN
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
|
||||
if ext == ".txt":
|
||||
return FileType.TXT
|
||||
elif ext == ".pdf":
|
||||
return FileType.PDF
|
||||
elif ext == ".epub":
|
||||
return FileType.EPUB
|
||||
elif ext == ".md":
|
||||
return FileType.MD
|
||||
elif ext == ".docx":
|
||||
return FileType.DOCX
|
||||
else:
|
||||
return FileType.UNKNOWN
|
||||
|
||||
|
||||
def parse_txt_file(file_path: str) -> Tuple[List[str], str]:
|
||||
"""
|
||||
Phân tích tệp TXT
|
||||
|
||||
Returns:
|
||||
(Danh sách đoạn văn, Thông tin trạng thái)
|
||||
"""
|
||||
try:
|
||||
# Hỗ trợ truyền vào các đối tượng hoặc đường dẫn tệp
|
||||
if hasattr(file_path, 'read'):
|
||||
fobj: IO = file_path
|
||||
# Cố gắng lấy thuộc tính kích thước
|
||||
try:
|
||||
fobj.seek(0, os.SEEK_END)
|
||||
file_size = fobj.tell()
|
||||
fobj.seek(0)
|
||||
except Exception:
|
||||
file_size = 0
|
||||
else:
|
||||
file_size = os.path.getsize(file_path)
|
||||
|
||||
if file_size and file_size > MAX_FILE_SIZE:
|
||||
return [], t("file_parser.file_too_large", size=f"{file_size / 1024 / 1024:.1f}")
|
||||
|
||||
paragraphs: List[str] = []
|
||||
buf_lines: List[str] = []
|
||||
total_chars = 0
|
||||
|
||||
# Đọc từng dòng để giảm áp lực bộ nhớ
|
||||
if hasattr(file_path, 'read'):
|
||||
stream = file_path
|
||||
else:
|
||||
stream = open(file_path, 'r', encoding='utf-8', errors='ignore')
|
||||
|
||||
try:
|
||||
for line in stream:
|
||||
stripped = line.rstrip('\n')
|
||||
total_chars += len(stripped)
|
||||
|
||||
if stripped.strip() == '':
|
||||
# Dòng trống -> cuối đoạn
|
||||
if buf_lines:
|
||||
para = '\n'.join(buf_lines).strip()
|
||||
if len(para) >= MIN_PARAGRAPH_LENGTH:
|
||||
paragraphs.append(para)
|
||||
buf_lines = []
|
||||
continue
|
||||
|
||||
# hàng thông thường
|
||||
buf_lines.append(stripped)
|
||||
|
||||
# đoạn cuối
|
||||
if buf_lines:
|
||||
para = '\n'.join(buf_lines).strip()
|
||||
if len(para) >= MIN_PARAGRAPH_LENGTH:
|
||||
paragraphs.append(para)
|
||||
|
||||
finally:
|
||||
if not hasattr(file_path, 'read'):
|
||||
stream.close()
|
||||
|
||||
logger.info(f"TXT parse done: {len(paragraphs)} paragraphs")
|
||||
return paragraphs, t("file_parser.parse_complete", count=len(paragraphs), chars=total_chars)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TXT parse failed: {e}")
|
||||
return [], t("file_parser.read_failed", error=str(e))
|
||||
|
||||
|
||||
def parse_pdf_file(file_path: str) -> Tuple[List[str], str]:
|
||||
"""
|
||||
Phân tích tệp PDF
|
||||
|
||||
Returns:
|
||||
(Danh sách đoạn văn, Thông tin trạng thái)
|
||||
"""
|
||||
try:
|
||||
import fitz
|
||||
except ImportError:
|
||||
return [], t("file_parser.missing_pymupdf")
|
||||
|
||||
try:
|
||||
file_size = os.path.getsize(file_path)
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
return [], t("file_parser.file_too_large", size=f"{file_size / 1024 / 1024:.1f}")
|
||||
|
||||
text_parts = []
|
||||
doc = fitz.open(file_path)
|
||||
|
||||
for page_num, page in enumerate(doc):
|
||||
try:
|
||||
page_text = page.get_text("text")
|
||||
text_parts.append(page_text)
|
||||
except Exception as e:
|
||||
logger.warning(f"PDF page {page_num} parse failed: {e}")
|
||||
|
||||
doc.close()
|
||||
text = "\n".join(text_parts)
|
||||
|
||||
paragraphs = _split_paragraphs(text)
|
||||
logger.info(f"PDF parse done: {len(paragraphs)} paragraphs")
|
||||
return paragraphs, t("file_parser.parse_complete", count=len(paragraphs), chars=len(text))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PDF parse failed: {e}")
|
||||
return [], t("file_parser.read_failed", error=str(e))
|
||||
|
||||
|
||||
def parse_epub_file(file_path: str) -> Tuple[List[str], str]:
|
||||
"""
|
||||
Phân tích tệp EPUB
|
||||
|
||||
Returns:
|
||||
(Danh sách đoạn văn, Thông tin trạng thái)
|
||||
"""
|
||||
try:
|
||||
from ebooklib import epub
|
||||
from bs4 import BeautifulSoup
|
||||
except ImportError:
|
||||
return [], t("file_parser.missing_ebooklib")
|
||||
|
||||
try:
|
||||
file_size = os.path.getsize(file_path)
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
return [], t("file_parser.file_too_large", size=f"{file_size / 1024 / 1024:.1f}")
|
||||
|
||||
text_parts = []
|
||||
book = epub.read_epub(file_path)
|
||||
|
||||
for item in book.get_items():
|
||||
if item.get_type() == epub.ITEM_DOCUMENT:
|
||||
try:
|
||||
soup = BeautifulSoup(item.get_content(), 'html.parser')
|
||||
text_parts.append(soup.get_text(separator="\n"))
|
||||
except Exception as e:
|
||||
logger.warning(f"EPUB chapter parse failed: {e}")
|
||||
|
||||
text = "\n".join(text_parts)
|
||||
paragraphs = _split_paragraphs(text)
|
||||
logger.info(f"EPUB parse done: {len(paragraphs)} paragraphs")
|
||||
return paragraphs, t("file_parser.parse_complete", count=len(paragraphs), chars=len(text))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"EPUB parse failed: {e}")
|
||||
return [], t("file_parser.read_failed", error=str(e))
|
||||
|
||||
|
||||
def parse_md_file(file_path: str) -> Tuple[List[str], str]:
|
||||
"""
|
||||
Phân tích tệp Markdown
|
||||
|
||||
Returns:
|
||||
(Danh sách đoạn văn, Thông tin trạng thái)
|
||||
"""
|
||||
try:
|
||||
# Hỗ trợ truyền vào các đối tượng hoặc đường dẫn tệp
|
||||
if hasattr(file_path, 'read'):
|
||||
fobj: IO = file_path
|
||||
# Cố gắng lấy thuộc tính kích thước
|
||||
try:
|
||||
fobj.seek(0, os.SEEK_END)
|
||||
file_size = fobj.tell()
|
||||
fobj.seek(0)
|
||||
except Exception:
|
||||
file_size = 0
|
||||
else:
|
||||
file_size = os.path.getsize(file_path)
|
||||
|
||||
if file_size and file_size > MAX_FILE_SIZE:
|
||||
return [], t("file_parser.file_too_large", size=f"{file_size / 1024 / 1024:.1f}")
|
||||
|
||||
paragraphs: List[str] = []
|
||||
buf_lines: List[str] = []
|
||||
total_chars = 0
|
||||
|
||||
# Đọc từng dòng để giảm áp lực bộ nhớ
|
||||
if hasattr(file_path, 'read'):
|
||||
stream = file_path
|
||||
else:
|
||||
stream = open(file_path, 'r', encoding='utf-8', errors='ignore')
|
||||
|
||||
try:
|
||||
for line in stream:
|
||||
stripped = line.rstrip('\n')
|
||||
total_chars += len(stripped)
|
||||
|
||||
if stripped.strip() == '':
|
||||
# Dòng trống -> cuối đoạn
|
||||
if buf_lines:
|
||||
para = '\n'.join(buf_lines).strip()
|
||||
if len(para) >= MIN_PARAGRAPH_LENGTH:
|
||||
paragraphs.append(para)
|
||||
buf_lines = []
|
||||
continue
|
||||
|
||||
# hàng thông thường
|
||||
buf_lines.append(stripped)
|
||||
|
||||
# đoạn cuối
|
||||
if buf_lines:
|
||||
para = '\n'.join(buf_lines).strip()
|
||||
if len(para) >= MIN_PARAGRAPH_LENGTH:
|
||||
paragraphs.append(para)
|
||||
|
||||
finally:
|
||||
if not hasattr(file_path, 'read'):
|
||||
stream.close()
|
||||
|
||||
logger.info(f"Markdown parse done: {len(paragraphs)} paragraphs")
|
||||
return paragraphs, t("file_parser.parse_complete", count=len(paragraphs), chars=total_chars)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Markdown parse failed: {e}")
|
||||
return [], t("file_parser.read_failed", error=str(e))
|
||||
|
||||
|
||||
def parse_docx_file(file_path: str) -> Tuple[List[str], str]:
|
||||
"""
|
||||
Phân tích tệp tài liệu Word
|
||||
|
||||
Returns:
|
||||
(Danh sách đoạn văn, Thông tin trạng thái)
|
||||
"""
|
||||
try:
|
||||
from docx import Document
|
||||
except ImportError:
|
||||
return [], t("file_parser.missing_docx")
|
||||
|
||||
try:
|
||||
file_size = os.path.getsize(file_path)
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
return [], t("file_parser.file_too_large", size=f"{file_size / 1024 / 1024:.1f}")
|
||||
|
||||
doc = Document(file_path)
|
||||
paragraphs: List[str] = []
|
||||
total_chars = 0
|
||||
|
||||
for para in doc.paragraphs:
|
||||
text = para.text.strip()
|
||||
if text and len(text) >= MIN_PARAGRAPH_LENGTH:
|
||||
paragraphs.append(text)
|
||||
total_chars += len(text)
|
||||
|
||||
logger.info(f"Word parse done: {len(paragraphs)} paragraphs")
|
||||
return paragraphs, t("file_parser.parse_complete", count=len(paragraphs), chars=total_chars)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Word parse failed: {e}")
|
||||
return [], t("file_parser.read_failed", error=str(e))
|
||||
|
||||
|
||||
def parse_novel_file(file_path: str) -> Tuple[List[str], str]:
|
||||
"""
|
||||
Phân tích tệp tiểu thuyết (tự động nhận dạng định dạng)
|
||||
|
||||
Args:
|
||||
file_path: Đường dẫn tệp
|
||||
|
||||
Returns:
|
||||
(Danh sách đoạn văn, Thông tin trạng thái)
|
||||
"""
|
||||
if not file_path:
|
||||
return [], t("file_parser.no_file")
|
||||
|
||||
# Xử lý các đối tượng tệp hoặc luồng tệp được tải lên bởi Gradio
|
||||
temp_path = None
|
||||
if hasattr(file_path, 'name') and isinstance(file_path.name, str) and os.path.exists(file_path.name):
|
||||
file_path = file_path.name
|
||||
elif hasattr(file_path, 'read'):
|
||||
# Ghi luồng đã tải lên vào một tệp tạm thời để các thư viện xuôi dòng xử lý (PDF/EPUB/DOCX yêu cầu Đường dẫn tệp)
|
||||
try:
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.tmp')
|
||||
chunk = file_path.read(8192)
|
||||
while chunk:
|
||||
if isinstance(chunk, str):
|
||||
tmp.write(chunk.encode('utf-8'))
|
||||
else:
|
||||
tmp.write(chunk)
|
||||
chunk = file_path.read(8192)
|
||||
tmp.close()
|
||||
temp_path = tmp.name
|
||||
file_path = temp_path
|
||||
except Exception as e:
|
||||
logger.error(f"Upload file processing failed: {e}")
|
||||
return [], t("file_parser.upload_read_failed", error=str(e))
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
return [], t("file_parser.file_not_exist", path=file_path)
|
||||
|
||||
file_type = get_file_type(file_path)
|
||||
|
||||
if file_type == FileType.TXT:
|
||||
try:
|
||||
return parse_txt_file(file_path)
|
||||
finally:
|
||||
if temp_path:
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except Exception:
|
||||
pass
|
||||
elif file_type == FileType.PDF:
|
||||
try:
|
||||
return parse_pdf_file(file_path)
|
||||
finally:
|
||||
if temp_path:
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except Exception:
|
||||
pass
|
||||
elif file_type == FileType.EPUB:
|
||||
try:
|
||||
return parse_epub_file(file_path)
|
||||
finally:
|
||||
if temp_path:
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except Exception:
|
||||
pass
|
||||
elif file_type == FileType.MD:
|
||||
try:
|
||||
return parse_md_file(file_path)
|
||||
finally:
|
||||
if temp_path:
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except Exception:
|
||||
pass
|
||||
elif file_type == FileType.DOCX:
|
||||
try:
|
||||
return parse_docx_file(file_path)
|
||||
finally:
|
||||
if temp_path:
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
return [], t("file_parser.unsupported_format")
|
||||
|
||||
|
||||
def _split_paragraphs(text: str, min_length: int = MIN_PARAGRAPH_LENGTH) -> List[str]:
|
||||
"""
|
||||
Chia văn bản thành các đoạn văn
|
||||
|
||||
Args:
|
||||
text: Văn bản gốc
|
||||
min_length: Độ dài đoạn văn tối thiểu
|
||||
|
||||
Returns:
|
||||
Danh sách đoạn văn
|
||||
"""
|
||||
# Chia theo nhiều dòng mới
|
||||
raw_paragraphs = re.split(r'\n\s*\n+', text)
|
||||
|
||||
# Làm sạch và lọc
|
||||
paragraphs = []
|
||||
for para in raw_paragraphs:
|
||||
para = para.strip()
|
||||
# Xóa các điểm đánh dấu đặc biệt như tiêu đề chương
|
||||
para = re.sub(r'^(第\d+章|Chapter \d+|第 \d+ 章)[:::]?\s*', '', para)
|
||||
para = re.sub(r'^\s*\*+\s*|\s*\*+\s*$', '', para)
|
||||
|
||||
if len(para) >= min_length:
|
||||
paragraphs.append(para)
|
||||
|
||||
return paragraphs
|
||||
|
||||
|
||||
def estimate_word_count(text: str) -> int:
|
||||
"""Số lượng ký tự tiếng Trung ước tính (ước tính sơ bộ)"""
|
||||
chinese_count = len(re.findall(r'[\u4e00-\u9fff]', text))
|
||||
english_count = len(re.findall(r'\b[a-zA-Z]+\b', text))
|
||||
# Tiếng Trung được tính là 1 ký tự, tiếng Anh được tính là 0,5 ký tự
|
||||
return chinese_count + int(english_count * 0.5)
|
||||
|
||||
|
||||
def parse_novel_by_chapters(
|
||||
file_path: str,
|
||||
pattern_name: str = "default",
|
||||
custom_pattern: str = ""
|
||||
) -> Tuple[List[ChapterInfo], str]:
|
||||
"""
|
||||
Phân tích tệp tiểu thuyết theo chương
|
||||
|
||||
Args:
|
||||
file_path: Đường dẫn tệp
|
||||
pattern_name: Tên mẫu định sẵn
|
||||
custom_pattern: Biểu thức chính quy tùy chỉnh (nếu được cung cấp, sẽ được ưu tiên sử dụng)
|
||||
|
||||
Returns:
|
||||
(Danh sách chương, Thông tin trạng thái)
|
||||
"""
|
||||
try:
|
||||
# đọc văn bản
|
||||
file_type = get_file_type(file_path)
|
||||
|
||||
if file_type == FileType.TXT:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
text = f.read()
|
||||
elif file_type == FileType.PDF:
|
||||
import fitz
|
||||
text_parts = []
|
||||
doc = fitz.open(file_path)
|
||||
for page in doc:
|
||||
text_parts.append(page.get_text("text"))
|
||||
doc.close()
|
||||
text = "\n".join(text_parts)
|
||||
elif file_type == FileType.EPUB:
|
||||
from ebooklib import epub
|
||||
from bs4 import BeautifulSoup
|
||||
text_parts = []
|
||||
book = epub.read_epub(file_path)
|
||||
for item in book.get_items():
|
||||
if item.get_type() == epub.ITEM_DOCUMENT:
|
||||
soup = BeautifulSoup(item.get_content(), 'html.parser')
|
||||
text_parts.append(soup.get_text(separator="\n"))
|
||||
text = "\n".join(text_parts)
|
||||
elif file_type == FileType.MD:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
text = f.read()
|
||||
elif file_type == FileType.DOCX:
|
||||
from docx import Document
|
||||
doc = Document(file_path)
|
||||
text = ""
|
||||
for para in doc.paragraphs:
|
||||
text += para.text + "\n"
|
||||
else:
|
||||
return [], t("file_parser.unsupported_chapter_format")
|
||||
|
||||
# Xác định biểu thức chính quy để sử dụng
|
||||
if custom_pattern and custom_pattern.strip():
|
||||
patterns = [custom_pattern.strip()]
|
||||
elif pattern_name in CHAPTER_PATTERNS:
|
||||
patterns = CHAPTER_PATTERNS[pattern_name]
|
||||
else:
|
||||
patterns = CHAPTER_PATTERNS.get("default", list(CHAPTER_PATTERNS.values())[0])
|
||||
|
||||
# Tìm tất cả các tiêu đề chương
|
||||
chapters = []
|
||||
lines = text.split('\n')
|
||||
|
||||
current_chapter_num = 0
|
||||
current_chapter_title = ""
|
||||
current_chapter_content = []
|
||||
chapter_start_pos = 0
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
line_stripped = line.strip()
|
||||
is_chapter_header = False
|
||||
|
||||
# Kiểm tra xem có mẫu chương nào khớp không
|
||||
for pattern in patterns:
|
||||
if re.match(pattern, line_stripped, re.IGNORECASE):
|
||||
is_chapter_header = True
|
||||
break
|
||||
|
||||
if is_chapter_header:
|
||||
# Lưu chương trước
|
||||
if current_chapter_num > 0:
|
||||
content = '\n'.join(current_chapter_content).strip()
|
||||
if content:
|
||||
chapters.append(ChapterInfo(
|
||||
num=current_chapter_num,
|
||||
title=current_chapter_title,
|
||||
content=content,
|
||||
start_pos=chapter_start_pos,
|
||||
end_pos=i
|
||||
))
|
||||
|
||||
# Trích xuất số chương và tiêu đề
|
||||
current_chapter_num += 1
|
||||
current_chapter_title = line_stripped
|
||||
current_chapter_content = []
|
||||
chapter_start_pos = i
|
||||
else:
|
||||
# Bỏ qua dòng trống nhưng giữ nguyên nội dung
|
||||
if line_stripped or current_chapter_content:
|
||||
current_chapter_content.append(line)
|
||||
|
||||
# lưu chương cuối
|
||||
if current_chapter_num > 0 and current_chapter_content:
|
||||
content = '\n'.join(current_chapter_content).strip()
|
||||
if content:
|
||||
chapters.append(ChapterInfo(
|
||||
num=current_chapter_num,
|
||||
title=current_chapter_title,
|
||||
content=content,
|
||||
start_pos=chapter_start_pos,
|
||||
end_pos=len(lines)
|
||||
))
|
||||
|
||||
logger.info(f"Chapter parse done: {len(chapters)} chapters")
|
||||
return chapters, t("file_parser.chapter_parse_complete", count=len(chapters))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Chapter parse failed: {e}")
|
||||
return [], t("file_parser.chapter_parse_failed", error=str(e))
|
||||
|
||||
|
||||
def parse_novel_with_custom_template(
|
||||
file_path: str,
|
||||
custom_template: str
|
||||
) -> Tuple[List[ChapterInfo], str]:
|
||||
"""
|
||||
Sử dụng mẫu tùy chỉnh để phân tích tiểu thuyết
|
||||
|
||||
Args:
|
||||
file_path: Đường dẫn tệp
|
||||
custom_template: Mẫu chương tùy chỉnh (hỗ trợ chỗ dành sẵn placeholders)
|
||||
Ví dụ: "Chương {n} {title}" hoặc "Chapter {n}: {title}"
|
||||
|
||||
Returns:
|
||||
(Danh sách chương, Thông tin trạng thái)
|
||||
"""
|
||||
if not custom_template or not custom_template.strip():
|
||||
return parse_novel_by_chapters(file_path, "default", "")
|
||||
|
||||
# Chuyển đổi mẫu thành biểu thức chính quy
|
||||
# {n} hoặc {num} -> (\d+)
|
||||
# {title} -> (.*)
|
||||
pattern = custom_template.strip()
|
||||
pattern = re.escape(pattern)
|
||||
pattern = pattern.replace(r'\{n\}', r'(\d+)')
|
||||
pattern = pattern.replace(r'\{num\}', r'(\d+)')
|
||||
pattern = pattern.replace(r'\{title\}', r'(.*)')
|
||||
pattern = pattern.replace(r'\{.*?\}', r'.*') # Các placeholder khác
|
||||
|
||||
# Đảm bảo khớp với đầu dòng
|
||||
if not pattern.startswith('^'):
|
||||
pattern = '^' + pattern
|
||||
|
||||
return parse_novel_by_chapters(file_path, custom_pattern=pattern)
|
||||
|
||||
|
||||
def split_by_word_count(text: str, word_count: int) -> List[str]:
|
||||
"""
|
||||
Chia đoạn theo số chữ
|
||||
|
||||
Args:
|
||||
text: Văn bản gốc
|
||||
word_count: Số chữ mỗi đoạn
|
||||
|
||||
Returns:
|
||||
Danh sách văn bản sau khi chia đoạn
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return []
|
||||
|
||||
if word_count <= 0:
|
||||
raise ValueError(t("file_parser.word_count_positive"))
|
||||
|
||||
# Chia đều cho số từ
|
||||
segments = []
|
||||
total_length = len(text)
|
||||
start = 0
|
||||
|
||||
while start < total_length:
|
||||
end = start + word_count
|
||||
if end > total_length:
|
||||
end = total_length
|
||||
|
||||
segment = text[start:end].strip()
|
||||
if segment:
|
||||
segments.append(segment)
|
||||
|
||||
start = end
|
||||
|
||||
logger.info(f"Word count split done: {len(segments)} segments, ~{word_count} each")
|
||||
return segments
|
||||
|
||||
|
||||
def split_by_pattern(text: str, pattern: str, keep_marker: bool = True) -> List[str]:
|
||||
"""
|
||||
Chia đoạn theo văn bản/biến cố định
|
||||
|
||||
Args:
|
||||
text: Văn bản gốc
|
||||
mẫu: Đánh dấu đoạn (Biến hỗ trợ: % Chương (Chương), % Phần (Tiết), % Quay lại (Hồi), hoặc văn bản tùy chỉnh)
|
||||
keep_marker: Có giữ lại đánh dấu chia đoạn không
|
||||
|
||||
Returns:
|
||||
Danh sách văn bản sau khi chia đoạn
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return []
|
||||
|
||||
if not pattern or not pattern.strip():
|
||||
raise ValueError(t("file_parser.split_pattern_empty"))
|
||||
|
||||
# Nhận dạng thông minh: Nếu người dùng nhập "Chương x", "Chương X", v.v., nó sẽ tự động được chuyển đổi thành biểu thức chính quy
|
||||
# Kiểm tra xem nó có chứa sự kết hợp của "chương" và "chương", "phần" và "trở lại" không
|
||||
pattern_lower = pattern.strip().lower()
|
||||
|
||||
# Kiểm tra xem đó có phải là chế độ đơn giản hóa hay không (chẳng hạn như "Chương x", "Chương X")
|
||||
if pattern_lower in ['第x章', '第x章', '第x章', '第x章']:
|
||||
# Hỗ trợ cả chữ số Trung Quốc và chữ số Ả Rập, sử dụng + để đảm bảo khớp ít nhất một chữ số
|
||||
# Sử dụng cái nhìn phủ định để đảm bảo rằng "Chương x" không thể được theo sau bởi các ký tự tiếng Trung (ngoại trừ dấu cách và dấu chấm câu)
|
||||
# Định dạng phù hợp: Chương x, Chương x:, Chương x:, Chương x (dấu cách), Chương x (ngắt dòng sau dấu cách)
|
||||
# Hỗ trợ định dạng Markdown: ## Chương x
|
||||
# Nhưng nó không khớp: đây là chương đầu tiên, nội dung chương đầu tiên, v.v. (có chữ Hán sau đó)
|
||||
regex_pattern = r'^[\s# )'
|
||||
logger.info("Detected chapter pattern, auto-converting to regex")
|
||||
elif pattern_lower in ['第x节', '第x节', '第x节', '第x节']:
|
||||
regex_pattern = r'^\s*第\s*[一二三四五六七八九十百千万零〇0123456789]+\s*节\s*[::\s]*(?![\u4e00-\u9fff])'
|
||||
logger.info("Detected section pattern, auto-converting to regex")
|
||||
elif pattern_lower in ['第x回', '第x回', '第x回', '第x回']:
|
||||
regex_pattern = r'^\s*第\s*[一二三四五六七八九十百千万零〇0123456789]+\s*回\s*[::\s]*(?![\u4e00-\u9fff])'
|
||||
logger.info("Detected episode pattern, auto-converting to regex")
|
||||
elif '%章' in pattern_lower or '%节' in pattern_lower or '%回' in pattern_lower:
|
||||
# Sử dụng thay thế biến
|
||||
regex_pattern = pattern.strip()
|
||||
# %Chương -> Khớp "Chương X", "Chương x", v.v. (hỗ trợ chữ số Trung Quốc và Ả Rập)
|
||||
regex_pattern = regex_pattern.replace('%章', r'[一二三四五六七八九十百千万零〇0123456789]+\s*章')
|
||||
# %Phần -> Khớp "Phần X", "Phần x", v.v. (hỗ trợ chữ số Trung Quốc và Ả Rập)
|
||||
regex_pattern = regex_pattern.replace('%节', r'[一二三四五六七八九十百千万零〇0123456789]+\s*节')
|
||||
# %chapter -> Khớp "chương X", "chương x", v.v. (hỗ trợ chữ số Trung Quốc và Ả Rập)
|
||||
regex_pattern = regex_pattern.replace('%回', r'[一二三四五六七八九十百千万零〇0123456789]+\s*回')
|
||||
# Đảm bảo biểu thức chính quy bắt đầu bằng ^ (khớp với đầu dòng)
|
||||
if not regex_pattern.startswith('^'):
|
||||
regex_pattern = '^' + regex_pattern
|
||||
else:
|
||||
# Không chứa đánh dấu chương, sử dụng trực tiếp chế độ gốc
|
||||
regex_pattern = pattern.strip()
|
||||
|
||||
# Hãy thử chia theo mẫu
|
||||
try:
|
||||
# Nếu mã thông báo được giữ lại, hãy sử dụng biểu thức chính quy để tìm tất cả các vị trí phù hợp
|
||||
if keep_marker:
|
||||
# Tìm tất cả các vị trí phù hợp
|
||||
matches = list(re.finditer(regex_pattern, text, flags=re.MULTILINE | re.IGNORECASE))
|
||||
|
||||
if not matches:
|
||||
# Không khớp, trả lại toàn bộ văn bản
|
||||
logger.warning(f"No pattern match: {regex_pattern}, returning full text")
|
||||
return [text.strip()] if text.strip() else []
|
||||
|
||||
segments = []
|
||||
prev_end = 0
|
||||
|
||||
for match in matches:
|
||||
# Nhận thẻ phù hợp
|
||||
marker = match.group(0)
|
||||
|
||||
# Lấy nội dung trước dấu (nếu có)
|
||||
if prev_end < match.start():
|
||||
prev_content = text[prev_end:match.start()].strip()
|
||||
if prev_content:
|
||||
segments.append(prev_content)
|
||||
|
||||
# Thêm thẻ
|
||||
segments.append(marker.strip())
|
||||
|
||||
prev_end = match.end()
|
||||
|
||||
# Thêm đoạn cuối
|
||||
if prev_end < len(text):
|
||||
last_content = text[prev_end:].strip()
|
||||
if last_content:
|
||||
segments.append(last_content)
|
||||
|
||||
# Hợp nhất đánh dấu và nội dung
|
||||
result = []
|
||||
i = 0
|
||||
while i < len(segments):
|
||||
# Nếu nó hiện là một nhãn hiệu và có nội dung đằng sau nó
|
||||
if i + 1 < len(segments):
|
||||
result.append((segments[i] + segments[i + 1]).strip())
|
||||
i += 2
|
||||
else:
|
||||
# chỉ đánh dấu hoặc nội dung
|
||||
if segments[i].strip():
|
||||
result.append(segments[i].strip())
|
||||
i += 1
|
||||
|
||||
segments = result
|
||||
else:
|
||||
# Không giữ lại điểm đánh dấu và chia trực tiếp
|
||||
segments = re.split(regex_pattern, text, flags=re.MULTILINE | re.IGNORECASE)
|
||||
|
||||
# Dọn dẹp các đoạn văn trống
|
||||
segments = [seg.strip() for seg in segments if seg.strip()]
|
||||
|
||||
logger.info(f"Pattern split done: {len(segments)} segments")
|
||||
return segments
|
||||
|
||||
except re.error as e:
|
||||
raise ValueError(t("file_parser.invalid_regex", error=str(e)))
|
||||
Reference in New Issue
Block a user