ShellScript: 特定のwebサイトから必要な情報を抜きし,過去の結果を比較し差分をとりだす.


はじめに

 仕事や個人的関心で,特定のキーワードを検索システムを用いて定期的に検索することがある.検索結果を保存し,新しい情報があればそれを取り出せると便利である.shellscriptを用いて特定のサイトでやってみた.

環境

  • macOS: 10.15.4
  • zsh: 5.7.1

スクレイピングを行う際の注意点.

  • 過去に何度も記事にされている.一般的な注意点に留意して行う[2][10].

手順概要

  • pubmedというwebサイトでcoronaを検索して得られるソースsourceをzshのcurlコマンドで取得する.
  • そこから,論文タイトルURLを取得する.
  • 結果はcsv形式で保存する.
  • 後日同じ操作をし,新しい検索結果と古い検索結果と差分を取り出す.
  • 適宜wc -lgrep --color=autoなど用いて確認する.
  • 注)pubmedとは"生命科学や生物医学に関する参考文献や要約を掲載するMEDLINEなどへの無料検索エンジンである"[1]

方法

目標のwebページを直接訪問してURL取得する.

  • https://www.ncbi.nlm.nih.gov/pubmed/
  • 上記を訪れ,画面上部の検索フォームにcoronaと入力して検索.
  • https://www.ncbi.nlm.nih.gov/pubmed/?term=coronaが得られる.

(pubmedのメインページ.画面上部に検索キーワードを入力するボックスがある)


coronaで検索すると関連する論文が列挙される.)

curlコマンドでhtmlを取得する.

  • 変数`tgtUrlに上記で取得したURLを代入.
  • tgtはtargetの意味.
curlコマンドでsouce(情報)を取得.
tgtUrl='https://www.ncbi.nlm.nih.gov/pubmed/?term=corona'
curl $tgtUrl

必要な部分を抜き出す.

  • 取り出す部分がどこか,取り出す部分を示すマーカーとなる文字列が何か,などは当初はわからない.
  • 試しに検索結果の一番上にある論文のタイトルの一部HISTORICALgrepで検索してみる.
  • 文字数が多く視認性が悪いので,オプション--color=autoで検索語に色をつける.
  • すると,webページに列挙されていた論文群は,sourceにおいては1行にまとまめられていることがわかる.
curl $tgtUrl | grep --color=auto 'HISTORICAL'

grep結果

  • 検索語が赤文字になっている.
  • 複数の論文情報が1行に並んでいる.

ブロック抽出に毎回使える検索ワードを探す.

  • 同じブロック内で使えそうな検索ワードを探す.
  • タイトル直前のdocsum_titleが使えそうなので,やってみたらうまくいった.
grep --color=auto 'docsum_title'

論文へのURLを抽出する.

  • URLそのものはない.ただし,"/pubmed/32324963"のように論文の整理番号があるのでとりあえずそれを取り出す.
  • 後で,文字列をつなげて以下のような形にすればよい.
  • https://www.ncbi.nlm.nih.gov/pubmed/32324963

必要とする文字列の出現順を把握する.

  • URLの代用とする論文整理番号論文タイトルの順に出現する.

1行ごとに各論文がおさまるようにする.

  • URLの代用とする論文整理番号の直前で改行をすれば各行ごとに1つの論文情報がはいることとなる(下図).
  • <a href="/pubmed/を文末改行に置換する.
  • sedで文末改行に置換する方法は過去に何度か記事にされている[11].
sed 's/<a\ href\=\"\/pubmed\//\'$'\n/g'
# 上記には不要なエスケープ(\)があるかもしれません.
  • 下図の如く,1行目は不要にて削除.削除にsedを利用する.
sed -n '1!p'

必要とする文字列(タイトル)の直前に目印となる文字列を挿入する.

  • 他にも方法はあるが,取り扱いやすさなど考慮し,今回はこのようにした.
  • 目的の文字列の抽出につかったdocsum_title"を目印YYYYに置換する.
sed 's/docsum_title\">/YYYY/g'

各行毎に必要な文字列(整理番号とタイトルの2箇所)をとりだす.

sedで最短一致をする.

  • sedは最長一致が基本.
  • 行頭からみて初めのダブルクオート"までの文字列をとりだしたい(最短一致)ときは以下の前者ではうまくいかない.後者のようにする[3][4].
最長一致,うまくいかない例
sed '^.*"'
最短一致
sed '^[^"]*"'
  • 1つ目のキャレット^は行頭を表す正規表現.2つめの^文字クラスの否定を表す[5].すなわち,[^"]"以外の文字クラス.[^"]*"以外の文字が0個以上ならぶことを意味する.

sedで文字列を切り出す.

  • かっこ()を利用する.
  • たとえば,行頭のURLXXXからURLを抜き出すときは()で挟む.
  • かっこで挟むと「セーブ」される.[6]
  • かっこは\でエスケープ する.
  • 置換後ブロックで\1でかっこで挟んだ箇所が取り出せる.
sed 's/\(^URL\)XXX/\1/'
  • 取り出したい箇所が複数離れて存在するときは\1\2・・・とする.
sed 's/\(^URL\)XXX\(タイトル\)YYY/\1/\2/'

sedの区切り文字はスラッシュ/以外でもよい.

  • 検索文字列の中に/が混ざっており,sedの区切り文字で/を使うとエスケープなど面倒くさい.
  • 今回はsedの区切り文字としてパイプ|を使う.[3][7]

ところで,デリミタがスラッシュ(/)でなければならないアドレスとは違って,正規表現は空白と改行以外のどんな文字もデリミタに使える.だからパターンにスラッシュが入っているときには,たとえば次のようにエクスクラメーションマークなど別の文字をデリミタにすると便利だ[7].

今回の場合

  • 下図のようにする.
  • 論文整理番号\1論文タイトル\2との間にカンマ,`をいれる.
