LoginSignup
75
79

More than 5 years have passed since last update.

POSIX原理主義に基づく究極のスクレイピング

Last updated at Posted at 2014-12-24

稿 Advent Calendar 20141225


199X
使
Bourne Shell


POSIXUNIXOS使



UNIXOS

:AdventCalendar


 Advent Calendar 201411/30~1/3


0)





 Advent Calendar 2014HTML1


Web

crawler2014.html(仮にこういう名前にしておいた)
<!DOCTYPE html><html xmlns:og="http://ogp.me/ns#"><head><meta charset="UTF-8" /><title>クローラー/スクレイピング Advent Calendar 2014 - Qiita</title><meta charset="UTF-8" /><meta content="width=device-width,height=device-height,initial-scale=1" name="viewport" /><meta content="クローラー/スクレイピングに関する話題ならなんでも誰でも OK な Advent Calendar です。
   :




UNIXOS1


POSIXUNIXOS


XML"parsrx.sh"1


POSIXXMLjqxmllint

1)XML


POSIX使UNIX調HTMLXML"parsrx.sh"
./parsrx.sh crawler2014.html

すると、「DOM階層」(XPath形式)と「そこの値」という2列から構成されるテキストに置換されます。

/html/@xmlns:og http://ogp.me/ns#
/html/head/meta/@charset UTF-8
/html/head/meta
/html/head/title クローラー/スクレイピング Advent Calendar 2014 - Qiita
/html/head/meta/@charset UTF-8
/html/head/meta
/html/head/meta/@content width=device-width,height=device-height,initial-scale=1
/html/head/meta/@name viewport
/html/head/meta
/html/head/meta/@content クローラー/スクレイピングに関する話題ならなんでも誰でもOKな Advent Calendar です。\nWebからどうやって情報を集めるか、いろいろな方法を共有しましょう。\n\n例:\n\n言語別のクローラー/スクレイピング方法\nノンプログラムで使えるサービス\nやっぱりExcel最高!!\n情報収集に関する注意点(著作権法、岡崎図書館事件)\n
   :
/html/body/div/div/div/table/tbody/tr/td/p/a &nbsp;richmikan@github
/html/body/div/div/div/table/tbody/tr/td/p
/html/body/div/div/div/table/tbody/tr/td/p/@class adventCalendar_calendar_entry
/html/body/div/div/div/table/tbody/tr/td/p POSIX原理主義に基づく究極のスクレイピング
   :

/html/body/div/div/div/table/tbody

2)


DOMgrep/html/body/div/div/div/table/tbody&nbsp;
./parsrx.sh crawler2014.html                          |
grep ^/html/body/div/div/div/table/tbody/tr           |
sed 's:/html/body/div/div/div/table/tbody::'          | # 深くてウザいので浅く
sed 's/&nbsp;//g'                                       # &nbsp;がウザいのでトル
/tr/td/@class adventCalendar_calendar_day
/tr/td/p/@class adventCalendar_calendar_date
/tr/td/p 30
/tr/td
/tr/td/@class adventCalendar_calendar_day
/tr/td/p/@class adventCalendar_calendar_date
/tr/td/p 1
/tr/td/div/@class adventCalendar_calendar_join
/tr/td/div/button/@class btn btn-success btn-block js-adventCalendar_button-join
/tr/td/div/button/@data-day 1
   :
/tr/td/p/a/i
/tr/td/p/a クローラーをデーモンとして動かす ― Scrapyd
/tr/td/p
   :




  Scrapyd


UNIX

