71

I'm trying to extract the attributes of a anchor tag (<a>). So far I have this expression:

(?<name>\b\w+\b)\s*=\s*("(?<value>[^"]*)"|'(?<value>[^']*)'|(?<value>[^"'<> \s]+)\s*)+

which works for strings like

<a href="test.html" class="xyz">

and (single quotes)

<a href='test.html' class="xyz">

but not for a string without quotes:

<a href=test.html class=xyz>

How can I modify my regex making it work with attributes without quotes? Or is there a better way to do that?

Update: Thanks for all the good comments and advice so far. There is one thing I didn't mention: I sadly have to patch/modify code not written by me. And there is no time/money to rewrite this stuff from the bottom up.

splattne
  • 102,760
  • 52
  • 202
  • 249
  • For C#, I went with [AngleSharp](https://github.com/AngleSharp/AngleSharp) and its `HtmlParser` class. – Uwe Keim Jul 25 '19 at 12:04

20 Answers20

117

Update 2021: Radon8472 proposes in the comments the regex https://regex101.com/r/tOF6eA/1 (note regex101.com did not exist when I wrote originally this answer)

<a[^>]*?href=(["\'])?((?:.(?!\1|>))*.?)\1?

Update 2021 bis: Dave proposes in the comments, to take into account an attribute value containing an equal sign, like <img src="test.png?test=val" />, as in this regex101:

(\w+)=["']?((?:.(?!["']?\s+(?:\S+)=|\s*\/?[>"']))+.)["']?

Update (2020), Gyum Fox proposes https://regex101.com/r/U9Yqqg/2 (again, note regex101.com did not exist when I wrote originally this answer)

(\S+)=["']?((?:.(?!["']?\s+(?:\S+)=|\s*\/?[>"']))+.)["']?

Applied to:

<a href=test.html class=xyz>
<a href="test.html" class="xyz">
<a href='test.html' class="xyz">
<script type="text/javascript" defer async id="something" onload="alert('hello');"></script>
<img src="test.png">
<img src="a test.png">
<img src=test.png />
<img src=a test.png />
<img src=test.png >
<img src=a test.png >
<img src=test.png alt=crap >
<img src=a test.png alt=crap >

Original answer (2008): If you have an element like

<name attribute=value attribute="value" attribute='value'>

this regex could be used to find successively each attribute name and value

(\S+)=["']?((?:.(?!["']?\s+(?:\S+)=|[>"']))+.)["']?

Applied on:

<a href=test.html class=xyz>
<a href="test.html" class="xyz">
<a href='test.html' class="xyz">

it would yield:

'href' => 'test.html'
'class' => 'xyz'

Note: This does not work with numeric attribute values e.g. <div id="1"> won't work.

Edited: Improved regex for getting attributes with no value and values with " ' " inside.

([^\r\n\t\f\v= '"]+)(?:=(["'])?((?:.(?!\2?\s+(?:\S+)=|\2))+.)\2?)?

Applied on:

<script type="text/javascript" defer async id="something" onload="alert('hello');"></script>

it would yield:

'type' => 'text/javascript'
'defer' => ''
'async' => ''
'id' => 'something'
'onload' => 'alert(\'hello\');'
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • 1
    What about “foo="bar' bar='bla"”? – Gumbo Feb 22 '09 at 13:52
  • 1
    @Gumbo: this regexp should take into account single or double quotes, since it used character class ['"] – VonC Feb 22 '09 at 14:52
  • 1
    It could not, of course manage quotes within an attribute value – VonC Feb 22 '09 at 14:54
  • 2
    I know. But the value of foo would be “bar' bar='bla” and not just “bar”. – Gumbo Feb 22 '09 at 15:04
  • Von, this partially works, but when there is a mix of unquoted and quoted in 1 tag, it doesnt work anymore. See my answer for another version of regex – fedmich Nov 29 '12 at 03:46
  • It would be great if it could catch empty attributes such as "selected" and "checked" – Andrea Silvestri Jun 26 '14 at 07:43
  • 1
    Also I found out that, with a singular character as value, the regexp will return the quotes as well. – Andrea Silvestri Jun 26 '14 at 08:28
  • @AndreaSilvestri can you edit this answer with a fixed regexp which would avoid that? – VonC Jun 26 '14 at 08:29
  • 2
    @PauloCosta you can see the regex at https://regex101.com/r/bY3kM1/1: it will unpack the regex for you. – VonC Aug 17 '15 at 14:54
  • @VonC this appears to fail when you have an attribute like data-test="1" the value selected is `"1` instead of just `1` – Jed Grant Oct 30 '15 at 21:08
  • @SlavikMeltser Yes, I highlighted a more robust solution below 4 years ago: http://stackoverflow.com/a/13618472/6309 – VonC Oct 02 '16 at 06:15
  • I added a solution that allows unquoted / quoted, single / double quotes, escaped quotes inside attributes, spaces around equals signs, different number of attributes, check only for attributes inside tags, and manage different quotes within an attribute value: http://stackoverflow.com/a/38305337/1204332 – Ivan Chaer Oct 07 '16 at 13:57
  • Yeah VonC, but it was just recently that @SlavikMeltser shed a light on lookbehinds that allowed me to improve my answer. Thanks! :) – Ivan Chaer Oct 07 '16 at 15:11
  • @user1932634 That certainly won't be matched by my 9 years-old regexp... – VonC Jan 23 '18 at 21:08
  • There's an issue with this sample: ``. This yields **src** => **test.png />** with the second regex, and **src** => **test.png /** with the first one – Gyum Fox Aug 20 '20 at 08:14
  • @GyumFox Indeed. I have updated the regex in https://regex101.com/r/k5qzMI/2, but it does not work for values which are not wrapped by quotes (single or double) – VonC Aug 20 '20 at 08:42
  • I have better result modifying your first regex (altough I only tested the cases I was interested in...): https://regex101.com/r/U9Yqqg/1/ – Gyum Fox Aug 20 '20 at 08:56
  • @GyumFox Thank you. I have included your comment in the answer for more visibility. As illustrated by https://regex101.com/r/U9Yqqg/2, it does not cover *all* cases, but it is close. – VonC Aug 20 '20 at 12:00
  • how can I include the html-tag to e.g. filter all hrefs of `a`-tags ? – Radon8472 May 07 '21 at 14:35
  • @Radon8472 Can you show me an example through https://regex101.com/, where the regex mentioned in the answer fails to include what you want? – VonC May 07 '21 at 15:28
  • @VonC I thing that I already found own solution: https://regex101.com/r/tOF6eA/1 Feel free to improve it, if you have suggestions – Radon8472 Jul 29 '21 at 12:41
  • 1
    @Radon8472 Interesting. I have included your comment in the answer for more visibility. – VonC Jul 29 '21 at 12:46
  • This regex won't work if an attribute value contains an equal sign. For example: ``. – Dave Sep 10 '21 at 12:08
  • @Dave The second regex from my answer, the one from 2020, seems to work, no? I have included your test case in https://regex101.com/r/991GfD/1. – VonC Sep 10 '21 at 13:39
  • @VonC your new test case does not contain an equal sign in the query string. Try `` instead of ``. I fixed it in my project by changing the first `\S` to `\w`. – Dave Sep 10 '21 at 16:37
  • Yep! With that change, Group 1 and 2 have the proper name and value for the match. – Dave Sep 10 '21 at 21:04
  • 1
    @Dave Perfect: I have included your comment/regex in the answer for more visibility. – VonC Sep 11 '21 at 01:49
22

Although the advice not to parse HTML via regexp is valid, here's a expression that does pretty much what you asked:

/
   \G                     # start where the last match left off
   (?>                    # begin non-backtracking expression
       .*?                # *anything* until...
       <[Aa]\b            # an anchor tag
    )??                   # but look ahead to see that the rest of the expression
                          #    does not match.
    \s+                   # at least one space
    ( \p{Alpha}           # Our first capture, starting with one alpha
      \p{Alnum}*          # followed by any number of alphanumeric characters
    )                     # end capture #1
    (?: \s* = \s*         # a group starting with a '=', possibly surrounded by spaces.
        (?: (['"])        # capture a single quote character
            (.*?)         # anything else
             \2           # which ever quote character we captured before
        |   ( [^>\s'"]+ ) # any number of non-( '>', space, quote ) chars
        )                 # end group
     )?                   # attribute value was optional
/msx;

"But wait," you might say. "What about *comments?!?!" Okay, then you can replace the . in the non-backtracking section with: (It also handles CDATA sections.)

(?:[^<]|<[^!]|<![^-\[]|<!\[(?!CDATA)|<!\[CDATA\[.*?\]\]>|<!--(?:[^-]|-[^-])*-->)
  • Also if you wanted to run a substitution under Perl 5.10 (and I think PCRE), you can put \K right before the attribute name and not have to worry about capturing all the stuff you want to skip over.
Axeman
  • 29,660
  • 2
  • 47
  • 102
  • The non-quoted value, "( [^>\s'"]+ )", will fail for including the '/' in the value. It probably should be something like (untested) "(.*?)(?:\s|>|/>|'|") # chars up to first space, >, />, quote" – mheyman Aug 04 '12 at 17:11
  • Just what I was looking for - I needed to remove a bunch of attributes (same attribute, different values" and attribute=".*?" did the trick in my case ... a very handy reference, thanks @mheyman – Jayx Mar 04 '15 at 13:46
13

Token Mantra response: you should not tweak/modify/harvest/or otherwise produce html/xml using regular expression.

there are too may corner case conditionals such as \' and \" which must be accounted for. You are much better off using a proper DOM Parser, XML Parser, or one of the many other dozens of tried and tested tools for this job instead of inventing your own.

I don't really care which one you use, as long as its recognized, tested, and you use one.

my $foo  = Someclass->parse( $xmlstring ); 
my @links = $foo->getChildrenByTagName("a"); 
my @srcs = map { $_->getAttribute("src") } @links; 
# @srcs now contains an array of src attributes extracted from the page. 
Kent Fredric
  • 56,416
  • 14
  • 107
  • 150
  • "corner case conditionals such as \' and \" which must be accounted for" ... you can't escape quotes in a HTML attribute. The only way to include them is to encode them as an entity, " – nickf Nov 25 '08 at 11:55
  • 3
    Yes, the specification of HTML states you should entity encode them, but however, due to people *using* backslashing browsers adapt to make it work, and more people use it,thus, your parser must be able to handle it when they do :) – Kent Fredric Jan 17 '09 at 02:28
  • This is not true as of 2020 in Chrome 79; backslash-quote is *not* recognized as an escaped quote, rather, the slash gets dropped and the quote is either recognized as a value delimiter or as part of the value, depending on its position/surroundings. – John Frazer Feb 20 '20 at 06:30
12

You cannot use the same name for multiple captures. Thus you cannot use a quantifier on expressions with named captures.

So either don’t use named captures:

(?:(\b\w+\b)\s*=\s*("[^"]*"|'[^']*'|[^"'<>\s]+)\s+)+

Or don’t use the quantifier on this expression:

(?<name>\b\w+\b)\s*=\s*(?<value>"[^"]*"|'[^']*'|[^"'<>\s]+)

This does also allow attribute values like bar=' baz='quux:

foo="bar=' baz='quux"

Well the drawback will be that you have to strip the leading and trailing quotes afterwards.

Gumbo
  • 643,351
  • 109
  • 780
  • 844
11

Just to agree with everyone else: don't parse HTML using regexp.

It isn't possible to create an expression that will pick out attributes for even a correct piece of HTML, never mind all the possible malformed variants. Your regexp is already pretty much unreadable even without trying to cope with the invalid lack of quotes; chase further into the horror of real-world HTML and you will drive yourself crazy with an unmaintainable blob of unreliable expressions.

There are existing libraries to either read broken HTML, or correct it into valid XHTML which you can then easily devour with an XML parser. Use them.

bobince
  • 528,062
  • 107
  • 651
  • 834
7

PHP (PCRE) and Python

Simple attribute extraction (See it working):

((?:(?!\s|=).)*)\s*?=\s*?["']?((?:(?<=")(?:(?<=\\)"|[^"])*|(?<=')(?:(?<=\\)'|[^'])*)|(?:(?!"|')(?:(?!\/>|>|\s).)+))

Or with tag opening / closure verification, tag name retrieval and comment escaping. This expression foresees unquoted / quoted, single / double quotes, escaped quotes inside attributes, spaces around equals signs, different number of attributes, check only for attributes inside tags, and manage different quotes within an attribute value. (See it working):

(?:\<\!\-\-(?:(?!\-\-\>)\r\n?|\n|.)*?-\-\>)|(?:<(\S+)\s+(?=.*>)|(?<=[=\s])\G)(?:((?:(?!\s|=).)*)\s*?=\s*?[\"']?((?:(?<=\")(?:(?<=\\)\"|[^\"])*|(?<=')(?:(?<=\\)'|[^'])*)|(?:(?!\"|')(?:(?!\/>|>|\s).)+))[\"']?\s*)

(Works better with the "gisx" flags.)


Javascript

As Javascript regular expressions don't support look-behinds, it won't support most features of the previous expressions I propose. But in case it might fit someone's needs, you could try this version. (See it working).

(\S+)=[\'"]?((?:(?!\/>|>|"|\'|\s).)+)
Ivan Chaer
  • 6,980
  • 1
  • 38
  • 48
  • You're RegEx extraction is not accurate. See updated example from a fork of your test case: https://regex101.com/r/y3DOf5/1 – Slavik Meltser Oct 02 '16 at 06:06
  • Yeah, no way to do a conditional search based on checking the occurrence of a previous character (single / double quotes in this case). Not with a single regex. That's why a parser is the way to go. This regex is only approximative, unfortunately. – Ivan Chaer Oct 03 '16 at 13:56
  • 1
    actually it is possible to create a conditional search only with RegEx using lookahead and lookbehind groups. I will post a correct answer that does this soon, when I find more time. – Slavik Meltser Oct 05 '16 at 06:55
  • Thanks for the hints, @SlavikMeltser! I updated my answer with some lookbehinds. – Ivan Chaer Oct 06 '16 at 14:52
  • Sadly this does not work with javascript since it doesn't support lookbehinds. – choise Dec 15 '16 at 17:38
  • 1
    @choise True! I added a simplified expression for JS. – Ivan Chaer Dec 27 '16 at 10:47
7

This is my best RegEx to extract properties in HTML Tag:

# Trim the match inside of the quotes (single or double)

(\S+)\s*=\s*([']|["])\s*([\W\w]*?)\s*\2

# Without trim

(\S+)\s*=\s*([']|["])([\W\w]*?)\2

Pros:

  • You are able to trim the content inside of quotes.
  • Match all the special ASCII characters inside of the quotes.
  • If you have title="You're mine" the RegEx does not broken

Cons:

  • It returns 3 groups; first the property then the quote ("|') and at the end the property inside of the quotes i.e.: <div title="You're"> the result is Group 1: title, Group 2: ", Group 3: You're.

This is the online RegEx example: https://regex101.com/r/aVz4uG/13



I normally use this RegEx to extract the HTML Tags:

I recommend this if you don't use a tag type like <div, <span, etc.

<[^/]+?(?:\".*?\"|'.*?'|.*?)*?>

For example:

<div title="a>b=c<d" data-type='a>b=c<d'>Hello</div>
<span style="color: >=<red">Nothing</span>
# Returns 
# <div title="a>b=c<d" data-type='a>b=c<d'>
# <span style="color: >=<red">

This is the online RegEx example: https://regex101.com/r/aVz4uG/15

The bug in this RegEx is:

<div[^/]+?(?:\".*?\"|'.*?'|.*?)*?>

In this tag:

<article title="a>b=c<d" data-type='a>b=c<div '>Hello</article>

Returns <div '> but it should not return any match:

Match:  <div '>

To "solve" this remove the [^/]+? pattern:

<div(?:\".*?\"|'.*?'|.*?)*?>


The answer #317081 is good but it not match properly with these cases:

<div id="a"> # It returns "a instead of a
<div style=""> # It doesn't match instead of return only an empty property
<div title = "c"> # It not recognize the space between the equal (=)

This is the improvement:

(\S+)\s*=\s*["']?((?:.(?!["']?\s+(?:\S+)=|[>"']))?[^"']*)["']?

vs

(\S+)=["']?((?:.(?!["']?\s+(?:\S+)=|[>"']))+.)["']?

Avoid the spaces between equal signal: (\S+)\s*=\s*((?:...

Change the last + and . for: |[>"']))?[^"']*)["']?

This is the online RegEx example: https://regex101.com/r/aVz4uG/8

5

Tags and attributes in HTML have the form

<tag 
   attrnovalue 
   attrnoquote=bli 
   attrdoublequote="blah 'blah'"
   attrsinglequote='bloob "bloob"' >

To match attributes, you need a regex attr that finds one of the four forms. Then you need to make sure that only matches are reported within HTML tags. Assuming you have the correct regex, the total regex would be:

attr(?=(attr)*\s*/?\s*>)

The lookahead ensures that only other attributes and the closing tag follow the attribute. I use the following regular expression for attr:

\s+(\w+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^><"'\s]+)))?

Unimportant groups are made non capturing. The first matching group $1 gives you the name of the attribute, the value is one of $2or $3 or $4. I use $2$3$4 to extract the value. The final regex is

\s+(\w+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^><"'\s]+)))?(?=(?:\s+\w+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^><"'\s]+))?)*\s*/?\s*>)

Note: I removed all unnecessary groups in the lookahead and made all remaining groups non capturing.

4

splattne,

@VonC solution partly works but there is some issue if the tag had a mixed of unquoted and quoted

This one works with mixed attributes

$pat_attributes = "(\S+)=(\"|'| |)(.*)(\"|'| |>)"

to test it out

<?php
$pat_attributes = "(\S+)=(\"|'| |)(.*)(\"|'| |>)"

$code = '    <IMG title=09.jpg alt=09.jpg src="http://example.com.jpg?v=185579" border=0 mce_src="example.com.jpg?v=185579"
    ';

preg_match_all( "@$pat_attributes@isU", $code, $ms);
var_dump( $ms );

$code = '
<a href=test.html class=xyz>
<a href="test.html" class="xyz">
<a href=\'test.html\' class="xyz">
<img src="http://"/>      ';

preg_match_all( "@$pat_attributes@isU", $code, $ms);

var_dump( $ms );

$ms would then contain keys and values on the 2nd and 3rd element.

$keys = $ms[1];
$values = $ms[2];
fedmich
  • 5,343
  • 3
  • 37
  • 52
3

I suggest that you use HTML Tidy to convert the HTML to XHTML, and then use a suitable XPath expression to extract the attributes.

activout.se
  • 6,058
  • 4
  • 27
  • 37
3

something like this might be helpful

'(\S+)\s*?=\s*([\'"])(.*?|)\2
user273314
  • 31
  • 1
2

If you want to be general, you have to look at the precise specification of the a tag, like here. But even with that, if you do your perfect regexp, what if you have malformed html?

I would suggest to go for a library to parse html, depending on the language you work with: e.g. like python's Beautiful Soup.

Piotr Lesnicki
  • 9,442
  • 2
  • 28
  • 26
2

If youre in .NET I recommend the HTML agility pack, very robust even with malformed HTML.

Then you can use XPath.

splattne
  • 102,760
  • 52
  • 202
  • 249
Andrew Bullock
  • 36,616
  • 34
  • 155
  • 231
1

I'd reconsider the strategy to use only a single regular expression. Sure it's a nice game to come up with one single regular expression that does it all. But in terms of maintainabilty you are about to shoot yourself in both feet.

innaM
  • 47,505
  • 4
  • 67
  • 87
1

My adaptation to also get the boolean attributes and empty attributes like:

<input autofocus='' disabled />

/(\w+)=["']((?:.(?!["']\s+(?:\S+)=|\s*\/[>"']))+.)["']|(\w+)=["']["']|(\w+)/g

KJ. Estevez
  • 19
  • 1
  • 4
0

I also needed this and wrote a function for parsing attributes, you can get it from here:

https://gist.github.com/4153580

(Note: It doesn't use regex)

Furkan Mustafa
  • 794
  • 7
  • 12
  • Hey Furkan, regex solution might be best for this situation as its faster :) see my answer as well – fedmich Nov 29 '12 at 03:47
  • I also think regex should be better, but didn't want to deal with details with regex, like an attribute like this value="Tester's Device", that single qoute is going to confuse stuff with simple regex patterns, or even sometimes there's no quotes around values. I've done it in a fool-proof way. If it was in C, it would be faster than regex too but I cannot say the same thing for php. – Furkan Mustafa Feb 03 '13 at 13:01
0

I have created a PHP function that could extract attributes of any HTML tags. It also can handle attributes like disabled that has no value, and also can determine whether the tag is a stand-alone tag (has no closing tag) or not (has a closing tag) by checking the content result:

/*! Based on <https://github.com/mecha-cms/cms/blob/master/system/kernel/converter.php> */
function extract_html_attributes($input) {
    if( ! preg_match('#^(<)([a-z0-9\-._:]+)((\s)+(.*?))?((>)([\s\S]*?)((<)\/\2(>))|(\s)*\/?(>))$#im', $input, $matches)) return false;
    $matches[5] = preg_replace('#(^|(\s)+)([a-z0-9\-]+)(=)(")(")#i', '$1$2$3$4$5<attr:value>$6', $matches[5]);
    $results = array(
        'element' => $matches[2],
        'attributes' => null,
        'content' => isset($matches[8]) && $matches[9] == '</' . $matches[2] . '>' ? $matches[8] : null
    );
    if(preg_match_all('#([a-z0-9\-]+)((=)(")(.*?)("))?(?:(\s)|$)#i', $matches[5], $attrs)) {
        $results['attributes'] = array();
        foreach($attrs[1] as $i => $attr) {
            $results['attributes'][$attr] = isset($attrs[5][$i]) && ! empty($attrs[5][$i]) ? ($attrs[5][$i] != '<attr:value>' ? $attrs[5][$i] : "") : $attr;
        }
    }
    return $results;
}

Test Code

$test = array(
    '<div class="foo" id="bar" data-test="1000">',
    '<div>',
    '<div class="foo" id="bar" data-test="1000">test content</div>',
    '<div>test content</div>',
    '<div>test content</span>',
    '<div>test content',
    '<div></div>',
    '<div class="foo" id="bar" data-test="1000"/>',
    '<div class="foo" id="bar" data-test="1000" />',
    '< div  class="foo"     id="bar"   data-test="1000"       />',
    '<div class id data-test>',
    '<id="foo" data-test="1000">',
    '<id data-test>',
    '<select name="foo" id="bar" empty-value-test="" selected disabled><option value="1">Option 1</option></select>'
);

foreach($test as $t) {
    var_dump($t, extract_html_attributes($t));
    echo '<hr>';
}
Taufik Nurrohman
  • 3,329
  • 24
  • 39
0

This works for me. It also take into consideration some end cases I have encountered.

I am using this Regex for XML parser

(?<=\s)[^><:\s]*=*(?=[>,\s])
Roei Sabag
  • 111
  • 1
  • 4
-1

Extract the element:

var buttonMatcherRegExp=/<a[\s\S]*?>[\s\S]*?<\/a>/;
htmlStr=string.match( buttonMatcherRegExp )[0]

Then use jQuery to parse and extract the bit you want:

$(htmlStr).attr('style') 
Tom Chiverton
  • 670
  • 6
  • 18
-1

have a look at this Regex & PHP - isolate src attribute from img tag

perhaps you can walk through the DOM and get the desired attributes. It works fine for me, getting attributes from the body-tag

Community
  • 1
  • 1
Stefan
  • 11
  • 5