شرح Pandas بالعربي لتحليل بيانات السيو
50 دقيقة تقريبًا
- Variable
- String
- Integer
- Float
- Boolean
- List
- Dictionary
- 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فتحنا الملف.
- 2قرأنا الصفوف.
- 3مررنا على كل Row.
- 4فحصنا شرط.
- 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 بشكل أقرب إلى:
وهي تعمل الفلترة. لكن قبل ما نستخدمها، لازم نثبتها.
تثبيت Pandas
مكتبة csv اللي استخدمناها في الدرس السابق موجودة أصلًا مع بايثون. علشان كده كتبنا import csv وخلاص.
لكن Pandas مكتبة خارجية، يعني لازم تتثبت على الجهاز الأول. افتح الـ Terminal أو Command Prompt واكتب:
بعد انتهاء التثبيت، تقدر تستخدمها في بايثون.
لو الأمر السابق معملش عندك بشكل صحيح، ممكن تستخدم:
التثبيت بتعمله مرة واحدة على بيئة بايثون اللي شغال عليها، مش كل مرة تكتب فيها برنامج.
بعد التثبيت نقدر نبدأ.
استيراد Pandas
نكتب:
ممكن تسأل: ليه مكتبناش import pandas؟ ينفع. لكن المتعارف عليه تقريبًا في كل مكان إننا نستورد Pandas بالاسم المختصر pd.
يعني بدل ما نكتب بعد كده pandas.read_csv() هنكتب pd.read_csv().
إذن import pandas as 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 اختصار شائع لكلمة 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أول
Row: 200 == 404 →النتيجة False. - 2ثالث
Row: 404 == 404 →النتيجة True. - 3يعني
df["Status"] == 404لم ترجع الصفحات نفسها. - 4هي رجعت لنا مجموعة True وFalse بتحدد الصفوف اللي حققت الشرط.
نستخدم النتيجة في الفلترة
دلوقتي نقدر نقول:
نكتب:
URL Status Title Indexability 2 https://example.com/old-page/ 404 Old Page Non-Indexable
وده أول Filter باستخدام Pandas.
السطر df["Status"] == 404 هو الشرط. والجزء df[ ... ] بيقول:
فلما جمعناهم 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:
ودي فكرة هتستخدمها كتير: تحسب حاجة من الأعمدة الموجودة، وتحطها في عمود جديد تقدر تفلتر عليه.
دلوقتي نطلّع الـ 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
خلينا نفهم السطر ده:
- 1crawl هو الجدول الأساسي.
- 2gsc هو الجدول اللي هنجيب منه بيانات زيادة.
- 3
on="URL"يعني اربط الصفين اللي ليهم نفس الـ URL. - 4
how="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
اقرأ السطر ده كده:
في ثواني عرفت إن الـ Blog بيجيب 344 Click والـ Services بيجيب 115. ولو الموقع فيه 50,000 صفحة، نفس السطر هيشتغل بنفس الطريقة.
ولاحظ إن الـ 344 فيهم 4 Clicks جايين من صفحة 404. يعني حتى الأرقام المجمّعة محتاج تبص على اللي جواها قبل ما تبني عليها قرار.
دلوقتي جرّب إنت:
ادمج الـ 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 ويقول:
وده غلط. اللي اتعلمناه قبل 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 كامل بـ 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. يُحتسب الدرس مكتملًا حين تحلّها كلها، ويمكنك العودة إليها متى شئت.
أحسنت، وصلت إلى نهاية الدرس.