Python Преобразование HTML в JSON с помощью Soup

Это правила

  1. Теги HTML будут начинаться с любого из следующих <p>, <ol> или <ul>.
  2. Содержимое HTML при обнаружении любого из тегов шага 1 будет содержать только следующие теги: <em>, <strong> или <span style="text-decoration:underline">.
  3. Сопоставьте теги шага два со следующими: <strong> будет этот элемент {"bold":True} в JSON, <em> будет {"italics":True} и <span style="text-decoration:underline"> будет {"decoration":"underline"}
  4. Любой найденный текст будет {"text": "this is the text"} в формате JSON.

Допустим, у меня есть приведенный ниже HTML-код: Используя это:

soup = Soup("THIS IS THE WHOLE HTML", "html.parser")
allTags = [tag for tag in soup.find_all(recursive=False)]

Что производит этот массив:

[
    <p>The name is not mine it is for the people<span style="text-decoration: underline;"><em><strong>stephen</strong></em></span><em><strong> how can</strong>name </em><strong>good</strong> <em>his name <span style="text-decoration: underline;">moneuet</span>please </em><span style="text-decoration: underline;"><strong>forever</strong></span><em>tomorrow<strong>USA</strong></em></p>,
    <p>2</p>,
    <p><strong>moment</strong><em>Africa</em> <em>China</em> <span style="text-decoration: underline;">home</span> <em>thomas</em> <strong>nothing</strong></p>,
    <ol><li>first item</li><li><em><span style="text-decoration: underline;"><strong>second item</strong></span></em></li></ol>
]

Применяя приведенные выше правила, вы получите следующий результат:

Первый элемент массива будет преобразован в этот JSON:

{
    "text": [
        "The name is not mine it is for the people",
        {"text": "stephen", "decoration": "underline", "bold": True, "italics": True}, 
        {"text": "how can", "bold": True, "italics": True},
        {"text": "name", "italics": True},
        {"text": "good", "bold": True},
        {"text": "his name", "italics": True},
        {"text": "moneuet", "decoration": "underline"},
        {"text": "please ", "italics": True},
        {"text": "forever", "decoration": "underline", "bold":True},
        {"text": "tomorrow", "italics": True},
        {"text": "USA", "bold": True, "italics": True}
    ]
}

Второй элемент массива будет преобразован в этот JSON:

{"text": ["2"] }

Третий элемент массива будет преобразован в этот JSON:

{
    "text": [
        {"text": "moment", "bold": True},
        {"text": "Africa", "italics": True},
        {"text": "China", "italics": True},
        {"text": "home", "decoration": "underline"},
        {"text": "thomas", "italics": True},
        {"text": "nothing", "bold": True}
    ]
}

Четвертый элемент массива будет преобразован в этот JSON:

{
    "ol": [
        "first item", 
        {"text": "second item", "decoration": "underline", "italics": True, "bold": True}
    ]
}

Это моя попытка, так что я могу углубиться. Но вопрос в том, как обрабатывать массив arrayOfTextAndStyles

soup = Soup("THIS IS THE WHOLE HTML", "html.parser")
allTags = [tag for tag in soup.find_all(recursive=False)]
for foundTag in allTags:
   foundTagStyles = [tag for tag in foundTag.find_all(recursive=True)]
      if len(foundTagStyles ) > 0:
         if str(foundTag.name) == "p":
              arrayOfTextAndStyles = [{"tag": tag.name, "text": 
                  foundTag.find_all(text=True, recursive=False) }] +  
                    [{"tag":tag.name, "text": foundTag.find_all(text=True, 
                    recursive=False) } for tag in foundTag.find_all()]

         elif  str(foundTag.name) == "ol":

         elif  str(foundTag .name) == "ul":

person Ernest Appiah    schedule 29.09.2017    source источник
comment
Вам нужно придумать более последовательный формат вывода; почему второй абзац не приводит к списку, а все остальные делают? Почему в третьем абзаце нет начального текстового элемента перед всеми вложенными словарями?   -  person Martijn Pieters    schedule 29.09.2017
comment
В качестве альтернативы, почему бы не обернуть весь текст в словарь? Итак, для первого примера первым элементом будет {"text": "The name is not mine it is for the people"}.   -  person Martijn Pieters    schedule 29.09.2017
comment
@MartijnPieters отредактировал вывод 2   -  person Ernest Appiah    schedule 29.09.2017
comment
Куда делся can в вашем первом примере? Как на самом деле следует обращаться с <em><strong> how can</strong>name </em>? Это вложенная структура с текстом на двух уровнях.   -  person Martijn Pieters    schedule 29.09.2017
comment
Между «‹сильно›хорошо‹/сильно›» и <em>his name ... есть пробел, за которым следует дополнительное вложение.   -  person Martijn Pieters    schedule 29.09.2017
comment
@MartijnPieters отредактировал вопрос, чтобы включить банку в вывод. я пропустил это   -  person Ernest Appiah    schedule 29.09.2017
comment
@MartijnPieters, вы можете игнорировать пробел.   -  person Ernest Appiah    schedule 29.09.2017
comment
Тогда ваш текст не может быть реконструирован. Вы должны отслеживать пробелы. Используйте согласованную вложенную структуру. Мой ответ ниже создает такую ​​​​структуру.   -  person Martijn Pieters    schedule 29.09.2017


