爬虫代码已复制

Python爬虫实战C03爬虫实战练习案例解析

打开Python爬虫实战练习C03页面 爬虫实战练习C03,看到页面上是鸢尾花数据集(Iris Dataset)。先点击“立即验证”看看这一关要求校验什么数据,发现是要求“Sepal Width列的总和”。

回到C03的页面,把页面都浏览一遍。在页面底部发现了有分页的页码。

这时候先按快捷键 F12 打开浏览器开发者工具,切换到 Network 标签,点击其中一个页码,发现浏览器向 scraper-practice-c03 这个地址发送了一个 POST 请求。点击这个请求,在开发者工具右边切换到 Payload 选项卡,发现了以下的发送参数:

{
  xorResult: 1744472901,
  random: 8827,
  timestamp: 1744472903,
  hash: "347443ae0a3ff6eabc80b48b117b867e"
}

回到页面上,右键 -> 查看页面源码,发现分页页码调用了一个 getIrisData() 函数,并把页码作为参数传递了进去,并且在 html 源码的第 384 行发现了这个函数,函数名正常显示,但其它 JavaScript 代码都被加密混淆了。

根据经验,这种情况就有两种解决方法:一种就是使用 Selenium;另外一种就逆向这段 JavaScript。我们是在练习爬虫,不做选择,全都要!

先尝试 Selenium 来爬取页面,先找到页码的 xpath 表达式,因为我们等一下需要通过 Selenium 自动化点击。并且记住总页数,因为我们需要循环点击进行翻页,可以直接跳过第一页的点击,因为打开就是第一页了,不用点就出来了。同时也要分析出 Sepal Width 数据列的 xpath 表达式,因为我们要获取数据进行计算。

基于 Selenium 的爬虫核心代码如下:

client = webdriver.Chrome()
client.get(url)
time.sleep(5)
html = client.page_source
# print(html)
for i in range(1,5):
    client.find_elements(By.XPATH, '//ul/li/a')[i].click()
    time.sleep(5)
    html = client.page_source

client.quit()

以上爬虫代码使用 Selenium 打开页面后输出 html 源码并进行解析,然后点击第二页重复上述操作,直至最后一页。

接下来我们就开始第二种方法:逆向 getIrisData() 函数的 JavaScript 代码。

回到开发者工具,切换到 Sources (源码)选项卡,最左侧 Page 那里有个树形菜单,找到并点击 scraper-practice-c03,就能看到 html 源码在中间展开了。找到我们刚才的 getIrisData() 函数所有的第 384 行,因为加密混淆的原因,这个函数的代码是在同一行里面的。这样不利于我们分析代码,这时候有个调试 JavaScript 代码的小技巧:点击代码左下角的大括号就能把代码格式化成便于阅读分析的格式。

大括号格式化JavaScript代码

在格式化后的 getIrisData() 函数内任意一行下个断点,然后刷新页面再点击任意的翻页页码,就发现代码停在了断点处,并且右边的 Local 处出现了函数前几个变量的值。如果断点断得比较靠前没有出现全部变量的值,可以按 F10 一行一行执行 JavaScript 代码。

JavaScript调试

此时把光标移动到类似 \u0066 这样的 Unicode 编码上,就能看到这些编码后的内容。而 0xbe628 这样的字符串在 JavaScript 开发语言中代表十六进制数值。 结合前面的 Payload,我们就能根据这些值对应起每个变量的意义:

  • _0x39669e = xorResult # 出现了 JavaScript 异或计算符号 ^
  • _0x1fa068 = random # 出现了 random 关键字
  • _0x542b78 = timestamp # 出现了 now 关键字
  • _0x56e6b4 = hash # 出现了 md5 关键字

通过操作系统自带的计算器,把十六进制的值转换后就能得到十进制的值,并且可以直接利用计算器的计算功能计算出如:(0xba7af ^ 0xbb8ef) 的值( ^ 对应计算器上的 xor)。结合以上的分析可以写出 getIrisData() 函数生成请求参数的 JavaScript 代码如下:

function getIrisData(pageno){
  const randomNum = Math.floor(Math.random() * 8000 + 2000);
  const timestamp = Math.floor(Date.now() / 1000);
  const xorResult = pageno ^ timestamp;
  const _md5 = md5(`${xorResult}${timestamp}`).toString();
  const payload = {
      xorResult: xorResult,
      random: randomNum,
      timestamp: timestamp,
      hash: _md5
  };
  ...
}

有了业务逻辑我们就可以使用 Python 编写爬虫代码了:

pageno = 1
random_value = random.randint(2000, 10000)
timestamp = int(time.time())
xorResult = pageno ^ timestamp
md5_hash = hashlib.md5()
md5_hash.update(f'{xorResult}{timestamp}'.encode('utf-8'))
hash = md5_hash.hexdigest()
payload = {
    'random': random_value,
    'timestamp': timestamp,
    'hash': hash,
    'xorResult': xorResult
}
# print(payload)
json_response = requests.post(base_url, headers=myheaders,json=payload).text

print(json_response)
json_data = json.loads(json_response)
for item in json_data:
    print(item)

这样一来,我们就能直接通过 Python 代码向接口发送 POST 请求并直接获取 Json 数据了。然后对这些 Json 数据进行解析及统计就能得到我们这一关的答案了。

完整的爬虫案例代码:爬虫案例代码