Exercise 1 solution import re acc_re = re.compile( r'(?P\S+) \S+ \S+ \[(?P[^\]]+)\] ' r'"(?P[A-Z]+) (?P\S+) [^"]*" ' r'(?P\d{3}) (?P\d+|-)' ) def parse_access(line): m = acc_re.fullmatch(line) if not m: return None d = m.groupdict() d["status"] = int(d["status"]) d["size"] = 0 if d["size"] == "-" else int(d["size"]) return d lines = [ '192.168.1.5 - - [02/Nov/2026:09:15:03] "GET /index.html HTTP/1.1" 200 512', '10.0.0.8 - - [02/Nov/2026:09:15:09] "POST /login HTTP/1.1" 302 -', 'not an access line', ] for l in lines: print(parse_access(l)) # {'ip': '192.168.1.5', 'when': '02/Nov/2026:09:15:03', 'method': 'GET', 'path': '/index.html', 'status': 200, 'size': 512} # {'ip': '10.0.0.8', 'when': '02/Nov/2026:09:15:09', 'method': 'POST', 'path': '/login', 'status': 302, 'size': 0} # None (All lines were run and printed exactly what is shown.) Explanation: [^\]]+ takes everything up to the closing bracket, and [^"]* takes the rest of the quoted request without crossing the closing quote. The size group allows digits or a single hyphen, and the dictionary values for status and size are converted afterwards, once we know they matched.