holypug1274 commited on
Commit
1d1716e
·
1 Parent(s): e9effd4

deploy: privacy-sanitizer backend 2026-05-05

Browse files
Files changed (2) hide show
  1. app/services/extractor.py +91 -2
  2. requirements.txt +2 -0
app/services/extractor.py CHANGED
@@ -1,10 +1,11 @@
1
  """Document text extraction service.
2
 
3
  Pure functions to extract plain text from uploaded documents.
4
- Supported formats: PDF, DOCX, TXT, MD.
5
  """
6
  from __future__ import annotations
7
 
 
8
  import io
9
 
10
 
@@ -78,6 +79,79 @@ def extract_plain(data: bytes) -> str:
78
  raise ExtractionError("Could not decode the file as text.")
79
 
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  # Mapping content-type / extension → handler
82
  _HANDLERS = {
83
  "pdf": extract_pdf,
@@ -85,6 +159,9 @@ _HANDLERS = {
85
  "txt": extract_plain,
86
  "md": extract_plain,
87
  "markdown": extract_plain,
 
 
 
88
  }
89
 
90
 
@@ -98,14 +175,26 @@ def detect_kind(filename: str, content_type: str | None) -> str | None:
98
  return "md"
99
  if name.endswith(".txt"):
100
  return "txt"
 
 
 
 
 
 
101
 
102
  ct = (content_type or "").lower()
103
  if "pdf" in ct:
104
  return "pdf"
105
  if "wordprocessingml" in ct or "docx" in ct:
106
  return "docx"
 
 
107
  if "markdown" in ct:
108
  return "md"
 
 
 
 
109
  if ct.startswith("text/"):
110
  return "txt"
111
  return None
@@ -114,6 +203,6 @@ def detect_kind(filename: str, content_type: str | None) -> str | None:
114
  def extract_text(filename: str, content_type: str | None, data: bytes) -> str:
115
  kind = detect_kind(filename, content_type)
116
  if kind is None:
117
- raise ExtractionError("Unsupported file type. Allowed: PDF, DOCX, TXT, MD.")
118
  handler = _HANDLERS[kind]
119
  return handler(data)
 
1
  """Document text extraction service.
2
 
3
  Pure functions to extract plain text from uploaded documents.
4
+ Supported formats: PDF, DOCX, TXT, MD, PPTX, CSV, HTML.
5
  """
6
  from __future__ import annotations
7
 
8
+ import csv
9
  import io
10
 
11
 
 
79
  raise ExtractionError("Could not decode the file as text.")
80
 
81
 
82
+ def extract_pptx(data: bytes) -> str:
83
+ try:
84
+ from pptx import Presentation
85
+ except ImportError as e: # pragma: no cover
86
+ raise ExtractionError("PPTX support not installed on the server.") from e
87
+
88
+ try:
89
+ prs = Presentation(io.BytesIO(data))
90
+ except Exception as e:
91
+ raise ExtractionError("Could not parse PPTX — file may be corrupted.") from e
92
+
93
+ parts: list[str] = []
94
+ for slide in prs.slides:
95
+ for shape in slide.shapes:
96
+ if hasattr(shape, "text") and shape.text and shape.text.strip():
97
+ parts.append(shape.text.strip())
98
+
99
+ if not parts:
100
+ raise ExtractionError("No text could be extracted from this PPTX.")
101
+
102
+ return "\n".join(parts).strip()
103
+
104
+
105
+ def extract_csv(data: bytes) -> str:
106
+ for encoding in ("utf-8", "utf-8-sig", "latin-1"):
107
+ try:
108
+ text = data.decode(encoding)
109
+ break
110
+ except UnicodeDecodeError:
111
+ continue
112
+ else:
113
+ raise ExtractionError("Could not decode the CSV file.")
114
+
115
+ try:
116
+ reader = csv.reader(io.StringIO(text))
117
+ rows = [" | ".join(cell.strip() for cell in row if cell.strip()) for row in reader]
118
+ result = "\n".join(r for r in rows if r)
119
+ except Exception as e:
120
+ raise ExtractionError("Could not parse CSV.") from e
121
+
122
+ if not result.strip():
123
+ raise ExtractionError("No text could be extracted from this CSV.")
124
+
125
+ return result.strip()
126
+
127
+
128
+ def extract_html(data: bytes) -> str:
129
+ try:
130
+ from bs4 import BeautifulSoup
131
+ except ImportError as e: # pragma: no cover
132
+ raise ExtractionError("HTML support not installed on the server.") from e
133
+
134
+ for encoding in ("utf-8", "utf-8-sig", "latin-1"):
135
+ try:
136
+ text = data.decode(encoding)
137
+ break
138
+ except UnicodeDecodeError:
139
+ continue
140
+ else:
141
+ raise ExtractionError("Could not decode the HTML file.")
142
+
143
+ soup = BeautifulSoup(text, "html.parser")
144
+ # Remove script and style elements
145
+ for tag in soup(["script", "style", "head", "meta", "link"]):
146
+ tag.decompose()
147
+
148
+ result = soup.get_text(separator="\n", strip=True)
149
+ if not result.strip():
150
+ raise ExtractionError("No text could be extracted from this HTML.")
151
+
152
+ return result.strip()
153
+
154
+
155
  # Mapping content-type / extension → handler
156
  _HANDLERS = {
157
  "pdf": extract_pdf,
 
159
  "txt": extract_plain,
160
  "md": extract_plain,
161
  "markdown": extract_plain,
162
+ "pptx": extract_pptx,
163
+ "csv": extract_csv,
164
+ "html": extract_html,
165
  }
166
 
167
 
 
175
  return "md"
176
  if name.endswith(".txt"):
177
  return "txt"
178
+ if name.endswith(".pptx"):
179
+ return "pptx"
180
+ if name.endswith(".csv"):
181
+ return "csv"
182
+ if name.endswith(".html") or name.endswith(".htm"):
183
+ return "html"
184
 
185
  ct = (content_type or "").lower()
186
  if "pdf" in ct:
187
  return "pdf"
188
  if "wordprocessingml" in ct or "docx" in ct:
189
  return "docx"
190
+ if "presentationml" in ct or "pptx" in ct:
191
+ return "pptx"
192
  if "markdown" in ct:
193
  return "md"
194
+ if "csv" in ct:
195
+ return "csv"
196
+ if "html" in ct:
197
+ return "html"
198
  if ct.startswith("text/"):
199
  return "txt"
200
  return None
 
203
  def extract_text(filename: str, content_type: str | None, data: bytes) -> str:
204
  kind = detect_kind(filename, content_type)
205
  if kind is None:
206
+ raise ExtractionError("Unsupported file type. Allowed: PDF, DOCX, TXT, MD, PPTX, CSV, HTML.")
207
  handler = _HANDLERS[kind]
208
  return handler(data)
requirements.txt CHANGED
@@ -14,3 +14,5 @@ python-dateutil
14
  python-multipart==0.0.20
15
  pypdf==5.1.0
16
  python-docx==1.1.2
 
 
 
14
  python-multipart==0.0.20
15
  pypdf==5.1.0
16
  python-docx==1.1.2
17
+ python-pptx==1.0.2
18
+ beautifulsoup4==4.13.4