Programujeme ve Visual Basic .NET - 26. díl - vyhledávání a úpravy textu

Tento díl seriálu o VB.NET věnovaný práci s textovými řetězci se zabývá metodami prostředí .NET pro vyhledávání, nahrazování a další podobné úpravy textových řetězců.
Často potřebnou a používanou operací je vyhledávání a náhrada znaků uvnitř delšího řetězce. Použití těchto metod je oproti předchozím poněkud složitější a prostředí VB.NET pro ně nabízí více variant, proto jsme si jejich popis ponechali na závěr výkladu o práci s textem.

Operátor Like

Pokud nepožadujeme určit přesnou pozici v cílovém řetězci, ale spokojíme se zjištěním, zda je uvnitř jiného řetězce lze výhodně využít VB.NET operátor Like, který představuje speciální logický operátor použitelný pouze pro porovnávání řetězců, vracející logickou hodnotu True (pravda) nebo False (nepravda). Oproti metodám pro prohledávání řetězců má tento operátor výhodu, že hledaný řetězec můžeme specifikovat literálem, obsahujícím speciální zástupné znaky, (tzv. wildcards) známé ze příkazů dir a copy souborového systému. Literálu obsahujícímu zástupné znaky se někdy říká maska (pattern) a hledání podle takovéto skupiny znaků se označuje jako hledání "podle masky". Přehled zástupných znaků podporovaných operátorem Like je uveden v následujícím přehledu:

 hledané znaky  nalezené znaky
 ?  Jakýkoliv jednotlivý alfanumerický znak
 *  Nula nebo více alfanumerických znaků pohromadě
 #  Jednotlivá číslice v rozsahu 0 - 9
 [seznam]  Kterýkoliv jednotlivý znak ze seznamu. Seznam může obsahovat rozsah (interval) znaků vymezený pomlčkou.
 [!seznam]  Kterýkoliv jednotlivý znak neobsažený v seznamu. Seznam může obsahovat rozsah znaků vymezený pomlčkou. 

Operátor Like provádí porovnávání řetězců podle aktuálního nastavení direktivy Option Compare (viz díl č. 23 seriálu). Výchozí nastavení prostředí VB.NET je "Binary" - pokud neuvedeme jinak, operátor Like tedy rozlišuje malá a velká písmena. Použití operátoru Like vysvítá z následujících ukázek:

Option Compare Binary
. . . 
Dim bTest As Boolean
bTest = "F" Like "F"      ` Odpovídá řetězec "F" řetězci "F"? Vrací True.
bTest = "F" Like "f"      ` Odpovídá řetězec "F" řetězci "f"? Vrací False.
bTest = "F" Like "FFF"     ` Odpovídá řetězec "F" masce "FFF"? Vrací False.
bTest = "aBBBa" Like "a*a"     ` Obsahuje řetězec "aBBBa" písmena "a" na začátku a na konci
     `   a libovolný počet znaků mezi nimi? Vrací True.
bTest = "F" Like "[A-Z]"     ` Vyskytuje se řetězec "F" v seznamu znaků A - Z? Vrací True.
bTest = "F" Like "[!A-Z]"       ` Nevyskytuje se "F" v seznamu znaků A až Z? Vrací False.
bTest = "a2a" Like "a#a"       ` Obsahuje řetězec "a2a" znak "a" na začátku a na konci a obsahuje
                                 `   mezi nimi jednu číslici z rozmezí 0 až 9? Vrací True.
bTest = "aM5b" Like "a[L-P]#[!c-e]"  ` Splňuje řetězec "aM5b" následující pravidla:
     `   Začíná znakem "a" a libovolným znakem od "L" po "P",
     `   dále následuje libovolná číslice z rozmezí 0 až 9
     `   a končí libovolným znakem, který nepatří mezi znaky "c" až "e"?
     `   Vrací True.
