Tag of Google news title for beautiful soup

Viewed 488

I am trying to extract the result of a search from Google news (vaccine for example) and provide some sentiment analysis based on the headline collected.

So far, I can't seem to find the correct tag to collect the headlines.

Here is my code:

from textblob import TextBlob
import requests
from bs4 import BeautifulSoup

class Analysis:
    def __init__(self, term):
        self.term = term
        self.subjectivity = 0
        self.sentiment = 0
        self.url = 'https://www.google.com/search?q={0}&source=lnms&tbm=nws'.format(self.term)

    def run (self):
        response = requests.get(self.url)
        print(response.text)
        soup = BeautifulSoup(response.text, 'html.parser')
        headline_results = soup.find_all('div', class_="phYMDf nDgy9d")
        for h in headline_results:
            blob = TextBlob(h.get_text())
            self.sentiment += blob.sentiment.polarity / len(headline_results)
            self.subjectivity += blob.sentiment.subjectivity / len(headline_results)
a = Analysis('Vaccine')
a.run()
print(a.term, 'Subjectivity: ', a.subjectivity, 'Sentiment: ' , a.sentiment)

The result are always 0 for the sentiment and 0 for the subjectivity. I feel like the issue is with the class_="phYMDf nDgy9d".

3 Answers

If you browse into that link, you are going to see the finished state of page but requests.get does not exeute or load any more data other than the page you request. Luckily there is some data and you can scrape that. I suggest you to use html prettifier services like codebeautify to get better understanding about what the page structure is.

Also if you see classes like phYMDf nDgy9d be sure to avoid finding with them. They are minified versions of classes so at any moment if they change a part of the CSS code, the class you are looking for is going to get a new name.

What I did is probably overkill but, I managed to dig down to scrape specific parts and your code works now.

enter image description here

When you look at the prettier version of requested html file, necessary contents are in a div with an id of main shown above. Then it's children are starting with a div element Google Search, continuing with a style element and after one empty div element, there are post div elements. The last two elements in that children list are footer and script elements. We can cut these off with [3:-2] and then under that tree we have pure data (pretty much). If you check the remaining part of the code after the posts variable, you can understand it I think.

Here is the code:

from textblob import TextBlob
import requests, re
from bs4 import BeautifulSoup
from pprint import pprint

class Analysis:
    def __init__(self, term):
        self.term = term
        self.subjectivity = 0
        self.sentiment = 0
        self.url = 'https://www.google.com/search?q={0}&source=lnms&tbm=nws'.format(self.term)

    def run (self):
        response = requests.get(self.url)
        #print(response.text)
        soup = BeautifulSoup(response.text, 'html.parser')
        mainDiv = soup.find("div", {"id": "main"})
        posts = [i for i in mainDiv.children][3:-2]
        news = []
        for post in posts:
            reg = re.compile(r"^/url.*")
            cursor = post.findAll("a", {"href": reg})
            postData = {}
            postData["headline"] = cursor[0].find("div").get_text()
            postData["source"] = cursor[0].findAll("div")[1].get_text()
            postData["timeAgo"] = cursor[1].next_sibling.find("span").get_text()
            postData["description"] = cursor[1].next_sibling.find("span").parent.get_text().split("· ")[1]
            news.append(postData)
        pprint(news)
        for h in news:
            blob = TextBlob(h["headline"] + " "+ h["description"])
            self.sentiment += blob.sentiment.polarity / len(news)
            self.subjectivity += blob.sentiment.subjectivity / len(news)
a = Analysis('Vaccine')
a.run()

print(a.term, 'Subjectivity: ', a.subjectivity, 'Sentiment: ' , a.sentiment)

A few outputs:

[{'description': 'It comes after US health officials said last week they had '
                 'started a trial to evaluate a possible vaccine in Seattle. '
                 'The Chinese effort began on...',
  'headline': 'China embarks on clinical trial for virus vaccine',
  'source': 'The Star Online',
  'timeAgo': '5 saat önce'},
 {'description': 'Hanneke Schuitemaker, who is leading a team working on a '
                 'Covid-19 vaccine, tells of the latest developments and what '
                 'needs to be done now.',
  'headline': 'Vaccine scientist: ‘Everything is so new in dealing with this '
              'coronavirus’',
  'source': 'The Guardian',
  'timeAgo': '20 saat önce'},
 .
 .
 .
Vaccine Subjectivity:  0.34522727272727277 Sentiment:  0.14404040404040402
[{'description': '10 Cool Tech Gadgets To Survive Working From Home. From '
                 'Wi-Fi and cell phone signal boosters, to noise-cancelling '
                 'headphones and gadgets...',
  'headline': '10 Cool Tech Gadgets To Survive Working From Home',
  'source': 'CRN',
  'timeAgo': '2 gün önce'},
 {'description': 'Over the past few years, smart home products have dominated '
                 'the gadget space, with goods ranging from innovative updates '
                 'to the items we...',
  'headline': '6 Smart Home Gadgets That Are Actually Worth Owning',
  'source': 'Entrepreneur',
  'timeAgo': '2 hafta önce'},
 .
 .
 .
Home Gadgets Subjectivity:  0.48007305194805205 Sentiment:  0.3114683441558441

I used headlines and description data to do the operations but you can play with that if you want. You have the data now :)

use this

headline_results = soup.find_all('div', {'class' : 'BNeawe vvjwJb AP7Wnd'})

you already printed the response.text, if you want to find the specific data please search from the response.text result

Try to use select() instead. CSS selectors are more flexible. CSS selectors reference.

Have a look at SelectorGadget Chrome extension to grab CSS selectors by clicking on the desired element in your browser.

If you want to get all titles and so on, then you are looking for this container:

soup.select('.dbsr')

Make sure to pass user-agent, because Google might block your requests eventually and you'll receive a different HTML thus empty output. Check what is your user-agent

Pass user-agent:

headers = {
    "User-agent":
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582"
}

requests.get("YOUR_URL", headers=headers)

I'm not sure what exactly are you trying to do but a solution from Guven Degirmenci is a bit overkill as he mentioned, with slicing, regex, doing something in div#main. It's much simpler.


Code and example in the online IDE:

from textblob import TextBlob
import requests
from bs4 import BeautifulSoup

headers = {
   "User-agent":
   "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582"
}

class Analysis:
    def __init__(self, term):
        self.term = term
        self.subjectivity = 0
        self.sentiment = 0
        self.url = f"https://www.google.com/search?q={self.term}&tbm=nws"
    
 
    def run (self):
        response = requests.get(self.url, headers=headers)
        soup = BeautifulSoup(response.text, "html.parser")

        news_data = []

        for result in soup.select('.dbsr'):
          title = result.select_one('.nDgy9d').text
          link = result.a['href']
          source = result.select_one('.WF4CUc').text
          snippet = result.select_one('.Y3v8qd').text
          date_published = result.select_one('.WG9SHc span').text

          news_data.append({
            "title": title,
            "link": link,
            "source": source, 
            "snippet": snippet,
            "date_published": date_published
          })

        for h in news_data:
            blob = TextBlob(f"{h['title']} {h['snippet']}")
            self.sentiment += blob.sentiment.polarity / len(news_data)
            self.subjectivity += blob.sentiment.subjectivity / len(news_data)


a = Analysis("Lasagna")
a.run()

print(a.term, "Subjectivity: ", a.subjectivity, "Sentiment: " , a.sentiment)

# Vaccine Subjectivity:  0.3255952380952381 Sentiment:  0.05113636363636363
# Lasagna Subjectivity:  0.36556818181818185 Sentiment:  0.25386093073593075

Alternatively, you can achieve the same thing by using Google News Results API from SerpApi. It's a paid API with a free plan.

The difference in your case is that you don't have to maintain the parser, figure out how to parse certain elements or figuring out why something isn't working as it should, and understand how to bypass blocks from Google. All that needs to be done is to iterate over structured JSON and get what you want fast.

Code integrated with your example:


from textblob import TextBlob
import os
from serpapi import GoogleSearch


class Analysis:
    def __init__(self, term):
        self.term = term
        self.subjectivity = 0
        self.sentiment = 0
        self.url = f"https://www.google.com/search"
    
 
    def run (self):
        params = {
          "engine": "google",
          "tbm": "nws",
          "q": self.url,
          "api_key": os.getenv("API_KEY"),
        }

        search = GoogleSearch(params)
        results = search.get_dict()

        news_data = []

        for result in results['news_results']:
          title = result['title']
          link = result['link']
          snippet = result['snippet']
          source = result['source']
          date_published = result['date']

          news_data.append({
            "title": title,
            "link": link,
            "source": source, 
            "snippet": snippet,
            "date_published": date_published
          })

        for h in news_data:
            blob = TextBlob(f"{h['title']} {h['snippet']}")
            self.sentiment += blob.sentiment.polarity / len(news_data)
            self.subjectivity += blob.sentiment.subjectivity / len(news_data)


a = Analysis("Vaccine")
a.run()

print(a.term, "Subjectivity: ", a.subjectivity, "Sentiment: " , a.sentiment)


# Vaccine Subjectivity:  0.30957251082251086 Sentiment:  0.06277056277056277
# Lasagna Subjectivity:  0.30957251082251086 Sentiment:  0.06277056277056277

P.S - I wrote a bit more detailed blog post about how to scrape Google News.

Disclaimer, I work for SerpApi.

Related