Python爬虫实战:如何从京东获取150+万条数据

博客地址:从京东"窃取"150+万条数据

github 地址: jd_spider

使用 scrapy, scrapy-redis, graphite 实现的京东分布式爬虫,以 mongodb 实现底层存储。分布式 实现,解决带宽和性能的瓶颈,提高爬取的效率。实现 scrapy-redis 对进行 url 的去重 以及调度,利用 redis 的高效和易于扩展能够轻松实现高效率下载:当 redis 存储或者访问速 度遇到瓶颈时,可以通过增大 redis 集群数和爬虫集群数量改善

爬取策略

获取 <a href> 标签里面的 url 值,然后迭代爬取,并且把 url 限定在 xxx.jd.com 范围内,防止无限广度的问题。

反爬虫策略

禁用 cookie

通过禁用 cookie, 服务器就无法根据 cookie 判断出爬虫是否访问过网站

伪装成搜索引擎

要说最著名的爬虫是谁?肯定是搜索引擎,它本质上也是爬虫,而且是非常强大的爬虫。 而且这些爬虫可以光明正大地去爬取各式网站,相信各式网站也很乐意被它爬。 现在可以通过修改 user-agent 伪装成搜索引擎

 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
 'Mozilla/5.0 (compatible; Bingbot/2.0; +http://www.bing.com/bingbot.htm)',
 'Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)',
 'DuckDuckBot/1.0; (+http://duckduckgo.com/duckduckbot.html)',
 'Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)',
 'Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)',
 'ia_archiver (+http://www.alexa.com/site/help/webmasters; [email protected])',

轮转 user-agent

为了提高突破反爬虫策略的成功率,定义多个 user-agent, 然后每次请求都随机选择 user-agent。本爬虫实现了一个 RotateUserAgentMiddleware 类来实现 user-agent 的轮转

代理 IP

使用代理 IP, 防止 IP 被封

爬虫状态监控

将分布式爬虫 stats 信息(请求个数,item 下载个数,dropItem 个数,日志)保存到 redis 中 实现了一个针对分布式的 stats collector,并将其结果用 graphite 以图表形式动态实时显示

并发请求和深度控制

通过 setting.py 中的 CONCURRENT_REQUESTS = 32 配置来控制并发请求数量,通过 DepthMiddle 类的 DEPTH_LIMIT=max 参数来控制爬虫的的递归深度

运行截图

http://imgur.com/a/Q9Usi

http://imgur.com/a/FBvay

http://imgur.com/a/zgFWa


Python爬虫实战:如何从京东获取150+万条数据

88 回复

屌屌屌。


要爬京东这种大型电商网站,数据量还这么大,你得用点技巧。直接硬刚很容易被封IP,而且京东的反爬现在挺严的。我给你个思路和核心代码,用异步aiohttp来搞,速度快,再配上代理IP池和随机User-Agent。

核心就是分几步走:

  1. 先搞定商品列表页,拿到所有商品的SKU ID。
  2. 用这些ID去拼商品详情页和评论页的链接。
  3. 异步并发去请求这些页面,解析数据。
  4. 数据存下来,建议用MongoDB或者直接存JSON文件,150万条数据量不小。

这里有个关键点,京东的评论数据有个专门的接口,比从网页上扒容易。列表页的翻页可以用他们的“懒加载”接口。

给你看个爬评论的异步示例:

import aiohttp
import asyncio
import json
from datetime import datetime

async def fetch_jd_comments(sku_id, session, max_pages=100):
    """抓取单个商品的评论"""
    all_comments = []
    base_url = "https://club.jd.com/comment/productPageComments.action"
    
    for page in range(0, max_pages):
        params = {
            'productId': sku_id,
            'score': 0,  # 0表示全部评价
            'sortType': 5,  # 按时间排序
            'page': page,
            'pageSize': 10,  # 每页10条
            'isShadowSku': 0
        }
        
        try:
            async with session.get(base_url, params=params, timeout=10) as response:
                if response.status == 200:
                    data = await response.json()
                    comments = data.get('comments', [])
                    
                    if not comments:  # 没有更多评论了
                        break
                    
                    for comment in comments:
                        cleaned_comment = {
                            'sku_id': sku_id,
                            'content': comment.get('content', '').strip(),
                            'creation_time': comment.get('creationTime', ''),
                            'score': comment.get('score', 0),
                            'user_level': comment.get('userLevelName', ''),
                            'useful_vote_count': comment.get('usefulVoteCount', 0)
                        }
                        all_comments.append(cleaned_comment)
                    
                    print(f"SKU {sku_id}: 第{page+1}页完成,获取{len(comments)}条评论")
                    await asyncio.sleep(0.5)  # 礼貌性延迟
                    
        except Exception as e:
            print(f"SKU {sku_id} 第{page}页请求失败: {e}")
            continue
    
    return all_comments

