Splitte opp tekst i enkeltkomponenter

Innledning

Som verktøy for å splitte opp strenger i enkeltkomponenter, blir det for ny kode anbefalt å bruke String-klassens split-metode eller noen av verktøyene i pakken java.util.regex. Det er også mulig å bruke en Scanner til slik oppsplitting. En Scanner kan som kilde for det som skal splittes opp også bruke en hvilken som helst input-strøm, for eksempel en fil. Dessuten kan den gjenkjenne forskjellige datatyper. Tidligere var det objekter av klassen StringTokenizer som ble brukt til å foreta oppsplitting av tekst i enkeltkomponenter. Av hensyn til kompatibilitet bakover finnes klassen fortsatt, men anbefales ikke brukt i ny kode. Bruk av StringTokenizer-objekter er beskrevet i notatet Lese tekst ord for ord ved hjelp av en StringTokenizer.

For å definere skille mellom enkeltkomponentene i den teksten som skal splittes opp, brukes det et regulært uttrykk. (For en StringTokenizer brukes det bare en streng.) Hva som menes med regulære uttrykk er beskrevet i notatet Regulære uttrykk.

String-klassens split-metode

Metoden finnes i to versjoner. Som regel er det den som har bare én parameter vi har bruk for:

  public String[] split(String regex)

Parameteren regex er det regulære uttrykket som definerer skillet mellom enkeltkomponentene. Metoden returnerer er en array som inneholder de enkelte komponentene som oppsplittingen har gitt. Arrayen kan være av vilkårlig størrelse. Merk deg imidlertid at oppsplittingen kan inneholde komponenter som består av tomme strenger. Men eventuelle tomme strenger på slutten vil ikke bli tatt med. Dersom den strengen som skal splittes opp ikke har noen delstreng som passer med det regulære uttrykket, vil den returnerte arrayen ha ett element og dette inneholder den streng som skulle splittes opp. Metoden har samme virkning som å bruke den andre versjonen av metoden (se nedenfor) med grense-parameter lik null.

Eksempel 1

  String test = "Dette er en test på bruk av split-metoden.";
  String[] resultat = test.split( "\\s" );
  for ( int i = 0; i < resultat.length; i++ )
    System.out.println( resultat[ i ] );

vil gi følgende utskrift:

  Dette
  er
  en
  test
  på
  bruk
  av
  split-metoden.

Husk at det regulære uttrykket \s betyr såkalt whitespace. Siden uttrykket inngår i en streng når det brukes i metodekallet, må det være en ekstra bakoverskråstrek foran. (Se notatet om regulære uttrykk.) Bindestreken og punktumet i den siste komponenten som ble skrevet ut, er ikke whitespace. Derfor får vi det ut på denne måten.

Eksempel 2
Strengen "20:00:00" resulterer i følgende oppsplitting med de regulære uttrykk som er angitt i tabellens første kolonne:

Regulært uttrykkResultat
:{ "20", "00", "00" }
0{ "2", ":", "", ":" }

Legg merke til at vi i det siste tilfelle får en tom streng innimellom, men at den tomme strengen på slutten ikke blir tatt med i returen.

Den andre versjonen av split-metoden har to parametre:

  public String[] split(String regex, int grense)

Første parameter har samme betydning som i første versjon av metoden. Parameteren grense kontrollerer antall ganger det regulære uttrykket skal brukes. Parameteren vil derfor påvirke størrelsen på den returnerte arrayen. Dersom grensen n er større enn null, vil det regulære uttrykket bli brukt høyst n - 1 ganger. Den returnerte arrayen kan dermed ikke få mer enn n plasser. Siste element i arrayen vil da inneholde all tekst i kildestrengen som følger etter siste match til det regulære uttrykket. Dersom n er null eller negativ, vil det regulære uttrykket bli brukt så mange ganger som mulig og den returnerte arrayen kan ha hvilken som helst lengde. Det samme er tilfelle når n er null, men eventuelle tomme strenger på slutten vil da bli droppet.

Eksempler
Følgende eksempler viser hvordan forskjellige verdier for grenseparameteren påvirker oppsplittingen av strengen "20:00:00" med de regulære uttrykk som er angitt i tabellens første kolonne.

Regulært uttrykkGrenseResultat
:2{ "20", "00:00" }
:5{ "20", "00", "00" }
:-2{ "20", "00", "00" }
05{ "2", ":", "", ":", "" }
0-2{ "2", ":", "", ":", "" }
00{ "2", ":", "", ":" }

Legg merke til når den returnerte arrayen inneholder tomme strenger på slutten, og når den ikke gjør det.

Splitte opp input ved hjelp av en Scanner

En Scanner kan brukes til å splitte opp tekstlig input i enkeltkomponenter. Som input kan det brukes en streng eller en strøm, for eksempel en fil eller input fra tastaturet. Byte fra inputstrømmen blir konvertert til tegn. Det kan enten være i samsvar med default-tegnsettet til den underliggende plattform (noe som er unødvendig å spesifisere), eller i samsvar med et spesifisert tegnsett. I tillegg til å splitte opp i strengkomponenter har en Scanner evne til å gjenkjenne verdier av javas primitive datatyper (gitt på strengform), unntatt typen char, slik at den kan returnere neste verdi av en spesifisert type. Også typene BigInteger og BigDecimal kan den gjenkjenne og returnere. I numeriske verdier blir det påstått at det også kan være tusen-separator i samsvar med den formatteringsstandard som gjelder på stedet (eller på en spesifisert lokalitet). I Norge er det mellomrom (blankt tegn) som er den korrekte tusen-separatoren. Strengen "32 767" skulle derfor på korrekt måte bli lest som int-verdien 32767. Det viser seg imidlertid at dette ikke stemmer. Strengen vil bli tolket som to separate heltallsverdier. Derimot blir komma på riktig måte tolket som desimaltegn i en streng som "8,5".

Dersom annet ikke er spesifisert, er det såkalt whitespace som brukes som skille mellom komponentene når oppsplittingen foretas. Men det er ved hjelp av metoden useDelimiter mulig å spesifisere hvilket som helst regulært uttrykk som skille mellom komponentene. Ønsker vi seinere å gå tilbake til default-virkemåten med bruk av whitespace som skille, kan vi oppnå det ved kall på metoden reset().

Når vi er ferdig med å bruke en Scanner, er det nødvendig å lukke den ved kall på dens close()-metode for å signalisere at vi er ferdig med å bruke dens eventuelle underliggende strøm. Denne vil da også automatisk bli lukket.

Eksempel 1

I programmet Forfatternavn.java som er gjengitt nedenfor, blir det fra tekstfeltet input lest inn navn på formatet
Etternavn, Fornavn (eventuelt et eller flere mellomnavn).
Navnene skrives ut igjen i tekstfeltet output på formatet
Fornavn (eventuelt et eller flere mellomnavn) Etternavn.

Eksempel: Innlest navn
Beauvoir, Simone de
blir skrevet ut igjen som
Simone de Beauvoir

I programmet er navnene kalt forfatternavn, men det kan selvsagt være hvilke som helst andre navn på samme format. Driverklasse for programmet finnes i fila Tokentest.java

Innlesing, konvertering og utskrift av navn skjer i metoden konverterNavn(). Den splitter opp det innleste navn ved hjelp av en Scanner der komma og mellomrom oppfattes som skille mellom ordene. Først trekkes etternavnet ut av den innleste strengen og lagres i en egen variabel. Deretter trekkes det ut fornavn og eventuelle mellomnavn så lenge det finnes flere. De tilføyes etter tur i en egen variabel for fornavn og mellomnavn. Til slutt blir strengen som inneholder etternavnet tilføyd, og det hele vises i tekstfeltet for utskrift. For øvrig blir det mellom de enkelte navnene lagt inn mellomrom.

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;

public class Forfatternavn extends JFrame
{
  private JTextField input, output;

  public Forfatternavn()
  {
    super( "Forfatternavn" );
    input = new JTextField( 30 );
    output = new JTextField( 30 );
    output.setEditable( false );
    Container c = getContentPane();
    c.setLayout( new FlowLayout() );
    c.add( new JLabel( "Forfatternavn (ettenavn, fornavn):" ) );
    c.add( input );
    c.add( new JLabel( "Forfatternavn (fornavn etternavn):" ) );
    c.add( output );
    input.addActionListener( new Inputlytter() );
    setSize( 600, 100 );
    setVisible( true );
  }

  public void konverterNavn()
  {
    String navn = input.getText(); //strengen som skal konverteres
    Scanner leser = new Scanner( navn );
    leser.useDelimiter("[,\\s]+"); //komma eller whitespace er skille
    String etternavn = "";
    if ( leser.hasNext() ) // strengen var ikke tom
      etternavn = leser.next(); // henter ut etternavnet
    String fornavn = "";
    while ( leser.hasNext() ) //henter ut fornavn, så mange det er
      fornavn += leser.next() + " ";
    leser.close();

    if ( !etternavn.equals( "" ) )
      output.setText( fornavn + etternavn );
    else
      output.setText( "Ingen navn lest inn." );
  }

