在C#中解析CSS:提取所有URL

时间:2013-08-15 21:42:46

标签: c# css regex parsing url

我需要从CSS文件中获取所有URL(url()表达式)。例如:

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) }

我需要获取所有img*个网址,但不需要noimg*个网址(无效的语法或无效的属性或内部评论)。

我尝试过使用旧的正则表达式。经过一些试验和错误,我得到了这个:

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)。此外,每个人都会说使用正则表达式来解析复杂的语法错误

让我们尝试另一种方法。根据{{​​3}},唯一可行的选择是this question(其他选项太简单或过时)。有了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);
    }

与正则表达式解决方案不同,此解决方案不会列出无效的网址。但它没有列出一些有效的!即,9和10.看起来这是ExCSS,如果不从头开始重写整个库,就无法修复它。 ANTLR重写似乎是known issue with some CSS syntax

问题:如何从CSS文件中提取所有网址? (我需要解析任何 CSS文件,而不仅仅是上面提供的那个文件。请不要注意“noimg”或假设单行声明。)

N.B。这不是一个“工具推荐”问题,因为任何解决方案都可以,无论是一段代码,上述解决方案之一,图书馆还是其他任何解决方案;我已经明确定义了我需要的功能。

9 个答案:

答案 0 :(得分:6)

最后得到Alba.CsCss,我的Mozilla Firefox的CSS解析器端口,正在工作。

首先,该问题包含两个错误

  1. url (img)语法不正确,因为CSS语法中url(之间不允许有空格。因此,“img6”,“img7”和“img8”不应作为网址返回。

  2. url函数(url('img))中未公开的引用是一个严重的语法错误; Web浏览器(包括Firefox)似乎无法从中恢复,只是跳过CSS文件的其余部分。因此,要求解析器返回“img9”和“img10”是不必要的(但如果删除了两个有问题的行,则必须这样做。)

  3. 使用CsCss,有两种解决方案。

    第一个解决方案是rely just on the tokenizer CssScanner

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

    这将返回所有“img”URL(上面错误#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();
    

    如果删除了两个有问题的行,则会返回所有正确的“img”网址。

    (LINQ查询很复杂,因为CSS3中的background-image属性可以包含URL列表。)

答案 1 :(得分:5)

RegEx是一个非常强大的工具。但是当需要更多的灵活性时,我更愿意只编写一些代码。

因此,对于非RegEx解决方案,我想出了以下内容。请注意,需要更多的工作才能使此代码更通用以处理任何CSS文件。为此,我还会使用我的text parsing helper class

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;
    }
}

<强>更新

您似乎要问的内容似乎超出了stackoverflow的简单操作方法的范围。我不相信你会使用正则表达式得到满意的结果。您将需要一些代码来解析您的CSS,并处理随之而来的所有特殊情况。

由于我已经写了很多解析代码并且有一点时间,所以我决定稍微玩一下。我写了一个简单的CSS解析器并写了一篇关于它的文章。您可以在A Simple CSS Parser上阅读该文章并下载代码(免费)。

我的代码解析CSS块并将信息存储在数据结构中。我的代码分离并存储每个规则的每个属性/值对。但是,从属性值中获取URL仍需要更多工作。您需要从属性值中解析它们。

我最初发布的代码将为您提供如何处理此问题的开始。但如果您想要一个真正强大的解决方案,那么将需要一些更复杂的代码。您可能想看看我的代码来解析CSS。我在该代码中使用了可用于轻松处理url('img(1)')等值的技术,例如解析引用的值。

我认为这是一个非常好的开始。我也可以为你编写剩下的代码。但那有什么好玩的呢。 :)

答案 2 :(得分:2)

在我看来,你创建了太复杂的RegExp。 工作人员如下:url\s*[(][\s'""]*(?<Url>img[\w]*)[\s'""]*[)]。我将尝试解释我在搜索的内容:

  1. url
  2. 开始
  3. 然后是所有空格(\s*
  4. 接下来只是一个左括号([(]
  5. 0个或更多字符:whitespace,“,”([\s'""]*
  6. 接下来的“网址”,以img开头,以零个或多个字母数字字符结尾((?<Url>img[\w]*)
  7. 同样有0个或更多个字符:whitespace,“,”([\s'""]*
  8. 以右括号[)]
  9. 结束

    完整的工作代码:

            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();
    

答案 3 :(得分:2)

你可以尝试这样的模式,还有更多的帮助

@import ([""'])(?<url>[^""']+)\1|url\(([""']?)(?<url>[^""')]+)\2\)

或者

http://www.c-sharpcorner.com/uploadfile/rahul4_saxena/reading-and-parsing-a-css-file-in-Asp-Net/

答案 4 :(得分:1)

可能不是最优雅的解决方案,但似乎完成了你需要做的工作。

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;
}

答案 5 :(得分:1)

如果没有/*这样的*/,你需要负面的反馈,看看是否有(?<!\/\*([^*]|\*[^\/])*)

(?<!

这似乎不可读,这意味着:

\/\* - &gt;在此比赛之前可能不是:

([^*] - &gt; / *(使用转义斜杠)后跟

* - &gt;任何不是|\*[^\/])

的角色

* - &gt;或者 /的字符,但其后面跟着任何不是*)

的字符

not a * or a * without a / - &gt;这个url()字符中我们可以有0或更多,最后关闭负面的背后隐藏

您需要正面的lookbehind来查看正在设置的属性是否是接受background:值的css属性。如果您只对background-image:(?<!\/\*([^*]|\*[^\/])*) (?<=background(?:-image)?:\s*) url\s*\(\s*(('|")?)[^\n'"]+\1\s*\) 感兴趣,那么这将是整个正则表达式:

background:

由于此版本要求在url()之前使用css属性background-image:'url(noimg4)',因此它不会检测(?<=(?:border-image|background(?:-image)?):\s*)。您可以使用简单的管道添加更多可接受的css属性:\1

我使用\k<Quote>而不是[^\n'"],因为我不熟悉该语法,这意味着您需要?:不捕获不需要的子组。据我所知,这可行。

最后我使用了[^\)]作为实际网址,因为我从您的评论中了解到网址(&#39; img(1)&#39;)应该有效并且您的OP赢得了{{1}}&# 39;解析那个。

答案 6 :(得分:1)

此解决方案可以避免评论,并处理background-image。它也会处理background,其中可能包含background-colorbackground-positionrepeat等属性,而background-image则不然。这就是我添加这些案例的原因:noimg5img11img12

数据:

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();
}

答案 7 :(得分:1)

对于这样的问题,更简单的方法可以解决问题。

  1. 打破行中的所有css命令(简化了css),在这种情况下我会打破“;”或“}”命令。

  2. 读取url(*)中的所有内容,即使是错误的内容。

  3. 使用命令模式创建一个管道,以检测哪些行真正合格

    • 3.1 Command1(检测评论)
    • 3.2 Command2(检测语法错误URL)
    • 3.3 ......
  4. 标记好行后,提取确定网址

  5. 这是一个简单的方法,解决了效率问题,没有超复杂的无法管理的魔法正则表达式。

答案 8 :(得分:1)

RegEx似乎解决了提供的示例:

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