JobPosting構造化データをクローリングする

この記事は [ただの集団 AdventCalendar PtW.2019](https://gw-advent.9wick.com/calendars/21) の1日目の記事です。
明日は kzone さんの [DjangoをTerraformでFargateにデプロイする話](https://qiita.com/kazunee/items/329d8a99151383706ad8) です。

# はじめに
今までは求人サイトから求人情報をクロールして集めようとしても、各サイトで作りがバラバラなので自動で正確なデータを収集することが困難だった。
しかし、 "Google for jobs" がJobPosting形式のクローリングを始めたことで、各求人サイトが求人情報をJobPosting形式で掲載するようになってきたため、求人情報クローリングが今までより簡単になるかもしれない。

# JobPosting構造化データとは？
- [JobPosting構造化データ](https://schema.org/JobPosting) とは、検索エンジン用に様々な構造化データを定義している [schema.org](https://schema.org/) が作った求人用の構造化データ定義。
- 現在は34項目程度のプロパティがある。
- Microdata, RDFa, JSON-LDの3形式に対応している。

JSON-LDの場合の例

```html
<script type="application/ld+json">
{
  "@context": "http://schema.org/",
  "@type": "JobPosting",
  "name": "Mobile App Developer",
  "hiringOrganization": {
    "@type": "Organization",
    "name": "ACME Software",
  },
  "relevantOccupation": {
    "@type": "Occupation",
    "name": "Software Developers, Applications",
    "occupationalCategory": "15-1132.00"
  }
}
</script>
```

# JobPostingに対応している求人サイト
「求人」でGoogle検索して上位にきたサイトのうち、ログインしないと求人情報が見れないサイトを除いた上位30サイトを調査したところ21サイトが対応していて、7割のサイトが既に対応済みということになる。
特筆すべきは対応していた全サイトがGoogle推奨のJSON-LD形式であること。

|siteName|url|JobPosting|
|---|---|---|
|エン転職|https://employment.en-japan.com/|☓|
|リクルートエージェント|https://www.r-agent.com/|☓|
|Indeed|https://jp.indeed.com/|☓|
|タウンワーク|https://townwork.net/|☓|
|求人ボックス|https://求人ボックス.com/|○|
|はたらいく|https://www.hatalike.jp/|○|
|とらばーゆ|https://toranet.jp/|☓|
|フロムエー|https://www.froma.com/|○|
|ハローワークの求人を検索|https://www.hellowork.careers/|☓|
|マイナビバイト|https://baito.mynavi.jp/|○|
|しゅふJOBパート|https://part.shufu-job.jp/|○|
|バイトル|https://www.baitoru.com/|○|
|キャリアインデックス |https://careerindex.jp/|○|
|an|https://weban.jp/|☓|
|ディースターNET|https://d-starjob.com/|○|
|ハローワークインターネットサービス|https://www.hellowork.go.jp/index.html|☓|
|マイナビミドルシニア|https://mynavi-ms.jp/|○|
|イーアイデム|https://www.e-aidem.com/|○|
|doda|https://doda.jp/|○|
|求人ジャーナルネット|https://www.job-j.net/|○|
|女の転職type|https://woman-type.jp/|○|
|Green|https://www.green-japan.com/|○|
|dジョブ|https://job.dmkt-sp.jp/|○|
|ものコレ|https://monocolle.jp/work/|○|
|クックビズ|https://cookbiz.jp/|○|
|YAHOO!しごと検索|https://feature-job.yahoo.co.jp/|☓|
|求人＠飲食店.COM|https://job.inshokuten.com|○|
|求人情報もってけ！|https://mtke-job.jp/|○|
|しゅふきた|https://www.shufukita.jp/|○|
|転職会議|https://jobtalk.jp/|○|


# GoogleのJobPostingの扱い
-  [Googleの求人情報に関する構造化データ](https://developers.google.com/search/docs/data-types/job-posting?hl=ja) に詳細が書いてある。

## 必須プロパティ
|プロパティ名|内容|所感|
|---|---|---|
|datePosted|雇用主が求人情報を投稿した最初の日付（ISO 8601 形式）。|前回と変更があるかの確認や新着表示のために使えそう|
|description|HTML形式での求人の詳細な説明。|HTML形式なので色々な表現ができそう|
|hiringOrganization|職位を提供している組織。|ロゴも含められるようなので視認性も豊かになりそう|
|jobLocation|オフィスや作業現場など、従業員の職場となる特定の場所（求人情報を投稿した場所ではない）。|かなり柔軟に細かく入力できるみたい|
|title|職務の名称（求人情報のタイトルではない）。|目立つ文言を入れたくなるけど職務の名称のみが推奨されている|
|validThrough|求人情報が期限切れになる日付（ISO 8601 形式）。|必須と言いつつ有効期限がある場合のみで良いみたい|

## 推奨プロパティ
|プロパティ名|内容|所感|
|---|---|---|
|applicantLocationRequirements|従業員がリモートワークを行うために所在する必要のある地域。|リモートワークに関する項目が推奨されているのがGoogleらしい|
|baseSalary|雇用主から提示された実際の基本給（概算額ではない）。|時給〜年収、min〜maxなど細かく指定できる|
|employmentType|雇用形態。|雇用形態を知らずに応募できないので必須でも良い気がする|
|identifier|求人に関する採用側組織の一意の識別子。|掲載側でIDが付けられるのは嬉しい|
|jobLocationType|業務時間中、自宅など本人が選択した場所で常にリモートワークする求人の場合、このフィールドに TELECOMMUTE を設定します。|リモートワークに関するプロパティが多い|

# JobPosting構造化データを取得する
Scalaとjsoupを使って実際にJobPosting構造化データを取得してみる。
ソースは [job-postiong-crawl-sample](https://github.com/nhiguchi/job-postiong-crawl-sample) にアップした。

```scala
import org.jsoup.Jsoup
import io.circe._, io.circe.parser._

object Main extends App {
  val detailUrl = "https://cookbiz.jp/job/job54024.html"
  val jobPostingScriptTag = Jsoup.connect(detailUrl).get().select("script[type=\"application/ld+json\"]")
  val jobPostingJson = parse(jobPostingScriptTag.html()) match { case Right(json) => json }

  // required values
  println("datePosted=[%s]".format(extract(jobPostingJson, "datePosted")))
  println("description=[%s]".format(extract(jobPostingJson, "description")))
  println("hiringOrganization=[%s]".format(extract(jobPostingJson, "hiringOrganization", "name")))
  println("jobLocation=[%s]".format(extract(jobPostingJson, "jobLocation", "addressRegion")))
  println("title=[%s]".format(extract(jobPostingJson, "title")))
  println("validThrough=[%s]".format(extract(jobPostingJson, "validThrough")))

  // recommended values
  println("applicantLocationRequirements=[%s]".format(extract(jobPostingJson, "applicantLocationRequirements")))
  println("baseSalary=[%s: %s~%s]".format(extract(jobPostingJson, "baseSalary", "unitText"), extract(jobPostingJson, "baseSalary", "minValue"), extract(jobPostingJson, "baseSalary", "maxValue")))
  println("employmentType=[%s]".format(extract(jobPostingJson, "employmentType")))
  println("identifier=[%s.%s]".format(extract(jobPostingJson, "identifier", "name"), extract(jobPostingJson, "identifier", "value")))
  println("jobLocationType=[%s]".format(extract(jobPostingJson, "jobLocationType")))

  def extract(json: Json, keys: String*) = {
    val result = keys.foldLeft(json)((j, k) => j.\\(k) match {
      case head :: _ => head
      case _ => Json.Null
    })
    if (result == Json.Null) "" else result.toString
  }
}
```

実行結果

```
datePosted=["2019-04-26"]
description=["◆大人から子どもまで、みんなが満たされる大衆寿司居酒屋「回転寿司だと子どもたちは楽しめるけど、お父さんはちょっと物足りない…」「カウンターの寿司店だと、子どもを連れていくには懐も雰囲気も気がきでない」そんなファミリー層もお腹と舌を満たせるような寿司店をめや盛り付け、寿司の提供を習得してください。対面式カウンターなので、調理をしながら接客や会話もお願いします。また、ホールでの接客も経験してください。店長就任後は、シフト管理・棚卸・発注・計数管理など店舗運営全般をお任せします。◆たくさんの「楽しい！」が生まれる場所としすることで、お客さまがスタッフに話しかけてくれます。そこからコミュニケーションが生まれ、お店の雰囲気もにぎやかになるんです。提供した料理を見た瞬間「なにこれ！」と驚いたり喜んだり。とにかくその場にいるみんなが「楽しい！」と思えるお店づくりを心掛けています。お客さまに喜んでもらえる仕組みをたくさん作っていきませんか？"]
hiringOrganization=["株式会社スシロークリエイティブダイニング"]
jobLocation=["東京23区"]
title=["店舗スタッフ（店長・副店長候補）"]
validThrough=[]
applicantLocationRequirements=[]
baseSalary=["MONTH": 306000~370800]
employmentType=["FULL_TIME"]
identifier=["株式会社スシロークリエイティブダイニング"."54024"]
jobLocationType=[]
```

値の入っている項目は全て取得できた。
かなり汎用的な作りが可能になりそう。

# まとめ
今まで正確に自動化することが難しかった求人情報クローリングの分野に"Google for Jobs"が参入したことによって、JobPosting構造化データでの求人公開が一般的になりつつあり、今後は自動化がかなり楽になりそう。

# 参考にした本・サイト
[クローリングハック あらゆるWebサイトをクロールするための実践テクニック](https://www.amazon.co.jp/dp/B074FYX8QN)
[Webスクレイピングの注意事項一覧](https://qiita.com/nezuq/items/c5e827e1827e7cb29011)
[JobPosting](https://schema.org/JobPosting)
[Googleの求人情報に関する構造化データ](https://developers.google.com/search/docs/data-types/job-posting?hl=ja)