python爬虫实例(post目录与get详情页双线程)

本文介绍了如何使用Python的requests库和selenium工具爬取一个小型贴纸商品网站的商品信息。在理解了网站的GET和POST请求机制后,通过多线程分别处理目录页和商品详情页的爬取,实现了从获取商品链接到解析HTML、下载图片的完整流程。在过程中遇到了编码问题和动态网页的挑战,最终通过解析HTML和模拟POST请求成功爬取了目标数据。
摘要由CSDN通过智能技术生成

前言

        最近出于朋友个人需求,需要爬取一个小型的贴纸商品网站,主要目标是商品的名称、税前后价格以及商品的图片,

        目标网站:https://www.brickstickershop.com/

        在爬虫方面完全是路人级,之前在校做NLP时第一次实际遇到,那时也是糊里糊涂地找师姐协助才爬到的源数据,工作后偶尔爬过零星几个静态网页,都是比较简单的(类似拼URL就能成的)。这回首次应需去爬一个“野生”网站,并且同时用到了POST和GET,因此稍微记录下。

网页探查

        商品详情页:

        由于目标包括获得商品尽可能清楚的图片,因此需要到每个商品详情页去爬取商品的信息。商品详情页如下图:

        需要爬取的部分已经用红框标记了出来,经过定位标签和请求测试,发现该页面通过GET就可以获得包含所需信息的HTML(静态),那么通过解析HTML获取标签内容就可以了。

        商品目录页:

        已经确定了一个商品信息的爬取的方法,那么我们需要从目录页获得每个商品的链接来进行一一爬取。目录页如下图:

        这里有趣的事情就发生了,首先尝试get后发现获取的HTML里并没有控制台Elements里看到的目标标签,其次再点击换页之后网址也没有发生变化,猜测可能是动态网页了,通过控制台Network查看抓包发现,的确是在换页时产生了一个ajax的application/x-www-form-urlencoded请求,并且返回的是HTML,那就好说了,直接POST获得一个HTML解析就好了。查看formdata如下:

        接下来只要按formdata的样式POST就可以,这里有个比较坑的地方, 就是这个参数xajaxargs[],urlencode会把中括号编成%5B%5D,但是实际请求里原中括号还是存在的。。。

        所以需要注意替换下,即:

        form_data=form_data.replace("xajaxargs%5B%5D=","xajaxargs[]=&xajaxargs[]=")

爬取逻辑

        探查完网站后,可以梳理出一个爬取流程:

        步骤1:post获取每个目录页,并从目录页中提取到所有商品详情页的链接。

        步骤2:通过get去从每一个商品详情链接(步骤1获得)中提取需要信息以及下载图片。

        两个步骤,因此可以考虑多线程,创建个全局Queue线程通信,步骤1生产,步骤2消费。

代码尝试

        目标具体成效:

        最终目标是一个商品一个文件夹,了尽量简化,这里将爬取的文本信息(商品名称、税前价格、税后价格)用于命名文件夹,文件夹中只保存商品的相关图片。可以先选取一些具体商品详址进行尝试,基于不同商品特征(比如商品名中有特殊符号、没有图片等情况)做简单的测试。略。

        插曲——selenium尝试:

        其实因为urlencode编码"[]"的问题一开始没注意到,导致自认为步骤1的post是整不明白了,又是小需求,selenium算了,于是步骤1还写了个selenium版。浏览器驱动的安装这里不再赘述了,贴下粗糙的代码。。。

# -*- coding: utf-8 -*-
from urllib.parse import urlencode
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
import time

