Kirjoittaja: qalle
Kirjoitettu: 28.05.2016 – 17.07.2016
Tagit: ohjelmointitavat, teksti, koodi näytille, sovellus, vinkki
Ohjelma lukee tekstitiedoston sekä listaa kunkin siinä esiintyvän merkin, sen Unicode-koodipisteen, tyypin (General Category), nimen, esiintymismäärän ja osuuden kaikista merkeistä. Listaus kirjoitetaan UTF-8-koodattuna toiseen tekstitiedostoon lajiteltuna koodipisteiden, nimien, tyyppien tai esiintymismäärän mukaan.
Ohjelma demonstroi unicodedata-moduulia, lajittelua, komentoriviparametreja (getopt-moduuli) ja tekstipohjaisen taulukon tulostamista siististi (kunkin sarakkeen leveydeksi tulee pisin siinä esiintyvä arvo).
charfreq.py v. 1.2 - laskee tekstitiedostossa esiintyvien merkkien määrät ja kirjoittaa ne UTF-8-muotoiseen tekstitiedostoon Parametrit: ASETUKSET LÄHDE KOHDE ASETUKSET (kaikki vapaaehtoisia): -eX, --encoding=X määritä lähdetiedoston merkistökoodaukseksi X; esimerkkejä: utf-8 UTF-8 (oletus) utf-16-le UTF-16 little-endian utf-16-be UTF-16 big-endian cp1252 Windows-1252 katso myös: http://docs.python.org/3/library/codecs.html#standard-encodings -l (pieni L), --lowercase laske kaikki lähdetiedoston kirjaimet pieniksi kirjaimiksi -sX, --sort=X järjestys, jossa merkit listataan; X on: c koodipiste (oletus) n Unicode-nimi g tyyppi (General Category) f esiintymismäärä -7, --7bit älä näytä kohdetiedostossa muiden kuin 7-bittiseen ASCII:hin kuuluvien merkkien ulkoasua LÄHDE: luettavan tekstitiedoston nimi KOHDE: kirjoitettavan tekstitiedoston nimi
Lähdetiedosto: "xxx.txt", utf-8, 15 tavu(a), 13 merkki(ä), 10 eri merkki(ä) Laskettiinko kaikki kirjaimet pieniksi: ei Sarakkeet: - koodipiste 10-kantaisena - koodipiste 16-kantaisena - tyyppi (General Category) - merkki (jos tulostettava) - nimi - esiintymismäärä - osuus kaikista merkeistä 10 A Cc (tuntematon) 1 7.69% 32 20 Zs SPACE 1 7.69% 46 2E Po . FULL STOP 1 7.69% 97 61 Ll a LATIN SMALL LETTER A 3 23.08% 101 65 Ll e LATIN SMALL LETTER E 2 15.38% 105 69 Ll i LATIN SMALL LETTER I 1 7.69% 111 6F Ll o LATIN SMALL LETTER O 1 7.69% 117 75 Ll u LATIN SMALL LETTER U 1 7.69% 337 151 Ll ő LATIN SMALL LETTER O WITH DOUBLE ACUTE 1 7.69% 916 394 Lu Δ GREEK CAPITAL LETTER DELTA 1 7.69%
"""charfreq.py - laskee tekstitiedostossa esiintyvien merkkien määrät""" import sys import os.path import getopt import time import unicodedata # merkkejä, joiden General Category -ominaisuuden ensimmäinen merkki Unicodessa # on jokin näistä, ei tulosteta (mm. ohjausmerkkejä, kuten rivinvaihtoja) NONPRINTABLE_HILEVEL_CATS = ("C", "M", "Z") # monenko lähdetiedostosta luetun rivin välein tulostetaan näytölle piste (".") # edistymisen merkiksi LINES_PER_DOT = 10 ** 6 # sarakkeiden väliin tulostettava merkki/merkit COLUMN_SEPARATOR = " " # mitä näytetään niiden merkkien nimenä, joita ei löydy unicodedata:sta UNKNOWN_CHAR_NAME = "(tuntematon)" HELP_TEXT = """\ charfreq.py v. 1.2 - laskee tekstitiedostossa esiintyvien merkkien määrät ja kirjoittaa ne UTF-8-muotoiseen tekstitiedostoon Parametrit: ASETUKSET LÄHDE KOHDE ASETUKSET (kaikki vapaaehtoisia): -eX, --encoding=X määritä lähdetiedoston merkistökoodaukseksi X; esimerkkejä: utf-8 UTF-8 (oletus) utf-16-le UTF-16 little-endian utf-16-be UTF-16 big-endian cp1252 Windows-1252 katso myös: http://docs.python.org/3/library/codecs.html#standard-encodings -l (pieni L), --lowercase laske kaikki lähdetiedoston kirjaimet pieniksi kirjaimiksi -sX, --sort=X järjestys, jossa merkit listataan; X on: c koodipiste (oletus) n Unicode-nimi g tyyppi (General Category) f esiintymismäärä -7, --7bit älä näytä kohdetiedostossa muiden kuin 7-bittiseen ASCII:hin kuuluvien merkkien ulkoasua LÄHDE: luettavan tekstitiedoston nimi KOHDE: kirjoitettavan tekstitiedoston nimi\ """ # kohdetiedoston alkuun kirjoitettava teksti INTRO_TEXT = """\ Lähdetiedosto: "{file:s}", {encoding:s}, {fileSize:d} tavu(a), \ {totalCharCount:d} merkki(ä), {uniqueCharCount:d} eri merkki(ä) Laskettiinko kaikki kirjaimet pieniksi: {lowerCase:s} Sarakkeet: - koodipiste 10-kantaisena - koodipiste 16-kantaisena - tyyppi (General Category) - merkki (jos tulostettava) - nimi - esiintymismäärä - osuus kaikista merkeistä \ """ def format_for_stdout(text): """Muotoilee tekstin niin, että se voidaan tulostaa stdout:iin. (Korvaa ei-tulostettavat merkit kenoviiva-alkuisilla koodeilla.) Parametrit: text: teksti merkkijonona Palautusarvo: teksti merkkijonona """ return ( text .encode(sys.stdout.encoding, errors = "backslashreplace") .decode(sys.stdout.encoding) ) def count_chars(hnd, lowerCase): """Laskee eri koodipisteiden esiintymismäärät tiedostossa. Parametrit: hnd: luettavan tiedoston kahva lowerCase: lasketaanko kaikki kirjaimet pieniksi Palautusarvo: dict, jossa koodipisteet avaimina ja esiintymismäärät arvoina Tulostaa myös tiedoston lukemisen edistymisen näytölle pisteinä ("."). """ hnd.seek(0) freqs = {} for (lineNum, line) in enumerate(hnd): if lowerCase: line = line.lower() for char in line: CP = ord(char) freqs[CP] = freqs.get(CP, 0) + 1 if lineNum % LINES_PER_DOT == 0: print(".", end = "", flush = True) print() return freqs def digits_required(number, base = 10): """Laskee, montako numeroa kokonaisluvun esittämiseen tarvitaan. (Logaritmit olisivat epätarkkoja suurten lukujen kanssa.) Parametrit: number: esitettävä kokonaisluku base: esityksen kantaluku (2, 8, 10 tai 16; oletus on 10) Palautusarvo: luvun esittämiseen tarvittavien numeroiden määrä kokonaislukuna """ # valitse kantalukukoodi format()-funktiota varten code = {2: "b", 8: "o", 10: "d", 16: "x"}.get(base) if code is None: raise ValueError return len(format(number, code)) def create_line_format(CPFreqs): """Muodostaa .format()-metodille kelpaavan muotoilukoodin ohjelman tulosrivien kirjoittamista varten. Parametrit: CPFreqs: dict, jossa koodipisteet avaimina ja esiintymismäärät arvoina Palautusarvo: muotoilukoodi merkkijonona """ # suurin koodipiste maxCP = max(CPFreqs) # yleisimmän merkin esiintymismäärä maxFreq = max(CPFreqs.values()) # merkkien kokonaismäärä totalChars = sum(CPFreqs.values()) # montako merkkiä tarvitaan merkkien nimien esittämiseen maxNameLen = max(len(unicodedata.name(chr(CP), "")) for CP in CPFreqs) maxNameLen = max(maxNameLen, len(UNKNOWN_CHAR_NAME)) # montako merkkiä tarvitaan prosenttiosuuksien esittämiseen maxPercentageLen = len(format(maxFreq / totalChars * 100, ".2f")) # muodosta muotoilukoodi return COLUMN_SEPARATOR.join(( "{{CP:{0:d}d}}".format(digits_required(maxCP)), "{{CP:{0:d}X}}".format(digits_required(maxCP, 16)), "{cat:s}", "{char:s}", "{{name:{0:d}s}}".format(maxNameLen), "{{freq:{0:d}d}}".format(digits_required(maxFreq)), "{{percentage:{0:d}.2f}}%".format(maxPercentageLen), )) def sort_chars(CPFreqs, sortBy): """Lajittelee koodipisteet tulostusta varten. Parametrit: CPFreqs: dict, jossa koodipisteet avaimina ja esiintymismäärät arvoina sortBy: lajitteluperuste: c = koodipiste n = Unicode-nimi, koodipiste g = tyyppi (General Category), koodipiste f = esiintymismäärä, koodipiste Palautusarvo: lista, jossa lajitellut koodipisteet """ # lajittele nousevasti koodipisteen mukaan; tämä jää toissijaiseksi # lajitteluperusteeksi, jos myöhemmin tehdään toinen lajittelu sortedCPs = sorted(CPFreqs) # mahdollinen toinen lajittelu if sortBy == "n": # nouseva Unicode-nimijärjestys sortedCPs.sort(key = lambda CP: unicodedata.name(chr(CP), "")) elif sortBy == "g": # nouseva tyyppijärjestys (General Category) sortedCPs.sort(key = lambda CP: unicodedata.category(chr(CP))) elif sortBy == "f": # laskeva yleisyysjärjestys sortedCPs.sort(key = lambda CP: CPFreqs[CP], reverse = True) elif sortBy != "c": raise ValueError return sortedCPs def print_chars(CPFreqs, sortBy, ASCIIOnly, hnd): """Kirjoittaa merkkien tiedot kohdetiedostoon. Parametrit: CPFreqs: dict, jossa koodipisteet avaimina ja esiintymismäärät arvoina sortBy: lajitteluperuste, ks. sort_chars() ASCIIOnly: näytetäänkö vain 7-bittiseen ASCII:hin kuuluvien merkkien ulkoasu (boolean) hnd: kirjoitettavan tiedoston kahva Palautusarvo: None """ # muodosta tulosrivien muotoilukoodi lineFormat = create_line_format(CPFreqs) # lajittele koodipisteet tulostusta varten sortedCPs = sort_chars(CPFreqs, sortBy) # merkkien kokonaismäärä totalChars = sum(CPFreqs.values()) # kirjoita merkkien tiedot kohdetiedostoon for CP in sortedCPs: char = chr(CP) cat = unicodedata.category(char) freq = CPFreqs[CP] if cat[0] in NONPRINTABLE_HILEVEL_CATS or ASCIIOnly and CP >= 0x80: printableChar = " " else: printableChar = char print(lineFormat.format( CP = CP, cat = cat, char = printableChar, name = unicodedata.name(char, UNKNOWN_CHAR_NAME), freq = freq, percentage = freq / totalChars * 100, ), file = hnd) return None def main(): """Pääohjelma.""" # koko ohjelman suoritusaika näytetään lopuksi startTime = time.time() # jos komentoriviparametreja ei ole annettu, näytä ohje ja poistu if len(sys.argv) == 1: exit(HELP_TEXT) # lue komentoriviparametrit getopt-moduulilla (opts:iin "-"- ja # "--"-alkuiset, args:iin muut) try: (opts, args) = getopt.getopt( sys.argv[1:], "e:ls:7", ["encoding=", "lowercase", "sort=", "7bit"] ) except getopt.GetoptError: exit('Virhe "-"- ja "--"-alkuisissa komentoriviparametreissa.') if len(args) != 2: exit("Virhe: tiedostoparametreja on oltava kaksi.") # muunna "-"-alkuiset parametrit dict:iksi, jotta ne on helpompi lukea opts = dict(opts) # hae asetukset parametreista (sourceFile, targetFile) = args inputEncoding = opts.get("--encoding", opts.get("-e", "utf-8")) lowerCase = "--lowercase" in opts or "-l" in opts sortBy = opts.get("--sort", opts.get("-s", "c")) printASCIIOnly = "--7bit" in opts or "-7" in opts # poistu, jos lajitteluperuste ei kelpaa if sortBy not in ("c", "n", "g", "f"): exit("Virhe: lajitteluperuste ei kelpaa.") # poistu, jos merkistökoodaus on tuntematon try: temp = "a".encode(inputEncoding) except LookupError: exit( 'Virhe: tuntematon merkistökoodaus "{0:s}".' .format(format_for_stdout(inputEncoding)) ) # poistu, jos lähde- ja kohdetiedosto ovat samat try: if os.path.samefile(sourceFile, targetFile): exit("Virhe: lähde- ja kohdetiedosto eivät saa olla samat.") except FileNotFoundError: pass except OSError: # esim. "zzz:" Windowsissa pass # lue lähdetiedosto print( 'Luetaan lähdetiedosto "{0:s}"...' .format(format_for_stdout(os.path.basename(sourceFile))) ) try: with open(sourceFile, "rt", encoding = inputEncoding) as inHnd: fileSize = inHnd.seek(0, 2) if fileSize == 0: exit("Virhe: tiedosto on tyhjä.") # laske merkkien esiintymismäärät try: CPFreqs = count_chars(inHnd, lowerCase) except UnicodeDecodeError: exit( 'Virhe: tiedosto ei ole merkistökoodauksen "{0:s}" ' 'mukainen.'.format(format_for_stdout(inputEncoding)) ) except FileNotFoundError: exit("Virhe: tiedostoa ei löydy.") except PermissionError: exit("Virhe: tiedoston lukemiseen ei ole oikeuksia.") except Exception as e: exit("Virhe luettaessa tiedostoa: " + str(e)) # kirjoita kohdetiedosto print( 'Kirjoitetaan kohdetiedosto "{0:s}"...' .format(format_for_stdout(os.path.basename(targetFile))) ) try: with open(targetFile, "wt", encoding = "utf-8") as outHnd: # kirjoita alkutekstit print(INTRO_TEXT.format( file = os.path.basename(sourceFile), fileSize = fileSize, encoding = inputEncoding, lowerCase = "kyllä" if lowerCase else "ei", totalCharCount = sum(CPFreqs.values()), uniqueCharCount = len(CPFreqs), ), file = outHnd) # kirjoita merkkien tiedot print_chars(CPFreqs, sortBy, printASCIIOnly, outHnd) except FileNotFoundError: exit("Virhe: tiedoston polkua ei ole olemassa.") except PermissionError: exit("Virhe: tiedoston kirjoittamiseen ei ole oikeuksia.") except Exception as e: exit("Virhe kirjoitettaessa tiedostoa: " + str(e)) print("OK. Aikaa meni {0:.1f} s.".format(time.time() - startTime)) # suorita pääohjelma, jos tätä moduulia ei ajeta toisen moduulin alla if __name__ == "__main__": main()
Hyvä vinkki, julkaistaan saman tien.
Pari korjausta myöhemmäksi:
Hakasulut ovat turhat kohdassa max([generaattori]); tarpeettomasti luodaan väliaikainen lista, vaikka max voisi suoraan käsitellä generaattorin tuottamia arvoja.
Näin pitkässä koodissa herää jo kysymys, voisiko työvaiheita koota funktioiksi.
Uusi versio 1.1. Ks. versiohistoria.
Siirtäisin lopunkin koodin funktioihin lukuunottamatta
if __name__ == '__main__': main()
-kohtaa. Tällöin kooditiedostosi toimisi sujuvasti importin avulla toisesta sovelluksesta käsin.
Koodin tyyli poikkeaa hieman suositusta PEP 8 -ohjeesta. Se ei ole välttämättä huono asia, mutta moni Python-käyttäjä on tottunut sen mukaiseen koodiin.
Tiedostojen osalta käyttäisin with-rakennetta erillisten open- ja close-kutsujen sijasta. Siinä on omat etunsa.
Kiitos. Uusi versio 1.2. Ks. versiohistoria.