昨日のTSV(Seesaaにある旧ブログの記事リスト)を作った手順です。約50個のWebページから、リンク情報を抽出・整理してTSV出力する例として。

実行環境

• Windows7 x64 + Cygwin 2.5.1 + ConEmu 150813g
• Windows版PostgreSQL 9.5.3 + Cygwin版psql
• Windowsは管理者権限ユーザ、PostgreSQL接続はスーパーユーザ

Bash + cURLでHTMLを一括取得・保存

元となるHTMLは、旧ブログ(途中で小リニューアルしたので2つ)の記事リストページ全てです。自分の場合、最初のリストがブログトップ(ドキュメントルート)を兼ね、2番目以降はindex-■.htmlというアドレス。これがリニューアル前は42個、後は7個あり、まとめると ↓ こうなります。
http://kenpg.seesaa.net/
http://kenpg.seesaa.net/index-2.html … index-42.html 
http://kenpg2.seesaa.net/
http://kenpg2.seesaa.net/index-2.html … index-7.html
これをcURLで自動・連続取得し、1つのHTMLファイルに追記保存していきます。抽出する記事リンクは「どのindex-■.htmlに存在したのか」不明になりますが、今回それは不要なので、処理をしやすく1ファイルに。 BashシェルでForループを使い ↓ こんな風にしました。2つのドキュメントルートはindex-1.htmlからリダイレクトされ、それをURLにすれば全体を単純なループで処理可能。curlの-Lオプションでリダイレクト先ページも取得できます。
$ export outfile='r:/tmp/indexes.html';
[[ ! -f $outfile ]] || rm $outfile;
for i in $(seq 1 42); do
    curl -L http://kenpg.seesaa.net/index-$i.html >> $outfile
    if (($i <= 7)); then
        curl -L http://kenpg2.seesaa.net/index-$i.html >> $outfile 
    fi
done
↓ 結果のファイル。約50個のHTMLが1つになり、ファイルサイズは約1MB。参考までzipにして置いておきます。
» indexes.html.zip(75kB) 文字コードはSeesaaブログのままでShift JIS(画像中のオレンジの囲み)。実は記事タイトルの中にpgAdminのようにローマ数字があり、UTF-8で処理する際は注意が要ります。詳細は次項で。

HTMLの改行・タブを全削除してPostgreSQLに格納

今回、抽出対象(各記事のリンク等の情報)がHTMLの1行ずつに収まってません。例えば記事の日付とタイトルが別の行だったり。そのままだと正規表現が面倒になるので、改行を全削除し(ついでにタブも)HTMLファイルを「一つながりの大きな文字列」と見なしてPostgreSQLに格納します。 ↓ 行った操作と、結果できたテーブルサイズの確認。 TEXT型の1列だけを持つテーブルを作り、psqlの\copyコマンドにfrom programを使いインポートしてます。ここでHTMLをcat ... trすることで改行・タブを全削除。文字コードは\copyコマンドのオプションでsjisと指定。
# create table html_raw_tmp (line text not null);
# \copy html_raw_tmp from program 'cat r:/tmp/indexes.html | tr -d "\n\r\t"' (encoding 'sjis') 
COPY 1

