摸摸鱼 利用投屏和Halcon识别 自动记录单词

1.先利用投屏,把手机的单词投屏到电脑上,然后使用C#保存投屏截图 ,对一个单词截图进行保存

2.利用Halcon OCR识别 单词截图,保存单词文本

3.根据单词文本,进行http访问,保存词义词源

下面截图是我初步完成的时候,功能是正常的,后续可以根据自己想法改动或者优化,比如自动生成一个txt文档的单词表,

第一步 C#截图程序:

截图的时候要看一下位置到底对不对,结合屏幕分辨率设定截取的位置和大小,比如我的屏幕分辨率是1920*1080,我把投屏放在左下角,我的截屏位置就可以靠近1080。实际上代码中我使用的是:

可以先保存下来看看。

cs 复制代码
  public static class ScreenCaptureHelper
  {
      // 声明 Windows API 函数
      [DllImport("user32.dll")]
      [return: MarshalAs(UnmanagedType.Bool)]
      private static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect);

      [DllImport("user32.dll")]
      private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

      [DllImport("user32.dll")]
      private static extern IntPtr GetForegroundWindow();

      [DllImport("user32.dll")]
      private static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);

      // 定义 RECT 结构体
      [StructLayout(LayoutKind.Sequential)]
      public struct RECT
      {
          public int Left;
          public int Top;
          public int Right;
          public int Bottom;
      }

      /// <summary>
      /// 捕获屏幕指定区域并保存为图片文件
      /// </summary>
      public static bool CaptureAndSave(int x, int y, int width, int height,
                                        string filePath, ImageFormat format = null)
      {
          if (format == null) format = ImageFormat.Png;

          try
          {
              using (Bitmap bitmap = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
              {
                  using (Graphics graphics = Graphics.FromImage(bitmap))
                  {
                      graphics.CopyFromScreen(x, y, 0, 0, new System.Drawing.Size(width, height));
                  }

                  string directory = Path.GetDirectoryName(filePath);
                  if (!string.IsNullOrEmpty(directory))
                  {
                      Directory.CreateDirectory(directory);
                  }

                  bitmap.Save(filePath, format);
              }

              return true;
          }
          catch (Exception ex)
          {
              MessageBox.Show($"截图失败: {ex.Message}", "错误",
                  MessageBoxButtons.OK, MessageBoxIcon.Error);
              return false;
          }
      }

      /// <summary>
      /// 通过窗口标题查找并捕获窗口
      /// </summary>
      /// <param name="windowTitle">窗口标题(部分匹配)</param>
      /// <param name="filePath">保存路径</param>
      /// <param name="format">图片格式</param>
      /// <returns>是否成功</returns>
      public static bool CaptureWindowByTitle(string windowTitle, string filePath, ImageFormat format = null)
      {
          // 查找窗口句柄
          IntPtr hWnd = FindWindow(null, windowTitle);

          if (hWnd == IntPtr.Zero)
          {
              MessageBox.Show($"未找到标题为 '{windowTitle}' 的窗口", "提示",
                  MessageBoxButtons.OK, MessageBoxIcon.Warning);
              return false;
          }

          return CaptureWindowByHandle(hWnd, filePath, format);
      }

      /// <summary>
      /// 通过窗口句柄捕获窗口
      /// </summary>
      public static bool CaptureWindowByHandle(IntPtr hWnd, string filePath, ImageFormat format = null)
      {
          if (hWnd == IntPtr.Zero)
          {
              MessageBox.Show("窗口句柄无效", "错误",
                  MessageBoxButtons.OK, MessageBoxIcon.Error);
              return false;
          }

          // 获取窗口矩形区域
          RECT rect = new RECT();
          if (!GetWindowRect(hWnd, ref rect))
          {
              MessageBox.Show("获取窗口位置失败", "错误",
                  MessageBoxButtons.OK, MessageBoxIcon.Error);
              return false;
          }

          int width = rect.Right - rect.Left;
          int height = rect.Bottom - rect.Top;

          if (width <= 0 || height <= 0)
          {
              MessageBox.Show("窗口尺寸无效", "错误",
                  MessageBoxButtons.OK, MessageBoxIcon.Error);
              return false;
          }

          return CaptureAndSave(rect.Left, rect.Top, width, height, filePath, format);
      }

      /// <summary>
      /// 捕获当前活动窗口
      /// </summary>
      public static bool CaptureActiveWindow(string filePath, ImageFormat format = null)
      {
          IntPtr hWnd = GetForegroundWindow();

          if (hWnd == IntPtr.Zero)
          {
              MessageBox.Show("未找到活动窗口", "提示",
                  MessageBoxButtons.OK, MessageBoxIcon.Warning);
              return false;
          }

          return CaptureWindowByHandle(hWnd, filePath, format);
      }

      /// <summary>
      /// 获取当前活动窗口的标题
      /// </summary>
      public static string GetActiveWindowTitle()
      {
          IntPtr hWnd = GetForegroundWindow();
          if (hWnd == IntPtr.Zero) return string.Empty;

          System.Text.StringBuilder sb = new System.Text.StringBuilder(256);
          GetWindowText(hWnd, sb, sb.Capacity);
          return sb.ToString();
      }
  }

