質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.31%
PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

Q&A

解決済

2回答

4506閲覧

WindowsのPHPで、ディレクトリ内ファイル一覧がうまく取得できない

kyone

総合スコア17

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

2グッド

2クリップ

投稿2020/08/13 19:26

編集2020/08/14 14:05

ディレクトリ内のファイルの一覧をPHPで取得しようと思っています。

glob関数を用い処理しようとしたところ、Windows環境で取得するファイルが重複している場合があります。

  • ファイルが大量にある場合に発生します(数万個)
  • Windowsのphpの場合に発生します(linuxでは発生しません)
  • globで取得した場合も、scandir、opendirとreaddir、DirectoryIteratorなどを使った場合でも同じように発生します。

以下のプログラムでは、testディレクトリ内に10個ファイルが作成されるので、
当然「10」が出力されますが、100000個作成した場合などに「100001」などとなります。
配列内を見てみると例えば「68979.txt」が2つあったりします。

どのようなことが原因で考えられますでしょうか?

PHP

1<?php 2 3$dir="test/"; 4for ($i=1;$i<=10;$i++){ 5 file_put_contents($dir.$i.".txt",$i); 6} 7$files=glob($dir."*"); 8echo count($files); 9 10?>

補足情報

OS:Windows10 Pro,Home
PHP version:PHP 7.2.26 (cli)、PHP7.4.9 (cli)

追記

みなさま、回答コメントありがとうございます。
とりあえず自分の環境でだけ起こっているのではない、ということが確認でき安心しました。

別OS,別バージョン,別ファイルシステムについては、試す環境と時間がないので申し訳ありませんが、追加の情報はありません。

何個目のファイルから、どのファイルが重複してカウントされるかを確認するために、ファイル名の数字の桁を10桁にそろえて確認しました。

PHP

1<?php 2 3$max=65535*2; 4 5$dir='test/'; 6for ($i=1;$i<=$max;$i++){ 7 file_put_contents($dir.sprintf("%010d",$i).".txt",$i); 8} 9 10$files=glob($dir."*"); 11echo count($files); 12 13?> 14

65533ファイルまでは問題なく動作し、65534ファイルになるとglobで取得する値が65535個となり、0000065534.txtを2回取得しています。
1回目の重複
その次は、131069(≒65535×2)ファイルで、同様に重複する値を取得します。
2回目の重複

これ以上は検証していませんが、多分、65535ファイル毎に重複する
ファイルが1つずつ増えていく感じでしょうか。

現時点で確認している不具合はファイルの一覧を取得した際、存在しているファイルを重複して取得するだけなので、m6uさんのarray_unique()で対処可能です。

ただ、重複するのではなく、例えば取りこぼす(ファイルが存在するのに取得しない)ような不具合が仮にあったとすれば、array_unique()では対処できないので、別の方法を考える必要がありそうです。

tanat, kai0310👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2020/08/13 23:09

再現できる環境がないので興味本位の追記依頼ですが、重複カウントされるファイルに規則性があったりしますか?(例えば毎回「68979.txt」とか) また、再現性はどの程度あるのでしょうか?いくつかの環境で試されてるようですが、再現可能な環境と再現しない環境の確認済みリストを提示することは可能でしょうか?(例えば、Win7 や Ver 5.6 で確認したのかが今の提示内容では判断できません。)
退会済みユーザー

退会済みユーザー

2020/08/13 23:22

XAMPP(PHP Version 7.3.20)ですが、うちでも再現できました。
退会済みユーザー

退会済みユーザー

2020/08/14 20:30

とりあえず最新Master(8.0)でデバッグビルドしてWin10Home(1903)64bitで動かしてみました(517c9938afe32b7e9ffa27a2413137382ac9e29b)。 時間掛かるので10万ファイルで一度だけ動作させて再現しませんでした。確率はどれくらいですか? なおCのソースでは、 win32/glob.c win32/readdir.c 辺りが該当します。
退会済みユーザー

退会済みユーザー

2020/08/14 20:40

