электронной почте HTML с помощью BeautifulSoup
В документации к BeautifulSoup говорится, что он экономит программистам часы или дни работы. Это преуменьшение.
Я создал программу чтения электронной почты IMAP , используя IMAPClient и Flask. Устройство для чтения электронной почты IMAP декодирует электронное письмо в действительный HTML. Затем ему нужно отобразить этот HTML через браузер. Пока все работает нормально.
В этом посте я описываю, как я реализовал опцию в моем IMAP E-Mail Reader для блокировки небезопасных ресурсов в HTML. Для этого я использую BeautifulSoup и операции регулярных выражений Python.
Зачем блокировать небезопасные ресурсы
Внешние ресурсы в HTML обычно представляют собой файлы, которые включены в веб-страницу. Примерами являются images, stylesheets, библиотеки JavaScript. Проблема в том, что они подключают вас к удаленным системам. Если вы заботитесь о конфиденциальности, вы хотите избежать этого.
В своем браузере Firefox я использую uBlockOrigin, это не просто блокировщик рекламы. С сайта:
'uBlock Origin - это бесплатное и кроссплатформенное расширение браузера с открытым исходным кодом для фильтрации контента, в первую очередь направленное на нейтрализацию вторжения в частную жизнь эффективным, userдружественным методом'.
Большинство электронных писем HTML , которые мы получаем, содержат ссылки на внешние ресурсы, часто images. Подключившись к такому изображению, вы можете быть отслежены. Многие программы электронной почты пытаются блокировать эти внешние ресурсы по умолчанию и предлагают опцию их разрешения. В результате электронная почта может выглядеть странно, а конфиденциальность имеет свою цену.
Также могут быть письма HTML с намеренно добавленным кодом, Javascript, чтобы взломать ваш компьютер. Мы должны удалить этот код.
Почему BeautifulSoup
Вот некоторые способы, которые мы можем использовать для фильтрации электронной почты HTML :
- re: Python операции с регулярными выражениями
- BeautifulSoup: библиотека для соскабливания информации с веб-страниц и модификации содержимого
- Scrapy: веб-скрейпинг framework
Python's re очень низкого уровня. Я часто использую его, но здесь он не кажется лучшим выбором. Scrapy - это framework и, вероятно, перебор. Остается BeautifulSoup.
Производительность не так важна для моего IMAP E-Mail Reader. Мне не нужно фильтровать тысячи страниц. Фильтрация нужна только при показе письма. В высокопроизводительной среде мы можем хранить отфильтрованные письма.
Парсер BeautifulSoup
Вначале у меня возникли проблемы с парсером (по умолчанию) 'html.parser'. Он работал, но в нескольких тегах изображений url изображения не заменялся. Конечно, это была моя ошибка, TLDR. BeautifulSoup рекомендует использовать либо парсер lxml, либо парсер html5lib.
Поскольку я хотел чистое решение Python , я выбрал html5parser, который обрабатывает HTML так, как это делает веб-браузер. Это очень важно. Без BeautifulSoup пришлось бы потратить целую вечность, чтобы написать код, способный справиться с (намеренно) плохим HTML.
Вывод BeautifulSoup
BeautifulSoup - это библиотека для извлечения данных из HTML. Это хорошо, но в нашем случае мы сначала удаляем элементы и изменяем элементы, а затем показываем результат в браузере.
BeautifulSoup имеет несколько вариантов вывода, но он всегда изменяет по крайней мере несколько элементов. Иногда это может быть хорошо, например, добавление недостающих тегов, но в других случаях это может быть не то, что мы хотим. Что-то вроде: давайте подождем и посмотрим.
Как блокировать небезопасные ресурсы
Самые важные вещи, которые мы должны сделать:
- Полностью удалить небезопасные ресурсы
- Заменить небезопасные ресурсы
- Исправить небезопасные ресурсы
Полностью удалить небезопасные ресурсы
Мы всегда должны удалять все Javascript из письма HTML . Мы также хотим удалить другие элементы, например, ссылки на stylesheets.
Замена небезопасных ресурсов
Если мы удалим images , то письмо может испортиться. Чтобы предотвратить это, мы заменяем images в письме локальным изображением, прозрачным пикселем.
Исправление плохих ресурсов
Некоторые ссылки могут не содержать атрибута:
target="_blank"
Мы также хотим добавить этот атрибут:
rel="noopener noreferrer"
Это предотвращает передачу информации о реферере на целевой сайт.
Но подождите, есть еще CSS
В письме HTML могут быть CSS-стили, ссылающиеся на внешние images, шрифты. Сначала я хотел использовать пакет CSSUtils, но он не очень прощающий. Например:
background-image: url ('my_url')
генерирует исключение, потому что между 'url' и '(' есть пробел. Я также не смог найти другой подходящий пакет, поэтому решил использовать операции регулярных выражений Python.
Я хочу заменить CSS-код, который содержит 'url()' в части значения. На странице HTML мы можем иметь:
- Встроенный CSS
- CSS-элементы
Чтобы сократить код, для встроенного CSS мы полностью удаляем свойство, а для CSS-элементов заменяем часть значения на 'url()'.
HTMLMailFixer Class
Для фильтрации писем HTML я создал класс HTMLMailFixer, код которого прост для понимания.
# html_mail_fixer.py
from bs4 import BeautifulSoup
import re
class HTMLMailFixer:
def __init__(
self,
parser='html5lib',
):
self.__parser = parser
self.forbidden = [
'script', 'object', 'iframe',
]
self.__allow_remote_resources = None
self.__block_img_url = None
self.__soup = None
def __remove_forbidden_elems(self):
for elem in self.__soup():
if elem.name in self.forbidden:
# remove
elem.extract()
def __fix_a_elems(self):
for a_elem in self.__soup.find_all('a'):
# add/replace
a_elem['target'] = '_blank'
a_elem['rel'] = 'noopener noreferrer'
def __remove_link_elems(self):
for link_elem in self.__soup.find_all('link'):
link_href = link_elem.get('href')
if link_href is None:
continue
# remove
link_elem.extract()
def __fix_img_elems(self):
for img_elem in self.__soup.find_all('img'):
img_src = img_elem.get('src')
if img_src is None:
continue
# replace
img_elem['src'] = self.__block_img_url
def __fix_style_elems(self):
"""
objective: remove any property with a value starting with 'url('
actual: replace value 'url(url)' by 'url()'
"""
match_url_start_pattern = re.compile(r':\s*url\s*\(', re.I)
for style_elem in self.__soup.find_all('style'):
# 'contents': [
# '\n.section-block {\n padding: 1em;\n background-image: url(https://whatever_image_1);\n}\n#logo {\n margin-top: 10em;\n background-image: url(https://whatever_image_1);\n}\n'
# ]
new_contents = []
for content in style_elem.contents:
chunks = re.split(match_url_start_pattern, content)
for i, chunk in enumerate(chunks):
if i == 0:
continue
chunks[i] = re.sub(r'.*?\)', ')', chunk)
# reconstruct
new_content = ': url(\'\''.join(chunks)
new_contents.append(new_content)
# replace
style_elem.string.replace_with('\n'.join(new_contents))
def __fix_inline_style(self):
"""
search elems with style attribute.
remove any property name:value having a value starting with 'url('
"""
match_url_start_pattern = re.compile(r'^url\s*\(', re.I)
for elem in self.__soup.find_all(attrs={'style': True}):
style_attr = elem['style']
new_props = []
props = style_attr
if ';' in style_attr:
props = style_attr.split(';')
for i, prop in enumerate(props):
if ':' not in prop:
# malformed, skip
continue
name, value = prop.split(':', 1)
if isinstance(value, list):
# malformed, skip
continue
value = value.strip()
if re.match(match_url_start_pattern, value):
# found value starting with 'url(' so skip
continue
new_props.append(name + ': ' + value)
# replace
elem['style'] = '; '.join(new_props)
def fix_all(
self,
html=None,
allow_remote_resources=False,
block_img_url=None,
):
self.__allow_remote_resources = allow_remote_resources
self.__block_img_url = block_img_url
# start soup
self.__soup = BeautifulSoup(html, self.__parser)
# remove, modify html elements
self.__remove_forbidden_elems()
self.__fix_a_elems()
if not self.__allow_remote_resources:
self.__remove_link_elems()
if self.__block_img_url is not None:
self.__fix_img_elems()
self.__fix_inline_style()
self.__fix_style_elems()
# output
output = str(self.__soup)
return re.sub(r'\n\n+', '\n\n', output)
Использование:
from .htm_mail_fixer import HTMLMailFixer
html = 'YOUR_HTML'
allow_remote_resources = False
block_img_url = 'YOUR_PIXEL_URL'
html_mail_fixer = HTMLMailFixer()
html_fixed = html_mail_fixer.fix_all(
html=html,
allow_remote_resources=allow_remote_resources,
block_img_url=block_img_url,
)
Пример
Это простой пример с некоторыми уродливыми HTML и CSS:
html = """<!DOCTYPE html PUBLIC "- / /w3c / /dtd html 4.01 transitional / /en">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet"><style>
.section-block{background-image: url ( ' https://whatever_image_1') !important;
font-size: 1.1em}
#logo {
margin-top: 10em;background-image: url ( https://whatever_image_1 )}
.wino { color: #ff0000 }
</style>
</head><body style="font: inherit; font-size: 100%; margin:0; padding:0; background-image: url( ' https://whatever_image_2' )">
<a href="https://whatever_a_href_1"><img src="https://whatever_img_src_1" width="60"></a>
<p>Amount: € 1,50</p>
<a href=https://whatever_a_href_2 target=_top><img src=https://whatever_img_src_2 width="60"></a>
<script> a = 'b' </script>
"""
После передачи его в HTMLMailFixer результат таков:
html_fixed = <!DOCTYPE html PUBLIC "- / /w3c / /dtd html 4.01 transitional / /en">
<html><head><style>
.section-block{background-image: url('') !important;
font-size: 1.1em}
#logo {
margin-top: 10em;background-image: url('')}
.wino { color: #ff0000 }
</style>
</head><body style="font: inherit; font-size: 100%; margin: 0; padding: 0">
<a href="https://whatever_a_href_1" rel="noopener noreferrer" target="_blank"><img src="YOUR_PIXEL_URL" width="60"/></a>
<p>Amount: € 1,50</p>
<a href="https://whatever_a_href_2" rel="noopener noreferrer" target="_blank"><img src="YOUR_PIXEL_URL" width="60"/></a>
</body></html>
Обратите внимание, что BeautifulSoup сделал некоторые изменения, такие как добавление недостающих тегов и преобразование в UTF-8.
Откуда мы знаем, что все замены были сделаны?
Что касается элементов HTML , мы не знаем. Если BeautifulSoup потерпит неудачу, мы потерпим неудачу. К счастью, парсер html5lib парсит как браузер. Что касается свойств CSS, я сделал быструю и грязную замену. Я должен более детально изучить, как CSS может включать внешние ресурсы.
Резюме
Использовать BeautifulSoup было несложно, он очень мощный и сэкономил мне много времени. Было бы здорово, если бы было что-то подобное для разбора и модификации (плохого) CCS. В любом случае, конечный результат - письма HTML фильтруются и отображаются в моем браузере с удаленными и заблокированными небезопасными ресурсами. С помощью кнопки я могу разрешить внешние ресурсы.
Блокирование внешних ресурсов должно быть более гибким. Например, мы всегда хотим блокировать Facebook, Google, но разрешить другие ресурсы.
Я не претендую на то, что представленное здесь решение идеально, это только начало.
Ссылки / кредиты
Beautiful Soup Documentation
https://www.crummy.com/software/BeautifulSoup/bs4/doc/
HTML <link> Tag
https://www.w3schools.com/Tags/tag_link.asp
Подробнее
BeautifulSoup
Недавний
- График временного ряда с Flask, Bootstrap и Chart.js
- Использование IPv6 с Microk8s
- Использование Ingress для доступа к RabbitMQ на кластере Microk8s
- Простая видеогалерея с Flask, Jinja, Bootstrap и JQuery
- Базовое планирование заданий с помощью APScheduler
- Коммутатор базы данных с HAProxy и HAProxy Runtime API
Большинство просмотренных
- Использование PyInstaller и Cython для создания исполняемого файла Python
- Уменьшение времени отклика на запросы на странице Flask SQLAlchemy веб-сайта
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Подключение к службе на хосте Docker из контейнера Docker
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов