Emails Extractor






4.85/5 (4投票s)
该程序从文本文件中提取一组人的电子邮件和数据(姓名、电话和性别),并将它们整理到 Datagridview 中,然后可以保存为 .csv 文件,该文件包含可以导入到雅虎邮箱账户中的联系人信息。
引言
许多公司/大学的网站都包含该公司每位员工的列表,此列表包含每位员工的姓名、电子邮件、电话甚至地址。作为寻求与该公司/大学合作的代理人,您需要将这些人添加到您的雅虎列表中,以便联系他们或询问任何可能对您有帮助的信息,无论他们是否对您的产品感兴趣。
但是,您如何将他们添加到您的电子邮件列表中呢?这需要大量的时间和精力,而您可能没有。因为您必须输入该人的姓名(名、中间名和姓)、电子邮件、电话甚至性别。是的,没错,如果您的产品仅针对女性或仅针对男性,那么您将需要更多时间来弄清楚这个人是男性还是女性!!!
Email Extractor 是我在遇到这个问题时开始的一个简单程序。通过利用互联网上已有的正则表达式,该程序可以扫描文本字段,查找人员并提取他们的姓名、电话、电子邮件,甚至性别以及您在开始与此人联系时可能需要了解的许多其他信息。
该程序不仅可以提取此人的数据,还可以将其数据整理到 DataGridView 控件中,该控件包含关于该人的每条可用详细信息的行,而且它还可以将此数据保存为雅虎邮件接受的导入文件类型“.CSV”文件,以便提取联系人并将其插入您的邮件中。它还允许您通过定义自己的正则表达式来指定您真正需要的数据类型,您可以将这些表达式保存在数据库中,并指定一个组名以便以后使用。
通过执行正则表达式来处理数据也是可行的,以维护和准备混合数据,以便以编程方式提取属于一个人的特定数据。
从文本池中提取属于一个人的数据几乎是不可能的,因此属于一个人的每个数据块必须用两个以上的空格“\n”与其他数据分开。这将使代码能够将数据分割成块,每个块属于特定的人,您还可以指定一个用于文本块分割的正则表达式。
背景
雅虎邮件是互联网上最受欢迎的邮件提供商之一,它提供了许多处理电子邮件的工具。雅虎还提供了一个重要的工具——导入联系人,该工具可以从其他邮件联系人(雅虎、Gmail 等)或 CSV 文件等文件中导入联系人。
如何将逗号分隔值文件(.CSV)添加到您的雅虎邮箱联系人?
1. 打开您的雅虎邮箱。
2. 点击“联系人”。
3. 添加一个新的联系人列表。
4. 点击导入联系人。
5. 选择“其他”,以指定您将从其他方法导入联系人。
6. 对于步骤 1,取消选择“另一个雅虎账户”。
7. 选择“桌面电子邮件程序(Outlook、Apple Mail 等)”。
8. 浏览到需要添加的特定 .CSV 文件。
9. 点击“继续”。