3)"""_"


UNIX

sed
./parsrx.sh crawler2014.html                          |
(途中省略)                                             |
sed 's/ /_/g; s/_/ /'

こうするとスペースが階層区切りの部分だけになり、AWK等で簡単に扱えるようになります。

/tr/td/@class adventCalendar_calendar_day
/tr/td/p/@class adventCalendar_calendar_date
/tr/td/p 30
/tr/td
/tr/td/@class adventCalendar_calendar_day
/tr/td/p/@class adventCalendar_calendar_date
/tr/td/p 1
/tr/td/div/@class adventCalendar_calendar_join
/tr/td/div/button/@class btn_btn-success_btn-block_js-adventCalendar_button-join
/tr/td/div/button/@data-day 1
   :
/tr/td/p/a/i
/tr/td/p/a クローラーをデーモンとして動かす_―_Scrapyd
/tr/td/p
   :

4)階層区切りの"/"を">"に変更

この後の作業でDOM階層文字列部分を正規表現に掛け、値の意味を判定するのですが、"/"だと厄介です。ですから">"という文字に置き換えてしまいましょう。

./parsrx.sh crawler2014.html                          |
(途中省略)                                             |
awk '{gsub(/\//,">",$1);print;}' # 第1列の"/"だけ全置換する
>tr>td>@class adventCalendar_calendar_day
>tr>td>p>@class adventCalendar_calendar_date
>tr>td>p 30
>tr>td
>tr>td>@class adventCalendar_calendar_day
>tr>td>p>@class adventCalendar_calendar_date
>tr>td>p 1
>tr>td>div>@class adventCalendar_calendar_join
>tr>td>div>button>@class btn_btn-success_btn-block_js-adventCalendar_button-join
>tr>td>div>button>@data-day 1
   :
>tr>td>p>a Ruby+Mechanizeで対話型のスクレイピング
>tr>td>p
>tr>td
>tr
>tr>td>@class adventCalendar_calendar_day
>tr>td>p>@class adventCalendar_calendar_date
>tr>td>p 7
   :


5)


"td""tr"
./parsrx.sh crawler2014.html                          |
(途中省略)                                             |
awk 'BEGIN       {r=1;c=1;     }                      #
     $1==">tr"   {r++;c=1;     }                      #
     $1==">tr>td"{c++;         }                      #
     "at_last"   {print r,c,$0;}'

コードにある"at_last"というのは、「各行の処理の最後にここを実行する」ということをコメントするために、私が勝手につけただけなので気になる人は書かなくて構いませんよ。

このコードを通したものがこちらです。このようにして、行頭に行列番号がつくことで各行4列(一部3列)になります。

1 1 >tr>td>@class adventCalendar_calendar_day
1 1 >tr>td>p>@class adventCalendar_calendar_date
1 1 >tr>td>p 30
1 2 >tr>td
1 2 >tr>td>@class adventCalendar_calendar_day
1 2 >tr>td>p>@class adventCalendar_calendar_date
1 2 >tr>td>p 1
1 2 >tr>td>div>@class adventCalendar_calendar_join
1 2 >tr>td>div>button>@class btn_btn-success_btn-block_js-adventCalendar_button-join
1 2 >tr>td>div>button>@data-day 1
   :

34grep '^3 4 '便

6)



./parsrx.sh crawler2014.html                          |
(途中省略)                                             |
awk 'NF==4'

7)値のうち、単純文字列とリンクに印を付けて抽出




href
./parsrx.sh crawler2014.html                              |
(途中省略)                                                 |
awk '$1!=r0||$2!=c0           {                           #
       str_n=1;                                           #
       lnk_n=1;               }                           #
     $3==">tr>td>p>a>@href"   {                           #
       $3="lnk" lnk_n;                                    #
       print;                                             #
       lnk_n++;               }                           #
     $3^>tr>td>p([a-z>]*)?$/{                             #
       $3="str" str_n;                                    #
       print;                                             #
       str_n++;               }                           #
     "at_last"                {                           #
       r0=$1;                                             #
       c0=$2;                 }'

上記のコードを通したものが次のテキストです。セルの中で実際に表示される文字列には"str n "という印を、リンク(hrefプロパティー)文字列には"lnk n "という印を、階層パスと置き換えるかたちでつけてあります。

1 1 str1 30
1 2 str1 1
1 3 str1 2
1 3 lnk1 /dkfj
1 3 str2 dkfj
1 3 lnk2 http://blog.takuros.net/entry/2014/12/02/234959
1 3 str3 スクレイピングのお仕事について
1 4 str1 3
1 4 lnk1 /nezuq
1 4 str2 nezuq
   :

なんだか、規則性が見えてきましたね。

8)日にち,著者名,著者URL,記事名,記事URLを横一列化

上記のテキストを見ると

意味
str1 日にち
str2 著者名
lnk1 著者リンク
str3 記事名
lnk2 記事リンク

11

"-"
./parsrx.sh crawler2014.html                              |
(途中省略)                                                 |
awk '$1!=r0||$2!=c0                                {      #
       print r0,c0,day,aut,aln,ttl,tln;                   #
       day="-"; aut="-"; aln="-"; ttl="-"; tln="-";}      #
     $3=="lnk1"{aln=$4;                            }      #
     $3=="lnk2"{tln=$4;                            }      #
     $3=="str1"{day=$4;                            }      #
     $3=="str2"{aut=$4;                            }      #
     $3=="str3"{ttl=$4;                            }      #
     "at_last"{r0=$1; c0=$2;                       }      #
     END      {print r0,c0,day,aut,aln,ttl,tln;    }'

横一列が長くなってしまって見にくいかもしれませんが、これでOKです。

1 1 30 - - - -
1 2 1 - - - -
1 3 2 dkfj /dkfj スクレイピングのお仕事について http://blog.takuros.net/entry/2014/12/02/234959
1 4 3 nezuq /nezuq Webスクレイピングの法律周りの話をしよう! /nezuq/items/3cc9772118ad112c18dc
1 5 4 shogookamoto /shogookamoto 普及して欲しくないアンチスクレイピングサービス http://happyou-info.hatenablog.com/entry/2014/12/04/005504
1 6 5 dkfj /dkfj Ruby+Nokogiriでスクレイピング http://blog.takuros.net/entry/2014/12/05/061034
1 7 6 dkfj /dkfj Ruby+Mechanizeで対話型のスクレイピング http://blog.takuros.net/entry/2014/12/06/235232
2 1 7 orangain /orangain Pythonでクローリング・スクレイピングに使えるライブラリいろいろ http://orangain.hatenablog.com/entry/scraping-in-python
2 2 8 dkfj /dkfj クローラー/スクレイピングのWebサービス 「Kimono」のユースケース http://blog.takuros.net/entry/2014/12/08/100216
2 3 9 sue445 /sue445 ccc_privacy_bot_を支える技術 http://sue445.hatenablog.com/entry/2014/12/09/000000

AWKを使えば、「2行1列目の記事名」とか「14日を担当している著者名とそのリンク」なんていう指定も簡単です。

「2行1列目の記事名」が欲しいなら、これを付け足す
awk '$1==2 && $2==1 {print $6}'
「14日を担当している著者名とそのリンク」が欲しいなら、これを付け足す
awk '$3==14 {print $4,$5}'

9)CSVにする


ExcelCSVRFC 4180



ExcelShift_JISCR+LF
./parsrx.sh crawler2014.html                              |
(途中省略)                                                 |
sed 's/"/""/g'                                            |
awk '$1!=r0{printf("\n");                              }  #
     $2>1  {printf("," );                              }  #
     "at_last"                                         {  #
       printf("\"%s\n%s\n%s\n%s\n%s\"",$3,$4,$5,$6,$7);   #
       r0=$1;                                             #
       c0=$2;                                          }' |
awk 'NR>5'                                                |
iconv -f UTF8 -t SJIS                                     |
sed "s/\$/$(printf '\r')/"                                > crawler2014.csv

こうしてできたファイルをExcelで開いたものがこちらです。

CSV化したここのAdventCalendar2014

できあがり

ここまでのコードをまとめると、こうなります。

# === 1)HTMLパース ====================================== #
./parsrx.sh crawler2014.html                              |
# 1:値のパス 2:値文字列                                   #
#                                                         #
# === 2)表の部分だけに絞り込む ========================== #
grep ^/html/body/div/div/div/table/tbody/tr               |
sed 's:/html/body/div/div/div/table/tbody::'              | # 深くてウザいので浅く
sed 's/&nbsp;//g'                                         | # &nbsp;がウザいのでトル
#                                                         #
# === 3)値としての" "は"_"に置換 ======================== #
sed 's/ /_/g; s/_/ /'                                     |
#                                                         #
# === 4)階層の"/"がウザいので">"に変更 ================== #
awk '{gsub(/\//,">",$1);print;}'                          |
#                                                         #
# === 5)セル上の行列番号を付加 ========================== #
awk 'BEGIN       {r=1;c=1;     }                          #
     $1==">tr"   {r++;c=1;     }                          #
     $1==">tr>td"{c++;         }                          #
     "at_last"   {print r,c,$0;}'                         |
# 1:行番号 2:列番号 3:値のパス 4:値文字列(あれば)       #
#                                                         #
# === 6)値を持たない行はトル ============================ #
awk 'NF==4'                                               |
# 1:行番号 2:列番号 3:値のパス 4:値文字列                 #
#                                                         #
# === 7)値のうち,単純文字列とリンクに印を付けて抽出 ===== #
awk '$1!=r0||$2!=c0           {                           #
       str_n=1;                                           #
       lnk_n=1;               }                           #
     $3==">tr>td>p>a>@href"   {                           #
       $3="lnk" lnk_n;                                    #
       print;                                             #
       lnk_n++;               }                           #
     $3^>tr>td>p([a-z>]*)?$/{                             #
       $3="str" str_n;                                    #
       print;                                             #
       str_n++;               }                           #
     "at_last"                {                           #
       r0=$1;                                             #
       c0=$2;                 }'                          |
# 1:行番号 2:列番号 3:値の種別と出現番号 4:値文字列       #
#                                                         #
# === 8)日にち,著者名(URL),記事名(URL)を横一列化 ======== #
awk '$1!=r0||$2!=c0                                {      #
       print r0,c0,day,aut,aln,ttl,tln;                   #
       day="-"; aut="-"; aln="-"; ttl="-"; tln="-";}      #
     $3=="lnk1"{aln=$4;                            }      #
     $3=="lnk2"{tln=$4;                            }      #
     $3=="str1"{day=$4;                            }      #
     $3=="str2"{aut=$4;                            }      #
     $3=="str3"{ttl=$4;                            }      #
     "at_last"{r0=$1; c0=$2;                       }      #
     END      {print r0,c0,day,aut,aln,ttl,tln;    }'     |
# 1:行番号 2:列番号 3:日にち 4:著者 5:著者URL 6:記事名 7:記事URL
#                                                         #
# === 9)CSVにする ======================================= #
sed 's/"/""/g'                                            |
awk '$1!=r0{printf("\n");                              }  #
     $2>1  {printf("," );                              }  #
     "at_last"                                         {  #
       printf("\"%s\n%s\n%s\n%s\n%s\"",$3,$4,$5,$6,$7);   #
       r0=$1;                                             #
       c0=$2;                                          }' |
awk 'NR>5'                                                |
iconv -f UTF8 -t SJIS                                     |
sed "s/\$/$(printf '\r')/"                                > crawler2014.csv

1コマンドをプログラムの1ステップにするのがUNIXの美学


"|"15UNIX使XML"parsrx.sh"26

UNIX11AWK使1



tee


POSIXUnixOS使








UNIX



POSIX


POSIX12/30 西2 -33aShell Script 2014
75
79
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

75
79