Замените длинный список слов в большом текстовом файле - PullRequest
4 голосов
/ 24 декабря 2011

мне нужен быстрый метод для работы с большим текстовым файлом

У меня есть 2 файла, большой текстовый файл (~ 20 Гб) и другой текстовый файл, содержащий ~ 12 миллионов списков комбо-слов

Я хочу найти все комбинированные слова в первом текстовом файле и заменить его другим комбинированным словом (комбинированное слово с подчеркиванием)

пример «Информация о компьютере»> Заменить на> «Информация о компьютере»

Я использую этот код, но производительность очень низкая (я тестирую на сервере Hp G7 с 16Gb Ram и 16 Core)

public partial class Form1 : Form
{
    HashSet<string> wordlist = new HashSet<string>();

    private void loadComboWords()
    {
        using (StreamReader ff = new StreamReader(txtComboWords.Text))
        {
            string line;
            while ((line = ff.ReadLine()) != null)
            {
                wordlist.Add(line);
            }
        }
    }

    private void replacewords(ref string str)
    {

        foreach (string wd in wordlist)
        {
          //  ReplaceEx(ref str,wd,wd.Replace(" ","_"));
            if (str.IndexOf(wd) > -1)
                str.Replace(wd, wd.Replace(" ", "_"));
        }
    }

    private void button3_Click(object sender, EventArgs e)
    {
        string line;
        using (StreamReader fread = new StreamReader(txtFirstFile.Text))
        {
            string writefile = Path.GetFullPath(txtFirstFile.Text) + Path.GetFileNameWithoutExtension(txtFirstFile.Text) + "_ReplaceComboWords.txt";
            StreamWriter sw = new StreamWriter(writefile);
            long intPercent;
            label3.Text = "initialing";
            loadComboWords();

            while ((line = fread.ReadLine()) != null)
            {
                replacewords(ref line);
                sw.WriteLine(line);

                intPercent = (fread.BaseStream.Position * 100) / fread.BaseStream.Length;
                Application.DoEvents();
                label3.Text = intPercent.ToString();
            }
            sw.Close();
            fread.Close();
            label3.Text = "Finished";
        }
    }
}

любая идея сделать эту работу в разумные сроки

Спасибо

Ответы [ 2 ]

3 голосов
/ 24 декабря 2011

Несколько идей:

  1. Я думаю, что будет эффективнее разбить каждую строку на слова и посмотреть, появится ли каждое из нескольких слов в вашем списке слов. 10 поисков в хешсете лучше, чем миллионы поисков подстроки. Если у вас есть составные ключевые слова, создайте соответствующие индексы: один, содержащий все отдельные слова, встречающиеся в реальных ключевых словах, и другой, содержащий все реальные ключевые слова.
  2. Возможно, загрузку строк в StringBuilder лучше заменить.
  3. Обновление прогресса после, скажем, 10000 обработанных строк, а не после каждой.
  4. Процесс в фоновых потоках. Это не сделает это намного быстрее, но приложение ответственно.
  5. Распараллелить код, как предложил Джереми.

UPDATE

Вот пример кода, который демонстрирует идею индекса по словам:

static void ReplaceWords()
{
  string inputFileName = null;
  string outputFileName = null;

  // this dictionary maps each single word that can be found
  // in any keyphrase to a list of the keyphrases that contain it.
  IDictionary<string, IList<string>> singleWordMap = null;

  using (var source = new StreamReader(inputFileName))
  {
    using (var target = new StreamWriter(outputFileName))
    {
      string line;
      while ((line = source.ReadLine()) != null)
      {
        // first, we split each line into a single word - a unit of search
        var singleWords = SplitIntoWords(line);

        var result = new StringBuilder(line);
        // for each single word in the line
        foreach (var singleWord in singleWords)
        {
          // check if the word exists in any keyphrase we should replace
          // and if so, get the list of the related original keyphrases
          IList<string> interestingKeyPhrases;
          if (!singleWordMap.TryGetValue(singleWord, out interestingKeyPhrases))
            continue;

          Debug.Assert(interestingKeyPhrases != null && interestingKeyPhrases.Count > 0);

          // then process each of the keyphrases
          foreach (var interestingKeyphrase in interestingKeyPhrases)
          {
            // and replace it in the processed line if it exists
            result.Replace(interestingKeyphrase, GetTargetValue(interestingKeyphrase));
          }
        }

        // now, save the processed line
        target.WriteLine(result);
      }
    }
  }
}

