Blog Mateusza Chodyły czyli interdyscyplinarnego .NETowca 8-)

Część 7.3 (Globalization, drawing, text manipulation; obsługa tekstu i wyr. reg.)

4. kwietnia 2010 20:04

Klasy: StringBuilder, Regex, Match, MatchCollection, Group, GroupCollection, Capture, CaptureCollection; Encoding, EncodingInfo, ASCIIEncoding, UnicodeEncoding, UTF8Encoding, EncoderFallback, Decoder, DecoderFallback.

Zacznę od klasy Regex, jednak zanim do niej przejdę opiszę krótko wyrażenia regularne. Najczęstszym błędem jest niezawarcie wzorca pomiędzy ^ i $ (wtedy wejście nie musi mu dokładnie odpowiadać):

  • ^\d{5}$ - dokładnie 5 cyfr np. 12345,
  • \d{5}$ - zarówno 12345, jak i abc12345.

Można to sprawdzić empirycznie :) :

string[] toValidate = new string[] { "12345", "asb12345" };
string regExp = @"\d{5}$"; // dodawaj "małpę"

for (int i = 0; i < toValidate.Length; i++)
{
  if (Regex.IsMatch(toValidate[i], regExp))
    Console.WriteLine(toValidate[i] + " - pasuje");
  else
    Console.WriteLine(toValidate[i] + " - NIE pasuje");
}

Powszechnie używane metaznaki:

  • \G - dopasowanie musi wystąpić w punkcie, gdzie poprzednie się zakończyło; podczas użycia z Match.NextMatch zapewnia, że dopasowania (matches) są sąsiednie,
  • \b - dopasowanie musi wystąpić na granicy pomiędzy \w (znaki alfanumeryczne) i \W (znaki niealfanumeryczne),
  • \B - negacja powyższego np. mamy tablicę { "car", "tocar", "carton", "car123", "car qwe", "car_qwe" }
    • dla @"car\b" mamy (p = pasuje, n = nie pasuje): p, p, n, n, p, n,
    • dla @"car\B" mamy na odwrót, czyli: n, n, p, p, n, p.

Powszechnie używane metaznaki ucieczki (metacharacter escapes):

  • \t - tabulator,
  • \r - powrót karetki (carriage return),
  • \n - nowa linia,
  • \e - escape,
  • \040 - spacja,
  • \cC - Ctrl+C,
  • \znak_specjalny np. \* = *, \\ = \

Wieloznaczne (wildcards):

  • *   - 0 lub więcej = {0,}
  • +   - jeden lub więcej np. “to+n” pasuje do “ton” lub “toooooon”, ale nie do “tn” = {1,}
  • {n}    - określona ilość
  • {min,max}   - bez spacji pomiędzy min i max!, max możesz zostawić puste
  • ?   - opcjonalny np. “to?n” odpowiada “ton” lub “tn”, ale nie “tooon”; grupy tworzysz z użyciem nawiasów np. “do(es)?” odpowiada “do” lub “does” (ale w takiej formie także np. “doesnt”) = “do(es){0,1}”
  • .    - dowolny pojedynczy znak oprócz \n; aby określić dowolny pojedynczy znak włączając w to \n użyj “[\s\S]”
  • [znaki]   - jeden z paru znaków np. “to[ro]n” odpowiada “toon” lub “torn”, ale nie “ton” lub “toron”; możesz też określić zakres znaków: “to[o-r]n”
  • x|y   - odpowiada x lub y np. “z|food” odpowiada “z” lub “food”, ale “(z|f)ood” odpowiada “zood” lub “food”
  • (grupaZnaków)   - np. “foo(loo){1,3}hoo” lub “foo(loo|roo)hoo”; możesz także nazwać grupę, żeby później pobrać dane, które jej odpowiadają - użyj formatu (?<name>pattern) np. “foo(?<mid>loo|roo)hoo” odpowiada “fooloohoo”, później możesz odwołać się do grupy “mid” do pobrania “loo”

Powszechne zakresy znaków:

  • \d   - [0-9],
  • \D   - znak nie będący cyfrą = [^0-9],
  • \s   - dowolny white-space char = [\f\n\r\t\v],
  • \S   - jak się można domyślić negacja powyższego, czyli [^\f\n\r\t\v],
  • \w   - [A-Za-z0-9_],
  • \W   - [^A-Za-z0-9_].

Do dyspozycji mamy też backreferences - dostarczają one sposobu do znajdowania powtarzających się grup znaków. Użyj:

  • nazwanych grup i metaznaku \k   -   tzn. “\k<name>”,
  • apostrofów zamiast “<” i “>” albo backslasha poprzedzonego przez jedną liczbę (“\liczba”).

