Spotted Flower Is a Sequel, and Why

I was discussing with someone about Genshiken chapter 121 on reddit the other day, then our discussion turned to Spotted Flower, which is commonly viewed as story of another universe. I disagreed with it, and said Spotted Flower is actually a sequel of Genshiken. Here's my reply:

Ah, I have to say, maybe you know Genshiken well, but you didn't read Spotted Flower very carefully. I happen to get some time now, so I'll explain my theory.

Before that, there's one thing I forgot to say, which is really important: I'm not trying to predict the development of the story(Spotted Flower), what I'm trying to do is reveal Shimoku Kio's intention, or in other words, what does Spotted Flower tell us.

First, let's look at the title "Spotted Flower". What does it mean? I bet you don't know, those who translate this manga probably do. "Spot", in Chinese/Japanese, is "斑", which is the first character of Rame's full name 斑目晴信. "Saki" is "咲", and the meaning of "咲" in Japanese is bloom/flowering. So "Spotted Flower" literally means "Rame and Saki", there's no argument about that. Moreover, the author plays on word to summarize the story:he uses "Spotted Flower" rather than "Spot Flower", why? I can't think of other possibility than saying Saki belongs to Rame.

Then I'll step through the current chapters to show all clues. I'll just call the main characters Rame and Saki.

ch1
Rame wears glasses, Sasahara doesn't. In Genshiken, he wears the same glasses once when Saki told him to change his appearance;
after a CM, Saki asked Rame "Did you hang out with everyone", Rame said "Yeah(skipped some dialog)...That's why you should have come as well", Saki replied "I'm kidding. I wouldn't want tension in the air because I sat next to my ex-boyfriend.";
Saki said "...That I ended up with 2 otaku guys back to back."

ch3
Kaichou cosplay http://www.heymanga.xyz/manga/Spotted_Flower/3/1;

ch4
Rame and saki come to see Saki's grandma. In Genshiken, Saki mentioned that her name is given by her grandma, other people are surprised and say "saki" is like a character's name in Anime/Manga so they can't imagine an old lady can think of such name. In this chapter, the old lady gave many Anime/Manga-ish name too, so obviously it's the same person. Saki also said to her grandma: "You're the one who named me, grandma!", which proves she is indeed Saki.

ch5
Saki said: "You used to be a hopeless otaku...Back then, I thought you were pretty pathetic, too."; Rame said: "I really would've liked to raise a little girl...", Saki said: "Hey, your dad is really gross, you know that? He's a lolicon." The only lolicon in genshiken is Rame, Sasahara prefers mature women.

ch7
Ohno came to see Saki, and persuaded her to cosplay, like in Genshiken. Ohno has two babies, that is because she married early and became a full-time housewife. This is implied in Genshiken;

ch9
Rame broke his arm, again...his hand is swollen just like the first time.

ch12
This is the first time Hato appeared in SF, and his relationship with Rame is still very subtle. Saki thought:"But every time they meet, he leaves that scent on my husband's clothes, like he's plaunting it. it's perfume. Men can only get pregnant in video games, dammit! I think he still hasn't given up."
This is an important chapter which gives us many information to guess what happened between Hato and Rame. I just came up with a guess: Rame chooses Hato, they are together for a while, then, Saki breaks up with Kousaka, which gives Rame a chance. There's no doubt that Rame would choose Saki among the two. Finally Saki accepts him, here comes Spotted Flower.

ch12.2 & 12.5
Yajima has lost weight successfully. Remember in Genshiken she said she's gonna lose weight?
This chapter also tells us Hato had breast augmentation.

ch14
The cosplay costume Ohno brought is the character Kousaka cosplayed after Rame made love confession to Saki and got rejected.
This is the funniest scene of the manga and I really couldn't stop laughing.

ch17
This is the chapter that really makes me believe SF is a sequel.
Hato said to Rame "Alcohol was what led you to trip me, and push me down onto a bed, even before I was who I am today."
It means, whether or not the two stories split in the future, at least they are the same till that point, which is near the latest chapter of Genshiken so far. Since I'm talking about revealing the author's intention, this is a clear evidence that he wants us to believe two stories won't split. Whether Shimoku will follow this till the end or make a twist ending is not something to be discussed here.

I ignored some details like the relationship between Yajima and Hato. What's going on next in Genshiken will probably gives a conclusion on if SF is a sequel. My point is fairly simple: since SF matches almost every detail of Genshiken, at least we could say that somehow Shimoku wants us to believe it's a sequel. I can't believe so many people misidentify Saki as Ogiue, it's horrible, there's nothing this character has in common with Ogiue except they are both female and have black hair. As for the hair, by the way, apparently Saki no longer dyes her hair after getting married, that doesn't make her Ogiue, does it?

