⇤ ← Версия 1 от 2010-05-09 15:31:07
13097
Комментарий:
|
40030
|
Удаления помечены так. | Добавления помечены так. |
Строка 307: | Строка 307: |
= Beautiful Soup дает тебе Unicode, черт побери = Во время синтаксического разбора документа он перекодируется в Unicode. В своих структурах данных Beautiful Soup хранит только строки Unicode. {{{#!highlight python from BeautifulSoup import BeautifulSoup soup = BeautifulSoup("Hello") soup.contents[0] # u'Hello' soup.originalEncoding # 'ascii' }}} Вот пример с японским документом в кодировке UTF-8: {{{#!highlight python from BeautifulSoup import BeautifulSoup soup = BeautifulSoup("\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf") soup.contents[0] # u'\u3053\u308c\u306f' soup.originalEncoding # 'utf-8' str(soup) # '\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf' # Note: this bit uses EUC-JP, so it only works if you have cjkcodecs # installed, or are running Python 2.4. soup.__str__('euc-jp') # '\xa4\xb3\xa4\xec\xa4\xcf' }}} Beautiful Soup использует класс с именем UnicodeDammit для определения кодировки передаваемых вами документов и перекодировки его в Unicode, не беспокойтесь об этом. Если это также необходимо для других документов (без их синтаксического разбора с помощью Beautiful Soup), то вы можете использовать UnicodeDammit отдельно. Он в значительной мере основан на коде из [[http://www.feedparser.org/|Universal Feed Parser]]. Если вы работаете с Python более ранних версий, чем 2.4, то заранее скачайте и установите [[http://cjkpython.i18n.org/|cjkcodecs и iconvcodec]], которые добавляют в Python поддержку многих кодеков, особенно кодеков CJK. Для лучшего автоопределения кодировки установите также библиотеку [[http://chardet.feedparser.org/|chardet]]. Перед перекодировкой в Unicode Beautiful Soup проверяет кодировки в следующем порядке: * Кодировка, переданная конструктору супа в параметре fromEncoding. * Кодировка, обнаруженная в самом документе: например, в декларации XML или (для документов HTML) в атрибуте http-equiv тега META. Как только Beautiful Soup обнаружит подобное указание о кодировке документа, он повторно приступит к синтаксическому разбору документа с самого начала, но уже применяя найденную кодировку. Избежать этого можно только явным заданием кодировки, которая будет работать: тогда любая найденная в документе кодировка будет игнорироваться. * Кодировка, вычисленная по нескольким первым байтам файла. Если кодировка определяется на этом этапе, то она будет либо UTF-*, либо EBCDIC, либо ASCII. * Кодировка, вычисленная библиотекой [[http://chardet.feedparser.org/|chardet]], в случае если она была установлена. * UTF-8 * Windows-1252 Если Beautiful Soup сможет сделать предположение, то почти всегда оно будет верным. Но для документов без декларации или в неизвестной кодировке он не сможет сделать каких-либо предположений. В таком случае – скорее всего, ошибочно, – будет использоваться кодировка Windows-1252. Вот пример в кодировке EUC-JP, когда предположение Beautiful Soup о кодировке будет ошибочным. (Кроме того, поскольку используется кодировка EUC-JP пример будет работать только под Python 2.4 или если установлен cjkcodecs): {{{#!highlight python from BeautifulSoup import BeautifulSoup euc_jp = '\xa4\xb3\xa4\xec\xa4\xcf' soup = BeautifulSoup(euc_jp) soup.originalEncoding # 'windows-1252' str(soup) # '\xc2\xa4\xc2\xb3\xc2\xa4\xc3\xac\xc2\xa4\xc3\x8f' # Wrong! }}} Но если задать кодировку с помощью fromEncoding, то синтаксический разбор документа будет корректным и его можно будет перекодировать в UTF-8 или обратно в – EUC-JP. {{{#!highlight python soup = BeautifulSoup(euc_jp, fromEncoding="euc-jp") soup.originalEncoding # 'windows-1252' str(soup) # '\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf' # Right! soup.__str__(self, 'euc-jp') == euc_jp # True }}} Если обрабатывать с помощью Beautiful Soup документ в кодировке Windows-1252 (или подобных, например, ISO-8859-1 или ISO-8859-2), то Beautiful Soup обнаружит и уничтожит изящные кавычки и другие символы, специфичные для Windows. Чтобы этого избежать Beautiful Soup преобразует эти символы в сущности HTML (BeautifulSoup) или в сущности XML (BeautifulStoneSoup). Чтобы этого избежать можно передать параметр smartQuotesTo=None в конструктор супа: тогда кавычки будут конвертироваться в Unicode также, как и другие символы данной кодировки. Для изменения поведения BeautifulSoup и BeautifulStoneSoup можно передать в параметре smartQuotesTo значения "xml" или "html". {{{#!highlight python from BeautifulSoup import BeautifulSoup, BeautifulStoneSoup text = "Deploy the \x91SMART QUOTES\x92!" str(BeautifulSoup(text)) # 'Deploy the ‘SMART QUOTES’!' str(BeautifulStoneSoup(text)) # 'Deploy the ‘SMART QUOTES’!' str(BeautifulSoup(text, smartQuotesTo="xml")) # 'Deploy the ‘SMART QUOTES’!' BeautifulSoup(text, smartQuotesTo=None).contents[0] # u'Deploy the \u2018SMART QUOTES\u2019!' }}} = Печать документа = Документ Beautiful Soup (или любое их подмножество) можно превратить в строку с помощью функции str или методов prettify или renderContents. Также можно использовать функцию unicode для получения всего документа в виде строки Unicode. Метод prettify добавляет значимые переводы строки и пробелы для придания структуре документа лучшей читабельности. Метод также удаляет текстовые узлы, состоящие только из пробелов, что может изменить смысл XML документа. Функции str и unicode не удаляют текстовые узлы, состоящие только из пробелов, и не добавляют пробелы между узлами. Приведем пример. {{{#!highlight python from BeautifulSoup import BeautifulSoup doc = "<html><h1>Heading</h1><p>Text" soup = BeautifulSoup(doc) str(soup) # '<html><h1>Heading</h1><p>Text</p></html>' soup.renderContents() # '<html><h1>Heading</h1><p>Text</p></html>' soup.__str__() # '<html><h1>Heading</h1><p>Text</p></html>' unicode(soup) # u'<html><h1>Heading</h1><p>Text</p></html>' soup.prettify() # '<html>\n <h1>\n Heading\n </h1>\n <p>\n Text\n </p>\n</html>' print soup.prettify() # <html> # <h1> # Heading # </h1> # <p> # Text # </p> # </html> }}} Обратите внимание, что функции str и renderContents дают различный результат, когда используются для тегов внутри документа. Функция str печатает и теги и их содержимое, а функция renderContents - только содержимое. {{{#!highlight python heading = soup.h1 str(heading) # '<h1>Heading</h1>' heading.renderContents() # 'Heading' }}} При вызове функций __str__, prettify или renderContents, вы можете задать кодировку вывода. Кодировкой по умолчанию (используется функцией str) является UTF-8. Вот пример разбора строки ISO-8851-1 с последующим выводом на экран той же строки в других кодировках: {{{#!highlight python from BeautifulSoup import BeautifulSoup doc = "Sacr\xe9 bleu!" soup = BeautifulSoup(doc) str(soup) # 'Sacr\xc3\xa9 bleu!' # UTF-8 soup.__str__("ISO-8859-1") # 'Sacr\xe9 bleu!' soup.__str__("UTF-16") # '\xff\xfeS\x00a\x00c\x00r\x00\xe9\x00 \x00b\x00l\x00e\x00u\x00!\x00' soup.__str__("EUC-JP") # 'Sacr\x8f\xab\xb1 bleu!' }}} Если в исходном документе в декларации указывалась кодировка, то при обратном преобразовании в строку Beautiful Soup запишет в декларацию новую кодировку. Это означает, что если загрузить документ HTML в BeautifulSoup, а потом распечатать его, то HTML не только будет вычищен, но и прозрачно перекодирован в UTF-8. Пример HTML: {{{#!highlight python from BeautifulSoup import BeautifulSoup doc = """<html> <meta http-equiv="Content-type" content="text/html; charset=ISO-Latin-1" > Sacr\xe9 bleu! </html>""" print BeautifulSoup(doc).prettify() # <html> # <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> # Sacré bleu! # </html> }}} Пример XML: {{{#!highlight python from BeautifulSoup import BeautifulStoneSoup doc = """<?xml version="1.0" encoding="ISO-Latin-1">Sacr\xe9 bleu!""" print BeautifulStoneSoup(doc).prettify() # <?xml version='1.0' encoding='utf-8'> # Sacré bleu! }}} = Дерево синтаксического разбора = До сих пор мы рассматривали загрузку и запись документов. Однако, большую часть времени будет приковывать к себе дерево синтаксического разбора: структуры данных Beautiful Soup, которые создаются по мере синтаксического разбора документа. Объект парсера (экземпляр класса BeautifulSoup или BeautifulStoneSoup) обладает большой глубиной вложенности связанных структур данных, соответствующих структуре документа XML или HTML. Объект парсера состоит из объектов двух других типов: объектов Tag, которые соответствуют тегам, к примеру, тегу <TITLE> и тегу <B>; и объекты NavigableString, соответствующие таким строкам как "Page title" или "This is paragraph". Класс NavigableString имеет несколько подклассов (CData, Comment, Declaration и ProcessingInstruction), которые соответствуют специальным конструкциям в XML. Они работают также как NavigableStringи, за исключением того, что когда приходит время выводить их на экран, они содержат некоторые дополнительные данные. Вот документ с включенным в него комментарием: {{{#!highlight python from BeautifulSoup import BeautifulSoup import re hello = "Hello! <!--I've got to be nice to get what I want.-->" commentSoup = BeautifulSoup(hello) comment = commentSoup.find(text=re.compile("nice")) comment.__class__ # <class 'BeautifulSoup.Comment'> comment # u"I've got to be nice to get what I want." comment.previousSibling # u'Hello! ' str(comment) # "<!--I've got to be nice to get what I want.-->" print commentSoup # Hello! <!--I've got to be nice to get what I want.--> }}} Итак, давайте более внимательно посмотрим на тот документ, что приводился в начале документации: {{{#!highlight python from BeautifulSoup import BeautifulSoup doc = ['<html><head><title>Page title</title></head>', '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.', '<p id="secondpara" align="blah">This is paragraph <b>two</b>.', '</html>'] soup = BeautifulSoup(''.join(doc)) print soup.prettify() # <html> # <head> # <title> # Page title # </title> # </head> # <body> # <p id="firstpara" align="center"> # This is paragraph # <b> # one # </b> # . # </p> # <p id="secondpara" align="blah"> # This is paragraph # <b> # two # </b> # . # </p> # </body> # </html> }}} == Атрибуты Tag-ов == Объекты Tag и NavigableString имеют множество полезных элементов, большая часть которых описывается в разделах Навигация по дереву синтаксического разбора и Поиск в дереве синтаксического разбора. Тем не менее, рассмотрим здесь один аспект объектов Tag: атрибуты. Теги SGML имеют атрибуты: например, каждый тег <P> в приведенном выше примере HTML имеет атрибуты "id" и "align". К атрибутам тегов можно обращаться таким же образом, как если бы объект Tag был словарем: {{{#!highlight python firstPTag, secondPTag = soup.findAll('p') firstPTag['id'] # u'firstPara' secondPTag['id'] # u'secondPara' }}} NavigableString objects don't have attributes; only Tag objects have them. = Навигация по дереву синтаксического разбора = Все объекты Tag содержат элементы, перечисленные ниже (тем не менее, фактическое значение элемента может равняться None). Объекты NavigableString имеют все из них за исключением contents и string. == parent == В примере выше, родителем объекта <HEAD> Tag является объект <HTML> Tag. Родителем объекта <HTML> Tag является сам объект парсера BeautifulSoup. Родитель объекта парсера равен None. Передвигаясь по объектам parent, можно перемещаться по дереву синтаксического разбора: {{{#!highlight python soup.head.parent.name # u'html' soup.head.parent.parent.__class__.__name__ # 'BeautifulSoup' soup.parent == None # True }}} == contents == С помощью parent вы перемещаетесь вверх по дереву синтаксического разбора. С помощью contents вы перемещаетесь вниз по дереву синтаксического разбора. contents является упорядоченным списком объектов Tag и NavigableString, содержащихся в элементе страницы (page element). Только объект парсера самого высокого уровня и объекты Tag содержат contents. Объекты NavigableString являются простыми строками и не могут содержать подэлементов, поэтому они не содержат contents. В примере выше, элемент contents первого объекта <P> Tag является списком, содержащим объект NavigableString ("This is paragraph "), объекта <B> Tag и еще одного объекта NavigableString ("."). Элемент contents объекта <B> Tag: список, состоящий из одного объекта NavigableString ("one"). {{{#!highlight python pTag = soup.p pTag.contents # [u'This is paragraph ', <b>one</b>, u'.'] pTag.contents[1].contents # [u'one'] pTag.contents[0].contents # AttributeError: 'NavigableString' object has no attribute 'contents' }}} == string == Для вашего удобства сделано так, что в случае, когда тег имеет только один дочерний узел, который является строкой, дочерний узел будет доступен через tag.string точно также как и через tag.contents[0]. В примере выше soup.b.string является объектом NavigableString отображающий Unicode-строку "one". Эта строка содержится в первом объекте <B> Tag дерева синтаксического разбора. {{{#!highlight python soup.b.string # u'one' soup.b.contents[0] # u'one' }}} Но soup.p.string равен None, поскольку первый объект <P> Tag в дереве синтаксического разбора имеет более одного дочернего элемента. soup.head.string также равен None, хотя объект <HEAD> Tag имеет только один дочерний элемент, поскольку этот дочерний элемент – Tag (объект <TITLE> Tag), а не объект NavigableString. {{{#!highlight python soup.p.string == None # True soup.head.string == None # True }}} == nextSibling и previousSibling == Эти элементы позволяют пропускать следующий или предыдущий элемент на этом же уровне дерева синтаксического разбора. В представленном выше документе, элемент nextSibling объекта <HEAD> Tag равен объекту <BODY> Tag, поскольку объект <BODY> Tag является следующим вложенным элементом по отношению к объекту <html> Tag. Элемент nextSibling объекта <BODY> Tag равен None, поскольку в нем больше нет вложенных по отношению к объекту <HTML> Tag элементов. {{{#!highlight python soup.head.nextSibling.name # u'body' soup.html.nextSibling == None # True }}} И наоборот, элемент previousSibling объекта <BODY> Tag равен объекту <HEAD> tag, а элемент previousSibling объекта <HEAD> Tag равен None: {{{#!highlight python soup.body.previousSibling.name # u'head' soup.head.previousSibling == None # True }}} Несколько примеров: элемент nextSibling первого объекта <P> Tag равен второму объекту <P> Tag. Элемент previousSibling объекта <B> Tag внутри второго объекта <P> Tag равен объекту NavigableString "This is paragraph". Элемент previousSibling данного объекта NavigableString равен None, внутри первого объекта <P> Tag. {{{#!highlight python soup.p.nextSibling # <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p> secondBTag = soup.findAlll('b')[1] secondBTag.previousSibling # u'This is paragraph' secondBTag.previousSibling.previousSibling == None # True }}} == next и previous == Данные элементы позволяют передвигаться по элементам документа в том порядке, в котором они были обработаны парсером, а не в порядке появления в дереве. Например, элемент next для объекта <HEAD> Tag равен объекту <TITLE> Tag, а не объекту <BODY> Tag. Это потому, что в исходном документе, тег <TITLE> идет сразу после тега <HEAD>. {{{#!highlight python soup.head.next # u'title' soup.head.nextSibling.name # u'body' soup.head.previous.name # u'html' }}} Поскольку элементы next и previous связаны, содержимое элемента contents объекта Tag обновляется до того как элемент nextSibling. Как правило, эти элементы не используют, но иногда это наиболее быстрый способ получить что-либо скрытое в глубине дерева синтаксического разбора. == Итерации над объектом Tag == Над элементом contents объекта Tag можно производить итерации, рассматривая его качестве списка. Это полезное упрощение. Подобным образом можно узнать, сколько дочерних узлов имеет объект Tag, вызвав функцию len(tag) вместо len(tag.contents). В терминах документа выше: {{{#!highlight python for i in soup.body: print i # <p id="firstpara" align="center">This is paragraph <b>one</b>.</p> # <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p> len(soup.body) # 2 len(soup.body.contents) # 2 }}} == Используем имена тегов как элементы == Гораздо легче перемещаться по дереву синтаксического разбора, если в качестве имен тегов выступали элементы парсера или объекта Tag. Будем так делать на протяжении следующих примеров. В терминах документа выше, soup.head возвращает нам первый (как и следовало ожидать, единственный) объекта <HEAD> Tag документа: {{{#!highlight python soup.head # <head><title>Page title</title></head> }}} Вообще, вызов mytag.foo возвратит первого потомка mytag, чем, как ни странно, будет объект <FOO> Tag. Если каких-либо объектов <FOO> Tag внутри mytag нет, то mytag.foo возвратит None. Вы можете использовать это для очень быстрого обхода дерева синтаксического разбора: {{{#!highlight python soup.head.title # <title>Page title</title> soup.body.p.b.string # u'one' }}} Также это можно использовать для быстрого перехода к определенной части дерева синтаксического разбора. Например, если вас не беспокоит отсутствие тегов <TITLE> на предусмотренных для них месте, вы можете просто использовать soup.title для получения названия документа HTML. Вам не нужно использовать soup.head.title: {{{#!highlight python soup.title.string # u'Page title' }}} soup.p перейдет к первому тегу <P> внутри документа, где бы тот ни был. soup.table.tr.td перейдет к первому столбцу первой строки первой же таблицы документа. Фактически эти элементы – алиасы для метода first, описываемого ниже. Я упоминаю их здесь потому, что алиасы делают очень легким увеличение масштаба интересующей вас части хорошо знакомого дерева синтаксического разбора. Альтернативная форма этого стиля позволяет обращаться к первому тегу <FOO> как .fooTag вместо .foo. Например, soup.table.tr.td можно также отобразить как soup.tableTag.trTag.tdTag или даже soup.tableTag.tr.tdTag. Это полезно если вы предпочитаете быть более осведомленным о том, что делаете или если вы разбираете XML, в котором имена тегов конфликтуют с именами методов и элементов Beautiful Soup. {{{#!highlight python from BeautifulSoup import BeautifulStoneSoup xml = '<person name="Bob"><parent rel="mother" name="Alice">' xmlSoup = BeautifulStoneSoup(xml) xmlSoup.person.parent # A Beautiful Soup member # <person name="Bob"><parent rel="mother" name="Alice"></parent></person> xmlSoup.person.parentTag # A tag name # <parent rel="mother" name="Alice"></parent> }}} Если вы присмотритесь к именам тегов, то увидите что они не являются корректными идентификаторами Python (как hyphenated-name), вам нужно использовать first. |
Beautiful Soup - это парсер для синтаксического разбора файлов HTML/XML, написанный на языке программирования Python, который может преобразовать даже неправильную разметку в дерево синтаксического разбора. Он поддерживает простые и естественные способы навигации, поиска и модификации дерева синтаксического разбора. В большинстве случаев он поможет программисту сэкономить часы и дни работы. Написанный на языке программирования Ruby порт называется Rubyful Soup.
Данный документ иллюстрирует основные возможности Beautiful Soup версии 3.0 на примерах. Вы увидите, для чего лучше использовать данную библиотеку, как она работает, как ее использовать, как добиться необходимых вам результатов и что делать, когда она не оправдывает ваших ожиданий.
Содержание
Быстрый старт
Скачать Beautiful Soup можно здесь. Список изменений содержит отличия версии 3.0 от более ранних.
Подключить Beautiful Soup к вашему приложению можно с помощью одной из ниже приведенных строк:
Следующий код демонстрирует основные возможности Beautiful Soup. Можете скопировать его в сессию Python и запустить.
1 from BeautifulSoup import BeautifulSoup
2 import re
3
4 doc = ['<html><head><title>Page title</title></head>',
5 '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
6 '<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
7 '</html>']
8 soup = BeautifulSoup(''.join(doc))
9
10 print soup.prettify()
11 # <html>
12 # <head>
13 # <title>
14 # Page title
15 # </title>
16 # </head>
17 # <body>
18 # <p id="firstpara" align="center">
19 # This is paragraph
20 # <b>
21 # one
22 # </b>
23 # .
24 # </p>
25 # <p id="secondpara" align="blah">
26 # This is paragraph
27 # <b>
28 # two
29 # </b>
30 # .
31 # </p>
32 # </body>
33 # </html>
Продемонстрируем несколько способов навигации по супу:
1 soup.contents[0].name
2 # u'html'
3
4 soup.contents[0].contents[0].name
5 # u'head'
6
7 head = soup.contents[0].contents[0]
8 head.parent.name
9 # u'html'
10
11 head.next
12 # <title>Page title</title>
13
14 head.nextSibling.name
15 # u'body'
16
17 head.nextSibling.contents[0]
18 # <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>
19
20 head.nextSibling.contents[0].nextSibling
21 # <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>
А вот как искать в супе определенные теги или теги с заданными атрибутами:
1 titleTag = soup.html.head.title
2 titleTag
3 # <title>Page title</title>
4
5 titleTag.string
6 # u'Page title'
7
8 len(soup('p'))
9 # 2
10
11 soup.findAll('p', align="center")
12 # [<p id="firstpara" align="center">This is paragraph <b>one</b>. </p>]
13
14 soup.find('p', align="center")
15 # <p id="firstpara" align="center">This is paragraph <b>one</b>. </p>
16
17 soup('p', align="center")[0]['id']
18 # u'firstpara'
19
20 soup.find('p', align=re.compile('^b.*'))['id']
21 # u'secondpara'
22
23 soup.find('p').b.string
24 # u'one'
25
26 soup('p')[1].b.string
27 # u'two'
Изменять суп также весьма просто:
1 titleTag['id'] = 'theTitle'
2 titleTag.contents[0].replaceWith("New title")
3 soup.html.head
4 # <head><title id="theTitle">New title</title></head>
5
6 soup.p.extract()
7 soup.prettify()
8 # <html>
9 # <head>
10 # <title id="theTitle">
11 # New title
12 # </title>
13 # </head>
14 # <body>
15 # <p id="secondpara" align="blah">
16 # This is paragraph
17 # <b>
18 # two
19 # </b>
20 # .
21 # </p>
22 # </body>
23 # </html>
24
25 soup.p.replaceWith(soup.b)
26 # <html>
27 # <head>
28 # <title id="theTitle">
29 # New title
30 # </title>
31 # </head>
32 # <body>
33 # <b>
34 # two
35 # </b>
36 # </body>
37 # </html>
38
39 soup.body.insert(0, "This page used to have ")
40 soup.body.insert(2, " <p> tags!")
41 soup.body
42 # <body>This page used to have <b>two</b> <p> tags!</body>
Приведем реальный пример. Скачаем ICC Commercial Crime Services weekly piracy report, произведем его синтаксический разбор с помощью Beautiful Soup и выведем на экран сообщения о случаях пиратства (piracy incidents):
1 import urllib2
2 from BeautifulSoup import BeautifulSoup
3
4 page = urllib2.urlopen("http://www.icc-ccs.org/prc/piracyreport.php")
5 soup = BeautifulSoup(page)
6 for incident in soup('td', width="90%"):
7 where, linebreak, what = incident.contents[:3]
8 print where.strip()
9 print what.strip()
10 print
Синтаксический разбор документа
Для работы конструктору Beautiful Soup требуется документ XML или HTML в виде строки (или открытого файлоподобного объекта). Он произведет синтаксический разбор и создаст в памяти структуры данных, соответствующие документу.
Если обработать с помощью Beautiful Soup хорошо оформленный документ, то разобранная структура будет выглядеть также как и исходный документ. Но если его разметка будет содержать ошибки, то Beautiful Soup использует эвристические методы для построения наиболее подходящей структуры данных.
Синтаксический разбор HTML
Используйте класс BeautifulSoup для синтаксического разбора документа HTML. Несколько фактов, которые необходимо знать о BeautifulSoup:
Некоторые теги могут быть вложенными (<BLOCKQUOTE>), а некоторые - нет (<P>). Таблицы и списки тегов имеют естественный порядок вложенности. Например, теги <TD> появляются только в обрамлении тегов <TR> и никак иначе. Содержимое тега <SCRIPT> не будет участвовать в разборе HTML. Тег <META> может определять кодировку документа. Вот как это работает:
1 from BeautifulSoup import BeautifulSoup
2 html = "<html><p>Para 1<p>Para 2<blockquote>Quote 1<blockquote>Quote 2"
3 soup = BeautifulSoup(html)
4 print soup.prettify()
5 # <html>
6 # <p>
7 # Para 1
8 # </p>
9 # <p>
10 # Para 2
11 # <blockquote>
12 # Quote 1
13 # <blockquote>
14 # Quote 2
15 # </blockquote>
16 # </blockquote>
17 # </p>
18 # </html>
Обратите внимание на то, что BeautifulSoup вычисляет наиболее вероятные места для закрывающих тегов, даже если они отсутствуют в исходном документе.
Хотя приведенный документ HTML является невалидным, с ним все же можно работать. А вот действительно отвратительный документ. Помимо всего прочего он содержит тег <FORM>, который начинается вне тега <TABLE>, но закрывается внутри тега <TABLE>. (Пример такого HTML был найден на веб-сайте одной из ведущих веб-компаний.)
Beautiful Soup справится с обработкой и такого документа:
1 print BeautifulSoup(html).prettify()
2 # <html>
3 # <form>
4 # <table>
5 # <td>
6 # <input name="input1" />
7 # Row 1 cell 1
8 # </td>
9 # <tr>
10 # <td>
11 # Row 2 cell 1
12 # </td>
13 # </tr>
14 # </table>
15 # </form>
16 # <td>
17 # Row 2 cell 2
18 # <br />
19 # This
20 # sure is a long cell
21 # </td>
22 # </html>
Последняя ячейка таблицы находится вне тега <TABLE>; Beautiful Soup решил закрыть тег <TABLE> там, где закрыт тег <FORM>. Автор исходного документа планировал, вероятно, продлить действие тега <FORM> до конца таблицы, но Beautiful Soup не сможет об этом догадаться. Даже в таком необычном случае Beautiful Soup произведет синтаксический разбор и предоставит вам доступ ко всем данным документа.
Синтаксический разбор XML
Класс BeautifulSoup содержит эвристики, полностью аналогичные применяющимся в веб-браузерах, что позволяет делать предположения о замыслах авторов HTML файлов. Но в XML нет фиксированного порядка тегов такие эвристики здесь не пригодятся. Поэтому BeautifulSoup не сможет хорошо работать с XML.
Используйте класс BeautifulStoneSoup для синтаксического разбора документов XML. Это основной класс, не требующий знания диалекта XML и имеющий очень простые правила о вложенности тегов: Вот он в действии:
1 from BeautifulSoup import BeautifulStoneSoup
2 xml = "<doc><tag1>Contents 1<tag2>Contents 2<tag1>Contents 3"
3 soup = BeautifulStoneSoup(xml)
4 print soup.prettify()
5 # <doc>
6 # <tag1>
7 # Contents 1
8 # <tag2>
9 # Contents 2
10 # </tag2>
11 # </tag1>
12 # <tag1>
13 # Contents 3
14 # </tag1>
15 # </doc>
Одним из общеизвестных недостатков BeautifulStoneSoup является то, что он не знает о самозакрывающихся тегах. В HTML имеется фиксированный набор самозакрывающихся тегов, но в случае с XML все зависит от того, что записано в DTD. Вы можете сообщить BeautifulStoneSoup, что определенные теги являются самозакрывающимися, передав их имена через аргумент конструктора selfClosingTags:
1 from BeautifulSoup import BeautifulStoneSoup
2 xml = "<tag>Text 1<selfclosing>Text 2"
3 print BeautifulStoneSoup(xml).prettify()
4 # <tag>
5 # Text 1
6 # <selfclosing>
7 # Text 2
8 # </selfclosing>
9 # </tag>
10
11 print BeautifulStoneSoup(xml, selfClosingTags=['selfclosing']).prettify()
12 # <tag>
13 # Text 1
14 # <selfclosing />
15 # Text 2
16 # </tag>
Если это не работает
Имеется несколько других классов парсеров, эвристики которых отличаются от двух описанных выше. Также вы можете создать подкласс и настроить парсер и задать в нем свои собственные эвристики.
Beautiful Soup дает тебе Unicode, черт побери
Во время синтаксического разбора документа он перекодируется в Unicode. В своих структурах данных Beautiful Soup хранит только строки Unicode.
Вот пример с японским документом в кодировке UTF-8:
1 from BeautifulSoup import BeautifulSoup
2 soup = BeautifulSoup("\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf")
3 soup.contents[0]
4 # u'\u3053\u308c\u306f'
5 soup.originalEncoding
6 # 'utf-8'
7
8 str(soup)
9 # '\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf'
10
11 # Note: this bit uses EUC-JP, so it only works if you have cjkcodecs
12 # installed, or are running Python 2.4.
13 soup.__str__('euc-jp')
14 # '\xa4\xb3\xa4\xec\xa4\xcf'
Beautiful Soup использует класс с именем UnicodeDammit для определения кодировки передаваемых вами документов и перекодировки его в Unicode, не беспокойтесь об этом. Если это также необходимо для других документов (без их синтаксического разбора с помощью Beautiful Soup), то вы можете использовать UnicodeDammit отдельно. Он в значительной мере основан на коде из Universal Feed Parser.
Если вы работаете с Python более ранних версий, чем 2.4, то заранее скачайте и установите cjkcodecs и iconvcodec, которые добавляют в Python поддержку многих кодеков, особенно кодеков CJK. Для лучшего автоопределения кодировки установите также библиотеку chardet.
Перед перекодировкой в Unicode Beautiful Soup проверяет кодировки в следующем порядке:
- Кодировка, переданная конструктору супа в параметре fromEncoding.
- Кодировка, обнаруженная в самом документе: например, в декларации XML или (для документов HTML) в атрибуте http-equiv тега META. Как только Beautiful Soup обнаружит подобное указание о кодировке документа, он повторно приступит к синтаксическому разбору документа с самого начала, но уже применяя найденную кодировку. Избежать этого можно только явным заданием кодировки, которая будет работать: тогда любая найденная в документе кодировка будет игнорироваться.
- Кодировка, вычисленная по нескольким первым байтам файла. Если кодировка определяется на этом этапе, то она будет либо UTF-*, либо EBCDIC, либо ASCII.
Кодировка, вычисленная библиотекой chardet, в случае если она была установлена.
- UTF-8
- Windows-1252
Если Beautiful Soup сможет сделать предположение, то почти всегда оно будет верным. Но для документов без декларации или в неизвестной кодировке он не сможет сделать каких-либо предположений. В таком случае – скорее всего, ошибочно, – будет использоваться кодировка Windows-1252. Вот пример в кодировке EUC-JP, когда предположение Beautiful Soup о кодировке будет ошибочным. (Кроме того, поскольку используется кодировка EUC-JP пример будет работать только под Python 2.4 или если установлен cjkcodecs):
Но если задать кодировку с помощью fromEncoding, то синтаксический разбор документа будет корректным и его можно будет перекодировать в UTF-8 или обратно в – EUC-JP.
Если обрабатывать с помощью Beautiful Soup документ в кодировке Windows-1252 (или подобных, например, ISO-8859-1 или ISO-8859-2), то Beautiful Soup обнаружит и уничтожит изящные кавычки и другие символы, специфичные для Windows. Чтобы этого избежать Beautiful Soup преобразует эти символы в сущности HTML (BeautifulSoup) или в сущности XML (BeautifulStoneSoup).
Чтобы этого избежать можно передать параметр smartQuotesTo=None в конструктор супа: тогда кавычки будут конвертироваться в Unicode также, как и другие символы данной кодировки. Для изменения поведения BeautifulSoup и BeautifulStoneSoup можно передать в параметре smartQuotesTo значения "xml" или "html".
1 from BeautifulSoup import BeautifulSoup, BeautifulStoneSoup
2 text = "Deploy the \x91SMART QUOTES\x92!"
3
4 str(BeautifulSoup(text))
5 # 'Deploy the ‘SMART QUOTES’!'
6
7 str(BeautifulStoneSoup(text))
8 # 'Deploy the ‘SMART QUOTES’!'
9
10 str(BeautifulSoup(text, smartQuotesTo="xml"))
11 # 'Deploy the ‘SMART QUOTES’!'
12
13 BeautifulSoup(text, smartQuotesTo=None).contents[0]
14 # u'Deploy the \u2018SMART QUOTES\u2019!'
Печать документа
Документ Beautiful Soup (или любое их подмножество) можно превратить в строку с помощью функции str или методов prettify или renderContents. Также можно использовать функцию unicode для получения всего документа в виде строки Unicode.
Метод prettify добавляет значимые переводы строки и пробелы для придания структуре документа лучшей читабельности. Метод также удаляет текстовые узлы, состоящие только из пробелов, что может изменить смысл XML документа. Функции str и unicode не удаляют текстовые узлы, состоящие только из пробелов, и не добавляют пробелы между узлами.
Приведем пример.
1 from BeautifulSoup import BeautifulSoup
2 doc = "<html><h1>Heading</h1><p>Text"
3 soup = BeautifulSoup(doc)
4
5 str(soup)
6 # '<html><h1>Heading</h1><p>Text</p></html>'
7 soup.renderContents()
8 # '<html><h1>Heading</h1><p>Text</p></html>'
9 soup.__str__()
10 # '<html><h1>Heading</h1><p>Text</p></html>'
11 unicode(soup)
12 # u'<html><h1>Heading</h1><p>Text</p></html>'
13
14 soup.prettify()
15 # '<html>\n <h1>\n Heading\n </h1>\n <p>\n Text\n </p>\n</html>'
16
17 print soup.prettify()
18 # <html>
19 # <h1>
20 # Heading
21 # </h1>
22 # <p>
23 # Text
24 # </p>
25 # </html>
Обратите внимание, что функции str и renderContents дают различный результат, когда используются для тегов внутри документа. Функция str печатает и теги и их содержимое, а функция renderContents - только содержимое.
При вызове функций str, prettify или renderContents, вы можете задать кодировку вывода. Кодировкой по умолчанию (используется функцией str) является UTF-8. Вот пример разбора строки ISO-8851-1 с последующим выводом на экран той же строки в других кодировках:
1 from BeautifulSoup import BeautifulSoup
2 doc = "Sacr\xe9 bleu!"
3 soup = BeautifulSoup(doc)
4 str(soup)
5 # 'Sacr\xc3\xa9 bleu!' # UTF-8
6 soup.__str__("ISO-8859-1")
7 # 'Sacr\xe9 bleu!'
8 soup.__str__("UTF-16")
9 # '\xff\xfeS\x00a\x00c\x00r\x00\xe9\x00 \x00b\x00l\x00e\x00u\x00!\x00'
10 soup.__str__("EUC-JP")
11 # 'Sacr\x8f\xab\xb1 bleu!'
Если в исходном документе в декларации указывалась кодировка, то при обратном преобразовании в строку Beautiful Soup запишет в декларацию новую кодировку. Это означает, что если загрузить документ HTML в BeautifulSoup, а потом распечатать его, то HTML не только будет вычищен, но и прозрачно перекодирован в UTF-8.
Пример HTML:
1 from BeautifulSoup import BeautifulSoup
2 doc = """<html>
3 <meta http-equiv="Content-type" content="text/html; charset=ISO-Latin-1" >
4 Sacr\xe9 bleu!
5 </html>"""
6
7 print BeautifulSoup(doc).prettify()
8 # <html>
9 # <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
10 # Sacré bleu!
11 # </html>
Пример XML:
Дерево синтаксического разбора
До сих пор мы рассматривали загрузку и запись документов. Однако, большую часть времени будет приковывать к себе дерево синтаксического разбора: структуры данных Beautiful Soup, которые создаются по мере синтаксического разбора документа.
Объект парсера (экземпляр класса BeautifulSoup или BeautifulStoneSoup) обладает большой глубиной вложенности связанных структур данных, соответствующих структуре документа XML или HTML. Объект парсера состоит из объектов двух других типов: объектов Tag, которые соответствуют тегам, к примеру, тегу <TITLE> и тегу <B>; и объекты NavigableString, соответствующие таким строкам как "Page title" или "This is paragraph".
Класс NavigableString имеет несколько подклассов (CData, Comment, Declaration и ProcessingInstruction), которые соответствуют специальным конструкциям в XML. Они работают также как NavigableStringи, за исключением того, что когда приходит время выводить их на экран, они содержат некоторые дополнительные данные. Вот документ с включенным в него комментарием:
1 from BeautifulSoup import BeautifulSoup
2 import re
3 hello = "Hello! <!--I've got to be nice to get what I want.-->"
4 commentSoup = BeautifulSoup(hello)
5 comment = commentSoup.find(text=re.compile("nice"))
6
7 comment.__class__
8 # <class 'BeautifulSoup.Comment'>
9 comment
10 # u"I've got to be nice to get what I want."
11 comment.previousSibling
12 # u'Hello! '
13
14 str(comment)
15 # "<!--I've got to be nice to get what I want.-->"
16 print commentSoup
17 # Hello! <!--I've got to be nice to get what I want.-->
Итак, давайте более внимательно посмотрим на тот документ, что приводился в начале документации:
1 from BeautifulSoup import BeautifulSoup
2 doc = ['<html><head><title>Page title</title></head>',
3 '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
4 '<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
5 '</html>']
6 soup = BeautifulSoup(''.join(doc))
7
8 print soup.prettify()
9 # <html>
10 # <head>
11 # <title>
12 # Page title
13 # </title>
14 # </head>
15 # <body>
16 # <p id="firstpara" align="center">
17 # This is paragraph
18 # <b>
19 # one
20 # </b>
21 # .
22 # </p>
23 # <p id="secondpara" align="blah">
24 # This is paragraph
25 # <b>
26 # two
27 # </b>
28 # .
29 # </p>
30 # </body>
31 # </html>
Атрибуты Tag-ов
Объекты Tag и NavigableString имеют множество полезных элементов, большая часть которых описывается в разделах Навигация по дереву синтаксического разбора и Поиск в дереве синтаксического разбора. Тем не менее, рассмотрим здесь один аспект объектов Tag: атрибуты.
Теги SGML имеют атрибуты: например, каждый тег <P> в приведенном выше примере HTML имеет атрибуты "id" и "align". К атрибутам тегов можно обращаться таким же образом, как если бы объект Tag был словарем:
NavigableString objects don't have attributes; only Tag objects have them.
Навигация по дереву синтаксического разбора
Все объекты Tag содержат элементы, перечисленные ниже (тем не менее, фактическое значение элемента может равняться None). Объекты NavigableString имеют все из них за исключением contents и string.
parent
В примере выше, родителем объекта <HEAD> Tag является объект <HTML> Tag. Родителем объекта <HTML> Tag является сам объект парсера BeautifulSoup. Родитель объекта парсера равен None. Передвигаясь по объектам parent, можно перемещаться по дереву синтаксического разбора:
contents
С помощью parent вы перемещаетесь вверх по дереву синтаксического разбора. С помощью contents вы перемещаетесь вниз по дереву синтаксического разбора. contents является упорядоченным списком объектов Tag и NavigableString, содержащихся в элементе страницы (page element). Только объект парсера самого высокого уровня и объекты Tag содержат contents. Объекты NavigableString являются простыми строками и не могут содержать подэлементов, поэтому они не содержат contents.
В примере выше, элемент contents первого объекта <P> Tag является списком, содержащим объект NavigableString ("This is paragraph "), объекта <B> Tag и еще одного объекта NavigableString ("."). Элемент contents объекта <B> Tag: список, состоящий из одного объекта NavigableString ("one").
string
Для вашего удобства сделано так, что в случае, когда тег имеет только один дочерний узел, который является строкой, дочерний узел будет доступен через tag.string точно также как и через tag.contents[0]. В примере выше soup.b.string является объектом NavigableString отображающий Unicode-строку "one". Эта строка содержится в первом объекте <B> Tag дерева синтаксического разбора.
Но soup.p.string равен None, поскольку первый объект <P> Tag в дереве синтаксического разбора имеет более одного дочернего элемента. soup.head.string также равен None, хотя объект <HEAD> Tag имеет только один дочерний элемент, поскольку этот дочерний элемент – Tag (объект <TITLE> Tag), а не объект NavigableString.
nextSibling и previousSibling
Эти элементы позволяют пропускать следующий или предыдущий элемент на этом же уровне дерева синтаксического разбора. В представленном выше документе, элемент nextSibling объекта <HEAD> Tag равен объекту <BODY> Tag, поскольку объект <BODY> Tag является следующим вложенным элементом по отношению к объекту <html> Tag. Элемент nextSibling объекта <BODY> Tag равен None, поскольку в нем больше нет вложенных по отношению к объекту <HTML> Tag элементов.
И наоборот, элемент previousSibling объекта <BODY> Tag равен объекту <HEAD> tag, а элемент previousSibling объекта <HEAD> Tag равен None:
Несколько примеров: элемент nextSibling первого объекта <P> Tag равен второму объекту <P> Tag. Элемент previousSibling объекта <B> Tag внутри второго объекта <P> Tag равен объекту NavigableString "This is paragraph". Элемент previousSibling данного объекта NavigableString равен None, внутри первого объекта <P> Tag.
next и previous
Данные элементы позволяют передвигаться по элементам документа в том порядке, в котором они были обработаны парсером, а не в порядке появления в дереве. Например, элемент next для объекта <HEAD> Tag равен объекту <TITLE> Tag, а не объекту <BODY> Tag. Это потому, что в исходном документе, тег <TITLE> идет сразу после тега <HEAD>.
Поскольку элементы next и previous связаны, содержимое элемента contents объекта Tag обновляется до того как элемент nextSibling. Как правило, эти элементы не используют, но иногда это наиболее быстрый способ получить что-либо скрытое в глубине дерева синтаксического разбора.
Итерации над объектом Tag
Над элементом contents объекта Tag можно производить итерации, рассматривая его качестве списка. Это полезное упрощение. Подобным образом можно узнать, сколько дочерних узлов имеет объект Tag, вызвав функцию len(tag) вместо len(tag.contents). В терминах документа выше:
Используем имена тегов как элементы
Гораздо легче перемещаться по дереву синтаксического разбора, если в качестве имен тегов выступали элементы парсера или объекта Tag. Будем так делать на протяжении следующих примеров. В терминах документа выше, soup.head возвращает нам первый (как и следовало ожидать, единственный) объекта <HEAD> Tag документа:
Вообще, вызов mytag.foo возвратит первого потомка mytag, чем, как ни странно, будет объект <FOO> Tag. Если каких-либо объектов <FOO> Tag внутри mytag нет, то mytag.foo возвратит None. Вы можете использовать это для очень быстрого обхода дерева синтаксического разбора:
Также это можно использовать для быстрого перехода к определенной части дерева синтаксического разбора. Например, если вас не беспокоит отсутствие тегов <TITLE> на предусмотренных для них месте, вы можете просто использовать soup.title для получения названия документа HTML. Вам не нужно использовать soup.head.title:
soup.p перейдет к первому тегу <P> внутри документа, где бы тот ни был. soup.table.tr.td перейдет к первому столбцу первой строки первой же таблицы документа.
Фактически эти элементы – алиасы для метода first, описываемого ниже. Я упоминаю их здесь потому, что алиасы делают очень легким увеличение масштаба интересующей вас части хорошо знакомого дерева синтаксического разбора.
Альтернативная форма этого стиля позволяет обращаться к первому тегу <FOO> как .fooTag вместо .foo. Например, soup.table.tr.td можно также отобразить как soup.tableTag.trTag.tdTag или даже soup.tableTag.tr.tdTag. Это полезно если вы предпочитаете быть более осведомленным о том, что делаете или если вы разбираете XML, в котором имена тегов конфликтуют с именами методов и элементов Beautiful Soup.
1 from BeautifulSoup import BeautifulStoneSoup
2 xml = '<person name="Bob"><parent rel="mother" name="Alice">'
3 xmlSoup = BeautifulStoneSoup(xml)
4
5 xmlSoup.person.parent # A Beautiful Soup member
6 # <person name="Bob"><parent rel="mother" name="Alice"></parent></person>
7 xmlSoup.person.parentTag # A tag name
8 # <parent rel="mother" name="Alice"></parent>
Если вы присмотритесь к именам тегов, то увидите что они не являются корректными идентификаторами Python (как hyphenated-name), вам нужно использовать first.