sed 's|\(^[^"]*\)".*YYYY\(.*\)</a>.*$|\1,\2|g'

URLを完全な形にする.

  • 論文管理番号を以下の文字列に連結する.
  • https://www.ncbi.nlm.nih.gov/pubmed/
  • 最後のsed置換のときに行う.
  • 変数urlPart_1に納め,sed内で展開するので,一旦シングルクオート'を外し,変数展開し,再び'で挟んでいる.
urlPart_1='https://www.ncbi.nlm.nih.gov/pubmed/'
#中略
sed 's|\(^[^"]*\)".*YYYY\(.*\)</a>.*$|'$urlPart_1'\1,\2|g'

リダイレクトでcsv形式で保存する.

  • test1.csvとする.
  • 後日,同じ操作を行い,標準出力として新しいデータを得る.今回は簡単のため新しくファイルを作るtest2.csv
  • test1.csvtest2.csvとを比較する.
  • 差分(新しい記事)があれば,csvファイルに書き加える.
  • 差分は「配列と集合操作を利用する方法」用いて取り出す[8].

applescriptを併用して新しい記事があれば~件ですと通知させる.

  • 変数oldLineにもとのcsvファイルの行数を代入.
  • 変数newLineに新しい記事を追加したcsvファイルの行数を代入.
  • newLine-oldLineが新しい記事の数.
  • osascriptヒアドキュメントを用いて,applescriptでdisplay dialogを実行する.この際,Shellscriptからapplescriptへ変数渡しをする.
  • 変数渡しにはちょっとしたテクニックが必要[9].

コード全体

  • 見通しを良くするため適宜改行している.
tgtUrl='https://www.st-va.ncbi.nlm.nih.gov/pubmed/?term=corona'
urlPart_1='https://www.ncbi.nlm.nih.gov/pubmed/'

# メイン
curl $tgtUrl \
| grep --color=auto 'docsum_title' \
| sed 's/<a\ href\=\"\/pubmed\//\'$'\n/g' \
| sed -n '1!p' \
| sed 's/docsum_title\">/YYYY/g' \
| sed 's|\(^[^"]*\)".*YYYY\(.*\)</a></p>.*$|'$urlPart_1'\1,\2|g' > '/Users/myACCOUNT/Desktop/test2.csv'

