vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Java】【C#】ローカルのファイルを開いて指定の文字列が含まれているか調べてみる

はじめに

諸事情により、指定したディレクトリ以下にあるファイルの中から、特定の文字列が含まれるファイルを調べたくなりました。

このような希望を叶えてくれるツールはきっとたくさんあると思いはするのですが、自分でやってみたかったので作ってみることにしました。

言語は Java で。理由は興味がある以外にはありません。

ただ、 Java でいきなり書き始めると書くのが久しぶりすぎて迷走しそうな気がしたので、まず C# で作ったあとできるだけ同じ動きとなるように Java で作ってみることにします。

あ、 OS はいつも通り Windows 10 です。

C# で書く

まずは C# で書いてみます。

環境は .NET Core ver.2.2.101 で、コンソールアプリとして作ります。

Main メソッドで async/await を使いたいので C# 7.1 にしておきますよ。

SearchContainedFiles.csproj

< Project Sdk="Microsoft.NET.Sdk">
  < PropertyGroup>
    < OutputType>Exe< /OutputType>
    < TargetFramework>netcoreapp2.2< /TargetFramework>
    < LangVersion>7.1< /LangVersion>
  < /PropertyGroup>
< /Project>

Program.cs

using System;
using FileLoaders;
using System.Threading.Tasks;

namespace SearchContainedFiles
{
    class Program {
        static async Task Main(string[] args) {
            if(args == null || args.Length < 2){
                Console.WriteLine("Need two args");
                return;
            }
            foreach(string file in (await FileLoader.SearchAsync(new Uri(args[0]), args[1]))){
                Console.WriteLine("File: " + file);
            }
        }
    }
}

FileLoader.cs

using System;
using System.IO;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace FileLoaders
{
    public class FileLoader{
        public static async Task< List< string>> SearchAsync(Uri rootDirectoryPath, string keyword){
            if(Directory.Exists(rootDirectoryPath.AbsolutePath) == false){
                return new List< string>();
            }
            List< string> files = new List< string>();            
            foreach(string file in Directory.GetFiles(rootDirectoryPath.AbsolutePath, 
                    "*", SearchOption.AllDirectories)){
                if(await CheckKeywordExistAsync(file, keyword)){
                    files.Add(file);
                }
            }
            return files;
        }
        private static async Task< bool> CheckKeywordExistAsync(string filePath, string keyword){
            using(StreamReader stream = new StreamReader(filePath)){
                while(stream.Peek() >= 0){
                    if(stream.Peek() < 0){
                        break;
                    }
                    if((await stream.ReadLineAsync()).Contains(keyword)){
                        return true;
                    }
                }
            }
            return false;
        }
    }
}

コマンドライン引数で渡されたパス以下のファイルパスを一括取得 -> 1つずつ開いて確認しています。

Java で書く

次は Java です。

環境は下記の通り。

  • OpenJDK ver.12.0.1
  • Gradle ver.5.4.1

Gradle は gradle init でプロジェクト作るとかビルドとか実行とかに使ってます。

こちらもコマンドラインアプリで。

Files.lines でファイルを読む(失敗)

ネットの情報などを頼りにまずは書いてみることにします。

App.java

package SearchContainedFiles;

import java.nio.file.Path;
import java.nio.file.Paths;

import SearchContainedFiles.FileLoaders.FileLoader;

public class App {
    public static void main(String[] args) {
        if(args == null || args.length < 2){
            System.out.println("Need two args");
            return;
        }
        for(Path file: FileLoader.Search(Paths.get(args[0]), args[1])){
            System.out.println(file.toString());
        }
    }
}

FileLoader.java

package SearchContainedFiles.FileLoaders;

import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.stream.Stream;

public class FileLoader{
    public static Path[] Search(Path rootDirectory, String keyword) {
        if(Files.notExists(rootDirectory, LinkOption.NOFOLLOW_LINKS)){
            return new Path[0];
        }
        try(Stream< Path> files = Files.walk(rootDirectory,FileVisitOption.FOLLOW_LINKS)
            .filter(f -> f.getFileName().toString().matches("..*[.][a-zA-Z]+$"))){

           return files.filter(f -> CheckKeywordExistAsync(f, keyword))
            .toArray(Path[]::new);
        }
        catch(IOException e){
            System.out.println(e.getMessage());
        }
        return new Path[0];
    }
    private static boolean CheckKeywordExist(Path filePath, String keyword){
        try{
            return Files.lines(filePath)
                .anyMatch(p -> p.contains(keyword));
        }
        catch(IOException e){
            System.out.println(e.getMessage());
            return false;
        }
    }
}
  • Files.walk は指定のパス以下のフォルダ・ファイルの Path を一括で取得します。ファイルだけに絞りたかったため、正規表現でフィルタリングしてみました(後述しますが、素直にディレクトリかどうかを確認した方が良さそうです)。
  • C# の async/await 相当の処理ができないかどうかはいったん後回しにしています。

