HTML Validate 一括処理

HTML Validatorのコラムでは、ローカル環境でW3C Validation Serviceを構築する手順を説明しました。それぞれの環境に応じて設定してください。

この記事では、いよいよ上記ローカルサーバを利用して、HTMLファイルのバリデートを一括処理するスクリプトをご紹介します。

ちょっと探せば色々と見つかるのですが、あくまでも大量のファイルを一度に処理するスクリプトに限って説明します。

HTML5なページをチェックする場合は、下記のjavaのタブを開いてください。それ以外は、HTML4/XHTML専用スクリプトです。

一覧表をHTMLファイルで書き出すタイプ

validate.pl

r271-635さんが公開されているスクリプトです。結構前のものですが、問題なく使えます。というか、これが私のベストプログラムです。

#!/usr/local/bin/myperl

use strict;
use warnings;
use utf8;
use File::Basename;
use Getopt::Long;
use XML::Parser;
use XML::SimpleObject;
use WebService::Validator::HTML::W3C;

my $xml_filepath;
my $output_html;
my @arr_urls;
#my $wait_sec = 15;

# プログラム引数を取り込む
GetOptions('xml=s' => \$xml_filepath,
    'output=s' => \$output_html);

if(!defined($xml_filepath) || !defined($output_html)){
    die("W3C html validator\n usage :\n ".basename($0)." -xml=[sitemap.xml] -output=[out.html]\n");
}

# 無効文字のチェックと削除
$xml_filepath =~ s/[\x00-\x2c|\x3a-\x40|\x5b-\x5e|\x60|\x7b-\xff]//g;
$output_html =~ s/[\x00-\x2c|\x3a-\x40|\x5b-\x5e|\x60|\x7b-\xff]//g;

unless( -f $xml_filepath ){
    die("Error : sitemap file ".$xml_filepath." not found\n");
}

print("Target File : ".$xml_filepath."\nOutput File : ".$output_html."\n");

# XMLからURLを抜き出して配列に格納する
my $p = new XML::Parser(Style=>'Tree');
my $s = new XML::SimpleObject($p->parsefile($xml_filepath));
my @arr = $s->child('urlset')->child('url');
foreach(@arr){
    push(@arr_urls, $_->child('loc')->value);
}

if($#arr_urls < 0){
    die("Error : no url found from sitemap file\n");
}

