PHPの覚書

テキストファイルを途中から読み込む

fseek関数にてテキストファイルを途中から読み込むことができる。
先頭から読み込むのと比べ、途中読込は処理時間を大幅に短くできる。

途中読込の実装はfseek関数にオフセット(offset)を指定する。
オフセットは現在のカーソル位置であり、先頭から何バイト目かを示している。

オフセットはftell関数で取得可能である。
テキストファイル読み込みを途中中断する際、ftell関数でオフセットを取得および保持しておき、 再開するときfseek関数に保持していたオフセットを指定すれば、途中から読み込みを再開できる。

応用

巨大なCSVファイルからデータを取り出しDBに保存する処理を、分割で処理にするのに役立つ。


サンプル

テキストファイル(text3.txt)
	色はにほへど
	散りぬるを
	我が世たれぞ
	常ならむ
	有為の奥山
	今日越えて
	浅き夢見じ
	酔ひもせず
	

ソースコード
	<?php 
	$txtFn='test3.txt';
	$txtFn = mb_convert_encoding ( $txtFn, 'SJIS', 'UTF-8' );//全角ファイル名に対応
	echo "<table border='1'><thead><tr><th>offset</th><th>行テキスト</th></tr></thead><tbody>";
	
	if ($fp = fopen ( $txtFn, "r" )) {
		fseek($fp, 54);//54バイト目から読み込む
		$data = array ();
		while ( false !== ($line = fgets ( $fp )) ) {
			$offset=ftell($fp);
			echo "<tr><td>{$offset}</td><td>{$line}</td></tr>";
		}
	}
	fclose ( $fp );
	
	echo '</tbody></table>';
	?>
	

出力
offset行テキスト
67常ならむ
83有為の奥山
99今日越えて
115浅き夢見じ
130酔ひもせず
※offsetは現在のカーソル位置であり、先頭から何バイト目かを示している。

大容量CSVファイル1000行ずつ読込 | メモリ使用状況も確認

17万行(50MB)のCSVを1000行ず読み込んだ時のメモリ使用状況を調べた。
メモリ消費はだいぶ抑えられている。

一行ずつstrlen関数で容量を取得し合計した値は、 filesize関数で取得した容量と同値になった。


	<?php 
	$mem1 =  memory_get_usage();
	
	$fn = "big.csv";
	
	$fsize = filesize ($fn);
	echo "ファイルサイズ:{$fsize}<br>";
	$end_flg = false;
	$offset = 0;
	$str_size_total = 0;
	for($x_i=0;$x_i<1000000;$x_i++){
		
		if ($fp = fopen ( $fn, "r" )) {
			fseek( $fp, $offset );
	
			for($i=0;$i<1000;$i++){
				$line = fgets ($fp);
				if($line == false){
					$end_flg=true;
					break;
				}
				$str_size_total += strlen($line);
				//echo $line . '<br>';
			}
			$offset = ftell($fp);
			fclose($fp);
		}
		
		$mem2 =  memory_get_usage() - $mem1;
		echo "----------{$x_i}  {$mem2}<br>";
		
		if($end_flg == true){
			echo '終了しました。<br>';
			break;
		}
	}
	echo "文字サイズ合計:{$str_size_total}";
	?>
	

出力

ファイルサイズ:54349576
----------0  648
----------1  520
----------2  584
----------3  520
----------4  584
----------5  200
----------6  584
----------7  584
----------8  456
----------9  584
----------10  520
----------11  520
----------12  456
----------13  520
----------14  456
----------15  520
----------16  520
----------17  520
----------18  520
----------19  520
----------20  584
----------21  584
----------22  584
----------23  520
----------24  520
----------25  584
~ 省略 ~
----------165  616
----------166  552
----------167  488
----------168  616
----------169  552
----------170  232
終了しました。
文字サイズ合計:54349576
	

forループによるテキストファイル処理 | 最大20行までのテキストを出力する


	<?php 
	// 最大20行までのテキストを出力する
	$fn = "test.txt";
	if ($fp = fopen ( $fn, "r" )) {
		for($i=0;$i<20;$i++){
			$line = fgets ($fp);
			if($line == false) break; // ファイル内テキストが末尾に達したら処理抜け
			echo $line . '<br>';
		}
	}
	fclose ( $fp );
	?>
	

POSTデータの容量を増やす | Warning: POST Content-Length of ...

