1 // ================================================================================================
3 // MediaWikiのウェブサイト(システム)をあらわすモデルクラスソース</summary>
5 // <copyright file="MediaWiki.cs" company="honeplusのメモ帳">
6 // Copyright (C) 2013 Honeplus. All rights reserved.</copyright>
9 // ================================================================================================
11 namespace Honememo.Wptscs.Websites
14 using System.Collections.Generic;
18 using System.Xml.Linq;
19 using System.Xml.Serialization;
20 using Honememo.Models;
21 using Honememo.Utilities;
22 using Honememo.Wptscs.Models;
23 using Honememo.Wptscs.Properties;
24 using Honememo.Wptscs.Utilities;
27 /// MediaWikiのウェブサイト(システム)をあらわすモデルクラスです。
29 public class MediaWiki : Website, IXmlSerializable
34 /// 名前空間情報取得用にアクセスするAPI。
36 private string metaApi;
39 /// MediaWiki記事データ取得用にアクセスするAPI。
41 private string contentApi;
44 /// MediaWiki言語間リンク取得用にアクセスするAPI。
46 private string interlanguageApi;
51 private int? templateNamespace;
56 private int? categoryNamespace;
61 private int? fileNamespace;
64 /// MediaWiki書式のシステム定義変数。
66 private ISet<string> magicWords;
69 /// MediaWikiの名前空間の情報。
71 private IDictionary<int, IgnoreCaseSet> namespaces;
74 /// MediaWikiのウィキ間リンクのプレフィックス情報。
76 private IgnoreCaseSet interwikiPrefixs;
79 /// MediaWikiのウィキ間リンクのプレフィックス情報(APIから取得した値と設定値の集合)。
81 private IgnoreCaseSet interwikiPrefixCaches;
84 /// <see cref="InitializeByMetaApi"/>同期用ロックオブジェクト。
86 private object lockLoadMetaApi = new object();
93 /// 指定された言語, サーバーのMediaWikiを表すインスタンスを作成。
95 /// <param name="language">ウェブサイトの言語。</param>
96 /// <param name="location">ウェブサイトの場所。</param>
97 /// <exception cref="ArgumentNullException"><paramref name="language"/>または<paramref name="location"/>が<c>null</c>の場合。</exception>
98 /// <exception cref="ArgumentException"><paramref name="location"/>が空の文字列の場合。</exception>
99 public MediaWiki(Language language, string location)
100 : base(language, location)
105 /// 指定された言語のWikipediaを表すインスタンスを作成。
107 /// <param name="language">ウェブサイトの言語。</param>
108 /// <exception cref="ArgumentNullException"><c>null</c>が指定された場合。</exception>
109 public MediaWiki(Language language)
111 // 親で初期化していないのは、languageのnullチェックの前にnull参照でエラーになってしまうから
112 this.Language = language;
113 this.Location = string.Format(Settings.Default.WikipediaLocation, language.Code);
117 /// 空のインスタンスを作成(シリアライズ or 拡張用)。
119 protected MediaWiki()
125 #region 設定ファイルに初期値を持つプロパティ
128 /// MediaWikiメタ情報取得用にアクセスするAPI。
130 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
131 public string MetaApi
135 if (string.IsNullOrEmpty(this.metaApi))
137 return Settings.Default.MediaWikiMetaApi;
145 this.metaApi = value;
150 /// MediaWiki記事データ取得用にアクセスするAPI。
152 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
153 public string ContentApi
157 if (string.IsNullOrEmpty(this.contentApi))
159 return Settings.Default.MediaWikiContentApi;
162 return this.contentApi;
167 this.contentApi = value;
172 /// MediaWiki言語間リンク取得用にアクセスするAPI。
174 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
175 public string InterlanguageApi
179 if (string.IsNullOrEmpty(this.interlanguageApi))
181 return Settings.Default.MediaWikiInterlanguageApi;
184 return this.interlanguageApi;
189 this.interlanguageApi = value;
194 /// テンプレートの名前空間を示す番号。
196 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
197 public int TemplateNamespace
201 return this.templateNamespace ?? Settings.Default.MediaWikiTemplateNamespace;
206 this.templateNamespace = value;
213 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
214 public int CategoryNamespace
218 return this.categoryNamespace ?? Settings.Default.MediaWikiCategoryNamespace;
223 this.categoryNamespace = value;
230 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
231 public int FileNamespace
235 return this.fileNamespace ?? Settings.Default.MediaWikiFileNamespace;
240 this.fileNamespace = value;
245 /// MediaWiki書式のシステム定義変数。
248 /// 値が指定されていない場合、デフォルト値を返す。
251 public ISet<string> MagicWords
255 if (this.magicWords == null)
257 // ※ 初期値は http://www.mediawiki.org/wiki/Help:Magic_words 等を参考に設定。
258 // APIからも取得できるが、2012年2月現在 #expr でなければ認識されないものが
259 // exprで返ってきたりとアプリで使うには情報が足りないため人力で対応。
260 return new HashSet<string>(Settings.Default.MediaWikiMagicWords.Cast<string>());
263 return this.magicWords;
268 this.magicWords = value;
274 #region サーバーから値を取得するプロパティ
277 /// MediaWikiの名前空間の情報。
280 /// サーバーから情報を取得。大文字小文字を区別しない。
282 public IDictionary<int, IgnoreCaseSet> Namespaces
286 // 値が設定されていない場合、サーバーから取得して初期化する
287 // ※ コンストラクタ等で初期化していないのは、通信の準備が整うまで行えないため
288 // ※ 余計なロック・通信をしないよう、ロックの前後に値のチェックを行う
289 if (this.namespaces != null)
291 return this.namespaces;
294 lock (this.lockLoadMetaApi)
296 if (this.namespaces != null)
298 return this.namespaces;
301 this.InitializeByMetaApi();
304 return this.namespaces;
309 this.namespaces = value;
314 /// MediaWikiのウィキ間リンクのプレフィックス情報。
317 /// 値が設定されていない場合デフォルト値とサーバーから、
318 /// 設定されている場合その内容とサーバーから取得した情報を使用する。
321 public IgnoreCaseSet InterwikiPrefixs
325 // 値が準備されていない場合、サーバーと設定ファイルから取得して初期化する
326 // ※ コンストラクタ等で初期化していないのは、通信の準備が整うまで行えないため
327 // ※ 余計なロック・通信をしないよう、ロックの前後に値のチェックを行う
328 if (this.interwikiPrefixCaches != null)
330 return this.interwikiPrefixCaches;
333 lock (this.lockLoadMetaApi)
335 if (this.interwikiPrefixCaches != null)
337 return this.interwikiPrefixCaches;
340 this.InitializeByMetaApi();
343 return this.interwikiPrefixCaches;
349 this.interwikiPrefixs = value;
350 this.interwikiPrefixCaches = null;
359 /// Template:仮リンク(他言語へのリンク)で書式化するためのフォーマット。
361 /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
362 public string LinkInterwikiFormat
369 /// Template:Langで書式化するためのフォーマット。
371 /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
372 public string LangFormat
379 /// 言語名の記事が存在するようなサイトか?
382 /// そうした記事が存在する場合<c>true</c>。
383 /// Wikipediaには[[日本語]]といった記事が存在するが、Wikitravelであればそうした記事は存在し得ない。
384 /// 言語名をリンクにするかといった判断に用いる。
386 public bool HasLanguagePage
399 /// <param name="title">ページタイトル。</param>
400 /// <returns>取得したページ。</returns>
401 /// <exception cref="InvalidDataException">APIから取得したデータが想定外。</exception>
402 /// <exception cref="NullReferenceException">APIから取得したデータが想定外。</exception>
403 /// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
405 /// ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。
406 /// このメソッドでは記事本文や日時といった情報は取得しない。
407 /// そうした情報は、実際にアクセスされたタイミングで動的に取得する。
409 public override Page GetPage(string title)
411 // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
412 // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
413 string escapeTitle = title;
414 if (new Uri(this.Location).IsFile)
416 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
420 Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.InterlanguageApi, escapeTitle));
422 // ページの言語間リンク情報XMLデータをMediaWikiサーバーから取得
424 using (Stream reader = this.WebProxy.GetStream(uri))
426 doc = XElement.Load(reader);
434 qe = (from n in doc.Elements("query")
437 catch (InvalidOperationException)
439 throw new InvalidOperationException("parse failed : api/query element is not found");
442 // クエリーからページ情報を読み込み返す
443 // ※ ページが無い場合などは、例外が投げられる
444 return MediaWikiPage.GetFromQuery(this, uri, qe);
450 /// <param name="title">ページタイトル。</param>
451 /// <returns>取得したページ。</returns>
452 /// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
453 /// <exception cref="EndPeriodException">末尾がピリオドのページの場合(既知の不具合への対応)。</exception>
454 /// <remarks>ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。</remarks>
455 public Page GetPageBodyAndTimestamp(string title)
457 // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
458 // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
459 string escapeTitle = title;
460 if (new Uri(this.Location).IsFile)
462 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
466 Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.ContentApi, escapeTitle));
468 // ページのXMLデータをMediaWikiサーバーから取得
470 using (Stream reader = this.WebProxy.GetStream(uri))
472 doc = XElement.Load(reader);
476 // ※ この問い合わせでは、ページが無い場合も要素自体は毎回ある模様
477 // 一件しか返らないはずなので先頭データを対象とする
481 pe = (from query in doc.Elements("query")
482 from pages in query.Elements("pages")
483 from n in pages.Elements("page")
486 catch (InvalidOperationException)
488 throw new InvalidOperationException("parse failed : query/pages/page element is not found");
492 if (pe.Attribute("missing") != null)
494 // missing属性が存在する場合、ページ無し
495 throw new FileNotFoundException("page not found");
499 var re = (from revisions in pe.Elements("revisions")
500 from n in revisions.Elements("rev")
504 return new MediaWikiPage(
506 XmlUtils.Value(pe.Attribute("title"), title),
508 new DateTime?(DateTime.Parse(re.Attribute("timestamp").Value)),
513 /// 指定された文字列がMediaWikiのシステム変数に相当かを判定。
515 /// <param name="text">チェックする文字列。</param>
516 /// <returns>システム変数に相当する場合<c>true</c>。</returns>
517 /// <remarks>大文字小文字は区別する。</remarks>
518 public bool IsMagicWord(string text)
520 // {{CURRENTYEAR}}や{{ns:1}}みたいなパターンがある
521 string s = StringUtils.DefaultString(text);
522 foreach (string variable in this.MagicWords)
524 if (s == variable || s.StartsWith(variable + ":"))
534 /// 指定されたリンク文字列がMediaWikiのウィキ間リンクかを判定。
536 /// <param name="link">チェックするリンク文字列。</param>
537 /// <returns>ウィキ間リンクに該当する場合<c>true</c>。</returns>
538 /// <remarks>大文字小文字は区別しない。</remarks>
539 public bool IsInterwiki(string link)
541 // ※ ウィキ間リンクには入れ子もあるが、ここでは意識する必要はない
542 string s = StringUtils.DefaultString(link);
544 // 名前空間と被る場合はそちらが優先、ウィキ間リンクと判定しない
545 if (this.IsNamespace(link))
550 // 文字列がいずれかのウィキ間リンクのプレフィックスで始まるか
551 int index = s.IndexOf(':');
557 return this.InterwikiPrefixs.Contains(s.Remove(index));
561 /// 指定されたリンク文字列がMediaWikiのいずれかの名前空間に属すかを判定。
563 /// <param name="link">チェックするリンク文字列。</param>
564 /// <returns>いずれかの名前空間に該当する場合<c>true</c>。</returns>
565 /// <remarks>大文字小文字は区別しない。</remarks>
566 public bool IsNamespace(string link)
568 // 文字列がいずれかの名前空間のプレフィックスで始まるか
569 string s = StringUtils.DefaultString(link);
570 int index = s.IndexOf(':');
576 string prefix = s.Remove(index);
577 foreach (IgnoreCaseSet prefixes in this.Namespaces.Values)
579 if (prefixes.Contains(prefix))
589 /// <see cref="LinkInterwikiFormat"/> を渡された記事名, 言語, 他言語版記事名, 表示名で書式化した文字列を返す。
591 /// <param name="title">記事名。</param>
592 /// <param name="lang">言語。</param>
593 /// <param name="langTitle">他言語版記事名。</param>
594 /// <param name="label">表示名。</param>
595 /// <returns>書式化した文字列。<see cref="LinkInterwikiFormat"/>が未設定の場合<c>null</c>。</returns>
596 public string FormatLinkInterwiki(string title, string lang, string langTitle, string label)
598 if (string.IsNullOrEmpty(this.LinkInterwikiFormat))
603 return StringUtils.FormatDollarVariable(this.LinkInterwikiFormat, title, lang, langTitle, label);
607 /// <see cref="LangFormat"/> を渡された言語, 文字列で書式化した文字列を返す。
609 /// <param name="lang">言語。</param>
610 /// <param name="text">文字列。</param>
611 /// <returns>書式化した文字列。<see cref="LangFormat"/>が未設定の場合<c>null</c>。</returns>
613 /// この<paramref name="lang"/>と<see cref="Language"/>のコードは、厳密には一致しないケースがあるが
614 /// (例、simple→en)、2012年2月現在の実装ではそこまで正確さは要求していない。
616 public string FormatLang(string lang, string text)
618 if (string.IsNullOrEmpty(this.LangFormat))
623 return StringUtils.FormatDollarVariable(this.LangFormat, lang, text);
628 #region XMLシリアライズ用メソッド
631 /// シリアライズするXMLのスキーマ定義を返す。
633 /// <returns>XML表現を記述する<see cref="System.Xml.Schema.XmlSchema"/>。</returns>
634 public System.Xml.Schema.XmlSchema GetSchema()
640 /// XMLからオブジェクトをデシリアライズする。
642 /// <param name="reader">デシリアライズ元の<see cref="XmlReader"/></param>
643 public void ReadXml(XmlReader reader)
645 XmlDocument xml = new XmlDocument();
649 // ※ 以下、基本的に無かったらNGの部分はいちいちチェックしない。例外飛ばす
650 XmlElement siteElement = xml.DocumentElement;
651 this.Location = siteElement.SelectSingleNode("Location").InnerText;
653 using (XmlReader r = XmlReader.Create(
654 new StringReader(siteElement.SelectSingleNode("Language").OuterXml), reader.Settings))
656 this.Language = new XmlSerializer(typeof(Language)).Deserialize(r) as Language;
659 this.MetaApi = XmlUtils.InnerText(siteElement.SelectSingleNode("MetaApi"));
660 this.ContentApi = XmlUtils.InnerText(siteElement.SelectSingleNode("ContentApi"));
661 this.InterlanguageApi = XmlUtils.InnerText(siteElement.SelectSingleNode("InterlanguageApi"));
664 if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("TemplateNamespace")), out namespaceId))
666 this.TemplateNamespace = namespaceId;
669 if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("CategoryNamespace")), out namespaceId))
671 this.CategoryNamespace = namespaceId;
674 if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("FileNamespace")), out namespaceId))
676 this.FileNamespace = namespaceId;
680 ISet<string> variables = new HashSet<string>();
681 foreach (XmlNode variableNode in siteElement.SelectNodes("MagicWords/Variable"))
683 variables.Add(variableNode.InnerText);
686 if (variables.Count > 0)
689 this.MagicWords = variables;
693 IgnoreCaseSet prefixs = new IgnoreCaseSet();
694 foreach (XmlNode prefixNode in siteElement.SelectNodes("InterwikiPrefixs/Prefix"))
696 prefixs.Add(prefixNode.InnerText);
699 if (prefixs.Count > 0)
702 this.InterwikiPrefixs = prefixs;
705 this.LinkInterwikiFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LinkInterwikiFormat"));
706 this.LangFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LangFormat"));
707 bool hasLanguagePage;
708 if (bool.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("HasLanguagePage")), out hasLanguagePage))
710 this.HasLanguagePage = hasLanguagePage;
715 /// オブジェクトをXMLにシリアライズする。
717 /// <param name="writer">シリアライズ先の<see cref="XmlWriter"/></param>
718 public void WriteXml(XmlWriter writer)
720 writer.WriteElementString("Location", this.Location);
721 new XmlSerializer(this.Language.GetType()).Serialize(writer, this.Language);
724 // ※ 設定ファイルに初期値を持つものは、プロパティではなく値から出力
725 writer.WriteElementString("MetaApi", this.metaApi);
726 writer.WriteElementString("ContentApi", this.contentApi);
727 writer.WriteElementString("InterlanguageApi", this.interlanguageApi);
728 writer.WriteElementString(
730 this.templateNamespace.HasValue ? this.templateNamespace.ToString() : string.Empty);
731 writer.WriteElementString(
733 this.templateNamespace.HasValue ? this.categoryNamespace.ToString() : string.Empty);
734 writer.WriteElementString(
736 this.templateNamespace.HasValue ? this.fileNamespace.ToString() : string.Empty);
739 writer.WriteStartElement("MagicWords");
740 if (this.magicWords != null)
742 foreach (string variable in this.magicWords)
744 writer.WriteElementString("Variable", variable);
749 writer.WriteEndElement();
750 writer.WriteStartElement("InterwikiPrefixs");
751 if (this.interwikiPrefixs != null)
753 foreach (string prefix in this.interwikiPrefixs)
755 writer.WriteElementString("Prefix", prefix);
759 writer.WriteEndElement();
760 writer.WriteElementString("LinkInterwikiFormat", this.LinkInterwikiFormat);
761 writer.WriteElementString("LangFormat", this.LangFormat);
762 writer.WriteElementString("HasLanguagePage", this.HasLanguagePage.ToString());
770 /// <see cref="MetaApi"/>を使用してサーバーからメタ情報を取得する。
772 /// <exception cref="System.Net.WebException">通信エラー等が発生した場合。</exception>
773 /// <exception cref="InvalidDataException">APIから取得した情報が想定外のフォーマットの場合。</exception>
774 private void InitializeByMetaApi()
776 // APIのXMLデータをMediaWikiサーバーから取得
777 XmlDocument xml = new XmlDocument();
778 using (Stream reader = this.WebProxy.GetStream(new Uri(new Uri(this.Location), this.MetaApi)))
783 // ルートエレメントまで取得し、フォーマットをチェック
784 XmlElement rootElement = xml["api"];
785 if (rootElement == null)
787 // XMLは取得できたが空 or フォーマットが想定外
788 throw new InvalidDataException("parse failed : api element is not found");
792 XmlElement queryElement = rootElement["query"];
793 if (queryElement == null)
796 throw new InvalidDataException("parse failed : query element is not found");
799 // クエリー内のネームスペース・ネームスペースエイリアス、ウィキ間リンクを読み込み
800 this.namespaces = this.LoadNamespacesElement(queryElement);
801 this.interwikiPrefixCaches = this.LoadInterwikimapElement(queryElement);
803 // ウィキ間リンクは読み込んだ後に設定ファイルorプロパティの分をマージ
804 // ※ 設定ファイルの初期値は下記より作成。
805 // http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/maintenance/interwiki.sql?view=markup
806 // APIに加えて設定ファイルも持っているのは、2012年2月現在APIから返ってこない
807 // 項目(wikipediaとかcommonsとか)が存在するため。
808 this.interwikiPrefixCaches.UnionWith(
809 this.interwikiPrefixs == null
810 ? Settings.Default.MediaWikiInterwikiPrefixs.Cast<string>()
811 : this.interwikiPrefixs);
815 /// <see cref="MetaApi"/>から取得したXMLのうち、ネームスペースに関する部分を読み込む。
817 /// <param name="queryElement">APIから取得したXML要素のうち、api→query部分のエレメント。</param>
818 /// <returns>読み込んだネームスペース情報。</returns>
819 /// <exception cref="InvalidDataException">namespacesエレメントが存在しない場合。</exception>
820 private IDictionary<int, IgnoreCaseSet> LoadNamespacesElement(XmlElement queryElement)
822 // ネームスペースブロックを取得、ネームスペースブロックまでは必須
823 XmlElement namespacesElement = queryElement["namespaces"];
824 if (namespacesElement == null)
827 throw new InvalidDataException("parse failed : namespaces element is not found");
831 IDictionary<int, IgnoreCaseSet> namespaces = new Dictionary<int, IgnoreCaseSet>();
832 foreach (XmlNode node in namespacesElement.ChildNodes)
834 XmlElement namespaceElement = node as XmlElement;
835 if (namespaceElement != null)
839 int id = decimal.ToInt16(decimal.Parse(namespaceElement.GetAttribute("id")));
840 IgnoreCaseSet values = new IgnoreCaseSet();
841 values.Add(namespaceElement.InnerText);
842 namespaces[id] = values;
845 string canonical = namespaceElement.GetAttribute("canonical");
846 if (!string.IsNullOrEmpty(canonical))
848 values.Add(canonical);
853 // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
854 System.Diagnostics.Debug.WriteLine("MediaWiki.LoadNamespacesElement > 例外発生 : " + e);
859 // ネームスペースエイリアスブロックを取得、無い場合も想定
860 XmlElement aliasesElement = queryElement["namespacealiases"];
861 if (aliasesElement != null)
864 foreach (XmlNode node in aliasesElement.ChildNodes)
866 XmlElement namespaceElement = node as XmlElement;
867 if (namespaceElement != null)
871 int id = decimal.ToInt16(decimal.Parse(namespaceElement.GetAttribute("id")));
872 ISet<string> values = new HashSet<string>();
873 if (namespaces.ContainsKey(id))
875 values = namespaces[id];
878 values.Add(namespaceElement.InnerText);
882 // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
883 System.Diagnostics.Debug.WriteLine("MediaWiki.LoadNamespacesElement > 例外発生 : " + e);
893 /// <see cref="MetaApi"/>から取得したXMLのうち、ウィキ間リンクに関する部分を読み込む。
895 /// <param name="queryElement">APIから取得したXML要素のうち、api→query部分のエレメント。</param>
896 /// <returns>読み込んだウィキ間リンク情報。</returns>
897 /// <exception cref="InvalidDataException">interwikimapエレメントが存在しない場合。</exception>
898 private IgnoreCaseSet LoadInterwikimapElement(XmlElement queryElement)
900 // ウィキ間リンクブロックを取得、ウィキ間リンクブロックまでは必須
901 XmlElement interwikimapElement = queryElement["interwikimap"];
902 if (interwikimapElement == null)
905 throw new InvalidDataException("parse failed : interwikimap element is not found");
909 IgnoreCaseSet interwikiPrefixs = new IgnoreCaseSet();
910 foreach (XmlNode node in interwikimapElement.ChildNodes)
912 XmlElement interwikiElement = node as XmlElement;
913 if (interwikiElement != null)
915 string prefix = interwikiElement.GetAttribute("prefix");
916 if (!string.IsNullOrWhiteSpace(prefix))
918 interwikiPrefixs.Add(prefix);
923 return interwikiPrefixs;