Подтвердить что ты не робот

Следует ли использовать диапазон [a-z] как ошибку?

В моем языке (et_EE) [a-z] означает:

abcdefghijklmnopqrsšz

Итак, 6 символов ASCII (tuvwxy) и один из эстонского алфавита (ž) не включены. Я вижу много модулей, которые все еще используют регулярные выражения, например

/\A[0-9A-Z_a-z]+\z/

Для меня это неправильный способ определить диапазон буквенно-цифровых символов ASCII, и я думаю, что он должен быть заменен на:

/\A\p{PosixAlnum}+\z/

Является ли первый по-прежнему идиоматическим способом? Или принято решение? Или ошибка?

Или последнее предупреждение?

4b9b3361

Ответ 1

Вернувшись в старый Perl 3.0, все было ASCII, и Perl это отразилось. \w означает то же, что и [0-9A-Z_a-z]. И нам это понравилось!

Однако Perl больше не привязан к ASCII. Некоторое время назад я перестала использовать [a-z], потому что я закричала, когда программы, которые я написал, не работали с языками, которые не были английскими. Вы, должно быть, воображали мое удивление как американец, чтобы обнаружить, что в этом мире есть как минимум несколько тысяч человек, которые не говорят по-английски.

В любом случае Perl имеет лучшие способы обработки [0-9A-Z_a-z]. Вы можете использовать набор [[:alnum:]] или просто использовать \w, который должен делать правильно. Если у вас должны быть только строчные буквы, вы можете использовать [[:lower:]] вместо [a-z] (что предполагает английский язык). (Perl переходит на некоторые длины, чтобы получить [a-z] означает только 26 символов a, b, c,... z даже на платформах EBCDIC.)

Если вам нужно указать только ASCII, вы можете добавить квалификатор /a. Если вы имеете в виду специфику локали, вы должны скомпилировать регулярное выражение в лексической области "языковой стандарт использования". (Избегайте модификатора /l, поскольку это относится только к шаблону регулярного выражения, и ничего более. Например, в 's/[[: lower:]]/\ U $&/lg' шаблон скомпилирован с использованием языка, но \U - нет. Это, вероятно, следует рассматривать как ошибку в Perl, но это то, как в настоящее время работают. Модификатор /l действительно предназначен только для внутренней бухгалтерии и не должен быть введен непосредственно.) На самом деле, лучше перевести данные локали при вводе в программу и перевести ее обратно на выход, используя Unicode. Если ваш языковой стандарт является одним из новых моделей UTF-8, новая функция в 5.16 "use locale": not_characters "" доступна, чтобы позволить другим частям вашей локали работать без проблем на Perl.

$word =~ /^[[:alnum:]]+$/   # $word contains only Posix alphanumeric characters.
$word =~ /^[[:alnum:]]+$/a  # $word contains only ASCII alphanumeric characters.
{ use locale;
  $word =~ /^[[:alnum:]]+$/;# $word contains only alphanum characters for your locale
}

Теперь, это ошибка? Если программа не работает должным образом, это ошибка простая и простая. Если вам действительно нужна последовательность ASCII, [a-z], тогда программист должен был использовать [[:lower:]] с квалификатором /a. Если вы хотите, чтобы все возможные символы нижнего регистра, в том числе и на других языках, вам просто нужно использовать [[:lower:]].

Ответ 2

Возможные ошибки локали

Проблема, с которой вы сталкиваетесь, заключается не в классах символов POSIX как таковой, а в том, что классы зависят от языка. Например, regex (7) говорит:

В выражении скобки имя класса символов, заключенного в "[:" и ":]", обозначает список всех символов, принадлежащих этому классу... Они обозначают классы символов, определенные в wctype (3). Локаль может предоставлять другие.

Акцент мой, но на странице руководства ясно сказано, что классы символов зависят от языка. Кроме того, wctype (3) говорит:

Поведение wctype() зависит от категории LC_CTYPE текущей локали.

Другими словами, если ваш язык неверно определяет класс символов, то это ошибка, которая должна быть подана против конкретной локали. С другой стороны, если класс символов просто определяет набор символов таким образом, которого вы не ожидаете, то это может быть не ошибка; это может быть просто проблема, которая должна быть закодирована вокруг.

Классы символов как ярлыки