To tyle na temat teorii. Czas na klasy. Zacznę od klasy Regex. Klasa zawiera kilka metod statycznych, które umożliwiają użycie wyrażeń regularnych bez tworzenia obiektu. W wersji 2.0 frameworka wyr. reg. kompilowane z wywołań statycznych metod są cache’owane, w przeciwieństwie do tych kompilowanych z wywołań metod na obiekcie (domyślnie jest cache’owanych 15 ostatnio używanych wyrażeń, ale można to zmienić przez własność CacheSize). Jeżeli więc aplikacja znacząco wykorzystuje wyrażenia reg. (stały ich zbiór) to powinieneś/aś wywoływać metody statyczne. Z klasy Regex dziedziczą m.in. TagRegex (dostarcza wyrażenia regularnego do parsowania ASP.NET Web server control tag), DirectiveRegex (dyrektywa ASP.NET) czy RunatServerRegex (parsowanie atrybutu runat). Przykład:

public static void Main()
{
  string exp = @"^-?\d+(\.\d{2})?$";

  Regex rx = new Regex(exp, RegexOptions.Compiled); // szybsze wykonanie, ale wolniejszy początkowy start; wybrane opcje: IgnoreCase, Multiline (zmiana znaczenia ^ i $ (linia a nie cały string)), Singleline (zmienia znaczenie kropki - odpowiada każdemu znakowi (także \n)), CultureInvariant

  string[] tests = {"-42", "19.99", "0.001", "100 USD", ".34", "0.34", "1,052.21"};

  foreach (string test in tests)
  {
    if (rx.IsMatch(test))
      Console.WriteLine("{0} true", test);
    else
      Console.WriteLine("{0} false", test);
  }

  printGroups(@"\b(?<word>\w+)\s+(\k<word>)\b", "The the quick brown fox    fox jumped over the lazy dog dog lazy.");
  printGroups(@"(?<word>\w)\k<word>", "I’ll have a small coffee");
}

private static void printGroups(string exp, string text)
{
  Regex rx = new Regex(exp, RegexOptions.Compiled | RegexOptions.IgnoreCase);

  MatchCollection matches = rx.Matches(text);
  Console.WriteLine("{0} matches found in:\n{1}", matches.Count, text);

  foreach (Match match in matches)
  {
    GroupCollection groups = match.Groups;
    Console.WriteLine("'{0}' repeated at positions {1} and {2}", groups["word"].Value, groups[0].Index, groups[1].Index);
  }
}

Wynik działania:

-42 true
19.99 true
0.001 false
100 USD false
.34 false
0.34 true
1,052.21 false
3 matches found in:
The the quick brown fox    fox jumped over the lazy dog dog lazy.
'The' repeated at positions 0 and 4
'fox' repeated at positions 20 and 27
'dog' repeated at positions 52 and 56
4 matches found in:
I'll have a small coffee
'l' repeated at positions 2 and 2
'l' repeated at positions 15 and 15
'f' repeated at positions 20 and 20
'e' repeated at positions 22 and 22

Można jeszcze dodać 1 przykład: (?<first>a)(?<second>\1b)*   - wzorzec: (a)(ab)(abb)(abbb)…, ponieważ metaznak \1 odnosi się do pierwszej grupy.

Kolejny przykład:

public static void Main()
{
  string input = "Company Name: Contoso, Inc.";
  Match m = Regex.Match(input, @"Company Name: (.*$)"); // nienazwana grupa otoczona nawiasami
  Console.WriteLine(m.Groups[1]); // numerowanie od 1! // Contoso, Inc.

  returnProtocolAndPort("http://www.wp.pl:80"); // http:80
  returnProtocolAndPort("http://:80"); // http
  returnProtocolAndPort("http://80"); // http
  returnProtocolAndPort("http:///wp.pl"); // FAIL
  returnProtocolAndPort("http:// wp.pl  : 80"); // http
  returnProtocolAndPort("http:// wp.pl  :80"); // http:80

  changeDateFormat("11/24/92"); // 24-11-92
  changeDateFormat("1/10/2000"); // 10-1-2000
  changeDateFormat("1/1/22222"); // 1/1/22222
}

private static void returnProtocolAndPort(string url)
{
  try
  {
    Regex r = new Regex(@"^(?<proto>\w+)://[^/]+?(?<port>:\d+)?$", RegexOptions.Compiled);
    Console.WriteLine(url + " -> " + r.Match(url).Result("${proto}${port}"));
  }
  catch (NotSupportedException)
  {
    Console.WriteLine(url + " -> FAIL");
  }
}

