奥门新浦京官方网站PHP爬虫:百万级别知乎用户数据爬取与分析

本次抓取了110万的客户数据,数据分析结果如下:

python完成八线程抓取腾讯网客户,python三二十四线程

内需采用的包:

beautifulsoup4
html5lib
image
requests
redis
PyMySQL

pip安装具备注重包:

pip install 
Image 
requests 
beautifulsoup4 
html5lib 
redis 
PyMySQL

运转条件亟待扶植粤语

测量检验运行情况python3.5,不保证其余运转条件能完备运转

亟需设置mysql和redis

配置 config.ini 文件,设置好mysql和redis,并且填写您的和讯帐号

向数据库导入 init.sql

Run

始发抓取数据: python get_user.py
查阅抓取数量: python check_redis.py

效果

奥门新浦京官方网站 1
奥门新浦京官方网站 2

全部思路

1.率先是模仿登入新浪,利用保存登入的cookie消息
2.抓取微博页面的html代码,留待下一步继续展开剖判提取新闻
3.剖判提取页面中客户的性格化url,归入redis(那Ritter别讲澳优下redis的笔触用法,将提取到的顾客的脾性化url归入redis的多少个名为already_get_user的hash
table,表示已抓取的客户,对于已抓取过的顾客剖断是不是存在于already_get_user以去除重复抓取,同不正常候将特性化url归入user_queue的行列中,供给抓取新客户时pop队列获取新的顾客)
4.获取客商的青睐列表和观众列表,继续插入到redis
5.从redis的user_queue队列中拿到新客户继续重复步骤3

依傍登录和讯

首先是登入,登入成效作为一个包封装了在login里面,方便整合调用

header部分,这里Connection最棒设为close,不然可能会遭逢max retireve
exceed的谬误
缘由在于平常的接连是keep-alive的但是却又未有小憩

# http请求的header
headers = {
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
  "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
  "Host": "www.zhihu.com",
  "Referer": "https://www.zhihu.com/",
  "Origin": "https://www.zhihu.com/",
  "Upgrade-Insecure-Requests": "1",
  "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  "Pragma": "no-cache",
  "Accept-Encoding": "gzip, deflate, br",
  'Connection': 'close'
}

# 验证是否登陆
def check_login(self):
  check_url = 'https://www.zhihu.com/settings/profile'
  try:
    login_check = self.__session.get(check_url, headers=self.headers, timeout=35)
  except Exception as err:
    print(traceback.print_exc())
    print(err)
    print("验证登陆失败,请检查网络")
    sys.exit()
  print("验证登陆的http status code为:" + str(login_check.status_code))
  if int(login_check.status_code) == 200:
    return True
  else:
    return False

进去首页查看http状态码来验证是不是登录,200为已经登录,平常304就是被重定向所以尽管没有登录

# 获取验证码
def get_captcha(self):
  t = str(time.time() * 1000)
  captcha_url = 'http://www.zhihu.com/captcha.gif?r=' + t + "&type=login"
  r = self.__session.get(captcha_url, headers=self.headers, timeout=35)
  with open('captcha.jpg', 'wb') as f:
    f.write(r.content)
    f.close()
    # 用pillow 的 Image 显示验证码
    # 如果没有安装 pillow 到源代码所在的目录去找到验证码然后手动输入
  '''try:
    im = Image.open('captcha.jpg')
    im.show()
    im.close()
  except:'''
  print(u'请到 %s 目录找到captcha.jpg 手动输入' % os.path.abspath('captcha.jpg'))
  captcha = input("请输入验证码n>")
  return captcha

得到验证码的方法。当登录次数太多有希望会要求输入验证码,这里落成那些效应

# 获取xsrf
def get_xsrf(self):
  index_url = 'http://www.zhihu.com'
  # 获取登录时需要用到的_xsrf
  try:
    index_page = self.__session.get(index_url, headers=self.headers, timeout=35)
  except:
    print('获取知乎页面失败,请检查网络连接')
    sys.exit()
  html = index_page.text
  # 这里的_xsrf 返回的是一个list
  BS = BeautifulSoup(html, 'html.parser')
  xsrf_input = BS.find(attrs={'name': '_xsrf'})
  pattern = r'value="(.*?)"'
  print(xsrf_input)
  self.__xsrf = re.findall(pattern, str(xsrf_input))
  return self.__xsrf[0]

拿到xsrf,为啥要收获xsrf呢,因为xsrf是生机勃勃种防御跨站攻击的手法,具体介绍能够看这里csrf
在取得到xsrf之后把xsrf存入cookie此中,並且在调用api的时候带上xsrf作为尾部,不然的话今日头条会回来403