print(($#arr_urls+1)." URLs found.\nW3C query start ...\n");

my $v = WebService::Validator::HTML::W3C->new(detailed => 1,
    validator_uri => 'http://localhost:8889/w3c-validator/check');

open(FH, '>'.$output_html) or die("Error : ".$output_html." open error\n");

# バッファリング無効化
my $old_fh = select FH;
$|=1;
select $old_fh;

print(FH "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n".
    "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n".
    "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"ja-JP\" xml:lang=\"ja-JP\">\n".
    "<head>\n".
     "<title>W3C html validator resultvalidate($target_url)){
        if ( $v->is_valid ) {
            printf (FH "<tr><th colspan=\"3\"><a target=\"_blank\" href=\"%s\">%s</a> : valid</th></tr>\n", $v->uri, $v->uri);
        }
        else {
            printf (FH "<tr><th colspan=\"3\"><a target=\"_blank\" href=\"%s\">%suri, $v->uri);
            foreach my $error ( @{$v->errors} ) {
                printf(FH "<tr><td></td><td>%d</td><td>%s</td></tr>\n", $error->line, $error->msg);
            }
        }
    }
    else {
        printf (FH "<tr><th colspan=\"3\"><a target=\"_blank\" href=\"%s\">%s</a> : Fatal Error : %s</th></tr>\n", $target_url, $target_url, $v->validator_error);
    }

#  sleep($wait_sec); # W3Cサーバ負荷防止のため、ウエイトを入れる
}

print(FH "</table>\n</body>\n</html>\n");

close(FH);

print("program end\n");

私の環境では、Perlバージョン5.18.1で無いと動いてくれません。それ以上のバージョンだと、XML::SimpleObjectがコンパイルエラーとなってしまうためです。
15行目は、ローカルサーバで動かす前提のためコメントにしてあります。また、50行目は、ご自分の環境に合わせて書き換えてください。

$ sudo perl -MCPAN -e shell
  cpan[1]> install XML::Parser
  cpan[2]> install XML::SimpleObject
  cpan[3]> install WebService::Validator::HTML::W3C
  cpan[4]> install XML::XPath
  cpan[5]> install Bundle::W3C::Validator(必要なら)
$ perl validate.pl -xml=sitemap.xml -output=out.html

これで、バリデート結果の一覧がHTMLファイルで書き出されます。

一ファイルごとに結果を書き出すタイプ

checkhtml.pl

もうひとつのタイプは、Automatisierter Aufruf des W3C-Validatorsさんで紹介されているプログラムです。
validate.plと違うのは、ローカルに置いたサイトファイルごとに結果を、W3C Validation Serviceの結果ページ(もちろん今回は、ローカルサーバで検証した結果のページ)を一ファイルごとにサイト構造通りに書き出してくれるところでしょうか。
どちらのタイプが好みかは、使う目的にもよるでしょうが、検証したHTMLファイルの数だけファイルが新規に作成されるので、視認性の点で面倒かなと思います。

#!/usr/local/bin/perl -w 
##
## This program is free software; you can redistribute it
## and/or modify it under the terms of the GNU General
## Public License as published by the Free Software Foundation
## (http://www.gnu.org/); either version 2 of the License, or
## (at your option) any later version.
##
## Author : Dirk Steinchen (dirk@atelier89.de)
## Webpage: http://www.atelier89.de/users/dirk/checkhtml.html
## Version: 1.0

use strict;
use LWP::UserAgent;
use HTTP::Request::Common;

## edit this to your needs
    ## upload (post) files or get validation results
    ##  via uri? 0=uri, 1=upload
    my $upload = 0;
    ## absolute path to your local html files
    ##  e.g. '/exports/www/testsite/' or 'c:/www/testsite/'
    ##  don't forget the "/"
    my $releasedir = '/Library/WebServer/Documents/target_dir/';
    ## absolute path to your files on local www-server
    ##  e.g. '/testsite/' don't forget the "/"
    my $localw3dir = '/target_dir/';
    ## your local www-server
    ##  don't insert the "/"
    my $localserv = 'http://localhost:8889';
    ## the validator listens here
    my $validator = 'http://localhost:8889/w3c-validator/check';
    ## use "\n\r" for DOS/Win and "\n" for unix-like OS, "\r" for MAC
    my $cr = "\n";
    ## Generate Output: 0=yes, 1=no
    my $quiet = 0;
    ## mask for files we test, actually *.htm and *.html
    my $html_regex = '^([a-z0-9\-\_\.]*?)\.html?$';
    ## name of the user-agent for testing, libwww version is appended later
    my $useragent = "W3C_Validator/local ";

## edit lines below this line shouldn't be needed
    ## error-counter
    my $errors = 0;
    ## file-counter
    my $files = 0;
    ## start-time
    my $time = time;

    print $cr, $cr, "Beginn processing", $cr, "-----", $cr if $quiet == 0;

    ## process (and recurse) the given directory
    process_directory($releasedir);
    ## statistics
    $time = time - $time;
    print "----", $cr, "Finished: $files files in $time sec" if $quiet == 0;
    print " - $errors errors found.", $cr, $cr if $quiet == 0;
    ## return 0 if no errors otherwise the error-count
    exit $errors;

sub process_directory {
    my $directory = $_[0];
    ## internal var's
    my (@subdir, @verzeichnis,
     $key, $content, $ua, $req, $data, $file);

    ## read current directory
    opendir (VERZEICHNIS, $directory) || die "$directory: $!".$cr;
    @verzeichnis = readdir (VERZEICHNIS);
    closedir (VERZEICHNIS);

    print $cr, "Processing directory $directory", $cr if $quiet == 0;

    ## process each entry in directory list
    foreach $key (@verzeichnis) {
    ## is current entry a html-file?
    if ($key =~ /$html_regex/i) {
        ## if so, process this entry
        print " processing\t$key\t" if $quiet == 0;
        $data = $directory;
        ## if errors found, they go in this file
        $file = $key.".err.html";
            ## create user-agent
        $ua = new LWP::UserAgent;
        ## user agent's name
        $ua->agent($useragent.$ua->agent);
        ## validation by uri oder upload?
        if ($upload == 0) {
        ## get the resource name on local www-server
        $data =~ s/$releasedir/$localw3dir/i;
        ## request for validation (get via uri)
        $req = new HTTP::Request (
                    GET=>"$validator?uri=$localserv$data$key");
        } else {
        ## request for validation (upload file)
        $req = POST "$validator",
            Content_Type => 'multipart/form-data',
            Content => [
            uploaded_file => [ "$data$key" ]
            ];
        }
        ## result of validation
        $content = $ua->request($req)->content;
        ## one more file...
        $files++;
        ## write result to file
        open (DATEI, "> $directory$file") || die $!;
        print DATEI $content;
        close (DATEI);
        ## valid/wellformed document?
        if ($content =~ /No errors found\!/) {
        ## if so, clean up
        print "o.k.\t" if $quiet == 0;
        unlink "$directory$file";
        }
        else {
        ## if document is not valid, inc error-counter
        ##  and leave file with results on disk
        $errors++;
        print "Error\t" if $quiet == 0;
        }
        ## that's it for this page
        print " done.$cr" if $quiet == 0;
    } else {
        ## if directory entry is a subdir
        if (-d "$directory$key") {
        ## save this entry for later processing
        push (@subdir, $key)
        }
    }
    }
    ## process subdir's in current directory
    foreach $key (@subdir) {
    ## process only entrys that not refer to
    ##  current (.) or upper (..) directory
    if (!($key =~ /^\.*$/)) {
        ## process subdirectory (and recursivly it's subdirectorys)
            process_directory ("$directory$key/");
    }
    }
}
$ perl checkhtml.pl

これで、ターミナル上に処理の進行状況とターゲットディレクトリに結果ファイルが書き出されます。処理結果の一覧もファイルに書き出したいときは、リダイレクトをしましょう。perl checkhtml.pl > result.txt勿論、パスの通っているところに置いておけば、最初のperlは必要ありません。
実際やってみると、validなページもエラーと判定されますが、書き出された結果ページはちゃんとvalidになっています。ちょっと困りものですね。

一ファイルごとに結果を書き出すタイプ

validate.py

W3C HTML batch Validator in Pythonさんが公開されているスクリプトです。Perl版のcheckhtml.plのPython移植版です。

"""HTML batch validator

Usage: python validate.py config.txt

adapted from  Perl version at http://atelier89.de/users/dirk/checkhtml.html

You need httplib_multipart.py and if you want to validate files like
.shtml or .php you need the specially modified version which uses a
default content-type mimetype of "text/html" (UPLOAD should be 0 in this
case).

specify the following values in config:

    VALIDATORURL = w3

    FILESERVERROOT = 'e:\files'
        absolute path to your local fileroot, Unix: use /, Win use \ separator!

    LOCALSERVERURL = 'http://example.org'
        GET from this URL if UPLOAD=0 or UPLOADFROMURL=1

    VALIDATEPATH = 'home'
        validate all files starting from this path

    SKIPPATHS = ['WEB-INF']
        skip all files and subdirectories in these paths

    UPLOAD = 1
        POST (upload from FILESERVERROOT) files (=1) or GET (from LOCALSERVERURL) results (=0)

    UPLOADFROMURL = 1
        If UPLOAD=1 (POST) HTML to validate will be fetched from LOCALSERVERURL,
    else from FILESERVERROOTVALIDATEPATH. Filenames to be fetched will always
    be the filenames in FILESERVERROOTVALIDATEPATH.

    EXTS = ['html', 'htm']
        validate files with these extensions

    REPORTDIR = '__validator'
        reports are saved in this directory

    OPENREPORTS = 0
        If =1 automatically open report pages in default HTML viewer (normally a webbrowser).
"""
__version__ = '1.7'
__author__ = 'Christof Hoeke 090620'

import fnmatch
import httplib
# httplib.HTTPConnection.debuglevel = 1
import os
import sys
import time
import urllib
import urlparse
import webbrowser

import httplib_multipart

# validator URLs

w3 = 'validator.w3.org:80'

# use
VALIDATORURL = w3
PATH = '/check'
PROTOCOL = 'http'
UPLOAD = 1
UPLOADFROMURL = 1
FILESERVERROOT = 'e:\files'
LOCALSERVERURL = 'http://example.org'
VALIDATEPATH = 'home'
SKIPPATHS = ['include', 'WEB-INF']
EXTS = ['html', 'htm']
REPORTDIR = '__validator'
OPENREPORTS = 0


HTML = {
    'valid': '[Valid]',
    'invalid': '[Invalid]'
}


def saveresult(relpath, result):
    """
    save error result to fn + ERR.html
    """
    fpath, fname = os.path.split(relpath)
    if os.path.isabs(fpath):
        fpath = fpath[1:]
    resultpath = os.path.join(REPORTDIR, fpath)
    if not os.path.isdir(resultpath):
        try:
            os.makedirs(resultpath)
        except OSError, e:
            print e
    rfname = '%s.ERR.html' % (os.path.join(resultpath, fname))
    open(rfname, 'w').write(result)
    print rfname
    if OPENREPORTS:
        webbrowser.open(rfname)


def main():
    """
    build file list, validate and print result
    """
    print 'Using config:'
    print '\tValidator URL:\n\t\t%s://%s%s' % (PROTOCOL, VALIDATORURL, PATH)
    print '\tLocal files:\n\t\t%s' % FILESERVERROOT
    print '\tValidate files in:\n\t\t"%s"' % VALIDATEPATH
    print '\tSkip directories:\n\t\t%s' % SKIPPATHS
    if not UPLOAD:
        print '\tURL to validate files:\n\t\t%s' % LOCALSERVERURL
    else:
        if UPLOADFROMURL:
            print '\tUploaded files are fetched from \n\t\t%s' % LOCALSERVERURL
    print '\tError Reports are saved to\n\t\t%s' % REPORTDIR
    print '-' * 40

    start = time.time()

    # find files to validate
    files = []
    for dir, dirs, fs in os.walk(
            os.path.join(FILESERVERROOT, VALIDATEPATH)
            ):
        skip = False
        for sp in SKIPPATHS:
            if dir.startswith(os.path.join(FILESERVERROOT, sp)):
                skip = True
                break
        if not skip:
            for ext in EXTS:
                names = fnmatch.filter(fs, '*.%s' % ext)
                names.sort()
                dirfile = [(dir, n) for n in names if not n.endswith('.ERR.html')]
                files.extend(dirfile)

    ok = 0
    num = 0
    errors = 0
    unknown = 0
    # validate
    for fpath, fname in files:
        abspath = os.path.join(fpath, fname)
        relpath = abspath.replace(FILESERVERROOT, '')
        relurl = urllib.pathname2url(relpath)

        # via UPLOAD
        if UPLOAD:
            if UPLOADFROMURL:
                content = urllib.urlopen(urlparse.urljoin(LOCALSERVERURL, relurl)).read()
            else:
                content = open(abspath, 'r').read()

            fields = [('uri', relurl)]
            files = [('uploaded_file', relurl, content)]
            errcode, errmsg, result = httplib_multipart.post_multipart(
                VALIDATORURL, PATH, fields, files)

        # via URL REQUEST
        else:
            url = urlparse.urlunsplit(
                (PROTOCOL, VALIDATORURL, PATH, 'uri=%s' % urlparse.urljoin(LOCALSERVERURL, relurl), None)
                )
            result = urllib.urlopen(url).read()

        num += 1
        # print result
        if HTML['valid'] in result:
            ok += 1
            print '%s\n\t%s' % (relurl, HTML['valid'])
        elif HTML['invalid'] in result:
            errors += 1
            print '%s\n\t%s see' % (relurl, HTML['invalid']),
            saveresult(relpath, result)
        else:
            unknown += 1
            print '%s\n\tUNKNOWN ERROR' % relurl,
            if UPLOAD and  err code <> '200':
                print '[', errcode, errmsg, ']',
            print ', see',
            saveresult(relpath, result)

    print
    print '-' * 40
    print 'Finished %s files in %s sec.' % (num, time.time() - start)
    print '%d invalid, %d valid, %d unknown results' % (errors, ok, unknown)
    if (errors > 0 or unknown > 0) and OPENREPORTS != 1:
        print '\nNotes:'
        print '* open reports automatically if OPENREPORTS = 1.'
        if not UPLOAD:
            print '* The Validator may not be able to request local files, you may need to use option UPLOAD = 1.\n'


if __name__ == '__main__':
    if len(sys.argv) == 1:
        print 'Usage: python validate.py config.txt\n'
    else:
        config = open(sys.argv[1]).read()
        exec(config)
    main()

プログラム内に直接設定項目を記述しても良いのですが、設定だけを抜き出したconfig.txtを利用する方が便利でしょう。

# v1.7 090620
FILESERVERROOT = r'/Library/WebServer/Documents/'
LOCALSERVERURL = 'http://localhost/' # needed if UPLOADFROMURL = 1
VALIDATEPATH = 'target'
SKIPPATHS = ['img','css','js'] # actually subtrees
EXTS = ['html', 'htm']
UPLOAD = 0
UPLOADFROMURL = 0
REPORTDIR = '__validator'
OPENREPORTS = 1

適宜書き換えてください。コマンドを実行したディレクトリ直下のREPORTDIRに設定した名称のディレクトリに結果ファイルが書き出されます。

$ cd /path/to/validate1.7/
$ python validate.py config.txt

config.txtは設定ファイルなので、xx-site.txtみたいな名称でサイトごとにいくつも作っておくのが良いでしょう。
OSX環境下で、”osascript: OpenScripting.framework – scripting addition “/Users/jinn/Library/ScriptingAdditions/XML Tools.osax” cannot be used with the current OS because it has no OSAXHandlers entry in its Info.plist”なエラーが出たら、 Late Night SOFTWAREから最新版をインストールしましょう。インストール先は、~/Library/ScriptingAdditions/です。

HTML5を一括処理

vnu.jar

HTML5で構築されているサイトは、今のところvun.jarを使う方法しかありません。
HTML Validatorのコラムの最後に作成した、vnu.jarの使用法です。

$ java -jar ~/vnu.jar --format json --skip-non-html target_derictory/ > result.txt 2>&1

コマンドライン HTML5 Validator

上記vun.jarを使っているのが、このHTML5 Validatorです。
中身はpythonですが、こちらで紹介します。pip install html5validatorでインストールするように説明がありますが、この方法でインストールしたものは、私の環境では、Javaのエラー(StackOverflowError)で使用できませんでした。試しに、gitで持ってきたものをコンパイルしたものを試したところ無事に動作しました。

$ git clone git@github.com:svenkreiss/html5validator.git
$ cd html5validator
$ python setup.py build
$ python setup.py install
$ html5validator --root target_directory/  --ignore "Attribute "ng-[a-z-]+" not allowed"

おまけ

sitemap.xmlの作成

サイトマップをXML形式で書き出してくれるソフトは、有料だとそこそこ見つかるのですが、フリーウェアだとなかなか良いものがありません。Googleのウェブサービスを使うのが一番効率的でしょうか。

ローカルにあるサイト構造をサイトマップ化するのでひと手間はかかりますが、業務で使う場合はすでに手元に対象サイトをまるごと落としてあるでしょうから不都合はないでしょう。ということで、このpython製のスクリプトをご紹介しておきます。

John D. Cookで提供されている、sitemapmaker.pyです。

ただし、このままでは再帰読み込みをしてくれないので、少々手を入れてあります。ちなみに、これが私のはじめてのpythonプログラム(改造版)です。

# Python code to create an XML sitemap.
# See http://www.sitemaps.org/protocol.php
# Place in same directory as your content and pipe output to a file.
# Assumes all content to index is in one directory.
# Written by John D. Cook, http://www.johndcook.com

import os, time, glob

# your base URL
url = "http://localhost/"

# file types to include in sitemap
extensions_to_keep = ['.htm', '.html', '.pdf'] 


print '<?xml version="1.0" encoding="UTF-8"?>'
print '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'

def fild_all_files(directory):
    for root, dirs, files in os.walk(directory):
        yield root
        for file in files:
            yield os.path.join(directory, file) 
for file in fild_all_files(“/target_directory/"):
    
     # exclude proof-of-ownership files
     if file.startswith( ("google", "y_key_") ):
        continue
    
     file_extension = os.path.splitext(file)[1]    
     if file_extension in extensions_to_keep:
        file_mod_time = time.gmtime(os.path.getmtime(file)) 
        utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", file_mod_time)
        print "    '<url>"
        print "        '<loc>%s%s'</loc>" % (url, file)
        print "        '<lastmod>%s'</lastmod>" % utc
        print "    '</url>"
        
print "</urlset>"

10行目は、バリデートプログラムで都合の良いように書き換えてください。また、13行目のpdfもバリデートには必要ないでしょうから、削除しておいても良いでしょう。24行目はにはターゲットのディレクトリ名を入れてください。

作成したプログラムを、当該ディレクトリのトップに置きます。

$ python sitemapmaker.py > sitemap.xml

リンクチェッカー

W3C LinkChecker packageをダウンロード。

$ cd /path/to/W3C-LinkChecker-X.X
$ perl Makefile.PL
$ make
$ make test
$ sudo make install
// 必要に応じてパールモジュールのインストール
      $ sudo perl -MCPAN -e shell
      cpan[1]> install W3C::LinkChecker
      cpan[2]> install LWP::Protocol::https
      cpan[3]> install Net::IP
      cpan[4]> install Term::ReadKey
$ checklink —version
$ checklink —help

checklinkをサーバーにコピーすればブラウザからも使えるようになります。私は、perlbrewで管理しているので、以下のようにします。また、マックビルトインのapacheサーバとMAMP等の仮想サーバの両方にコピーしてみます。

$ sudo cp /Users/[YourID]/.perlbrew/perls/perl-5.18.1/bin/checklink /Library/WebServer/CGI-Executables/ (ビルトインapache)
$ sudo cp /Users/[YourID]/.perlbrew/perls/perl-5.18.1/bin/checklink /Applications/MAMP/cgi-bin/ (MAMP環境)
$ sudo chmod 755 /Library/WebServer/CGI-Executables/checklink
$ sudo chmod 755 /Applications/MAMP/cgi-bin/checklink

お試しあれ。これで、ウェブサイトのチェックはひととおりこなせますね。あとは、アクセシビリティのチェックだけど、自動では無理なんだよなあ。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください