最近接了一个活,给定一些链接,统计每个链接的跳转次数以及最终跳转至的网址。

刚开始是用scrapy写的,通过Google与stack overflow了解到下面这种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
conn = pymysql.connect(
host='host',
user='user',
password='password',
db='database')
cursor = conn.cursor()
sql = '''
select distinct click_url from table
where country = "{}" and status=0'''.format(country)
cursor.execute(sql)
urls = cursor.fetchall()
start_urls = (x[0] for x in urls)
# 无限跳转 测试使用
# start_urls = ('http://maoshuai.sinaapp.com/cnblog/302test.php', )
def start_requests(self):
for url in self.start_urls:
yield scrapy.Request(url, meta={'original_url': url})
def parse(self, response):
# print('response url', response.url)
# print('response status', response.status)
request = response.request
redirected_url = request.url
original_url = response.meta['original_url']
if original_url not in self.redirect_count.keys():
self.redirect_count.update({original_url: 0})
if response.status >= 300 and response.status < 400:
self.redirect_count[original_url] += 1
location = to_native_str(
response.headers['location'].decode('latin1'))
request = response.request
redirected_url = urljoin(request.url, location)
# print('redirected to ', redirected_url)
yield scrapy.Request(
redirected_url,
meta={'original_url': original_url},
callback=self.parse)
else:
redirected = request.replace(
url=redirected_url, method='GET', body='')
redirected.headers.pop('Content-Type', None)
redirected.headers.pop('Content-Length', None)
yield redirected
count = self.redirect_count.get(original_url)
if count >= 1 and response.url.startswith('https://play.google.com'):
print(self.redirect_count.get(original_url))
self.update_count(
original_url, self.redirect_count.get(original_url))

大致说一下上面代码的思路,在start_requests中,yield request时携带原始URL,从而利用dict统计该URL的跳转次数;parse函数是核心功能代码,用于处理302重定向,通过递归调用parse函数处理重定向后的URL。

上面这种代码是可行的,但是由于需要在生产环境中运行代码,而自己对上面代码的整个执行流程不够清楚,因此最后还是选用requests库来解决该问题,尽管速度上会慢一些,但是自己对整个程序的执行有更清晰的把控。(PS:也是从这里我意识到当性能不是程序的瓶颈时,一定不要牺牲稳定性来提升程序性能,这种行为带来的代价往往是巨大的!)

改用requests改写后,写起来很轻松,因为使用response.history就可以轻松获取重定向最终链接以及中间重定向次数。具体代码参考requests官方文档即可。

但随后又有了新需求,当使用移动客户端访问这些链接时,链接可能最终跳转至以market://开头的URL,而这种URL会被requests认为是无效URL而报错,为了能够正确处理这种情况,也即获取以market://开头的最终URL以及中间的跳转次数,很明显简单地使用response.history无法达到目的。

当然,如果在访问一个URL时,禁止重定向,然后自行处理重定向过程也是一个可行的办法。但是在实践中遇到302中response的Location字段跳转至本站根域名下的其他链接,也就是说Location字段保存的是一个相对URL,因此自行处理重定向就需要额外处理相对URL,这让重定向问题又一步复杂化,故放弃这个方法。

之后想到的方法是查阅官方文档,相信官方文档应该有对重定向问题更加自定义的支持。果不其然,在官方文档的角落里翻阅到requests中有resolve_redirects()方法,虽然文档中对该方法无较多介绍,但通过Google大法也是找到了一些代码片段,通过对代码进行加工改造,得到能够完美解决我的难题的以下代码版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
def iter_send(self, session, request, **kwargs):
# 返回重定向过程中一系列response
# 如果最终结果为应用内跳转url,类似market://形式则返回上一个response
resp = session.send(request, allow_redirects=False,
timeout=10, **kwargs)
redir_iter = session.resolve_redirects(resp, request, timeout=10)
while resp.is_redirect:
try:
resp = next(redir_iter)
yield resp
except requests.exceptions.InvalidSchema:
break
yield resp

虽然问题的整个解决过程自己没有进行复杂地编程,但是可以发现代码写的多了能够训练出程序员的一种直觉,接着再去验证自己的直觉往往能够节省很多时间,可见经验的重要性了。