Разбирайте HTML и сохраняйте исходный контент.
У меня есть много файлов HTML. Я хочу заменить некоторые элементы, оставив все остальное без изменений. Например, я хотел бы выполнить это выражение jQuery (или его эквивалент):
$('.header .title').text('my new content')
в следующем документе HTML:
<div class=header><span class=title>Foo</span></div>
<p>1<p>2
<table><tr><td>1</td></tr></table>
и имеют следующий результат:
<div class=header><span class=title>my new content</span></div>
<p>1<p>2
<table><tr><td>1</td></tr></table>
Проблема заключается в том, что все синтаксические анализаторы Ive пытались (Nokogiri, BeautifulSoup, html5lib) сериализуйте его примерно так:
<html>
<head></head>
<body>
<div class=header><span class=title>my new content</span></div>
<p>1</p><p>2</p>
<table><tbody><tr><td>1</td></tr></tbody></table>
</body>
</html>
например. они добавляют:
- html, элементы головы и тела
- закрытие p-тегов
- TBODY
Есть ли синтаксический анализатор, который удовлетворяет мои потребности? Он должен работать в Node.js, Ruby или Python.
Ответы
Ответ 1
Я очень рекомендую пакет pyquery для python. Это jQuery-подобный интерфейс, наложенный поверх чрезвычайно надежного пакета lxml, привязка python к libxml2.
Я считаю, что это делает именно то, что вы хотите, с довольно знакомым интерфейсом.
from pyquery import PyQuery as pq
html = '''
<div class=header><span class=title>Foo</span></div>
<p>1<p>2
<table><tr><td>1</td></tr></table>
'''
doc = pq(html)
doc('.header .title').text('my new content')
print doc
Вывод:
<div><div class="header"><span class="title">my new content</span></div>
<p>1</p><p>2
</p><table><tr><td>1</td></tr></table></div>
Невозможно помочь закрывающий тэг. lxml
поддерживает только значения из исходного документа, а не капризы оригинала. Пункты могут быть сделаны двумя способами, и он выбирает более стандартный способ при сериализации. Я не верю, что вы найдете (без ошибок) парсер, который будет лучше.
Ответ 2
Примечание. Я нахожусь на Python 3.
Это будет обрабатывать только подмножество селекторов CSS, но этого может быть достаточно для ваших целей.
from html.parser import HTMLParser
class AttrQuery():
def __init__(self):
self.repl_text = ""
self.selectors = []
def add_css_sel(self, seltext):
sels = seltext.split(" ")
for selector in sels:
if selector[:1] == "#":
self.add_selector({"id": selector[1:]})
elif selector[:1] == ".":
self.add_selector({"class": selector[1:]})
elif "." in selector:
html_tag, html_class = selector.split(".")
self.add_selector({"html_tag": html_tag, "class": html_class})
else:
self.add_selector({"html_tag": selector})
def add_selector(self, selector_dict):
self.selectors.append(selector_dict)
def match_test(self, tagwithattrs_list):
for selector in self.selectors:
for condition in selector:
condition_value = selector[condition]
if not self._condition_test(tagwithattrs_list, condition, condition_value):
return False
return True
def _condition_test(self, tagwithattrs_list, condition, condition_value):
for tagwithattrs in tagwithattrs_list:
try:
if condition_value == tagwithattrs[condition]:
return True
except KeyError:
pass
return False
class HTMLAttrParser(HTMLParser):
def __init__(self, html, **kwargs):
super().__init__(self, **kwargs)
self.tagwithattrs_list = []
self.queries = []
self.matchrepl_list = []
self.html = html
def handle_starttag(self, tag, attrs):
tagwithattrs = dict(attrs)
tagwithattrs["html_tag"] = tag
self.tagwithattrs_list.append(tagwithattrs)
if debug:
print("push\t", end="")
for attrname in tagwithattrs:
print("{}:{}, ".format(attrname, tagwithattrs[attrname]), end="")
print("")
def handle_endtag(self, tag):
try:
while True:
tagwithattrs = self.tagwithattrs_list.pop()
if debug:
print("pop \t", end="")
for attrname in tagwithattrs:
print("{}:{}, ".format(attrname, tagwithattrs[attrname]), end="")
print("")
if tag == tagwithattrs["html_tag"]: break
except IndexError:
raise IndexError("Found a close-tag for a non-existent element.")
def handle_data(self, data):
if self.tagwithattrs_list:
for query in self.queries:
if query.match_test(self.tagwithattrs_list):
line, position = self.getpos()
length = len(data)
match_replace = (line-1, position, length, query.repl_text)
self.matchrepl_list.append(match_replace)
def addquery(self, query):
self.queries.append(query)
def transform(self):
split_html = self.html.split("\n")
self.matchrepl_list.reverse()
if debug: print ("\nreversed list of matches (line, position, len, repl_text):\n{}\n".format(self.matchrepl_list))
for line, position, length, repl_text in self.matchrepl_list:
oldline = split_html[line]
newline = oldline[:position] + repl_text + oldline[position+length:]
split_html = split_html[:line] + [newline] + split_html[line+1:]
return "\n".join(split_html)
См. пример использования ниже.
html_test = """<div class=header><span class=title>Foo</span></div>
<p>1<p>2
<table><tr><td class=hi><div id=there>1</div></td></tr></table>"""
debug = False
parser = HTMLAttrParser(html_test)
query = AttrQuery()
query.repl_text = "Bar"
query.add_selector({"html_tag": "div", "class": "header"})
query.add_selector({"class": "title"})
parser.addquery(query)
query = AttrQuery()
query.repl_text = "InTable"
query.add_css_sel("table tr td.hi #there")
parser.addquery(query)
parser.feed(html_test)
transformed_html = parser.transform()
print("transformed html:\n{}".format(transformed_html))
Вывод:
transformed html:
<div class=header><span class=title>Bar</span></div>
<p>1<p>2
<table><tr><td class=hi><div id=there>InTable</div></td></tr></table>
Ответ 3
Хорошо, я сделал это на нескольких языках, и я должен сказать, что лучший парсер, который я видел, сохраняет пробелы и даже комментарии HTML:
Jericho, который, к сожалению, Java.
Это Джерико знает, как разбирать и сохранять фрагменты.
Да, я знаю его Java, но вы можете легко сделать RESTful-сервис с небольшим количеством Java, который возьмет полезную нагрузку и преобразует ее. В службе REST Java вы можете использовать JRuby, Jython, Rhino Javascript и т.д. Для координации с Jericho.
Ответ 4
Вы можете использовать фрагмент HTML Nokogiri для этого:
fragment = Nokogiri::HTML.fragment('<div class=header><span class=title>Foo</span></div>
<p>1<p>2
<table><tr><td>1</td></tr></table>')
fragment.css('.title').children.first.replace(Nokogiri::XML::Text.new('HEY', fragment))
frament.to_s #=> "<div class=\"header\"><span class=\"title\">HEY</span></div>\n<p>1</p><p>2\n</p><table><tr><td>1</td></tr></table>"
Проблема с тегом p
сохраняется, поскольку это недопустимый HTML, но это должно возвращать ваш документ без тегов html, head или body и tbody.
Ответ 5
С Python - использование lxml.html
довольно прямолинейно:
(Он соответствует точкам 1 и 3, но я не думаю, что многое можно сделать около 2 и обрабатывает неуказанные class=
)
import lxml.html
fragment = """<div class=header><span class=title>Foo</span></div>
<p>1<p>2
<table><tr><td>1</td></tr></table>
"""
page = lxml.html.fromstring(fragment)
for span in page.cssselect('.header .title'):
span.text = 'my new value'
print lxml.html.tostring(page, pretty_print=True)
Результат:
<div>
<div class="header"><span class="title">my new content</span></div>
<p>1</p>
<p>2
</p>
<table><tr><td>1</td></tr></table>
</div>
Ответ 6
Это немного отдельное решение, но если это только для нескольких простых экземпляров, то, возможно, CSS является ответом.
Сгенерированный контент
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
<style type="text/css">
#header.title1:first-child:before {
content: "This is your title!";
display: block;
width: 100%;
}
#header.title2:first-child:before {
content: "This is your other title!";
display: block;
width: 100%;
}
</style>
</head>
<body>
<div id="header" class="title1">
<span class="non-title">Blah Blah Blah Blah</span>
</div>
</body>
</html>
В этом случае вы можете просто заменить jQuery классы, и вы получите бесплатное изменение css. Я не тестировал это конкретное использование, но он должен работать.
Мы используем это для таких вещей, как сообщения об отключении.
Ответ 7
Если вы используете приложение Node.js, этот модуль будет делать именно то, что вы хотите, манипулятор DOM в стиле JQuery: https://github.com/cheeriojs/cheerio
Пример из их wiki:
var cheerio = require('cheerio'),
$ = cheerio.load('<h2 class="title">Hello world</h2>');
$('h2.title').text('Hello there!');
$('h2').addClass('welcome');
$.html();
//=> <h2 class="title welcome">Hello there!</h2>