# 进行模拟登陆
def do_login(self):
  try:
    # 模拟登陆
    if self.check_login():
      print('您已经登录')
      return
    else:
      if self.config.get("zhihu_account", "username") and self.config.get("zhihu_account", "password"):
        self.username = self.config.get("zhihu_account", "username")
        self.password = self.config.get("zhihu_account", "password")
      else:
        self.username = input('请输入你的用户名n> ')
        self.password = input("请输入你的密码n> ")
  except Exception as err:
    print(traceback.print_exc())
    print(err)
    sys.exit()
  if re.match(r"^1d{10}$", self.username):
    print("手机登陆n")
    post_url = 'http://www.zhihu.com/login/phone_num'
    postdata = {
      '_xsrf': self.get_xsrf(),
      'password': self.password,
      'remember_me': 'true',
      'phone_num': self.username,
    }
  else:
    print("邮箱登陆n")
    post_url = 'http://www.zhihu.com/login/email'
    postdata = {
      '_xsrf': self.get_xsrf(),
      'password': self.password,
      'remember_me': 'true',
      'email': self.username,
    }
  try:
    login_page = self.__session.post(post_url, postdata, headers=self.headers, timeout=35)
    login_text = json.loads(login_page.text.encode('latin-1').decode('unicode-escape'))
    print(postdata)
    print(login_text)
    # 需要输入验证码 r = 0为登陆成功代码
    if login_text['r'] == 1:
      sys.exit()
  except:
    postdata['captcha'] = self.get_captcha()
    login_page = self.__session.post(post_url, postdata, headers=self.headers, timeout=35)
    print(json.loads(login_page.text.encode('latin-1').decode('unicode-escape')))
  # 保存登陆cookie
  self.__session.cookies.save()

那个正是大旨的登入作用啦,特别首要的便是用到了requests库,非经常有益的保存到session
大家这边全局都是用单例情势,统一运用同三个requests.session对象开展会见到成效果,保持登陆情形的黄金年代致性

最后关键调用登入的代码为

# 创建login对象
lo = login.login.Login(self.session)
# 模拟登陆
if lo.check_login():
  print('您已经登录')
else:
  if self.config.get("zhihu_account", "username") and self.config.get("zhihu_account", "username"):
    username = self.config.get("zhihu_account", "username")
    password = self.config.get("zhihu_account", "password")
  else:
    username = input('请输入你的用户名n> ')
    password = input("请输入你的密码n> ")
  lo.do_login(username, password)

微博模拟登录到此就造成啦

博客园客户抓取

def __init__(self, threadID=1, name=''):
  # 多线程
  print("线程" + str(threadID) + "初始化")
  threading.Thread.__init__(self)
  self.threadID = threadID
  self.name = name
  try:
    print("线程" + str(threadID) + "初始化成功")
  except Exception as err:
    print(err)
    print("线程" + str(threadID) + "开启失败")
  self.threadLock = threading.Lock()
  # 获取配置
  self.config = configparser.ConfigParser()
  self.config.read("config.ini")
  # 初始化session
  requests.adapters.DEFAULT_RETRIES = 5
  self.session = requests.Session()
  self.session.cookies = cookielib.LWPCookieJar(filename='cookie')
  self.session.keep_alive = False
  try:
    self.session.cookies.load(ignore_discard=True)
  except:
    print('Cookie 未能加载')
  finally:
    pass
  # 创建login对象
  lo = Login(self.session)
  lo.do_login()
  # 初始化redis连接
  try:
    redis_host = self.config.get("redis", "host")
    redis_port = self.config.get("redis", "port")
    self.redis_con = redis.Redis(host=redis_host, port=redis_port, db=0)
    # 刷新redis库
    # self.redis_con.flushdb()
  except:
    print("请安装redis或检查redis连接配置")
    sys.exit()
  # 初始化数据库连接
  try:
    db_host = self.config.get("db", "host")
    db_port = int(self.config.get("db", "port"))
    db_user = self.config.get("db", "user")
    db_pass = self.config.get("db", "password")
    db_db = self.config.get("db", "db")
    db_charset = self.config.get("db", "charset")
    self.db = pymysql.connect(host=db_host, port=db_port, user=db_user, passwd=db_pass, db=db_db,
                 charset=db_charset)
    self.db_cursor = self.db.cursor()
  except:
    print("请检查数据库配置")
    sys.exit()
  # 初始化系统设置
  self.max_queue_len = int(self.config.get("sys", "max_queue_len"))

这个是get_user.py的构造函数,重要功用正是开始化mysql连接、redis连接、验证登录、生成全局的session对象、导入系统计划、开启多线程。