Классы символов являются ярлыками для определения наборов. Вы, конечно, не ограничены заранее определенными наборами для своей локали, и вы можете использовать наборы символов Unicode, определенные perlre (1), или просто создавать наборы явно, если это обеспечивает большую точность.

Вы уже знаете это, поэтому я не пытаюсь быть педантичным. Я просто указываю, что если вы не можете или не будете исправлять языковой стандарт (который является источником проблемы здесь), вы должны использовать явный набор, как вы это сделали.

Класс удобства удобен только в том случае, если он работает для вашего случая использования. Если это не так, выбросьте его за борт!

Ответ 3

Поскольку этот вопрос выходит за рамки Perl, мне было интересно узнать, как это происходит в целом. Тестирование этого на популярных языках программирования с поддержкой собственных регулярных выражений, Perl, PHP, Python, Ruby, Java и Javascript, заключаются в следующем:

  • [a-z] будет соответствовать ASCII-7 диапазон az в каждом из этих языков, всегда и языковые настройки не влияют это никак. Символы типа ž и š никогда не совпадают.
  • \w может соответствовать или не соответствовать символам ž и š, в зависимости от языка программирования и параметров, заданных при создании регулярного выражения. Для этого выражения разнообразие является наибольшим, так как на некоторых языках они никогда не сопоставляются, не имеют отношения к опциям, в других они всегда соответствуют друг другу, а в некоторых из них зависят.
  • POSIX [[:alpha:]] и Unicode \p{Alpha} и \p{L}, если они поддерживаются системой регулярных выражений соответствующего языка программирования, и соответствующая конфигурация будет соответствовать символам типа ž и š.

Обратите внимание, что "Соответствующая конфигурация" не требовала изменения локали: изменение локали не оказало влияния на результаты ни в одном из тестируемых системы.

Чтобы быть в безопасности, я также тестировал командную строку Perl, grep и awk. Оттуда командная строка Perl ведет себя одинаково с приведенным выше. Тем не менее, grep и awk, похоже, отличаются от других в отношении того, что для них, locale имеет значение и для [a-z]. Поведение также зависит от версии и реализации.

В этом контексте - grep, awk или подобные инструменты командной строки - я бы согласился с тем, что использование диапазона a-z без определения языкового стандарта можно считать ошибкой, поскольку вы не можете действительно знать, что у вас получилось.


Если мы перейдем к более подробным сведениям на один язык, статус будет выглядеть следующим образом:

Java

В java \p{Alpha} работает как [a-z], если класс unicode не указан, и алфавитный символ unicode, если это так, сопоставление ž. \w будет соответствовать символам типа ž, если присутствует флаг unicode, а не если нет, а \p{L} будет соответствовать независимо от флага unicode. Для [[alpha]] нет регулярных выражений, поддерживающих язык, или поддержки.

PHP

В PHP \w, [[:alpha:]] и \p{L} будут совпадать с символами типа ž, если присутствует unicode-переключатель, а не если это не так. \p{Alpha} не поддерживается. Язык не влияет на регулярные выражения.

Python

\w будет соответствовать указанным символам, если флаг unicode присутствует, а флаг локали отсутствует. Для строк unicode флаг unicode принимается по умолчанию, если используется Python 3, но не с Python 2. Unicode \p{Alpha}, \p{L} или POSIX [[:alpha:]] не поддерживаются в Python.

Модификатор для использования регулярных выражений, специфичных для локали, видимо работает только для наборов символов с 1 байтом на символ, что делает его непригодным для использования в Юникоде.

Perl

\w соответствует ранее указанным символам в дополнение к соответствию [a-z]. Unicode \p{Letter}, \p{Alpha} и POSIX [[:alpha:]] поддерживаются и работают должным образом. Флаги Unicode и locale для регулярного выражения не изменяли результаты, а также не изменяли языковой стандарт или use locale;/no locale;.

Behavour не изменяется, если мы запускаем тесты с использованием командной строки Perl.

рубин

[a-z] и \w обнаруживают только символы [a-z], не относящиеся к параметрам. Unicode \p{Letter}, \p{Alpha} и POSIX [[:alpha:]] поддерживаются и работают как ожидалось. Язык не влияет.

Javascript