そしてエラー

これを実行すると、 Files.lines(filePath) の部分で MalformedInputException が発生しました。

Exception in thread "main" java.io.UncheckedIOException: java.nio.charset.MalformedInputException: Input length = 1

開こうとしたファイルの文字コードが違っていると。

BufferedReader でファイルを読む 1(失敗)

FileLoader.java

package SearchContainedFiles.FileLoaders;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.Charset;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.stream.Stream;
import java.io.InputStreamReader;

~省略~
    private static boolean CheckKeywordExistAsync(Path filePath, String keyword){
        try(InputStreamReader streamReader = new InputStreamReader(
            new FileInputStream(filePath.toString()))){
                try(BufferedReader reader = Files.newBufferedReader(filePath, Charset.forName(streamReader.getEncoding()))){
                    return reader.lines()
                        .anyMatch(p -> p.contains(keyword));
                }
                catch(IOException e){
                    System.out.println(e.getMessage());
                    return false;
                }
        }
        catch(FileNotFoundException ex){
            System.out.println(ex.getMessage());
            return false;
        }
        catch(IOException ex){
            System.out.println(ex.getMessage());
            return false;
        }
    }
}

何か色々見失いつつあるような気がしますが。。。

エンコードが違っているのが原因であれば、ファイルのエンコーディングを取得してセットしてやれば良いのでは?

と思ったのですが、結果は特に変わりませんでした。

なお、エラーが起きるファイルのエンコーディングは MS932 でした。

ぐぬぬ。。。

getEncoding ではなく直接 Charset.forName("MS932") と指定した場合も結果は変わりませんでした。

なんでや。。。

BufferedReader でファイルを読む 2(成功)

結局 Files.~ を使うのをやめて new BufferedReader としたところ、問題なく読み込めるようになりました。

しかもエンコーディング指定なしで。

FileLoader.java

~省略~
    private static boolean CheckKeywordExistAsync(Path filePath, String keyword){
       
        try(BufferedReader reader = new BufferedReader(
            new InputStreamReader(
                new FileInputStream(filePath.toString())))){
                    return reader.lines()
                        .anyMatch(p -> p.contains(keyword));
        }
        catch(IOException ex){
            System.out.println(ex.getMessage());
            return false;
        }        
    }
~省略~

問題が解決したのは良いですが、なんとも釈然としない。。。(´・ω・`)

犯人はヤ……俺

ググった限り、別に MS932 のファイルを読むことができない、という話ではなさそうです。

ではなぜ?と思い、ファイルを一つずつ見ていったところ、問題が発生するのは .doc や .zip のような、テキストエディターでは開けない(文字化けしたような文字の羅列として表示される)ファイルでした。

あまり何も考えずにパス指定してそこにあるファイルを読み込む、といったことをしていたために、テキストとして扱えないファイルが混ざってエラーになっていた、と。

では逆にエラーにならずに読み込めたこの三つのクラス、

役割としては、まず画像データなどの raw データをストリームに変換できる FileInputStream がデータを読み込み、 InputStreamReader が文字型入力ストリームに変換 -> BufferedReader で文字列データを読み込む、ということのようです。

raw データから文字型入力ストリームに変換せずに、文字型でないデータを読み込んでしまうとここまで見てきたような問題が発生する、ということのようです。

.doc や .zip ファイルは、一行ずつ読み込めたとして検索できないような気がするのですが、変換なしで文字型入力ストリームとして扱えるかを確認する方法はあるのでしょうか。。。?
(今回調べた限りだと、実際に読み込んでみてエラーになるかを確認するか、拡張子を指定するぐらいしか思いつきませんでした)

というところまでを考えると、確実にテキストファイル(として扱えるファイル)のみを読み込む場合を除き、FileInputStream 、 InputStreamReader 、 BufferedReader を使うのが良いような気はしました。

まぁ今回は外部のライブラリ、フレームワークなどを( Gradle 以外)極力使わないようにしたのですが、それらを含めるともう少し選択肢もありそうですね。

いったん切ります。

参照

Java

Gradle