开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

使用Python、Neo4j和GraphQL,爬取俄罗斯Tw

发表于 2017-11-17

Scraping Russian Twitter Trolls With Python, Neo4j, and GraphQL

12 Nov 2017

Last week as a result of the House Intelligence Select Committee investigation, Twitter released the screen names of 2752 Twitter accounts tied to Russia’s Internet Research Agency that were involved in spreading fake news, presumably with the goal of influencing the 2016 election. In this post we explore how to scrape tweets from the user pages of cached versions of these pages, import into Neo4j for analysis, and how to
build a simple GraphQL API exposing this data through GraphQL.

Russian Twitter Trolls

While Twitter released the screen names and user ids of these accounts, they did not release any data (such as tweets or follower network information) associated with the accounts. In fact, Twitter has suspended these accounts which means their tweets
have been removed from Twitter.com and are no longer accessible through the Twitter API. Analyzing the tweets made by these accounts is the first step in understanding how social media accounts run by Russia may have been used to influence the
US Election. So our first step is simply to find potential sources for the data.

Internet Archive

Internet Archive is a non-profit library that provides cached version of some websites: a snapshot of a webpage at a given point in time that can be viewed later. One option for obtaining some of the Russian Troll
tweets is by using Internet Archive to find any Twitter user pages that may have been cached by Internet Archive.

For example, if you visit web.archive.org/web/2017081… we can see the Twitter page for @TEN_GOP, one of the Russia Troll accounts
that was designed to look like an account associated with the Tennessee Republican party.

This snapshot page contains several of @TEN_GOP’s most recent tweets (before the snapshot was taken by Internet Archive).

Finding Available Cached Pages

Using the screen names provided by the House Intelligence Committee we can use Internet Archive’s Wayback API to see if the user’s Twitter profile page was cached by Internet Archive at any point in time. We’ll write a simple Python script to iterate
through the list of Russian Troll twitter accounts, checking the Wayback API for any available cached pages.

We can do this by making a request to http://archive.org/wayback/available?url=http://twitter.com/TWITTER_SCREEN_NAME_HERE. This will return the url and timestamp of any caches made, if they exist. So we iterate through the list of twitter
screen names, checking the Wayback API for any available caches.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码import requests
items = []
initial = "http://archive.org/wayback/available"
# iterate through list of flagged twitter screen names
with open('./data/twitter_handle_urls.csv') as f:
for line in f:
params = {'url': line}
r = requests.get(initial, params=params)
d = r.json()
#print(d)
items.append(d)
# write URL of any available archives to file
with open('./data/avail_urls.txt', 'w') as f:
for item in items:
if 'archived_snapshots' in item:
if 'closest' in item['archived_snapshots']:
f.write(item['archived_snapshots']['closest']['url'] + '\n')

With this, we end up with a file twitter_handle_urls.csv that contains a list of Internet Archive urls for any of the Russian troll accounts that were archived by Internet Archive. Unfortunately, we only find just over 100 Russia Troll
accounts that were cached by Internet Archive. This is just a tiny sample of the overall accounts, but we should still be able to scrape tweets for these 100 users.

Scraping Twitter Profile Pages

Now, we’re ready to scrape the HTML from the Internet Archive caches to extract all the tweet content that we can.

We’ll make use of the BeautifulSoup Python package to help us extract the tweet data from the HTML. First, we’ll use Chrome devtools to inspect the structure of the HTML, seeing what elements contain the data we’re looking for:

Since the caches were taken at different times, the structure of the HTML may have changed. We’ll need to write code that can handle parsing these different formats. We’ve found two versions of the Twitter user pages in the caches. One from ~2015,
and one used around ~2016-2017.

Here is the code for scraping the data for one of the versions. The full code is available here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码import urllib
from bs4 import BeautifulSoup
import csv
import requests

# testing for new version

url = "http://web.archive.org/web/20150603004258/https://twitter.com/AlwaysHungryBae"
page = requests.get(url).text
soup = BeautifulSoup(page, 'html.parser')

tweets = soup.find_all('li', attrs={'data-item-type': 'tweet'})

for t in tweets:
tweet_obj = {}
tweet_obj['tweet_id'] = t.get("data-item-id")
tweet_container = t.find('div', attrs={'class': 'tweet'})
tweet_obj['screen_name'] = tweet_container.get('data-screen-name')
tweet_obj['permalink'] = tweet_container.get('data-permalink-path')
tweet_content = tweet_container.find('p', attrs={'class': 'tweet-text'})
tweet_obj['tweet_text'] = tweet_content.text
tweet_obj['user_id'] = tweet_container.get('data-user-id')

tweet_time = tweet_container.find('span', attrs={'class': '_timestamp'})
tweet_obj['timestamp'] = tweet_time.get('data-time-ms')

hashtags = tweet_container.find_all('a', attrs={'class': 'twitter-hashtag'})
tweet_obj['hashtags'] = []
tweet_obj['links'] = []

for ht in hashtags:
ht_obj = {}
ht_obj['tag'] = ht.find('b').text
ht_obj['archived_url'] = ht.get('href')
tweet_obj['hashtags'].append(ht_obj)

links = tweet_container.find_all('a', attrs={'class': 'twitter-timeline-link'})
for li in links:
li_obj = {}
if li.get('data-expanded-url'):
li_obj['url'] = li.get('data-expanded-url')
elif li.get('data-resolved-url-large'):
li_obj['url'] = li.get('data-resolved-url-large')
else:
li_obj['url'] = li.text
li_obj['archived_url'] = li.get('href')
tweet_obj['links'].append(li_obj)

print(tweet_obj)

BeautifulSoup allows us to select HTML elements by specifying attributes to match against. By inspecting the structure of the HTML page we can see which bits of the tweets are stored in different HTML elements so we know which to grab with BeautifulSoup.
We build up an array of tweet objects as we parse all the tweets on the page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码
{
'tweet_id': '561931644785811457',
'screen_name': 'AlwaysHungryBae',
'permalink': '/AlwaysHungryBae/status/561931644785811457',
'tweet_text': 'Happy Super Bowl Sunday \n#superbowlfood pic.twitter.com/s6rwMtdLom',
'user_id': '2882130846',
'timestamp': '1422809918000',
'hashtags': [
{'tag': 'superbowlfood',
'archived_url': '/web/20150603004258/https://twitter.com/hashtag/superbowlfood?src=hash'
}
],
'links': [
{'url': 'pic.twitter.com/s6rwMtdLom',
'archived_url': 'http://web.archive.org/web/20150603004258/http://t.co/s6rwMtdLom'
},
{'url': 'https://pbs.twimg.com/media/B8xh2fFCQAE-vxU.jpg:large', '
archived_url': '//web.archive.org/web/20150603004258/https://twitter.com/AlwaysHungryBae/status/561931644785811457/photo/1'
}
]
}

Once we’ve extracted the tweets we write them to a json file:

1
2
3
4
复制代码# write tweets to file
import json
with open('./data/tweets_full.json', 'w') as f:
json.dump(tweet_arr, f, ensure_ascii=False, sort_keys=True, indent=4)

We end up finding about 1500 tweets from 187 Twitter accounts. This is only a fraction of the tweets sent by the Russian Trolls, but is still too much data for us to analyze by reading every tweet. We’ll make use of the Neo4j graph database to help
us make sense of the data. Using Neo4j we’ll be able to ask questions such as “What hashtags are used together most frequently?”, or “What are the domains of URLs shared in tweets that mention Trump?”.

Importing Into Neo4j

Now that we have our scraped tweet data we’re ready to insert into Neo4j. We have several options for importing data into Neo4j. We’ll do our import by loading the JSON data and
passing it as a parameter to a Cypher query, using the Python driver for Neo4j.

We’ll use a simple graph data model, treating Hashtags and Links as nodes in the graph, as well as the Tweet and User who posted the tweet.

Datamodel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码from neo4j.v1 import GraphDatabase
import json

driver = GraphDatabase.driver("bolt://localhost:7687")
with open('./data/tweets_full.json') as json_data:
tweetArr = json.load(json_data)

import_query = '''
WITH $tweetArr AS tweets
UNWIND tweets AS tweet
MERGE (u:User {user_id: tweet.user_id})
ON CREATE SET u.screen_name = tweet.screen_name
MERGE (t:Tweet {tweet_id: tweet.tweet_id})
ON CREATE SET t.text = tweet.tweet_text,
t.permalink = tweet.permalink
MERGE (u)-[:POSTED]->(t)

FOREACH (ht IN tweet.hashtags |
MERGE (h:Hashtag {tag: ht.tag })
ON CREATE SET h.archived_url = ht.archived_url
MERGE (t)-[:HAS_TAG]->(h)
)

FOREACH (link IN tweet.links |
MERGE (l:Link {url: link.url})
ON CREATE SET l.archived_url = link.archived_url
MERGE (t)-[:HAS_LINK]->(l)
)

'''

def add_tweets(tx):
tx.run(import_query, tweetArr=tweetArr)