# 获取首页html
def get_index_page(self):
  index_url = 'https://www.zhihu.com/'
  try:
    index_html = self.session.get(index_url, headers=self.headers, timeout=35)
  except Exception as err:
    # 出现异常重试
    print("获取页面失败,正在重试......")
    print(err)
    traceback.print_exc()
    return None
  finally:
    pass
  return index_html.text
# 获取单个用户详情页面
def get_user_page(self, name_url):
  user_page_url = 'https://www.zhihu.com' + str(name_url) + '/about'
  try:
    index_html = self.session.get(user_page_url, headers=self.headers, timeout=35)
  except Exception as err:
    # 出现异常重试
    print("失败name_url:" + str(name_url) + "获取页面失败,放弃该用户")
    print(err)
    traceback.print_exc()
    return None
  finally:
    pass
  return index_html.text
# 获取粉丝页面
def get_follower_page(self, name_url):
  user_page_url = 'https://www.zhihu.com' + str(name_url) + '/followers'
  try:
    index_html = self.session.get(user_page_url, headers=self.headers, timeout=35)
  except Exception as err:
    # 出现异常重试
    print("失败name_url:" + str(name_url) + "获取页面失败,放弃该用户")
    print(err)
    traceback.print_exc()
    return None
  finally:
    pass
  return index_html.text
def get_following_page(self, name_url):
  user_page_url = 'https://www.zhihu.com' + str(name_url) + '/followers'
  try:
    index_html = self.session.get(user_page_url, headers=self.headers, timeout=35)
  except Exception as err:
    # 出现异常重试
    print("失败name_url:" + str(name_url) + "获取页面失败,放弃该用户")
    print(err)
    traceback.print_exc()
    return None
  finally:
    pass
  return index_html.text
# 获取首页上的用户列表,存入redis
def get_index_page_user(self):
  index_html = self.get_index_page()
  if not index_html:
    return
  BS = BeautifulSoup(index_html, "html.parser")
  self.get_xsrf(index_html)
  user_a = BS.find_all("a", class_="author-link") # 获取用户的a标签
  for a in user_a:
    if a:
      self.add_wait_user(a.get('href'))
    else:
      continue

那生机勃勃部分的代码便是用来抓取各种页面包车型客车html代码

# 加入带抓取用户队列,先用redis判断是否已被抓取过
def add_wait_user(self, name_url):
  # 判断是否已抓取
  self.threadLock.acquire()
  if not self.redis_con.hexists('already_get_user', name_url):
    self.counter += 1
    print(name_url + " 加入队列")
    self.redis_con.hset('already_get_user', name_url, 1)
    self.redis_con.lpush('user_queue', name_url)
    print("添加用户 " + name_url + "到队列")
  self.threadLock.release()
# 获取页面出错移出redis
def del_already_user(self, name_url):
  self.threadLock.acquire()
  if not self.redis_con.hexists('already_get_user', name_url):
    self.counter -= 1
    self.redis_con.hdel('already_get_user', name_url)
  self.threadLock.release()

客户步入redis的操作,在数据库插入出错时我们调用del_already_user删除插入出错的客商

