Repository: data-for-change/anyway

Issue:
Walla RSS is inconsistent
When fetching RSS from the CI, we get items like this one:
```
<item>
    <title>123 בני אדם נדבקו בקורונה ביממה האחרונה</title>
    <link>https://news.walla.co.il/item/3365121</link>
    <description>&lt;p style="direction:rtl; clear:both"&gt;&lt;a href="https://news.walla.co.il/item/3365121"&gt;&lt;img hspace=5 border=0 align="right" src="https://img.wcdn.co.il/f_auto,t_18/2/9/7/9/2979578-46.jpg" /&gt;&lt;/a&gt; &lt;br/&gt; &lt;/p&gt;</description>
    <pubDate>Sat, 06 Jun 2020 11:39:00 +0300</pubDate>
</item>
```

But accessing it from a browser, you get this:
```
<item>
    <title><![CDATA[123 בני אדם נדבקו בקורונה ביממה האחרונה]]></title>
    <link>https://news.walla.co.il/break/3365121</link>
    <guid>https://news.walla.co.il/break/3365121</guid>
    <pubDate>Sat, 06 Jun 2020 08:39:00 GMT</pubDate>
    <description><![CDATA[<p style="direction:rtl; clear:both"><a href="https://news.walla.co.il/break/3365121"></a><br/>123 בני אדם נדבקו בקורונה ביממה האחרונה בישראל, כך הודיע היום (שבת) משרד הבריאות. זאת לאחר ש-16,133 בדיקות נערכו אתמול, המספר הגבוה ביותר מאז פרוץ המגיפה.</p>]]></description>
</item>
```

With a `guid` field, and different `pubDate` format.


Discussion:

Resolving patch:
diff --git a/anyway/parsers/rss_sites.py b/anyway/parsers/rss_sites.py
--- a/anyway/parsers/rss_sites.py
+++ b/anyway/parsers/rss_sites.py
@@ -5,6 +5,7 @@
 
 from .location_extraction import extract_geo_features
 from .news_flash_classifiers import classify_rss
+from . import timezones
 
 
 def parse_walla(rss_soup, html_soup):
@@ -29,15 +30,10 @@ def parse_ynet(rss_soup, html_soup):
 
 sites_config = {
     "ynet": {
-        "rss": "https://www.ynet.co.il/Integration/StoryRss1854.xml",
-        "time_format": "%a, %d %b %Y %H:%M:%S %z",
+        "rss": "https://www.ynet.co.il:443/Integration/StoryRss1854.xml",
         "parser": parse_ynet,
     },
-    "walla": {
-        "rss": "https://rss.walla.co.il/feed/22",
-        "time_format": "%a, %d %b %Y %H:%M:%S %Z",
-        "parser": parse_walla,
-    },
+    "walla": {"rss": "https://rss.walla.co.il:443/feed/22", "parser": parse_walla,},
 }
 
 
@@ -48,18 +44,20 @@ def _fetch(url: str) -> str:
 def scrape(site_name, *, fetch_rss=_fetch, fetch_html=_fetch):
     config = sites_config[site_name]
     rss_text = fetch_rss(config["rss"])
-    rss_soup = BeautifulSoup(rss_text, "lxml")
+
+    # Patch RSS issue in walla. This might create duplicate `guid` field
+    rss_text = rss_text.replace("link", "guid")
+
+    rss_soup = BeautifulSoup(rss_text, features="lxml")
     rss_soup_items = rss_soup.find_all("item")
 
     assert rss_soup_items
 
     for item_rss_soup in rss_soup_items:
-        raw_date = item_rss_soup.pubdate.get_text()
         link = item_rss_soup.guid.get_text()
+        date = timezones.parse_creation_datetime(item_rss_soup.pubdate.get_text())
 
-        date = datetime.datetime.strptime(raw_date, config["time_format"]).replace(tzinfo=None)
-
-        html_text = fetch_html(link)
+        html_text = fetch_html(link.replace(".com/", ".com:443/"))
         item_html_soup = BeautifulSoup(html_text, "lxml")
 
         author, title, description = config["parser"](item_rss_soup, item_html_soup)
diff --git a/anyway/parsers/timezones.py b/anyway/parsers/timezones.py
new file mode 100644
--- /dev/null
+++ b/anyway/parsers/timezones.py
@@ -0,0 +1,28 @@
+from datetime import datetime, timezone, timedelta
+
+ISREAL_SUMMER_TIMEZONE = timezone(offset=timedelta(hours=3))
+
+
+def parse_creation_datetime(raw_date: str):
+    """Unified time format detection and parsing.
+
+    Examples:
+    ynet: 'Sun, 31 May 2020 11:26:18 +0300'
+    twitter: 'Sun May 31 08:26:18 +0000 2020'
+    walla: 'Sun, 31 May 2020 08:26:18 GMT' or like ynet
+    """
+    try:
+        # +0300 (ynet or walla)
+        return datetime.strptime(raw_date, "%a, %d %b %Y %H:%M:%S %z").replace(
+            tzinfo=ISREAL_SUMMER_TIMEZONE
+        )
+    except ValueError as ex:
+        print(ex)
+        try:
+            # +0000 (twitter)
+            time = datetime.strptime(raw_date, "%a %b %d %H:%M:%S %z %Y")
+        except ValueError as ex:
+            print(ex)
+            # GMT (walla)
+            time = datetime.strptime(raw_date, "%a, %d %b %Y %H:%M:%S %Z")
+        return time.replace(tzinfo=timezone.utc).astimezone(tz=ISREAL_SUMMER_TIMEZONE)
diff --git a/anyway/parsers/twitter.py b/anyway/parsers/twitter.py
--- a/anyway/parsers/twitter.py
+++ b/anyway/parsers/twitter.py
@@ -1,4 +1,3 @@
-from datetime import datetime, timezone, timedelta
 import re
 
 import tweepy