# select pg_size_pretty(pg_table_size(regclass('html_raw_tmp')));
+----------------+
| pg_size_pretty |
+----------------+
| 424 kB         |
+----------------+
(1 row)
前項の最後に触れたShift JISのローマ数字の件。正常にインポートされているか、pgAdminⅢとその周辺の文字列を正規表現関数regexp_matchesで検索したら ↓ 大丈夫でした。ローマ数字が化けたり消えてれば、検索結果がゼロになるはず。
# select regexp_matches(line, '.{15}pgadminⅢ.{15}', 'gi') from html_raw_tmp; 
+-------------------------------------------------------------+
|                       regexp_matches                        |
+-------------------------------------------------------------+
| {"-space:nowrap\">pgAdminⅢ プラグインを VBS だけに"}        |
| {"-space:nowrap\">pgAdminⅢ で COPY … STDIN"}                | 
| {"-space:nowrap\">pgAdminⅢ 1.18 ベータ版のバグ?<"}         |
| {"-space:nowrap\">pgAdminⅢ プラグイン改良 : R また"}        |
| {"-space:nowrap\">pgAdminⅢ から VBScript を起"}             |
| {"e:nowrap\">コードを pgAdminⅢ で選んで実行</a></li"}       |
| {">コードをポスグレ内に格納し pgAdminⅢ から実行</a></li><"} |
| {"nowrap\">RServeをpgAdminⅢから起動</a></li></"}            |
| {"-space:nowrap\">pgAdminⅢから外部プログラム起動</a>"}      |
+-------------------------------------------------------------+
(9 rows)
PostgreSQLのクライアントエンコーディングは、sjis以外にSHIFT_JIS_2004もあるので文字種によってはその方が良いかも。(参考 : ドキュメント「文字セットサポート」) 今回のHTML、psqlの\copyコマンドでなくiconvで「sjis」から変換すると ↓ 失敗します。yanok.net - CP932変換表の問題が顕在化する例にあるとおり「SHIFT_JISX0213」を指定すればOKで、pgAdminⅢを含む9行(上と同様)が見つかりました。
$ cat r:/tmp/indexes.html | iconv -f sjis -t utf8 > r:/test.txt
iconv: (stdin):15837:190: cannot convert

$ cat r:/tmp/indexes.html | iconv -f SHIFT_JISX0213 -t utf8 > r:/test.txt

$ cat r:/test.txt | grep 'Ⅲ'
                        <li style="white-space:nowrap">2014/01/28 : PostgreSQL : <a href="http://kenpg.seesaa.net/article/386200009.html" style="font-weight:bold; white-space:nowrap">pgAdminⅢ プラグインを VBS だけに簡素化</a></li>
                        <li style="white-space:nowrap">2013/08/14 : PostgreSQL : <a href="http://kenpg.seesaa.net/article/371994508.html" style="font-weight:bold; white-space:nowrap">pgAdminⅢ で COPY … STDIN の代替策</a></li>
                        <li style="white-space:nowrap">2013/07/23 : PostgreSQL : <a href="http://kenpg.seesaa.net/article/370053301.html" style="font-weight:bold; white-space:nowrap">pgAdminⅢ 1.18 ベータ版のバグ?</a></li>
                        <li style="white-space:nowrap">2013/06/12 : PostgreSQL : <a href="http://kenpg.seesaa.net/article/366110260.html" style="font-weight:bold; white-space:nowrap">pgAdminⅢ プラグイン改良 : R または VBS 起動</a></li> 
                        <li style="white-space:nowrap">2013/06/02 : PostgreSQL : <a href="http://kenpg.seesaa.net/article/364366438.html" style="font-weight:bold; white-space:nowrap">pgAdminⅢ から VBScript を起動</a></li>
                        <li style="white-space:nowrap">2013/05/11 : R : <a href="http://kenpg.seesaa.net/article/358832461.html" style="font-weight:bold; white-space:nowrap">コードを pgAdminⅢ で選んで実行</a></li>
                        <li style="white-space:nowrap">2013/05/10 : R : <a href="http://kenpg.seesaa.net/article/358826574.html" style="font-weight:bold; white-space:nowrap">コードをポスグレ内に格納し pgAdminⅢ から実行</a></li>
                        <li style="white-space:nowrap">2013/05/09 : R : <a href="http://kenpg.seesaa.net/article/358530357.html" style="font-weight:bold; white-space:nowrap">RServeをpgAdminⅢから起動</a></li>
                        <li style="white-space:nowrap">2013/04/30 : PostgreSQL : <a href="http://kenpg.seesaa.net/article/357864525.html" style="font-weight:bold; white-space:nowrap">pgAdminⅢから外部プログラム起動</a></li>

PostgreSQLの正規表現で記事リンクを抽出・整理

抽出したい記事情報は ↓ こんな感じで、2種類のタグ構造(ブログの小リニューアル前と後)があります。別々に処理してもいいけど「年月日~URL~記事タイトル」の大まかな並びは共通なので、今回はいっぺんに。以下、PostgreSQLの正規表現関数でクエリを作った過程です。
<li ...>
    YYYY/MM/DD : CATEGORY : <a href="URL" ...>TITLE</a> 
