Show HN: Using functions to construct Regex in Python
github.com
github.com
Many of the comments here are speculating on what this might/could be used for, but could simply look at how it's used in various Emacs modes compared to writing raw regexes in string form.
It's not fully comparable, since a large reason to use rx.el in Emacs is Emacs's nasty regex syntax coupled with having no native quoting construct for regex, making writing anything an exercise in bashing your backslash key.
But it also makes it easier to programmatically generate regexes, which as your Python implementation also shows can lead to much clearer code.
> Emacs's nasty regex syntax
It's also case-sensitive too. Writing regex by hand when there is a mixture of case sensitivity to recognise is not at all pleasant.
I find the naming of the classes off.
Why not call X as Any?
Why use nouns for some things, and verbs for others? I found Include and Exclude to be confusion. Inclusion and exclusion are relative to something. You include something with something else. I think Only and AnyBut would be better names.
Why call it Seq when it's closer to a Python Range? Seq usually means sequentiality.
Why not call Times as Repeat or Repetition?
It didn't look like this had any compatibility with existing regexes. I can't parse an existing regex into this library.
It looks like debugging could be a nightmare since it just passes things off to Python's re library. So if an error happens there, it will be tough to trace it to your original construction.
All of this class hierarchy building seems prime to replace with algebraic data types, which would probably cut the code down to just a fraction of the size.
Repeat = Times
etc.
you wouldnt use these structures in python code, you use them to build your regex then compile it and use the regex in your programs. It is possible to add some type safety and messages to help debugging which would be better than with natural regex syntax.
Pattern(Include(Seq('a', 'm')), 'c')
That is sort of building the following set of strings.
ac bc cc . . .
When you use Exclude, it would be like you are picking a char that is not belonging to the sequence, like in:
Pattern(Exclude(Seq('0, 9')), c)
then you would end up with a set like:
ac bc cc . . .
It means, a set whose first character of the elements arent digits.
I like it.
def Repeating(pattern, *args, min=None, max=None, exact=None):...
Repeating(P) => P*
Repeating(P, a, b) and
Repeating(P, min=a, max=b) => P{a, b}
Repeating(P, n) and
Repeating(P, exact=n) => P{n, n}
Repeating(P, min=n) => P{n,}
Repeating(P, max=n) => P{0, n}
"Exclude" and "Include" suffer from the aforementioned problem of being procedural words for a declarative syntax. And they're easy to confuse with one another, having only two differing letters. And they don't make it clear that they match exactly one character. I would use "AnyBut" and "OneOf".Why the split between Group and NamedGroup? Just add a name parameter to Group!
Seq as mentioned by parent is also problematic. Use Range or maybe Between or FromTo. To me FromTo flows the best, but Range is also ok.
I'm tempted to advocate adding operator overloading, so | would make a pattern that matches any one of two alternatives, * would be used for specifying repeats, etc. So you could write
Digit = FromTo('0', '9')
CapitalLetter = FromTo('A', 'Z')
POCode = CapitalLetter * 2 + Digit * 3
SmallLetter = FromTo('a', 'z')
Letter = CapitalLetter or SmallLetter
Name = CapitalLetter + Letter * (0, Inf)
FirstName = Group(name='first', Name)
LastName = Group(name='last', Name)
FullName = FirstName + ' ' + LastName
However, implemented badly that can hurt readability much more than having to write functions everywhere.For repeat I think using indexing would make more sense though it might look somewhat unusual:
Pattern[:]
would be * Pattern[1:]
would be + aka {1,} Pattern[5]
would be {5} Pattern[m:n]
would be {m,n}It also uses a single operator for all repetition operations which is nice.
About the Repeating/Repeat it could be just an idiomatic way of writing Include which stands for just one char thats fetched from a given sequence.
The tree syntax actually uses symbols and lists, idiomatic in symbolic programming settings. This has the hugely convenient property of being trivially serializable, just as the string representation is. This isn't true with an unadorned object representation.
The documentation for the tree syntax is here [1]. Some examples from the page are reproduced below. The PARSE-STRING function isn't used by the user except for testing. All of the regex scanning and matching functions allow either a string or a tree.
* (parse-string "(ab)*")
(:GREEDY-REPETITION 0 NIL (:REGISTER "ab"))
* (parse-string "(a(b))")
(:REGISTER (:SEQUENCE #\a (:REGISTER #\b)))
* (parse-string "(?:abc){3,5}")
(:GREEDY-REPETITION 3 5 (:GROUP "abc"))
;; (:GREEDY-REPETITION 3 5 "abc") would also be OK
* (parse-string "a(?i)b(?-i)c")
(:SEQUENCE #\a
(:SEQUENCE (:FLAGS :CASE-INSENSITIVE-P)
(:SEQUENCE #\b (:SEQUENCE (:FLAGS :CASE-SENSITIVE-P) #\c))))
;; same as (:SEQUENCE #\a :CASE-INSENSITIVE-P #\b :CASE-SENSITIVE-P #\c)
* (parse-string "(?=a)b")
(:SEQUENCE (:POSITIVE-LOOKAHEAD #\a) #\b)
[0] http://weitz.de/cl-ppcre/I think for beginners a better approach would be using something like RegexBuddy (https://www.regexbuddy.com not affiliated, just found it super useful when I started writing regexes).
It is useful for maintainability. The regexp syntax was designed for a write-only scenario (the command line), but complex regexps quickly become unwieldy.
The regular grammar is all about composition, and the regexp syntax (in JS) doesn't allow one to store sub-expressions in variables for reuse and readability.
So this allows one to create regexps piece by piece, to write tests for the sub-expressions, etc...
Maybe if this was implemented on a prefix notation language it may have would looked/read better
Also, it's only for Python2.
Some improvements could be to use operators to reduce some boilerplate, like using * instead of Times(), i.e `(X() * 3) * 5`
mail = '(?P<name>[a-z][a-z0-1\_\.\-]{1,})'
hostname = '(?P<hostname>python[a-z]{1,})'
domain = '(?P<domain>br[a-z])'
match_mail = f'{mail}\@{hostname}\.{domain}'
quite more readable and easy to verify than the 50-line example in the link. The real advantage of parser combinators over regexes is the ability to parse data structures instead of just capturing regex groups. import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
String regexStr = "";
regexStr += "\\b"; //Begin match at the word boundary(whitespace boundary)
regexStr += "\\d{3}"; //Match three digits
regexStr += "[-.]?"; //Optional - Match dash or dot
regexStr += "\\d{3}"; //Match three digits
regexStr += "[-.]?"; //Optional - Match dash or dot
regexStr += "\\d{4}"; //Match four digits
regexStr += "\\b"; //End match at the word boundary(whitespace boundary)
if (args[0].matches(regexStr)) {
System.out.println("Match!");
} else {
System.out.println("No match.");
}
}
}
[0] https://codereview.stackexchange.com/questions/47432/comment... def main(args):
regexStr = \
"""
\\b # begin match at word boundary
\\d{3} # match three digits
[-.]? # optional – match dash or dot
"""
if args[0].match(regexStr):
print("match")
else:
print("no match")
It's even better with nested structures, because it allows indentation as well! i++; /* increase i by one */
sure, break up a larger regex to reflect its structure and comment those sections, but something like \\d{3} or [-.]? should really not need commenting\A(?:[a-z0-9!#$%&'+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'+/=?^_`{|}~-]+)* | "(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f] | \\[\x01-\x09\x0b\x0c\x0e-\x7f])") @ (?:(?:[a-z0-9](?:[a-z0-9-][a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-][a-z0-9])? | \[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-][a-z0-9]: (?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f] | \\[\x01-\x09\x0b\x0c\x0e-\x7f])+) \])\z
according to http://www.regular-expressions.info/email.html
I honestly think that one would be more readable and comprehensive in crocs's syntax. It is not about being only readable it is about a way of reasoning to come up with your regex, crocs permit you to better reason than using regex's syntax. When one uses regex's syntax to reason it is sort of using a low level programming lang to reason, when things get complex it doesnt really pay off the shortness of regex's.
But my main point was that since crocs compiles down to regex, it doesn't let you do more than regex already does, and it inherits most of regexes' weaknesses. It does afford some level of type safety though, which could be argued makes up for the verbosity (and whether that verbosity is good or bad is probably a religious war).
Most things need to process and validate only RFC5321 (and 5890, 5891 and 5892 for Unicode) addresses.
https://docs.python.org/3/library/re.html#re.VERBOSE
a = re.compile(r"""\d + # the integral part
\. # the decimal point
\d * # some fractional digits""", re.X)
Or is there more at stake than making readable, self-documenting regular expressions?Why use functions when you can have one big main(args: Array<String>) with inline comments?
btw, a hello-world snippet like /\d+\.\d*/ isn't very scathing criticism of a library.
One could write the filter using crocs format, compile it to regex then use in their programs. Using crocs you sort of define your data type then apply functions on those data types to fetch your required output.
The crocs framework is a way to give more power to your imagination and readability to others about what your imagination produces.
I even named the library 'hu_regex', where 'hu' stands for human, but never did anything further, as I couldn't think of any good builder patter for regex that seemed intuitive.
Anyway, thanks for this. This comment section has introduced some good libraries as well.
https://github.com/iogf/crocs/wiki
It would be interesting to hear comments :)
I tend to find that regex patterns are effectively constants in the program
And if it's just for readability it seems a bit of a sledgehammer
The python parsec package is really terribly documented, but there's a good example here:
http://www.valuedlessons.com/2008/02/pysec-monadic-combinato...
EDIT:
The name of the package may have changed; it looks like this is the pypi package being documented (if not; i'm confused because pypi's pysec package is something totally different):
There's three things you need:
1 - concatenation - basically one thing next to another, 'a' goes next to 'b' to make 'ab' which matches any string with that in it. Examples:
'xzyqr2321abtwe' - matches
'zyxabc' - matches
'abcxyz' - matches
'ab' - matches
'ba' - doesn't match
'azb' - doesn't match
2 - alternation - one thing or another. The operator for this is the pipe symbol '|'. So 'a|b' is 'a or b'.
'xzyqr2321abtwe' - matches
'xzyqr2321atwe' - matches
'xzyqr2321btwe' - matches
'zyxabc' - matches
'abcxyz' - matches
'ab' - matches
'azb' - matches
Here's another 'abc|cab' 'abc or cab'
'xzyqr2321abtwa' - doesn't match
'zyxabc' - matches
'zyxcba' - doesn't match
'zyxcab' - matches
If it helps, you can use parentheses to group blocks of things for clarity.
'(abc)|(cab)'
3 - repetition - the operator we want to care about is '* ' also called the Kleene star. This means that anything that comes before the star can repeat 0 or more times.
so 'a* ' means a pattern of 'a' 0 or more times.
'xzyqr2321abtwe' - matches
'xzyqr2321btwe' - matches (because of zero or more times)
'xzyqr2321btwaaaaaaaaa' - matches
so if you want to match an 'a' one or more times you can simply use rule #1 (concatenation) and put 'aa* '
'xzyqr2321abtwe' - matches
'xzyqr2321btwe' - doesn't match
'xzyqr2321btwaaaaaaaaa' - matches
It turns out this is such a common regex, that some "syntactic sugar" was invented to make it easier to work with, the '+' symbol. Which is used exactly the same way: 'aa* ' = 'a+ '
Congratulations, you now know everything you need to make a regex that can pretty much do anything. So what about all that other line noise one usually sees in a regex? That's more "syntactic sugar", designed to make certain kinds of regexes simpler to write.
For example, a regex using the above rules that can match any alphabet letter from a through f is: a|b|c|d|e|f
This is a lot of typing, so you can use square brackets to build what's called a "character class". '[abcdef]' = 'a|b|c|d|e|f'
If you have a long string of characters, you can use some more sugar and simply put a '-' in between the first and last characters '[a-f]' = '[abcdef]'
Here's a complex example, a regex that can match any lower-case alphabet character or number '[0-9a-z]'
It also turns out that a special character class pattern that can match any character (except for newlines) is so common that a special operator '.' was created. '.' matches literally anything.
For example, concatenating it with an "any number" regex gives you '.[0-9]' which means any character followed by a number.
For repetition, there's also some syntactic sugar, here's a table, these all go after the thing you want repeated:
* - 0 or more times
+ - 1 or more times
{p} - p times
{p,q} - p to q times
{,q} - 0 to q times
{p,} - at least p times
? - 0 or 1 times
Finally, in the list of basic regex stuff, there's "capturing", this is how regexes get used to parse strings. It works basically like this, anything that is matched inside of a parentheses gets "copied" to a special variable. It depends on your language on where this variable is. The name of the variable is typically some 1, indexed variable like $1, $2, $3. The number of the variable is the number of left parentheses in the regex.
Example: 'abc(.+)123'
abcd123 - matches, $1 is 'd'
abcf123 - matches, $1 is 'f'
abcqqq123 - matches, $1 is 'qqq'
abc123 - doesn't match
Another example: (abc([0-9]+))
abc123 - matches $1 is abc123 $2 is 123
If you want to suppress this capturing behavior, the '(' parentheses should be written '(?:'
(?:abc([0-9]+))
Try these out here, it does a nice job highlighting https://regex101.com/ it calls the capture variable "groups" and shows them on the right under "capture information"
There, that's about it. Other advanced techniques like greedy matching, negative character classes, callbacks, etc. flow pretty nicely from these basic ideas.