Frog Advent Calendar 2017初日は海外の求人情報スクレイピングをやっていく

太平洋時間でこんばんは、chan_gamiです!

Frog Advent Calendar 2017が始まりました、相変わらずAdventarの使い心地が良くて安心です。去年にも増して多くの方に参加していただけるので、新年まで楽しく過ごせます。やっていくぞ!!!!!

今年のテーマは「海外就職を希望する人たちに役立つこと」なので、初日はカナダ・バンクーバーでの求人情報をさらっと見ていきましょう。

まずは求人を見てみる

初心者であれ、勉強中であれ、マイルストーンと現状の差分を意識できなければ遠回りしてしまうかもしれません。海外で就職したいのであれば現状がどんなステータスであったとしても、まず現地の求人情報を見ることをオススメします。

では、王道のindeedで行きましょう。
https://www.indeed.ca/

希望するジョブタイトルと就職場所を入力して検索します。とりあえずwhatに”developer”、whereに”vancouver”でポチっとな。

Boostされている求人情報にはピンクで囲ったような表示があります。

結果の中から気になる求人情報を見てください。

Required Skills とか Requirement とか Qualifications とか書かれている部分が要求されているスキルセットです。Basic Requirementのような記述があれば基本条件、Preferred Requirementのような記述はあったら嬉しいスキル。ここに並んでいる単語でわからないものがあれば、そのスキルを身に付けていくように調整して就職活動フェーズに入れると動きやすいですね。

自身に不足しているスキルを意識して、想定しているレールと企業が求めているスキルセットの溝を埋めていきましょう。

スクレイピングしてみる

本題です。今の手順で得られるデータを整理してみると、何か見えてくることがあるかもしれません。スクレイピングしてみましょう。当たり前ですが、極力相手に負荷をかけないよう注意しましょう。

参考:ウェブスクレイピング – Wikipedia

今回はNode.jsでプログラムを書いていきます。

cheerio と puppeteer

Node.jsには cheerio というDOMを扱うための優れたパッケージがあります。今まで、cheerio と request を合わせてスクレイピングを行うのが速くて便利でした。もちろん phantomJS も便利でした。

ただ、最近ではSPAを筆頭に動的なサイトが増えてきました。上の2つでは動的に生成される要素に簡単に対応できません。

そこで個人的に注目しているのが puppeteer です。
これはGoogle Chrome開発チームが作っている、ヘッドレスChromeをNode.jsから簡単に扱えるようにしたものです。ヘッドレス、つまりGUIなしのChromeです。でもブラウザなのでSPAでも何でも、Chromeで見ることができるものは対応可能です。

この puppeteer を使っていきます。

スクレイピングをやっていく

Node.js v7.6.0以上が使える環境で行っていきます。async/awaitをガンガン使っていきます。

どこか適当なディレクトリでnpm initしてEnter連打してから、npm install --save puppeteerで準備完了です。

求人を見たときに自分がやったこと・プログラム上でやりたいことを洗い出しましょう。

  • indeed.caにアクセスする
  • whatフォームに”developer”、whereフォームに”vancouver”と入力する
  • 結果の中から気になるタイトルを選ぶ
  • 気になるタイトルがあればクリックしてリンクを開く

こんな感じでしょうか。
全てのRequimentを取得して比較するには余白が足りなさすぎるので、

  • indeed.caにアクセスする
  • whatフォームに”developer”、whereフォームに”vancouver”と入力する
  • 結果の全てのタイトルを取得する
  • 全てのタイトルでどのような単語が使われているかランク付けする

くらいで収めたいと思います。

コーディングのお時間です。

同じ階層にindex.jsを作成してください。そして以下を書いてください。

const puppeteer = require('puppeteer');
const baseURL = "https://www.indeed.ca/";

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(baseURL, {waitUntil: 'networkidle', timeout: 0});

  await page.pdf({path: 'test.pdf', scale: 'A4'});
  await browser.close();
})();

package.jsonも少し編集します。scriptsの中にstartコマンドを追加してください。

