браузеры
Этот режим основан на стандартном режиме с некоторыми исключениями: отображение изображений внутри ячеек таблицы и рисунков друг под другом происходит как в режиме совместимости. Для переключения в почти стандартный режим применяется один из следующих доктайпов.
Для фреймов в HTML:
Для фреймов в XHTML:
Стандартный режим
Продолжая тему режимов браузера, рассмотрим стандартный режим — режим поддержки стандартов (X)HTML и CSS. Для переключения браузера в этот режим используется один из следующих доктайпов.
Режим совместимости
Этот режим предназначен для отображения веб-страницы подобно старым браузерам. В режиме совместимости игнорируются стандарты HTML и CSS, и поведение браузеров становится непредсказуемым. Для переключения в режим совместимости существует множество доктайпов, вот лишь некоторые из них.
Также переход происходит, если доктайп вообще не указан или не может быть распознан.
Режимы браузеров
Во время противостояния браузеров Internet Explorer и Netscape каждый из разработчиков старался улучшить своё детище, чтобы усилить позиции программы на рынке и привлечь больше пользователей. Netscape 4 и IE4 ужасно поддерживали веб-стандарты, поэтому следующая версия, IE5 должна была не только исправить ошибки IE4, но и показать улучшенную поддержку спецификации CSS. Это было необходимо еще и по политическим мотивам, поскольку компания Майкрософт вошла в группу W3C и начала оказывать сильное влияние на разработку HTML и CSS.
В процессе работы над браузером IE5 его разработчики столкнулись с неожиданной трудностью. Разница при отображении страницы в разных версиях браузера была настолько велика, что множество сайтов оказались бы неработоспособными при просмотре в IE5. Идея сделать кнопку для переключения в режим совместимости пришла только в версии 8.0, поэтому разработчики IE5 пошли другим путём. Все старые страницы отображались по старым правилам, а для включения режима поддержки стандартов в код страницы необходимо добавить элемент (доктайп).
Режимы Internet Explorer
Вокруг Internet Explorer сложилась ситуация, которая больше не прослеживается ни с одним другим браузером — разброс версий начинается с 6.0 и заканчивается 9.0. Причины использования устаревших версий могут быть совершенно разные.
- Internet Explorer встроен в операционную систему Windows и неопытные пользователи даже не подозревают о наличии альтернативы.
- Корпоративные пользователи с ограниченным доступом не могут самостоятельно обновить версию или сменить браузер.
- Обновление IE до новой версии происходит через систему Windows Update, которая часто отключается по требованиям безопасности или для снижения интернет-трафика.
- Некоторые приложения, например, банковские, могут быть «заточены» для работы только под конкретную версию IE.
- В силу инерции мышления, когда пользователю нравится та программа, с которой он привык работать.
Браузер
В данный момент с помощью браузера вы можете читать эту веб-страницу, написанную на HTML.
HTML-документы
Веб-страницы — это HTML-документы, подобные другим файлам на вашем компьютере. Это просто текстовые файлы с расширением .html.
На вашем компьютере у вас, вероятно, есть различные типы файлов:
- .jpg для изображений;
- .mp3 для музыки;
- .avi для видео;
- .doc для документов Word;
- .xls для электронных таблиц Excel.
Каждый из этих типов файлов может быть открыт с помощью специальной программы. Некоторые из этих программ могут только открывать эти файлы, в то время как другие могут как открывать, так и создавать файлы.
По умолчанию расширения файлов скрыты. В этом уроке мы должны иметь возможность редактировать эти расширения. Поэтому перейдите к этим инструкциям для отображения расширений файлов:
- Windows 10: Показать или скрыть расширения файлов
- Mac: Показать и скрыть расширения файлов
Программой, используемой для открытия HTML-документов, является браузер, вроде Firefox или Chrome. Программой, используемой для создания HTML-документов, является текстовый редактор, вроде Notepad++ или Sublime Text.
Исходный код HTML
Код HTML выглядит следующим образом:
Привет, мир!
Этот код записывается с помощью текстового редактора. Вы можете увидеть теги <р>ир> , которые задают абзац.
При открытии в браузере эти теги не отображаются, а интерпретируются браузером:
Браузер видит теги и
и понимает, что «Привет, мир!» это текстовый абзац.
Помните, что HTML-документ может быть открыт двумя способами:
- с помощью текстового редактора, который видит исходный код;
- с помощью браузера, который интерпретирует исходный код.
Список браузеров
По мере того как веб стал основным использованием Интернета, каждый компьютер и смартфон поставляется с установленным браузером.
Наиболее популярными из них являются:
Chrome
Firefox
Internet Explorer
Opera
Safari
Список текстовых редакторов
Наиболее популярные из них:
Notepad++
Sublime Text
Вам нужно будет установить один для написания HTML и CSS. Хотя остальная часть этого учебника не требует написания кода, рекомендуем опробовать примеры самостоятельно.
10.1. Браузер и HTML-документ
При программировании сценариев на JavaScript вы всегда будете использовать объекты в качестве основных инструментальных средств. Объекты – это элементы рабочей области браузера и HTML-документа. Окно браузера, веб-страница, ее фоновый цвет, изображения, текст и все, что находится на странице, – это объекты. В языке JavaScript принято различать объекты браузера и объекты HTML-документа. Рассмотрим их подробнее.
Объекты браузера создаются автоматически при загрузке веб-страницы. К ним относятся Window, Navigator, Screen, History, Location. Приведу их краткую характеристику.
• Window – дает доступ к окну браузера.
• History – дает доступ к истории посещенных ссылок.
• Navigator – дает доступ к характеристикам браузера.
• Location – содержит текущий URL страницы.
• Screen – дает доступ к характеристикам экрана монитора.
С точки зрения языка JavaScript окно вашего браузера – это объект Window. Данный объект, в свою очередь, также содержит некоторые объекты – элементы оформления, например строку состояния и полосу прокрутки.
Внутри окна браузера размещается веб-страница – HTML-документ. Он является ни чем иным, как объектом Document. В свою очередь, объект Document содержит другие объекты – объекты HTML. Это ссылки, изображения, формы, цвет фона, то есть все, что находится на веб-странице.
Итак, в языке JavaScript есть объекты. Их можно сравнить с существительными или предметами. У каждого объекта есть свои свойства, или характеристики. Свойства описывают объекты, как прилагательные описывают существительные. В синтаксисе языка JavaScript свойство любого объекта описывается так: oбъект. свойство. Например, установить желтый цвет фона веб-страницы с помощью языка JavaScript можно следующим образом: document.bgColor = «yellow». В этой конструкции document – объект (веб-страница, HTML-документ), bgColor – свойство объекта Document (фоновый цвет веб-страницы), yellow – значение свойства bgColor.
Кроме того, над каждым объектом можно совершать определенные действия. Эти действия принято называть методами. Согласно синтаксису языка JavaScript после метода всегда ставятся скобки по схеме oбъект. мeтoд(). Например, чтобы с помощью языка JavaScript отобразить на веб-странице текст «Hello world!», нужно использовать следующую конструкцию: document. write («Hello world! «). Здесь document – объект, write() – метод, присущий объекту Document, а выражение в скобках – текст, который должен отобразиться на странице в результате использования метода write(). На языке программирования это называется «метод возвращает результат».
В листинге 10.1 представлен пример сценария, демонстрирующий рассмотренные ранее правила записи свойства и метода объекта Document.
Листинг 10.1. Свойство и метод объекта Document
document.write(«Hello world!»); //пишем текст на странице
document.bgColor=»yellow» //устанавливаем желтый фон страницы
Обратите внимание, что строки внутри сценария отделяются друг от друга точкой с запятой. Данный сценарий меняет фоновый цвет веб-страницы и выводит текст Hello world! (рис. 10.1).
Рис. 10.1. Вывод текста и изменение фонового цвета страницы
Таким образом, каждый элемент браузера и HTML-документа для сценария на JavaScript предстает в виде объекта. И каждый такой объект может иметь определенные свойства и методы. Можно сказать, что свойства объектов – это данные, связанные с объектом, методы – функции для обработки данных объекта. При этом у разных объектов разные свойства и методы. Более того, по отношению друг к другу объекты не равноценны. В JavaScript существует строгая иерархия объектов.
Данный текст является ознакомительным фрагментом.
Продолжение на ЛитРес
Читайте также
12.2. Внедрение сценариев в HTML-документ
12.2. Внедрение сценариев в HTML-документ В данном разделе будут рассмотрены вопросы, касающиеся встраивания сценариев в веб-страницы, поэтому предполагается, что вы хотя бы немного знакомы с языком разметки HTML. Обычно сценарии внедряются в HTML-документ тремя стандартными
Совет 36: Чистый браузер
Совет 36: Чистый браузер Стоило зайти на сайт банка, как баннеры на всех сайтах запестрели предложениями выгодных вкладов и доступных кредитов? Никакой магии, причина в cookie — небольших текстовых файлах, необходимых сайтам, чтобы запоминать пользователей и их
Глава 2 Браузер – окно в Интернет
Глава 2 Браузер – окно в Интернет • Internet Explorer• Mozilla Firefox• Opera• Google Chrome• Офлайн-браузерыПодключиться к Интернету – только полдела. Для путешествий по Глобальной сети понадобится браузер – программа, позволяющая просматривать веб-страницы, переходить по ним, сохранять и
Запускаем браузер
Запускаем браузер Запустить Internet Explorer можно разными способами.– Во-первых, его значок находится на панели быстрого запуска (рис. 5.1), если она у вас активизирована, конечно. Рис. 5.1. Значок Internet Explorer на панели быстрого запуска– Во-вторых, существует меню Пуск, в котором
Урок 9 Изменяем браузер внешне и внутренне
Урок 9 Изменяем браузер внешне и внутренне В браузере все должно быть прекрасно: и панельки, и кнопочки, и мои фотки. Женский взгляд Оксаны Лермонтовой Когда мы открываем браузер, он предстает перед нами (в своем великолепии, естественно) в неком сформированном виде и с
Работаем через обычный браузер
Работаем через обычный браузер Как уже отмечалось ранее, к серверу VNC можно подключиться из обычного браузера, который поддерживает работу с Java-апплетами. На сегодняшний день язык программирования Java широко распространен по всему миру, поэтому можно смело утверждать,
Работа через браузер: вывод
Работа через браузер: вывод Поработав и с программой-клиентом, и с Java-апплетом, я могу сделать следующий вывод: с программой работать намного удобней, потому что она поддерживает большее число настроек. С апплетом в этом смысле дела обстоят сложнее, к тому же его окно
Браузер – окно в Интернет
Браузер – окно в Интернет Подключиться к Интернету – только полдела. Для путешествий по Глобальной сети понадобится браузер – программа, позволяющая совершать просмотр веб-страниц, переход по ним, их сохранение и пр. Например, в состав операционных систем семейства Windows
10.1. Браузер Firefox
10.1. Браузер Firefox Как пользоваться браузером Firefox, надеюсь, знают почти все. Поэтому в этой главе мы поговорим о его усовершенствовании. По умолчанию установленный браузер Firefox не поддерживает Java-аплеты и Flash-ролики. Что касается Java, то его бум прошел — уже редко встречаются
21.5.2. Команда lynx — текстовый браузер
21.5.2. Команда lynx — текстовый браузер Если графический режим недоступен (например, на сервере), а по сети побродить хочется, командой lynx можно вызвать текстовый браузер lynx. В Ubuntu программа lynx не устанавливается по умолчанию. Для ее установки нужно ввести команду: sudo apt-get install
Браузер Интернета
Браузер Интернета Знакомство с обозревателем Internet ExplorerКак вы уже знаете, для обмена данными в Интернете используется технология клиент-сервер, а доступ к определенным ресурсам осуществляется с помощью соответствующей клиентской программы. Для просмотра Web-страниц
Браузер для орков
Браузер для орков Браузер, как почти любой программный продукт, может стать популярным только в том случае, если будет понятен каждой домохозяйке. Поклонники Warcraft добавят: «Браузер должен быть таким, чтобы даже орки могли пользоваться Интернетом».Мощь орковГруппа
9.1. Браузер Internet Explorer
9.1. Браузер Internet Explorer Прежде чем открыть браузер и посетить какой-нибудь сайт, давайте поговорим о WWW (World Wide Web — Всемирная паутина) и Интернете вообще. Нужно понимать, с чем мы будем иметь
4.4. Браузер Safari
4.4. Браузер Safari Браузер Safari является встроенным обозревателем в операционную систему Mac OS X, являющимся одним из самых быстрых на сегодняшний день браузеров. Если вы раньше не работали с Safari, то сразу почувствуете разницу в скорости загрузки web-страниц. Также к
Разрабатываем свой браузер с нуля. Часть первая: HTML