Ответы (1)


Я бы использовал функцию для разбора каждого элемента, а не один огромный цикл. Выберите теги p и ol и сгенерируйте исключение при синтаксическом анализе, чтобы пометить все, что не соответствует вашим конкретным правилам:

from bs4 import NavigableString

def parse(elem):
    if elem.name == 'ol':
        result = []
        for li in elem.find_all('li'):
            if len(li) > 1:
                result.append([parse_text(sub) for sub in li])
            else:
                result.append(parse_text(next(iter(li))))
        return {'ol': result}
    return {'text': [parse_text(sub) for sub in elem]}

def parse_text(elem):
    if isinstance(elem, NavigableString):
        return {'text': elem}

    result = {}
    if elem.name == 'em':
        result['italics'] = True
    elif elem.name == 'strong':
        result['bold'] = True
    elif elem.name == 'span':
        try:
            # rudimentary parse into a dictionary
            styles = dict(
                s.replace(' ', '').split(':') 
                for s in elem.get('style', '').split(';')
                if s.strip()
            )
        except ValueError:
            raise ValueError('Invalid structure')
        if 'underline' not in styles.get('text-decoration', ''):
            raise ValueError('Invalid structure')
        result['decoration'] = 'underline'
    else:
        raise ValueError('Invalid structure')

    if len(elem) > 1:
        result['text'] = [parse_text(sub) for sub in elem]
    else:
        result.update(parse_text(next(iter(elem))))
    return result

Затем вы анализируете свой документ:

for candidate in soup.select('ol,p'):
    try:
        result = parse(candidate)
    except ValueError:
        # invalid structure, ignore
        continue
    print(result)

Используя pprint, это приводит к:

{'text': [{'text': 'The name is not mine it is for the people'},
          {'bold': True,
           'decoration': 'underline',
           'italics': True,
           'text': 'stephen'},
          {'italics': True,
           'text': [{'bold': True, 'text': ' how can'}, {'text': 'name '}]},
          {'bold': True, 'text': 'good'},
          {'text': ' '},
          {'italics': True,
           'text': [{'text': 'his name '},
                    {'decoration': 'underline', 'text': 'moneuet'},
                    {'text': 'please '}]},
          {'bold': True, 'decoration': 'underline', 'text': 'forever'},
          {'italics': True,
           'text': [{'text': 'tomorrow'}, {'bold': True, 'text': 'USA'}]}]}
{'text': [{'text': '2'}]}
{'text': [{'bold': True, 'text': 'moment'},
          {'italics': True, 'text': 'Africa'},
          {'text': ' '},
          {'italics': True, 'text': 'China'},
          {'text': ' '},
          {'decoration': 'underline', 'text': 'home'},
          {'text': ' '},
          {'italics': True, 'text': 'thomas'},
          {'text': ' '},
          {'bold': True, 'text': 'nothing'}]}
{'ol': [{'text': 'first item'},
        {'bold': True,
         'decoration': 'underline',
         'italics': True,
         'text': 'second item'}]}

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

Структура также достаточно последовательна; ключ 'text' будет указывать либо на одну строку, или на список словарей. Такой список никогда не будет смешивать типы. Вы могли бы улучшить это еще; пусть 'text' только указывает на строку и использует другой ключ для обозначения вложенных данных, например contains или nested или аналогичный, а затем использует только один или другой. Все, что потребуется, это изменить ключи 'text' в случае len(elem) > 1 и в функции parse().

person Martijn Pieters    schedule 29.09.2017
comment
Я тестирую, и я скоро вернусь. Спасибо за вашу помощь - person Ernest Appiah; 29.09.2017
comment
Можно ли преобразовать весь результат в допустимый массив json, например json.dumps(result) для окончательного результата. Результат выглядит очень многообещающе, однако конечный результат не в формате json, т.е. результат - person Ernest Appiah; 29.09.2017
comment
@ErnestAppiah: я обновил ответ, чтобы исправить небольшую ошибку при обработке вложенных элементов с несколькими дочерними элементами. - person Martijn Pieters; 29.09.2017
comment
@ErnestAppiah: окончательный результат сделать несложно. Вместо print(result) в последнем фрагменте (где я перебираю soup.select('ol,p')) добавьте результат в список. Затем используйте json.dumps(list_produced). - person Martijn Pieters; 29.09.2017
comment
Спасибо большое. Ты обалденный. Как я могу дать вам 500 звезд. Спасибо большое. Я очень ценю вашу помощь - person Ernest Appiah; 29.09.2017
comment
Определенно принесет вам одно из вашего списка желаний amazon.co.uk/registry/wishlist/1E5V9AJISGDO5 - person Ernest Appiah; 29.09.2017