with driver.session() as session:
session.write_transaction(add_tweets)

Graph Queries

Now that we have the data in Neo4j we can write queries to help make sense of what the Russian Trolls were tweeting about.

Interesting Queries

1
2
3
4
5
复制代码// Tweets for @TEN_GOP
MATCH (u:User)-[:POSTED]->(t:Tweet)-[:HAS_TAG]->(h:Hashtag)
WHERE u.screen_name = "TEN_GOP"
OPTIONAL MATCH (t)-[:HAS_LINK]->(l:Link)
RETURN *

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码// What are the most common hashtags
MATCH (u:User)-[:POSTED]->(t:Tweet)-[:HAS_TAG]->(ht:Hashtag)
RETURN ht.tag AS hashtag, COUNT(*) AS num
ORDER BY num DESC LIMIT 10



╒══════════════════════╤═══════════╕
│"hashtag" │"num"│
╞══════════════════════╪═══════════╡
│"JugendmitMerkel" │90 │
├──────────────────────┼───────────┤
│"TagderJugend" │89 │
├──────────────────────┼───────────┤
│"politics" │61 │
├──────────────────────┼───────────┤
│"news" │30 │
├──────────────────────┼───────────┤
│"sports" │28 │
├──────────────────────┼───────────┤
│"Merkel" │26 │
├──────────────────────┼───────────┤
│"ColumbianChemicals" │25 │
├──────────────────────┼───────────┤
│"WorldElephantDay" │22 │
├──────────────────────┼───────────┤
│"crime" │21 │
├──────────────────────┼───────────┤
│"UnitedStatesIn3Words"│21 │
└──────────────────────┴───────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码// What hashtags are used together most frequently
MATCH (h1:Hashtag)<-[:HAS_TAG]-(t:Tweet)-[:HAS_TAG]->(h2:Hashtag)
WHERE id(h1) < id(h2)
RETURN h1.tag, h2.tag, COUNT(*) AS num ORDER BY num DESC LIMIT 15

╒═════════════════╤══════════════════╤═════╕
│"h1.tag" │"h2.tag" │"num"│
╞═════════════════╪══════════════════╪═════╡
│"JugendmitMerkel"│"TagderJugend" │89 │
├─────────────────┼──────────────────┼─────┤
│"TagderJugend" │"WorldElephantDay"│22 │
├─────────────────┼──────────────────┼─────┤
│"JugendmitMerkel"│"WorldElephantDay"│22 │
├─────────────────┼──────────────────┼─────┤
│"JugendmitMerkel"│"Dschungelkönig" │21 │
├─────────────────┼──────────────────┼─────┤
│"TagderJugend" │"Dschungelkönig" │21 │
├─────────────────┼──────────────────┼─────┤
│"Merkel" │"JugendmitMerkel" │17 │
├─────────────────┼──────────────────┼─────┤
│"Merkel" │"TagderJugend" │17 │
├─────────────────┼──────────────────┼─────┤
│"CDU" │"JugendmitMerkel" │12 │
├─────────────────┼──────────────────┼─────┤
│"CDU" │"TagderJugend" │12 │
├─────────────────┼──────────────────┼─────┤
│"TagderJugend" │"Thailand" │11 │
└─────────────────┴──────────────────┴─────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码// Most common domains shared in tweets
MATCH (t:Tweet)-[:HAS_LINK]->(u:Link)
WITH t, replace(replace(u.url, "http://", '' ), "https://", '') AS url
RETURN COUNT(t) AS num, head(split(url, "/")) ORDER BY num DESC LIMIT 10

╒═════╤═════════════════════════╕
│"num"│"head(split(url, \"/\"))"│
╞═════╪═════════════════════════╡
│835 │"pic.twitter.com" │
├─────┼─────────────────────────┤
│120 │"bit.ly" │
├─────┼─────────────────────────┤
│105 │"\n\n" │
├─────┼─────────────────────────┤
│100 │"pbs.twimg.com" │
├─────┼─────────────────────────┤
│32 │"vk.com" │
├─────┼─────────────────────────┤
│21 │"riafan.ru" │
├─────┼─────────────────────────┤
│21 │"inforeactor.ru" │
├─────┼─────────────────────────┤
│20 │"nevnov.ru" │
├─────┼─────────────────────────┤
│17 │"goodspb.livejournal.com"│
├─────┼─────────────────────────┤
│15 │"www.fox5atlanta.com" │
└─────┴─────────────────────────┘

GraphQL API

In addition to querying Neo4j using Cypher directly, we can also take advantage of the neo4j-graphql integrations to easily build a GraphQL API for our tweets.

First, we define a GraphQL schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码type Tweet {
tweet_id: ID!
text: String
permalink: String
author: User @relation(name: "POSTED", direction: "IN")
hashtags: [Hashtag] @relation(name: "HAS_TAG", direction: "OUT")
links: [Link] @relation(name: "HAS_LINK", direction: "OUT")
}

type User {
user_id: ID!
screen_name: String
tweets: [Tweet] @relation(name: "POSTED", direction: "OUT")
}

type Hashtag {
tag: ID!
archived_url: String
tweets(first: Int): [Tweet] @relation(name: "HAS_TAG", direction: "IN")
}

type Link {
url: ID!
archived_url: String


}


type Query {
Hashtag(tag: ID, first: Int, offset: Int): [Hashtag]
}

Our GraphQL schema defines the types and fields available in the data, as well as the entry points for our GraphQL service. In this case we have a single entry point Hashtag, allowing us to search for tweets by hashtag.

With the neo4j-graphql-js integration, the GraphQL schema maps to the graph database model and translates any arbitrary GraphQL query to Cypher, allowing anyone to query the data through
the GraphQL API without writing Cypher.

Implementing the GraphQL server is simply a matter of passing the GraphQL query to the integration function in the resolver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码import {neo4jgraphql} from 'neo4j-graphql-js';

const resolvers = {
// root entry point to GraphQL service
Query: {
// fetch movies by title substring
Hashtag(object, params, ctx, resolveInfo) {
// neo4jgraphql inspects the GraphQL query and schema to generate a single Cypher query
// to resolve the GraphQL query. Assuming a Neo4j driver instance exists in the context
// the query is executed against Neo4j
return neo4jgraphql(object, params, ctx, resolveInfo);
}
}
};

React App

One of the advantages of having a GraphQL API is that makes it very easy to build web and mobile applications that consume the GraphQL service. To make the data easily searchable we’ve make a simple React web app that allows for searching tweets in
Neo4j by hashtag.

Here we’re searching for tweets that contain the hashtag #crime. We can see that a Russia Troll account @OnlineCleveland is tweeting fake news about crimes in Ohio, making it seem that more crime is occurring in Cleveland. Why would a
Russia Troll account be tweeting about crime in Cleveland leading up to the election? Typically when voters want a “tough on crime” politician elected they vote Republican…

In this post we’ve scraped tweet data from Internet Archive, imported in Neo4j for analysis, built a GraphQL API for exposing the data, and a simple GRANDstack app for allowing anyone to easily search the tweets
by hashtag.

While we were only able to find a small fraction of the tweets posted by the Russian Twitter Troll accounts, we will continue to explore options for finding more of the data ;-)

All code is available on Github at github.com/johnymontan….

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 形参与实参

发表于 2017-11-17

前几天在头条上看到一道经典面试题,引发了一些思考。也是写这篇文章的导火索。

背景

请看题:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public	class Main {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
System.out.println("a=" + a + ",b=" + b);
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}

private static void swap(Integer numa, Integer numb) {
//请实现
}
}

看到这个题后 瞬间觉得有坑。也觉得为什么要书写一个swap方法呢?如下实现不是更简单:

1
2
3
4
5
6
7
8
9
复制代码public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
System.out.println("a=" + a + ",b=" + b);
Integer tmp = a;
a = b;
b = tmp;
System.out.println("a=" + a + ",b=" + b);
}

输出:

1
2
复制代码a=1,b=2
a=2,b=1

完美实现交换。但是请注意,这是一道面试题,要的就是考验一些知识点。所以还是老老实实的实现swap方法吧。
有的同学可能会想,Integer 是一个包装类型,是对Int的装箱和拆箱操作。其实也是一个对象。既然是对象,直接更改对象的引用不就行了?
思路没问题,我们首先看看实现:

1
2
3
4
5
6
复制代码private static void swap(Integer numa, Integer numb) {
Integer tmp = numa;
numa = numb;
numb = tmp;
System.out.println("numa=" + numa + ",numb=" + numb);
}

输出:

1
2
3
复制代码a=1,b=2
numa=2,numb=1
a=1,b=2

不出意外,没有成功
这是什么原因呢?
技术老手一看就知道问题出在形参和实参
混淆了

JAVA的形参和实参的区别:

形参 顾名思义:就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的。
形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元。
因此,形参只在方法内部有效,所以针对引用对象的改动也无法影响到方法外。

