extractor.py → MD-пайплайн (docx_to_markdown, pdf_to_markdown), obfuscator.py — новый пайплайн без docx-объектов
Deploy drhider / validate (push) Waiting to run
Deploy drhider / validate (push) Waiting to run
This commit is contained in:
+156
-148
@@ -1,95 +1,193 @@
|
|||||||
"""
|
"""
|
||||||
Извлечение текста из документов разных форматов.
|
Извлечение текста из документов — все форматы → Markdown.
|
||||||
|
|
||||||
Поддерживает:
|
Поддерживает:
|
||||||
- .docx (через python-docx)
|
- .docx → MD (python-docx: стили, жирный/курсив, таблицы)
|
||||||
- .pdf (через pdfplumber)
|
- .pdf → MD (pdfplumber: текст + таблицы, без форматирования)
|
||||||
- .doc (бинарный — не парсится)
|
- .doc (бинарный — не парсится, возвращает заглушку)
|
||||||
- .txt и прочие (как UTF-8)
|
- .txt и прочие → как есть
|
||||||
|
|
||||||
Также содержит:
|
Также содержит:
|
||||||
- _expand_zips — распаковка ZIP с защитой от ZIP-бомб
|
- expand_zips — распаковка ZIP с защитой от ZIP-бомб
|
||||||
- _convert_pdfs_to_docx — конвертация PDF → DOCX
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Tuple, Optional
|
from typing import List, Tuple
|
||||||
|
|
||||||
log = logging.getLogger("drhider")
|
log = logging.getLogger("drhider")
|
||||||
|
|
||||||
|
|
||||||
def extract_text(fname: str, content: bytes, ctype: str) -> Tuple[str, Optional[object]]:
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
"""Извлечь текст из одного файла.
|
# DOCX → Markdown
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def docx_to_markdown(content: bytes) -> str:
|
||||||
|
"""Конвертировать DOCX в Markdown с сохранением базового форматирования.
|
||||||
|
|
||||||
|
Маппинг стилей:
|
||||||
|
- Heading 1/2/3 → # / ## / ###
|
||||||
|
- Bold → **текст**
|
||||||
|
- Italic → *текст*
|
||||||
|
- Таблицы → | col1 | col2 | (с разделителем)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Бинарное содержимое .docx файла
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Строка в формате Markdown
|
||||||
|
"""
|
||||||
|
from docx import Document
|
||||||
|
|
||||||
|
doc = Document(io.BytesIO(content))
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
for para in doc.paragraphs:
|
||||||
|
if not para.text.strip():
|
||||||
|
lines.append("") # Пустая строка — разделитель абзацев
|
||||||
|
continue
|
||||||
|
|
||||||
|
text = para.text
|
||||||
|
|
||||||
|
# Применяем форматирование из runs (bold, italic)
|
||||||
|
if para.runs:
|
||||||
|
formatted_parts = []
|
||||||
|
for run in para.runs:
|
||||||
|
part = run.text
|
||||||
|
if run.bold:
|
||||||
|
part = f"**{part}**"
|
||||||
|
if run.italic:
|
||||||
|
part = f"*{part}*"
|
||||||
|
formatted_parts.append(part)
|
||||||
|
text = "".join(formatted_parts)
|
||||||
|
|
||||||
|
# Стиль абзаца
|
||||||
|
style_name = para.style.name if para.style else ""
|
||||||
|
|
||||||
|
if style_name.startswith("Heading"):
|
||||||
|
# Heading 1 → #, Heading 2 → ##, ...
|
||||||
|
try:
|
||||||
|
level = int(style_name.split()[-1])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
level = 1
|
||||||
|
prefix = "#" * min(level, 6)
|
||||||
|
lines.append(f"{prefix} {text}")
|
||||||
|
else:
|
||||||
|
lines.append(text)
|
||||||
|
|
||||||
|
# ── Таблицы ──
|
||||||
|
for table in doc.tables:
|
||||||
|
lines.append("") # Отступ перед таблицей
|
||||||
|
for ri, row in enumerate(table.rows):
|
||||||
|
cells = [cell.text.replace("\n", " ").strip() for cell in row.cells]
|
||||||
|
lines.append("| " + " | ".join(cells) + " |")
|
||||||
|
# Разделитель после первой строки (заголовка таблицы)
|
||||||
|
if ri == 0:
|
||||||
|
lines.append("|" + "|".join(["---"] * len(cells)) + "|")
|
||||||
|
lines.append("") # Отступ после таблицы
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# PDF → Markdown
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def pdf_to_markdown(content: bytes) -> str:
|
||||||
|
"""Конвертировать PDF в Markdown (только текст + таблицы, без форматирования).
|
||||||
|
|
||||||
|
pdfplumber не различает стили (bold/italic/heading), поэтому
|
||||||
|
всё извлекается как plain text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Бинарное содержимое .pdf файла
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Строка в формате Markdown (plain text + таблицы)
|
||||||
|
"""
|
||||||
|
import pdfplumber
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
with pdfplumber.open(io.BytesIO(content)) as pdf:
|
||||||
|
for page in pdf.pages:
|
||||||
|
# ── Текст страницы ──
|
||||||
|
text = page.extract_text()
|
||||||
|
if text:
|
||||||
|
lines.append(text)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# ── Таблицы ──
|
||||||
|
tables = page.extract_tables()
|
||||||
|
for table in tables:
|
||||||
|
if not table:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Чистим строки таблицы
|
||||||
|
clean_rows = [
|
||||||
|
[str(c or "").strip() for c in (row or [])]
|
||||||
|
for row in table
|
||||||
|
]
|
||||||
|
clean_rows = [r for r in clean_rows if any(r)]
|
||||||
|
|
||||||
|
if not clean_rows:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Выводим таблицу в MD
|
||||||
|
for ri, row in enumerate(clean_rows):
|
||||||
|
lines.append("| " + " | ".join(row) + " |")
|
||||||
|
if ri == 0:
|
||||||
|
lines.append("|" + "|".join(["---"] * len(row)) + "|")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# Единая точка входа
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def extract_text(fname: str, content: bytes, ctype: str = "") -> str:
|
||||||
|
"""Извлечь текст из одного файла → Markdown.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
fname: Имя файла (с расширением)
|
fname: Имя файла (с расширением)
|
||||||
content: Бинарное содержимое файла
|
content: Бинарное содержимое файла
|
||||||
ctype: MIME-тип (не используется в текущей версии)
|
ctype: MIME-тип (зарезервировано, не используется)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(text, docx_document_or_None):
|
Строка в формате Markdown
|
||||||
text — извлечённый текст (строка)
|
|
||||||
doc — объект python-docx Document или None
|
|
||||||
"""
|
"""
|
||||||
doc = None
|
|
||||||
text = ""
|
|
||||||
ext = os.path.splitext(fname)[1].lower()
|
ext = os.path.splitext(fname)[1].lower()
|
||||||
|
|
||||||
# ── .docx: извлекаем текст + сохраняем Document для замен с форматированием ──
|
# ── .docx: полный Markdown с форматированием ──
|
||||||
if ext == '.docx':
|
if ext == '.docx':
|
||||||
try:
|
try:
|
||||||
from docx import Document
|
return docx_to_markdown(content)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Если python-docx не установлен — читаем как plain text
|
return content.decode('utf-8', errors='replace')
|
||||||
text = content.decode('utf-8', errors='replace')
|
|
||||||
return text, None
|
|
||||||
|
|
||||||
doc = Document(io.BytesIO(content))
|
# ── .pdf: текст + таблицы ──
|
||||||
# Собираем текст из параграфов
|
|
||||||
paragraphs = [p.text for p in doc.paragraphs]
|
|
||||||
# Добавляем текст из таблиц
|
|
||||||
for table in doc.tables:
|
|
||||||
for row in table.rows:
|
|
||||||
row_text = " | ".join(cell.text for cell in row.cells)
|
|
||||||
paragraphs.append(row_text)
|
|
||||||
text = "\n".join(paragraphs)
|
|
||||||
|
|
||||||
# ── .pdf: извлекаем текст через pdfplumber ──
|
|
||||||
elif ext == '.pdf':
|
elif ext == '.pdf':
|
||||||
try:
|
try:
|
||||||
import pdfplumber
|
return pdf_to_markdown(content)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
text = content.decode('utf-8', errors='replace')
|
return content.decode('utf-8', errors='replace')
|
||||||
return text, None
|
|
||||||
|
|
||||||
parts = []
|
# ── .doc: бинарный — пока не поддерживается ──
|
||||||
with pdfplumber.open(io.BytesIO(content)) as pdf:
|
|
||||||
for page in pdf.pages:
|
|
||||||
# Текст страницы
|
|
||||||
t = page.extract_text()
|
|
||||||
if t:
|
|
||||||
parts.append(t)
|
|
||||||
# Текст из таблиц
|
|
||||||
for table in page.extract_tables():
|
|
||||||
for row in table:
|
|
||||||
row_str = " | ".join(str(c) if c else "" for c in row)
|
|
||||||
parts.append(row_str)
|
|
||||||
text = "\n".join(parts)
|
|
||||||
|
|
||||||
# ── .doc: бинарный формат — без libreoffice не парсим ──
|
|
||||||
elif ext == '.doc':
|
elif ext == '.doc':
|
||||||
text = "[DOC binary — not parsed]"
|
return "[DOC binary — not supported. Convert to .docx first.]"
|
||||||
return text, None
|
|
||||||
|
|
||||||
# ── .txt и прочие: читаем как UTF-8 ──
|
# ── .txt и прочие ──
|
||||||
else:
|
else:
|
||||||
text = content.decode('utf-8', errors='replace')
|
return content.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
return text, doc
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# ZIP-распаковка (без изменений)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def expand_zips(files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, str]]:
|
def expand_zips(files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, str]]:
|
||||||
"""Распаковать ZIP-файлы в списке, заменив их содержимым.
|
"""Распаковать ZIP-файлы в списке, заменив их содержимым.
|
||||||
@@ -110,14 +208,12 @@ def expand_zips(files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, s
|
|||||||
result: List[Tuple[str, bytes, str]] = []
|
result: List[Tuple[str, bytes, str]] = []
|
||||||
|
|
||||||
for fname, content, ctype in files:
|
for fname, content, ctype in files:
|
||||||
# Пропускаем не-ZIP
|
|
||||||
if not fname.lower().endswith('.zip'):
|
if not fname.lower().endswith('.zip'):
|
||||||
result.append((fname, content, ctype))
|
result.append((fname, content, ctype))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with zipfile.ZipFile(io.BytesIO(content)) as zf:
|
with zipfile.ZipFile(io.BytesIO(content)) as zf:
|
||||||
# Проверка: не более 500 файлов в архиве
|
|
||||||
if len(zf.infolist()) > 500:
|
if len(zf.infolist()) > 500:
|
||||||
log.warning("ZIP too many files, skipping: %s", fname)
|
log.warning("ZIP too many files, skipping: %s", fname)
|
||||||
result.append((fname, content, ctype))
|
result.append((fname, content, ctype))
|
||||||
@@ -126,28 +222,24 @@ def expand_zips(files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, s
|
|||||||
total_uncompressed = 0
|
total_uncompressed = 0
|
||||||
|
|
||||||
for info in zf.infolist():
|
for info in zf.infolist():
|
||||||
# Пропускаем директории
|
|
||||||
if info.is_dir():
|
if info.is_dir():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Проверка на ZIP-бомбу: ratio
|
|
||||||
if info.compress_size > 0:
|
if info.compress_size > 0:
|
||||||
ratio = info.file_size / info.compress_size
|
ratio = info.file_size / info.compress_size
|
||||||
if ratio > 100: # Файл сжимается более чем в 100 раз
|
if ratio > 100:
|
||||||
log.warning(
|
log.warning(
|
||||||
"ZIP bomb ratio %.0f:1, skipping: %s", ratio, fname
|
"ZIP bomb ratio %.0f:1, skipping: %s", ratio, fname
|
||||||
)
|
)
|
||||||
result.append((fname, content, ctype))
|
result.append((fname, content, ctype))
|
||||||
break # Пропускаем весь архив
|
break
|
||||||
|
|
||||||
# Декодируем имя файла: cp437 → utf-8
|
|
||||||
name = info.filename
|
name = info.filename
|
||||||
try:
|
try:
|
||||||
name = name.encode("cp437").decode("utf-8", errors="replace")
|
name = name.encode("cp437").decode("utf-8", errors="replace")
|
||||||
except (UnicodeDecodeError, UnicodeEncodeError):
|
except (UnicodeDecodeError, UnicodeEncodeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Защита от path traversal
|
|
||||||
name = os.path.basename(name)
|
name = os.path.basename(name)
|
||||||
if (
|
if (
|
||||||
not name
|
not name
|
||||||
@@ -158,11 +250,10 @@ def expand_zips(files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, s
|
|||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Читаем и проверяем накопительный размер
|
|
||||||
inner_data = zf.read(info)
|
inner_data = zf.read(info)
|
||||||
total_uncompressed += len(inner_data)
|
total_uncompressed += len(inner_data)
|
||||||
|
|
||||||
if total_uncompressed > 500 * 1024 * 1024: # 500 MB
|
if total_uncompressed > 500 * 1024 * 1024:
|
||||||
log.warning(
|
log.warning(
|
||||||
"ZIP uncompressed limit exceeded, stopping: %s", fname
|
"ZIP uncompressed limit exceeded, stopping: %s", fname
|
||||||
)
|
)
|
||||||
@@ -175,86 +266,3 @@ def expand_zips(files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, s
|
|||||||
result.append((fname, content, ctype))
|
result.append((fname, content, ctype))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def convert_pdfs_to_docx(
|
|
||||||
files: List[Tuple[str, bytes, str]],
|
|
||||||
) -> List[Tuple[str, bytes, str]]:
|
|
||||||
"""Конвертировать PDF-файлы в DOCX через pdfplumber.
|
|
||||||
|
|
||||||
Не-PDF файлы проходят без изменений.
|
|
||||||
При совпадении имён к имени добавляется суффикс '_из_pdf'.
|
|
||||||
|
|
||||||
Конвертация:
|
|
||||||
- Текст страницы → параграфы DOCX
|
|
||||||
- Таблицы → таблицы DOCX со стилем 'Table Grid'
|
|
||||||
|
|
||||||
Args:
|
|
||||||
files: [(filename, content_bytes, content_type), ...]
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Новый список файлов (PDF заменены на DOCX)
|
|
||||||
"""
|
|
||||||
import pdfplumber
|
|
||||||
from docx import Document as DocxDocument
|
|
||||||
|
|
||||||
result: List[Tuple[str, bytes, str]] = []
|
|
||||||
existing_names = {f[0] for f in files}
|
|
||||||
|
|
||||||
for fname, content, ctype in files:
|
|
||||||
# Пропускаем не-PDF
|
|
||||||
if not fname.lower().endswith('.pdf'):
|
|
||||||
result.append((fname, content, ctype))
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
doc = DocxDocument()
|
|
||||||
|
|
||||||
with pdfplumber.open(io.BytesIO(content)) as pdf:
|
|
||||||
for page in pdf.pages:
|
|
||||||
# ── Таблицы ──
|
|
||||||
tables = page.extract_tables()
|
|
||||||
for table in tables:
|
|
||||||
if not table:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Чистим строки: убираем полностью пустые
|
|
||||||
rows = [
|
|
||||||
[str(c or "").strip() for c in (row or [])]
|
|
||||||
for row in table
|
|
||||||
]
|
|
||||||
rows = [r for r in rows if any(r)]
|
|
||||||
|
|
||||||
if rows:
|
|
||||||
t = doc.add_table(rows=len(rows), cols=len(rows[0]))
|
|
||||||
t.style = 'Table Grid'
|
|
||||||
for ri, row in enumerate(rows):
|
|
||||||
for ci, cell_text in enumerate(row):
|
|
||||||
t.rows[ri].cells[ci].text = cell_text
|
|
||||||
|
|
||||||
# ── Текст ──
|
|
||||||
text = page.extract_text()
|
|
||||||
if text:
|
|
||||||
for line in text.split('\n'):
|
|
||||||
line = line.strip()
|
|
||||||
if line:
|
|
||||||
doc.add_paragraph(line)
|
|
||||||
|
|
||||||
# Сохраняем DOCX в буфер
|
|
||||||
buf = io.BytesIO()
|
|
||||||
doc.save(buf)
|
|
||||||
|
|
||||||
# Формируем новое имя
|
|
||||||
new_name = fname[:-4] + '.docx'
|
|
||||||
if new_name in existing_names:
|
|
||||||
new_name = fname[:-4] + '_из_pdf.docx'
|
|
||||||
existing_names.add(new_name)
|
|
||||||
|
|
||||||
result.append((new_name, buf.getvalue(), ctype))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log.warning("PDF→DOCX error for %s: %s", fname, e)
|
|
||||||
# При ошибке — оставляем оригинальный PDF
|
|
||||||
result.append((fname, content, ctype))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|||||||
+17
-19
@@ -8,6 +8,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Tuple, Callable, Optional
|
from typing import Dict, List, Tuple, Callable, Optional
|
||||||
|
|
||||||
@@ -54,32 +55,30 @@ class TwoPassObfuscator:
|
|||||||
) -> Tuple[bytes, str]:
|
) -> Tuple[bytes, str]:
|
||||||
"""Обфусцировать список файлов.
|
"""Обфусцировать список файлов.
|
||||||
|
|
||||||
|
Все форматы → Markdown → замена → результат.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
files: [(filename, content_bytes, content_type), ...]
|
files: [(filename, content_bytes, content_type), ...]
|
||||||
content_type — MIME-тип (может быть пустой строкой)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(zip_bytes, csv_string):
|
(zip_bytes, csv_string):
|
||||||
zip_bytes — ZIP-архив с обфусцированными файлами + mapping.csv
|
zip_bytes — ZIP-архив с обфусцированными .md файлами + mapping.csv
|
||||||
csv_string — содержимое mapping.csv как строка
|
csv_string — содержимое mapping.csv как строка
|
||||||
"""
|
"""
|
||||||
# ── Предобработка: распаковать ZIP, конвертировать PDF ──
|
# ── Предобработка: распаковать ZIP ──
|
||||||
files = extractor.expand_zips(files)
|
files = extractor.expand_zips(files)
|
||||||
files = extractor.convert_pdfs_to_docx(files)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# ── Проход 1: сбор сущностей ──
|
# ── Проход 1: сбор сущностей ──
|
||||||
|
# Извлекаем Markdown из каждого файла
|
||||||
all_texts: Dict[str, str] = {}
|
all_texts: Dict[str, str] = {}
|
||||||
all_docx: Dict[str, object] = {}
|
|
||||||
|
|
||||||
for fname, content, ctype in files:
|
for fname, content, ctype in files:
|
||||||
text, doc = extractor.extract_text(fname, content, ctype)
|
text = extractor.extract_text(fname, content, ctype)
|
||||||
all_texts[fname] = text
|
all_texts[fname] = text
|
||||||
if doc is not None:
|
|
||||||
all_docx[fname] = doc
|
|
||||||
|
|
||||||
# Regex-сканирование (быстрое, локальное)
|
# Regex-сканирование (быстрое, локальное)
|
||||||
if text and text != "[DOC binary — not parsed]":
|
if text and not text.startswith("[DOC binary"):
|
||||||
scanner.scan_regex(text, self._mapping)
|
scanner.scan_regex(text, self._mapping)
|
||||||
|
|
||||||
# LLM-сканирование (медленное, сетевое — только если есть клиент)
|
# LLM-сканирование (медленное, сетевое — только если есть клиент)
|
||||||
@@ -98,19 +97,18 @@ class TwoPassObfuscator:
|
|||||||
obf_content = content # По умолчанию — без изменений
|
obf_content = content # По умолчанию — без изменений
|
||||||
|
|
||||||
if fname.endswith('.doc'):
|
if fname.endswith('.doc'):
|
||||||
# .doc — бинарный формат, оставляем как есть
|
# .doc — бинарный формат, пока не поддерживается
|
||||||
pass
|
pass
|
||||||
elif fname in all_docx:
|
|
||||||
# DOCX: замена с сохранением форматирования
|
|
||||||
obf_content = replacer.replace_in_docx(
|
|
||||||
all_docx[fname], self._mapping, self._sorted_keys
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# Plain text / PDF-текст: простая замена
|
# Все форматы → Markdown → замена в тексте
|
||||||
txt = all_texts.get(fname, '')
|
md_text = all_texts.get(fname, '')
|
||||||
obf_content = replacer.replace_in_text(
|
replaced = replacer.apply_replacements(
|
||||||
txt, fname, self._mapping, self._sorted_keys
|
md_text, self._mapping, self._sorted_keys
|
||||||
)
|
)
|
||||||
|
# Имя файла: заменить расширение на .md
|
||||||
|
md_name = os.path.splitext(fname)[0] + '.md'
|
||||||
|
obf_content = replaced.encode('utf-8')
|
||||||
|
fname = md_name
|
||||||
|
|
||||||
results.append((fname, obf_content))
|
results.append((fname, obf_content))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user