private static void changeDateFormat(String input)
{
  Console.WriteLine(Regex.Replace(input, @"\b(?<month>\d{1,2})/(?<day>\d{1,2})/(?<year>\d{2,4})\b", "${day}-${month}-${year}")); // użycie nazwanych backreferences, można też: "$2-$1-$3"
}

Odnośnie ostatniej funkcji - substytucje oznaczone przez metaznak $ i character escapes to jedyne specjalne konstrukcje rozpoznawane we wzorcu zamiany tzn. np. “a*${txt}b” będzie traktowany jako wstawienie ciągu “a*” (gwiazdka nie jest rozpoznawana), później dodanie podciągu odpowiadającego grupie txt i dodanie ciągu “b”. Ostatni przykład z Regex:

public static void Main()
{
  string text = "One car red car blue car";
  string pat = @"(\w+)\s+(car)";
  Regex r = new Regex(pat, RegexOptions.IgnoreCase);
  Match m = r.Match(text); // reprezentuje wyniki z pojedynczego dopasowania
  int matchCount = 0;
  while (m.Success)
  {
    Console.WriteLine("--- Match " + (++matchCount) + " ---");
    for (int i = 1; i <= 2; i++)
    {
      Group g = m.Groups[i]; // reprezentuje wyniki z pojedynczej capturing group (może zawierać 0, 1 lub więcej ciągów w pojedynczym dopasowaniu z powodu kwantyfikatorów (dlatego zawiera obiekty Capture))
      Console.WriteLine("Group " + i + " = '" + g + "'");
      CaptureCollection cc = g.Captures;
      for (int j = 0; j < cc.Count; j++)
      {
        Capture c = cc[j];
        System.Console.WriteLine("Capture " + j + " = '" + c + "', Position = " + c.Index);
      }
    }
    m = m.NextMatch();
  }

  MatchCollection matches = r.Matches(text); // wszystkie wystąpienia
  Console.WriteLine("Ilość pasujących: " + matches.Count + "\n");
  
  Regex regex = new Regex(@"(-)|(/)");
  foreach (string result in regex.Split(@"07/14/2007"))
    Console.WriteLine("'{0}'", result);

  foreach(string result in Regex.Split(@"07.14.2007", @"/|-|\.")) // bez nawiasów nie zwraca separatora
    Console.WriteLine("'{0}'", result);
}

Wynik działania:

--- Match 1 ---
Group 1 = 'One'
Capture 0 = 'One', Position = 0
Group 2 = 'car'
Capture 0 = 'car', Position = 4
--- Match 2 ---
Group 1 = 'red'
Capture 0 = 'red', Position = 8
Group 2 = 'car'
Capture 0 = 'car', Position = 12
--- Match 3 ---
Group 1 = 'blue'
Capture 0 = 'blue', Position = 16
Group 2 = 'car'
Capture 0 = 'car', Position = 21
Ilość pasujących: 3

'07'
'/'
'14'
'/'
'2007'
'07'
'14'
'2007'

Przed przejściem do kodowania napiszę parę słów o klasie StringBuilder. Korzystałem z niej już nie raz. Reprezentuje ona zmienny ciąg znaków. W przypadku konkatenacji ciągów mamy do wyboru 2 metody (dla każdej istnieje też parę przeładowań):

  • public static string Concat(string str0, string str1) /klasa String/,
  • public StringBuilder AppendFormat(string format, Object arg0) /klasa StringBuilder/.

Pierwsza zawsze tworzy nowy obiekt z istniejącego stringa i nowych danych. Natomiast obiekt StringBuilder utrzymuje bufor dla akomodacji konkatenacji nowych danych (dane są dopisywane do końca buforu, jeśli jest przestrzeń; w przeciwnym wypadku, nowy, większy bufor jest alokowany i dane są przenoszone). Możesz zarządzać pojemnością przez:

  • public int Capacity { get; set; } - max ilość znaków, które mogą istnieć w pamięci zaalokowanej przez bieżącą instancję; ArgumentOutOfRangeException jeśli ustawisz wartość, która jest mniejsza niż bieżąca długość tej instancji (możesz ją zmniejszać, ale musisz być pewien, że nie jest mniejsza niż Length); klasa StringBuilder dynamicznie alokuje więcej miejsca gdy go potrzeba (z reguły więcej niż potrzeba ze względów wydajnościowych),
  • public int EnsureCapacity(int capacity) - pilnuje żeby pojemność była co najmniej określoną wartością (podajesz min pojemność, zwraca nową pojemność - automatycznie realokuje pamięć).

