カテゴリー
プログラム 社内SE

PHP ヤマト配送状況取得 2021年サイト変更対応

2021年9月 クロネコヤマトの荷物問い合わせシステムのページレイアウトが変更になって、送り状番号から荷物の配送状況を取得していたバッチがエラーとなった。
APIを使用すればよいという話だが、ヤマトビジネスメンバーズは他部署で契約&契約した人は退職で詳細不明になっている。まあ、面倒なので、PHPで問い合わせページから荷物状況を取得するプログラムを書いた。

実際はベタ書きしているが、かっこ悪いので、公開に当たりクラス化。1回テスト確認はしたが、使っていないので、エラーがああるかも。PHP5.4だが、7でも8でも動くと思う。取得した状況には「▶」が付いているので不要であれば消して使ってください。
内部でサイト解析のためDOMDocument使っているので使える環境のこと。以下、使い方とクラス。

使い方

$truckNumbers = array(
'444486855064',
'4444-9099-9090',
'444490999101',
); // いくつでも可

$objYamato = new clsGetYamatoDeliverStatus();
$aryDelivStatus = $objYamato->getYamatoDeliverStatus($truckNumbers);
if ($aryDelivStatus === false){ /* エラー ヤマト追跡番号 状況取得失敗 */ }

var_dump($aryDelivStatus);
/*
結果は[
(‐無しの追跡番号),
(‐有りの追跡番号),
状況日,
配達状況,
問い合わせ時間]の配列
array(3) {
  [0] =>
  array(5) {
    [0] =>
    string(12) "444486855064"
    [1] =>
    string(14) "4444-8685-5064"
    [2] =>
    string(5) "10/20"
    [3] =>
    string(16) "▶ 配達完了"
    [4] =>
    string(19) "2021/10/12 09:59:10"
  }
  [1] =>
  array(5) {
    [0] =>
    string(12) "444490999090"
    [1] =>
    string(14) "4444-9099-9090"
    [2] =>
    string(5) "10/20"
    [3] =>
    string(16) "▶ 配達完了"
    [4] =>
    string(19) "2021/10/12 09:59:10"
  }
  [2] =>
  array(5) {
    [0] =>
    string(12) "444490999101"
    [1] =>
    string(14) "4444-9099-9101"
    [2] =>
    string(5) "10/20"
    [3] =>
    string(16) "▶ 配達完了"
    [4] =>
    string(19) "2021/10/12 09:59:10"
  }
}
*/

荷物状況取得 クラス

<?php

class clsGetYamatoDeliverStatus{

	// 配列$truckNumbersに指定された追跡番号の状況を全て取得する。$truckNumbersは‐有でも無しでも可。
	// 戻り値は次の配列の配列 ["123456781234"(‐無しの追跡番号),"1234-5678-1234"(‐有りの追跡番号),mm/dd 状況日,配達状況,Y/m/d H:i:s 問い合わせ時間]
	// 戻り値がfalseの場合、エラー(ヤマト追跡サイトにアクセス出来ない)
	public function getYamatoDeliverStatus($truckNumbers){
		// 10件ずつヤマトのサイトからトラッキング情報を取得する。
		$numnerCnt = count($truckNumbers);
		$getTruckNumbers = array();
		$aryAllStatusInfo = array();
		for ($i=0;$i < $numnerCnt;$i++){
			$getTruckNumbers[] = $truckNumbers[$i];
			if (count($getTruckNumbers) == 10){
				$aryRetStatus = $this->getYamatoDeliverStatusCore($getTruckNumbers);
				$aryTemp = array_merge($aryAllStatusInfo, $aryRetStatus);
				if ($aryTemp === false){ return false; }
				$aryAllStatusInfo = $aryTemp;
				$getTruckNumbers = array();
			}
		}

		// 残っている場合
		if (count($getTruckNumbers) > 0){
			$aryRetStatus = $this->getYamatoDeliverStatusCore($getTruckNumbers);
			$aryTemp = array_merge($aryAllStatusInfo, $aryRetStatus);
			if ($aryTemp === false){ return false; }
			$aryAllStatusInfo = $aryTemp;
		}

		return $aryAllStatusInfo;
	}

	// ヤマトの追跡サイトにアクセスし、10件づつ結果を取得する。
	// 引数の$truckNumbersは配列で10件まで。
	// 戻り値は次の配列の配列 ["123456781234"(‐無しの追跡番号),"1234-5678-1234"(‐有りの追跡番号),mm/dd 状況日,配達状況,Y/m/d H:i:s 問い合わせ時間]
	// 戻り値がfalseの場合エラー(サイトにアクセス出来ない場合のみ)
	private function getYamatoDeliverStatusCore($truckNumbers){
		$aryRet = array();

		$url = 'https://toi.kuronekoyamato.co.jp/cgi-bin/tneko';
		$tnumCnt = count($truckNumbers);

		$aryPOST = array(
			'mypagesession' => '',
			'backaddress' => '',
			'backrequest' => '',
			'number00' => 2
		);
		$j=0;
		for ($i=1;$i<=10;$i++){
			$postName = 'number';
			if ($i < 10){
				$postName .= '0' . $i;
			}else{
				$postName .= $i;
			}
			if ($j < $tnumCnt){
				$aryPOST[$postName] = $truckNumbers[$j];
			}else{
				$aryPOST[$postName] = '';
			}
			$j++;
		}

		$http_build = http_build_query($aryPOST,'','&');
		$header = array(
			"Content-Type: application/x-www-form-urlencoded",
			"Content-Length: ".strlen($http_build)
		);
		// オプション
		$opts = array("http" => array(
			"method" => "POST",
			"header"  => implode("\r\n", $header),
			"content" => $http_build)
		);
		$scc = stream_context_create($opts);

		// 読み取り
		$get_html = file_get_contents($url, false, $scc);
		if ($get_html === false){ return false; }

		// staus取得時間
		$getTime = Date('Y/m/d H:i:s');

		$dom = new DOMDocument();
		$ret = $dom->loadHTML($get_html);

		$divResult = $this->getElementsByClassName($dom, 'tracking-box-area', 'div');
		$divLen = count($divResult);
		for($i=0;$i < $divLen;$i++){
			// このdivの下のinputのvalueを取得
			$domInputs = $this->getDomNodeFromTagName($divResult[$i], 'input');
			if (count($domInputs) == 0){ continue; }
			$trNum = $domInputs[0]->attributes->getNamedItem('value')->nodeValue;
			if ($trNum == ''){ continue; }

			// このdiv以下の日付を取得
			$date = '';
			$dateNodes = $this->getDomNodeFromClassName($divResult[$i], 'data date');
			if (count($dateNodes) == 0){ continue; }
			$date = $dateNodes[0]->nodeValue;

			// このdiv以下の状況を取得
			$state = '';
			$dateStates = $this->getDomNodeFromClassName($divResult[$i], 'data state');
			if (count($dateStates) == 0){ continue; }
			$state = $dateStates[0]->nodeValue;

			$aryRet[] = array(
				str_replace('-','',$trNum), // ‐なし
				$trNum, // ‐あり
				$date, // 状況になった日付
				$state, // 状況
				$getTime // 取得時刻
			);

		}

		return $aryRet;
	}

	private function getElementsByClassName($dom, $ClassName, $tagName=null) {
		if($tagName){
			$Elements = $dom->getElementsByTagName($tagName);
		}else {
			$Elements = $dom->getElementsByTagName("*");
		}
		$Matched = array();
		for($i=0;$i<$Elements->length;$i++) {
			if($Elements->item($i)->attributes->getNamedItem('class')){
				if(strpos($Elements->item($i)->attributes->getNamedItem('class')->nodeValue, $ClassName) !== false) {
					$Matched[]=$Elements->item($i);
				}
			}
		}
		return $Matched;
	}

	// 子ノード以下に指定のタグ名を持つdomNodeの一覧を返す。
	private function getDomNodeFromTagName($domNode, $tagName){
		$aryRet = array();
		foreach ( $domNode->childNodes as $chiledNode ){
			if ($chiledNode->nodeName == $tagName){
				$aryRet[] = $chiledNode;
			}
			if ($chiledNode->hasChildNodes()){
				$aryTemp = $this->getDomNodeFromTagName($chiledNode, $tagName);
				foreach($aryTemp as $temp){
					$aryRet[] = $temp;
				}
			}
		}
		return $aryRet;
	}

	// 子ノード以下に指定のクラス名を持つdomNodeの一覧を返す。
	private function getDomNodeFromClassName($domNode, $className){
		$aryRet = array();
		foreach ( $domNode->childNodes as $chiledNode ){
			if ($chiledNode->hasAttributes()){
				$nodAttr = $chiledNode->attributes->getNamedItem('class');
				if ($nodAttr != null){
					$nodeClasses = $nodAttr->nodeValue;
					if (strpos($nodeClasses, $className) !== false){
						$aryRet[] = $chiledNode;
					}
				}
			}
			if ($chiledNode->hasChildNodes()){
				$aryTemp = $this->getDomNodeFromClassName($chiledNode, $className);
				foreach($aryTemp as $temp){
					$aryRet[] = $temp;
				}
			}
		}
		return $aryRet;
	}
}
カテゴリー
プログラム 社内SE

Pear Mailで送信したメールを 送信済みフォルダに設置する。

PHPのPear::Mailでシステムからメールを送信し、送信したメールを送信済みフォルダに入れたい事案があった。
解決方法がググっても出てこなかったので、自己解決。

基本的には送信したデータをIMAP関数で送信済みのフォルダに設置するだけ。

以下ソース。

include_once("Mail.php");
include_once("Mail/mime.php");

mb_language("uni");
mb_internal_encoding("UTF-8");

// 宛先とヘッダーとボディを作る。省略。以下mimeの例
$mime = new Mail_mime();
$mime->setHTMLBody('something<br />anything');
$body = $mime->get(array(
  'text_encoding' => '7bit',
  'text_charset' => 'UTF-8',
  'html_charset' => 'UTF-8',
  'head_charset' => 'UTF-8'));
$headers = $mime->headers(array(/*省略*/));
$recipients = array(/*省略*/);

// メール送信
$mailObject = Mail::factory("smtp", Array(/*省略*/)); 
$send = $mailObject->send($recipients, $headers, $body);
if (PEAR::isError($send)){ return 'Mail send error! ' . $send->getMessage(); }

////////////////////////////////////////////////////////////////
// IMAPフォルダに設定。

// 送信済みフォルダに設置するデータ作成
$message = '';
foreach($headers as $key => $value){
  $message .= $key . ': ' . $value . "\r\n";
}
$message .= "\r\n" . $body;
// 設置フォルダー
$folderName = '送信済み'; // 自分のIMAPの送信済みフォルダ
$encodeFolder = mb_convert_encoding($folderName, 'UTF7-IMAP', 'UTF-8'); // エンコード
// imap_openの説明を読んで。SSLならポート以降を:993/imap/ssl
$path = "{imap.hogehoge.com:143/imap/notls}INBOX." . $encodeFolder;
$imapStream = imap_open($path,'username@hogehoge.com','password');
if ($imapStream === FALSE){ die(); }
$result = imap_append($imapStream, $path, $message, "\\Seen");
if ($result === FALSE){ die(); }
imap_close($imapStream);

/* end */

以上

カテゴリー
プログラム 社内SE

cordova-plugin-buildinfoのiOSでDebugがfalseになる。

Cordovaのplugin cordova-plugin-buildinfoを利用した際に、iOSでdebugビルドしても”BuildInfo.debug”がfalseになって困っていたところ、以下の方法で解決できたので、備忘録として残しておく。

1)プラグインのバージョンを4.0以上に上げる。
関係ないが、4.0以下だと「このプラグインのせいでiOSのアプリが異常に遅くなることがある」との投稿があったので、とりあえず4.0以上に上げた。

2)XCODEでプロジェクトを開きBuild Settingsから以下2点を変更する。

・Apple LLVM 9.0 – Preprocessing -> Preprocessor Macrosの下にdebugという項目があるので、そこに”DEBUG=1″を追加する。(”Macros”で設定項目を検索するとすぐ見つかる)
・Swift compiler – Custom Flags -> Other Swift Flags
の下にdebugという項目があるので、”-D DEBUG”を追加する。
(”Swift”で設定項目を検索すると見つかる。)

※XCodeプロジェクトの開き方は、Cordova環境直下の./platforms/iosの下に拡張子”.xcworkspace”のファイルがあるのでこれを開く。同じようなファイルで拡張子”.xcodeproj”があるが、こちらではないので注意。

これで、デバッグビルドの場合のみ[BuildInfo.debug]がfalseとなった。

カテゴリー
社内SE 窓口対応

qmail メール送信時エラー( TLS connect failed )までが長い

qmailでメール送信後、数日してから「メールが送信できなかった」とのエラーメールが返ってくるとの相談があった。

エラーメールの内容はだいたい以下のような感じ。

Hi. This is the qmail-send program at xxxxxxxx.
I'm afraid I wasn't able to deliver your message to the following addresses.
This is a permanent error; I've given up. Sorry it didn't work out.

XXXXXXX
TLS connect failed: error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1
alert protocol version; connected to xxxxxxxxx
I'm not going to try again; this message has been in the queue too long.

以下のような場合もある。
LS connect failed: error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3

どうも相手先のサーバに接続できなかった場合、延々とリトライをしているようだ。
設定を調べてみると、設定ファイル「/var/qmail/control/queuelifetime」に秒数を設定するとその時間以上はリトライをしなくなるらしい。デフォルト値は7日(604800)であった。送れなかったらさっさとエラーを返してほしいが、デフォルト値が長いのにあまり短くするのも怖いので、とりあえず1時間(3600)を設定した。

vi /var/qmail/control/queuelifetime
---
3600
---

ファイルには数字のみ記入する。再起動が必要なので以下をして終了。

qmailctl stop
qmailctl start

qmailctl restartはプロセスが残ってしまうので当方環境ではNGだった。

カテゴリー
社内SE 窓口対応

PDFを印刷した際に一部のページでフォントがおかしくなる

「フォントがおかしい」1年に数回はこの厄介な質問を受けます。今回も、「PDFを印刷したら、XXページ目のフォントだけ違う、何とかしてくれ」という問い合わせが来ました。

該当のPDFを表示した段階では全く違いがわからなかったものの、印刷後の紙で確認すると確かに一部のページで若干文字が濃くなっている感じがします。

PDF自体はシステムから出力していて、特定のページでフォントを変えるような処理はしていませんし、そのシステムを組んでから数年、そんな質問が来たことはありませんでした。

「印刷機の可能性が高く、どうにもなりませんよ。」と回答しかけたところで、思いあたりました。

「PDFを表示するソフトのせいか」

試してみると、Windows 10の旧EdgeでPDFを表示し印刷した時だけ特定のページでフォントがほんの少し濃くなります。Adobe ReaderやChromeで表示、印刷したときは大丈夫なので、旧Edgeが原因なのは間違いなさそうです。

なので「面倒だけど、Edgeで表示したPDFをダウンロードして、それからChromeで表示して印刷してください。」と回答して終わりです。

まあ、面倒だからEdgeで印刷するのでしょうけど。。

カテゴリー
OS 社内SE

FreeBSD : sshやtelnetでログインしたまま、ターミナルを閉じてしまった場合の対処方法

掲題ままですが、残った仮想端末のプロセスを削除する方法を書いておきます。通常、ターミナルを落としたら、プロセスも消えるのでこのような状況にはならないのですが、不安なときの確認にもなるので。。。

  1. ターミナルから切断したユーザでログインし、以下のコマンドを打ち、現在の仮想端末の番号を確認します。
    > who
    以下のような結果が出ます。
    taro  pts/0 6月 16 04:52 (xxxxxx.or.jp)
    taro  pts/1 6月 16 04:56 (xxxxxx.or.jp)
    最も新しくログインしたものが自分なので、ここではpts/1が現在のターミナルです。
     
  2. そのまま以下のコマンドを実行し、仮想端末のプロセスを探します。
    > ps -aux | grep `whoami`
  3. 以下のような結果が表示されます。
    root 27048 0.0 1.4 20428 7976 - Is 04:52 0:00.02 sshd: taro [priv] (sshd) 
    taro 27050 0.0 1.4 20432 8008 - S 04:52 0:00.02 sshd: taro@pts/0 (sshd) 
    root 27124 0.0 1.4 20428 8012 - Is 04:56 0:00.02 sshd: taro [priv] (sshd) 
    taro 27126 0.0 1.4 20432 8036 - I 04:56 0:00.00 sshd: taro@pts/1 (sshd) 
    taro 27051 0.0 0.5 12628 2880 0 Ss 04:52 0:00.01 -sh (sh) 
    taro 27132 0.0 0.4 12324 2636 0 R+ 04:57 0:00.00 ps -aux
    
  4. 古いプロセスをkillします。
    kill 27050