实参 顾名思义:就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的。
在本例中 swap 方法 的numa, numb 就是形参,传递给 swap 方法的 a,b 就是实参

注意:
在值传递调用过程中,只能把实参传递给形参,而不能把形参的值反向作用到实参上。在函数调用过程中,形参的值发生改变,而实参的值不会发生改变。
而在引用传递调用的机制中,实际上是将实参引用的地址传递给了形参,所以任何发生在形参上的改变也会发生在实参变量上。
那么问题来了,什么是值传递和引用传递

值传递和引用传递

在谈值传递和引用传递之前先了解下 Java的数据类型有哪些

JAVA的数据类型

Java 中的数据类型分为两大类,基本类型和对象类型。相应的,变量也有两种类型:基本类型和引用类型
基本类型的变量保存原始值,即它代表的值就是数值本身,原始值一般对应在内存上的方法区
而引用类型的变量保存引用值,引用值指向内存空间的地址。代表了某个对象的引用,而不是对象本身。对象本身存放在这个引用值所表示的地址的位置。被引用的对象对应内存上的堆内存区。
基本类型包括:byte,short,int,long,char,float,double,boolean 这八大基本数据类型
引用类型包括:类类型,接口类型和数组

变量的基本类型和引用类型的区别

基本数据类型在声明时系统就给它分配空间

1
2
3
复制代码int a;//虽然没有赋值,但声明的时候虚拟机就会 分配 4字节 的内存区域,而引用数据类型不同,它声明时只给变量分配了引用空间,而不分配数据空间:	
String str;//声明的时候没有分配数据空间,只有 4byte 的引用大小,在方法区,而在堆内存区域没有任何分配
str.length(); //这个操作就会报错,因为堆内存上还没有分配内存区域,而 a = 1; 这个操作就不会报错。

好了,Java的数据类型说完了,继续我们的值传递和引用传递的话题。
先背住一个概念:基本类型的变量是值传递;引用类型的变量
结合前面说的 形参和实参。

值传递

方法调用时,实际参数把它的值传递给对应的形式参数,函数接收的是原始值的一个copy,
此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值的修改,不影响实际参数的值

引用传递

也称为地址传递,址传递。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的是原始值的内存地址
在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象
通过例子来说话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码static class Person {
int age;
Person(int age) {
this.age = age;
}
}

private static void test() {
int a = 100;
testValueT(a);
System.out.println("a=" + a);
Person person = new Person(20);
testReference(person);
System.out.println("person.age=" + person.age);
}

private static void testValueT(int a) {
a = 200;
System.out.println("int testValueT a=" + a);
}

private static void testReference(Person person) {
person.age = 10;
}

输出:

1
2
3
复制代码int testValueT a=200
a=100
person.age=10

看见 值传递 a的值并没有改变,而 引用传递的 persion.age已经改变了
有人说

1
2
3
复制代码private static void testReference(Person person) {
person = new Person(100);
}

为什么 输出的 person.age 还是20呢?
我想说 了解一下什么是引用类型吧? 方法内把 形参的地址引用换成了另一个对象,并没有改变这个对象,并不能影响 外边实参还引用原来的对象,因为 形参只在方法内有效哦。

有人或许还有疑问,按照文章开头的例子,Integer也是 引用类型该当如何呢?
其实 类似的 String,Integer,Float,Double,Short,Byte,Long,Character等等基本包装类型类。因为他们本身没有提供方法去改变内部的值,例如Integer 内部有一个value 来记录int基本类型的值,但是没有提供修改它的方法,而且 也是final类型的,无法通过常规手段更改。
所以虽然他们是引用类型的,但是我们可以认为它是值传递,这个也只是认为,事实上还是引用传递,址传递。


好了,基础知识补充完毕,然我们回到面试题吧


回归正题

1
2
3
4
5
6
复制代码private static void swap(Integer numa, Integer numb) {
Integer tmp = numa;
numa = numb;
numb = tmp;
System.out.println("numa=" + numa + ",numb=" + numb);
}

通过补习基础知识,我们很明显知道 上面这个方法实现替换 是不可行的。因为Interger虽然是引用类型
但是上述操作只是改变了形参的引用,而没有改变实参对应的对象。

那么思路来了,我们通过特殊手段改变 Integer内部的value属性

1
2
3
4
5
6
7
8
9
10
11
复制代码private static void swap(Integer numa, Integer numb) {
Integer tmp = numa;
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);//成功的将numa 引用的 1的对象 值改为 2
field.set(numb, tmp); //由于 tmp 也是指向 numa 未改变前指向的堆 即对象1 ,经过前一步,已经将对象1的值改为了2,自然 numb 也是2,所以改动失效
} catch (Exception e) {
e.printStackTrace();
}
}

输出结果:

1
2
复制代码a=1,b=2
a=2,b=2

又来疑问了?为何 a的值改变成功,而b的改变失败呢?

见代码注释
所以其实 field.set(numb, tmp); 是更改成功的,只是 tmp 经过前一行代码的执行,已经变成了 2。
那么如何破呢?
我们有了一个思路,既然是 tmp的引用的对象值变量,那么我让tmp不引用 numa了

1
2
3
4
5
6
7
8
9
10
11
复制代码private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();//tmp 定义为基本数据类型
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);//这个时候并不改变 tmp 的值
field.set(numb, tmp);
} catch (Exception e) {
e.printStackTrace();
}
}

这种情况下 对 numa 这个对象的修改就不会导致 tmp 的值变化了,看一下运行结果

1
2
复制代码a=1,b=2
a=2,b=2

这是为啥?有没有快疯啦?
难道我们的思路错了?
先别着急,我们看看这个例子:
仅仅是将前面的例子 a的值改为 129,b的值改为130

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public static void main(String[] args) {
Integer a = 129;
Integer b = 130;

System.out.println("a=" + a + ",b=" + b);
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}

private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);
field.set(numb, tmp);
} catch (Exception e) {
e.printStackTrace();
}
}

运行结果:

1
2
复制代码a=129,b=130
a=130,b=129

有没有怀疑人生?我们的思路没有问题啊?为什么 换个数值就行了呢?
我们稍微修改一下程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public static void main(String[] args) {
Integer a = new Integer(1);
Integer b = new Integer(2);

System.out.println("a=" + a + ",b=" + b);
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}

private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);
field.set(numb, tmp);
} catch (Exception e) {
e.printStackTrace();
}
}

运行结果:

1
2
复制代码a=1,b=2
a=2,b=1

哎?为啥 1 和 2 也可以了?
我们这时肯定猜想和Integer的装箱 拆箱有关

装箱,拆箱 概念

Integer的装箱操作

为什么 Integer a = 1 和 Integer a = new Integer(1) 效果不一样
那就瞅瞅源码吧?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码public Integer(int value) {
this.value = value;
}

/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

通过注释知道,java推荐 Integer.valueOf 方式初始化一个Interger因为有 缓存了-128 - 127的数字
我们直接定义 Integer a = 1 具有这个功能,所以 Jvm 底层实现 是通过 Integer.valueOf这个方法
再看 field.set(numb, tmp);
我们打断点,发现通过反射设置
value时 竟然走了 Integer.valueOf 方法
下面是 我们调用 swap前后的 IntegerCache.cache 值得变化

反射修改前:

反射修改后


在反射修改前

1
2
3
复制代码IntegerCache.cache[128]=0
IntegerCache.cache[129]=1
IntegerCache.cache[130]=2

通过反射修改后

1
2
3
复制代码IntegerCache.cache[128]=0
IntegerCache.cache[129]=2
IntegerCache.cache[130]=2

再调用 field.set(numb, tmp) tmp这时等于1 对应的 角标 129 ,但是这个值已经变成了2
所以出现了刚才 奇怪的结果
原来都是缓存的锅
下面趁机再看个例子 加深理解

1
2
3
4
5
6
复制代码Integer testA = 1;
Integer testB = 1;

Integer testC = 128;
Integer testD = 128;
System.out.println("testA=testB " + (testA == testB) + ",\ntestC=testD " + (testC == testD));

输出结果:

1
2
复制代码testA=testB true,
testC=testD false

通过这小示例,在 -128 到 127的数字都走了缓存,这样 testA 和 testB引用的是同一片内存区域的同一个对象。
而 testC testD 数值大于127 所以 没有走缓存,相当于两个Integer对象,在堆内存区域有两个对象。
两个对象自如不相等。
在前面的示例中 我们 通过

1
2
复制代码Integer a = new Integer(1);
Integer b = new Integer(2);

方式初始化 a,b 我们的交换算法没有问题,也是这个原因。

那么到目前为止我们的swap 方法可以完善啦
1
2
3
4
5
6
7
8
9
10
11
复制代码private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);
field.set(numb, new Integer(tmp));
} catch (Exception e) {
e.printStackTrace();
}
}

只需将之前的 field.set(numb, tmp) 改为 field.set(numb, new Integer(tmp))