下記のエラーが発生する場合、POSTデータの容量不足である。
Warning: POST Content-Length of 16698999 bytes exceeds the limit of 8388608 bytes in Unknown on line 0

POSTデータの容量を増やすphp.iniの設定

php.iniをテキストエディタで開き、「post_max_size」の値を増やせばよい。

変更前
post_max_size=8M

変更後の例
post_max_size=80M

値を増やしたら、XAMPP Control PanelのApacheを再Startすれば完了。

テキストファイルからオフセットで指定した1行を読み込む

テキストファイル(text3.txt)
	色はにほへど
	散りぬるを
	我が世たれぞ
	常ならむ
	有為の奥山
	今日越えて
	浅き夢見じ
	酔ひもせず
	

ソースコード
	<?php 
	$txtFn='test3.txt';
	$txtFn = mb_convert_encoding ( $txtFn, 'SJIS', 'UTF-8' );//全角ファイル名に対応
	
	echo '<pre>';
	if ($fp = fopen ( $txtFn, "r" )) {
		fseek($fp, 54);//54バイト目から読み込む
		
		$line = fgets ( $fp );
		echo $line;
		
		$offset=ftell($fp);
		echo '→次のオフセット:'.$offset;
		
	}
	fclose ( $fp );
	echo '</pre>';
	?>
	

出力
	常ならむ
	→次のオフセット:67
	

テキストファイルの先頭行文字列を取得する


	/**
	 * テキストファイルの先頭行文字列を取得する
	 * @param string $fn テキストファイルパス
	 * @return string 先頭行文字列
	 */
	private function getHeadsFromTextfile($fn){

		$head_str = '';
		if ($fp = fopen ( $fn, "r" )) {
			$head_str = fgets ($fp);
		}
		fclose ( $fp );
		
		$head_str = $this->deleteBom($head_str); // UTF8ファイルのテキストに付いているBOMを除去する

		return $head_str;
	}
	
	
	/**
	 * UTF8ファイルのテキストに付いているBOMを除去する
	 * @param string $str UTF8ファイルから取得したテキストの文字列
	 * @return string BOMを除去した文字列
	 */
	private function deleteBom($str){
		if (($str == NULL) || (mb_strlen($str) == 0)) {
			return $str;
		}
		if (ord($str{0}) == 0xef && ord($str{1}) == 0xbb && ord($str{2}) == 0xbf) {
			$str = substr($str, 3);
		}
		return $str;
	}
	

波ダッシュを全角チルダに変換する

	$wave_dash1 = '〜'; // 波ダッシュ → 文字参照は「 &#12316;」
	$tilde1=preg_replace("/\xE3\x80\x9C/", "\xEF\xBD\x9E", $wave_dash1);
	echo $wave_dash1.' → '.$tilde1;
	

出力
〜 → ~

参考サイト

期間を指定月間で分割

期間を指定した月数で分割し、日付リストとして返す関数、splitByMonthRange。


サンプル

	$first_date='2015-1-1';
	$end_date = date('Y-m-d');
	$month_range=4;
	$format='Y-m-d H:i:s';
	$res = splitByMonthRange($first_date,$end_date,$month_range,$format);
	echo var_dump($res);
	
	/**
	 * 期間を指定月間で分割
	 * 	 
	 * 期間は開始日と終了日で指定する。
	 * 開始日に月末日を指定すると月がずれて分割されてしまう。
	 * なので、開始日や終了日はなるべく第一日(月の初日)を指定する。
	 * 
	 * @param string/date $first_date 期間の開始日(月の第一日)
	 * @param string/date  $end_date 期間の終了日
	 * @param int  $month_range 指定月間
	 * @param string $format 返りデータの日付フォーマット(省略可、秒単位まで指定可)
	 * @return array 分割日付リスト
	 */
	function splitByMonthRange($first_date,$end_date,$month_range,$format='Y-m-d'){
		$start = new DateTime($first_date);
		$end = new DateTime($end_date);
		$interval = DateInterval::createFromDateString($month_range.' month');
		$period = new DatePeriod($start,$interval,$end);
		$dates = array();
		foreach($period as $d){
			$dates[] = $d->format($format);
		}
		return $dates;
	}
	

出力

	array (size=5)
	  0 => string '2015-01-01 00:00:00' (length=19)
	  1 => string '2015-05-01 00:00:00' (length=19)
	  2 => string '2015-09-01 00:00:00' (length=19)
	  3 => string '2016-01-01 00:00:00' (length=19)
	  4 => string '2016-05-01 00:00:00' (length=19)
	
