Скремблирование веб-страниц в Python с красивым супом: поиск и модификация DOM

12 января 2018

В последнем уроке вы узнали основы библиотеки Beautiful Soup. Помимо навигации по дереву DOM, вы также можете искать элементы с заданным классом или идентификатором. Вы также можете изменить дерево DOM с помощью этой библиотеки.

В этом уроке вы узнаете о различных методах, которые помогут вам в поиске и изменениях. Мы будем соскабливать ту же страницу Википедии о Python из нашего последнего урока.

Фильтры для поиска дерева

В Beautiful Soup есть много способов поиска дерева DOM. Эти методы очень похожи и используют фильтры того же типа, что и аргументы. Поэтому имеет смысл правильно понять различные фильтры, прежде чем читать о методах. Я буду использовать тот же метод find_all (), чтобы объяснить разницу между различными фильтрами.

Простейшим фильтром, который вы можете передать любому методу поиска, является строка. Красивый суп будет искать в документе, чтобы найти тег, который точно соответствует строке.

for heading in soup.find_all('h2'):
    print(heading.text)
# Contents
# History[edit]
# Features and philosophy[edit]
# Syntax and semantics[edit]
# Libraries[edit]
# Development environments[edit]
# ... and so on.

Вы также можете передать объект регулярного выражения методу find_all (). На этот раз Beautiful Soup будет фильтровать дерево, сопоставляя все теги с заданным регулярным выражением.

import re
for heading in soup.find_all(re.compile("^h[1-6]")):
    print(heading.name + ' ' + heading.text.strip())
# h1 Python (programming language)
# h2 Contents
# h2 History[edit]
# h2 Features and philosophy[edit]
# h2 Syntax and semantics[edit]
# h3 Indentation[edit]
# h3 Statements and control flow[edit]
# ... an so on.

Код будет искать все теги, начинающиеся с «h», а за ними следует цифра от 1 до 6. Другими словами, он будет искать все теги заголовка в документе.

Вместо использования регулярного выражения вы можете добиться того же результата, передав список всех тегов, которые вы хотите, чтобы Beautiful Soup соответствовал документу.

for heading in soup.find_all(["h1", "h2", "h3", "h4", "h5", "h6"]):
    print(heading.name + ' ' + heading.text.strip())

Вы также можете передать True в качестве параметра методу find_all (). Затем код вернет все теги в документе. Выведенный ниже результат означает, что в настоящее время на странице Википедии есть 4 339 тегов, которые мы анализируем.

len(soup.find_all(True))
# 4339

Если вы все еще не можете найти то, что ищете, с помощью любого из перечисленных выше фильтров, вы можете определить свою собственную функцию, которая берет элемент в качестве единственного аргумента. Функция также должна вернуть True, если есть совпадение, а False - в противном случае. В зависимости от того, что вам нужно, вы можете сделать эту функцию настолько сложной, насколько это необходимо для выполнения задания. Вот очень простой пример:

def big_lists(tag):
    return len(tag.contents) > 20 and tag.name == 'ul'
len(soup.find_all(big_lists))
# 13

Вышеупомянутая функция проходит через ту же страницу на Википедии Python и ищет неупорядоченные списки, в которых более 20 детей.

Поиск дерева DOM с использованием встроенных функций

Одним из наиболее популярных методов поиска по DOM является find_all (). Он пройдет через всех потомков тега и вернет список всех потомков, соответствующих вашим критериям поиска. Этот метод имеет следующую подпись:

find_all(name, attrs, recursive, string, limit, **kwargs)

Аргумент name - это имя тега, который вы хотите, чтобы эта функция выполнялась при просмотре дерева. Вы можете предоставить строку, список, регулярное выражение, функцию или значение True в качестве имени.

Вы также можете фильтровать элементы в дереве DOM на основе разных атрибутов, таких как id, href и т. Д. Вы также можете получить все элементы с определенным атрибутом независимо от его значения, используя атрибут = True. Поиск элементов с определенным классом отличается от поиска обычных атрибутов. Поскольку класс является зарезервированным ключевым словом в Python, вам придется использовать аргумент ключевого слова class_ при поиске элементов с определенным классом.