什么是正则表达式?
正则表达式是一组指定模式的字符。术语“regular”与高纤维饮食无关。它源自用于描述语法和形式语言的术语。
当您想搜索包含特定模式的特定文本行时,将使用正则表达式。大多数 UNIX 实用程序一次处理一个 ASCII 文件中的一行。正则表达式在单个行上搜索模式,而不是搜索从一行开始并在另一行结束的模式。
搜索特定单词或字符串很简单。几乎所有计算机系统上的编辑器都可以做到这一点。正则表达式更强大、更灵活。您可以搜索特定大小的单词。您可以搜索包含四个或更多元音字母且以“s”结尾的单词。数字、标点符号,应有尽有,正则表达式都可以找到。程序找到它之后会发生什么,这是另一回事。有些只是搜索模式。有些打印出包含模式的行。编辑器可以将字符串替换为新模式。这取决于实用程序。
正则表达式会使人们感到困惑,因为它们看起来很像 shell 使用的文件匹配模式。它们的作用也几乎相同。方括号相似,星号的作用类似于正则表达式中的星号,但又不完全相同。特别是,Bourne shell、C shell、find 和 cpio 使用文件名匹配模式,而不是正则表达式。
请记住,shell 元字符在 shell 将参数传递给程序之前会被展开。为了防止这种展开,在从 shell 作为选项传递时,正则表达式中的特殊字符必须被引用。您已经知道如何做到这一点,因为我在上个月的教程中涵盖了这一主题。
您可以在此处了解有关 Regex Expressions 的更多信息。
搜索人员
这是该程序的主要方法,它首先通过使用包含 2 个空格的规则来分割从主 RichtextBox (MixedEmailSRichtextBox) 派生的文本数据(因为不同人的不同数据必须前面有空格 ([\n]{2}),然后将其导入 GetPeopleFromBlock 方法,该方法执行不同的正则表达式来提取特定人员的数据,如电子邮件、姓名、电话号码……。
private void SearchForPeople() { WrongRegex.Clear(); string Block = MixedEmailSRichtextBox.Text; //control contains text data for different people string Regex = @"(\n){2,}(\w)*?"; if (BlockSplitingText.Enabled == true && BlockSplitingText.Text.Length > 0) Regex = BlockSplitingText.Text; if (BlockSplitingText.Enabled == true && BlockSplitingText.Text.Length > 0) Regex = BlockSplitingText.Text; List<Person> People = GetPeopleFromBlock(Block, Regex); this.EmailsdataGridView.DataSource = PeopleTable(People, GetAdditionalRegexfrom(RegexDataGridView)); } static List<Person> GetPeopleFromBlock(string Data,string RegexSplitting) { List<Person> MyPData = new List<Person>(); try { string[] M = Regex.Split(Data, RegexSplitting); ; foreach (var Pdata in M) { if (Pdata.Length > 20) { Person newPerson = new Person(Pdata); MyPData.Add(newPerson); } } } catch (Exception e) { MessageBox.Show(e.Message); } return MyPData; }
人员表函数
此方法导入已加载人员及其数据块的列表,然后执行 GetPersonFromBlock 方法,该方法执行类中已有的正则表达式以及用户定义的正则表达式“AdditionalRegex”。
private DataTable PeopleTable(List<Person> People, List<AdditionalRegexExpression> AdditionalRegex) { DataTable PeopleTable = new DataTable(); AddCoulmnsForPeopleTable(PeopleTable); for (int i = 0; i < People.Count; i++) { DataRow NewPerson = PeopleTable.NewRow(); List<PersonData> newPErsonData =People[i].GetPersonFromBlock(AdditionalRegex,NoDataWithoutEmail); for (int P = 0; P < newPErsonData.Count; P++) { if (newPErsonData[P].dataType == null) continue; string ColumnName = newPErsonData[P].dataType.ToString().Replace('_', ' '); if(ColumnName=="Email Address") ColumnName="E-mail Address"; if (ColumnName == "Email Display Name") ColumnName = "E-mail Display Name"; string AlreadyExistedData = NewPerson[ColumnName].ToString();//if there are existed data already NewPerson[ColumnName] = AlreadyExistedData + CleanString(newPErsonData[P].Data.ToString());//in case if data contains ',' this will confuse excel program } if(newPErsonData.Count>0) PeopleTable.Rows.Add(NewPerson); } return PeopleTable; } static string CleanString(string X) { return Regex.Replace(X,RegexExpressionCollection.CleanStringForExcel," "); }
.CSV 文件导出问题解决方案
如您在前面的代码中注意到的,有一个名为 Cleanstring 的方法,此方法起着重要作用,因为它会删除任何可能使 .csv 读取程序(excel 等)在排序行中的单元格值时感到困惑的符号。它执行一个特定的正则表达式来删除逗号(.csv 依赖逗号作为单元格分隔符)和其他符号。
public static string CleanStringForExcel { get { return @"\n|\s|\t|\f|\v|\e|,"; } }
类 Person
此类在构造函数中获取块数据,并根据调用它的方法执行正则表达式。它还包含一个枚举作为数据类型 enum,可以指定某些列
class Person { public enum DataType { First_Name, Middle_Name, Last_Name, Department, Job_Title, Business_State, Home_Postal_Code, Business_Phone, Home_Phone, Birthday, Email_Address, Email_Display_Name, Gender,Language, Notes, Web_Page, Business_Fax } string DataBlock { get; set; } public Person(string DataBlock) //gets the data which will be use in data exctraction { this.DataBlock = DataBlock; } public PersonData getEmailPersonFromBlock() { List<string> Emails = new List<string>(); Regex emailRegex = new Regex(RegexExpressionCollection.Email, RegexOptions.IgnoreCase); //find items that matches with our pattern MatchCollection emailMatches = emailRegex.Matches(this.DataBlock); foreach (Match emailMatch in emailMatches) { Emails.Add(emailMatch.Value); } //retutn OnlyOneEmail if(emailMatches.Count==0) return new PersonData(DataType.Email_Address, ""); return new PersonData(DataType.Email_Address, Emails[0]); } public PersonData Gender() { string Name = GetPersonNameFromBlock()[0].Data;//just First Name; return Gender(Name); } // other Regex performing methods //. //. //. //.
Person 类包含 GetPersonFromBlock,它一次执行所有可用的正则表达式,并将所需数据提取为 Person 数据类型。
public List<PersonData> GetPersonFromBlock(List<AdditionalRegexExpression> AddationalRegex, bool EmailIsMainData) { PersonData PEmailData = getEmailPersonFromBlock(); if (!Regex.IsMatch( PEmailData.Data.ToString(),RegexExpressionCollection.Email)&& EmailIsMainData)//if this doesn't match Email Catagory and Email is Needed return empty return new List<PersonData>(); List<PersonData> PNameData = new List<PersonData>(); if (PEmailData.Data != "") PNameData = GetPersonNameFromBlock(PEmailData.Data.ToString()); //if Name is Empty Take Email first part as name PersonData PPhone = GetPhonePersonFormBlock(); PersonData PbirthDaY = GetPhonePersonBirthDate(); PersonData PEMialDisplayName = new PersonData(); if(PNameData.Count>0) PEMialDisplayName = new PersonData(DataType.Email_Display_Name, PNameData[0].Data); PersonData PPostalCode = GetPersonPostalCodeFromBlocK(); PersonData Gender = new PersonData(); if(PNameData.Count>0) Gender= this.Gender(PNameData[0].Data); PersonData Notes = new PersonData(DataType.Notes,this.DataBlock); //just in Case The Data Was Wrong U have to consider that every thing beside Email is Notes PersonData WebPage = GetPersonWebPageFromBlock(); PersonData Business_Phone = GetPersonBussinessPhone(); PersonData Lang = GetPersonLanguage(); PersonData Business_Fax = getPersonBusinessFax(); PersonData Business_State = GetPersonBusiness_State(); PersonData Jop_Tile = new PersonData(DataType.Job_Title, Business_State.Data); PersonData Department = GetPersonDepartment(); List<PersonData> AddaationalData = GetAddationalData(AddationalRegex); List<PersonData> TotalData = new List<PersonData>(); TotalData.AddRange(PNameData); TotalData.AddRange(new List<PersonData> { Business_Fax, PEmailData, PPhone, PbirthDaY, PEMialDisplayName, PPostalCode, Gender, Notes, WebPage, Business_Phone, Lang, Business_State, Jop_Tile, Department }); TotalData.AddRange(AddaationalData);//it must be at last becuase it maybe erase data taken by another regex return TotalData; } private string[] StringSplittededBySpace(string Data) { return Regex.Split(Data, @"[\s|\.|\,|\-]+?"); } static List<int> WrongRegex = new List<int>();
这非常重要,因为它执行已添加到由用户定义的 Regex Expression 表中的正则表达式。
public List<PersonData> GetAddationalData(List<AdditionalRegexExpression> AdditionalRegex) { List<PersonData> AdditionalData = new List<PersonData>(); for (int i = 0; i < AdditionalRegex.Count; i++) { if (WrongRegex.Contains(i)) continue; try { MatchCollection RegexMatches = Regex.Matches(this.DataBlock, AdditionalRegex[i].RgExp.ToString()); int count = 0; if (AdditionalRegex[i].NumOfResults == 0) { count = RegexMatches.Count; } else count = AdditionalRegex[i].NumOfResults; if (RegexMatches.Count < count) count = RegexMatches.Count; string Data = ""; for (int c = 0; c < count; c++) { Data += RegexMatches[c].Value.ToString(); } string targetColumn = AdditionalRegex[i].RgNam; AdditionalData.Add(new PersonData(targetColumn, Data)); } catch (Exception e) { WrongRegex.Add(i); throw e; } } return AdditionalData; } }
类 Regex Expression Type
当用户选择在 Regex Expression Table 中添加新的 Regex Expression 时,必须定义提取数据将被存储的目标列以及结果的数量。
class AdditionalRegexExpression { public AdditionalRegexExpression(string Name, string Regex,int NumberOfResults) { this.RgNam = Name; this.RgExp = Regex; this.NumOfResults = NumberOfResults; } public string RgExp { get; set; } // The Regex Expression public string RgColumnTarget { get; set; } //The targeted column public int NumOfResults { get; set; } //the number of Results as defined by user }
在 Rich textbox 中打开文本文件:
此方法用于将混合数据从文本文件加载到 Richtextbox 控件中,此混合数据将用于提取不同人员的数据。区分与特定人员相关的数据与另一人员的数据,并控制提取结果的准确性,取决于:
1) 不同人员的相关数据之间必须有超过 2 个“\n”(空格);这将帮助程序准确地分割这些数据并将其推入数据提取过程。
2) 如果用户输入了其他 Regex,它必须准确并且存在于人员块之间。
3) 数据同质性——对于不同类型的数据必须有一个通用规则——例如,“Tel[:]+(\d+[-])+”用于电话号码,这将避免您编写不同的 Regex 来只获取一种类型的数据,如电子邮件。
using (OpenFileDialog dlgOpen = new OpenFileDialog()) { try { // Available file extensions dlgOpen.Filter = "All files(*.*)|*.*"; // Initial directory dlgOpen.InitialDirectory = "D:"; // OpenFileDialog title dlgOpen.Title = "Open"; // Show OpenFileDialog box if (dlgOpen.ShowDialog() == DialogResult.OK) { // Create new StreamReader StreamReader sr = new StreamReader(dlgOpen.FileName, Encoding.Default); // Get all text from the file string str = sr.ReadToEnd(); // Close the StreamReader sr.Close(); // Show the text in the rich textbox rtbMain MixedEmailSRichtextBox.Text = str; } } catch (Exception errorMsg) { MessageBox.Show(errorMsg.Message); } }
不同数据类型的 Regex
RegexExpressionCollection 类包含一些重要的可用 Regex 表达式,它们将帮助您识别有关此人的重要详细信息。我们同意,这里的所有 Regex 表达式都可以通过添加新的 Regex 表达式并调整目标列来覆盖,以便随心所欲。
1. 传真号码 Regex:
由于传真号码和电话号码的唯一区别是传真号码前面带有“fax”这个词,因此 Regex 搜索“fax”这个词作为区分传真号码和电话号码的通用关键字。
public static string Fax{ get { return @"Fax:\s[+]*([\d+[-]*)+"; }}
2. 2.电话号码 Regex
public static string Phone { get { return @"(Tel|phone|telephone|mobile):\s[+]*([\d+[-]*)+"; } }
3. 网页:
网页有不同的 Regex 表达式,这是我发现的最简单、最准确的。
public static string WebPage{get { return @"(http|https|ftp)\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(:[a-zA-Z0-9]*)?/?([a-zA-Z 0-9\-\._\?\,\'/\\\+&%\$#\=~])*"; } }
4. 出生日期:
public static string BirthDate{get { return @"Fax:\s[+]*([\d+[-]*)+"; } }
5. 电子邮件:
由于电子邮件是您这里最重要的内容,如果此方法没有返回任何值,程序将自动从结果中将其删除,除非您勾选了标有“不要删除无电子邮件结果”的复选框。
public static string Email{get { return @"\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"; } }
6. 性别:
为每个客户猜测性别非常重要,这可以确定此人是否真的对您的产品感兴趣。假设您收到了一份很长的邮寄名单,并且需要编写一个程序来确定名单上的每个人代表男性还是女性,也许是为了在正式信函的称呼中加上“先生”或“女士”。您会怎么做?您首先想到的可能是创建一个包含所有个人姓名及其通常与之关联的性别的表格。然后,对于邮寄名单中的每个姓名,在表格中查找相应的性别,最有可能使用哈希函数来提高速度。
这种方法有两个关键问题。首先,拼写上的差异,无论多么微小,都会导致算法失效。人类可以分辨 Caren、Caryn、Karyn 和 Karin 都是同一个名字的不同拼写,但表格查找算法不行。如果这四种拼写中的一种不在表格中,程序就无法将该名称与性别匹配。其次,将数千个姓名-性别对输入表格既耗时又容易出错。此外,由于拼写变体,您可能永远无法拥有一个完整的列表。如果我告诉您,一个性别检测程序可以用大约 50 行代码和数据编写,那会怎么样?
该程序不是匹配纯文本,而是根据文本中的模式进行匹配。该策略是,如果一个名称匹配某种字母模式,它就必须是某种性别。例如,如果一个名称包含字母序列 ann(大写或小写),它一定是女性(不包括外来名称、英语名称的缩写以及模棱两可的名称)。这条规则可以匹配很多名称,包括 Ann、Anna、Annabelle、Annacarol、Annalisa、Annaliz、Annamarie、Anne、Anne-marie、Annette、Annie……
使用模式匹配方法有许多优点,甚至超出了不必将数千个名称及其相关性别输入表格所需的时间。首先,程序通常可以处理尚未遇到的名称或名称的变体。其次,同样地,键入错误的名称和拼写错误的名称仍然倾向于产生与正确转录相同的性别。第三,程序保证为给定的任何名称返回一个性别。您无法让代码感到困惑。
这些 Regex 集合揭示了 Page "This tour-de-force yiWK program reveals the strengths and weaknesses of the rule-based paradigm and will prove useful for mailing-list programmers” PREDICT GENDER GIVEN A FIRST NAME by Scott Pakin, August 1991 lawker.googlecode.com/svn/fridge/share/pdf/pakin1991.pdf”
·男性姓名 Regex:
public static bool IsMale(string Name) { string[] ArrayOfRegex = { "^[^S].*r[rv]e?y?$" //{ sex = ,"m," } # Barry, Larry, Perry,... ,"^[^G].*v[ei]$" //{ sex = ,"m," } # Clive, Dave, Steve,... ,"^[^AJKLMNP][^o][^eit]*([glrsw]ey|lie)$" //{ sex = ,"m," } # Dewey, Stanley, Wesley,... ,"^[CGJWZ][^o][^dnt]*y$" //{ sex = ,"m," } # Gregory, Jeremy, Zachary,... ,"^.*[Rlr][abo]y$" //{ sex = ,"m," } # Leroy, Murray, Roy,... ,"^.*[GRguw][ae]y?ne$" //{ sex = ,"m," } # Duane, Eugene, Rene,... ,"^[CLMQTV].*[^dl][in]c.*[ey]$" //{ sex = ,"m," } # Lance, Quincy, Vince,... ,"^.*[ay][dl]e$" //{ sex = ,"m," } # Clyde, Kyle, Pascale,... ,"^[^o]*ke$" //{ sex = ,"m," } # Blake, Luke, Mike,... ,"^[^EL].*o(rg?|sh?)?(e|ua)$" //{ sex = ,"m," } # George, Joshua, Theodore,.. ,"^[^JPSWZ].*[denor]n.*y$" //{ sex = ,"m," } # Anthony, Henry, Rodney,... ,"^Br[aou][cd].*[ey]$" //{ sex = ,"m," } # Bradley, Brady, Bruce,... ,"^[ILW][aeg][^ir]*e$" //{ sex = ,"m," } # Ignace, Lee, Wallace,... ,"^[ABEIUY][euz]?[blr][aeiy]$" //{ sex = ,"m," } # Ari, Bela, Ira,... ,"^[ART][^r]*[dhn]e?y$" //{ sex = ,"m," } # Randy, Timothy, Tony,... ,"^.*oi?[mn]e$" //{ sex = ,"m," } # Antoine, Jerome, Tyrone,... ,"^D.*[mnw].*[iy]$" //{ sex = ,"m," } # Danny, Demetri, Dondi,... ,"^[^BG](e[rst]|ha)[^il]*e$" //{ sex = ,"m," } # Pete, Serge, Shane,... }; for (int i = 0; i < ArrayOfRegex.Length; i++) if(Regex.IsMatch(Name,ArrayOfRegex[i])) return true; return false; }
· 女性姓名 Regex
public static string[] Male{get{ return new string[]{ "^[^S].*r[rv]e?y?$" //{ sex = ,"m," } # Barry, Larry, Perry,... ,"^[^G].*v[ei]$" //{ sex = ,"m," } # Clive, Dave, Steve,... ,"^[^AJKLMNP][^o][^eit]*([glrsw]ey|lie)$" //{ sex = ,"m," } # Dewey, Stanley, Wesley,... ,"^[CGJWZ][^o][^dnt]*y$" //{ sex = ,"m," } # Gregory, Jeremy, Zachary,... ,"^.*[Rlr][abo]y$" //{ sex = ,"m," } # Leroy, Murray, Roy,... ,"^.*[GRguw][ae]y?ne$" //{ sex = ,"m," } # Duane, Eugene, Rene,... ,"^[CLMQTV].*[^dl][in]c.*[ey]$" //{ sex = ,"m," } # Lance, Quincy, Vince,... ,"^.*[ay][dl]e$" //{ sex = ,"m," } # Clyde, Kyle, Pascale,... ,"^[^o]*ke$" //{ sex = ,"m," } # Blake, Luke, Mike,... ,"^[^EL].*o(rg?|sh?)?(e|ua)$" //{ sex = ,"m," } # George, Joshua, Theodore,.. ,"^[^JPSWZ].*[denor]n.*y$" //{ sex = ,"m," } # Anthony, Henry, Rodney,... ,"^Br[aou][cd].*[ey]$" //{ sex = ,"m," } # Bradley, Brady, Bruce,... ,"^[ILW][aeg][^ir]*e$" //{ sex = ,"m," } # Ignace, Lee, Wallace,... ,"^[ABEIUY][euz]?[blr][aeiy]$" //{ sex = ,"m," } # Ari, Bela, Ira,... ,"^[ART][^r]*[dhn]e?y$" //{ sex = ,"m," } # Randy, Timothy, Tony,... ,"^.*oi?[mn]e$" //{ sex = ,"m," } # Antoine, Jerome, Tyrone,... ,"^D.*[mnw].*[iy]$" //{ sex = ,"m," } # Danny, Demetri, Dondi,... ,"^[^BG](e[rst]|ha)[^il]*e$" }; //{ sex = ,"m," } # Pete, Serge, Shane,...; }}}
7. 以及其他 Regex 表达式
将 DataGridView 保存为逗号分隔值 (CSV) 文件:
此方法用于将 DataGridView 保存为逗号分隔值文件。它首先使用 StringBuilder 提取列名,然后添加“,”,它将此字符串定义为列值。-.csv 将第一行作为列名,接下来的字符串作为行值。此方法非常简单,准确性非常低,因为如果数据包含“,”,Excel 程序将会感到困惑,结果的顺序将不准确。
private void ExtractDataToCSV(DataGridView dgv) { // Don't save if no data is returned if (dgv.Rows.Count == 0) { return; } StringBuilder sb = new StringBuilder(); // Column headers string columnsHeader = ""; for (int i = 0; i < dgv.Columns.Count; i++) { columnsHeader += dgv.Columns[i].Name + ","; } sb.Append(columnsHeader + Environment.NewLine); // Go through each cell in the datagridview foreach (DataGridViewRow dgvRow in dgv.Rows) { // Make sure it's not an empty row. if (!dgvRow.IsNewRow) { for (int c = 0; c < dgvRow.Cells.Count; c++) { // Append the cells data followed by a comma to delimit. sb.Append(dgvRow.Cells[c].Value + ","); } // Add a new line in the text file. sb.Append(Environment.NewLine); } } // Load up the save file dialog with the default option as saving as a .csv file. SaveFileDialog sfd = new SaveFileDialog(); sfd.Filter = "CSV files (*.csv)|*.csv"; if (sfd.ShowDialog() == System.Windows.Forms.DialogResult.OK) { // If they've selected a save location... using (System.IO.StreamWriter sw = new System.IO.StreamWriter(sfd.FileName, false)) { // Write the stringbuilder text to the the file. sw.WriteLine(sb.ToString()); } } // Confirm to the user it has been completed. MessageBox.Show("CSV file saved."); }
将 Regex 表达式保存在数据库中
有一个集成的数据库可供您使用以保存您的代码,以便以后使用。您必须先单击“新组”按钮,在文本框中输入组名,然后按“保存到数据库”,然后它将被添加到下拉组合框中。
历史
Emails Extractor (版本 1.0) -------- 2012 年 9 月。