サンプル


アップロード容量を増やすphp.iniの設定

PHP.iniを開き、以下項目の値を増やすと、アップロード容量が増える。

	;メモリ上限
	memory_limit = 500M
	
	;POST最大サイズ
	post_max_size = 500M
	
	;アップロード最大サイズ
	upload_max_filesize = 500M
	

phpMyAdminで大容量データをインポートするとき、必須の設定である。

参考サイト:【設定方法その1】サーバー管理画面から設定する


複数の改行コードに対応した改行分割 | preg_split

preg_splitに「/\R/」を指定すると3種類の改行コード「\r」「\n」[\r\n」に対応した分割をすることができる。
	$text = "シャムネコ\nやぎ\rハイイロオオカミ\r\nライオンタマリン";
	$ary =preg_split( "/\R/", $text );
	echo var_dump($ary);
	

出力
	array(
		(int) 0 => 'シャムネコ',
		(int) 1 => 'やぎ',
		(int) 2 => 'ハイイロオオカミ',
		(int) 3 => 'ライオンタマリン'
	)
	

注意

Ajaxで送られてきた日本語文字列に対して「/\R/」で分割するとバグが起こる。
日本語文字列の特定文字で分割されてしまう。しかも特定文字に該当するケースは多い。
文字コードの問題かもしれない。
この場合、「/\n/」で分割するとバグは起こらない。\n以外の改行には対応できなくなるが・・・。


改行コードを統一する


	<?php 
	$str = "い\nろ \r\nは\rにほ\r\n";
	
	echo json_encode($str) . '<br>';
	$str1 = alignLineFeedCodes($str);
	$str2 = alignLineFeedCodes($str,"\r\n");
	$str3 = alignLineFeedCodes($str,"\r");
	
	echo json_encode($str1) . '<br>';
	echo json_encode($str2) . '<br>';
	echo json_encode($str3) . '<br>';
	
	/**
	 * 改行コードを統一する
	 * @param string $str 改行コードを含む文字列
	 * @param string $code 統一する改行コード
	 * @return string 改行コードを統一した文字列
	 */
	function alignLineFeedCodes($str,$code="\n"){
		return preg_replace("/\r\n|\r|\n/", $code, $str);
	}
	
	?>
	

配列から空白を除去

配列から空白行を除去するにはarray_filterとarray_valuesを組み合わせて使う。
インデックスの振り直しが不要であれば、array_filterだけでよい。

	$list=array("カラス","","スズメ","ムクドリ");
	$list = array_filter($list, "strlen");// 空白行を除去(indexの降り直しは行われない)
	$list = array_values($list);// インデックスの振り直し
	echo var_dump($list);
	

出力
	array (size=3)
	  0 => string 'カラス' (length=9)
	  1 => string 'スズメ' (length=9)
	  2 => string 'ムクドリ' (length=12)	
	

緯度経度を度分秒表記(60進数)から10進数に変換

60進数表記の緯度経度を10進数表記に変換する。
60進数表記も「度分秒」、「°’”」など複数の記号表記に対応している。

	/**
	 * 緯度経度を度分秒表記(60進数)から10進数に変換
	 * @param string or array $p 60進数緯度経度
	 * - 例
	 * - 26,40,32.73
	 * - 26度40分32.73秒
	 * - 26°40’32.73”
	 * - 26,40'32.73"
	 * - N26,40'32.73"
	 * - array(26,40,32.73)
	 * @return 10進数緯度経度
	 */
	function latlon60to10($p){
		$res = null;
		if(is_array($p)){
			$ary = $p;
		}else{
			
			if(!is_numeric(mb_substr($p ,0 ,1))){
				$p = mb_substr($p ,1 );
	
			}
			$ary = preg_split("/,|度|分|秒|°|’|”|'|\"/",$p);
			
			if(count($ary) < 3){
				return null;
			}
		}
		
		$res = $ary[0] + $ary[1]/60 + $ary[2]/3600;
		
		return $res;
		
	}
	

検証

60進数表記 10進数表記
26,40,32.7326.675758333333
26度40分32.73秒26.675758333333
26°40’32.73”26.675758333333
N26,40'32.73"26.675758333333
			array (size=3)
			  0 => int 26
			  1 => int 40
			  2 => float 32.73
			
26.675758333333