Free lancer’s blog

フリーランス活動に関する記録を記して行こうと思います。

Java 版 CSV パーサ (ダブルクォーテーション対応)

CSV に何のこだわりがあるかわかりませんが、今回は Java バージョンです。
デフォルトのパーサが見当たりませんでしたので、自作しました。

package file;

import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;

import java.util.regex.Pattern;
import java.util.ArrayList;

import java.io.FileNotFoundException;
import java.io.IOException;
import gadgets.Debug;

public class CSVParser {
	public static String[][] parse ( String filename ) {
		String[][] ret = null;
		ArrayList<String[]> tmp = new ArrayList<String[]>();
		String line;
		String[] row;
		int i = 0;
		Pattern pattern = Pattern.compile( "^\".*\"$" );
		
		try {
			BufferedReader br = new BufferedReader( new FileReader( new File( filename ) ) );
			
			while ( ( line = br.readLine() ) != null ) {
				row = line.split( ",(?=(([^\"]*\"){2})*[^\"]*$)", -1 );
				for ( i = 0; i < row.length; i ++ ) {
					if ( pattern.matcher( row[ i ] ).find() ) {
						row[ i ] = row[ i ].substring( 1, row[ i ].length() - 1 ).replace( "\"\"" , "\"" );
						System.out.println( row[ i ] );
						
					}
				}	
				tmp.add( row );
			}
			
			ret = new String[ tmp.size() ][];
			for ( i = 0; i < tmp.size(); i ++ ) {
				ret[ i ] = tmp.get( i );
			}
			
			br.close();
		} catch ( FileNotFoundException e ) {
			Debug.exit( filename + " is not found." );
		} catch ( IOException e ) {
			Debug.exit( "faild in read " + filename );
		}
		
		return ret;
	}
}

リクエストのフィルタリングクラス

リクエストをフィルタリングするクラスを作ってみました。

<?php
class Request {
	const GET	= 0;
	const POST	= 1;
	const COOKIE	= 2;
	const SERVER	= 3;
	
	const INT	= 0;
	const STR	= 1;
	const REG	= 2;
	const HEX	= 3;
	const TXT	= 4;
	const DATE	= 5;
	const EMAIL	= 6;
	const PASSWORD	= 7;
	const ASCII	= 8;
	const CHECK 	= 9;
	const NOTEMPTY	= 10;
	
	public static $Bans = array( 'http', '.net', '.com', '.jp', '.co.jp' );
	
	private static function AllocateMethod ( $method ) {
		switch ( $method ) {
		case self::GET		: return $_GET;
		case self::POST		: return $_POST;
		case self::COOKIE	: return $_COOKIE;
		case self::SERVER	: return $_SERVER;
		}
	}
	
	public static function Checked ( $method, $name ) {
		$ary = self::AllocateMethod( $method );
		
		return isset( $ary[ $name ] ) && strtolower( $ary[ $name ] ) == 'on';
	}
	
	public static function SpecialCharacterCheck ( $method ) {
		$ary = self::AllocateMethod( $method );
		
		foreach ( $ary as $name => $value ) {
			if ( preg_match( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $value ) ) {
				$name = Log::Escape( $name );
				$value = Log::Escape( $value );
				Log::Record( "Request containing special characters. ({$name}={$value})" );
				header( "HTTP/1.0 403 Forbidden" );
				exit;
			}
		}
	}
	
	public static function SafeString ( $str ) {
		return !preg_match( '/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/', $str );
	}
	
	public static function Filter ( $method, $name, $type, $option1 = null, $option2 = null ) {
		$ary = self::AllocateMethod( $method );
		$ret = false;
		
		if ( isset( $ary[ $name ] ) && self::SafeString( (binary)$ary[ $name ] ) ) {
			$value = $ary[ $name ];
			
			switch ( $type ) {
			case self::INT :
				if ( $ret = $value == (string)(int)$value ) {
					if ( $option1 ) $ret = $option1 <= $value;
					if ( $option2 ) $ret = $value <= $option2;
				}
				break;
			case self::STR :
			case self::PASSWORD : $ret = mb_check_encoding( $value ); break;
			case self::TXT : $ret = mb_check_encoding( $value ) && TextCheck::NotContainBan( $option1, $value, static::$Bans ); break;
			case self::REG : $ret = preg_match( $option1, $value ); break;
			case self::HEX : $ret = preg_match( '/^[0-9a-fA-F]*$/', $value ); break;
			case self::DATE: $ret = strtotime( $value ) !== false; break;
			case self::EMAIL: $ret = empty( $value ) || filter_var( $value, FILTER_VALIDATE_EMAIL ); break;
			case self::ASCII: $ret = preg_match( '/^[\x00-\x7F]*$/', $value );
			case self::CHECK: $ret = empty( $value ) || ( strtolower( $value ) == 'on' || strtolower( $value ) == 'off' ); break;
			case self::NOTEMPTY: $ret = !empty( $value ); break;
			}
		}
		
		if ( $ret == false && !empty( $value ) ) {
			$value	= Log::Escape( $value );
			$name	= Log::Escape( $name );
			Log::Record( "{$name} is Illegal value.({$name}={$value})" );
		}
		
		return $ret;
	}
	
	public static function FilterAll ( $methods ) {
		$bit	= 1;
		$code	= 0;
		foreach ( $methods as $method => $names ) {
			foreach ( $names as $name => $options ) {
				if ( self::Filter(
					 $method
					,$name
					,$options[ 0 ]
					,isset( $options[ 1 ] ) ? $options[ 1 ] : null
					,isset( $options[ 2 ] ) ? $options[ 2 ] : null
				) == false ) $code |= $bit;
				
				$bit <<= 1;
			}
		}
		
		return $code;
	}
}

