В предыдущей статье, я написал, как можно на этапе скачивания отбросить лишние страницы до непосредственной загрузки. Оставшиеся приходится хранить. Несмотря на большие и дешёвые диски, объёмы получаются существенными. Например, у меня сейчас в БД загружено 12 тыс страниц. И чистый HTML без картинок и прочего весит 900 МБ. Это неприемлемо много, поскольку для одного только habrahabr в очереди на скачивание осталось 20 тыс. страниц, в действительности их больше, т.е. только один сайт займёт более 2 ГБ на диске. Нужно как-то решать проблему.
Сжатие
Это очевидная и простая оптимизация.
Для Go много готовых архиваторов, различающихся по эффективности сжатия и скорости работы. Я протестировал три самых популярных:
Тут "level" - уровень сжатия. А цифры в столбце показывают какой размер я получил после сжатия.
Как видно, тут zlib и gzip показывают одинаковые результаты, а вот lzma - гораздо эффективнее по степени сжатия.
Так же я замерил время работы при сжатии:
Тут опять же zlib и gzip отличаются на уровне погрешности (правда в пользу zlib). А lzma сильно проигрывает.
Потом проверил скорость разархивации:
zlib опять лидер, gzip не сильно хуже, а вот lzma - отстаёт, иногда до 2 раз.
Конечно хорошо было бы замерить потребление памяти и нагрузку на CPU, но я пока ни в то, ни в другое не упираюсь.
Если интересно можно посмотреть на аналоги. Snappy: сжала текст до 57 MB за 2 секунды, скорость конечно высокая, но степень сжатия мне не подходит. Ещё есть lzw из стандартной библиотеки - сжал до 106 MB за 5.1s - слишком слабо, как по мне. Кстати, есть проект который реализует lzma на нативном Go, правда сжимает он почему-то до 122 MB за 3m59.2s, да и сам автор говорит, что его работа далека от завершения. Вообще алгоритмов сжатия - десятки, если не сотни и можно провести не один день исследуя их, но похоже разница в результатах сжатия при сопоставимом времени работы в современных архиваторах - минимальна.
В итоге для себя - я выбрал zlib с уровнем сжатия "6", дальнейшее увеличение уровня почти не уменьшает размер сжатых данных, а вот время заметно увеличивается. lzma хоть и даёт результат лучше примерно на 10%, работает до трех раз медленнее, плюс требует C-библиотеку, что мне особенно не нравится.
Для Go много готовых архиваторов, различающихся по эффективности сжатия и скорости работы. Я протестировал три самых популярных:
- zlib - входит в стандартную библиотеку Go.
- gzip - тоже входит в стандартную библиотеку.
- lzma - взял одну из обёрток вокруг C библиотеки реализующей LZMA, приводить ссылку не буду, т.к. их десятки, а какая лучшая - не знаю.
Level | lzma | zlib | gzip |
---|---|---|---|
0 | 43 MB | 240 MB | 240 MB |
1 | 41 MB | 53 MB | 53 MB |
2 | 40 MB | 51 MB | 51 MB |
3 | 40 MB | 49 MB | 49 MB |
4 | 39 MB | 47 MB | 47 MB |
5 | 38 MB | 45 MB | 45 MB |
6 | 38 MB | 44 MB | 44 MB |
7 | 38 MB | 44 MB | 44 MB |
8 | 38 MB | 44 MB | 44 MB |
9 | 38 MB | 44 MB | 44 MB |
Тут "level" - уровень сжатия. А цифры в столбце показывают какой размер я получил после сжатия.
Как видно, тут zlib и gzip показывают одинаковые результаты, а вот lzma - гораздо эффективнее по степени сжатия.
Так же я замерил время работы при сжатии:
Level | lzma | zlib | gzip |
---|---|---|---|
0 | 13.3s | 676.2ms | 826.3ms |
1 | 17.0s | 7.3s | 7.5s |
2 | 18.7s | 7.5s | 7.7s |
3 | 22.6s | 8.1s | 8.3s |
4 | 45.3s | 10.7s | 10.9s |
5 | 1m0.7s | 13.5s | 13.7s |
6 | 1m12.9s | 17.1s | 17.3s |
7 | 1m28.1s | 19.3s | 19.4s |
8 | 1m58.1s | 22.6s | 22.6s |
9 | 1m55.2s | 25.8s | 25.8s |
Тут опять же zlib и gzip отличаются на уровне погрешности (правда в пользу zlib). А lzma сильно проигрывает.
Потом проверил скорость разархивации:
Level | lzma | zlib | gzip |
---|---|---|---|
0 | 4.1s | 735.3ms | 950.7ms |
1 | 3.8s | 2.9s | 3.1s |
2 | 3.7s | 2.8s | 3.0s |
3 | 3.7s | 2.7s | 2.9s |
4 | 3.7s | 2.6s | 2.9s |
5 | 3.6s | 2.5s | 2.8s |
6 | 3.6s | 2.5s | 2.7s |
7 | 3.6s | 2.5s | 2.7s |
8 | 6.7s | 2.5s | 2.7s |
9 | 6.5s | 2.5s | 2.7s |
zlib опять лидер, gzip не сильно хуже, а вот lzma - отстаёт, иногда до 2 раз.
Конечно хорошо было бы замерить потребление памяти и нагрузку на CPU, но я пока ни в то, ни в другое не упираюсь.
Если интересно можно посмотреть на аналоги. Snappy: сжала текст до 57 MB за 2 секунды, скорость конечно высокая, но степень сжатия мне не подходит. Ещё есть lzw из стандартной библиотеки - сжал до 106 MB за 5.1s - слишком слабо, как по мне. Кстати, есть проект который реализует lzma на нативном Go, правда сжимает он почему-то до 122 MB за 3m59.2s, да и сам автор говорит, что его работа далека от завершения. Вообще алгоритмов сжатия - десятки, если не сотни и можно провести не один день исследуя их, но похоже разница в результатах сжатия при сопоставимом времени работы в современных архиваторах - минимальна.
В итоге для себя - я выбрал zlib с уровнем сжатия "6", дальнейшее увеличение уровня почти не уменьшает размер сжатых данных, а вот время заметно увеличивается. lzma хоть и даёт результат лучше примерно на 10%, работает до трех раз медленнее, плюс требует C-библиотеку, что мне особенно не нравится.
Поиск дубликатов
Как бы я не старался заниматься нормализацией ссылок, обработкой редиректов и т.п. Всё равно 0.6% скачанных страниц - дубликаты. А это лишние мегабайты на диске и мусор в БД.
Избавиться от дубликатов легко. Что бы не сравнивать большие куски HTML - сохраняйте вместе с данными ещё и хеш страницы. Поиск по хешу - почти не занимает времени (главное помнить про коллизии). Хеш ещё крайне полезен для проверки целостности данных. Будет обидно работать с данными у которых БД или архиватор случайно потеряет пару байт.
Избавиться от дубликатов легко. Что бы не сравнивать большие куски HTML - сохраняйте вместе с данными ещё и хеш страницы. Поиск по хешу - почти не занимает времени (главное помнить про коллизии). Хеш ещё крайне полезен для проверки целостности данных. Будет обидно работать с данными у которых БД или архиватор случайно потеряет пару байт.
Чистка HTML
HTML такой нехороший формат, который мешает извлекать текст из страницы. А значит нужно привести скачанную страницу к подмножеству HTML, почистив "мусор". Что бы делать это системно - нужно вооружиться каким-нибудь справочником по тегам.
Первое, что я сделал - вырезал теги, вместе с содержимым, которые меня не интересуют. Это "input", "button", "br", "audio" и им подобные.
Потом пришёл черёд строчных тегов. Нужно конечно смотреть на каждый тег по отдельности, но в большинстве случаев их нужно "раскрывать".
Например при вот такой вёрстке:
После парсинга "в лоб" можно получить три слова "При", "в" и "ет", хотя на самом деле там только одно "привет". А если строчный тег "b" удалить, оставив его содержимое получим правильное:
Тут нужно действовать аккуратно, часть тегов содержит полезную информацию в своих атрибутах. Например тег "abbr" в атрибуте "title" хранит текст, который я бы хотел сохранить, поэтому я добавляю его к тексту внутри тега, а сам тег - удаляю.
В атрибутах можно найти полезную мета информации, которую не хочется хранить рядом с текстом. Поэтому я отдельным шагом - извлекаю её и сохраняю отдельно. Примером могут служить ссылки, заголовок страницы и т.п. А вот например кодировку страницы даже отдельно хранить не нужно - сразу конвертируем текст в UTF-8. Затем на этапе чистки HTML такие атрибуты удаляются.
Блочные элементы, в основном, я преобразовывал в тег "div", как наиболее универсальный.
Конечно по каждому тегу решение принимается отдельно, в зависимости от задачи. У меня в итоге после преобразования не осталось ничего кроме тегов "div" и текста. Кто-то наоборот оставляет максимальное количество тегов и применяет эвристические алгоритмы для анализа. Например для рекламных блоков характерна ситуация, когда разметки больше чем собственно самого текста, а значит его нужно отбросить. Можно уйти в другую крайность и удалить вообще все теги оставив только текст. Но я планирую в дальнейшем использовать информацию о том, как текст разбит на блоки.
Ещё отмечу, что в современном вебе, невозможно правильно извлечь текст из страницы только анализируя теги. Как минимум нужно ещё смотреть на CSS, а в идеале и выполнять JS код. Но для этого нужны слишком сложные алгоритмы.
Этот шаг сократил размер несжатого HTML в 4 раза. Очень хороший результат, правда долгий в реализации.
Первое, что я сделал - вырезал теги, вместе с содержимым, которые меня не интересуют. Это "input", "button", "br", "audio" и им подобные.
Потом пришёл черёд строчных тегов. Нужно конечно смотреть на каждый тег по отдельности, но в большинстве случаев их нужно "раскрывать".
Например при вот такой вёрстке:
<div>При<b>в</b>ет</div>
<div>Привет</div>
В атрибутах можно найти полезную мета информации, которую не хочется хранить рядом с текстом. Поэтому я отдельным шагом - извлекаю её и сохраняю отдельно. Примером могут служить ссылки, заголовок страницы и т.п. А вот например кодировку страницы даже отдельно хранить не нужно - сразу конвертируем текст в UTF-8. Затем на этапе чистки HTML такие атрибуты удаляются.
Блочные элементы, в основном, я преобразовывал в тег "div", как наиболее универсальный.
Конечно по каждому тегу решение принимается отдельно, в зависимости от задачи. У меня в итоге после преобразования не осталось ничего кроме тегов "div" и текста. Кто-то наоборот оставляет максимальное количество тегов и применяет эвристические алгоритмы для анализа. Например для рекламных блоков характерна ситуация, когда разметки больше чем собственно самого текста, а значит его нужно отбросить. Можно уйти в другую крайность и удалить вообще все теги оставив только текст. Но я планирую в дальнейшем использовать информацию о том, как текст разбит на блоки.
Ещё отмечу, что в современном вебе, невозможно правильно извлечь текст из страницы только анализируя теги. Как минимум нужно ещё смотреть на CSS, а в идеале и выполнять JS код. Но для этого нужны слишком сложные алгоритмы.
Этот шаг сократил размер несжатого HTML в 4 раза. Очень хороший результат, правда долгий в реализации.
Минимизация текста
Если возможно ужать теги, то почему нельзя то же сделать и с текстом? Например всевозможные звёздочки, стрелочки, сердечки и т.п. - лишние. Впрочем, и знакомые с детства точки, запятые и скобки несут сомнительную ценность. Я поступил просто: оставил символы русского и английского алфавита, цифры и десяток символов (вроде '&', '-'…), а остальные заменил на пробелы. Потом убрал лишние пробелы между словами, что бы уменьшить размер. Дальше осталось только удалить ставшие пустыми теги.
Результат, я привёл к нижнему регистру - анализировать потом проще, да и коэффициент сжатия лучше будет. В итоге по отношению к предыдущему шагу получил сжатие в 1.2 раза.
Результат, я привёл к нижнему регистру - анализировать потом проще, да и коэффициент сжатия лучше будет. В итоге по отношению к предыдущему шагу получил сжатие в 1.2 раза.
Результаты
Напомню, что исходный размер HTML - 900 МБ, после чистки HTML, минимизации текста и сжатия размер сократился в 16.4 раза и стал 54 МБ.