Не скажу, что пост очень интересный. В основном, рассказывается, какими путями я шел, какие костыли при этом строил и что из этого вышло.
Я создал кучу велосипедов, иногда по незнанию аналогов, иногда просто потому, что хотел попробовать сделать это сам. Тем не менее, ближе к делу.
Всё началось с того, что появилась некоторая надобность в Android-клиенте для Табуна.
Не найдя по запросу «java парсер LiveStreet» ничего путного, я сел и и написал что-то очень кривое. Оно загружало страницу построчно и пропускало её через данный загрузчику парсер. Парсеры последовательно переключались и потом из них доставались данные.
И это работало, причём достаточно быстро (не смотря на
Поэтому, чуть позже я написал свой собственный парсер тегов. Назло всему миру, я сделал это регулярными выражениями. Он тут, если кто хочет посмотреть на это весёлое зрелище.
Он был неимоверно медленным, но работал. Вскоре мне он надоел, и я переписал этот парсер в посимвольный вариант, и это примерно в 40 раз ускорило нахождение тегов.
И да, тот
Эта штука работала, классно работала, и долгое время меня устраивала, но мне надоело лазить по деревьям самому. Да и сам парсер занимался доставанием страницы в том же потоке, что и загружал её, так что после возвращения к этому проекту я придумал сделать так: Скачивать текст страницы в одном потоке, передавать строки в другой поток, который находил и доставал теги, теги передавать в третий поток, который анализировал HTML на наличие ошибок и немного исправлял их.
Эта штука работала замечательно — в конце XML — дерево я получал и использовал на нём свой небольшой вариант xPath.
Вместе с тем я добавил модули — объекты, которым передаваласть страница после загрузки для того, чтобы они вернули готовый кусок данных. Из них было удобно строить страницы.
После того, как всё это заработало, я задался вопросом — зачем элементу, который состоит из одного тега, может понадобится вся страница? И как я буду доставать комментарии, их же много и они все одинаковые?
Поэтому я ещё немного всё поменял. Теперь, анализатор HTML при получении закрывающего тега ищет открывающий для него и создаёт объект, содержащий оба этих тега, по запросу собирающий дерево.
Этот объект передаётся в обработчики. Если он кого-то интересует, то обработчик об этом говорит, и ему передают дерево, содержащееся под этим тегом. Обработчик с ним возится и отдаёт уже готовый комментарий/пост/что-нибудь ещё.
Все обработчики собраны в объекте страницы, у которой есть два основных метода — привязать обработчики и обработать пришедший готовый объект. Каждый обработчик привязывается к его ID, и под этим же ID возвращает объект — как requestCode в Android. К примеру, как выглядит страница поста.
В итоге, я получил модульный, достаточно быстрый, удобный и сравнительно красивый парсер. Несмотря на то, что анализатор довольно сырой, и может ломаться в кривых частях разметки, всё вполне работает и годно к использованию. Лицензировано под GPLv3, находится тут. Замечу, что в ветке master находится довольно старая версия, а многие новые не смерджены в dev. Надеюсь, кому-нибудь помогу тем, что я написал.
Спаcибо за потраченное время и удачного дня!
P.S: Если у кого-нибудь есть способ писать @ Annotation без пробела и без преобразования в ссылку, или способ делать спойлеры по умолчанию закрытыми — расскажите, если не сложно.
Я создал кучу велосипедов, иногда по незнанию аналогов, иногда просто потому, что хотел попробовать сделать это сам. Тем не менее, ближе к делу.
Всё началось с того, что появилась некоторая надобность в Android-клиенте для Табуна.
Не найдя по запросу «java парсер LiveStreet» ничего путного, я сел и и написал что-то очень кривое. Оно загружало страницу построчно и пропускало её через данный загрузчику парсер. Парсеры последовательно переключались и потом из них доставались данные.
Парсер комментариев, версия 1
public static class CommentParser implements ResponseFactory.Parser { public Comment comment = new Comment(); int part = 0; @ Override public boolean line(String line) { switch (part) { case 0: // Находим заголовок if (line.contains("<section id=\"comment_id")) { comment.id = Integer.parseInt(U.sub(line, "_id_", "\"")); part++; } break; case 1: // Находим текст if (line.contains("<div class=\" text\">")) { part++; } break; case 2: // Записываем текст // Изменить эту фигню на что-нибудь более правдоподобное. А то ведь и табуляцию сменить могут. if (line.equals("\t\t\t</div>")) part++; else comment.body += line.replace("\t", ""); break; case 3: // Находим автора if (line.contains("http://tabun.everypony.ru/profile/")) { comment.author = U.sub(line, "http://tabun.everypony.ru/profile/", "/"); comment.avatar = U.sub(line, "img src=\"", "\""); part++; } break; case 4: // Находим дату публикации if (line.contains("<time datetime")) { comment.time = U.sub(line, "datetime=\"", "\""); part++; } break; case 5: // Находим рейтинг if (line.contains("vote_total_comment")) { comment.votes = Integer.parseInt(U.sub(line, ">", "<")); part++; } break; case 6: // Пытаемся найти родительский комментарий if (line.contains("goToParentComment")) comment.parent = Integer.parseInt(U.sub(line, ",", ");")); if (line.contains("</section>")) return false; break; } return true; } }
И это работало, причём достаточно быстро (не смотря на
String text
), но писать такие костыли было долго, муторно и они часто ломались. А нужно их было много.Поэтому, чуть позже я написал свой собственный парсер тегов. Назло всему миру, я сделал это регулярными выражениями. Он тут, если кто хочет посмотреть на это весёлое зрелище.
Он был неимоверно медленным, но работал. Вскоре мне он надоел, и я переписал этот парсер в посимвольный вариант, и это примерно в 40 раз ускорило нахождение тегов.
Парсер комментариев, версия 2
public static class CommentParser implements ResponseFactory.Parser { public Comment comment = new Comment(); int part = 0; String text = ""; @ Override public boolean line(String line) { switch (part) { case 0: // Находим заголовок if (line.contains("<section id=\"comment_id")) { comment.id = U.parseInt(U.sub(line, "_id_", "\"")); text += line + '\n'; part++; } break; case 1: if (line.contains("</section>")) { text += line; HTMLParser parser = new HTMLParser(text); comment.body = parser.getContents(parser.getTagIndexByProperty("class", " text")).replaceAll("\t", ""); // Тут чуточку сложнее. comment.time = parser.getParserForIndex(parser.getTagIndexByProperty("class", "comment-date")).getTagByName("time").props.get("datetime"); // Достаём автора и аватарку. HTMLParser author = parser.getParserForIndex(parser.getTagIndexByProperty("class", "comment-author ")); { comment.author = U.bsub(author.getTagByName("a").props.get("href"), "profile/", "/"); comment.avatar = parser.getTagByName("img").props.get("src"); } // Попытка достать род. комментарий: try { HTMLParser comment_parent_goto = parser.getParserForIndex(parser.getTagIndexByProperty("class", "goto goto-comment-parent")); comment.parent = U.parseInt(U.bsub(comment_parent_goto.getTagByName("a").props.get("onclick"), ",", ");")); } catch (Error e) { comment.parent = 0; } comment.votes = U.parseInt(parser.getContents(parser.getTagByProperty("class", "vote-count")).trim()); return false; } else text += line + '\n'; break; } return true; }
И да, тот
String text
всё ещё тут. Эта штука работала, классно работала, и долгое время меня устраивала, но мне надоело лазить по деревьям самому. Да и сам парсер занимался доставанием страницы в том же потоке, что и загружал её, так что после возвращения к этому проекту я придумал сделать так: Скачивать текст страницы в одном потоке, передавать строки в другой поток, который находил и доставал теги, теги передавать в третий поток, который анализировал HTML на наличие ошибок и немного исправлял их.
Эта штука работала замечательно — в конце XML — дерево я получал и использовал на нём свой небольшой вариант xPath.
Вместе с тем я добавил модули — объекты, которым передаваласть страница после загрузки для того, чтобы они вернули готовый кусок данных. Из них было удобно строить страницы.
После того, как всё это заработало, я задался вопросом — зачем элементу, который состоит из одного тега, может понадобится вся страница? И как я буду доставать комментарии, их же много и они все одинаковые?
Поэтому я ещё немного всё поменял. Теперь, анализатор HTML при получении закрывающего тега ищет открывающий для него и создаёт объект, содержащий оба этих тега, по запросу собирающий дерево.
Этот объект передаётся в обработчики. Если он кого-то интересует, то обработчик об этом говорит, и ему передают дерево, содержащееся под этим тегом. Обработчик с ним возится и отдаёт уже готовый комментарий/пост/что-нибудь ещё.
Все обработчики собраны в объекте страницы, у которой есть два основных метода — привязать обработчики и обработать пришедший готовый объект. Каждый обработчик привязывается к его ID, и под этим же ID возвращает объект — как requestCode в Android. К примеру, как выглядит страница поста.
Парсер комментариев, текущая версия
public class CommentModule extends ModuleImpl<Comment> { @ Override public Comment extractData(HTMLTree page, AccessProfile profile) { Comment comment = new Comment(); comment.id = U.parseInt(page.get(0).get("id").replace("comment_id_", "")); try { comment.text = page.getContents(page.xPathFirstTag("section/div/div&class=*text*")).trim(); } catch (Exception ex) { comment.deleted = true; } HTMLTree info = page.getTree(page.xPathFirstTag("ul&class=comment-info")); Tag parent = info.xPathFirstTag("li&class=*parent*/a"); if (parent == null) comment.parent = 0; else comment.parent = U.parseInt(SU.bsub(parent.get("onclick"), ",", ");")); comment.author.nick = SU.bsub(info.xPathFirstTag("li/a").get("href"), "profile/", "/"); comment.author.small_icon = info.xPathFirstTag("li/a/img").get("src"); comment.author.fillImages(); comment.is_new = page.get(0).get("class").contains("comment-new"); comment.time = info.xPathFirstTag("li/time").get("datetime"); comment.votes = U.parseInt(info.xPathStr("li/span&class=vote-count")); return comment; } @ Override public boolean doYouLikeIt(Tag tag) { return "section".equals(tag.name) && String.valueOf(tag.get("class")).contains("comment"); } }
В итоге, я получил модульный, достаточно быстрый, удобный и сравнительно красивый парсер. Несмотря на то, что анализатор довольно сырой, и может ломаться в кривых частях разметки, всё вполне работает и годно к использованию. Лицензировано под GPLv3, находится тут. Замечу, что в ветке master находится довольно старая версия, а многие новые не смерджены в dev. Надеюсь, кому-нибудь помогу тем, что я написал.
Спаcибо за потраченное время и удачного дня!
P.S: Если у кого-нибудь есть способ писать @ Annotation без пробела и без преобразования в ссылку, или способ делать спойлеры по умолчанию закрытыми — расскажите, если не сложно.