</li>

<div ... >
    YYYY/MM/DD<a href="URL" ...>TITLE</a>
    <span ...>TAG_1</span>
    <span ...>TAG_2</span>
    ...
</div>
(1)HTML全体から記事リンクをざっくり抽出
2つのタグ構造とも、記事リンク部分の先頭は年月日。終わりのタグはLIとDIVどちらかなので、1つの正規表現で ↓ 両方をざっくり拾えます。年月日はここで処理終了。
# select
    ary[1],
    ary[2]
from html_raw_tmp,
    regexp_matches(line,'(\d{4}/\d{2}/\d{2})(.+?)</(li|div)>', 'g') as ary;
+------------+---------------------------------------------------------------------------------------------------------------- 
|    ary     |
+------------+----------------------------------------------------------------------------------------------------------------
| 2014/09/16 |  : このブログについて : <a href="http://kenpg.seesaa.net/article/405510842.html" style="font-weight:bold; white
| 2014/09/14 |  : R : <a href="http://kenpg.seesaa.net/article/405425703.html" style="font-weight:bold; white-space:nowrap">指
| 2014/09/13 |  : R : <a href="http://kenpg.seesaa.net/article/405374546.html" style="font-weight:bold; white-space:nowrap">例
| 2014/09/12 |  : PostgreSQL : <a href="http://kenpg.seesaa.net/article/405030672.html" style="font-weight:bold; white-space:n
| 2014/09/11 |  : PostgreSQL : <a href="http://kenpg.seesaa.net/article/404550720.html" style="font-weight:bold; white-space:n
...
(707 rows)
この時点で、全体が結構長いHTML(約1MB)から大幅に絞り込めました。後の処理を速くしたければ、一時的なテーブルかマテビューに保存するといいです。 (2)記事URLとタイトルを抽出
これも2つのタグ構造で共通の &lt;a href="URL">TITLE&lt;/a> という形なので、一つの正規表現を追加して拾えます。↓
# select
    ary[1],
    r[1] as url,
    r[2] as title
from html_raw_tmp,
    regexp_matches(line,'(\d{4}/\d{2}/\d{2})(.+?)</(li|div)>', 'g') as ary, 
    regexp_matches(ary[2], '<a href="http://([^"]+)"[^>]*>([^<]+)</a>') as r;
+------------+------------------------------------------+-------------------------------------------------------------+
|    ary     |                   url                    |                            title                            |
+------------+------------------------------------------+-------------------------------------------------------------+
| 2014-09-16 | kenpg.seesaa.net/article/405510842.html  | 続きます → http://kenpg2.seesaa.net/                        | 
| 2014-09-14 | kenpg.seesaa.net/article/405425703.html  | 指数表記で 3.00e+08 とかする方法                            |
| 2014-09-13 | kenpg.seesaa.net/article/405374546.html  | 例のメッセージなしで Rgui を起動                            |
| 2014-09-12 | kenpg.seesaa.net/article/405030672.html  | 列なしテーブルの使いみち                                    |
| 2014-09-11 | kenpg.seesaa.net/article/404550720.html  | VBS + BAT でクエリの試行錯誤                                |
...
(3)記事のカテゴリ/タグを抽出
これは位置からして違うので、別々の正規表現で対応せざるを得ません。一つ目はコロンに狭まれたカテゴリ(常に一つ)。↓
# select
    ary[1],
    substring(ary[2], ': ([^ ]+) :')
from html_raw_tmp,
    regexp_matches(line,'(\d{4}/\d{2}/\d{2})(.+?)</(li|div)>', 'g') as ary; 