{
  ...
  "scripts": {
    "start": "node index.js",
  ...
}

はい、最初の一歩目ができました。これでnpm startしたらスクリーンショットのPDFが出力されます。どうですか?
何をやっているかというと、Chromeを立ち上げてhttps://www.indeed.ca/を開き、画面のスクリーンショットを取ってChromeを終了しています。

簡単ですね。

ではもう少し進んでみましょう。

index.js

  ...
  await page.goto(baseURL, {waitUntil: 'networkidle', timeout: 0});

  // ここから追加
  // whatフォームに"developer"と入力する
  await page.type('input#what', 'developer');

  // whereフォームに"Vancouver"と入力して末尾までの余分な文字列を削除する
  await page.focus('input#where');
  await page.keyboard.type('Vancouver');
  await page.keyboard.down('Shift');
  await page.keyboard.press('ArrowDown');
  await page.keyboard.up('Shift');
  await page.keyboard.press('Backspace');

  // 検索ボタンをクリックして読み込みが終わるのを待つ
  await page.click('input#fj');
  await page.waitForNavigation({waitUntil: 'networkidle', timeout: 0});
  // ここまで追加

  await page.pdf({path: 'test.pdf', scale: 'A4'});
  ...

フォーム入力を行って検索ボタンを押し、表示された画面のスクリーンショットをPDFで保存しています。あとはこのDOMを取得できればOKですね。

index.js

  ...
  await page.waitForNavigation({waitUntil: 'networkidle', timeout: 0});

  // ここから追加
  let doms = await page.$$('h2.jobtitle a.turnstileLink, a.jobtitle.turnstileLink');

  for (const dom of doms) {
    let result = await (await dom.getProperty('textContent')).jsonValue();
    result.trim();
    console.log(result);
    let link = await (await dom.getProperty('href')).jsonValue();
    link.trim();
    console.log(link);
  }
  // ここまで追加

  await browser.close();
  ...

取得したい対象のSelectorを実際にChromeの開発者ツールなどを見ながらjQueryのように取得してやります。絞り込みに少しコツが必要かもしれませんが慣れておいて損はないはずです。
その後、取得した対象のテキストとhrefプロパティを取って出力しています。

スクレイピングの方法はこんな感じです。

多く含まれている単語を調べてみた

さて本日の最終章です。

puppeteerを使った基本的な操作は把握できているかと思います。

今回12ページ分250件のタイトルを取得して、どのような単語がジョブオファーのタイトルに選ばれているのか調べてみました。

出現した回数と単語の結果一覧だけ載せておきます。

{
  "210":"Developer"
  "77":"Software"
  "50":"Web"
  "46":"End"
  "40":"Front"
  "35":"Full"
  "34":"Stack"
  "34":"Engineer"
  "25":"Intermediate"
  "25":"Junior"
  "14":"Java"
  "9":"C#"
  "9":"Back"
  "9":"Javascript"
  "9":"Android"
  "6":"NET"
  "6":"Part"
  "5":"Development"
  "5":"Systems"
  "5":"Frontend"
  "5":"Application"
  "5":"Senior"
  "5":"React"
  "5":"Backend"
  "4":"end"
  "4":"Contract"
  "4":"PHP"
  "4":"Time"
  "4":"Instructor"
  "4":"or"
  "4":"Mobile"
  "3":"Designer"
  "3":"Tools"
  "3":"Test"
  "3":"Co"
  "3":"Analyst"
  "3":"Developers"
  "3":"Remote"
  "3":"Programmer"
  "2":"The"
  "2":"Design"
  "2":"Uniface"
  "2":"4GL"
  "2":"js"
  "2":"ASP"
  "2":"Xamarin"
  "2":"Generalists"
  "2":"Native"
  "2":"in"
  "2":"Gears"
  "2":"of"
  "2":"Blockchain"
  "2":"Visibility"
  "2":"3D"
  "2":"Wordpress"
  "2":"War"
  "2":"App"
  "2":"Coalition"
  "2":"IOS"
  "2":"time"
  "2":"Applications"
  "2":"JavaScript"
  "2":"Unity"
  "2":"op"
  "2":"Azure"
  "2":"Services"
  "2":"New"
  "2":"Grad"
  "2":"Matchmaking"
  "2":"Drupal"
  "2":"Pulse"
  "1":"Business"
  "1":"Ruby"
  "1":"on"
  "1":"Redux"
  "1":"Fullstack"
  "1":"Rails"
  "1":"Levels"
  "1":"iOS"
  "1":"Website"
  "1":"6"
  "1":"Op"
  "1":"MINISIS"
  "1":"WEB"
  "1":"APPLICATION"
  "1":"DEVELOPER"
  "1":"SOFTWARE"
  "1":"ENGINEER"
  "1":"Month"
  "1":"Permanent"
  "1":"Site"
  "1":"FTT"
  "1":"months"
  "1":"Desktop"
  "1":"Core"
  "1":"Graphic"
  "1":"Hot"
  "1":"Start"
  "1":"up"
  "1":"Django"
  "1":"12"
  "1":"FIFA"
  "1":"Embedded"
  "1":"Shopify"
  "1":"Associate"
  "1":"1"
  "1":"Per"
  "1":"Apps"
  "1":"Project"
  "1":"stack"
  "1":"Localization"
  "1":"Computer"
  "1":"Bas"
  "1":"All"
  "1":"Security"
  "1":"Node"
  "1":"2D"
  "1":"AR"
  "1":"Tekken"
  "1":"Gastown"
  "1":"front"
  "1":"dev"
  "1":"opportunity"
  "1":"at"
  "1":"a"
  "1":"startup"
  "1":"Game"
  "1":"UI"
  "1":"Team"
  "1":"w"
  "1":"Ambitions"
  "1":"4"
  "1":"Evo"
  "1":"Car"
  "1":"Share"
  "1":"Pipeline"
  "1":"Develo"
  "1":"year"
  "1":"contract"
  "1":"Swift"
}

おおかた予想通りですかね?

本日上がっている募集ではJuniorIntermediateが同じで、Seniorは1/5程度でした。分野はSoftwareWebが多く、Mobileは1/12程度。言語単位ではJavaが多く、C#もぼちぼちというのが意外です。業種はFrontendFullstackBackendの順ですね。

この結果の中で気になる単語を使って検索をかけると、より自分にマッチした職種・業種の情報に絞りこめると思います。


以上です。ありがとうございました。