async def main(sku_list):
    """主函数,并发抓取多个商品"""
    connector = aiohttp.TCPConnector(limit=10)  # 控制并发数
    timeout = aiohttp.ClientTimeout(total=30)
    
    async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
        tasks = [fetch_jd_comments(sku, session) for sku in sku_list]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        # 合并结果
        all_comments = []
        for result in results:
            if isinstance(result, list):
                all_comments.extend(result)
        
        # 保存数据
        with open(f'jd_comments_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json', 'w', encoding='utf-8') as f:
            json.dump(all_comments, f, ensure_ascii=False, indent=2)
        
        print(f"总共获取 {len(all_comments)} 条评论数据")
        return all_comments

if __name__ == "__main__":
    # 示例SKU列表,实际需要从列表页获取
    sample_skus = ['100012043978', '100006779472', '100010386576']
    
    # 运行异步主函数
    asyncio.run(main(sample_skus))

要爬150万条,你得先搞到足够多的商品SKU。可以从分类页入手,或者用他们的搜索接口。记得控制好请求频率,加上代理IP,不然你的IP几分钟就没了。数据存储建议分批存,比如每1万条存一个文件,这样不容易丢数据。

总结:核心是异步请求加代理池,从评论接口入手效率更高。

很猛!

代理 ip 用的是哪一家?

欢迎大家不吝指教.

同问。代理 IP 来源

代理 IP 用的不是一家,是用这两位的项目  https://github.com/qiyeboy/IPProxyPool   https://github.com/qiyeboy/IPProxyPool .只是个人感觉还是不够方便,还需要自己安装和配置环境,所以我就把这两个项目做成 docker image. 然后映射  IP 出来,通过  api 获取代理 IP. 只是 image 还有点问题,所以还没有分享出来.

爬虫监控状态这个是怎么做的。

感觉很屌。

主要是将 scrapy.spider 的 stats 保存到 Redis (单机或者是集群),然后通过将这些数据发送给 graphite 来生成监控动态图.主要参考  https://github.com/gnemoug/distribute_crawler  的做法.只是这个项目已经没有维护了,并且是 python2,所以要自己看源码做一些适配.

反反爬虫策略…?

此外,graphite 的配置安装是很麻烦的事情,并且是不支持 windows  (虽说我是 Linuxer), 所以用 docker 就非常方便.我自己就在别人做好的  graphite 基础上做了些许配置的修改然后生成新的 image 适配 scrapy+graphite

突破反爬虫策略嘛,所以就叫反反爬虫策略.见笑了.

#9 用了多少台机器在爬

代理部分,下次可以试试我写的代理程序 [https://www.v2ex.com/t/369905]( https://www.v2ex.com/t/369905) (。・`ω´・)

一台 32Gb 内存的工作站,跑了三个实例.

其实主要的问题是免费的代理大部分都不能用,而且即使现在能用,下一分钟也可能不能用.并且以文本形式来存储代理 IP,在运行的时候将代理 IP 全部加载进内存,但是如果你的爬虫是长时间运行,代理应该很快用完.所以比较理想的是"动态"加载代理 IP. 即可以在运行时继续加载 IP. 个人小小的看法.也很感谢你的项目啦 :)

readme 里说可以全加载到内存,只不过想体现这个程序的方便而已,我个人喜欢把所有的代理都放在 redis 里,每间隔 15min 就把 proxylist 拉下来换一批,2000+个代理,15min 内一般项目一般都够用了,用 proxylist 的优点是自己不用再维护一个 proxy pool, 缺点是没法给每个 proxy 打分统计、proxylist 上的代理,可能在某个地区没法使用。

