Статьи

Очистка веб-страниц в Python с Beautiful Soup: поиск и модификация DOM

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

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

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

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

01
02
03
04
05
06
07
08
09
10
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 отфильтрует дерево, сопоставив все теги с данным регулярным выражением .

01
02
03
04
05
06
07
08
09
10
11
12
13
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 соответствовал документу.

1
2
for heading in soup.find_all([«h1», «h2», «h3», «h4», «h5», «h6»]):
   print(heading.name + ‘ ‘ + heading.text.strip())

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

1
2
len(soup.find_all(True))
# 4339

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

1
2
3
4
5
def big_lists(tag):
    return len(tag.contents) > 20 and tag.name == ‘ul’
     
len(soup.find_all(big_lists))
# 13

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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 тега с атрибутом class и 425 тегов с атрибутом id . Если вам нужны только первые несколько из этих результатов, вы можете передать в метод число в качестве значения limit . Передача этого значения заставит Beautiful Soup прекратить поиск дополнительных элементов, как только он достигнет определенного числа. Вот пример:

1
2
3
4
5
6
soup.find_all(class_=»mw-headline», limit=4)
 
# <span class=»mw-headline» id=»History»>History
# <span class=»mw-headline» id=»Features_and_philosophy»>Features and philosophy
# <span class=»mw-headline» id=»Syntax_and_semantics»>Syntax and semantics
# <span class=»mw-headline» id=»Indentation»>Indentation

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

1
2
3
4
5
6
7
8
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() просто возвращает результат.

1
2
3
4
5
soup.find_all(«h2», limit=1)
# [<h2>Contents</h2>]
 
soup.find(«h2»)
# <h2>Contents</h2>

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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() . Вот несколько примеров:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
len(soup.select(«pa»))
# 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, чтобы найти элемент, но и изменять его. Очень легко переименовать тег и изменить его атрибуты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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() . Это просто оставит вас с самим тегом и его атрибутами.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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>

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

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

1
2
3
4
5
6
7
8
9
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
 
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

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

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

1
2
3
4
heading_tree = soup.select(«h3:nth-of-type(2)»)[0].extract()
 
len(heading_tree.contents)
# 2

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

01
02
03
04
05
06
07
08
09
10
11
12
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 .

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

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