  private class Inputlytter implements ActionListener
  {
    public void actionPerformed( ActionEvent e )
    {
      if ( e.getSource() == input )
        konverterNavn();
    }
  }
}

Merknad I eksemplet ovenfor brukes strengen "[,\\s]+" som det regulære uttrykk som definerer skille mellom komponentene. Det betyr at skille er enten minst et blankt tegn (egentlig minst et såkalt whitespace), eller minst et komma, eventuelt etterfulgt av et antall blanke.

Eksempel 2

Programmet ScanSum som er gjengitt nedenfor er hentet fra Sun's Java Tutorial, men litt tilpasset norske forhold. Programmet leser inn tall (i form av sifferstrenger) fra fila tall.txt og summerer dem. (Slik programmet er skrevet, må både programfil og tallfil ligge i en underkatalog som heter tekst.) Tallfila har følgende innhold:

8,5
32767
3,14159
1000000,1

Legg merke til at den inneholder en blanding av hele tall og desimaltall, og at det er brukt komma som desimaltegn. Når programmet blir kjørt, vil det skrive ut følgede resultat:

1032778.74159

Programkode:

package tekst;

import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Scanner;

public class ScanSum
{
  public static void main(String[] args) throws IOException
  {
    Scanner s = null;
    double sum = 0;
    try
    {
      s = new Scanner(
          	new BufferedReader(
                      new FileReader("tekst/tall.txt")));

      while (s.hasNext())
      {
      	if (s.hasNextDouble())
          sum += s.nextDouble();
      	else
          s.next();
      }
    }
    finally
    {
      s.close();
    }
    System.out.println(sum);
  }
}

Eksempel 3: Testprogram for regulære uttrykk

Dersom vi ønsker å sjekke om en gitt tekststreng er i samsvar med et bestemt mønster, er det ikke alltid så lett å finne ut hvordan vi skal skrive et korrekt regulært uttrykk for å beskrive vedkommende mønster. Programmet RegexTestHarness gjengitt nedenfor er en bearbeidet versjon av et program fra Sun's Java Tutorial. Programmets brukervindu er vist på følgende bilde.

I første tekstfeltet skriver man inn det regulære uttrykket (mønsteret) man vil teste på. Det blir lest inn og registrert når man trykker på returtasten. I det andre tekstfeltet skriver man inn den teksten man vil det skal søkes i. Deretter trykker man på returtasten. Søket blir da utført og resultatet blir skrevet ut i tekstområdet nedenfor.

package tekst;

import java.util.regex.Pattern;
import java.util.regex.Matcher;
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;

public class RegexTestHarness extends JFrame
{
  private JTextField input1, input2;
  private JTextArea utskrift;
  private String reguttr, teststreng;
  private Pattern pattern;
  private Matcher matcher;

  public RegexTestHarness()
  {
    input1 = new JTextField( 20 );
    input2 = new JTextField( 30 );
    utskrift = new JTextArea( 10, 40 );
    utskrift.setEditable( false );
    Container c = getContentPane();
    c.setLayout(new FlowLayout());
    c.add(new JLabel("Regulært uttrykk det skal testes på"));
    c.add(input1);
    c.add(new JLabel("Uttrykk det skal søkes i"));
    c.add(input2);
    c.add(new JScrollPane(utskrift));
    Inputlytter lytter = new Inputlytter();
    input1.addActionListener(lytter);
    input2.addActionListener(lytter);
    setSize( 500, 300 );
    setVisible(true);
  }

  public static void main(String[] args)
  {
    new RegexTestHarness();
  }

  private class Inputlytter implements ActionListener
  {
    public void actionPerformed( ActionEvent e )
    {
      if ( e.getSource() == input1 )
      {
        try
        {
          reguttr = input1.getText();
          pattern = Pattern.compile(reguttr);
          utskrift.setText("");
        }
        catch ( PatternSyntaxException pe )
        {
          utskrift.setText("Syntaksfeil i regulært uttrykk!" );
        }
      }
      else if ( e.getSource() == input2 )
      {
        try
        {
          teststreng = input2.getText();
          matcher = pattern.matcher(teststreng);
          boolean found = false;
          while (matcher.find())
          {
            utskrift.append("Jeg fant tekstkomponenten "
              + matcher.group()
              + " med start i indeks " + matcher.start()
              + " og slutt i indeks " + matcher.end() + "\n");
            found = true;
          }
          if(!found)
          {
            utskrift.append("Fant ikke noe som matchet.\n");
          }
        }
        catch ( NullPointerException ex)
        {
          utskrift.setText("Mangler mønster til å sammenlikne med!");
        }
      }
    }
  }
}

Copyright © Kjetil Grønning, 2007