24  جرف البيانات من الويب

24.1 مقدمة

يقدم لك هذا الفصل أساسيات جرف البيانات من الويب باستخدام حزمة rvest. يُعد جرف البيانات من الويب (Web scraping) أداة مفيدة جدًا لاستخراج البيانات من صفحات الويب. بعض المواقع الإلكترونية ستوفر واجهة برمجة تطبيقات (API)، وهي مجموعة من طلبات HTTP المنظمة التي تُرجع البيانات بصيغة JSON، والتي تتعامل معها باستخدام التقنيات المشروحة في الفصل 23. وحيثما أمكن، يُفضل استخدام واجهة برمجة التطبيقات (API)1؛ لأنها تمنحك في الغالب بيانات أكثر موثوقية. ولكن لسوء الحظ، فإن البرمجة باستخدام واجهات برمجة تطبيقات الويب تخرج عن نطاق هذا الكتاب. وبدلاً من ذلك، سنقوم بتعليمك تقنية جرف البيانات، وهي طريقة تعمل سواء يوفر الموقع واجهة برمجة تطبيقات أم لا.

في هذا الفصل، سنناقش أولاً أخلاقيات وقانونية جرف البيانات قبل أن نغوص في أساسيات لغة HTML. ستتعلم بعد ذلك أساسيات محددات CSS لتحديد عناصر معينة على الصفحة، وكيفية استخدام دوال rvest للحصول على البيانات من النصوص والسمات (attributes) واستخراجها من HTML إلى R. ثم سنناقش بعض التقنيات لمعرفة محدد CSS الذي تحتاجه للصفحة التي تقوم بجرف بياناتها، قبل أن نختتم ببعض دراسات الحالة ومناقشة موجزة للمواقع الإلكترونية الديناميكية.

24.1.1 المتطلبات المسبقة

في هذا الفصل، سنركز على الأدوات التي توفرها حزمة rvest. حزمة rvest هي جزء من منظومة tidyverse، ولكنها ليست عضوًا أساسيًا، لذا ستحتاج لتحميلها بشكل صريح. سنقوم أيضًا بتحميل منظومة tidyverse بالكامل لأننا سنجدها مفيدة عمومًا عند التعامل مع البيانات التي قمنا بجرفها.

24.2 أخلاقيات وقانونية جرف البيانات

قبل أن نبدأ في مناقشة الكود الذي ستريد استخدامه لتنفيذ جرف البيانات، نحتاج إلى الحديث عما إذا كان القيام بذلك قانونيًا وأخلاقيًا. بشكل عام، الموقف معقد فيما يتعلق بـكلا الأمرين.

تعتمد الجوانب القانونية كثيرًا على المكان الذي تعيش فيه. ومع ذلك، كقاعدة عامة، إذا كانت البيانات عامة، وغير شخصية، وحقيقية (موضوعية)، فمن المرجح أن تكون في أمان2. هذه العوامل الثلاثة مهمة لأنها مرتبطة بشروط وأحكام الموقع، والمعلومات التي تميز الهوية الشخصية، وحقوق الطبع والنشر، كما سنناقش أدناه.

إذا لم تكن البيانات عامة، أو كانت شخصية، أو غير حقيقية، أو إذا كنت تقوم بجرف البيانات خصيصًا لكسب المال منها، فستحتاج إلى التحدث إلى محامٍ. في كل الأحوال، يجب عليك احترام موارد الخادم الذي يستضيف الصفحات التي تجرفها. والأهم من ذلك، هذا يعني أنه إذا كنت تقوم بجرف العديد من الصفحات، فيجب عليك التأكد من الانتظار قليلاً بين كل طلب وآخر. إحدى الطرق السهلة للقيام بذلك هي استخدام حزمة polite التي طورها دميترو بيريبولكين (Dmytro Perepolkin). ستقوم الحزمة تلقائيًا بالتوقف المؤقت بين الطلبات وتخزين النتائج مؤقتًا (cache) حتى لا تطلب نفس الصفحة مرتين أبداً.

24.2.1 شروط الخدمة

إذا نظرت بتمحيص، ستجد أن العديد من المواقع الإلكترونية تتضمن رابطًا لـ “الشروط والأحكام” أو “شروط الخدمة” في مكان ما على الصفحة، وإذا قرأت تلك الصفحة بتمعن، فستكتشف غالبًا أن الموقع يمنع جرف البيانات صراحةً. تميل هذه الصفحات إلى أن تكون بمثابة فرض سيطرة قانونية حيث تطلق الشركات ادعاءات واسعة النطاق جدًا. من الأدب احترام شروط الخدمة هذه قدر الإمكان، ولكن خذ أي ادعاءات ببعض التحفظ.

وجدت المحاكم الأمريكية عمومًا أن مجرد وضع شروط الخدمة في تذييل (footer) الموقع الإلكتروني ليس كافياً لإلزامك بها، مثل قضية HiQ Labs v. LinkedIn. بشكل عام، لكي تكون ملزمًا بشروط الخدمة، يجب أن تكون قد اتخذت إجراءً صريحًا مثل إنشاء حساب أو تحديد مربع الاختيار. لهذا السبب يُعد كَوْن البيانات عامة أمرًا مهمًا؛ إذا كنت لا تحتاج إلى حساب للوصول إليها، فمن غير المرجح أن تكون ملزمًا بشروط الخدمة. لاحظ، مع ذلك، أن الوضع مختلف تمامًا في أوروبا حيث وجدت المحاكم أن شروط الخدمة قابلة للتطبيق حتى لو لم توافق عليها صراحةً.