bTest = "BAT123khg" Like "B?T*"   ` Splňuje řetězec "BAT123khg" následující pravidla:
     `   Začíná znakem "B", který je následován 
     `   libovolným alfanumerickým znakem, který 
        `   je následován znakem "T" a 
     `   jedním nebo více znaky libovolného typu? Vrací True.
Poznámka:
  • Ačkoliv operátor Like může být pro většinu v praxi se vyskytujících případů - např. při prohledávání souborů v adresářové struktuře podle masky - docela užitečný, představuje ve skutečnosti jen omezenou skupinu možností, které pro porovnávání řetězců prostředí .NET Framework nabízí podporou regulárních výrazů Perlu, a která je zastřešena metodami Match() a Replace() třídy System.Text.RegExp.
    Podrobnějšímu popisu vyhledávání a záměny řetězců s použitím regulárních výrazů a třídy RegExp bude časem věnován samostatný díl seriálu, ukázky použití masky operátoru Like však mohou dát začátečníkovi určitou představu toho, jak regulární výrazy v praxi vypadají a fungují.

Vyhledávání řetězce uvnitř jiného řetězce

Pro zjištění, zda daný řetězec obsahuje určitý znak, řetězec nebo pole znaků lze použít metody instance třídy System.String IndexOf(), LastIndexOf(), IndexOfAny(), LastIndexOfAny(). Rozhraní modulu Microsoft.VisualBasic.Strings pro první dvě metody dále nabízí alternativy v podobě funkcí InStr() a InStrRev(). Všechny procedury vrací index prvního výskytu hledaného znaku nebo skupiny znaků v řetězci prohledávaném odleva, popř. zprava (LastIndexOf, InStrRev). Klasické a tedy rozšířené funkce modulu Microsoft.VisualBasic.Strings se liší od metod třídy System.String tím, že vracejí pozici číslovanou od jedné, což není konzistentní s indexováním polí .NET a proto bychom je v nových aplikacích neměli používat. S přihlédnutím k této skutečnosti všechny ukázky níže povedou k témuž výsledku:

Dim sInput As String = "C:\soubor.txt", iPos As Integer
iPos = sInput.IndexOf("\")   ` vrátí 2
iPos = sInput.LastIndexOf("\")   ` vrátí 2
iPos = Strings.InStr(sInput, "\") - 1  ` vrátí 2
iPos = Strings.InStrRev(sInput, "\") - 1 ` vrátí 2

Všechny metody kromě toho umožňují volitelným parametrem specifikovat, od jaké pozice ve vstupním řetězci má vyhledávání probíhat. To je vhodné v případě, že hodláme prohledávání zopakovat od posledně nalezené pozice jako součást rozkladu řetězce na jeho součásti (tzv. parsování), např. při separaci písmena diskové jednotky, adresářů a názvu souboru ze zadané cesty:

    Dim sPath As String = "C:\WINDOWS\SYSTEM\soubor.txt", iStart, iStop As Integer
    Do
      iStart = iStop
      iStop = sPath.IndexOf("\", iStart + 1)
      If iStop > 0 Then
        Console.WriteLine(sPath.Substring(iStart, iStop - iStart))
      Else
        Console.WriteLine(sPath.Substring(iStart)) : Exit Do
      End If
    Loop

V případě, že řetězec není ve vstupním řetězci nalezen, vracejí metody třídy System.String() arbitrárně hodnotu -1 (výchozí index pole nulové délky), zatímco funkce modulu Microsoft.VisualBasic.Strings vrací nulovou hodnotu. Příklad proto postupně vypíše komponenty zadané cesty:

C:
\WINDOWS
\SYSTEM
\soubor.txt
Poznámky
  • Výsledek hledání znaku v řetězci obecně závisí na tom, zda rozlišujeme velká a malá písmena (tedy nastavení prostředí direktivou Option Compare, popř. parametrem překladače vbc.exe).
    Toto nastavení je však na úrovni jednotlivých příkazů možné změnit volitelným parametrem metody funkce InStr():
        iPos = InStr("VyHlEdeJ", "e", CompareMethod.Text)  ` vrátí pozici znaku "E", tedy 5
        iPos = InStr("VyHlEdeJ", "e", CompareMethod.Binary) ` vrátí pozici znaku "e", tedy 7

    V obecném případě je výsledek hledání závislý na způsobu řazení, závisející na jazykovém prostředí (hodnotě CultureInfo.CurentCulture.CompareInfo) volajícího threadu. Proto je pro jazykově závislé prohledávání řetězců nutno použít metodu IndexOf() třídy System.Globalization.CompareInfo, bližší popis metody CompareInfo.IndexOf() ovšem přesahuje rámec úvodního kursu.

  • V případě, že řetězec parsujeme pomocí pevně daného oddělovače - který v našem případě představuje znak "\" (zpětné lomítko, tzv. backslash) - představuje mnohem elegantnější cestou pro rozklad řetězce použití metody String,Split(), např.:
       For Each sPart As String In "C:\WINDOWS\SYSTEM\soubor.txt".Split("\")
         Console.WriteLine(sPart)
       Next

    vypíše postupně komponenty úplné cesty k souboru:

       C:
       WINDOWS
       SYSTEM
       soubor.txt
    
  • V daném konkrétním (avšak dosti běžném) případě rozkladu cesty k souboru lze s výhodou použít rozhraní  třídy System.IO.FileInfo (soubor přitom na dané cestě fyzicky existovat nemusí):
       Dim FI As New System.IO.FileInfo("C:\WINDOWS\SYSTEM\soubor.txt")
       Console.WriteLine(FI.Directory.Root)
       Console.WriteLine(FI.Directory.Parent)
       Console.WriteLine(FI.Directory.Name)
       Console.WriteLine(FI.Name)

    Ukázka opět postupně vypíše komponenty cesty k souboru:

       C:   WINDOWS
       SYSTEM
       soubor.txt

Náhrada a zaměňování řetězců

Pro záměny znaků nebo skupin znaků uvnitř řetězce slouží metoda String.Replace() nebo alternativní funkce Replace() modulu Microsoft.VisualBasic.Strings. Metoda String.Replace() očekává dva povinné parametry: řetězec nebo pole znaků, který má být vyhledáván a řetězec, který má být obsah řetězce nahražen. Například odkazy na absolutní cesty k souborů ve webových stránkách se zadávají ve formátu "file:///C:/WINDOWS/soubor.txt". Záměnou řetězců odkaz převedeme na normální cestu souborového systému NTFS:

Dim sPath$ = "file:///C:/WINDOWS/soubor.txt".Replace("file:///", "").Replace("/", "\")

Opakované použití funkce Microsoft.VisualBasic.Strings.Replace() není zdaleka tak přehledné, protože tato procedura se používá imperativně a vstupní řetězec se předává jako parametr. Opakovaná záměna textu tedy vyžaduje několikanásobně zanořené volání funkce Replace():

Dim sPath$ = Replace(Replace("file:///C:/WINDOWS/soubor.txt", "file:///", ""), "/", "\")

nebo postupné ukládání jejích mezivýsledku do pomocné proměnné, což je pomalejší, ale přehlednější:

Dim sPath As String = "file:///C:/WINDOWS/soubor.txt"
sPath = Replace("file:///C:/WINDOWS/soubor.txt", "file:///", "")
sPath = Replace(sPath, "/", "\")    ` vrátí "C:\WINDOWS\soubor.txt"
Poznámky
  • Funkce Microsoft.VisualBasic.Strings.Replace() má pro začátečníka navíc tu výhodu, že umožňuje specifikovat, od jaké pozice řetězce a kolikrát dojde k záměně, popř. zda se má přitom ignorovat výchozí řazení řetězců, dané direktivou Option Compare, popř. parametrem překladače vbc.exe. Drobný problém je, že parametr Start metody Strings.Replace() je ignorován, takže obě varianty nakonec vracejí stejnou návratovou hodnotu:

    Dim sBug = Replace("aAaAaAaAaAaAaAa", "a", "*", 1, 2, CompareMethod.Binary) ` vrací *A*AaAaAaAaAa
    Dim sBug = Replace("aAaAaAaAaAaAaAa", "a", "*", 3, 2, CompareMethod.Binary) ` vrací *A*AaAaAaAaAa
  • Alternativní metodu záměny řetězců ve starších verzích VB představovalo použití příkazu Mid nebo v některých případech též metody Format, se kterou se seznámíme v následujícím dílu seriálu. Příkaz Mid však již ve VB.NET podporován není, zato je zde k dispozici velice efektivní a flexibilní způsob vyhledávání a současné záměny řetězce podle obecné masky dané regulárním výrazem (viz výše) je použití metody Replace() třídy System.Text.RegExp, s jejímž použitím se seznámíme později.
  • Příkaz Mid pro náhradu části řetězce uvnitř jiného řetezce lze svépomocně nahradit metodou Array.Copy() po nám již známém převedení  řetězců na pole znaků metodou String.ToCharArray() a zpět:
     Function Mid$(ByVal target$, ByVal source$, ByVal Start%, Optional ByVal Length% = 0)
       Dim aTarget() As Char = target.ToCharArray, aSource() As Char = source.ToCharArray
       If Length = 0 Or Length > aSource.Length Then Length = aSource.Length
       Array.Copy(aSource, 0, aTarget, Start, Length)
       Return New String(aTarget)
     End Function
     Dim s$ = Mid("**********", "aaa", 2)    ` vrátí "**aaa*****"
  • Pokud rozhodujícím kritériem není flexibilita ale výkon, je na místě použití metod StringBuilder.Insert()  a StringBuilder.Replace() - viz níže.

Metoda StringBuilder.Replace

V případě řetězců jako referenčního typu nejde o vlastně o čisté zaměňování znaků uprostřed řetězců. Ve skutečnosti tato "záměna" probíhá tak, že je řetězec znovu sestaven ze svých částí v nové instanci třídy System.String. To má své praktické důsledky, pokud nahražujeme byť jen několik znaků v delším řetězci - celá potřebná paměť se alokuje znovu a pokud není původní řetězec odložen do záložního fondu (viz díl č. 23), proces Garbage Collector musí po původním řetězci paměť uklidit, což činí opakované operace s delšími řetězci značně neefektivní. Tento nedostatek obchází použití metody Replace() třídy System.Text.StringBuilder. Práce s třídou StringBuilder bude probírána později při výkladu pokročilejších metod pro práci s řetězci, v tomto úvodním kursu si předvedeme ukázku základního použití metody Replace(). Metoda podporuje dva volitelné parametry, které umožňují specifikovat začátek (tzv. offset) a délku bloku, ve kterém si přejeme provést náhradu řetězce:

Imports System, System.Text
Module MStringBuilder
  Sub Main()
    Dim s As String = "Kdo rychle d#v#, dvakr#t d#v#."
    Dim SB As New StringBuilder(s)
    ShowRuler(SB, "Původní hodnota:")
    SB.Replace("#", "á", 10, 15)
    ShowRuler(SB)
    SB.Replace("#", "á")
    ShowRuler(SB, "Výsledná hodnota:")
  End Sub
  Sub ShowRuler(SB As StringBuilder, Optional Comment$ = "")
    Console.WriteLine("0----+----1----+----2----+----3----+----4---")
    Console.WriteLine("01234567890123456789012345678901234567890123")
    Console.WriteLine("{0}", SB.ToString)
    Console.WriteLine()
  End Sub
End Module

Ukázka po spuštění a proběhnutí vypíše do systémové konzole následující řádky. Barevně je zvýrazněn blok řetězce, ve kterém volání metody StringBuilder.Replace() následně provede záměnu znaků:

0----+----1----+----2----+----3----+----4---
01234567890123456789012345678901234567890123
Kdo rychle d#v#, dvakr#t d#v#.
0----+----1----+----2----+----3----+----4---
01234567890123456789012345678901234567890123
Kdo rychle dává, dvakrát d#v#.
0----+----1----+----2----+----3----+----4---
01234567890123456789012345678901234567890123
Kdo rychle dává, dvakrát dává.

Efektivní použití třídy StringBuilder vyžaduje určitou rozvahu, jinak může naopak vést ke ztrátě výkonu. Předně, třída StringBuilder podporuje alokaci dostatečného bloku paměti předem metodou StringBuilder.EnsureCapacity(). A pokud pracujeme s větším počtem řetězců, opakované použití třídy StringBuilder přestává být výhodné, protože vytvoření a opakované disponování třídy vyžaduje zřetelnou režii a pole znaků StringBuilder se nerecyklují v řetězcovém fondu .NET, jako obyčejné řetězce.

Metoda StringBuilder.Insert, StringBuilder.Remove

Metoda System.Text.StringBuilder.Insert() může v prostředí VB.NET sloužit jako výkonná náhrada příkazu Mid, kterým bylo možné ve starších verzích VB zaměnit část řetězce na dané pozici novým.

    Dim SB As New StringBuilder("**********")
    Dim s$ = SB.Insert(3, "aaa").ToString   ` vrátí hodnotu "***aaa*******"

Vložení cílového řetězce lze zopakovat použitím  dalšího parametru metody StringBuilder.Insert(). Ukázka níže zopakuje vložení řetězce "aaa" na pozici 2 tříkrát:

    Dim s$ = SB.Insert(3, "aaa", 3).ToString   ` vrátí hodnotu "***aaaaaaaaa*******"

Podobným způsobem, jako lze řetězec s pomocí třídy System.Text.StringBuilder prodlužovat jej lze také zkrátit. K tomu účelu slouží metoda StringBuilder.Remove(), přebírající dva parametry: počáteční pozici v řetězci, od které má dojít k jeho vypuštění a počet odstraňovaných znaků. O tento počet znaků je po zavolání metody výchozí řetězec zkrácen:

    Dim SB As New StringBuilder("**********")
    Dim s$ = SB.Remove(2, 4).ToString    ` vrátí hodnotu "******"

Metoda StringBuilder.Append

Pokud je třída StringBuilder použita vhodným způsobem, může představovat velmi výrazné urychlení výkonu. Můžeme si to demonstrovat např. na příkladu spojování řetězců, pro které třída StringBuilder disponuje metodou Append(). Přitom předem alokujeme prostor odpovídající očekávané délce výsledného řetězce pomocí volitelného parametru konstruktoru StringBuilder. Alternativně pro stejný účel použijeme nám již známou metodu Concat() třídy System.String(). A konečně celý cyklus zopakujeme s použitím obvyklého operátoru "&" (znak ampersand) pro spojování řetězců:

Imports System, System.Text, System.DateTime
Module MStringBuilder
  Sub Main()
    Const N As Integer = 100000
    Dim SB As StringBuilder, dStart As DateTime, s As String, i As Integer
    s = "" : SB = New StringBuilder(s, N) : dStart = Now
    For i = 1 To N : SB.Append("s") : Next ` použití metody StringBuilder.Append()
    ShowTime(dStart)
    s = "" : dStart = Now
    For i = 1 To N : s.Concat("s") : Next ` použití metody System.String.Concat()
    ShowTime(dStart)
    s = "" : dStart = Now
    For i = 1 To N : s &= "s" : Next  ` použití operátoru konkatenace "&="
    ShowTime(dStart)
  End Sub
  Sub ShowTime(ByVal StartTime As DateTime)
    Console.WriteLine("Operace trvala {0} milisekund.", Now.Subtract(StartTime).TotalMilliseconds)
  End Sub
End Module

Ukázka vypíše následující výsledky:

Operace trvala 0 milisekund.   ` použití metody StringBuilder.Append()
Operace trvala 15,625 milisekund.  ` použití metody System.String.Concat()
Operace trvala 13843,75 milisekund.  ` použití operátoru konkatenace "&="

Zde vidíme, že metoda StringBuilder.Append() s alokovaným prostorem pro řetězec na výkonném počítači proběhne prakticky okamžitě (pod hranicí rozlišení metody DateTime.Now() - cca 1/64 vteřiny), jelikož místo spojování řetězce probíhá pouze přiřazení hodnot do oblasti paměti alokované třídou StringBuilder. Srovnatelně rychlá je metoda String.Concat(),, naproti tomu spojování řetězců operátorem pro spojování řetězců je řádově pomalejší a tento rozdíl se bude navíc exponenciálně zvětšovat s prodlužující se délkou řetězce nebo rostoucím počtem operací přiřazení.

Stručné shrnutí

Pro manipulaci s textovými řetězci prostředí .NET často nabízí několik metod současně. Extrémním případem je v tomto směru metoda Replace, která je implementována třídou System.String, System.Text.StringBuilder a System.Text.RegExp i modulem Microsoft.VisualBasic.Strings. Jelikož se v praxi způsob použití jednotlivých metod liší, je vhodné se půběžně seznamovat s jejich vzájemnými rozdíly a předpokládanou oblastí použití. Metody třídy StringBuilder jsou zpravidla nejrychlejší, metody třídy System.String se jakožto nativní metody instance používají nejoperativněji, zatímco metody RegExp nabízejí flexibilitu regulárních výrazů a funkce modulu Microsoft.VisualBasic.Strings slučitelnost se staršími verzemi VB. Na příkladu parsování položky souborového systému bylo také demonstrováno, že k požadovanému výsledku lze často dospět několika nezávislými cestami současně.

V závěrečném dílu věnovaném práci s řetězci se seznámíme s metodami pro formátování hodnotových proměnných a úpravy textových řetězců.

Témata článku: Software, Windows, Programování, Původní hodnota, Showtime, Libovolný soubor, Výchozí vyhledávání, Sparta, Split, Vyhledávání, Ampersand, Jednotlivý soubor, Jednotlivý díl, Pozice, Aaa, Garbage, Díl, Libovolný znak, Match, Like, Pre, Cesta, Původní výhoda, Znak, Stejný účel

Určitě si přečtěte

Velká podzimní aktualizace Windows 10 je tady: Co přináší Fall Creators Update

Velká podzimní aktualizace Windows 10 je tady: Co přináší Fall Creators Update

** Po půl roce je tu další aktualizace Windows ** A opět přináší hlavně hromadu drobných kosmetických vylepšení ** Podívali jsme se na ty nejzajímavější

17.  10.  2017 | Jakub Čížek | 186

Jak funguje největší akumulátor v Česku: podívejte se do elektrárny Dlouhé Stráně

Jak funguje největší akumulátor v Česku: podívejte se do elektrárny Dlouhé Stráně

** Přečerpávací vodní elektrárna Dlouhé stráně je obdivuhodné technické dílo ** Stejná turbína vyrábí elektřinu i tlačí vodu zpět do horního jezera ** Strojovna elektrárny je zabudována v podzemí

19.  10.  2017 | David Polesný | 25

Přichází doba hypersonických zbraní. Hrozí zvýšené riziko jaderného konfliktu

Přichází doba hypersonických zbraní. Hrozí zvýšené riziko jaderného konfliktu

** Světové mocnosti vyvíjí nové, nesmírně rychlé zbraně ** Jsou schopné pokořit rychlost Mach 5 ** Tyto zbraně mohou zvýšit riziko rozpoutání válečného konfliktu

19.  10.  2017 | Stanislav Mihulka | 20


Aktuální číslo časopisu Computer

Nový seriál o programování elektroniky

Otestovali jsme 17 bezdrátových sluchátek

Jak na nákup vánočních dárků ze zahraničí

4 tankové tiskárny v přímém souboji