第二步 Halcon程序:

需要先根据单词的字母进行训练,我选取了几个单词,确保这些单词字母组合起来涵盖了26个字母。

编写第一个 Halcon程序 ----训练程序

训练这些字母,生成训练文件和分类器:

个人理解:

训练文件保存的是每一个字母和对应的图像特征;

分类器里面有图像特征到字母映射关系,这样就可以实现:传入一个图像特征,依赖分类器返回对应的字符;

这里参考了Halcon例程:

我贴出来我的halcon代码供参考:

Matlab 复制代码
* 
* OCR (Training)
* 
dev_close_window ()
dev_update_off ()
* Train Font 'arial'
FontName := 'arial'
FontImages := ['acknowledge','browse','gangster','jumble','laziness','naked','overall','pacify','quiver','theft','xerox']
NumImages := |FontImages|
read_image (Image,'D:/TheCodeWrittenByOneselfOrFromOthers/HalconOcr/TrainingImage/'+FontImages[0]+'.png')
get_image_size (Image, Width, Height)
dev_open_window (0, 0, Width / 2, Height / 2, 'black', WindowID)
set_display_font (WindowID, 16, 'mono', 'true', 'false')
dev_display (Image)
dev_update_off ()
* ---------- Characters to be recognized ----------

* for I := 0 to 25 by 1
*     Char[I] := chr(round(I + ord('A')))
* endfor
* for I := 0 to 25 by 1
*     Char[I + 26] := chr(round(I + ord('a')))
* endfor
* for I := 0 to 9 by 1
*     Char[I + 52] := chr(round(I + ord('0')))
* endfor