到此, 这个面试我们已经通过了,还有一个疑问我没有解答。
为什么 field.set(numb, tmp) 会执行 Integer.valueOf() 而 field.set(numb, new Integer(tmp)) 不会执行。
这就是Integer的装箱操作,当 给 Integer.value 赋值 int时,JVM
检测到 int不是Integer类型,需要装箱,才执行了Integer.valueOf()方法。而field.set(numb, new Integer(tmp)) 设置的 是Integer类型了,就不会再拆箱后再装箱。

Over Thanks

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python Scrapy爬虫框架学习

发表于 2017-11-17

Scrapy 是用Python实现一个为爬取网站数据、提取结构性数据而编写的应用框架。

一、Scrapy框架简介

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。

其最初是为了 页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。

二、架构流程图

接下来的图表展现了Scrapy的架构,包括组件及在系统中发生的数据流的概览(绿色箭头所示)。 下面对每个组件都做了简单介绍,并给出了详细内容的链接。数据流如下所描述。

1、组件

Scrapy Engine

引擎负责控制数据流在系统中所有组件中流动,并在相应动作发生时触发事件。 详细内容查看下面的数据流(Data Flow)部分。

调度器(Scheduler)

调度器从引擎接受request并将他们入队,以便之后引擎请求他们时提供给引擎。

下载器(Downloader)

下载器负责获取页面数据并提供给引擎,而后提供给spider。

Spiders

Spider是Scrapy用户编写用于分析response并提取item(即获取到的item)或额外跟进的URL的类。 每个spider负责处理一个特定(或一些)网站。 更多内容请看 Spiders 。

Item Pipeline

Item Pipeline负责处理被spider提取出来的item。典型的处理有清理、 验证及持久化(例如存取到数据库中)。 更多内容查看 Item Pipeline 。

下载器中间件(Downloader middlewares)

下载器中间件是在引擎及下载器之间的特定钩子(specific hook),处理Downloader传递给引擎的response。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。更多内容请看 下载器中间件(Downloader Middleware) 。

Spider中间件(Spider middlewares)

Spider中间件是在引擎及Spider之间的特定钩子(specific hook),处理spider的输入(response)和输出(items及requests)。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。更多内容请看 Spider中间件(Middleware) 。

2、数据流(Data flow)

Scrapy中的数据流由执行引擎控制,其过程如下:

  1. 引擎打开一个网站(open a domain),找到处理该网站的Spider并向该spider请求第一个要爬取的URL(s)。
  2. 引擎从Spider中获取到第一个要爬取的URL并在调度器(Scheduler)以Request调度。
  3. 引擎向调度器请求下一个要爬取的URL。
  4. 调度器返回下一个要爬取的URL给引擎,引擎将URL通过下载中间件(请求(request)方向)转发给下载器(Downloader)。
  5. 一旦页面下载完毕,下载器生成一个该页面的Response,并将其通过下载中间件(返回(response)方向)发送给引擎。
  6. 引擎从下载器中接收到Response并通过Spider中间件(输入方向)发送给Spider处理。
  7. Spider处理Response并返回爬取到的Item及(跟进的)新的Request给引擎。
  8. 引擎将(Spider返回的)爬取到的Item给Item Pipeline,将(Spider返回的)Request给调度器。
  9. (从第二步)重复直到调度器中没有更多地request,引擎关闭该网站。

3、事件驱动网络(Event-driven networking)

Scrapy基于事件驱动网络框架 Twisted 编写。因此,Scrapy基于并发性考虑由非阻塞(即异步)的实现。

关于异步编程及Twisted更多的内容请查看下列链接:

三、4步制作爬虫

  1. 新建项目(scrapy startproject xxx):新建一个新的爬虫项目
  2. 明确目标(编写items.py):明确你想要抓取的目标
  3. 制作爬虫(spiders/xxsp der.py):制作爬虫开始爬取网页
  4. 存储内容(pipelines.py):设计管道存储爬取内容

四、安装框架

这里我们使用 conda 来进行安装:

1
复制代码conda install scrapy

或者使用 pip 进行安装:

1
复制代码pip install scrapy

查看安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码➜  spider scrapy -h
Scrapy 1.4.0 - no active project

Usage:
scrapy <command> [options] [args]

Available commands:
bench Run quick benchmark test
fetch Fetch a URL using the Scrapy downloader
genspider Generate new spider using pre-defined templates
runspider Run a self-contained spider (without creating a project)
settings Get settings values
shell Interactive scraping console
startproject Create new project
version Print Scrapy version
view Open URL in browser, as seen by Scrapy

[ more ] More commands available when run from project directory

Use "scrapy <command> -h" to see more info about a command

1.创建项目

1
2
3
4
5
6
7
8
复制代码➜  spider scrapy startproject SF
New Scrapy project 'SF', using template directory '/Users/kaiyiwang/anaconda2/lib/python2.7/site-packages/scrapy/templates/project', created in:
/Users/kaiyiwang/Code/python/spider/SF

You can start your first spider with:
cd SF
scrapy genspider example example.com
➜ spider

使用 tree 命令可以查看项目结构:

1
2
3
4
5
6
7
8
9
10
11
复制代码➜  SF tree
.
├── SF
│   ├── __init__.py
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│   └── __init__.py
└── scrapy.cfg

2.在spiders 目录下创建模板

1
2
3
4
复制代码➜  spiders scrapy genspider sf "https://segmentfault.com"
Created spider 'sf' using template 'basic' in module:
SF.spiders.sf
➜ spiders

这样,就生成了一个项目文件 sf.py

1
2
3
4
5
6
7
8
9
10
11
复制代码# -*- coding: utf-8 -*-
import scrapy


class SfSpider(scrapy.Spider):
name = 'sf'
allowed_domains = ['https://segmentfault.com']
start_urls = ['http://https://segmentfault.com/']

def parse(self, response):
pass

命令:

1
2
3
4
5
复制代码# 测试爬虫是否正常, sf为爬虫的名称
➜ scrapy check sf

# 运行爬虫
➜ scrapy crawl sf

相关文章:

Scrapy入门教程

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

PHP 代码复用的方式

发表于 2017-11-17

什么是 Trait?

自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 Trait。

  • Trait 是为了单继承语言而准备的一种代码复用机制。
  • Trait 和 Class 相似,它为传统的继承增加了水平的特性的组合,多个无关的 Class 之间不需要互相继承
  • Trait 使得无关的 Class 可以使用相同的属性和方法。

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
复制代码<?php

trait Test
{
public function echoHello()
{
echo 'Hello Trait';
}
}

class Base
{
public function index()
{
echo 'index';
}
}

class One extends Base
{
use Test;
}

class Two extends Base
{
use Test;
}

$one = new One();
$two = new Two();

echo $one->echoHello();
echo $one->index();
echo $two->echoHello();

结果输出 Hello Trait index Hello Trait。

从基类继承的成员会被 Trait 插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 Trait 的方法,而 Trait 则覆盖了被继承的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
复制代码<?php

trait Test
{
public function echoHello()
{
echo 'Hello Trait';
}
}

class Base
{
use Test;

public function echoHello()
{
echo 'Hello Base';
}
}

class One extends Base
{
use Test;

public function echoHello()
{
echo 'Hello One';
}
}

class Two extends Base
{
use Test;
}

$one = new One();
$two = new Two();
$base = new Base();

echo $one->echoHello();

echo $two->echoHello();

echo $base->echoHello();

结果输出 Hello One Hello Trait Hello Base。

  • class one 示例覆盖基类和 Trait Test,说明当前类的方法优先级高于他们。
  • class Two 示例覆盖基类,Trait 的有优先级高于继承的基类。
  • class Base 示例覆盖 Trait Test,说明当前类的方法优先级高于 Trait。

通过逗号分隔,在 use 声明列出多个 trait,可以都插入到一个类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码<?php

trait Test
{
public function echoHello()
{
echo 'Hello ';
}
}

trait TestTwo
{
public function echoWord()
{
echo 'word !';
}
}


class One
{
use Test,TestTwo;
}

$one = new One();

echo $one->echoHello();
echo $one->echoWord();

结果输出 Hello word !。

如果两个 Trait 都插入了一个同名的方法,如果没有明确解决冲突将会产生一个致命错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
复制代码<?php

trait Test
{
public function echoHello()
{
echo 'Hello Test';
}

public function echoWord()
{
echo 'word Test';
}
}

trait TestTwo
{
public function echoHello()
{
echo 'Hello TestTwo ';
}

public function echoWord()
{
echo 'word TestTwo';
}
}

class One
{
use Test, TestTwo {
Test::echoHello as echoTest;
Test::echoWord insteadof TestTwo;
TestTwo::echoHello insteadof Test;
}
}

$one = new One();

echo $one->echoTest();
echo $one->echoWord();
echo $one->echoHello();

输出结果:Hello Test word Test Hello TestTwo。

  • 使用 as 作为别名,即 Test::echoHello as echoTest; 输出 Trait Test 中的 echoHello.
  • 使用 insteadof 操作符用来排除掉其他 Trait,即 Test::echoWord insteadof TestTwo; 输出的是 word Test,使用 Trait Test 中的 echoWord