[a-z] и \w всегда обнаруживают только символы [a-z]. В ECMA2015 поддерживается поддержка /u unicode-переключателя, который в основном поддерживается основными браузерами, но не поддерживает [[:alpha:]], \p{Alpha} или \p{L} или изменяет поведение \w. Коммутатор unicode добавляет обращение к символам unicode как к одному символу, что было проблемой раньше.

Ситуация такая же для javascript на стороне клиента, а также Node.js.

AWK

Для AWK существует более подробное описание статуса, опубликованного в статье A.8 Диапазоны регулярных выражений и локали: длинная печальная история, В нем подробно сказано, что в старом мире инструментов unix [a-z] был правильным способом обнаружения строчных букв, и именно так работали инструменты времени. Однако 1992 POSIX представил локали и изменил интерпретацию классов символов, чтобы порядок символов был определен для каждого порядка сортировки, привязывая его к языку. Это также было принято AWK того времени (серия 3.x), что привело к нескольким вопросам. Когда была разработана серия 4.x, POSIX 2008 определил порядок undefined, а сопровождающий вернулся к исходному поведению.

В настоящее время используется в основном версия 4.x AWK. Когда это используется, [a-z] соответствует a-z, игнорируя любые изменения локали, а \w и [[:alpha:]] будут соответствовать языковым символам. Unicode\p {Alpha} и \p {L} не поддерживаются.

Grep

Grep (а также sed, ed) используют GNU Basic Regular Expressions, который является старым ароматом. Он не поддерживает классы символов Unicode.

По крайней мере, gnu grep 2.16 и 2.25, по-видимому, следует за 1992 годом в том, что языковой вопрос имеет значение также для [a-z], а также для \w и [[:alpha:]]. Это означает, например, что [a-z] соответствует только z в наборе xuzvöä, если используется эстонский язык.


Тестовый код, используемый ниже для каждого языка.

Java (1.8.0_131)

import java.util.regex.*;
import java.util.Locale;

public class RegExpTest {
    public static void main(String args[]) {
        verify("v", 118);
        verify("š", 353);
        verify("ž", 382);

        tryWith("v");
        tryWith("š");
        tryWith("ž");
    }
    static void tryWith(String input) {
        matchWith("[a-z]", input);
        matchWith("\\w", input);
        matchWith("\\p{Alpha}", input);
        matchWith("\\p{L}", input);
        matchWith("[[:alpha:]]", input);
    }

    static void matchWith(String pattern, String input) {
        printResult(Pattern.compile(pattern), input);
        printResult(Pattern.compile(pattern, Pattern.UNICODE_CHARACTER_CLASS), input);
    }
    static void printResult(Pattern pattern, String input) {
        System.out.printf("%s\t%03d\t%5s\t%-10s\t%-10s\t%-5s%n",
          input, input.codePointAt(0), Locale.getDefault(),
          specialFlag(pattern.flags()),
          pattern, pattern.matcher(input).matches());
    }
    static String specialFlag(int flags) {
      if ((flags & Pattern.UNICODE_CHARACTER_CLASS) == Pattern.UNICODE_CHARACTER_CLASS) {
          return "UNICODE_FLAG";
      }
      return "";
    }
    static void verify(String str, int code) {
        if (str.codePointAt(0) != code) {
            throw new RuntimeException("your editor is not properly configured for this character: " + str);
        }
    }
}

PHP (7.1.5)

<?php
/*
PHP, even with 7, only has binary strings that can be operated with unicode-aware
functions, if needed. So functions operating them need to be told which charset to use.

When there is encoding assumed and not specified, PHP defaults to ISO-8859-1.
*/


// PHP7 and extension=php_intl.dll enabled in PHP.ini is needed for IntlChar class
function codepoint($char) {
  return IntlChar::ord($char);
}

function verify($inputp, $code) {
  if (codepoint($inputp) != $code) {
    throw new Exception(sprintf('Your editor is not configured correctly for %s (result %s, should be %s)',
      $inputp, codepoint($inputp), $code));
  }
}

$rowindex = 0;
$origlocale = getlocale();

verify('v', 118);
verify('š', 353); // https://en.wikipedia.org/wiki/%C5%A0#Computing_code
verify('ž', 382); // https://en.wikipedia.org/wiki/%C5%BD#Computing_code