# 分析粉丝页面获取用户的所有粉丝用户
# @param follower_page get_follower_page()中获取到的页面,这里获取用户hash_id请求粉丝接口获取粉丝信息
def get_all_follower(self, name_url):
  follower_page = self.get_follower_page(name_url)
  # 判断是否获取到页面
  if not follower_page:
    return
  BS = BeautifulSoup(follower_page, 'html.parser')
  # 获取关注者数量
  follower_num = int(BS.find('span', text='关注者').find_parent().find('strong').get_text())
  # 获取用户的hash_id
  hash_id = 
    json.loads(BS.select("#zh-profile-follows-list")[0].select(".zh-general-list")[0].get('data-init'))[
      'params'][
      'hash_id']
  # 获取关注者列表
  self.get_xsrf(follower_page) # 获取xsrf
  post_url = 'https://www.zhihu.com/node/ProfileFollowersListV2'
  # 开始获取所有的关注者 math.ceil(follower_num/20)*20
  for i in range(0, math.ceil(follower_num / 20) * 20, 20):
    post_data = {
      'method': 'next',
      'params': json.dumps({"offset": i, "order_by": "created", "hash_id": hash_id})
    }
    try:
      j = self.session.post(post_url, params=post_data, headers=self.headers, timeout=35).text.encode(
        'latin-1').decode(
        'unicode-escape')
      pattern = re.compile(r"class="zm-item-link-avatar"[^"]*"([^"]*)", re.DOTALL)
      j = pattern.findall(j)
      for user in j:
        user = user.replace('\', '')
        self.add_wait_user(user) # 保存到redis
    except Exception as err:
      print("获取正在关注失败")
      print(err)
      traceback.print_exc()
      pass
# 获取正在关注列表
def get_all_following(self, name_url):
  following_page = self.get_following_page(name_url)
  # 判断是否获取到页面
  if not following_page:
    return
  BS = BeautifulSoup(following_page, 'html.parser')
  # 获取关注者数量
  following_num = int(BS.find('span', text='关注了').find_parent().find('strong').get_text())
  # 获取用户的hash_id
  hash_id = 
    json.loads(BS.select("#zh-profile-follows-list")[0].select(".zh-general-list")[0].get('data-init'))[
      'params'][
      'hash_id']
  # 获取关注者列表
  self.get_xsrf(following_page) # 获取xsrf
  post_url = 'https://www.zhihu.com/node/ProfileFolloweesListV2'
  # 开始获取所有的关注者 math.ceil(follower_num/20)*20
  for i in range(0, math.ceil(following_num / 20) * 20, 20):
    post_data = {
      'method': 'next',
      'params': json.dumps({"offset": i, "order_by": "created", "hash_id": hash_id})
    }
    try:
      j = self.session.post(post_url, params=post_data, headers=self.headers, timeout=35).text.encode(
        'latin-1').decode(
        'unicode-escape')
      pattern = re.compile(r"class="zm-item-link-avatar"[^"]*"([^"]*)", re.DOTALL)
      j = pattern.findall(j)
      for user in j:
        user = user.replace('\', '')
        self.add_wait_user(user) # 保存到redis
    except Exception as err:
      print("获取正在关注失败")
      print(err)
      traceback.print_exc()
      pass

调用博客园的API,获取具备的爱慕顾客列表和观者客户列表,递归获取客户
此处需求小心的是底部要记得带上xsrf不然会抛出403

# 分析about页面,获取用户详细资料
def get_user_info(self, name_url):
  about_page = self.get_user_page(name_url)
  # 判断是否获取到页面
  if not about_page:
    print("获取用户详情页面失败,跳过,name_url:" + name_url)
    return
  self.get_xsrf(about_page)
  BS = BeautifulSoup(about_page, 'html.parser')
  # 获取页面的具体数据
  try:
    nickname = BS.find("a", class_="name").get_text() if BS.find("a", class_="name") else ''
    user_type = name_url[1:name_url.index('/', 1)]
    self_domain = name_url[name_url.index('/', 1) + 1:]
    gender = 2 if BS.find("i", class_="icon icon-profile-female") else (1 if BS.find("i", class_="icon icon-profile-male") else 3)
    follower_num = int(BS.find('span', text='关注者').find_parent().find('strong').get_text())
    following_num = int(BS.find('span', text='关注了').find_parent().find('strong').get_text())
    agree_num = int(re.findall(r'<strong>(.*)</strong>.*赞同', about_page)[0])
    appreciate_num = int(re.findall(r'<strong>(.*)</strong>.*感谢', about_page)[0])
    star_num = int(re.findall(r'<strong>(.*)</strong>.*收藏', about_page)[0])
    share_num = int(re.findall(r'<strong>(.*)</strong>.*分享', about_page)[0])
    browse_num = int(BS.find_all("span", class_="zg-gray-normal")[2].find("strong").get_text())
    trade = BS.find("span", class_="business item").get('title') if BS.find("span",
                                       class_="business item") else ''
    company = BS.find("span", class_="employment item").get('title') if BS.find("span",
                                         class_="employment item") else ''
    school = BS.find("span", class_="education item").get('title') if BS.find("span",
                                        class_="education item") else ''
    major = BS.find("span", class_="education-extra item").get('title') if BS.find("span",
                                           class_="education-extra item") else ''
    job = BS.find("span", class_="position item").get_text() if BS.find("span",
                                      class_="position item") else ''
    location = BS.find("span", class_="location item").get('title') if BS.find("span",
                                         class_="location item") else ''
    description = BS.find("div", class_="bio ellipsis").get('title') if BS.find("div",
                                          class_="bio ellipsis") else ''
    ask_num = int(BS.find_all("a", class_='item')[1].find("span").get_text()) if 
      BS.find_all("a", class_='item')[
        1] else int(0)
    answer_num = int(BS.find_all("a", class_='item')[2].find("span").get_text()) if 
      BS.find_all("a", class_='item')[
        2] else int(0)
    article_num = int(BS.find_all("a", class_='item')[3].find("span").get_text()) if 
      BS.find_all("a", class_='item')[3] else int(0)
    collect_num = int(BS.find_all("a", class_='item')[4].find("span").get_text()) if 
      BS.find_all("a", class_='item')[4] else int(0)
    public_edit_num = int(BS.find_all("a", class_='item')[5].find("span").get_text()) if 
      BS.find_all("a", class_='item')[5] else int(0)
    replace_data = 
      (pymysql.escape_string(name_url), nickname, self_domain, user_type,
       gender, follower_num, following_num, agree_num, appreciate_num, star_num, share_num, browse_num,
       trade, company, school, major, job, location, pymysql.escape_string(description),
       ask_num, answer_num, article_num, collect_num, public_edit_num)
    replace_sql = '''REPLACE INTO
           user(url,nickname,self_domain,user_type,
           gender, follower,following,agree_num,appreciate_num,star_num,share_num,browse_num,
           trade,company,school,major,job,location,description,
           ask_num,answer_num,article_num,collect_num,public_edit_num)
           VALUES(%s,%s,%s,%s,
           %s,%s,%s,%s,%s,%s,%s,%s,
           %s,%s,%s,%s,%s,%s,%s,
           %s,%s,%s,%s,%s)'''
    try:
      print("获取到数据:")
      print(replace_data)
      self.db_cursor.execute(replace_sql, replace_data)
      self.db.commit()
    except Exception as err:
      print("插入数据库出错")
      print("获取到数据:")
      print(replace_data)
      print("插入语句:" + self.db_cursor._last_executed)
      self.db.rollback()
      print(err)
      traceback.print_exc()
  except Exception as err:
    print("获取数据出错,跳过用户")
    self.redis_con.hdel("already_get_user", name_url)
    self.del_already_user(name_url)
    print(err)
    traceback.print_exc()
    pass

末段,到顾客的about页面,分析页面成分,利用正则或然beatifulsoup剖判抓取页面包车型客车数目
此间大家SQL语句用REPLACE INTO而不用INSERT
INTO,这样能够很好的警务器材数据再一次难题

# 开始抓取用户,程序总入口
def entrance(self):
  while 1:
    if int(self.redis_con.llen("user_queue")) < 1:
      self.get_index_page_user()
    else:
      # 出队列获取用户name_url redis取出的是byte,要decode成utf-8
      name_url = str(self.redis_con.rpop("user_queue").decode('utf-8'))
      print("正在处理name_url:" + name_url)
      self.get_user_info(name_url)
      if int(self.redis_con.llen("user_queue")) <= int(self.max_queue_len):
        self.get_all_follower(name_url)
        self.get_all_following(name_url)
    self.session.cookies.save()
def run(self):
  print(self.name + " is running")
  self.entrance()

最后,入口

if __name__ == '__main__':
  login = GetUser(999, "登陆线程")
  threads = []
  for i in range(0, 4):
    m = GetUser(i, "thread" + str(i))
    threads.append(m)
  for i in range(0, 4):
    threads[i].start()
  for i in range(0, 4):
    threads[i].join()

此间就是八十四十六线程的展开,须求敞开多少个线程就把4换来多少就能够了

Docker

嫌麻烦的能够参见一下自家用docker轻易的搭建三个幼功条件:

mysql和redis都是官方镜像

docker run --name mysql -itd mysql:latest
docker run --name redis -itd mysql:latest

再利用docker-compose运行python镜像,我的python的docker-compose.yml:

python:
 container_name: python
 build: .
 ports:
  - "84:80"
 external_links:
  - memcache:memcache
  - mysql:mysql
  - redis:redis
 volumes:
  - /docker_containers/python/www:/var/www/html
 tty: true
 stdin_open: true
 extra_hosts:
  - "python:192.168.102.140"
 environment:
  PYTHONIOENCODING: utf-8

末段附上源代码: GITHUB

本站下载地址:

要求采纳的包: beautifulsoup4 html5lib image requests redis PyMySQL
pip安装具备注重包: pip install Image…

奥门新浦京官方网站 3

支付前的希图

安装Linux系统(Ubuntu14.04),在VMWare虚构机下安装三个Ubuntu;

设置PHP5.6或上述版本;

安装MySQL5.5或以上版本;

安装curl、pcntl扩展。

选拔PHP的curl扩张抓取页面数据

PHP的curl扩展是PHP匡助的同意你与各个服务器使用各体系型的说道举办一连和通讯的库。

本程序是抓取今日头条的客户数据,要能访谈客户个人页面,需求客商登入后的才具访问。当我们在浏览器的页面中式茶食击三个客户头像链接步入客商个人基本页面包车型客车时候,之所以能够见到客商的音信,是因为在点击链接的时候,浏览器帮您将地面的cookie带上一齐提交到新的页面,所以你就能够步向到客商的私家基本页面。因而达成访谈个人页面之前需求先拿走顾客的cookie音讯,然后在每趟curl要求的时候带上cookie音信。在获得cookie音信方面,笔者是用了自个儿的cookie,在页面中得以看看本人的cookie音讯:

奥门新浦京官方网站 4

四个个地复制,以”__utma=?;__utmb=?;”那样的样式结合贰个cookie字符串。接下来就能够使用该cookie字符串来发送乞请。

千帆竞发的演示:

$url = 'http://www.zhihu.com/people/mora-hu/about'; //此处mora-hu代表用户ID
$ch = curl_init($url); //初始化会话
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_COOKIE, $this->config_arr['user_cookie']);  //设置请求COOKIE
curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);  //将curl_exec()获取的信息以文件流的形式返回,而不是直接输出。
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);  
$result = curl_exec($ch);
return $result;  //抓取的结果