あ、後、繰り返したなら消してから再実行したのか、そのまま再実行したのか、初回で現象が発生したかを覚えてたら教えて下さい。
kyone

2020/08/14 21:46

自分の環境では、不具合が発生した場合、もう一度実行すると必ず再現し、重複するファイルも常に同じになります。 ファイルを作成し、最初の実行で発生し、消さずに再実行でも発生。 ディレクトリごと削除して再度実行しても発生、別のディレクトリに作成しても発生します。
退会済みユーザー

退会済みユーザー

2020/08/14 21:52 編集

ありがとうございます。 バイナリどころか、バージョンもビルドも違うので、環境依存なのかすら分かりませんが、あの後さらに毎回削除して6回続けてますが、一度も再現していません。 つかぬことを聞きますが、ウィルス対策ソフトは使ってらっしゃいますか?もし使ってたら、一旦無効にして再現するかどうか試してもらえないでしょうか?(今できるのならですが...)
退会済みユーザー

退会済みユーザー

2020/08/15 00:14

続報です。8.0のデバッグビルドを使って、空ディレクトリからを10回と10万ファイルの状態で10回再現確認してみました。 結論は、私の環境ではいずれも一度も再現しない、です。時間的には計測に8~9分、削除に1分弱程度かかります。セキュリティソフトはWindowsのデフォルトしか入ってません(今はたまたまMcAfee Web Advisorというのは理由があって入れてますが...そのうち消します)。 次は7.4.9をダウンロードしたバイナリで入れてみますね。
退会済みユーザー

退会済みユーザー

2020/08/15 01:41 編集

VC15 x64 Thread Safe (2020-Aug-04 15:17:50)のZip版で試しました まだ初回ですが、いきなり再現しました。 php.exeを含むディレクトリで、testフォルダ作ってphp hoge.phpを何も考えずに叩いただけです。hoge.phpは質問最初のコードを使っています。 XAMPPを使ったことがなかったので、普通Thread Safeなのか確信できず、一度XAMPPをインストールしたりしたので時間かかりました。これから空状態から10回と10万ファイル状態から10回確認します。
退会済みユーザー

退会済みユーザー

2020/08/15 07:00 編集

あ、確認終わったの忘れてました。 空状態から10回と10万ファイル状態から10回ともに全て再現しました。 以上から、VC15 x64 Thread Safe (2020-Aug-04 15:17:50)のZip版では10万件のファイルが1ディレクトリにあるケースでglob()は必ず10万1件のファイルを検出する、と仮定します。 つまりfile_put_contents()が直前にあるかないか無関係と仮定するということです。 また、上記条件が整えば100%再現する問題だと仮定します。 次は同バージョンのローカルDebugビルドで1回試します。
退会済みユーザー

退会済みユーザー

2020/08/15 07:56

ローカルDebugビルドで1回中1回再現しました。 次は、readdir.cに仕込みを入れてビルドし、PHP側でも全ファイルを出力して比較してみます。
退会済みユーザー

退会済みユーザー

2020/08/15 08:44

readdir.cのreaddir()に、FindNextFileW後、php_win32_cp_conv_w_to_anyをした直後に fprintf(stdout, "readdir: "); fwrite(dp->fileinfo.cFileName, 1, reclen, stdout); fprintf(stdout, "\n"); のようなコードを仕込み、標準出力に出力したところ、phpで出力した重複ファイルと同じファイルが重複していました。現象はWin32API付近でも確認できるようです。 次はFindNextFileWの直後に仕込みを入れてみます。
退会済みユーザー

退会済みユーザー

2020/08/15 09:10

以下な仕込みを入れたのですが、 if (FindNextFileW(dp->handle, &(dp->fileinfo)) == 0) { dp->finished = 1; return NULL; } fprintf(stdout, "readdir: "); fwrite(dp->fileinfo.cFileName, 1, 12, stdout); fprintf(stdout, "\n"); この部分からは10万行の出力しか認められませんでした。
退会済みユーザー

退会済みユーザー

2020/08/15 09:17