pro_list=[]
url="https://www.brickstickershop.com/website/index.php?Show=Search&KeyWord=#filter:7c7c874cc679560ca3e52b99a0114df7"
s=Service("./webdriver")
browser = webdriver.Chrome(service=s)
page=1
try:
    browser.get(url)
    ww = WebDriverWait(browser,20)
    while True:
        #一页24个商品,直接拼xpath获取各个商品详情页链接
        for n in range(1,25):
            xpath='/html/body/div[1]/div/div[5]/div/div/div[{n}]/div/div/a'.format(n=str(n))
            #until很重要,等页面加载
            product=ww.until(EC.presence_of_element_located((By.XPATH,xpath)))
            product_url=product.get_attribute('href')
            print(product_url)
            pro_list.append(str(product_url))
        try:
            #until很重要,等页面加载
            objBtn=ww.until(EC.presence_of_element_located((By.XPATH,'/html/body/div[1]/div/div[6]/div/div/button[2]')))
            #点击按钮(下一页)
            objBtn.click()
            page+=1
            #保守等加载
            time.sleep(5)
        except:
            print("无法获得新的目录页,当前页:%d"%(page))
            break
finally:
    #关闭浏览器
    browser.close()

完整代码

        selenium虽然好用且稳,但是使用驱动毕竟还是速率有失,本案例也并不复杂,因此还是使用requests来完成整个任务。

# -*- coding: utf-8 -*-
import re
import os
import time
import requests
from threading import Thread
from queue import Queue,Empty
from lxml import etree
from bs4 import BeautifulSoup
from urllib.parse import urlencode
# import urllib.request


#用于读取目录页,目录页都是ajax返回的(返回的整个HTML),因此POST方式获得每页并解析出商品的URL(每页24个)放入Queue共另一线程读取商品详细页面
def postProductUrl(url,headers,xajaxr):
    s=requests.Session()
    #可以保留一个pro_list列表,记录所有商品的详情页面URL
    pro_list=[]
    page=0
    cyctag=True
    while cyctag:
        page+=1
        form_data={
        "xajax": "ProductFilter",
        "xajaxr": xajaxr,
        "xajaxargs[]":"",
        "xajaxargs[]":"<xjxquery><q>SearchMethod=PARTMATCH&Filter[1171652]=&SortingOrder=ProductName&CurrentPage=%d&CategoryLayoutId=&SecondColorSchemeId=</q></xjxquery>"%(page)
        }
        #对表单进行url编码
        form_data=urlencode(form_data)
        #中括号和参数空值编码会出现与网页源请求不同,因此需要替换下(很重要!)
        form_data=form_data.replace("xajaxargs%5B%5D=","xajaxargs[]=&xajaxargs[]=")
        #表单POST
        response = s.post(url= url,headers=headers,data=form_data)
        html_data=response.text
        try:
            #BS准备解析HTML
            soup = BeautifulSoup(html_data, 'lxml')
            #超过有效页数后会返回一个有'h5'标签的页面,如果有'h5'即认为到头了(大概测试了下,也可以使用别的识别方法)
            end_note=soup.find('h5')
            if end_note:
                print("目录获取完毕,共计%d页"%(page))
                cyctag=False
                break
            else:
                #BS解析标签获得HTML中的目标内容(这里是各个商品详情页的链接)
                product_list=soup.find_all(attrs={'class':'c-product-block'})
                for j in range(len(product_list)):
                    product_url=product_list[j].find('a').get('href')
                    pro_list.append(str(product_url))
                    #product_url_q为全局Queue
                    product_url_q.put(str(product_url))
                #没代理的话sleep一下尽可能避免被拒
                time.sleep(5)
        except:
            print("获取第%d页目录失败"%(page))
            cyctag=False
            break