运维方面包车型客车代码能够拿走mora-hu客商的私人民居房基本页面。利用该结果再接收正则表明式对页面进行管理,就能获得到姓名,性别等所需求抓取的消息。

图片防盗链

在对回到结果开展正则管理后输出个人音讯的时候,发未来页面中输出客商头像时束手缚脚展开。经过查阅资料获知,是因为微博对图纸做了防盗链管理。解决方案就是号召图片的时候在恳求头里冒充八个referer。

在接受正则表明式获取到图片的链接之后,再发三回号召,那时候带上海体育场所片央浼的来源,表明该乞请来自博客园网址的转向。具体育赛事举个例子下:

function getImg($url, $u_id)
{
    if (file_exists('./images/' . $u_id . ".jpg"))
    {
        return "images/$u_id" . '.jpg';
    }
    if (empty($url))
    {
        return '';
    }
    $context_options = array(  
        'http' =>  
        array(
            'header' => "Referer:http://www.zhihu.com"//带上referer参数
      )
  );

    $context = stream_context_create($context_options);  
    $img = file_get_contents('http:' . $url, FALSE, $context);
    file_put_contents('./images/' . $u_id . ".jpg", $img);
    return "images/$u_id" . '.jpg';
}

爬取越多客户

抓取了协调的个人音信后,就要求再拜望客商的关心者和关怀了的顾客列表获取越来越多的顾客新闻。然后风流倜傥层意气风发层地拜候。可以看看,在个人基本页面里,有三个链接如下:

奥门新浦京官方网站 5

此间有四个链接,一个是关爱了,另二个是关怀者,以“关怀了”的链接为例。用正则相称去相配到相应的链接,获得url之后用curl带上cookie再发一遍呼吁。抓取到客户关怀了的用于列表页之后,能够赢得上边包车型大巴页面:

奥门新浦京官方网站 6

解析页面包车型地铁html布局,因为生龙活虎旦得到客户的音讯,所以只要求框住的这一块的div内容,客商名都在这里在那之中。可以看来,客户关怀了的页面包车型大巴url是:

奥门新浦京官方网站 7

不等的客商的那一个url差不多是大同小异的,分裂之处就在于客商名这里。用正则相配获得顾客名列表,一个三个地拼url,然后再每种发央求(当然,多个四个是很慢的,下边有施工方案,这一个稍后会聊起)。踏向到新客户的页面之后,再另行上边包车型地铁步子,就这样持续循环,直到达到你所要的数据量。

Linux总结文件数量

脚本跑了风流倜傥段时间后,要求探视到底取得了稍稍图片,当数据量非常大的时候,张开文件夹查看图片数量就有一点点慢。脚本是在Linux意况下运作的,因而能够使用Linux的吩咐来总计文件数量:

ls -l | grep "^-" | wc -l

其中,
ls -l 是长列表输出该目录下的文件信息(这里的文件可以是目录、链接、设备文件等); grep "^-" 过滤长列表输出信息, "^-" 只保留一般文件,如果只保留目录是 "^d" ; wc -l 是统计输出信息的行数。下面是一个运行示例:

奥门新浦京官方网站 8

插入MySQL时再一次数据的管理

程序运转了蓬蓬勃勃段时间后,发掘存大多用户的数据是双重的,由此必要在插入重复顾客数据的时候做管理。管理方案如下:

1)插入数据库以前检查数据是不是早就存在数据库;

2)增添独一索引,插入时采取 INSERT INTO ... ON DUPLICATE KEY UPDATE...

3)增添独一索引,插入时使用 INSERT INGNORE INTO...

4)增添独一索引,插入时行使 REPLACE INTO...