Wynika z tego, że wydajność operacji konkatenacji dla String i StringBuilder zależy od tego, jak często następuje alokacja pamięci. Klasa String jest preferowana dla stałej ilości obiektów. W tym przypadku indywidualne operacje konkatenacji mogą być nawet połączone w pojedynczą operację przez kompilator. StringBuilder jest preferowany jeśli dowolna ilość stringów jest łączona.

StringBuilder sb = new StringBuilder("ABC", 20);
sb.Append(new char[] { 'D', 'E', 'F' });
sb.AppendFormat("GHI{0}{1}", 'J', 'k');
Console.WriteLine("Length: {0}, Capacity: {1}", sb.Length, sb.Capacity); // Length: 11, Capacity: 20
sb.Insert(0, "Alphabet: ");
sb.Replace('k', 'K');
Console.WriteLine("Length: {0}, Capacity: {1}", sb.Length, sb.Capacity); // Length: 21, Capacity: 40
Console.WriteLine(sb.ToString());

Ostatnią rzeczą do egzaminu jest znajomość klas służących do (de)kodowania. Jest to przydatne zwłaszcza podczas korzystania z interoperacyjności z legacy lub UNIX systems. Zacznę oczywiście od American Standard Code for Information Interchange (ASCII). ASCII przypisuje znaki używając liczb od 0 do 127 (7 bitów) - angielskie duże/małe litery, liczby, interpunkcje i kilka specjalnych znaków, np.:

  • 0x21 = !
  • 0x32 = 1
  • 0x43 = C (67 w systemie dziesiętnym)
  • 0x63 = c
  • 0x7D = }

ASCII nie zawiera znaków używanych w innych alfabetach - dlatego pozostałe wartości od 128 do 255 (8 bit) różne lokalizacje używały do reprezentowania znaków ze swoich alfabetów. Oczywiście transfer dokumentów pomiędzy różnymi językami powodował problemy - tak powstały strony kodowe (standardowe znaki ASCII od 0 do 127 i specyficzne od 128 do 255). Przykład nagłówku: Content-Type: text/plain; charset=ISO-8859-1 => code page 28591 (Western European ISO). Ich następcą został Unicode, który jest potężną stroną kodową z dziesiątkami tysięcy znaków. .NET Framework używa Unicode UTF-16 (Unicode Transformation Format, 16-bit encoding form) do reprezentowania znaków (czasem też UTF-8 jest używany wewnętrznie).

Klasa Encoding reprezentuje kodowanie znaków (czyli proces transformacji zbioru znaków Unicode na sekwencję bajtów). Dziedziczą z niej klasy:

  • ASCIIEncoding (możliwy dostęp przez własność ASCII),
  • UTF7Encoding (możliwy dostęp przez własność UTF7),
  • UTF8Encoding (możliwy dostęp przez własność UTF8),
  • UnicodeEncoding (używa UTF-16, możliwy dostęp przez własność Unicode i BigEndianUnicode),
  • UTF32Encoding (używa UTF-32, możliwy dostęp przez własność UTF32).

Przykład:

public static void Main()
{
  string unicodeString = "Pi: \u03a0, Sigma: \u03a3";

  Encoding ascii = Encoding.ASCII;
  Encoding unicode = Encoding.Unicode;

  byte[] unicodeBytes = unicode.GetBytes(unicodeString);
  byte[] asciiBytes = Encoding.Convert(unicode, ascii, unicodeBytes); // Encoding srcEncoding, Encoding dstEncoding, byte[] bytes

  char[] asciiChars = new char[ascii.GetCharCount(asciiBytes, 0, asciiBytes.Length)];
  ascii.GetChars(asciiBytes, 0, asciiBytes.Length, asciiChars, 0);
  string asciiString = new string(asciiChars);

  MessageBox.Show("Original string: " + unicodeString);
  MessageBox.Show("Ascii converted string: " + asciiString);

  Encoding e1 = Encoding.GetEncoding(28592); // po codepage (iso-8859-2)
  Encoding e2 = Encoding.GetEncoding("utf-32"); // po nazwie
  Console.WriteLine("e1 equals e2? {0}", e1.Equals(e2));

  EncodingInfo[] ei = Encoding.GetEncodings();
  foreach (EncodingInfo e in ei)
    Console.WriteLine("{0, -15} {1, -25} {2, -25}", e.CodePage, e.Name, e.DisplayName);

  string hello = "Hello, World!";
  StreamWriter swUtf7 = new StreamWriter("utf7.txt", false, Encoding.UTF7);
  swUtf7.WriteLine(hello); // 19 bajtów (błędnie wyświetlane przez Notatnik)
  swUtf7.Close();
  StreamWriter swUtf8 = new StreamWriter("utf8.txt", false, Encoding.UTF8);
  swUtf8.WriteLine(hello); // 18 bajtów
  swUtf8.Close();
  StreamWriter swUtf16 = new StreamWriter("utf16.txt", false, Encoding.Unicode);
  swUtf16.WriteLine(hello); // 32 bajty
  swUtf16.Close();
  StreamWriter swUtf32 = new StreamWriter("utf32.txt", false, Encoding.UTF32);
  swUtf32.WriteLine(hello); // 64 bajty (błędnie wyświetlane przez Notatnik)
  swUtf32.Close();
  // gdy nie określisz, to będzie: Encoding.Default, u mnie: BodyName = "iso-8859-2", CodePage = 1250 (plik ma w takim przypadku rozmiar 15 bajtów)
}