Anyway, Shimoku Kio is such a talented mangaka, I've never seen other people drawing two related manga at the same time like he's been doing. He's done a brilliant work so far, I hope he could bring us more.

update 2016.3.28
Madarame chooses no one in chapter 122, which doesn't surprise me at all. I wrote "What's going on next in Genshiken will probably gives a conclusion on if SF is a sequel. ", now I would say the possibility raises to 90%.

可以好好听音乐了

去年十一月我写了一篇文章《在线音乐,有关版权,亦无关版权》。其实当时我在等虾米买歌,结果他们并没有这么做。自然,我也调研了网易云音乐和 QQ 音乐。QQ 的歌很全,大部分虾米下架的歌曲都能找到,一度想转向 QQ。不过一些缺点让我最终没有这么做:QQ 音乐的歌曲管理混乱,同样的一首歌可以有一堆人传,缺乏审核以保证唯一性;歌曲的信息也是乱七八糟,和虾米没法比;即使是 Vip,也没法在网页上下载歌曲,必须用客户端下。最关键的一点是,QQ 始终还是缺一些歌,不能满足听歌的集中性。

遂决定不依赖云音乐们,自己动手。搜索“music streaming server”,能找到一大堆解决方案,目的都是让用户可以自己架设音乐服务器。开源方案有 Ampachekoel,不开源的有 Subsonic。最终选择用 Subsonic,因为安装简单,用的人也多,算是非常成熟的产品。

首先按官方给的步骤安装好,然后修改一下配置文件里的用户和端口。再访问相应端口就行了。因为服务器在美国,从网页上传稍微慢点,所以我直接拿 FTP 传了。界面看起来是这个样子,该有的功能比如播放列表,各种播放模式都有,还是相当不错的。

虽然是美国的服务器,不过基本可以保证听歌不卡,本来还挺担心这点,看来问题不大。

Requests' secret: pool_connections and pool_maxsize

Requests is one of the, if not the most well-known Python third-party library for Python programmers. With its simple API and high performance, people tend to use requests rather than urllib2 in standard library for HTTP requests. However, people who use requests every day may not know the internals, and today I want to explain two concepts: pool_connections and pool_maxsize.

Let's start with Session:

import requests

s = requests.Session()
s.get('https://www.google.com')

It's pretty simple. You probably know requests' Session persists cookie. Cool. But do you know Session has a mount method?

mount(prefix, adapter)
Registers a connection adapter to a prefix.
Adapters are sorted in descending order by key length.

No? Well, in fact you've already used this method when you initialize a Session object:

class Session(SessionRedirectMixin):

    def __init__(self):
        ...
        # Default connection adapters.
        self.adapters = OrderedDict()
        self.mount('https://', HTTPAdapter())
        self.mount('http://', HTTPAdapter())

Now comes the interesting part. If you've read Ian Cordasco's article Retries in Requests, you should know that HTTPAdapter can be used to provide retry functionality. But what is an HTTPAdapter really? Quoted from doc:

class requests.adapters.HTTPAdapter(pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False)

The built-in HTTP Adapter for urllib3.

Provides a general-case interface for Requests sessions to contact HTTP and HTTPS urls by implementing the Transport Adapter interface. This class will usually be created by the Session class under the covers.

Parameters:
* pool_connections – The number of urllib3 connection pools to cache. * pool_maxsize – The maximum number of connections to save in the pool. * max_retries(int) – The maximum number of retries each connection should attempt. Note, this applies only to failed DNS lookups, socket connections and connection timeouts, never to requests where data has made it to the server. By default, Requests does not retry failed connections. If you need granular control over the conditions under which we retry a request, import urllib3’s Retry class and pass that instead. * pool_block – Whether the connection pool should block for connections. Usage:

>>> import requests
>>> s = requests.Session()
>>> a = requests.adapters.HTTPAdapter(max_retries=3)
>>> s.mount('http://', a)

If the above documentation confuses you, here's my explanation: what HTTP Adapter does is simply providing different configurations for different requests according to target url. Remember the code above?

self.mount('https://', HTTPAdapter())
self.mount('http://', HTTPAdapter())

It creates two HTTPAdapter objects with the default argument pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False, and mount to https:// and http:// respectively, which means configuration of the first HTTPAdapter() will be used if you try to send a request to http://xxx, and the second HTTPAdapter() for requests to https://xxx. Though in this case two configurations are the same, requests to http and https are still handled separately. We'll see what it means later.

As I said, the main purpose of this article is to explain pool_connections and pool_maxsize.

First let's look at pool_connections. Yesterday I raised a question on stackoverflow cause I'm not sure if my understanding is correct, the answer eliminates my uncertainty. HTTP, as we all know, is based on TCP protocol. An HTTP connection is also a TCP connection, which is identified by a tuple of five values:

(<protocol>, <src addr>, <src port>, <dest addr>, <dest port>)

Say you've established an HTTP/TCP connection with www.example.com, assume the server supports Keep-Alive, next time you send request to www.example.com/a or www.example.com/b, you could just use the same connection cause none of the five values change. In fact, requests' Session automatically does this for you and will reuse connections as long as it can.

The question is, what determines if you can reuse old connection or not? Yes, pool_connections!

pool_connections – The number of urllib3 connection pools to cache.

I know, I know, I don't want to brought so many terminologies either, this is the last one, I promise. For easy understanding, one connection pool corresponds to one host, that's what it is.

Here's an example(unrelated lines are ignored):

s = requests.Session()
s.mount('https://', HTTPAdapter(pool_connections=1))
s.get('https://www.baidu.com')
s.get('https://www.zhihu.com')
s.get('https://www.baidu.com')

"""output
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.baidu.com
DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 None
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.zhihu.com
DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 2621
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.baidu.com
DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 None
"""

HTTPAdapter(pool_connections=1) is mounted to https://, which means only one connection pool persists at a time. After calling s.get('https://www.baidu.com'), the cached connection pool is connectionpool('https://www.baidu.com'). Now s.get('https://www.zhihu.com') came, and the session found that it cannot use the previously cached connection because it's not the same host(one connection pool corresponds to one host, remember?). Therefore the session had to create a new connection pool, or connection if you would like. Since pool_connections=1, session cannot hold two connection pools at the same time, thus it abandoned the old one which is connectionpool('https://www.baidu.com') and kept the new one which is connectionpool('https://www.zhihu.com'). Next get is the same. This is why we see three Starting new HTTPS connection in log.

What if we set pool_connections to 2:

s = requests.Session()
s.mount('https://', HTTPAdapter(pool_connections=2))
s.get('https://www.baidu.com')
s.get('https://www.zhihu.com')
s.get('https://www.baidu.com')
"""output
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.baidu.com
DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 None
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.zhihu.com
DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 2623
DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 None
"""

Great, now we only created connection twice and saved one connection establishing time.

Finally, pool_maxsize.

First and foremost, you should be caring about pool_maxsize only if you use Session in a multithreaded environment, like making concurrent requests from multiple threads using the same Session.

Actually, pool_maxsize is an argument for initializing urllib3's HTTPConnectionPool, which is exactly the connection pool we mentioned above. HTTPConnectionPool is a container for a collection of connections to a specific host, and pool_maxsize is the number of connections to save that can be reused. If you're running your code in one thread, it's neither possible nor needed to create multiple connections to the same host, cause requests library is blocking, so that HTTP request are always sent one after another.

Things are different if there are multiple threads.

def thread_get(url):
    s.get(url)

s = requests.Session()
s.mount('https://', HTTPAdapter(pool_connections=1, pool_maxsize=2))
t1 = Thread(target=thread_get, args=('https://www.zhihu.com',))
t2 = Thread(target=thread_get, args=('https://www.zhihu.com/question/36612174',))
t1.start();t2.start()
t1.join();t2.join()
t3 = Thread(target=thread_get, args=('https://www.zhihu.com/question/39420364',))
t4 = Thread(target=thread_get, args=('https://www.zhihu.com/question/21362402',))
t3.start();t4.start()
"""output
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.zhihu.com
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (2): www.zhihu.com
DEBUG:requests.packages.urllib3.connectionpool:"GET /question/36612174 HTTP/1.1" 200 21906
DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 2606
DEBUG:requests.packages.urllib3.connectionpool:"GET /question/21362402 HTTP/1.1" 200 57556
DEBUG:requests.packages.urllib3.connectionpool:"GET /question/39420364 HTTP/1.1" 200 28739
"""

See? It established two connections for the same host www.zhihu.com, like I said, this can only happen in a multithreaded environment. In this case, we create a connectionpool with pool_maxsize=2, and there're no more than two connections at the same time, so it's enough. We can see that requests from t3 and t4 did not create new connections, they reused the old ones.

