世の中には、W3C Markup Validation Serviceと、W3C CSS Validation ServiceというHTMLやCSSが妥当な形式で書かれているかをチェックしてくれるサービスがあります。
W3C Markup Validation Service
http://validator.w3.org/
W3C CSS Validation Service
http://jigsaw.w3.org/css-validator/
Webページのデザインを考え、それをHTMLやCSSで表現することは大事ですが、書かれたHTMLやCSSが妥当なものかどうかを確認するのも、実装品質として確認すべき項目だと思います。
で、今の仕事でデザイン会社からもらうHTMLやCSSを確認しているのですが、あまりにこのあたりのデキがひどくて(デザインとしても崩れたりするので、そういう面でもアレなのですが…)、手でチェックするのがバカバカしくなってきたので、入力されたディレクトリ配下のHTMLやCSSをスキャンして機械的にValidation Serviceで検証するスクリプトを書いてみました。
Groovy+HTTPBuilder+JSoupです。
なお、今回は簡単のためにそれぞれのServiceのサイトそのものを使用しましたが、できれば自分が使う環境に、このServiceを構築した方がよいかなと。構築手順も公開されていたはずです。
W3C Markup Validation Serviceを使ったHTML検証
まずは、W3C Markup Validation Serviceを使ってHTML検証をやってみます。
Web上で検証する時は、「Direct Input」を使用して、検証するHTMLをテキストエリアに
貼り付けました。
一応、検証はHTML5で行います。
この画像には写っていませんが、下にいくと「Check」というボタンがあるので、こちらを押すとValidなHTMLなら緑の帯で「This document was successfully checked as HTML5!」と言われ
NGなHTMLを突っ込むと赤い帯となり
下の方にいくと、何が悪かったのか教えてくれます。
ちなみに、今回は「Direct Input」を使いましたが、他にURIを指定して確認したり、ファイルアップロードもできたりしますからね。
で、これを自動化したスクリプトがこちら。
exec-html-validator.groovy
import java.nio.charset.StandardCharsets @Grab('org.jsoup:jsoup:1.7.3') import org.jsoup.Jsoup @Grab('commons-codec:commons-codec:1.9') @Grab('org.codehaus.groovy.modules.http-builder:http-builder:0.6') @GrabExclude('commons-codec:commons-codec') import groovyx.net.http.ContentType import groovyx.net.http.HTTPBuilder import groovyx.net.http.Method @Grab('org.apache.httpcomponents:httpmime:4.2.1') import org.apache.http.entity.mime.HttpMultipartMode import org.apache.http.entity.mime.MultipartEntity import org.apache.http.entity.mime.content.StringBody import org.apache.http.util.EntityUtils def url = 'http://validator.w3.org/check' // 必要に応じて、社内とかのサーバへ if (args.size() == 0) { println('[ERROR] 入力するHTMLファイルのルートディレクトリを指定してください') System.exit(1) } def ignoreErrorPredicates = [] def validateHtmlFile = { htmlFile -> def http = new HTTPBuilder(url) def body = [ fragment: htmlFile.getText('UTF-8'), charset: 'utf-8', prefill: '0', doctype: 'HTML5', prefill_doctype: 'html401', group: '0' ] def resp = http.request(Method.POST) { req -> headers.'User-Agent' = 'Mozilla/5.0' def entity = new MultipartEntity() body.each { entity.addPart(it.key, new StringBody(it.value, StandardCharsets.UTF_8)) } req.entity = entity response.success = { resp -> [resp.getHeaders('X-W3C-Validator-Status'), resp.getHeaders('X-W3C-Validator-Errors'), EntityUtils.toString(resp.entity, 'UTF-8')] } } def validatorStatus = resp.get(0) def errorsHeader = resp.get(1) def entity = resp.get(2) if (validatorStatus.grep { /^Invalid$/ }.size() > 0) { if (errorsHeader.size() > 0) { def html = Jsoup.parse(entity) def msgErrs = html.getElementsByClass('msg_err') def errors = msgErrs.collect { [ location: it.getElementsByTag('em').size() > 0 ? it.getElementsByTag('em').collect { it.text() }.head() : '', msg: it.getElementsByClass('msg').size() > 0 ? it.getElementsByClass('msg').collect { it.text() }.head() : '', input: it.getElementsByClass('input').size() > 0 ? it.getElementsByClass('input').collect { it.text() }.head() : '', title: it.getElementsByTag('dt').size() > 0 ? it.getElementsByTag('dt').collect { it.text() }.head() : '', reason: it.getElementsByTag('dd').size() > 0 ? it.getElementsByTag('dd').collect { it.text() }.head() : '' ] } def ignoreErrors = errors.findAll { e -> ignoreErrorPredicates.inject(false) { acc, pred -> acc || pred(e) } } ignoreErrors.each { errors.remove(it) } [file: htmlFile.path, errorCount: errors.size(), errors: errors] } else { [file: htmlFile.path, errorCount: errorsHeader.size()] } } else { [file: htmlFile.path, errorCount: 0] } } def isHtml = { file -> file.file && file.name.endsWith('.html') } def validate = { file -> try { def result = validateHtmlFile(file) if (result.errorCount > 0) { //println("$result.file\t$result.errorCount") result.errors.each { error -> //def line = "\t" + def line = [ result.file, error.location, error.msg, error.input, error.title, error.reason ].join("\t") println(line) } } } catch (e) { println("[ERROR] file => $file.path") e.printStackTrace() } } def eachFile = { fileName -> def f = new File(fileName) if (f.exists()) { if (f.directory) { def dir = f dir.eachFileRecurse { file -> if (isHtml(file)) { validate(file) } } } else { def file = f if (isHtml(file)) { validate(file) } } } else { println("[WARN] 指定されたファイルまたはディレクトリ[$fileName]が存在しません") } } args.each(eachFile)
指定したディレクトリ配下の「.html」ファイルを探してきて、W3C Markup Validation ServiceにPOSTします。結果、戻ってきたHTTPヘッダとHTMLの内容から、エラーがあればそれを表示します。
あくまで、「エラーのみ」です。他の情報が欲しければ、W3C Markup Validation Serviceが返却するHTMLの内容を解析してみてください。
とりあえず、動かしてみましょう。
先ほどエラーになっていたHTMLをファイルとして用意して
input/index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>HTML5サンプル</title> </head> <body> <p>HTML5で作成しました!</p> <span id="duplicate-id">IDを重複指定しています</span> <span id="duplicate-id">IDを重複指定しています</span> <span class="" class="">同じ属性を2重定義</span> </body> </html>
スクリプトを実行。
$ groovy exec-html-validator.groovy input
結果は、こんな感じで出してくれます。
input/index.html Line 10, Column 28 Duplicate ID duplicate-id. <span id="duplicate-id">IDを重複指定しています</span> input/index.html Line 12, Column 25 Duplicate attribute class. <span class="" class="">同じ属性を2重定義</span>
フォーマットを変えたりしたければ、適当にいじってください。
自分は、後にPOIを使ってExcelに出力するようにしました。
W3C CSS Validation Serviceを使ったCSS検証
続いて、W3C CSS Validation Serviceを使ってCSSの検証を行います。
こちらも、直接入力でCSSを入力します。「Profile」は、「CSS レベル 3」で。
まあ、入力しているCSSはCSS3でもなんでもないですが…。
こちらは、下の方に「検証する」というボタンがあるので、こちら押します。
問題なければ、以下のような表示になり
エラーがあればW3C Markup Validator Service同様に赤くなり、エラーのあった箇所を教えてくれます。
で、自動化したスクリプト。
exec-css-validator.groovy
import java.nio.charset.StandardCharsets @Grab('org.jsoup:jsoup:1.7.3') import org.jsoup.Jsoup @Grab('commons-codec:commons-codec:1.9') @Grab('org.codehaus.groovy.modules.http-builder:http-builder:0.6') @GrabExclude('commons-codec:commons-codec') import groovyx.net.http.ContentType import groovyx.net.http.HTTPBuilder import groovyx.net.http.Method @Grab('org.apache.httpcomponents:httpmime:4.2.1') import org.apache.http.entity.mime.HttpMultipartMode import org.apache.http.entity.mime.MultipartEntity import org.apache.http.entity.mime.content.StringBody import org.apache.http.util.EntityUtils def url = 'http://jigsaw.w3.org/css-validator/validator' // 必要に応じて、社内とかのサーバへ if (args.size() == 0) { println('[ERROR] 入力するCSSファイルのルートディレクトリを指定してください') System.exit(1) } def ignoreErrorPredicates = [] def validateCssFile = { cssFile -> def http = new HTTPBuilder(url) def body = [ text: cssFile.getText('UTF-8'), profile: 'css3', usermedium: 'all', type: 'none', warning: '1', vextwarning: '', lang: 'ja' ] def resp = http.request(Method.POST) { req -> def entity = new MultipartEntity() body.each { entity.addPart(it.key, new StringBody(it.value, StandardCharsets.UTF_8)) } req.entity = entity response.success = { resp -> [resp.getHeaders('X-W3C-Validator-Status'), resp.getHeaders('X-W3C-Validator-Errors'), EntityUtils.toString(resp.entity, 'UTF-8')] } } def validatorStatus = resp.get(0) def errorsHeader = resp.get(1) def entity = resp.get(2) if (validatorStatus.grep { /^Invalid$/ }.size() > 0) { if (errorsHeader.size() > 0) { def html = Jsoup.parse(entity) def errorMessages = html.getElementsByClass('error') def errors = errorMessages.collect { [ location: it.getElementsByClass('linenumber').size() > 0 ? it.getElementsByClass('linenumber').collect { it.text() }.head() : '', context: it.getElementsByClass('codeContext').size() > 0 ? it.getElementsByClass('codeContext').collect { it.text() }.head() : '', parseError: it.getElementsByClass('parse-error').size() > 0 ? it.getElementsByClass('parse-error').collect { it.text() }.head() : '' ] } def ignoreErrors = errors.findAll { e -> ignoreErrorPredicates.inject(false) { acc, pred -> acc || pred(e) } } ignoreErrors.each { errors.remove(it) } [file: cssFile.path, errorCount: errors.size(), errors: errors] } else { [file: cssFile.path, errorCount: errorsHeader.size()] } } else { [file: cssFile.path, errorCount: 0] } } def isCss = { file -> file.file && file.name.endsWith('.css') } def validate = { file -> try { def result = validateCssFile(file) if (result.errorCount > 0) { //println("$result.file\t$result.errorCount") result.errors.each { error -> //def line = "\t" + def line = [ result.file, error.location, error.context, error.parseError ].join("\t") println(line) } } } catch (e) { println("[ERROR] file => $file.path") e.printStackTrace() } } def eachFile = { fileName -> def f = new File(fileName) if (f.exists()) { if (f.directory) { def dir = f dir.eachFileRecurse { file -> if (isCss(file)) { validate(file) } } } else { def file = f if (isCss(file)) { validate(file) } } } else { println("[WARN] 指定されたファイルまたはディレクトリ[$fileName]が存在しません") } } args.each(eachFile)
こちらも、指定したディレクトリ配下の「.css」ファイルを探してきて、W3C CSS Validation ServiceにPOSTします。結果、戻ってきたHTTPヘッダとHTMLの内容から、エラーがあればそれを表示します。ほぼ、W3C Markup Validation Serviceを自動化したスクリプトと同じ動きをします。
続いて、チェック対象のCSSとして、先ほどエラーになっていたCSSをファイルとして作成します。
input/sample.css
html { margin: 0; padding: 0; height: 100%; } body { background-repeat: no-repeat; background-position: center top background-attachment: fixed; font-siz: 12px; color: midnightblue; }
実行。
$ groovy exec-css-validator.groovy input input/sample.css 10 body 次のプロパティが正しくありません : background-position プロパティ名の前にセミコロン(;)を追加してみて下さい input/sample.css 12 body プロパティ font-siz は存在しません : 12px
ちゃんと、同じエラー内容が得られました。
両方のスクリプトとも、特にエラーがなければ何も言いませんし、指定したディレクトリ内にHTMLファイルやCSSファイルがあれば、それを見つけて検証してくれます。
なので、書いておいてなんですが、意図せず大量のリクエストを発行しないように注意しましょう…。そういう用途で使うのであれば、自分でW3C Markup Validation ServiceやW3C CSS Validation Serviceのサーバを構築するのが、望ましいと思います。