首先种方案是最简便但也是效能最差的方案,由此不利用。二和四方案的推行结果是雷同的,区别的是,在碰着相同的多寡时,
INSERT INTO … ON DUPLICATE KEY UPDATE 是一向更新的,而 REPLACE INTO
是先删除旧的数量然后插入新的,在这里个进度中,还索要重新维护索引,所以速度慢。所以在二和四两个间选拔了第三种方案。而第二种方案,
INSERT INGNORE
会忽略试行INSERT语句现身的荒唐,不会忽视语法难点,不过忽视主键存在的意况。这样一来,使用
INSERT INGNORE
就更加好了。最后,思忖到要在数据库中著录重复数据的条数,因而在程序中使用了第二种方案。

使用curl_multi完毕三十二线程抓取页面

刚开端单进程何况单个curl去抓取数据,速度不快,挂机爬了三个晚上只得抓到2W的数码,于是便想到能或无法在步入新的顾客页面发curl哀告的时候叁次性央浼八个顾客,后来意识了curl_multi那么些好东西。curl_multi那类函数能够完结同不常间伸手多少个url,而不是二个个诉求,那看似于linux系统中贰个进程开多条线程实施的效用。上面是使用curl_multi实现三四线程爬虫的示范:

    $mh = curl_multi_init(); //返回一个新cURL批处理句柄
    for ($i = 0; $i < $max_size; $i++)
    {
        $ch = curl_init();  //初始化单个cURL会话
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_URL, 'http://www.zhihu.com/people/' . $user_list[$i] . '/about');
        curl_setopt($ch, CURLOPT_COOKIE, self::$user_cookie);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        $requestMap[$i] = $ch;
        curl_multi_add_handle($mh, $ch);  //向curl批处理会话中添加单独的curl句柄
    }

    $user_arr = array();
    do {
                    //运行当前 cURL 句柄的子连接
        while (($cme = curl_multi_exec($mh, $active)) == CURLM_CALL_MULTI_PERFORM);

        if ($cme != CURLM_OK) {break;}
                    //获取当前解析的cURL的相关传输信息
        while ($done = curl_multi_info_read($mh))
        {
            $info = curl_getinfo($done['handle']);
            $tmp_result = curl_multi_getcontent($done['handle']);
            $error = curl_error($done['handle']);

            $user_arr[] = array_values(getUserInfo($tmp_result));

            //保证同时有$max_size个请求在处理
            if ($i < sizeof($user_list) && isset($user_list[$i]) && $i < count($user_list))
            {
                $ch = curl_init();
                curl_setopt($ch, CURLOPT_HEADER, 0);
                curl_setopt($ch, CURLOPT_URL, 'http://www.zhihu.com/people/' . $user_list[$i] . '/about');
                curl_setopt($ch, CURLOPT_COOKIE, self::$user_cookie);
                curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36');
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
                $requestMap[$i] = $ch;
                curl_multi_add_handle($mh, $ch);

                $i++;
            }

            curl_multi_remove_handle($mh, $done['handle']);
        }

        if ($active)
            curl_multi_select($mh, 10);
    } while ($active);

    curl_multi_close($mh);
    return $user_arr;

HTTP 429 Too Many Requests

使用curl_multi函数能够并且发三个须求,不过在实行进度中使同不经常间发200个诉求的时候,发现许多伸手不能够回去了,即开采了丢包的事态。进一层解析,使用
curl_getinfo 函数打字与印刷各类乞求句柄消息,该函数重返一个包括HTTP
response新闻的关全面组,在那之中有二个字段是http_code,表示央浼再次来到的HTTP状态码。见到有多数少个央浼的http_code都是429,那些再次来到码的意思是发送太多央求了。小编猜是搜狐做了防爬虫的堤防,于是小编就拿任何的网址来做测量试验,开采贰遍性发200个央浼时没问题的,注明了自己的推断,博客园在此方面做了防范,即一遍性的伏乞数量是有约束的。于是笔者不住地减小央求数量,发未来5的时候就不曾丢包境况了。表达在此个程序里贰遍性最四只好发5个诉求,尽管非常少,但这也是贰回小进步了。

动用Redis保存已经访谈过的顾客

抓取客商的经过中,开掘成些顾客是曾经访问过的,并且她的关怀者和关心了的顾客都早已赢得过了,即使在数据库的局面做了双重数据的拍卖,然则程序依然会接收curl发要求,那样重复的出殡央求就有繁多双重的互连网开销。还会有二个就是待抓取的顾客必要权且保留在三个地点以便下贰次施行,刚初步是置于数组里面,后来开采要在前后相继里添增添种经营过,在多进度编制程序里,子进程会分享程序代码、函数库,不过经过使用的变量与别的进程所利用的一丝一毫不一样。区别进度之间的变量是分手的,不能被此外进程读取,所以是不能够应用数组的。因而就想到了利用Redis缓存来保存已经管理好的客户以至待抓取的顾客。那样每回实行完的时候都把客户push到一个already_request_queue队列中,把待抓取的顾客(即每种客户的关怀者和关心了的顾客列表)push到request_queue里面,然后每一趟实践前都从request_queue里pop一个客户,然后判别是或不是在already_request_queue里面,若是在,则进行下贰个,不然就继续试行。