# test.csvに納めている論文の数.
oldLine=$(cat '/Users/myACCOUNT/Desktop/test.csv' | wc -l)

# 配列と集合演算を利用して差分を取り出す.
originalIFS=$IFS
IFS=$'\n'
oldArr=($(cat "/Users/myACCOUNT/Desktop/test.csv"))
newArr=($(cat "/Users/myACCOUNT/Desktop/test2.csv"))
IFS=$originalIFS
comArr=("$oldArr[@]" "$newArr[@]")

(
printf "%s\n" "$comArr[@]" | sort | uniq\
; printf "%s\n" "$oldArr[@]")\
| sort | uniq -u >> "/Users/myACCOUNT/Desktop/test.csv"

# 新しい記事を追加した後の記事数.
newLine=$(cat '/Users/myACCOUNT/Desktop/test.csv' | wc -l)

# 新しく集めた記事の数.
dN=$(($newLine-$oldLine))

# applescriptを利用して新規記事の数を通知.
osascript <<EOL
    display dialog "新しい記事は " & $dN & " 件です" as string
EOL

考察

  • 1日数十から100程度のタイトルを流し読みする程度ならば今回のようなことをするよりも手作業の方が早い.
  • 対象とするwebサイトの仕様がかわると書き換えなくてはならなくて面倒[10].pubmedも,この記事を書いている間にインターフェイスが変わってしまった.
  • 今回は試用なので,文末の改行有無の確認などしておらず,うまく働いていない箇所があるかもしれない.

まとめ

  • shellscriptを用いてスクレイピングを行った.
  • 集めるデータ数に応じて,手作業でやるかプログラムを組むかの選択をする必要がある.

参考

[1]https://ja.wikipedia.org/wiki/PubMed
PubMed - Wikipedia
[2]https://qiita.com/nezuq/items/c5e827e1827e7cb29011
Webスクレイピングの注意事項一覧 - Qiita
[3]https://ja.wikipedia.org/wiki/PubMed,PubMed - Wikipedia
https://orebibou.com/2017/07/sed%E3%81%A7%E7%BD%AE%E6%8F%9B%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AB%E6%9C%80%E7%9F%AD%E4%B8%80%E8%87%B4%E3%81%A7%E6%8C%87%E5%AE%9A%E3%82%92%E3%81%99%E3%82%8B/
sedで置換する際に最短一致で指定をする | 俺的備忘録 〜なんかいろいろ〜
[4]http://dokuwiki.fl8.jp/bash/script/12_sed_shortest_match
12 Sed 最短一致 [fl8 Wiki]
[5]Dale Dougherty, Arnold Robbins,1997年,sed & awkプログラミング 改訂版,O'REILLY,福崎俊博(訳),p41,3.2.4.2 文字クラスの除外
[6]Dale Dougherty, Arnold Robbins,1997年,sed & awkプログラミング 改訂版,O'REILLY,福崎俊博(訳),p91,5.3.1 置換文字列メタキャラクタ
[7]Dale Dougherty, Arnold Robbins,1997年,sed & awkプログラミング 改訂版,O'REILLY,福崎俊博(訳),p90,5.3 置換
[8]https://qiita.com/BlackCat_617/items/f3f8ad1068682aa114f5
shell script: 2つのcsvファイルの差分を,配列を利用した集合演算を用いて取り出す. - Qiita
[9]https://qiita.com/kkdd/items/b9d41aa57ba5c40829b9
シェルスクリプトで AppleScript 利用 - Qiita
[10]https://qiita.com/miyabisun/items/9883f7b7006c09efa5a0
スクレイピングのやり方をQ&Aサイトで質問するな - Qiita
[11]https://qiita.com/kkdd/items/725e53572bc69e4b51b7
sed による置換で改行\nを出力する - Qiita