* ---------- Segment characters ----------
dev_set_check ('~give_error')
delete_file (FontName + '.trf')
dev_set_check ('give_error')
gen_empty_obj (TrainImage)
gen_empty_obj (TrainRegion)
dev_set_colored (12)
dev_set_draw ('margin')
for I := 1 to NumImages by 1
    read_image (Image,'D:/TheCodeWrittenByOneselfOrFromOthers/HalconOcr/TrainingImage/'+FontImages[I-1]+'.png')
    rgb1_to_gray (Image, GrayImage)
    Char := []
    for i := 0 to strlen(FontImages[I-1])-1 by 1
        tuple_substr(FontImages[I-1], i, i, SubChar)
    Char := [Char, SubChar]
    endfor
    NumChar := |Char|
  * 2. 提取字母区域 - 使用动态阈值
    threshold (GrayImage, Region, 0, 150)
    connection (Region, ConnectedRegions)
    select_shape (ConnectedRegions, Letters, 'area', 'and', 30, 10000)


    * 先按Column排序
    sort_region (Letters, LettersSorted, 'first_point', 'true', 'column')

    * 获取排序后的中心坐标
    area_center (LettersSorted, Area, Row, Column)

    * 遍历合并相邻区域(Column相差5以内)
    gen_empty_region (MergedRegions)
    select_obj (LettersSorted, CurrentRegion, 1)
    CurrentCol := Column[0]

    for i := 2 to |Column| by 1
        select_obj (LettersSorted, NextRegion, i)
        if (Column[i-1] - CurrentCol <= 5)
            union2 (CurrentRegion, NextRegion, CurrentRegion)
        else
            concat_obj (MergedRegions, CurrentRegion, MergedRegions)
        CurrentRegion := NextRegion
        CurrentCol := Column[i-1]
        endif
    endfor
    concat_obj (MergedRegions, CurrentRegion, MergedRegions)
    remove_obj (MergedRegions, MergedRegionsWithoutFirst, 1)

    * 重新连通并筛选
*     connection (MergedRegions, LettersConnected)
    select_shape (MergedRegionsWithoutFirst, LettersFinal, 'area', 'and', 30, 5000)
    sort_region (MergedRegionsWithoutFirst, Characters, 'first_point', 'true', 'column')

    count_obj (Characters, Num)
    if (Num == NumChar)
        concat_obj (TrainImage, GrayImage, TrainImage)
        concat_obj (TrainRegion, Characters, TrainRegion)
    endif
    dev_display (Image)
    dev_display (Characters)
    disp_message (WindowID, 'Segment characters', 'window', 12, 12, 'black', 'true')
    append_ocr_trainf (Characters, GrayImage, Char, FontName + '.trf')
endfor
count_obj (TrainImage, NumImages)
disp_continue_message (WindowID, 'black', 'true')
stop ()
* ---------- Training of the OCR classifier ---------
dev_display (Image)
disp_message (WindowID, 'Train the font classifier', 'window', 12, 12, 'black', 'true')
Char := []
for I := 0 to 25 by 1
    Char[I] := chr(round(I + ord('a')))
endfor
create_ocr_class_mlp (8, 10, 'constant', 'default', Char, 10, 'none', 10, 42, OCRHandle)
trainf_ocr_class_mlp (OCRHandle, FontName + '.trf', 200, 1, 0.01, Error, ErrorLog)
disp_continue_message (WindowID, 'black', 'true')
stop ()
* Check for correct classification
* dev_set_color ('green')
* for I := 1 to NumImages by 1
*     select_obj (TrainImage, Image, I)
*     copy_obj (TrainRegion, Characters, 1 + (I - 1) * NumChar, NumChar)
*     dev_clear_window ()
*     dev_display (Image)
*     dev_set_color ('green')
*     dev_display (Characters)
*     disp_message (WindowID, 'Check result', 'window', 12, 12, 'black', 'true')
*     do_ocr_multi_class_mlp (Characters, Image, OCRHandle, RecChar, Confidence)
*     Char := []
*     for i := 0 to strlen(FontImages[I-1])-1 by 1
*         tuple_substr(FontImages[I-1], i, i, SubChar)
*     Char := [Char, SubChar]
*     endfor
*     CheckDiff := ord(Char) [!=] ord(RecChar)
*     select_mask_obj (Characters, WrongChar, CheckDiff)
*     dev_set_color ('red')
*     dev_display (WrongChar)
*     disp_continue_message (WindowID, 'black', 'true')
*     stop ()
* endfor
* ---------- Write OCR classifier ---------
write_ocr_class_mlp (OCRHandle, FontName)

编写第二个 Halcon程序 ----识别程序

就是依靠分类器进行识别字母的程序:

cs 复制代码
* 关闭窗口更新
dev_update_off ()