Продолжаем цикл статей по разработке браузерного движка.
В данной статье я расскажу как создать самый быстрый HTML-парсер c DOM. Мы рассмотрим HTML спецификацию и чем она плоха относительно производительности и потребления ресурсов при разборе HTML.
С данной темой я докладывался на прошедшем HighLoad++. Конференцию не каждый может посетить, плюс в статье больше деталей.
Я предполагаю, что читатель обладает базовыми знаниями об HTML: теги, ноды, элементы, пространство имён.
Спецификация HTML
Прежде чем начать хоть как-то затрагивать реализацию HTML-парсера необходимо понять какой HTML спецификации верить.
Существует две HTML спецификации:
Естественно, выбор пал на лидеров отрасли — WHATWG . Живой стандарт, большие компании у каждой из которых есть свой браузер/браузерный движок.
UPDATE: К сожалению, приведенные ссылки на спецификации не открываются из России. Видимо, «эхо войны» с телеграмм.
Процесс парсинга HTML
Процесс построения HTML дерева можно разделить на четыре части:
- Декодер
- Предварительная обработка
- Токенизатор
- Построение дерева
Рассмотрим каждую стадию по отдельности.
Декодер
Токенизатор принимает на вход юникод символы (code points). Соответственно, нам необходимо конвертировать текущий байтовый поток в юникод символы. Для этого необходимо воспользоваться спецификацией Encoding.
Если мы имеем HTML с неизвестной кодировкой (нет HTTP заголовка) то нам необходимо её определить до начала декодирования. Для этого мы воспользуемся алгоритмом encoding sniffing algorithm.
Если очень кратко то суть алгоритма сводится к тому, что мы ждем 500мс или первые 1024 байта из байтового потока и запускаем алгоритм prescan a byte stream to determine its encoding который пробует найти тег с атрибутами http-equiv , content или charset и пытается понять какую кодировку указал разработчик HTML.
В спецификации Encoding оговаривается минимальный набор поддерживаемых кодировок браузерным движком (всего 21): UTF-8, ISO-8859-2, ISO-8859-7, ISO-8859-8, windows-874, windows-1250, windows-1251, windows-1252, windows-1254, windows-1255, windows-1256, windows-1257, windows-1258, gb18030, Big5, ISO-2022-JP, Shift_JIS, EUC-KR, UTF-16BE, UTF-16LE и x-user-defined.
Предварительная обработка
После того как мы декодировали байты в юникод символы нам необходимо провести «зачистку». А именно, заменить все символы возврата каретки ( \r ) за которыми следует символ перевода строки ( \n ) на символ возврата каретки ( \r ). Затем, заменить все символы возврата каретки на символ перевода строки ( \n ).
Так описано в спецификации. То есть, \r\n => \r , \r => \n .
Но, на самом деле так никто не делает. Делают проще:
Если попался символ возврата каретки ( \r ) то смотрим есть ли символ перевода строки ( \n ). Если есть то меняем оба символа на символ перевода строки ( \n ), если нет то меняем только первый символ ( \r ) на перевод строки ( \n ).
На этом предварительная обработка данных завершена. Да, всего-то надо избавиться от символов возврата каретки, чтобы они не попадали в токенизатор. Токенизатор не ожидает и не знает, что делать с символом возврата каретки.
Ошибки парсинга
Чтобы в дальнейшем не возникало вопросов стоит сразу рассказать, что такое ошибка парсинга ( parse error ).
На самом деле ничего страшного. Звучит грозно, но по факту это лишь предупреждение о том, что мы ожидали одно, а имеем другое.
Ошибка парсинга не остановит процесс обработки данных или построение дерева. Это сообщение которое сигнализирует, что у нас не валидный HTML.
Ошибку парсига можно получить за суррогатные пары, \0 , неверное расположение тега, неверный и ещё всякие.
К слову, некоторые ошибки парсинга ведут к последствиям. К примеру, если указать «плохой» то HTML дерево будет помечен как QUIRKS и изменится логика работы некоторых DOM функций.
Токенизатор
Как уже было сказано ранее, токенизатор принимает на вход юникод символы. Это конечный автомат (state machine) который имеет 80 состояний. В каждом состоянии условия для юникод символов. В зависимости от пришедшего символа токенизатор может:
- Поменять своё состояние
- Сформировать токен и поменять состояние
- Ничего не делать, ждать следующий символ
Токенизатор создает токены шести видов: DOCTYPE, Start Tag, End Tag, Comment, Character, End-Of-File. Которые поступают в стадию построения дерева.
Примечательно, что токенизатор знает не о всех своих состояниях, а где о 40% (взял с потолка, для примера). «Зачем же остальные?» — спросите вы. Об остальных 60% знает стадия построения дерева.
Это сделано для того, чтобы правильно парсить такие теги как , , , и так далее. То есть, обычно те теги в которых мы не ожидаем других тегов, а только закрытие себя.
К примеру, тег не может содержать в себе других тегов. Любые теги в будут восприниматься как текст пока он не встретит закрывающий тег для себя .
Зачем так сделано? Ведь можно было просто указать токенизатору, что если встретим тег то идем по «нужному нам пути». И это было бы верно если не namespaces! Да, пространство имён влияет на поведение стадии построения дерева, которая в свою очередь меняет поведение токенизатора.
Для примера, рассмотрим поведение тега в HTML и SVG пространстве имен:
HTML
Текст
Результат построения дерева:
"Текст"
SVG
Результат построения дерева:
"Текст"
Мы видим, что в первом случаи (HTML namespace) тег является текстом, не был создан элемент span . Во втором случае (SVG namespace) на основе тега был создан элемент. То есть, в зависимости от пространства имени теги ведут себя по-разному.
Но, это ещё не всё. Тортиком на этом «празднике жизни» служит тот факт, что сам токенизатор должен знать в каком пространстве имен находится стадия построения дерева. И нужно это исключительно для того, чтобы правильно обработать CDATA .
Рассмотрим два примера с CDATA , два пространства имён:
HTML
Результат построения дерева:
SVG
Результат построения дерева:
" Текст "
В первом случаи (HTML namespace) токенизатор воспринял CDATA за комментарий. Во втором случае токенизатор разобрал структуру CDATA и получил данные из неё. В общем, правило такое: если встречаем CDATA не в HTML пространстве имён то парсим его, иначе считаем за комментарий.
Вот такая получается жёсткая связь между токенизатором и построением дерева. Токенизатор должен знать в каком пространстве имен сейчас находится стадия построения дерева, а стадия построения дерева может менять состояния токенизатора.
Токены
Ниже будут рассмотрены все шесть видов токенов создаваемых токенизатором. Тут стоит отметить, что все токены имеют подготовленные данные, то есть уже обработанные и «готовые к употреблению». Это значит, что все именнованные символьные ссылки (named character references), вроде © , будут преобразованы в юникод символы.
DOCTYPE Token
Токен DOCTYPE имеет свою структуру не похожую на остальные теги. Токен содержит в себе:
- Имя
- Public-идентификатор
- System-идентификатор
В современном HTML единственный правильный/валидный DOCTYPE должен иметь следующий вид:
Все остальные будут считаться ошибкой парсинга.
Start Tag Token
Открывающий тег может содержать в себе:
Открывающий тег может содержать флаг self-closing . Данный флаг никак не влияет на закрытие тега, но может вызвать ошибку парсинга для не void элементов.
End Tag Token
Закрывающий тег. Обладает всеми свойствами токена открывающего тега, но имеет перед именем тега косую / .
Закрывающий тег может содержать флаг self-closing который вызовет ошибку парсинга. Так же, ошибку парсинга вызовут атрибуты у закрывающего тега. Они будут правильно распарсены, но выброшены на стадии построения дерева.
Comment Token
Токен комментария содержит в себе весь текст комментария. То есть, он полностью копируется из потока в токен.
Character Token
Пожалуй самый интересный токен. Токен юникод символа. Может содержать в себе один (только один) символ.
На каждый символ в HTML будет создаваться токен и отправляться в стадию построения дерева. Это очень накладно.
Давайте рассмотрим как это работает.
Возьмём HTML данные:
Слава императору! ®
Как вы думаете сколько будет создано токенов для приведенного примера? Ответ: 22.
Рассмотрим список создаваемых токенов:
Start tag token: Character token: С Character token: л Character token: а Character token: в Character token: а Character token: Character token: и Character token: м Character token: п Character token: е Character token: р Character token: а Character token: т Character token: о Character token: р Character token: у Character token: ! Character token: Character token: End tag token: End-of-file token
Не утешительно, правда? Но, конечно, многие создатели парсеров HTML по факту имеет только один токен в процессе обработки. Пуская его по кругу и затирая каждый раз новыми данными.
Забежим вперед и ответим на вопрос: зачем так сделано? Почему не брать этот текст единым куском? Ответ кроется в стадии построения дерева.
Токенизатор бесполезен без стадии построения HTML дерева. Именно на стадии построения дерева происходит склейка текста с разными условиями.
Условия, примерно, такие:
- Если пришел символьный токен с U+0000 ( NULL ) то вызываем ошибку парсинга и игнорируем токен.
- Если пришёл один из U+0009 ( CHARACTER TABULATION ), U+000A ( LINE FEED (LF) ), U+000C ( FORM FEED (FF) ) или U+0020 ( SPACE ) character токен тогда вызвать алгоритм восстановить активные элементы форматирования и вставить токен в дерево.
Символьный токен добавляется в дерево по алгоритму:
- Если текущая позиция вставки это не текстовая нода, тогда создать текстовую ноду, вставить её в дерево и добавить к ней данные из токена.
- Иначе добавить данные из токена к существующей текстовой ноде.
Это поведение создает много проблем. Необходимость на каждый символ создавать токен и отправлять на анализ в стадию построения дерева. Мы не знаем размер текстовой ноды и нам приходится либо заранее выделить много памяти, либо делать реалоки. Всё это крайне накладно по памяти, либо по времени.
End-Of-File Token
Простой и понятный токен. Данные закончились — сообщим об этом стадии построения дерева.
Построение дерева
Построение дерева — это конечный автомат имеющий 23 состояния с множеством условий для токенов (тегов, текста). Стадия построения дерева самая большая, занимает значимую часть спецификации, а так же способна вызывать летаргический сон и раздражение.
Всё устроено очень просто. На вход принимаются токены и в зависимости от токена переключается состояние построения дерева. На выходе мы имеем настоящий DOM.
Проблемы?
Давольно очевидными выглядят следующие проблемы:
Посимвольное копирование
На вход каждое состояние токенизатора принимает по одному символу которые он копирует/конвертирует в случаи необходимости: имена тегов, атрибуты, комментарии, символы.
Это очень расточительно как по памяти так и по времени. Мы вынуждены заранее выделить неизвестное количество памяти под каждый атрибут, имя тега, комментарий и так далее. А это, соответственно, ведет к реалокам, а реалоки ведут к потерянному времени.
А если представить, что HTML содержит 1000 тегов, а в каждом теге хотя бы по одному атрибуту, то мы получим адски медленную работу парсера.
Символьный токен
Второй проблемой является символьный токен. Получается, что мы создаем токен на каждый символ и отдаём его в построение дерева. Построение дерева не знает сколько у нас будет этих токенов и не может сразу выделить память под нужное количество символов. Соответственно, тут все те же реалоки + постоянные проверки на присутствие текстовой ноды в текущем состоянии дерева.
Монолитность системы
Большой проблемой является зависимость всего от всего. То есть, токенизатор зависит от состояния построения дерева, а построение дерева может управлять токенизатором. И всему виной пространство имен (namespaces).
Как будем решать проблемы?
Далее я опишу реализацию HTML парсера в своём проекте Lexbor, а так же решения всех озвученных проблем.
Предварительная обработка
Убираем предварительную обработку данных. Обучим токенизатор понимать возврат каретки ( \r ) как пробельный символ. Таким образом он будет прокинут в стадию построения дерева где мы с ним и разберёмся.
Токены
Легким движением руки мы унифицируем все токены. У нас будет один токен на всё. Вообще, во всём процессе парсинга будет только один токен.
Наш унифицированный токен будет содержать следующие поля:
Tag ID
Мы не будем работать с текстовым представлением имени тега. Переводим всё в цифры. Цифры легко сравнивать, с ними легче работать.
Создаём статическую хеш-таблицу из всех известных тегов. Создаём перечисление (enum) из всех известных тегов. То есть, нам нужно жестко назначить каждому тегу идентификатор. Соответственно, в хеш-таблице ключом будет имя тега, а значением записать из перечисления.
typedef enum < LXB_TAG__UNDEF = 0x0000, LXB_TAG__END_OF_FILE = 0x0001, LXB_TAG__TEXT = 0x0002, LXB_TAG__DOCUMENT = 0x0003, LXB_TAG__EM_COMMENT = 0x0004, LXB_TAG__EM_DOCTYPE = 0x0005, LXB_TAG_A = 0x0006, LXB_TAG_ABBR = 0x0007, LXB_TAG_ACRONYM = 0x0008, LXB_TAG_ADDRESS = 0x0009, LXB_TAG_ALTGLYPH = 0x000a, /* . */ >
Из примера видно, что мы создали теги для END-OF-FILE токена, для текста, документа. Всё это ради дальнейшего удобства. Приоткрывая занавес скажу, что в ноде ( DOM Node Interface ) у нас будет присутствовать Tag ID . Сделано это для того, чтобы не делать два сравнения: на тип ноды и на элемент. То есть, если нам нужен DIV элемент то мы делаем одну проверку в ноде:
if (node->tag_id == LXB_TAG_DIV) < /* Best code */ >
Но, можно конечно сделать и так:
if (node->type == LXB_DOM_NODE_TYPE_ELEMENT && node->tag_id == LXB_TAG_DIV) < /* Oh, code */ >
Два нижних подчёркивания в LXB_TAG__ нужны для того, чтобы отделить общие теги от системных. Иначе говоря, пользователь может создать тег с именем text или end-of-file и если мы потом будем искать по имени тега то никаких ошибок не возникнет. Все системные теги начинаются с символа # .
Но всё же, нода может хранить текстовое представление имени тега. Для 98.99999% нод этот параметр будет равен NULL . В некоторых пространствах имён нам необходимо прописать префикс или имя тега с фиксированным регистром. К примеру, baseProfile в SVG namespace.
Логика работы простая. Если мы имеем тег с чётко оговоренным регистром то:
- Добавляем его в общую базу тегов в нижнем регистре. Получаем идентификатор тега.
- К ноде добавляем идентификатор тега и оригинальное имя тега в текстовом представлении.
Пользовательские теги (custom elements)
Разработчик может создавать любые теги в HTML. Так-как мы имеем в статической хеш-таблице только те теги о которых знаем, а пользователь может создать любые то нам нужна динамическая хеш-таблица.
Выглядит всё очень просто. Когда к нам прийдет тег мы посмотрим есть ли он в статической хеш-таблицы. Если тега нет то посмотрим в динамической, если и там нет то увеличиваем счётчик идентификаторов на один и добавляем тег в динамическую таблицу.
Всё описанное происходит на стадии токенизатора. Внутри токенизатора и после все сравнения идут по Tag ID (за редким исключением).
Begin and End
Теперь в токенизаторе у нас не будет обработки данных. Мы ничего не будет копировать и конвертировать. Мы просто берём указатели на начало и конец данных.
Вся обработка данных, таких как символьные ссылки, будет происходить на стадии построения дерева.
Таким образом мы будем знать размер данных для последующего выделения памяти.
Attributes
Тут всё так же просто. Мы ничего не копируем, а просто сохраняем указатели на начало/конец имени и значения атрибутов. Все преобразования происходят в момент построения дерева.
Flags
Так как мы унифицировали токены нам надо как-то сообщить построению дерева о типе токена. Для этого используется битмап поле Flags.
Поле может содержать в себе следующие значения:
enum < LXB_HTML_TOKEN_TYPE_OPEN = 0x0000, LXB_HTML_TOKEN_TYPE_CLOSE = 0x0001, LXB_HTML_TOKEN_TYPE_CLOSE_SELF = 0x0002, LXB_HTML_TOKEN_TYPE_TEXT = 0x0004, LXB_HTML_TOKEN_TYPE_DATA = 0x0008, LXB_HTML_TOKEN_TYPE_RCDATA = 0x0010, LXB_HTML_TOKEN_TYPE_CDATA = 0x0020, LXB_HTML_TOKEN_TYPE_NULL = 0x0040, LXB_HTML_TOKEN_TYPE_FORCE_QUIRKS = 0x0080, LXB_HTML_TOKEN_TYPE_DONE = 0x0100 >;
Помимо типа токена, открывающий или закрывающий, есть значения для конвертера данных. Только токенизатор знает как правильно конвертировать данные. Соответственно, токенизатор помечает в токене как данные должны быть обработаны.
Character Token
Из ранее описанного можно сделать вывод, что символьный токен у нас исчез. Да, теперь у нас есть новый тип токена: LXB_HTML_TOKEN_TYPE_TEXT . Теперь мы создаём токен на весь текст между тегами помечая, как его в дальнейшем надо обработать.
Из-за этого нам прийдется изменить условия в построении дерева. Нам нужно обучить его работать не с символьными токенами, а с текстовыми: конвертировать, удалять ненужные символы, пропускать пробелы и так далее.
Но, здесь ничего сложного нет. В стадии построения дерева изменения будут минимальны. А вот токенизатор теперь у нас не соответствует спецификации от слова совсем. Но, он нам и не нужен, это нормально. Наша задача получить HTML/DOM дерево полностью соответствующее спецификации.
Стадии токенизатора
Чтобы обеспечить высокую скорость обработки данных в токенизаторе мы в каждую стадию добавим свой итератор. По спецификации у нас каждая стадия принимает по одному символу и в зависимости от пришедшего символа принимает решения. Но, правда в том, что это очень затратно.
К примеру, чтобы перейти от стадии ATTRIBUTE_NAME к стадии ATTRIBUTE_VALUE нам нужно найти пробельный символ в имени атрибута, что будет свидетельствовать об его окончании. По спецификации мы должны скармливать по символу в стадию ATTRIBUTE_NAME пока не встретиться пробельный символ, и эта стадия не переключится на другую. Это очень затратно, обычно это реализуют через вызов функции на каждый символ или колбека вроде «tkz->next_code_point()».
Мы же добавляем в стадию ATTRIBUTE_NAME цикл и передадим входящий буфер целиком. В цикле ищем нужные нам символы для переключения и продолжаем работу на следующей стадии. Тут мы получим много выигрыша, даже оптимизации компилятора.
Но! Самое страшное, что мы тем самым сломали поддержку чанков (chunks) из коробки. Благодаря посимвольной обработки в каждой стадии токенизатора у нас была поддержка чанков, а теперь мы это сломали.
Как исправить? Как реализовать поддержку чанков?! Всё просто, вводим понятия входящих буферов (Incoming Buffer).
Incoming Buffer
Часто HTML парсят кусками (chunks). Например, если данные мы получаем по сети. Чтобы не простаивать в ожидании оставшихся данных мы можем отправить на обработку/парсинг уже полученные данные. Естественно, данные могут быть «порваны» в любом месте. К примеру, имеем два буфера:
Первый
Второй
s="oh-no-oh-no">
Так как мы ничего не копируем на стадии токенизации, а только берем указатели на начало и конец данных то у нас появляется проблема. Указатели на разные пользовательские буфера. А учитывая тот факт, что разработчики часто используют один и тот же буфер для данных то мы имеем дело с указателем на начало несуществующих данных.
Чтобы решить озвученные проблемы нам необходимо понимать когда данные из пользовательского буфера больше не нужны.
Решение очень простое:
- Если мы парсим кусками то каждый входящий кусок мы копируем во входящий буфер (Incoming Buffer).
- После парсинга текущего куска (ранее скопированного) мы смотрим, а не остался ли какой нибудь незавершенный токен? То есть, присутствуют ли указатели на текущий пользовательский буфер в последнем токене. Если указатели отсутствуют то освобождаем входящий буфер, если нет то оставляем его до тех пор пока он нужен. В 99% случаев входящий буфер уничтожится при поступлении следующего куска.
Флагом «копировать входящий буфер» можно управлять. Для цельных данных копирование не происходит.
Конечно, данную логику можно оптимизировать и усложнить. Можно не копировать предварительно весь буфер, а после обработки скопировать только нужные (оставшийся хвост) данные и поменять указатель в токене на наш новый буфер. Получится всё лучше и быстрее. Но, данная оптимизация, на текущий момент, является преждевременной. Текущая реализация работает быстро и не затратно по памяти.
Проблема: данные в токене
С проблемой парсинга кусков мы разобрались, можно выдохнуть. Но, при нашем подходе хранения указателей возникает другая проблема: в определенный момент возникает необходимость посмотреть на данные в токене. Мы этого сделать не можем. Если мы будем заглядывать в данные токена то нам придётся склеивать их (если они из разных кусков), а это дико затратно по времени. В спецификации есть пару моментов когда надо посмотреть чего мы там накопили в токене.
Решается данная проблема просто: добавлением дополнительных стадий обработки в токенизатор. То есть, мы заранее знаем когда будут проверки и можем пойти по разному пути обработки данных в токенизаторе.
Стадия построения дерева
Тут изменения минимальны.
У нас больше нет символьных токенов, но зато появились текстовые. Соответственно, нам необходимо обучить стадию построения дерева понимать новый токен.
Вот как это выглядит:
По спецификации
tree_build_in_body_character(token) < if (token.code_point == '\0') < /* Parse error, ignore token */ >else if (token.code_point == whitespaces) < /* Insert element */' >/* . */ >
В Lexbor HTML
tree_build_in_body_character(token) < lexbor_str_t str = ; lxb_html_parser_char_t pc = ; pc.drop_null = true; tree->status = lxb_html_token_parse_data(token, &pc, &str, tree->document->mem->text); if (token->type & LXB_HTML_TOKEN_TYPE_NULL) < /* Parse error */ >/* Insert element if not empty */ >
Из примера видно, что мы убрали все условия на проверку символов и создали общую функцию для обработки текста. В функцию передается аргумент с параметрами о том как преобразовывать данные:
pc.replace_null /* Заменить все '\0' на заменяющий символ (REPLACEMENT CHARACTER (U+FFFD)) */ pc.drop_null /* Удалить все '\0' */ pc.is_attribute /* Если данные являются значением атрибута то парсить их надо "с умом" */ pc.state /* Стадия обработки. Можно указать с парсингом символьных ссылок или без. */
В каждой стадии построения дерева есть свои условия для символьных токенов. Где-то надо выкидывать \0 , а где-то заменять их на REPLACEMENT CHARACTER . Где-то надо конвертировать символьные ссылки, а где-то нет. И все эти параметры могу как угодно комбинироваться.
Конечно же, на словах всё звучит просто. По факту необходимо быть предельно внимательным. К примеру, все пробельные символы до начала тега должны быть выброшены. Возникает проблема, если к нам прийдет текстовый токен у которого в начале пробелы, а дальше текст: » а тут текст «. Мы должны отрезать пробелы в начале текста и посмотреть не осталось ли там чего, если данные ещё остались то под видом нового токена продолжить обработку.
Смешной тег
В HTML спецификации (в разделе построения дерева) говорится о теге sarcasm . Я видел не раз как разработчики парсеров слепо включали обработку этого тега.
An end tag whose tag name is "sarcasm" Take a deep breath, then act as described in the "any other end tag" entry below.
Писатели спецификации шутят.
Итог
После всех перестановок и манипуляций мы имеем самый быстрый и полноценный HTML парсер с поддержкой DOM/HTML Interfaces который строит HTML/DOM дерево полностью соответствующее HTML спецификации.
Если суммировать всё то, что мы изменили:
- Убирали предварительную обработку (перенесли в токенизатор)
- Токенизатор
- Добавили Incoming Buffer
- Унифицировали токены
- Вместо имён Tag ID
- Символьный токен: не один, а N+ символов
- Свой итератор в каждом состоянии
- Обработка токена в построении дерева
- Один токен на всё
- Построение дерева
- Модифицируем условия для символьных токенов
На i7 2012 года, на одном ядре, мы имеем среднюю скорость парсинга 235MB в секунду (Amazon-страницы).
Конечно же, я знаю как увеличить этот показатель в 1.5/2 раза, то есть запас ещё есть. Но, мне необходимо двигаться дальше. Собственно, а дальше парсинг CSS и создание собственного грамара (Grammar, то есть, генерация эффективного кода для парсинга по Grammar). В отличии от парсинга HTML, парсинг CSS намного сложнее, там есть где «развернуться».
Исходники
Описанный подход парсинга и построения HTML дерева реализован в Lexbor HTML.
P.S.
Следующая статья будет о парсинге CSS и Grammar. Как обычно, статья будет на готовый код. Ждать где-то 6-8 месяцев.
Для тех, у кого есть время
Не стесняйтесь помогать проекту. К примеру, если вы любите в свободное время писать документацию.
Не стесняйтесь поддерживать проект рублём (на другие валюты я тоже не обижусь). Об этом в личку.
