LoginSignup
1
1

C#でWord文書内を文字列検索して連番に置換するプログラムを書いてみた

Last updated at Posted at 2019-10-26

C#からMS Officeの操作をするとき、Excelに比べてWordの情報が少ないので、書いてみた。
(検索すれば、開く・保存するくらいはすぐに見つかるが、それ以上のことがあまり出てこない。)

やれること

C#でWord文書上でキーワードを検索して、連番に置き換える。
Windows7以降であればインストール不要で使えます。(Wordは要るけど。)

処理前のWord文書

image.png

処理後のWord文書

image.png

画面

ラベル振るのさぼりました。
使い方はソースをみてください。。。

image.png

注意事項

  • COMオブジェクトの解放はGCまかせにしてます。
  • 途中でWord文書を閉じたりすると、例外で落ちます。
  • 開く対象のWord文書は、あらかじめ閉じておいてください。
  • C#から操作した場合に、Word文書内の変更履歴に残るのか未確認
  • 検索対象が素のテキストだけか未確認。(相互参照とか目次とかヘッダーフッターとか引っかかってしまうかも)
  • 何か起きても責任はとれませんので、自己責任でご利用ください。

ソースコード

WordInteropTest.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.Runtime.CompilerServices;

using Word = Microsoft.Office.Interop.Word;


// 再度実行するには、本アプリケーションで起動したWordを閉じ、本アプリケーションを再起動してください。

class WordTest : Form
{
    const int ForwardLentgh = 50;

    Word.Application app;
    Word.Document doc;

    Button btnLoad;
    TextBox txtFindPrefix;           // 検索対象の接頭語
    TextBox txtFindRegex;            // 検索対象の正規表現(接頭語を含める)
    TextBox txtReplacerPrefix;       // 置換後のIDの接頭語
    NumericUpDown nudReplacerDigits; // 置換後のIDの数値部の桁数
    NumericUpDown nudReplacerId;     // 置換後のIDの数値部
    Button btnResetPos;
    Button btnFindReplace;
    
    WordTest()
    {
        btnLoad = new Button();
        btnLoad.Text = "Open Word File.";
        btnLoad.Size = new Size(150,25);
        btnLoad.Click += (sender,e)=>{LoadWordFile();};
        Controls.Add(btnLoad);

        txtFindPrefix = new TextBox();
        txtFindPrefix.Location = new Point(0,50);
        txtFindPrefix.Text = "BeforeID"; // Wordが認識する特殊文字あり注意(^t や ^p など))
        Controls.Add(txtFindPrefix);

        txtFindRegex = new TextBox();
        txtFindRegex.Location = new Point(0,75);
        txtFindRegex.Width = 150;
        txtFindRegex.Text = @"BeforeID[0-9]{5}\b"; //  \b は(C#の正規表現では)単語境界を示す。
        Controls.Add(txtFindRegex);

        nudReplacerDigits = new NumericUpDown();
        nudReplacerDigits.Location = new Point(150,75);
        nudReplacerDigits.Width = 45;
        nudReplacerDigits.Maximum = 7;
        nudReplacerDigits.Value = 5;
        nudReplacerDigits.Minimum = 1;
        Controls.Add(nudReplacerDigits);

        txtReplacerPrefix = new TextBox();
        txtReplacerPrefix.Location = new Point(0,120);
        txtReplacerPrefix.Text = "AfterID";
        Controls.Add(txtReplacerPrefix);

        nudReplacerId = new NumericUpDown();
        nudReplacerId.Location = new Point(100,120);
        nudReplacerId.Width = 80;
        nudReplacerId.Maximum = 9999999;
        nudReplacerId.Value = 1;
        nudReplacerId.Minimum = 0;
        Controls.Add(nudReplacerId);

        
        btnResetPos = new Button();
        btnResetPos.Location = new Point(0,200);
        btnResetPos.Text = "検索位置を先頭に戻す";
        btnResetPos.Size = new Size(150,25);
        btnResetPos.Click += (sender,e)=>{ResetFindPos();};
        btnResetPos.Enabled = false;
        Controls.Add(btnResetPos);

        
        btnFindReplace = new Button();
        btnFindReplace.Location = new Point(0,250);
        btnFindReplace.Text = "Find and Replace";
        btnFindReplace.Size = new Size(150,25);
        btnFindReplace.Click += (sender,e)=>{FindAndReplace();};
        btnFindReplace.Enabled = false;
        Controls.Add(btnFindReplace);

        ClientSize = new Size(350,400);
        Text = "Word ID replacer";
    }


