شرح Pandas بالعربي لتحليل بيانات السيو

50 دقيقة تقريبًا

ما تحتاج أن تعرفه قبل هذا الدرس
  • Variable
  • String
  • Integer
  • Float
  • Boolean
  • List
  • Dictionary
  • print
  • type
  • key
  • index
  • append
  • len
  • if
  • else
  • elif
  • Comparison Operators
  • and
  • or
  • not
  • for
  • while
  • break
  • continue
  • Function
  • def
  • Parameter
  • Argument
  • return
  • open
  • with
  • read
  • write
  • strip
  • startswith
  • endswith
  • import
  • urlparse
  • csv
  • reader
  • DictReader
  • writer
  • DictWriter
  • fieldnames
  • set
  • lower
  • rstrip
  • replace
  • split
ما يضيفه هذا الدرس
  • pandas
  • DataFrame
  • read_csv
  • head
  • columns
  • shape
  • value_counts
  • isna
  • notna
  • sort_values
  • contains
  • to_csv
  • duplicated
  • drop_duplicates
  • rename
  • merge
  • groupby

يحسب العدّاد في الأعلى تمارين الدرس، وعددها 3، أما القراءة وتشغيل الأمثلة فلا تُحتسب.

في الدرس السابق اشتغلنا على ملف CSV بالشكل ده:

وقدرنا باستخدام مكتبة csv نقرأ الصفوف، ونفحص كل Row، وبعدها نطلع مثلًا صفحات 404:

مثال
الناتج المتوقع
https://example.com/old-page/

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

إحنا
  1. 1فتحنا الملف.
  2. 2قرأنا الصفوف.
  3. 3مررنا على كل Row.
  4. 4فحصنا شرط.
  5. 5حفظنا النتائج.

لكن تخيل إن ملف الـ Crawl عندك فيه 100,000 URL و40 عمود، وعايز تعمل حاجات زي:

  • هات صفحات 404.
  • هات صفحات 200 غير القابلة للفهرسة.
  • رتب الصفحات حسب عدد الكلمات.
  • اعرف عندي كام صفحة لكل Status Code.
  • هات الصفحات اللي Title بتاعها فاضي.
  • اختار 5 أعمدة فقط من أصل 40.
  • احفظ النتيجة في CSV جديد.

نقدر نعمل كل ده بالـ Loops والـ Lists. لكن هنكتب كود كتير جدًا. وهنا تدخل مكتبة Pandas.

الهدف من درس النهاردة مش إننا نحفظ أوامر Pandas. الهدف إننا نفهم هي بتغير طريقة تعاملنا مع بيانات SEO إزاي.

الأول، إيه هي Pandas؟

مكتبة Pandas معمولة أساسًا علشان التعامل مع البيانات الجدولية. يعني بيانات بالشكل ده:

وده تقريبًا شكل جزء كبير جدًا من البيانات اللي هنتعامل معاها في SEO.

سواء جاية من:

في النهاية غالبًا عندك Rows + Columns.

مكتبة Pandas بتخلينا نتعامل مع الجدول ده كوحدة واحدة.

بدل ما نعمل for row in reader: في كل تحليل، هنقدر نقول لـ Pandas بشكل أقرب إلى:

ملحوظة
هاتلي الصفوف اللي Status فيها 404.

وهي تعمل الفلترة. لكن قبل ما نستخدمها، لازم نثبتها.

تثبيت Pandas

مكتبة csv اللي استخدمناها في الدرس السابق موجودة أصلًا مع بايثون. علشان كده كتبنا import csv وخلاص.

لكن Pandas مكتبة خارجية، يعني لازم تتثبت على الجهاز الأول. افتح الـ Terminal أو Command Prompt واكتب:

بعد انتهاء التثبيت، تقدر تستخدمها في بايثون.

لو الأمر السابق معملش عندك بشكل صحيح، ممكن تستخدم:

التثبيت بتعمله مرة واحدة على بيئة بايثون اللي شغال عليها، مش كل مرة تكتب فيها برنامج.

ملحوظة
على الصفحة دي التثبيت مش محتاجه — بايثون هنا شغالة جوه المتصفح، وPandas بتتحمّل تلقائيًا أول مرة تدوس فيها على زرار تشغيل في هذا الدرس. لكن على جهازك، الأمر فوق هو اللي هتستخدمه فعلًا.

بعد التثبيت نقدر نبدأ.

استيراد Pandas

نكتب:

ممكن تسأل: ليه مكتبناش import pandas؟ ينفع. لكن المتعارف عليه تقريبًا في كل مكان إننا نستورد Pandas بالاسم المختصر pd.

يعني بدل ما نكتب بعد كده pandas.read_csv() هنكتب pd.read_csv().

إذن import pandas as pd معناها:

ملحوظة
استورد مكتبة Pandas، واستخدم الاسم pd للإشارة ليها داخل الكود.

نقرأ أول CSV باستخدام Pandas

هنستخدم نفس الملف crawl.csv ومحتواه:

نكتب:

مثال
الناتج المتوقع
                             URL  Status         Title   Indexability
0           https://example.com/     200          Home      Indexable
1     https://example.com/about/     200      About Us      Indexable
2  https://example.com/old-page/     404      Old Page  Non-Indexable
3   https://example.com/private/     200  Private Page  Non-Indexable
4      https://example.com/blog/     200          Blog      Indexable