Pozostałe klasy:

  • Encoder - konwertuje zbiór znaków na sekwencję bajtów,
  • Decoder - na odwrót: konwertuje sekwencję zakodowanych bajtów na zbiór znaków; zarówno pierwsza jak i druga są abstrakcyjne; pobieraj przez odpowiednio GetEncoder()/GetDecoder() implementacji klasy Encoding,
  • EncoderFallback - dostarcza mechanizm obsługi błędów (failure-handling mechanism nazywany fallback) dla znaku, który nie może być skonwertowany do sekwencji bajtów (nie może być reprezentowany przez kodowanie np. ASCIIEncoding nie może kodować znaku, który daje wartość Unicode większą od zakresu U+0000 do U+007F); możesz użyć predefiniowanych encoder i decoder fallbacks lub utworzyć własną encoder/decoder fallback dziedziczącą z EncoderFallback i EncoderFallbackBuffer/DecoderFallback i DecoderFallbackBuffer. 2 predefiniowane klasy:
    • EncoderReplacementFallback - zamienia dostarczony string w miejsce znaku, który nie może być przekonwertowany,
    • EncoderExceptionFallback - rzuca EncoderFallbackException kiedy natrafi na niepoprawny znak

Przykład:

public static void Main()
{
  Encoding ae = Encoding.GetEncoding("us-ascii", new EncoderExceptionFallback(), new DecoderExceptionFallback());

  string inputString = "\u00abX\u00bb"; // reprezentacja Unicode: U+00AB, U+0058, U+00BB
  Console.WriteLine("Input string ({0} characters): \"{1}\"", inputString.Length, inputString);
  Console.Write("Input string in hexadecimal: ");
  foreach (char c in inputString.ToCharArray())
    Console.Write("0x{0:X2} ", (int)c);

  byte[] encodedBytes = new byte[ae.GetMaxByteCount(inputString.Length)];
  int numberOfEncodedBytes;

  try
  {
    Console.WriteLine("\n");
    numberOfEncodedBytes = ae.GetBytes(inputString, 0, inputString.Length, encodedBytes, 0);
  }
  catch (EncoderFallbackException e)
  {
    Console.WriteLine(e);
  }

  
  ae = Encoding.GetEncoding("us-ascii", new EncoderReplacementFallback("(unknown)"), new DecoderReplacementFallback("(error)"));
  encodedBytes = new byte[ae.GetByteCount(inputString)];
  numberOfEncodedBytes = ae.GetBytes(inputString, 0, inputString.Length, encodedBytes, 0);

  Console.Write("\n\nEncoded bytes in hexadecimal ({0} bytes):\n", numberOfEncodedBytes);
  int ix = 0;
  foreach (byte b in encodedBytes)
  {
    Console.Write("0x{0:X2} ", (int)b);
    ix++;
    if (0 == ix % 6)
      Console.WriteLine();
  }
  
  string decodedString = ae.GetString(encodedBytes);
  Console.WriteLine("\nInput string:  \"{0}\"", inputString);
  Console.WriteLine("Decoded string:\"{0}\"", decodedString);
}

Wynik:

predefFallbacks

Teraz nic tylko zdawać :)

Tagi:

70-536

Dodaj komentarz





  • Komentarz
  • Podgląd
Loading



Zmodyfikowany BlogEngine.NET (Bazowa szata graficzna: Mads Kristensen/Zdjęcia panoramiczne)
Hosting dzięki uprzejmości PCSS/Centrum Innowacji Microsoft
(c) Mateusz Chodyła (Panel logowania)