今度はこうしてみました。 if (dp->offset != 0) { if (FindNextFileW(dp->handle, &(dp->fileinfo)) == 0) { dp->finished = 1; return NULL; } //fprintf(stdout, "readdir: "); //fwrite(dp->fileinfo.cFileName, 1, 12, stdout); //fprintf(stdout, "\n"); } else { fprintf(stdout, "readdir: "); fwrite(dp, sizeof(*dp), 1, stdout); fprintf(stdout, "\n"); } 実行したところ、readdir()が呼ばれたときにdp->offset==0であるタイミングが2回あるのですが、どうも2回目の呼出の際にファイル名と思しきデータが入っています。
退会済みユーザー

退会済みユーザー

2020/08/15 09:27

コメントを外して何回目が調べたところ、2回めのdp->offset==0は65537回目の呼出時のようです。
退会済みユーザー

退会済みユーザー

2020/08/15 09:54

なぜoffset==0のときにWin32APIを呼ばないのか分かりませんが、その結果ファイル名を返せば重複するのは明らかなので、offset==0のときはワイド文字を空文字列にしてしまいました。 if (dp->offset != 0) { if (FindNextFileW(dp->handle, &(dp->fileinfo)) == 0) { dp->finished = 1; return NULL; } fprintf(stdout, "readdir(offset=%d): ", dp->offset); fwrite(dp->fileinfo.cFileName, 1, 12, stdout); fprintf(stdout, "\n"); } else { fprintf(stdout, "readdir(offset=0): "); fwrite(dp, sizeof(*dp), 1, stdout); fprintf(stdout, "\n"); if (dp->fileinfo.cFileName) { fprintf(stdout, "readdir(special): "); fwrite(dp->fileinfo.cFileName, 1, 12, stdout); fprintf(stdout, "\n"); *(short*)(dp->fileinfo.cFileName) = 0; } } 結果、正しく重複なく10万ファイル取得できました。masterで現象が起きなかった理由などよく分かりませんし、これ以上は当人が調べたり問い合わせたりするべきだと思ったので、私の調査はこれで終わります。
kyone

2020/08/15 17:12

大変長いお時間をかけての検証、ありがとうございました。 私は現時点で時間もスキルもないため、これ以上、この不具合の根本的な解決を求めることは諦め、windows環境でのphpの使用をあきらめるつもりです。 php8では不具合の確認がされなかったという事で、今後に希望が持てました。 今後、時間が取れ次第、dameoさんの検証いただいた内容を自分でも試してみたいと思います。
退会済みユーザー

退会済みユーザー

2020/08/15 17:26

裏で流してただけだから実時間はさほど使ってないですよ。お気になさらず。 サブディレクトリ含め65536件以上Nodeのある場所をglob()すると危ないです。win32/*下のソースでしたがUnix系で使ってないかどうかの確認まではしてないので、他の環境なら大丈夫かと聞かれたら分かりませんし、8.0以降でなぜ大丈夫なのか見てないので、安心していいのかも分かりませんが、windows環境を諦めるのは賢明な判断かもしれませんね。私はglob()を使うこと自体がないし、そもそもphp自体をあまり使わないのでWin32API以降の問題でなかった時点で正直気になってません。
退会済みユーザー

退会済みユーザー

2020/08/15 20:34

dameo さん、検証お疲れさまでした。 dp->offset==0 のトリッキーな記述の原因が分かれば、色々わかりそうですね。 状況見ると、8.0 では修正されてるってことなんでしょうか。 興味深い。。。 該当箇所はコレですかね? https://github.com/php/php-src/blob/master/win32/readdir.c kyone さん、本検証を質問本文に追記して、タグに C を追加して、C な人たちを呼び込みましょう! それか、質問を仕切りなおして一旦新規で立てて、結果をここにフィードバックしてしまうのもありかと思います。
guest

回答2

5

自己解決

PHPの公式に問い合わせたら、14年前に既に報告されてるバグのようでした。
https://bugs.php.net/bug.php?id=36365

で、php8のほうで修正されているようです。
https://github.com/php/php-src/commit/767a77ac19af1192aa8b674d62f75b08abb199d6

このまま、過去のバージョンは修正されない、ってことなんでしょうかね?
そのあたりよくわかりませんが、とりあえず、解決済みとさせていただきます。

みなさま、お付き合いいただきありがとうございました。

投稿2020/08/18 20:25

編集2020/08/18 20:33
kyone

総合スコア17

tanat, kai0310, kei344👍を押しています

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2020/08/18 22:07

PR も何度か上がってるように見えるけど、何だったんでしょうね^^; お疲れさまでした。
退会済みユーザー

退会済みユーザー

2020/08/19 01:51

せめて7系にも反映してほしいのに。わかってよかった。
guest

1

うちでも再現できたので、ググってみたところ、
関係ありそうな情報を見つけました。

php - glob() can't find file names with multibyte characters on Windows? - Stack Overflow

詳しくはわかりませんが、
WIN32APIを使って実装されているために、
ファイルシステム上でUnicodeを許容している場合に問題が生じやすいってことのようです。

じゃぁarray_unique()を駆使したらどうかっていうと、
これで抑え込むことができました。

php

1<?php 2 3$dir="test/"; 4//for ($i=1;$i<=100000;$i++){ 5// file_put_contents($dir.$i.".txt",$i); 6//} 7$files=glob($dir."*"); 8echo count($files); 9echo PHP_EOL; 10//var_export($files); 11//echo PHP_EOL; 12 13$unique_files = array_unique(array_values($files), SORT_STRING); 14echo count($unique_files); 15echo PHP_EOL;

ちなみに、php.iniの設定値も添えておきます。
mbstring.language = Japanese
mbstring.internal_encoding = UTF-8
;mbstring.http_input =
;mbstring.http_output =
;mbstring.encoding_translation = Off
;mbstring.detect_order = auto
;mbstring.substitute_character = none
;mbstring.func_overload = 0
;mbstring.strict_detection = On
;mbstring.http_output_conv_mimetype=
;mbstring.regex_stack_limit=100000

投稿2020/08/13 23:44

編集2020/08/14 00:04
退会済みユーザー

退会済みユーザー

総合スコア0

tanat👍を押しています

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2020/08/14 00:03 編集

リンク先のBAだと、XAMPP では発生しないような記述が見受けられます。 原因は別じゃないかなぁ。。。 興味深いですけど、奇妙な現象ですね^^;
退会済みユーザー

退会済みユーザー

2020/08/14 00:06

100000としたときに、質問文中に示された数字がうちでも重複したので、 Windowsでの実装がアレなのかなぁ、などと考えてしまいます。 モヤモヤする。
退会済みユーザー

退会済みユーザー

2020/08/14 00:08

「68979.txt」が重複したってことですか?
退会済みユーザー

退会済みユーザー

2020/08/14 00:10

Yes、再現したというのは、そういう意味です。
退会済みユーザー

退会済みユーザー

2020/08/14 00:12

マジですか! ローカル汚したくなかったから、XAMPP 入れたくないんだけど、試してみたくなるなぁ。。。 明日までに解決してください。そうしないと多分入れてしまうw
退会済みユーザー

退会済みユーザー

2020/08/14 00:14

Windowsでの実装状況をソースコードレベルで観られる人じゃないと、解決できそうにないかも。 (なので、対処療法しか思いつかず。)
kyoya0819

2020/08/14 04:30

この不具合ですがどのくらいのファイル数から発生したでしょうか? (1万だとでないですが、10万つくろうとすると40分近くかかるので。。<-性能の問題
退会済みユーザー

退会済みユーザー

2020/08/14 04:47

書き出し先をUSBメモリ上のフォルダをシンボリックリンクとかして試すと時間短縮できるかも? 少なくとも6万5千を超えるあたりから怪しくなるような気がして、にんともかんとも。
kyoya0819

2020/08/14 05:00

Twitter上でNTFSが悪い説を提唱されている方がいらしたので、手持ちのUSBで動かしてみたのですが作成に時間がかかりすぎて。。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.31%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問