24.2.2 المعلومات التي تميز الهوية الشخصية

حتى لو كانت البيانات عامة، يجب أن تكون حذرًا للغاية بشأن جرف المعلومات التي تميز الهوية الشخصية مثل الأسماء، وعناوين البريد الإلكتروني، وأرقام الهواتف، وتواريخ الميلاد، وما إلى ذلك. تفرض أوروبا قوانين صارمة بشكل خاص بشأن جمع أو تخزين مثل هذه البيانات (GDPR)، وبغض النظر عن المكان الذي تعيش فيه، فمن المرجح أن تدخل في مأزق أخلاقي. على سبيل المثال، في عام 2016، قامت مجموعة من الباحثين بجرف معلومات الملف الشخصي العام (مثل أسماء المستخدمين، والعمر، والجنس، والموقع، إلخ) لحوالي 70,000 شخص على موقع التعارف OkCupid ونشروا هذه البيانات علنًا دون أي محاولات لإخفاء الهوية. في حين شعر الباحثون أنه لا يوجد خطأ في ذلك لأن البيانات كانت عامة بالفعل، إلا أن هذا العمل قوبل بإنكار وإدانة واسعة النطاق بسبب المخاوف الأخلاقية المتعلقة بإمكانية تحديد هوية المستخدمين الذين نُشرت معلوماتهم في مجموعة البيانات. إذا كان عملك يتضمن جرف معلومات تميز الهوية الشخصية، فنوصي بشدة بالقراءة عن دراسة OkCupid3 بالإضافة إلى دراسات مماثلة ذات أخلاقيات بحثية مشكوك فيها تتضمن الحصول على معلومات تميز الهوية الشخصية ونشرها.

24.2.3 حقوق الطبع والنشر

أخيرًا، تحتاج أيضًا إلى القلق بشأن قانون حقوق الطبع والنشر. إن قانون حقوق الطبع والنشر معقد، ولكن الأمر يستحق إلقاء نظرة على القانون الأمريكي الذي يصف بالضبط ما هو محمي: “[…] الأعمال الأصلية للمؤلفين المثبتة في أي وسيط تعبير ملموس، […]”. ثم يستمر في وصف فئات محددة ينطبق عليها مثل الأعمال الأدبية، والأعمال الموسيقية، والصور المتحركة وغيرها. ومن الملفت للنظر عدم وجود البيانات ضمن حماية حقوق الطبع والنشر. هذا يعني أنه طالما تقصر جرف البيانات على الحقائق، فإن حماية حقوق الطبع والنشر لا تنطبق. (ولكن لاحظ أن أوروبا لديها حق منفصل من نوع خاص “sui generis” يحمي قواعد البيانات).

كمثال سريع، في الولايات المتحدة، قوائم المكونات والتعليمات ليست محمية بحقوق الطبع والنشر، لذا لا يمكن استخدام حقوق الطبع والنشر لحماية وصفة طعام. ولكن إذا كانت قائمة الوصفات هذه مصحوبة بمحتوى أدبي مبتكر وجوهري، فهذا محمي بحقوق الطبع والنشر. لهذا السبب عندما تبحث عن وصفة طعام على الإنترنت، تجد دائمًا الكثير من المحتوى النصي قبل الوصفة.

إذا كنت بحاجة إلى جرف محتوى أصلي (مثل النصوص أو الصور)، فقد تظل محميًا بموجب عقيدة الاستخدام العادل. الاستخدام العادل ليس قاعدة صارمة وثابتة، ولكنه يوازن بين عدد من العوامل. ومن المرجح أن ينطبق إذا كنت تجمع البيانات لأغراض البحث أو لأغراض غير تجارية وإذا اقتصرت في جرفك على ما تحتاجه فقط.

24.3 أساسيات HTML

لجرف صفحات الويب، تحتاج أولاً إلى فهم القليل عن HTML، وهي اللغة التي تصف صفحات الويب. ترمز HTML إلى HyperText Markup Language (لغة ترميز النص الفائق) وتدوّن بشيء يشبه هذا:

<html>
<head>
  <title>Page title</title>
</head>
<body>
  <h1 id='first'>A heading</h1>
  <p>Some text &amp; <b>some bold text.</b></p>
  <img src='myimg.png' width='100' height='100'>
</body>

تمتلك HTML بنية هرمية تتكون من العناصر (elements) والتي تتكون من وسم بداية (مثل <tag>)، وسمات (attributes) اختيارية (id='first')، ووسم نهاية4 (مثل </tag>)، ومحتويات (contents) (كل شيء بين وسم البداية والنهاية).

نظرًا لاستخدام < و > لوسوم البداية والنهاية، لا يمكنك كتابتها مباشرة. بدلاً من ذلك، عليك استخدام رمز الإفلات (escapes) الخاص بـ HTML وهو &gt; (أكبر من) و &lt; (أصغر من). ونظرًا لأن رموز الإفلات هذه تستخدم &، فإذا كنت تريد علامة عطف صريحة، فعليك كتابتها كـ &amp;. هناك مجموعة واسعة من رموز الإفلات المحتملة في HTML ولكن لا داعي للقلق بشأنها كثيرًا لأن rvest تتعامل معها تلقائيًا من أجلك.

