Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

176 строки
6.5KB

  1. """Export configured EPUB books into chapter-scoped source-text Markdown."""
  2. import json
  3. import posixpath
  4. import re
  5. import sys
  6. import zipfile
  7. from pathlib import Path
  8. from xml.etree import ElementTree as ET
  9. from lxml import html
  10. ROOT = Path(__file__).resolve().parent.parent / "library"
  11. XLINK = "{http://www.w3.org/1999/xlink}href"
  12. BLOCK_TAGS = {
  13. "address", "article", "aside", "blockquote", "br", "caption", "div",
  14. "figcaption", "figure", "footer", "h1", "h2", "h3", "h4", "h5", "h6",
  15. "header", "hr", "li", "main", "nav", "ol", "p", "section", "table",
  16. "tbody", "td", "tfoot", "th", "thead", "tr", "ul",
  17. }
  18. def ch(number):
  19. return f"Chapter-{number:02d}"
  20. def slug(title, limit=60):
  21. text = re.sub(r"^\d+(\.\d+)?[a-z]?\s*[-)]\s*", "", title.strip())
  22. text = re.sub(r"[:,.'\"‘’“”?/\\()]", "", text)
  23. text = re.sub(r"[^\w\s-]", "", text)
  24. words, output = text.split(), ""
  25. for word in words:
  26. candidate = f"{output}-{word}" if output else word
  27. if len(candidate) > limit:
  28. break
  29. output = candidate
  30. return re.sub(r"-+", "-", output) or "Untitled"
  31. def folder_name(key, title):
  32. return key if key in ("Front-Matter", "Back-Matter") else f"{key}-{slug(title)}"
  33. def outrageous_sections():
  34. front = ["titlepage.xhtml", "text/part0000.html", "text/part0001.html"]
  35. front += [f"text/part{number:04d}.html" for number in range(2, 8)]
  36. titles = [
  37. "Take The OUTRAGEOUS Path To OUTRAGEOUS Success",
  38. "Defining Advertising And The Three Possible Responses",
  39. "Applying OUTRAGEOUS Advertising",
  40. "Using The Most Powerful Tool - Direct Mail",
  41. "Presenting Critical Components",
  42. "Going The Extra Mile For Extra Money",
  43. "Harnessing The Power Of Testimonials",
  44. "Using Premiums To Skyrocket Response",
  45. "Reminding Sequentially Until They Buy",
  46. "Swiping Ideas And Deploying Them Into Your Business",
  47. "Celebrating Holidays & Special Occasions",
  48. "Sampling Some Really Outrageous Ideas",
  49. "Putting All The Pieces Together",
  50. ]
  51. sections = [("Front-Matter", "Front matter, endorsements, foreword, preface, and contents", front)]
  52. for number, title in enumerate(titles, start=1):
  53. sections.append((ch(number), title, [f"text/part{number + 7:04d}.html"]))
  54. sections.append((
  55. "Back-Matter",
  56. "Appendices A-C and bonus material",
  57. [f"text/part{number:04d}.html" for number in range(21, 25)],
  58. ))
  59. return sections
  60. BOOKS = {
  61. "Outrageous Advertising That's Outrageously Successful": outrageous_sections,
  62. }
  63. def epub_spine(epub):
  64. container = ET.fromstring(epub.read("META-INF/container.xml"))
  65. opf_path = container.find(".//{*}rootfile").attrib["full-path"]
  66. opf = ET.fromstring(epub.read(opf_path))
  67. base = posixpath.dirname(opf_path)
  68. manifest = {
  69. item.attrib["id"]: item.attrib
  70. for item in opf.findall(".//{*}manifest/{*}item")
  71. }
  72. return [
  73. posixpath.normpath(posixpath.join(base, manifest[item.attrib["idref"]]["href"]))
  74. for item in opf.findall(".//{*}spine/{*}itemref")
  75. ]
  76. def normalize_text(parts):
  77. text = "".join(parts).replace("\u00a0", " ")
  78. text = re.sub(r"[ \t]+", " ", text)
  79. text = re.sub(r" *\n *", "\n", text)
  80. text = re.sub(r"\n{3,}", "\n\n", text)
  81. return text.strip()
  82. def document_text(epub, document, image_ocr):
  83. root = html.fromstring(epub.read(document))
  84. for unwanted in root.xpath("//*[local-name()='style' or local-name()='script']"):
  85. unwanted.drop_tree()
  86. parts = []
  87. def walk(element):
  88. tag = element.tag.split("}")[-1].lower() if isinstance(element.tag, str) else ""
  89. if tag in ("img", "image"):
  90. href = element.get("src") or element.get(XLINK) or element.get("xlink:href") or element.get("href")
  91. if href:
  92. ref = posixpath.normpath(posixpath.join(posixpath.dirname(document), href))
  93. record = image_ocr.get(ref, {})
  94. asset = record.get("asset", ref)
  95. parts.append(f"\n\n[Embedded image: {asset}]\n")
  96. ocr_text = record.get("text", "").strip()
  97. if ocr_text:
  98. parts.append("[OCR from embedded image; verify layout in the extracted image asset.]\n")
  99. parts.append(ocr_text + "\n")
  100. else:
  101. parts.append("[No readable text recognized; consult the extracted image asset.]\n")
  102. return
  103. if element.text:
  104. parts.append(element.text)
  105. for child in element:
  106. walk(child)
  107. if child.tail:
  108. parts.append(child.tail)
  109. if tag in BLOCK_TAGS:
  110. parts.append("\n")
  111. walk(root)
  112. return normalize_text(parts)
  113. def main(book):
  114. book_root = ROOT / book
  115. epub_path = book_root / "source-file.epub"
  116. ocr_path = book_root / "ocr-embedded-images.json"
  117. image_ocr = json.loads(ocr_path.read_text(encoding="utf-8")) if ocr_path.exists() else {}
  118. sections = BOOKS[book]()
  119. with zipfile.ZipFile(epub_path) as epub:
  120. spine = epub_spine(epub)
  121. covered = [item for _, _, items in sections for item in items]
  122. missing = [item for item in spine if item not in covered]
  123. extras = [item for item in covered if item not in spine]
  124. duplicates = sorted({item for item in covered if covered.count(item) > 1})
  125. if missing or extras or duplicates:
  126. raise SystemExit(f"spine mismatch missing={missing} extras={extras} duplicates={duplicates}")
  127. for key, title, documents in sections:
  128. body = []
  129. for document in documents:
  130. body.append(f"<!-- EPUB item {document} -->\n{document_text(epub, document, image_ocr)}\n")
  131. out = book_root / folder_name(key, title)
  132. out.mkdir(exist_ok=True)
  133. header = [
  134. f"# {book} — {key}: {title}",
  135. f"- **Source**: /library/{book}/source-file.epub",
  136. f"- **EPUB items**: {', '.join(documents)}",
  137. "- **Image note**: Embedded images were extracted to the book's images folder; OCR is included beside image markers when text was recognized.",
  138. ]
  139. (out / f"{key}-source-text.md").write_text(
  140. "\n".join(header) + "\n\n---\n\n" + "\n".join(body),
  141. encoding="utf-8",
  142. newline="\n",
  143. )
  144. print(f"{key:14} items={documents} {title[:55]}")
  145. print(f"spine items covered exactly once: {len(spine)}")
  146. if __name__ == "__main__":
  147. main(sys.argv[1])

Powered by TurnKey Linux.