Анализ CSS в С#: извлечение всех URL-адресов

Мне нужно получить все URL-адреса (выражения url()) из файлов CSS. Например:

b { background: url(img0) }
b { background: url("img1") }
b { background: url('img2') }
b { background: url( img3 ) }
b { background: url( "img4" ) }
b { background: url( 'img5' ) }
b { background: url (img6) }
b { background: url ("img7") }
b { background: url ('img8') }
{ background: url('noimg0) }
{ background: url(noimg1') }
/*b { background: url(noimg2) }*/
b { color: url(noimg3) }
b { content: 'url(noimg4)' }
@media screen and (max-width: 1280px) { b { background: url(img9) } }
b { background: url(img10) }

Мне нужно получить все URL img*, но не noimg* URL (недопустимый синтаксис или недопустимое свойство или внутренние комментарии).

Я пробовал использовать старые добрые регулярные выражения. После некоторых проб и ошибок я получил следующее:

private static IEnumerable<string> ParseUrlsRegex (string source)
{
    var reUrls = new Regex(@"(?nx)
        url \s* \( \s*
            (
                (?! ['""] )
                (?<Url> [^\)]+ )
                (?<! ['""] )
                |
                (?<Quote> ['""] )
                (?<Url> .+? )
                \k<Quote>
            )
        \s* \)");
    return reUrls.Matches(source)
        .Cast<Match>()
        .Select(match => match.Groups["Url"].Value);
}

Это одно сумасшедшее регулярное выражение, но оно все равно не работает - оно соответствует 3 недействительным URL (а именно 2, 3 и 4). Кроме того, каждый скажет, что использование регулярного выражения для разбора сложной грамматики неверно.

Попробуем другой подход. Согласно этому вопросу, единственным жизнеспособным вариантом является ExCSS (другие либо слишком простой, либо устаревшей). С ExCSS я получил следующее:

    private static IEnumerable<string> ParseUrlsExCss (string source)
    {
        var parser = new StylesheetParser();
        parser.Parse(source);
        return parser.Stylesheet.RuleSets
            .SelectMany(i => i.Declarations)
            .SelectMany(i => i.Expression.Terms)
            .Where(i => i.Type == TermType.Url)
            .Select(i => i.Value);
    }

В отличие от решения regex, в этом списке не указаны недопустимые URL-адреса. Но он не перечисляет некоторые действительные! А именно, 9 и 10. Похоже, что это известная проблема с некоторым синтаксисом CSS, и она не может быть исправлена ​​без перезаписи всей библиотеки из царапина. ANTLR переписывается как отказался.

Вопрос. Как извлечь все URL-адреса из файлов CSS? (Мне нужно разобрать любые файлы CSS, а не только те, которые приведены в качестве примера выше. Пожалуйста, не обращайте внимание на "noimg" или принимайте однострочные объявления.)

N.B. Это не вопрос "рекомендации по инструменту", так как любое решение будет в порядке, будь то фрагмент кода, исправление для одного из вышеупомянутых решений, библиотека или что-то еще; и я четко определил нужную мне функцию.

Ответ 1

Наконец, получил Alba.CsCss, мой порт парсера CSS из Mozilla Firefox, работающий.

Прежде всего, вопрос содержит две ошибки:

  • Синтаксис
  • url (img) неверен, поскольку в CSS-грамматике пробел не допускается между url и (. Поэтому "img6", "img7" и "img8" не должны возвращаться как URL-адреса.

  • Незакрытая цитата в функции url (url('img)) является серьезной синтаксической ошибкой; веб-браузеры, включая Firefox, похоже, не восстанавливаются и просто пропускают остальную часть файла CSS. Поэтому, требуя, чтобы синтаксический анализатор возвращал "img9" и "img10", не нужен (но необходим, если две проблемные строки удалены).

С CsCss существует два решения.

Решение first относится к полагаться только на токенизатор CssScanner.

List<string> uris = new CssLoader().GetUris(source).ToList();

Это вернет все URL-адреса "img" (кроме указанных в ошибке № 1 выше), но также будет содержать "noimg3", поскольку имена свойств не отмечены.

Решение второе должно правильно проанализировать файл CSS. Это наиболее точно отражает поведение браузеров (включая остановку синтаксического анализа после незакрытой цитаты).

var css = new CssLoader().ParseSheet(source, SheetUri, BaseUri);
List<string> uris = css.AllStyleRules
    .SelectMany(styleRule => styleRule.Declaration.AllData)
    .SelectMany(prop => prop.Value.Unit == CssUnit.List
        ? prop.Value.List : new[] { prop.Value })
    .Where(value => value.Unit == CssUnit.Url)
    .Select(value => value.OriginalUri)
    .ToList();

Если две проблемные строки удалены, это вернет все правильные URL-адреса "img".

(Запрос LINQ является сложным, поскольку свойство background-image в CSS3 может содержать список URL-адресов.)

Ответ 2

RegEx - очень мощный инструмент. Но когда требуется немного больше гибкости, я предпочитаю просто написать небольшой код.

Итак, для решения, отличного от RegEx, я придумал следующее. Обратите внимание, что потребуется немного больше работы, чтобы сделать этот код более универсальным для обработки любого файла CSS. Для этого я также использовал бы класс для синтаксического разбора текста.

IEnumerable<string> GetUrls(string css)
{
    char[] trimChars = new char[] { '\'', '"', ' ', '\t', };

    foreach (var line in css.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
    {
        // Extract portion within curly braces (this version assumes all on one line)
        int start = line.IndexOf('{');
        int end = line.IndexOf('}', start + 1);
        if (start < 0 || end < 0)
            continue;
        start++; end--; // Remove braces

        // Get value portion
        start = line.IndexOf(':', start);
        if (start < 0)
            continue;

        // Extract value and trime whitespace and quotes
        string content = line.Substring(start + 1, end - start).Trim(trimChars);

        // Extract URL from url() value
        if (!content.StartsWith("url", StringComparison.InvariantCultureIgnoreCase))
            continue;
        start = content.IndexOf('(');
        end = content.IndexOf(')', start + 1);
        if (start < 0 || end < 0)
            continue;
        start++;
        content = content.Substring(start, end - start).Trim(trimChars);

        if (!content.StartsWith("noimg", StringComparison.InvariantCultureIgnoreCase))
            yield return content;
    }
}

UPDATE:

То, что вы, кажется, задаете, выходит за рамки простого практического вопроса для stackoverflow. Я не верю, что вы получите удовлетворительные результаты, используя регулярные выражения. Вам понадобится некоторый код для анализа вашего CSS и обработки всех особых случаев, которые приходят с ним.

Поскольку я написал много кода синтаксического анализа и немного времени, я решил немного поиграть с этим. Я написал простой синтаксический анализатор CSS и написал статью об этом. Вы можете прочитать статью и скачать код (бесплатно) в Простой CSS Parser.

Мой код анализирует блок CSS и сохраняет информацию в структурах данных. Мой код разделяет и сохраняет каждую пару свойств/значений для каждого правила. Однако для получения URL-адреса из значений свойств требуется еще немного работы. Вам нужно будет проанализировать их из значения свойства.

Код, который я изначально опубликовал, даст вам представление о том, как вы можете это сделать. Но если вы хотите действительно надежное решение, вам понадобится еще более сложный код. Вы можете взглянуть на мой код, чтобы проанализировать CSS. Я использую методы в этом коде, которые можно использовать для упрощения дескрипторов, таких как url('img(1)'), например, для разбора цитируемого значения.

Я думаю, что это довольно хорошее начало. Я мог бы написать оставшийся код для вас. Но какое удовольствие в этом.:)

Ответ 3

По-моему, вы создали слишком сложный RegExp. Рабочий выглядит следующим образом: url\s*[(][\s'""]*(?<Url>img[\w]*)[\s'""]*[)]. Я попытаюсь объяснить, что я ищу:

  • Начните с url
  • Затем все пробелы после него (\s*)
  • Далее находится ровно одна левая скобка ([(])
  • 0 или более символов: whitespace, ", '([\s'""]*)
  • Затем "URL", поэтому что-то начинающееся с img и заканчивающееся нулем или более буквенно-числовыми символами ((?<Url>img[\w]*))
  • Опять 0 или больше символов типа: пробел, ", '([\s'""]*)
  • И оканчиваемся правой скобкой [)]

Полный рабочий код:

        var source =
            "b { background: url(img0) }\n" +
            "b { background: url(\"img1\") }\n" +
            "b { background: url(\'img2\') }\n" +
            "b { background: url( img3 ) }\n" +
            "b { background: url( \"img4\" ) }\n" +
            "b { background: url( \'img5\' ) }\n" +
            "b { background: url (img6) }\n" +
            "b { background: url (\"img7\") }\n" +
            "b { background: url (\'img8\') }\n" +
            "{ background: url(\'noimg0) }\n" +
            "{ background: url(noimg1\') }\n" +
            "/*b { background: url(noimg2) }*/\n" +
            "b { color: url(noimg3) }\n" +
            "b { content: \'url(noimg4)\' }\n" +
            "@media screen and (max-width: 1280px) { b { background: url(img9) } }\n" +
            "b { background: url(img10) }";


        string strRegex = @"url\s*[(][\s'""]*(?<Url>img[\w]*)[\s'""]*[)]";
        var reUrls = new Regex(strRegex);

        var result = reUrls.Matches(source)
                           .Cast<Match>()
                           .Select(match => match.Groups["Url"].Value).ToArray();
        bool isOk = true;
        for (var i = 0; i <= 10; i++)
        {
            if (!result.Contains("img" + i))
            {
                Console.WriteLine("Missing img"+i);
                isOk = false;
            }
        }
        for (var i = 0; i <= 4; i++)
        {
            if (result.Contains("noimg" + i))
            {
                Console.WriteLine("Redundant noimg" + i);
                isOk = false;
            }
        }
        if (isOk)
        {
            Console.WriteLine("Yes. It is ok :). The result is:");
            foreach (var s in result)
            {
                Console.WriteLine(s);
            }

        }
        Console.ReadLine();

Ответ 5

Вероятно, это не самое элегантное возможное решение, но, похоже, выполняет работу, которую вам нужно выполнить.

public static List<string> GetValidUrlsFromCSS(string cssStr)
{
    //Enter properties that can validly contain a URL here (in lowercase):
    List<string> validProperties = new List<string>(new string[] { "background", "background-image" });

    List<string> validUrls = new List<string>();
    //We'll use your regex for extracting the valid URLs
    var reUrls = new Regex(@"(?nx)
        url \s* \( \s*
            (
                (?! ['""] )
                (?<Url> [^\)]+ )
                (?<! ['""] )
                |
                (?<Quote> ['""] )
                (?<Url> .+? )
                \k<Quote>
            )
        \s* \)");
    //First, remove all the comments
    cssStr = Regex.Replace(cssStr, "\\/\\*.*?\\*\\/", String.Empty);
    //Next remove all the the property groups with no selector
    string oldStr;
    do
    {
        oldStr = cssStr;
        cssStr = Regex.Replace(cssStr, "(^|{|})(\\s*{[^}]*})", "$1");
    } while (cssStr != oldStr);
    //Get properties
    var matches = Regex.Matches(cssStr, "({|;)([^:{;]+:[^;}]+)(;|})");
    foreach (Match match in matches)
    {
        string matchVal = match.Groups[2].Value;
        string[] matchArr = matchVal.Split(':');
        if (validProperties.Contains(matchArr[0].Trim().ToLower()))
        {
            //Since this is a valid property, extract the URL (if there is one)
            MatchCollection validUrlCollection = reUrls.Matches(matchVal);
            if (validUrlCollection.Count > 0)
            {
                validUrls.Add(validUrlCollection[0].Groups["Url"].Value);
            }
        }
    }
    return validUrls;
}

Ответ 6

Вам нужен отрицательный lookbehind, чтобы увидеть, нет ли /* без следующего */, как это:

(?<!\/\*([^*]|\*[^\/])*)

Это кажется нечитаемым, это означает:

(?<! → перед этим совпадением может не быть:

\/\* → /* (с косой чертой), а затем

([^*] → любой символ, который не является *

|\*[^\/]) → или символ *, но за ним следует все, что не является /

*) → этого символа not a * or a * without a / мы можем иметь 0 или более и, наконец, закрыть отрицательный lookbehind

И вам нужен положительный lookbehind, чтобы узнать, является ли заданное свойство свойством css, которое принимает значения url(). Если вас интересуют только background: и background-image:, это будет все регулярное выражение:

(?<!\/\*([^*]|\*[^\/])*)
(?<=background(?:-image)?:\s*)
url\s*\(\s*(('|")?)[^\n'"]+\1\s*\)

Так как эта версия требует, чтобы свойство css background: или background-image: предшествовало url(), оно не обнаруживает 'url(noimg4)'. Вы можете использовать простые каналы для добавления более приемлемых свойств css: (?<=(?:border-image|background(?:-image)?):\s*)

Я использовал \1, а не \k<Quote>, потому что я не знаком с этим синтаксисом, а это значит, что вам нужно?: не захватывать нежелательные подгруппы. Насколько я могу проверить это работает.

Наконец, я использовал [^\n'"] для фактического URL-адреса, потому что я понимаю из ваших комментариев, что url ('img (1)') должен работать, а [^\)] из вашего OP не будет анализировать это.

Ответ 7

Это решение может избежать комментариев и имеет дело с background-image. Он также относится к background, который может содержать такие свойства, как background-color, background-position или repeat, что не относится к background-image. Вот почему я добавил эти случаи: noimg5, img11, img12.

Данные:

string subject =
    @"b { background: url(img0) }
      b { background: url(""img1"") }
      b { background: url('img2') }
      b { background: url( img3 ) }
      b { background: url( ""img4"" ) }
      b { background: url( 'img5' ) }
      b { background: url (img6) }
      b { background: url (""img7"") }
      b { background: url ('img8') }
      { background: url('noimg0) }
      { background: url(noimg1') }
      /*b { background: url(noimg2) }*/
      b { color: url(noimg3) }
      b { content: 'url(noimg4)' }
      @media screen and (max-width: 1280px) { b { background: url(img9) } }
      b { background: url(img10) }
      b { background: #FFCC66 url('img11') no-repeat }
      b { background-image: url('img12'); }
      b { background-image: #FFCC66 url('noimg5') }";

Образец:

Комментарии избегают, потому что они совпадают в первую очередь. Если комментарий остается открытым (без */, то все содержимое после считается комментарием (?>\*/|$).

Результат сохраняется в названной записи url.

string pattern = @"
        /\*  (?> [^*] | \*(?!/) )*  (?>\*/|$)  # comments
      |
        (?<=
            background
            (?>
                -image \s* :     # optional '-image'
              |
                \s* :
                (?>              # allowed content before url 
                    \s*
                    [^;{}u\s]+   # all that is not a ; { } u
                    \s           # must be followed by one space at least
                )?
            )

            \s* url \s* \( \s*
            ([""']?)             # optional quote (single or double) in group 1
        )
        (?<url> [^""')\s]+ )     # named capture 'url' with an url inside
        (?=\1\s*\))              # must be followed by group 1 content (optional quote)
              ";
RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace;
Match m = Regex.Match(subject, pattern, options);
List<string> urls = new List<string>();
while (m.Success)
{
    string url = m.Groups["url"].ToString();
    if (url!="") {
        urls.Add(url);
        Console.WriteLine(url);
    }
    m = m.NextMatch();
}

Ответ 8

Для такой проблемы более простой подход может сделать трюк.

  • Перерыв всех команд css в строках (предположим, что css упрощен), в этом случае я сломался бы в ";" или "}".

  • Прочитайте все вхождения внутри url (*), даже неправильные.

  • Создайте конвейер с шаблоном команды, который определяет, какие строки действительно имеют право

    • 3.1 Command1 (Detect comment)
    • 3.2 Command2 (определить URL-адрес синтаксической ошибки)
    • 3.3...
  • При отмеченных линиях OK извлеките OK Url

Это простой подход и решает проблему с эффективностью и без ультра сложного неуправляемого магического Regex.

Ответ 9

Этот RegEx, кажется, разрешает приведенный пример:

background: url\s*\(\s*(["'])?\K\w+(?(1)(?=\1)|(?=\s*\)))(?!.*\*/)