为什么cURL的响应正确但scrapy不正确?
为什么cURL能得到正确的响应,而scrapy却不行呢?
我想抓取的网站使用JavaScript来填写一个表单,然后发送这个表单并进行验证,最后才会提供内容。
我在Python中模拟了这个JavaScript,首先从初始的GET请求中抓取了参数。我的"TS644333_75"的值和JavaScript中的值是一样的(我通过使用document.write(..)来测试,而不是正常提交),而且如果把结果复制粘贴到cURL中也是可以正常工作的。例如:
curl --http1.0 'http://www.betvictor.com/sports/en/football' -H 'Connection: keep-alive'
-H 'Accept-Encoding: gzip,deflate' -H 'Accept-Language: en'
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
-H 'Referer: http://www.betvictor.com/sports/en/football' -H 'User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0'
--data
'TS644333_id=3&
TS644333_75=286800b2a80cd3334cd2895e42e67031%3Ayxxy%3A3N6QfX3q%3A1685704694&
TS644333_md=1&
TS644333_rf=0&
TS644333_ct=0&
TS644333_pd=0' --compressed
关于TS644333_75,我只是简单地复制了我在模拟JavaScript时Python代码计算出的结果。
在wireshark中监控数据包显示这个POST请求的内容,可以在这里查看(我添加了一些空行让POST数据更易读,但其他部分和wireshark中看到的一样)。
但是如果我启动一个scrapy shell:
1) scrapy shell "http://www.betvictor.com/sports/en/football"
然后构造一个表单请求:
2) from scrapy.http import FormRequest
req=FormRequest(
url='http://www.betvictor.com/sports/en/football',
formdata={
'TS644333_id': '3',
'TS644333_75': '286800b2a80cd3334cd2895e42e67031:yxxy:3N6QfX3q:1685704694',
'TS644333_md': '1',
'TS644333_rf': '0',
'TS644333_ct': '0',
'TS644333_pd': '0'
},
headers={
'Referer': 'http://www.betvictor.com/sports/en/football',
'Connection': 'keep-alive'
}
)
接着获取它:
3) fetch(req)
我得到的响应内容只是另一个JavaScript挑战,而不是我想要的内容。
然而在wireshark中看到的数据包(同样为了让POST参数更易读我加了一些换行)可以在这里查看,看起来完全相同。
到底出了什么问题?看起来相同的数据包为什么会导致不同的服务器响应?为什么在scrapy中不行呢?
可能是我POST的参数中的":"的编码有问题,但看起来编码是正确的,而且在wireshark中也匹配,所以我觉得这不是问题所在。
2 个回答
结果发现,对于这个服务器,参数的顺序真的很重要(我猜是因为它模拟了一个有序输入的隐藏表单,这也是一个额外的验证检查)。在使用 Python 的 requests 库时,如果手动用 POST 字符串和 URL 编码(也就是把 ':'
转换成 '%3A'
),就能让事情正常运作。所以虽然用 Wireshark 捕获的数据包几乎一模一样,它们唯一的不同就是参数字符串的顺序,确实这是关键所在。
在 Scrapy
中,像这样传递一个元组:
ot= ( ('TS644333_id', '3'),
('TS644333_75', value),
('TS644333_md', '1'),
('TS644333_rf', '0'),
('TS644333_ct', '0'),
('TS644333_pd', '0')
)
给 formdata=
而不是字典,这样可以保持顺序,也能正常工作。
另外,头部信息 {'Content-Type': 'application/x-www-form-urlencoded'}
是必须的。
正如 anana
在他的回答中提到的,给所有请求的 URL 后面加一个斜杠 '/' 也能解决问题。实际上,如果这样做,你甚至可以只用 GET 请求,不需要 JavaScript 模拟和表单 POST!
如果你在网址后面加一个斜杠,它似乎就能正常工作了。所以你可以用同样的scrapy请求,只需要把网址改成:
http://www.betvictor.com/sports/en/football/
额外的例子:
我在测试另一个网站时也遇到了同样的问题,那个页面在用curl
的时候运行得很好,但用requests
却不行。经过一番折腾后,发现加上额外的斜杠就解决了这个问题。
import requests
import json
r = requests.get(r'https://bet.hkjc.com/marksix/getJSON.aspx/?sd=20190101&ed=20190331&sb=0')
pretty_json = json.loads(r.text)
print (json.dumps(pretty_json, indent=2))
返回的是:
[
{
"id": "19/037",
"date": "30/03/2019",
"no": "15+17+18+37+39+49",
"sno": "31",
"sbcode": "",
...
...
在.aspx后面的斜杠是很重要的。没有它就不行。如果没有斜杠,页面会返回一个空的javascript挑战。
import requests
import json
#no slash
r = requests.get(r'https://bet.hkjc.com/marksix/getJSON.aspx?sd=20190101&ed=20190331&sb=0')
print(r.text)
返回的是:
<HTML>
<head>
<script>
Challenge=341316;
ChallengeId=49424326;
GenericErrorMessageCookies="Cookies must be enabled in order to view this page.";
</script>
<script>
function test(var1)
{
var var_str=""+Challenge;
var var_arr=var_str.split("");
var LastDig=var_arr.reverse()[0];
var minDig=var_arr.sort()[0];
var subvar1 = (2 * (var_arr[2]))+(var_arr[1]*1);
var subvar2 = (2 * var_arr[2])+var_arr[1];
var my_pow=Math.pow(((var_arr[0]*1)+2),var_arr[1]);
var x=(var1*3+subvar1)*1;
var y=Math.cos(Math.PI*subvar2);
var answer=x*y;
answer-=my_pow*1;
answer+=(minDig*1)-(LastDig*1);
answer=answer+subvar2;
return answer;
}
</script>
<script>
client = null;
if (window.XMLHttpRequest)
{
var client=new XMLHttpRequest();
}
else
{
if (window.ActiveXObject)
{
client = new ActiveXObject('MSXML2.XMLHTTP.3.0');
};
}
if (!((!!client)&&(!!Math.pow)&&(!!Math.cos)&&(!![].sort)&&(!![].reverse)))
{
document.write("Not all needed JavaScript methods are supported.<BR>");
}
else
{
client.onreadystatechange = function()
{
if(client.readyState == 4)
{
var MyCookie=client.getResponseHeader("X-AA-Cookie-Value");
if ((MyCookie == null) || (MyCookie==""))
{
document.write(client.responseText);
return;
}
var cookieName = MyCookie.split('=')[0];
if (document.cookie.indexOf(cookieName)==-1)
{
document.write(GenericErrorMessageCookies);
return;
}
window.location.reload(true);
}
};
y=test(Challenge);
client.open("POST",window.location,true);
client.setRequestHeader('X-AA-Challenge-ID', ChallengeId);
client.setRequestHeader('X-AA-Challenge-Result',y);
client.setRequestHeader('X-AA-Challenge',Challenge);
client.setRequestHeader('Content-Type' , 'text/plain');
client.send();
}
</script>
</head>
<body>
<noscript>JavaScript must be enabled in order to view this page.</noscript>
</body>
</HTML>