高卒求人票のデータ(pdf)をexcelにするメモ

2020.03.25


概要

 厚生労働省職業安定局の高卒求人情報Web提供サービスのページにログインして
複数の求人番号から求人票をまとめてダウンロードして必要項目をexcelファイルに
出力するシステム。

 システムはwebサーバー内でCGIで動く。 収集したい求人番号のリストをexcelで作成し、
それをコピペで Webページのリストボックスに貼り付ける。そのあと変換ボタンをクリック
するとサーバーは高卒求人情報Web提供サービスにログインをして順次、求人番号から求人票(PDF)
をダウンロード。excelに変換する。excelファイルが出来上がったらWebページにダウンロード
用のリンクを表示する。



1 システム

 サーバー  Amazon AWS  LightSail   20GB 512Mb RAM  OS: Debian9.5 レンタル料3.5$/month
 
  プログラム言語 python3 ( python3.5以上)

  以下のパッケージをapt-get installで追加インストール
  poppler-utils  (pdfをhtmlに変換するpdftohtmlなどが入っている)
 python3-pip    (python3.x の追加モジュールをインスールするツール)
  xvfb           プログラムの中では直接使わないがpyvirtualdisplayモジュールがこれを必要とするらしい
  以下のモジュールをpip3 install で追加インストール
 (apt-get install でインストールするとなぜかうまく動かない)
   pandas (csvファイルを操作するために使う)
   xlrd (excelファイルを読み込むためのもの)
   xlwt(excelファイルに書き込むためのもの)
  openpyxl(excelファイルを扱うモジュール)
  selenium(ブラウザ操作に使う)
  chromedriver(chromeの操作用ドライバ)
   pyvirtualdisplay(chromeを仮想ディスプレイで上で動かすために使う)


2 Webサーバーの設定


 サーバー apache2.4.25
  /home/karappi/public_html/qjin/ 内でcgiを動かす

 /etc/apache2/sites-available/karappi.mydns.jp.conf (一部抜粋)

    ServerAdmin karappi.mydns.jp
    DocumentRoot /home/karappi/public_html/

    <Directory /home/karappi/public_html/>
        Options ExecCGI Indexes FollowSymLinks MultiViews Includes
        AllowOverride All
        Order allow,deny
        allow from all
        ServerSignature Off
        AddHandler cgi-script .cgi .pl .php .py
        AddType text/html .rhtml .shtml .php .py
    </Directory>

.pyの拡張子もブラウザからアクセスすれば動いてしまう。。このような拡張子のファイルを送られて
動かされたら危険である。一応ローカルな領域で使うということで。
 #a2ensite karappi.mydns.jp.conf で登録してapache2を再起動する。なお削除するときは
  a2dissite karappi.mydns.jp.conf


3 入力

 excelなどの表計算ソフト上で1列目 整理番号(学校独自の半角英数字を想定)、2列め13桁の求人番号の
リストを作りそれをWebページのテキストボックスにコピペする。


A
B
1
整理番号
求人番号
2
1000-1
1001000123456
3
1000-2
1001000123459
4
1003
1001001234566
5
2001
1304012121278
*整理番号は独自の書式でOK。上の例では同一企業で職種が異なるものに枝番をつけている。最初が1で始まるものが県内求人。2から始まるものが 県外となっている。処理上、この整理番号の書式については関知しないのでハイフォン含めて半角英数字8桁まで入力OKとする。ただし、空欄はだ め。求人番号は求人票のバーコードをリーダーで読むとすべて13桁の半角数字になる。主番は5桁、枝番は8桁であるがもし8桁に満たない場合は前 に0を詰めた形式になる。
  例 10010-12345 の場合 1001000012345

 ここで上のフィールド名(整理番号と求人番号)を除いたデータ部分のエリアをコピペで
Webページのテキストエリアにコピペしてexcel変換ボタンをクリックするとサーバー内で
qjinxlsx.pyがデータを受け取り処理をする。

 
1000-1 1001000123456
1000-2 1001000123459
1003 1001001234566
2001 1304012121278



 テキストエリアとsubmitボタン、クリアボタ付きのタグ
<form method="post" action="qjinxlsxw.py">
<input value="excel" name="henkan_button" type="submit"> &emsp;&emsp;&emsp;
<input type="reset" value="クリア"><br>
<textarea name="src_text" rows="20" cols="24" wrap="off"></textarea>



4 処理(プログラムのキモの部分抜粋)

(1)初期設定、モジュール等
python3 は# -*- coding: utf-8 -*- という記述は不要 とどこかのサイトにあったので2行目以降はモジュールのimport
#!/usr/bin/env python3.5 
import cgi
import cgitb
cgitb.enable() #CGIスクリプトのエラーがブラウザ上に出る

