.txt
文件中搜索字符串。是否可以优化代码,使其以最快的方式搜索字符串?假设文本文件很大(500MB-1GB)我不想使用正则表达式。
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class StringFinder {
public static void main(String[] args)
{
double count = 0,countBuffer=0,countLine=0;
String lineNumber = "";
String filePath = "C:\Users\allen\Desktop\TestText.txt";
BufferedReader br;
String inputSearch = "are";
String line = "";
try {
br = new BufferedReader(new FileReader(filePath));
try {
while((line = br.readLine()) != null)
{
countLine++;
//System.out.println(line);
String[] words = line.split(" ");
for (String word : words) {
if (word.equals(inputSearch)) {
count++;
countBuffer++;
}
}
if(countBuffer > 0)
{
countBuffer = 0;
lineNumber += countLine + ",";
}
}
br.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Times found at--"+count);
System.out.println("Word found at--"+lineNumber);
}
}
#1 楼
快速付出了代价。...代码复杂性和可读性。假设您的代码现在可以产生正确的结果....这是一个很大的假设,因为:
期望单词在行的开头/结尾,或者被空格包围(不是逗号,标点符号等)。
它不会在另一个字符串中查找单词,因此将匹配'are',但不匹配'bare'。
OK,一种更快的方法(将其保持为Java)是执行以下操作:
将搜索字符串('are')转换为与文件相同编码的字节数组。
从文件上的文件通道中打开内存映射的字节缓冲区。
扫描ByteBuffer,查找与搜索字节数组匹配的内容。
关闭字节缓冲区。
关闭ByteBuffer
如果文件大于内存,则必须偶尔重新定位字节缓冲区。我建议使用约4MB的Emopry映射大小加上搜索字符串的大小。这样,您可以搜索4MB窗口,然后在下一个4mb边界处开始下一个窗口。
一旦进入该窗口,这将很有意义。
此系统将很快,因为您将不必将文件的数据复制到Java中。一切实际上都会发生在事物的本机方面。
要使它起作用,需要阅读很多东西。
我将从教程开始....
StudyTrails
Yaldix
LinuxTopia
当然,如果您想真正快速,请使用grep。
这里以下是一些示例代码,可以帮助您入门:
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class NIOGrep {
public static void main(String[] args) throws IOException {
if (args.length != 2) {
throw new IllegalArgumentException();
}
String grepfor = args[0];
Path path = Paths.get(args[1]);
String report = searchFor(grepfor, path);
System.out.println(report);
}
private static final int MAPSIZE = 4 * 1024 ; // 4K - make this * 1024 to 4MB in a real system.
private static String searchFor(String grepfor, Path path) throws IOException {
final byte[] tosearch = grepfor.getBytes(StandardCharsets.UTF_8);
StringBuilder report = new StringBuilder();
int padding = 1; // need to scan 1 character ahead in case it is a word boundary.
int linecount = 0;
int matches = 0;
boolean inword = false;
boolean scantolineend = false;
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
final long length = channel.size();
int pos = 0;
while (pos < length) {
long remaining = length - pos;
// int conversion is safe because of a safe MAPSIZE.. Assume a reaosnably sized tosearch.
int trymap = MAPSIZE + tosearch.length + padding;
int tomap = (int)Math.min(trymap, remaining);
// different limits depending on whether we are the last mapped segment.
int limit = trymap == tomap ? MAPSIZE : (tomap - tosearch.length);
MappedByteBuffer buffer = channel.map(MapMode.READ_ONLY, pos, tomap);
System.out.println("Mapped from " + pos + " for " + tomap);
pos += (trymap == tomap) ? MAPSIZE : tomap;
for (int i = 0; i < limit; i++) {
final byte b = buffer.get(i);
if (scantolineend) {
if (b == '\n') {
scantolineend = false;
inword = false;
linecount ++;
}
} else if (b == '\n') {
linecount++;
inword = false;
} else if (b == '\r' || b == ' ') {
inword = false;
} else if (!inword) {
if (wordMatch(buffer, i, tomap, tosearch)) {
matches++;
i += tosearch.length - 1;
if (report.length() > 0) {
report.append(", ");
}
report.append(linecount);
scantolineend = true;
} else {
inword = true;
}
}
}
}
}
return "Times found at--" + matches + "\nWord found at--" + report;
}
private static boolean wordMatch(MappedByteBuffer buffer, int pos, int tomap, byte[] tosearch) {
//assume at valid word start.
for (int i = 0; i < tosearch.length; i++) {
if (tosearch[i] != buffer.get(pos + i)) {
return false;
}
}
byte nxt = (pos + tosearch.length) == tomap ? (byte)' ' : buffer.get(pos + tosearch.length);
return nxt == ' ' || nxt == '\n' || nxt == '\r';
}
}
评论
\ $ \ begingroup \ $
使用map-reduce样式算法可以更快地实现此想法,该算法可以同时搜索文件的多个区域。
\ $ \ endgroup \ $
–mauhiz
2014年11月11日下午5:13
\ $ \ begingroup \ $
“该系统将很快,因为您将不必将文件的数据复制到Java中。所有事情实际上都将发生在事物的本机方面。”并非完全正确,您正在使用buffer.get(i)一次将数据复制到Java。
\ $ \ endgroup \ $
–艾米莉·L。
2014年3月11日17:08
#2 楼
如果要提高性能,可以尝试使用其他算法。这就是grep
的作用:GNU grep使用著名的Boyer-Moore算法,该算法首先查找目标字符串的最后一个字母,并使用查找表来告诉它
为什么GNU grep快速(您将在此页面上找到其他聪明的想法)。
它会在输入中跳过多远。 br />
您可以在相应的Wikipedia页面上找到更多详细信息。
#3 楼
如果您希望将“ are”与其周围的空格匹配,只需添加如下空格:“ are”,然后查看该行是否包含该字符串(考虑一些边缘情况)。 String paddedInput = " " + inputSearch + " ";
String paddedInputStart = inputSearch + " ";
String paddedInputEnd = " " +inputSearch ;
while((line = br.readLine()) != null)
{
countLine++;
if(line.equals(inputSearch) ||
line.startsWith(paddedInputStart) ||
line.endsWith(paddedInputEnd) ||
(line.contains(paddedInput)) {
lineNumber += countLine + ",";
}
}
首先以最少的费用进行失败的检查。等于将首先检查字符串的长度,因此这是一种快速检查,除非该行到搜索空间的长度相等(不是那么频繁)。功能
startsWith
和endsWith
是快速检查,因为它们不执行搜索。 contains
最后一次完成,因为这是最昂贵的。以上内容避免了单词列表的拆分(可能很慢)和迭代。而是让最有可能在本机代码中实现的字符串API为您完成工作。必须确保在循环之前构造使用的字符串,以避免重复的字符串操作,尽管我不确定Java编译器会对此进行优化,但我不确定。
String.contains()
的良好实现将使用Boyer-Moore,但没有必要这样做。 Java并没有规定它是哪种算法。如果您想确定,请参见答案中的链接:https://codereview.stackexchange.com/a/44042/36120 评论
\ $ \ begingroup \ $
是的@Emily你是对的。现在唯一困扰我的是我必须处理所有标点符号。例如:标点如“,。!)'-> /
\ $ \ endgroup \ $
–艾伦·萨维奥(Allen Savio)
2014年11月11日17:38
\ $ \ begingroup \ $
在这种情况下,更容易使用int i = String.indexOf(inputSearch),如果(i!= -1),则查看line.charAt(i-1)和line.charAt(i + inputSearch)。长度)(用于标点符号或空格)(明显检查行的范围)。如果匹配失败,请使用String.indexOf(inputSearch,i + inputSearch.length)搜索下一个重复出现并重复。
\ $ \ endgroup \ $
–艾米莉·L。
2014年11月11日17:53
#4 楼
由于您想找到匹配成功的行号,因此,我将尝试根据您基于BufferedReader.readLine()
的当前策略进行改进,并仅在必要时才采用NIO等更奇特的方式。有两个昂贵的操作是字符串拆分和字符串连接。
将行拆分为单词时,它必须为每个单词分配并复制新的
String
的字符,以及用于存储结果的数组相反,您可以在行中搜索,然后检查匹配的开始和结尾是否出现在单词边界处。int len = inputSearch.length();
int countInLine = 0;
int pos = -1;
while ((pos = line.indexOf(inputSearch, pos + 1)) >= 0) {
if ((pos == 0 || Character.isWhitespace(line.charAt(pos - 1))) &&
(pos + len == line.length() || Character.isWhitespace(line.charAt(pos + len))) {
countInLine++;
}
}
您应该将标点符号视为空格作为单词边界。
另一个低效率是重复的字符串连接。 Java中的字符串是不可变的。每当为字符串
a + b
和a
编写b
时,代码实际上都会编译为new StringBuilder(a).append(b).toString()
。因此,lineNumber
应该改为StringBuilder
,以便您可以高效地附加到其后。请注意,
FileNotFoundException
是IOException
的一种。您可以使用一个catch-block来处理两者。但是,如果出现IOException
,您可能不应该尝试报告字数统计,这可能是无效的。为此,您可以从main()
中消除所有try-catch块,然后声明main(String[] args) throws IOException
。然后,如果发生错误,它将只打印堆栈跟踪并退出。评论
\ $ \ begingroup \ $
现在我们处理空白。会有大量标点符号要处理。
\ $ \ endgroup \ $
–艾伦·萨维奥(Allen Savio)
2014年11月11日17:42
#5 楼
我不想使用正则表达式。
也许您应该使用。
一个鲜为人知的事实是,
Matcher
不会占用String
作为参数,但CharSequence
。并且String
实现了该接口。由于您正在处理大型文本文件,所以,我只为您提供一个库:largetext。它在大型文本文件上实现
CharSequence
,这意味着LargeText
实例可直接与Matcher
一起使用:private static final Pattern PATTERN = ...
private static final LargeTextFactory = LargeTextFactory.defaultFactory();
// in code;
final Path path = Paths.get("...");
try (
final LargeText largeText = factory.fromPath(...);
) {
final Matcher m = PATTERN.matcher(largeText);
// do stuff with m
}
评论
\ $ \ begingroup \ $
我尝试了您的库并使其正常运行-很好!-但问题是仅返回匹配项的索引而不是行号,因此尽管LargeText很酷,但我不确定它是不是非常适合此任务。
\ $ \ endgroup \ $
–james.garriss
15年7月17日在14:38
\ $ \ begingroup \ $
@ james.garriss嗯,对不起,Matcher也有其局限性;)
\ $ \ endgroup \ $
–fge
15年7月17日在15:55
#6 楼
您可以将字符串搜索为字节数组:在https://stackoverflow.com/questions/22234021/search-for-a-string-as-an-byte-in-a-binary-stream/中检查我的public static int search(byte[] input, byte[] searchedFor)
版本22236277#22236277 当然,在进行字节搜索时,您必须捕获所有“换行符”字符并对其进行计数,以便向用户提供找到匹配项的行号。 br />
评论
您是否对使用哪种算法进行过研究?例如,当搜索固定的字符串(即没有通配符)时,Knuth-Morris-Pratt的速度明显快于朴素的匹配。这可能会有所作为,请使用StringBuilderstackoverflow.com/questions/4645020/…