من أول نظرة هتلاحظ إن بايثون بقت بتعرض البيانات كجدول. مش كل Row كـ Dictionary لوحده.

وده لأن pd.read_csv() قرأ ملف CSV وحوله لحاجة اسمها DataFrame. ودي أهم فكرة في درس النهاردة.

يعني إيه DataFrame؟

مش عايزك تحفظ تعريف أكاديمي. بص على البيانات: ده جدول. عنده Rows. وعنده Columns.

مكتبة Pandas بتمثل الجدول ده داخل بايثون باستخدام DataFrame. يعني لما كتبنا df = pd.read_csv("crawl.csv") إحنا بنقول:

ملحوظة
اقرأ الملف وحط الجدول الناتج داخل المتغير df.

والاسم df اختصار شائع لكلمة DataFrame. ممكن تسمي المتغير crawl أو seo_data أو أي اسم تاني. لكن إحنا هنستخدم df في البداية لأنك هتشوفه كتير جدًا في أكواد Pandas.

نتأكد إن df فعلًا DataFrame

مثال
الناتج المتوقع
<class 'pandas.DataFrame'>

يعني البيانات الموجودة في df من نوع DataFrame.

أول حاجة تعملها لما تفتح ملف كبير

لو عندك ملف فيه 100,000 Row، مش منطقي تعمل print(df) وتطبع الملف كله.

إحنا غالبًا عايزين نبص على أول كام Row فقط علشان نتأكد إن البيانات اتقرت صح. هنا نستخدم head(). نجرب:

مثال
الناتج المتوقع
                             URL  Status         Title   Indexability
0           https://example.com/     200          Home      Indexable
1     https://example.com/about/     200      About Us      Indexable
2  https://example.com/old-page/     404      Old Page  Non-Indexable
3   https://example.com/private/     200  Private Page  Non-Indexable
4      https://example.com/blog/     200          Blog      Indexable

الدالة head() بتعرض أول 5 Rows بشكل افتراضي (وملفنا فيه 5 Rows بالظبط، فده كل الملف). لو عايز أول صفين فقط:

مثال
الناتج المتوقع
                          URL  Status     Title Indexability
0        https://example.com/     200      Home    Indexable
1  https://example.com/about/     200  About Us    Indexable

يعني الرقم 2 بيقول: اعرض أول صفين. ودي من أول الحاجات اللي هتعملها تقريبًا كل مرة تفتح Data Set جديد.

نعرف أسماء الأعمدة

في الدرس السابق استخدمنا reader.fieldnames علشان نشوف أسماء أعمدة CSV. في Pandas الموضوع أبسط. نستخدم df.columns. نجرب:

مثال
الناتج المتوقع
Index(['URL', 'Status', 'Title', 'Indexability'], dtype='str')

المهم بالنسبة لنا حاليًا هو الجزء URL وStatus وTitle وIndexability. يعني دول الأعمدة الموجودة في الملف.

ومع ملفات SEO الحقيقية، الخطوة دي مفيدة جدًا قبل ما تبدأ تكتب تحليل، لأن أسماء الأعمدة بتختلف من Export للتاني.

نعرف عدد الصفوف والأعمدة

لو عندك ملف كبير، طبيعي تسأل: الملف فيه كام Row وكام Column؟

نقدر نستخدم df.shape. نجرب:

مثال
الناتج المتوقع
(5, 4)

الرقمين معناهم 5 Rows و4 Columns. يعني أول رقم عدد الصفوف. وثاني رقم عدد الأعمدة.

ممكن نطبعهم بشكل أوضح:

مثال
الناتج المتوقع
Rows: 5
Columns: 4

لاحظ إننا استخدمنا Index. القيمة df.shape رجعت (5, 4). فـ df.shape[0] جابت أول قيمة. وdf.shape[1] جابت ثاني قيمة.

نختار عمود واحد

دلوقتي عندنا DataFrame كامل. لكن ممكن أكون عايز الـ URLs فقط. نكتب:

مثال
الناتج المتوقع
0             https://example.com/
1       https://example.com/about/
2    https://example.com/old-page/
3     https://example.com/private/
4        https://example.com/blog/
Name: URL, dtype: str

ركز في df["URL"]. إحنا بنقول: من الـ DataFrame اللي اسمها df، هات عمود URL. ودي من أهم أشكال التعامل مع Pandas.

طيب إيه الأرقام اللي ظهرت على الشمال؟

عندنا 0 و1 و2 و3 و4. دي مش جزء من الـ URL. ودي مش عمود موجود أصلًا في CSV.

Pandas بتدي كل Row رقم يساعدها تعرف الصف. الرقم ده اسمه Index. أول Row رقمه 0، بعده 1، وهكذا.

وخلي بالك من نفس القاعدة اللي قابلناها في بايثون قبل كده. العد بيبدأ من 0 مش 1.

دلوقتي مش محتاجين نعمل حاجة بالـ Index، المهم بس تعرف هو إيه علشان متفتكرش إنه جزء من البيانات.

اختيار أكتر من عمود

لو عايز URL وStatus هنكتب:

مثال
الناتج المتوقع
                             URL  Status
0           https://example.com/     200
1     https://example.com/about/     200
2  https://example.com/old-page/     404
3   https://example.com/private/     200
4      https://example.com/blog/     200

هنا فيه تفصيلة مهمة. عمود واحد:

أكتر من عمود:

لاحظ إن فيه [[]]. ليه؟ القائمة الداخلية ["URL", "Status"] هي List بأسماء الأعمدة اللي عايزينها. والـ Brackets الخارجية بتستخدمها Pandas علشان تختار الأعمدة من df.

مش مهم تحفظ الجملة دي، المهم تعرف الفرق في الاستخدام.

نبدأ أول Filtering حقيقي

دي النقطة اللي هتشوف فيها ليه Pandas مفيدة. إحنا عايزين صفحات 404.

بالطريقة القديمة كنا بنعمل:

في Pandas هنبدأ الأول بحاجة بسيطة. نكتب:

مثال
الناتج المتوقع
0    False
1    False
2     True
3    False
4    False
Name: Status, dtype: bool

ممكن النتيجة تبدو غريبة في البداية. لكن اللي حصل بسيط جدًا.

Pandas راحت لكل Row وسألت: هل الـ Status يساوي 404؟

ليه طلع كده؟
  1. 1أول Row: 200 == 404 → النتيجة False.
  2. 2ثالث Row: 404 == 404 → النتيجة True.
  3. 3يعني df["Status"] == 404 لم ترجع الصفحات نفسها.
  4. 4هي رجعت لنا مجموعة True وFalse بتحدد الصفوف اللي حققت الشرط.

نستخدم النتيجة في الفلترة

دلوقتي نقدر نقول:

ملحوظة
هاتلي من df الصفوف اللي الشرط بتاعها True.

نكتب:

مثال
الناتج المتوقع
                             URL  Status     Title   Indexability
2  https://example.com/old-page/     404  Old Page  Non-Indexable

وده أول Filter باستخدام Pandas.

السطر df["Status"] == 404 هو الشرط. والجزء df[ ... ] بيقول:

ملحوظة
هات الصفوف من DataFrame اللي تحقق الشرط.

فلما جمعناهم df[df["Status"] == 404] المعنى: من df هات الصفوف اللي Status فيها يساوي 404.

ليه كتبنا 404 كرقم المرة دي؟

في الدرس السابق كتبنا row["Status"] == "404" يعني 404 كـ String. لكن هنا كتبنا df["Status"] == 404 كرقم. ليه؟

لأن Pandas لما قرأت العمود شافت إن القيم كلها أرقام، فحولت العمود تلقائيًا لنوع رقمي.

نقدر نتأكد باستخدام df.dtypes:

مثال
الناتج المتوقع
URL               str
Status          int64
Title             str
Indexability      str
dtype: object

المهم عندنا هنا Status int64، يعني Pandas بتتعامل مع Status كأرقام صحيحة.

مش محتاج دلوقتي تتعمق في الفرق بين int وint64. بالنسبة للمرحلة الحالية اعتبر إن int64 نوع رقمي صحيح.

نطلع URLs فقط من صفحات 404

النتيجة الحالية فيها كل الأعمدة. لكن لو عايز URL فقط:

مثال
الناتج المتوقع
2    https://example.com/old-page/
Name: URL, dtype: str

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

فلترة النصوص

عايزين الصفحات Non-Indexable. نكتب:

مثال
الناتج المتوقع
                             URL  Status         Title   Indexability
2  https://example.com/old-page/     404      Old Page  Non-Indexable
3   https://example.com/private/     200  Private Page  Non-Indexable

لاحظ إننا هنا استخدمنا "Non-Indexable" بين Quotes. ليه؟ لأنها String. إذن نفس قواعد المقارنة اللي اتعلمناها من بداية الكورس لسه شغالة.

الفرق إن Pandas بتطبق المقارنة على العمود كله بدل ما نعمل Loop بإيدنا.

أكتر من شرط

عايزين الصفحات اللي Status = 200 وفي نفس الوقت Indexability = Non-Indexable. طبيعي تفكر تكتب and لأن ده اللي اتعلمناه قبل كده.

لكن مع شروط Pandas هنستخدم &. والرمز ده هنا معناه: الشرط الأول والشرط الثاني لازم يكونوا صحيحين.

لكن فيه قاعدة مهمة جدًا. كل شرط لازم يكون داخل أقواس:

والشرط الثاني:

وبينهم &. يبقى:

نجرب:

مثال
الناتج المتوقع
                            URL  Status         Title   Indexability
3  https://example.com/private/     200  Private Page  Non-Indexable

عندنا صفحة واحدة حققت الشرطين.

ليه مستخدمناش and؟

دي نقطة لازم تكون واضحة. مع متغيرين عاديين ممكن تعمل:

مثال
الناتج المتوقع
True

هنا كل شرط بيرجع قيمة واحدة. لكن في Pandas df["Status"] == 200 مش بيرجع قيمة واحدة. بيرجع نتيجة لكل Row.

وعلشان نجمع مجموعتين من الشروط Row بـ Row، بنستخدم & بدل and. في المرحلة الحالية افتكر القاعدة العملية دي:

القاعدة
  • and → &
  • or → |

وكل شرط تحطه بين أقواس.

نجرب or في Pandas

لو عايزين Status 404 أو 301 هنستخدم |. نكتب:

مثال

يعني & الاتنين لازم يتحققوا. بينما | يكفي شرط واحد منهم يتحقق.

اختيار الأعمدة بعد الفلترة

في ملفات الـ Crawl الحقيقية ممكن يكون عندك 30 أو 40 عمود. لكن لما تطلع صفحات 404، ممكن تكون محتاج فقط URL وStatus وTitle.

