# 京东显卡商品信息分布式爬虫 ### 介绍 本爬虫分两个部分: - 第一部分搜索页爬虫是使用selenium爬取搜索页, 主要获取的是商品的sku和价格 - 第二部分商品页爬虫使用scrapy爬取详情页的商品介绍信息, 主要获取商品的型号 由于京东商品种类繁多, 不同的商品品类间差异巨大, 商品页爬虫的代码也就不近相同. 出于成本和时间考虑, 搜索页爬虫使用**3080**作为搜索关键词, 商品页爬虫爬取搜索页爬虫获取的商品对应的详情页 其整体架构如下: ![](D:\python_class\jd-distributed-crawler\img\2022-04-15-16-28-10-image.png) ### 电商网站筛选 爬虫的目的就是为了获取一定量的有价值的信息, 就目前中文互联网来说, 电商网站是信息密度和价值密度比较高的地方, 因此使用爬虫获取信息有一定的可用性. 目前而言, 淘宝获取的商品信息结果层次最多, 品类最全. 但是首先淘宝商品搜索页混杂天猫,天猫国际, 淘宝卖家等各类各不相同的店铺, 因此其商品详情页结构也大相径庭; 其次是淘宝历史包袱比较重, 因此有许多结构混乱的页面混杂其中, 难以提取数据. 因此, 实际考虑后确认, 淘宝不是太适合新人小团队去学习试炼爬虫爬取信息. 相对而言, 京东搜索页基本同一为京东自己的页面, 京东自营, 旗舰店和商家店结构基本没有区别, 详情页结构也一致, 对爬虫比较友好(京东唯一不太友好的地方只有, 其主站页面下并没有写robots.txt). 因此考虑爬取京东. ### 搜索页爬虫 主要由`settings.yaml`和`jdSearchSeleniumSpider.py`两部分组成,`settings.yaml`是配置文件, `jdSearchSeleniumSpider.py`是代码执行主体 #### 前期工作 基本而言, 任何有搜索功能的网站都会对其搜索功能做一定的限制, 以防止单个ip过高频率的访问, 占用过多资源. 京东也不例外, 对于未登录的请求, 甚至是带着header和cookie的请求, 往往都是秉持拒绝的态度, 要求验证登录: ![](D:\python_class\jd-distributed-crawler\img\2022-04-15-13-25-48-image.png) ![](D:\python_class\jd-distributed-crawler\img\2022-04-15-13-26-42-image.png) 搜索引擎的爬虫验证一般级别都比较高, 难以绕过. 考虑到搜索页的数量相对于商品页的数量往往是少一个数量级, 且京东单个搜索词的页面量也大致在几十几百页的数量级之间, 因此使用python的`selenium`库, 驱动浏览器去自动化访问搜索页, 进而获取数据, 是比较简单且可行的方案. #### settings.yaml `setting.yaml`是搜索页爬虫的配置文件, 其内容如下: ```yaml search: keyword: '3080' sleep: 5 redis: host: '120.24.87.40' port: '6379' password: 'Guet@207' db: 1 mongodb: host: '120.24.87.40' port: '27017' username: "admin" password: "123456" result: rpush_key: "jd:start_urls" ``` - `search` - `keyword`: 要搜索的关键字, 即在京东商品搜索上的要搜索的商品; - `sleep`: 休眠时间, 单位: 秒; 由于**selenium**操作页面的动作(例如点击翻页, 下拉)是**异步**的, 而读取页面是**同步**的. 京东需要执行下拉这个操作后才加载出所有的商品信息, 为了读取全当前的页面信息, 需要休眠一段时间等待异步操作完成; - `redis` - `host`: redis部署的服务器地址; - `port`: redis部署时开放的端口号; - `password`: redis部署时设置的auth; - `db`: 需要使用redis的第几个数据库 - `mongodb` - `host`: MongoDB部署的服务器地址; - `port`: MongoDB部署时开放的端口号; - `username`: MongoDB部署时设置的auth的用户名, 或者说要使用的MongoDB的数据库名; - `password`: MongoDB部署时设置的auth. - `result` - `rpush_key`: 爬虫获取到的数据形成的url要放入redis时设置的key, url在Redis中存储方式是列表 #### jdSearchSeleniumSpider.py 搜索页爬虫的程序入口, 根据京东搜索页面爬取页面元素. 启动程序后进入京东的商品搜索页面. 搜索页的搜索关键字为`settings.py`中设定的`search.keyword`. 京东的搜索页面会分两个阶段加载, 第一个加载阶段加载成功后(即加载出30个商品的信息), 爬虫会执行`scroll_by_xpath()`, 作用是滚动网页到网页底部, 触发京东搜索页面的第二个加载阶段, 把一个页面上的信息都加载到浏览器里. `scroll_by_xpath()` 内部原理就是驱动浏览器移动视图, 使视图可以看到指定的元素. 在该爬虫中, 指定的网页元素的**xpath**为`//*[@id="J_bottomPage"]/span[1]/a[9]`, 代表的是京东搜索页面的下一页按钮这一网页元素. 两个阶段都加载完成后, 获取网页中所有商品的sku和价格. ![](D:\python_class\jd-distributed-crawler\img\2022-04-15-18-40-23-image.png) sku是京东给商品的编号, 且可以根据sku拼接对应商品的详情页的URL. 获取到sku和价格后, 将sku拼接为URL, rpush到Redis设置好的数据库的`result.rpush_key`中, 并且将每个商品的数据以`{'sku': sku, 'price': price}`的形式, 放入MongoDB设置好的数据库中. 放入MongoDB前会根据sku字段查询数据是否存在(原子操作, 可分布式), 防止重复插入. ### 商品页爬虫 商品页爬虫使用了`scrapy_redis`去写, 即`Scrapy`的redis版本, 他与`Scrapy`的主要区别就是把存放URL于Reids数据库中, 利用Redis的特性, 即在多个请求请求同一个数据时, 会默认对请求排序, 有一个队列效果, 保证了分布式爬虫获取到不同的URL. `scrapy_redis`框架下的爬虫执行是异步的, 即, 从Redis数据库的列表中获取URL后, 发起Request, 便可以执行下一次从Redis中获取Redis, 而发起Request得到的response则等得到后再由回调函数处理(即执行`jdsku.py`里的`parse`方法). 该爬虫只需要在**本爬虫目录下**, 调用`scrapy crawl jdsku`即可启动. #### settings.py 配置文件, 具体配置查看`Scrapy`的配置, 其中有几个是`scrapy_redis`的配置或本次项目引入的配置, 在此说明: 个人配置: `PROXY_SERVER_URL`: IP代理商提供的直连IP的API地址; `TEST_PAGE`: 用于测试IP代理商提供的IP代理服务是否能访问JD的测试地址, 可以按需更换不同的京东商品地址; `mongodb`: 配置时需要以字典形式配置 - `host`: MongoDB服务器的地址; - `port`: MongoDB服务器暴露的端口; - `username`: MongoDB要使用的用户名(也是数据库名) - `password`: MongoDB对应用户名的验证密码 `cookies`: 浏览器访问京东时的`cookies`, 有助于模拟浏览器访问, 降低失败率 scrapy_redis配置: `DONT_FILTER`: Redis访问不去重, True代表不去重. 不去重的原因是使用代理访问商品详情页时可能会失败, 此时会将URL重新放回Redis的列表开头; `SCHEDULER='scrapy_redis.scheduler.Scheduler'`: 使用scrapy_redis中的调度器, 即保证每台主机爬取的URL地址都不同的Scheduler. `DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'`: 配置scrapy使用的去重类, 即RFPDupeFilter. `SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"`: 使用`scrapy_redis.picklecompat`作为序列化, 不序列化的数据可能会出现乱码.    `AUTOTHROTTLE_ENABLED = True`: 开启自动限速, 控制爬取速度,降低对方服务器压力. #### items.py 代码如下: ```python import scrapy class JdskuspiderItem(scrapy.Item): # sku-id sku = scrapy.Field() # title title = scrapy.Field() # 显卡型号 model = scrapy.Field() # 显存类型 memoryType = scrapy.Field() # 显存位宽 bitWidth = scrapy.Field() # 显存容量 memoryCapacity = scrapy.Field() ``` item是爬虫数据的容器, 方便我们按对应字段存储数据 #### jdsku.py 商品页爬虫的程序入口, 根据redis数据库对应的列表访问对应URL, 在京东商品详情页面爬取页面元素. 因为使用`scrapy_redis`框架, 也就是`Scrapy`的Redis分布式版本, 因此大部分代码都是在重写方法. 对于`class JdskuSpider(RedisSpider)`: **属性**: `name`: 爬虫的名字, 后续在终端启动爬虫时需要输入的爬虫名; `allowed_domains`: 运行爬取的URL范围; `redis_key`: Redis数据库中存放要爬取的URL的列表的Key名, 会获取列表中的第一个数据; `redis_connection`: 存放一个新的Redis连接, 该连接后续用于访问失败时重新把URL返回给Redis列表; `mongo_connection`: 存放一个新的MongoDB连接, 该连接后续用于存放最终数据. **方法**: 两个方法都是重写方法, 方法名和参数都固定, 两个方法都是自动调用. `make_requests_from_url(self, url)`: - `scrapy_redis`框架会调用该方法, 发起一个访问参数里URL的request, 返回一个response, 框架会将response发给他的回调函数处理(即下述的`parse`); - `meta={'url':url}`表示会将`{'url': url}`作为数据传递下去, 这样在他的回调函数(`parse`)里就能使用到该数据. `parse(self, response)`: - `scrapy_redis`发送的请求得到response后的回调函数; - 作用是解析商品详情页(针对特定商品), 提取数据, 并将其保存到MongoDB中, 若提取数据表现出来的是被京东拦截, 那么把该次访问的URL重新返回到Redis列表开头.