Russian (Pусский) translation by Anna k.Ivanova (you can also view the original English article)
В последнем уроке вы узнали основы библиотеки Beautiful Soup. Помимо навигации по дереву DOM, вы также можете искать элементы с заданным class
или id
. Вы также можете изменить дерево DOM с помощью этой библиотеки.
В этом уроке вы узнаете о различных методах, которые помогут вам в поиске и изменениях. Мы будем скрапить ту же страницу Википедии о Python из нашего последнего урока.
Фильтры для поиска по дереву
У Beautiful Soup есть много способов поиска в дереве DOM. Эти методы очень похожи и используют фильтры того же типа, что и аргументы. Поэтому имеет смысл правильно понять различные фильтры, прежде чем читать о методах. Я буду использовать тот же метод find_all()
, чтобы объяснить разницу между различными фильтрами.
Самый простой фильтр, который вы можете передать любому методу поиска, - это строка. Beautiful Soup будет искать в документе, чтобы найти тег, который точно соответствует строке.
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
и т.д. Вы также можете получить все элементы с определенным атрибутом независимо от его значения, используя attribute=True
. Поиск элементов с определенным классом отличается от поиска обычных атрибутов. Поскольку class
является зарезервированным ключевым словом в 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 тега с атрибутом class
и 425 тегами с атрибутом id
. Если вам нужны только первые из этих результатов, вы можете передать число методу в качестве значения limit
. Передача этого значения даст указание 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, чтобы пройти через всех потомков данного тега, чтобы найти то, что вам нужно. Иногда вы хотите искать элемент только в прямых дочерних элементах в теге. Это может быть достигнуто путем передачи recursive=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)
. Если вы полностью хотите удалить элемент или тег и весь контент внутри него из дерева, вы можете использовать метод decompose()
.
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
.
Финальные мысли
Прочитав два учебника этой серии, вы должны теперь иметь возможность анализировать различные веб-страницы и извлекать важные данные из документа. Вы также можете получить исходную веб-страницу, изменить ее в соответствии с вашими потребностями и сохранить измененную версию локально.
Если у вас есть какие-либо вопросы относительно этого урока, пожалуйста, дайте мне знать в комментариях.