@@ -6,6 +5,7 @@
 from .news_flash_classifiers import classify_tweets
 from .location_extraction import extract_geo_features
 from . import secrets
+from . import timezones
 
 to_hebrew = {"mda_israel": "מגן דוד אדום"}
 
@@ -32,14 +32,6 @@ def scrape(screen_name, latest_tweet_id=None, count=100):
         yield parse_tweet(tweet, screen_name)
 
 
-def parse_creation_datetime(created_at):
-    # Example: 'Sun May 31 11:26:18 +0000 2020'
-    time_format = "%a %b %d %H:%M:%S %z %Y"
-    time = datetime.strptime(created_at, time_format)
-    summer_timezone = timezone(offset=timedelta(hours=3))
-    return time.replace(tzinfo=timezone.utc).astimezone(tz=summer_timezone).replace(tzinfo=None)
-
-
 def extract_accident_time(text):
     # Currently unused
     reg_exp = r"בשעה (\d{2}:\d{2})"
@@ -52,7 +44,7 @@ def extract_accident_time(text):
 def parse_tweet(tweet, screen_name):
     return {
         "link": "https://twitter.com/{}/status/{}".format(screen_name, tweet["id_str"]),
-        "date": parse_creation_datetime(tweet["created_at"]),
+        "date": timezones.parse_creation_datetime(tweet["created_at"]),
         "source": "twitter",
         "author": to_hebrew[screen_name],
         "title": tweet["full_text"],


Test patch:
diff --git a/tests/test_news_flash.py b/tests/test_news_flash.py
--- a/tests/test_news_flash.py
+++ b/tests/test_news_flash.py
@@ -6,6 +6,7 @@
 from anyway.parsers import rss_sites, twitter, location_extraction
 from anyway.parsers.news_flash_classifiers import classify_tweets
 from anyway.parsers import secrets
+from anyway.parsers import timezones
 
 
 def fetch_html_walla(link):
@@ -31,7 +32,7 @@ def fetch_rss_ynet(link):
 def test_scrape_walla():
     items_expected = [
         {
-            "date": datetime.datetime(2020, 5, 23, 16, 55),
+            "date": datetime.datetime(2020, 5, 23, 19, 55, tzinfo=timezones.ISREAL_SUMMER_TIMEZONE),
             "title": 'פרקליטי רה"מ יתלוננו נגד רביב דרוקר על שיבוש הליכי משפט',
             "link": "https://news.walla.co.il/break/3362504",
             "source": "walla",
@@ -39,7 +40,7 @@ def test_scrape_walla():
             "description": 'פרקליטיו של ראש הממשלה בנימין נתניהו מתכוונים להגיש הערב (שבת) תלונה ליועץ המשפטי לממשלה, אביחי מנדלבליט, נגד העיתונאי רביב דרוקר בטענה ששיבש הליכי משפט והדיח עד בתוכניתו "המקור". התלונה מתייחסת לראיונות שנתנו לתוכנית עדי תביעה במשפטו של נתניהו, בהם שאול אלוביץ\' ומומו פילבר.]]>',
         },
         {
-            "date": datetime.datetime(2020, 5, 22, 13, 14),
+            "date": datetime.datetime(2020, 5, 22, 16, 14, tzinfo=timezones.ISREAL_SUMMER_TIMEZONE),
             "title": "פקיסטן: לפחות נוסע אחד שרד את התרסקות המטוס",
             "link": "https://news.walla.co.il/break/3362389",
             "source": "walla",
@@ -59,7 +60,7 @@ def test_scrape_walla():
 def test_scrape_ynet():
     items_expected = [
         {
-            "date": datetime.datetime(2020, 5, 22, 18, 27, 32),
+            "date": datetime.datetime(2020, 5, 22, 18, 27, 32, tzinfo=timezones.ISREAL_SUMMER_TIMEZONE),
             "title": "קפריסין הודיעה: ישראלים יוכלו להיכנס למדינה החל מה-9 ביוני",
             "link": "http://www.ynet.co.il/articles/0,7340,L-5735229,00.html",
             "source": "ynet",
@@ -67,7 +68,7 @@ def test_scrape_ynet():
             "description": ": \"שר התחבורה של קפריסין הודיע על תוכנית לפתיחת שדות התעופה וחידוש הטיסות החל מה-9 ביוני. התוכנית שאושרה בידי הממשלה חולקה לשני שלבים לפי תאריכים ומדינות שיורשו להיכנס בשעריה. עד ה-19 ביוני נוסעים מכל המקומות יצטרכו להיבדק לקורונה 72 שעות לפני מועד הטיסה. מה-20 ביוני יידרשו לכך רק נוסעים משוויץ, פולין רומניה, קרואטיה, אסטוניה וצ'כיה. בתי המלון ייפתחו ב-1 ביוני, וחובת הבידוד תבוטל ב-20 ביוני.   ",
         },
         {
-            "date": datetime.datetime(2020, 5, 22, 15, 8, 48),
+            "date": datetime.datetime(2020, 5, 22, 15, 8, 48, tzinfo=timezones.ISREAL_SUMMER_TIMEZONE),
             "link": "http://www.ynet.co.il/articles/0,7340,L-5735178,00.html",
             "source": "ynet",
             "author": "אלישע בן קימון",
@@ -98,7 +99,7 @@ def test_scrape_sanity_online():
 twitter_expected_list = [
     {
          'link': 'https://twitter.com/mda_israel/status/1267054794587418630',
-         'date': datetime.datetime(2020, 5, 31, 14, 26, 18),
+         'date': datetime.datetime(2020, 5, 31, 14, 26, 18, tzinfo=timezones.ISREAL_SUMMER_TIMEZONE),
          'source': 'twitter',
          'author': 'מגן דוד אדום',
          'title': 'בשעה 13:19 התקבל דיווח במוקד 101 של מד"א במרחב ירושלים על פועל שנפצע במהלך עבודתו במפעל באזור התעשיה עטרות בירושלים. חובשים ופראמדיקים של מד"א מעניקים טיפול רפואי ומפנים לבי"ח שערי צדק גבר בן 31 במצב קשה, עם חבלת ראש.',
@@ -108,7 +109,7 @@ def test_scrape_sanity_online():
     },
     {
          'link': 'https://twitter.com/mda_israel/status/1267037315869880321',
-         'date': datetime.datetime(2020, 5, 31, 13, 16, 51),
+         'date': datetime.datetime(2020, 5, 31, 13, 16, 51, tzinfo=timezones.ISREAL_SUMMER_TIMEZONE),
          'source': 'twitter',
          'author': 'מגן דוד אדום',
          'title': 'בשעה 12:38 התקבל דיווח במוקד 101 של מד"א במרחב ירדן על ת.ד סמוך למסעדה. חובשים ופראמדיקים של מד"א מעניקים טיפול רפואי ל4 פצועים, בהם 1 מחוסר הכרה.',
@@ -169,3 +170,10 @@ def test_extract_location():
     location_extraction.extract_geo_features(actual)
     # check only the extracted part
     assert actual == expected
+
+
+def test_timeparse():
+    twitter = timezones.parse_creation_datetime('Sun May 31 08:26:18 +0000 2020')
+    ynet = timezones.parse_creation_datetime('Sun, 31 May 2020 11:26:18 +0300')
+    walla = timezones.parse_creation_datetime('Sun, 31 May 2020 08:26:18 GMT')
+    assert twitter == ynet == walla
diff --git a/tests/walla.xml b/tests/walla.xml
--- a/tests/walla.xml
+++ b/tests/walla.xml
@@ -17,7 +17,6 @@
         <item>
             <title><![CDATA[פרקליטי רה"מ יתלוננו נגד רביב דרוקר על שיבוש הליכי משפט]]></title>
             <link>https://news.walla.co.il/break/3362504</link>
-            <guid>https://news.walla.co.il/break/3362504</guid>
             <pubDate>Sat, 23 May 2020 16:55:00 GMT</pubDate>
             <description><![CDATA[<p style="direction:rtl; clear:both"><a href="https://news.walla.co.il/break/3362504"></a><br/>פרקליטיו של ראש הממשלה בנימין נתניהו מתכוונים להגיש הערב (שבת) תלונה ליועץ המשפטי לממשלה, אביחי מנדלבליט, נגד העיתונאי רביב דרוקר בטענה ששיבש הליכי משפט והדיח עד בתוכניתו "המקור". התלונה מתייחסת לראיונות שנתנו לתוכנית עדי תביעה במשפטו של נתניהו, בהם שאול אלוביץ' ומומו פילבר.</p>]]></description>
         </item>
diff --git a/tests/ynet.xml b/tests/ynet.xml
--- a/tests/ynet.xml
+++ b/tests/ynet.xml
@@ -18,7 +18,7 @@
         <item>
             <category>מבזקים</category>
             <title>קפריסין הודיעה: ישראלים יוכלו להיכנס למדינה החל מה-9 ביוני</title>
-            <description></description>
+            <description/>
             <link>http://www.ynet.co.il/articles/0,7340,L-5735229,00.html</link>
             <pubDate>Fri, 22 May 2020 18:27:32 +0200</pubDate>
             <guid>http://www.ynet.co.il/articles/0,7340,L-5735229,00.html</guid>