* 1. 读取图像
read_image (Image, 'D:/TheCodeWrittenByOneselfOrFromOthers/HalconOcr/Screenshot_20260619_154428.png')
rgb1_to_gray (Image, GrayImage)
get_image_size (Image, Width, Height)
dev_close_window ()
dev_open_window_fit_image (Image, 0, 0, -1, -1, WindowHandle)
set_display_font (WindowHandle, 16, 'mono', 'true', 'false')
dev_display (GrayImage)

* 2. 提取字母区域 - 使用动态阈值
threshold (GrayImage, Region, 0, 150)
connection (Region, ConnectedRegions)
select_shape (ConnectedRegions, Letters, 'area', 'and', 30, 5000)


* 先按Column排序
sort_region (Letters, LettersSorted, 'first_point', 'true', 'column')

* 获取排序后的中心坐标
area_center (LettersSorted, Area, Row, Column)

* 遍历合并相邻区域(Column相差5以内)
gen_empty_region (MergedRegions)
select_obj (LettersSorted, CurrentRegion, 1)
CurrentCol := Column[0]

for i := 2 to |Column| by 1
    select_obj (LettersSorted, NextRegion, i)
    if (Column[i-1] - CurrentCol <= 5)
        union2 (CurrentRegion, NextRegion, CurrentRegion)
    else
        concat_obj (MergedRegions, CurrentRegion, MergedRegions)
        CurrentRegion := NextRegion
        CurrentCol := Column[i-1]
    endif
endfor
concat_obj (MergedRegions, CurrentRegion, MergedRegions)
remove_obj (MergedRegions, MergedRegionsWithoutFirst, 1)


select_shape (MergedRegionsWithoutFirst, LettersFinal, 'area', 'and', 30, 5000)
sort_region (MergedRegionsWithoutFirst, LettersSortedFinal, 'first_point', 'true', 'column')

* 3. 使用内置OCR
read_ocr_class_mlp ('D:/TheCodeWrittenByOneselfOrFromOthers/HalconOcr/arial.omc', OCRHandle)
do_ocr_multi_class_mlp (LettersSortedFinal, GrayImage, OCRHandle, Class, Confidence)

* 4. 将Class数组按照顺序组合成单词字符串
WordString := ''
for i := 0 to |Class| - 1 by 1
    WordString := WordString + Class[i]
endfor

* 5. 释放OCR资源
clear_ocr_class_mlp (OCRHandle)

就是打开刚刚保存的分类器(omc文件),传入每一个字母的特征,返回对应的字符;

效果:

接下来导出C#代码,稍微改动一下传入图片地址,传出单词字符串:

cs 复制代码
internal class FindWord
{
    public static void action(string path,out string word)
    {

        word = string.Empty;
        // Stack for temporary objects 
        HObject[] OTemp = new HObject[20];

        // Local iconic variables 

        HObject ho_Image, ho_GrayImage, ho_Region;
        HObject ho_ConnectedRegions, ho_Letters, ho_LettersSorted;
        HObject ho_MergedRegions, ho_CurrentRegion, ho_NextRegion = null;
        HObject ho_MergedRegionsWithoutFirst, ho_LettersFinal, ho_LettersSortedFinal;

        // Local control variables 

        HTuple hv_Width = new HTuple(), hv_Height = new HTuple();
        HTuple hv_WindowHandle = new HTuple(), hv_Area = new HTuple();
        HTuple hv_Row = new HTuple(), hv_Column = new HTuple();
        HTuple hv_CurrentCol = new HTuple(), hv_i = new HTuple();
        HTuple hv_OCRHandle = new HTuple(), hv_Class = new HTuple();
        HTuple hv_Confidence = new HTuple(), hv_WordString = new HTuple();
        // Initialize local and output iconic variables 
        HOperatorSet.GenEmptyObj(out ho_Image);
        HOperatorSet.GenEmptyObj(out ho_GrayImage);
        HOperatorSet.GenEmptyObj(out ho_Region);
        HOperatorSet.GenEmptyObj(out ho_ConnectedRegions);
        HOperatorSet.GenEmptyObj(out ho_Letters);
        HOperatorSet.GenEmptyObj(out ho_LettersSorted);
        HOperatorSet.GenEmptyObj(out ho_MergedRegions);
        HOperatorSet.GenEmptyObj(out ho_CurrentRegion);
        HOperatorSet.GenEmptyObj(out ho_NextRegion);
        HOperatorSet.GenEmptyObj(out ho_MergedRegionsWithoutFirst);
        HOperatorSet.GenEmptyObj(out ho_LettersFinal);
        HOperatorSet.GenEmptyObj(out ho_LettersSortedFinal);
        try
        {
           

            //1. 读取图像
            ho_Image.Dispose();
            HOperatorSet.ReadImage(out ho_Image, path);
            ho_GrayImage.Dispose();
            HOperatorSet.Rgb1ToGray(ho_Image, out ho_GrayImage);
            hv_Width.Dispose(); hv_Height.Dispose();
            HOperatorSet.GetImageSize(ho_Image, out hv_Width, out hv_Height);
            if (HDevWindowStack.IsOpen())
            {
                HOperatorSet.CloseWindow(HDevWindowStack.Pop());
            }
            hv_WindowHandle.Dispose();
          
         

            //2. 提取字母区域 - 使用动态阈值
            ho_Region.Dispose();
            HOperatorSet.Threshold(ho_GrayImage, out ho_Region, 0, 150);
            ho_ConnectedRegions.Dispose();
            HOperatorSet.Connection(ho_Region, out ho_ConnectedRegions);
            ho_Letters.Dispose();
            HOperatorSet.SelectShape(ho_ConnectedRegions, out ho_Letters, "area", "and",
                30, 5000);


            //先按Column排序
            ho_LettersSorted.Dispose();
            HOperatorSet.SortRegion(ho_Letters, out ho_LettersSorted, "first_point", "true",
                "column");

            //获取排序后的中心坐标
            hv_Area.Dispose(); hv_Row.Dispose(); hv_Column.Dispose();
            HOperatorSet.AreaCenter(ho_LettersSorted, out hv_Area, out hv_Row, out hv_Column);

            //遍历合并相邻区域(Column相差5以内)
            ho_MergedRegions.Dispose();
            HOperatorSet.GenEmptyRegion(out ho_MergedRegions);
            ho_CurrentRegion.Dispose();
            HOperatorSet.SelectObj(ho_LettersSorted, out ho_CurrentRegion, 1);
            hv_CurrentCol.Dispose();
            using (HDevDisposeHelper dh = new HDevDisposeHelper())
            {
                hv_CurrentCol = hv_Column.TupleSelect(
                    0);
            }

            for (hv_i = 2; (int)hv_i <= (int)(new HTuple(hv_Column.TupleLength())); hv_i = (int)hv_i + 1)
            {
                ho_NextRegion.Dispose();
                HOperatorSet.SelectObj(ho_LettersSorted, out ho_NextRegion, hv_i);
                if ((int)(new HTuple((((hv_Column.TupleSelect(hv_i - 1)) - hv_CurrentCol)).TupleLessEqual(
                    5))) != 0)
                {
                    {
                        HObject ExpTmpOutVar_0;
                        HOperatorSet.Union2(ho_CurrentRegion, ho_NextRegion, out ExpTmpOutVar_0
                            );
                        ho_CurrentRegion.Dispose();
                        ho_CurrentRegion = ExpTmpOutVar_0;
                    }
                }
                else
                {
                    {
                        HObject ExpTmpOutVar_0;
                        HOperatorSet.ConcatObj(ho_MergedRegions, ho_CurrentRegion, out ExpTmpOutVar_0
                            );
                        ho_MergedRegions.Dispose();
                        ho_MergedRegions = ExpTmpOutVar_0;
                    }
                    ho_CurrentRegion.Dispose();
                    ho_CurrentRegion = new HObject(ho_NextRegion);
                    hv_CurrentCol.Dispose();
                    using (HDevDisposeHelper dh = new HDevDisposeHelper())
                    {
                        hv_CurrentCol = hv_Column.TupleSelect(
                            hv_i - 1);
                    }
                }
            }
            {
                HObject ExpTmpOutVar_0;
                HOperatorSet.ConcatObj(ho_MergedRegions, ho_CurrentRegion, out ExpTmpOutVar_0
                    );
                ho_MergedRegions.Dispose();
                ho_MergedRegions = ExpTmpOutVar_0;
            }
            ho_MergedRegionsWithoutFirst.Dispose();
            HOperatorSet.RemoveObj(ho_MergedRegions, out ho_MergedRegionsWithoutFirst,
                1);


            ho_LettersFinal.Dispose();
            HOperatorSet.SelectShape(ho_MergedRegionsWithoutFirst, out ho_LettersFinal,
                "area", "and", 30, 5000);
            ho_LettersSortedFinal.Dispose();
            HOperatorSet.SortRegion(ho_MergedRegionsWithoutFirst, out ho_LettersSortedFinal,
                "first_point", "true", "column");

            //3. 使用内置OCR
            hv_OCRHandle.Dispose();
            HOperatorSet.ReadOcrClassMlp("D:/TheCodeWrittenByOneselfOrFromOthers/HalconOcr/arial.omc",
                out hv_OCRHandle);
            hv_Class.Dispose(); hv_Confidence.Dispose();
            HOperatorSet.DoOcrMultiClassMlp(ho_LettersSortedFinal, ho_GrayImage, hv_OCRHandle,
                out hv_Class, out hv_Confidence);

            //4. 将Class数组按照顺序组合成单词字符串
            hv_WordString.Dispose();
            hv_WordString = "";
            for (hv_i = 0; (int)hv_i <= (int)((new HTuple(hv_Class.TupleLength())) - 1); hv_i = (int)hv_i + 1)
            {
                using (HDevDisposeHelper dh = new HDevDisposeHelper())
                {
                    {
                        HTuple
                          ExpTmpLocalVar_WordString = hv_WordString + (hv_Class.TupleSelect(
                            hv_i));
                        hv_WordString.Dispose();
                        hv_WordString = ExpTmpLocalVar_WordString;
                    }
                }
            }
            word = hv_WordString;
            //5. 释放OCR资源
            HOperatorSet.ClearOcrClassMlp(hv_OCRHandle);
        }
        catch (HalconException HDevExpDefaultException)
        {
            ho_Image.Dispose();
            ho_GrayImage.Dispose();
            ho_Region.Dispose();
            ho_ConnectedRegions.Dispose();
            ho_Letters.Dispose();
            ho_LettersSorted.Dispose();
            ho_MergedRegions.Dispose();
            ho_CurrentRegion.Dispose();
            ho_NextRegion.Dispose();
            ho_MergedRegionsWithoutFirst.Dispose();
            ho_LettersFinal.Dispose();
            ho_LettersSortedFinal.Dispose();

            hv_Width.Dispose();
            hv_Height.Dispose();
            hv_WindowHandle.Dispose();
            hv_Area.Dispose();
            hv_Row.Dispose();
            hv_Column.Dispose();
            hv_CurrentCol.Dispose();
            hv_i.Dispose();
            hv_OCRHandle.Dispose();
            hv_Class.Dispose();
            hv_Confidence.Dispose();
            hv_WordString.Dispose();

            throw HDevExpDefaultException;
        }
        ho_Image.Dispose();
        ho_GrayImage.Dispose();
        ho_Region.Dispose();
        ho_ConnectedRegions.Dispose();
        ho_Letters.Dispose();
        ho_LettersSorted.Dispose();
        ho_MergedRegions.Dispose();
        ho_CurrentRegion.Dispose();
        ho_NextRegion.Dispose();
        ho_MergedRegionsWithoutFirst.Dispose();
        ho_LettersFinal.Dispose();
        ho_LettersSortedFinal.Dispose();

        hv_Width.Dispose();
        hv_Height.Dispose();
        hv_WindowHandle.Dispose();
        hv_Area.Dispose();
        hv_Row.Dispose();
        hv_Column.Dispose();
        hv_CurrentCol.Dispose();
        hv_i.Dispose();
        hv_OCRHandle.Dispose();
        hv_Class.Dispose();
        hv_Confidence.Dispose();
        hv_WordString.Dispose();

    }
}

