这篇文章是一个爬虫小白三个晚上的成果,由于感觉十分有意义,因此开通博客,将碰到的问题以及如何处理记录下来,希望能帮助到他人以及未来的自己。
动机
由于一些原因购买了某个网站的资源,但该资源获取较为麻烦,不支持搜索,仅支持翻页。遂产生了将其资源全部下载的想法。
考虑到安全以及隐私问题。此处不给出具体的网页名。
由于该资源通过微信公众号下载,因此首先在电脑上将该微信公众号使用chrome浏览器打开并分析。发现有多个难题。
一、 该网页是通过扫码登录,且每次退出需要重新扫码。
二、 网页中的资源列表不是一次放出,而是需要多次点击“查看更多”才可获取列表。获取资源时需要点击该资源名字。而网页元素中并没有该按钮控件,即该网页是通过“点击”动作获取XHR数据。
三、 该网页一旦点击某资源名称,会直接跳转,禁止打开新标签跳转。且跳转后,若想返回资源列表,则原网页所有元素将会重置,即又需要点击多次来获取更多的资源列表。
针对以上问题,考虑seleniumwire库来进行动态网页爬取。
三个问题的解决方案
Seleniumwire是Selenium扩展库,在Selenium的基础上,添加了部分功能,不仅可以模拟点击自动化操作,还可以获取网页交互信息。
对于问题一,在下载好chrome版本对应驱动后,直接打开网页并等待30s,这期间进行手动扫码登录。虽然考虑过记录cookie后,不需要多次扫码,但还未实现。
options = webdriver.ChromeOptions()
options.add_argument("--start-maximized")
service = ChromeService(executable_path=r'C:\Program Files\Google\Chrome\Application\chromedriver.exe')
driver = webdriver.Chrome(service=service, options=options)
driver.get(base_url)
print("Please scan the QR code to log in...")
time.sleep(30)
对于问题二,经过分析网页数据后发现,该网页所有的可点击按钮都不是以按钮元素,而是图片元素,因此通过文本直接定位该图片所在位置。随后执行点击操作。考虑到点击操作不能过于频繁,因此设置了随机等待2-3秒的策略。
问题三的解决是关键,即,不同资源间的跳转到底有何规律。经过分析发现,每次点击“查看更多,都会传输一个名字为2.0.0的XHR文件,该文件的data中有一个list,其中有一个字典为“resource_id”。通过点击资源并分析网址发现。资源网址主要由以下三个特征构成:源网址+ resource_id+pro_id。其中pro_id是在每次扫码登录后都会获得的一个字符串。因此,只需要获取所有资源的resource_id后,再通过构建网址来跳转到对应id的网页。就可以获得资源。此外,第一次获得是6个resource_id是主页上的,因此删去。
XHR文件分析:在该分析中,出现一些问题,一开始读取数据发现全为乱码,经过分析后,发现需要通过gzip方法解压再使用“utf-8”解码后才能获得详细数据。以及,在一开始以为list是字典,但之后发现,只有将list单独提取后,才可以使用字典方法进行提取。
资源网页分析:该资源形式是多张图片,由于资源网页属于静态网页,滚动到底部等待几秒后,就可以通过定位网页元素获取图片链接,从而得到资源。此外,考虑到网络原因,偶尔会出现图片资源下载失败,或下载图片为空白。因此加入了筛选:如果图片大小低于20kb,则认为下载未成功,会刷新网页重新下载。下载完毕后,将网页标题作为文件夹名称,将该网页下下载的所有图片编号后放在文件夹内。
以上就是分析网页的全部过程。此外,考虑到速度过快可能会被认为是robot,因此采用随机停止策略,在爬取完毕后随机停止2-3秒。并且在测试过程中发现,有时会突然产生错误,这些错误的原因目前还未弄清楚,但为了保证再次执行后能从上一次的resourc_id开始下载,创建了日志。
在执行过程中发现,一旦出现一次图片下载失败,即下载的图片大小小于20kb后。后续的所有资源下载都会出现该情况。目前还没有搞清楚该问题的出现的原因,因此只能采用log日志记录第一次出现问题的resource_id,然后重新执行程序。
以下代码的原型通过chatgpt生成,经过修改、测试和验证后确认可用。完整代码如下所示:
import os
import time
import json
import random
import gzip
from seleniumwire import webdriver # 用于捕获XHR请求
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from io import BytesIO
import requests
def save_strings_to_file(strings, output_file):
"""将提取的字符串保存到文件"""
strings = strings[6:] # 忽略前6个无关的字符串
try:
with open(output_file, "w", encoding="utf-8") as f:
for string in strings:
f.write(string + "\n")
print(f"Saved {len(strings)} strings to {output_file}.")
except Exception as e:
print(f"Failed to save strings to file: {e}")
def save_progress(log_file, last_processed_string):
"""保存当前进度到日志文件"""
try:
with open(log_file, "w", encoding="utf-8") as f:
f.write(last_processed_string)
print(f"Progress saved: {last_processed_string}")
except Exception as e:
print(f"Failed to save progress: {e}")
def load_progress(log_file):
"""从日志文件加载上次处理的进度"""
if os.path.exists(log_file):
try:
with open(log_file, "r", encoding="utf-8") as f:
last_processed_string = f.read().strip()
print(f"Resuming from: {last_processed_string}")
return last_processed_string
except Exception as e:
print(f"Failed to load progress: {e}")
return None
def decompress_gzip(data):
"""解压gzip压缩的字节流"""
try:
with gzip.GzipFile(fileobj=BytesIO(data)) as f:
return f.read()
except Exception as e:
print(f"Failed to decompress gzip data: {e}")
return None
def download_image(img_url, folder_path, filename, min_size=20):
"""下载图片并保存到指定路径"""
try:
response = requests.get(img_url, stream=True)
if response.status_code == 200:
# 判断图片大小,如果小于 min_size KB,则认为是空白图片
if len(response.content) < min_size * 1024:
print(f"Image at {img_url} is too small ({len(response.content)} bytes), retrying...")
return False # 返回 False 表示需要重新加载
with open(os.path.join(folder_path, filename), 'wb') as f:
for chunk in response.iter_content(1024):
f.write(chunk)
print(f"Downloaded {filename} successfully.")
return True # 返回 True 表示下载成功
else:
print(f"Failed to download {img_url}. HTTP status code: {response.status_code}")
return False
except Exception as e:
print(f"Error downloading {img_url}: {e}")
return False
def extract_and_download_images(driver, target_string, base_folder, pro_id):
"""根据目标字符串提取图片并下载"""
url = f"https://app8ejmhqxv9489.h5.xiaoeknow.com/p/course/text/{target_string}?product_id={pro_id}"
print(f"Navigating to {url}")
driver.get(url)
time.sleep(5) # 等待页面加载初始内容
# 滚动到底部,确保页面所有元素加载
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(20) # 等待 20 秒,确保页面加载完成
# 提取页面标题作为子文件夹名称
title_element = WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.TAG_NAME, "title"))
)
page_title = title_element.get_attribute("textContent").strip()
print(f"Page title: {page_title}")
safe_title = "".join(c if c.isalnum() or c in " _-" else "_" for c in page_title)
page_folder = os.path.join(base_folder, safe_title)
os.makedirs(page_folder, exist_ok=True)
# 等待图片容器加载完成
WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.CLASS_NAME, "xe-preview__content"))
)
containers = driver.find_elements(By.CLASS_NAME, "xe-preview__content")
img_urls = []
for container in containers:
images = container.find_elements(By.TAG_NAME, "img")
img_urls.extend([img.get_attribute("src") for img in images if img.get_attribute("src")])
print(f"Extracted {len(img_urls)} images.")
for img_index, img_url in enumerate(img_urls):
filename = f"image_{img_index + 1}.jpg"
success = download_image(img_url, page_folder, filename)
if not success:
print(f"Retrying to download image at {img_url}")
# 重新加载页面并等待 20 秒
driver.get(url)
time.sleep(20)
success = download_image(img_url, page_folder, filename)
if success:
print(f"Downloaded {filename} successfully after retry.")
def extract_data_from_xhr(driver, output_file, max_clicks=1):
"""从XHR响应中提取目标字符串,点击查看更多"""
extracted_strings = []
# 模拟点击查看更多
click_count = 0
while click_count < max_clicks:
try:
load_more_div = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'moreInfo') and text()='查看更多']"))
)
ActionChains(driver).move_to_element(load_more_div).click().perform()
click_count += 1
print(f"Clicked '查看更多' {click_count}/{max_clicks} times.")
time.sleep(random.uniform(2, 3))
except Exception as e:
print("No more '查看更多' elements or an error occurred:", e)
break
# 捕获并处理 XHR 请求
for request in driver.requests:
if request.response:
try:
if "2.0.0" in request.path:
response_body = decompress_gzip(request.response.body)
if response_body:
response_text = response_body.decode('utf-8')
data = json.loads(response_text)
resources = data.get("data", {}).get("list", [])
for resource in resources:
target_string = resource.get("resource_id")
if target_string:
extracted_strings.append(target_string)
print(f"Extracted: {target_string}")
except Exception as e:
print(f"Error processing request {request.path}: {e}")
save_strings_to_file(extracted_strings, output_file)
return extracted_strings
def process_strings(driver, strings_file, base_folder, pro_id, log_file):
"""从保存文件中读取目标字符串并逐一处理"""
with open(strings_file, "r", encoding="utf-8") as f:
target_strings = f.read().splitlines()
last_processed_string = load_progress(log_file)
start_index = 0
if last_processed_string:
# 找到上次处理的字符串,开始从下一个字符串开始处理
if last_processed_string in target_strings:
start_index = target_strings.index(last_processed_string) + 1
print(f"Resuming from index {start_index}...")
if start_index == 0: # 如果没有找到进度记录,默认从第70个字符串开始
start_index = 82
print(f"Starting from index {start_index} as first run...")
for idx, target_string in enumerate(target_strings[start_index:], start=start_index + 1):
try:
print(f"Processing {idx}/{len(target_strings)}: {target_string}")
extract_and_download_images(driver, target_string, base_folder, pro_id)
save_progress(log_file, target_string)
if idx % 100 == 0:
print(f"Processed {idx} strings. Pausing for 1 minute...")
time.sleep(60)
except Exception as e:
print(f"Error processing {target_string}: {e}")
break
def scrape_with_login_and_xhr(base_url, output_file):
"""模拟登录并从XHR响应中提取数据"""
options = webdriver.ChromeOptions()
options.add_argument("--start-maximized")
service = ChromeService(executable_path=r'C:\Program Files\Google\Chrome\Application\chromedriver.exe')
driver = webdriver.Chrome(service=service, options=options)
try:
driver.get(base_url)
print("Please scan the QR code to log in...")
time.sleep(30)
current_url = driver.current_url
pro_id = current_url.split("pro_id=")[-1].split("&")[0]
print(f"Extracted pro_id: {pro_id}")
#如果字符串已经读取完毕,那么该函数可以不被执行
#、extract_data_from_xhr(driver, output_file, max_clicks=25)
base_folder = "downloaded_images"
os.makedirs(base_folder, exist_ok=True)
# 读取字符串文件并开始爬取图片
process_strings(driver, output_file, base_folder, pro_id, "progress.log")
except Exception as e:
print(f"Error occurred: {e}")
finally:
driver.quit()
if __name__ == "__main__":
base_url = ‘目标网页’
output_file = "extracted_strings1.txt"
scrape_with_login_and_xhr(base_url, output_file)
浙公网安备 33010602011771号