大きな配列がメモリを超えたのでCSVファイルにして1行ずつ読み込む(PHP)
前回,検索エンジンに品詞情報を付加する:具体的な検索アルゴリズムの解説(stanza)(PHP)でstanzaを使った具体的な検索方法を考えてみました。
しかしながら,収録するデータが大きくなってきたところでメモリオーバーが発生しました。そこで,データの扱い方を変えることにします。また,検索アルゴリズムにも不具合があったので修正しています。
CSVファイルを用意する
前回まで json ファイルを使っていましたが,ファイル内のデータを部分的に取り出すには csv 形式の方が良いようです。そこで,stanza を使って抽出したデータを csv で保存します。
base_contents.csv
Medical technology has # greatly in recent years.,argued,announced,advanced,attached,亜細亜大
data.csv
Medical,technology,has,argued,greatly,in,recent,years,.
Medical,technology,has,announced,greatly,in,recent,years,.
Medical,technology,has,advanced,greatly,in,recent,years,.
Medical,technology,has,attached,greatly,in,recent,years,.
Medical,technology,have,argue,greatly,in,recent,year,.
Medical,technology,have,announce,greatly,in,recent,year,.
Medical,technology,have,advanced,greatly,in,recent,year,.
Medical,technology,have,attach,greatly,in,recent,year,.
JJ,NOUN,VERB,VBN,RB,IN,JJ,NOUN,.
JJ,NOUN,VERB,VBN,RB,IN,JJ,NOUN,.
JJ,NOUN,VERB,VBN,RB,IN,JJ,NOUN,.
JJ,NOUN,VERB,VBN,RB,IN,JJ,NOUN,.
1つのレコードは上のようになります。
base_contents.csv には問題文,4つの選択肢,大学名が格納されています。
data.csv にはそれぞれの選択肢を問題文に挿入したもの,単語をレンマ化したもの,品詞情報が格納されています。このファイルは1レコードが12行で構成されています。
データの読み込み
$handle = gzopen("base_contents.csv.gz","r");
$id = 0;
while($base_text = fgetcsv($handle)) {
$text[$id] = $base_text;
$id += 1;
}
fclose($handle);
問題文の方は,すべてのデータを一度に配列に格納します。
gzopen()
はgz形式で圧縮されたファイルをオープンします。ファイルが大きくなると,そもそもサーバー側がアップロードを受け付けてくれなかったりするので,圧縮した上でアップロードしています。
fgetcsv()
はcsvファイルからデータを1行ずつ読み込みます。これを配列$text
に格納していきます。$text
は二次元配列になります。
while()
を使えば,すべての行についてデータを取り出すことができます。
$handle = gzopen("data.csv.gz","r");
$id = 0;
while($chars[0] = fgetcsv($handle)) {
$chars[1] = fgetcsv($handle);
$chars[2] = fgetcsv($handle);
$chars[3] = fgetcsv($handle);
$lemmas[0] = fgetcsv($handle);
$lemmas[1] = fgetcsv($handle);
$lemmas[2] = fgetcsv($handle);
$lemmas[3] = fgetcsv($handle);
$xposes[0] = fgetcsv($handle);
$xposes[1] = fgetcsv($handle);
$xposes[2] = fgetcsv($handle);
$xposes[3] = fgetcsv($handle);
次に,単語$chars
,レンマ$lemmas
,品詞情報$xposes
を読み込みます。
やり方は上と同じですが,1つのレコードにつき12行を読み込みます。
このようにwhile()
でレコードを1つ分ずつ読み込んで,検索文字列との判定処理を行っていきます。
判定処理
for ($i = 0; $i < $q_len; $i++) {
$q_char = $q_chars[$i];
//問題文の先頭から検索
for ($j = 0; $j < $chars_len; $j++) {
if ($seq == -1 || $j > $seq) {
//品詞オプションあり
if(preg_match('/\[.*\]/u',$q_char)) {
$char = $xposes[$option][$j];
if($q_char == '['.$char.']') {
if($i == 0) {
$score_by_option[$option] += 100 - $j;
$seq = $j;
break;
} else {
$score_by_option[$option] += 100 - ($j - $seq)*10;
$seq = $j;
break;
}
}
判定処理は単語ごとに行います。$i
は入力された単語の順番を表しています。$j
はレコードの単語の順番を表しています。これによって,入力された単語とレコードの単語を一つずつ判定していきます。
単語が [ ] で囲まれているときは品詞情報の配列 $xposes
の要素と一致するかどうかで判定し,スコアを加算していきます。
同様に,単語が ( ) で囲まれているときはレンマの配列 $lemmas$
,単語そのものの場合は単語の配列 $chars
の要素と一致するかどうかを判定します。
修正により処理速度も大幅に改善しましたが,どうも文字コードを変換する mb_convert_encoding
使っていたことが原因だったようです。今回は,csv ファイルを作成する時点で文字コードをUTF-8に統一するようにしたので,文字コード変換を省くことができました。
最後に,コード全体を示します。
<html lang="ja">
<head>
<meta charset="utf-8">
<title>英文法・語句選択問題検索</title>
<!--boostrap-->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<link rel="stylesheet" href="gsearch.css" type="text/css">
<!--[if lt IE 9]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<script src="http://css3-mediaqueries-js.googlecode.com/svn/trunk/css3-mediaqueries.js"></script>
<![endif]-->
</head>
<body>
<?php
$ln1 = array("1.","1","①","A.","A","a.","a","ア");
$ln2 = array("2.","2","②","B.","B","b.","b","イ");
$ln3 = array("3.","3","③","C.","C","c.","c","ウ");
$ln4 = array("4.","4","④","D.","D","d.","d","エ");
$query = htmlspecialchars(@$_GET['search'], ENT_QUOTES, 'UTF-8');
$ln = @$_GET['label'];
$q_chars = explode(" ",$query);
$q_len = count($q_chars);
$text = [];
$chars = [];
$lemmas = [];
$xposes = [];
//検索結果の表示
function Gsearch_text_option() {
global $text, $lemmas, $xposes, $ln, $ln1, $ln2, $ln3, $ln4;
global $query, $q_chars, $q_len;
$handle = gzopen("base_contents.csv.gz","r");
$id = 0;
while($base_text = fgetcsv($handle)) {
$text[$id] = $base_text;
$id += 1;
}
fclose($handle);
$handle = gzopen("data.csv.gz","r");
$id = 0;
while($chars[0] = fgetcsv($handle)) {
$chars[1] = fgetcsv($handle);
$chars[2] = fgetcsv($handle);
$chars[3] = fgetcsv($handle);
$lemmas[0] = fgetcsv($handle);
$lemmas[1] = fgetcsv($handle);
$lemmas[2] = fgetcsv($handle);
$lemmas[3] = fgetcsv($handle);
$xposes[0] = fgetcsv($handle);
$xposes[1] = fgetcsv($handle);
$xposes[2] = fgetcsv($handle);
$xposes[3] = fgetcsv($handle);
for ($option = 0; $option <= 3; $option++) {
$score_by_option = array(0,0,0,0);
$chars_len = count($chars[$option]);
$seq = -1;
//検索文字列を単語ごとに判定
for ($i = 0; $i < $q_len; $i++) {
$q_char = $q_chars[$i];
//問題文の先頭から検索
for ($j = 0; $j < $chars_len; $j++) {
if ($seq == -1 || $j > $seq) {
//品詞オプションあり
if(preg_match('/\[.*\]/u',$q_char)) {
$char = $xposes[$option][$j];
if($q_char == '['.$char.']') {
if($i == 0) {
$score_by_option[$option] += 100 - $j;
$seq = $j;
break;
} else {
$score_by_option[$option] += 100 - ($j - $seq)*10;
$seq = $j;
break;
}
}
} elseif(preg_match('/\(.*\)/u',$q_char)) {
//レンマオプションあり
$char = $lemmas[$option][$j];
if($q_char == '('.$char.')') {
if($i == 0) {
$score_by_option[$option] += 100 - $j;
$seq = $j;
break;
} else {
$score_by_option[$option] += 100 - ($j - $seq)*10;
$seq = $j;
break;
}
}
} else {
//通常の単語
$char = $chars[$option][$j];
if ($q_char == $char) {
if($i == 0) {
$score_by_option[$option] += 100 - $j;
$seq = $j;
break;
} else {
$score_by_option[$option] += 100 - ($j - $seq)*10;
$seq = $j;
break;
}
}
}
}
}
if ($score_by_option[$option] == 0) {
break;
}
}
}
$scores[$id] = max($score_by_option);
$id += 1;
}
fclose($handle);
arsort($scores);
$count = 0;
echo "<p>検索結果:</p>";
foreach($scores as $key => $value) {
if($scores[$key] > 0) {
echo "<p>";
echo str_ireplace("#","( )",$text[$key][0]);
echo " <span class='univ_name'>(".$text[$key][5].")</span>";
echo "</p>";
echo "<p style='text-indent:1.5em' class='mb-4'>";
echo $ln1[$ln]." ".$text[$key][1];
echo " ".$ln2[$ln]." ".$text[$key][2];
echo " ".$ln3[$ln]." ".$text[$key][3];
echo " ".$ln4[$ln]." ".$text[$key][4];
echo "</p>";
$count += 1;
}
if ($count == 100) {
break;
}
}
}
function Gsearch_option() {
global $text, $chars, $xposes, $ln, $ln1, $ln2, $ln3, $ln4;
global $query, $q_chars, $q_len;
$count = 0;
for ($id = 0; $id < count($text); $id++) {
$score_min = 100;
for($i=0;$i<=3;$i++){
$score = levenshtein(mb_strtolower($query), mb_strtolower(substr($text[$id][$i],0,15)));
if ($score < $score_min) {
$score_min = $score;
}
}
$scores[$id] = $score_min;
}
asort($scores);
$count = 0;
foreach($scores as $key => $value) {
echo "<p class='mb-4'>";
echo $ln1[$ln]." ".$text[$key][1];
echo " ".$ln2[$ln]." ".$text[$key][2];
echo " ".$ln3[$ln]." ".$text[$key][3];
echo " ".$ln4[$ln]." ".$text[$key][4];
echo "</p>";
$count += 1;
if ($count == 100) {
break;
}
}
}
?>
<div class="container">
<header class="mt-2">
<p>
<?php
echo "英語・語句選択問題検索<span class='badge badge-success ml-2 mr-2'>ベータ版</span>データベース登録数10650問";
?>
</p>
</header>
<article>
<form method="get">
<div class="form-row align-items-center">
<div>
<?php
//検索ボックス
echo '<input type="text" id="search_form" class="form-control mt-2 mb-2" name="search" size="33" maxlength="50"';
echo 'value="'.htmlspecialchars(@$_GET['search'], ENT_QUOTES, 'UTF-8').'" autofocus>';
?>
</div>
<div class="col-auto">
<input type="submit" class="btn btn-outline-info" value="検索">
</div>
<div class="btn-group btn-group-toggle ml-2" data-toggle="buttons">
<?php
//一つ目のトグルボタン
$checked = array("","");
$content = array("問題文と選択肢","選択肢のみ");
if (@$_GET['display'] == null) {
$checked[0] = "checked";
} else {
$checked[@$_GET['display']] = "checked";
}
for ($i = 0; $i < count($checked); $i++) {
echo '<label class="btn btn-outline-info">';
echo '<input type="radio" name="display" value="'.$i.'" '.$checked[$i].'>'.$content[$i];
echo '</label>';
}
?>
</div>
<div class="btn-group btn-group-toggle ml-2" data-toggle="buttons">
<?php
//二つ目のトグルボタン
$checked = array("","","","","","","","");
$content = array("1.","1","①","A.","A","a.","a","ア");
if (@$_GET['label'] == null) {
$checked[0] = "checked";
} else {
$checked[@$_GET['label']] = "checked";
}
for ($i = 0; $i < count($checked); $i++) {
echo '<label class="btn btn-outline-info">';
echo '<input type="radio" name="label" value="'.$i.'" '.$checked[$i].'>'.$content[$i];
echo '</label>';
}
?>
</div>
</div>
</form>
<div class="alert alert-success" role="alert">
<p>品詞オプション</p>
<hr>
<p>[CC]等位接続詞 [IN]前置詞/従位接続詞 [JJ]形容詞 [JJR]形容詞比較級 [JJS]形容詞最上級 [MD]助動詞 [NOUN]名詞 [RB]副詞 [RBR]副詞比較級 [RBS]副詞最上級 [VBD]動詞過去形 [VBG]動名詞/現在分詞 [VBN]過去分詞 [VERB]動詞 [WDT]whatやwhichなど [WP]関係代名詞 [WRB]関係副詞</p>
</div>
<div id="result" class="mt-4">
<?php
if (@$_GET['search'] != null && @$_GET['display'] == 0) {
Gsearch_text_option(); #問題文と選択肢の検索
}
if (@$_GET['search'] != null && @$_GET['display'] == 1) {
Gsearch_option(); #選択肢のみ検索
}
?>
</div>
</article>
<footer class="text-right mb-5">2020 <a href="https://mmsankosho.com">mm参考書</a></footer>
</div>
</body>
</html>
SNSでシェア
mm参考書 2019-2023