twitter爬取摘要

相比国内的微博,twitter的反爬似乎没那么厉害。好奇之下,开始动手了,先用requests来测试下。拿了老福的页面来爬下粉丝关注信息,页面在https://twitter.com/TheWoodWalker/following 需要梯子 这里假设大家都能科学上网,可以查看下自己ss的端口号

import requests from requests.exceptions import RequestException win_proxies = { http:127.0.0.1:1080, https: 127.0.0.1:1080 } def get_response(url,params={},headers={},proxies={}): try: response = requests.get(url,params=params,headers=headers,proxies=proxies) if response.status_code == 200: return response.text return None except RequestException as e: print(err: %s % e) html = get_response(,headers=headers,proxies=win_proxies) print(html)

我用的是搬瓦工的服务器做的梯子,给设置下代理就行了,本地代理一般是1080端口,mac的设置差不多,只不过是socks5协议

mac_proxies = { http:socks5://127.0.0.1:1086, https: socks5://127.0.0.1:1086 }

运行下,啥信息都没有,以为是js渲染的页面,刚想用splash试试,瞅见了,

原来是没登录被重定向了,为了简单,我就不做模拟登陆了,直接把浏览器的请求cookies带过去看看

注意 这里复制的是浏览器Request Headers的信息,一般cookie会把用户登陆的信息带过去,还有在python里面,设置header不需要":"开头的字符,看红色的圈圈,

import requests from requests.exceptions import RequestException headers = { cookie: , referer: , } win_proxies = { http:127.0.0.1:1080, https: 127.0.0.1:1080 } def get_response(url,params={},headers={},proxies={}): try: response = requests.get(url,params=params,headers=headers,proxies=proxies) if response.status_code == 200: return response.text return None except RequestException as e: print(err: %s % e) html = get_response(,headers=headers,proxies=win_proxies) print(html)

然后去掉不重要的参数,为了隐私,我把cookies的参数放空了,根据自己账号的参数复制进去。运行一下,成功获取到页面数据了~ 接下来我们用scrapy来爬取

scrapy startproject twitter cd twitter scrapy genspider user twitter.com/TheWoodWalker/following

跟requests一样,scrapy也需要梯子,这个时候先看下项目的大概结构

没错,user文件就是我们爬取的核心逻辑,现在先来设置代理,scrapy的代理设置是通过downlload middle来实现的,对应的文件在middlewares.py,打开

from scrapy import signals import logging class ProxyMiddleware(object): logger = logging.getLogger(__name__) # 改写代理请求 def process_request(self, request, spider): request.meta[proxy] = :1080 # 失败重试 def process_exception(self, request, exception, spider): self.logger.debug(Try second) request.meta[proxy] = :1080 return request

已经存在middleware不用管它,我们编写这样一个middleware,功能就是可以将请求改写,将请求发起后发送给spider。注意,设置代理这块,mac的梯子http端口默认是1087,可以在客户端查看,并且笔者亲自测试,发现scrapy并不支持socks协议的代理,所以mac只能设置http代理了。接下来在setting.py开启这个middleware,找到DOWNLOADER_MIDDLEWARES 取消注释

DOWNLOADER_MIDDLEWARES = { twitter.middlewares.ProxyMiddleware: 543, }

然后我们来到spider的user文件,

import scrapy class UserSpider(scrapy.Spider): name = user allowed_domains = [] start_urls = [] def parse(self, response): print(response)

将请求改为https,然后打印下response,运行

scrapy crawl user

这里的user就是我们一开始新建的spider名称,

基本上看到200就意味着请求成功了,大家也可以试试DOWNLOADER_MIDDLEWARES 注释掉,看下请求能不能成功(*_*) 。尽管成功返回,还是被重定向到了登录页面,

一开始用requests测试爬取的时候就发现了twitter是怎么反爬的 通过判断header的referer性是否正确,然后使用cookie来读取用户信息,注意,跟requests不同的是,scrapy的cookie是不在header属性里面设置的,我们要为cookie特地设置,而且cookie也不能像requests一样一堆字符串放上去,需要转换成字典对象,接下来我们就来做着2件事,首先是设置header的重要字段(需要亲自测试,一般是host referer user-agent等),用custom_settings来设置局部的header参数,这个字段可以覆盖setting的设置

from scrapy import Request,Spider,cmdline class UserSpider(Spider): name = user allowed_domains = [twitter.com] start_urls = [] cookies = personalization_id="v1_+IFi/GRP+lNdtg8CelnzRA=="; guest_id=v1%3A; external_referer=padhuUp37zjgzgv1mFJ12Ozwit7o|0|8e8t2xd8A2w%3D; _ga=GA1.2..; ads_prefs="HBERAAA="; kdt=ea4XihSoDAsPnrK9H8GRdCGk6NcpeYS3c2Nbostm; remember_checked_on=1; twid="u="; auth_token=ef669d870ea20db0865fa9815ee7e1afe3; csrf_same_site_set=1; csrf_same_site=1; lang=zh-cn; _twitter_sess=BAh7CSIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNo%250ASGFzaHsABjoKQHVzZWR7ADoPY3JlYXRlZF9hdGwrCD2jD%252FVoAToHaWQiJWI2%250AMzY2NzM2M2ZiNTNhZTM3ODlmNWIyMDhjOTAxMDliOgxjc3JmX2lkIiVhMDYx%250ANjc4Yjk4ZmYyZGEzNzYwZTM3OTgxYzJjYjAwOQ%253D%253D--e7eabc824f0e0992c68218d56f70ab; ct0=b0b2ff7b8a065d3cbb21ba3fc680e9d2; _gid=GA1.2.. custom_settings = { "DEFAULT_REQUEST_HEADERS": { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36, referer: , } }

然后将浏览器复制过来的cookie转换字典

cookies = ct0=b0b2ff7b8a065d3cbb21ba3fc680e9d2; _gid=GA1.2.. def start_requests(self): cookie ={} for line in self.cookies.split(;): key,value = line.split(=,1) cookie[key] = value yield Request(, cookies=cookie)

cookies因为隐私问题我就简化了 ,原本是一大串的。

这个时候返回的就是我们想要的页面了。简单解析一下,

def parse(self, response): lis = response.css(.GridTimeline) for item in lis: name = item.css(.fullname::text).extract() acccount_name = item.css(.ProfileCard-screenname .u-linkComplex-target::text).extract() desc = item.css(.ProfileCard-bio).xpath(string(.)).extract()

这里抓取的是下面这几个字段,

其中下面这个圈我用desc字段表示,是一个多层嵌套的标签结构,用text只能提取最外面的文本,我们需要把里面子标签的文本也提取出来,用xpath(string(.))提取。

到目前为止 第一页的爬取到此为止,接下来的item跟入库就省略了,可以参考我最后给出的代码。为什么说第一页呢,难道后面的是不一样的?没错,twitter后面的请求观察浏览器的行为就可以发现,在往下拉的过程,浏览器会发送一个请求,把用户列表动态的插进原有的html中,我们爬取工作就是 怎么模拟接口的请求。先来观察前几页的请求跟返回

# 第1页请求 /users? include_available_features=1& include_entities=1& max_position=& reset_error_state=false # 第1页返回 has_more_items: true items_html: "↵↵↵<div class="Grid Grid--withGutter" da" min_position: "" new_latent_count: 18 # 第2页请求 /users? include_available_features=1& include_entities=1& max_position=& reset_error_state=false # 第2页返回 has_more_items: true items_html: "↵↵↵<div class="Grid Grid--withGutter" da" min_position: "" new_latent_count: 18 # 第3页请求 /users? include_available_features=1& include_entities=1& max_position=& reset_error_state=false

不知道你是否看出来了,请求最重要的参数只有一个max_position,而 后面页的请求max_position参数其实是前一个请求返回的min_position,也就是说确定好第一个max_position,后面的min_position也就确定下来了,爬取策略也只能一个接着一个爬,因为后面请求的参数是根据前一个请求返回的字段发出的。has_more_items这个字段可以让我们判断是否还有下一个接口。这里还有个小细节,对于页面的解析,用scrapy返回的response对象即可直接解析,但是后续的请求是json字段里面的html,

这样的情况如何解析呢?

from scrapy import Request,Spider,Selector def parse_more(self, response): res =json.loads(response.text) has_more_items = res[has_more_items] min_position = res[min_position] selector = Selector(text=res[items_html], type="html") lis = selector.css(.Grid-cell) for item in lis: userItem = TwitterUserItem() userItem[name] = item.css(.fullname::text).extract_first() userItem[acccount_name] = item.css(.ProfileCard-screenname .u-linkComplex-target::text).extract_first() userItem[desc] = item.css(.ProfileCard-bio).xpath(string(.)).extract_first() yield userItem if has_more_items: yield self.page_page(min_position)

由于是api返回的是json对象,我们先解析出来,然后用scrapy提供的Selector方法来替代原来response对象。

到此为止,爬取思路就很清晰了, 通过确定第一个max_position参数来调用第一个请求,后面的请求的构造都是根据前一个请求返回的min_position来作为当前请求的max_position。

入库我用mongodb,在pipline里面编写存库思路

import pymongo class MongoPipeline(object): def __init__(self, mongo_url, mongo_db): print(__init__) self.mongo_url = mongo_url self.mongo_db = mongo_db @classmethod def from_crawler(cls, crawler): print(from_crawler) return cls( mongo_url = crawler.settings.get(MONGO_URL), mongo_db = crawler.settings.get(MONGO_DB) ) def open_spider(self, spider): print(open_spider) self.client = pymongo.MongoClient(self.mongo_url) self.db = self.client[self.mongo_db] def process_item(self, item, spider): name = item.__class__.__name__ if self.db[name].update({url_token: dict(item)[url_token]},{$set: dict(item)}, True): print(保存成功) return item def close_spider(self, spider): print(close_spider) self.client.close()

源代码请参考

wuzhenbin/twitter-spider