一部のHTMLタグを通すフィルタ
Posted feedbacks - Python
htmlって曖昧だから、あまり自信なし。例えば、 <a href=foo.com> みたいに属性がクオートされてない場合はサポートしていない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import re
def filter(html):
def repl(m):
content = m.group(1)
m = re.match('(/?)\s*(a|br|strong)(\s.*|(/?))', content, re.I + re.S)
if m:
beg_slash, tag, other, end_slash = m.groups()
def attrs(*names):
yield ""
for m in re.finditer(r"""(\S+)\s*=\s*(['"]).+?\2""", other, re.S):
if m.group(1).lower() in names:
yield m.group(0)
def combine(*names):
return "<%s%s%s%s>" % (beg_slash, tag, " ".join(attrs(*names)), end_slash or "")
if tag.lower() == "a":
return combine("href", "name")
else:
return combine()
return "<" + content + ">"
return re.compile('<([^>]*)>', re.S).sub(repl, html)
def main():
print filter("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>""")
if __name__ == '__main__':
main()
|
なるほど、そんな場合があるのか・・・というわけで修正版です。
属性をパースする正規表現が二度出てくるのが美しくないですが・・・このあたりをPythonでうまく処理する方法ってあるのかな。
属性をパースする正規表現が二度出てくるのが美しくないですが・・・このあたりをPythonでうまく処理する方法ってあるのかな。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import re
def filter(html):
def repl(m):
beg_slash, tag_name, attrs, _, end_slash = m.groups()
def combine(*names):
s = "<" + beg_slash + tag_name
for m in re.finditer(r"""(\w+)\s*=\s*(["']).+?\2""", attrs, re.S):
if m.group(1).lower() in names:
s += " " + m.group(0)
return s + end_slash + ">"
if tag_name.lower() == 'a':
return combine("href", "name")
elif tag_name.lower() in ('br', 'strong'):
return combine()
else:
return "<" + m.group(0)[1:]
return re.compile(r"""<(/?)(\w+)((?:\s*\w+\s*=\s*(["']).+?\4)*)\s*(/?)>""", re.S).sub(repl, html)
def main():
print filter("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>""")
if __name__ == '__main__':
main()
|
素直にライブラリを使いましたが、お題によれば無効にしたタグ部分は 置き換えた文字以外、そのままにすべきなんでしょうが (例えば大文字小文字など)、ライブラリの都合上、end側タグまわりの情報が劣化しています(27行目あたり)。 これは基底クラスのソース見てなんとかする必要がありそうなので とりあえず手抜き版として投稿させていただきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | from HTMLParser import HTMLParser
class HTMLParser2(HTMLParser):
def reset(self):
HTMLParser.reset(self)
self.buf = ''
def handle_starttag(self, tag, attrs):
if tag == 'a':
self.buf += '<a '+' '.join(["%s='%s'" % t for t in attrs if t[0] in ['href', 'name']])+'>'
elif tag in ['br', 'strong']:
self.buf += '<%s>' % tag
else:
self.buf += self.get_starttag_text().replace('<', '<')
def handle_startendtag(self, tag, attrs):
if tag == 'br':
self.buf += '<br/>'
else:
self.buf += self.get_starttag_text().replace('<', '<')
def handle_endtag(self, tag):
if tag in ['a', 'br', 'strong']:
self.buf += '</%s>' % tag
else:
self.buf += '</%s>' % tag
def handle_data(self, data):
self.buf += data
s = """<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='al
ert("NG")'>click<br/>me!</strong>"""
h = HTMLParser2()
h.feed(s)
print h.buf
|
お題の更新に対応してみました。 一応「&」も「&」に置き換えるようにしました。 <BR>と<BR/>を受付け</BR>は置き換えるように変更しました。 ただし、サイズが少々でかすぎますね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | from HTMLParser import HTMLParser
from urllib import quote
class HTMLParser2(HTMLParser):
def reset(self):
HTMLParser.reset(self)
self.buf = ''
self.__endtag_text = ''
def parse_endtag(self, i):
try:
j = self.rawdata.index('>', i+1)
self.__endtag_text = self.rawdata[i:j+1]
except:
pass
return HTMLParser.parse_endtag(self, i)
def get_endtag_text(self):
return self.__endtag_text
def replace(self, s):
return s.replace('&', '&').replace('<', '<').replace('>', '>')
def handle_starttag(self, tag, attrs):
if tag == 'a':
self.buf += '<a '+' '.join(['%s="%s"' % (a, quote(b)) for a, b in attrs if a in ['href', 'name']])+'>'
elif tag in ['br', 'strong']:
self.buf += '<%s>' % tag
else:
self.buf += self.replace(self.get_starttag_text())
def handle_startendtag(self, tag, attrs):
if tag == 'br':
self.buf += '<br/>'
else:
self.buf += self.replace(self.get_starttag_text())
def handle_endtag(self, tag):
if tag in ['a', 'strong']:
self.buf += '</%s>' % tag
else:
self.buf += self.replace(self.get_endtag_text())
def handle_data(self, data):
self.buf += data
def f(s):
h = HTMLParser2()
h.feed(s)
print s
print h.buf
f('''<script foo="<script>alert('bar')</script>">alert('foo')</script>''')
f('''<script foo="<a href='link'>link</a>">alert('foo')</script>''')
f('''<a href='www.g>oogle.com'>link</a>''')
|
HTMLParser,htmllibを使わずに、スクラッチから書いてみました。 フィルタの登録が簡単にできます。 ※ 字句解析部分は、十分にテストしてないので、全然 自信なしです。エンティティ等も未サポート・・・どころか誤動作起こす恐れもあり。 ※ フィルタの適用部分は、余計なメソッド呼び出しがたくさん発生する、冗長(無駄の多い)な実装です。 ※ 終了タグの扱いがad-hoc。(scan_htmlとpack_tag) ということなので、最初は汎用性とか考えて書いてた割りに、下の実装の品質は低いです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | import sys
import string
from cgi import escape
from urllib import quote
from itertools import imap
letters = string.letters + string.digits
TAG, ATTR, QUOTE, TEXT = range(4)
def scan_html(html):
tag = ''
text = ''
attr = ''
attrs = []
value = ''
state = TEXT
quoted = ''
closed = False
escaped = False
for c in html:
if state == TAG:
if c == ">":
yield TAG, tag, attrs, None
state = TEXT
elif c == '/' and tag == '':
tag += c
elif c in letters:
tag += c
elif c in string.whitespace:
state = ATTR
else:
pass
elif state == ATTR:
if c in letters:
attr += c
elif c in string.whitespace:
pass
elif c == '=' and not quoted:
state = QUOTE
elif c == '/':
tag += c
state = TAG
elif c == '>':
yield TAG, tag, attrs, None
state = TEXT
elif state == QUOTE:
if quoted:
if c == quoted:
if escaped:
escaped = False
value += c
else:
attrs.append((attr,value))
state = TAG
elif c == '\\':
escaped = True
value += c
else:
value += c
elif c in ('"', "'"):
quoted = c
else: # TEXT
if c == "<":
if text:
yield TEXT, tag, None, text
tag = ''
text = ''
attr = ''
attrs = []
state = TAG
else:
text += c
class MyHTMLParser:
def __init__(self):
self.tag_filters = {}
self.attr_filters = {}
self.text_filters = {}
self.allow_tags = []
self.forbid_tags = []
def filter(self, (state, tag, attrs, text)):
find_filter = lambda x:x.get((state,tag.lower()), lambda x:x)
tag,attrs = find_filter(self.tag_filters)((tag,attrs))
attrs = find_filter(self.attr_filters)(attrs)
text = find_filter(self.text_filters)(text)
return state, tag, attrs, text
def parse(self, html, output=sys.stdout.write):
def is_allowed_tag(tag):
if self.allow_tags and tag in self.allow_tags:
return True
if self.forbid_tags and not tag in self.forbid_tags:
return True
return False
def pack_tag(tag, attrs):
if tag.startswith("/") and not attrs:
return "<%s>" % tag
elif tag.endswith("/"):
if not attrs:
return "<%s />" % tag.strip('/ ')
return "<%s %s />" % (tag.strip('/ ')," ".join(map('%s="%s"'.__mod__,attrs)))
else:
if not attrs:
return "<%s>" % tag.strip('/ ')
return "<%s %s>" % (tag," ".join(map('%s="%s"'.__mod__,attrs)))
for event in imap(self.filter, scan_html(html)):
state,tag,attrs,text = event
if state == TAG:
if is_allowed_tag(tag.lower().strip(' /')):
output(pack_tag(tag,attrs))
else:
output(escape(pack_tag(tag,attrs)))
elif state == TEXT:
output(text)
def test(html):
def allow_attrs(*names):
return lambda attrs: [(k,quote(v)) for k,v in attrs if k.lower() in names]
def remove_all_attrs(attrs):
return []
p = MyHTMLParser()
p.allow_tags += ['a', 'br', 'strong']
p.attr_filters[(TAG,'a')] = allow_attrs('href', 'name')
p.attr_filters[(TAG,'br')] = remove_all_attrs
p.attr_filters[(TAG,'strong')] = remove_all_attrs
p.parse(html)
print
test('''<script foo="<script>alert('bar')</script>">alert('foo')</script>''')
test('''<script foo="<a href='link'>link</a>">alert('foo')</script>''')
test('''<a href='www.g>oogle.com'>link</a>''')
test('''<br />''')
test('''<img src="foo.jpg" />''')
test("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>""")
test("""<strong style="color:red;">ok</strong>""")
test("""<br /><a href='localhost'>link</a><strong onClick='alert("NG")'>ok</strong>""")
|




にしお
#3410()
Rating0/0=0.00
このお題はperezvonさんの提案を元にしています。ありがとうございました。 ただ、いきなりだと難しいかと思ったので、肝の部分以外を先に出題しました。このお題は続編で徐々に難しくなっていきます。
追記:属性に<や>が含まれてしまうケースに漏れのある解答が多いようなのでテストケースを追加します。 これは「この出力なら十分」という意味です。この出力の通りでなければいけないという意味ではありません。 <script foo="<script>alert('bar')</script>">alert('foo')</script> <script foo="<script>alert('bar')</script>">alert('foo')</script> <script foo="<a href='link'>link</a>">alert('foo')</script> <script foo="<a href='link'>link</a>">alert('foo')</script> <a href='www.g>oogle.com'>link</a> <a href="./www.g%3Eoogle.com">link</a>[ reply ]