+------------+--------------------+
|    ary     |     substring      |
+------------+--------------------+
| 2014-09-16 | このブログについて |
| 2014-09-14 | R                  |
| 2014-09-13 | R                  |
| 2014-09-12 | PostgreSQL         |
| 2014-09-11 | PostgreSQL         |
| 2014-09-10 | 音楽、PC、他       |
| 2014-09-09 | このブログについて |
| 2014-09-08 | このブログについて |
| 2014-09-07 | R                  |
| 2014-09-06 | R                  |
| 2014-09-05 | PostGIS            |
| 2014-09-04 | PostGIS            |
| 2015-05-23 |                    |
| 2015-04-19 |                    |
...
カテゴリが見つからなかった行は、リニューアル後でタグが複数あり。それを全て拾ってカンマでつなぎます。↓ regexp_matchesが配列を返すので、string_agg関数で連結する形。
# select
    ary[1], 
    (select string_agg(r[1], ', ')
         from regexp_matches(ary[2], '>([^<]+)</span>', 'g') as r)
from html_raw_tmp,
    regexp_matches(line,'(\d{4}/\d{2}/\d{2})(.+?)</(li|div)>', 'g') as ary; 
+------------+----------------------------------+
|    ary     |            string_agg            |
+------------+----------------------------------+
| 2014-09-16 |                                  |
| 2014-09-14 |                                  |
| 2014-09-13 |                                  |
...
| 2014-09-05 |                                  |
| 2014-09-04 |                                  |
| 2015-05-23 | info                             |
| 2015-04-19 | info                             |
| 2015-04-18 | WebSocket, Python, PostgreSQL    |
| 2015-04-17 | WebSocket, Python                |
| 2015-04-16 | Python, Firefox, WebSocket       |
| 2015-04-15 | BAT, Windows, PC                 |
| 2015-04-14 | PC, Windows                      |
| 2015-04-13 | Python, PostgreSQL               |
| 2015-04-12 | BAT, Python, WSH                 |
| 2015-04-11 | Python, 実行環境, BAT            |
| 2015-04-10 | PostgreSQL, 実行環境, Python     |
| 2015-04-09 | Python, 実行環境, PostgreSQL     |
| 2015-04-08 | Python, EXCEL, xml               |
(4)全体をまとめてクエリ
(1)~(3)を一つにまとめ、where句で不要な行(記事以外へのリンク等)を削り、日付降順にすれば出来上がり。実際クエリすると ↓ こうなります。
# select distinct
    ary[1] as ymd,
    r[1] as url,
    r[2] as title,
    coalesce(substring(str, ': ([^ ]+) :'),
        (select string_agg(r[1], ', ')
         from regexp_matches(str, '>([^<]+)</span>', 'g') as r)
     ) as tag
from html_raw_tmp,
    regexp_matches(line,'(\d{4}/\d{2}/\d{2})(.+?)</(li|div)>', 'g') as ary, 
    cast(ary[2] as text) as str,
    regexp_matches(str, '<a href="http://([^"]+)"[^>]*>([^<]+)</a>') as r