使い方はこんな感じ

<?php
$code = Request::FilterAll (
	array (
		Request::POST => array (
			 'ID'		=> array ( Request::INT, 1 )		// ID は 1 以上の整数値で
			,'NAME'		=> array ( Request::TXT, $dbh )		// NAME は禁止語句を含まないバイナリセーフな文字列
			,'EMAIL'	=> array ( Request::EMAIL )		// EMAIL はメールアドレス
		)
	)
);

例えば、ID と EMAIL が引っかかった場合 0b0101 が返ってきます。ですので、

<?php
if ( $code & 0x01 ) echo 'IDが不正です';
if ( $code & 0x02 ) echo 'NAMEが不正です';
if ( $code & 0x04 ) echo 'EMAILが不正です';

のようなことができます。

PHP で制御文字をエスケープ

制御文字を削除する記事はよく見かけるけど、エスケープする記事がなかったので自作

<?php
public static function Escape( $str ) {
	return preg_replace_callback( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', function ( $m ) {
		return '0x' . strtoupper( implode( '', unpack( 'H*', $m[ 0 ] ) ) );	
	}, $str );
}

大文字に変換してるのは、単純に個人的にといいますか、16進数は大文字で書くようにしてます。

水平タブと、改行は残してあります。

これに引っかかるときって、考えると、ISO-2022-JP を使ってるとか、携帯の絵文字?よく調べてないけど

などなど考えられるけど

それ以外と言ったら、やはり悪意のあるコードになる。

ヌルバイトの後にコードを埋め込んだりとか。

PHP関数はバイナリセーフになっているものの、そんなものを読ませたくはない。

引っかかったらログ取って終わり。



検索すると、制御文字を消すコードが多数引っかかるけど、どこで使うのかわからない。

消して処理続行?

いい加減にもほどがある。

もし、特殊文字をユーザーが入力する可能性があるなら、受け入れられませんってアラート表示するだろうし。

消すメリットってなんだろ。


因みにこのコードは、処理続行させるためのものじゃなく、ログをとるだけのためのものです。
何を送ってきたかわかるから、対策もできるでしょ。

htmlentities が近しい動作をするので、それを使ってもいいかもしれません。

セッションが消える

不可解なログが連発していて、頭を抱えていたのですが、ようやく原因のようなものが見つかりました。

 

session_regenerate_id()でセッションが切れる|シラサヤ備忘館

 

それから、元ネタ忘れましたが、相対パスで書くと、ドメインを保存しないブラウザがあるらしいということです。

 

ひとまず実装してもう一度調査してみます。

個人事業主申請してきました

今日、事情があって予定を少し繰り上げて個人事業主申請してきました。

 

屋号 Fruits basket

 

メンバーのご紹介をしたいと思います。

 

=ユウ=(私) : 屋主 兼 SE

ソウタ : デザイナ

クロスケ : デザイナ

お茶 : 秘書 もとい お茶くみ

 

冗談です。実質メンバーは私だけ。3方は私の愛する方々です。

 

事業内容

Banana Chat 新感覚チャットサイト(18禁

の運営

 

上記チャットサイトを足掛かりに、事業を展開していく予定です。

PHP で CSV 文字列生成

PHPCSV 文字列が必要になったんですが、

str_getcsv はあるものの、str_putcsv なる関数がない

ネーミングがおかしい気がしますが str2csv ?

仕方なく自作することに

因みに implode( ',', $array); でいいじゃんって人は帰ってください。

いいんじゃないですか?それで

で、話を戻すと、fputcsv がすぐに思い浮かぶ

そして、バッファも確かあったなと

出力バッファがまず思い浮かんだけど、用途として間違っている

出力バッファ関数郡は魅力的だけど


出力バッファを使ってしまうと
この処理をする前に何か出力されていたら、相当面倒なことになる。

出力バッファなんて使ったら何してんのって話だ。


てことで、しょうがなく、バッファで遠回りして csv 生成

一から、csv 生成関数作ってもいいような気もするけど、その辺は時短ということで

<?php
if ( $fh = fopen( 'php://memory', 'w+' ) ) {
			
	foreach ( $ary as $line ) {
		fputcsv( $fh, $line );
	}
			
	fseek( $fh, 0 );
	$str_csv = stream_get_contents( $fh );
		
	fseek( $fh, 0 );
	ftruncate( $fh, 0 );
	fclose( $fh );
}


サイズが気になる方は
php://temp

を検討してもいいと思います。

今回は、サイズの小さいものに限られていたので、php://memory で
エイヤとやっています。

N-Gram

テキスト検索する必要性が出てきたので、N-Gram プログラム作成。
というか数行・・・

<?php
function NGram( $str, $n ) {
  mb_internal_encoding( 'UTF-8' );
  $l = mb_strlen( $str );
  $ret = '';

  for ( $i = 0; $i < $l; $i ++ ) {
    $ret .=  mb_substr( $str, $i, $n ) . ' ';
  }

  retrun trim( $ret );
}

検索してたらこんなのが出てきた

[PHP] 文字列をN-Gramで変換する - Qiita

突っ込んでいいですか?

何をしてるのか知らないけど、長いのはいいとして、N-Gram 使うときって基本マルチバイトじゃないんでしょうか・・・


さて、N-Gram ができたら MySQL の FULLTEXT で全文検索どん。

おわり