Парсинг HTML и XML в Python: лучшие инструменты и библиотеки
Для кого эта статья:
- Python-разработчики, стремящиеся улучшить свои навыки парсинга данных
- Начинающие специалисты, желающие освоить веб-скрейпинг и обработку XML
Профессионалы, готовящиеся к собеседованиям в области разработки и анализа данных
Извлечение данных из веб-страниц и XML-документов — одна из самых востребованных задач в мире Python-разработки. Владение техниками парсинга открывает двери к автоматизации рутинных процессов, созданию мощных аналитических систем и построению баз данных без необходимости ручного копирования информации. Неудивительно, что на собеседованиях эти навыки часто проверяют в первую очередь — умение эффективно извлекать структурированные данные отличает профессионала от новичка. Давайте погрузимся в мир парсинга HTML и XML с помощью Python и разберем лучшие инструменты и подходы для решения этих задач. 🚀
Хотите овладеть профессиональными навыками парсинга данных? Программа Обучение Python-разработке от Skypro включает глубокое погружение в веб-скрейпинг с практическими заданиями на реальных проектах. Вы научитесь не только извлекать данные с любых веб-сайтов, но и строить на их основе эффективные бизнес-решения. Наставники с опытом в крупных IT-компаниях покажут, как обходить защиту от парсинга и работать с динамическим контентом. Ваше резюме будет выделяться среди других!
Основы парсинга данных в Python: HTML и XML
Парсинг — это процесс анализа и извлечения структурированной информации из текстовых данных. В контексте веб-разработки и обработки данных мы обычно имеем дело с HTML-страницами и XML-документами. Python предоставляет мощные инструменты для работы с обоими форматами, что делает его идеальным языком для задач веб-скрейпинга и обработки структурированных данных. 📊
Перед погружением в специфические библиотеки, важно понять разницу между HTML и XML:
| Характеристика | HTML | XML |
|---|---|---|
| Основное назначение | Отображение данных | Хранение и передача данных |
| Строгость синтаксиса | Менее строгий (браузеры прощают ошибки) | Очень строгий (ошибки вызывают сбой парсинга) |
| Теги | Предопределенный набор | Пользовательские (определяемые разработчиком) |
| Вложенность | Обязательна, но часто нарушается | Строго обязательна |
| Популярные библиотеки для парсинга | Beautiful Soup, lxml, requests-html | ElementTree, minidom, lxml |
Базовый процесс парсинга обычно включает следующие шаги:
- Получение данных — загрузка HTML-страницы или чтение XML-файла
- Анализ структуры — преобразование текста в объектную модель
- Навигация — поиск нужных элементов в документе
- Извлечение данных — получение текста, атрибутов или других свойств элементов
- Обработка данных — преобразование в нужный формат, фильтрация, сохранение
Начальным этапом парсинга является установка необходимых библиотек. Для большинства задач достаточно установить следующий набор:
pip install requests beautifulsoup4 lxml
Теперь, имея базовое понимание процесса и установленные инструменты, мы готовы перейти к конкретным методам парсинга HTML с использованием популярных библиотек. 🛠️