    void LoadWordFile()
    {
        object oMis = Missing.Value; // System.Reflection.Missing.Value
        object oTru = true;
        object oFal = false;

        
        object oPth = GetOpenFileNameFromDialog();
        if ( oPth == null ) {
            return;
        }

        btnLoad.Enabled = false;
        btnResetPos.Enabled = true;
        btnFindReplace.Enabled = true;

        app = new Word.Application();
        
        app.Application.Visible = true; // true:Wordを表示させる  false:表示させない
        // app.Application.DisplayAlerts = Word.WdAlertLevel.wdAlertsNone; // ← 表示させない場合はこれも併せて指定するとよいらしい

        // https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.office.interop.word.documents.open?view=word-pia
        // ※Revert引数は検討必要。
        doc = app.Documents.Open(
            ref oPth,// FileName
                     // ※相対パスで指定すると、ドキュメントフォルダを探しにいくようなので注意。
                     //   (Fileクラスと同じような振る舞いを期待しているとはまります。)
            ref oMis,// ConfirmConversions:
                     //   True to display the Convert File dialog box if the file isn't in Microsoft Word format.
            ref oFal,// ReadOnly: True to open the document as read-only.
            ref oMis,// AddToRecentFiles:
                     //   True to add the file name to the list of recently used files at the bottom of the File menu.
            ref oMis,// PasswordDocument:  The password for opening the document.
            ref oMis,// PasswordTemplate:  The password for opening the template.
            ref oFal,// Revert:  Controls what happens if FileName is the name of an open document. 
                     //   True to discard any unsaved changes to the open document and reopen the file.
                     //   False to activate the open document.
            ref oMis,// WritePasswordDocument: The password for saving changes to the document.
            ref oMis,// WritePasswordTemplate: The password for saving changes to the template.
            ref oMis,// Format: The file converter to be used to open the document. Can be a WdOpenFormat constant.
            ref oMis,// Encoding: Can be any valid MsoEncoding constant. The default value is the system code page.
            ref oMis,// Visible:
            ref oMis,// OpenAndRepair
            ref oMis,// DocumentDirection: wdLeftToRight or wdRightToLeft
            ref oMis,// NoEncodingDialog:
                     //   True to skip displaying the Encoding dialog box that Word displays if the text encoding cannot be recognized.
            ref oMis // XMLTransform
        );
    }

    
    [MethodImpl(MethodImplOptions.NoInlining)]
    void FindAndReplace()
    {
        object oMis = Missing.Value; // System.Reflection.Missing.Value
        object oTru = true;
        object oFal = false;
        object oFindStop = Word.WdFindWrap.wdFindStop;
        object oNoReplace = Word.WdReplace.wdReplaceNone;

        Regex r;
        try {
            r = new Regex("^" + txtFindRegex.Text); 
        }
        catch(ArgumentException e) {
            MessageBox.Show(e.ToString());
            return;
        }

        object oFindText = txtFindPrefix.Text;
        string prefixNew = txtReplacerPrefix.Text;
        int idNew = (int)nudReplacerId.Value;
        int digits = (int)nudReplacerDigits.Value;


        
        if ( app.Selection.Document != doc ) {
            // テキスト選択
            doc.Select();
            app.Selection.Start = 0;
            app.Selection.End = 0;
        }
        else {
            app.Selection.End = app.Selection.Start;
        }

        // https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.office.interop.word.find.execute?view=word-pia

        for (;;) {
            // 固定部分を探す
            bool foundPrefix = app.Selection.Find.Execute (
                ref oFindText,// FindText: 特殊文字あり注意。
                            //   You can search for special characters by specifying appropriate character codes.
                            //   For example, "^p" corresponds to a paragraph mark and "^t" corresponds to a tab character. 
                ref oTru,// MatchCase: True to specify that the find text be case-sensitive.
                ref oFal,// MatchWholeWord: 単語単位での検索
                ref oFal,// MatchWildcards
                ref oFal,// MatchSoundsLike: あいまい検索
                ref oFal,// MatchAllWordForms:
                        //   True to have the find operation locate all forms of the find text (for example, "sit" locates "sitting" and "sat").
                ref oTru,// Forward: Trueで前方検索
                ref oFindStop,// Wrap: 検索しきった後に、最初から検索しなおすかどうかを指定する
                ref oMis,// Format: よくわからない
                ref oMis,// ReplaceWith
                ref oNoReplace,// Replace: ここではReplaceしない指定とする (oMisでもよいのかも)
                ref oMis,// MatchKashida: アラビア語関連の機能らしい。
                ref oMis,// MatchDiacritics: 右から左に読む言語関連の機能らしい。
                ref oMis,// MatchAlefHamza: アラビア語関連の機能らしい。
                ref oMis // MatchControl: 右から左に読む言語関連の機能らしい。
            );

            // 見つからなかったら検索終了
            if ( !foundPrefix ) {
                MessageBox.Show("Not found.");
                app.Selection.Start = 0;
                app.Selection.End = 0;
                break;
            }

            // 選択範囲を拡張する。
            app.Selection.End += ForwardLentgh;

            // 指定された正規表現にマッチするかチェックする
            string s = app.Selection.Text;
            Match m = r.Match(s);

            if ( m.Success ) {
                string matchPart = m.Groups[0].Value;
                int startPos = app.Selection.Start;
                string replacerStr = prefixNew + idNew.ToString("D"+digits.ToString());

                app.Selection.End = startPos + matchPart.Length; // 選択範囲を変更。(先頭は一致している前提。)
                app.Selection.Text = replacerStr;

                nudReplacerId.Value++;

                // 次に検索ボタンを押したときのために、検索位置を進める
                app.Selection.Start = startPos + replacerStr.Length;
                app.Selection.End = app.Selection.Start;

                break;
            }
            else { // 正規表現にはマッチしなかった。
                // 次を検索させる。
                Console.WriteLine("Info: Regex mismatch. " + s);
                app.Selection.Start = app.Selection.End;
            }
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    void ResetFindPos()
    {
        app.Selection.Start = 0;
        app.Selection.End = 0;
    }

    string GetOpenFileNameFromDialog()
    {
        var ofd = new OpenFileDialog();
        ofd.Filter = "MS Word File(*.doc;*.docx)|*.doc;*.docx|All files|*";
        ofd.Title = "Select file";
        ofd.RestoreDirectory = true;

        if ( ofd.ShowDialog() == DialogResult.OK ) {
            return ofd.FileName;
        }
        else {
            return null;
        }
    }


    // RunApp()に処理を分離して属性指定している目的:
    //   JITによるinline展開を禁止することで、
    //   Mainメソッド内のGC.Collect()までに余計な(Wordの)COM参照オブジェクトが残らないことを期待する。
    //   ただし、期待通りに動作しているかは確認していない。(そもそも要るのか不明。。)
    [MethodImpl(MethodImplOptions.NoInlining)]
    static void RunApp()
    {
        Application.Run(new WordTest());
    }

    [STAThread]
    static void Main()
    {
        RunApp();
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

コンパイル方法

dllの場所は環境依存と思います。
C:\Windows\assembly\GAC_MSIL\以下をdir /s Microsoft.Office.Interop.Word.dllで検索すればいくつか出てくるはずなので、最新っぽいのを使えばよいはず。

csc /r:C:\Windows\assembly\GAC_MSIL\Microsoft.Office.Interop.Word\15.0.0.0__71e9bce111e9429c\Microsoft.Office.Interop.Word.dll ^
 WordInteropTest.cs

cscのパスの通し方はググってね。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1