在PHP中使用redis示例:

<?php
    $redis = new Redis();
    $redis->connect('127.0.0.1', '6379');
    $redis->set('tmp', 'value');
    if ($redis->exists('tmp'))
    {
        echo $redis->get('tmp') . "n";
    }

选择PHP的pcntl扩大完毕多进程

改用了curl_multi函数实现多线程抓取客户新闻之后,程序运营了一个夜晚,最后获得的数码有10W。还不可能落得和煦的佳绩指标,于是便一而再三番五次优化,后来察觉php里面有一个pcntl扩充可以兑现多进度编制程序。下边是多编制程序编制程序的现身说法:

//PHP多进程demo
//fork10个进程
for ($i = 0; $i < 10; $i++) {
    $pid = pcntl_fork();
    if ($pid == -1) {
        echo "Could not fork!n";
        exit(1);
    }
    if (!$pid) {
        echo "child process $i runningn";
        //子进程执行完毕之后就退出,以免继续fork出新的子进程
        exit($i);
    }
}

//等待子进程执行完毕,避免出现僵尸进程
while (pcntl_waitpid(0, $status) != -1) {
    $status = pcntl_wexitstatus($status);
    echo "Child $status completedn";
}

在Linux下查看系统的cpu消息

兑现了多进程编制程序之后,就想着多开几条长河不断地抓取客商的多少,后来开了8调进程跑了四个晚上后发觉只好获得20W的数据,未有多大的提拔。于是查阅资料开掘,依照系统优化的CPU质量调优,程序的最大进度数不能够随意给的,要依据CPU的核数和来给,最大进度数最佳是cpu核数的2倍。因而需求查阅cpu的音信来拜会cpu的核数。在Linux下查看cpu的信息的下令:

cat /proc/cpuinfo

结果如下:

奥门新浦京官方网站 9

中间,model name表示cpu类型音讯,cpu
cores表示cpu核数。这里的核数是1,因为是在虚构机下运作,分配到的cpu核数少之甚少,因而只好开2条经过。最终的结果是,用了贰个星期六就抓取了110万的客户数量。

多进度编制程序中Redis和MySQL连接难点

在多进度条件下,程序运营了生龙活虎段时间后,发现数目不可能插入到数据库,会报mysql
too many connections的错误,redis也是这么。

下边这段代码会举办倒闭:

<?php
     for ($i = 0; $i < 10; $i++) {
          $pid = pcntl_fork();
          if ($pid == -1) {
               echo "Could not fork!n";
               exit(1);
          }
          if (!$pid) {
               $redis = PRedis::getInstance();
               // do something     
               exit;
          }
     }

根本原因是在逐个子进程创设时,就早就三番五次了父进度后生可畏份完全平等的正片。对象足以拷贝,可是已开立的连年无法被拷贝成四个,因此发生的结果,正是各种进度都施用同四个redis连接,各干各的事,最后爆发岂有此理的冲突。

消除措施:
>程序不可能一心保障在fork进程从前,父进度不会创制redis连接实例。由此,要消除这一个题材不能不靠子进度本人了。试想一下,如若在子进程中拿走的实例只与当下进度有关,那么这些主题材料就不设有了。于是实施方案就是微微改良一下redis类实例化的静态方式,与当下历程ID绑定起来。

改换后的代码如下:

<?php
     public static function getInstance() {
          static $instances = array();
          $key = getmypid();//获取当前进程ID
          if ($empty($instances[$key])) {
               $inctances[$key] = new self();
          }

          return $instances[$key];
     }

PHP总括脚本实践时间

因为想明白种种进程花销的时日是微微,由此写个函数总计脚本试行时间:

function microtime_float()
{
     list($u_sec, $sec) = explode(' ', microtime());
     return (floatval($u_sec) + floatval($sec));
}

$start_time = microtime_float();

//do something
usleep(100);

$end_time = microtime_float();
$total_time = $end_time - $start_time;

$time_cost = sprintf("%.10f", $total_time);

echo "program cost total " . $time_cost . "sn";

若文中有不科学之处,望各位建议以便更改。

代码托管地址:

发表评论

电子邮件地址不会被公开。 必填项已用*标注