该站点使用 javascript 生成数据表。有一些替代方案,如scrapyjs或splash允许获取 js 渲染的 html 页面。如果您只需要抓取一页,则最好使用 Selenium。
否则,您可能需要进入核心模式并使用数据对站点中正在发生的事情进行逆向工程。我将向您展示如何做到这一点。
首先,启动scrapy shell
我们可以浏览网页:
scrapy shell http://www.paddypower.com/football/football-matches/premier-league
注意:我使用的是 python 2.7.4、ipython 0.13.2 和 scrapy 0.18.0。
如果您在浏览器中查找“Crystal Palace v Fulham”的源代码,您将看到有一个包含该参考的 JavaScript 代码。该<script>
块看起来像:
document.bodyOnLoad.push(function() {
lb_fb_cpn_init(
"",
"html",
"MR_224",
{category: 'SOCCER',
我们在 shell 中查找这个元素:
In [1]: hxs.select('//script[contains(., "lb_fb_cpn_init")]')
Out[1]: [<HtmlXPathSelector xpath='//script[contains(., "lb_fb_cpn_init")]' data=u'<script type="text/javascript">\n/* $Id: '>]
如果您查找lb_fb_cpn_init
参数,您将看到我们正在查找的数据以这种形式作为参数传递:
[{names: {en: 'Newcastle v Liverpool'}, ...
事实上,有这样的三个论点:
In [2]: hxs.select('//script[contains(., "lb_fb_cpn_init")]').re('\[{names:')
Out[2]: [u'[{names:', u'[{names:', u'[{names:']
所以我们将它们全部提取出来,注意我们使用了很多正则表达式:
In [3]: js_args = hxs.select('//script[contains(., "lb_fb_cpn_init")]').re(r'(\[{names:(?:.+?)\]),')
In [4]: len(js_args)
Out[4]: 3
这里的想法是我们想要将 javascript 代码(它是一个文字对象)解析为 python 代码(一个 dict)。我们可以使用json.loads
,但是要这样做,js 代码必须是一个有效的 json 对象,也就是说,字段名称和字符串包含在""
.
我们继续这样做。首先,我将单个字符串中的参数作为 javascript 列表加入:
In [5]: args_raw = '[{}]'.format(', '.join(js_args))
然后我们将字段名称括入""
并用双引号替换为单引号:
In [6]: import re
In [7]: args_json = re.sub(r'(,\s?|{)(\w+):', r'\1"\2":', args_raw).replace("'", '"')
这可能并不总是适用于所有情况,因为 javascript 代码可能具有不太容易用单个re.sub
和/或替换的模式.replace
。
我们准备将 javascript 代码解析为 json 对象:
In [8]: import json
In [9]: data = json.loads(args_json)
In [10]: len(data)
Out[10]: 3
在这里,我只是在寻找事件名称和赔率。您可以查看data
内容以了解其外观。
幸运的是,数据似乎具有相关性:
In [11]: map(len, data)
Out[11]: [20, 20, 60]
dict
您也可以使用该ev_id
字段从它们三个中构建一个。我将假设它具有直接相关性,并且data[0]
每个事件包含 3 个项目。这可以通过以下方式轻松验证:data[1]
data[2]
In [12]: map(lambda v: v['ev_id'], data[2])
Out [12]:
[5889932,
5889932,
5889932,
5889933,
5889933,
5889933,
...
使用一些 python-fu,我们可以合并记录:
In [13]: odds = iter(data[2])
In [14]: odds_merged = zip(odds, odds, odds)
In [15]: data_merged = zip(data[0], data[1], odds_merged)
In [16]: len(data_merged)
Out[16]: 20
最后,我们收集数据:
In [17]: get_odd = lambda obj: (obj['names']['en'], '/'.join([obj['lp_num'], obj['lp_den']]))
In [18]: event_odds = []
In [19]: for event, _, odds in data_merged:
....: event_odds.append({'name': event['names']['en'], 'odds': dict(map(get_odd, odds)), 'url': event['url']})
....:
In [20]: event_odds
Out[20]:
[{'name': u'Newcastle v Liverpool',
'odds': {u'Draw': u'14/5', u'Liverpool': u'17/20', u'Newcastle': u'3/1'},
'url': u'http://www.paddypower.com/football/football-matches/premier-league-matches/Newcastle%2dv%2dLiverpool-5889932.html'},
{'name': u'Arsenal v Norwich',
'odds': {u'Arsenal': u'3/10', u'Draw': u'9/2', u'Norwich': u'9/1'},
'url': u'http://www.paddypower.com/football/football-matches/premier-league-matches/Arsenal%2dv%2dNorwich-5889933.html'},
{'name': u'Chelsea v Cardiff',
'odds': {u'Cardiff': u'10/1', u'Chelsea': u'1/4', u'Draw': u'5/1'},
'url': u'http://www.paddypower.com/football/football-matches/premier-league-matches/Chelsea%2dv%2dCardiff-5889934.html'},
{'name': u'Everton v Hull',
'odds': {u'Draw': u'10/3', u'Everton': u'4/9', u'Hull': u'13/2'},
'url': u'http://www.paddypower.com/football/football-matches/premier-league-matches/Everton%2dv%2dHull-5889935.html'},
{'name': u'Man Utd v Southampton',
'odds': {u'Draw': u'3/1', u'Man Utd': u'8/15', u'Southampton': u'11/2'},
'url': u'http://www.paddypower.com/football/football-matches/premier-league-matches/Man%2dUtd%2dv%2dSouthampton-5889939.html'},
...
如您所见,网页抓取可能非常具有挑战性(而且很有趣!)。这一切都取决于网站如何显示数据。在这里你可以只使用 Selenium 来节省时间,但是如果你想抓取一个大型网站,与 Scrapy 相比,Selenium 会非常慢。
此外,您还必须考虑网站是否会经常更新代码,在这种情况下,您将花费更多时间对 js 代码进行逆向工程。在这种情况下,像scrapyjs或splash这样的解决方案可能是更好的选择。
最后的评论:
- 现在您拥有提取数据所需的所有代码。您需要将此集成到您的蜘蛛回调中并构建您的项目。
- 不要使用
log.start
. 使用设置LOG_FILE
(命令行参数:)--set LOG_FILE=mylog.txt
。
- 记住
.extract()
总是返回一个列表。