import re
len(soup.find_all(id=True))
# 425
len(soup.find_all(class_=True))
# 1734
len(soup.find_all(class_="mw-headline"))
# 20
len(soup.find_all(href=True))
# 1410
len(soup.find_all(href=re.compile("python")))
# 102

Вы можете видеть, что в документе есть 1734 тега с атрибутом класса и 425 тегами с атрибутом id. Если вам нужны только первые из этих результатов, вы можете передать число методу в качестве значения предела. Передача этого значения даст указание Beautiful Soup прекратить поиск большего количества элементов, как только он достигнет определенного числа. Вот пример:

soup.find_all(class_="mw-headline", limit=4)
# <span class="mw-headline" id="History">History</span>
# <span class="mw-headline" id="Features_and_philosophy">Features and philosophy</span>
# <span class="mw-headline" id="Syntax_and_semantics">Syntax and semantics</span>
# <span class="mw-headline" id="Indentation">Indentation</span>

Когда вы используете метод find_all (), вы говорите Beautiful Soup, чтобы пройти через всех потомков данного тега, чтобы найти то, что ищете. Иногда вы хотите искать элемент только в прямых дочерних элементах в теге. Это может быть достигнуто путем передачи рекурсивного = False методу find_all ().

len(soup.html.find_all("meta"))
# 6
len(soup.html.find_all("meta", recursive=False))
# 0
len(soup.head.find_all("meta", recursive=False))
# 6

Если вам интересно найти только один результат для определенного поискового запроса, вы можете использовать метод find (), чтобы найти его, а не передавать limit = 1 в find_all (). Единственная разница между результатами, возвращаемыми этими двумя методами, заключается в том, что find_all () возвращает список только с одним элементом, а find () просто возвращает результат.

soup.find_all("h2", limit=1)
# [<h2>Contents</h2>]
soup.find("h2")
# <h2>Contents</h2>

Методы find () и find_all () осуществляют поиск по всем потомкам данного тега для поиска элемента. Есть еще десять очень похожих методов, которые можно использовать для итерации через дерево DOM в разных направлениях.

find_parents(name, attrs, string, limit, **kwargs)
find_parent(name, attrs, string, **kwargs)
find_next_siblings(name, attrs, string, limit, **kwargs)
find_next_sibling(name, attrs, string, **kwargs)
find_previous_siblings(name, attrs, string, limit, **kwargs)
find_previous_sibling(name, attrs, string, **kwargs)
find_all_next(name, attrs, string, limit, **kwargs)
find_next(name, attrs, string, **kwargs)
find_all_previous(name, attrs, string, limit, **kwargs)
find_previous(name, attrs, string, **kwargs)

Методы find_parent () и find_parents () пересекают дерево DOM, чтобы найти данный элемент. Методы find_next_sibling () и find_next_siblings () будут перебирать всех братьев и сестер элемента, следующего за текущим. Точно так же методы find_previous_sibling () и find_previous_siblings () будут перебирать всех братьев и сестер элемента, которые идут до текущего.

Методы find_next () и find_all_next () будут перебирать все теги и строки, которые появляются после текущего элемента. Аналогично, методы find_previous () и find_all_previous () будут перебирать все теги и строки, которые предшествуют текущему элементу.

Вы также можете искать элементы с помощью селекторов CSS с помощью метода select (). Вот несколько примеров:

len(soup.select("p a"))
# 411
len(soup.select("p > a"))
# 291
soup.select("h2:nth-of-type(1)")
# [<h2>Contents</h2>]
len(soup.select("p > a:nth-of-type(2)"))
# 46
len(soup.select("p > a:nth-of-type(10)"))
# 6
len(soup.select("[class*=section]"))
# 80
len(soup.select("[class$=section]"))
# 20

Изменение дерева

Вы можете не только искать в дереве DOM поиск элемента, но и изменять его. Очень легко переименовать тег и изменить его атрибуты.

