Пишем простой парсер файлов (для начинающих)
В этой статье я хотел бы рассказать как написать простой парсер на примере сайтов aimp.ru и geekbrains.ru. Статья предназначена строго для тех, кто уже имеет базовые знания о языке программирования C# и уже написал свой первый «Hello world».
Мне всегда нравился аудиоплеер Aimp (нет, это не реклама), но встроенных скинов у него слишком мало, а заходить на сайт, смотреть скины, скачивать и пробовать как они будут смотреться на деле не было никакого желания. Поэтому я решил написать парсер скинов с данного сайта. Немного посмотрев сайт, я заметил, что скины там хранятся последовательно с присвоенным id. Т.к. до недавнего времени я знал только 1С и немного командную строку, то недолго думая я решил написать его в командной строке. Но при тестировании обнаружил, что если скачивать большое количество файлов, то во-первых часть может просто не скачаться, а во-вторых может произойти переполнение оперативной памяти. В итоге я тогда бросил эту затею.
Не так давно начав изучать C# я решил вернуться к этой идее, дабы попрактиковаться немного. Что из этого получилось читайте под катом.
Для разработки нам понадобится только среда разработки, я использовал Visual Studio, вы можете использовать любую другую на ваш вкус.
Я не буду углубляться в базовые понятия C#, для этого написано множество различных книг и отснято бесчисленное количество роликов.
Для начала запустим Visual Studio и создадим консольное приложение (т.к. мне лень делать формы нам не нужен интерфейс). Среда разработки нам сама подготовит шаблон проекта. У нас получится что-то вроде этого:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApplication2 < class Program < static void Main(string[] args) < >> >
Удаляем директивы которыми мы пользоваться сейчас не будем:
using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;
И добавляем те директивы, которыми будем пользоваться:
using System.Diagnostics; // Нужна, чтобы запускать внешние процессы using System.Net; // Нужна, чтобы работать с Web using System.Threading; // Нужна, чтобы скоротать время
После чего в методе Main объявляем переменную:
WebClient wc = new WebClient(); // Она нужна непосредственно для работы с Web
Парсим Aimp скины
Далее мы пишем саму функцию:
static string DownloadSkinsForAimp(WebClient wc) < Console.WriteLine("Downloading began"); try < for (int i = 0; i catch < Console.WriteLine("Download" + name + "failed"); >> > catch < return "\nSomething went is wrong"; >return "\nDownloading complete"; >
Все скины скачиваются в директорию, заданную в настройках браузера. Конструкции try/catch нам нужны для того, чтобы программа не «вываливалась» из-за ошибок. Хотя можно было обойтись и без них.
Вы могли заметить функцию GetNameOfSkin. Она нужна для того, чтобы получить название скина, который мы скачиваем. Можно обойтись и без неё, она нужна только для красоты, но раз мы только учимся, то напишем и её:
static string GetNameOfSkin(WebClient wc, string id) < // Получаем строку с html разметкой string html = wc.DownloadString("http://www.aimp.ru/index.php?do=catalog&rec_id=" + id); // Находим в ней первое упоминание нужного нам id и удаляем ненужную левую часть // Название скина начинается через 5 символов после этого id string rightPartOfHtml = html.Substring(html.IndexOf(id) + 5); // Находим конец названия и удаляем оставшуюся правую часть string name = rightPartOfHtml.Substring(0, rightPartOfHtml.IndexOf("<")).Replace(" ", "_"); // В итоге нам возвращается только само название скина return name; >
Далее в методе Main нужно вызвать скачивание на выполнение:
Console.WriteLine(DownloadSkinsForAimp(wc)); // На консоль нужно выводить потому, что // метод возвращает нам строку с результатом выполнения
Парсим сертификаты Geekbrains
Сертификаты на сайте хранятся в открытом виде, и открыв их через сайт, как скины aimp мы сможем их скачать только вручную нажав кнопку скачать. Но это не дело, мы же программисты.
Тут нам на помощь приходит класс WebClient, а именно его метод DownloadFile. Ему мы просто передаем путь для скачивания и путь для сохранения и он все делает за нас. Звучит легко, попробуем сделать:
static string DownloadCertificates(WebClient wc) < // Нужна для того, чтобы определить имя текущего пользователя string currentUser = Environment.UserName; Console.WriteLine("Downloading began"); try < for (int i = 0; i catch < Console.WriteLine("Download certificate №7075" + i + " is failed"); >> > catch < return "\nSomething went is wrong"; >return "\nDownloading certificates are complite!"; >
И после чего точно также вызываем эту функцию из метода Main.
Вообще обе эти функции ещё есть куда дорабатывать, но я думаю для ознакомления и самых базовых функций парсинга они вполне подойдут. Кому лень все это собирать в один проект — вот ссылка на GitHub.
Спасибо за внимание и надеюсь, кому-нибудь это поможет.
P.S.: Сертификаты с geekbrains можно скачать и изменить имя и фамилию владельца на свою полюбоваться на них.
P.P.S.: Все скины, скачанные с сайта Aimp, хранятся в формате ‘.zip’ и при желании функцию можно доработать, чтобы она сама их разархивировала. Также можно добавить, чтобы они сразу переносились в папку со скинами Aimp.
P.P.P.S.: Статья является исключительно познавательной и не несет рекламный характер.
Как написать парсер сайта на Python
В этой статье мы рассмотрим, как создать базовый парсер сайта на Python, используя библиотеки BeautifulSoup и requests. Он сможет спарсить информацию со страниц сайта и сохранять ее для последующего анализа.
Что такое веб-парсинг?
Парсинг — это процесс извлечения данных из веб-страниц. Эти данные могут включать любую информацию, доступную на веб-странице: текст, ссылки, изображения, метаданные и т.д. Веб-парсеры используются для различных задач, включая мониторинг цен, анализ социальных медиа, веб-майнинг, веб-аналитику и т.д.
Необходимые инструменты
Для начала, нам необходимо установить две библиотеки Python: requests и beautifulsoup4 . Это можно сделать при помощи pip :
pip install requests beautifulsoup4
Requests — это библиотека Python, что позволяет нам выполнять HTTP-запросы, а BeautifulSoup — мощная библиотека для парсинга HTML и XML документов.
Начало работы
Для демонстрации мы напишем простой веб-парсер, который соберет заголовки статей с главной страницы блога на условном домене example.com. Первым шагом будет получение HTML-кода страницы. Мы воспользуемся для этого библиотекой requests :
import requests url = 'https://example.com/blog/' response = requests.get(url)
Если все прошло гладко, response.text теперь содержит HTML-код главной страницы блога.
Парсинг HTML
Теперь, когда у нас есть HTML-код страницы, мы можем воспользоваться BeautifulSoup для его парсинга:
from bs4 import BeautifulSoup soup = BeautifulSoup(response.text, 'html.parser')
BeautifulSoup преобразует HTML-строку в объект, с которым легко работать, предоставляя различные методы для навигации и поиска в HTML-структуре.
Поиск данных
С помощью BeautifulSoup мы можем использовать CSS-селекторы для поиска элементов на странице. Например, давайте найдем все заголовки статей на странице. Просмотрев код страницы, мы видим, что заголовки находятся в тегах , которые имеют класс blog-title .
titles = soup.select('h2.blog-title')
select возвращает список всех найденных элементов. Если нам нужен только первый найденный элемент, мы можем использовать метод select_one .
Извлечение данных
Теперь, когда мы нашли наши заголовки, мы можем вытянуть из них текст:
for title in titles: print(title.get_text())
Используя метод get_text() , мы можем получить весь текст, который находится внутри элемента, включая все его дочерние элементы.
Сохранение данных
Последний шаг — это сохранение собранных данных. Мы можем сохранить их в файл, базу данных или любое другое место в зависимости от наших потребностей. Для простоты давайте сохраним их в текстовый файл:
with open('titles.txt', 'w') as f: for title in titles: f.write(title.get_text() + '\n')
Теперь у нас есть простой парсер, который собирает заголовки с сайта и сохраняет их в текстовый файл.
Итоги
В этой статье мы рассмотрели основы написания веб-парсера на Python с использованием библиотек requests и BeautifulSoup . Это базовый пример, но принципы, которые мы здесь использовали, могут быть применены для написания намного более сложных веб-парсеров. Благодаря Python и его прекрасным библиотекам, парсинг становится простым и доступным инструментом для сбора данных из Интернета.
Если вы хотите расширить свои знания и навыки в написании парсеров на Python, вот несколько рекомендаций:
- Изучить больше о CSS селекторах и их использовании в BeautifulSoup для поиска нужных элементов.
- Ознакомиться с различными методами для навигации по DOM-структуре, такими как .parent, .children, .next_sibling и другие.
- Рассмотреть использование других библиотек Python для веб-парсинга, таких как lxml, html5lib или PyQuery.
- Исследовать возможности использования веб-парсеров для автоматического заполнения форм, работы с авторизацией на сайтах и обхода защиты от парсинга (например, CAPTCHA).
Кроме того, при написании веб-парсеров важно учитывать этические аспекты и соблюдать правила использования веб-сайтов. Проверяйте, разрешен ли парсинг и уважайте ограничения на частоту запросов.
С опытом и соблюдением лучших практик, написание парсеров на Python станет неотъемлемой частью вашего набора навыков, которая поможет вам собирать и анализировать данные из Интернета для различных целей.
Освоить профессию python разработчика вы можете на нашем курсе Python с трудоустройством.
Написание парсера с нуля: так ли страшен черт?
В прошлом топике я рассказывал о том, как мы с другом решили ради развлечения написать свой встраиваемый язык программирования для платформы .NET. У первой версии был серьезный недостаток — парсер был реализован на F# с помощью сторонней библиотеки. Из-за этого требовалась куча зависимостей, парсер работал медленно, а поддержка его была крайне муторным занятием.
Очевидно, что парсер нужно было переписать на C#, но при мысли о написании парсера с нуля вдруг находилась дюжина других срочных дел. Таким образом таск перекидывался и откладывался практически полгода и казался непосильным, а в итоге был сделан за 4 дня. Под катом я расскажу об удобном способе, позволившим реализовать парсер достаточно сложной грамматики без использования сторонних библиотек и не тронуться умом, а также о том, как это позволило улучшить язык LENS.
Но обо всем по порядку.
Первый блин
Как было сказано выше, в качестве ядра парсера мы использовали библиотеку FParsec. Причины данного выбора скорее исторические, нежели объективные: понравился легковесный синтаксис, хотелось поупражняться в использовании F#, и автор библиотеки очень оперативно отвечал на несколько вопросов по email.
Главным недостатком этой библиотеки для нашего проекта оказались внешние зависимости:
- Примерно десятимегабайтный F# Runtime
- 450 кб сборок самого FParsec
Другой проблемой было отображение ошибок. Лаконичная запись грамматики на местном DSL при некорректно введенной программе выдавала нечитаемую ошибку c перечислением ожидаемых лексем:
> let x = > Ошибка: ожидается идентификатор или число или скобка или вызов функции или 'new' или .
Хотя кастомная обработка ошибок и возможна, DSL для нее явно не предназначен. Описание грамматики уродливо распухает и становится абсолютно неподдерживаемым.
Еще одним неприятным моментом была скорость работы. При «холодном старте» компиляция любого, даже самого простого скрипта занимала на моей машине примерно 350-380 миллисекунд. Судя по тому, что повторный запуск такого же скрипта занимал уже всего-то 5-10 миллисекунд, задержка была вызвана JIT-компиляцией.
Сразу оговорюсь — для большинства реальных задач время разработки куда критичнее, чем пара дополнительных библиотек или сотни миллисекунд, которые тратятся на разбор. С этой точки зрения написание рукопашного парсера является скорее учебным или эзотерическим упражнением.
Немного теории
Сферический парсер в вакууме представляет собой функцию, которая принимает исходный код, а возвращает некое промежуточное представление, по которому удобно будет сгенерировать код для используемой виртуальной машины или процессора. Чаще всего это представление имеет древовидную структуру и называется абстрактным синтаксическим деревом — АСД (в иностранной литературе — abstract syntactic tree, AST).
Древовидная структура особенно хороша тем, что ее обход в глубину отлично сочетается со стековой организацией, используемой во многих современных виртуальных машинах (например, JVM или .NET). Генерация кода в данной статье рассматриваться не будет, однако элементы синтаксического дерева, как результат работы парсера, будут время от времени упоминаться.
Итак, на входе мы имеем строку. Набор символов. Работать с ней в таком виде напрямую не слишком удобно — приходится учитывать пробелы, переносы строк и комментарии. Для упрощения себе жизни разработчики парсеров обычно разделяют разбор на несколько проходов, каждый из которых выполняет какую-то одну простую задачу и передает результат своей работы следующему:
- Лексический анализатор: string -> IEnumerable
- Синтаксический анализатор: IEnumerable -> IEnumerable
- Семантический анализатор: IEnumerable -> ?
Лексический анализатор
- Скорость работы
- Легкость расширения
- Простота реализации
- Отслеживание положения в исходном тексте
Все лексемы изначально стоит поделить на 2 типа — статические и динамические. К первым относятся те лексемы, которые можно выразить обычной строкой — ключевые слова и операторы. Лексемы типа идентификаторов, чисел или строк проще описать регулярным выражением.
Статические лексемы, в свою очередь, есть резон поделить на операторы и ключевые слова. Ключевые слова сопоставляются только в том случае, если следующий за ними символ не является допустимым для идентификатора (или дальше — конец строки). В противном случае возникнут проблемы с идентификаторами, чье начало совпадает с ключевым словом: например, «information» -> keyword(in), keyword(for), identifier(mation) .
Пример реализации
enum LexemKind < Var, Print, Plus, Minus, Multiply, Divide, Assign, Semicolon, Identifier, Number >class LocationEntity < public int Offset; public int Length; >class Lexem : LocationEntity < public LexemKind Kind; public string Value; >class LexemDefinition < public LexemKind Kind < get; protected set; >public T Representation < get; protected set; >> class StaticLexemDefinition : LexemDefinition < public bool IsKeyword; public StaticLexemDefinition(string rep, LexemKind kind, bool isKeyword = false) < Representation = rep; Kind = kind; IsKeyword = isKeyword; >> class DynamicLexemDefinition : LexemDefinition < public DynamicLexemDefinition(string rep, LexemKind kind) < Representation = new Regex(@"\G" + rep, RegexOptions.Compiled); Kind = kind; >> static class LexemDefinitions < public static StaticLexemDefinition[] Statics = new [] < new StaticLexemDefinition("var", LexemKind.Var, true), new StaticLexemDefinition("print", LexemKind.Print, true), new StaticLexemDefinition("=", LexemKind.Assign), new StaticLexemDefinition("+", LexemKind.Plus), new StaticLexemDefinition("-", LexemKind.Minus), new StaticLexemDefinition("*", LexemKind.Multiply), new StaticLexemDefinition("/", LexemKind.Divide), new StaticLexemDefinition(";", LexemKind.Semicolon), >; public static DynamicLexemDefinition[] Dynamics = new [] < new DynamicLexemDefinition("[a-zA-Z_][a-zA-Z0-9_]*", LexemKind.Identifier), new DynamicLexemDefinition("(0|[1-9][0-9]*)", LexemKind.Number), >; > class Lexer < private char[] SpaceChars = new [] < ' ', '\n', '\r', '\t' >; private string Source; private int Offset; public IEnumerable Lexems < get; private set; >public Lexer(string src) < Source = src; Parse(); >private void Parse() < var lexems = new List(); while(InBounds()) < SkipSpaces(); if(!InBounds()) break; var lex = ProcessStatic() ?? ProcessDynamic(); if(lex == null) throw new Exception(string.Format("Unknown lexem at ", Offset)); lexems.Add(lex); > Lexems = lexems; > private void SkipSpaces() < while(InBounds() && Source[Offset].IsAnyOf(SpaceChars)) Offset++; >private Lexem ProcessStatic() < foreach(var def in LexemDefinitions.Statics) < var rep = def.Representation; var len = rep.Length; if(Offset + len >Source.Length || Source.Substring(Offset, len) != rep) continue; if(Offset + len < Source.Length && def.IsKeyword) < var nextChar = Source[Offset + len]; if(nextChar == '_' || char.IsLetterOrDigit(nextChar)) continue; >Offset += len; return new Lexem < Kind = def.Kind, Offset = Offset, Length = len >; > return null; > private Lexem ProcessDynamic() < foreach(var def in LexemDefinitions.Dynamics) < var match = def.Representation.Match(Source, Offset); if(!match.Success) continue; Offset += match.Length; return new Lexem < Kind = def.Kind, Offset = Offset, Length = match.Length, Value = match.Value >; > return null; > private bool InBounds() < return Offset < Source.Length; >>
- Работает быстро
- Элементарное устройство, можно написать за полчаса
- Новые лексемы добавляются очень просто
- Способ подходит для множества грамматик
- Танцы с бубном при разборе языка со значимыми пробелами
- Порядок объявления лексем важен: желательно сортировать по длине
Синтаксический анализатор
- Легкость расширения при изменении грамматики
- Возможность описывать подробные сообщения об ошибках
- Возможность заглядывать вперед на неограниченное количество позиций
- Автоматическое отслеживание положения в исходном коде
- Лаконичность, близость к исходной грамматике
- Описание — один конкретный узел:
(var_expr = «var» identifier » ;» > - Альтернатива — выбор из нескольких узлов
(stmt = var_expr | print_expr | assign_expr | other)
Правило-перечисление — это цикл. Чтобы вернуть последовательность значений, в C# есть очень удобный функционал для создания генераторов с помощью yield return .
Правило-альтернатива по очереди вызывает правила-варианты с помощью специальной обертки, которая позволяет откатиться в исходное состояние. Правила просто вызываются по порядку, пока хотя бы одно из них не совпадет, связанные оператором coalesce ( ?? ).
Тут пытливый читатель спросит:
— Как это, просто вызываются по порядку? А как же опережающие проверки? Например, так:
if(CurrentLexem.Type == LexemType.Var) return parseVar(); if(CurrentLexem.Type == LexemType.For) return parseFor(); .
Признаюсь, свой первый серьезный парсер я написал именно так. Однако это плохая идея!
Во-первых, заглянуть можно только на фиксированное число символов. Для всяких for или var , конечно, подойдет. Но, допустим, у нас есть такие правила в грамматике:
assign = id_assign | member_assign | index_assign id_assign = identifier "=" expr member_assign = lvalue "." identifier "=" expr index_assign = lvalue "[" expr "]" " spoiler">Скрытый текстpartial class Parser < private ListLexems; private int LexemId; #region Lexem handlers [DebuggerStepThrough] private bool Peek(params LexemType[] types) < var Lexems.Length - 1); var lex = Lexems[id]; return lex.Type.IsAnyOf(types); >[DebuggerStepThrough] private Lexem Ensure(LexemType type, string msg, params object[] args) < var lex = Lexems[LexemId]; if(lex.Type != type) error(msg, args); Skip(); return lex; >[DebuggerStepThrough] private bool Check(LexemType lexem) < var lex = Lexems[LexemId]; if (lex.Type != lexem) return false; Skip(); return true; >[DebuggerStepThrough] private void Skip(int count = 1) < LexemId = Math.Min(LexemId + count, Lexems.Length - 1); >#endregion #region Node handlers [DebuggerStepThrough] private T Attempt(Func getter) where T : LocationEntity < var backup = LexemId; var result = Bind(getter); if (result == null) LexemId = backup; return result; >[DebuggerStepThrough] private T Ensure(Func getter, string msg) where T : LocationEntity < var result = Bind(getter); if (result == null) throw new Exception(msg); return result; >[DebuggerStepThrough] private T Bind(Func getter) where T : LocationEntity < var startId = LexemId; var start = Lexems[LexemId]; var result = getter(); if (result != null) < result.StartLocation = start.StartLocation; var endId = LexemId; if (endId >startId && endId > 0) result.EndLocation = Lexems[LexemId - 1].EndLocation; > return result; > #endregion >
С их помощью реализация приведенной выше грамматики становится практически тривиальной:
partial class Parser < public Node ParseAssign() < return Attempt(ParseIdAssign) ?? Attempt(ParseMemberAssign) ?? Ensure(ParseIndexAssign, "Неизвестный тип выражения!"); >public Node ParseIdAssign() < var (id == null) return null; if (!Check(LexemType.Assign)) return null; var expr = Ensure(ParseExpr, "Ожидается присваиваемое выражение!"); return new IdAssignNode < Identifier = id, Expression = expr >; > public Node ParseMemberAssign() < var lvalue = Attempt(ParseLvalue); if (lvalue == null) return null; if (!Check(LexemType.Dot)) return null; var member = TryGetValue(LexemType.Identifier); if (member == null) return null; if (!Check(LexemType.Assign)) return null; var expr = Ensure(ParseExpr, "Ожидается присваиваемое выражение!"); return new MemberAssignNode < Lvalue = lvalue, MemberName = member, Expression = expr >; > public Node ParseIndexAssign() < var lvalue = Attempt(ParseLvalue); if (lvalue == null) return null; if (!Check(LexemType.SquareBraceOpen)) return null; var index = Ensure(ParseExpr, "Ожидается выражение индекса!"); Ensure(LexemType.SquareBraceClose, "Не закрыта скобка!"); Ensure(LexemType.Assign, "Ожидается знак присваивания!"); var expr = Ensure(ParseExpr, "Ожидается присваиваемое выражение!"); return new IndexAssignNode < Lvalue = lvalue, Index = index, Expression = expr >; > >
Атрибут DebuggerStepThrough сильно помогает при отладке. Поскольку все вызовы вложенных правил так или иначе проходят через Attempt и Ensure, без этого атрибута они будут постоянно бросаться в глаза при Step Into и забивать стек вызовов.
Преимущества данного метода:
- Откат состояния — очень дешевая операция
- Легко управлять тем, до куда можно откатываться
- Легко отображать детальные сообщения об ошибках
- Не требуются никакие внешние библиотеки
- Небольшой объем генерируемого кода
- Реализация парсера вручную занимает время
- Сложность написания и оптимальность работы зависят от качества грамматики
- Леворекурсивные грамматики следует разруливать самостоятельно
Операторы и приоритеты
Неоднократно я видел в описаниях грамматик примерно следующие правила, показывающие приоритет операций:
expr = expr_1 < op_1 expr_1 >expr_1 = exp2_2 < op_2 expr_2 >expr_2 = exp2_3 < op_3 expr_3 >expr_3 = int | float | identifier op_1 = "+" | "-" op_2 = "*" | "/" | "%" op_3 = "**"
Теперь представим, что у нас есть еще булевы операторы, операторы сравнения, операторы сдвига, бинарные операторы, или какие-нибудь собственные. Сколько правил получается, и сколько всего придется поменять, если вдруг придется добавить новый оператор с приоритетом где-то в середине?
Вместо этого, можно убрать из грамматики вообще все описание приоритетов и закодить его декларативно.
Пример реализации
expr = sub_expr < op sub_expr >sub_expr = int | float | identifier
partial class Parser < private static List>> Priorities = new List>> < new Dictionary> < < LexemType.Plus, (a, b) =>new AddNode(a, b) >, < LexemType.Minus, (a, b) =>new SubtractNode(a, b) > >, new Dictionary> < < LexemType.Divide, (a, b) =>new DivideNode(a, b) >, < LexemType.Multiply, (a, b) =>new MultiplyNode(a, b) >, < LexemType.Remainder, (a, b) =>new RemainderNode(a, b) > >, new Dictionary> < < LexemType.Power, (a, b) =>new PowerNode(a, b) > >, >; public NodeBase ProcessOperators(Func next, int priority = 0) < if (priority == Priorities.Count) return getter(); var node = ProcessOperators(next, priority + 1); var ops = Priorities[priority]; while (Lexems[LexemId].IsAnyOf(ops.Keys)) < foreach (var curr in ops) < if (check(curr.Key)) < node = curr.Value( node, ensure(() =>ProcessOperators(next, priority + 1), "Ожидается выражение!") ); > > > return node; > >
Теперь для добавления нового оператора необходимо лишь дописать соответствующую строчку в инициализацию списка приоритетов.
Добавление поддержки унарных префиксных операторов оставляю в качестве тренировки для особо любопытных.
Что нам это дало?
Написанный вручную парсер, как ни странно, стало гораздо легче поддерживать. Добавил правило в грамматику, нашел соответствующее место в коде, дописал его использование. Backtracking hell, который частенько возникал при добавлении нового правила в старом парсере и вызывал внезапное падение целой кучи на первый взгляд не связанных тестов, остался в прошлом.
Итого, сравнительная таблица результатов:
Параметр
FParsec Parser
Pure C#
Время парсинга при 1 прогоне
220 ms
90 ms
Время парсинга при дальнейших прогонах
5 ms
6 ms
Размер требуемых библиотек
800 KB + F# Runtime
260 KB
Скорее всего, возможно провести оптимизации и выжать из синтаксического анализатора больше производительности, но пока и этот результат вполне устраивает.
Избавившись от головной боли с изменениями в грамматике, мы смогли запилить в LENS несколько приятных вещей:
Цикл for
Используется как для обхода последовательностей, так и для диапазонов:
var data = new [1; 2; 3; 4; 5] for x in data do println "value = " x for x in 1..5 do println "square = " x
Композиция функций
С помощью оператора :> можно создавать новые функции, «нанизывая» существующие:
let invConcat = (a:string b:string) -> b + a let invParse = incConcat :> int::Parse invParse "37" "13" // 1337
Частичное применение возможно с помощью анонимных функций:
fun add:int (x:int y:int) -> x + y let addTwo = int::TryParse :> (x:int -> add 2 x) addTwo "40" // 42
Улучшения синтаксиса
Однострочные комментарии:
somecode () // comment
var x : int
var inc = x:int -> x + 1
if x then a () else b () while a < b do println "a = " a a = a + 1
print " = " a[1] SomeType::b
- Объявление generic-типов и функций
- Возможность пометить функции или типы атрибутами
- Поддержку событий
Как правильно написать парсер на python?
Я написал код нахождения заголовков и скачивания изображений. Только по отдельности коды работают, а вместе нет. Мне нужно сделать чтобы название было заголовком и к нему скачивалось его изображение. Вот сам код:
from bs4 import BeautifulSoup as bs import requests page = 1 while page '.format(link)) with open(names.format(link), 'wb') as f: f.write(requests.get(link).content) page += 1
Отслеживать
82 14 14 бронзовых знаков
задан 3 июн 2022 в 21:47
user501488 user501488
2 ответа 2
Сортировка: Сброс на вариант по умолчанию
Если я правильно понял задачу, то правильный парсер, с моей точки зрения, мог бы выглядеть так:
pip install fake-useragent requests tqdm
from tqdm.auto import tqdm import re from requests import Session from requests.adapters import HTTPAdapter from urllib3.util import Retry from fake_useragent import UserAgent from multiprocessing.pool import ThreadPool as Pool from bs4 import BeautifulSoup as Soup, Tag from pathlib import Path # Определим папку, куда мы будем сохранять изображения downloads = Path('downloads') # Создадим эту папку если ее не существует # exists_ok: позволит не поднимать исключение если # папка уже существует # parents: позволит создать весь путь до конечной # папки в случае его отсутствия downloads.mkdir(exist_ok=True, parents=True) ua = UserAgent() s = Session() # Для некоторых особо капризных серверов желательно добавить в сессию адаптер, # для корректной обработки запросов. В данном случае в этом нет необходимости. # Сайт не капризничает и отдает все нормально s.mount( 'https://ananasposter.ru', HTTPAdapter( max_retries=Retry( connect=5, read=10, total=25, backoff_factor=.005, allowed_methods=['GET'], ) ) ) # Добавим в заголовок сессии User-Agent, чтобы сайт думал что мы FireFox s.headers.update( ) url = 'https://ananasposter.ru/catalogue' # Не все символы разрешено использовать для названия файлов, # поэтому создадим словарь для подмены запрещенных символов subs = < '\\': '-', '/': '-', ':': '~', '*': '#', '?': '', '': ']', '"': "'", '|': '~', '\n': ' ' > def get_image(img: Tag): # Получим ссылку на изображение из тега img link = img.get('src') # Используя регулярное выражение определим системное # имя и расширение файла изображения, # опустив его размер idx, extension = m.groups() if ( m := re.search(r'(?' if isinstance( tt := img.get('title'), str ) else f'.' downloads.joinpath(title).write_bytes( s.get(link).content ) def process_page(page_num: int): # Получим response от страницы с указанным номером # Все, что идет после знака вопроса в url лучше указывать в качестве параметров, # а не придумывать способы их конкатенации resp = s.get( url, params= < 'page': page_num >) soup = Soup(resp.content, 'html.parser') # Если номер обрабатываемой страницы равен 1, то вычисляем номер последней страницы # (в противном случае в этом нет необходимости и данную операцию можно пропустить) # для этого находим все теги a, параметр href которых # соответствует регулярному выражению # получаем из них параметр href, удаляем из полученного url # все кроме номера страницы # и находим максимальный по ключу int # В случае если список необходимых url окажется пустым, # вернем единицу как максимальное значение из списка [1] pages = None if page_num - 1 else int(max( [ a.get('href').removeprefix('https://ananasposter.ru/catalogue?page=') for a in soup.find_all('a', href=re.compile(r'(?<=page=)\d+$')) ] or [1], key=int )) # Каждый найденный тег img класса main_image for image in soup.find_all('img', class_='main_image'): # Отдадим на обработку в функцию get_image get_image(image) # Функция вернет вычисленный выше номер последней страницы return pages # Запустим обработку первой страницы и получим номер последней для # использования его в создании цикла pages_total = process_page(1) # Ввиду немалого количества страниц предлагаю использовать пул из 20-ти потоков with Pool(20) as pool: # Использование бара tqdm дает возможно отслеживать процесс обработки for _ in tqdm( # imap_unordered даст возможность задействовать освободившиеся # потоки вне очереди pool.imap_unordered( process_page, # Начинаем обработку со второй страницы, так-как # первую мы уже обработали range(2, pages_total) ), total=pages_total, initial=2 ): # Поскольку функция process_page уже не возвращает # ничего нужного, здесь мы ничего не делаем pass
# 100%|██████████| 1303/1303 [22:40
UPD:
В общем я оказался прав. Дело было в одинаковых названиях:
Добавьте импорт:
from uuid import uuid4