إن جرف البيانات من الويب ممكن لأن معظم الصفحات التي تحتوي على بيانات تريد جرفها تمتلك عمومًا بنية متسقة.

24.3.1 العناصر (Elements)

هناك أكثر من 100 عنصر في HTML. ومن أهمها:

  • يجب أن تكون كل صفحة HTML داخل عنصر <html>، ويجب أن تحتوي على عنصرين فرعيين (children): <head>، الذي يحتوي على البيانات الوصفية للمستند مثل عنوان الصفحة، و <body>، الذي يحتوي على المحتوى الذي تراه في المتصفح.

  • الوسوم الكتليّة (Block tags) مثل <h1> (العنوان الرئيسي 1)، و <section> (قسم)، و <p> (فقرة)، و <ol> (قائمة مرتبة) تشكل البنية العامة للصفحة.

  • الوسوم المضمنة (Inline tags) مثل <b> (عريض)، و <i> (مائل)، و <a> (رابط) تقوم بتنسيق النص داخل الوسوم الكتليّة.

إذا واجهت وسمًا لم تره من قبل، يمكنك معرفة ما يفعله من خلال البحث السريع في جوجل. مكان آخر جيد للبدء هو توثيق MDN للويب والذي يصف كل جانب تقريبًا من جوانب برمجة الويب.

يمكن لجميع العناصر تقريبًا أن تحتوي على محتوى بين وسوم البداية والنهاية الخاصة بها. يمكن أن يكون هذا المحتوى إما نصًا أو عناصر أخرى. على سبيل المثال، كود HTML التالي يحتوي على فقرة نصية، مع كلمة واحدة بخط عريض.

<p>
  Hi! My <b>name</b> is Hadley.
</p>

العناصر الفرعية (children) هي العناصر التي يحتوي عليها العنصر، لذا فإن عنصر <p> أعلاه يحتوي على عنصر فرعي واحد، وهو عنصر <b>. لا يحتوي عنصر <b> على عناصر فرعية، ولكنه يحتوي على محتويات (النص “name”).

24.3.2 السمات (Attributes)

يمكن أن تحتوي الوسوم على سمات مسماة تبدو مثل name1='value1' name2='value2'. اثنتان من أهم السمات هما id و class، اللتان تُستخدمان جنبًا إلى جنب مع CSS (أنماط الصفحات المتصاعدة) للتحكم في المظهر البصري للصفحة. غالباً ما تكون هذه السمات مفيدة عند جرف البيانات من الصفحة. تُستخدم السمات أيضًا لتسجيل وجهة الروابط (سمة href في عناصر <a>) ومصدر الصور (سمة src في عنصر <img>).

24.4 استخراج البيانات

لبدء جرف البيانات، ستلاحظ أنك بحاجة إلى رابط URL للصفحة التي تريد جرفها، والذي يمكنك عادةً نسخه من متصفح الويب الخاص بك. ستحتاج بعد ذلك إلى قراءة كود HTML لتلك الصفحة في R باستخدام read_html(). يقوم هذا بإرجاع كائن من نوع xml_document5 والذي ستقوم بعد ذلك بالتعامل معه باستخدام دوال rvest:

html <- read_html("http://rvest.tidyverse.org/")
html
#> {html_document}
#> <html lang="en">
#> [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UT ...
#> [2] <body>\n    <a href="#container" class="visually-hidden-focusable">Ski ...

تتضمن rvest أيضًا دالة تسمح لك بكتابة HTML في السطر (inline). سنستخدم هذه الدالة كثيرًا في هذا الفصل ونحن نشرح كيفية عمل دوال rvest المختلفة مع أمثلة بسيطة.

