每天我乘地铁去我的办公室,我的手机根本没有信号。 但是中型应用程序无法让我离线阅读故事,因此我决定自己制作一个新闻抓取工具。
我不想做一个花哨的应用程序,所以我只完成了可以满足我的需求的最小原型。 这个概念很简单:
- 寻找一些新闻来源
- 使用Python抓取新闻页面
- 解析html并使用BeautifulSoup提取内容
- 将其转换为可读格式,然后向我自己发送电子邮件
现在让我解释一下我是如何做的。
新闻来源:Reddit
人们提交指向Reddit的链接并对其进行投票,因此Reddit是阅读新闻的好消息来源。 现在的问题是:如何每天获取热门新闻列表?
在考虑进行网络抓取之前,我们应该尝试确定目标网站是否提供任何API,因为使用API是完全合法的,而且最重要的是,API提供了机器可读的数据,因此我们无需解析HTML。
幸运的是,Reddit提供了API。 从其API列表中,我们可以轻松找到所需内容: /top
。 该端点将返回有关Reddit或给定subreddit的最新新闻。 这样我们就可以使用它从感兴趣的子索引中检索最新新闻。
下一个问题是:我们如何访问此API?
阅读Reddit文档后,我找到了访问此端点的最佳方法。
第一步是在Reddit上创建一个应用程序。 登录到我的帐户,然后转到“首选项→应用程序”。 底部有一个名为“创建另一个应用程序…”的按钮。 单击它并创建一个“脚本”类型的应用程序。 请注意,我们不需要提供“关于URL”或“重定向URL”,因为我们的应用程序无意公开或其他任何人访问。
创建应用后,我们可以在应用信息框中找到应用ID和密码。
下一个问题是如何使用此凭据。 由于我们只需要获取给定subreddit的最新消息,因此我们不需要访问任何与用户相关的信息,因此从技术上讲,我们不需要提供任何用户信息,例如用户名或密码。 Reddit提供了“ 仅应用程序OAuth ”,通过它我们的应用程序可以匿名访问公共信息。 尝试使用以下命令来测试API:
$ curl -X POST -H 'User-Agent: myawesomeapp/1.0' -d grant_type=client_credentials --user 'OUR_CLIENT_ID:OUR_CLIENT_SECRET' https://www.reddit.com/api/v1/access_token
我们将获得如下访问令牌:
{"access_token": "ABCDEFabcdef0123456789", "token_type": "bearer", "expires_in": 3600, "scope": "*"}
优秀的! 使用访问令牌,我们可以做任何事情。
最后,我们不想从头开始编写API访问代码。 我们可以使用python客户端库:
让我们做一个快速测试。 我们从/r/Python
获取前5个提交:
>>> import praw
>>> import pprint
>>> reddit = praw.Reddit(client_id='OUR_CLIENT_ID',
... client_secret='OUR_SECRET',
... grant_type='client_credentials',
... user_agent='mytestscript/1.0')
>>> subs = reddit.subreddit('Python').top(limit=5)
>>> pprint.pprint([(s.score, s.title) for s in subs])
[(6555, 'Automate the boring stuff with python - tinder'),
(4548,
'MS is considering official Python integration with Excel, and is asking for '
'input'),
(4102, 'Python Cheet Sheet for begineers'),
(3285,
'We started late, but we managed to leave Python footprint on r/place!'),
(2899, "Python Section at Foyle's, London")]
大!
刮新闻页面
下一步非常简单。 从最后一步,我们可以获得Submission
对象,其url
属性正是我们想要的URL。 我们还可以通过检查domain
属性来过滤URL,以确保仅从Reddit以外的网站上抓取链接。
subs = [sub for sub in subs if not sub.domain.startswith('self.')]
接下来,我们要做的就是获取URL。 这可以通过requests
完成:
for sub in subs:
res = requests.get(sub.url)
if (res.status_code == 200 and 'content-type' in res.headers and
res.headers.get('content-type').startswith('text/html')):
html = res.text
在这里,我们跳过了内容类型不是text/html
的提交。 这是因为用户可能会提交不是我们目标的直接图像链接。
提取内容
下一步是从新闻HTML中提取文本内容。 我们的目标是提取新闻标题和新闻内容,并忽略诸如页眉,页脚,侧边栏之类的内容或不需要阅读的内容。
这是一项艰巨的任务,老实说,还没有通用,完美的解决方案。 我们可以使用BeautifulSoup
提取文本内容,但是它将提取所有内容,包括页眉和页脚。
我很幸运地发现,现代网站的格式要比其祖先的格式好。 我们不再看到表格的布局以及<font>
和<br>
,而是在文章页面上用<h1>
标题和每个段落<p>
清楚地标记。 我发现大多数网站会将标题和主要内容放在同一容器元素中,例如:
<header>Site Navigation</header>
<div id="#main">
<section>
<h1 class="title">Page Title</h1>
</section>
<section>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</section>
</div>
<aside>Sidebar</aside>
<footer>Copyright...</footer>
在这里,顶层<div id="#main">
是标题和内容的通用容器。 因此,我们可以制定一种算法来查找内容:
- 查找
<h1>
作为标题。 通常,出于SEO的目的,页面中只有一个<h1>
。 - 查找
<h1>
的父级,并测试该父级是否具有足够的<p>
元素。 - 重复步骤2,直到找到具有足够
<p>
元素的父元素或到达<body>
标签。 如果找到足够的<p>
,那么父元素就是主要的内容标签。 如果在找到足够的<p>
之前已到达<body>
标记,则该页面不包含可读内容。
这是一个简单的算法,没有考虑任何语义信息,但实际上对于我们的目的来说效果很好。 无论如何,如果此算法失败,我们可以简单地忽略新闻,这对我来说少读一点新闻也#main
……您绝对可以通过解析<header>
, <footer>
或#main
, .sidebar
制作更准确的算法ID /类。
使用此算法,我们可以轻松编写解析器代码:
soup = BeautifulSoup(text, 'html.parser')
# find the article title
h1 = soup.body.find('h1')
# find the common parent for <h1> and all <p>s.
root = h1
while root.name != 'body' and len(root.find_all('p')) < 5:
root = root.parent
if len(root.find_all('p')) < 5:
return None
# find all the content elements.
ps = root.find_all(['h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre'])
在这里,我们使用len(root.find_all('p')) < 5
作为主要内容的条件,因为真正的新闻不太可能少于5个段落。 您可以根据需要增加此值。
转换为可读格式
最后一步是将内容转换为可读格式。 在此示例中,我选择了Markdown,但您可以自己制作花哨的转换器。
在此示例中,我仅提取了<h#>
和<p>
, <pre>
因此快速功能可以轻松地将它们转换为Markdown。
ps = root.find_all(['h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre'])
ps.insert(0, h1) # add the title
content = [tag2md(p) for p in ps]
def tag2md(tag):
if tag.name == 'p':
return tag.text
elif tag.name == 'h1':
return f'{tag.text}\n{"=" * len(tag.text)}'
elif tag.name == 'h2':
return f'{tag.text}\n{"-" * len(tag.text)}'
elif tag.name in ['h3', 'h4', 'h5', 'h6']:
return f'{"#" * int(tag.name[1:])} {tag.text}'
elif tag.name == 'pre':
return f'```\n{tag.text}\n```'
放在一起
最后是完整的代码:
恰好100行! 尝试运行它…
Scraping /r/Python...
- Retrieving https://imgs.xkcd.com/comics/python_environment.png
x fail or not html
- Retrieving https://thenextweb.com/dd/2017/04/24/universities-finally-realize-java-bad-introductory-programming-language/#.tnw_PLAz3rbJ
=> done, title = "Universities finally realize that Java is a bad introductory programming language"
- Retrieving https://github.com/numpy/numpy/blob/master/doc/neps/dropping-python2.7-proposal.rst
x fail or not html
- Retrieving http://www.thedurkweb.com/sms-spoofing-with-python-for-good-and-evil/
=> done, title = "SMS Spoofing with Python for Good and Evil"
...
和刮擦的新闻:
然后,我要做的最后一件事是将该脚本放在服务器上,设置cron作业以每天运行一次,然后将生成的文件发送到我的电子邮件中。
我没有在细节上花费太多的精力,因此仍有很多地方可以改进。 您可以继续向此脚本添加更多功能,例如使结果文件更漂亮并提取图像。
谢谢阅读! 希望该脚本很有用,至少可以作为访问Reddit API的示例。 如果您喜欢,请推荐这篇文章。
From: https://hackernoon.com/i-made-a-news-scrapper-with-100-lines-of-python-2e1de1f28f22