استخلاص البيانات من الويب بايثون 3: دليل عملي للمطورين
مقدمة إلى استخلاص البيانات من الويب
استخلاص البيانات من الويب (Web Scraping) هو عملية جمع المعلومات المنظمة من المواقع الإلكترونية. قبل الشروع في هذه العملية، من الضروري التحقق من شروط خدمة الموقع المستهدف للتأكد من سماحه بذلك. كما يُنصح بالبحث عن واجهات برمجة التطبيقات (APIs) التي قد يوفرها الموقع كبديل أكثر أمانًا وفعالية. عمليات الاستخلاص المكثفة قد تضع ضغطًا هائلاً على خوادم الموقع، مما قد يؤدي إلى حجب الخدمة (Denial of Service)، وهو أمر يجب تجنبه.
لمن هذا الدليل؟
هذا المقال موجه للقراء المتقدمين الذين لديهم إلمام مسبق بلغة البرمجة بايثون. على الأقل، يجب أن تكون على دراية بمفاهيم مثل فهم القوائم (list comprehension)، ومديري السياق (context manager)، والدوال (functions). كما يجب أن تعرف كيفية إعداد بيئة افتراضية (virtual environment). سنقوم بتشغيل الأكواد على جهازك المحلي لاستكشاف بعض المواقع، ومع بعض التعديلات، يمكنك تشغيلها على خادم أيضًا.
ماذا ستتعلم في هذا المقال؟
بنهاية هذا المقال، ستكون قادرًا على تنزيل صفحة ويب، وتحليلها لاستخراج المعلومات الهامة، وتنسيقها بطريقة قابلة للاستخدام لمزيد من المعالجة. تُعرف هذه العملية أيضًا باسم ETL (Extract, Transform, Load). سيوضح هذا المقال أيضًا كيفية التعامل مع المواقع التي تستخدم JavaScript لعرض المحتوى، مثل تطبيقات React.js أو Angular.
المتطلبات الأساسية
قبل البدء، نحتاج إلى التأكد من جاهزيتنا. يرجى إعداد بيئة بايثون افتراضية وتثبيت الحزم التالية فيها:
beautifulsoup4(الإصدار 4.9.0 وقت كتابة هذا المقال)requests(الإصدار 2.23.0 وقت كتابة هذا المقال)wordcloud(الإصدار 1.17.0 وقت كتابة هذا المقال، اختياري)selenium(الإصدار 3.141.0 وقت كتابة هذا المقال، اختياري)
يمكنك العثور على الكود الخاص بهذا المشروع في مستودع git هذا على GitHub.
لأغراض هذا المثال، سنقوم باستخلاص بيانات “القانون الأساسي لجمهورية ألمانيا الاتحادية” (Basic Law for the Federal Republic of Germany). (لا تقلق، لقد تحققت من شروط الخدمة الخاصة بهم. إنهم يقدمون نسخة XML للمعالجة الآلية، لكن هذه الصفحة ستكون بمثابة مثال لمعالجة HTML، لذا لا توجد مشكلة).
الخطوة الأولى: تنزيل المصدر
إنشاء قائمة عناوين URL
أولاً، سأقوم بإنشاء ملف باسم urls.txt يحتوي على جميع عناوين URL التي أرغب في تنزيلها:
https://www.gesetze-im-internet.de/gg/art_1.html
https://www.gesetze-im-internet.de/gg/art_2.html
https://www.gesetze-im-internet.de/gg/art_3.html
https://www.gesetze-im-internet.de/gg/art_4.html
https://www.gesetze-im-internet.de/gg/art_5.html
https://www.gesetze-im-internet.de/gg/art_6.html
https://www.gesetze-im-internet.de/gg/art_7.html
https://www.gesetze-im-internet.de/gg/art_8.html
https://www.gesetze-im-internet.de/gg/art_9.html
https://www.gesetze-im-internet.de/gg/art_10.html
https://www.gesetze-im-internet.de/gg/art_11.html
https://www.gesetze-im-internet.de/gg/art_12.html
https://www.gesetze-im-internet.de/gg/art_12a.html
https://www.gesetze-im-internet.de/gg/art_13.html
https://www.gesetze-im-internet.de/gg/art_14.html
https://www.gesetze-im-internet.de/gg/art_15.html
https://www.gesetze-im-internet.de/gg/art_16.html
https://www.gesetze-im-internet.de/gg/art_16a.html
https://www.gesetze-im-internet.de/gg/art_17.html
https://www.gesetze-im-internet.de/gg/art_17a.html
https://www.gesetze-im-internet.de/gg/art_18.html
https://www.gesetze-im-internet.de/gg/art_19.html
كود تنزيل صفحات HTML
بعد ذلك، سأكتب بعض أكواد بايثون في ملف يسمى scraper.py لتنزيل محتوى HTML لهذه الملفات. في سيناريو حقيقي، قد يكون هذا مكلفًا للغاية، وستستخدم قاعدة بيانات بدلاً من ذلك. لتبسيط الأمور، سأقوم بتنزيل الملفات في نفس الدليل واستخدام اسمها كاسم للملف.
from os import path
from pathlib import PurePath
import requests
with open('urls.txt', 'r') as fh:
urls = fh.readlines()
urls = [url.strip() for url in urls] # strip `\n`
for url in urls:
file_name = PurePath(url).name
file_path = path.join('.', file_name)
text = ''
try:
response = requests.get(url)
if response.ok:
text = response.text
except requests.exceptions.ConnectionError as exc:
print(exc)
with open(file_path, 'w') as fh:
fh.write(text)
print('Written to', file_path)
من خلال تنزيل الملفات، يمكنني معالجتها محليًا قدر ما أريد دون الاعتماد على الخادم. حاول أن تكون “مواطن ويب صالح”، أليس كذلك؟
الخطوة الثانية: تحليل المصدر
فحص بنية HTML
الآن بعد أن قمت بتنزيل الملفات، حان الوقت لاستخراج الميزات الهامة منها. لذلك، سأنتقل إلى إحدى الصفحات التي قمت بتنزيلها، وأفتحها في متصفح الويب، وأضغط على Ctrl-U لعرض مصدرها. سيكشف فحص المصدر عن بنية HTML. في حالتي، وجدت أنني أريد نص القانون بدون أي ترميز (markup). العنصر الذي يغلفه له معرف (id) هو container.
استخدام BeautifulSoup لتحليل المحتوى
باستخدام مكتبة BeautifulSoup، يمكنني أن أرى أن مزيجًا من الدالة find والدالة get_text() سيحقق ما أريد. بما أن لدي خطوة ثانية الآن، سأقوم بإعادة هيكلة الكود قليلًا عن طريق وضعه في دوال وإضافة واجهة سطر أوامر (CLI) بسيطة.
from os import path
from pathlib import PurePath
import sys
from bs4 import BeautifulSoup
import requests
def download_urls(urls, dir):
paths = []
for url in urls:
file_name = PurePath(url).name
file_path = path.join(dir, file_name)
text = ''
try:
response = requests.get(url)
if response.ok:
text = response.text
else:
print('Bad response for', url, response.status_code)
except requests.exceptions.ConnectionError as exc:
print(exc)
with open(file_path, 'w') as fh:
fh.write(text)
paths.append(file_path)
return paths
def parse_html(path):
with open(path, 'r') as fh:
content = fh.read()
return BeautifulSoup(content, 'html.parser')
def download(urls):
return download_urls(urls, '.')
def extract(path):
return parse_html(path)
def transform(soup):
container = soup.find(id='container')
if container is not None:
return container.get_text()
def load(key, value):
d = {}
d[key] = value
return d
def run_single(path):
soup = extract(path)
content = transform(soup)
unserialised = load(path, content.strip() if content is not None else '')
return unserialised
def run_everything():
l = []
with open('urls.txt', 'r') as fh:
urls = fh.readlines()
urls = [url.strip() for url in urls]
paths = download(urls)
for path in paths:
print('Written to', path)
l.append(run_single(path))
print(l)
if __name__ == "__main__":
args = sys.argv
if len(args) is 1:
run_everything()
else:
if args[1] == 'download':
download([args[2]])
print('Done')
if args[1] == 'parse':
path = args[2]
result = run_single(path)
print(result)
تشغيل الكود
الآن يمكنني تشغيل الكود بثلاث طرق:
- بدون أي وسيطات لتشغيل كل شيء (أي تنزيل جميع عناوين
URLواستخراجها، ثم حفظها على القرص) عبر:python scraper.py - مع وسيط
downloadوعنوانURLلتنزيله:python scraper.py download https://www.gesetze-im-internet.de/gg/art_1.html. هذا لن يعالج الملف. - مع وسيط
parseومسار ملف لتحليله:python scraper.py art_1.html. هذا سيتخطى خطوة التنزيل.
مع ذلك، هناك شيء أخير مفقود.
الخطوة الثالثة: تنسيق المصدر لمزيد من المعالجة
إنشاء سحابة الكلمات (Word Cloud)
لنفترض أنني أرغب في إنشاء سحابة كلمات لكل مقال. يمكن أن تكون هذه طريقة سريعة للحصول على فكرة عن محتوى النص. لهذا الغرض، قم بتثبيت حزمة wordcloud وقم بتحديث الملف على النحو التالي:
from os import path
from pathlib import Path, PurePath
import sys
from bs4 import BeautifulSoup
import requests
from wordcloud import WordCloud
STOPWORDS_ADDENDUM = ['Das', 'Der', 'Die', 'Diese', 'Eine', 'In', 'InhaltsverzeichnisGrundgesetz', 'im', 'Jede', 'Jeder', 'Kein', 'Sie', 'Soweit', 'Über']
STOPWORDS_FILE_PATH = 'stopwords.txt'
STOPWORDS_URL = 'https://raw.githubusercontent.com/stopwords-iso/stopwords-de/master/stopwords-de.txt'
def download_urls(urls, dir):
paths = []
for url in urls:
file_name = PurePath(url).name
file_path = path.join(dir, file_name)
text = ''
try:
response = requests.get(url)
if response.ok:
text = response.text
else:
print('Bad response for', url, response.status_code)
except requests.exceptions.ConnectionError as exc:
print(exc)
with open(file_path, 'w') as fh:
fh.write(text)
paths.append(file_path)
return paths
def parse_html(path):
with open(path, 'r') as fh:
content = fh.read()
return BeautifulSoup(content, 'html.parser')
def download_stopwords():
stopwords = ''
try:
response = requests.get(STOPWORDS_URL)
if response.ok:
stopwords = response.text
else:
print('Bad response for', url, response.status_code)
except requests.exceptions.ConnectionError as exc:
print(exc)
with open(STOPWORDS_FILE_PATH, 'w') as fh:
fh.write(stopwords)
return stopwords
def download(urls):
return download_urls(urls, '.')
def extract(path):
return parse_html(path)
def transform(soup):
container = soup.find(id='container')
if container is not None:
return container.get_text()
def load(filename, text):
if Path(STOPWORDS_FILE_PATH).exists():
with open(STOPWORDS_FILE_PATH, 'r') as fh:
stopwords = fh.readlines()
else:
stopwords = download_stopwords()
# Strip whitespace around stopwords
stopwords = [stopword.strip() for stopword in stopwords]
# Extend stopwords with own ones, which were determined after first run
stopwords = stopwords + STOPWORDS_ADDENDUM
try:
cloud = WordCloud(stopwords=stopwords).generate(text)
cloud.to_file(filename.replace('.html', '.png'))
except ValueError:
print('Could not generate word cloud for', key)
def run_single(path):
soup = extract(path)
content = transform(soup)
load(path, content.strip() if content is not None else '')
def run_everything():
with open('urls.txt', 'r') as fh:
urls = fh.readlines()
urls = [url.strip() for url in urls]
paths = download(urls)
for path in paths:
print('Written to', path)
run_single(path)
print('Done')
if __name__ == "__main__":
args = sys.argv
if len(args) is 1:
run_everything()
else:
if args[1] == 'download':
download([args[2]])
print('Done')
if args[1] == 'parse':
path = args[2]
run_single(path)
print('Done')
التغييرات المطبقة
ما الذي تغير؟ أولاً، قمت بتنزيل قائمة بالكلمات الوقفية الألمانية (German stopwords) من GitHub. بهذه الطريقة، يمكنني إزالة الكلمات الأكثر شيوعًا من نص القانون الذي تم تنزيله. ثم أقوم بإنشاء مثيل WordCloud مع قائمة الكلمات الوقفية التي قمت بتنزيلها ونص القانون. سيتم تحويله إلى صورة بنفس الاسم الأساسي. بعد التشغيل الأول، اكتشفت أن قائمة الكلمات الوقفية غير مكتملة. لذلك أضفت كلمات إضافية أرغب في استبعادها من الصورة الناتجة. بهذا، يكون الجزء الرئيسي من استخلاص الويب قد اكتمل.
مكافأة: ماذا عن تطبيقات الصفحة الواحدة (SPAs)؟
تطبيقات الصفحة الواحدة (SPAs – Single Page Applications) هي تطبيقات ويب يتم التحكم في تجربتها بالكامل بواسطة JavaScript، والذي يتم تنفيذه في المتصفح. على هذا النحو، فإن تنزيل ملف HTML لا يجدي نفعًا كبيرًا. فماذا يجب أن نفعل بدلاً من ذلك؟
استخدام Selenium للتعامل مع JavaScript
سنستخدم المتصفح نفسه، وذلك بمساعدة مكتبة Selenium. تأكد من تثبيت مشغل (driver) أيضًا. قم بتنزيل أرشيف .tar.gz وفك ضغطه في مجلد bin الخاص ببيئتك الافتراضية حتى يتمكن Selenium من العثور عليه. هذا هو الدليل الذي يمكنك أن تجد فيه السكربت activate (على أنظمة GNU/Linux).
كمثال، سأستخدم موقع Angular هنا. Angular هو إطار عمل SPA شائع مكتوب بلغة JavaScript ومضمون أن يتم التحكم فيه بواسطتها في الوقت الحالي. نظرًا لأن الكود سيكون أبطأ، سأقوم بإنشاء ملف جديد يسمى crawler.py له. يبدو المحتوى كالتالي:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from wordcloud import WordCloud
def extract(url):
elem = None
driver = webdriver.Firefox()
driver.get(url)
try:
found = WebDriverWait(driver, 10).until(
EC.visibility_of(
driver.find_element(By.TAG_NAME, "article")
)
)
# Make a copy of relevant data, because Selenium will throw if
# you try to access the properties after the driver quit
elem = {"text": found.text}
finally:
driver.close()
return elem
def transform(elem):
return elem["text"]
def load(text, filepath):
cloud = WordCloud().generate(text)
cloud.to_file(filepath)
if __name__ == "__main__":
url = "https://angular.io/"
filepath = "angular.png"
elem = extract(url)
if elem is not None:
text = transform(elem)
load(text, filepath)
else:
print("Sorry, could not extract data")
هنا، يقوم بايثون بفتح نافذة متصفح Firefox، وتصفح الموقع والبحث عن عنصر <article>. يقوم بنسخ نصه إلى قاموس (dictionary)، والذي يتم قراءته في خطوة transform وتحويله إلى سحابة كلمات (WordCloud) أثناء خطوة load. عند التعامل مع المواقع التي تعتمد بشكل كبير على JavaScript، غالبًا ما يكون من المفيد استخدام Waits وربما تشغيل execute_script() لإسناد الأمر إلى JavaScript إذا لزم الأمر.
ملخص ما تعلمناه
شكرًا لقراءتك حتى الآن! دعنا نلخص ما تعلمناه:
- كيفية استخلاص البيانات من موقع ويب باستخدام حزمة
requestsفي بايثون. - كيفية ترجمة البيانات المستخلصة إلى بنية ذات معنى باستخدام مكتبة
beautifulsoup. - كيفية معالجة تلك البنية بشكل أكبر لتحويلها إلى شيء يمكنك العمل معه.
- ماذا تفعل إذا كانت الصفحة المستهدفة تعتمد على
JavaScriptلعرض المحتوى.
الخلاصة التقنية
يُعد استخلاص البيانات من الويب أداة قوية في ترسانة المطورين وعلماء البيانات، لكنه يتطلب فهمًا عميقًا لكل من البروتوكولات الويب وأخلاقيات الاستخدام. لقد أظهرنا في هذا الدليل كيف يمكن لبايثون، بمساعدة مكتبات مثل requests و BeautifulSoup، أن يبسط عملية جمع البيانات من المواقع الثابتة. الأهم من ذلك، تناولنا التحدي المتزايد الذي تفرضه تطبيقات الصفحة الواحدة (SPAs) وكيف يمكن لـ Selenium أن يوفر حلاً فعالاً من خلال محاكاة تفاعل المستخدم مع المتصفح. إن الالتزام بالمعايير الأخلاقية، مثل احترام شروط الخدمة واستخدام واجهات API عند توفرها، لا يقل أهمية عن الكفاءة التقنية في هذا المجال.