نعمل:

مثال
الناتج المتوقع
                             URL  Status     Title
2  https://example.com/old-page/     404  Old Page

كده إحنا عملنا عمليتين منفصلتين. الأولى فلترنا الـ Rows. والثانية اخترنا الـ Columns.

ودي نقطة مهمة جدًا. في تحليل البيانات عندك حاجتين مختلفتين: أي Rows أريد؟ وأي Columns أريد؟ مش نفس الحاجة.

ترتيب البيانات

نفترض إن عندنا ملف:

وعايزين نرتب الصفحات حسب عدد الكلمات. هنستخدم sort_values(). نجرب:

مثال
الناتج المتوقع
                           URL  Status  Words
3  https://example.com/page-4/     404     50
1  https://example.com/page-2/     200    120
0  https://example.com/page-1/     200    850
2  https://example.com/page-3/     200   2200

Pandas رتبت من الأصغر للأكبر. لكن لو عايزين الأكبر الأول، نستخدم ascending=False. يبقى:

كلمة ascending معناها الترتيب التصاعدي. لما تكون True، الترتيب من الأصغر للأكبر. ولما نكتب False، نعكسه.

عندي كام صفحة من كل Status Code؟

دي من الحاجات اللي هتعملها كتير. عندنا مثلًا:

عايز أعرف 200 كام مرة، 404 كام مرة، 301 كام مرة. Pandas عندها value_counts(). نجرب:

مثال
الناتج المتوقع
Status
200    3
404    2
301    1
Name: count, dtype: int64

يعني 200 = 3 صفحات، 404 = صفحتين، 301 = صفحة واحدة.

فـ value_counts() بتحسب عدد مرات ظهور كل قيمة داخل العمود.

البيانات الفارغة

في شغل SEO هتقابل حاجة مهمة جدًا. ممكن يكون عندك:

الصفحتين الأخيرتين مفيهمش Title. لما Pandas تقرأ الملف، هتشوف:

مثال
الناتج المتوقع
                           URL  Status     Title
0         https://example.com/     200      Home
1   https://example.com/about/     200  About Us
2  https://example.com/page-1/     200       NaN
3  https://example.com/page-2/     200       NaN

ظهر عندنا شيء جديد: NaN. في السياق اللي يهمنا دلوقتي، تقدر تفهم NaN ببساطة على إنها: قيمة مفقودة. يعني الخلية مفيهاش قيمة.

إزاي نجيب الصفوف اللي Title فيها فاضي؟

Pandas غالبًا حولت القيم الفارغة إلى NaN. فالأفضل نستخدم isna(). نجرب الأول:

مثال
الناتج المتوقع
0    False
1    False
2     True
3     True
Name: Title, dtype: bool

نستخدمها في الفلترة:

مثال
الناتج المتوقع
                           URL  Status Title
2  https://example.com/page-1/     200   NaN
3  https://example.com/page-2/     200   NaN

ودي أول مرة نبدأ نستخدم Pandas لاكتشاف مشكلة SEO مباشرة من البيانات.

طيب لو عايز الصفحات اللي عندها Title؟ عندنا العكس notna(). يعني: القيمة ليست مفقودة. نجرب:

مثال
الناتج المتوقع
                          URL  Status     Title
0        https://example.com/     200      Home
1  https://example.com/about/     200  About Us

إذن isna() القيمة مفقودة. وnotna() القيمة موجودة.

دمج أكتر من شرط مع القيم المفقودة

عايزين صفحات 200 اللي Title بتاعها مفقود. عندنا شرطين: df["Status"] == 200 وdf["Title"].isna(). نجمعهم بـ &:

مثال
الناتج المتوقع
                                URL  Status Title Indexability  Words
3  https://example.com/blog/python/     200   NaN    Indexable   1500
7   https://example.com/empty-page/     200   NaN    Indexable     80

كده إحنا مش بنطلع أي خلية Title فاضية وخلاص. إحنا حددنا نوع الصفحات اللي عايزين نراجعها.

التعامل مع النصوص داخل الأعمدة

في الدرس السابع استخدمنا "/blog/" in url علشان نعرف هل URL فيه /blog/.

مع Pandas إحنا مش بنتعامل مع URL واحد. عندنا عمود كامل. لو عايزين الصفوف اللي URL بتاعها يحتوي على /blog/، هنستخدم str.contains().

عندنا df["URL"] يعني عمود URL. ولأن القيم اللي جواه Strings، هنستخدم .str وبعدها .contains("/blog/"). يبقى الشكل:

نجرب:

مثال
الناتج المتوقع
                                URL  Status      Title Indexability  Words
2     https://example.com/blog/seo/     200  SEO Guide    Indexable   2200
3  https://example.com/blog/python/     200        NaN    Indexable   1500

دلوقتي بدأنا نطبق نفس منطق الدروس السابقة، لكن على آلاف الصفوف مرة واحدة.

خد بالك من علامة ? في str.contains()

في الدرس السابع طلّعنا الروابط اللي فيها Parameters. طيب لو عايزين نعملها بـ Pandas؟ أول حاجة هتيجي في بالك:

مثال

لو شغلته هتلاقي Error شكله غريب: nothing to repeat at position 0

ليه؟ لأن str.contains() بشكل افتراضي بتفهم النص اللي بتديهولها على إنه Regular Expression، مش نص عادي. وفي الـ Regex علامة ?ليها معنى خاص.

