Showing posts with label PHP - Regular Expressions. Show all posts
Showing posts with label PHP - Regular Expressions. Show all posts
Thursday, November 29, 2012
PHP : Regular Expressions - [13.2] Switching From ereg to preg
13.2.1 Problem
13.2.2 Solution
First, you have to add delimiters to your patterns:
preg_match('/pattern/', 'string')
preg_match('/pattern/i', 'string');
When using integers instead of strings as patterns or
replacement values, convert the number to hexadecimal and specify it using an
escape sequence:
$hex = dechex($number);
preg_match("/\x$hex/", 'string');
13.2.3 Discussion
There are a few
major differences between ereg and preg. First, when you use
preg functions, the pattern
isn't just the string pattern; it also needs delimiters, as in Perl, so
it's /pattern/ instead.[1] So:
[1] Or {}, <>, ||, ##, or whatever your favorite delimiters are. PHP supports them all.
ereg('pattern', 'string');
becomes:
preg_match('/pattern/', 'string');
When choosing your
pattern delimiters, don't put your delimiter character inside the
regular-expression pattern, or you'll close the pattern early. If you can't find
a way to avoid this problem, you need to escape any instances of your delimiters
using the backslash. Instead of doing this by hand, call addcslashes(
).
For example, if you use / as your delimiter:
$ereg_pattern = '<b>.+</b>'; $preg_pattern = addcslashes($ereg_pattern, '/');
The value of $preg_pattern is now
<b>.+<\/b>.
The preg functions don't have a parallel series of
case-insensitive functions. They have a case-insensitive modifier instead. To
convert, change:
eregi('pattern', 'string');
to:
preg_match('/pattern/i', 'string');
Adding the i after the closing delimiter makes the
change.
Finally, there is one last obscure difference. If you use a
number (not a string) as a pattern or replacement value in ereg_replace(
) , it's assumed you are
referring to the ASCII value of a
character. Therefore, since 9 is the ASCII representation
of tab (i.e., \t), this code inserts tabs at the beginning of each
line:
$tab = 9;
$replaced = ereg_replace('^', $tab, $string);
$converted = ereg_replace(10, 12, $text);
To avoid this feature in ereg functions, use this
instead:
$tab = '9';
On the other hand, preg_replace( ) treats the number 9
as the number 9, not as a tab substitute. To convert these character codes for
use in preg_replace( ), convert them to hexadecimal and prefix them
with \x. For example, 9 becomes \x9 or \x09,
and 12 becomes \x0c. Alternatively, you can use \t , \r,
and \n for tabs, carriage returns, and linefeeds, respectively.
PHP : Regular Expressions - [13.3] Matching Words
13.3.1 Problem
13.3.2 Solution
The key to this is carefully defining what you mean by a word.
Once you've created your definition, use the special character types to create
your regular expression:
/\S+/ // everything that isn't whitespace /[A-Z'-]+/i // all upper and lowercase letters, apostrophes, and hyphens
13.3.3 Discussion
The simple question "what is a word?" is surprisingly
complicated. While the Perl compatible regular expressions have a built-in word
character type, specified by \w, it's important
to understand exactly how PHP defines a word. Otherwise, your results may not be
what you expect.
Normally, because it comes directly from Perl's definition of a
word, \w encompasses all letters, digits, and underscores; this means
a_z is a word, but the email address php@example.com is not.
PHP : Regular Expressions - [13.4] Finding the nth Occurrence of a Match
13.4.1 Problem
13.4.2 Solution
Use preg_match_all( ) to pull
all the matches into an array; then pick out the specific matches you're
interested in:
preg_match_all ("/$pattern/$modifiers", $string, $matches)
foreach($matches[1] as $match) {
print "$match\n";
}
13.4.3 Discussion
Unlike in Perl, PHP's Perl-compatible regular expressions don't
support the /g modifier that allows you to loop through the string one
match at a time. You need to use preg_match_all( ) instead of
preg_match( ).
The preg_match_all( ) function returns a
two-dimensional array. The first element holds an array of matches of the
complete pattern. The second element also holds an array of matches, but of the
parenthesized submatches within each complete match. So, to get the third
potato, you access the third element of the second element of the
$matches array:
$potatoes = 'one potato two potato three potato four';
preg_match_all("/(\w+)\s+potato\b/", $potatoes, $matches);
print $matches[1][2];
three
Instead of returning an array divided into full matches and
then submatches, preg_match_all( ) returns an array divided by matches,
with each submatch inside. To trigger this, pass PREG_SET_ORDER in as
the fourth argument. Now, three isn't in $matches[1][2], as
previously, but in $matches[2][1].
Check the return value of preg_match_all( ) to find
the number of matches:
print preg_match_all("/(\w+)\s+potato\b/", $potatoes, $matches);
3
Note that there are only three matches, not four, because
there's no trailing potato after the word four in the string.
PHP : Regular Expressions - [13.5] Choosing Greedy or Nongreedy Matches
13.5.1 Problem
13.5.2 Solution
// find all bolded sections
preg_match_all('#<b>.+?</b>#', $html, $matches);
// find all bolded sections
preg_match_all('#<b>.+</b>#U', $html, $matches);
13.5.3 Discussion
By default, all regular expressions in PHP are what's known as
greedy. This means a quantifier always tries to match as many characters
as possible.
For example, take the pattern p.*, which matches a
p and then 0 or more characters, and match it against the string
php. A greedy regular expression finds one match, because after it
grabs the opening p, it continues on and also matches the hp.
A nongreedy regular expression, on the other hand, finds a pair of matches. As
before, it matches the p and also the h, but then instead of
continuing on, it backs off and leaves the final p uncaptured. A second
match then goes ahead and takes the closing letter.
The following code shows that the greedy match finds only one
hit; the nongreedy ones find two:
print preg_match_all('/p.*/', "php"); // greedy
print preg_match_all('/p.*?/', "php"); // nongreedy
print preg_match_all('/p.*/U', "php"); // nongreedy
1
2
2
Greedy matching is also known as maximal matching and nongreedy
matching can be called minimal matching, because these options
match either the maximum or minimum number of characters possible.
Initially, all regular expressions were strictly greedy.
Therefore, you can't use this syntax with ereg( ) or ereg_replace(
). Greedy matching isn't supported by the older engine that powers these
functions; instead, you must use Perl-compatible functions.
Nongreedy
matching is frequently useful when trying to perform simplistic HTML parsing.
Let's say you want to find all text between bold tags. With greedy matching, you
get this:
$html = '<b>I am bold.</b> <i>I am italic.</i> <b>I am also bold.</b>';
preg_match_all('#<b>(.+)</b>#', $html, $bolds);
print_r($bolds[1]);
Array
(
[0] => I am bold.</b> <i>I am italic.</i> <b>I am also bold.
)
Because there's a second set of bold tags, the pattern extends
past the first </b>, which makes it impossible to correctly break
up the HTML. If you use minimal matching, each set of tags is self-contained:
$html = '<b>I am bold.</b> <i>I am italic.</i> <b>I am also bold.</b>';
preg_match_all('#<b>(.+?)</b>#', $html, $bolds);
print_r($bolds[1]);
Array
(
[0] => I am bold.
[1] => I am also bold.
)
Of course, this can break down if your markup isn't 100% valid,
and there are stray bold tags lying around.[2] If your goal is just to remove all (or
some) HTML tags from a block of text, you're better off not using a regular
expression. Instead, use the built-in function strip_tags( ); it's faster and it works correctly. See Section 11.12 for more details.
[2] It's possible to have valid HTML and still get into trouble. For instance, if you have bold tags inside a comment. A true HTML parser ignores this section, but our pattern won't.
Finally, even
though the idea of nongreedy matching comes from Perl, the -U modifier
is incompatible with Perl and is unique to PHP's Perl-compatible regular
expressions. It inverts all quantifiers, turning them from greedy to nongreedy
and also the reverse. So, to get a greedy quantifier inside of a pattern
operating under a trailing /U, just add a ? to the end, the
same way you would normally turn a greedy quantifier into a nongreedy one.
PHP : Regular Expressions - [13.6] Matching a Valid Email Address
13.6.1 Problem
13.6.2 Solution
This is a popular question and everyone has a different answer,
depending on their definition of valid. If valid means a mailbox belonging to a
legitimate user at an existing hostname, the real answer is that you can't do it
correctly, so don't even bother. However, sometimes a regular expression can
help weed out some simple typos and obvious bogus attempts. That said, our
favorite pattern that doesn't require maintenance is:
/^[^@\s]+@([-a-z0-9]+\.)+[a-z]{2,}$/i
$parsed = imap_rfc822_parse_adrlist($email_address, $default_host)
if ('INVALID_ADDRESS' == $parsed['mailbox']) {
// bad address
}
Ironically, because this function is so RFC-compliant, it may
not give the results you expect.
13.6.3 Discussion
The pattern in the Solution accepts any email address that has
a name of any sequence of characters that isn't a @ or whitespace.
After the @, you need at least one domain name consisting of the
letters a-z, the numbers 0-9, and the hyphen, separated by
periods, and proceed it with as many subdomains you want. Finally, you end with
either a two-digit country code or another top-level domain, such as
.com or .edu.
The solution pattern is handy because it still works if ICANN
adds new top-level domains.
However, it does allow through a few false positives. This more strict pattern
explicitly enumerates the current noncountry top-level domains:
/
^ # anchor at the beginning
[^@\s]+ # name is all characters except @ and whitespace
@ # the @ divides name and domain
(
[-a-z0-9]+ # (sub)domains are letters, numbers, and hyphens
\. # separated by a period
)+ # and we can have one or more of them
(
[a-z]{2} # TLDs can be a two-letter alphabetical country code
|com|net # or one of
|edu|org # many
|gov|mil # possible
|int|biz # three-letter
|pro # combinations
|info|arpa # or even
|aero|coop # a few
|name # four-letter ones
|museum # plus one that's six-letters long!
)
$ # anchor at the end
/ix # and everything is case-insensitive
Both patterns are intentionally liberal in what they accept,
because we assume you're only trying to make sure someone doesn't accidentally
leave off their top-level domain or type in something fake such as "not
telling." For instance, there's no domain "-.com", but
"foo@-.com" flies through without a blip. (It wouldn't be hard to
modify the pattern to correct this, but that's left as an exercise for you.) On
the other hand, it is legal to have an address of "Tim
O'Reilly@oreilly.com", and our pattern won't accept this. However, spaces
in email addresses are rare; because a space almost always represents a mistake,
we flag that address as bad.
The canonical definition of what's a valid address is
documented in RFC 822; however, writing code to handle all cases isn't a pretty
task. Here's one example of what you need to consider: people are allowed to
embed comments inside addresses! Comments are set inside parentheses, so it's
valid to write:
Tim (is the man @ computer books) @ oreilly.com
That's equivalent to "tim@oreilly.com". (So, again,
the pattern fails on that address.)
Alternatively, the IMAP extension has an RFC 822-compliant address parser. This
parser correctly navigates through whitespace comments and other oddities, but
it allows obvious mistakes because it assumes that addresses without hostnames
are local:
$email = 'stephen(his account)@ example(his host)'; $parsed = imap_rfc822_parse_adrlist($email,''); print_r($parsed); Array ( [0] => stdClass Object ( [mailbox] => stephen [host] => example [personal] => his host ) )
Reassembling the mailbox and host, you get
"stephen@example", which probably isn't what you want. The empty string
you must pass in as the second argument defeats your ability to check for valid
hostnames.
Some people like behind-the-scenes
processing such as DNS lookups, to check if the address is valid. This doesn't
make much sense because that technique won't always work, and you may end up
rejecting perfectly valid people from your site, due to no fault of their own.
(Also, its unlikely a mail administrator would fix his mail handling just to
work around one web site's email validation scheme.)
Another consideration when validating email addresses is that
it doesn't take too much work for a user to enter a completely legal and working
address that isn't his. For instance, one of the authors used to have a bad
habit of entering "billg@microsoft.com" when signing up for Microsoft's
web sites because "Hey! Maybe Bill doesn't know about that new version of
Internet Explorer?"
If the primary concern is to avoid typos, make people enter
their address twice, and compare the two. If they match, it's probably correct.
Also, filter out popular bogus addresses, such as
"president@whitehouse.gov" and the previously mentioned
"billg@microsoft.com". (This does have the downside of not letting The
President of the United States of America or Bill Gates sign up for your site.)
However, if you need to ensure people actually have access to
the email address they provide, one technique is to send a message to their
address and require them to either reply to the message or go to a page on your
site and type in a special code printed in the body of the message to confirm
their sign-up. If you do choose the special code route, we suggest that you
don't generate a random string of letters, such as HSD5nbADl8. Since it
looks like garbage, it's hard to retype it correctly. Instead, use a word list
and create code words such as television4coatrack. While, on occasion,
it's possible to divine hidden meanings in these combos, you can cut the error
rate and your support costs.
PHP : Regular Expressions - [13.7] Finding All Lines in a File That Match a Pattern
13.7.1 Problem
13.7.2 Solution
13.7.3 Discussion
There are two ways to do this. Here's the faster method:
$pattern = "/\bo'reilly\b/i"; // only O'Reilly books
$ora_books = preg_grep($pattern, file('/path/to/your/file.txt'));
Use the file( ) command to
automatically load each line of the file into an array element and
preg_grep( ) to filter the bad lines out.
Here's the more efficient method:
$fh = fopen('/path/to/your/file.txt', 'r') or die($php_errormsg);
while (!feof($fh)) {
$line = fgets($fh, 4096);
if (preg_match($pattern, $line)) { $ora_books[ ] = $line; }
}
fclose($fh);
Since the first method reads in everything all at once, it's
about three times faster then the second way, which parses the file line by line
but uses less memory. One downside, however, is that because the regular
expression works only on one line at a time, the second method doesn't find
strings that span multiple lines.
PHP : Regular Expressions - [13.8] Capturing Text Inside HTML Tags
13.8.1 Problem
You want to capture text inside HTML
tags. For example, you want to find all the headings in a HTML document.
13.8.2 Solution
Read the HTML file into a string and use nongreedy matching in
your pattern:
$html = join('',file($file));
preg_match('#<h([1-6])>(.+?)</h\1>#is', $html, $matches);
In this example, $matches[2] contains an array of
captured headings.
13.8.3 Discussion
True parsing of HTML is difficult using a simple regular
expression. This is one advantage of using XHTML; it's significantly easier to
validate and parse.
For instance, the pattern in the Solution is smart enough to
find only matching headings, so <h1>Dr.
Strangelove<h1> is okay, because it's wrapped inside
<h1> tags, but not <h2>How I
Learned to Stop Worrying and
Love the Bomb</h3>, because the opening tag is
an <h2> while the closing tag is not.
This technique also works for finding all text inside bold and
italic tags:
$html = join('',file($file));
preg_match('#<([bi])>(.+?)</\1>#is', $html, $matches);
However, it breaks on nested headings. Using that regular
expression on:
<b>Dr. Strangelove or: <i>How I Learned to Stop Worrying and Love the Bomb</i></b>
doesn't capture the text inside the <i> tags as
a separate item.
This wasn't a problem earlier; because headings are block level
elements, it's illegal to nest them. However, as inline elements, nested bold
and italic tags are valid.
Captured text can be processed by looping through the array of
matches. For example, this code parses a document for its headings and
pretty-prints them with indentation according to the heading level:
$html = join('',file($file));
preg_match('#<h([1-6])>(.+?)</h\1>#is', $html, $matches);
for ($i = 0, $j = count($matches[0]); $i < $j; $i++) {
print str_repeat(' ', 2 * ($matches[1][$i] - 1)) . $matches[2][$i] . "\n";
}
So, with one representation of this recipe in HTML:
$html =<<<_END_
<h1>PHP Cookbook</h1>
Other Chapters
<h2>Regular Expressions</h2>
Other Recipes
<h3>Capturing Text Inside of HTML Tags</h3>
<h4>Problem</h4>
<h4>Solution</h4>
<h4>Discussion</h4>
<h4>See Also</h4>
_END_;
preg_match_all('#<h([1-6])>(.+?)</h\1>#is', $html, $matches);
for ($i = 0, $j = count($matches[0]); $i < $j; $i++) {
print str_repeat(' ', 2 * ($matches[1][$i] - 1)) . $matches[2][$i] . "\n";
}
You get:
PHP Cookbook
Regular Expressions
Capturing Text Inside of HTML Tags
Problem
Solution
Discussion
See Also
By capturing the heading level and heading text separately, you
can directly access the level and treat it as an integer when calculating the
indentation size. To avoid a two-space indent for all lines, subtract 1 from the
level.
PHP : Regular Expressions - [13.9] Escaping Special Characters in a Regular Expression
13.9.1 Problem
You want to
have characters such as * or + treated as literals, not as
metacharacters, inside a regular expression. This is useful when allowing users
to type in search strings you want to use inside a regular expression.
13.9.2 Solution
$pattern = preg_quote('The Education of H*Y*M*A*N K*A*P*L*A*N').':(\d+)';
if (preg_match("/$pattern/",$book_rank,$matches)) {
print "Leo Rosten's book ranked: ".$matches[1];
}
Use quotemeta( ) to escape POSIX metacharacters:
$pattern = quotemeta('M*A*S*H').':[0-9]+';
if (ereg($pattern,$tv_show_rank,$matches)) {
print 'Radar, Hot Lips, and the gang ranked: '.$matches[1];
}
13.9.3 Discussion
Here are the characters that preg_quote( )
escapes:
. \ + * ? ^ $ [ ] ( ) { } < > = ! | :
. \ + * ? ^ $ [ ] ( )
These functions escape the metacharacters with backslash.
The quotemeta( ) function doesn't match all POSIX
metacharacters. The characters {, }, and | are also
valid metacharacters but aren't converted. This is another good reason to use
preg_match( ) instead of ereg( ).
You can also pass preg_quote( ) an additional
character to escape as a second argument. It's useful to pass your pattern
delimiter (usually /) as this argument so it also gets escaped. This is
important if you incorporate user input into a regular-expression pattern. The
following code expects $_REQUEST['search_term'] from a web form and
searches for words beginning with $_REQUEST['search_term'] in a string
$s:
$search_term = preg_quote($_REQUEST['search_term'],'/');
if (preg_match("/\b$search_term/i",$s)) {
print 'match!';
}
Using preg_quote( ) ensures the regular expression is
interpreted properly if, for example, a Magnum, P.I. fan enters t.c. as
a search term. Without preg_quote( ), this matches tic,
tucker, and any other words whose first letter is t and third
letter is c. Passing the pattern delimiter to preg_quote( ) as
well makes sure that user input with forward slashes in it, such as
CP/M, is also handled correctly.
PHP : Regular Expressions - [13.10] Reading Records with a Pattern Separator
13.10.1 Problem
You want to read
in records from a file, in which each record is separated by a pattern you can
match with a regular expression.
13.10.2 Solution
$filename = '/path/to/your/file.txt';
$fh = fopen($filename, 'r') or die($php_errormsg);
$contents = fread($fh, filesize($filename));
fclose($fh);
$records = preg_split('/[0-9]+\) /', $contents);
13.10.3 Discussion
This breaks apart a numbered list and places the individual
list items into array elements. So, if you have a list like this:
1) Gödel 2) Escher 3) Bach
You end up with a four-element array, with an empty opening
element. That's because preg_split( ) assumes the delimiters are
between items, but in this case, the numbers are before items:
Array ( [0] => [1] => Gödel [2] => Escher [3] => Bach )
From one point of view, this can be a feature, not a bug, since
the nth element holds the nth item. But, to compact the array, you can eliminate
the first element:
$records = preg_split('/[0-9]+\) /', $contents);
array_shift($records);
Another modification you might want is to strip new lines from
the elements and substitute the empty string instead:
$records = preg_split('/[0-9]+\) /', str_replace("\n",'',$contents));
array_shift($records);
PHP doesn't allow you to change the input record separator to
anything other than a newline, so this technique is also useful for breaking
apart records divided by strings. However, if you find yourself splitting on a
string instead of a regular expression, substitute explode( ) for preg_split( ) for a more efficient operation.
Subscribe to:
Posts (Atom)