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 nimiLä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.