其实最大的问题是,知道这一刻的代理可用,但是却完全没办法保证下一刻依旧可用.:(

不错,准备学习一个!

这不算什么问题吧,出现 exception 就换下一个代理,商业代理偶尔也有抽风的时候,也会出现下一刻不可用,就看换代理的频率是不是可以接收的范围内。

休斯顿的?

关键还是在于代理。



不我的意思是似乎少打字了…

README 上面已经修改,但是 V2EX 就没办法修改 :(

我是用这个中间件来管理代理 IP 的  https://github.com/aivarsk/scrapy-proxies,随机选择代理 IP,不可用就把代理 IP 丢了. 只是还是会出现运行到一段时间之后,代理 IP 全部被丢完的情况

不太懂你表达的意思?

厉害了,屌屌

天猫难抓,禁止蜘蛛,无 cookie 直接重定向到登录页添加 cookie.

不错不错,马克下

二话不说,丢个星

https://github.com/aivarsk/scrapy-proxies 这代码也是从一个文本读内容啊,完全可以自己实现一个 download middleware 从 redis 里取。

666666,感谢楼主科普

分布式爬虫, 看起来很好玩, 赶紧给个星

https://36kr.com/p/5078918.html 你的爬虫会把自己送进监狱吗?

按 2017 年 6 月 1 日,《网络安全法》以及最新刑事司法解释:

未经授权爬取用户手机通讯录超过 50 条记录,老板进去最高可达 3 年

未经授权抓取用户淘宝交易记录超过 500 条的,老板进去最高可达 3 年

未经授权读取用户运营商网站通话记录超过 500 条以上的,老板进去最高可达 7 年

未经授权读取用户公积金社保记录的超过 50000 条的,老板进去最高可达 7 年

爬取难度 淘宝>天猫>京东.淘宝的反爬实在太强.即使用上我提到的所有反反爬虫策略.半个小时内就会爬不动,即使爬取频率不高.

只是爬取商品信息和商品评论,并未涉及到交易记录.

我后来才知道,没有调度功能的爬虫在某种意义上来说不算是分布式爬虫…
不过 redis 真的好用~~

我知道,所以我之前提到的文本的代理 IP 会用尽就是我在爬虫过程中实际遇到的问题.可能我表达不够清晰,让你误解了.抱歉.其实我觉得爬虫的反反爬虫策略是一个问题.还有需要重点关注的是去重.之前我写的单机爬虫我都是用布隆过滤器来实现去重.只是这个项目是分布式的,用到 redis.就直接在 redis 去重.但是感觉效率没有布隆过滤器高.尤其是我这种直接从<a>标签中爬取 URL 的策略.去重就尤其重要.

就 scrapy-redis 而言,因为用 redis 取代了 scrapy 自带的 collection.deque,就可以把需要爬取的队列从保存到内存中变成了保存到内存数据库中,但是这个时候,原来配置 collection.deque 使用的调度器就没办法使用了,也没办法进行分布式调度,于是 scrapy-redis 重写了 scrapy 的调度器.

哈,别老是欺负京东了。。。搞搞淘宝天猫的试试,感觉比京东的麻烦了很多

淘宝天猫阿里妈妈的那叫一个难爬。成功了几天又被封了

淘宝天猫真的欺负不动.是被欺负 :(

大概有 10 天了?
原来是你干的…

很强!

楼主,我们交个朋友吧

我干了什么好事?

好啊.可以相互交流,相互学习嘛

其实 QQ 空间也很难爬,想起我之前写的爬取 QQ 空间照片的爬虫. https://github.com/samrayleung/qzonePictureSpider .用了一个星期来解决登陆问题,还有需要根据 cookie 计算密钥.

#45
#48 这位是京东运营团队

看一下评论,涨知识了

为京东引入流量 :)(不要打我)


天猫的容易抓, 加个 cookie 头就行。


速卖通的才难

套路啦

加 cookie 可以抓.但速度太难上去了,除非手上高质量代理 IP 多.

借楼问一下这个会触发最新的 gov 的爬虫策略吗?

并没有了解过…

直接用浏览器模拟呀

所有的爬虫,最核心最关键就是代理 ip 的问题,其他都不是事儿。

我就卡在 IP 这块儿

github 上 README 的博客地址指向不对。。。

已经修改.谢谢你的建议.

相信你指的应该是  PhantomJS  和  selenium  配合使用来模拟浏览器,但是这样对于爬虫来说实在太耗费资源了,相当于打开一个没有 GUI 的浏览器,然后解析需要爬取的页面.这个只能作为不是办法的办法了.

其实,并不是那么绝对的,例如爬淘宝,即使你经常换代理 IP,也没办法突破它的反爬策略,因为爬虫无论怎么慢,对比人来说,操作都太快了.再比如爬取 QQ 空间,并不是简单的模拟请求登陆就可以了,再你登陆之后,服务器会在 cookie 中返回三个值 p_skey,skey,rv2,然后作移位和与或操作得到一个 gtk 值,然后每次请求都要附上这个 gtk 值,不然都是 403. 更不要说 Google 的人机检验机制,不是人类根本用不了.代理 IP 是一个关键点,但是并不是全部.如果你爬取的网页超过 1000 + w,光是去重就是一个值得深究的问题了.个人小小愚见 :)

附加一个个人推测,但未证实.在双11或者是 618 搞活动的时候,电商网站的反爬虫策略一般都会关闭,或者不会那么严格,因为面对洪峰一样的流量,需要把尽量多的资源用于处理客户端请求,而越复杂的反爬机制,需要的验证就越多,耗费的资源也会相应增加.不过这个只是个人主观感觉+合理推测,未证实.

1000+w 的数据去重也可以用 bloom filter 啊,就用 Redis 的 bitmap 存 bit 数组就可以了。

第六十四条 网络运营者、网络产品或者服务的提供者违反本法第二十二条第三款、第四十一条至第四十三条规定,侵害个人信息依法得到保护的权利的,由有关主管部门责令改正,可以根据情节单处或者并处警告、没收违法所得、处违法所得一倍以上十倍以下罚款,没有违法所得的,处一百万元以下罚款,对直接负责的主管人员和其他直接责任人员处一万元以上十万元以下罚款;情节严重的,并可以责令暂停相关业务、停业整顿、关闭网站、吊销相关业务许可证或者吊销营业执照。
违反本法第四十四条规定,窃取或者以其他非法方式获取、非法出售或者非法向他人提供个人信息,尚不构成犯罪的,由公安机关没收违法所得,并处违法所得一倍以上十倍以下罚款,没有违法所得的,处一百万元以下罚款。

中华人民共和国网络安全法
http://www.npc.gov.cn/npc/xinwen/2016-11/07/content_2001605.htm

我怎么记得京东基本不限制爬虫呢?

在这个项目中,我是直接用 scrapy-redis 来,而 scrapy-redis 是把 request 的指纹保存起来进去去重的.只是感觉这样的做法不是很符合我自己的做法,因为就我看来,去重应该是直接对 url 去重.感觉这是不同的取舍,所以感慨一下.

我倾向于使用的就是 bloomfilter,只是当变成分布式爬虫的时候,可能会因为分布式的设计不一样而导致不一样的问题出现.单机使用 bloomfilter 和分布式不一样,可以新建一个 master 节点,然后所有的需要爬取和已爬取的 url 都保存在 master,然后 slave 节点只需请求待爬 url 就好.但是这种方式不同于  scrapy-redis,就需要自己使用 bloomfilter 以及编写调度器

可能我说的不清楚,我现在说的就是分布式爬虫的情况,可以稍微修改一下 scrapy-redis 里的 dupefilter 文件,不使用之前 redis set 去重的方式,在这里改成使用 bloom filter,将 bloom filter 所使用的 bit 数组用 Redis 的 bitmap 进行存取。其他地方不需要修改。

另外使用整个 request 进行 hash 还是 只使用 url 进行 hash,也可以在 dupefilter 里修改,都不是特别难。

抱歉,一直理解错了你的观点 :(

非常酷。这几天用 java 写 php 期末商城作业。只会一点 node 和 Jsoup 的我望而却步,最后还是死皮赖脸的跟图书馆老师要了全学校的图书馆数据。

对于学校图书馆的网站,真的只会把网站爬崩,不会有反爬策略这种东西存在的.

平日泡图书馆比较多,跟老师比较熟。其实那个商城的数据本来想爬京东图书数据的,无奈不会 py,只会一点 node,而且图书分类获取非常麻烦,京东图书的分类太过于详细。所以跟老师要了图书馆所以的图书数据。总共 30W+

感谢分享 马上回去试试=w=

我一直认为随机 UA 就是个没卵用的东西
一般选取的 UA 都是常见的,本身访问量很大的;多你爬虫几十万还是几百万根本没区别。
如果对方能从某个 UA 的正常用户中分辨出爬虫,那么你换 UA,对方也能分辨出来。

这个并不能完全反反爬虫,只是可以提高突破的成功率.何况这个轮转操作并不是非常复杂和耗费资源,更何况我 UA 模拟的并不是正常用户,而是搜素引擎,搜索引擎本身也是爬虫.

说明人脉有时候比技术好用 :)

我能羞羞的问问, 爬来了怎么卖钱?

这… 我写这爬虫只是用来分享,并未打算用于商业用途.商业用途就有待你发掘了,但是前提是遵守相关法规 :)

借楼问下,楼主最后破解了 QQ 空间吗?就是如果不使用 phantomjs。

最后我成功登陆并获取到 cookie,但是我是使用了 phantomjs 的,如果不使用的话,我折腾了一星期也不成功.

回到顶部