A címkefelhők/szófelhők népszerűek, sok weboldalon megtalálhatóak. A CMS rendszerekben beépített szolgáltatás is lehet, vagy külön bővítmény/plugin is megvalósíthatja. Egy szövegben előforduló szavakból a gyakrabban előfordulókat nagyobb betűmérettel emeli ki. Eredménye lehet listás, táblázatos, esetleg képpé generált is. Kétféleképpen is megközelíthető, erre utal a Word Cloud és a Tag Cloud elnevezés. Utóbbi inkább egy blog taxonomiájához kapcsolódik és kategóriákra/címkékre érvényesül. A szakmai blogunkhoz is tartozik egy táblázatos címkefelhő. A szófelhő a szöveg betűméretén túl megjelenítheti a szavak előfordulását, például Java forráskód (70), címkefelhő (2).
Példánkban tetszőleges szöveget dolgozunk fel. Ebből felépítünk egy előfordulást is mutató listás szófelhőt, amely rendezett, és a szavak betűmérete 32-16-ig változik. Azok a szavak kerülnek a szófelhőbe, amelyek legalább 5-ször előfordulnak. Kezelünk kivételeket is, például olyan szavakat, amiket nem érdemes szófelhőbe tenni. Lépésenként haladva ismertetjük a megvalósító forráskódot, és külön megjeleníthetők az egyes lépések részeredményei.
A Java programozási nyelv csomagjait, osztályait, interfészeit, metódusait, műveleteit használjuk. Különböző adatszerkezetek kerülnek elő: tömb, generikus lista, generikus map, generikus folyam. Építünk a Stream API szolgáltatásaira és a lambda kifejezésekre. A megvalósítás könnyen testre szabható, kezeli a tipikusan előforduló igényeket.
1. Szövegforrás előkészítése
Generálunk egy 10 bekezdésből álló szöveget a Lorem Ipsum – All the facts – Lipsum generator weboldalon és a későbbi feldolgozáshoz mentjük a Java projekt files mappájába lorem.txt néven. A fájl mérete: 5781 bájt. Szövegfájl:
2. Szöveges tartalom előkészítése
1 2 3 |
String s=new String(Files.readAllBytes(Paths.get("./files/lorem.txt"))); s=s.replace("\n", "").replace("\r", "").replace(",", ""). replace(".", "").toLowerCase(); |
A megadott útvonalról a java.nio csomag metódusaival betöltjük a szövegfájl tartalmát byte[]-be, majd az s szövegbe. A replace() metódus hívásaival eltávolítjuk a szövegből a sor és bekezdés végét jelző soremelés ( LF="\n") és kocsi vissza ( CR="\r") vezérlőkaraktereket, a vessző és a pont írásjeleket (mindet külön-külön cseréljük a semmire), végül kisbetűssé alakítjuk ( toLowerCase()) a szöveget. A szöveg 5563 db karakterből áll. Előkészített szöveg:
3. Szólista elkészítése
1 |
List<String> wordList=Arrays.asList(s.split(" ")); |
A szóközök mentén darabolva ( split()) a szöveget elkészül belőle egy névtelen szövegtömb ( String[]), amit rögtön átalakítunk ( Arrays.asList()) szöveg típusú generikus listává ( List<String>). A lista 826 db elemből áll. Generikus lista:
4. Csoportosítás és megszámolás
1 2 |
Map<String, Long> wordCountMap=wordList.stream().collect( Collectors.groupingBy(Function.identity(), Collectors.counting())); |
A szólistát csoportosítjuk és megszámoljuk, hogy az egyes szavak hányszor fordulnak elő (másképpen: egy-egy csoport hány elemű). Elkészül a wordCountMap generikus map, amely kulcs-érték párok halmaza (leképezés). A kulcs a szó ( String), az érték a darabszáma ( Long). Alkalmazkodunk ahhoz, hogy a csoportosítás során használt counting() megszámoló művelet Long típusú értéket ad vissza. 188 db kulcs-érték párt kapunk. Generikus map:
5. Szűrés és rendezés
1 2 3 4 5 6 7 8 9 10 11 |
List<String> exceptList= Arrays.asList(new String[] {"at", "et", "in", "ut"}); Stream<Entry<String, Long>> sortedWordCountStream= wordCountMap.entrySet().stream(). filter(e -> !exceptList.contains(e.getKey())). filter(e -> e.getValue()>=5). sorted((e1, e2) -> (e1.getValue().equals(e2.getValue())) ? e1.getKey().compareTo(e2.getKey()) : e2.getValue().compareTo(e1.getValue()) ); |
A generikus map-et kétszer szűrjük ( filter() művelet) úgy, hogy a kivételeket tartalmazó exceptList-ben ne szerepeljen a szó, valamint csak a legalább 5-ször előforduló szavakat hagyjuk meg. 71 db elemből álló folyam marad. Ebből a maradékból készítünk rendezett generikus folyamot ( sortedWordCountStream). A sorted() művelet két kulcs-érték párt hasonlít össze. A rendezés érték/darabszám szerint ( getValue()) csökkenő, azon belül kulcs/szavak szerint ( getKey()) növekvő sorrendet biztosít. Másképpen: ha az értékek megegyeznek, akkor a növekvő sorrendet a szavak ábécé sorrendje határozza meg, egyébként a darabszámok csökkenő sorrendje dönti el. Most már könnyen látható, hogy a leggyakrabban előforduló kevés szóból 15 van, 14 előfordulás nincs… Rendezett generikus folyam:
6. Saját típusú listává konvertálás
Definiálunk egy WordCount POJO-t, String típusú word nevű, Long típusú count nevű, int típusú fontSize nevű tulajdonságokkal, getter/setter metódusokkal, és toString() függvénnyel.
1 2 3 4 |
List<WordCount> sortedWordCountList= sortedWordCountStream. map(e -> new WordCount(e.getKey(), e.getValue())). collect(Collectors.toList()); |
A map() intermediate művelettel a rendezett generikus folyamot bejárva, előállítjuk a POJO/ WordCount típusú kimeneti objektumok rendezett generikus listáját. Továbbra is 71 elemmel dolgozunk. Rendezett generikus lista:
7. Darabszámok összegyűjtése
1 2 3 |
List<Long> distinctCountList= sortedWordCountList.stream().map(e -> e.getCount()).distinct(). collect(Collectors.toList()); |
A POJO típusú rendezett generikus listában lévő objektumoktól elkért darabszámok ( getCount() POJO függvény) közül a különbözőeket ( distinct() művelet) összegyűjtjük egy Long típusú generikus listába ( distinctCountList). Az egyediesítő művelet nincs hatással az adatok sorrendjére. Tízféle előfordulást kapunk. Generikus lista:
8. Betűméret lépésköze
1 2 3 4 |
final int MAX_FONT_SIZE=32; final int MIN_FONT_SIZE=16; long countCount=distinctCountList.size(); double stepFontSize=(double)(MAX_FONT_SIZE-MIN_FONT_SIZE+1)/countCount; |
A szófelhőben a szavak gyakorisága alapján határozzuk meg a betűméretet. A betűméret 32-ről indul és fokozatosan csökken 16-ig. A betűméret léptetéséhez a tízféle gyakoriság/előfordulás meghatározza a stepFontSize lépésközt. Lépésköz:
9. Betűméret kiszámítása
1 2 3 4 5 6 7 8 9 10 11 |
int i=0, gi=0; while(i<sortedWordCountList.size()) { long count=sortedWordCountList.get(i).getCount(); int fontSize=(int)Math.round(MAX_FONT_SIZE-gi*stepFontSize); while(i<sortedWordCountList.size() && count==sortedWordCountList.get(i).getCount()) { sortedWordCountList.get(i).setFontSize(fontSize); i++; } gi++; } |
Csoportváltást alkalmazunk és a csoportot gi-vel indexeljük. Egy csoportba azok a POJO objektumok tartoznak, amelyeknél a szavak előfordulása megegyezik. Az algoritmus 2. lépésében az aktuális csoportra érvényesen kiszámítjuk a betűméretet ( fontSize), ami az algoritmus 3. lépésében a csoportba tartozó minden POJO objektumnál beállításra kerül a setFontSize() POJO eljárással. Az algoritmus 4. lépésében léptetjük a csoport gi indexét. A POJO-k esetén először csak a word és count tulajdonságok kerültek beállításra, de most már a fontSize tulajdonság is értéket kapott. Generikus lista:
10. HTML tartalom előállítása
1 2 3 4 5 6 7 |
StringBuilder sbHTML=new StringBuilder("<p>"); sortedWordCountList.forEach(wordCount -> sbHTML.append("<span style=\"font-size: "). append(wordCount.getFontSize()).append("px\">"). append(wordCount.toString()).append(" </span>") ); sbHTML.append("</p>"); |
A generikus lista POJO objektumain végighaladva, a forEach() záró művelettel összeállítható a weboldal szófelhőt tartalmazó része ( sbHTML). A 71 db szóból álló szófelhő HTML forráskódjának mérete 3409 bájt. HTML forráskód:
Eredmény
Szöveges formában:
Képként (a 3. lépés részeredményéből a WordClouds.com weboldalon generálva):
A bejegyzéshez tartozó teljes forráskódot ILIAS e-learning tananyagban tesszük elérhetővé tanfolyamaink résztvevői számára.
A feladat a Java SE szoftverfejlesztő tanfolyam szakmai moduljának több alkalmához is kötődik. A Stream API-val és a lambda kifejezésekkel sokszor foglalkozunk.
Az azonos darabszámú szavakat a 10. lépésben egyetlen
span
elemmel is elő tudtam állítani. Megmutatnám a holnapi órán.Miklós: rendben, kíváncsi vagyok a megoldásodra. Én is átgondoltam ciklussal és funkcionális művelettel is. Hasonlítsuk majd össze a megoldásainkat.
Végül erre jutottunk a 10. lépés közös továbbfejlesztésével. A csoportváltás algoritmust használtuk Miklóssal.
Java forráskód:
Szöveges eredmény:
A szöveg hossza: 1152.
Szuper Miklós, Balázs: akit motivált ez a továbbfejlesztés, oldja meg ezt a részfeladatot funkcionális programozással. Várjuk a megoldást.
A Rómeó és Júlia példa átalakításával kaptam 23692 szót. Generáltam belőle szófelhőt:
Kösz Erik. Nagyon kreatív vagy. Megnézem majd az ILIAS-ra feltöltött megoldásodat, mert kíváncsi vagyok hogyan kezelted az írásjeleket a szavak végén.