يقوم هذا المشروع بتنفيذ خوارزمية مخصصة لاستخراج الجمل والكلمات الرئيسية من المقالات الإسبانية والإنجليزية.
تم تطويره بالكامل في Python وهو مستوحى من مشاريع مماثلة التي شوهدت على Reddit News Subreddits التي تستخدم تردد الوثيقة المتكررة المصطلح ( tf–idf ).
أهم 3 ملفات هي:
scraper.py : برنامج نصي Python الذي يؤدي تجريف الويب على مصدر HTML معين ، فإنه يستخرج عنوان المقالة والتاريخ والجسم.
summary.py : نص Python الذي يطبق خوارزمية مخصصة على سلسلة من النص ويستخرج الجمل والكلمات المرتبة الأولى.
bot.py : روبوت Reddit الذي يتحقق من subreddit لأحدث عروضه. يدير قائمة التقديمات التي تمت معالجتها بالفعل لتجنب التكرارات.
يستخدم هذا المشروع مكتبات Python التالية
spaCy : تستخدم لتكرار المقالة في جمل وكلمات.PRAW : يجعل استخدام API Reddit سهل للغاية.Requests : لأداء HTTP get طلبات إلى عناوين URL المقالات.BeautifulSoup : تستخدم لاستخراج نص المقالة.html5lib : حصل هذا المحلل على توافق أفضل عند استخدامه مع BeautifulSoup .tldextract : يستخدم لاستخراج المجال من عنوان URL.wordcloud : يستخدم لإنشاء غيوم الكلمات مع نص المقالة. بعد تثبيت مكتبة spaCy ، يجب عليك تثبيت نموذج لغة لتتمكن من تمييز المقالة.
Spanish يمكنك تشغيل هذا:
python -m spacy download es_core_news_sm
للغات الأخرى ، يرجى التحقق من الرابط التالي: https://spacy.io/usage/models
الروبوت بسيط في الطبيعة ، ويستخدم مكتبة PRAW التي هي واضحة للغاية للاستخدام. استطلاعات الروبوتات Subreddit كل 10 دقائق للحصول على أحدث الطلبات.
يكتشف أولاً ما إذا لم تتم معالجة التقديم بالفعل ثم يتحقق مما إذا كان عنوان URL لتقديمه في القائمة البيضاء. يتم تنسيق هذه القائمة البيضاء حاليًا بنفسي.
إذا قام المنشور وعنوان URL بإقرار كلا الشيكين ، فسيتم تطبيق عملية تجريف الويب على عنوان URL ، فهذا هو المكان الذي تبدأ فيه الأمور في أن تصبح مثيرة للاهتمام.
قبل الرد على التقديم الأصلي ، يتحقق من النسبة المئوية للتخفيض الذي تم تحقيقه ، إذا كان منخفضًا جدًا أو مرتفعًا جدًا ، فإنه يتخطىه ويتحول إلى التقديم التالي.
يوجد بالفعل في القائمة البيضاء أكثر من 300 موقع مختلف من المقالات والمدونات الإخبارية. إن إنشاء كاشطات ويب متخصصة لكل واحد ليس ممكنًا.
ثاني أفضل شيء يجب فعله هو جعل المكشطة دقيقة قدر الإمكان.
نبدأ مكشطة الويب بالطريقة المعتادة ، مع Requests ومكتبات BeautifulSoup .
with requests . get ( article_url ) as response :
if response . encoding == "ISO-8859-1" :
response . encoding = "utf-8"
html_source = response . text
for item in [ "</p>" , "</blockquote>" , "</div>" , "</h2>" , "</h3>" ]:
html_source = html_source . replace ( item , item + " n " )
soup = BeautifulSoup ( html_source , "html5lib" ) عدة مرات حصلت على مشكلات ترميز بسبب تخمين ترميز غير صحيح. لتجنب هذه المشكلة ، أجبر Requests فك تشفير مع utf-8 .
الآن بعد أن تم تحليل مقالتنا في كائن soup ، سنبدأ باستخراج العنوان والوقت المنشور.
لقد استخدمت طرقًا متشابهة لاستخراج كلتا القيمتين ، أتحقق أولاً من العلامات الأكثر شيوعًا والتراجع إلى البدائل الشائعة التالية.
لا تكشف جميع مواقع الويب تاريخها المنشور ، فنحن في بعض الأحيان ينتهي بسلسلة فارغة.
article_title = soup . find ( "title" ). text . replace ( " n " , " " ). strip ()
# If our title is too short we fallback to the first h1 tag.
if len ( article_title ) <= 5 :
article_title = soup . find ( "h1" ). text . replace ( " n " , " " ). strip ()
article_date = ""
# We look for the first meta tag that has the word 'time' in it.
for item in soup . find_all ( "meta" ):
if "time" in item . get ( "property" , "" ):
clean_date = item [ "content" ]. split ( "+" )[ 0 ]. replace ( "Z" , "" )
# Use your preferred time formatting.
article_date = "{:%d-%m-%Y a las %H:%M:%S}" . format (
datetime . fromisoformat ( clean_date ))
break
# If we didn't find any meta tag with a datetime we look for a 'time' tag.
if len ( article_date ) <= 5 :
try :
article_date = soup . find ( "time" ). text . strip ()
except :
passعند استخراج النص من علامات مختلفة ، غالبًا ما حصلت على الأوتار دون فصل. لقد قمت بتطبيق القليل من الاختراق لإضافة خطوط جديدة إلى كل علامة تحتوي عادة على نص. هذا تحسن بشكل كبير من الدقة الكلية للرمز المميز.
كانت فكرتي الأصلية هي قبول المواقع الإلكترونية التي استخدمت علامة <article> . لقد نجح الأمر بشكل جيد في مواقع الويب الأولى التي اختبرتها ، لكنني سرعان ما أدركت أن قلة قليلة من مواقع الويب تستخدمها وأن أولئك الذين يستخدمونها لا يستخدمونها بشكل صحيح.
article = soup . find ( "article" ). text عند الوصول إلى خاصية .text الخاصة بعلامة <article> لاحظت أنني كنت أحصل أيضًا على رمز JavaScript. لقد تراجعت قليلاً وأزلت جميع العلامات التي يمكن أن تضيف ضوضاء إلى نص المقالة.
[ tag . extract () for tag in soup . find_all (
[ "script" , "img" , "ul" , "time" , "h1" , "h2" , "h3" , "iframe" , "style" , "form" , "footer" , "figcaption" ])]
# These class names/ids are known to add noise or duplicate text to the article.
noisy_names = [ "image" , "img" , "video" , "subheadline" ,
"hidden" , "tract" , "caption" , "tweet" , "expert" ]
for tag in soup . find_all ( "div" ):
tag_id = tag [ "id" ]. lower ()
for item in noisy_names :
if item in tag_id :
tag . extract ()تمت إزالة الكود أعلاه معظم التسميات التوضيحية ، والتي عادة ما تكرر ما بداخله في المقالة.
بعد ذلك قمت بتطبيق عملية 3 خطوات للحصول على نص المقالة.
أولاً ، راجعت جميع العلامات <article> وأمسكت بأطول نص.
article = ""
for article_tag in soup . find_all ( "article" ):
if len ( article_tag . text ) >= len ( article ):
article = article_tag . text عملت بشكل جيد بالنسبة لمواقع الويب التي تستخدم علامة <article> بشكل صحيح. أطول علامة دائمًا تحتوي على المقالة الرئيسية.
لكن هذا لم ينجح تمامًا كما هو متوقع ، لاحظت جودة رديئة على النتائج ، وأحيانًا كنت أحصل على مقتطفات لمقالات أخرى.
وذلك عندما قررت إضافة الاحتياط ، Lnstead للبحث فقط عن العلامة <article> سأبحث عن علامات <div> و <section> مع id's الشائع.
# These names commonly hold the article text.
common_names = [ "artic" , "summary" , "cont" , "note" , "cuerpo" , "body" ]
# If the article is too short we look somewhere else.
if len ( article ) <= 650 :
for tag in soup . find_all ([ "div" , "section" ]):
tag_id = tag [ "id" ]. lower ()
for item in common_names :
if item in tag_id :
# We guarantee to get the longest div.
if len ( tag . text ) >= len ( article ):
article = tag . text لقد زاد ذلك من الدقة قليلاً ، كررت الكود ولكن بدلاً من سمة id ، كنت أبحث أيضًا عن سمة class .
# The article is still too short, let's try one more time.
if len ( article ) <= 650 :
for tag in soup . find_all ([ "div" , "section" ]):
tag_class = "" . join ( tag [ "class" ]). lower ()
for item in common_names :
if item in tag_class :
# We guarantee to get the longest div.
if len ( tag . text ) >= len ( article ):
article = tag . textباستخدام جميع الأساليب السابقة زادت بشكل كبير من الدقة الكلية للكاشطة. في بعض الحالات ، استخدمت الكلمات الجزئية التي تشترك في نفس الحروف باللغة الإنجليزية والإسبانية (Artic -> المقالة/Articulo). كان المكشطة متوافقة الآن مع جميع عناوين URL التي اختبرتها.
نقوم بإجراء فحص نهائي ، وإذا كانت المقالة لا تزال قصيرة جدًا ، فنحن نواجه العملية وننتقل إلى عنوان URL التالي ، وإلا ننتقل إلى خوارزمية الملخص.
تم تصميم هذه الخوارزمية للعمل في المقام الأول على المقالات المكتوبة الإسبانية. يتكون من عدة خطوات:
قبل البدء ، نحتاج إلى تهيئة مكتبة spaCy .
NLP = spacy . load ( "es_core_news_sm" ) سيقوم هذا السطر من الكود بتحميل النموذج Spanish الذي أستخدمه أكثر من غيره. إذا كنت تستخدم لغة أخرى ، فيرجى الرجوع إلى قسم Requirements حتى تعرف كيفية تثبيت النموذج المناسب.
عند استخراج النص من المقالة ، عادة ما نحصل على الكثير من المساحات البيضاء ، معظمها من فواصل الخط ( n ).
نقسم النص بهذا الحرف ، ثم تجريد جميع المساحات البيضاء والانضمام إليه مرة أخرى. هذا ليس مطلوبًا تمامًا للقيام به ولكنه يساعد كثيرًا أثناء تصحيح العملية برمتها.
في الجزء العلوي من البرنامج النصي ، نعلن عن مسار الملفات النصية للكلمات. سيتم إضافة كلمات التوقف هذه إلى set ، لا تضمن أي تكرارات.
لقد أضفت أيضًا قائمة مع بعض الكلمات الإسبانية والإنجليزية التي لا تتوقف عن الكلمات لكنها لا تضيف أي شيء كبير إلى المقال. كان تفضيلي الشخصي هو رمزها الثابت في شكل صغير.
ثم أضفت نسخة من كل كلمة في شكل كبير ونموذج العنوان. مما يعني أن set ستكون 3 أضعاف الحجم الأصلي.
with open ( ES_STOPWORDS_FILE , "r" , encoding = "utf-8" ) as temp_file :
for word in temp_file . read (). splitlines ():
COMMON_WORDS . add ( word )
with open ( EN_STOPWORDS_FILE , "r" , encoding = "utf-8" ) as temp_file :
for word in temp_file . read (). splitlines ():
COMMON_WORDS . add ( word )
extra_words = list ()
for word in COMMON_WORDS :
extra_words . append ( word . title ())
extra_words . append ( word . upper ())
for word in extra_words :
COMMON_WORDS . add ( word ) قبل بدء رمز كلماتنا ، يجب أن نمر أولاً بتمرير مقالنا الذي تم تنظيفه إلى خط أنابيب NLP ، ويتم ذلك مع سطر واحد من التعليمات البرمجية.
doc = NLP ( cleaned_article ) يحتوي كائن doc هذا على العديد من التكرار ، و 2 سوف نستخدمها هي tokens sents (الجمل).
في هذه المرحلة أضفت لمسة شخصية إلى الخوارزمية. أولاً ، قمت بنسخة من المقالة ثم أزلت جميع الكلمات الشائعة منها.
بعد ذلك ، استخدمت مجموعة collections.Counter للقيام بالتسجيل الأولي.
ثم قمت بتطبيق مكافأة مضاعفة على الكلمات التي تبدأ في الحرف الكبيرة وتكون متساوية أو أطول من 4 أحرف. معظم الوقت هذه الكلمات هي أسماء للأماكن أو الأشخاص أو المنظمات.
أخيرًا ، قمت بتعيين النتيجة لجميع الكلمات التي هي في الواقع أرقام.
words_of_interest = [
token . text for token in doc if token . text not in COMMON_WORDS ]
scored_words = Counter ( words_of_interest )
for word in scored_words :
if word [ 0 ]. isupper () and len ( word ) >= 4 :
scored_words [ word ] *= 3
if word . isdigit ():
scored_words [ word ] = 0الآن بعد أن حصلنا على الدرجات النهائية لكل كلمة ، حان الوقت لتسجيل كل جملة من المقالة.
للقيام بذلك ، نحتاج أولاً إلى تقسيم المقال إلى جمل. لقد جربت العديد من الأساليب ، بما في ذلك RegEx ، لكن تلك التي عملت بشكل أفضل هي مكتبة spaCy .
سنقوم بالتكرار مرة أخرى عبر كائن doc الذي حددناه في الخطوة السابقة ، لكن هذه المرة سوف نتكرر على خاصية sents الخاصة به.
هناك شيء يجب ملاحظة أننا ننشئ قائمة tokens الجملة وداخل تلك الرموز المميزة يمكننا استرداد نص الجمل من خلال الوصول إلى خاصية text الخاصة بهم.
article_sentences = [ sent for sent in doc . sents ]
scored_sentences = list ()
or index , sent in enumerate ( article_sentences ):
# In some edge cases we have duplicated sentences, we make sure that doesn't happen.
if sent . text not in [ sent for score , index , sent in scored_sentences ]:
scored_sentences . append (
[ score_line ( sent , scored_words ), index , sent . text ]) scored_sentences هي قائمة بالقوائم. تحتوي كل قائمة داخلية على 3 قيم. نقاط الجملة ، فهرسها والجملة نفسها. سيتم استخدام هذه القيم في الخطوة التالية.
يوضح الرمز أدناه كيفية تسجيل الخطوط.
def score_line ( line , scored_words ):
# We remove the common words.
cleaned_line = [
token . text for token in line if token . text not in COMMON_WORDS ]
# We now sum the total number of ocurrences for all words.
temp_score = 0
for word in cleaned_line :
temp_score += scored_words [ word ]
# We apply a bonus score to sentences that contain financial information.
line_lowercase = line . text . lower ()
for word in FINANCIAL_WORDS :
if word in line_lowercase :
temp_score *= 1.5
break
return temp_score نحن نطبق مضاعف على الجمل التي تحتوي على أي كلمة تشير إلى المال أو التمويل.
هذا هو الجزء الأخير من الخوارزمية ، فنحن نستفيد من وظيفة sorted() للحصول على الجمل العليا ثم إعادة ترتيبها في مواقفها الأصلية.
نحن نحدد scored_sentences بترتيب عكسي ، وهذا سيعطينا الجمل المسجلة الأولى أولاً. نبدأ متغيرًا صغيرًا مضادًا بحيث يكسر الحلقة بمجرد أن يصل إلى 5. نتجاهل أيضًا جميع الجمل التي هي 3 أحرف أو أقل (في بعض الأحيان هناك أحرف ذات عرض صفر متستر).
top_sentences = list ()
counter = 0
for score , index , sentence in sorted ( scored_sentences , reverse = True ):
if counter >= 5 :
break
# When the article is too small the sentences may come empty.
if len ( sentence ) >= 3 :
# We append the sentence and its index so we can sort in chronological order.
top_sentences . append ([ index , sentence ])
counter += 1
return [ sentence for index , sentence in sorted ( top_sentences )]في النهاية ، نستخدم فهم قائمة لإرجاع الجمل التي يتم فرزها بالفعل بالترتيب الزمني.
فقط للمتعة أضفت كلمة سحابة إلى كل مقالة. للقيام بذلك ، استخدمت مكتبة wordcloud . هذه المكتبة سهلة الاستخدام للغاية ، بل تحتاج فقط إلى إعلان كائن WordCloud واستخدام طريقة generate مع سلسلة من النص كمعلمة له.
wc = wordcloud . WordCloud () # See cloud.py for full parameters.
wc . generate ( prepared_article )
wc . to_file ( "./temp.png" ) بعد إنشاء الصورة ، قمت بتحميلها إلى Imgur ، ورد على رابط عنوان URL وأضفته إلى رسالة Markdown .

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