第三步 访问Http,进行翻译:

这里的网址是我常用的一个网址,只是一个例程,说明了可以实现对应的需求:

cs 复制代码
  public async void Translate(string word)
 {
     List<string> outputLines = new List<string>();
     
      // 生成格式:2026-06-19.txt
     var fileName = DateTime.Now.ToString("yyyy-MM-dd") + ".txt";
     var outputFilePath  = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
     if (!File.Exists(outputFilePath))
     {
        //StreamWriter 自动创建
         //File.Create(outputFilePath);
     }

     using (HttpClient client = new HttpClient())
     {
         client.DefaultRequestHeaders.Add("User-Agent",
             "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");


         if (string.IsNullOrWhiteSpace(word))
         {
             return;
         }

         string trimmedWord = word.Trim();

         string url = $"https://www.dictool.com/w/{trimmedWord}";
         string meaning = string.Empty;
         string etymology = string.Empty;

         try
         {
             using (HttpResponseMessage response = await client.GetAsync(url))
             {
                 response.EnsureSuccessStatusCode();
                 string htmlContent = await response.Content.ReadAsStringAsync();

                 // 解析HTML
                 HtmlDocument doc = new HtmlDocument();
                 doc.LoadHtml(htmlContent);

                 // 获取词义 (id: yd-word-meaning)
                 HtmlNode meaningNode = doc.GetElementbyId("yd-word-meaning");
                 if (meaningNode != null)
                 {
                     meaning = meaningNode.InnerText.Trim();
                     this.txtTranslate.Text = meaning;
                 }

                 // 获取中文词源 (id: yd-ciyuan)
                 HtmlNode etymologyNode = doc.GetElementbyId("yd-ciyuan");
                 if (etymologyNode != null)
                 {
                     etymology = etymologyNode.InnerText.Trim();
                     // 移除开头的单词和空格(如 "mermaid 美人鱼" 中的 "mermaid ")
                     if (etymology.StartsWith(trimmedWord + " "))
                     {
                         etymology = etymology.Substring(trimmedWord.Length + 1);
                         this.txtSource.Text = etymology;
                     }
                 }

                 // 如果未找到词义,记录一个占位符
                 if (string.IsNullOrEmpty(meaning))
                 {
                     meaning = "[词义未找到]";
                 }

                

             }
         }
         catch (HttpRequestException ex)
         {
             UpdateStatus($"  请求失败: {ex.Message}");
             meaning = "[请求失败]";
             etymology = string.Empty;
         }
         catch (Exception ex)
         {
             UpdateStatus($"  处理失败: {ex.Message}");
             meaning = "[处理出错]";
             etymology = string.Empty;
         }
         // 按照格式添加到输出行
         outputLines.Add($"{trimmedWord}");
         outputLines.Add($"{meaning}");
         outputLines.Add(string.IsNullOrEmpty(etymology) ? "" : etymology);

        

     }
     using (StreamWriter writer = new StreamWriter(outputFilePath, true, Encoding.UTF8))
     {
         foreach (string line in outputLines)
         {
             await writer.WriteLineAsync(line);
         }
     }

 }

效果: