我想用Python抓取网页的标题。我需要为成千上万的网站做到这一点,所以它必须快速。我之前看过像retrieving just the title of a webpage in python这样的问题,但是我发现所有这些问题都是在检索标题之前下载整个页面,这看起来非常低效,因为大多数情况下标题都包含在HTML的前几行中。
是否可以只下载网页的部分,直到找到标题?
我已尝试过以下操作,但page.readline()
会下载整个页面。
import urllib2
print("Looking up {}".format(link))
hdr = {'User-Agent': 'Mozilla/5.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
'Accept-Encoding': 'none',
'Accept-Language': 'en-US,en;q=0.8',
'Connection': 'keep-alive'}
req = urllib2.Request(link, headers=hdr)
page = urllib2.urlopen(req, timeout=10)
content = ''
while '</title>' not in content:
content = content + page.readline()
- 编辑 -
请注意,我当前的解决方案使用BeautifulSoup仅限于处理标题,因此我可以优化的唯一地方可能无法读取整个页面。
title_selector = SoupStrainer('title')
soup = BeautifulSoup(page, "lxml", parse_only=title_selector)
title = soup.title.string.strip()
- 编辑2 -
我发现BeautifulSoup本身会将内容拆分为self.current_data中的多个字符串 变量(see this function in bs4),但我不确定如何修改代码,基本上在找到标题后停止阅读所有剩余的内容。一个问题可能是重定向应该仍然有效。
- 编辑3 -
所以这是一个例子。我有一个链接www.xyz.com/abc,我必须通过任何重定向(几乎所有我的链接使用一点点链接缩短)。我对重定向后出现的标题和域感兴趣。
- 编辑4 -
非常感谢您的所有帮助! Kul-Tigin的答案非常有效并且已被接受。我会保留赏金,直到它耗尽,看看是否有更好的答案(如时间测量比较所示)。
- 编辑5 -
对于任何感兴趣的人:我已经将接受的答案计时大约是使用BeautifulSoup4的现有解决方案的两倍。
答案 0 :(得分:14)
您可以通过启用requests
的流模式推迟下载整个响应正文。
Requests 2.14.2 documentation - Advanced Usage
默认情况下,当您发出请求时,响应的正文是 立即下载。您可以覆盖此行为并推迟 下载响应正文,直到您访问
Response.content
带有stream
参数的属性:...
如果您在发出请求时将
stream
设置为True
,则除非您使用所有数据或致电Response.close
。 这可能导致连接效率低下。如果您在使用stream=True
时发现自己部分阅读请求正文(或根本没有阅读它们),则应考虑使用contextlib.closing(documented here)
因此,使用此方法,您可以按块读取响应块,直到遇到title标记。由于重定向将由库处理,您可以随时使用。
这是一个容易出错的代码,使用 Python 2.7.10 和 3.6.0 进行测试:
try:
from HTMLParser import HTMLParser
except ImportError:
from html.parser import HTMLParser
import requests, re
from contextlib import closing
CHUNKSIZE = 1024
retitle = re.compile("<title[^>]*>(.*?)</title>", re.IGNORECASE | re.DOTALL)
buffer = ""
htmlp = HTMLParser()
with closing(requests.get("http://example.com/abc", stream=True)) as res:
for chunk in res.iter_content(chunk_size=CHUNKSIZE, decode_unicode=True):
buffer = "".join([buffer, chunk])
match = retitle.search(buffer)
if match:
print(htmlp.unescape(match.group(1)))
break
答案 1 :(得分:4)
问题:...我可以优化的唯一地方可能无法在整个页面中阅读。
这不会读取整个页面。
注意:如果您在中间剪切Unicode序列,则Unicode
.decode()
将raise Exception
。使用.decode(errors='ignore')
删除这些序列。
例如:
import re
try:
# PY3
from urllib import request
except:
import urllib2 as request
for url in ['http://www.python.org/', 'http://www.google.com', 'http://www.bit.ly']:
f = request.urlopen(url)
re_obj = re.compile(r'.*(<head.*<title.*?>(.*)</title>.*</head>)',re.DOTALL)
Found = False
data = ''
while True:
b_data = f.read(4096)
if not b_data: break
data += b_data.decode(errors='ignore')
match = re_obj.match(data)
if match:
Found = True
title = match.groups()[1]
print('title={}'.format(title))
break
f.close()
<强>输出强>:
title =欢迎使用Python.org
标题=谷歌
title = Bitly | URL Shortener和链接管理平台
使用Python测试:3.4.2和2.7.9
答案 2 :(得分:2)
您正在使用标准REST请求抓取网页,我不知道任何只返回标题的请求,因此我认为不可能。
我知道这并不一定只能帮助获得标题,但我通常会使用BeautifulSoup进行任何网页抓取。这更容易。这是一个例子。
代码:
import requests
from bs4 import BeautifulSoup
urls = ["http://www.google.com", "http://www.msn.com"]
for url in urls:
r = requests.get(url)
soup = BeautifulSoup(r.text, "html.parser")
print "Title with tags: %s" % soup.title
print "Title: %s" % soup.title.text
print
输出:
Title with tags: <title>Google</title>
Title: Google
Title with tags: <title>MSN.com - Hotmail, Outlook, Skype, Bing, Latest News, Photos & Videos</title>
Title: MSN.com - Hotmail, Outlook, Skype, Bing, Latest News, Photos & Videos
答案 3 :(得分:2)
你想要的东西我认为无法完成,因为网页的设置方式,你可以在解析任何内容之前获得请求的响应。通常不会有流媒体&#34;如果遇到<title>
,则停止向我提供数据&#34;旗。如果有爱看到它,但有一些东西可以帮助你。请记住,并非所有网站都尊重这一点。因此,有些网站会强制您下载整个页面源,然后才能对其进行操作。但是很多它们都允许你指定一个范围标题。所以在请求示例中:
import requests
targeturl = "http://www.urbandictionary.com/define.php?term=Blarg&page=2"
rangeheader = {"Range": "bytes=0-150"}
response = requests.get(targeturl, headers=rangeheader)
response.text
你得到了
'<!DOCTYPE html>\n<html lang="en-US" prefix="og: http://ogp.me/ns#'
现在当然在这里遇到了这个问题 如果您指定的范围太短而无法获取页面标题,该怎么办? 目标是什么? (速度与准确性的结合) 如果页面不尊重Range,会发生什么? (大多数时候你只是得到你没有它的整个回应。)
我不知道这对你有帮助吗?希望如此。但我做过类似的事情,只能获取下载检查的文件头。
EDIT4:
所以我想到了另一种可能有帮助的hacky事情。几乎每个页面都有一个404页面找不到页面。我们也许能够利用这个优势。而不是请求常规页面。请求类似的东西。
http://www.urbandictionary.com/nothing.php
一般页面将包含大量信息,链接和数据。但404页面只不过是一条消息,而且(在这种情况下)是一个视频。而且通常没有视频。只是一些文字。
但你也注意到标题仍然出现在这里。所以也许我们可以在任何页面上请求我们知道不存在的东西,例如。
X5ijsuUJSoisjHJFk948.php
并为每个页面获取404。这样你只需下载一个非常小而简约的页面。而已。这将显着减少您下载的信息量。从而提高速度和效率。
下面是这个方法的问题:你需要检查一下这个页面是否提供了自己的404版本。大多数页面都有它,因为它看起来不错。及其标准做法包括一个。但并非所有人都这样做。确保处理这种情况。但我认为这可能值得一试。在数千个站点的过程中,它将为每个html节省许多ms的下载时间。
EDIT5:
正如我们所讨论的那样,因为您对重定向的网址感兴趣。我们可能会使用http头部请求。哪个不会得到网站内容。只是标题。所以在这种情况下:
response = requests.head('http://myshortenedurl.com/5b2su2')
用tunyurl替换我的shortenedurl跟随。
>>>response
<Response [301]>
很好,所以我们知道这会重定向到某个东西。
>>>response.headers['Location']
'http://stackoverflow.com'
现在我们知道网址重定向到的位置,而不实际关注或下载任何网页来源。现在我们可以应用之前讨论的任何其他技术。
下面是一个例子,使用请求和lxml模块并使用404页面的想法。 (请注意,我必须用bit替换bit.ly,因此堆栈溢出不会生气。)
#!/usr/bin/python3
import requests
from lxml.html import fromstring
links = ['http://bit'ly/MW2qgH',
'http://bit'ly/1x0885j',
'http://bit'ly/IFHzvO',
'http://bit'ly/1PwR9xM']
for link in links:
response = '<Response [301]>'
redirect = ''
while response == '<Response [301]>':
response = requests.head(link)
try:
redirect = response.headers['Location']
except Exception as e:
pass
fakepage = redirect + 'X5ijsuUJSoisjHJFk948.php'
scrapetarget = requests.get(fakepage)
tree = fromstring(scrapetarget.text)
print(tree.findtext('.//title'))
所以这里我们得到404页面,它将遵循任意数量的重定向。现在继续这个输出:
Urban Dictionary error
Page Not Found - Stack Overflow
Error 404 (Not Found)!!1
Kijiji: Page Not Found
所以你可以看到我们确实得到了冠军。但我们发现该方法存在一些问题。即一些标题添加的东西,有些只是没有一个好的标题。那就是那个方法的问题。然而,我们也可以尝试范围方法。这样做的好处是标题是正确的,但有时我们可能会错过它,有时我们必须下载整个页面来获取它。增加所需时间。
同样归功于alecxe这部分快速而又脏的脚本
tree = fromstring(scrapetarget.text)
print(tree.findtext('.//title'))
以范围方法为例。在链接中的链接循环:将try catch语句之后的代码更改为:
rangeheader = {"Range": "bytes=0-500"}
scrapetargetsection = requests.get(redirect, headers=rangeheader)
tree = fromstring(scrapetargetsection.text)
print(tree.findtext('.//title'))
输出是:
None
Stack Overflow
Google
Kijiji: Free Classifieds in...
这里我们看到都市词典没有标题或我错过了返回的字节。在任何这些方法中都存在权衡。接近总体准确度的唯一方法是下载我认为的每个页面的整个源。
答案 4 :(得分:2)
使用urllib你可以设置Range标头来请求一定范围的字节,但是会有一些后果:
(编辑) - 为避免标题标记在两个远程请求之间分割,使您的范围重叠,请参阅&#39; range_header_overlapped&#39;我的example code
中的功能import urllib
req = urllib.request.Request(&#39; http://www.python.org/&#39;)
req.headers [&#39;范围&#39;] =&#39;字节=%S-%S&#39; %(0,300)
f = urllib.request.urlopen(req)
content_range = f.headers.get(&#39;内容范围&#39)
打印(content_range)
答案 5 :(得分:0)
我的代码也解决了在块之间分割标题标记的情况。
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
Created on Tue May 30 04:21:26 2017
====================
@author: s
"""
import requests
from string import lower
from html.parser import HTMLParser
#proxies = { 'http': 'http://127.0.0.1:8080' }
urls = ['http://opencvexamples.blogspot.com/p/learning-opencv-functions-step-by-step.html',
'http://www.robindavid.fr/opencv-tutorial/chapter2-filters-and-arithmetic.html',
'http://blog.iank.org/playing-capitals-with-opencv-and-python.html',
'http://docs.opencv.org/3.2.0/df/d9d/tutorial_py_colorspaces.html',
'http://scikit-image.org/docs/dev/api/skimage.exposure.html',
'http://apprize.info/programming/opencv/8.html',
'http://opencvexamples.blogspot.com/2013/09/find-contour.html',
'http://docs.opencv.org/2.4/modules/imgproc/doc/geometric_transformations.html',
'https://github.com/ArunJayan/OpenCV-Python/blob/master/resize.py']
class TitleParser(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self.match = False
self.title = ''
def handle_starttag(self, tag, attributes):
self.match = True if tag == 'title' else False
def handle_data(self, data):
if self.match:
self.title = data
self.match = False
def valid_content( url, proxies=None ):
valid = [ 'text/html; charset=utf-8',
'text/html',
'application/xhtml+xml',
'application/xhtml',
'application/xml',
'text/xml' ]
r = requests.head(url, proxies=proxies)
our_type = lower(r.headers.get('Content-Type'))
if not our_type in valid:
print('unknown content-type: {} at URL:{}'.format(our_type, url))
return False
return our_type in valid
def range_header_overlapped( chunksize, seg_num=0, overlap=50 ):
"""
generate overlapping ranges
(to solve cases when title tag splits between them)
seg_num: segment number we want, 0 based
overlap: number of overlaping bytes, defaults to 50
"""
start = chunksize * seg_num
end = chunksize * (seg_num + 1)
if seg_num:
overlap = overlap * seg_num
start -= overlap
end -= overlap
return {'Range': 'bytes={}-{}'.format( start, end )}
def get_title_from_url(url, proxies=None, chunksize=300, max_chunks=5):
if not valid_content(url, proxies=proxies):
return False
current_chunk = 0
myparser = TitleParser()
while current_chunk <= max_chunks:
headers = range_header_overlapped( chunksize, current_chunk )
headers['Accept-Encoding'] = 'deflate'
# quick fix, as my locally hosted Apache/2.4.25 kept raising
# ContentDecodingError when using "Content-Encoding: gzip"
# ContentDecodingError: ('Received response with content-encoding: gzip, but failed to decode it.',
# error('Error -3 while decompressing: incorrect header check',))
r = requests.get( url, headers=headers, proxies=proxies )
myparser.feed(r.content)
if myparser.title:
return myparser.title
current_chunk += 1
print('title tag not found within {} chunks ({}b each) at {}'.format(current_chunk-1, chunksize, url))
return False