heading_tag = soup.select("h2:nth-of-type(2)")[0]
heading_tag.name = "h3"
print(heading_tag)
# <h3><span class="mw-headline" id="Features_and_philosophy">Feat...
heading_tag['class'] = 'headingChanged'
print(heading_tag)
# <h3 class="headingChanged"><span class="mw-headline" id="Feat...
heading_tag['id'] = 'newHeadingId'
print(heading_tag)
# <h3 class="headingChanged" id="newHeadingId"><span class="mw....
del heading_tag['id']
print(heading_tag)
# <h3 class="headingChanged"><span class="mw-headline"...

Продолжая наш последний пример, вы можете заменить содержимое тега на заданную строку, используя атрибут.string. Если вы не хотите заменять содержимое, но добавляете что-то лишнее в конце тега, вы можете использовать метод append ().

Аналогично, если вы хотите вставить что-то внутри тега в определенном месте, вы можете использовать метод insert (). Первым параметром для этого метода является позиция или индекс, в который вы хотите вставить содержимое, а второй параметр - это сам контент. Вы можете удалить все содержимое внутри тега, используя метод clear (). Это просто оставит вас с самим тегом и его атрибутами.

heading_tag.string = "Features and Philosophy"
print(heading_tag)
# <h3 class="headingChanged">Features and Philosophy</h3>
heading_tag.append(" [Appended This Part].")
print(heading_tag)
# <h3 class="headingChanged">Features and Philosophy [Appended This Part].</h3>
print(heading_tag.contents)
# ['Features and Philosophy', ' [Appended This Part].']
heading_tag.insert(1, ' Inserted this part ')
print(heading_tag)
# <h3 class="headingChanged">Features and Philosophy Inserted this part  [Appended This Part].</h3>
heading_tag.clear()
print(heading_tag)
# <h3 class="headingChanged"></h3>

В начале этого раздела вы выбрали заголовок уровня 2 из документа и изменили его на заголовок третьего уровня. С помощью того же селектора снова появится следующий заголовок второго уровня, который появился после оригинала. Это имеет смысл, потому что исходный заголовок уже не является заголовком второго уровня.

Теперь исходный заголовок можно выбрать с помощью h3: nth-of-type (2). Если вы полностью хотите удалить элемент или тег и весь контент внутри него из дерева, вы можете использовать метод разложения ().

soup.select("h3:nth-of-type(2)")[0]
# <h3 class="headingChanged"></h3>
soup.select("h3:nth-of-type(3)")[0]
# <h3><span class="mw-headline" id="Indentation">Indentation</span>...
soup.select("h3:nth-of-type(2)")[0].decompose()
soup.select("h3:nth-of-type(2)")[0]
# <h3><span class="mw-headline" id="Indentation">Indentation</span>...

После того, как вы разложили или удалили исходный заголовок, заголовок в третьем месте займет свое место.

Если вы хотите удалить тег и его содержимое из дерева, но не хотите полностью уничтожить тег, вы можете использовать метод extract (). Этот метод вернет тег, который он извлек. Теперь у вас будет два разных дерева, которые вы можете проанализировать. Корнем нового дерева будет тег, который вы только что извлекли.

heading_tree = soup.select("h3:nth-of-type(2)")[0].extract()
len(heading_tree.contents)
# 2

Вы также можете заменить тег внутри дерева чем-то другим по вашему выбору, используя метод replace_with (). Этот метод вернет тег или строку, которые он заменил. Это может быть полезно, если вы хотите поместить замененное содержимое в другое место в документе.

soup.h1
# <h1 class="firstHeading">Python (programming language)</h1>
bold_tag = soup.new_tag("b")
bold_tag.string = "Python"
soup.h1.replace_with(bold_tag)
print(soup.h1)
# None
print(soup.b)
# <b>Python</b>

В приведенном выше коде основной заголовок документа был заменен тегом b. В документе больше нет тега h1, поэтому print (soup.h1) теперь печатает None.

Заключительные мысли

После прочтения двух учебников этой серии вы должны теперь анализировать различные веб-страницы и извлекать важные данные из документа. Вы также можете получить исходную веб-страницу, изменить ее в соответствии с вашими потребностями и сохранить измененную версию локально.

Если у вас есть какие-либо вопросы относительно этого учебника, пожалуйста, дайте мне знать в комментариях.