function tryWith($input) {
  matchWith('[a-z]', $input);
  matchWith('\\w', $input);
  matchWith('[[:alpha:]]', $input); // POSIX, http://www.regular-expressions.info/posixbrackets.html
  matchWith('\p{L}', $input);
}
function matchWith($pattern, $input) {
  global $origlocale;
  selectLocale($origlocale);
  printResult("/^$pattern\$/", $input);
  printResult("/^$pattern\$/u", $input);
  selectLocale('C'); # default (root) locale
  printResult("/^$pattern\$/", $input);
  printResult("/^$pattern\$/u", $input);
  selectLocale(['et_EE', 'et_EE.UTF-8', 'Estonian_Estonia.1257']);
  printResult("/^$pattern\$/", $input);
  printResult("/^$pattern\$/u", $input);
  selectLocale($origlocale);
}
function selectLocale($locale) {
  if (!is_array($locale)) {
    $locale = [$locale];
  }
  // On Windows, no UTF-8 locale can be set
  // https://stackoverflow.com/a/16120506/365237
  // https://msdn.microsoft.com/en-us/library/x99tb11d.aspx
  // Available Windows locales
  // https://docs.moodle.org/dev/Table_of_locales
  $retval = setlocale(LC_ALL, $locale);
  //printf("setting locale %s, retval was %s\n", join(',', $locale), $retval);
  if ($retval === false || $retval === null) {
    throw new Exception(sprintf('Setting locale %s failed', join(',', $locale)));
  }
}
function getlocale() {
  return setlocale(LC_ALL, 0);
}
function printResult($pattern, $input) {
  global $rowindex;
  printf("%2d: %s\t%03d\t%-20s\t%-25s\t%-10s\t%-5s\n",
        $rowindex, $input, codepoint($input), getlocale(),
        specialFlag($pattern), 
        $pattern, (preg_match($pattern, $input) === 1)?'true':'false');
  $rowindex = $rowindex + 1;
}
function specialFlag($pattern) {
  $arr = explode('/',$pattern);
  $lastelem = array_pop($arr);
  if (strpos($lastelem, 'u') !== false) {
    return 'UNICODE';
  }
  return '';
}

tryWith('v');
tryWith('š');
tryWith('ž');

Python (3.5.3)

# -*- coding: utf-8 -*-

# with python, there are two strings: unicode strings and regular ones.
# when you use unicode strings, regular expressions also take advantage of it,
# so no need to tell that separately. However, if you want to be using specific
# locale, that you need to tell.

# Note that python3 regexps defaults to unicode mode if unicode regexp string is used,
# python2 does not. Also strings are unicode strings in python3 by default.

# summary: [a-z] is always [a-z], \w will match if unicode flag is present and
# locale flag is not present, no unicode \p{Letter} or POSIX :alpha: exists.
# Letters outside ascii-7 never match \w if locale-specific
# regexp is used, as it only supports charsets with one byte per character
# (https://lists.gt.net/python/python/850772).

# Note that in addition to standard https://docs.python.org/3/library/re.html, more
# complete https://pypi.python.org/pypi/regex/ third-party regexp library exists.

import re, locale

def verify(inputp, code):
  if (ord(inputp[0]) != code):
    raise Exception('Your editor is not configured correctly for %s (result %s)' % (inputp, ord(inputp[0])))
  return

rowindex = 0
origlocale = locale.getlocale(locale.LC_ALL)  

verify(u'v', 118)
verify(u'š', 353)
verify(u'ž', 382)

def tryWith(input):
  matchWith(u'[a-z]', input)
  matchWith(u'\\w', input)

def matchWith(pattern, input):
  global origlocale
  locale.setlocale(locale.LC_ALL, origlocale)
  printResult(re.compile(pattern), input)
  printResult(re.compile(pattern, re.UNICODE), input)
  printResult(re.compile(pattern, re.UNICODE | re.LOCALE), input)

  matchWith2(pattern, input, 'C') # default (root) locale
  matchWith2(pattern, input, 'et_EE')
  matchWith2(pattern, input, 'et_EE.UTF-8')
  matchWith2(pattern, input, 'Estonian_Estonia.1257') # Windows locale
  locale.setlocale(locale.LC_ALL, origlocale)

def matchWith2(pattern, input, localeParam):
  try:
    locale.setlocale(locale.LC_ALL, localeParam) # default (root) locale
    printResult(re.compile(pattern), input)
    printResult(re.compile(pattern, re.UNICODE), input)
    printResult(re.compile(pattern, re.UNICODE | re.LOCALE), input)
  except locale.Error:
    print("Locale %s not supported on this platform" % localeParam)