Библиотеки Beautiful Soup и requests для обработки HTML
Михаил Проценко, технический директор проекта по агрегации данных
Когда мы запускали наш сервис сравнения цен, нам требовалось ежедневно собирать информацию с более чем 200 интернет-магазинов. Я помню, как начинал с простых регулярных выражений и неделю боролся с мельчайшими различиями в вёрстке сайтов. Переход на связку requests + Beautiful Soup сократил время разработки в 3 раза. Особенно впечатлила меня устойчивость Beautiful Soup к ошибкам в HTML — ведь в реальном мире идеальных страниц почти не существует. Этот подход помог нам масштабироваться до обработки миллиона товарных позиций в день, а главное — легко поддерживать код при изменениях в структуре сайтов.
Библиотека requests — это мощный, но простой инструмент для отправки HTTP-запросов в Python. Beautiful Soup дополняет ее возможностями для разбора и навигации по HTML-документам. Вместе они образуют идеальный тандем для веб-скрейпинга. 🤝
Начнем с простого примера: получим заголовки новостей с сайта.
import requests
from bs4 import BeautifulSoup
# Отправляем GET-запрос
response = requests.get('https://news.ycombinator.com/')
# Проверяем успешность запроса
if response.status_code == 200:
# Создаем объект BeautifulSoup для парсинга
soup = BeautifulSoup(response.text, 'html.parser')
# Находим все заголовки новостей
titles = soup.select('.titleline > a')
# Выводим заголовки
for title in titles:
print(title.text)
else:
print(f"Ошибка запроса: {response.status_code}")
Beautiful Soup предоставляет несколько методов для поиска элементов:
- find() и find_all() — поиск по тегам, атрибутам и содержимому
- select() и select_one() — поиск с помощью CSS-селекторов
- parent, children, next_sibling, previous_sibling — навигация по структуре документа
Пример использования различных методов поиска:
# Найти первый div с классом 'content'
content_div = soup.find('div', class_='content')
# Найти все ссылки внутри абзацев
links_in_paragraphs = soup.select('p a')
# Найти элемент по ID
element_by_id = soup.find(id='main-content')
# Найти все элементы с определенным атрибутом
elements_with_attr = soup.find_all(attrs={'data-type': 'article'})
# Извлечение данных из таблицы
table = soup.find('table')
rows = table.find_all('tr')
for row in rows:
cells = row.find_all('td')
row_data = [cell.text.strip() for cell in cells]
print(row_data)
При работе с requests важно учитывать некоторые нюансы:
- User-Agent — многие сайты блокируют запросы с дефолтным User-Agent библиотеки requests
- Cookies и сессии — для доступа к некоторым страницам требуется авторизация
- Throttling — ограничение частоты запросов во избежание блокировки
# Использование заголовков для имитации браузера
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
}
# Создание сессии для сохранения cookies
session = requests.Session()
session.headers.update(headers)
# Авторизация
login_data = {'username': 'user', 'password': 'pass'}
session.post('https://example.com/login', data=login_data)
# Доступ к защищенным страницам
protected_page = session.get('https://example.com/protected')
Beautiful Soup особенно удобна для обработки "грязного" HTML, однако имеет некоторые ограничения по производительности при работе с большими документами. Для таких случаев лучше обратиться к более мощной библиотеке lxml. 🚀
Продвинутый веб-скрейпинг с помощью lxml
Библиотека lxml сочетает в себе скорость C-библиотек libxml2 и libxslt с гибкостью и простотой использования, характерными для Python. Она идеально подходит для обработки больших объемов данных и сложных HTML/XML-документов. 💪
Основные преимущества lxml по сравнению с Beautiful Soup:
- Производительность — lxml работает значительно быстрее при парсинге больших документов
- Поддержка XPath — мощный язык запросов для навигации по XML-документам
- Встроенная поддержка XSLT — преобразование XML-документов с помощью таблиц стилей
- Низкое потребление памяти — эффективная работа с большими файлами
Начнем с простого примера использования lxml для парсинга HTML:
import requests
from lxml import html
# Отправляем GET-запрос
response = requests.get('https://news.ycombinator.com/')
# Создаем дерево элементов
tree = html.fromstring(response.content)
# Используем XPath для поиска заголовков
titles = tree.xpath('//span[@class="titleline"]/a/text()')
links = tree.xpath('//span[@class="titleline"]/a/@href')
# Выводим результаты
for title, link in zip(titles, links):
print(f"{title} – {link}")
XPath — это мощный инструмент для навигации по XML-документам. Вот несколько примеров использования XPath в lxml:
# Выбрать все элементы div
all_divs = tree.xpath('//div')
# Выбрать div с определенным классом
content_divs = tree.xpath('//div[@class="content"]')
# Выбрать элементы с конкретным атрибутом
data_elements = tree.xpath('//*[@data-id]')
# Комбинирование условий
specific_elements = tree.xpath('//div[@class="item" and @data-type="product"]')
# Выбор по позиции
first_item = tree.xpath('(//div[@class="item"])[1]')
# Выбор по тексту
elements_with_text = tree.xpath('//a[contains(text(), "Python")]')
# Получение родительского элемента
parent_elements = tree.xpath('//a[@href="#top"]/parent::*')
# Получение атрибутов
all_links = tree.xpath('//a/@href')
lxml также позволяет создавать и модифицировать HTML/XML-документы:
from lxml import etree
# Создание нового XML-документа
root = etree.Element("root")
child = etree.SubElement(root, "child")
child.text = "This is a child element"
child.set("attribute", "value")
# Добавление комментария
comment = etree.Comment("This is a comment")
root.append(comment)
# Преобразование в строку
xml_string = etree.tostring(root, pretty_print=True).decode()
print(xml_string)
Для сложных задач веб-скрейпинга lxml можно комбинировать с CSS-селекторами, что делает код более читаемым:
from lxml.cssselect import CSSSelector
# Создание CSS-селектора
selector = CSSSelector('div.product')
products = selector(tree)
# Эквивалентно
products = tree.cssselect('div.product')
# Обработка результатов
for product in products:
name = product.cssselect('h2.title')[0].text_content()
price = product.cssselect('span.price')[0].text_content()
print(f"{name}: {price}")
| Характеристика | Beautiful Soup | lxml |
|---|---|---|
| Скорость парсинга | Средняя | Очень высокая |
| Потребление памяти | Высокое | Низкое |
| Обработка "грязного" HTML | Отличная | Хорошая |
| Поддержка XPath | Нет | Да |
| Поддержка CSS-селекторов | Встроенная | Через модуль cssselect |
| Удобство API | Очень высокое | Среднее |
| Кривая обучения | Пологая | Крутая |
lxml особенно полезен при работе с XML-документами, но его возможности выходят далеко за рамки обычного парсинга. В следующем разделе мы рассмотрим другие библиотеки, специализирующиеся именно на работе с XML. 📈
Работа с XML: ElementTree и minidom в действии
XML (eXtensible Markup Language) — это формат данных, который широко используется для хранения и передачи структурированной информации. Python предлагает несколько специализированных библиотек для работы с XML, наиболее популярные из которых — ElementTree и minidom из стандартной библиотеки. 🔍
ElementTree представляет XML-документ в виде древовидной структуры, что делает навигацию и манипуляции с документом интуитивно понятными:
import xml.etree.ElementTree as ET
# Парсинг XML-файла
tree = ET.parse('sample.xml')
root = tree.getroot()
# Получение информации о корневом элементе
print(f"Корневой элемент: {root.tag}")
print(f"Атрибуты: {root.attrib}")
# Итерация по дочерним элементам
for child in root:
print(f"Дочерний элемент: {child.tag}, атрибуты: {child.attrib}")
# Поиск всех элементов с определенным тегом
for item in root.findall('./item'):
# Получение текста из дочернего элемента
name = item.find('name').text
price = item.find('price').text
print(f"Товар: {name}, Цена: {price}")
# Получение значения атрибута
for item in root.findall('./item[@category="electronics"]'):
print(f"Электроника: {item.find('name').text}")
ElementTree также позволяет создавать и изменять XML-документы:
# Создание нового XML-документа
root = ET.Element("catalog")
item = ET.SubElement(root, "item", category="book")
ET.SubElement(item, "name").text = "Python Programming"
ET.SubElement(item, "price").text = "29.99"
# Сохранение в файл
tree = ET.ElementTree(root)
tree.write("new_catalog.xml", encoding="utf-8", xml_declaration=True)
Альтернативой ElementTree является minidom, который реализует DOM API (Document Object Model) и может быть более знакомым для разработчиков, имеющих опыт работы с DOM в других языках:
from xml.dom import minidom
# Парсинг XML-файла
doc = minidom.parse('sample.xml')
# Получение всех элементов с определенным тегом
items = doc.getElementsByTagName('item')
for item in items:
# Получение значения атрибута
category = item.getAttribute('category')
# Получение текста из дочернего элемента
name_element = item.getElementsByTagName('name')[0]
name = name_element.firstChild.data
print(f"Категория: {category}, Название: {name}")
# Создание нового документа
new_doc = minidom.getDOMImplementation().createDocument(None, "catalog", None)
root = new_doc.documentElement
# Создание нового элемента
item = new_doc.createElement("item")
item.setAttribute("category", "software")
root.appendChild(item)
# Добавление текстового содержимого
name = new_doc.createElement("name")
text = new_doc.createTextNode("Development Tools")
name.appendChild(text)
item.appendChild(name)
# Сохранение в файл с красивым форматированием
xml_string = new_doc.toprettyxml(encoding="utf-8")
with open("new_catalog_dom.xml", "wb") as f:
f.write(xml_string)
Анна Демидова, архитектор информационных систем
В одном из проектов мне пришлось интегрировать данные из CRM-системы клиента с нашим внутренним API. Проблема заключалась в том, что CRM выгружала 2GB XML-файлы с миллионами записей о клиентах. Сначала я попробовала использовать DOM-парсер, но система падала с ошибкой нехватки памяти. Затем перешла на SAX, который работал, но создавал сложно поддерживаемый код. Настоящим открытием стал iterparse из ElementTree. Он позволял читать XML потоково, обрабатывать только нужные элементы и сразу очищать память. Ранее неподъемная задача стала выполняться за 15 минут без значительного потребления ресурсов. Этот опыт научил меня, что выбор правильного парсера под конкретную задачу критически важен, особенно при работе с большими данными.
При работе с большими XML-файлами следует обратить внимание на эффективность использования памяти. ElementTree предлагает специальный метод iterparse для потоковой обработки больших документов:
# Эффективная обработка больших XML-файлов
for event, elem in ET.iterparse('large.xml', events=('start', 'end')):
if event == 'end' and elem.tag == 'item':
# Обработка элемента
print(elem.find('name').text)
# Очистка элемента для освобождения памяти
elem.clear()
Выбор между разными XML-парсерами зависит от конкретных требований проекта:
- ElementTree — простой API, хорошая производительность, подходит для большинства задач
- minidom — полная реализация DOM API, более сложный, но знакомый интерфейс
- lxml — расширенный API, высокая производительность, поддержка XPath и XSLT
- SAX (Simple API for XML) — событийно-ориентированный парсер, очень эффективен по памяти
Теперь, вооружившись знаниями о библиотеках для работы с HTML и XML, мы можем перейти к практическим примерам применения этих инструментов в реальных проектах. 🛠️
Практические проекты по извлечению и анализу данных
Давайте рассмотрим несколько практических проектов, которые демонстрируют применение изученных нами инструментов для решения реальных задач веб-скрейпинга и обработки XML. 🔍
Проект 1: Мониторинг цен на товары
Создадим скрипт для отслеживания цен на товары в интернет-магазинах и уведомления о снижении цены:
import requests
from bs4 import BeautifulSoup
import time
import smtplib
from email.message import EmailMessage
# Список товаров для отслеживания
products = [
{"url": "https://example.com/product1", "name": "Смартфон X", "desired_price": 15000},
{"url": "https://example.com/product2", "name": "Ноутбук Y", "desired_price": 45000},
]
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
def check_price(url, name, desired_price):
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
# Селекторы зависят от конкретного сайта
price_element = soup.select_one('.product-price .value')
if not price_element:
return None
# Извлечение и преобразование цены
price_text = price_element.text.strip().replace(' ', '').replace('₽', '')
current_price = float(price_text)
if current_price <= desired_price:
return current_price
return None
def send_notification(product, price):
msg = EmailMessage()
msg.set_content(f"Цена на {product['name']} снизилась до {price} ₽!")
msg['Subject'] = 'Снижение цены на товар'
msg['From'] = 'your_email@gmail.com'
msg['To'] = 'your_email@gmail.com'
# Настройки SMTP-сервера
server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()
server.login('your_email@gmail.com', 'your_password')
server.send_message(msg)
server.quit()
def monitor_prices():
while True:
for product in products:
price = check_price(product['url'], product['name'], product['desired_price'])
if price:
print(f"Цена на {product['name']} снизилась до {price} ₽!")
send_notification(product, price)
else:
print(f"Цена на {product['name']} всё ещё высока.")
# Проверка раз в день
time.sleep(86400)
if __name__ == "__main__":
monitor_prices()
Проект 2: Агрегатор новостей
Создадим скрипт, который собирает новости с нескольких источников и сохраняет их в базу данных:
import requests
from bs4 import BeautifulSoup
import sqlite3
import datetime
from concurrent.futures import ThreadPoolExecutor
# Настройка базы данных
conn = sqlite3.connect('news.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS news (
id INTEGER PRIMARY KEY,
title TEXT,
url TEXT UNIQUE,
source TEXT,
published_date TEXT,
content TEXT,
collected_at TIMESTAMP
)
''')
conn.commit()
# Список источников новостей
news_sources = [
{"name": "Tech News", "url": "https://example.com/tech", "article_selector": ".article",
"title_selector": "h2", "link_selector": "a", "content_selector": ".article-content"},
{"name": "Science Daily", "url": "https://example.com/science", "article_selector": ".news-item",
"title_selector": ".title", "link_selector": "a", "content_selector": ".content"},
]
def parse_news_list(source):
try:
response = requests.get(source["url"])
soup = BeautifulSoup(response.text, 'html.parser')
articles = soup.select(source["article_selector"])
news_items = []
for article in articles:
title_elem = article.select_one(source["title_selector"])
if not title_elem:
continue
title = title_elem.text.strip()
link_elem = article.select_one(source["link_selector"])
if not link_elem or not link_elem.has_attr('href'):
continue
link = link_elem['href']
# Обработка относительных URL
if link.startswith('/'):
link = source["url"] + link if source["url"].endswith('/') else source["url"] + '/' + link
news_items.append({"title": title, "url": link, "source": source["name"]})
return news_items
except Exception as e:
print(f"Ошибка при парсинге {source['name']}: {e}")
return []
def parse_article_content(article):
try:
response = requests.get(article["url"])
soup = BeautifulSoup(response.text, 'html.parser')
# Поиск источника в списке
source_config = next((s for s in news_sources if s["name"] == article["source"]), None)
if not source_config:
return article
content_elem = soup.select_one(source_config["content_selector"])
if content_elem:
article["content"] = content_elem.text.strip()
# Поиск даты публикации (может отличаться в зависимости от сайта)
date_elem = soup.select_one('.published-date, .date, time')
if date_elem:
if date_elem.has_attr('datetime'):
article["published_date"] = date_elem['datetime']
else:
article["published_date"] = date_elem.text.strip()
else:
article["published_date"] = datetime.datetime.now().strftime("%Y-%m-%d")
return article
except Exception as e:
print(f"Ошибка при парсинге статьи {article['url']}: {e}")
article["content"] = "Не удалось получить содержимое"
article["published_date"] = datetime.datetime.now().strftime("%Y-%m-%d")
return article
def save_to_database(article):
try:
cursor.execute(
"INSERT OR IGNORE INTO news (title, url, source, published_date, content, collected_at) VALUES (?, ?, ?, ?, ?, ?)",
(article["title"], article["url"], article["source"], article["published_date"],
article.get("content", ""), datetime.datetime.now())
)
conn.commit()
except Exception as e:
print(f"Ошибка при сохранении в БД: {e}")
def collect_news():
# Сбор списков новостей со всех источников
all_news = []
for source in news_sources:
news_items = parse_news_list(source)
all_news.extend(news_items)
print(f"Найдено {len(all_news)} новостей")
# Параллельный парсинг содержимого статей
with ThreadPoolExecutor(max_workers=5) as executor:
articles_with_content = list(executor.map(parse_article_content, all_news))
# Сохранение в базу данных
for article in articles_with_content:
save_to_database(article)
print("Новости успешно собраны и сохранены")
if __name__ == "__main__":
collect_news()
conn.close()
Проект 3: Обработка XML-фидов RSS
Создадим скрипт для обработки RSS-лент и генерации дайджеста новостей:
import requests
import xml.etree.ElementTree as ET
import datetime
import html
import os
# Список RSS-лент
rss_feeds = [
{"name": "Tech News", "url": "https://example.com/tech/rss"},
{"name": "Science News", "url": "https://example.com/science/rss"},
{"name": "Business News", "url": "https://example.com/business/rss"}
]
def parse_rss_feed(feed):
try:
response = requests.get(feed["url"])
# Парсинг XML-содержимого
root = ET.fromstring(response.content)
# Обработка различных форматов RSS
items = root.findall('.//item') or root.findall('.//{http://www.w3.org/2005/Atom}entry')
articles = []
for item in items:
# Извлечение данных с учетом возможного наличия пространства имен
title_elem = item.find('./title') or item.find('.//{http://www.w3.org/2005/Atom}title')
link_elem = item.find('./link') or item.find('.//{http://www.w3.org/2005/Atom}link')
desc_elem = (item.find('./description') or item.find('.//{http://www.w3.org/2005/Atom}summary')
or item.find('.//{http://www.w3.org/2005/Atom}content'))
date_elem = (item.find('./pubDate') or item.find('.//{http://www.w3.org/2005/Atom}updated')
or item.find('.//{http://www.w3.org/2005/Atom}published'))
if title_elem is None or (link_elem is None and not hasattr(link_elem, 'attrib')):
continue
title = title_elem.text
# Обработка различных форматов элемента link
if hasattr(link_elem, 'attrib') and 'href' in link_elem.attrib:
link = link_elem.attrib['href']
else:
link = link_elem.text
description = desc_elem.text if desc_elem is not None else ""
pub_date = date_elem.text if date_elem is not None else ""
articles.append({
"title": title,
"link": link,
"description": description,
"pub_date": pub_date,
"source": feed["name"]
})
return articles
except Exception as e:
print(f"Ошибка при парсинге ленты {feed['name']}: {e}")
return []
def clean_html(text):
# Простая очистка HTML-тегов и декодирование HTML-сущностей
if text:
# Удаляем теги
text = text.replace('<p>', '\n').replace('</p>', '\n')
text = ''.join([line for line in text.splitlines() if line.strip()])
text = html.unescape(text)
# Ограничиваем длину для дайджеста
if len(text) > 200:
text = text[:197] + '...'
return text
def generate_digest(articles):
today = datetime.datetime.now().strftime("%Y-%m-%d")
digest = f"# Дайджест новостей за {today}\n\n"
# Группировка по источникам
sources = {}
for article in articles:
source = article["source"]
if source not in sources:
sources[source] = []
sources[source].append(article)
# Формирование дайджеста
for source, items in sources.items():
digest += f"## {source}\n\n"
for item in items[:5]: # Берем до 5 новостей из каждого источника
digest += f"### {item['title']}\n"
digest += f"{clean_html(item['description'])}\n"
digest += f"[Читать полностью]({item['link']})\n\n"
# Сохранение в файл
file_path = f"news_digest_{today}.md"
with open(file_path, "w", encoding="utf-8") as f:
f.write(digest)
print(f"Дайджест сохранен в файл: {os.path.abspath(file_path)}")
def collect_and_digest():
all_articles = []
for feed in rss_feeds:
articles = parse_rss_feed(feed)
all_articles.extend(articles)
# Сортировка по дате (если есть)
all_articles.sort(key=lambda x: x.get("pub_date", ""), reverse=True)
generate_digest(all_articles)
if __name__ == "__main__":
collect_and_digest()
Эти проекты демонстрируют, как парсинг HTML и XML может быть применен для решения практических задач автоматизации. В зависимости от конкретных требований и специфики данных, вы можете комбинировать различные библиотеки и подходы для достижения оптимальных результатов. 🚀
Освоение парсинга HTML и XML в Python открывает перед вами мощные возможности автоматизации сбора и обработки данных. Библиотеки Beautiful Soup, requests, lxml, ElementTree — это ключи от дверей информационного мира, позволяющие трансформировать веб-страницы и XML-документы в структурированные данные для дальнейшего анализа. Помните, что эффективный парсинг — это баланс между производительностью, читаемостью кода и этичностью использования. Выбирайте инструменты исходя из масштаба задачи, и ваши скрипты будут не только работать, но и легко поддерживаться и масштабироваться с ростом ваших проектов.