html <- minimal_html("
  <p>This is a paragraph</p>
  <ul>
    <li>This is a bulleted list</li>
  </ul>
",)
html
#> {html_document}
#> <html>
#> [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UT ...
#> [2] <body>\n<p>This is a paragraph</p>\n  <ul>\n<li>This is a bulleted lis ...

الآن بعد أن أصبح لديك كود HTML في R، حان الوقت لاستخراج البيانات التي تهمك. ستتعلم أولاً عن محددات CSS التي تسمح لك بتحديد العناصر المطلوبة ودوال rvest التي يمكنك استخدامها لاستخراج البيانات منها. ثم سنغطي بشكل موجز جداول HTML، والتي تحتوي على بعض الأدوات الخاصة.

24.4.1 البحث عن العناصر

CSS هي اختصار لـ Cascading Style Sheets (ورقات الأنماط المتصاعدة)، وهي أداة لتحديد التنسيق البصري لمستندات HTML. تتضمن CSS لغة مصغرة لاختيار العناصر على الصفحة تُسمى محددات CSS (CSS selectors). تحدد محددات CSS أنماطًا لتحديد موقع عناصر HTML، وهي مفيدة لجرف البيانات لأنها توفر طريقة موجزة لوصف العناصر التي تريد استخراجها.

سنعود إلى محددات CSS بمزيد من التفصيل في قسم 24.5، ولكن لحسن الحظ يمكنك قطع شوط طويل باستخدام ثلاثة محددات فقط:

  • p يحدد جميع عناصر <p>.

  • .title يحدد جميع العناصر التي تحتوي على class قيمته “title”.

  • #title يحدد العنصر الذي تحتوي سمة id الخاصة به على القيمة “title”. يجب أن تكون سمات Id فريدة داخل المستند، لذا لن يحدد هذا دائمًا سوى عنصر واحد فقط.

لنجرب هذه المحددات مع مثال بسيط:

html <- minimal_html("
  <h1>This is a heading</h1>
  <p id='first'>This is a paragraph</p>
  <p class='important'>This is an important paragraph</p>
",)

استخدم html_elements() للعثور على جميع العناصر التي تطابق المحدد:

html |> html_elements("p")
#> {xml_nodeset (2)}
#> [1] <p id="first">This is a paragraph</p>
#> [2] <p class="important">This is an important paragraph</p>
html |> html_elements(".important")
#> {xml_nodeset (1)}
#> [1] <p class="important">This is an important paragraph</p>
html |> html_elements("#first")
#> {xml_nodeset (1)}
#> [1] <p id="first">This is a paragraph</p>

دالة أخرى مهمة هي html_element() والتي تُرجع دائمًا نفس عدد المخرجات مثل المدخلات. إذا قمت بتطبيقها على مستند كامل، فستعطيك التطابق الأول:

html |> html_element("p")
#> {html_node}
#> <p id="first">

هناك فرق مهم بين html_element() و html_elements() عندما تستخدم محددًا لا يطابق أي عناصر. تُرجع html_elements() متجهًا بطول 0، بينما تُرجع html_element() قيمة مفقودة (missing value). سيكون هذا مهمًا بعد قليل.

html |> html_elements("b")
#> {xml_nodeset (0)}
html |> html_element("b")
#> {xml_missing}
#> <NA>

24.4.2 التحديد المتداخل (Nesting selections)

في معظم الحالات، ستستخدم html_elements() و html_element() معًا، وتحديدًا عن طريق استخدام html_elements() لتحديد العناصر التي ستصبح مشاهدات (observations) ثم استخدام html_element() للعثور على العناصر التي ستصبح متغيرات (variables). لنرى هذا عملياً باستخدام مثال بسيط. لدينا هنا قائمة غير مرتبة (<ul>) حيث يحتوي كل عنصر قائمة (<li>) على بعض المعلومات حول أربع شخصيات من حرب النجوم (StarWars):

html <- minimal_html("
  <ul>
    <li><b>C-3PO</b> is a <i>droid</i> that weighs <span class='weight'>167 kg</span></li>
    <li><b>R4-P17</b> is a <i>droid</i></li>
    <li><b>R2-D2</b> is a <i>droid</i> that weighs <span class='weight'>96 kg</span></li>
    <li><b>Yoda</b> weighs <span class='weight'>66 kg</span></li>
  </ul>
  ")

يمكننا استخدام html_elements() لإنشاء متجه حيث يتوافق كل عنصر مع شخصية مختلفة:

characters <- html |> html_elements("li")
characters
#> {xml_nodeset (4)}
#> [1] <li>\n<b>C-3PO</b> is a <i>droid</i> that weighs <span class="weight"> ...
#> [2] <li>\n<b>R4-P17</b> is a <i>droid</i>\n</li>
#> [3] <li>\n<b>R2-D2</b> is a <i>droid</i> that weighs <span class="weight"> ...
#> [4] <li>\n<b>Yoda</b> weighs <span class="weight">66 kg</span>\n</li>

لاستخراج اسم كل شخصية، نستخدم html_element()، لأنه عند تطبيقها على مخرجات html_elements() يضمن إرجاع استجابة واحدة لكل عنصر:

characters |> html_element("b")
#> {xml_nodeset (4)}
#> [1] <b>C-3PO</b>
#> [2] <b>R4-P17</b>
#> [3] <b>R2-D2</b>
#> [4] <b>Yoda</b>

التمييز بين html_element() و html_elements() ليس مهمًا بالنسبة للاسم، ولكنه مهم بالنسبة للوزن. نريد الحصول على وزن واحد لكل شخصية، حتى لو لم يكن هناك وسم <span> للوزن. هذا ما تفعله دالة html_element():

characters |> html_element(".weight")
#> {xml_nodeset (4)}
#> [1] <span class="weight">167 kg</span>
#> [2] NA
#> [3] <span class="weight">96 kg</span>
#> [4] <span class="weight">66 kg</span>

بينما تعثر html_elements() على جميع عناصر <span> الخاصة بالوزن التي تعد عناصر فرعية لـ characters. هناك ثلاثة عناصر فقط من هذه، لذا نفقد الربط بين الأسماء والأوزان:

characters |> html_elements(".weight")
#> {xml_nodeset (3)}
#> [1] <span class="weight">167 kg</span>
#> [2] <span class="weight">96 kg</span>
#> [3] <span class="weight">66 kg</span>

الآن بعد أن قمت بتحديد العناصر المطلوبة، ستحتاج إلى استخراج البيانات، إما من المحتويات النصية أو من بعض السمات.

24.4.3 النصوص والسمات

تستخرج الدالة html_text2()6 محتويات النص الصريح لعنصر HTML:

characters |> 
  html_element("b") |> 
  html_text2()
#> [1] "C-3PO"  "R4-P17" "R2-D2"  "Yoda"

characters |> 
  html_element(".weight") |> 
  html_text2()
#> [1] "167 kg" NA       "96 kg"  "66 kg"

لاحظ أنه سيتم التعامل مع أي رموز إفلات تلقائيًا؛ ولن ترى رموز إفلات HTML إلا في كود HTML المصدري فقط، وليس في البيانات التي تُرجعها rvest.

تستخرج الدالة html_attr() البيانات من السمات:

html <- minimal_html("
  <p><a href='[https://en.wikipedia.org/wiki/Cat](https://en.wikipedia.org/wiki/Cat)'>cats</a></p>
  <p><a href='[https://en.wikipedia.org/wiki/Dog](https://en.wikipedia.org/wiki/Dog)'>dogs</a></p>
",)

html |> 
  html_elements("p") |> 
  html_element("a") |> 
  html_attr("href")
#> [1] "[https://en.wikipedia.org/wiki/Cat](https://en.wikipedia.org/wiki/Cat)"
#> [2] "[https://en.wikipedia.org/wiki/Dog](https://en.wikipedia.org/wiki/Dog)"

تُرجع html_attr() دائمًا سلسلة نصية (string)، لذا إذا كنت تستخرج أرقامًا أو تواريخ، فستحتاج إلى إجراء بعض المعالجة اللاحقة.

24.4.4 الجداول

إذا كنت محظوظًا، فستكون بياناتك مخزنة بالفعل في جدول HTML، وسيكون الأمر مجرد مسألة قراءتها من ذلك الجدول. عادة ما يكون من السهل والواضح التعرف على الجدول في متصفحك: سيكون له بنية مستطيلة من الصفوف والأعمدة، ويمكنك نسخه ولصقه في أداة مثل Excel.

تتكون جداول HTML من أربعة عناصر رئيسية: <table>، و <tr> (صف الجدول)، و <th> (رأس الجدول)، و <td> (بيانات الجدول). إليك جدول HTML بسيطًا يتكون من عمودين وثلاثة صفوف:

html <- minimal_html("
  <table class='mytable'>
    <tr><th>x</th>   <th>y</th></tr>
    <tr><td>1.5</td> <td>2.7</td></tr>
    <tr><td>4.9</td> <td>1.3</td></tr>
    <tr><td>7.2</td> <td>8.1</td></tr>
  </table>
  ")

توفر rvest دالة تعرف كيفية قراءة هذا النوع من البيانات: html_table(). تُرجع هذه الدالة قائمة تحتوي على tibble واحد لكل جدول يتم العثور عليه في الصفحة. استخدم html_element() لتحديد الجدول الذي تريد استخراجه:

html |> 
  html_element(".mytable") |> 
  html_table()
#> # A tibble: 3 × 2
#>       x     y
#>   <dbl> <dbl>
#> 1   1.5   2.7
#> 2   4.9   1.3
#> 3   7.2   8.1

لاحظ أنه تم تحويل x و y تلقائيًا إلى أرقام. هذا التحويل التلقائي لا يعمل دائمًا، لذا في السيناريوهات الأكثر تعقيدًا، قد ترغب في إيقاف تشغيله باستخدام convert = FALSE ثم القيام بالتحويل الخاص بك.

24.5 العثور على المحددات المناسبة

عادةً ما يكون تحديد المحدد (selector) الذي تحتاجه لبياناتك هو الجزء الأصعب في المشكلة. ستحتاج غالبًا إلى إجراء بعض التجارب للعثور على محدد يكون دقيقًا وخاصًا (أي لا يحدد أشياء لا تهتم بها) وحساسًا (أي يحدد كل شيء تهتم به). الكثير من عمليات المحاولة والخطأ هي جزء طبيعي من هذه العملية! هناك أداتان رئيسيتان متاحتان لمساعدتك في هذه العملية: SelectorGadget وأدوات التطوير في متصفحك.

SelectorGadget هو تطبيق كود إشارة مرجعية (bookmarklet) بلغة جافا سكريبت يقوم تلقائيًا بإنشاء محددات CSS بناءً على الأمثلة الإيجابية والسلبية التي تقدمها. إنه لا يعمل دائمًا، ولكن عندما يعمل، يكون الأمر أشبه بالسحر! يمكنك تعلم كيفية تثبيت واستخدام SelectorGadget إما بقراءة https://rvest.tidyverse.org/articles/selectorgadget.html أو مشاهدة فيديو Mine على https://www.youtube.com/watch?v=PetWV5g1Xsc.

يأتي كل متصفح حديث مزودًا ببعض أدوات التطوير، ولكننا نوصي بمتصفح Chrome، حتى لو لم يكن متصفحك المعتاد: أدوات تطوير الويب الخاصة به هي من بين الأفضل وتكون متاحة فورًا. انقر بزر الماوس الأيمن على أي عنصر في الصفحة وانقر على فحص Inspect. سيفتح هذا عرضًا قابلاً للتوسيع لصفحة HTML الكاملة، متمحورًا حول العنصر الذي نقرت عليه للتو. يمكنك استخدام هذا لاستكشاف الصفحة والحصول على فكرة عن المحددات التي قد تعمل. انتبه بشكل خاص لسمات class و id، حيث تُستخدم هذه غالبًا لتشكيل البنية البصرية للصفحة، وبالتالي تُعد أدوات جيدة لاستخراج البيانات التي تبحث عنها.

داخل عرض العناصر (Elements view)، يمكنك أيضًا النقر بزر الماوس الأيمن على عنصر واختيار نسخ كمحدد Copy as Selector لإنشاء محدد يحدد العنصر المطلوب بشكل فريد.

إذا قام SelectorGadget أو أدوات تطوير Chrome بإنشاء محدد CSS لا تفهمه، فجرّب أداة Selectors Explained التي تترجم محددات CSS إلى اللغة الإنجليزية البسيطة. إذا وجدت نفسك تفعل هذا كثيرًا، فقد ترغب في تعلم المزيد عن محددات CSS بشكل عام. نوصي بالبدء بالدليل التعليمي الممتع CSS dinner ثم الرجوع إلى توثيق MDN للويب.

24.6 تجميع كل الأجزاء معًا

دعنا نجمع كل هذا معًا لجرف بعض المواقع الإلكترونية. هناك بعض المخاطر المتمثلة في أن هذه الأمثلة قد لا تعمل عندما تقوم بتشغيلها — والتحدي الأساسي لجرف البيانات من الويب هو: إذا تغيرت بنية الموقع، فستضطر إلى تغيير كود الجرف الخاص بك.

24.6.1 حرب النجوم (StarWars)

تتضمن rvest مثالاً بسيطًا جدًا في vignette("starwars"). هذه صفحة بسيطة تحتوي على حد أدنى من HTML لذا فهي مكان جيد للبدء. أشجعك على الانتقال إلى تلك الصفحة الآن واستخدام “فحص العنصر (Inspect Element)” لفحص أحد العناوين التي تمثل عنوان فيلم من أفلام Star Wars. استخدم لوحة المفاتيح أو الماوس لاستكشاف الهيكل الهرمي لـ HTML ولمعرفة ما إذا كان يمكنك فهم البنية المشتركة التي يستخدمها كل فيلم.

يجب أن تكون قادرًا على رؤية أن كل فيلم له بنية مشتركة تبدو كالتالي:

<section>
  <h2 data-id="1">The Phantom Menace</h2>
  <p>Released: 1999-05-19</p>
  <p>Director: <span class="director">George Lucas</span></p>
  
  <div class="crawl">
    <p>...</p>
    <p>...</p>
    <p>...</p>
  </div>
</section>

هدفنا هو تحويل هذه البيانات إلى إطار بيانات (data frame) مكون من 7 صفوف مع المتغيرات title و year و director و intro. سنبدأ بقراءة كود HTML واستخراج جميع عناصر <section>:

url <- "https://rvest.tidyverse.org/articles/starwars.html"
html <- read_html(url)

section <- html |> html_elements("section")
section
#> {xml_nodeset (7)}
#> [1] <section><h2 data-id="1">\nThe Phantom Menace\n</h2>\n<p>\nReleased: 1 ...
#> [2] <section><h2 data-id="2">\nAttack of the Clones\n</h2>\n<p>\nReleased: ...
#> [3] <section><h2 data-id="3">\nRevenge of the Sith\n</h2>\n<p>\nReleased:  ...
#> [4] <section><h2 data-id="4">\nA New Hope\n</h2>\n<p>\nReleased: 1977-05-2 ...
#> [5] <section><h2 data-id="5">\nThe Empire Strikes Back\n</h2>\n<p>\nReleas ...
#> [6] <section><h2 data-id="6">\nReturn of the Jedi\n</h2>\n<p>\nReleased: 1 ...
#> [7] <section><h2 data-id="7">\nThe Force Awakens\n</h2>\n<p>\nReleased: 20 ...

يسترجع هذا سبعة عناصر تطابق الأفلام السبعة الموجودة في تلك الصفحة، مما يشير إلى أن استخدام section كمحدد هو أمر جيد. استخراج العناصر الفردية أمر مباشر لأن البيانات توجد دائمًا في النص. إنها مجرد مسألة العثور على المحدد المناسب:

section |> html_element("h2") |> html_text2()
#> [1] "The Phantom Menace"      "Attack of the Clones"   
#> [3] "Revenge of the Sith"     "A New Hope"             
#> [5] "The Empire Strikes Back" "Return of the Jedi"     
#> [7] "The Force Awakens"

section |> html_element(".director") |> html_text2()
#> [1] "George Lucas"     "George Lucas"     "George Lucas"    
#> [4] "George Lucas"     "Irvin Kershner"   "Richard Marquand"
#> [7] "J. J. Abrams"

بمجرد قيامنا بذلك لكل مكون، يمكننا تجميع كل النتائج في جدول tibble:

tibble(
  title = section |> 
    html_element("h2") |> 
    html_text2(),
  released = section |> 
    html_element("p") |> 
    html_text2() |> 
    str_remove("Released: ") |> 
    parse_date(),
  director = section |> 
    html_element(".director") |> 
    html_text2(),
  intro = section |> 
    html_element(".crawl") |> 
    html_text2()
)
#> # A tibble: 7 × 4
#>   title                   released   director         intro                  
#>   <chr>                   <date>     <chr>            <chr>                  
#> 1 The Phantom Menace      1999-05-19 George Lucas     "Turmoil has engulfed …
#> 2 Attack of the Clones    2002-05-16 George Lucas     "There is unrest in th…
#> 3 Revenge of the Sith     2005-05-19 George Lucas     "War! The Republic is …
#> 4 A New Hope              1977-05-25 George Lucas     "It is a period of civ…
#> 5 The Empire Strikes Back 1980-05-17 Irvin Kershner   "It is a dark time for…
#> 6 Return of the Jedi      1983-05-25 Richard Marquand "Luke Skywalker has re…
#> # ℹ 1 more row

قمنا بإجراء المزيد من المعالجة على released للحصول على متغير سيكون من السهل استخدامه لاحقًا في تحليلنا.

24.6.2 أفضل الأفلام حسب تقييم IMDB

في مهمتنا التالية، سنعالج شيئًا أكثر صعوبة قليلاً، وهو استخراج أفضل 250 فيلمًا من قاعدة بيانات الأفلام على الإنترنت (IMDb). في الوقت الذي كتبنا فيه هذا الفصل، كانت الصفحة تبدو مثل الشكل 24.1.

تعرض لقطة الشاشة جدولاً بأعمدة "الترتيب والعنوان"، و"تقييم IMDb"، و"تقييمك". تم عرض 9 أفلام من أصل أفضل 250 فيلمًا. أفضل 5 أفلام هي The Shawshank Redemption، و The Godfather، و The Dark Knight، و The Godfather: Part II، و 12 Angry Men.
الشكل 24.1: لقطة شاشة لصفحة ويب أفضل أفلام IMDb تم التقاطها في 05-12-2022.

تحتوي هذه البيانات على بنية جدولية واضحة، لذا يستحق الأمر البدء بـ html_table():

url <- "https://web.archive.org/web/20220201012049/https://www.imdb.com/chart/top/"
html <- read_html(url)

table <- html |> 
  html_element("table") |> 
  html_table()
table
#> # A tibble: 250 × 5
#>   ``    `Rank & Title`                    `IMDb Rating` `Your Rating`   ``   
#>   <lgl> <chr>                                     <dbl> <chr>           <lgl>
#> 1 NA    "1.\n      The Shawshank Redempt…           9.2 "12345678910\n… NA   
#> 2 NA    "2.\n      The Godfather\n      …           9.1 "12345678910\n… NA   
#> 3 NA    "3.\n      The Godfather: Part I…           9   "12345678910\n… NA   
#> 4 NA    "4.\n      The Dark Knight\n    …           9   "12345678910\n… NA   
#> 5 NA    "5.\n      12 Angry Men\n       …           8.9 "12345678910\n… NA   
#> 6 NA    "6.\n      Schindler's List\n   …           8.9 "12345678910\n… NA   
#> # ℹ 244 more rows

يتضمن هذا بعض الأعمدة الفارغة، ولكنه بشكل عام يؤدي عملاً جيدًا في التقاط المعلومات من الجدول. ومع ذلك، نحتاج إلى إجراء المزيد من المعالجة لجعل استخدامها أسهل. أولاً، سنعيد تسمية الأعمدة ليكون التعامل معها أسهل، ونزيل المساحات الفارغة الزائدة في الترتيب والعنوان. سنفعل ذلك باستخدام select() (بدلاً من rename()) لإعادة التسمية وتحديد هذين العمودين فقط في خطوة واحدة. ثم سنزيل الأسطر الجديدة والمساحات الإضافية، ثم نطبق separate_wider_regex() (من قسم 15.3.4) واستخراج العنوان والسنة والترتيب في متغيرات خاصة بها.

ratings <- table |>
  select(
    rank_title_year = `Rank & Title`,
    rating = `IMDb Rating`
  ) |> 
  mutate(
    rank_title_year = str_replace_all(rank_title_year, "\n +", " ")
  ) |> 
  separate_wider_regex(
    rank_title_year,
    patterns = c(
      rank = "\\d+", "\\. ",
      title = ".+", " +\\(",
      year = "\\d+", "\\)"
    )
  )
ratings
#> # A tibble: 250 × 4
#>   rank  title                    year  rating
#>   <chr> <chr>                    <chr>  <dbl>
#> 1 1     The Shawshank Redemption 1994     9.2
#> 2 2     The Godfather            1972     9.1
#> 3 3     The Godfather: Part II   1974     9  
#> 4 4     The Dark Knight          2008     9  
#> 5 5     12 Angry Men             1957     8.9
#> 6 6     Schindler's List         1993     8.9
#> # ℹ 244 more rows

حتى في هذه الحالة التي تأتي فيها معظم البيانات من خلايا الجدول، لا يزال الأمر يستحق النظر في كود HTML الخام. إذا قمت بذلك، فستكتشف أنه يمكننا إضافة القليل من البيانات الإضافية باستخدام إحدى السمات. هذا أحد الأسباب التي تجعل من المفيد قضاء القليل من الوقت في استكشاف مصدر الصفحة؛ فقد تجد بيانات إضافية، أو قد تجد مسارًا للإعراب والتحليل البرمجي يكون أسهل قليلاً.

html |> 
  html_elements("td strong") |> 
  head() |> 
  html_attr("title")
#> [1] "9.2 based on 2,536,415 user ratings"
#> [2] "9.1 based on 1,745,675 user ratings"
#> [3] "9.0 based on 1,211,032 user ratings"
#> [4] "9.0 based on 2,486,931 user ratings"
#> [5] "8.9 based on 749,563 user ratings"  
#> [6] "8.9 based on 1,295,705 user ratings"

يمكننا دمج هذا مع البيانات الجدولية وتطبيق separate_wider_regex() مرة أخرى لاستخراج جزء البيانات الذي نهتم به:

ratings |>
  mutate(
    rating_n = html |> html_elements("td strong") |> html_attr("title")
  ) |> 
  separate_wider_regex(
    rating_n,
    patterns = c(
      "[0-9.]+ based on ",
      number = "[0-9,]+",
      " user ratings"
    )
  ) |> 
  mutate(
    number = parse_number(number)
  )

24.7 المواقع الديناميكية (Dynamic sites)

حتى الآن ركزنا على المواقع الإلكترونية التي تُرجع فيها html_elements() ما تراه في المتصفح وناقشنا كيفية إعراب ما تُرجعه وكيفية تنظيم تلك المعلومات في أطر بيانات منسقة (tidy data frames). ومع ذلك، من وقت لآخر، ستصادف موقعًا لا ترجع فيه html_elements() ومثيلاتها أي شيء مما تراه في المتصفح. في كثير من الحالات، يعود ذلك إلى أنك تحاول جرف موقع إلكتروني ينشئ محتوى الصفحة ديناميكيًا باستخدام جافا سكريبت (javascript). لا يعمل هذا حاليًا مع rvest، لأن rvest تنزل كود HTML الخام ولا تقوم بتشغيل أي جافا سكريبت.

لا يزال من الممكن جرف هذه الأنواع من المواقع، ولكن rvest تحتاج إلى استخدام عملية أكثر كلفة: محاكاة متصفح الويب بالكامل بما في ذلك تشغيل كل الجافا سكريبت. هذه الوظيفة غير متاحة في وقت كتابة هذا الفصل، ولكنها شيء نعمل عليه بجد وقد تكون متاحة بحلول الوقت الذي تقرأ فيه هذا. تستخدم هذه الوظيفة حزمة chromote التي تقوم في الواقع بتشغيل متصفح Chrome في الخلفية، وتمنحك أدوات إضافية للتفاعل مع الموقع، مثل شخص يكتب نصًا وينقر على الأزرار. تحقق من موقع rvest لمزيد من التفاصيل.

24.8 ملخص

في هذا الفصل، تعلمت السبب، والسبب المانع، وكيفية جرف البيانات من صفحات الويب. أولاً، تعلمت أساسيات HTML واستخدام محددات CSS للإشارة إلى عناصر معينة، ثم تعلمت كيفية استخدام حزمة rvest لاستخراج البيانات من HTML إلى R. ثم قمنا بتوضيح جرف البيانات من الويب من خلال دراستي حالة: سيناريو أبسط لجرف البيانات حول أفلام StarWars من موقع حزمة rvest، وسيناريو أكثر تعقيدًا لجرف أفضل 250 فيلمًا من IMDB.

يمكن أن تكون التفاصيل الفنية لجرف البيانات من الويب معقدة، خاصة عند التعامل مع المواقع الكبيرة، ومع ذلك يمكن أن تكون الاعتبارات القانونية والأخلاقية أكثر تعقيدًا. من المهم بالنسبة لك أن تثقف نفسك حولهما قبل البدء في جرف البيانات.

ينقلنا هذا إلى نهاية جزء الاستيراد (import) من الكتاب حيث تعلمت تقنيات للحصول على البيانات من مكان وجودها (جداول البيانات، وقواعد البيانات، وملفات JSON، والمواقع الإلكترونية) وتحويلها إلى شكل منسق (tidy) في R. الآن حان الوقت لتوجيه أنظارنا إلى موضوع جديد: الاستفادة القصوى من R بلغة برمجة.


  1. وتوجد بالفعل حزم جاهزة في مستودع CRAN تُغلف العديد من واجهات برمجة التطبيقات الشهيرة، لذا ابدأ بإجراء بحث بسيط أولاً!↩︎

  2. بالطبع نحن لسنا محامين، وهذا ليس مشورة قانونية. ولكن هذا هو أفضل ملخص يمكننا تقديمه بعد قراءة الكثير حول هذا الموضوع.↩︎

  3. نشرت مجلة Wired أحد الأمثلة على المقالات حول دراسة OkCupid: https://www.wired.com/2016/05/okcupid-study-reveals-perils-big-data-science.↩︎

  4. عدد من الوسوم (بما في ذلك <p> و <li>) لا تتطلب وسوم نهاية، ولكننا نرى أنه من الأفضل تضمينها لأنها تجعل رؤية بنية HTML أسهل قليلاً.↩︎

  5. تأتي هذه الفئة من حزمة xml2. حزمة xml2 هي حزمة منخفضة المستوى تبني rvest أدواتها عليها.↩︎

  6. توفر rvest أيضًا دالة html_text()، ولكن يجب عليك استخدام html_text2() تقريبًا في كل الحالات لأنها تقوم بعمل أفضل في تحويل عناصر HTML المتداخلة إلى نصوص.↩︎