import pandas as pd #csvファイルのためのモジュール
import xlrd #excel 読み込みのためのモジュール
import xlwt #excel 書き込みのためのモジュール
from qjin_web import qjin_login,  qjinhyoget
from counterup import counterup #カウンタアップモジュール
from counterup import counterchk #カウンタチェックモジュール
import os
import sys,codecs
import io
from time import sleep
import datetime #今日の日付を取得-->出力ファイル名に
from addrAna import addrAna
from webpost import htmlpost

from pyvirtualdisplay import Display
#カウンタ値をテンポラリディレクトリ名等に使用
from folder_soup import target_f #ファイル名取得モジュール
import pathlib
import subprocess #shellコマンド実行で使う


(2)プログラムの流れ
 topページからデータが来なければ、topページを表示する。もしデータがあればelse:以降、処理をする。
 
form=cgi.FieldStorage() #cgiオブジェクトの作成
if form.list==[]:
    toppage()
    sys.ext()
else:
  '''求人票pdfをダウンロードしてexcelファイルを作る


(2)topページの記述
 
def toppage():
    txt='''
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
      <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
            <title>高卒(新卒)求人票を集めるページ</title>
      </head>
      <body vlink="#551A8B" text="#000000" bgcolor="#ffcccc" alink="#EE0000"
        link="#0000EE">
            <h4>新規高卒用求人票のデータを求人番号からexcelに変換するページ</h4>
            <div align="right"> 令和2年3月26日ver1.07<br>
              <div align="left">
            <hr size="2" width="100%">
                <b>入力</b>
            &nbsp;整理番号(学校独自のもの)と求人番号(13桁)のリスト テキストボックスに貼り付ける<br>
            <b>出力</b>
            excelファイル<br>
            <hr size="2" width="100%"><br>
                <form method="post" action="qjinxlsx.py">
                        <input value="excel変換" name="henkan_button" type="submit"> &emsp;&emsp;&emsp;
                        <input type="reset" value="クリア"><br>
                    <textarea name="src_text" rows="20" cols="24" wrap="off"></textarea>
                </form>
                    1回の最大処理件数は28件までとします。<br>
                                処理が終わるとcsvファイルのリンクが表示されます。<br>
                                excel変換をクリックしたらブラウザはしばらくそのままにして下さい。<br>
                                処理時間はおおよそ6秒/件です。ただし非公開等の求人票はその判定に1件あたり約15秒かかります。<br>
                                他のタブを開いて他のサイトを閲覧することは問題ありません。<br>
              </div>
              <br>
            </div>
      </body>
    </html>'''

    print('Content-type: text/html \n\n')
    # 上の行の末尾の改行コード2つはちょっとしたノウハウ
    print(txt)

  webページがなかなか表示できない。 print('Content-type: text/html \n\n')の改行コードを\n 2つ入れて解決。そういえばrubyでもそう だった。

(3)排他処理について
 Webサーバー上で動かすので作業領域や一時ファイルが重複すると困る。そこでアクセスカウンタをディレクトリ名にすることにした。
 またスペックが512Mbと非常に低スペックなサーバーなので。処理を開始したプログラムはアクセスカウンタが奇数のときは処理をしないことに した。
 偶数のときはアクセスカウンタを1upしてから処理を開始し、最後にアクセスカウンタを再度1upして偶数にして処理終了。

 
(4)サーバー上でブラウザを動かすには
  

  webページがなかなか仮想ディスプレイを設定してそこで動作させる。ただし仮想ディスプレイを最後に停止するのを忘れるとメモリを専有したり、CGIが クライアント側ブラウザに
表示されないなどの現象が起こるので注意。

 #仮想Display起動e
 display = Display(visible=0, size=(1024, 768))
 display.start()

 
     この中でseleniumによるchrome制御を行う


  display.stop()#処理が終わったらこれをしないとメモリが開放されない


  高卒求人情報Web提供サービスのページにselenium * chromeでログインするにはqjin_loginという関数を作成した。
 引数として login_page:urlアドレス   login_ID:ログインID   login_Password:ログインパスワード
       downloadsFilePath:pdfのダウンロードファイルパス

 通常chromehはデフォルトでpdfファイルのリンクをクリックするとpdfが開く設定になっているが、それを開かず指定した
pathにダウンロードする設定にする。  
 
呼び出し側 
  browser=qjin_login(login_page,login_ID,login_Password,downloadsFilePath)


呼ばれる方のqjin_login関数
###################################################
# 求人Webサイトログイン 求人番号検索ページへ
#   入力 ID パスワード ダウンロード用ディレクトリ
#    ※ダウンロード用ディレクトリはこの関数では使用しない
#     がブラウザでpdfを直接ダウンロードする
#     モードにして開くために必要
###################################################
def qjin_login(login_page,login_ID,login_Password,downloadsFilePath):
    # pdfを開かずに直にダウンロードするモードの設定
    options = webdriver.ChromeOptions()
    options.add_experimental_option("prefs", {
        "download.default_directory": downloadsFilePath,
        "plugins.always_open_pdf_externally": True
    })
    browser = webdriver.Chrome(options=options)
    
    # 高卒求人Webを開く
    browser.get(login_page)
    # 待ち時間を設定(10秒)
    browser.implicitly_wait(10)
    # ログイン画面に入りユーザーネームを選択
    username = browser.find_element_by_class_name('menu01-middle').find_element_by_id('ID_txtRiyoshaId')
    # send_keysでユーザーID入力します
    username.send_keys(login_ID)
    # パスワードを選択
    password = browser.find_element_by_class_name('menu01-middle').find_element_by_id('ID_txtPassword')
    # send_keysでパスワード入力します
    password.send_keys(login_Password)
    # ログインボタンを選択します
    elem = browser.find_element_by_class_name('menu01-middle').find_element_by_class_name('menu-login')
    # 待ち時間を設定(10秒)
    browser.implicitly_wait(10)
    # ログインボタンをクリック
    elem.click()
    # 待ち時間を設定(10秒)
    browser.implicitly_wait(10)
    
    #'求人情報の検索'のリンクを検索
    elem = browser.find_element_by_link_text("求人情報の検索")
    # '求人情報の検索'をクリック
    elem.click()
    # 待ち時間を設定(10秒)
    browser.implicitly_wait(10)
    sleep(1)#<--(あまりや早すぎても怪しまれるので一応1秒くらいやすませてみた)
    return browser

(5)求人票のダウンロード
  (4)でbrowser という名前でchromeがインスタンス化されている。これと求人番号、ダウンロードパスを引数とした
  処理関数qjindownloadを作成した。出力はpdf名であるがダウンロードできなかったときはpdf名の代わりにエラーメッ
  セージが入る
 
  呼び出し側 
 pdf_name=qjinhyoget(browser,q_No,downloadsFilePath)


 呼び出されるqjinhyoget関数
###################################################################
#    求人番号を入力して求人票を取り出すモジュール
#      入力 動作中のchromeブラウザ、求人番号, ダウンロードファイルパス
###################################################################
def qjinhyoget(browser,q_No,downloadsFilePath):
    q_No=str(q_No) #まず数字かもしれないので文字列にする
    q_No=q_No.replace(' ','') #スペース除去   
    if '-' in q_No:
        a=q_No.split('-')
        q_main=a[0]
        q_eda=a[1]
    else:
        q_No=q_No.replace('-','') #ハイフォン除去
        if len(q_No)<=13: #0サプレスを修正
            q_No='0000'+q_No
            q_No=q_No[-13:]
            q_main=q_No[:5] #先頭から5文字までが主番
            q_eda=q_No[5:] #先頭から5文字を除去すると枝番
   
    browser.find_element_by_id("ID_txtKjNoKkan").clear()
    browser.find_element_by_id("ID_txtKjNoEdaban").clear()
    browser.find_element_by_id("ID_txtKjNoKkan").send_keys(q_main) #MAX 5桁
    browser.find_element_by_id("ID_txtKjNoEdaban").send_keys(q_eda) #MAX 8桁
    browser.implicitly_wait(10)
    sleep(1)
    # 求人番号検索ボタンクリック(ダウンロード開始)
    elem = browser.find_element_by_name('commonDetailInfo')
    elem.click()
    browser.implicitly_wait(10)
    ##############################################
    #ファイル名の取得(12秒超えたら時間切れ
    ##############################################
    time_c=0
    pdf_name=''
    while  pdf_name=='':
        sleep(3)
        pdf_name=target_f(downloadsFilePath)
        time_c+=3
        if time_c > 12:
            pdf_name='error:the number is unpublish or not exist.'
            #erro_no+=1 #求人票ファイルが取得できない
            break
    return pdf_name


(6)pdfファイルからexcelファイルへの変換

 まずpdfファイルをテキストに変換するためにpoppler-utils (PDF 向けユーティリティ(Popplerベース))をインストール
 このユーティリティのpdftotextを使おうと思ったが変換でかなりの情報が抜け落ちる。そこでpdftohtmlを使ってhtmlに
 変換してから余計なタグを取り除き解析することにした。

 excelへの変換はまずあらかじめフィールド名だけあるexcelファイル(header.xlsx)を用意し作業用のフォルダにコピーし、
 pythonのcsvファイルや表などのデータフレームを扱うライブラリpandasを使用して書き込みを行った。

(7)終了処理
 最後にまずseleniumで起動したbrowserを終了させる。
  これは browser.close() やbowser.exit()を使っては行けない。大量のゴミがメモリに残る。
 必ず   browser.quit()で終了させる。
 最後に
 display.stop()#これをしないとWebページが表示されない
 


(8)補足
 求人票を何回もダウンロードするループの中で何回もseleniumを起動したりdisplayを起動させないように注意
 メモリが足りなくなってしまう。


 ascii codecエラーの・ようなものが出たときの対策
 プログラムの最初のところで次のコマンドを実行しておく
import sys,codecs
import io

sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')