修改 方法的控制权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
复制代码<?php

trait Test
{
public function echoHello()
{
echo 'Hello';
}

public function echoWord()
{
echo 'word';
}
}

trait TestTwo
{
public function echoHello()
{
echo 'Hello TestTwo ';
}

public function echoWord()
{
echo 'word TestTwo';
}
}

class One
{
use Test {
echoHello as private;
}
}

class Two
{
use Test {
echoHello as private echoTwo;
}
}

$one = new One();
$two = new Two();

echo $two->echoHello();
  • 输出结果 Hello。
  • class one 中使用 as 将 echoHello 设为私有,则通过 class one 不能访问 echoHello。
  • class two 中使用 as 先将其重新命名,然后将新命名方法设置为私有,原 Trait 中的方法可以正常访问。

Trait 中还可以像类一样定义属性。就是很好用的啦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

citus,一个PostgreSQL的扩展

发表于 2017-11-17

Citus Banner

Build Status Slack Status Latest Docs

What is Citus?

  • Open-source PostgreSQL extension (not a fork)
  • Scalable across multiple machines through sharding and replication
  • Distributed engine for query parallelization
  • Database designed to scale multi-tenant applications

Citus is a distributed database that scales across commodity servers using transparent sharding and replication. Citus extends the underlying database rather than forking it, giving developers and enterprises the power and familiarity of a relational
database. As an extension, Citus supports new PostgreSQL releases, and allows you to benefit from new features while maintaining compatibility with existing PostgreSQL tools.

Citus serves many use cases. Two common ones are:

  1. Multi-tenant database: Most B2B applications already have the notion of a tenant / customer / account built into their data model.
    Citus allows you to scale out your transactional relational database to 100K+ tenants with minimal changes to your application.
  2. Real-time analytics: Citus enables ingesting large volumes of data and running analytical queries on that data in human real-time. Example
    applications include analytic dashboards with subsecond response times and exploratory queries on unfolding events.

To learn more, visit citusdata.com and join the mailing list to stay on top of the latest developments.

Getting started with Citus

The fastest way to get up and running is to create a Citus Cloud account. You can also setup a local Citus cluster with Docker.

Citus Cloud

Citus Cloud runs on top of AWS as a fully managed database as a service and has development plans available for getting started. You can provision a Citus Cloud account at console.citusdata.com and get started with just a few clicks.

Local Citus Cluster

If you’re looking to get started locally, you can follow the following steps to get up and running.

  1. Install Docker Community Edition and Docker Compose
  • Mac:
    1. Download and install Docker.
    2. Start Docker by clicking on the application’s icon.
  • Linux:
1
2
3
4
5
6
复制代码curl -sSL https://get.docker.com/ | sh
sudo usermod -aG docker $USER && exec sg docker newgrp `id -gn`
sudo systemctl start docker

sudo curl -sSL https://github.com/docker/compose/releases/download/1.11.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

The above version of Docker Compose is sufficient for running Citus, or you can install the latest version.

  1. Pull and start the Docker images
1
2
复制代码curl -sSLO https://raw.githubusercontent.com/citusdata/docker/master/docker-compose.yml
docker-compose -p citus up -d
  1. Connect to the master database
1
复制代码docker exec -it citus_master psql -U postgres
  1. Follow the first tutorial instructions
  2. To shut the cluster down, run
1
复制代码docker-compose -p citus down

Talk to Contributors and Learn More

Documentation Try the Citus tutorial for a hands-on introduction or the documentation for a more comprehensive reference.
Google Groups The Citus Google Group is our place for detailed questions and discussions.
Slack Chat with us in our community Slack channel.
Github Issues We track specific bug reports and feature requests on our project issues.
Twitter Follow @citusdata for general updates and PostgreSQL scaling tips.

Contributing

Citus is built on and of open source, and we welcome your contributions. The CONTRIBUTING.md file explains how to get started developing the Citus extension itself and
our code quality guidelines.

Who is Using Citus?

Citus is deployed in production by many customers, ranging from technology start-ups to large enterprises. Here are some examples:

  • CloudFlare uses Citus to provide real-time analytics on 100 TBs of data from over 4 million customer websites. Case
    Study
  • MixRank uses Citus to efficiently collect and analyze vast amounts of data to allow inside B2B sales teams to find new customers. Case
    Study
  • Neustar builds and maintains scalable ad-tech infrastructure that counts billions of events per day using Citus and HyperLogLog.
  • Agari uses Citus to secure more than 85 percent of U.S. consumer emails on two 6-8 TB clusters. Case
    Study
  • Heap uses Citus to run dynamic funnel, segmentation, and cohort queries across billions of users and tens of billions of events. Watch
    Video

Copyright © 2012–2017 Citus Data, Inc.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

异步加载的基本逻辑与浏览器抓包一般流程

发表于 2017-11-17

本篇内容不涉及任何R语言或者Python代码实现,仅从异步加载的逻辑实现过程以及浏览器抓包分析的角度来给大家分享一下个人近期学习爬虫的一些心得。

涉及到的工具有Chrome浏览器(开发者工具)、postman(一款非常优秀的Chrome网络请求构造工具,你可以在Chrome浏览器在线商店里搜到,也可以下载桌面版)。

1、异步加载概念及实现过程
2、浏览器抓包分析一般流程

异步加载的英文简称是ajax,即“Asynchronous Javascript And XML”(异步JavaScript和XML)是指一种创建交互式网页应用的网页开发技术。它可以在无需重新加载整个网页的情况下,通过在后台与服务器进行局部数据交换,使得网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的特定部分进行更新。

这是百度百科对于异步加载的一般定义,在传统web开发中,使用同步加载模式,更新网页时,所有内容必须重载,导致多请求进程阻塞,网页迟迟无法加载,给web端体验造成很大的伤害。但是异步加载则解决了这个问题,通过异步加载,不仅提高了web端浏览体验,而且减缓了服务器端压力。

但异步加载却给网络数据抓取造成了很大的困难。困难在于,异步加载把所有网络资源分成了两大部分,一部分是静态的html文档(DOM文档),另一部分是嵌入在HTML文档内的js动态脚本。(这里暂时忽略css重叠样式表,它与任务目标几乎没什么联系)。这些js脚本通过<script>元素标签进行引用,是预定义好的js事件函数,我们所说的异步加载便是通过这些js脚本内的事件函数驱动的。

(浏览器在接受静态文档的同时,可以执行js脚本,与服务器交换数据并更新html内的数据块,但是R或者Python这种请求发送终端是无法做到这一点儿的)

这些事件函数内部,从新构建了一系列网络请求,这些网络请求可能是GET类型,也有可能是POST类型,这些请求便是异步加载的核心实现方式——XMLHttpRequest。XHR是由js脚本构建的,而js脚本是由其嵌入html的位置(<script>元素的所处位置)的html动作控制的。这些动作可能是鼠标点击事件、鼠标悬浮事件、下拉菜单、输入框输入查询关键词之后的回车等。

打开浏览器,并通过网址链接到主网页之后,浏览器会自动加载HTML文档,而同时内嵌的js脚本也会通过异步加载方式初始化一部分数据,这些js脚本加载过程与浏览器渲染html的过程并不相互影响。当用户在浏览器界面的特定位置点击或者实施某些html动作时,这些动作会驱动对应位置的js脚本执行其预定义的事件函数,构建XHR请求,这些XHR请求与服务器进行部分数据交互,返回数据之后,再通过回调函数操作对应位置html元素,将数据插入对应位置,至此数据交换的流程结束。


而对于我们这些爬虫学习者而言,其实想要找的就是这些js脚本构建的异步加载请求对象,通过截获这些请求,伪装成浏览器的身份,进而替代浏览器完成数据请求,并获取返回数据。这些异步请求在Chrome的开发者工具中往往都能截获到。

那么在浏览器得开发者工具中,以上所述得各部分又是如何是怎么对应的呢?

打开网易云课堂得主页,按F12进入开发者工具工作台。


Elements模块是浏览器加载后后的带有数据得完整HTML文档。



如何你是使用请求网页的方式来提取数据,那么通常你需要关注得便是这个模块。但是今天我们的主角是异步加载,所以定位到第二个模块——Network,该模块涉及到所有的浏览器与web服务器之间的交互请求记录。在Network模块的all子模块中,是所有请求列表,它包含了请求的所有dom文件、js脚本、css重叠样式表、img文件(图形对象)、Media文件(流媒体文件)、字体文件等。


而在XHR子菜单中,你可以看到这些加载文件中,以异步加载方式进行的文件对象。(xhr就是XMLHttpRequest的缩写),这个栏目将是我们爬虫抓包的主战场,所以要熟练这个界面的所有信息。


在XHR模块的Name列表中,有很多异步加载请求,你需要迅速过滤出我们想要的异步加载请求对象。

这里有一个秘诀!