private static string GetTargetValue(string interestingKeyword)
{
  throw new NotImplementedException();
}

static IEnumerable<string> SplitIntoWords(string keyphrase)
{
  throw new NotImplementedException();
}

Код показывает основные идеи:

  1. Мы разбили ключевые фразы и обработанные строки на эквивалентные единицы, которые можно эффективно сравнить: слова.
  2. Мы храним словарь, который для любого слова быстро дает нам ссылки на все ключевые фразы, содержащие слово.
  3. Тогда мы применим вашу оригинальную логику. Однако мы делаем это не для всех 12 млн. Ключевых фраз, а для очень небольшого подмножества ключевых фраз, которые имеют по крайней мере пересечение одного слова с обработанной строкой.

Оставшуюся часть реализации я оставлю вам.

Однако в коде есть несколько проблем:

  1. * * * * * * * * SplitIntoWords должен фактически нормализовать слова к некоторой канонической форме Это зависит от требуемой логики. В простейшем случае вы, вероятно, будете в порядке с разделением пробельных символов и нижним регистром. Но может случиться так, что вам потребуется морфологическое сопоставление - это будет сложнее (это очень близко к задачам полнотекстового поиска).
  2. Ради скорости, вероятно, было бы лучше, если бы метод GetTargetValue вызывался один раз для каждой ключевой фразы перед обработкой ввода.
  3. Если во многих ваших ключевых словах есть совпадающие слова, у вас все равно будет значительный объем дополнительной работы. В этом случае вам нужно будет сохранять позиции ключевых слов в ключевых фразах, чтобы использовать вычисление расстояния между словами, чтобы исключить ненужные ключевые фразы при обработке строки ввода.
  4. Кроме того, я не уверен, действительно ли StringBuilder быстрее в данном конкретном случае. Вы должны поэкспериментировать с StringBuilder и string, чтобы узнать правду.
  5. В конце концов, это образец. Дизайн не очень хороший. Я хотел бы рассмотреть возможность извлечения некоторых классов с согласованными интерфейсами (например, KeywordsIndex).
3 голосов
/ 24 декабря 2011

На первый взгляд выбранный вами подход выглядит нормально - он должен работать нормально, и нет ничего очевидного, что может вызвать, например, много мусора.

Главное, что я думаю, это то, что вы будете толькоиспользовать одно из этих шестнадцати ядер: нечего делить нагрузку между остальными пятнадцатью.

I думаю самый простой способ сделать это - разделить большой 20-гигабайтный файл на шестнадцатькуски, затем проанализируйте каждый из кусков вместе, затем снова объедините куски вместе.Дополнительное время, необходимое для разделения и повторной сборки файла, должно быть минимальным по сравнению с ~ 16-кратным выигрышем при сканировании этих шестнадцати блоков вместе.

В общих чертах, один из способов сделать это может быть:

    private List<string> SplitFileIntoChunks(string baseFile)
    {
        // Split the file into chunks, and return a list of the filenames.
    }

    private void AnalyseChunk(string filename)
    {
        // Analyses the file and performs replacements, 
        // perhaps writing to the same filename with a different
        // file extension
    }

    private void CreateOutputFileFromChunks(string outputFile, List<string> splitFileNames)
    {
        // Combines the rewritten chunks created by AnalyseChunk back into
        // one large file, outputFile.
    }

    public void AnalyseFile(string inputFile, string outputFile)
    {
        List<string> splitFileNames = SplitFileIntoChunks(inputFile);

        var tasks = new List<Task>();
        foreach (string chunkName in splitFileNames)
        {
            var task = Task.Factory.StartNew(() => AnalyseChunk(chunkName));
            tasks.Add(task);
        }

        Task.WaitAll(tasks.ToArray());

        CreateOutputFileFromChunks(outputFile, splitFileNames);
    }

Одна крошечная гнида: перенесите расчет длины потока из цикла, вам нужно получить его только один раз.

РЕДАКТИРОВАТЬ: также включите идею @Pavel Gatilov, чтобы инвертировать логикувнутренний цикл и поиск каждого слова в строке в списке 12 миллионов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...