مش هندخل في Regex دلوقتي. كل اللي محتاج تعرفه: لو عايز تدور على النص زي ما هو بالظبط، ضيف regex=False:

مثال
الناتج المتوقع
                                    URL  Status
1      https://example.com/shop/?page=2     200
2  https://example.com/shop/?sort=price     200

ونفس الكلام لو بتدور على علامة زي . أو + أو (. وخلي بالك إن . بالذات مش هتطلع Error، هتطلع نتيجة غلط وإنت مش واخد بالك. فلو مش قاصد Regex، استخدم regex=False وريّح دماغك.

طول الـ Title

من أول الحاجات اللي بتتبص عليها في أي Audit: الـ Title طويل زيادة ولا لأ؟

في بايثون العادي كنا بنعرف طول النص بـ len(). في Pandas عندنا نفس الفكرة على العمود كله: str.len().

عندنا الملف ده:

نجرب:

مثال
الناتج المتوقع
                                    URL  Title Length
0                  https://example.com/             4
1  https://example.com/blog/python-seo/            69
2            https://example.com/about/             8
3         https://example.com/services/            12

ركز في السطر ده:

إحنا هنا عملنا عمود جديد اسمه Title Length مكانش موجود في الملف أصلًا. يعني بنقول لـ Pandas:

ملحوظة
احسب طول كل Title، وحط النتيجة في عمود جديد اسمه Title Length.

ودي فكرة هتستخدمها كتير: تحسب حاجة من الأعمدة الموجودة، وتحطها في عمود جديد تقدر تفلتر عليه.

دلوقتي نطلّع الـ Titles اللي أطول من 60 حرف:

مثال
الناتج المتوقع
                                    URL  Title Length
1  https://example.com/blog/python-seo/            69

بس خلي بالك: 60 حرف مش قانون. ده Threshold إنت اللي اخترته. جوجل بتقص الـ Title في النتايج حسب عرضه مش بعدد الحروف بالظبط، والـ Title الطويل مش دايمًا مشكلة. البرنامج هنا بيطلّع لك صفحات تراجعها، مش أخطاء لازم تتصلح.

الـ Titles المكررة

مشكلة تانية مشهورة جدًا: أكتر من صفحة ليهم نفس الـ Title.

عندنا الملف ده:

Pandas عندها duplicated(). نجرب:

مثال
الناتج المتوقع
0    False
1     True
2    False
3     True
4    False
Name: Title, dtype: bool

لاحظ حاجة مهمة. أول صفحة /shoes/ طلعت False، مع إن الـ Title بتاعها متكرر.

ليه؟ لأن duplicated() بتعتبر أول ظهور للقيمة هو الأصل، وأي مرة بعد كده هي المكررة.

لكن في SEO إحنا عايزين نشوف كل الصفحات اللي داخلة في التكرار، علشان نقارن بينهم. هنا نستخدم keep=False، ومعناها: متعتبرش أي واحدة فيهم الأصل، علّم عليهم كلهم.

من غير ما تشغّل، توقّع الناتج:

توقّع الناتج

كام صف هيكون في duplicate_titles؟

نشوف الصفوف نفسها:

مثال
الناتج المتوقع
                                     URL  Status             Title
0             https://example.com/shoes/     200  Buy Shoes Online
1   https://example.com/shoes/?color=red     200  Buy Shoes Online
3  https://example.com/shoes/?color=blue     200  Buy Shoes Online

ولاحظ إن التلات صفحات دول هما نفس الصفحة بـ Parameters مختلفة. ده ممكن يكون طبيعي لو عليهم Canonical صح. فمرة تانية: البرنامج بيكتشف، وإنت اللي بتقرر.

ولو الصفوف نفسها متكررة؟

أحيانًا بتجمع روابط من أكتر من Export، فنفس الـ URL يتكرر كصف كامل. هنا نستخدم drop_duplicates():

مثال
الناتج المتوقع
Before: 5
After: 3

subset="URL" معناها: اعتبر الصف مكرر لو الـ URL بتاعه اتكرر. ولاحظ إننا كتبنا df = قبلها، لأن drop_duplicates() مش بتعدّل df نفسها، بترجع نسخة جديدة من غير التكرار.

ودي نفس فكرة الـ Set اللي اتعلمناها في الدرس الثاني، بس على جدول كامل.

نحفظ النتيجة في CSV

افترض إننا طلعنا صفحات 404 وعايزين نحفظها. هنستخدم to_csv(). نكتب:

مثال
الناتج المتوقع
,URL,Status,Title,Indexability
2,https://example.com/old-page/,404,Old Page,Non-Indexable

ده هيعمل الملف. لكن لو فتحته، هتلاقي Pandas حفظت كمان الـ Index. إحنا مش محتاجينه في التقرير. فنستخدم index=False. يبقى:

مثال
الناتج المتوقع
URL,Status,Title,Indexability
https://example.com/old-page/,404,Old Page,Non-Indexable

نقرأ ملف Excel

لحد دلوقتي استخدمنا pd.read_csv(). لكن في الدرس السابق قلنا إن Excel مختلف عن CSV.

الميزة هنا إن Pandas تقدر تقرأ Excel برضه. لو عندك crawl.xlsx تقدر تستخدم:

لكن قراءة ملفات .xlsx غالبًا تحتاج مكتبة إضافية اسمها openpyxl. لو مش موجودة عندك، ثبتها من الـ Terminal:

بعدها df = pd.read_excel("crawl.xlsx") تقرأ بيانات Excel وتحولها لنفس الحاجة اللي اتعاملنا معاها طول الدرس: DataFrame.

ودي نقطة مهمة جدًا. بغض النظر البيانات جاية من CSV أو Excel، بعد ما Pandas تقراها، إحنا بنتعامل معاها كـ DataFrame.

حفظ النتيجة كـ Excel

زي ما عندنا to_csv() عندنا to_excel(). مثلًا:

ولو openpyxl مثبتة، هيتعمل ملف Excel.

خلينا نعمل تحليل SEO متكامل

هنستخدم ملف أكبر شوية crawl.csv ومحتواه:

هنقرأ الملف، وعايزين صفحات 404، وRedirects، وصفحات 200 غير القابلة للفهرسة، وصفحات 200 بدون Title، وصفحات 200 اللي عدد الكلمات فيها أقل من 100.

لاحظ الشرط df["Words"] < 100. مفيهوش حاجة جديدة من ناحية بايثون. إحنا اتعلمنا < قبل كده. الجديد فقط إن Pandas بتطبق المقارنة على كل Row في العمود.

مثال
الناتج المتوقع
First Rows:
                                   URL  Status      Title   Indexability  Words
0                 https://example.com/     200       Home      Indexable    850
1           https://example.com/about/     200   About Us      Indexable    600
2        https://example.com/blog/seo/     200  SEO Guide      Indexable   2200
3     https://example.com/blog/python/     200        NaN      Indexable   1500
4  https://example.com/product/laptop/     200     Laptop  Non-Indexable    300

Columns:
Index(['URL', 'Status', 'Title', 'Indexability', 'Words'], dtype='str')

File Shape:
(8, 5)

Status Codes:
Status
200    6
404    1
301    1
Name: count, dtype: int64

404 Pages:
                             URL  Status
5  https://example.com/old-page/     404

Redirects:
                             URL  Status
6  https://example.com/redirect/     301

200 Non-Indexable Pages:
                                   URL   Indexability
4  https://example.com/product/laptop/  Non-Indexable

Missing Titles:
                                URL Title
3  https://example.com/blog/python/   NaN
7   https://example.com/empty-page/   NaN

Low Word Pages:
                               URL  Words
7  https://example.com/empty-page/     80

وفي نفس المجلد ممكن نحفظ كل مجموعة في ملفها الخاص:

يعني برنامج واحد يقرأ Crawl واحد ويطلع منه أكتر من Report. وده جزء كبير من القيمة اللي هنبدأ نبني عليها المشروع النهائي للمرحلة.

ملف Screaming Frog الحقيقي مش شبه ملفاتنا

لحد دلوقتي كل الملفات اللي اشتغلنا عليها أعمدتها بسيطة: URL وStatus وTitle.

لكن لما تعمل Export حقيقي من Screaming Frog، الأعمدة هتبقى بالشكل ده:

نشوف الأعمدة:

مثال
الناتج المتوقع
Index(['Address', 'Status Code', 'Title 1', 'Indexability', 'Word Count'], dtype='str')

لو كتبت الكود اللي اتعلمناه زي ما هو:

مثال

هيطلع KeyError: 'Status'، لأن العمود اسمه Status Code مش Status.

عندك حلين.

الحل الأول: تستخدم الأسماء زي ما هي

أسماء الأعمدة ممكن يكون فيها مسافات عادي، طالما كتبتها بين الأقواس صح:

مثال
الناتج المتوقع
1    https://example.com/old-page/
Name: Address, dtype: str

الحل التاني: تغيّر أسماء الأعمدة

لو عايز الكود اللي كتبته طول الدرس يشتغل زي ما هو، غيّر أسماء الأعمدة للأسماء اللي متعود عليها باستخدام rename():

مثال
الناتج المتوقع
Index(['URL', 'Status', 'Title', 'Indexability', 'Words'], dtype='str')

جوه rename() إحنا بعتنا Dictionary. الـ Key هو الاسم القديم، والـ Value هو الاسم الجديد. زي ما اتعلمنا في الدرس الثاني بالظبط.

وخلي بالك من df = في الأول. rename() زي drop_duplicates()، مش بتعدّل df نفسها، بترجع نسخة جديدة. لو نسيت df = الأسماء هتفضل زي ما هي.

ومفيش أداتين الأعمدة بتاعتهم بنفس الأسماء. Ahrefs غير Semrush غير Search Console. علشان كده أول حاجة تعملها مع أي ملف جديد هي print(df.columns)، وبعدها تقرر.

نجمع بيانات من ملفين: merge()

في الدرس الثامن قلنا إن من أهم الحاجات اللي هنعملها بـ Pandas: دمج الـ Crawl مع Search Console. تعالى نفكر ليه ده مهم.

ملف الـ Crawl بيقولك الصفحة شغالة ولا لأ، ومفهرسة ولا لأ. لكنه ميعرفش حاجة عن الزيارات. وملف Search Console بيقولك كل صفحة جابت كام Click، لكنه ميعرفش الصفحة Status بتاعها إيه.

لما نحطهم جنب بعض، نقدر نسأل أسئلة مكانش ينفع نسألها لكل ملف لوحده.

عندنا ملف الـ Crawl:

وملف Search Console:

أول حاجة، نشوف أعمدة Search Console:

مثال
الناتج المتوقع
Index(['Top pages', 'Clicks', 'Impressions', 'CTR', 'Position'], dtype='str')

عمود الروابط هنا اسمه Top pages، وفي الـ Crawl اسمه URL. وعلشان نربط الملفين لازم العمود المشترك يبقى بنفس الاسم. فنستخدم rename() اللي لسه متعلمينها.

بعدها نستخدم merge():

مثال
الناتج المتوقع
                                        URL  Status  Clicks
0                      https://example.com/     200  1200.0
1             https://example.com/blog/seo/     200   340.0
2          https://example.com/blog/python/     200     NaN
3   https://example.com/services/seo-audit/     200    90.0
4  https://example.com/services/consulting/     200    25.0
5        https://example.com/blog/old-post/     404     4.0

خلينا نفهم السطر ده:

يعني
  1. 1crawl هو الجدول الأساسي.
  2. 2gsc هو الجدول اللي هنجيب منه بيانات زيادة.
  3. 3on="URL" يعني اربط الصفين اللي ليهم نفس الـ URL.
  4. 4how="left" يعني خلي كل صفحات الـ Crawl زي ما هي، حتى لو ملهاش بيانات في Search Console.

ولاحظ حاجتين.

الأولى: صفحة /blog/python/ الـ Clicks بتاعها NaN. دي صفحة موجودة في الـ Crawl، لكن ملهاش أي صف في Search Console.

التانية: الأرقام بقت 1200.0 بدل 1200. ده لأن العمود بقى فيه NaN، وPandas بتحوله لنوع رقمي يقبل القيم المفقودة. مش هنقف عندها دلوقتي.

دلوقتي الأسئلة اللي مكانش ينفع نسألها

صفحات 200 ملهاش أي بيانات في Search Console:

مثال
الناتج المتوقع
2    https://example.com/blog/python/
Name: URL, dtype: str

صفحة شغالة، ومفيش ليها أي ظهور. ليه؟ ممكن جديدة. ممكن مش متفهرسة فعلًا. ممكن مفيش Internal Links بتوصلها. دي صفحة تستاهل تتراجع.

وصفحات 404 لسه بتجيب Clicks:

مثال
الناتج المتوقع
                                  URL  Clicks
5  https://example.com/blog/old-post/     4.0

دي من أهم النتائج في أي Audit. صفحة واقعة، وجوجل لسه بيبعت لها ناس. غالبًا دي أول صفحة تفكر لها في Redirect. لكن زي ما قلنا: البرنامج بيوريك الصفحة، والقرار عندك.

كل قسم في الموقع بيجيب كام Click؟ groupby()

في الدرس السابع عرفنا نجيب القسم اللي الصفحة تابعة له بـ split("/"). في Pandas نقدر نعملها على العمود كله مرة واحدة:

مثال
الناتج المتوقع
['', 'blog', 'blog', 'services', 'services', 'blog']

زي ما عملنا في الدرس السابع بالظبط: str.split("/") قسّمت كل رابط لـ List، وstr[3] خدت العنصر رقم 3 من كل List. وtolist() حوّلت العمود لـ List عادية علشان نشوفه بسهولة.

الصفحة الرئيسية ملهاش قسم، فطلعت نص فاضي. هنسميها home علشان تبقى واضحة، باستخدام replace() اللي اتعلمناها في الدرس السابع:

دلوقتي عايزين نعرف كل قسم بيجيب كام Click. هنا بنستخدم groupby():

مثال
الناتج المتوقع
Section
blog         344.0
home        1200.0
services     115.0
Name: Clicks, dtype: float64

اقرأ السطر ده كده:

ملحوظة
جمّع الصفوف حسب Section، وبعدين اجمع الـ Clicks بتاعة كل مجموعة.

في ثواني عرفت إن الـ Blog بيجيب 344 Click والـ Services بيجيب 115. ولو الموقع فيه 50,000 صفحة، نفس السطر هيشتغل بنفس الطريقة.

ولاحظ إن الـ 344 فيهم 4 Clicks جايين من صفحة 404. يعني حتى الأرقام المجمّعة محتاج تبص على اللي جواها قبل ما تبني عليها قرار.

دلوقتي جرّب إنت:

تحدّي SEO

ادمج الـ Crawl مع Search Console

عندك crawl.csv وgsc.csv (عمود الروابط فيه اسمه Top pages). غيّر اسم العمود لـ URL، وادمج الملفين في DataFrame اسمها merged بحيث تفضل كل صفحات الـ Crawl. بعدها اعمل broken_with_clicks فيها صفحات 404 اللي لسه بتجيب Clicks، وno_search_data فيها صفحات 200 اللي ملهاش أي بيانات في Search Console.

Pandas وفرت علينا إيه؟

تعالى نقارن جزء صغير. بالطريقة القديمة:

وباستخدام Pandas:

لكن أهم نقطة هنا مش إن الكود أقصر. القيمة الحقيقية إننا بقينا بنتعامل مع البيانات كجدول.

يعني نقدر نعمل على نفس البيانات: Filter وSort وCount واختيار أعمدة، واكتشاف قيم مفقودة، وExport — من غير ما نعيد بناء كل عملية باستخدام Loop جديدة.

وده السبب الحقيقي اللي بيخلي Pandas مهمة في SEO.

متستخدمش Pandas وتحاول تنسى بايثون

في غلطة بتحصل كتير أثناء التعلم. الشخص يبدأ Pandas ويقول:

خد بالك
خلاص أنا مش محتاج Loops أو Conditions أو Lists.

وده غلط. اللي اتعلمناه قبل Pandas هو اللي بيخليك تفهم أصلًا إنت بتعمل إيه.

لما تشوف df["Status"] == 404 إنت فاهم المقارنة.

ولما تشوف (df["Status"] == 200) & (df["Words"] < 100) إنت فاهم إن عندك شرطين.

ولما تشوف ["URL", "Status", "Title"] إنت عارف إن دي List.

ولما نكتب Functions بعد كده علشان ننظم التحليل، إنت بالفعل فاهم Functions.

Pandas مش بديل عن بايثون. هي أداة بنستخدمها فوق الأساس اللي اتعلمناه.

إيه اللي لازم تكون فاهمه من الدرس؟

لما تقرأ CSV: df = pd.read_csv("crawl.csv") النتيجة DataFrame.

تقدر تشوف أول Rows باستخدام df.head(). وتعرف أسماء الأعمدة df.columns. وتعرف عدد الصفوف والأعمدة df.shape.

وتختار عمود df["URL"]. وتختار أكتر من عمود df[["URL", "Status"]].

وتعمل Filter df[df["Status"] == 404]. ولو عندك شرطين df[(df["Status"] == 200) & (df["Indexability"] == "Non-Indexable")].

وتعرف القيم المفقودة df["Title"].isna(). وتعد القيم df["Status"].value_counts(). وترتب البيانات df.sort_values("Words").

وتحفظ النتيجة df.to_csv("result.csv", index=False). وكمان تقدر تقرأ Excel pd.read_excel("file.xlsx") وتحفظ Excel df.to_excel("result.xlsx", index=False).

وتعرف طول النصوص df["Title"].str.len()، والقيم المكررة df["Title"].duplicated(keep=False)، وتغيّر أسماء الأعمدة df.rename(columns={...}).

وتدمج ملفين pd.merge(crawl, gsc, on="URL", how="left")، وتجمّع حسب قيمة معينة df.groupby("Section")["Clicks"].sum().

مش مطلوب تحفظ القائمة دي. المطلوب إنك تكون فاهم كل واحدة استخدمناها ليه.

الاختبار التطبيقي

ملحوظة
الملف seo-audit.csv جاهز ليك بنفس المحتوى اللي هيتقالك. الناتج مخفي لحد ما تحل.

اعمل ملف اسمه seo-audit.csv وحط فيه:

المطلوب الأول تقرأ الملف باستخدام Pandas pd.read_csv(). بعدها اطبع أول 5 Rows. ثم اطبع أسماء الأعمدة. ثم اطبع df.shape واعرف بنفسك كام Row وكام Column؟

بعدها اعمل DataFrame اسمها broken_pages تحتوي على صفحات 404 فقط.

واعمل redirects تحتوي على صفحات 301.

واعمل non_indexable_200 تحتوي على الصفحات اللي تحقق الشرطين: Status = 200 وIndexability = Non-Indexable.

واعمل missing_titles تحتوي على صفحات 200 اللي Title بتاعها مفقود.

واعمل low_word_pages تحتوي على صفحات 200 اللي عدد الكلمات فيها أقل من 100.

بعدها اطبع عدد الصفحات الموجودة لكل Status باستخدام value_counts(). المفروض النتيجة تكون 200 سبع مرات، و301 مرة، و404 مرة.

بعد كده طلّع صفحات Blog فقط باستخدام str.contains(). المفروض تحصل على https://example.com/blog/seo/ وhttps://example.com/blog/python/.

بعدها رتب صفحات 200 حسب عدد الكلمات من الأقل للأكبر.

وأخيرًا احفظ broken_pages داخل 404-pages.csv، واحفظ missing_titles داخل missing-titles.csv، واحفظ low_word_pages داخل low-word-pages.csv، مع منع Pandas من حفظ الـ Index داخل الملفات. يعني لازم تستخدم index=False.

تحدّي SEO

حلّل ملف SEO كامل بـ Pandas

اقرأ seo-audit.csv بـ pd.read_csv()، واعمل broken_pages وredirects وnon_indexable_200 وmissing_titles وlow_word_pages زي ما اتشرح فوق، ثم احفظ الثلاثة الأولى منها في ملفات CSV منفصلة من غير Index.

لو قدرت تعمل الاختبار ده بنفسك، فأنت وصلت لنقطة مهمة جدًا في المرحلة الأولى. إنت دلوقتي تقدر تاخد ملف SEO حقيقي، تقرأه، تفهم أعمدته، تعمل Filtering، تجمع أكتر من شرط، تكتشف Missing Values، ترتب البيانات، تعد القيم، وتطلع Reports جديدة.

وده الأساس اللي هنستخدمه في الدرس العاشر، لأننا مش هنتعلم فيه أمر بايثون جديد لمجرد التعلم، هنبدأ نجمع اللي اتعلمناه كله في مشروع SEO Audit Analyzer كامل.

بقيت تمارين لم تحلّها

0/3

التمارين المتبقية: 3 من 3. يُحتسب الدرس مكتملًا حين تحلّها كلها، ويمكنك العودة إليها متى شئت.

أحسنت، وصلت إلى نهاية الدرس.