# 用于读取商品详情页,详情页是静态的
def getProductInfo(headers,path):
    totalcnt=0
    s=requests.Session()
    cyctag=True
    while cyctag:
        try:
            #获得Queue里的url,等待20秒后认为不会再有新消息
            url=product_url_q.get(block=True, timeout=20)
            print(url)
            #Queue的Empty抛错,结束工作
        except Empty:
            print("商品URL列表已空")
            cyctag=False
            break
        if url.strip()=='':
            continue
        #GET即可
        response = s.get(url= url,headers=headers)
        totalcnt+=1
        wb_data = response.text
        soup = BeautifulSoup(wb_data, 'lxml')
        #获取title
        title=soup.find(attrs={'class':'content-page-title product-title'}).text
        #title=url[33:] #URL里其实也有title
        #获取税后价格
        price1_exc=soup.find(attrs={'id':'Price1_exc'}).text[2:]
        #获取税前价格
        price1_inc=soup.find(attrs={'id':'Price1_inc'}).text[2:]
        #获取有图片的标签
        product__thumb=soup.find(attrs={'class':'product__images','id':'product__images'})
        if not product__thumb:
            print("没得图呢")
            continue
        #想把爬到的文字信息直接放文件夹名里,太天真了,不规范的命名很容易建夹时报错,这里就先嗯替换和截取了,建议使用更好的方案
        #PS.最好是对文件夹名用生成的编码,再单独整个全局文件用于记录商品编码、名字(其他爬取到的文字信息)与网页的映射
        title=title.replace(' ','-')
        title=title.replace('\\','-')
        title=title.replace('/','-')
        title=title.replace(':','-')
        title=title.replace(')','-')
        title=title.replace('(','-')
        #再倒截下怕超长和重名。这块对title的操作很智障,并且破坏了爬取到信息的可用性,有时间再改了。
        title=title[-20:]
        #获取图片链接列表
        img_list=product__thumb.find_all('img')
        #文件夹名:税后价格$商品title$税前价格
        path_new=path+price1_inc+'$'+title+'$'+price1_exc
        if not os.path.exists(path_new):
            os.mkdir(path_new)
            #打印新建文件夹
            print("新建文件夹:",path_new)
            for i in range(len(img_list)):
                img_url=img_list[i].get('src')
                #图片不是链接时跳过
                print(img_url)
                if img_url[:4]!='http':
                    print("无法识别的URI")
                    continue
                #被名字整伤了,同个文件夹里直接用序号了
                target=path_new+'\\'+str(i)+'.jgp'
                #下载图片到目标路径
                #urllib.request.urlretrieve有时会被SSL卡住,时灵时不灵的,决定用最简朴的,但它是真的慢
                with open(target,'wb') as f:
                    img = requests.get(img_url,headers = stage2_headers).content
                    f.write(img)
            print('处理完成url:',url)
            totalcnt+=1
        #优化可以加日志
        #单IP每处理10个商品页面停顿10秒避免请求被拒
        #随缘的,这边之前串行时设置的是停8秒没有被卡掉,多线程下似乎8秒是不够的,建议还是挂代理
        if totalcnt%10==0:
            time.sleep(10)
            
if __name__ == "__main__":
    #创建线程通信的全局Queue
    product_url_q = Queue()
    #实际中建议挂代理,请求时添加参数proxies=proxies
#     proxy = 'ip:port'
#     proxies = {
#          'http': 'http://' + proxy,
#          'https': 'https://' + proxy
#      }
    #part-1-postProductUrl
    #post的目标URL
    stage1_url="https://www.brickstickershop.com/website/Includes/AjaxFunctions/WebsiteAjaxHandler.php?Show=Search"
    stage1_headers= {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
    "content-type": "application/x-www-form-urlencoded",
    "referer": "https://www.brickstickershop.com/website/index.php?Show=Search&KeyWord=",
    "origin": "https://www.brickstickershop.com",
    "cookie": "" #手工进遍网站把cookie贴过来
    }
    stage1_xajaxr="" #手工选一个目录页,看formdata复制个xajaxr过来
    #part-2-getProductInfo
    #第二个阶段的请求头不必有"content-type": "application/x-www-form-urlencoded"
    stage2_headers= {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
    "cookie": "" #手工进遍网站把cookie贴过来
    }
    #假设存到D盘ret文件夹里
    target_path='D:\\ret\\'
    #起线程
    t1 = Thread(target=postProductUrl, args=(stage1_url,stage1_headers,stage1_xajaxr))
    t2 = Thread(target=getProductInfo, args=(stage2_headers,target_path))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('任务结束')

        实际上因为该网站的商品规模比较少(截至一月中旬时,大概80来页,每页24个商品),所以在没有使用代理的情况下,当时是直接串行执行而没有使用到多线程的。当时测试,串行情况下,步骤2大概每10个商品“sleep”8秒是不会被拒的。使用多线程后步骤1、2近乎同时在请求,可能更容易被拒,因此实际使用还是建议挂代理。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值