ジョブカンをＷＥＢスクレイピング（スタッフ情報）

日本で一番有名な勤怠管理システム「ジョブカン」（ごますりすり）！
残念ながら公開ＡＰＩがなく自社システムへデータを連動させる仕組みを提示してくれていません。
ＣＳＶダウンロード機能はありますが、自分の手でＣＳＶを一旦ダウンロードして、それを自社システムへアップロードして・・・という手間が必要です。
ここのところを自動化したいという思いがありまして、連動させるための仕組みを調べてみた次第です。

この記事はジョブカンから如何にしてデータを取り出してくるかのトライアル記事です。

同じお悩みをもった方への一助になれば幸いです。

# はじめに

わかる人にはわかるはず！という乱暴な投稿です。

残念ながらジョブカンにはＡＰＩが公開されておらず、スタッフ情報（一覧）、スタッフ打刻情報を取り出す正規の方法がありません。でもやってみたいので下記の情報を無理やり取り出してみました。

・ スタッフ情報 <== ここで紹介
・ 打刻時刻情報 <== 第二弾で紹介！[ここです！](https://qiita.com/harhid/items/e9a23cc6a2f56eec164c)


ここで記載する方法は「ジョブカン」運営に怒られても責任とれません。
いつこの方法を「ジョブカン」運営がガードするかもわかりません。

自己責任でご利用ください。
とは言っても全ソースをここでは公開しないのでないところは自分で作りこんでくださいね。


# 前提

Java を使ってごりごりと実装します。(Java 1.8 で確認）
必要なライブラリはMavenみたいなかんじで。

## 利用するライブラリ
jsoup
gson
lombok

# わかったこと

・WEBスクレイピングでジョブカン管理画面へログインできること
・スタッフ情報をJSON形式で取得できること
・取得できる情報は下記のとおり(リスト形式）。

| JSON       | 内容 | 補記    |
|:-----------------|:------------------|:------------------|
| employee_id  | スタッフの連番 |(*註1) |
| name   | スタッフ名 | ジョブカンへ登録した名前 |
| employee_code  | スタッフコード | ジョブカンへ登録したコード |
| group_id  | スタッフが所属するグループのID | (*註2) |

(*註1) ジョブカンへスタッフ登録したときに自動で割り振られた連番。
この連番はジョブカン管理画面で見ることはできません。
スタッフ情報のプライマリキーのようなものと理解すればよさそう。
(*註2) ジョブカンで登録した所属グループごとの連番。プライマリキー？


# JSoup を使ううえでクッキーを保持するために

```:JobCanCookies.java
public class JobCanCookies {

	@SuppressWarnings("unused")
	private static Logger logger 
            = LoggerFactory.getLogger(JobCanCookies.class);

	@Getter
	/** JobCan Cookies */
	private Map<String,String> cookies;

	public JobCanCookies(){
		init();
	}
	
	public void init(){
		this.cookies = new HashMap<String,String>();
	}
	
	public void save(Map<String,String> cookies){
		for(String key : cookies.keySet()){
			this.cookies.put(key, cookies.get(key));
		}
	}
}

```

# スタッフ情報を取り出す

```
JobCanCookies cookies = new JobCanCookies();
JobCanStaffDownloadParams pStaff = new JobCanStaffDownloadParams();
JobCanWebStaffDL staffDL = new JobCanWebStaffDL(cookies);
String rsltStaff = staffDL.download(pStaff);

// rsltStaff 
// ↑ 下記のように JSON形式で取得できる！
// {"count":2,
//    "list":[
//       {"employee_id":"1",
//         "name":"empName01",
//         "employee_code":"empCode01",
//         "group_id":"xx"
//       },
//       {"employee_id":"2",
//         "name":"empName02",
//         "employee_code":"empCode02",
//         "group_id":"xx"
//       },
//     ],"employee_id":-1,"direct":0,"no_error":0}
//
// 後はご自由にご利用ください。下記はGsonで変換している例です
Gson gson = new Gson();
JobCanEmployeeInfo info = gson.fromJson(json, JobCanEmployeeInfo.class);

```
```:JobCanEmployeeInfo.java
public class JobCanEmployeeInfo {

	@SerializedName("count")
	@Expose
	private Integer count;
	@SerializedName("list")
	@Expose
	private List<JobCanEmployee> list = null;
	@SerializedName("employee_id")
	@Expose
	private Integer employeeId;
	@SerializedName("direct")
	@Expose
	private Integer direct;
	@SerializedName("no_error")
	@Expose
	private Integer noError;

	public Integer getCount() {
		return count;
	}

	public void setCount(Integer count) {
		this.count = count;
	}

	public List<JobCanEmployee> getList() {
		return list;
	}

	public void setList(List<JobCanEmployee> list) {
		this.list = list;
	}

	public Integer getEmployeeId() {
		return employeeId;
	}

	public void setEmployeeId(Integer employeeId) {
		this.employeeId = employeeId;
	}

	public Integer getDirect() {
		return direct;
	}

	public void setDirect(Integer direct) {
		this.direct = direct;
	}

	public Integer getNoError() {
		return noError;
	}

	public void setNoError(Integer noError) {
		this.noError = noError;
	}
}
```
```:JobCanEmployee.java
public class JobCanEmployee {

	@SerializedName("employee_id")
	@Expose
	private String employeeId;

	@SerializedName("name")
	@Expose
	private String name;
	@SerializedName("employee_code")
	@Expose
	private String employeeCode;
	@SerializedName("group_id")
	@Expose
	private String groupId;

	public String getEmployeeId() {
		return employeeId;
	}

	public void setEmployeeId(String employeeId) {
		this.employeeId = employeeId;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getEmployeeCode() {
		return employeeCode;
	}

	public void setEmployeeCode(String employeeCode) {
		this.employeeCode = employeeCode;
	}

	public String getGroupId() {
		return groupId;
	}

	public void setGroupId(String groupId) {
		this.groupId = groupId;
	}

```
```:JobCanStaffDownloadParams.java
public class JobCanStaffDownloadParams implements Serializable{

}
```
```:JobCanWebStaffDL.java
public class JobCanWebStaffDL {

	private static Logger logger 
             = LoggerFactory.getLogger(JobCanWebStaffDL.class);
	/** スタッフページ（スタッフ一覧） */
	private static final String STAFF_URI 
             = "https://ssl.jobcan.jp/client/employee";
	/** ダウンロード */
	private static final String DOWNLOAD_URI 
             = "https://ssl.jobcan.jp/default/employee-query";

	/** JobCan Cookies */
	private JobCanCookies cookies;
	
	public JobCanWebStaffDL(JobCanCookies cookies){
		
		this.cookies = cookies;
	}
	/**
	 * スタッフ一覧ページにアクセスする（ログイン未の場合はログインする）
	 * @param params
	 * @return
	 * @throws JobCanWebException
	 */
	private Response staffPage(JobCanStaffDownloadParams params) 
                                            throws JobCanWebException{
		
		StringBuilder b = new StringBuilder();
		b.append(STAFF_URI);
		try {
			URL targetUrl = new URL(b.toString());
			Connection con = Jsoup.connect(targetUrl.toString())
                                        // ContentTypeを無視する
					.ignoreContentType(true) 
					.cookies(this.cookies.getCookies())
					.userAgent(JobCanConstants.Jsoup.UA)
					.method(Method.GET)
					.followRedirects(true);
			Response res = con.execute();
			this.cookies.save(res.cookies());
			
			if(JobCanWeb.isLogin(res)){
				return res;
			}else{
				Response reRes 
                                  = JobCanWeb.login(targetUrl, this.cookies);
				this.cookies.save(reRes.cookies());
				return reRes;
			}
			
		} catch (IOException e) {
			e.printStackTrace();
			logger.error(e.getMessage());
			throw new JobCanWebException(e);
		}
		
	}
	
	/**
	 * スタッフ情報ダウンロード
	 * @param params
	 * @return
	 * @throws JobCanWebException
	 */
	public String download(JobCanStaffDownloadParams params) 
                                            throws JobCanWebException{
		
		// 注意：スタッフ一覧を表示しておかないとダウンロードできない！
		staffPage(params);
		StringBuilder b = new StringBuilder();
		b.append(DOWNLOAD_URI);
		try {
			URL targetUrl = new URL(b.toString());
			Connection con = Jsoup.connect(targetUrl.toString())
                                        // ContentTypeを無視する
					.ignoreContentType(true) 
					.cookies(this.cookies.getCookies())
					.userAgent(JobCanConstants.Jsoup.UA)
					.method(Method.GET)
					.followRedirects(true);
			Response res = con.execute();
			this.cookies.save(res.cookies());
			
			if(JobCanWeb.isLogin(res)){
				return res.body();
			}else{
				throw new JobCanWebException("ログインできない");
			}
			
		} catch (IOException e) {
			e.printStackTrace();
			logger.error(e.getMessage());
			throw new JobCanWebException(e);
		}
	}
}

```
```:JobCanWeb.java

public class JobCanWeb {
	
	private static Logger logger = LoggerFactory.getLogger(JobCanWeb.class);

	/** LOGIN URL */
	private static final String LOGIN_URL 
                          = "https://ssl.jobcan.jp/login/client";

	/** LOGOUT URL */
	private static final String LOGOUT_URL 
                          = "https://ssl.jobcan.jp/client/logout/";

	/** 勤怠会社ID */
	public static final String CLIENT_LOGIN_ID = "client_login_id";
	/** グループ管理者ログインID */
	public static final String CLIENT_MANAGER_LOGIN_ID = "client_manager_login_id";
	/** グループ管理者パスワード */
	public static final String CLIENT_LOGIN_PASSWORD  = "client_login_password";
	/** 勤怠会社ID */
	private static final String ITIMES_JOBCAN_COMPANY_ID = "C1xxxx-xxxxx-xxxxx";
	/** グループ管理者ログインID */
	private static final String ITIMES_JOBCAN_MANAGER_ID = "mxxxxxxx";
	/** グループ管理者パスワード */
	private static final String ITIMES_JOBCAN_MANAGER_PW = "XXXXXXXX";

	private static final String LOGIN_URL_PATTERN = "^https://ssl.jobcan.jp/login/client.*$";

	public static boolean isLogin(Response res){
		String urlStr = res.url().toString();
		if(urlStr != null && urlStr.matches(LOGIN_URL_PATTERN)){
			return false;
		}
		return true;
	}
	
	public static Response logout(JobCanCookies cookies) throws JobCanWebException{
		Response res;
		try {
			res = Jsoup.connect(LOGOUT_URL)
					.userAgent(JobCanConstants.Jsoup.UA)					
					.cookies(cookies.getCookies())
					.method(Method.GET)
					.followRedirects(true)
					.execute();
			return res;
		} catch (IOException e) {
			e.printStackTrace();
			logger.error(e.getMessage());
			throw new JobCanWebException(e);
		}
	}
	
	public static Response login(URL targetUrl, JobCanCookies cookies) throws JobCanWebException{
		
		try {
			Map<String, String> hiddens = new HashMap<String, String>();
			hiddens.put(CLIENT_LOGIN_ID, ITIMES_JOBCAN_COMPANY_ID);
			hiddens.put(CLIENT_MANAGER_LOGIN_ID, ITIMES_JOBCAN_MANAGER_ID);
			hiddens.put(CLIENT_LOGIN_PASSWORD, ITIMES_JOBCAN_MANAGER_PW);
			//ログイン後の自動遷移先
			hiddens.put("url", targetUrl.toString()); 
			hiddens.put("login_type", "2");
			hiddens.put("save_login_info", "0");
			Connection con = Jsoup.connect(LOGIN_URL)
					.userAgent(JobCanConstants.Jsoup.UA)
					// ContentTypeを無視する
					.ignoreContentType(true) 
					.cookies(cookies.getCookies())
					.data(hiddens)
					.followRedirects(true)
					.method(Method.POST);		
			Response res = con.execute();
			return res;
			
		} catch (IOException e) {
			e.printStackTrace();
			logger.error(e.getMessage());
			throw new JobCanWebException(e);
		}
	}
	
}

```