Клон-элемент с beautifulsoup

Мне нужно скопировать часть одного документа в другой, но я не хочу изменять документ, который я копирую.

Если я использую .extract(), он удаляет элемент из дерева. Если я просто добавлю выделенный элемент, например document2.append(document1.tag), он все равно удаляет элемент из документа1.

Как я использую реальные файлы, я могу просто не сохранять документ1 после модификации, но есть ли способ сделать это без искажения документа?

Ответы

Ответ 1

В BeautifulSoup нет версий функции native clone в версиях до 4.4 (выпущен в июле 2015 года); вам нужно будет создать глубокую копию самостоятельно, что сложно, поскольку каждый элемент поддерживает ссылки на остальную часть дерева.

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

from bs4 import Tag, NavigableString

def clone(el):
    if isinstance(el, NavigableString):
        return type(el)(el)

    copy = Tag(None, el.builder, el.name, el.namespace, el.nsprefix)
    # work around bug where there is no builder set
    # https://bugs.launchpad.net/beautifulsoup/+bug/1307471
    copy.attrs = dict(el.attrs)
    for attr in ('can_be_empty_element', 'hidden'):
        setattr(copy, attr, getattr(el, attr))
    for child in el.contents:
        copy.append(clone(child))
    return copy

Этот метод чувствителен к текущей версии BeautifulSoup; Я тестировал это с 4.3, будущие версии могут добавлять атрибуты, которые также нужно скопировать.

Вы также можете обезвредить эту функцию в BeautifulSoup:

from bs4 import Tag, NavigableString


def tag_clone(self):
    copy = type(self)(None, self.builder, self.name, self.namespace, 
                      self.nsprefix)
    # work around bug where there is no builder set
    # https://bugs.launchpad.net/beautifulsoup/+bug/1307471
    copy.attrs = dict(self.attrs)
    for attr in ('can_be_empty_element', 'hidden'):
        setattr(copy, attr, getattr(self, attr))
    for child in self.contents:
        copy.append(child.clone())
    return copy


Tag.clone = tag_clone
NavigableString.clone = lambda self: type(self)(self)

позволяет вам напрямую называть .clone() на элементах:

document2.body.append(document1.find('div', id_='someid').clone())

Мой запрос функции в проект BeautifulSoup был принят и изменен, чтобы использовать copy.copy() функция; теперь, когда выпущен BeautifulSoup 4.4, вы можете использовать эту версию (или новее) и делать:

import copy

document2.body.append(copy.copy(document1.find('div', id_='someid')))

Ответ 2

Это может быть не самое быстрое решение, но оно короткое и, похоже, работает...

clonedtag = BeautifulSoup(str(sourcetag))

Эта идея была выведена из комментария Питера Вудса выше.