where r is not null
order by 1 desc;
+------------+------------------------------------------+-------------------------------------------------------------+----------------------------------+
|    ymd     |                   url                    |                            title                            |               tag                |
+------------+------------------------------------------+-------------------------------------------------------------+----------------------------------+
| 2015-05-23 | kenpg2.seesaa.net/article/419462759.html | http://kenpg.bitbucket.org/ に移転しました                  | info                             |
| 2015-04-19 | kenpg2.seesaa.net/article/417500233.html | 移転の予告(来月から)                                      | info                             |
| 2015-04-18 | kenpg2.seesaa.net/article/417234379.html | pywebsocket & PostgreSQL の Web クライアント(暫定)        | WebSocket, Python, PostgreSQL    |
| 2015-04-17 | kenpg2.seesaa.net/article/417146780.html | pywebsocket でブラウザ ⇒ サーバに処理中止を指示(暫定)     | WebSocket, Python                | 
| 2015-04-16 | kenpg2.seesaa.net/article/417129799.html | ポータブルな Firefox & Python で WebSocket 通信テスト       | Python, Firefox, WebSocket       |
| 2015-04-15 | kenpg2.seesaa.net/article/417084348.html | Windows コマンドプロンプトをポータブルな Console に変更     | BAT, Windows, PC                 |
| 2015-04-14 | kenpg2.seesaa.net/article/417041397.html | ffmpeg でパソコン操作画面の録画 & VIDEO タグのテスト        | PC, Windows                      |
| 2015-04-13 | kenpg2.seesaa.net/article/416929125.html | psycopg2 でリアルタイムに PostgreSQL のメッセージ取得       | Python, PostgreSQL               |
| 2015-04-12 | kenpg2.seesaa.net/article/416918807.html | Python コマンドプロンプト非表示で Web サーバ起動            | BAT, Python, WSH                 |
| 2015-04-11 | kenpg2.seesaa.net/article/416873383.html | Python バッチファイル一つで Web サーバの起動も再起動もする  | Python, 実行環境, BAT            |
| 2015-04-10 | kenpg2.seesaa.net/article/416824303.html | Python & PostgreSQL のポータブルな Web クライアント(2)    | PostgreSQL, 実行環境, Python     |
| 2015-04-09 | kenpg2.seesaa.net/article/416763140.html | Python & PostgreSQL のポータブルな Web クライアント(1)    | Python, 実行環境, PostgreSQL     |
| 2015-04-08 | kenpg2.seesaa.net/article/416668183.html | Python 2.7 + win32com で Excel ファイルを XML に変換        | Python, EXCEL, xml               |
...
(693 rows)
クエリ結果を確認したらTSV出力。psqlの表示設定をTSV用にし、\gコマンド+出力先ファイルパスを打ちます。別のクエリを実行してなければ、何回でも\gで同じクエリを呼び出せて便利。
# \f '\t' \\ \t on \\ \pset format unaligned 
# \g r:/tmp/kenpg_seesaa_lists.tsv

-- check result
# \! less r:/tmp/kenpg_seesaa_lists.tsv
2015-05-23      kenpg2.seesaa.net/article/419462759.html        http://kenpg.bitbucket.org/ に移転しました     info
2015-04-19      kenpg2.seesaa.net/article/417500233.html        移転の予告(来月から)     info
2015-04-18      kenpg2.seesaa.net/article/417234379.html        pywebsocket & PostgreSQL の Web クライアント(暫定)       WebSocket, Python, PostgreSQL
2015-04-17      kenpg2.seesaa.net/article/417146780.html        pywebsocket でブラウザ ⇒ サーバに処理中止を指示(暫定)    WebSocket, Python
2015-04-16      kenpg2.seesaa.net/article/417129799.html        ポータブルな Firefox & Python で WebSocket 通信テスト      Python, Firefox, WebSocket
2015-04-15      kenpg2.seesaa.net/article/417084348.html        Windows コマンドプロンプトをポータブルな Console に変更   BAT, Windows, PC
2015-04-14      kenpg2.seesaa.net/article/417041397.html        ffmpeg でパソコン操作画面の録画 & VIDEO タグのテスト      PC, Windows
2015-04-13      kenpg2.seesaa.net/article/416929125.html        psycopg2 でリアルタイムに PostgreSQL のメッセージ取得  Python, PostgreSQL
2015-04-12      kenpg2.seesaa.net/article/416918807.html        Python コマンドプロンプト非表示で Web サーバ起動 BAT, Python, WSH
2015-04-11      kenpg2.seesaa.net/article/416873383.html        Python バッチファイル一つで Web サーバの起動も再起動もする  Python, 実行環境, BAT
2015-04-10      kenpg2.seesaa.net/article/416824303.html        Python & PostgreSQL のポータブルな Web クライアント(2)      PostgreSQL, 実行環境, Python 
2015-04-09      kenpg2.seesaa.net/article/416763140.html        Python & PostgreSQL のポータブルな Web クライアント(1)      Python, 実行環境, PostgreSQL 
2015-04-08      kenpg2.seesaa.net/article/416668183.html        Python 2.7 + win32com で Excel ファイルを XML に変換    Python, EXCEL, xml
こうして出力したTSV(kenpg_seesaa_lists.tsv.txt, 78.4 kB)を、昨日の記事リストにそのまま使いました。HTMLにはテーブルの中身を一切置かず、JavaScriptで動的にTSVをテーブル化する方法。個々の記事URLも、単なる文字からハイパーリンクに変換してます。次回はそれについてもう少し詳しく書く予定。