そうそう、あなたは正規表現を使ってHTMLを解析できる!
あなたがしようとしているタスクでは、正規表現は完全にうまくいきます!
ほとんどの人が正規表現を使用してHTMLを解析することの難しさを過小評価しているため、それが不十分であることは事実です。
しかし、これは計算理論に関連するいくつかの根本的な欠陥ではありません。その愚かさはこの辺りでたくさんオウムされていますが、あなたはそれらを信じていませんか?
それは確かに(この投稿はこの議論の余地事実の存在証明となります)行うことができますしながら、だから、それはそれは意味しない べきで あること。
正規表現から専用の特別な目的のHTMLパーサーに相当するものを作成するかどうかを自分で決める必要があります。ほとんどの人はそうではありません。
しかし、私はそうです。☻
一般的な正規表現ベースのHTML解析ソリューション
最初に、正規表現を使用して任意の HTML を解析するのがいかに簡単かを示します。完全なプログラムはこの投稿の最後にありますが、パーサーの中心は次のとおりです。
for (;;) {
given ($html) {
last when (pos || 0) >= length;
printf "\@%d=", (pos || 0);
print "doctype " when / \G (?&doctype) $RX_SUBS /xgc;
print "cdata " when / \G (?&cdata) $RX_SUBS /xgc;
print "xml " when / \G (?&xml) $RX_SUBS /xgc;
print "xhook " when / \G (?&xhook) $RX_SUBS /xgc;
print "script " when / \G (?&script) $RX_SUBS /xgc;
print "style " when / \G (?&style) $RX_SUBS /xgc;
print "comment " when / \G (?&comment) $RX_SUBS /xgc;
print "tag " when / \G (?&tag) $RX_SUBS /xgc;
print "untag " when / \G (?&untag) $RX_SUBS /xgc;
print "nasty " when / \G (?&nasty) $RX_SUBS /xgc;
print "text " when / \G (?&nontag) $RX_SUBS /xgc;
default {
die "UNCLASSIFIED: " .
substr($_, pos || 0, (length > 65) ? 65 : length);
}
}
}
それがどれほど簡単に読めるか見てください。
書かれているとおり、HTMLの各部分を識別し、その部分が見つかった場所を通知します。あなたは簡単にそれを修正して、与えられたタイプのピース、またはこれらよりも特定のタイプに対してあなたが望む他のことをすることができます。
失敗するテストケースはありません(左:):100,000を超えるHTMLファイルでこのコードを正常に実行しました。すべてのファイルをすばやく簡単に手に入れることができました。それ以外にも、ナイーブパーサーを壊すために特別に構築されたファイルに対しても実行しました。
これはナイーブなパーサーではありません。
ああ、それは完璧ではないと確信していますが、まだそれを壊すことができていません。プログラムが明確な構造を持っているため、何かがあったとしても、修正は簡単に当てはまると思います。正規表現を多用するプログラムでさえ、構造を持つ必要があります。
それが邪魔にならないところで、OPの質問について説明しましょう。
正規表現を使用してOPのタスクを解決するデモ
html_input_rx
以下に含める小さなプログラムは、次の出力を生成します。これにより、正規表現を使用したHTMLの解析が、目的の動作に対して問題なく機能することがわかります。
% html_input_rx Amazon.com-_Online_Shopping_for_Electronics,_Apparel,_Computers,_Books,_DVDs_\&_more.htm
input tag #1 at character 9955:
class => "searchSelect"
id => "twotabsearchtextbox"
name => "field-keywords"
size => "50"
style => "width:100%; background-color: #FFF;"
title => "Search for"
type => "text"
value => ""
input tag #2 at character 10335:
alt => "Go"
src => "http://g-ecx.images-amazon.com/images/G/01/x-locale/common/transparent-pixel._V192234675_.gif"
type => "image"
入力タグを解析、悪意のない入力を参照
上記の出力を生成したプログラムのソースは次のとおりです。
#!/usr/bin/env perl
#
# html_input_rx - pull out all <input> tags from (X)HTML src
# via simple regex processing
#
# Tom Christiansen <tchrist@perl.com>
# Sat Nov 20 10:17:31 MST 2010
#
################################################################
use 5.012;
use strict;
use autodie;
use warnings FATAL => "all";
use subs qw{
see_no_evil
parse_input_tags
input descape dequote
load_patterns
};
use open ":std",
IN => ":bytes",
OUT => ":utf8";
use Encode qw< encode decode >;
###########################################################
parse_input_tags
see_no_evil
input
###########################################################
until eof(); sub parse_input_tags {
my $_ = shift();
our($Input_Tag_Rx, $Pull_Attr_Rx);
my $count = 0;
while (/$Input_Tag_Rx/pig) {
my $input_tag = $+{TAG};
my $place = pos() - length ${^MATCH};
printf "input tag #%d at character %d:\n", ++$count, $place;
my %attr = ();
while ($input_tag =~ /$Pull_Attr_Rx/g) {
my ($name, $value) = @+{ qw< NAME VALUE > };
$value = dequote($value);
if (exists $attr{$name}) {
printf "Discarding dup attr value '%s' on %s attr\n",
$attr{$name} // "<undef>", $name;
}
$attr{$name} = $value;
}
for my $name (sort keys %attr) {
printf " %10s => ", $name;
my $value = descape $attr{$name};
my @Q; given ($value) {
@Q = qw[ " " ] when !/'/ && !/"/;
@Q = qw[ " " ] when /'/ && !/"/;
@Q = qw[ ' ' ] when !/'/ && /"/;
@Q = qw[ q( ) ] when /'/ && /"/;
default { die "NOTREACHED" }
}
say $Q[0], $value, $Q[1];
}
print "\n";
}
}
sub dequote {
my $_ = $_[0];
s{
(?<quote> ["'] )
(?<BODY>
(?s: (?! \k<quote> ) . ) *
)
\k<quote>
}{$+{BODY}}six;
return $_;
}
sub descape {
my $string = $_[0];
for my $_ ($string) {
s{
(?<! % )
% ( \p{Hex_Digit} {2} )
}{
chr hex $1;
}gsex;
s{
& \043
( [0-9]+ )
(?: ;
| (?= [^0-9] )
)
}{
chr $1;
}gsex;
s{
& \043 x
( \p{ASCII_HexDigit} + )
(?: ;
| (?= \P{ASCII_HexDigit} )
)
}{
chr hex $1;
}gsex;
}
return $string;
}
sub input {
our ($RX_SUBS, $Meta_Tag_Rx);
my $_ = do { local $/; <> };
my $encoding = "iso-8859-1"; # web default; wish we had the HTTP headers :(
while (/$Meta_Tag_Rx/gi) {
my $meta = $+{META};
next unless $meta =~ m{ $RX_SUBS
(?= http-equiv )
(?&name)
(?&equals)
(?= (?"e)? content-type )
(?&value)
}six;
next unless $meta =~ m{ $RX_SUBS
(?= content ) (?&name)
(?&equals)
(?<CONTENT> (?&value) )
}six;
next unless $+{CONTENT} =~ m{ $RX_SUBS
(?= charset ) (?&name)
(?&equals)
(?<CHARSET> (?&value) )
}six;
if (lc $encoding ne lc $+{CHARSET}) {
say "[RESETTING ENCODING $encoding => $+{CHARSET}]";
$encoding = $+{CHARSET};
}
}
return decode($encoding, $_);
}
sub see_no_evil {
my $_ = shift();
s{ <! DOCTYPE .*? > }{}sx;
s{ <! \[ CDATA \[ .*? \]\] > }{}gsx;
s{ <script> .*? </script> }{}gsix;
s{ <!-- .*? --> }{}gsx;
return $_;
}
sub load_patterns {
our $RX_SUBS = qr{ (?(DEFINE)
(?<nv_pair> (?&name) (?&equals) (?&value) )
(?<name> \b (?= \pL ) [\w\-] + (?<= \pL ) \b )
(?<equals> (?&might_white) = (?&might_white) )
(?<value> (?"ed_value) | (?&unquoted_value) )
(?<unwhite_chunk> (?: (?! > ) \S ) + )
(?<unquoted_value> [\w\-] * )
(?<might_white> \s * )
(?<quoted_value>
(?<quote> ["'] )
(?: (?! \k<quote> ) . ) *
\k<quote>
)
(?<start_tag> < (?&might_white) )
(?<end_tag>
(?&might_white)
(?: (?&html_end_tag)
| (?&xhtml_end_tag)
)
)
(?<html_end_tag> > )
(?<xhtml_end_tag> / > )
) }six;
our $Meta_Tag_Rx = qr{ $RX_SUBS
(?<META>
(?&start_tag) meta \b
(?:
(?&might_white) (?&nv_pair)
) +
(?&end_tag)
)
}six;
our $Pull_Attr_Rx = qr{ $RX_SUBS
(?<NAME> (?&name) )
(?&equals)
(?<VALUE> (?&value) )
}six;
our $Input_Tag_Rx = qr{ $RX_SUBS
(?<TAG> (?&input_tag) )
(?(DEFINE)
(?<input_tag>
(?&start_tag)
input
(?&might_white)
(?&attributes)
(?&might_white)
(?&end_tag)
)
(?<attributes>
(?:
(?&might_white)
(?&one_attribute)
) *
)
(?<one_attribute>
\b
(?&legal_attribute)
(?&might_white) = (?&might_white)
(?:
(?"ed_value)
| (?&unquoted_value)
)
)
(?<legal_attribute>
(?: (?&optional_attribute)
| (?&standard_attribute)
| (?&event_attribute)
# for LEGAL parse only, comment out next line
| (?&illegal_attribute)
)
)
(?<illegal_attribute> (?&name) )
(?<required_attribute> (?#no required attributes) )
(?<optional_attribute>
(?&permitted_attribute)
| (?&deprecated_attribute)
)
# NB: The white space in string literals
# below DOES NOT COUNT! It's just
# there for legibility.
(?<permitted_attribute>
accept
| alt
| bottom
| check box
| checked
| disabled
| file
| hidden
| image
| max length
| middle
| name
| password
| radio
| read only
| reset
| right
| size
| src
| submit
| text
| top
| type
| value
)
(?<deprecated_attribute>
align
)
(?<standard_attribute>
access key
| class
| dir
| ltr
| id
| lang
| style
| tab index
| title
| xml:lang
)
(?<event_attribute>
on blur
| on change
| on click
| on dbl click
| on focus
| on mouse down
| on mouse move
| on mouse out
| on mouse over
| on mouse up
| on key down
| on key press
| on key up
| on select
)
)
}six;
}
UNITCHECK {
load_patterns();
}
END {
close(STDOUT)
|| die "can't close stdout: $!";
}
よし!それに何もない!:)
あなただけ が、正規表現のスキルが特定の解析タスクに達しているかどうかを判断できます。誰もがスキルのレベルは異なり、すべての新しいタスクは異なります。明確に定義された入力セットがあるジョブの場合、処理するHTMLの制限されたサブセットがある場合にいくつかをまとめることは簡単であるため、正規表現は明らかに正しい選択です。正規表現の初心者でも、正規表現を使用してこれらのジョブを処理する必要があります。それ以外はやりすぎです。
ただし、HTMLの釘付けが少なくなり、予測できない方法で影響が出始めたとしても、それが完全に合法であり、より多くの種類のものやより複雑な依存関係と一致させなければならない場合、最終的には解析クラスを使用する必要がある場合よりも、正規表現を使用するソリューションを実行する場合は、さらに努力する必要があります。その損益分岐点がどこに当てはまるかは、正規表現を使用した自分の快適レベルに依存します。
だから私は何をすべきですか?
何をしなければならないか、何をしてはいけないかをお話しするつもりはありません。それは間違っていると思います。私はあなたに可能性を提示したいのです。少し目を開けてください。あなたはあなたが何をしたいか、そしてあなたがそれをどのようにしたいかを選ぶことができます。絶対的なものはありません。そして、あなた自身のようにあなた自身の状況を他の誰も知りません。作業が多すぎると思われる場合は、まあ、そうかもしれません。プログラミングは楽しいはずです。そうでない場合は、間違っている可能性があります。
私のhtml_input_rx
プログラムはいくつもの有効な方法で見ることができます。その1つは、正規表現を使用してHTMLを実際に解析できることです。しかし、もう1つは、ほとんどの人が思っているよりもずっと難しいということです。これは非常に難しいので、私のプログラムはあなたがしてはいけないことの証拠であることを簡単に結論付けることができます。
私はそれに同意しません。確かに、私が私のプログラムで行うすべてのことは、いくつかの研究の後であなたにとって意味がない場合は、この種のタスクに正規表現を使用しようとするべきではありません。特定のHTMLの場合、正規表現は優れていますが、一般的なHTMLの場合、狂気と同じです。私はいつもパースクラスを使用しています。特にそれがHTMLの場合は、自分で生成していません。
小さな HTML解析問題に最適な正規表現、大きな問題には悲観的
私のプログラムが、一般的なHTMLの解析に正規表現を使用してはいけない理由の例として取り上げられたとしても、それは大丈夫です。私はそれをmeantにするつもりだったためです。読みにくく、構造化されておらず、維持できないパターンを書くという厄介で厄介な癖。
パターンは醜い必要はなく、難しいものである必要もありません。醜いパターンを作成する場合、それはそれらではなくあなたへの反映です。
驚異的に絶妙な正規表現言語
私の問題に対する私の解決策はPerlで書かれていることを指摘するように言われました。驚きましたか?気づかなかったの?この啓示は爆弾ですか?
他のすべてのツールやプログラミング言語が、正規表現に関しては、Perlと同じくらい便利で表現力があり、強力であるとは限りません。そこには大きなスペクトルがあり、他のものよりも適切なものもあります。一般に、正規表現をライブラリとしてではなくコア言語の一部として表現した言語の方が、操作が簡単です。Cを使用している場合はプログラムの構成が異なりますが、たとえばPCREで実行できない正規表現では何もしていません。
最終的に他の言語は、Perlが正規表現に関して今どこにあるかに追いつくでしょう。私がこれを言ったのは、Perlが始まった頃は、Perlの正規表現のようなものは誰もいなかったからです。あなたが好きなことを言ってください、しかしこれはPerlが明らかに勝った場所です:開発のさまざまな段階ではありますが、誰もがPerlの正規表現をコピーしました。Perlは、使用するツールや言語に関係なく、現代のパターンで今日依存するようになったほとんど(すべてではありませんが)のすべてを開拓しました。したがって、最終的には他の人が追いつくでしょう。
しかし、彼らは今と同じように、Perlが過去のある時点に追いつくだけです。すべてが進みます。正規表現では、Perlがリードする他に何もない場合、他の人が従います。Perlがいったいどこにいるのか、ついにPerlが今どこにいるのに追いつくのか?わかりませんが、私たちも引っ越してきます。おそらく、Perlのクラフトパターンのスタイルに近づくでしょう。
そのようなものが好きで、Perl₅で使用したい場合は、Damian Conwayの素晴らしい Regexp :: Grammarsモジュールに興味があるかもしれません。それは完全に素晴らしく、私のプログラムでここで行ったことは、空白やアルファベットの識別子なしで人々が詰め込むパターンを作るのと同じくらい原始的に見えます。見てみな!
シンプルなHTMLチャンカー
これが、この投稿の最初に目玉を示したパーサーの完全なソースです。
私はないあなたが厳密にテスト解析クラスの上にこれを使用する必要があることを示唆しています。しかし、私は正規表現だけのためにその誰もができ、解析HTMLを装っていない人々の疲れている、彼らはできません。あなたは明らかにそうすることができ、このプログラムはその主張の証拠です。
確かに、それは簡単ではありませんが、それは可能です!
そして、そうすることは時間のひどい無駄です。なぜなら、このタスクに使用する必要がある優れた解析クラスが存在するからです。任意の HTML を解析しようとする人々に対する正しい答えは、それが不可能であるということではありません。それは簡単で不誠実な答えです。正直な正解は、ゼロから理解するのは面倒すぎるので、彼らはそれを試みるべきではないということです。彼らは、完璧に機能するホイールを作り直すために努力している背中を壊すべきではありません。
一方、予測可能なサブセットに含まれる HTML は、正規表現で解析するのが非常に簡単です。人々がそれらを使おうとするのも不思議ではありません。なぜなら、小さな問題、おもちゃの問題では、おそらくもっと簡単なことはないでしょう。そのため、特定のアプローチと一般的なタスクの2つのタスクを区別することが非常に重要です。これらは必ずしも同じアプローチを必要としないからです。
HTMLと正規表現についての質問を、より公正で正直に扱うことをここで期待しています。
これが私のHTMLレクサーです。検証の解析は行われません。字句要素を識別するだけです。HTMLパーサーではなく、HTML チャンカーと考える方がよいでしょう。壊れたHTMLを許容することはあまりできませんが、その方向での許容範囲は非常に小さくなります。
自分で完全なHTMLを解析したことがなくても(そしてなぜそうすべきなのか、それが解決された問題です!)、このプログラムには多くの人々が多くのことを学ぶことができると信じている素晴らしい正規表現ビットがたくさんあります。楽しい!
#!/usr/bin/env perl
#
# chunk_HTML - a regex-based HTML chunker
#
# Tom Christiansen <tchrist@perl.com
# Sun Nov 21 19:16:02 MST 2010
########################################
use 5.012;
use strict;
use autodie;
use warnings qw< FATAL all >;
use open qw< IN :bytes OUT :utf8 :std >;
MAIN: {
$| = 1;
lex_html(my $page = slurpy());
exit();
}
########################################################################
sub lex_html {
our $RX_SUBS; ###############
my $html = shift(); # Am I... #
for (;;) { # forgiven? :)#
given ($html) { ###############
last when (pos || 0) >= length;
printf "\@%d=", (pos || 0);
print "doctype " when / \G (?&doctype) $RX_SUBS /xgc;
print "cdata " when / \G (?&cdata) $RX_SUBS /xgc;
print "xml " when / \G (?&xml) $RX_SUBS /xgc;
print "xhook " when / \G (?&xhook) $RX_SUBS /xgc;
print "script " when / \G (?&script) $RX_SUBS /xgc;
print "style " when / \G (?&style) $RX_SUBS /xgc;
print "comment " when / \G (?&comment) $RX_SUBS /xgc;
print "tag " when / \G (?&tag) $RX_SUBS /xgc;
print "untag " when / \G (?&untag) $RX_SUBS /xgc;
print "nasty " when / \G (?&nasty) $RX_SUBS /xgc;
print "text " when / \G (?&nontag) $RX_SUBS /xgc;
default {
die "UNCLASSIFIED: " .
substr($_, pos || 0, (length > 65) ? 65 : length);
}
}
}
say ".";
}
#####################
# Return correctly decoded contents of next complete
# file slurped in from the <ARGV> stream.
#
sub slurpy {
our ($RX_SUBS, $Meta_Tag_Rx);
my $_ = do { local $/; <ARGV> }; # read all input
return unless length;
use Encode qw< decode >;
my $bom = "";
given ($_) {
$bom = "UTF-32LE" when / ^ \xFf \xFe \0 \0 /x; # LE
$bom = "UTF-32BE" when / ^ \0 \0 \xFe \xFf /x; # BE
$bom = "UTF-16LE" when / ^ \xFf \xFe /x; # le
$bom = "UTF-16BE" when / ^ \xFe \xFf /x; # be
$bom = "UTF-8" when / ^ \xEF \xBB \xBF /x; # st00pid
}
if ($bom) {
say "[BOM $bom]";
s/^...// if $bom eq "UTF-8"; # st00pid
# Must use UTF-(16|32) w/o -[BL]E to strip BOM.
$bom =~ s/-[LB]E//;
return decode($bom, $_);
# if BOM found, don't fall through to look
# for embedded encoding spec
}
# Latin1 is web default if not otherwise specified.
# No way to do this correctly if it was overridden
# in the HTTP header, since we assume stream contains
# HTML only, not also the HTTP header.
my $encoding = "iso-8859-1";
while (/ (?&xml) $RX_SUBS /pgx) {
my $xml = ${^MATCH};
next unless $xml =~ m{ $RX_SUBS
(?= encoding ) (?&name)
(?&equals)
(?"e) ?
(?<ENCODING> (?&value) )
}sx;
if (lc $encoding ne lc $+{ENCODING}) {
say "[XML ENCODING $encoding => $+{ENCODING}]";
$encoding = $+{ENCODING};
}
}
while (/$Meta_Tag_Rx/gi) {
my $meta = $+{META};
next unless $meta =~ m{ $RX_SUBS
(?= http-equiv ) (?&name)
(?&equals)
(?= (?"e)? content-type )
(?&value)
}six;
next unless $meta =~ m{ $RX_SUBS
(?= content ) (?&name)
(?&equals)
(?<CONTENT> (?&value) )
}six;
next unless $+{CONTENT} =~ m{ $RX_SUBS
(?= charset ) (?&name)
(?&equals)
(?<CHARSET> (?&value) )
}six;
if (lc $encoding ne lc $+{CHARSET}) {
say "[HTTP-EQUIV ENCODING $encoding => $+{CHARSET}]";
$encoding = $+{CHARSET};
}
}
return decode($encoding, $_);
}
########################################################################
# Make sure to this function is called
# as soon as source unit has been compiled.
UNITCHECK { load_rxsubs() }
# useful regex subroutines for HTML parsing
sub load_rxsubs {
our $RX_SUBS = qr{
(?(DEFINE)
(?<WS> \s * )
(?<any_nv_pair> (?&name) (?&equals) (?&value) )
(?<name> \b (?= \pL ) [\w:\-] + \b )
(?<equals> (?&WS) = (?&WS) )
(?<value> (?"ed_value) | (?&unquoted_value) )
(?<unwhite_chunk> (?: (?! > ) \S ) + )
(?<unquoted_value> [\w:\-] * )
(?<any_quote> ["'] )
(?<quoted_value>
(?<quote> (?&any_quote) )
(?: (?! \k<quote> ) . ) *
\k<quote>
)
(?<start_tag> < (?&WS) )
(?<html_end_tag> > )
(?<xhtml_end_tag> / > )
(?<end_tag>
(?&WS)
(?: (?&html_end_tag)
| (?&xhtml_end_tag) )
)
(?<tag>
(?&start_tag)
(?&name)
(?:
(?&WS)
(?&any_nv_pair)
) *
(?&end_tag)
)
(?<untag> </ (?&name) > )
# starts like a tag, but has screwed up quotes inside it
(?<nasty>
(?&start_tag)
(?&name)
.*?
(?&end_tag)
)
(?<nontag> [^<] + )
(?<string> (?"ed_value) )
(?<word> (?&name) )
(?<doctype>
<!DOCTYPE
# please don't feed me nonHTML
### (?&WS) HTML
[^>]* >
)
(?<cdata> <!\[CDATA\[ .*? \]\] > )
(?<script> (?= <script ) (?&tag) .*? </script> )
(?<style> (?= <style ) (?&tag) .*? </style> )
(?<comment> <!-- .*? --> )
(?<xml>
< \? xml
(?:
(?&WS)
(?&any_nv_pair)
) *
(?&WS)
\? >
)
(?<xhook> < \? .*? \? > )
)
}six;
our $Meta_Tag_Rx = qr{ $RX_SUBS
(?<META>
(?&start_tag) meta \b
(?:
(?&WS) (?&any_nv_pair)
) +
(?&end_tag)
)
}six;
}
# nobody *ever* remembers to do this!
END { close STDOUT }