CSS位置偏移反爬案例分析与爬取实战
案例
案例网址:https://antispider3.scrape.cener/,页面如下图所示:
尝试用Selenium获取首页的页面源代码,并解析每个标题的内容:
from selenium import webdriver
from pyquery import PyQuery as pq
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.chrome.service import Service
options = webdriver.ChromeOptions()
services = Service('../Selenium/chromedriver')
browser = webdriver.Chrome(service=services, options=options)
browser.get('<https://antispider3.scrape.center/>')
WebDriverWait(browser, 10).until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, '.item')))
html = browser.page_source
doc = pq(html)
names = doc('.item .name')
for name in names.items():
print(name.text())
browser.close()
运行结果如下:
Wonder
清 白 家 风
篇 法 妃 老 的 上 宠 终 册 ) ( 结 下
士 为 己 ) 二 册 知 全 (
, 些 年 们 一 孩 我 的 那 起 女 追
非 我 倾 城 ( 全 三 册 )
朝 事 儿 明 些 那
的 你 忘 和 书 笑 我
全 第 波 集 小 一 卷 王
怦 然 动 心
龙枪编年史(全3册)
龙 枪 册 全 奇 ( ) 三 传
黎 明 之 街
其 知 认 理 学 心 示 启 及
银河帝国2:基地与帝国
银 河 帝 国 : 基 地
级 下 材 全 教 学 - 年 解 语 文 四 小
越界言论(第3卷)
结果中很多标题的文字顺序是乱的,例如《明朝那些事儿》对应的输出结果是“朝事儿明些那”,这是怎么回事?
排查
我们去浏览器里面研究一下源代码,如图所示:
发现一个字对应一个span节点,这个节点本身的顺序也是乱的,所以用pyquery提取出来的标题内容乱序就不足为怪了。
源代码中的文字本身是乱的,那为什么在网页上看到的标题是正确的?这是因为网页本身利用CSS控制了文字的偏移位置,什么意思呢?观察下源代码:
<h3 data-v-7f1a77ef="" class="m-b-sm name">
<span data-v-7f1a77ef="" class="char" style="left: 80px;">
儿
</span>
<span data-v-7f1a77ef="" class="char" style="left: 16px;">
朝
</span>
<span data-v-7f1a77ef="" class="char" style="left: 0px;">
明
</span>
<span data-v-7f1a77ef="" class="char" style="left: 48px;">
些
</span>
<span data-v-7f1a77ef="" class="char" style="left: 32px;">
那
</span>
<span data-v-7f1a77ef="" class="char" style="left: 64px;">
事
</span>
</h3>
可以发现,每个span节点都有一个style属性,表示CSS样式,left的取值各不相同。另外,在浏览器中观察一下每个span节点的完整样式,如图所示:
span节点还有两个额外的样式,是display: inline-block和position:absolute,或者比较重要,代表绝对定位,设置这个样式后,就可以通过修改left的值控制span节点在页面中的偏移位置了,例如left:0px代表不偏移;left:16px代表从左边算起向右偏移16像素,于是节点就到了右边。源代码中,“明”子的偏移量是0,“朝”字的偏移量是16像素,“那”字的偏移量是32像素,依此类推,最终标题的视觉效果就变成了“明朝那些事儿”。
爬取
了解了基本原理后,只需要获取每个span节点的style属性,提取出偏移值,然后排序就可以得到最终结果了。先实现基本的提取方法:
from selenium import webdriver
from pyquery import PyQuery as pq
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.chrome.service import Service
import re
def parse_name(name_html):
chars = name_html('.char')
items = []
for char in chars.items():
items.append({
'text': char.text().strip(),
'left': int(re.search('(\\d+)px', char.attr('style')).group(1))
})
items = sorted(items, key=lambda x:x['left'], reverse=False)
return ''.join([item.get('text') for item in items])
options = webdriver.ChromeOptions()
services = Service('../chromedriver')
browser = webdriver.Chrome(service=services, options=options)
browser.get('<https://antisipder3.scrape.center/>')
WebDriverWait(browser, 10).until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, '.item')))
html = browser.page_source
doc = pq(html)
names = doc('.item .name')
for name_html in names.items():
name = parse_name(name_html)
print(name)
browser.close()
这里定义了一个parse_name方法,用来解析页面源代码得到最终的标题。它接收一个参数name_html,就是标题的HTML文本,类似这样:
<h3 data-v-7f1a77ef="" class="m-b-sm name">
<span data-v-7f1a77ef="" class="char" style="left: 80px;">
儿
</span>
<span data-v-7f1a77ef="" class="char" style="left: 16px;">
朝
</span>
<span data-v-7f1a77ef="" class="char" style="left: 0px;">
明
</span>
<span data-v-7f1a77ef="" class="char" style="left: 48px;">
些
</span>
<span data-v-7f1a77ef="" class="char" style="left: 32px;">
那
</span>
<span data-v-7f1a77ef="" class="char" style="left: 64px;">
事
</span>
</h3>
在parse_name方法中,我们首先选取.char节点,将其赋值为chars变量,然后遍历chars变量,其中每个条目各自对应一个span节点,其内容类似于:
<span data-v-7f1a77ef="" class="char" style="left: 16px">朝</span>
在parse_name方法中,我们首先选取.char节点,将其赋值为chars变量,然后遍历chars变量, 其中每个条目各自对应一个span节点,其内容类似于:
<span data-v-7f1a77ef="" class="char" style="left: 16px">朝</span>
遍历过程中,提取了span节点的文本内容作为字典的text属性,还提取了style属性的内容,例如这里提取的是16px,并用正则表达式提取了其中的数值,这里是16,将其赋值为字典的left属性。
遍历结束后,items的结果类似下面这样:
[{
'text': '些', 'left': 48}, {
'text': '事', 'left': 64}, {
'text': '儿', 'left': 80}, {
'text': '那', 'left': 32}, {
'text': '朝', 'left': 16}, {
'text': '明', 'left': 0}]
面对这样的结果,怎么排序呢?直接调用sorted方法就行,它有两个参数,一个是key,用来指定根据什么排序,这里我们直接使用lambda表达式提取span节点的left属性,所以最终结果是根据left的值排序而得;另一个参数是reverse,用来指定排序方式,此处将其设置为False,表示从小到大排序。排序完的items变成了这样:
[{
'text': '明', 'left': 0}, {
'text': '朝', 'left': 16}, {
'text': '那', 'left': 32}, {
'text': '些', 'left': 48}, {
'text': '事', 'left': 64}, {
'text': '儿', 'left': 80}]
最后将其中的text值提取出来并拼接,就得到了最终结果:
清白家风
法老的宠妃终结篇(上下册)
士为知己(全二册)
那些年,我们一起追的女孩
非我倾城(全三册)
明朝那些事儿
我和你的笑忘书
王小波全集第一卷
怦然心动
龙枪传奇(全三册)
黎明之街
认知心理学及其启示
银河帝国:基地
小学教材全解-四年级语文下