はじめに
私の通っている大学のポータルサイトはPC画面表示にしか対応していません。
そのため周りの友人は
「スマホで見るたびにストレスが溜まるんよね」
とポータルサイトを見るたびにぼやいていました。
私自身、同じように日々ストレスを感じていましたので、スマホで見やすく表示できるアプリを作ってみようと頑張ってみました。
今回はそんな奮闘の一部である「スクレイピング」にフォーカスを当てて備忘録を書いてみます。
環境
・開発環境
Swift 5
Xcode 13.4
CocoaPods
Alamofire
※環境の構築はスキップします。
・ポータルサイトのスペック
Active Academy Advance という大学経営システム(?)
結構いろんな大学で使われてるっぽい。
ASP.NET(.aspx)
.aspx ???
まずここで躓きました。
htmlじゃないんかい。。。
ASP.NETとは
私自身、講義でHTMLやCSSなどをやった程度ですが、.aspxに関してはマイクロソフト系(Office系)のWebアプリなんかでよく見かけますよね。
結局なにか影響があるといえば今回はログイン方法に癖があるだけで、レスポンスとして帰ってくる物の構造もほぼhtmlのそれです。よって詳細はスルーします。
今回のお目当てはレスポンスとして帰ってきたページソースの中にある時間割テーブルのデータです。
では早速やってみます。
1. ログイン準備
今回は諸々の通信をAlamofireに任せます。
まず、ログインの際にどんなパラメータを設定すればいいか確認します。
Chromeのデベロッパーツールなどでログインした際の通信を見てみました。
するとPOSTのペイロードでは以下の値が確認できました。(例)
__LASTFOCUS : 値なし
__EVENTTARGET : 値なし
__EVENTARGUMENT : 値なし
__VIEWSTATE : ごにょごにょ
__VIEWSTATEGENERATOR : ごにょごにょ
__EVENTVALIDATION : ごにょごにょ
txtUserid : 入力されたユーザーID
txtUserpw : 入力されたパスワード
ibtnLogin.x : マウスカーソルの位置(x軸)
ibtnLogin.y : マウスカーソルの位置(y軸)
私の大学のポータルサイトの場合、どうやら必要な値は
VIEWSTATE
VIEWSTATEGENERATOR
EVENTVALIDATION
txtUserid
txtUserpw
ibtnLogin.x
ibtnLogin.y
の7つっぽいです。
中でも
VIEWSTATE
VIEWSTATEGENERATOR
EVENTVALIDATION
この3つは毎回固定です。
ibtnLogin.x
ibtnLogin.y
はマウスカーソルの位置ですが、値は整数なら何でも良さそうです。
2. ログインしてみよう
POSTする値がわかったところで早速Alamofireで通信してみます。
以下の質問と回答を参考にしました。
How to login to a site using POST request? (Swift,iOS)
リンク先によると、一度前項の値を取得するための通信をしたあとに、続けてログイン処理をする必要があるみたいです。
import Foundation
import Alamofire
import Kanna
func logIn() {
var parameter: Parameters = [:]
var viewstate: String = ""
var eventvalidation: String = ""
var viewstategenerator: String = ""
let headers: HTTPHeaders = ["Content-Type": "application/x-www-form-urlencoded"]
AF.request("https://xxxxxx.ac.jp/").responseString { response in
print("ログイン前")
print("\(response.result)")
if let html = response.value {
if let doc = try? HTML(html: html, encoding: String.Encoding.utf8) {
//CSSタグから値をパース
for show in doc.css("input[id='__VIEWSTATE']") {
viewstate=show["value"]!
}
for show in doc.css("input[id='__VIEWSTATEGENERATOR']") {
viewstategenerator=show["value"]!
}
for show in doc.css("input[id='__EVENTVALIDATION']") {
eventvalidation=show["value"]!
}
}
}
//パラメータを設定
parameter = ["__LASTFOCUS":"",
"__EVENTTARGET":"",
"__EVENTARGUMENT":"",
"__VIEWSTATE":viewstate,
"__VIEWSTATEGENERATOR":viewstategenerator,
"__EVENTVALIDATION":eventvalidation,
"txtUserid":"学籍番号", //ユーザーID
"txtUserpw":"パスワード", //パスワード
"ibtnlogin.x":"44",
"ibtnlogin.y":"13" //マウスカーソルの位置 多分なんでもいい
]
//値を取得するための通信をしたあとに、更に続けてログイン処理をする通信をする
AF.request ("https://xxxxxx.ac.jp/",method: .post, parameters: parameter, headers: headers).responseString { response in
print("ログイン後")
print("\(response.result)")
}
}
}
デバッグコンソールを見ると、ログイン前のソースと、ログイン成功後のページソースがズラッと表示されるはずです。
3. 帰ってきたレスポンスをパースしてみる
前項で取得できたresponseのvalueをパースしていきます。
今回は予定されている授業の詳細をひとコマずつまとめて格納することが目標です。
さっそくXPathを用いてほしい値を探します。
(このXPathが曲者だった)
以下のサイトが参考になります。
クローラ作成に必須!XPATHの記法まとめ
そしてリンク先にもあるように、ブラウザから要素をカーソルに合わせるだけ一発でXPathを調べることができる超優れたChrome拡張機能があります。(これを知るまで半日は格闘しました)
こちらからダウンロードできます。
狙うはポータルサイトのトップページにある月表示の時間割の中の値たちです。
ポータルサイトは以下のような表示になっています。
3/21にあるような「春分の日」の部分に時間と教科名(略)が出ます。
ここの「大学」というところ以外
時間、科目名、教授名(画像では隠してますが科目名の下にあります)をXPathで取得します。
試しに先程のXPath Helperを使って時間の要素を見ると
"//td[@id='tabCalender_tabPanelMonth_tblMCell4_2']/div[1]//span[@class='long_text jugyo_link_style']"
のように出てきました。(書き始めの"//"はそれ以前の要素を省略しています)
分解していくと
最初の要素のidの末尾、すなわち「...Cell4_2」は4段目の2列めということを指定しており、
続く「div[1]」はマウスオーバーした内容にある時間、科目名、教授名のなかの1つ目、時間を指定しています。(div[2]なら大学、div[3]なら科目名)
つまりここをうまく指定してやれば順番に要素を取得できるわけです。
for week in 1 ..< 6 { // 〜週目
for day in 1 ..< 7 { // 〜日目
for per in 1 ..< 5 { // 〜段落目(時間、科目名など)
perCount = 0 // 場合分け用
//XPATHでパース
for link in doc.xpath("//td[@id='tabCalender_tabPanelMonth_tblMCell" + String(week) + "_" + String(day) + "']/div[" + String(per) + "]//span[@class='long_text jugyo_link_style']") {
//Cell(week)_(day)
//div[(per)]
//perCountを使って場合分け
//for文の変数perを使うとうまく行かなかった
if perCount == 0 { // 時間
print("時間")
print(link.text)
} else if perCount == 1 { //大学
print("大学")
print(link.text)
} else if perCount == 2 { //科目名
print("科目名")
print(link.text)
} else if perCount == 3 { //教授名
print("教授名")
print(link.text)
}
perCount += 1
}
}
}
}
今回はfor文で回すという強行突破をしました。ひどいコードですね。。。(笑)
あとは煮るなり焼くなりできます。
ifで場合分けしてあるのでそれぞれを変数に代入していけば値表示に使えるでしょう。
私は追加で日付も取得してRealmに1限ごと分けて入れました。
全てのデータResults<timeTable> <> (
[0] timeTable {
dateValue = 20220418;
time = 13:30~15:00;
name = ネットワーク実習 月34;
professor = 教授の名前;
},
これで予定されている授業の詳細をひとコマずつまとめて格納することができました。
まとめなど
以上が今回アプリを作る上で奮闘した日々の一部を書き留めた備忘録です。
Alamofireでログイン処理するにはどうすればいいとか、XPathって何ってところとか。
調べまくってつまみつまみですがなんとか動くようにはできました。
試作ながらアプリはこんな感じになりました。
(Swiftのアイコンになっている部分は時限数のアイコンを入れますので、仮置きです。)
デモでも。 pic.twitter.com/UwnuD3qYPZ
— へじふく (@hukhedge) June 27, 2022
友人も「これなら見やすい」と興奮気味に言ってくれました。
よかったよかった。
実はこの時の友人の興奮した声がかなりのモチベーションになっていて、自分の作ったものが誰かの役に立つことがこんなにも素晴らしいことなのか!と、クサイですが本気で思ってしまいました(笑)
今後はこれを元に機能を増やし、学生が欲しい機能を詰め込んだようなアプリを目指して日々精進します。
最後まで読んでいただいてありがとうございました。