def printResult(pattern, input):
  global rowindex
  try:
    print("%2d: %s\t%03d\t%-20s\t%-25s\t%-10s\t%-5s" % \
          (rowindex, input, ord(input[0]), locale.getlocale(), \
          specialFlag(pattern.flags), \
          pattern.pattern, pattern.match(input) != None))
  except UnicodeEncodeError:
    print("%2d: %s\t%03d\t%-20s\t%-25s\t%-10s\t%-5s" % \
          (rowindex, '?', ord(input[0]), locale.getlocale(), \
          specialFlag(pattern.flags), \
          pattern.pattern, pattern.match(input) != None))
  rowindex = rowindex + 1      

def specialFlag(flags):
  ret = []
  if ((flags & re.UNICODE) == re.UNICODE):
    ret.append("UNICODE_FLAG")
  if ((flags & re.LOCALE) == re.LOCALE):
    ret.append("LOCALE_FLAG")
  return ','.join(ret)

tryWith(u'v')
tryWith(u'š')
tryWith(u'ž')

Perl (v5.22.3)

# Summary: [a-z] is always [a-z], \w always seems to recognize given test chars and
# unicode \p{Letter}, \p{Alpha} and POSIX :alpha: are supported.
# Unicode and locale flags for regular expression didn't matter in this use case.

use warnings;
use strict;
use utf8;
use v5.14;
use POSIX qw(locale_h);
use Encode;
binmode STDOUT, "utf8";

sub codepoint {
  my $inputp = $_[0];
  return unpack('U*', $inputp);
}
sub verify {
  my($inputp, $code) = @_;
  if (codepoint($inputp) != $code) {
    die sprintf('Your editor is not configured correctly for %s (result %s)', $inputp, codepoint($inputp))
  }
}

sub getlocale {
  return setlocale(LC_ALL);
}
my $rowindex = 0;
my $origlocale = getlocale();

verify('v', 118);
verify('š', 353);
verify('ž', 382);

# printf('orig locale is %s', $origlocale);

sub tryWith {
  my ($input) = @_;
  matchWith('[a-z]', $input);
  matchWith('\w', $input);
  matchWith('[[:alpha:]]', $input);
  matchWith('\p{Alpha}', $input);
  matchWith('\p{L}', $input);
}

sub matchWith {
  my ($pattern, $input) = @_;
  my @locales_to_test = ($origlocale, 'C','C.UTF-8', 'et_EE.UTF-8', 'Estonian_Estonia.UTF-8');
  for my $testlocale (@locales_to_test) {
    use locale;
    # printf("Testlocale %s\n", $testlocale);
    setlocale(LC_ALL, $testlocale);
    printResult($pattern, $input, '');
    printResult($pattern, $input, 'u');
    printResult($pattern, $input, 'l');
    printResult($pattern, $input, 'a');
   };
  no locale;
  setlocale(LC_ALL, $origlocale);
  printResult($pattern, $input, '');
  printResult($pattern, $input, 'u');
  printResult($pattern, $input, 'l');
  printResult($pattern, $input, 'a');
}


sub printResult{
  no warnings 'locale';
              # for this test, as we want to be able to test non-unicode-compliant locales as well
              # remove this for real usage

  my ($pattern, $input, $flags) = @_;
  my $regexp = qr/$pattern/;
  $regexp = qr/$pattern/u if ($flags eq 'u');
  $regexp = qr/$pattern/l if ($flags eq 'l');
  printf("%2d: %s\t%03d\t%-20s\t%-25s\t%-10s\t%-5s\n", 
        $rowindex, $input, codepoint($input), getlocale(),
        $flags, $pattern, (($input =~ $regexp) ? 'true':'false'));
  $rowindex = $rowindex + 1;
}

tryWith('v');
tryWith('š');
tryWith('ž');

Ruby (ruby 2.2.6p396 (2016-11-15 версия 56800) [x64-mingw32])

# -*- coding: utf-8 -*-

# Summary: [a-z] and \w are always [a-z], unicode \p{Letter}, \p{Alpha} and POSIX
# :alpha: are supported. Locale does not have impact.

# Ruby doesn't seem to be able to interact very well with locale without 'locale'
# rubygem (https://github.com/mutoh/locale), so that is used.

require 'rubygems'
require 'locale'

def verify(inputp, code)
  if (inputp.unpack('U*')[0] != code)
    raise Exception, sprintf('Your editor is not configured correctly for %s (result %s)', inputp, inputp.unpack('U*')[0])
  end
end

$rowindex = 0
$origlocale = Locale.current
$origcharmap = Encoding.locale_charmap

verify('v', 118)
verify('š', 353)
verify('ž', 382)

# printf('orig locale is %s.%s', $origlocale, $origcharmap)
def tryWith(input)
  matchWith('[a-z]', input)
  matchWith('\w', input)
  matchWith('[[:alpha:]]', input)
  matchWith('\p{Alpha}', input)
  matchWith('\p{L}', input)
end  

def matchWith(pattern, input)
  locales_to_test = [$origlocale, 'C', 'et_EE', 'Estonian_Estonia']
  for testlocale in locales_to_test
    Locale.current = testlocale
    printResult(Regexp.new(pattern), input)
    printResult(Regexp.new(pattern.force_encoding('utf-8'),Regexp::FIXEDENCODING), input)
  end
  Locale.current = $origlocale
end

def printResult(pattern, input)
  printf("%2d: %s\t%03d\t%-20s\t%-25s\t%-10s\t%-5s\n", 
        $rowindex, input, input.unpack('U*')[0], Locale.current,
        specialFlag(pattern),
        pattern, !pattern.match(input).nil?)
  $rowindex = $rowindex + 1
end

def specialFlag(pattern)
  return pattern.encoding
end

tryWith('v')
tryWith('š')
tryWith('ž')

Javascript (node.js) (v6.10.3)

function match(pattern, input) {
    try {
        var re = new RegExp(pattern, "u");
        return input.match(re) !== null;
    } catch(e) {
        return 'unsupported';
    }
}
function regexptest() {
    var chars = [
        String.fromCodePoint(118),
        String.fromCodePoint(353),
        String.fromCodePoint(382)
    ];
    for (var i = 0; i < chars.length; i++) {
        var char = chars[i];
        console.log(
            char
            +'\t'
            + char.codePointAt(0)
            +'\t'
            +(match("[a-z]", char))
            +'\t'
            +(match("\\w", char))
            +'\t'
            +(match("[[:alpha:]]", char))
            +'\t'
            +(match("\\p{Alpha}", char))
            +'\t'
            +(match("\\p{L}", char))
            );
    }
}

regexptest();

Javascript (веб-браузеры)

function match(pattern, input) {
    try {
        var re = new RegExp(pattern, "u");
        return input.match(re) !== null;
    } catch(e) {
        return 'unsupported';
    }
}
window.onload = function() {
    var chars = [
        String.fromCodePoint(118),
        String.fromCodePoint(353),
        String.fromCodePoint(382)
    ];
    for (var i = 0; i < chars.length; i++) {
        var char = chars[i];
        var table = document.getElementById('results');
        table.innerHTML += 
            '<tr><td>' + char
            +'</td><td>'
            + char.codePointAt(0)
            +'</td><td>'
            +(match("[a-z]", char))
            +'</td><td>'
            +(match("\\w", char))
            +'</td><td>'
            +(match("[[:alpha:]]", char))
            +'</td><td>'
            +(match("\\p{Alpha}", char))
            +'</td><td>'
            +(match("\\p{L}", char))
            +'</td></tr>';
    }
}
table {
    border-collapse: collapse;
}
table td, table th {
    border: 1px solid black;
}
table tr:first-child th {
    border-top: 0;
}
table tr:last-child td {
    border-bottom: 0;
}
table tr td:first-child,
table tr th:first-child {
    border-left: 0;
}
table tr td:last-child,
table tr th:last-child {
    border-right: 0;
}
<!DOCTYPE html> 
<html>
<head>
    <meta charset="utf-8" /> 
</head>
<body>
    <table id="results">
    <tr>
    	<td>char</td>
    	<td>codepoint</td>
    	<td>[a-z]</td>
    	<td>\w</td>
    	<td>[[:alpha:]]</td>
    	<td>\p{Alpha}</td>
    	<td>\p{L}</td>
    </tr>
    </table>
</body>
</html>