Untitled
Untitled
A Little Guide
Scientific Programmer
This book is for sale at http://leanpub.com/pythonregex
Python RegEx . . . . . . . . . . . . . . . . . . . . . . . . . 1
Python regex match function . . . . . . . . . . . . . . . . 4
Python regex search function . . . . . . . . . . . . . . . . 8
Python regex match vs. search functions . . . . . . . . . 12
Python regex group functions . . . . . . . . . . . . . . . . 14
Python regex sub function for search and replace . . . . 16
Python regex split function . . . . . . . . . . . . . . . . 18
Python regex findall function . . . . . . . . . . . . . . . 19
Python regex compile function . . . . . . . . . . . . . . . 21
Python regex finditer function . . . . . . . . . . . . . . 24
Python Regex - Lookarounds and Greedy Search . . . . 25
Python Lookahead . . . . . . . . . . . . . . . . . . . . . . 27
Python Look behind . . . . . . . . . . . . . . . . . . . . . 28
Python Lazy and Greedy Search . . . . . . . . . . . . . . 29
Project 2: Parsing data from a HTML file with Python
and REGEX . . . . . . . . . . . . . . . . . . . . . . 32
Project 3: PDF scraping in Python + REGEX . . . . . . . 34
Project 4: Web scraping in Python + REGEX . . . . . . . 37
Project 5: Amazon web crawling in Python + REGEX . . 40
Quiz # 1 - REGEX Patterns . . . . . . . . . . . . . . . . . 42
Quiz # 2 Python REGEX Functions . . . . . . . . . . . . . 45
References . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
CONTENTS 1
Python RegEx
Hello coders! Let’s start our quest with regular expressions (RegEx).
In Python, the module re provides full support for Perl-like regular
expressions in Python. We need to remember that there are many
characters in Python, which would have special meaning when
they are used in regular expression. To avoid bugs while dealing
with regular expressions, we use raw strings as r'expression'.
The re module in Python provides multiple methods to perform
queries on an input string. Here are the most commonly used
methods:
• re.match()
• re.search()
• re.split()
• re.sub()
• re.findall()
• re.compile()
The following table lists the regular expression syntax that is avail-
able in Python. Note that any Regex can be concatenated to form
new regular expressions; if X and Y are both regular expressions,
then XY is also a regular expression.
CONTENTS 2
Pattern Description
. Matches any single character except
newline. Using m option allows it to match
newline as well.
^ Matches the start of the string, and in
re.MULTILINE (see the next lesson on how to
change to multiline) mode also matches
immediately after each newline.
$ Matches end of line. In re.MULTILINE mode
also matches before a newline.
[.] Matches any single character in brackets.
[^.] Matches any single character not in brackets.
* Matches 0 or more occurrences of preceding
expression.
+ Matches 1 or more occurrence of preceding
expression.
? Matches 0 or 1 occurrence of preceding
expression.
{n} Matches exactly n number of occurrences of
preceding expression.
{n,} Matches n or more occurrences of preceding
expression.
{n, m} Matches at least n and at most m occurrences
of preceding expression. For example, x{3,5}
will match from 3 to 5 'x' characters.
Pattern Description
xy Matches either x or y.
\d Matches digits. Equivalent to [0-9].
\D Matches nondigits.
\w Matches word characters.
\W Matches nonword characters.
\z Matches end of string.
\G Matches point where last match finished.
\b Matches the empty string, but only at the
beginning or end of a word. Boundary
between word and non-word and /B is
opposite of /b. Example r"\btwo\b" for
searching two from 'one two three'.
CONTENTS 3
Pattern Description
\B Matches nonword boundaries.
\n, \t Matches newlines, carriage returns, tabs, etc.
\s Matches whitespace.
\S Matches nonwhitespace.
\A Matches beginning of string.
\Z Matches end of string. If a newline exists, it
matches just before newline.
Where,
Match Flags
Modifier Description
re.I Performs case-insensitive matching.
re.L Interprets words according to the current
locale. This interpretation affects the
alphabetic group (\w and \W), as well as
word boundary behavior (\b and \B).
re.M Makes $ match the end of a line and makes
^ match the start of any line.
re.S Makes a period (dot) match any character,
including a newline.
re.U Interprets letters according to the Unicode
character set. This flag affects the behavior
of \w, \W, \b, \B.
re.X It ignores whitespace (except inside a set []
or when escaped by a backslash and treats
unescaped # as a comment marker.
CONTENTS 5
Return values
Example 1
Let’s find the words before and after the word to:
1 #!/usr/bin/python
2 import re
3
4 line = "Learn to Analyze Data with Scientific Python";
5
6 m = re.match( r'(.*) to (.*?) .*', line, re.M|re.I)
7
8 if m:
9 print "m.group() : ", m.group()
10 print "m.group(1) : ", m.group(1)
11 print "m.group(2) : ", m.group(2)
12 else:
13 print "No match!!"
The first group (.*) identified the string: Learn and the next group
(*.?) identified the string: Analyze. Output:
CONTENTS 6
Example 2
1 #!/usr/bin/python
2 import re
3
4 line = "Learn Data, Python";
5
6 m = re.match( r'(\w+) (\w+)', line, re.M|re.I)
7
8 if m:
9 print "m.group() : ", m.groups()
10 print "m.group (1,2)", m.group(1, 2)
11 else:
12 print "No match!!"
Output:
Example 3
1 #!/usr/bin/python
2 import re
3
4 number = "124.13";
5
6 m = re.match( r'(?P<Expotent>\d+)\.(?P<Fraction>\d+)', nu\
7 mber)
8
9 if m:
10 print "m.groupdict() : ", m.groupdict()
11 else:
12 print "No match!!"
Example 4
1 import re
2
3 values = ["Learn", "Live", "Python"];
4
5 for value in values:
6 # Match the start of a string.
7 result = re.match("\AL.+", value)
8 if result:
9 print("START MATCH [L]:", value)
10
11 # Match the end of a string.
12 result2 = re.match(".+n\Z", value)
CONTENTS 8
13 if result2:
14 print("END MATCH [n]:", value)
Output:
1 output
2
3 ('START MATCH [L]:', 'Learn')
4 ('END MATCH [n]:', 'Learn')
5 ('START MATCH [L]:', 'Live')
6 ('END MATCH [n]:', 'Python')
Example 5
Match Flags
Modifier Description
re.I Performs case-insensitive matching.
re.L Interprets words according to the current
locale. This interpretation affects the
alphabetic group (\w and \W), as well as
word boundary behavior (\b and \B).
re.M Makes $ match the end of a line and makes
^ match the start of any line.
re.S Makes a period (dot) match any character,
including a newline.
re.U Interprets letters according to the Unicode
character set. This flag affects the behavior
of \w, \W, \b, \B.
re.X It ignores whitespace (except inside a set []
or when escaped by a backslash and treats
unescaped # as a comment marker.
Return values
Example 1
Let’s find the words before and after the word to:
CONTENTS 10
1 #!/usr/bin/python
2 import re
3
4 line = "Learn to Analyze Data with Scientific Python";
5
6 m = re.search( r'(.*) to (.*?) .*', line, re.M|re.I)
7
8 if m:
9 print "m.group() : ", m.group()
10 print "m.group(1) : ", m.group(1)
11 print "m.group(2) : ", m.group(2)
12 else:
13 print "No match!!"
Output
The first group (.*) identified the string: Learn and the next group
(*.?) identified the string: Analyze.
Example 2
1 #!/usr/bin/python
2 import re
3
4 line = "Learn Data, Python";
5
6 m = re.search( r'(\w+) (\w+)', line, re.M|re.I)
7
8 if m:
9 print "m.group() : ", m.groups()
10 print "m.group (1,2)", m.group(1, 2)
11 else:
12 print "No match!!"
Example 3
1 #!/usr/bin/python
2 import re
3
4 number = "124.13";
5
6 m = re.search( r'(?P<Expotent>\d+)\.(?P<Fraction>\d+)', n\
7 umber)
8
9 if m:
10 print "m.groupdict() : ", m.groupdict()
11 else:
12 print "No match!!"
CONTENTS 12
• match
• search
Example 1
Let’s try to find the word Python:
1 #!/usr/bin/python
2 import re
3
4 line = "Learn to Analyze Data with Scientific Python";
5
6 m = re.search( r'(python)', line, re.M|re.I)
7
8 if m:
9 print "m.group() : ", m.group()
10 else:
11 print "No match by obj.search!!"
12
CONTENTS 13
Output
1 m.group() : Python
2 No match by obj.match
You see above that, match function won’t find the word “Python”,
but search can! Also note the use of the re.I (case insensitive) option.
Example 2:
start([group]) and end([group]) return the indices of the start
and end of the substring matched by group. We need to use search
instead of match for this example:
1 #!/usr/bin/python
2 import re
3
4 email = "hello@leremove_thisarntoanalayzedata.com ";
5
6 # m = re.match ("remove_this", email) // This will not wo\
7 rk!
8 m = re.search("remove_this", email)
9
10 if m:
11 print "email address : ", email[:m.start()] + email[m.\
12 end():]
13 else:
14 print "No match!!"
CONTENTS 14
Output
1 #!/usr/bin/python
2 import re
3
4 # A string.
5 name = "Learn Scientific"
6
7 # Match with named groups.
8 m = re.match("(?P<first>\w+)\W+(?P<last>\w+)", name)
9
10 # Print groups using names as id.
11 if m:
12 print(m.group("first"))
13 print(m.group("last"))
Output:
1 Learn
2 Scientific
We can get the first name with the string “first” and the group()
method. We use “last” for the last name.
CONTENTS 15
1 import re
2
3 name = "Scientific Python"
4
5 # Match names.
6 m = re.match("(?P<first>\w+)\W+(?P<last>\w+)", name)
7
8 if m:
9 # Get dict.
10 d = m.groupdict()
11
12 # Loop over dictionary with for-loop.
13 for t in d:
14 print(" key:", t)
15 print("value:", d[t])
Output
Syntax
1 re.sub(pattern, repl, string, maximum=0)
Example 1
1 #!/usr/bin/python
2 import re
3
4 phone = "Please call the phone # +61-927 479-548"
5
6 # Remove anything other than digits
7 num = re.sub(r'\D', "", phone)
8 print "The raw phone numbe is : ", num
Output
CONTENTS 17
Example 2:
Let’s use the sub() function to “munge” a text, i.e., randomize the
order of all the characters in each word of a sentence except for the
first and last characters:
1 #!/usr/bin/python
2 import re
3 import random
4
5 def repl(m):
6 inner_word = list(m.group(2))
7 random.shuffle(inner_word)
8 return m.group(1) + "".join(inner_word) + m.group\
9 (3)
10
11 line = "Learn Scientific Python with Regex";
12 m = re.sub(r"(\w)(\w+)(\w)", repl, line);
13
14 if m:
15 print "munged : ", m;
16 else:
17 print "No match!!";
Output
Syntax
1 re.split(pattern, string, maxsplit=0, flags=0)
Example 1
1 #!/usr/bin/python
2 import re
3
4 line = "Learn, Scientific, Python"
5
6 m = re.split('\W+', line)
7
8 if m:
9 print m
10 else:
11 print "No match!"
Example 2
Now let’s make a second example, where a splitter can any alphabet
from [a-z]
1 #!/usr/bin/python
2 import re
3
4 line = "+61Lean7489Scientific324234"
5
6 m = re.split('[A-Za-z]+', line, re.I)
7
8 if m:
9 print m
10 else:
11 print "No match!"
Syntax
1 re.findall(pattern, string, flags=0)
Example 1
1 #!/usr/bin/python
2 import re
3
4 line = 'your alpha@scientificprograming.io, blah beta@sci\
5 entificprogramming.io blah user'
6
7 emails = re.findall(r'[\w\.-]+@[\w\.-]+', line)
8
9 if emails:
10 print emails
11 else:
12 print "No match!"
1 #!/usr/bin/python
2 import re
3
4 line = 'your alpha@scientificprograming.io, blah beta@sci\
5 entificprogramming.me blah user'
6
7 tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', line)
8
9 if tuples:
10 print tuples
11 else:
12 print "No match!"
Syntax
1 re.compile(pattern, flags=0)
1 m = re.match(pattern, string)
is equivalent to:
1 p = re.compile(pattern)
2 m = p.match(string)
Note that the programs that use only a few regular expressions at
a time don’t need to compile regular expressions (recent patterns
are cached automatically due to re._MAXCACHE setting).
Example
1 <html>
2 <header>SP:</header>
3 <body>
4 <h1>Learn</h1>
5 <p>Scientific Programming</p>
6 </body>
7 </html>
1 import re
2 import os
3 def main():
4 f = open('index.html')
5 pattern = re.compile(r'(?P<start><.+?>)(?P<content>.*\
6 ?)(</.+?>)')
7 output_text = []
8 for text in f:
9 match = pattern.match(text)
10 if match is not None:
11 output_text.append(match.group('content'))
12
13 fixed_content = ' '.join(output_text)
14
15 print fixed_content
16
17 f.close()
18
19 if __name__ == '__main__':
20 main()
Output
Learning Tasks
Syntax
1 re.finditer(pattern, string, flags=0)
Example 1
1 import re
2 import urllib2
3
4 html = urllib2.urlopen('https://docs.python.org/2/library\
5 /re.html').read()
6 pattern = r'\b(the\s+\w+)\s+'
7 regex = re.compile(pattern, re.IGNORECASE)
8 for match in regex.finditer(html):
9 print "%s: %s" % (match.start(), match.group(1))
Once you have the list of tuples, you can loop over it to do some
computation for each tuple.
Expected output:
1 output
2
3 3261: The Python
4 4210: the backslash
5 4451: the same
6 4474: the same
7 4651: the pattern
8 4679: the regular
9 4930: The solution
10 5937: The functions
11 6301: the standard
12 and so on...
Python Lookahead
Python positive lookahead matches at a position where the pattern
inside the lookahead can be matched. Matches only the position. It
does not consume any characters or expand the match.
Example
1 import re
2
3 string = "begin:learner1:scientific:learner2:scientific:l\
4 earner3:end"
5 print re.findall(r"(\w+)(?=:scientific)", string)
Output
1 ['learner1', 'learner2']
Note the output learner1 and learner2, but not learner3, which is
followed by the word :end.
Neagative Lookahead
Example
1 import re
2
3 string = "begin:learner1:scientific:learner2:scientific:l\
4 earner3:end"
5 print re.findall(r"(learner\d+)(?!:scientific)", string)
Output
1 ['learner3']
This matched all the words, not followed by the word scientific!
Positive Lookbehind
Example
1 import re
2
3 string = "begin:learner1:scientific:learner2:scientific:l\
4 earner3:end"
5 print re.findall(r"(?<=learner\d:)(\b\w*\b)", string)
Neagative Lookbehind
Example
1 import re
2
3 string = "begin:learner1:scientific:learner2:scientific:l\
4 earner3:end"
5 print re.findall(r"^(?<!learner\d:)(\b\w*\b)", string)
1 import re
2
3 Regex = re.compile(r'(scientific )?programming')
4 m1 = Regex.search('Learn programming')
5 m2 = Regex.search('Learn scientific programming')
6
7 print m1.group()
8 print m2.group()
1 programming
2 scientific programming
a special start codon ATG, and three stop codons, TGA, TAG, and TAA.
Example:
1 cgcgcATGcATGcgTGAcTAAcgTAGcgcgcgcgc
• ATGcATGcgTGA and
• ATGcgTGAcTAA.
1 from re import *
2
3 dna = 'cgcgcATGcATGcgTGAcTAAcgTAGcgcgcgcgc'
4 dna = dna.lower()
5 orfpat = r'(?x) ( atg (?: (?!tga|tag|taa) ... )* (?:tga\
6 |tag|taa) )'
7 print findall(orfpat,dna)
output ['atgcatgcgtga']
We want to find an ORF without consuming it, we can use a
positive lookahead assertion ((?= ( atg). We put the whole ORF
pattern inside the lookahead and find the two atgcatgcgtga and
atgcgtgactaa.
CONTENTS 32
1 from re import *
2
3 dna = 'cgcgcATGcATGcgTGAcTAAcgTAGcgcgcgcgc'
4 dna = dna.lower()
5 orfpat = r'(?x) (?= ( atg (?: (?!tga|tag|taa) ... )* (?\
6 :tga|tag|taa) ))'
7 s = findall(orfpat,dna)
8 if s:
9 print ', '.join(s)
Output
1 atgcatgcgtga, atgcgtgactaa
1 https://github.com/rexdwyer/Splitsville
CONTENTS 33
1 <html>
2 <head>
3 <style>
4 table, th, td {
5 border: 1px solid black;
6 border-collapse: collapse;
7 }
8 th, td {
9 padding: 5px;
10 }
11 th {
12 text-align: left;
13 }
14 </style>
15 </head>
16 <body>
17 <table style="width:100%">
18 <tr align="center"><td>1</td> <td>England</td> <td>Englis\
19 h</td></tr>
20 <tr align="center"><td>2</td> <td>Japan</td> <td>Japanese\
21 </td></tr>
22 <tr align="center"><td>3</td> <td>China</td> <td>Chinese<\
23 /td></tr>
24 <tr align="center"><td>4</td> <td>Middle-east</td> <td>Ar\
25 abic</td></tr>
26 <tr align="center"><td>5</td> <td>India</td> <td>Hindi</t\
27 d></tr>
28 <tr align="center"><td>6</td> <td>Thailand</td> <td>Thai<\
29 /td></tr>
30 </table>
31 </body>
32 </html>
If we load the HTML file onto a browser it should look like below:
CONTENTS 34
Solution
In this code, we first extract HTML data (data.html) and then find
and extract the values from the HTML code. “‘ import re
with open(‘data.html’, ‘r’) as myfile: data=myfile.read().replace(‘\n’,
‘’)
result=re.findall(r’<td>\w+</td>\s<td>(\w+)</td>\s<td>(\w+)</td>’,data)
print(result) “‘
Output :
1 [('England', 'English'),
2 ('Japan', 'Japanese'),
3 ('China', 'Chinese'),
4 ('India', 'Hindi'),
5 ('Thailand', 'Thai')]
Input file
Solution
2 http://main.diabetes.org/dforg/pdfs/2015/2015-cg-insulin-pumps.pdf
CONTENTS 36
1 import re
2
3 import pdfquery
4 from lxml import etree
5
6
7 PDF_FILE = 'data.pdf'
8
9 pdf = pdfquery.PDFQuery(PDF_FILE)
10 pdf.load()
11
12 product_info = []
13 page_count = len(pdf._pages)
14 for pg in range(page_count):
15 data = pdf.extract([
16 ('with_parent', 'LTPage[pageid="{}"]'.format(pg+1\
17 )),
18 ('with_formatter', None),
19 ('product_name', 'LTTextLineHorizontal:in_bbox("4\
20 0, 48, 181, 633")'),
21 ])
22
23 for ix, pn in enumerate(sorted([d for d in data['prod\
24 uct_name'] if d.text.strip()], key=lambda x: x.get('y0'),\
25 reverse=True)):
26 if ix % 2 == 0:
27 product_info.append({'Manufacturer': pn.text.\
28 strip(), 'page': pg, 'y_start': float(pn.get('y1')), 'y_e\
29 nd': float(pn.get('y1'))-150})
30 if ix > 0:
31 product_info[-2]['y_end'] = float(pn.get(\
32 'y0'))+10.0
33 else:
34 product_info[-1]['Model'] = pn.text.strip()
35
CONTENTS 37
36 pdf.file.close()
37
38
39 for p in product_info:
40 s = p['Manufacturer']
41 m = re.search(r"Tandem",s,re.I)
42 if m:
43 print('Manufacturer: {}[Model {}]\n'.format(p['Ma\
44 nufacturer'],p['Model']))
From this result we can see that there are two models T:fles
and T:slim supplied by the manufacturer called ‘Tandem Diabetes
Care’. The problem solution has been adopted and simplified from
the reddit user insainodwayno3 .
Scraping tabular data from the Boone Country Sherrif’s Dept website
Solution
1 import re
2 from pprint import pprint
3 import csv
4 import requests
5 from BeautifulSoup import BeautifulSoup
6
7
8 url = 'http://www.showmeboone.com/sheriff/JailResidents/J\
9 ailResidents.asp'
10 response = requests.get(url)
5 https://www.crummy.com/software/BeautifulSoup/
CONTENTS 39
11 html = response.content
12
13 soup = BeautifulSoup(html)
14 table = soup.find('tbody', attrs={'class': 'stripe'})
15
16 list_of_rows = []
17
18 for row in table.findAll('tr')[0:]:
19 list_of_cells = []
20 for cell in row.findAll('td'):
21 text = cell.text.replace(' ', '')
22 list_of_cells.append(text)
23 list_of_rows.append(list_of_cells)
24
25 for line in list_of_rows:
26 row = '\t'.join(str(i) for i in line) # python 2
27 s=row[0:5] # Select only the Last names (1st column)
28 m = re.search(r"^A",s,re.I)
29 if m:
30 print row
This examples has been adopted and extended from the Python-
BeautifulSoup’s first web scraper6 , originally developed by Chase
Davis, Jackie Kazil, Sisi Wei and Matt Wynn for bootcamps held by
Investigative Reporters and Editors at the University of Missouri in
Columbia, Missouri.
6 https://first-web-scraper.readthedocs.io/en/latest/
CONTENTS 40
Amazon web scraping for Startrek DVD movies with ‘bonus’ content
Solution
1 import re
2 from pprint import pprint
3 import csv
4 import requests
5
6
7 import requests
8 from bs4 import BeautifulSoup
9 def crawl_amazon_web(page,WebUrl):
10 if(page>0):
11 url = WebUrl
12 code = requests.get(url)
13 plain = code.text
14 s = BeautifulSoup(plain, "html.parser")
15
16 for link in s.findAll('a', {'class':'s-access-det\
17 ail-page'}):
18 movie_title = link.get('title')
19 m = re.search( r'Bonus',movie_title)
20 if m:
21 print(movie_title)
22 html_link = link.get('href')
23 print(html_link)
24
25 crawl_amazon_web(1,'https://www.amazon.com/s/ref=nb_sb_no\
26 ss_2?url=search-alias%3Dmovies-tv&field-keywords=starwars\
27 &rh=n%3A2625373011%2Ck%3Astarwars')
This solution has been adopted and extended from the Dev.to
post9 written by Pranay Das.
Question 1 of 10
By default, a single dot (.) matches:
1. A single char
2. Nothing
3. Unlimited numbers of chars
4. A and B
Correct answer: 1
Question 2 of 10
Regex pattern a+ matches
1. b and aaab
2. ab and aaab
3. b and a+b
4. a+b
Correct answer: 2
9 https://dev.to/pranay749254/build-a-simple-python-web-crawler
CONTENTS 43
Question 3 of 10
Regex [0-9]+ matches one or more occurrence of any digit
1. True
2. False
Correct answer: 1
Question 4 of 10
[^��� Matches any single character that is not in the class.
1. True
2. False
Correct answer: 1
Question 5 of 10
^abc matches - select multi answers
1. 123abc
2. abc123
3. aabc123
4. ^abc
Question 6 of 10
The vertical bar separates two or more alternatives. A match
occurs if any of the alternatives is satisfied. For example, learn
|scientific matches
Correct answer: 1
Question 7 of 10
By default, regular expressions are case-sensitive.
1. True
2. False
Correct answer: 1
Question 8 of 10
When you put a plus sign (+) after something in a regular expres-
sion,
Correct answer: 1
CONTENTS 45
Question 9 of 10
The star (*) has a similar meaning but also allows the pattern to
match zero times.
1. True
2. False
Correct answer: 1
Question 10 of 10
Putting {4} after an element, \d{4}
Correct answer: 1
Question 1 of 5
When you deal with the HTML and XML you may need:
1. XML
2. Verbose
3. Non-greedy matching
4. HTML code
Correct answer: 3
Question 2 of 5
Split the string into a list, splitting it wherever the RE matches
1. splitter()
2. sub()
3. splitn()
4. split()
Correct answer: 4
Question 3 of 5
We use RE compile
Correct answer: 2
Question 4 of 5
What is (?=...) - Multiple correct
4. Look-behind
1. ( and )
2. < and >
3. None
4. 1 and 2
Correct answers: 1
References
• Python REGEX
– Python Regular expression operations10 - Official doco.
– Regular Expression HOWTO11 by A.M. Kuchling
• Educative courses
– Python 3: An interactive deep dive12 by Mark Pilgrim,
implemented by the Educaitve Team
– Python 101: Interactively learn how to program with
Python 313 by Michael Driscoll
– Python 201 - Interactively Learn Advanced Concepts in
Python 314 by Michael Driscoll
10 https://docs.python.org/2/library/re.html
11 https://docs.python.org/2/howto/regex.html
12 https://www.educative.io/collection/10370001/5705097937944576?authorName=
Educative
13 https://www.educative.io/collection/5663684521099264/5707702298738688?authorName=
Michael%20Driscoll
14 https://www.educative.io/collection/5663684521099264/5693417237512192?authorName=
Michael%20Driscoll
CONTENTS 48
40 http://pymotw.com/
41 http://importpython.com/books/
42 http://www.pyschools.com/