这些请求对象一般包含两类,一类是.js文件,这些文件是javascript脚本文件,它们是事件驱动函数,是动作中介,尽管所有的异步加载请求都是由它们发起,返回数据也是由它们负责接收并且插入html文档的,但是它们本身并不包含数据,仅仅是一组脚本函数而已。所以在xhr中所有带有js结尾的文件都可以略过。(因为仅就抓包而言,你无须弄清楚这些请求实现的底层过程)。第二类是剩余的那些带有参数的链接、或者是以.json结尾文件。这些对象便是以上所说的js脚本构建的异步加载请求的目标,也是我们想要截获的请求。

针对本例而言,因为之前爬过网易云课堂,所以我心里知道想要的请求对象是studycourse.json,即便不知道,过滤掉js脚本之后,剩余链接中带有参数特征的,或者以json结尾的对象通常就是我们想要找的对象。

怎么验证呢,打开studycourse.json对象(鼠标点击),此时,右侧会出现五个子菜单。一般默认定位在Headers,这里的信息不足以让我们判断,该请求是否是想要的请求,此时你可以定位到第二项——Preview项目中。


当你定位到Preview项目,看到里面的json格式的数据包,打开后里面的内容与我们首页看到的刘凯老师的课程信息一致时,这时候就没错了,十拿九稳了。我们已经截获了想要的请求。



此时再次回到第一个菜单Headers中,Headers中一共四个模块,分别对应我们抓包分析中构造浏览器请求的四大部分。


General模块告诉我们的有用信息主要有两条:

该请求的地址是study.163.com/p/search/st…,这个地址也是我们伪造请求的唯一地址,请求类型是一个POST请求。

Response Headers

该模块是请求的响应报头,也即当请求构造成功之后,反回的数据有关内容。

它告诉我们的最为重要的信息(影响我们爬虫构建过程的)是返回的数据格式(Content-Type:application/json;charset=UTF-8),json返回值决定着我们需要对返回数据使用json的反序列化。(在R中可以使用jsonlite中的fromJSON,在Python中使用json包中的loads.json())。

Requests Headers

该模块是构造请求的请求报头,主要告知我们请求的一些具体信息,期待获取的数据,发送请求的终端类型,以及登录信息,参照页地址等。

重点关注其中的Cookies参数、Content-Type参数、Referer参数、User-Agent参数、edu-script-token参数。

User-Agent是标识请求发送的设备类型(常用于规避服务端反爬,可以伪造合法的终端类型)。Content-Type是请求参数提交的类型,这里是application/json,就是json对象(在R里可以通过jsonlite包的toJSON()函数构造,在Python里使用json.dumps()函数序列化参数)。

Referer是参照页地址,也就是我们在浏览器看到的想要抓取的内容主页。(注意区别与上述所说的抓包需要的请求地址)

edu-script-token是一个重要的随机参数(意味着每打开 一次浏览器都会变动),它应该是当前进程的一个标识(个人理解)。

Cookies是登录状态,用于表明用户登录身份认证。(requests参数虽然有常用的预定义参数,但是不同网站还有会有些独特的参数类型,实际抓包过程需要不断尝试)

Reqests Payload

最后是本次抓包分析的重头戏,查询条件部分。因为数据很多(通常情况下),不可能一次返回,所以我们需要构建一个查询表单,该表单是POST特有的(GET方法的查询参数包含在url中)。这些查询字符串规定了了返回数据中的活动课程 id,课程排序方式,课程作者,每次返回课程数目,页面课程最大数据,每次返回数据时课程偏移量等信息。

1
2
3
4
5
6
7
复制代码activityId:0
keyword:"刘凯"
orderType:5
pageIndex:1
pageSize:20
priceType:-1
searchTimeType:-1

这些表单需要构成一个json序列之后才能上传,R语言中稍微有些曲折,RCurl包中需要借助jsonlite包中的toJSON()函数进行参数序列化,httr包则含有可选的参数编码类型,直接指定即可。Python中的urllib、requests库,则直接通过json包的json.dumps()函数进行json序列化即可。

从新梳理一下:

General模块确定请求URL、请求方式:POST

Requests模块确定Cookies、Content-Type(请求参数提交格式)、Referer(请求定位的参照页)、User-Agent(设备类型)、edu-script-token(当前进程信息)

Resposes模块确定请求返回数据的格式:Content-Type,决定着我们使用什么函数处理返回值。

Request Payload模块决定提交参数信息(提交方式由Requests模块的Content-Type参数决定)。

至此异步加载的流程分析阶段完毕。

下面分享如何使用postman这款请求构造工具进行请求模拟,测试请求参数以及报头信息是否合法,是否可以 正常返回数据。


postman是一款很好用的网络请求模拟构造软件,打开之后,第一部分选择请求类型,第二部分输入请求URL,第三部分输入请求headers,第四部分输入请求的 查询表单体。


在输入body时,记得选择raw单选按钮,并且格式选择JSON(application/json),这是该请求指定的参数提交方式。



以上流程完成之后,可以点击send。

正常的话,在该界面底部就会返回json数据块儿,这些数据块会被自动按照其原格式解析和格式化,json返回值格式化之后如下所示:


在数据块中随便选中一个序列字段,使用Ctrl+F查找功能,查看一下一共有几个,与浏览器中课程数目是否一致。


结果一致,浏览器中的对应页面刚好也是9门课,本次请求构造成功,测试结束,需要构造的请求格式如下:

请求类型:POST
请求资源地址:study.163.com/p/search/st…
请求报头参数:
Content-Type:application/json
edu-script-token:40a297a878e54bdb9440a31345ad5f63
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36

请求查询表单参数:(发送前需要序列化为json对象)

1
2
3
4
5
6
7
8
9
复制代码{
"pageSize":20,
"pageIndex":1,
"searchTimeType":-1,
"orderType":5,
"priceType":-1,
"activityId":0,
"keyword":"刘凯"
}

将以上信息,使用R语言中的RCurl中的postForm函数、httr包中的POST函数,或者Python中的urllib包、requests包均可以模拟构造该请求,详细请求构造过程,不再重复,感兴趣可以参考这几篇文章。

网易云课堂Excel课程爬虫思路

左手用R右手Pyhon系列——趣直播课程抓取实战

Python数据抓取与可视化实战——网易云课堂人工智能与大数据板块课程实战

R语言网络数据抓取的又一个难题,终于攻破了!

R语言爬虫实战——网易云课堂数据分析课程板块数据爬取

R语言爬虫实战——知乎live课程数据爬取实战

在线课程请点击文末原文链接:

Hellobi Live | 9月12日 R语言可视化在商务场景中的应用
往期案例数据请移步本人GitHub:
github.com/ljtyduyu/Da…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

PHP中如何实现Hook机制

发表于 2017-11-17

所谓Hook机制,是从Windows编程中流行开的一种技术。其主要思想是提前在可能增加功能的地方埋好(预设)一个钩子,这个钩子并没有实际的意义,当我们需要重新修改或者增加这个地方的逻辑的时候,把扩展的类或者方法挂载到这个点即可。

笔者在学习钩子机制时,参考的是TP3.2.3的tag和Hook机制,使用的是自己开发的MVC框架,其目录风格等均模仿TP3.2.3,让大佬们见笑了。

举个简单的例子,我们现在要写一个用户注册的功能,如下图:

/App/Home/Controller/IndexController.class.php

突然有一天,客户说我需要增加一个功能,新用户注册奖励50积分,那我只好这样写:

客户看了很满意,但是你的另一个同事需要你的代码,你跟他说自己从git上pull。客户又提出一个要求,要在用户注册后给用户发一个邮件(忍住(╬▔皿▔)),那你会这样做:

如果在一个项目中,有大量的类似修改,你该怎么办?就那么修改?项目只会越来越臃肿,越发的杂乱不堪。捅死客户?别闹了,犯法的ㄟ( ▔, ▔ )ㄏ。辞职?想想房贷,再想想妻儿老小,我忍(。・`ω´・)。ps:程序员哪来的妻儿…( ̄∀ ̄)

言归正传,最好解决办法就是使用钩子机制。

首先来看一下我们写的Hook类:

/CutePHP/Lib/Cute/Hook.class.php

这是我仿照TP的Hook写的一个简单的Hook类,该类中包含了一个静态的私有属性Hooks用于记录所有已经注册的钩子

add方法传入钩子的名称和方法,即可将这个钩子存入数组中,listen则是用于监听某个钩子,只要有这个钩子将调用exec方法执行这个钩子

我们来测试一下,首先在/App/Home/Controller.class.php中埋入钩子:

然后在/App/Home/Plugin目录下面建立和钩子一样的文件夹 /App/Home/Plugin/register 下面建立和钩子名一样的文件register.php,写一个简单的类,名称也叫register,下面有个两个方法,一个叫before一个叫after:

然后在项目的公共配置中注册两个钩子:

/App/Conf/Hook.php

那么当我们访问Home下面Index控制器的Register方法时就会显示:

那么具体的原理究竟是怎么实现的呢?首先大家先来简单的了解一下我的这个框架,在Cute核心类中有个一个Start方法,用于加载路由启动框架,并且在加载控制器之前就先把/App/Conf/Hook.php文件加载进来:

那么在这个文件中,我们的代码是这样的:

大家想一下我们Hook的add方法,是把register这个钩子(类)实例化保存到Hooks这个私有数组中,这一步是在listen方法之前,那么我们再用listen方法去监听这个插件,发现存在,就执行一下这个对象的方法,也就实现了钩子的功能。

PS:文中很多代码和函数都是不存在的,仅用于描述逻辑过程。

PPS:文中使用的是本人自己仿照TP3.2.3写的一个简单MVC框架,仅本人自己使用,过于简陋让各路大佬见笑了。

PPPS:求收藏,求转发。原创不易,搬运注明。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据库中间件TDDL调研笔记 四,TDDL其他特性

发表于 2017-11-17

前篇:

  • 《数据库中间件cobar调研笔记》

13年底负责数据库中间件设计时的调研笔记,拿出来和大家分享,轻拍。

一,TDDL是什么

  • TDDL是Taobao Distribute Data Layer的简称
  • 淘宝一个基于客户端的数据库中间件产品
  • 基于JDBC规范,没有server,以client-jar的形式存在

画外音:数据库中间件有基于服务端的,也有基于客户端的,TDDL属于后者;而cobar是一个中间层服务,使用mysql协议,属于前者。

二,TDDL不支持什么SQL

  • 不支持各类join
  • 不支持多表查询
  • 不支持between/and
  • 不支持not(除了支持not like)
  • 不支持comment,即注释
  • 不支持for update
  • 不支持group by中having后面出现集函数
  • 不支持force index
  • 不支持mysql独有的大部分函数

画外音:分布式数据库中间件,join都是很难支持的,cobar号称的对join的支持即有限,又低效。

三,TDDL支持什么SQL

  • 支持CURD基本语法
  • 支持as
  • 支持表名限定,即”table_name.column”
  • 支持like/not like
  • 支持limit,即mysql的分页语法
  • 支持in
  • 支持嵌套查询,由于不支持多表,只支持单表的嵌套查询

画外音:分布式数据库中间件,支持的语法都很有限,但对于与联网的大数据/高并发应用,足够了,服务层应该做更多的事情。

四,TDDL其他特性

  • 支持oracle和mysql
  • 支持主备动态切换
  • 支持带权重的读写分离
  • 支持分库分表
  • 支持主键生成:oracle用sequence来生成,mysql则需要建立一个用于生成id的表
  • 支持单库事务,不支持夸库事务
  • 支持多库多表分页查询,但会随着翻页,性能降低

画外音:可以看到,其实TDDL很多东西都不支持,那么为什么它还如此流行呢?它解决的根本痛点是“分布式”“分库分表”等。

加入了解决“分布式”“分库分表”的中间件后,SQL功能必然受限,但是,我们应该考虑到:MYSQL的CPU和MEM都是非常珍贵的,我们应该将MYSQL从复杂的计算(事务,JOIN,自查询,存储过程,视图,用户自定义函数,,,)中释放解脱出来,将这些计算迁移到服务层。

当然,有些后台系统或者支撑系统,数据量小或者请求量小,没有“分布式”的需求,为了简化业务逻辑,写了一些复杂的SQL语句,利用了MYSQL的功能,这类系统并不是分布式数据库中间件的潜在用户,也不可能强行让这些系统放弃便利,使用中间件。

五,TDDL层次结构

TDDL是一个客户端jar,它的结构分为三层:

层次 说明 其他
matrix 可以理解为数据源的全部,它由多个group组成
group 可以理解为一个分组,它由多个atom组成
atom 可以理解为一个数据库,可能是读库,也可能是写库

对应上面图例:matrix数据水平分为了两个group,每个group有主备atom组成。

matrix层

  • 核心是规则引擎
  • 实现分库分表
  • 主要路径:sql解析 => 规则引擎计算(路由) => 执行 => 合并结果

group层

  • 读写分离
  • 权重计算
  • 写HA切换
  • 读HA切换
  • 动态新增slave(atom)节点

atom层

  • 单个数据库的抽象;
  • ip /port /user /passwd /connection 动态修改,动态化jboss数据源
  • thread count(线程计数):try catch模式,保护业务处理线程
  • 动态阻止某些sql的执行
  • 执行次数的统计和限制

整个SQL执行过程

  • BEGIN(sql+args),输入是sql和参数
  • sql解析
  • 规则计算
  • 表名替换
  • 选择groupDS执行sql
  • 根据权重选择atomDS
  • 具备重试策略的在atomDS执行sql
  • 读写控制,并发控制,执行sql,返回结果
  • 合并结果集
  • END(ResultSet),输出是结果集

画外音:感觉难点在SQL的解析上。

六,TDDL最佳实践

  • 尽可能使用1对多规则中的1进行数据切分(patition key),例如“用户”就是一个简单好用的纬度
  • 买家卖家的多对多问题,使用数据增量复制的方式冗余数据,进行查询
  • 利用表结构的冗余,减少走网络的次数,买家卖家都存储全部的数据

画外音:这里我展开一下这个使用场景。

以电商的买家卖家为例,业务方既有基于买家的查询需求,又有基于卖家的查询需求,但通常只能以一个纬度进行数据的分库(patition),假设以买家分库, 那卖家的查询需求如何实现呢?

如上图所示:查询买家所有买到的订单及商品可以直接定位到某一个分库,但要查询卖家所有卖出的商品,业务方就必须遍历所有的买家库,然后对结果集进行合并,才能满足需求。

所谓的“数据增量复制”“表结构冗余”“减少网络次数”,是指所有的数据以买家卖家两个纬度冗余存储两份,如下图:

采用一个异步的消息队列机制,将数据以另一个纬度增量复制一份,在查询的时候,可以直接以卖家直接定位到相应的分库。

这种方式有潜在的数据不一致问题。

继续tddl最佳实践:

  • 利用单机资源:单机事务,单机join
  • 存储模型尽量做到以下几点:
  • 尽可能走内存
  • 尽可能将业务要查询的数据物理上放在一起
  • 通过数据冗余,减少网络次数
  • 合理并行,提升响应时间
  • 读瓶颈通过增加slave(atom)解决
  • 写瓶颈通过切分+路由解决

画外音:相比数据库中间件内核,最佳实践与存储模型,对我们有更大的借鉴意义。

七、TDDL的未来?

  • kv是一切数据存取最基本的组成部分
  • 存储节点少做一点,业务代码就要多做一点
  • 想提升查询速度,只有冗余数据一条路可走
  • 类结构化查询语言,对查询来说非常方便

画外音:潜台词是,在大数据量高并发下,SQL不是大势所趋,no-sql和定制化的协议+存储才是未来方向?

13年底的调研笔记,文中的“画外音”是我当时的批注,希望能让大家对TDDL能有一个初步的认识,有疑问之处,欢迎交流。

相关文章:

《数据库中间件cobar调研笔记》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Hbase WAL 线程模型源码分析

发表于 2017-11-17

作者介绍:熊训德 腾讯云工程师

Hbase 的 WAL 机制是保证 hbase 使用 lsm 树存储模型把随机写转化成顺序写,并从内存 read 数据,从而提高大规模读写效率的关键一环。wal 的多生产者单消费者的线程模型让wal的写入变得安全而高效。

在文章《WAL在RegionServer调用过程》中从代码层面阐述了一个 client 的“写”操作是如何到达Hbase的RegionServer,又是如何真正地写入到 wal(FSHLog) 文件,再写入到 memstore 。但是 hbase 是支持 mvcc 机制的存储系统,本文档将说明
RegionServer 是如何把多个客户端的“写”操作安全有序地落地日志文件,又如何让 client 端优雅地感知到已经真正的落地。

wal 为了高效安全有序的写入,笔者认为最关键的两个机制是 wal 中使用的线程模型和多生产者单消费者模型。

线程模型

其线程模型主要实现实在FSHLog中,FSHLog是WAL接口的实现类,实现了最关键的apend()和sync()方法,其模型如图所示:

这个图主要描述了HRegion中调用append和sync后,hbase的wal线程流转模型。最左边是有多个client提交到HRegion的append和sync操作。

当调用append后WALEdit和WALKey会被封装成FSWALEntry类进而再封装成RinbBufferTruck类放入一个线程安全的Buffer(LMAX Disruptor RingBuffer)中。

当调用sync后会生成一个SyncFuture进而封装成RinbBufferTruck类同样放入这个Buffer中,然后工作线程此时会被阻塞等待被notify()唤醒。在最右边会有一个且只有一个线程专门去处理这些RinbBufferTruck,如果是FSWALEntry则写入hadoop sequence文件。因为文件缓存的存在,这时候很可能client数据并没有落盘。所以进一步如果是SyncFuture会被批量的放到一个线程池中,异步的批量去刷盘,刷盘成功后唤醒工作线程完成wal。

源码分析

下面将从源码角度分析其中具体实现过程和细节。

工作线程中当HRegion准备好一个行事务“写”操作的,WALEdit,WALKey后就会调用FSHLog的append方法:

FSHLog的append方法首先会从LAMX Disruptor RingbBuffer中拿到一个序号作为txid(sequence),然后把WALEdit,WALKey和sequence等构建一个FSALEntry实例entry,并把entry放到ringbuffer中。而entry以truck(RingBufferTruck,ringbuffer实际存储类型)通过sequence和ringbuffer一一对应。

如果client设置的持久化等级是USER_DEFAULT,SYNC_WAL或FSYNC_WAL,那么工作线程的HRegion还将调用FSHLog的sync()方法:

追踪代码可以分析出Sync()方法会往ringbuffer中放入一个SyncFuture对象,并阻塞等待完成(唤醒)。

像模型图中所展示的多个工作线程封装后拿到由ringbuffer生成的sequence后作为生产者放入ringbuffer中。在FSHLog中有一个私有内部类RingBufferEventHandler类实现了LAMX Disruptor的EventHandler接口,也即是实现了OnEvent方法的ringbuffer的消费者。Disruptor通过 java.util.concurrent.ExecutorService 提供的线程来触发 Consumer 的事件处理,可以看到hbase的wal中只启了一个线程,从源码注释中也可以看到RingBufferEventHandler在运行中只有单个线程。由于消费者是按照sequence的顺序刷数据,这样就能保证WAL日志并发写入时只有一个线程在真正的写入日志文件的可感知的全局唯一顺序。

RingBufferEventHandler类的onEvent()(一个回调方法)是具体处理append和sync的方法。在前面说明过wal使用RingBufferTruck来封装WALEntry和SyncFuture(如下图源码),在消费线程的实际执行方法onEvent()中就是被ringbuffer通知一个个的从ringbfer取出RingBufferTruck,如果是WALEntry则使用当前HadoopSequence文件writer写入文件(此时很可能写的是文件缓存),如果是SyncFuture则简单的轮询处理放入SyncRunner线程异步去把文件缓存中数据刷到磁盘。

这里再加一个异步操作去真正刷文件缓存的原因wal源码中有解释:刷磁盘是很费时的操作,如果每次都同步的去刷client的回应比较快,但是写效率不高,如果异步刷文件缓存,写效率提高但是友好性降低,在考虑了写吞吐率和对client友好回应平衡后,wal选择了后者,积累了一定量(通过ringbuffer的sequence)的缓存再刷磁盘以此提高写效率和吞吐率。这个决策从hbase存储机制最初采用lsm树把随机写转换成顺序写以提高写吞吐率,可以看出是目标一致的。

这部分源码可以看到RingBufferTruck类的结构,从注释可以看到选择SyncFuture和FSWALEntry一个放入ringbuffer中。

这部分源码可以看到append的最终归属就是根据sequence有序的把FSWALEntry实例entry写入HadoopSequence文件。这里有序的原因是多工作线程写之前通过ringbuffer线程安全的CAS得到一个递增的sequence,ringbuffer会根据sequence取出FSWALEntry并落盘。这样做其实只有在得到递增的sequence的时候需要保证线程安全,而java的CAS通过轮询并不用加锁,所以效率很高。具体有关ringbuffer说明和实现可以参考LMAX Disruptor文档。

这部分源码是说明sync操作的SyncFuture会被提交到SyncRunner中,这里可以注意SyncFuture实例其实并不是一个个提交到SyncRunner中执行的,而是以syncFutures(数组,多个SyncFuture实例)方式提交的。下面这部分源码是注释中说明批量刷盘的决策。

SyncRunner是一个线程,wal实际有一个SyncRunner的线程组,专门负责之前append到文件缓存的刷盘工作。

SyncRunner的线程方法(run())负责具体的刷写文件缓存到磁盘的工作。首先去之前提交的synceFutues中拿到其中sequence最大的SyncFuture实例,并拿到它对应ringbuffer的sequence。再去比对当前最大的sequence,如果发现比当前最大的sequence则去调用releaseSyncFuture()方法释放synceFuture,实际就是notify通知正被阻塞的sync操作,让工作线程可以继续往下继续。

前面解释了sequence是根据提交顺序过来的,并且解释了append到文件缓存的时候也是全局有序的,所以这里取最大的去刷盘,只要最大sequence已经刷盘,那么比这个sequence的也就已经刷盘成功。最后调用当前HadoopSequence文件writer刷盘,并notify对应的syncFuture。这样整个wal写入也完成了。

小结

Hbase的WAL机制是保证hbase使用lsm树存储模型把随机写转化成顺序写,并从内存read数据,从而提高大规模读写效率的关键一环。wal的多生产者单消费者的线程模型让wal的写入变得安全而高效,本文档从源码入手分析了其线程模型为以后更好开发和研究hbase其他相关知识奠定基础。


相关推荐

Hbase的WAL在RegionServer基本调用过程
HBase跨版本数据迁移总结
Hbase写入hdfs源码分析

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringMvc4x高级配置(四) 服务器端推送技术之S

发表于 2017-11-17

一. 点睛

在前面的文章SpringMvc4.x高级配置(三):服务器端推送技术之SSE中已经介绍了服务器端推送技术的第一种方案,下面演示第二种服务器端推送技术,基于Servlet3.0+异步方法处理。

二. 示例

1.开启异步方法支持

在文件WebInitializer的方法onStartup末尾增加以下代码开启异步方法支持,代码如下:

1
复制代码servlet.setAsyncSupported(true);//①

添加完成之后的代码如下所示:

1
2
3
4
5
复制代码 
Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(ctx)); //③
servlet.addMapping("/");
servlet.setLoadOnStartup(1);
servlet.setAsyncSupported(true);//①

代码解释:

① 此句开启异步方法支持。

  1. 演示控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码package org.light4j.springMvc4.web;

import org.light4j.springMvc4.service.PushService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;

@Controller
public class AysncController {
@Autowired
PushService pushService; //①

@RequestMapping("/defer")
@ResponseBody
public DeferredResult<String> deferredCall() { //②
return pushService.getAsyncUpdate();
}
}

代码解释:

异步任务实现是通过控制器从另外一个线程返回一个DeferredResult,这里的DeferredResult是从pushService中获得的。
① 定时任务,定时更新DeferredResult。
② 返回给客户端DeferredResult。

  1. 定时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码package org.light4j.springMvc4.service;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.async.DeferredResult;

@Service
public class PushService {
private DeferredResult<String> deferredResult; //①

public DeferredResult<String> getAsyncUpdate() {
deferredResult = new DeferredResult<String>();
return deferredResult;
}

@Scheduled(fixedDelay = 5000)
public void refresh() {
if (deferredResult != null) {
deferredResult.setResult(new Long(System.currentTimeMillis()).toString());
}
}
}

代码解释:

① 在PushService里产生DeferredResult给控制器使用,通过@Scheduled注解的方法定时更新DeferredResult

  1. 演示页面

在src/main/resources下新建async.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>servlet async support</title>

</head>
<body>

<script type="text/javascript" src="assets/js/jquery.js"></script>
<script type="text/javascript">

deferred();//①

function deferred(){
$.get('defer',function(data){
console.log(data); //②
deferred(); //③
});
}

</script>
</body>
</html>

代码解释:

此处的代码使用的是JQuery的Ajax请求,所以没有浏览器兼容性问题。
① 页面打开就向后台发送请求。
② 在控制台输出服务端推送的数据。
③ 一次请求完成后再向后台推送数据。

  1. 配置

在文件MyMvcConfig上使用注解@EnableScheduling开启计划任务的支持,代码如下:

1
2
3
4
5
6
7
复制代码@Configuration
@EnableWebMvc// ①
@EnableScheduling
@ComponentScan("org.light4j.springMvc4")
public class MyMvcConfig extends WebMvcConfigurerAdapter {

}

在文件MyMvcConfig的方法addViewControllers添加viewController映射访问演示页面async.jsp,代码如下:

1
复制代码registry.addViewController("/async").setViewName("/async");

添加完成之后的代码如下所示:

1
2
3
4
5
6
7
8
9
复制代码
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/index").setViewName("/index");
registry.addViewController("/toUpload").setViewName("/upload");
registry.addViewController("/converter").setViewName("/converter");
registry.addViewController("/sse").setViewName("/sse");
registry.addViewController("/async").setViewName("/async");
}
  1. 运行

访问http://localhost/springMvc4.x-servlet3/async,可以看到网络不断的在获取服务器端推送的消息,如下图所示:

查看浏览器控制台可以看到不断的在打印消息,如下图所示:

三. 源代码示例:

github地址:点击查看
码云地址:点击查看

打赏 欢迎关注人生设计师的微信公众账号
公众号ID:longjiazuoA

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…942943944…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%