What if there's not enough size?

s = requests.Session()
s.mount('https://', HTTPAdapter(pool_connections=1, pool_maxsize=1))
t1 = Thread(target=thread_get, args=('https://www.zhihu.com',))
t2 = Thread(target=thread_get, args=('https://www.zhihu.com/question/36612174',))
t1.start()
t2.start()
t1.join();t2.join()
t3 = Thread(target=thread_get, args=('https://www.zhihu.com/question/39420364',))
t4 = Thread(target=thread_get, args=('https://www.zhihu.com/question/21362402',))
t3.start();t4.start()
t3.join();t4.join()
"""output
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.zhihu.com
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (2): www.zhihu.com
DEBUG:requests.packages.urllib3.connectionpool:"GET /question/36612174 HTTP/1.1" 200 21906
DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 2606
WARNING:requests.packages.urllib3.connectionpool:Connection pool is full, discarding connection: www.zhihu.com
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (3): www.zhihu.com
DEBUG:requests.packages.urllib3.connectionpool:"GET /question/39420364 HTTP/1.1" 200 28739
DEBUG:requests.packages.urllib3.connectionpool:"GET /question/21362402 HTTP/1.1" 200 57556
WARNING:requests.packages.urllib3.connectionpool:Connection pool is full, discarding connection: www.zhihu.com
"""

Now, pool_maxsize=1,warning came as expected:

Connection pool is full, discarding connection: www.zhihu.com

We can also noticed that since only one connection can be saved in this pool, a new connection is created again for t3 or t4. Obviously this is very inefficient. That's why in urllib3's documentation it says:

If you’re planning on using such a pool in a multithreaded environment, you should set the maxsize of the pool to a higher number, such as the number of threads.

Last but not least, HTTPAdapter instances mounted to different prefixes are independent.

s = requests.Session()
s.mount('https://', HTTPAdapter(pool_connections=1, pool_maxsize=2))
s.mount('https://baidu.com', HTTPAdapter(pool_connections=1, pool_maxsize=1))
t1 = Thread(target=thread_get, args=('https://www.zhihu.com',))
t2 =Thread(target=thread_get, args=('https://www.zhihu.com/question/36612174',))
t1.start();t2.start()
t1.join();t2.join()
t3 = Thread(target=thread_get, args=('https://www.zhihu.com/question/39420364',))
t4 = Thread(target=thread_get, args=('https://www.zhihu.com/question/21362402',))
t3.start();t4.start()
t3.join();t4.join()
"""output
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): www.zhihu.com
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (2): www.zhihu.com
DEBUG:requests.packages.urllib3.connectionpool:"GET /question/36612174 HTTP/1.1" 200 21906
DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 2623
DEBUG:requests.packages.urllib3.connectionpool:"GET /question/39420364 HTTP/1.1" 200 28739
DEBUG:requests.packages.urllib3.connectionpool:"GET /question/21362402 HTTP/1.1" 200 57669
"""

The above code is easy to understand so I'm not gonna explain.

I guess that's all. Hope this article help you understand requests better. BTW I created a gist here which contains all of the testing code used in this article. Just download and play with it :)

Appendix

  1. For https, requests uses urllib3's HTTPSConnectionPool, but it's pretty much the same as HTTPConnectionPool so I didn't differeniate them in this article.
  2. Session's mount method ensures the longest prefix gets matched first. Its implementation is pretty interesting so I posted it here.

    def mount(self, prefix, adapter):
    """Registers a connection adapter to a prefix.
    Adapters are sorted in descending order by key length."""
    self.adapters[prefix] = adapter
    keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]
    for key in keys_to_move:
        self.adapters[key] = self.adapters.pop(key)
    

    Note that self.adapters is an OrderedDict.

  3. A more advanced config option pool_block
    Notice that when creating an adapter, we didn't introduce the last parameter pool_block HTTPAdapter(pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False) Setting it to True is equivalent to setting block=True when creating a urllib3.connectionpool.HTTPConnectionPool, the effect is this, quoted from urllib3's doc:

    If set to True, no more than maxsize connections will be used at a time. When no free connections are available, the call will block until a connection has been released. This is a useful side effect for particular multithreaded situations where one does not want to use more than maxsize connections per host to prevent flooding.

    But actually, requests doesn't set a timeout(https://github.com/shazow/urllib3/issues/1101), which means that if there's no free connection, an EmptyPoolError will be raised immediately.

  4. 刚发现,我就说为什么白天晕乎乎的,原来是吃了白加黑的黑片_(:3」∠)_


top