Ограничить глубину tag.text - PullRequest
0 голосов
/ 20 марта 2020

Я просто не могу понять это правильно. BeautifulSoup4 настолько сбивает с толку.

Я пытаюсь исправить неопубликованные ссылки Markdown в HTML тексте. Регулярное выражение:

REF = re.compile(r"\[(?P<title>.+?)\]\[(?P<identifier>.*?)\]")

Поскольку, очевидно, BS4 использует match с регулярными выражениями, я расширил регулярное выражение с помощью

REF = re.compile(r".*\[(?P<title>.+?)\]\[(?P<identifier>.*?)\].*", re.DOTALL)

Цель - найти такие строки и заменить их на фактические ссылки <a>, но не в том случае, если они находятся в теге <code> (независимо от глубины). У меня есть сопоставление для получения URL-адреса из identifier.

[<code>title<code>][identifier] должно быть сопоставлено, но <code>[title][identifier] не должно.

Если ввод:

<p>[<code>title<code>][identifier]</p>

Вывод должен быть:

<p><a id="identifier" href="http://example.com"><code>title<code></a></p>

Однако следующий ввод должен остаться без изменений:

<p><code>[title][identifier]</code></p>

Я пробовал следующее:

tags = [tag.parent for tag in soup.find_all(text=REF) if not tag.find_parent("code")]

... но отсутствовали теги. Я нашел объяснение в этом посте: BeautifulSoup - поиск по тексту внутри тега . Кажется, text (или новое имя string, хотя я обнаружил, что поведение отличается) вернет None, когда в теге есть другие теги, то есть тег <p>[<code>title<code>][identifier]</p> не будет совпадать.

Я также думал, что пост дал решение:

tags = list(
    soup.find_all(
        lambda tag: tag.name != "code" and
                    not tag.find_parent("code") and
                    REF.search(tag.text)
    )
)

... но теперь вместо того, чтобы давать мне метки рядом с листьями, он возвращает root метки, такие как <html> и <body>, потому что tag.text возвращает полный, рекурсивный текст всех потомков . Тогда, конечно, эти теги содержат текст, соответствующий регулярному выражению, но внутри <code> тегов .

Я думаю, что лучшим решением было бы попробовать регулярное выражение для текста тега, ограниченного определенная глубина. Если текст глубины 1 <p>[<code>title] [идентификатор]

равен [ ][identifier], а текст глубины 2 того же тега равен [<code>title] [идентификатор] , то глубина 2 все Мне нужно.

Есть ли способ сделать это? Или у вас есть другое решение? Я подумал, что, возможно, смогу перебрать все теги от листьев до root, в ширину, но у меня все еще будет та же проблема с tag.text, возвращающим также текст всех потомков.

1 Ответ

0 голосов
/ 20 марта 2020

Я много думал об этом и в конечном итоге использовал другой метод.

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

Так как я вообще не хочу касаться блоков кода, я заменяю code узлы во всем дереве на NavigableString, в котором создаются случайные уникальные значения. Эти значения являются заполнителями, которые мы будем использовать позже для восстановления оригинальных блоков кода. Значения построены с начальным числом, которое, я уверен, нигде не встречается в HTML, и случайным уникальным целым числом от 0 до 1000000. Я заполняю dict заполнителями в качестве ключей, а кодовые блоки - в качестве значений (строк).

Затем я получаю большую HTML строку с str(soup), и я просто запускаю re.sub в целом HTML!

Как только это будет сделано, я запускаю вторую re.sub с пользовательской функцией repl ( см. документы ), сопоставляя мои заполнители и заменяя их ранее сохраненными блоками кода.

Tada! Он отлично работает.

Вот класс Placeholder:

class Placeholder:
    def __init__(self):
        self.ids = {}
        self.seed = None
        self.set_seed()

    def store(self, value):
        i = self.get_id()
        while i in self.ids:
            i = self.get_id()
        self.ids[i] = value
        return i

    def get_id(self):
        return f"{self.seed}{random.randint(0, 1000000)}"

    def set_seed(self):
        self.seed = "".join(random.choices(string.ascii_letters + string.digits, k=16))

    def hide_code_tags(self, soup):

        def recursive_hide(tag):
            if hasattr(tag, "contents"):
                for i in range(len(tag.contents)):
                    child = tag.contents[i]
                    if child.name == "code":
                        tag.contents[i] = NavigableString(self.store(str(child)))
                    else:
                        recursive_hide(child)

        recursive_hide(soup)

        return str(soup)

    def restore_code_tags(self, soup_str):
        def replace_placeholder(match):
            placeholder = match.groups()[0]
            return self.ids[placeholder]

        return re.sub(rf"({self.seed}\d+)", replace_placeholder, soup_str)

И как его использовать:

placeholder = Placeholder()
while re.search(placeholder.seed, html) or any(placeholder.seed in url for url in MY_URLS_MAP.values()):
    placeholder.set_seed()

soup = BeautifulSoup(html, "html.parser")
soup_str = placeholder.hide_code_tags(soup)
fixed_soup_str = AUTO_REF.sub(MY_REPL_FUNCTION, soup_str)
final_soup_str = placeholder.restore_code_tags(fixed_soup_str)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...