/Cw7

Z Brain-wiki

TI:WTBD/Ćwiczenia 7

Rozbiór archiwum poczty (listy dyskusyjnej)

zapisanego w postaci pojedynczego pliku (typu mbox) na poszczególne maile, a następnie -- każdego maila, na nagłówki i treść, za pomocą wyrażeń regularnych.

Przykładowe dane można znaleźć np. w archiwum listy dyskusyjnej o Pythonie -- pod ,,Downloadable version".
#! /usr/bin/python
# coding: utf-8
# mailparse.py

import re
mailsrx = re.compile(r'^From \S+ at .*\n$')
mbodyrx = re.compile(r'\n\n')
headsrx = re.compile(r'^([-\w]+): +((?:.|\n +)*)$', re.MULTILINE)

def lines2mails(lines):
    '''wyciągamy linijka po linijce dopóki nie zbierze się z nich pełny mail,
    co poznamy gdy napotkamy linijkę separującą. Wtedy łączymy linijki 
    w jeden string, i "ustępujemy" (yield).
        lines: iterator podający kolejne linijki
        wynikiem jest generator kolejnych maili'''

    l_out = list()
    for line in lines:
        if not mailsrx.match(line):
            l_out.append(line)
        elif l_out:
            yield ''.join(l_out)
            l_out = list()
    yield ''.join(l_out)

def mailsplit(rawmail):
    '''rozdzielamy "surowy" mail na blok nagłówków i resztę.
    '''
    return tuple(mbodyrx.split(rawmail, maxsplit=1))

def parsehead(head):
    '''analizujemy blok nagłówków na poszczególne nagłówki, tzn. dwójki 
    (klucz, wartość).
    '''
    headers = headsrx.findall(head)
    return headers

def main():
    '''żeby to wszystko przetestować analizujemy plik(i) wejściow(y|e)
    i wypisujemy dane z nagłówków 'From', 'Date' i 'Subject'.'''

    from fileinput import input

    rawmails = lines2mails(input())
    splitmails = (mailsplit(item) for item in rawmails)
    parsedmails = ((parsehead(head), body) for (head, body) in splitmails)
    for pm in parsedmails:
        headers, body = pm
        headers = dict(headers)
        print '{From}\t{Date}\t{Subject}'.format(**headers)
        
if __name__ == '__main__':
    main()

Wykrywanie w plikach z kodem źródłowym w Pythonie instrukcji powodujących import modułów,

i sporządzenie na tej podstawie listy modułów, od których zależy dany kod.

Może to być zaskakujące, ale zadania takiego nie da się zrealizować w 100 procentach. Weźmy pod uwagę np.
'''
import re
'''

za pomocą jedynie technik opartych na wyrażeniach regularnych nie da się w pełni skutecznie rozpoznać, że ta instrukcja import jest częścią stałej napisowej, a nie prawdziwą instrukcją. Mniejsza zresztą o teoretyczną niemożliwość, na pewno w praktyce jest to zbyt trudne -- trzeba wziąć pod uwagę, że w programie może występować wiele stałych napisowych, a więc trzeba liczyć otwarcia/zamknięcia cudzysłowów, w dodatku cudzysłowy różnych rodzajów wolno zagnieżdżać... Widać, że rozbiór składniowy (parsowanie) języków programowania (jak Python) nie może się opierać na technice wyrażeń regularnych. Na szczęście, interesują nas głównie pliki danych, których składnia ma większe szanse być (przynajmniej w przybliżeniu) regularna, czyli możliwa do opanowania wyrażeniami regularnymi. Inne przypadki sprawiające problemy to np.:

if warunek: import re

try: import foobar
except: pass

Zwłaszcza ten pierwszy jest nie do opanowania: warunek może być bardzo złożonym wyrażeniem, z wielokrotnie zagnieżdżonymi nawiasami i cudzysłowami (stałe napisowe)! Te ,,wyjątkowe" przypadki mają tyle ze sobą wspólnego, że są w praktyce rzadkie. To znaczy, że mało kto w rzeczywistości tak pisze kod, a nikt tak pisać nie musi. Możemy więc założyć, że analizujemy kod nie tylko poprawny składniowo (chyba jest już jasne, że poprawności składniowej kodu w Pythonie z zasadniczych powodów nie da się zweryfikować tylko za pomocą wyrażeń regularnych); ale w dodatku przestrzegający pewnych zasad ,,dobrego stylu": np., że import wewnątrz instrukcji warunkowej zaczyna się od nowej linii; albo, że w stałej znakowej, linia zaczynająca się od słowa import powinna być ujęta w oddzielny cudzysłów. Z tymi zastrzeżeniami, spróbujmy znaleźć rozwiązanie, które skutkuje jeśli za przykład testowy potraktować coś takiego jak poniżej:

from os import *
import sys
import string , re
import\
csv
#import lineinput # do not import
from os.path\
    import isfile, isdir

def funkcja():
    import numpy as np
    pass

import numpy.random as rand, pylab

# wynik powinien być: csv numpy numpy.random os os.path pylab re string sys

W szczególności należy wziąć pod uwagę, że pomiędzy jednostkami leksykalnymi (słowami) tworzącymi polecenie, wszędzie tam, gdzie wymagany jest odstęp (ciąg co najmniej 1 spacji/tabulacji), może on mieć również postać '\' plus bezpośrednio po ukośniku przejście do nowej linii. Specyfikacja ogólnej postaci odstępu pomiędzy słowami może więc wyglądać tak: (?:[ \t]|\\\n)+ gdzie do grupowania wyrażeń użyłem (?: ... ) tworzącego grupę nieprzechwytującą (nie interesuje mnie, jakiej dokładnie postaci odstępu rozdzielającego użyto..). Ukośnik '\' musi być potrojony, by został potraktowany raz dosłownie, a drugi raz -- jako tworzący wraz z literą 'n' sekwencję reprezentującą kod nowej linii.

#! /usr/bin/python
# coding: utf-8
# findimports.py

import re

fromimportrx = re.compile(
                        r'^[ \t]*from'     # ewentualne wcięcie
                        r'(?:[ \t]|\\\n)+' # odstępy lub nowa linia z backslashem
                        r'(\w+(?:\.\w+)*)' # nazwa modułu
                        r'(?:[ \t]|\\\n)+' # znów odstępy itd.
                        r'import'          # dalej już nieważne co
                        , re.MULTILINE)
importrx = re.compile(
                    r'^[ \t]*import'
                    r'(?:[ \t]|\\\n)+'
                    r'((?:'
                    r'(?:\w+(?:\.\w+)*)' # nazwa modułu, ewent. słowo `as'
                    r'|(?:[ \t]|\\\n)'   # odstępy itd.
                    r'|,)+)' # ,p1.mod1 as alias1, mod2, p3 ...
                    , re.MULTILINE)

importas = re.compile(
                    r'^(\w+(?:\.\w+)*)'  # nazwa
                    r'(?:'
                    r'(?:[ \t]|\\\n)+'
                    r'as'
                    r'(?:[ \t]|\\\n)+'
                    r'\w+'               # alias, nieważne jaki
                    r')?'
                    r'$')

def find_imports(source):
    imports = set()
    found = importrx.findall(source)
    for s in found:
        imports |= set(importas.search(sss).group(1)
                        for sss in (
                            ss.strip() for ss in s.split(',')
                            )
                    )
    found = fromimportrx.findall(source)
    imports |= set(found)
    return imports

def test():
    imports = find_imports(test_string)
    for item in sorted(imports):
        print item,
    print

test_string = '''
from os import *
import sys
import string , re
import\
csv
#import lineinput # do not import
from os.path\
    import isfile, isdir

def funkcja():
    import numpy as np
    pass

import numpy.random as rand, pylab

# wynik powinien być: csv numpy numpy.random os os.path pylab re string sys
'''

def main():
    from optparse import OptionParser
    parser = OptionParser()
    parser.add_option('-t', '--test', action='store_true', dest='test', default=False)
    parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False)
    opts, args = parser.parse_args()
    if opts.test:
        test()
        return
    imports = dict()
    for fname in args:
        imports[fname] = find_imports(open(fname).read())
    all_imports = set().union(*imports.values())
    if opts.verbose:
        print 'All imports:',
    for item in sorted(all_imports):
        print item,
    print
    if opts.verbose:
        for fname in args:
            print fname + ':',
            for item in sorted(imports[fname]):
                print item,
            print

if __name__ == '__main__':
    main()
  • Do gromadzenia nazw importowanych modułów wykorzystuję zbiory, ponieważ nie interesuje mnie, ile razy który moduł był importowany, tylko czy był;
  • polecenia postaci import mod1, mod2 as alias, mod3... analizuję w dwóch etapach, za pomocą drugiego i trzeciego wyrażenia regularnego;
  • opcja --verbose (równoważnie, -v) powoduje wypisanie, oprócz listy nazw modułów importowanych we wszystkich plikach wymienionych na linii poleceń łącznie, list osobno dla poszczególnych plików.
  • opcja re.MULTILINE (w funkcji re.compile) służy temu, by ^ i $ pasowały do początku i końca linii, a nie jedynie początku i końca całego napisu.

C.D.N.